17 KiB
17 KiB
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 board(D-01/I-01)已覆盖该设计缺口,本次仅补充
BuildIngressAck的单元测试(与 review P1-2 对应),不改变 newapi 仍为 501 占位的事实。 - 不引入 testcontainers-go:P2-3 仅做 skip 回退与文档标注,不做完整容器化测试基础设施。
- 不改写 outbox 并发 claim / transactional outbox:属于旧 remediation board(I-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 任务必须自测)
go build ./...零错误。go vet ./...零警告。- 涉及改动的包:
go test -race ./<changed_pkg>/...通过。 - 若修改了 exported 函数签名,确认调用方编译通过。
7. 阶段门控结论
7.1 当前状态
- 设计完整性:本方案已覆盖 review 报告全部 P0/P1/P2 项,任务粒度 <= 5 分钟,文件路径与函数名已精确锁定。
- 风险可控性:P1-4/P2-4 有较高风险,但已设计明确的降级策略(超时兜底 + race 检测 + 回滚路径)。
- 与旧 remediation board 兼容性:
- 本次 P1-2(newapi_adapter 测试)与旧 board 的 I-01(newapi 假接通)正交:本次仅补测试覆盖,不改变 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 批 commit(docs / 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 XL(TechLead / 负责人)必补门控
- 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)继续保留,不得因本次方案而关闭或删除。
自检清单(返回时显式列出打勾状态)
- 架构设计覆盖 review 报告所有 P0/P1 项
- 每个任务 < 5分钟,有明确文件路径
- 风险评估完整
- 降级策略已设计
- 实施漂移检测点已定义
- 已明确标记是否可进入 Engineer 实现
- 已给出 Engineer / QA / XL 的下游执行约束摘要