feat(docs): add body field for webhook configuration in UI and enhance cURL import functionality to auto-fill request details

This commit is contained in:
keven1024
2026-05-01 21:29:30 +08:00
parent f4a28e369f
commit 4a3ec790b7
13 changed files with 523 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-28

View File

@@ -0,0 +1,102 @@
## Context
项目使用 Go 工作区go.work管理多模块`pkg/models``pkg/utils`(共享层)、`backend`Echo HTTP API`worker`Asynq 异步任务处理器)。前端已完整实现通知配置 UI`NotifyConfigField.vue`),调用 share 创建接口时会传入 `notify_types``notify_emails``notify_webhooks` 等字段。但后端 `ShareConfig` 与前端字段不匹配(`notify_email` 单数 vs `notify_emails` 复数,缺少 `notify_types``notify_webhooks`),且这些字段从未被持久化到 RedisWorker 中的 `ShareNotify` 也只是空 stub。
系统无用户账号体系,"用户" 通过 Session nanoid 标识,没有存储任何语言偏好。前端使用 `@nuxtjs/i18n` 管理 7 种语言zh-CN、zh-TW、en、ja、ko、fr、de创建分享时可感知当前语言。
## Goals / Non-Goals
**Goals:**
- 分享被下载时(`VaildateShare` 成功),向配置的邮件地址和 Webhook URL 发送通知
- 通知以 Asynq 异步任务执行,不阻塞下载响应
- 邮件和 Webhook 在同一任务中顺序执行;部分成功即视为任务成功(不重试);全部失败才返回 error 触发 Asynq 重试
- 邮件内容根据分享创建人的语言locale进行本地化支持 7 种语言
**Non-Goals:**
- 通知发送状态的持久化记录
- 通知的批量/去重处理(每次下载独立触发)
- 支持 TLS 客户端证书校验
- Webhook 内容的 i18n
## Decisions
### D1: 通知配置存储在 `RedisShareInfo` 中
**决策**: 将 `HasNotify``NotifyTypes``NotifyEmails``NotifyWebhooks` 直接存入 `RedisShareInfo`Worker 读取时通过 `shareId` 查找。
**备选**: 在任务 Payload 中携带完整通知配置。
**理由**: Payload 大小有限制Asynq 推荐 <1KBWebhook 配置可能较大。通过 `shareId` 查 Redis 是现有模式(`RemoveShare` 也这么做),保持一致性。
---
### D2: 部分成功即任务成功,全部失败才重试
**决策**: 遍历所有通知目标,收集错误;`成功数 > 0` 则返回 nil全部失败才返回聚合错误。
**备选**: 任一失败即重试(可能导致重复通知);全部失败也不重试(丢失通知)。
**理由**: 用户描述"反正通知了,除非全部失败才会重试"——保证至少一个通知到达,避免对已成功的渠道重复发送。
---
### D3: 邮件使用 `github.com/wneessen/go-mail`
**决策**: 使用 `go-mail` 发送邮件,配置通过环境变量注入(`smtp.host``smtp.port``smtp.username``smtp.password``smtp.from`)。
**备选**: Go 标准库 `net/smtp`
**理由**: `go-mail` 同时支持 STARTTLS587和 Implicit TLS465API 更直观邮件构造Subject、Body、Header更安全无需手动拼接 raw RFC 2822 格式。解决了标准库不支持 465 端口的问题。
---
### D4: Webhook Body 发送策略
**决策**:
- `none`: 不发送 Body
- `form-data`: 设置 `Content-Type: application/x-www-form-urlencoded`Body 原样发送
- `raw`: 不设置 Content-Type由 Headers 中用户自定义Body 原样发送
**理由**: 前端 Body 字段是 Textarea 原始字符串,不做解析。用户可通过 Headers 自定义 Content-Type 覆盖。
---
### D5: 邮件内容使用 `github.com/nicksnyder/go-i18n/v2` 本地化
**决策**: 在 `worker/internal/i18n/` 目录下维护各语言的 TOML 翻译文件(`active.en.toml``active.zh-CN.toml`Worker 启动时加载所有文件到 `i18n.Bundle`;发送邮件时按 `shareInfo.Locale` 查找对应 `Localizer`,缺失时 fallback 到 `en`
**备选**: 硬编码英文文本;使用 `text/template` 手动维护多语言字符串。
**理由**: `go-i18n` 是 Go 生态标准 i18n 方案,翻译文件独立于代码,未来新增语言只需添加文件,不改逻辑。
---
### D6: 创建人 locale 由前端随 `CreateShareProps` 传入
**决策**: 在 `ShareConfig`(或 `CreateShareProps` 顶层)新增 `Locale string json:"locale"` 字段,前端在创建分享时将 `useI18n().locale.value` 写入,后端存入 `RedisShareInfo.Locale`
**备选**:
- 读取 HTTP `Accept-Language` 头:值不可控,浏览器发送的可能与用户在 UI 选择的不同
- 不存 locale每次查用户偏好系统无用户账号无处存储
**理由**: 前端语言是用户显式选择的,最能代表其意图。直接传递是最简单可靠的方案。
## Risks / Trade-offs
- **SMTP 未配置时邮件通知静默跳过** → `smtp.host` 为空时跳过邮件(不计入失败数),记录 warn 日志
- **Webhook 目标不可达** → Asynq 默认重试策略兜底(全部失败时);单个失败不重试(部分成功语义)
- **Redis 中 share 已过期但任务还在队列** → Worker 查不到 `shareInfo` 时返回 nil静默跳过不重试
- **`notify_email`(旧字段名)迁移** → 旧字段不再写入Redis 历史数据无影响JSON unmarshal 忽略缺失字段)
- **go-mail 和 go-i18n 为新依赖** → 需更新 `worker/go.mod`;两个库均稳定无争议
## Migration Plan
1. 部署 `pkg/models` 变更(向后兼容,新字段默认零值)
2. 部署 Backend新字段开始写入 Redis前端同步传入 `locale`
3. 部署 Worker注册 `share:notify` handler加载 i18n bundle
4. 无需数据迁移,无需 rollback 特殊操作feature 是纯增量)
## Open Questions
- 邮件是否需要 HTML 格式,还是纯文本即可?当前设计使用纯文本。
- Webhook 通知是否也需要携带分享元数据(如文件名)在 Body 模板中?当前由用户自定义 Body。

View File

@@ -0,0 +1,33 @@
## Why
当前分享功能支持配置通知(邮件和 Webhook但通知逻辑从未被实现——下载时不会触发任何通知。用户配置了通知但永远不会收到导致该功能形同虚设。
## What Changes
- 新增 `NotifyWebhook` 结构体到共享模型层(`pkg/models`),存储 Webhook 的 URL、方法、请求头、Body 类型和 Body 内容
- 扩展 `RedisShareInfo` 模型,增加 `has_notify``notify_types``notify_emails``notify_webhooks` 字段
- 更新后端 `ShareConfig` 请求结构体,与前端实际发送的字段(`notify_types``notify_emails``notify_webhooks`)对齐
- 在创建分享时将完整通知配置持久化到 Redis
-`VaildateShare`(下载验证)成功后,若分享配置了通知,则入队 `share:notify` 任务
- Worker 实现 `ShareNotify` 处理器:读取分享配置,并发执行所有邮件和 Webhook 通知;仅当全部通知渠道均失败时才返回错误触发重试
## Capabilities
### New Capabilities
- `share-download-notify`: 下载通知能力——分享被下载时,通过邮件和/或 Webhook 向配置的接收方发送通知
### Modified Capabilities
(无既有 Spec 需要变更)
## Impact
- **`pkg/models/share.go`**: 新增 `NotifyWebhook` 类型,扩展 `RedisShareInfo` 字段(影响所有读写 Redis 分享信息的代码)
- **`backend/internal/controllers/share.go`**: `ShareConfig` 字段变更(`notify_email` 重命名为 `notify_emails`,新增 `notify_types``notify_webhooks`
- **`backend/internal/controllers/download.go`**: 新增 asynq 任务入队逻辑,需引入 `encoding/json``asynq` 依赖
- **`worker/internal/tasks/share.go`**: 实现真实的 `ShareNotify` 函数(当前为空 stub
- **`worker/internal/tasks/types.go`**: 新增 `ShareNotifyTaskPayload` 结构体
- **`worker/main.go`**: 注册 `share:notify` 路由
- **依赖**: Worker 使用已有的 `go-resty/resty`Webhook HTTP 请求)和 Go 标准库 `net/smtp`(邮件),无需新增外部依赖
- **配置**: 需新增 SMTP 相关配置项(`smtp.host``smtp.port``smtp.username``smtp.password``smtp.from`

View File

@@ -0,0 +1,81 @@
## ADDED Requirements
### Requirement: Notify config and creator locale persisted on share creation
创建分享时,系统 SHALL 将完整通知配置(`has_notify``notify_types``notify_emails``notify_webhooks`)以及创建人当前语言(`locale`)持久化到 Redis `RedisShareInfo` 中。
#### Scenario: Share created with email and webhook notify config and locale
- **WHEN** 客户端 POST `/share` 并携带 `config.has_notify=true``config.notify_types=["email","webhook"]``config.notify_emails``config.notify_webhooks``config.locale="zh-CN"`
- **THEN** Redis 中存储的 `RedisShareInfo` 包含完整的通知配置字段及 `locale="zh-CN"`
#### Scenario: Share created without notify
- **WHEN** 客户端 POST `/share` 并携带 `config.has_notify=false`
- **THEN** Redis 中 `has_notify=false`,通知相关字段为空/零值
---
### Requirement: Notify task enqueued on download validation
当分享下载验证(`VaildateShare`)成功且分享配置了通知时,系统 SHALL 向 Asynq 队列入队一个 `share:notify` 任务。
#### Scenario: Download validated with notify enabled
- **WHEN** 用户成功通过 `VaildateShare` 验证(密码正确、下载次数充足)且 `shareInfo.HasNotify=true` 且存在至少一个通知目标
- **THEN** 系统入队一条 `share:notify` 任务Payload 包含 `share_id`
- **THEN** 下载 token 正常返回,响应不受通知入队影响
#### Scenario: Download validated with notify disabled
- **WHEN** `shareInfo.HasNotify=false`
- **THEN** 不入队任何通知任务
---
### Requirement: Webhook notification sent on download
`share:notify` Worker 任务 SHALL 对 `notify_webhooks` 中每个配置项发起 HTTP 请求。
#### Scenario: POST webhook with form-data body
- **WHEN** Webhook `method=POST``bodyType=form-data``body="k=v"`
- **THEN** Worker 向 `url` 发起 POST 请求,`Content-Type: application/x-www-form-urlencoded`Body 为 `k=v`
#### Scenario: POST webhook with raw body
- **WHEN** Webhook `method=POST``bodyType=raw`
- **THEN** Worker 向 `url` 发起 POST 请求Body 原样发送,不强制设置 Content-Type
#### Scenario: Webhook with custom headers
- **WHEN** Webhook `headers` 中包含自定义请求头
- **THEN** Worker 将这些 Header 附加到请求中
#### Scenario: Webhook returns 4xx/5xx
- **WHEN** Webhook 目标返回 HTTP 状态码 >= 400
- **THEN** 该 Webhook 计为失败
---
### Requirement: Localized email notification sent on download
`share:notify` Worker 任务 SHALL 对 `notify_emails` 中每个邮件地址通过 SMTP 发送通知邮件,邮件语言使用 `RedisShareInfo.Locale` 对应的翻译,不支持的 locale 回退到英文。
#### Scenario: Email sent in creator's locale
- **WHEN** SMTP 配置完整(`smtp.host` 非空)且 `shareInfo.Locale="zh-CN"`
- **THEN** Worker 向目标地址发送邮件,邮件 Subject 和 Body 为中文内容
#### Scenario: Email locale fallback to English
- **WHEN** `shareInfo.Locale` 为空或不支持的语言代码
- **THEN** Worker 使用英文模板发送邮件
#### Scenario: SMTP not configured
- **WHEN** `smtp.host` 为空
- **THEN** 邮件通知被跳过(不计入失败数),记录 warn 日志
---
### Requirement: Partial success means task success; all-fail triggers retry
`share:notify` 任务 SHALL 仅在所有通知目标(邮件 + Webhook均失败时才返回 error只要有一个成功任务 SHALL 返回 nil。
#### Scenario: One webhook succeeds, one email fails
- **WHEN** 2 个通知目标中Webhook 成功、Email 失败
- **THEN** 任务返回 nil不重试
#### Scenario: All notifications fail
- **WHEN** 所有 Webhook 和 Email 均失败
- **THEN** 任务返回聚合 errorAsynq 按默认策略重试
#### Scenario: Share expired when task executes
- **WHEN** Worker 执行时 Redis 中已无对应 `shareInfo`share 已过期)
- **THEN** 任务返回 nil静默跳过不重试

View File

@@ -0,0 +1,37 @@
## 1. 共享模型层
- [x] 1.1 在 `pkg/models/share.go` 中新增 `NotifyWebhook` 结构体(字段:`URL``Method``Headers map[string]string``BodyType``Body`JSON tag 与前端保持一致:`bodyType`
- [x] 1.2 移除 `RedisShareInfo.NotifyEmail []string`(旧字段),新增 `HasNotify bool``NotifyTypes []string``NotifyEmails []string``NotifyWebhooks []NotifyWebhook``Locale string`
## 2. 后端 —— 创建分享
- [x] 2.1 更新 `backend/internal/controllers/share.go``ShareConfig` 结构体:移除 `NotifyEmail json:"notify_email"`,新增 `NotifyTypes []string json:"notify_types"``NotifyEmails []string json:"notify_emails"``NotifyWebhooks []models.NotifyWebhook json:"notify_webhooks"``Locale string json:"locale"`
- [x] 2.2 在 `CreateShareInfo``SetRedisShareInfo` 回调中,将 `HasNotify``NotifyTypes``NotifyEmails``NotifyWebhooks``Locale``r.Config` 赋值到 `shareInfo`
## 3. 后端 —— 下载验证
- [x] 3.1 在 `backend/internal/controllers/download.go` 中,`VaildateShare` 函数锁内的统计更新之后,若 `shareInfo.HasNotify && (len(shareInfo.NotifyEmails) > 0 || len(shareInfo.NotifyWebhooks) > 0)`,则入队 `share:notify` 任务Payload: `{"share_id": "..."}`)
- [x] 3.2 在 `download.go` 中补充 import`"encoding/json"``"github.com/hibiken/asynq"`
## 4. Worker —— 依赖与任务类型
- [x] 4.1 在 `worker/go.mod` 中执行 `go get github.com/wneessen/go-mail``go get github.com/nicksnyder/go-i18n/v2`,更新 `go.sum`
- [x] 4.2 在 `worker/internal/tasks/types.go` 中新增 `ShareNotifyTaskPayload struct { ShareId string json:"share_id" }`
## 5. Worker —— i18n 翻译文件
- [x] 5.1 创建目录 `worker/internal/i18n/`
- [x] 5.2 创建 `worker/internal/i18n/active.en.toml`:包含邮件 Subject`notify_email_subject`)和 Body`notify_email_body`的英文翻译Body 中使用模板变量 `{{.ShareType}}``{{.FileName}}`
- [x] 5.3 创建另外 6 个语言文件(`active.zh-CN.toml``active.zh-TW.toml``active.ja.toml``active.ko.toml``active.fr.toml``active.de.toml`),内容与 `en` 保持相同结构,翻译各语言对应文本
## 6. Worker —— 通知处理器
- [x] 6.1 在 `worker/internal/tasks/` 中新建 `notify.go`(或复用 `share.go`),实现 `loadI18nBundle() *i18n.Bundle`:在包级别 `sync.Once` 初始化,加载 `worker/internal/i18n/active.*.toml` 所有文件
- [x] 6.2 实现 `localizeEmail(locale, shareType, fileName string) (subject, body string)`:用 `go-i18n` Localizer 渲染 Subject 和 Bodylocale 不存在时 fallback 到 `en`
- [x] 6.3 实现 `sendWebhook(webhook models.NotifyWebhook) error`:使用 resty`BodyType` 设置 Content-Type附加 HeadersHTTP 状态 >= 400 视为失败
- [x] 6.4 实现 `sendEmail(to string, shareInfo *models.RedisShareInfo) error`:若 `smtp.host` 为空则记录 warn 日志并返回 nil不计失败否则使用 `go-mail` 构造邮件(调用 `localizeEmail` 获取文本)并通过 SMTP 发送
- [x] 6.5 实现 `ShareNotify(ctx, task)` 主函数:解析 Payload → 查 Redis → 遍历 Webhooks + Emails → 收集错误 → 全部失败返回聚合 error否则返回 nilshare 已过期(`shareInfo == nil`)直接返回 nil
## 7. Worker —— 路由注册
- [x] 7.1 在 `worker/main.go``mux.HandleFunc` 列表中新增 `mux.HandleFunc("share:notify", tasks.ShareNotify)`

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-01

View File

@@ -0,0 +1,33 @@
## 背景
后端(`pkg/models/share.go``NotifyWebhook``worker/internal/tasks/notify.go``sendWebhook`)已完整支持 `Body``BodyType`。前端 `WebhookItem` 接口和表单 UI 缺少 `body` 字段,`NotifyConfigField.vue` 中的 curl 导入处理函数仅为存根(解析命令后只 `console.log` 结果,未写入表单)。
前端 headers 存储格式为 `[string, string][]`(键值对数组),与 `KvInputGroupField` 的输出一致。后端模型使用 `map[string]string`——此不一致为既有问题,不在本次变更范围内。
## 目标 / 非目标
**目标:**
- 在 Webhook 高级面板中暴露 `body` textarea供用户配置请求体。
- 激活 curl 导入处理:粘贴 `curl` 命令 → 自动填入 URL、method、headers、body并展开高级面板。
- 为新字段添加 i18n 键。
**非目标:**
- `bodyType` 选择器form-data vs raw——body 默认以原始文本处理,本次不暴露 `bodyType` 字段。
- 修复前端 `[string, string][]` 与后端 `map[string]string` 的既有格式不一致问题。
- 后端及 Worker 变更——它们已支持 body。
## 决策
**D1 — 不在 UI 中暴露 `bodyType` 字段**
Worker 已对 `bodyType` 做分支处理form-data / raw / none但增加类型选择器会提升 UI 复杂度。默认使用 raw`bodyType` 未设置或为空 → body 原样发送)覆盖了大多数场景,也符合用户粘贴 curl 命令时的预期。后续可单独追加,不会产生 breaking change。
**D2 — curl 导入后自动展开高级面板**
curl 解析成功后headers 和 body 已被填入,但高级面板处于折叠状态,用户不可见。自动展开可让用户即时查看并验证填入的数据,避免出现隐式状态。
**D3 — curl 导入只预填表单,不自动提交**
导入仅预填表单字段,用户仍需手动点击保存按钮,避免意外副作用。
## 风险 / 权衡
- **`parseCurl``body` 字段不一定存在** — 对于 GET 请求,`sweet-curl-parser` 可能不填充 `data.body`。处理函数需用 `if (data.body)` 做判断再写入。→ 缓解:条件赋值,新建 Webhook 时默认 body 为空字符串。
- **curl headers 格式转换** — `parseCurl` 返回 `{name: string, value: string}[]`,表单期望 `[string, string][]`,转换逻辑为 `data.headers.map(h => [h.name, h.value])`。→ 无风险,但须显式处理。

View File

@@ -0,0 +1,24 @@
## 为什么
Webhook 通知目前无法配置请求体,导致所有需要 Payload 的 POST/PUT/PATCH Webhook 均无法使用。此外Webhook 配置界面的 curl 导入功能已部分实现但不可用——解析结果仅输出到控制台,从未写入表单。
## 变更内容
- 在前端 `WebhookItem` 类型中新增 `body` 字段,在 Webhook 高级设置面板中以 textarea 形式展示。
- 激活 curl 导入处理逻辑:用户在 URL 字段粘贴 `curl` 命令后,自动将 `url``method``headers``body` 填入表单,并展开高级面板。
- 在所有现有 locale 文件中新增 `webhookBody` i18n 翻译键。
## 功能模块
### 新增功能
- `webhook-body-field`Webhook 通知的请求体配置——用户可为每条 Webhook 指定随请求发送的原始 body 字符串。
### 修改功能
- `share-download-notify`Webhook 需求扩展——`notify_webhooks` 条目现在携带可选的 `body` 字段Worker 在该字段有值时 SHALL 将其作为 HTTP 请求体发送。
## 影响范围
- **前端**`front/components/Preprocessing/NotifyConfigField.vue` — 接口定义、模板、curl 处理函数、默认值。
- **前端 i18n**`front/` 下所有包含 `webhookHeaders` 键的 locale 文件。
- **后端模型**`pkg/models/share.go``NotifyWebhook` 结构体已有 `Body` 字段,无需变更。
- **Worker**`worker/internal/tasks/notify.go``sendWebhook` 已支持 `Body`,无需变更。

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Webhook notification sent on download
`share:notify` Worker 任务 SHALL 对 `notify_webhooks` 中每个配置项发起 HTTP 请求。
#### Scenario: POST webhook with form-data body
- **WHEN** Webhook `method=POST``bodyType=form-data``body="k=v"`
- **THEN** Worker 向 `url` 发起 POST 请求,`Content-Type: application/x-www-form-urlencoded`Body 为 `k=v`
#### Scenario: POST webhook with raw body
- **WHEN** Webhook `method=POST``bodyType=raw``bodyType` 为空、`body` 非空
- **THEN** Worker 向 `url` 发起 POST 请求Body 原样发送,不强制设置 Content-Type
#### Scenario: Webhook body empty or absent
- **WHEN** `body` 为空字符串或未设置
- **THEN** Worker 发起请求时不附加 Body
#### Scenario: Webhook with custom headers
- **WHEN** Webhook `headers` 中包含自定义请求头
- **THEN** Worker 将这些 Header 附加到请求中
#### Scenario: Webhook returns 4xx/5xx
- **WHEN** Webhook 目标返回 HTTP 状态码 >= 400
- **THEN** 该 Webhook 计为失败

View File

@@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Webhook body 字段可在 UI 中配置
系统 SHALL 在 Webhook 高级设置面板中暴露一个 `body` textarea允许用户为每条 Webhook 条目输入原始请求体字符串。
#### Scenario: 用户输入 body 文本
- **WHEN** 用户展开某条 Webhook 的高级面板并在 body textarea 中输入文本
- **THEN** body 值以 `notify_webhooks[n].body` 的路径存储到表单中
#### Scenario: body 字段默认为空
- **WHEN** 用户通过"添加"按钮新增一条 Webhook 条目
- **THEN** `body` 默认值为空字符串
---
### Requirement: curl 导入自动填充 method、headers 和 body
系统 SHALL 解析粘贴到 Webhook URL 字段的 `curl` 命令,并自动将解析结果中的 `url``method``headers``body` 填入表单,同时展开高级面板。
#### Scenario: 将包含 headers 的有效 curl 命令粘贴到 URL 字段
- **WHEN** 用户在 URL 输入框中粘贴以 `curl ` 开头的字符串,且字段失去焦点
- **THEN** `url` 被设置为 `data.url.fullUrl``method` 被设置为 `data.method`(大写),`headers` 被转换为 `[string, string][]` 键值对数组,对应 Webhook 条目的高级面板被展开
#### Scenario: curl 命令包含 body
- **WHEN** 解析后的 curl 结果中包含非空的 `body` 字段
- **THEN** `body` 被写入 `notify_webhooks[n].body`
#### Scenario: curl 命令不包含 body如 GET 请求)
- **WHEN** 解析后的 curl 结果中没有 `body` 字段或其值为空
- **THEN** `notify_webhooks[n].body` 保持为空字符串(不被 undefined 覆盖)
#### Scenario: 解析失败或输入不是 curl 命令
- **WHEN** URL 字段值不以 `curl ` 开头,或 `parseCurl` 返回 `success: false`
- **THEN** 表单中所有字段保持不变

View File

@@ -0,0 +1,28 @@
## 1. 前端 — 类型定义与默认值
- [x] 1.1 在 `NotifyConfigField.vue``WebhookItem` 接口中新增 `body?: string` 字段
- [x] 1.2 将"添加"按钮的默认值从 `{ url: '', method: 'POST', headers: [] }` 改为 `{ url: '', method: 'POST', headers: [], body: '' }`
## 2. 前端 — Body Textarea UI
- [x] 2.1 在 `NotifyConfigField.vue` 中从 `'../Field/TextareaField.vue'` 导入 `TextareaField`
- [x] 2.2 在 `v-show="expandedAdvanced.has(index)"` 区块内的 `<KvInputField>` 下方添加 `<TextareaField>`,绑定到 `notify_webhooks.${index}.body`label 使用 i18n 键 `page.shareOptions.notify.webhookBody``rows=4`placeholder 为 `{"key": "value"}`
## 3. 前端 — curl 导入处理函数
- [x] 3.1 将 `console.log('command', data)` 及注释掉的代码块替换为实际赋值逻辑:
- `setFieldValue(`notify_webhooks.${index}.url`, data.url.fullUrl)`
- `setFieldValue(`notify_webhooks.${index}.method`, data.method.toUpperCase())`
- `setFieldValue(`notify_webhooks.${index}.headers`, data.headers.map((h: any) => [h.name, h.value]))`
- `if (data.body) setFieldValue(`notify_webhooks.${index}.body`, data.body)`
- [x] 3.2 curl 解析成功后,将当前 index 加入 `expandedAdvanced` 以自动展开高级面板:`expandedAdvanced = new Set([...expandedAdvanced, index])`
## 4. i18n
- [x] 4.1 在 `front/i18n/locales/en.json``"webhookHeaders"` 键后新增 `"webhookBody": "Request Body"`
- [x] 4.2 在 `front/i18n/locales/zh-CN.json` 中新增 `"webhookBody": "请求体"`
- [x] 4.3 在 `front/i18n/locales/zh-TW.json` 中新增 `"webhookBody": "請求體"`
- [x] 4.4 在 `front/i18n/locales/de.json` 中新增 `"webhookBody": "Anfrage-Body"`
- [x] 4.5 在 `front/i18n/locales/fr.json` 中新增 `"webhookBody": "Corps de la requête"`
- [x] 4.6 在 `front/i18n/locales/ja.json` 中新增 `"webhookBody": "リクエストボディ"`
- [x] 4.7 在 `front/i18n/locales/ko.json` 中新增 `"webhookBody": "요청 본문"`

View File

@@ -0,0 +1,87 @@
# share-download-notify Specification
## Purpose
TBD - created by archiving change share-download-notify. Update Purpose after archive.
## Requirements
### Requirement: Notify config and creator locale persisted on share creation
创建分享时,系统 SHALL 将完整通知配置(`has_notify``notify_types``notify_emails``notify_webhooks`)以及创建人当前语言(`locale`)持久化到 Redis `RedisShareInfo` 中。
#### Scenario: Share created with email and webhook notify config and locale
- **WHEN** 客户端 POST `/share` 并携带 `config.has_notify=true``config.notify_types=["email","webhook"]``config.notify_emails``config.notify_webhooks``config.locale="zh-CN"`
- **THEN** Redis 中存储的 `RedisShareInfo` 包含完整的通知配置字段及 `locale="zh-CN"`
#### Scenario: Share created without notify
- **WHEN** 客户端 POST `/share` 并携带 `config.has_notify=false`
- **THEN** Redis 中 `has_notify=false`,通知相关字段为空/零值
---
### Requirement: Notify task enqueued on download validation
当分享下载验证(`VaildateShare`)成功且分享配置了通知时,系统 SHALL 向 Asynq 队列入队一个 `share:notify` 任务。
#### Scenario: Download validated with notify enabled
- **WHEN** 用户成功通过 `VaildateShare` 验证(密码正确、下载次数充足)且 `shareInfo.HasNotify=true` 且存在至少一个通知目标
- **THEN** 系统入队一条 `share:notify` 任务Payload 包含 `share_id`
- **THEN** 下载 token 正常返回,响应不受通知入队影响
#### Scenario: Download validated with notify disabled
- **WHEN** `shareInfo.HasNotify=false`
- **THEN** 不入队任何通知任务
---
### Requirement: Webhook notification sent on download
`share:notify` Worker 任务 SHALL 对 `notify_webhooks` 中每个配置项发起 HTTP 请求。
#### Scenario: POST webhook with form-data body
- **WHEN** Webhook `method=POST``bodyType=form-data``body="k=v"`
- **THEN** Worker 向 `url` 发起 POST 请求,`Content-Type: application/x-www-form-urlencoded`Body 为 `k=v`
#### Scenario: POST webhook with raw body
- **WHEN** Webhook `method=POST``bodyType=raw``bodyType` 为空、`body` 非空
- **THEN** Worker 向 `url` 发起 POST 请求Body 原样发送,不强制设置 Content-Type
#### Scenario: Webhook body empty or absent
- **WHEN** `body` 为空字符串或未设置
- **THEN** Worker 发起请求时不附加 Body
#### Scenario: Webhook with custom headers
- **WHEN** Webhook `headers` 中包含自定义请求头
- **THEN** Worker 将这些 Header 附加到请求中
#### Scenario: Webhook returns 4xx/5xx
- **WHEN** Webhook 目标返回 HTTP 状态码 >= 400
- **THEN** 该 Webhook 计为失败
### Requirement: Localized email notification sent on download
`share:notify` Worker 任务 SHALL 对 `notify_emails` 中每个邮件地址通过 SMTP 发送通知邮件,邮件语言使用 `RedisShareInfo.Locale` 对应的翻译,不支持的 locale 回退到英文。
#### Scenario: Email sent in creator's locale
- **WHEN** SMTP 配置完整(`smtp.host` 非空)且 `shareInfo.Locale="zh-CN"`
- **THEN** Worker 向目标地址发送邮件,邮件 Subject 和 Body 为中文内容
#### Scenario: Email locale fallback to English
- **WHEN** `shareInfo.Locale` 为空或不支持的语言代码
- **THEN** Worker 使用英文模板发送邮件
#### Scenario: SMTP not configured
- **WHEN** `smtp.host` 为空
- **THEN** 邮件通知被跳过(不计入失败数),记录 warn 日志
---
### Requirement: Partial success means task success; all-fail triggers retry
`share:notify` 任务 SHALL 仅在所有通知目标(邮件 + Webhook均失败时才返回 error只要有一个成功任务 SHALL 返回 nil。
#### Scenario: One webhook succeeds, one email fails
- **WHEN** 2 个通知目标中Webhook 成功、Email 失败
- **THEN** 任务返回 nil不重试
#### Scenario: All notifications fail
- **WHEN** 所有 Webhook 和 Email 均失败
- **THEN** 任务返回聚合 errorAsynq 按默认策略重试
#### Scenario: Share expired when task executes
- **WHEN** Worker 执行时 Redis 中已无对应 `shareInfo`share 已过期)
- **THEN** 任务返回 nil静默跳过不重试

View File

@@ -0,0 +1,37 @@
# webhook-body-field Specification
## Purpose
TBD - created by archiving change add-webhook-body-and-fix-curl-import. Update Purpose after archive.
## Requirements
### Requirement: Webhook body 字段可在 UI 中配置
系统 SHALL 在 Webhook 高级设置面板中暴露一个 `body` textarea允许用户为每条 Webhook 条目输入原始请求体字符串。
#### Scenario: 用户输入 body 文本
- **WHEN** 用户展开某条 Webhook 的高级面板并在 body textarea 中输入文本
- **THEN** body 值以 `notify_webhooks[n].body` 的路径存储到表单中
#### Scenario: body 字段默认为空
- **WHEN** 用户通过"添加"按钮新增一条 Webhook 条目
- **THEN** `body` 默认值为空字符串
---
### Requirement: curl 导入自动填充 method、headers 和 body
系统 SHALL 解析粘贴到 Webhook URL 字段的 `curl` 命令,并自动将解析结果中的 `url``method``headers``body` 填入表单,同时展开高级面板。
#### Scenario: 将包含 headers 的有效 curl 命令粘贴到 URL 字段
- **WHEN** 用户在 URL 输入框中粘贴以 `curl ` 开头的字符串,且字段失去焦点
- **THEN** `url` 被设置为 `data.url.fullUrl``method` 被设置为 `data.method`(大写),`headers` 被转换为 `[string, string][]` 键值对数组,对应 Webhook 条目的高级面板被展开
#### Scenario: curl 命令包含 body
- **WHEN** 解析后的 curl 结果中包含非空的 `body` 字段
- **THEN** `body` 被写入 `notify_webhooks[n].body`
#### Scenario: curl 命令不包含 body如 GET 请求)
- **WHEN** 解析后的 curl 结果中没有 `body` 字段或其值为空
- **THEN** `notify_webhooks[n].body` 保持为空字符串(不被 undefined 覆盖)
#### Scenario: 解析失败或输入不是 curl 命令
- **WHEN** URL 字段值不以 `curl ` 开头,或 `parseCurl` 返回 `success: false`
- **THEN** 表单中所有字段保持不变