Files
ai-customer-service/docs/REMEDIATION_PLAN_2026-05-11.md

247 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ai-customer-service 生产上线修复方案与技术任务拆解
> 生成日期2026-05-11
> 基线 commit`67922c5` (HEAD)
> 技术负责人TechLead
> 对应 Review 报告:`docs/CODE_REVIEW_REPORT_SYSTEMATIC_2026-05-11.md`
---
## 1. 设计范围
### 1.1 本次覆盖
| Review 报告项 | 优先级 | 本次是否覆盖 |
|---|---|---|
| P0-1 Dirty worktree 收口 | P0 | ✅ 覆盖 |
| P0-2 Makefile test 目标缺少 `-p 1` | P0 | ✅ 覆盖 |
| P1-1 ticket_handler.List 覆盖率 0% | P1 | ✅ 覆盖 |
| P1-2 newapi_adapter.BuildIngressAck 覆盖率 0% | P1 | ✅ 覆盖 |
| P1-3 Authz header 伪造风险未文档化 | P1 | ✅ 覆盖 |
| P1-4 RateLimiter GC 压力 | P1 | ✅ 覆盖 |
| P1-5 IPv6 地址在 rate limit key 中被错误截断 | P1 | ✅ 覆盖 |
| P2-1 配置解析失败静默回退 | P2 | ✅ 覆盖 |
| P2-2 callback worker 无连接池限制 | P2 | ✅ 覆盖 |
| P2-3 缺少 SQLite/内存测试回退 | P2 | ✅ 覆盖 |
| P2-4 worker 缺少优雅关闭等待 | P2 | ✅ 覆盖 |
### 1.2 明确不做
- **不引入 pgx 替换 lib/pq**review 报告建议项,但当前无功能缺口,不属于阻塞问题。
- **不实现 newapi 完整 ingress 逻辑**:旧 remediation boardD-01/I-01已覆盖该设计缺口本次仅补充 `BuildIngressAck` 的单元测试(与 review P1-2 对应),不改变 newapi 仍为 501 占位的事实。
- **不引入 testcontainers-go**P2-3 仅做 skip 回退与文档标注,不做完整容器化测试基础设施。
- **不改写 outbox 并发 claim / transactional outbox**:属于旧 remediation boardI-04/I-05范围本次不做。
### 1.3 与 review 报告对应关系
| 本方案章节 | Review 报告章节 | 问题编号 |
|---|---|---|
| 3.1 P0 修复 | 5. P0 — 阻塞级 | P0-1, P0-2 |
| 3.2 P1 修复 | 5. P1 — 必须修复 | P1-1 ~ P1-5 |
| 3.3 P2 修复 | 5. P2 — 建议修复 | P2-1 ~ P2-4 |
---
## 2. 修复方案总览
| 问题 | 技术方案概述 |
|---|---|
| **P0-1 Dirty worktree** | 分 2 批 commit① 文档文件8 modified + 4 untracked docs② 代码文件3 modified internal。commit 后打 tag `v0.9.1-pre`。 |
| **P0-2 Makefile test** | `Makefile:2` 改为 `go test ./... -count=1 -p 1`,与 README/CI 保持一致。 |
| **P1-1 ticket_handler.List 0%** | 在 `ticket_handler_test.go` 补充 `List` 的成功与错误分支单元测试,使用现有 `mockTicketService` + `memory.TicketStore`。 |
| **P1-2 newapi_adapter.BuildIngressAck 0%** | 新建 `internal/platformadapter/newapi_adapter_test.go`,覆盖 `BuildIngressAck(meta=nil)``meta!=nil` 两个分支。 |
| **P1-3 Authz 伪造风险** | 新建 `docs/SECURITY_BOUNDARY.md`,明确 `RequireRoles` 的信任边界;在 `authz.go` 函数注释中标注“依赖上游网关完成真实身份验证”。 |
| **P1-4 RateLimiter GC 压力** | `limits.go:67-73``var valid []time.Time` 新分配改为原地双指针过滤,复用 `sw.tokens` 底层数组。 |
| **P1-5 IPv6 截断** | `limits.go:110-114` 将手动 `lastIndexByte(addr, ':')` 替换为 `net.SplitHostPort`,正确提取 IPv6 host补充 IPv6 单元测试。 |
| **P2-1 配置静默回退** | `config.go` 新增 `mustGetEnvInt` / `mustGetEnvBool`(解析失败返回 error`Load()` 生产模式下对关键数值型配置启用严格解析。 |
| **P2-2 worker 连接池** | `app.go:172` 将裸 `&http.Client{Timeout: ...}` 替换为显式配置 `Transport.MaxIdleConns` / `MaxIdleConnsPerHost` 的 client。 |
| **P2-3 测试回退** | `test/e2e/*_test.go``test/integration/*_test.go``TestMain` 或每个 `Test*` 开头增加 PostgreSQL 连通性检测,不通则 `t.Skip`。 |
| **P2-4 worker 优雅关闭** | `app.go` 在 startWorker 中引入 `sync.WaitGroup``wg.Add(1)` + goroutine `defer wg.Done()`closer 中 `cancel()` 后执行 `wg.Wait()`(带 5s 超时兜底)。 |
---
## 3. 任务拆解表
> 粒度约束:每个任务 2-5 分钟,必须有具体文件路径和函数名。
### 3.1 P0 — 阻塞级
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P0-1a** | 全仓库 | `git status` | 确认 dirty 文件清单,按 "docs" / "code" 分组 | 2 min |
| **P0-1b** | 全仓库 | `git commit` | 批次 1提交 12 个 docs 文件modified + untracked消息 `docs: sync review reports, runbooks, and checklists` | 3 min |
| **P0-1c** | 全仓库 | `git commit` | 批次 2提交 3 个 internal/ 文件;消息 `fix: platform event store and builder drift` | 3 min |
| **P0-1d** | 全仓库 | `git tag` | 打 tag `v0.9.1-pre`;推送 tag | 2 min |
| **P0-2a** | `Makefile:2` | `test` target | 将 `go test ./...` 改为 `go test ./... -count=1 -p 1` | 2 min |
### 3.2 P1 — 必须修复
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P1-1a** | `internal/http/handlers/ticket_handler_test.go` | `TestTicketHandlerList_Success` | 新增测试:向 `mockTicketService.tickets` Create 2 条 ticket调用 `h.List`,断言返回 200 且 `items` 数组长度为 2 | 3 min |
| **P1-1b** | `internal/http/handlers/ticket_handler_test.go` | `TestTicketHandlerList_ServiceError` | 新增测试:注入一个返回 error 的 `TicketService` mock或改 `ListOpen` 返回 `errors.New("db down")`),断言返回 500 且 error code 为 `CS_SYS_5002` | 3 min |
| **P1-2a** | `internal/platformadapter/newapi_adapter_test.go` | `TestNewAPIAdapter_BuildIngressAck_NilMeta` | 新建文件与测试:`adapter.BuildIngressAck(nil, nil)` 断言返回 `map[string]any{"accepted":false,"platform":"newapi"}` | 3 min |
| **P1-2b** | `internal/platformadapter/newapi_adapter_test.go` | `TestNewAPIAdapter_BuildIngressAck_WithMeta` | 同上文件:传入 `&PlatformInboundMeta{EventID:"evt-1"}`,断言返回包含 `event_id:"evt-1"` | 2 min |
| **P1-3a** | `docs/SECURITY_BOUNDARY.md` | — | 新建文档:明确标注 `internal/http/middleware/authz.go:RequireRoles` 仅做 RBAC 白名单校验,不验证 header 真实性;生产部署必须前置 API Gateway / JWT 验证 | 3 min |
| **P1-3b** | `internal/http/middleware/authz.go:42` | `RequireRoles` | 在函数注释头增加 `// SECURITY: This middleware trusts the upstream gateway to authenticate the actor headers.` | 2 min |
| **P1-4a** | `internal/platform/httpx/limits.go:67-73` | `RateLimiter.Allow` | 将 `var valid []time.Time` + `append` 循环改为原地双指针过滤:`n := 0; for _, t := range sw.tokens { if t.After(cutoff) { sw.tokens[n] = t; n++ } }; sw.tokens = sw.tokens[:n]` | 3 min |
| **P1-4b** | `internal/platform/httpx/limits_test.go` | 现有测试 | 运行 `go test -race ./internal/platform/httpx/...`,确认零 DATA RACE | 2 min |
| **P1-5a** | `internal/platform/httpx/limits.go:98-115` | `rateLimitKey` | 导入 `net`;将 `lastIndexByte(addr, ':')` 截断逻辑替换为 `host, _, err := net.SplitHostPort(addr)`err==nil 则返回 host否则返回原值 | 3 min |
| **P1-5b** | `internal/platform/httpx/limits.go:117-124` | `lastIndexByte` | 删除 `lastIndexByte` 函数(若已无其他引用) | 2 min |
| **P1-5c** | `internal/platform/httpx/limits_test.go` | `TestRateLimitKey_IPv6` | 新增测试:`rateLimitKey``req.RemoteAddr = "[::1]:8080"` 应返回 `"::1"`;对 `"[2001:db8::1]:8080"` 应返回 `"2001:db8::1"` | 3 min |
### 3.3 P2 — 建议修复
| 任务 ID | 文件路径 | 函数/位置 | 预期变更 | 估计耗时 |
|---|---|---|---|---|
| **P2-1a** | `internal/config/config.go:201-255` | `getEnvInt` / `getEnvBool` | 新增 `mustGetEnvInt(key string) (int, error)``mustGetEnvBool(key string) (bool, error)`:解析失败时返回 `fmt.Errorf` 而非静默 fallback | 3 min |
| **P2-1b** | `internal/config/config.go:66-148` | `Load()` | 生产模式下,对 `AI_CS_WEBHOOK_MAX_SKEW_SECONDS` 等关键数值配置若环境变量存在但解析失败,返回 error可选仅替换最危险的 2-3 个字段以控制范围) | 4 min |
| **P2-2a** | `internal/app/app.go:172` | `startWorker` 内 client 创建 | 将 `&http.Client{Timeout: ...}` 替换为 `&http.Client{Timeout: ..., Transport: &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 10}}` | 3 min |
| **P2-3a** | `test/e2e/*_test.go` | `TestMain` 或首个 Test | 增加 `pgCheck()`:尝试 `sql.Open("postgres", dsn).Ping()`,失败则 `t.Skip("PostgreSQL not available")` | 3 min |
| **P2-3b** | `test/integration/*_test.go` | `TestMain` 或首个 Test | 同上增加 skip 逻辑 | 3 min |
| **P2-4a** | `internal/app/app.go:158-188` | `startWorker` 闭包 | 在 `New` 函数内声明 `var workerWg sync.WaitGroup``startWorker``workerWg.Add(1)`goroutine 内 `defer workerWg.Done()``worker.Start(workerCtx)` | 3 min |
| **P2-4b** | `internal/app/app.go:164-167` | worker closer | 将 closer 改为:`cancel(); done := make(chan struct{}); go func() { workerWg.Wait(); close(done) }(); select { case <-done: return nil; case <-time.After(5 * time.Second): return fmt.Errorf("worker %s shutdown timeout", platform) }` | 4 min |
| **P2-4c** | `internal/app/app.go` | `import` | 确认新增 `sync``time`time 通常已有)导入 | 2 min |
---
## 4. 风险与保护
### 4.1 风险清单
| 风险 ID | 来源任务 | 风险描述 | 等级 |
|---|---|---|---|
| R-01 | P0-1b/c | 批量 commit 可能将未评审代码带入基线 | 🟡 中 |
| R-02 | P1-4a | 限流器原地过滤改动若索引越界,可能 panic 核心路径 | 🔴 高 |
| R-03 | P1-5a | `net.SplitHostPort` 对 IPv6 兼容但可能改变 IPv4 行为(实际不会,但需验证) | 🟡 中 |
| R-04 | P2-1b | 严格配置解析可能破坏现有开发/测试环境(如 `AI_CS_MAX_BODY_BYTES=1MB` 拼写错误导致启动失败) | 🟡 中 |
| R-05 | P2-4a/b | `sync.WaitGroup` 使用不当可能导致 `Shutdown` 死锁或 panic`wg.Add` 在 goroutine 启动后调用) | 🔴 高 |
| R-06 | 全量 | 任何代码改动引入 race condition | 🟡 中 |
### 4.2 降级策略
| 风险 ID | 降级策略 |
|---|---|
| R-01 | commit 前执行 `git diff --cached` 复核docs 与代码分开 commit一旦有问题可单独 revert。 |
| R-02 | ① 改前通读 `limits_test.go` 确保有覆盖;② 改后必跑 `go test -race ./internal/platform/httpx/...`;③ 若发现异常,立即回滚到 `var valid []time.Time` 方案。 |
| R-03 | 在 `limits_test.go` 中保留原有 IPv4 用例并追加 IPv6 用例;若 CI 失败,回滚到字符串处理但修复 IPv6 专用分支。 |
| R-04 | P2-1b 仅对生产模式 (`cfg.Runtime.Env == "production"`) 生效;开发/测试环境保持静默回退。若生产启动失败,工程师可立即切回旧 `getEnvInt`。 |
| R-05 | ① `wg.Add(1)` 必须紧接在 `go func()` 之前(同一线程);② closer 中 `wg.Wait()` 必须带 `time.After` 超时;③ 改后运行 `go test ./internal/app/...` 并手动发送 SIGTERM 验证无死锁。 |
| R-06 | 全量代码任务完成后统一执行 `go test -race ./internal/... -count=1 -p 1`;任何 race 报告阻塞合并。 |
---
## 5. QA 交接与实施约束
### 5.1 编码后漂移检查点QA 可验证)
| 检查点 ID | 验证命令 / 步骤 | 通过标准 |
|---|---|---|
| CP-01 | `cd /home/long/project/ai-customer-service && git status --short` | 零 modified / 零 untracked或仅有本次计划外的新 review 报告) |
| CP-02 | `make test` | 等价于 `go test ./... -count=1 -p 1`零失败postgres/e2e/integration skip 属于预期行为) |
| CP-03 | `go test -race ./internal/... -count=1 -p 1` | 24/24 pass零 DATA RACE |
| CP-04 | `go test ./internal/http/handlers/... -coverprofile=/tmp/handlers.out && go tool cover -func=/tmp/handlers.out \| grep ticket_handler.go` | `List` 函数覆盖率 > 0% |
| CP-05 | `go test ./internal/platformadapter/... -coverprofile=/tmp/pa.out && go tool cover -func=/tmp/pa.out \| grep newapi_adapter.go` | `BuildIngressAck` 覆盖率 > 0% |
| CP-06 | `go test ./internal/platform/httpx/...` | 全部通过,包括新增 IPv6 用例 |
| CP-07 | `ls docs/SECURITY_BOUNDARY.md && head -n 20 docs/SECURITY_BOUNDARY.md` | 文件存在,且首段包含 "RequireRoles" 和 "upstream gateway" 关键词 |
| CP-08 | `grep -n "sync.WaitGroup\|workerWg" internal/app/app.go` | 至少出现 `workerWg.Add(1)``defer workerWg.Done()``workerWg.Wait()` 三处 |
| CP-09 | `grep -n "MaxIdleConns" internal/app/app.go` | 出现 `MaxIdleConns``MaxIdleConnsPerHost` |
| CP-10 | `go vet ./...` | 零警告 |
### 5.2 必查真实调用链路
| 链路 | 验证方式 |
|---|---|
| **RateLimiter 核心路径** | `TestRateLimiter_WithRateLimit` 必须实际触发 `Allow` 并通过QA 可单步确认 `rateLimitKey` 返回预期值。 |
| **Authz 信任边界** | QA 手动阅读 `docs/SECURITY_BOUNDARY.md``authz.go` 注释,确认两者口径一致。 |
| **Worker Graceful Shutdown** | QA 本地启动服务后发送 SIGTERM`kill -TERM <pid>`),观察日志确认 worker 在 5s 内完成退出,无 `shutdown timeout` error。 |
| **Config 严格模式** | QA 设置 `AI_CS_RUNTIME_ENV=production` + `AI_CS_WEBHOOK_MAX_SKEW_SECONDS=not_a_number`,启动服务应报错并退出。 |
---
## 6. Engineer 实施说明
### 6.1 文件级落点
| 目标文件 | 落点行号 | 改动性质 |
|---|---|---|
| `Makefile` | 第 2 行 | 替换 |
| `internal/http/handlers/ticket_handler_test.go` | 文件末尾 | 追加 2 个 Test 函数 |
| `internal/platformadapter/newapi_adapter_test.go` | 新建 | 2 个 Test 函数 |
| `docs/SECURITY_BOUNDARY.md` | 新建 | Markdown 文档 |
| `internal/http/middleware/authz.go` | 第 42 行上方 | 添加注释块 |
| `internal/platform/httpx/limits.go` | 第 67-73 行 | 替换为原地过滤 |
| `internal/platform/httpx/limits.go` | 第 98-124 行 | 替换 `rateLimitKey` + 删除 `lastIndexByte` |
| `internal/platform/httpx/limits_test.go` | 文件末尾 | 追加 IPv6 Test 函数 |
| `internal/config/config.go` | 第 201-255 行之后 | 追加 `mustGetEnvInt` / `mustGetEnvBool` |
| `internal/config/config.go` | 第 66-148 行 | 条件性替换部分 `getEnvInt` 调用 |
| `internal/app/app.go` | 第 158-188 行 | 重构 startWorker 闭包 |
| `internal/app/app.go` | 第 172 行 | 替换 http.Client 创建 |
| `test/e2e/*_test.go` | 首个 Test 或 TestMain | 追加 skip 逻辑 |
| `test/integration/*_test.go` | 首个 Test 或 TestMain | 追加 skip 逻辑 |
### 6.2 最小验证项Engineer 每完成一个 P1/P2 任务必须自测)
1. `go build ./...` 零错误。
2. `go vet ./...` 零警告。
3. 涉及改动的包:`go test -race ./<changed_pkg>/...` 通过。
4. 若修改了 exported 函数签名,确认调用方编译通过。
---
## 7. 阶段门控结论
### 7.1 当前状态
- **设计完整性**:本方案已覆盖 review 报告全部 P0/P1/P2 项,任务粒度 <= 5 分钟,文件路径与函数名已精确锁定。
- **风险可控性**P1-4/P2-4 有较高风险,但已设计明确的降级策略(超时兜底 + race 检测 + 回滚路径)。
- **与旧 remediation board 兼容性**
- 本次 P1-2newapi_adapter 测试)与旧 board 的 I-01newapi 假接通)正交:本次仅补测试覆盖,不改变 newapi 仍为 501 占位的事实。
- 本次不涉及旧 board 的 D-01/D-02/D-03/D-04平台能力矩阵、callback_target 契约、outbox 并发 claim这些仍按旧 board 排期执行。
### 7.2 结论
**✅ 可进入 Engineer 实现。**
前提条件Engineer 必须严格按照本方案第 3 章的任务拆解顺序执行,禁止自行扩大范围(如顺带重写 newapi adapter 或引入 pgx
---
## 8. 下游执行约束摘要
### 8.1 Engineer 禁止偏离
- **禁止**在修复 P1-2 时顺带实现 newapi 完整 ingress 逻辑(仍保持 501 占位)。
- **禁止**改动 `lib/pq``pgx`
- **禁止**修改任何不属于本方案列出的文件,除非发现编译阻断。
- **禁止**跳过 `go test -race` 自测。
- P0-1 必须分 2 批 commitdocs / code禁止一次性混提交。
### 8.2 QA 必查链路
- `make test` 行为与 `-p 1` 一致性。
- `go test -race ./internal/... -count=1 -p 1` 零 race。
- ticket_handler.List 与 newapi_adapter.BuildIngressAck 的覆盖率从 0% 提升到 > 0%。
- `docs/SECURITY_BOUNDARY.md``authz.go` 注释口径一致。
- IPv6 rate limit key 的正确性(通过单元测试)。
- Worker graceful shutdown 的 SIGTERM 手动验证。
### 8.3 XLTechLead / 负责人)必补门控
- P0-1 commit 后复核 `git log --oneline -5``git status`,确认 worktree 已清。
- P0-1d 打 tag 后XL 必须亲自确认 tag 存在:`git describe --tags`
- 全量任务完成后XL 执行一次 `go test ./... -count=1 -p 1` 并留存输出截图/文本作为最终证据。
- 旧 remediation board 中与本方案无冲突的项D-01 ~ I-05继续保留不得因本次方案而关闭或删除。
---
## 自检清单(返回时显式列出打勾状态)
- [x] 架构设计覆盖 review 报告所有 P0/P1 项
- [x] 每个任务 < 5分钟有明确文件路径
- [x] 风险评估完整
- [x] 降级策略已设计
- [x] 实施漂移检测点已定义
- [x] 已明确标记是否可进入 Engineer 实现
- [x] 已给出 Engineer / QA / XL 的下游执行约束摘要