mirror of
https://github.com/keven1024/015.git
synced 2026-05-26 07:08:02 +00:00
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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-28
|
||||
@@ -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`),且这些字段从未被持久化到 Redis,Worker 中的 `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 推荐 <1KB),Webhook 配置可能较大。通过 `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` 同时支持 STARTTLS(587)和 Implicit TLS(465),API 更直观,邮件构造(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。
|
||||
@@ -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`)
|
||||
@@ -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** 任务返回聚合 error,Asynq 按默认策略重试
|
||||
|
||||
#### Scenario: Share expired when task executes
|
||||
- **WHEN** Worker 执行时 Redis 中已无对应 `shareInfo`(share 已过期)
|
||||
- **THEN** 任务返回 nil(静默跳过,不重试)
|
||||
@@ -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 和 Body,locale 不存在时 fallback 到 `en`
|
||||
- [x] 6.3 实现 `sendWebhook(webhook models.NotifyWebhook) error`:使用 resty,按 `BodyType` 设置 Content-Type,附加 Headers,HTTP 状态 >= 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,否则返回 nil;share 已过期(`shareInfo == nil`)直接返回 nil
|
||||
|
||||
## 7. Worker —— 路由注册
|
||||
|
||||
- [x] 7.1 在 `worker/main.go` 的 `mux.HandleFunc` 列表中新增 `mux.HandleFunc("share:notify", tasks.ShareNotify)`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-01
|
||||
@@ -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])`。→ 无风险,但须显式处理。
|
||||
@@ -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`,无需变更。
|
||||
@@ -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 计为失败
|
||||
@@ -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** 表单中所有字段保持不变
|
||||
@@ -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": "요청 본문"`
|
||||
87
openspec/specs/share-download-notify/spec.md
Normal file
87
openspec/specs/share-download-notify/spec.md
Normal 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** 任务返回聚合 error,Asynq 按默认策略重试
|
||||
|
||||
#### Scenario: Share expired when task executes
|
||||
- **WHEN** Worker 执行时 Redis 中已无对应 `shareInfo`(share 已过期)
|
||||
- **THEN** 任务返回 nil(静默跳过,不重试)
|
||||
|
||||
37
openspec/specs/webhook-body-field/spec.md
Normal file
37
openspec/specs/webhook-body-field/spec.md
Normal 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** 表单中所有字段保持不变
|
||||
|
||||
Reference in New Issue
Block a user