docs(refactor): 插件化改造各阶段完成报告与审计记录归档

issues/plugin-refactor/:Phase-0~2 的任务完成报告、阶段总结、
两轮内核审计报告、Phase-2 接缝设计双视角评审与实施后对抗审计、调用点清单。
This commit is contained in:
shaw
2026-06-11 23:00:19 +08:00
parent 1cdab6f858
commit 7d67eb083a
28 changed files with 1072 additions and 0 deletions
@@ -0,0 +1,58 @@
# 阶段总结报告: Phase-0 回归安全网(Safety Net)
- **阶段状态**: Completed
- **完成时间**: 2026-06-11
- **关联阶段计划**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/PHASE_PLAN.md)
## 1. 阶段目标达成情况
目标"把'绝不引入回归'变成机器可验证的硬约束"已达成:
1. ✅ INVARIANTS.md:八类 30 条不变量,全部标注覆盖状态(含两处按实测修正的事实基线);
2. ✅ 新增测试在当前 main 上全绿(绿色基线成立);
3. ✅ 流式 SSE 逐事件 + 非流式逐字节透传断言就位(anthropic/openai/gemini 三路径);
4. ✅ 计费精度不变量含 PR 3061 三个漂移点(overages/1h-cache/ImageCount)全部金额级断言;
5. ✅ 调度不变量(粘性会话、failover 10/3/3 完整循环、槽配平、等待队列)就位;
6. ✅ 基准基线入库 + benchstat/awk 双对比脚本(allocs 严格 / ns 宽松 15%);
7. ✅ `go test ./internal/...`(无标签)与 `-tags=unit` 全量均 0 FAIL;vet 干净;CI invariants job 为 PR 阻断门禁。
## 2. 任务完成统计
| 任务ID | 任务描述 | 状态 | 完成报告 |
|:--|:--|:--|:--|
| TASK-001 | 回归不变量清单与缺口分析 | Completed | [链接](./TASK-001_invariants_inventory.md) |
| TASK-002 | 透传/拦截特征化测试 | Completed | [链接](./TASK-002_passthrough_characterization.md) |
| TASK-003 | 计费精度不变量测试 | Completed | [链接](./TASK-003_billing_invariants.md) |
| TASK-004 | 调度与并发不变量测试 | Completed | [链接](./TASK-004_scheduling_invariants.md) |
| TASK-005 | 性能基准基线与 CI gate | Completed | [链接](./TASK-005_bench_baseline_ci.md) |
**总计**: 5 个任务全部完成。新增测试代码约 3700 行(11 个测试文件、42+ 测试函数)+ 2 个全链路基准 + 脚本与 CI 配置;**业务代码零改动**。
## 3. 关键技术成果
- 后续所有插件化 PR 的硬性合并门禁:`make test-invariants`(CI invariants job)+ `scripts/bench-baseline.sh compare`(本地/PR 人工);
- 全链路 Forward 基准基线:非流式 ~10.2µs/93 allocs、流式 ~29.6µs/134 allocs(allocs 跨采样零漂移,可灵敏捕获 Phase-3 adapter 引入的开销)。
## 4. 遇到的问题与解决方案
- **问题1**: TASK-002/003 实施代理两次遭遇上游 API 过载(529)在收尾阶段中断
- **解决**: 产出文件已落盘且质量达标,主控完成验证(全量测试/vet/抽查断言粒度)并代写完成报告;后续任务改为单代理串行执行。
- **问题2**: INVARIANTS 初稿两处与实测不符(failover 耗尽错误体路径差异、计费为 float64 非 decimal)
- **解决**: 按"以代码为准"原则实测修正清单与任务规格,体现了"清单须经验证"流程的必要性。
## 5. 技术债务与待优化项(不阻塞,移交后续阶段处理)
- 生产热路径上有 stdlib `log.Printf`(透传分支每请求一条),基准已静音处理;Phase-3 anthropic 搬家时可顺手评估收敛到结构化日志(属行为等价优化,需单独 PR);
- `AcquireResult.ReleaseFunc` 无 once 保护(依赖 ZREM 幂等 + wrapReleaseOnDone),已固化现状,Phase-3 建缝时保持语义不变;
- gemini/openai 完整 Forward e2e 失败循环因内置长退避无法纳入单测(已用循环契约测试替代),真实环境冒烟清单中补充;
- golangci-lint 在本地 WSL /mnt 盘超时,依赖 CI 把关。
## 6. 经验总结与建议
- "先盘点缺口再补齐"避免了对 627 个现有测试文件的重复建设;
- characterization 纪律(固化现状而非应然值)在 TASK-004 发现错误体路径差异时发挥了作用——若按 INVARIANTS 初稿写"应然断言",会在 main 上直接红灯。
## 7. 下一阶段准备
- Phase-1(插件内核)就绪:5 个任务规格已细化(.claude/plugin-refactor/phases/phase-1_plugin-kernel/);
- Phase-1 全程以本阶段安全网为门禁:每个 PR 须 `make test-invariants` 全绿 + bench compare 无劣化。
@@ -0,0 +1,36 @@
# 完成报告: [TASK-001] 回归不变量清单与现有测试缺口分析
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/TASK-001_invariants_inventory.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
通过两轮并行代码调研(现有 627 个测试文件的覆盖盘点 + 不变量行为的代码事实核实),产出 `.claude/plugin-refactor/INVARIANTS.md`:八类共 30 条不变量,每条标注覆盖状态与代码事实位置,并为 TASK-002/003/004 划定了精确的补齐清单。
**调研中的重要发现(影响后续任务):**
1. 计费金额实现为 **float64**(非 decimal 库),TASK-003 规格中"decimal 精确断言"已修订为"固定期望值 + ≤1e-10 容差";
2. `make test-e2e` 引用的 `scripts/e2e-test.sh` **不存在**,可用的是 `test-e2e-local`(env 驱动);已列入 TASK-005 顺手处理;
3. failover 上限为 handler 层常量:anthropic=10(gateway_handler.go:77)、gemini=3(:78)、openai=3(openai_gateway_handler.go:111);
4. 内容审核为 **fail-open**(审核服务失败时放行)——这是必须锁定的安全语义;
5. PR 3061 三个计费漂移点在当前代码中的位置均已定位(overages=accounts.extra.allow_overages、5m/1h 双档缓存价、ImageOutputTokens 独立计价),现有测试对这三点均无金额级断言。
## 2. 核心计划回顾
> 1. 通读热路径关键文件,固化"外部可观测行为"列表(八类);
> 2. 盘点现有 gateway_*/billing_*/concurrency_* 测试,建立"不变量 → 现有测试"映射;
> 3. 重点核对 PR 3061 三个计费漂移点的现有断言情况;
> 4. 写 INVARIANTS.md,附覆盖标注与补齐分派。
全部按计划完成。
## 3. 文件变更详情
### 创建的文件
- `.claude/plugin-refactor/INVARIANTS.md` — 八类 30 条不变量清单 + 补齐分派 + 测试基础设施备注
### 修改的文件
- `.claude/plugin-refactor/phases/phase-0_safety-net/TASK-003_billing_invariants.md` — DoD 第 1 条按 float64 现实修订断言策略
### 业务代码
- 零改动(符合任务约束)
@@ -0,0 +1,46 @@
# 完成报告: [TASK-002] 网关流式/非流式透传特征化测试
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/TASK-002_passthrough_characterization.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
新增 4 个特征化测试文件(共 1147 行,17 个测试函数),覆盖 INVARIANTS.md 分派给本任务的全部 7 条不变量。实施代理在收尾阶段遭遇 API 过载中断,最终验证与报告由主控完成:`go test -tags=unit ./internal/...` 全量绿、`go vet` 干净、零业务代码改动。
## 2. 不变量 → 测试映射
| 不变量 | 测试函数 | 文件 |
|---|---|---|
| I-1.1 透传(逐字节) | TestGatewayCharacterization_AnthropicNonStreamPassthroughByteExact / _AnthropicStreamPassthroughEventSequence | service/gateway_passthrough_characterization_test.go |
| I-1.4 header 过滤 | TestGatewayCharacterization_ResponseHeaderFilter | 同上 |
| I-1.7 错误透传规则 | TestGatewayCharacterization_UpstreamErrorPassthroughRules / _Upstream400BodyPassthrough | 同上 |
| I-1.5 openai 透传 | TestGatewayCharacterization_OpenAIStreamPassthrough / _OpenAINonStreamPassthroughByteExact / _OpenAIPassthroughUpstreamErrorBodyVerbatim | service/openai_gateway_passthrough_characterization_test.go |
| I-1.6 gemini 路径 | TestGatewayCharacterization_GeminiStreamUnwrapsV1Internal / _GeminiNonStreamCollectsStream / _GeminiUpstreamErrorUnwrappedPassthrough | service/antigravity_gemini_characterization_test.go |
| I-7.1 内容审核 | TestGatewayCharacterization_ContentModerationBlock / _ContentModerationFailOpen | handler/gateway_intercept_characterization_test.go |
| I-7.2 版本检查 | TestGatewayCharacterization_ClaudeCodeVersionCheck / _CountTokensExempt | 同上 |
| (附加)平台路由 | TestGeminiV1BetaHandler_PlatformRoutingInvariant | 同上 |
I-7.3(可选项)未实施,符合规格中"优先级低"定位。
## 3. 固化的关键行为(characterization 发现)
- gemini 路径**并非逐字节透传**:流式会解包 v1internal 信封、非流式由流聚合而成——这是当前实际行为,已按 characterization 原则固化;
- 内容审核 fail-open(审核服务故障时放行)已锁定为显式断言。
## 4. 文件变更详情
### 创建的文件
- `backend/internal/service/gateway_passthrough_characterization_test.go`(411 行)
- `backend/internal/service/openai_gateway_passthrough_characterization_test.go`(181 行)
- `backend/internal/service/antigravity_gemini_characterization_test.go`(188 行)
- `backend/internal/handler/gateway_intercept_characterization_test.go`(367 行)
### 修改/删除
- 无(零业务代码改动)
## 5. 验证记录
- `go test -tags=unit -count=1 -run 'Characterization|Invariant' ./internal/service/ ./internal/handler/` → 全部 PASS;
- `go test -tags=unit -count=1 ./internal/...` → 全绿(无现有测试被破坏);
- `go vet ./internal/...` → 无告警。
@@ -0,0 +1,49 @@
# 完成报告: [TASK-003] 计费精度不变量测试
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/TASK-003_billing_invariants.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
新增 4 个表驱动不变量测试文件(共 1204 行,12 个测试函数),覆盖分派的全部 7 条不变量,每个金额期望值附人工核算算式注释、容差 1e-10。实施代理在收尾阶段遭遇 API 过载中断,最终验证与报告由主控完成:全量 unit 套件绿、vet 干净、零业务代码改动。
## 2. 不变量 → 测试映射
| 不变量 | 测试函数 | 文件 |
|---|---|---|
| I-2.2 5m/1h 双档缓存价 | TestBillingInvariant_CacheTier5mVs1h / _CacheTierBreakdownGating(动态定价门控)/ _CacheTier5mVs1hEndToEnd | billing_invariants_cost_test.go、billing_invariants_e2e_test.go |
| I-2.4 倍率叠加顺序 | TestBillingInvariant_ServiceTierThenRateMultiplier / _ZeroAndNegativeRateMultiplier | billing_invariants_cost_test.go |
| I-2.5 overages 语义 | TestBillingInvariant_OveragesFlagSemantics / _OveragesDeniedNoCreditsInjection | billing_invariants_overages_test.go |
| I-2.6 端到端 usage→金额 | TestBillingInvariant_EndToEndUsageBillingCommand | billing_invariants_e2e_test.go |
| I-3.3 API Key 配额增量 | 同上(apiKeyQuotaCost = ActualCost 断言) | 同上 |
| I-3.4 Account 配额增量 | 同上(accountQuotaCost = TotalCost × 账号倍率断言)+ _LegacyPathIncrements | 同上 |
| I-3.6 preflight 拒绝 | TestBillingInvariant_PreflightBalanceEligibility / _PreflightSubscriptionLimits / _PreflightUserPlatformQuota / _PreflightSimpleModeBypass | billing_invariants_preflight_test.go |
## 3. 固化的关键行为(characterization 发现)
- **双档缓存价门控**:仅当 LiteLLM 1h 单价存在且**严格大于** 5m 单价时才启用两档计费(防上游数据错误导致少收费);无 ephemeral 明细时全部回退 5m 档;
- **priority 显式价微妙行为**:显式 priority 价生效时 tierMultiplier 固定 1.0,cache_write 无 priority 价时按基础价计费**不做 2 倍上浮**——与"无显式价回退 2 倍 tier"路径行为不同;
- **负数 rateMultiplier 按 0 处理**(免费账号语义),TotalCost 保留、ActualCost=0;
- API Key 配额按 **ActualCost** 计,Account 配额按 **TotalCost × 账号倍率** 计——两者基数不同。
## 4. 文件变更详情
### 创建的文件
- `backend/internal/service/billing_invariants_cost_test.go`(291 行)
- `backend/internal/service/billing_invariants_e2e_test.go`(546 行)
- `backend/internal/service/billing_invariants_overages_test.go`(141 行)
- `backend/internal/service/billing_invariants_preflight_test.go`(226 行)
### 修改/删除
- 无(零业务代码改动)
## 5. 验证记录
- `go test -tags=unit -count=1 -run 'Invariant' ./internal/service/` → 全部 PASS;
- `go test -tags=unit -count=1 ./internal/...` → 全绿;
- `go vet ./internal/...` → 无告警。
## 6. 疑似 bug 清单
无(实施代理中断前未上报;主控抽查 cost/e2e 两文件未见"应然值"式断言,全部为当前行为固化)。
@@ -0,0 +1,52 @@
# 完成报告: [TASK-004] 调度与并发不变量测试
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/TASK-004_scheduling_invariants.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
新增 3 个测试文件(13 个顶层测试 / 25 个含子测试用例,全部 `//go:build unit`),覆盖分派的全部 8 条不变量。主控验证:新增测试全绿(handler 包 2.3s,其中池模式重试链固有 500ms×3 间隔)、`go vet` 干净、全量 unit 套件无破坏、零业务代码改动。
## 2. 不变量 → 测试映射
| 不变量 | 测试函数(节选) |
|---|---|
| I-4.1 粘性命中 + hash 优先级 | TestSchedulingInvariant_StickySession_SecondSelectionHitsSameAccount、_SessionHashSourcePriority(metadata→cacheable→IP+UA+APIKeyID 三级链) |
| I-4.2 TTL=1h | _StickySession_TTLIsOneHour(常量+硬编码双断言、miniredis FastForward 过期) |
| I-4.3 sticky_escape | _StickyEscape_ReselectsWhenBoundAccountUnavailable(三子场景,只断言逃逸后重选成功) |
| I-4.5 模型过滤 | _AccountModelMappingFiltersSelection、_ChannelMappingPricingRestrictionAffectsSelection |
| I-5.1 上限 10/3/3 + 耗尽错误 | _FailoverSwitchLimit_DefaultValues(三平台双断言)、_FailoverAnthropic_FullLoopExhaustion(12 账号恒定 500 → 恰好 11 次尝试 → 502)、_FailoverGemini_SwitchLimitLoopContract、_FailoverExhausted_ChatCompletionsErrorBody |
| I-5.2(加固) | FullLoopExhaustion 断言同一账号不被尝试两次 |
| I-5.3 同账号重试链 | _FailoverSameAccountRetry_FullChain(1+3=4 次同账号尝试 → 排除 → 502) |
| I-6.1 槽配平 | _SlotBalance_NormalSuccessPath / _InterceptEarlyReturnPath / _PanicPath、_WrapReleaseOnDone_ExactlyOnce、TestSchedulingCharacterization_ServiceReleaseFuncNotOnceGuarded |
| I-6.2 等待队列 | _UserSlotWaitQueue_FullWaitReleaseWakeup、_AccountSlotWait_TimeoutReturnsConcurrencyError |
## 3. 跳过条目
- gemini/openai 的完整 Forward e2e 失败循环:gemini compat Forward 内置 5 次重试 + 指数退避(完整循环约 2 分钟),openai 循环为 inline 代码且依赖过重,不改业务代码无法纳入单测预算。替代:构造默认上限双断言 + 按真实接线的 FailoverState 循环契约测试(3+1 次后耗尽)。
## 4. Characterization 发现(重要,已回写 INVARIANTS.md)
1. **I-5.1 原记录不精确**:`/v1/messages` 耗尽实际返回 502 + `upstream_error`/"Upstream service temporarily unavailable";`server_error`/"All available accounts exhausted" 属于 chat-completions/responses 兼容路径,且该路径 lastErr 非空时状态码透传上游、错误体无顶层 `"type":"error"` 包裹;
2. `AcquireResult.ReleaseFunc` 无 once 保护,配平依赖 Redis ZREM 幂等 + handler 层 wrapReleaseOnDone(exactly-once 已固化);
3. panic 路径下账号槽不走 defer,靠 wrapReleaseOnDone 的 context.AfterFunc 在请求 context 取消时兜底回收;
4. 测试性陷阱(非 prod bug):GatewayService cfg=nil 时 anthropic apikey 透传 Forward 必 panic(gateway_service.go:9951 附近);`SUB2API_DEBUG_GATEWAY_BODY` 环境变量会打开调试日志文件,测试用 t.Setenv 隔离。
## 5. 文件变更详情
### 创建的文件
- `backend/internal/service/scheduling_invariants_test.go`
- `backend/internal/handler/scheduling_invariants_failover_test.go`
- `backend/internal/handler/scheduling_invariants_slots_test.go`
### 修改的文件
- `.claude/plugin-refactor/INVARIANTS.md`(I-5.1 事实基线按实测修正)
### 业务代码
- 零改动
## 6. 验证记录
- `go test -tags=unit -count=1 -run 'SchedulingInvariant|SchedulingCharacterization' ./internal/service/ ./internal/handler/` → 全 PASS;
- `go vet ./...` → 0 问题;`go test -tags=unit ./internal/...` 全量 → 0 FAIL。
@@ -0,0 +1,35 @@
# 完成报告: [TASK-005] 性能基准基线与 CI gate
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-0_safety-net/TASK-005_bench_baseline_ci.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
热路径基准、采集/对比脚本、CI 门禁与 Makefile 修复全部落地。新增覆盖**完整 Forward 路径**的基准(非流式 ~10.2µs/93 allocs、流式 SSE ~29.6µs/134 allocs),allocs/op 在 6 次采样中完全稳定,时间抖动 ±2%,适合作为 Phase 1-3 改造的灵敏对比基线。
## 2. 文件变更详情
### 创建的文件
- `backend/internal/service/gateway_forward_benchmark_test.go` — BenchmarkGatewayForward_AnthropicNonStreamPassthrough / _AnthropicStreamPassthrough(复用 TASK-002 passChar 夹具,`//go:build unit`,基准内静音 stdlib log 降噪)
- `backend/scripts/bench-baseline.sh` — `collect`(采集基线 + 环境信息)/ `compare`(awk 阈值门禁:allocs/op 严格不允许增加、ns/op 容忍 15%,违反退出码 1;benchstat 可用时附带其报告)
- `backend/testdata/bench/baseline.txt` + `baseline.env.txt` — 基线数据(count=6)与采集环境(Go 1.26.4)
### 修改的文件
- `backend/Makefile` — 新增 `test-invariants` 目标;修复 `test-e2e`(原引用不存在的 scripts/e2e-test.sh,改为与 test-e2e-local 等价的 env 驱动 e2e)
- `.github/workflows/backend-ci.yml` — 新增 `invariants` job(`make test-invariants`,PR 阻断);带注释说明这些测试同时包含在 test-unit 中,独立 job 为了回归信号清晰
### 业务代码
- 零改动
## 3. 基准策略说明
- 基准 job 未加入 CI(共享 runner 时间指标不可靠):`compare` 设计为**本地/PR 人工运行**,时间阈值宽松(15%)、分配阈值严格(0);换机器或换 Go 版本后须重新 `collect`;
- 基准集合:2 个全链路 Forward 基准 + 5 个既有 SSE usage 解析基准。
## 4. 验证记录
- `./scripts/bench-baseline.sh collect` → 基线入库;`compare` 实测 7 个基准全部 ok(allocs 零漂移、时间 ≤2.1%);
- `make test-invariants` → 全绿;
- `go test ./internal/...`(无标签构建)与 `go test -tags=unit ./internal/...`(全量)→ 0 FAIL;
- `go vet ./internal/...` → 干净;golangci-lint 本地超时未跑完(WSL /mnt 盘 IO 慢),由 CI 把关。
@@ -0,0 +1,27 @@
# 插件化改造对抗式审计报告 — 第二轮(2026-06-11)
> 第一轮审计(见 AUDIT-REPORT.md)修复落地后的全量复审,扩展覆盖前端新增代码与 main.go Cleanup 交互推演。
> 方法:静态分析 + 临时 race 测试 + wire v0.7.0 重生成逐字节比对 + 配置畸形输入表驱动探针 + 3 个测试突变实验(与第一轮不重复)。
> 结论:**无 P0/P1**;6 个 P2,已全部修复(修复明细见 TASK-001 完成报告)。
## 发现汇总
| 编号 | 严重度 | 位置 | 问题 |
|---|---|---|---|
| A-1 | P2 | internal/plugin/runtime.go Build | 失败的 Build 可被静默重跑:guard 仅在成功路径置位,重跑会对已 Provision 的旧实例不 Stop 即覆盖(当前 main 不重试故不可达,属契约漏洞) |
| B-1 | P2 | internal/modules/hello/hello.go Stop | 传入已取消 ctx 时 select 双臂就绪随机选取,可把"worker 已干净退出"误报为失败 |
| B-2 | P2 | cmd/server/main.go | Build/Start 失败路径显式+defer 双 Cleanup,依赖 os.Exit 不双调;若未来改为 return 会双触发(Cleanup 未统一幂等) |
| C-1 | P2 | config 子树 + plugin/config.go | `enabled:`(YAML null)hard fail,与三态语义(未配置→默认)不一致且未文档化 |
| F-1 | P2 | scripts/bench-baseline.sh compare | `join` 只输出两侧共有基准:基准被删/改名后静默退出对比,compare 仍绿(回归网假阴性) |
| H-1 | P2 | frontend stores/modules.ts + ModulesView.vue | 取错误用 `data.detail`,项目 envelope 为 `message`(cosmetic,有兜底) |
## 通过项(验证方式)
- **并发**:Build/Start/Stop/Snapshot 全程持锁;50 goroutine 并发 Snapshot × Build/Start/Stop 的 -race 测试干净;Snapshot 在 Start 进行中被正确互斥;hello 无 goroutine 泄漏(NumGoroutine 对比 ×3);
- **生命周期**:回滚 Stop 失败经 errors.Join 不被吞(第一轮 B2 修复的回归测试在位);Stop 容错 + 尊重 ctx deadline;main.go 失败路径显式 Cleanup 先于 Fatalf(第一轮 B1 修复)设计正确;
- **配置**:畸形输入(大写 ID/含空格/enabled 数字/null 子树/列表型模块项/嵌套 map)全部 fail-fast 或安全规整,无 panic;与作者指南声明一致(除 C-1);
- **接入**:wire_gen 与声明 wire@v0.7.0 重生成逐字节一致;cleanup nil 防御在位;模块 Stop 严格先于 Redis/Ent;"配置未知合法模块 ID 绝不 fail"实测成立;
- **admin handler**:logredact 脱敏(ya29.* 实测抹除)、Snapshot 并发安全、envelope/鉴权链合规;
- **Phase-0 资产**:CI YAML safe_load 通过;test-invariants 实匹配 46 个测试函数、42 包绿;bench awk 解析正确(缺指标有警告);
- **测试可信度**(突变→红→恢复→绿,git diff 复核零残留):版本检查比较符翻转 → ClaudeCodeVersionCheck 红;计费 rateMultiplier 置 1 → ServiceTierThenRateMultiplier 红;failover 默认上限 10→9 → FailoverSwitchLimit_DefaultValues 红;
- **前端**:Module 类型与后端逐字段一致;store loading/error 管理正确;spec 6/6。
@@ -0,0 +1,32 @@
# 插件化改造对抗式审计报告(2026-06-11)
> 审计范围:插件内核(internal/plugin/)、hello 模块、Wire/main 接入、admin handler、config 改动、Phase-0 资产。
> 方法:静态分析 + go build/test(含 -race)+ 3 个特征化测试变异验证 + viper 边界实测 + log.Fatalf 行为实证。
> 审计执行于沙箱 shell 故障前,全部需执行的验证均已完成;报告全文由审计代理产出,主控归档。
## 发现汇总
| ID | 严重度 | 位置 | 问题 | 修复状态 |
|---|---|---|---|---|
| B1 | **P1** | cmd/server/main.go:156-165 | Build/Start 失败走 `log.Fatalf`→`os.Exit`,跳过 `defer app.Cleanup()`;而部分 Provider(如 ProvidePaymentOrderExpiryService,service/wire.go:627)构造期已自启动并持有 Redis leader 锁(TTL 3min)→ 启动失败时锁/后台服务不被优雅释放。**改造新引入的失败模式**(原 Fatalf 在 defer 注册之前)。 | ✅ 已修:失败分支先显式 `app.Cleanup()` 再 Fatalf |
| B2 | P2 | internal/plugin/runtime.go | Start 回滚时回滚 Stop 失败仅记日志、不并入返回值;该模块置 errored 后不再重试。 | ✅ 已修:rollback errs 经 errors.Join 并入返回错误 + 回归测试 TestRuntimeStartRollbackStopFailureJoinedIntoError |
| A | P2 | internal/plugin/runtime.go:249 | 单锁横跨模块 Start/Stop:慢模块会阻塞 Snapshot(admin /modules)。属模块作者隐性契约。 | ✅ 已修:Starter/Stopper 接口注释明确"必须快速返回,长任务自起 goroutine" |
| C | P2 | plugin/config.go | viper 静默小写所有 key:大写模块 ID 被规整而非报错(部署友好),但模块 mapstructure 标签必须全小写——未文档化。 | ✅ 已修:ParseConfig 注释点明;TASK-005 文档将收录 |
| F1 | P2 | scripts/bench-baseline.sh | awk 的均值计数耦合在 allocs/op 分支,去掉 -benchmem 时除零→inf/nan 静默失真。 | ✅ 已修:ns/allocs 独立计数,缺指标跳过并向 stderr 告警 |
| F2 | P2 | scripts/bench-baseline.sh | 基准名含空格时 $1 截断(当前基准均无空格,理论隐患)。 | 备查不修 |
## 通过项(验证方式)
- **并发正确性**:-race 下并发 Build/Start/Stop/Snapshot、registry 并发注册/读取无竞争(临时 race 测试,已清理);hello goroutine 无泄漏(cancel 先于等待,Stop 超时也会退出);
- **生命周期边界**:Build 失败后 Start 被拒、重复 Stop 幂等、Stop ctx 取消正确传递;
- **配置边界**:normalizeModulesSubtree/ParseConfig 对畸形输入(非 map、字符串 enabled、null 子树、嵌套 map)均返回错误或安全规整,不 panic(viper 实测);
- **接入正确性**:wire_gen 与声明一致(build 通过)、cleanup nil 防御在位、Build/Start 先于 HTTP listen、"配置了未注册的合法模块 ID 绝不 fail"成立;
- **admin handler**:logredact 脱敏、Snapshot 持锁拷贝值切片(并发只读安全)、envelope 合规;
- **CI YAML/Makefile**:静态判定结构正确(与既有 job 同构);
- **测试可信度**:3/3 变异实验(计费 ×2、槽配平跳过释放、header 白名单加 set-cookie)全部变红,产线代码 md5 校验逐字节还原。
## 审计遗留的环境收尾(待 shell 恢复执行)
1. `rm backend/internal/plugin/zzz_audit_race_test.go backend/internal/plugin/zzz_audit_lifecycle_test.go backend/internal/config/zzz_audit_normalize_test.go`(已确认均为无逻辑桩,不影响编译);
2. `git diff` 复核 billing_service.go / gateway_handler.go / responseheaders/ 零变异残留(代理已 md5 校验,双重确认);
3. 修复后全量门禁:build / `go test -race ./internal/plugin/` / modules+cmd/server+config 测试 / make test-invariants / vet / `bash -n bench-baseline.sh` / CI YAML python 校验。
@@ -0,0 +1,57 @@
# 阶段总结报告: Phase-1.5 模块开发套件与前端可观测(DevKit & Frontend)
- **阶段状态**: Completed
- **完成时间**: 2026-06-11
- **关联阶段计划**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/PHASE_PLAN.md)
- **由来**: 用户追加目标:①审计功能正确性 ②补齐前端部分 ③完整的模块开发 SDK 套件
## 1. 阶段目标达成情况(对照 PHASE_PLAN §3)
1. ✅ **审计**:两轮对抗式审计归档(AUDIT-REPORT.md / AUDIT-REPORT-R2.md),合计 1×P1 + 11×P2,10 项修复(全部配回归测试)+ 2 项备查不修(理由登记);测试可信度突变实验 6/6 变红;
2. ✅ **plugintest**:落地且 hello 测试自举改造(−35 行)验证可用性;不吞错由哨兵自测固化;
3. ✅ **脚手架**:`make new-module ID=job.demo` 真实演示开箱即编译、5 测试通过,产物清理后与演示前逐字节一致;
4. ✅ **前端**:admin"插件模块"页(/admin/modules)上线,6 spec + `make test-frontend` 全绿;
5. ✅ **文档**:作者指南新增"开发与调试工作流"完整闭环章节(指南 465→707 行),所有命令实测(含真实起服务器验证观测链路);
6. ✅ **零回归**:全量 unit 43 包 0 FAIL、安全网全绿、bench compare exit 0(allocs 零漂移)、默认配置行为不变。
## 2. 任务完成统计
| 任务ID | 任务描述 | 状态 | 完成报告 |
|:--|:--|:--|:--|
| TASK-001 | 对抗式审计与修复(两轮) | Completed | [链接](./TASK-001_audit_fixes.md) |
| TASK-002 | plugintest 测试夹具包 | Completed | [链接](./TASK-002_plugintest.md) |
| TASK-003 | 模块脚手架生成器 | Completed | [链接](./TASK-003_scaffold.md) |
| TASK-004 | 前端 admin 模块页面 | Completed | [链接](./TASK-004_frontend_modules_view.md) |
| TASK-005 | 开发与调试工作流文档 | Completed | [链接](./TASK-005_devflow_docs.md) |
## 3. 关键技术成果(模块开发"SDK 套件"全貌)
进程内模块的 SDK 形态(对标 Caddy 的 caddytest + xcaddy):
- **plugintest**(`internal/plugin/plugintest/`):NewHost(Option) + RunLifecycle/BuildOnly/BuildExpectingError;
- **脚手架**(`make new-module ID=...`):开箱即编译即测试的模块骨架 + 显式插装提示;
- **观测三件套**:Runtime 生命周期日志 / `GET /api/v1/admin/modules` / 前端 `/admin/modules` 页;
- **工作流文档**:作者指南第 8 章,脚手架→TDD→本地运行→观测→提交自检闭环 + 8 条常见坑。
审计修复的核心:P1 启动失败泄漏 leader 锁(第一轮发现并修)、Build 失败不可重试契约、enabled null 三态、bench compare 基准缺失假阴性。
## 4. 遇到的问题与解决方案
- **第一轮审计代理因 WSL 沙箱 /tmp EIO 中断**:其报告与修复已落盘,重启第二轮全量复审 + 扩展范围完成闭环;遗留桩文件清理;
- **规格与实现冲突**两处(未知模块 ID 语义、decimal vs float64):均以"后定的深思决策/代码现实"为准并在规格文件加注裁定。
## 5. 技术债务与待优化项
- 前端全量 vitest 在 WSL 下有 7 个环境性 flake(单跑即过,不在 make test-frontend 门禁内),登记备查;
- `make generate` 的 wire go.sum 预存在问题(指南 8.5 已写明 workaround),建议主线单独 PR 修复;
- modules 子树不支持环境变量逐键覆盖(viper 限制,指南已注明)。
## 6. 经验总结与建议
- 对抗式审计的"突变实验"(改坏行为验证测试变红)是验证安全网真实性的高价值手段,建议每阶段保留;
- "实施代理 + 主控复跑门禁"双重验证持续奏效(本阶段抓住了 detail/message、join 假阴性等多个细节)。
## 7. 下一阶段准备
- Phase-2(试点迁移:payment.provider + moderation 钩子)就绪:内核 API 经审计冻结、开发套件齐备、试点作者可直接按指南第 8 章工作流开发;
- 全部改动仍未 git commit,建议提交粒度:①Phase-0 安全网 ②Phase-1 内核与接入 ③Phase-1.5 审计修复+套件+前端 ④文档。
@@ -0,0 +1,52 @@
# 完成报告: [TASK-001] 对抗式审计与修复
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/TASK-001_audit_fixes.md)
- **完成日期**: 2026-06-11
## 1. 审计总结论(两轮合并)
- **第一轮**(报告 [AUDIT-REPORT.md](./AUDIT-REPORT.md)):发现 **1 个 P1** —— Build/Start 失败走 `log.Fatalf`→`os.Exit` 跳过 defer Cleanup,而部分 Provider 构造期已持有 Redis leader 锁(TTL 3min),启动失败会泄漏锁。已修:失败分支先显式 `app.Cleanup()` 再 Fatalf。另修 4 个 P2(回滚错误吞没→errors.Join + 回归测试、Starter/Stopper 快速返回契约注释、viper 小写化文档、bench awk 除零加固),1 个 P2 备查不修(基准名空格,当前无实例)。代理在收尾时因 WSL 沙箱故障中断,遗留桩文件已清理。
- **第二轮**(报告 [AUDIT-REPORT-R2.md](./AUDIT-REPORT-R2.md),重启后全量复审 + 扩展前端/Cleanup 推演):确认第一轮修复全部有效,**无新 P0/P1**;发现 6 个新 P2,全部修复并配回归测试。
- 测试可信度突变实验两轮合计 **6/6 变红**(计费×3、版本检查、槽配平、header 过滤、failover 上限,覆盖三大类),产线代码逐字节恢复确认。
## 2. 第二轮 P2 发现与修复明细
| 编号 | 问题 | 修复 | 回归测试 |
|---|---|---|---|
| A-1 | 失败的 Build 可被静默重跑,泄漏已 Provision 实例(guard 不覆盖失败路径) | runtime.go 增加 `buildAttempted` 守卫:失败后二次 Build/Start 一律拒绝(提示重启进程),doc comment 同步 | TestRuntimeBuildFailureBlocksRetry + TestRuntimeBuildSuccessThenRebuildStillRejected(internal/plugin/audit_regression_test.go) |
| B-1 | hello.Stop 传入已取消 ctx 时,worker 已干净退出仍可能误报失败(select 双臂随机选取) | Stop 先非阻塞探测 m.done 再进入双臂 select | TestStopWithPreCancelledContextAfterWorkerExit(internal/modules/hello/audit_regression_test.go) |
| B-2 | main.go Build/Start 失败路径显式+defer 双 Cleanup,依赖 os.Exit 才不双调 | main.go 加警示注释:此处不可改为 return(Cleanup 未统一幂等) | 注释级(无行为变化) |
| C-1 | `enabled:`(YAML null)hard fail,与三态语义不一致且未文档化 | ParseConfig 将 enabled nil 视为未配置(走模块默认值);作者指南 §5.2 三态表同步 | TestParseConfigEnabledNilTreatedAsUnset(同 A-1 文件) |
| F-1 | bench compare 的 join 静默丢弃增删基准 → 回归网假阴性 | 脚本增加基准名集合对比:基线有而本次缺失 → FAIL;新增基准 → warn 提示重新 collect | 实跑 compare 验证(7 项 ok、exit 0;语法 bash -n 通过) |
| H-1 | 前端取错误用 `data.detail`,项目 envelope 是 `message` | stores/modules.ts 与 ModulesView.vue 改为 `data.message` | ModulesView.spec 6/6 复跑通过 |
## 3. 审计中的"通过"项(验证方式存档)
- 并发:Build/Start/Stop/Snapshot 全程持锁,50 goroutine 并发 -race 干净;hello 无 goroutine 泄漏(NumGoroutine 前后对比 ×3);
- 回滚 Stop 失败不被吞(errors.Join,上一轮审计修复的回归测试仍在);
- main.go 失败路径显式 Cleanup 优先于 log.Fatalf 的设计正确(避免 leader 锁滞留);
- 配置畸形输入(大写 ID/含空格/enabled 为数字/null 子树/列表型模块项)全部 fail-fast 或安全规整,无 panic;
- 未知合法模块 ID 绝不影响启动(硬要求实测确认);
- wire_gen.go 与声明逐字节一致;cleanup nil 防御在位;模块 Stop 先于 Redis/Ent;
- admin handler 脱敏(logredact)+ envelope + 鉴权链正确;CI yaml 语法有效;`test-invariants` 实匹配 46 个测试函数。
## 4. 文件变更详情
### 修改(修复)
- `backend/internal/plugin/runtime.go`(A-1)、`internal/plugin/config.go`(C-1)、`internal/modules/hello/hello.go`(B-1)、`cmd/server/main.go`(B-2 注释)、`backend/scripts/bench-baseline.sh`(F-1)、`frontend/src/stores/modules.ts` + `views/admin/ModulesView.vue`(H-1)、`docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md`(C-1 文档)
### 创建(回归测试)
- `backend/internal/plugin/audit_regression_test.go`、`backend/internal/modules/hello/audit_regression_test.go`
## 5. 验证记录
- `go build ./...` ✅;`go test -race ./internal/plugin/ ./internal/modules/...` ✅(含新回归测试);
- `make test-invariants` 42 包 ✅;bench compare 实跑 exit 0、7 项 ok ✅;
- 前端 `pnpm exec vitest run ModulesView.spec.ts` 6/6 ✅;
- 审计过程的突变实验已全部恢复(审计员 git diff 复核 + 主控 git status 复核)。
## 6. 备注
- 首轮审计代理因宿主进程退出丢失,仅遗留一个空探针文件(已删);重启后完整完成;
- `scripts/bench-baseline.sh` 的 summarize 缺指标警告为审计期间的前置改进,一并保留。
@@ -0,0 +1,33 @@
# 完成报告: [TASK-002] plugintest 测试夹具包
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/TASK-002_plugintest.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`internal/plugin/plugintest/` 落地(host.go 103 行 + runtime.go 120 行 + 自测 318 行):一行构造 Host(Option 模式:WithRedis/WithConfig/WithObservedLogger)、一行跑完整生命周期(RunLifecycle 自动 t.Cleanup Stop)、BuildOnly / BuildExpectingError 变体。hello 测试自举改造(213→178 行),语义不变全绿。
## 2. 公开 API
`NewHost(tb, ...Option)`、`WithRedis(tb)`(miniredis + t.Cleanup)、`WithConfig(raw)`、`WithObservedLogger() (Option, *ObservedLogs)`、`RunLifecycle(tb, m, host, raw) *Runtime`(失败 t.Fatal 含模块 ID;cleanup Stop 10s 上限并断言无错)、`BuildOnly`、`BuildExpectingError`。包 doc 声明 test-only。
## 3. 关键裁量(已在代码注释登记)
- observer 句柄随 Option 双返回值(较草案 `Logs(t)` 更直接,无包内状态);
- 失败期望走独立 `BuildExpectingError`(调用处意图显式、不注册多余 cleanup);"Start 应失败"不预铺夹具(无样板需求);
- RunLifecycle 的 raw 为配置唯一来源(统一重绑 host.ConfigOf,消除歧义);
- disabled 模块不强制 enable,仅 t.Logf 提示(防 footgun 兼顾 default-noop 测试);
- 不吞错由 `recordingTB` 哨兵自测固化(Build/Start 失败 Fatal、cleanup Stop 失败 Errorf)。
## 4. 文件变更详情
- **创建**: `internal/plugin/plugintest/{host,runtime,plugintest_test}.go`
- **修改**: `internal/modules/hello/hello_test.go`(公共样板换夹具,−35 行)、`audit_regression_test.go`(仅 Host 构造样板替换,内部字段访问保留)
- 内核与生产代码零改动
## 5. 验证记录(主控复跑确认)
- `go test -race ./internal/plugin/... ./internal/modules/...` ✅(plugintest 17 自测全过);
- `make test-invariants`、全量 `go test -tags=unit ./internal/...`(43 包)✅;vet/lint/gofmt 0 issues;
- 无任何非 `_test.go` 文件 import plugintest。
@@ -0,0 +1,27 @@
# 完成报告: [TASK-003] 模块脚手架生成器
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/TASK-003_scaffold.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`make new-module ID=job.foo` 落地:embed 模板渲染生成 `internal/modules/foo/{foo.go,foo_test.go}`(四生命周期骨架 + 编译期断言 + EnabledByDefault=false + 示例私有配置 + 9 处 TODO;测试模板基于 plugintest),打印 next steps(确切 import 行 / config 启用示例 / 作者指南链接),**不自动改 imports.go**(显式插装可 review)。真实演示验证:job.demo 生成 → 插装 → `go build` + 5 测试全过 → 清理后 git status 与演示前 diff 为空。
## 2. 文件变更详情
- **创建**: `backend/tools/newmodule/main.go`、`templates/module.go.tmpl`、`templates/module_test.go.tmpl`、`main_test.go`(6 用例)
- **修改**: `backend/Makefile`(new-module 目标,ifndef ID 守卫)
## 3. 关键裁量
- **ID 校验走内核导出口径**(`plugin.ParseConfig` 间接复用 `ModuleID.validate()`):零规则复制,内核演进自动跟随;
- 包名校验 `go/token.IsIdentifier` + 拒绝 `_`(内核 ID 合法但包名非法的情形);
- **渲染即 `go/format.Source`**:模板腐化当场报错不写盘(防模板随内核演进静默腐化);
- 失败原子性:校验先于建目录,第二文件失败回滚整个目录;`-dir` flag 使单测渲染进 t.TempDir。
## 4. 验证记录(主控复跑确认)
- `go test ./tools/...` ✅(6 用例:产物 gofmt/内容/import 行、非法 ID×5、非法包名×4、已存在拒绝、缺参提示,拒绝路径断言无残留);
- 演示链路全过且清理干净(imports.go md5 一致、`internal/modules/` 仅 hello/standard——主控 ls 复核);
- `make test-invariants`、全量 unit(43 包)✅;vet 零问题。
@@ -0,0 +1,33 @@
# 完成报告: [TASK-004] 前端 admin 模块页面
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/TASK-004_frontend_modules_view.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
admin 后台"插件模块"只读页面落地(`/admin/modules`):列表 + 状态语义徽章 + 错误悬浮展示 + 手动刷新 + 空态;中英 i18n 齐备;6 个 vitest 用例。全链路复刻 AnnouncementsView 既有模式,零新依赖、后端零改动。
## 2. 文件变更详情
### 创建(4)
- `frontend/src/api/admin/modules.ts`、`src/stores/modules.ts`、`src/views/admin/ModulesView.vue`、`src/views/admin/__tests__/ModulesView.spec.ts`
### 修改(7)
- `src/types/index.ts`(ModuleState + Module,snake_case 与后端一致)、`src/api/admin/index.ts`、`src/stores/index.ts`、`src/router/index.ts`(requiresAdmin + titleKey)、`src/components/layout/AppSidebar.vue`(adminNavItems,hideInSimpleMode,复用既有 cube 图标)、`src/i18n/locales/en.ts` + `zh.ts`
## 3. 设计要点
- 徽章语义:running=success / errored=danger / registered=gray / stopped+provisioned=primary(次要色用法照 GroupsView 惯例);
- error 长文本 `truncate + :title` 悬浮(照现有惯例);
- 按规格刻意不做:写操作、轮询、详情页、featureFlag。
## 4. 验证记录
- `pnpm run lint:check`、`pnpm run typecheck` 通过;新增 spec 6/6(主控复跑确认);
- `make test-frontend` 全绿(lint + typecheck + 关键集 84 用例);附加回归 i18n/stores/AppSidebar/router 测试全过。
## 5. 发现与遗留
- `pnpm run test:run -- <file>` 的 `--` 透传会使 vitest 文件过滤失效(跑成全量);正确用法 `pnpm exec vitest run <file>`——已写入后续 TASK-005 调试文档素材;
- 全量 vitest 暴露 7 个与本任务无关的预存 flake(如 DashboardView.spec 单跑即过),不在 `make test-frontend` 门禁内,未处理,登记备查。
@@ -0,0 +1,23 @@
# 完成报告: [TASK-005] 开发与调试工作流文档
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1.5_devkit/TASK-005_devflow_docs.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
《模块作者指南》新增第 8 章"开发与调试工作流"(251 行),覆盖完整闭环:8.1 脚手架(实测全文输出)→ 8.2 plugintest 单测驱动(API 速览 + 三段真实代码用法)→ 8.3 本地运行(依赖准备/配置/实测启动与优雅关闭日志)→ 8.4 三种观测(结构化日志、admin API curl 实测含 423 合规门注记、前端 /admin/modules 页)→ 8.5 提交前自检(安全网 + bench 阈值四条语义 + wire 重生成坑)→ 8.6 常见坑 8 条表格。指南 465 → 707 行。
## 2. 衔接处理(消除重复)
原"完整示例"重编号为第 9 章并修正陈旧交叉引用;§7.1 重写(删除已不存在的 newIsolatedRegistry 旧模式,确立 plugintest 为默认入口);§7.3 预期输出收敛至 8.5;原启用与观测清单压缩为交叉引用。
## 3. 实测记录(亮点)
- 全部命令实测:脚手架生成(产物已清理)、生成模块 5/5 测试、test-invariants 16.4s、bench compare exit 0、wire 失败复现 + @v0.7.0 重生成 md5 不变;
- **真实服务器运行验证**:备份 config.yaml → 启用 job.hello → 构建运行 2 分钟 → 捕获 started/stopped/[Cleanup] 实测日志 → curl 实测 admin API(423 合规门 + 200 模块清单)→ SIGTERM 优雅退出 → 现场完整还原(config md5 一致、临时合规 DB 行删除、二进制/日志清理);
- 仅静态核对:前端页面渲染(其消费的 API 已实测)、docker-compose.dev 端口映射事实(已在文档如实注明 postgres/redis 默认不映射宿主端口)。
## 4. 文件变更详情
- **修改**: `docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md`(净增 242 行);零代码改动;现场零残留(主控 git status 复核)。
@@ -0,0 +1,53 @@
# 阶段总结报告: Phase-1 插件内核(Plugin Kernel)
- **阶段状态**: Completed
- **完成时间**: 2026-06-11
- **关联阶段计划**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/PHASE_PLAN.md)
## 1. 阶段目标达成情况(对照 PHASE_PLAN §3)
1. ✅ 内核接口按契约实现(签名与 ROADMAP 架构树逐项核对一致),单测覆盖硬性清单全部条目(含 Start 半途失败逆序回滚、Stop ctx deadline 专测,-race 全绿);
2. ✅ **零行为变更**:默认配置下 Phase-0 安全网 42 包全绿;`bench-baseline.sh compare` 7 个基准 allocs/op 完全一致、ns/op 全在噪声内;
3. ✅ wire v0.7.0 工具生成且复跑字节级一致;启动/优雅关闭含模块阶段日志(runtime.Stop 在 cleanup 并行组首位、先于 Redis/Ent);
4. ✅ `GET /api/v1/admin/modules` 可列出全部模块与状态;job.hello 启用后全生命周期测试通过、禁用后零副作用;
5. ✅ 《模块作者指南》入库(含 .gitignore 白名单放行);CLAUDE.md 更新(Ent 修正 + 插件开发小节 + 结构树)。
## 2. 任务完成统计
| 任务ID | 任务描述 | 状态 | 完成报告 |
|:--|:--|:--|:--|
| TASK-001 | 内核包:module/registry | Completed | [链接](./TASK-001_kernel_registry.md) |
| TASK-002 | Host/配置子树/Runtime | Completed | [链接](./TASK-002_host_runtime_config.md) |
| TASK-003 | Wire/启动接入 + 插装清单 + hello | Completed | [链接](./TASK-003_wire_bootstrap.md) |
| TASK-004 | Admin 模块可观测 API | Completed | [链接](./TASK-004_admin_modules_api.md) |
| TASK-005 | 作者指南 + CLAUDE.md | Completed | [链接](./TASK-005_author_guide_docs.md) |
**总计**: 5 个任务全部完成。新增 `internal/plugin/`(5 文件内核 + 4 文件单测)、`internal/modules/`(standard 插装清单 + hello 示例)、admin handler、作者指南;对现有代码改动极小且全部纯增量(config.go +41、main.go +10、wire.go +20、handler/routes 各数行、wire_gen.go 工具生成)。
## 3. 关键技术成果
- **Caddy 式进程内插件内核就位**:命名空间注册表(init() 注册 + panic 校验)、四段可选生命周期、Host ports 能力面、`modules:` 配置子树(enabled 三态)、Runtime 状态机(含逆序回滚/逆序关闭)、唯一插装清单 `modules/standard/imports.go`;
- 新增一个模块 = 新建模块包 + 插装清单加一行 import,核心零改动——Phase-2/3 的迁移底座已通电。
## 4. 遇到的问题与解决方案
- **viper 含点 key 拆层坑**:`viper.Unmarshal` 把 `modules.job.hello` 错拆两级 → Modules 字段 `mapstructure:"-"` + `viper.Get("modules")` 手工提取 + 形状校验;
- **规格与内核语义冲突**(未知模块 ID 处理):架构师裁定以内核为准(格式非法 fail-fast / 合法未注册忽略),规格文件已加注裁定记录;
- **交付物被 .gitignore 忽略**:按现有白名单模式补 `!docs/plugin-architecture/`。
## 5. 技术债务与待优化项
- `make generate` 的无版本号 wire 调用因 go.sum 缺 `github.com/google/subcommands` 失败(**预存在**,与本阶段改动无关):建议主线单独 PR `go get github.com/google/wire/cmd/wire@v0.7.0` 或 Makefile 改带版本调用;
- modules 子树暂不支持环境变量逐键覆盖(viper AutomaticEnv 限制),需要时后续补充;
- CLAUDE.md 既有过时描述(`service/ports/` 目录、`make wire` 目标)与 usage_log.go 残留 gorm tag,留待后续梳理;
- golangci-lint 在 WSL /mnt 盘超时,以 CI lint job 为最终判定。
## 6. 经验总结与建议
- "接口契约写进 PHASE_PLAN + 实施自由度留给工程师"运转良好:三个实施代理的裁量决定(EnabledByDefault 字段、独立 context、ProviderSet 拆分)都在契约内且有理有据;
- 每任务"主控复跑门禁"的双重验证流程两次发现了值得归档的信息(错误体路径差异、gitignore 问题),建议 Phase-2 保持。
## 7. 下一阶段准备
- Phase-2(试点迁移:payment.provider + moderation 钩子)就绪,待规划任务清单(按渐进式规划原则,PHASE_PLAN 在阶段启动时细化);
- **当前全部改动尚未 git commit**——建议先按"PR 粒度规划"提交:①Phase-0 安全网(测试+脚本+CI+Makefile)②Phase-1 内核与接入(plugin/modules/wire/main/config)③文档(指南+CLAUDE.md+gitignore),三个独立可 revert 的提交/PR。
@@ -0,0 +1,30 @@
# 完成报告: [TASK-001] 内核包:module/registry + 单测
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/TASK-001_kernel_registry.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`backend/internal/plugin/` 内核的类型与注册表部分落地:ModuleID(点分层级 + Namespace()/Name() + 校验)、ModuleInfo、Module 接口、四个可选生命周期接口(Provisioner/Validator/Starter/Stopper)、并发安全 Registry(包级默认实例 + NewRegistry 隔离实例)。接口签名与 ROADMAP 架构树逐项核对一致(主控抽查确认)。
## 2. 契约符合性要点
- RegisterModule:空/非法/重复 ID panic,panic 信息含冲突 ID 并提示检查插装清单;nil module/nil New 亦 panic(插装错误尽早暴露);
- GetModulesInNamespace:精确命名空间匹配(Caddy 同语义)、按 ID 字典序稳定排序;
- 裁量决定:ModuleInfo 增加 `EnabledByDefault bool`(注册时声明默认启用态,零值 false 保证新模块未配置时零行为变更);单段 ID 的 Namespace() 为 ""。
## 3. 文件变更详情
### 创建的文件
- `backend/internal/plugin/module.go`(126 行)、`registry.go`(113 行)
- `backend/internal/plugin/module_test.go`(55 行)、`registry_test.go`(176 行)
### 修改/删除
- 无
## 4. 验证记录
- `go test -race -count=1 ./internal/plugin/` → ok(含并发注册测试);
- `go vet`、`golangci-lint run ./internal/plugin/...` → 0 issues;
- 不引入第三方依赖。
@@ -0,0 +1,39 @@
# 完成报告: [TASK-002] Host / 配置子树 / Runtime 生命周期驱动 + 单测
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/TASK-002_host_runtime_config.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
Host(Logger/ConfigOf/DB/Redis 四项能力面)、`modules:` 配置子树(ParseConfig/Of,mapstructure 解码与 viper 语义对齐)、Runtime(Build/Start/Stop/Snapshot 完整状态机)落地,单测覆盖 PHASE_PLAN §3 硬性清单全部条目(含最易写错的 Start 半途失败逆序回滚专测、Stop ctx deadline 专测)。Phase-0 安全网全绿。
## 2. 契约符合性与裁量决定
- Host:`*zap.Logger` / `*ent.Client` / `redis.UniversalClient`(项目实际类型),注释写明扩展须登记 ROADMAP;
- Runtime:Build 稳定序实例化 enabled 模块 → Provision → Validate(失败含模块 ID 中止,%w 可穿透);Start 半途失败逆序 Stop 已启动模块;Stop 逆序、单失败 errors.Join 聚合继续、尊重 ctx deadline;Snapshot 返回 {ID, Enabled, State, Err(string)};
- **关键发现**:`viper.Unmarshal` 会把含点的模块 ID 键(`modules.job.hello`)错误拆成嵌套两级——Modules 字段故标记 `mapstructure:"-"`,在 load() 中经 `viper.Get("modules")` 手工提取并做形状校验(`normalizeModulesSubtree`);
- 未注册但格式合法的模块 ID 配置项放行(兼容不同编译变体共用配置文件),格式非法/enabled 类型错误报错(笔误尽早暴露);
- 纯增量补充 `NewRuntimeWithRegistry`(测试隔离);Build 失败不回卷已 Provision 模块(契约仅要求中止启动,进程退出兜底,已注释)。
## 3. 文件变更详情
### 创建的文件
- `backend/internal/plugin/host.go`(34 行)、`config.go`(113 行)、`runtime.go`(278 行)
- `backend/internal/plugin/config_test.go`(135 行)、`runtime_test.go`(366 行)
- `backend/internal/config/modules_config_test.go`(89 行,含"缺省时与现状完全等价"专测)
### 修改的文件
- `backend/internal/config/config.go`(+41 行纯增量:Modules 字段 + load() 提取 + normalizeModulesSubtree;零删改既有行)
- `backend/go.mod`(mapstructure 由 indirect 转直接依赖,未引入新依赖)
## 4. 验证记录(主控复跑确认)
- `go test -race -count=1 ./internal/plugin/` → ok;`go test -count=1 ./internal/config/`(全部既有测试)→ ok;
- `make test-invariants` → 41 包全绿;`go test -tags=unit -count=1 ./internal/...` → 零失败;
- `go vet`、golangci-lint(plugin/config 包)→ 0 issues;`go build ./...` + gofmt 通过。
## 5. 遗留事项
- modules 子树暂不支持环境变量逐键覆盖(viper AutomaticEnv 不合并进 Get("modules")),Phase-1 仅支持配置文件,需要时后续补充;
- go.mod 本身不完全 tidy(aws/smithy-go 标记、go.sum 冗余,与本任务无关),建议主线择机单独 `go mod tidy`。
@@ -0,0 +1,45 @@
# 完成报告: [TASK-003] Wire/启动接入 + 插装清单 + 示例模块
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/TASK-003_wire_bootstrap.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
插件系统正式"通电":内核经 Wire 装配进应用,main 启动时驱动模块 Build/Start、关闭时 runtime.Stop 在 cleanup 并行组首位(先于 Redis/Ent)执行;`modules/standard/imports.go` 成为唯一插装清单;`job.hello` 示例模块(默认 disabled)演示完整生命周期与私有配置。默认配置下零行为变更(专测 + Phase-0 安全网 42 包全绿佐证)。
## 2. 文件变更详情
### 创建的文件
- `backend/internal/plugin/wire.go` — ProviderSet:ProvideModuleConfig(ParseConfig 失败 fail-fast)/ ProvideHost(logger.L() + Config.Of + *ent.Client + *redis.Client)/ ProvideModuleRuntime
- `backend/internal/modules/standard/imports.go` — 唯一插装清单
- `backend/internal/modules/hello/hello.go` + `hello_test.go`(11 个测试)— `job.hello`:四生命周期接口 + 编译期断言,EnabledByDefault=false,私有配置 interval/greeting 带 Validate 校验
### 修改的文件
- `cmd/server/wire.go`(+20/-2)— Application.Runtime 字段、plugin.ProviderSet、provideCleanup 增参并在 parallelSteps 首位插入 PluginModuleRuntime 步骤
- `cmd/server/main.go`(+10)— 匿名 import standard;HTTP server 前 Build+Start,失败 log.Fatalf
- `cmd/server/wire_gen.go`(+24/-2)— **wire v0.7.0 工具生成**,复跑字节级一致
- `cmd/server/wire_gen_test.go`(+1)— provideCleanup 签名变更的连带修正(nil moduleRuntime,步骤有 nil 防御)
## 3. 关键证据
- **cleanup 顺序**:wire.go:125 PluginModuleRuntime(parallelSteps 首项)→ wire.go:322 runParallel 先于 :323 runSequential(infraSteps),Redis :280 / Ent :286 在 infraSteps——模块 Stop 严格先于基础设施关闭;
- **默认零行为**:TestRuntimeDefaultConfigIsNoop(空 modules 下 Build/Start/Stop 全 no-op、零日志);
- **启用后生命周期**:TestRuntimeEnabledHelloLifecycle(zap observer 断言启动/周期/停止日志与状态机);
- **非法配置中止启动**:TestRuntimeInvalidHelloConfigAbortsBuild。
## 4. 裁定与裁量
- **规格冲突裁定**:规格 §8 原文"未知模块 ID 报错中止"与内核语义冲突,架构师裁定以内核为准(格式非法 fail-fast / 格式合法未注册忽略),规格文件已加注;
- hello 模块 Start 用独立 context(与启动 ctx 解耦,由 Stop 统一取消);ProviderSet 拆三个 provider 使 modules 子树只解析一次。
## 5. 验证记录(主控复跑确认)
- `go build ./...` ✅;`go test ./internal/modules/... ./cmd/server/` ✅;`make test-invariants` 42 包全绿 ✅;
- 全量 `go test -tags=unit ./internal/...` 零失败;`go vet ./...`、golangci-lint(新增包)0 issues;
- wire 产物复跑字节级一致;`go generate ./ent` 零 diff。
## 6. 遗留事项
- **预存在问题**:`make generate` 中 `go run github.com/google/wire/cmd/wire`(无版本号)因 go.sum 缺 `github.com/google/subcommands` 条目失败(不依赖本次改动即可复现)。建议主线单独 PR:`go get github.com/google/wire/cmd/wire@v0.7.0` 或 Makefile 改带版本号调用;本任务按纪律未触碰 go.sum;
- 真实进程级冒烟(需 PG/Redis)留待 Phase-1 阶段验收时与 TASK-004 一并进行。
@@ -0,0 +1,32 @@
# 完成报告: [TASK-004] Admin 模块可观测 API
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/TASK-004_admin_modules_api.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`GET /api/v1/admin/modules` 只读可观测接口落地:数据源 Runtime.Snapshot(),项目统一 envelope(`response.Success`),字段 snake_case 按 id 字典序,error 文本经 `logredact.RedactText` 脱敏。完全复刻现有 admin handler 模式(私有小接口仿 SystemHandler、独立 register 函数、admin group 自动套 AdminAuthMiddleware + AdminComplianceGuard)。
## 2. 文件变更详情
### 创建的文件
- `backend/internal/handler/admin/module_handler.go` — ModuleHandler + List
- `backend/internal/handler/admin/module_handler_test.go` — 5 个单测(序列化含 disabled 模块、排序、错误脱敏、空快照 `[]` 非 null、真实 Runtime 端到端)
### 修改的文件
- `internal/handler/handler.go` — AdminHandlers 增加 Module 字段
- `internal/handler/wire.go` — provider 注册与参数
- `internal/server/routes/admin.go` — registerModuleRoutes
- `cmd/server/wire_gen.go` — wire v0.7.0 工具重新生成(未手改)
## 3. 验证记录(主控复跑确认)
- `go build ./...` ✅;`go test ./internal/handler/admin/` ✅;`make test-invariants` 42 包全绿 ✅;
- 全量 `go test -tags=unit ./internal/...` 零失败;`go vet ./...` ✅;
- **基准对比零回归**:`bench-baseline.sh compare` 7 个基准全 ok(allocs/op 完全一致,ns/op -2.9%~+0.5% 均在噪声内);
- golangci-lint 本地 80 分钟未跑完(WSL /mnt 盘 + staticcheck 全程序分析),已按 .golangci.yml 启用清单人工核查新文件(depguard/errcheck/gofmt/govet 等),最终判定交 CI lint job。
## 4. 遗留事项
- 无功能遗留;前端展示属后续可选项(本阶段 API-only,符合规格)。
@@ -0,0 +1,29 @@
# 完成报告: [TASK-005] 模块作者指南 + CLAUDE.md 更新
- **完成状态**: Success
- **关联任务规格**: [链接](../../../.claude/plugin-refactor/phases/phase-1_plugin-kernel/TASK-005_author_guide_docs.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
《模块作者指南》(docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md,约 380 行中文)落地,八章覆盖规格 §6 全部要求,所有代码示例摘自已落地真实代码(hello.go/imports.go/module.go/host.go),无"将来时"能力描述。CLAUDE.md 完成三处更新(GORM→Ent 修正、新增"插件模块开发"小节、项目结构补 plugin/modules 两行)。
## 2. 文件变更详情
### 创建的文件
- `docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md` — 章节:概述与设计理念 / 模块结构与命名空间 / 生命周期契约(含 Start 半途逆序回滚、Stop 逆序+ctx deadline)/ Host 能力面与铁律 / 配置子树(enabled 三态、viper 含点 key 注意事项)/ 插装清单 / 测试要求(含 make test-invariants + bench compare 硬性 gate)/ job.hello 完整示例 + 新模块检查清单
### 修改的文件
- `CLAUDE.md` — ①ORM 修正为 Ent(全文 2 处 GORM 引用,含 Repository 示例代码改为 ent.Client 写法);②新增"插件模块开发"小节(链接指南与 ROADMAP);③项目结构树补充 internal/plugin/ 与 internal/modules/
- `.gitignore`(主控裁定补充)— 新增 `!docs/plugin-architecture/` 白名单:原 `docs/*` 忽略规则会导致指南无法入库,按现有白名单模式(docs/legal/ 同款)放行;CLAUDE.md 与 .claude/ 的忽略保持原样(用户刻意本地化,改动仍生效)
## 3. 发现的既有文档债务(未处理,登记备查)
1. CLAUDE.md 描述 ports 接口在 `service/ports/*.go`,实际端口接口直接定义于 `internal/service` 包(`service/ports/` 目录不存在);`make wire` 目标实际为 `make generate`——均属既有过时描述,按"只改三点"纪律未动,建议后续单独梳理;
2. `internal/service/usage_log.go` 残留 2 处无实际作用的 `gorm:"column:..."` struct tag(go.mod 已无 gorm 依赖)。
## 4. 验证
- 指南代码示例与已落地 API 逐一对照(实施代理核对 + 主控抽查);
- `git check-ignore` 确认指南文件已可跟踪;
- 纯文档任务,零代码改动。
@@ -0,0 +1,53 @@
# 内容审核调用点清单(Phase-2 TASK-001 归档)
- **固定日期**: 2026-06-11
- **固定方法**: `grep -rn 'checkContentModeration|runContentModeration' internal/handler/ --include='*.go'`
- **裁决依据**: SEAM-DESIGN.md v2【裁决记录】——两评审计数 10/12 不一致,以本 grep 为准
- **结论**: **共 10 个真实调用点**(8 个 HTTP + 2 个 WebSocket)。
另有 3 处为辅助函数自身定义/转发(content_moderation_helper.go:15/33/40,`GatewayHandler.checkContentModeration` 与 `OpenAIGatewayHandler.checkContentModeration` 均转发到 `runContentModeration`),不计入调用点。
## 调用点明细
| # | 文件:行号 | 所属端点(handler 方法 / 路由) | 协议常量 | Blocked 后格式化函数 | body 入参表达式 | 格式类 |
|---|---|---|---|---|---|---|
| 1 | gateway_handler.go:198 | `GatewayHandler.Messages` — POST /v1/messages(anthropic 平台分组)| `ContentModerationProtocolAnthropicMessages` | `h.errorResponse(c, contentModerationStatus(decision), contentModerationErrorCode(decision), decision.Message)`(gateway_handler.go:1698) | `body` | **A**(anthropic) |
| 2 | gateway_handler_chat_completions.go:98 | `GatewayHandler.ChatCompletions` — POST /v1/chat/completions(anthropic 平台分组兼容层) | `ContentModerationProtocolOpenAIChat` | `h.chatCompletionsErrorResponse(...)`(gateway_handler_chat_completions.go:333) | `body` | **B**(chat_completions) |
| 3 | gateway_handler_responses.go:107 | `GatewayHandler.Responses` — POST /v1/responses(anthropic 平台分组兼容层) | `ContentModerationProtocolOpenAIResponses` | `h.responsesErrorResponse(...)`(gateway_handler_responses.go:312,**code 字段**) | `body` | **C**(responses) |
| 4 | gemini_v1beta_handler.go:190 | `GatewayHandler.GeminiV1BetaModels` — POST /v1beta/models/*modelAction(含 /antigravity 路由复用) | `ContentModerationProtocolGemini` | `googleError(c, contentModerationStatus(decision), decision.Message)`(gemini_v1beta_handler.go:663)——**不消费 errorCode** | `body` | **D**(googleError) |
| 5 | openai_gateway_handler.go:244 | `OpenAIGatewayHandler.Responses` — POST /openai/v1/responses(HTTP;openai 平台分组) | `ContentModerationProtocolOpenAIResponses` | `h.errorResponse(...)`(openai_gateway_handler.go:1920) | `body` | **B'**(与 B 字节级同形) |
| 6 | openai_gateway_handler.go:676 | `OpenAIGatewayHandler.Messages` — POST /v1/messages(openai 平台分组 dispatch) | `ContentModerationProtocolAnthropicMessages` | `h.anthropicErrorResponse(...)`(openai_gateway_handler.go:934) | `body` | **A'**(与 A 字节级同形) |
| 7 | openai_chat_completions.go:88 | `OpenAIGatewayHandler.ChatCompletions` — POST /openai/v1/chat/completions | `ContentModerationProtocolOpenAIChat` | `h.errorResponse(...)`(openai_gateway_handler.go:1920) | `body` | **B'** |
| 8 | openai_images.go:89 | `OpenAIGatewayHandler.Images` — POST /v1/images/generations、/v1/images/edits(含 /openai 前缀路由) | `ContentModerationProtocolOpenAIImages` | `h.errorResponse(...)`(openai_gateway_handler.go:1920) | **`parsed.ModerationBody()`**(全清单唯一非原始 body 入参:prompt + images 重组 JSON) | **B'** |
| 9 | openai_gateway_handler.go:1251 | `OpenAIGatewayHandler.ResponsesWebSocket` — GET /openai/v1/responses 等 WS 升级路由;**turn-1 首帧** | `ContentModerationProtocolOpenAIResponses` | `writeContentModerationWSError(ctx, wsConn, decision)`(openai_gateway_handler.go:1992)+ `closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, decision.Message)` | `firstMessage` | **E**(WS 错误帧 + close) |
| 10 | openai_gateway_handler.go:1426 | `OpenAIGatewayHandler.ResponsesWebSocket` — `OpenAIWSIngressHooks.BeforeRequest` 回调;**turn ≥ 2 每消息审核** | `ContentModerationProtocolOpenAIResponses` | `writeContentModerationWSError(ctx, wsConn, decision)` + `return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, decision.Message, nil)`(最终由 openai_gateway_handler.go:1567-1569 取 closeErr.StatusCode()/Reason() 关闭客户端 WS) | `payload`(model 取值链:`originalModel` → `payload.model` → `reqModel`) | **E** |
## 格式类定义(实测锁定值)
| 格式类 | JSON 形状(403 拦截时) | 关键差异点 |
|---|---|---|
| **A / A'**(anthropic) | `{"type":"error","error":{"type":"content_policy_violation","message":<BlockMessage>}}` | 有顶层 `type:"error"`;error 内用 `type` 字段 |
| **B / B'**(chat_completions / openai 网关通用) | `{"error":{"type":"content_policy_violation","message":<BlockMessage>}}` | **无**顶层 type;error 内用 `type` 字段 |
| **C**(responses,仅 GatewayHandler 侧) | `{"error":{"code":"content_policy_violation","message":<BlockMessage>}}` | **无**顶层 type;error 内用 **`code`**(字符串)而非 `type` |
| **D**(gemini googleError) | `{"error":{"code":403,"message":<BlockMessage>,"status":"PERMISSION_DENIED"}}` | **无**顶层 type、**无** error.type;`code` 为 **int** HTTP 状态;`status` 为 google 状态串(403→PERMISSION_DENIED);errorCode(content_policy_violation)**被丢弃** |
| **E**(WS) | 错误帧 `{"event_id":"evt_content_moderation_blocked","type":"error","error":{"type":"invalid_request_error","code":"content_policy_violation","message":<Message>}}` + close(1008 StatusPolicyViolation, reason=Message 截断 120 字节) | 帧内 error.type 固定 `invalid_request_error`,审核码降级为 `code` 字段 |
## 注意点(TASK-003 改造时的硬约束)
1. **同一协议常量 ≠ 同一格式**:`ContentModerationProtocolOpenAIResponses` 在调用点 3(GatewayHandler,C 格式 code 字段)与调用点 5(OpenAIGatewayHandler,B' 格式 type 字段)格式**不同**;格式由调用点的格式化函数决定,与协议常量无映射关系——印证裁决"格式化留在调用点"。
2. **gemini 调用点不消费 `contentModerationErrorCode`**:Decision.ErrorType 对 D 格式无效(与裁决记录一致)。
3. **images 调用点的 body 入参是 `parsed.ModerationBody()`**,不是原始请求 body——链改造时各点保留自己的入参表达式。
4. **WS 两点(9/10)按裁决排除在链改造之外**,保持现状。
## 特征化测试覆盖映射
| 格式类 | 测试函数 | 文件 |
|---|---|---|
| A(block)+ A fail-open | `TestGatewayCharacterization_ContentModerationBlock` / `..._ContentModerationFailOpen`(已有) | gateway_intercept_characterization_test.go |
| A'(openai 网关 anthropic 一族,调用点 6) | `TestP2Characterization_ModerationBlock_OpenAIGatewayAnthropicFormat` | gateway_moderation_format_characterization_test.go |
| B(调用点 2) | `TestP2Characterization_ModerationBlock_ChatCompletionsFormat` | 同上 |
| B fail-open(非 anthropic 协议) | `TestP2Characterization_ModerationFailOpen_ChatCompletionsFormat` | 同上 |
| B'(调用点 8 images;**归并覆盖调用点 5/7**——三点共用 openai_gateway_handler.go:1920 同一格式化函数,输出与 B 字节级同形) | `TestP2Characterization_ModerationBlock_OpenAIImagesFormat` | 同上 |
| C(调用点 3) | `TestP2Characterization_ModerationBlock_ResponsesFormat` | 同上 |
| D(调用点 4) | `TestP2Characterization_ModerationBlock_GeminiGoogleErrorFormat` | 同上 |
| E turn-1(调用点 9) | `TestOpenAIResponsesWebSocket_ContentModerationBlocksFirstFrame`(已有) | openai_gateway_handler_test.go |
| E turn-2(调用点 10) | `TestP2Characterization_OpenAIResponsesWSTurn2ModerationCloseError` | gateway_moderation_format_characterization_test.go |
@@ -0,0 +1,44 @@
# 阶段总结报告: Phase-2 接缝试点(payment 注册表化 + 网关钩子链)
- **阶段状态**: Completed
- **完成时间**: 2026-06-11
- **关联阶段计划**: [链接](../../../.claude/plugin-refactor/phases/phase-2_pilots/PHASE_PLAN.md)
## 1. 阶段目标达成情况(对照 PHASE_PLAN §3)
1. ✅ 特征化前置 gate 先行(7 个格式测试 + 调用点权威清单归档);
2. ✅ factory switch 清零(payment 私有注册表);8 个 HTTP 调用点经钩子链、WS 两点按裁决保持现状;外部行为零变化(安全网 44 包 + 7 格式测试 + bench allocs 全部不变);
3. ✅ 等价性硬约束逐条验证(unknown-key 文案、ApplicationError 透传、5 格式映射、protocol 常量复用、fail-open、11 字段日志——各有测试或核对记录);
4. ✅ 实施后对抗审计零 P0/P1(突变实验 4/4 被捕获);
5. ✅ 内核零改动;全量测试 + vet + wire 一致性通过。
## 2. 任务完成统计
| 任务ID | 任务描述 | 状态 | 完成报告 |
|:--|:--|:--|:--|
| TASK-001 | 拦截格式特征化测试(前置 gate) | Completed | [链接](./TASK-001_format_characterization.md) |
| TASK-002 | payment 私有注册表 | Completed | [链接](./TASK-002_payment_registry.md) |
| TASK-003 | gatewayhook 链 + 调用点替换 | Completed | [链接](./TASK-003_gatewayhook_chain.md) |
| TASK-004 | 实施后对抗审计 | Completed | [链接](./TASK-004_post_audit.md) |
## 3. 关键技术成果
- **多轮审计纪律首次全流程落地**:摸底 → 候选设计 → 双视角架构评审([REVIEW-ARCH](./REVIEW-ARCH.md)/[REVIEW-RISK](./REVIEW-RISK.md))→ 架构师裁决(SEAM-DESIGN v2)→ 特征化先行 → 实施 → 对抗审计(突变实验);
- **payment**:新增支付渠道 = 新增 provider 文件 + init 自注册,零核心改动;
- **gatewayhook**:协议无关 pre-flight 钩子链(panic 隔离、fail-open 默认、空链零分配),8 个调用点收敛,为 Phase-3 平台 Provider 与未来模块钩子建立管道;
- **诚实的架构修正**:评审证据证明 payment/moderation 均非真正模块候选(无生命周期/依赖超 Host),ROADMAP 命名空间表已如实改标 Deferred——接缝价值独立于"插件化里程碑"成立。
## 4. 遇到的问题与解决方案
- **评审分歧**(payment 进不进插件内核 / 命名空间收集做不做):以 YAGNI + 内核冻结纪律裁决,采纳更简方案;分歧与裁决理由全程归档;
- **评审事实纠错**:调用点数设计初稿写 8、两评审数出 10/12,实施 TASK-001 首步 grep 权威定数 10(8 HTTP + 2 WS),"12"系误计 helper 定义——三方核对的价值实证;
- **审计操作事故**:突变实验误用 `git checkout` 回退了未提交的 Phase-2 改动,已哈希核对恢复并主控复验。教训固化:未提交工作区上的突变实验必须用文件备份恢复。
## 5. 技术债务与待优化项
- OBS-1:C/D/B' 格式的 fail-open 无直接测试(结构同构论证保证),改 adapter 策略时须补;
- **全部 5 个阶段(Phase-0 至 Phase-2)的改动仍未 commit**——审计事故已实证此状态的脆弱性,强烈建议立即分段提交。
## 6. 下一阶段准备
Phase-3(平台 Provider)就绪条件已齐:gatewayhook 管道在位、内核稳定、安全网完备。Phase-3 启动时需先做的单独提案:Runtime 实例访问 API(`BuiltModulesInNamespace` 一类,与平台 Provider 真实需求合并设计)。
@@ -0,0 +1,28 @@
# Phase-2 接缝设计评审 — 架构合理性/Caddy 对齐/演进性视角(2026-06-11)
> 评审对象:SEAM-DESIGN.md v1。裁决采纳情况见 SEAM-DESIGN.md v2【裁决记录】。
## 结论
- 接缝 P:P-A 方向可行但"能力模块不经 Runtime"契约**破坏内核语义**(必须改)——绕过 Runtime 自行 New 导致 Snapshot/admin 可观测失效、Stop 失管;Caddy 的 ctx.LoadModule 合法是因为 Caddy 无集中 Runtime,本内核语义不同。修正案:模块经 Runtime Build,factory 查询已 Build 实例,需内核补 `BuiltModulesInNamespace` 只读 API(单独提案)。
- 接缝 H:H-A 总体可行,三处必须改:①钩子包必须独立(internal/gatewayhook,否则模块 import handler 层);②GinCtx 暴露是安全边界漏洞(钩子可 c.Set 覆盖认证、抢写响应),改只读访问器;③**事实遗漏:调用点 12 个非 8 个**,其中 2 个 WebSocket 路径(openai_gateway_handler.go:1251/:1426)是每消息语义 + WS 帧格式,必须排除在链改造外。
## 其他意见
- Subject → CallerInfo 最小视图(建议改,解耦 middleware 包);
- Chain 的 fail-open 必须返回 (nil,nil) 而非 (nil,err),保持"nil=放行"简单语义;
- "核心钩子(Wire)+ 插件钩子(Runtime 收集)"双轨模式认可;
- EnabledByDefault=true 迁移例外:有条件成立(须 ROADMAP 登记规则 + 双开关语义文档化);
- Phase-3 先例效应:P-A 绕过 Runtime / GinCtx 暴露 / Subject 耦合三个错误若带入平台 Provider 阶段会被放大,必须在 Phase-2 纠正。
## 评审请求裁决表
| # | 裁决 |
|---|---|
| Q1 | P-A 修正后可行(经 Runtime);P-B 否决;H-A 修正后可行;H-B 否决;H-C 不推荐(收集机制是 Phase-3 前置模式,值得现在验证) |
| Q2 | "不经 Runtime"否决;正确表述:经 Runtime Build + factory 查已 Build 实例 |
| Q3 | 成立,附条件(ROADMAP 规则化 + 双开关文档化) |
| Q4 | Decision 最小充分;GinCtx 必须改;Subject 建议改;WS 边界必须补充 |
| Q5 | 必须先补特征化测试(≥3 种 HTTP 格式),WS 排除 |
> 注:Q1/Q2 中"修正 P-A 经 Runtime"与"H-A 含收集"两项被最终裁决否决,采纳风险评审的更简方案(payment 私有注册表 + H-C),理由见 SEAM-DESIGN v2 裁决记录;本报告其余意见全部采纳。
@@ -0,0 +1,40 @@
# Phase-2 接缝设计评审 — 回归风险/等价性/YAGNI 视角(2026-06-11)
> 评审对象:SEAM-DESIGN.md v1。本报告的第四方案与 H-C 被最终裁决采纳(见 SEAM-DESIGN.md v2)。
## 结论
方向正确,但有一个事实硬伤(调用点 10 个非 8 个,含 2 个 WS 路径,Decision 抽象无法表达 WS 关闭码/帧)+ 一个铁律硬冲突(命名空间收集需给冻结的 Runtime 加实例访问 API——`moduleRecord.instance` 私有、Snapshot 只回元数据)。
## 风险清单(核对真实代码后)
**moderation 链替换:**
- A1 WS 两点(:1251 首帧 / :1426 turn 内每消息)走 writeContentModerationWSError + 关闭帧,必须排除;turn-2 close-error 路径无测试需补;
- A2 gemini `googleError(c, status, message)` 两参签名不消费 errType,输出 `{code:int, status:string}` 三字段——统一格式化会污染 gemini 格式;
- A3 顶层 `type:"error"` 包裹差异:gateway_handler.errorResponse 有 / chatCompletions 无 / responses 用 `code` 字段——共 5 种 HTTP 格式 + 1 WS(格式函数:gateway_handler.go:1698、gateway_handler_chat_completions.go:333、gateway_handler_responses.go:312、gemini_v1beta_handler.go:663、openai_gateway_handler.go:934,1920);
- A4 入参 5 种表达式(images 用 parsed.ModerationBody()、WS 用 payload)——链不得统一取 body;
- A5 protocol 常量真实值 `openai_chat_completions`(content_moderation.go:51-55)≠ 设计写的 `openai_chat`——必须复用现有常量;
- A6 fail-open 在 helper:60-66(Check 返回 err 即放行)——链层默认策略必须写死为 fail-open。
**factory 改造:**
- B1 `_validate_` 路径(payment_config_providers.go:27)依赖构造器 ApplicationError 原样透传做前端 i18n——查找层不得包裹错误;
- B2 unknown-key 文案(factory.go:22)保持逐字节一致;
- B3 RefreshProviders(Clear+重载)要求查找路径无包级可变状态副作用。
## YAGNI 审查
- 【砍】P-A 整体:无生命周期工厂进模块系统需三条契约例外,是为模式而模式。**第四方案**:payment 包私有 `map[string]ConstructorFunc` + provider init() 自注册,零内核耦合达成"消灭 switch";
- 【砍】命名空间收集:为零成员命名空间写收集代码 + 解冻内核 API,违 YAGNI——采纳 H-C,Phase-3 有真实模块钩子时与 Runtime 实例访问 API 合并设计;
- 【必补】链层 panic recover(抽象成接口遍历后,单钩子 panic 会炸整条请求);
- 【必补】`gateway_check_start/done` 11 字段结构化日志等价(运维依赖);
- 【必补】fail-open 默认策略入契约。
## 评审请求裁决表
| # | 裁决 |
|---|---|
| Q1 | P:双否决,采纳第四方案(payment 私有注册表);H:采纳 H-C |
| Q2 | 不接受;改用私有注册表则问题不存在 |
| Q3 | 不成立(第四方案下无此概念;死字段徒增认知负担) |
| Q4 | Request 不充分(WS 维度、入参非单一);GinCtx 过宽改只读访问器 |
| Q5 | 必须先补,按格式类去重:5~6 条 HTTP block 特征化 + WS turn-2 close-error 1 条,约半天成本 |
@@ -0,0 +1,32 @@
# 完成报告: [TASK-001] 拦截格式特征化测试补齐(实施前置 gate)
- **完成状态**: Success
- **关联设计**: [SEAM-DESIGN.md v2 裁决 H](../../../.claude/plugin-refactor/phases/phase-2_pilots/SEAM-DESIGN.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
调用点清单权威定数并归档([CALLSITE-INVENTORY.md](./CALLSITE-INVENTORY.md)):**10 个真实调用点 = 8 HTTP + 2 WS**(评审分歧 10/12 的"12"系误计 helper 函数定义)。新增 7 个格式特征化测试(`gateway_moderation_format_characterization_test.go`,p2Char 前缀),全部为实跑后固化的真实行为,TASK-003 的实施 gate 就绪。
## 2. 格式类 → 测试映射与锁定差异
- B chat_completions:403、error.type、**无顶层 type**;
- C responses(GatewayHandler 侧):error 内用 **code 字符串**字段;
- D gemini:error.code 为 **int**、status=PERMISSION_DENIED、**无 type 字段**(content_policy_violation 被丢弃);
- A' openai 网关 anthropic 格式:**有**顶层 type:"error";
- B' images:与 B 字节级同形(openai 网关三点共用 :1920 格式化函数,归并覆盖);
- fail-open(chat_completions):审核 500 → 放行至调度(503 非 403),与 block 测试同夹具互证;
- E WS turn-2:turn-1 正常转发 → turn-2 命中 → 错误帧(evt_content_moderation_blocked)→ close 1008 → **帧未达上游**(完整 E2E 夹具,3 次重复稳定)。
## 3. 关键发现
**同一协议常量 ≠ 同一格式**:`openai_responses` 在 GatewayHandler 侧是 C 格式(code 字段)、在 OpenAIGatewayHandler 侧是 B' 格式(type 字段)——印证评审裁决"Decision→格式化映射保留在各调用点,链不得统一格式化"。
## 4. 文件变更详情
- **创建**: `backend/internal/handler/gateway_moderation_format_characterization_test.go`、`issues/plugin-refactor/phase-2_pilots/CALLSITE-INVENTORY.md`
- 零业务代码改动
## 5. 验证记录(主控复跑确认)
- 7/7 PASS;全部 Characterization 集合 ok;`make test-invariants` 全绿;全量 unit 零失败;vet 干净。
@@ -0,0 +1,26 @@
# 完成报告: [TASK-002] payment 私有注册表(消灭 factory switch)
- **完成状态**: Success
- **关联设计**: [SEAM-DESIGN.md v2 裁决 P](../../../.claude/plugin-refactor/phases/phase-2_pilots/SEAM-DESIGN.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`provider/factory.go` 的 5-case switch 清零:包级私有 `map[string]ConstructorFunc` + 各 provider 文件 `init()` 闭包自注册(重复 key 写前 panic)。新增支付渠道 = 新增 provider 文件 + init 一行,factory/核心零改动。按评审裁决**未触碰插件内核**。
## 2. 等价性硬约束核对(评审 B1/B2/B3)
- **B2 unknown-key 文案**:`fmt.Errorf("unknown provider key: %s", ...)` 表达式逐字节相同 + `TestCreateProviderUnknownKeyMessage` 精确相等断言锁定;
- **B1 错误原样透传**:命中后 `return fn(...)` 无包裹;`TestCreateProviderPassesThroughApplicationError` 用**直接类型断言**(非 errors.As)证明 `*ApplicationError` 未被包裹(Reason/Metadata 字段级断言),另 4 个 provider 校验错误文案精确相等锁定;
- **B3 无可变状态副作用**:map 仅 init 期写入,CreateProvider 纯读(RefreshProviders Clear+重载幂等);panic 发生在写入前(注册表长度不变断言);`TestRegistryContainsExactlyAllProviderKeys` 锁定恰好 5 key 防漏注。
## 3. 文件变更详情
- **创建**: `internal/payment/provider/registry.go`、`registry_test.go`(6 测试)
- **修改**: `factory.go`(switch→map)、5 个 provider 文件(各 +init)
- 未触碰 internal/plugin、internal/modules、payment.Registry、PaymentService
## 4. 验证记录(主控复跑确认)
- factory.go switch 计数 = 0 ✅;`go test ./internal/payment/...` ✅;service 层 Payment 测试 ✅;
- `make test-invariants` 43 包 ✅;全量 unit 零失败;vet/gofmt 干净。
@@ -0,0 +1,32 @@
# 完成报告: [TASK-003] gatewayhook 链 + moderation 核心钩子 + HTTP 调用点替换
- **完成状态**: Success
- **关联设计**: [SEAM-DESIGN.md v2 裁决 H](../../../.claude/plugin-refactor/phases/phase-2_pilots/SEAM-DESIGN.md)
- **完成日期**: 2026-06-11
## 1. 任务完成简报
`internal/gatewayhook` 包落地(Request/CallerInfo/只读 RequestHeaders/Decision/PreFlightHook/Chain),Chain 含评审三件必补:每钩子 recover 隔离、error 默认 fail-open、`Run` 永不返回 error(nil=放行)。moderation 核心钩子 adapter 经 Wire 装配;**8 个 HTTP 调用点全部经链**(每点保留原入参表达式与格式化函数,逐点对照 TASK-001 格式测试验证);**WS 两点(:1255/:1430)完全未动**(helper 保留供其消费)。
## 2. 关键等价性证据
- 7 个格式特征化测试 + Phase-0 拦截特征化全绿(逐点替换时单独验证);
- 输入 builder 新旧逐字段等价测试、gateway_check_start 11 字段/check_done 8 字段日志名称+顺序锁定测试;
- adapter 自吞 Check error(保留原 `content_moderation.check_failed` 事件名)——日志事件名零变化;
- **bench compare:allocs/op 全部零增加**(93/134/40/37/2 不变,ns/op 噪声内);空链路径 AllocsPerRun=0 锁定。
## 3. 裁量决定(要点)
请求级 logger 经 handler 私有 ctx key 传入(契约 7 字段不扩);强制平台覆盖改读 ctxkey.ForcePlatform(middleware 双写恒一致,等价性有测试);GatewayHandler 彻底移除 moderation service 字段(无 WS 消费者),OpenAIGatewayHandler 双持(svc 供 WS、chain 供 HTTP);gatewayhook→service import 无环(service 不引用 gatewayhook,且契约含 *service.APIKey 本就需要)。
## 4. 文件变更详情
- **创建**: `internal/gatewayhook/{hook,chain,chain_test}.go`、`internal/handler/gateway_preflight.go` + `gateway_preflight_test.go`
- **修改**: 7 个 handler 文件(8 调用点替换)、content_moderation_helper.go(删 2 个零消费者函数、保留 WS 消费部分)、handler/wire.go、wire_gen.go(wire@v0.7.0 生成);TASK-001/Phase-0 测试文件夹具适配(断言零改动)
- 内核 internal/plugin 零改动
## 5. 验证记录(主控复跑确认)
- `go build ./...` ✅;Characterization|Invariant 集合(handler+service)✅;gatewayhook 包测试 ✅;
- 剩余 `checkContentModeration` 引用恰为 5 处(WS helper 定义 1 + 注释 2 + WS 调用点 2)✅;
- `make test-invariants`、全量 unit 44 包 0 失败、vet、bench compare exit 0(实施代理记录 + 主控抽查)。
@@ -0,0 +1,29 @@
# 完成报告: [TASK-004] 实施后对抗式审计与修复
- **完成状态**: Success(无需修复——零 P0/P1)
- **完成日期**: 2026-06-11
## 1. 审计结论
**整体通过,无 P0/P1**;3 个 P2 观察项(记录性质,无需改码);突变实验 **4/4** 被测试精确捕获;收尾工作区完整恢复(主控复验:构建 + 全部相关测试 + 安全网 44 包绿)。
## 2. 八项审计明细
- **A 裁决符合性 5/5 通过**:gatewayhook 零 handler/middleware 依赖;WS 两点未动;内核零改动;格式化零泄漏(preFlightStatus/ErrorCode 仅做钳制兜底,等价旧函数);Protocol 零新造枚举;
- **B 等价性矩阵**:block 5 格式全测试覆盖;fail-open 空白格(C/D/B')经代码路径论证结构同构(adapter 自吞错→(nil,nil)→不触格式化分支),机制由 A/B 两格测试验证;
- **C 链正确性**:panic recover 精确语义(该钩子放行、后续继续)、顺序/短路/并发安全(-race)/nil 链防御全过;
- **D adapter 等价性**:input builder 逐字段 + 整体 require.Equal 三场景;ForcePlatform 双写(middleware.go:37-44)证实 gin key 与 ctxkey 恒一致;logger nil 守卫逐块等价;日志契约锁定测试在位;
- **E payment**:typed-nil 新旧逐字节同构(同一转换边界);重复注册写前 panic;ApplicationError 透传双重断言;
- **F 突变 4/4 红**:链短路破坏 / gemini 格式化函数替换 / 注册 key 改名 / 删 panic 守卫——各自被对应测试捕获;
- **G 性能**:空链零分配锁定;审核 gate 不在 SSE 热环;bench compare PASS(allocs 全部不变,ns -0.3%~-4.6%);
- **H 测试可信度**:unit 标签在 CI 激活;TASK-001 测试的夹具适配确认断言零改动。
## 3. P2 观察项(登记备查)
- OBS-1:C/D/B' 的 fail-open 无直接测试(结构同构保证);若未来改动 adapter fail-open 策略须补这三格;
- OBS-2:被审计文件均为未提交状态,无 git baseline 可 diff——**尽快分段 commit 可消除此审计盲区**;
- OBS-3:typed-nil 属性为改造前已存在并逐字节保留,`_validate_` 只查 err 故无影响,留档备查。
## 4. 操作事故与教训(重要)
审计中一次 `git checkout <file>` 误将 gemini handler 回退到 pre-Phase-2 的 HEAD(丢失未提交改动),经重新应用 + index 哈希核对完整恢复(主控复验该文件格式测试绿)。**教训固化**:工作区含未提交工作时,突变实验必须用文件备份恢复,禁用 git checkout。此教训已写入本报告,后续审计任务下达时应显式传达;同时这是"尽快提交分段 PR"的最强论据。