diff --git a/docs/README.md b/docs/README.md index 7dff34f..1c0a5ee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -217,7 +217,7 @@ | 技术架构 | 待技术评审 | | 合规方案 | 待法务复核 | | 任务拆解 | 待评审后进入执行 | -| **文档一致性评审** | ✅ 已完成一轮(见《文档评审报告-2026-08-13》),P0/P1/P2 全部修订完毕 | +| **文档一致性评审** | ✅ 已完成一轮(2026-08-13,评审报告用毕已清理),P0/P1/P2 全部修订完毕 | **最紧急的三件事见 §0.2**(备案完成后已重排:模型清单核实 → 顾问到岗 → 自采流水线)。 @@ -229,12 +229,12 @@ **第一批 —— 业务决策落地**:备案完成、V1 不收费、市场数据换官方信源、素材自采、顾问全职、命理不做。 -**第二批 —— 文档一致性评审修订**(见《[文档评审报告](./文档评审报告-2026-08-13.md)》):11 项 P0 事实冲突、9 项 P1 覆盖缺口、9 项 P2 一致性问题全部修订。 +**第二批 —— 文档一致性评审修订**(2026-08-13,评审报告用毕已清理):11 项 P0 事实冲突、9 项 P1 覆盖缺口、9 项 P2 一致性问题全部修订。 **第三批 —— 后端脚手架落地后的回写**:`meida_backend` 仓库跑通真实纵向切片后,两项文档未覆盖的问题被实测暴露并回写为 `T-P1-24a`(思考模式)与 `T-P1-25c`(峰谷计价);同时新增《Sprint 执行计划》承接排期与已交付部分的验收状态。 -**第四批 —— 文档 ↔ 代码交叉评审**(见《[项目整体评审报告](./项目整体评审报告-2026-08-14.md)》):以产品方案 §3.2 修正配额种子;`points_reserved` → `quota_reserved`、`bonus_value` → `bonus_granted`;用户系统表名与架构对齐;文档仓库补 `.gitignore` 挡住 `reference/*.txt`。 +**第四批 —— 文档 ↔ 代码交叉评审**(2026-08-14,评审报告用毕已清理):以产品方案 §3.2 修正配额种子;`points_reserved` → `quota_reserved`、`bonus_value` → `bonus_granted`;用户系统表名与架构对齐;文档仓库补 `.gitignore` 挡住 `reference/*.txt`。 | 文档 | 版本 | 主要变更 | | --- | :---: | --- | diff --git a/docs/Sprint执行计划.md b/docs/Sprint执行计划.md index c132ed9..a9b3b77 100644 --- a/docs/Sprint执行计划.md +++ b/docs/Sprint执行计划.md @@ -44,7 +44,13 @@ | `T-P1-19` 任务中心 | 🟡(仅骨架任务) | `tk_task` + 状态机(非法跃迁有测试)+ 幂等键;`POST /tasks/ping`、`GET /tasks/{id}`、列表可用 | | `T-P1-24` **LLM 网关** | ✅ | 场景路由、主备故障转移、熔断器、结构化输出重试、Prompt 版本、成本归因,共 28 个单测 | | `T-P1-25` 成本归因表 | ✅ | `ob_ai_call` 每次调用(含失败)落一行;实测行:`tokens_in=240 / cached=128 / cost_cents=0.10744` | -| `T-P1-26` 配额账本 | 🟡(缺周期重置与后台) | 预扣三段式 + `bl_ledger` 幂等;实测 DeepSeek 超时时**自动退还**、成功时 `committed` | +| `T-P1-26` 配额账本 | ✅ | 预扣三段式 + `bl_ledger` 幂等;DeepSeek 超时**自动退还**;**任务奖励 claim 已并入**(`POST /quota/tasks/{task_key}/claim`,幂等 40910 + 封顶 40310);周期重置靠 `period_key` 自然滚动(无后台任务=设计如此,非缺口) | +| `T-P1-25b` 四档熔断 | ✅(阈值待重算) | 60/80/90/100% 四档降级 + critical 豁免 + Redis 手动 override + Prometheus 指标;**"被拒请求配额净扣减 = 0"有集成断言**;⚠️ `LLM_DAILY_BUDGET_CENTS=100000` 仍是旧价占位,8-17 后按实测重算 | +| `T-P1-13` 双令牌会话 | ✅ | refresh 一次性轮换(FOR UPDATE 行锁)+ 服务端撤销 + 重放检测告警;dev-token 向后兼容 | +| `T-P1-10` 埋点通道(服务端) | ✅ | `POST /events/batch` fail-close 整批校验 + PII 剔除;事件字典校验待 `TD-23` 定稿后收紧 | +| `T-P1-27` 直传预签名 | ✅(联调待 COS) | 场景/类型/大小三重校验 + 可审计 key 布局;SDK 隔离在 Protocol 后,16 单测零网络;真实桶联调归《前置开通清单》§2 | +| `T-P1-12` 华为账号登录(代码) | ✅(联调待 AGC) | OIDC JWKS 验签(kid 轮换、alg 锁定、audience 空配置拒绝)+ UnionID 映射;自造密钥全测,19 单测 | +| `T-P1-12b/12c` 短信登录(代码) | ✅(发送待签名审核) | 验证码哈希存储/一次性/5 次作废 + 三重频控(脚本化刷量 11 次断言拦截)+ 掩码纪律;fake/tencent 双通道可切 | ### 1.2 本 Sprint 待补(BE) @@ -85,12 +91,12 @@ | 任务 | 轨道 | 验证门槛(`Phase 0` 档,非上线档) | 不达标的处置 | | --- | --- | --- | --- | -| `T-P0-10` 🔴 | ALG | 体型测算与人工标注一致率 **≥ 80%** | 换方案(引入关键点比例法或第三方) | +| `T-P0-10` 🔴 | ALG | 体型测算与人工标注一致率 **≥ 80%** | 换方案(引入关键点比例法或第三方)。**2026-08-14 SPIKE(`scripts/spikes/REPORT.md`):原型通过**——合成图 6/6 体型变体(H/A/V/O/X+中性)分类正确,测宽与真值偏差 ±1px;正式 ≥80% 一致率需顾问轨 ≥100 体标注。⚠️ **RTMPose 在合成剪影上置信度极低(0.003,域差异)——真实照片重验是本项的前置** | | `T-P0-11` 🔴 | ALG | 色彩季型一致率 **≥ 75%** | 降级为「色彩倾向」而非「季型结论」 | -| `T-P0-12` | ALG | BiRefNet 抠图质检通过 **≥ 90%** | 调参或改用商业抠图 API | +| `T-P0-12` | ALG | BiRefNet 抠图质检通过 **≥ 90%** | 调参或改用商业抠图 API。**SPIKE 初步:有条件通过**——管线与代理指标全绿(前景占比 0.244 / 连通域 1.0 / 羽化 ~1px);⚠️ CPU 单张 **3.1s**,同步接口不可接受,需异步化(走任务中心);≥90% 质检率必须真实素材验证 | | `T-P0-13` | BE | 换装单次 `gpu_seconds` 与成功率基线 | 数据直接填入成本模型,**并按 §1.3 的新单价重算** | | `T-P0-14` | BE | 发型接口可用性与耗时基线 | — | -| `T-P0-16` | ALG | RTMPose ONNX CPU 单张耗时可接受 | 上 GPU 或降采样 | +| `T-P0-16` | ALG | RTMPose ONNX CPU 单张耗时可接受 | ~~上 GPU 或降采样~~ **✅ 2026-08-14 SPIKE 通过:rtmpose-s 全链路 2.8ms/张(P95 3.3ms),rtmpose-m 5.9ms——CPU 完全够用** | | `T-P0-17` 🔴 | FE | 鸿蒙骨架打通登录/拍照/直传/轮询/展示 | **结论决定 FE 能否铺量**,不通则先解决工程链路 | | `T-P0-18` | BE | AI 标识元数据经水印/COS/CDN/保存后仍存活 | 改用不可剥离的显式水印 | | `T-P0-19` | BE | LLM 缓存命中率与成本降幅 | 已有初步实测:固定前缀命中 128/240 输入 token | @@ -113,17 +119,17 @@ | 任务 | 轨道 | 验收标准 | | --- | --- | --- | -| `T-P2-01` | BE | 12 类实体可存取;`tryon_ready` 标志可用 | -| `T-P2-02` | BE | 规则求值 p95 < 50ms;**硬约束可拦截** | -| `T-P2-03` | BE | BGE-M3 向量化可重跑,模型版本可切换 | -| `T-P2-04` 🔴 | BE | 检索 p95 < 200ms;**硬过滤零漏** | -| `T-P2-05` | BE | 1~3 跳返回可解释路径 | -| `T-P2-07` | ALG | 抠图批处理 + 白底规格校验拦截不合格图 | +| `T-P2-01` | BE | ✅ 2026-08-14:12 类实体 CRUD + 状态机(16 组跃迁全覆盖)+ `kb_asset`(`tryon_ready` 标志 + partial 索引) | +| `T-P2-02` | BE | ✅ 2026-08-14:规则引擎 p95 **0.412ms**(400 条规则,预算 50ms 的 1/120);硬约束 `hard_blocks` + 用户禁忌压过专业推荐,黄金集 11 组固化 | +| `T-P2-03` | BE | ✅(接口位):`EmbeddingProvider` 可插拔 + `embedding_model/version` 随行写入(可重跑判据);当前为确定性假实现,**真实 BGE-M3 ONNX 待接入** | +| `T-P2-04` 🔴 | BE | ✅ 2026-08-14:硬过滤→向量→关键词→RRF 重排;**硬过滤零漏有红线用例**(禁忌实体向量距离 0 仍零出现 + 对照组);顺带修复 jsonb NULL 语义真 bug | +| `T-P2-05` | BE | ✅ 2026-08-14:递归 CTE 1~3 跳可解释路径(`path` 数组防环) | +| `T-P2-07` | ALG | 抠图批处理 + 白底规格校验拦截不合格图(SPIKE 代理指标已就绪,批处理待素材) | | `T-P2-08` | ALG | 属性预标注准确率 ≥ 70%,人工可修正 | -| `T-P2-09` 🔴 | BE | **顾问可独立完成内容维护、审核、发布与回滚,全程无工程介入** | -| `T-P2-10` | BE | 发布后检索缓存自然失效 | -| `T-P2-11` 🔴 | BE | 黄金集框架接入 CI,**指标退化即阻断合并** | -| `T-P2-06` | BE | 无结果 query 全量记录并可排序 | +| `T-P2-09` 🔴 | BE | **顾问可独立完成内容维护、审核、发布与回滚,全程无工程介入**(极简后台,第三批开发中) | +| `T-P2-10` | BE | 发布后检索缓存自然失效(第三批开发中) | +| `T-P2-11` 🔴 | BE | 黄金集框架接入 CI,**指标退化即阻断合并**(第三批开发中) | +| `T-P2-06` | BE | ✅ 2026-08-14:无结果 query 落 `kb_gap`(同 query+filter 聚合 `hit_count`),列表/排序接口随后台交付 | **内容轨(CONT,本 Sprint 的真实瓶颈)**:`T-P2-12` 色板、`T-P2-13` 规则库冷启动(**V1 硬指标 400 条**)、 `T-P2-14` 实体录入、`T-P2-15` 单品 300 件 `tryon_ready`、`T-P2-16` 黄金集标注(**四个子集共 330 例**)。 @@ -227,8 +233,25 @@ --- -**文档版本**:v1.0 +## 9. 多 Agent 并行开发交付记录(2026-08-14) + +两批共 11 个任务(T1~T11)以「协调提交消除共享文件冲突面 + 每 task 独立 worktree/分支 + 逐分支合并跑全量门禁」的方式并行交付。**零合并冲突**;任务契约与协作规程见 `开发任务包/`。 + +| 批次 | 任务 → WBS 映射 | 单测增量 | 集成增量 | +| --- | --- | --- | --- | +| 第一批(5 任务) | T1 四档熔断→`T-P1-25b`;T2 任务奖励→`T-P1-26` 收尾;T3 埋点→`T-P1-10`;T4 刷新令牌→`T-P1-13`;T5 直传预签名→`T-P1-27` | +69 | +12 | +| 第二批(6 任务) | T6 实体图谱→`T-P2-01/05`;T7 规则引擎→`T-P2-02`;T8 混合检索→`T-P2-03/04/06`;T9 视觉 SPIKE→`T-P0-10/12/16`(结论已回写 §2);T10 华为登录→`T-P1-12`;T11 短信登录→`T-P1-12b/12c` | +235 | +22 | +| **累计** | 单测 169→**473**,集成 3→**38** | | | + +**待联调清单**(代码就绪、等外部凭证):华为登录(AGC 配置)、短信真实发送(签名审核)、COS 直传(四桶创建)——对应《前置开通清单》。 + +**遗留提醒**:8-17 峰谷新价生效后重算 `LLM_DAILY_BUDGET_CENTS` 与四档阈值;RTMPose 真实照片重验;BiRefNet CPU 3.1s/张需异步化接入任务中心。 + +--- + +**文档版本**:v1.1 **创建日期**:2026-08-13 +**最后更新**:2026-08-14 **状态**:待评审 **关联文档**:《开发任务拆解》(任务定义与依赖)、《需求规格说明》(验收指标唯一事实源)、 《技术架构文档》(token 预算与成本模型)、《技术选型与调研报告》(`TD-00`~`TD-33`) diff --git a/docs/前置开通清单-云资源与资质.md b/docs/前置开通清单-云资源与资质.md new file mode 100644 index 0000000..52117a5 --- /dev/null +++ b/docs/前置开通清单-云资源与资质.md @@ -0,0 +1,90 @@ +# 前置开通清单:云资源与资质(本机开发期间并行办理) + +> 创建日期:2026-08-14 | 性质:**行动清单**(照着办,办完打勾) +> +> **为什么有这份清单**:开发策略是"本机开发完成、腾讯云部署放最后"。但以下三项**有审核周期或属于边开发边对接的配置**,必须现在启动,否则会在 Sprint 0 出口(双登录通道)和 Sprint 4(真机联调)反向卡住开发进度。 +> 它们只是"开通 + 拿到凭证",**不涉及任何应用部署**,成本接近零。 +> +> **不在本清单内的**(避免混淆范围): +> - CVM/TKE、CLB、域名 + HTTPS、CDN 全量、CLS、压测 —— 全部归 Phase 5 部署 +> - 真实环境红线验证(临时桶 2 小时实测、AI 标识元数据经 CDN 存活、天御误杀率)—— 归 Sprint 1/5 的专项验证任务 +> - 本机开发的一切(PG/Redis/GPU/LLM 调用)—— 不需要任何云资源 + +--- + +## 1. 腾讯云账号 + 实名 + 短信签名/模板审核 🔴 最先办(有审核周期) + +**对应**:`T-P1-12b/12c`、`TD-32`(短信是 V1 登录兜底通道,且是被刷时唯一直接现金损失项) + +| # | 事项 | 要点 | 完成 | +| --- | --- | --- | :---: | +| 1.1 | 注册腾讯云账号 + 实名认证 | 主体资质决定短信签名审核难度,个人主体限制多;按实际情况选,若可走企业主体优先 | ⬜ | +| 1.2 | 购买国内短信套餐包 | 最小档即可(验证码短信一条几分钱),目的是拿到发送资格 | ⬜ | +| 1.3 | **申请短信签名** | 签名内容与主体资质材料一致(App 名称/公司名等,以腾讯云审核要求为准);**审核有周期,被驳回按驳回原因补材料重提** | ⬜ | +| 1.4 | 申请验证码模板 | 登录验证码类型;模板变量只留 `{1}`(验证码),不含营销语 | ⬜ | +| 1.5 | 创建子账号 + CAM 最小授权 | 只授 SMS 与 COS 相关权限;**不用主账号密钥** | ⬜ | +| 1.6 | SecretId/SecretKey 落位 | **只写后端仓库 `.env`**(已 gitignore + CI 双密钥扫描);文档仓库 `reference/` 禁止出现密钥 | ⬜ | + +**验收**:用测试手机号在腾讯云控制台试发一条模板短信,能收到。 + +**开发对接参数**(实现 `T-P1-12c` 防刷时按此配置,源:`FR-ACC-14`):同号 60s 内 1 条、1 小时 ≤5、24h ≤10;同 IP 24h ≤20;同设备 24h ≤10。 + +--- + +## 2. COS 四桶创建 🔴 一并办(空桶成本 ≈ 0) + +**对应**:`T-P1-04`(开通部分)、`TD-03`、`T-P1-27`(直传联调时再对细节) + +| # | 事项 | 要点 | 完成 | +| --- | --- | --- | :---: | +| 2.1 | 确定地域 | **四桶 + 未来的 CVM/CDN 同地域**(内网直连、免流量费);定了就不要换 | ⬜ | +| 2.2 | 创建 `meida-temp` | 临时桶:**生命周期规则 = 2 小时清理**(`CR-02` 人脸原图不落盘的兜底,上线红线);私有读写 | ⬜ | +| 2.3 | 创建 `meida-user` | 用户内容桶:私有读写(结果图走预签名 URL,`NFR-SEC-02`) | ⬜ | +| 2.4 | 创建 `meida-kb` | 知识素材桶(白底图等) | ⬜ | +| 2.5 | 创建 `meida-report` | 报告产物桶(长图等) | ⬜ | +| 2.6 | 顺手做一次 2 小时实测 | 往 `meida-temp` 传一个文件,2 小时后确认被自动清理 —— **这正是 Sprint 0 出口条件之一**,今天办今天验,别留到上线前 | ⬜ | +| 2.7 | 记录四桶名称与地域 | 归档到团队可见处;后端 `.env` 后续按此填 | ⬜ | + +**暂不需要做**:CORS 跨域、直传预签名策略 —— 等 `T-P1-27` 开发时边联调边配。 + +--- + +## 3. 华为开发者后台(应用 + 证书指纹双套配置)🟡 本周内 + +**对应**:`T-P1-12/13`(华为账号登录)、用户系统 §2.1;`T-P0-17`(端侧骨架)的前置 + +| # | 事项 | 要点 | 完成 | +| --- | --- | --- | :---: | +| 3.1 | AGC 创建应用 | 包名定为最终包名(上架后难改);拿到 App ID | ⬜ | +| 3.2 | 申请/确认调试证书与发布证书 | **两套都要** | ⬜ | +| 3.3 | **证书指纹双套录入** | 调试证书与发布证书的 SHA256 指纹**都要**配到 AGC;**漏配发布指纹的表现是"调试包登录正常、发布包登录失败"**,这种错最晚才暴露 | ⬜ | +| 3.4 | 开通 Account Kit | 华为账号授权登录(UnionID/OpenID 映射 `id_user_identity`) | ⬜ | +| 3.5 | Push Kit 可后置 | 通知授权默认关闭(`TD-17`),Sprint 0 不阻塞,联调期再开 | ⬜ | + +**验收**:用调试证书签名的 App 能拉起华为授权登录页(发布通道的验证归 Phase 5)。 + +--- + +## 4. 办理顺序与完成标志 + +``` +今天 :1.1 → 1.2 → 1.3(短信签名审核周期最长,最先提交) + :2.1 → 2.2~2.5(半小时的事)→ 2.6 挂着等 2 小时 +本周内 :3.1 ~ 3.4(AGC 配置) +签名审核通过后 :1.4 模板 → 1.5/1.6 密钥落位 → 控制台试发验收 +``` + +**整体完成标志**(三条全绿即本清单关闭): +- [ ] 测试手机号收到验证码短信 +- [ ] 四桶就位且 `meida-temp` 实测 2 小时清理生效 +- [ ] 调试证书 App 拉起华为授权页 + +> 办理过程中的凭证(App ID、SdkId、签名 ID、桶名/地域等非机密信息)建议登记到本文末尾的"办理记录"表;**密钥类一律只进后端 `.env`**。 + +--- + +## 附:办理记录(办理时填写) + +| 日期 | 事项 | 结果 / 备注 | +| --- | --- | --- | +| | | | diff --git a/docs/开发任务包/README.md b/docs/开发任务包/README.md new file mode 100644 index 0000000..a38c686 --- /dev/null +++ b/docs/开发任务包/README.md @@ -0,0 +1,79 @@ +# 多 Agent 并行开发·协作规程 + +> 版本:v1.2 | 2026-08-14 | 前两批(T1~T5、T6~T11)已全部合并;本版适用第三批 **T12~T14** +> **每个 agent 开工前必须先读本文件,再读自己的任务文档。** + +--- + +## 1. 本轮架构决策(为什么这样切) + +六个任务按**文件所有权**切分:每个文件只属于一个任务。协调者(主会话)已提前在 main 分支做了一个「协调提交」,把**所有共享文件的改动一次性做完**(kb 七表迁移、ORM 模型、配置项、错误码、路由注册、依赖、gitignore)。因此: + +- **你不需要碰任何共享文件**。如果你的任务文档说某配置/错误码/表已就绪,它就真的已就绪——直接用。 +- 禁改清单见你的任务文档。**改了禁改文件 = 合并时被整体丢弃**。 + +## 2. 环境操作手册(Windows 宿主 + WSL 仓库) + +你的工作目录是 WSL 里的一个 **git worktree**(不是主仓库): + +| 任务 | worktree 路径(WSL) | 分支 | +| --- | --- | --- | +| T12 | `/home/xsl/meida_backend-t12` | `task/t12-admin-console` | +| T13 | `/home/xsl/meida_backend-t13` | `task/t13-golden-ci` | +| T14 | `/home/xsl/meida_backend-t14` | `task/t14-retrieval-cache` | + +- **读写文件**:用 Read / Write / Edit 工具,路径写成 UNC 形式: + `\\wsl.localhost\Ubuntu-22.04\home\xsl\meida_backend-t6\packages\...` + (Edit 前必须先 Read 目标文件。) +- **跑命令**(测试/lint/git): + ``` + MSYS_NO_PATHCONV=1 wsl.exe -d Ubuntu-22.04 -e bash -c "cd /home/xsl/meida_backend-t6 && .venv/bin/pytest -q" + ``` + 注意:venv 在**你自己的 worktree 里**(`.venv/bin/...`),不要用主仓库的(本批已含全部所需依赖,用 `--extra dev` 同步)。 +- PG(55432)/Redis(56379) 容器已在跑,`.env` 已复制到你的 worktree。 + +## 3. 提交前必须全绿(在你的 worktree 里) + +```bash +.venv/bin/ruff check . && .venv/bin/ruff format --check . +.venv/bin/mypy <你的任务文档里指定的包列表> +.venv/bin/lint-imports +.venv/bin/pytest -q # 单测(不连库) +# 若你的任务允许集成测试: +MEIDA_RUN_INTEGRATION=1 .venv/bin/pytest -m integration -q --no-cov +``` + +全绿后提交(**只提交一次,不要多次碎提交**): + +```bash +git add -A +git commit -m "<你的任务文档里给定的提交信息>" +``` + +**禁止 `git push`。禁止改动 `.git/`、禁止切分支、禁止 rebase/merge。** + +## 4. 硬性纪律(违反任何一条 = 返工) + +1. **禁改文件**以任务文档为准。通用禁改:`pyproject.toml`、`uv.lock`、`migrations/**`、`packages/shared/models.py`、`packages/shared/config.py`、`packages/shared/errors.py`、`.env.example`、`apps/api/main.py`、`apps/api/deps.py`、`.github/**`、`Makefile`、`scripts/smoke.sh`、`deploy/**`、`packages/domain/**`、`packages/llm/**`、`packages/billing/**`、`packages/agent/**`、`packages/task/**`、`packages/analytics/**`、`packages/storage/**`(除非任务文档明确允许某文件)。 +2. **不加任何新依赖**(不 `uv add`,不手写 import 第三方新包)。需要的库协调者已装(含 `spike` extra)。 +3. **不连外网**:单测禁止真实 HTTP。mock 方式参考 `tests/unit/test_deepseek_provider.py`(respx)与 `tests/integration/test_vertical_slice.py`(FakeProvider)。**唯一例外:T9 的模型下载脚本**(见其任务文档)。 +4. **代码风格对齐现状**:中文 docstring/注释(解释"为什么")、`from __future__ import annotations`、frozen+slots dataclass、结构化日志 `get_logger(__name__)`、错误一律走 `AppError(ErrorCode.X)`、响应一律 `ok(...)` 信封。 +5. **mypy strict** 适用于 `packages/domain`、`packages/llm`(如果你改这两处,标注必须完整)。其余包按现有宽松档,但新代码仍应写满类型标注。 +6. **ruff line-length 100**;格式化用 `ruff format`。 +7. 测试文件放 `tests/unit/test_<主题>.py`;集成测试放 `tests/integration/test_<主题>.py` **新文件**。**本批任何人不得编辑已有测试文件**(上批例外已用尽)。 +8. **不修改现有测试的断言语义**。 +9. 遇到与任务文档冲突的现实情况(比如文档说的符号不存在):**不要自行扩大范围**,在提交信息里写明偏差与原因。 + +## 5. 完成定义(DoD) + +1. 任务文档「验收标准」全部满足; +2. §3 的检查全绿; +3. 新增代码有对应测试(纯逻辑必须有单测;涉及 DB 的路径按任务文档要求补集成测试); +4. 提交信息按任务文档给定格式,正文用中文说明实现了什么、测了什么、有何偏差。 + +## 6. 冲突隔离原理(知其所以然) + +- 本批 3 个分支最终由协调者**顺序合并**,每合一个跑一轮全量门禁(含 `make golden`——CI 防线 5c); +- 你分支里的每一个文件改动,理论上不应与其它任务重叠——这是"并行不踩坑"的根本保证; +- 如果你觉得"必须"改一个禁改文件才能完成任务:停下来,在提交信息里说明需求,由协调者在合并阶段统一处理。 +- 前两批(11 任务)用同样的方案跑完:零合并冲突,单测 169→473。照纪律执行即可。 diff --git a/docs/开发任务包/T1-四档熔断降级.md b/docs/开发任务包/T1-四档熔断降级.md new file mode 100644 index 0000000..47bb566 --- /dev/null +++ b/docs/开发任务包/T1-四档熔断降级.md @@ -0,0 +1,106 @@ +# T1 · 全局日成本四档熔断降级 + +> 对应任务:`T-P1-25b` | 需求:`NFR-COST-02`、架构 §13.3 | 分支:`task/t1-tiered-budget` | worktree:`/home/xsl/meida_backend-t1` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +把现有"单一日预算阈值"(`DailyBudgetGuard`:到 100% 拒绝非 critical 场景)升级为架构 §13.3 的**四档分级降级**,并补上 T-P1-25b 的验收要求:**被熔断拒绝的请求配额净扣减 = 0(须有集成断言)**。 + +四档(阈值可配置,百分比相对 `LLM_DAILY_BUDGET_CENTS`): + +| 档位 | 阈值 | 动作 | +| --- | --- | --- | +| tier 1 `warn` | ≥60% | 只告警(日志 + 指标),不影响任何请求 | +| tier 2 `degrade_secondary` | ≥80% | 拒绝 blocked 列表中的场景(默认 `hd_export`、`report_regen`) | +| tier 3 `degrade_generation` | ≥90% | 追加拒绝(默认 `hairstyle`、`tryon`) | +| tier 4 `block_all` | ≥100% | 拒绝所有非 critical 场景,**但 allow 名单内的场景放行**(默认 `agent_chat`——架构原文"仅保留在途任务与纯文本对话") | + +规则: +- **critical 场景永远豁免**(现有 `SceneConfig.critical`,如 `INTENT_CLASSIFY`),但照常计入成本; +- **手动干预**:Redis 键可强制指定当前档位(运营解除/加严),优先级高于成本水位; +- 在途任务放行 = 熔断只拦网关**新请求**(现有行为已满足,不要在 worker 侧加检查)。 + +## 2. 已就绪的契约(协调提交已提供,直接用,禁止改动) + +### 配置(`packages/shared/config.py` 已加字段 + `.env.example` 已示例) + +```python +llm_budget_tiers: list[dict[str, Any]] # env: LLM_BUDGET_TIERS_JSON,**JSON 是一个列表**(无外层 key) +``` +默认 JSON(元素结构:`{"threshold": 0.6, "name": "warn", "blocked": []}`;`blocked` 为字符串列表,`"*"` 表示全部;`allow` 仅 `block_all` 档有意义): +```json +{"tiers":[ + {"threshold":0.6,"name":"warn","blocked":[]}, + {"threshold":0.8,"name":"degrade_secondary","blocked":["hd_export","report_regen"]}, + {"threshold":0.9,"name":"degrade_generation","blocked":["hairstyle","tryon"]}, + {"threshold":1.0,"name":"block_all","blocked":["*"],"allow":["agent_chat"]} +]} +``` + +### 错误码(`packages/shared/errors.py` 已加) + +- `LLM_SCENE_DEGRADED = 60006`,meta:http 503,message「该功能今日暂时关闭」,hint「AI 服务今日已达服务上限,明天恢复」。 + +### 现有代码事实(先读这些文件再动手) + +- `packages/llm/budget.py`:`DailyBudgetGuard(counter, *, daily_budget_cents, enabled, key_prefix, tz_offset_hours, clock)`;方法 `current_key()`(`llm:cost:daily:{UTC+8日期}`)、`spent_today()`、`ensure_within_budget(scene, critical)`、`add_cost(cost_cents)`;`CostCounter` Protocol(`InMemoryCostCounter`/`RedisCostCounter`)。 +- `packages/llm/gateway.py:110`:`await self._budget.ensure_within_budget(scene=scene.value, critical=config.critical)` —— 你要替换的就是这一处调用点。 +- `packages/llm/errors.py`:已有 `DailyBudgetExceededError`。 +- `apps/api/routers/agent.py:38`:已 catch `DailyBudgetExceededError → LLM_DAILY_BUDGET_EXCEEDED`。 +- `packages/agent/service.py`:reserve → commit → gateway 异常时**先 refund 再 raise**(配额净扣减=0 的机制已存在,你只需用集成测试证明它)。 + +## 3. 实现要点 + +全部改动限定在:`packages/llm/budget.py`、`packages/llm/errors.py`、`packages/llm/gateway.py`(仅调用点与构造参数)、`apps/api/routers/agent.py`(异常映射)、`apps/api/deps.py` **不许改**——预算档配置从 `Settings.llm_budget_tiers` 读取的地方在哪?`build_llm_gateway()` 已在 deps.py(禁改)。**因此**:`DailyBudgetGuard.__init__` 增加关键字参数 `tiers: list[dict[str, Any]] | None = None`(默认 None = 内置默认四档),deps.py 已在协调提交里改为传入 `tiers=list(settings.llm_budget_tiers)`。你只管实现。 + +1. **budget.py**: + - 新增 pydantic `BaseModel`:`BudgetTier(threshold: float, name: str, blocked: list[str], allow: list[str] = [])`(llm 层允许 pydantic); + - `DailyBudgetGuard` 扩展:`current_tier() -> int`(0=未触发,1~4 如上)、`tier_name()`、`blocked_scenes() -> frozenset[str] | "*"`; + - 新方法 `ensure_scene_allowed(*, scene: str, critical: bool = False) -> None`: + - `critical` → 直接返回; + - 手动 override(Redis `llm:budget:override`,值为 "1"~"4",**读不到/值非法则忽略**)优先于水位; + - 按档位判定:tier2/3 且 scene ∈ blocked → 抛 `SceneDegradedError(scene, tier_name)`(新异常,加到 `errors.py`);tier4 且 `blocked=="*"` 且 scene ∉ allow → 抛 `DailyBudgetExceededError`(**保持 100% 档的既有语义与 60003 错误码不变**——现有测试依赖它); + - 保留 `ensure_within_budget()` 兼容(内部转调 `ensure_scene_allowed`),或直接替换调用点后删除——二选一,保持 `gateway.py` 只调一个方法; + - 新增 `set_override(tier: int)` / `clear_override()`(写 Redis,TTL 到当日 UTC+8 24 点),供运营/演练用; + - **指标**:`prometheus_client` 的 `Gauge("llm_budget_tier", ...)` 与 `Counter("llm_budget_blocked_total", ..., ["scene"])`(参考 `packages/llm/recording.py` 里 Prometheus 的用法);档位上升时 `logger.warning("budget_tier_raised", tier=..., spent=..., budget=...)`(仅上升沿打一次,避免每请求刷日志——实例内记 last_tier 即可)。 +2. **gateway.py**:把 `ensure_within_budget` 调用换成 `ensure_scene_allowed(scene=scene.value, critical=config.critical)`;构造函数若需传 tiers,加默认参数(deps 已传)。 +3. **agent.py**:新增 `except SceneDegradedError as exc: raise AppError(ErrorCode.LLM_SCENE_DEGRADED) from exc`。 +4. **mypy strict**:budget.py 在 `packages/llm` 内,全量标注。 + +## 4. 测试要求 + +单测(`tests/unit/test_tiered_budget.py` 新文件;现有 `test_daily_budget.py` 保持通过,允许因 API 扩展做**增量**修改): +- 四档边界:59.9%/60%/79.9%/80%/89.9%/90%/99.9%/100% 时 `current_tier()` 正确; +- tier2 拒绝 `hd_export` 放行 `agent_chat`;tier4 拒绝非 allow 非 critical;critical 全档豁免; +- override 强制 tier4 / 清除后回落; +- `allow=["agent_chat"]` 在 tier4 生效; +- 计数用 `InMemoryCostCounter` + 注入 clock(参考现有测试)。 + +集成测试(**编辑 `tests/integration/test_vertical_slice.py`**,你是唯一允许编辑它的任务): +- 用 `monkeypatch.setenv` 把 `LLM_DAILY_BUDGET_CENTS` 设为极小值(如 "1")、`LLM_BUDGET_TIERS_JSON` 把 `block_all.blocked` 设为 `["agent_chat"]`(即在本测试里连纯文本对话也拦),`get_settings.cache_clear()` 后 `create_app`; +- 先用 Redis 客户端把当日成本键 `INCRBYFLOAT` 到预算以上(键格式照 `DailyBudgetGuard.current_key()` 计算); +- `POST /v1/agent/chat`(FakeProvider,参考现有 fixture)→ 断言:响应 code 为 60003(或 60006,取决于你把 agent_chat 归到哪个异常——按上面设计应为 60003)、**且 `bl_ledger` 中该用户当日 `consume` 流水净和为 0(reserved 后 refund)**——这正是 T-P1-25b 的验收"被熔断拒绝的请求配额净扣减 = 0,须有集成断言覆盖 reserve 之后才触发熔断这条路径"; +- 测试结束 `get_settings.cache_clear()` 恢复,避免污染其它用例。 + +## 5. 禁改清单(通用禁改之外) + +`packages/agent/**`、`packages/billing/**`、`packages/identity/**`、`packages/analytics/**`、`packages/storage/**`、`apps/api/routers/{auth,quota,tasks,events,uploads}.py`、`apps/api/schemas.py`。 + +## 6. 验收标准 + +1. 四档降级可由配置驱动,`warn` 档无用户可感知变化; +2. 手动 override 可加严/解除(redis-cli 可操作,写清键名与取值到 budget.py docstring); +3. 集成断言证明"reserve 之后熔断 → refund → 净扣减 0"; +4. §3 检查全绿(mypy 范围:`packages/llm` strict + `apps packages/shared packages/identity packages/billing packages/task packages/agent packages/compliance packages/analytics packages/storage`)。 + +## 7. 提交信息 + +``` +feat(llm): four-tier daily budget degradation with manual override + +四档熔断(warn/secondary/generation/block_all)+ critical 豁免 + Redis 手动 override ++ Prometheus 指标 + 集成断言:被拒请求配额净扣减为 0(T-P1-25b)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T10-华为登录代码先行.md b/docs/开发任务包/T10-华为登录代码先行.md new file mode 100644 index 0000000..4d8904e --- /dev/null +++ b/docs/开发任务包/T10-华为登录代码先行.md @@ -0,0 +1,86 @@ +# T10 · 登录·华为账号(代码先行,联调后置) + +> 对应任务:`T-P1-12` | 需求:`FR-ACC-01`(登录成功率 ≥99%、UnionID 映射唯一、首次自动建用户)| 分支:`task/t10-auth-huawei` | worktree:`/home/xsl/meida_backend-t10` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +实现 `POST /auth/huawei`:客户端传华为 Account Kit 签发的 **id_token**,服务端按 OIDC 标准做 **JWKS 验签 + claims 校验**,映射到内部用户并签发双令牌(access + refresh,复用 T4 已交付的 `issue_session`)。 + +**本任务性质是"代码先行"**:华为 AGC 后台尚未配置(见《前置开通清单》§3),因此真实联调后置;全部逻辑用**自造 RSA 密钥对 + 本地 JWKS** 做单测/集成,凭证到位后只改 `.env` 即可上线。 + +## 2. 已就绪的契约(协调提交已提供) + +### 配置(config.py 已加) + +```python +huawei_jwks_url: str = "https://oauth-login.cloud.huawei.com/oauth2/v3/certs" # HUAWEI_JWKS_URL +huawei_audience: str = "" # HUAWEI_AUDIENCE,= AGC App ID,联调时填 +huawei_issuer: str = "https://accounts.huawei.com" # HUAWEI_ISSUER(以 Account Kit 文档为准,可配) +huawei_jwks_cache_seconds: int = 86400 +``` + +### 可复用的现有设施(先读再写) + +- `packages/identity/service.py`:`get_or_create_by_external_identity(provider, union_id, open_id, register_source)`(T4 已有);`issue_session(user_id) -> SessionIssued`(T4 交付,含 refresh 轮换结构)。 +- `packages/shared/security.py`:`create_access_token(...)`(HS256 access)。 +- `packages/identity/schemas.py`:`TokenPairResponse`(T4 交付)——**直接复用,不要在本任务重复定义**。 +- `pyjwt[crypto]` 已装(含 cryptography,RS256 验签可用)。 + +### 端点归属 + +`apps/api/routers/auth.py` 归你(T4 已合并完,本批无他人碰它)。`apps/api/routers/auth_sms.py` 归 T11,**禁碰**。 + +## 3. 实现要点 + +允许改动:`packages/identity/huawei.py`(新建,核心逻辑全在此)、`apps/api/routers/auth.py`(追加端点)、测试文件。**不改 identity/service.py 与 identity/schemas.py**(避免与 T11 冲突——你的模型定义在 huawei.py 内)。 + +1. **`packages/identity/huawei.py`**: + - `HuaweiIdTokenVerifier`:构造参数 `(jwks_url, audience, issuer, cache_seconds, *, http_fetch=None, now=None)`——`http_fetch` 是 `Callable[[str], Awaitable[str]]`(单测注入本地 JWKS,**不连外网**);JWKS 内存缓存(TTL 按配置,kid 变化时强制刷新一次并重试验签); + - `verify(id_token) -> HuaweiClaims`(frozen dataclass:`union_id/open_id/exp/iat`):RS256 验签(`jwt.decode` + `PyJWK`/手工 kid 匹配)、`aud`(等于配置 audience,空配置时**拒绝**而不是跳过——联调前后都必须严格)、`iss`、`exp/iat`、`union_id` 缺失拒绝(华为子账号场景无 union_id 时降级用 open_id 的策略**不做**,注释说明留给联调期决定); + - 错误映射:验签失败/claims 不符 → `AppError(TOKEN_INVALID)`(40102,已有);JWKS 拉取失败 → `AppError(UPSTREAM_ERROR)`(60000,已有)——**不需要新错误码**。 +2. **路由**(auth.py 追加): + ```python + @router.post("/huawei", summary="华为账号授权登录(FR-ACC-01)") + async def huawei_login(payload: HuaweiLoginRequest, ...) -> ...: + claims = await verifier.verify(payload.id_token) + user = await identity.get_or_create_by_external_identity( + provider="huawei", union_id=claims.union_id, open_id=claims.open_id, + register_source="huawei") + issued = await identity.issue_session(user.user_id) + access = create_access_token(...) + return ok(TokenPairResponse(...)) # 复用 T4 模型 + ``` + - verifier 用依赖注入(路由模块内建一个 `get_huawei_verifier` 的 FastAPI Depends,从 settings 构造并缓存到 `app.state` 或模块级 lru——**不改 deps.py**,在 auth.py 内实现); + - `HuaweiLoginRequest(id_token: str)` 定义在 huawei.py。 +3. **模块 docstring** 写明:真实联调前置 = AGC 应用创建 + 证书指纹双套 + `HUAWEI_AUDIENCE` 填 App ID(引用《前置开通清单》§3)。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_huawei_verifier.py`:测试内用 `cryptography` 生成 RSA 密钥对、自签 id_token + 本地 JWKS dict: + 1. 合法 token → claims 解析正确; + 2. 错误签名(换密钥)→ 40102;过期 → 40101(复用 TOKEN_EXPIRED);aud 不符 → 40102;audience 未配置 → 40102; + 3. JWKS 拉取失败 → 60000;kid 轮换 → 缓存刷新后验签成功。 +- 集成 `tests/integration/test_auth_huawei.py`(新文件):monkeypatch 注入 FakeFetcher(本地 JWKS)→ `POST /v1/auth/huawei` → 200 双令牌;再次登录同 union_id → 同一 user_id(映射唯一);新 access 可调 `GET /v1/quota`。 + +## 5. 禁改清单 + +通用禁改之外:`packages/identity/{service,repo,schemas,tokens}.py`、`apps/api/routers/auth_sms.py`、`packages/{kb,rules,retrieval}/**`。 + +## 6. 验收标准(FR-ACC-01) + +1. 验签逻辑完整(签名/aud/iss/exp/iat 全查),audience 未配置时拒绝启动校验路径; +2. UnionID→内部 user_id 映射唯一,首次登录自动建用户(集成断言同 ID 复登); +3. 全链路不连外网(FakeFetcher 注入); +4. 全部检查绿(mypy 范围同 T6 列表)。 + +## 7. 提交信息 + +``` +feat(identity): Huawei Account Kit login via OIDC JWKS verification + +POST /auth/huawei:JWKS 验签(kid 缓存/轮换)+ claims 严格校验 + +UnionID 映射 + T4 双令牌签发(FR-ACC-01;真实联调待 AGC 配置)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T11-短信登录代码先行.md b/docs/开发任务包/T11-短信登录代码先行.md new file mode 100644 index 0000000..725745b --- /dev/null +++ b/docs/开发任务包/T11-短信登录代码先行.md @@ -0,0 +1,101 @@ +# T11 · 登录·手机号验证码(代码先行,联调后置) + +> 对应任务:`T-P1-12b/12c` | 需求:`FR-ACC-04`(6 位、5 分钟、一次性、失败 5 次作废)、`FR-ACC-14`(三重频控防刷)、`TD-32` | 分支:`task/t11-auth-sms` | worktree:`/home/xsl/meida_backend-t11` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +手机号验证码登录全链路(发送 → 校验 → 建户 → 双令牌)+ **短信轰炸防护**。腾讯云短信签名/模板还在审核(《前置开通清单》§1),因此真实发送后置:`SmsSender` 走 Protocol 隔离,默认 `fake` 实现(验证码打日志,dev 可用),腾讯实现写好待凭证。 + +**FR-ACC-04 纪律:与华为通道不得互相依赖**——本任务不 import 华为相关任何东西,登录成功路径只依赖 identity 域已有设施。 + +## 2. 已就绪的契约(协调提交已提供) + +### 配置(config.py 已加,.env.example 已示例) + +```python +sms_provider: str = "fake" # SMS_PROVIDER: fake | tencent +tencent_sms_secret_id/key/sdk_app_id/sign_name/template_id: str = "" +sms_code_ttl_seconds: int = 300 # 5 分钟(FR-ACC-04) +sms_code_length: int = 6 +sms_verify_max_attempts: int = 5 # 校验失败 5 次作废 +sms_resend_cooldown_seconds: int = 60 # 同号 60s 内 1 条 +sms_hourly_limit: int = 5 # 同号 1h ≤5 +sms_daily_limit: int = 10 # 同号 24h ≤10 +sms_ip_daily_limit: int = 20 # 同 IP 24h ≤20 +sms_device_daily_limit: int = 10 # 同设备 24h ≤10 +``` + +### 错误码(errors.py 已加,含 meta) + +- `SMS_CODE_INVALID = 40110`(401「验证码错误」hint「请检查后重试」) +- `SMS_CODE_EXPIRED = 40111`(401「验证码已过期」hint「请重新获取」) +- `SMS_SEND_TOO_FREQUENT = 42910`(429「发送太频繁了」hint「请稍后再试」) +- `SMS_DAILY_LIMIT = 42911`(429「今日发送次数已达上限」hint「明天再试」) +- `SMS_PROVIDER_FAILED = 61000`(502「短信服务暂时不可用」) + +### 依赖与设施 + +- `tencentcloud-sdk-python-sms` 已装(协调提交)。 +- 可复用(先读再写):`packages/identity/service.py` 的 `get_or_create_by_external_identity`(provider 用 `"phone"`)与 `issue_session`(T4);`packages/identity/schemas.py` 的 `TokenPairResponse`——**复用,不重复定义、不修改该文件**。 +- 路由骨架:`apps/api/routers/auth_sms.py` 空路由(prefix `/auth`)已注册 main.py——归你填充。`apps/api/routers/auth.py` 归 T10,**禁碰**。 + +### 号码存储决策(写进代码注释) + +V1 简化:`id_user_identity` 现有列无 `phone_enc`,用 `union_id = "phone:" + sha256(规范化号码)[:16]`(E.164 规范化:去 +86/前导零/空白后哈希)。**Redis 与日志中一律不存明文号码**——键用 `sha256(phone)` 前 16 位,日志打码 `138****1234`(FR-ACC-14 掩码要求)。`phone_enc` 列的补齐属后续迁移任务,不在本批。 + +## 3. 实现要点 + +允许改动:`packages/identity/sms_login.py`(新建,全部核心逻辑)、`apps/api/routers/auth_sms.py`、测试文件。**不改 identity/{service,repo,schemas,tokens,huawei}.py。** + +1. **`packages/identity/sms_login.py`**: + - `SmsSender` Protocol:`async def send_code(phone_masked_key: str, code: str) -> None`(收掩码键不收明文——防实现层记日志泄漏); + - `FakeSmsSender`:`logger.info("sms_code_issued", masked=..., code=...)`(dev 专用,注释写明生产必须切 tencent); + - `TencentSmsSender`:包官方 SDK(`SmsClient` + `SendSmsRequest`,参数从 settings 读);构造失败/响应非 ok → `AppError(SMS_PROVIDER_FAILED)`;**本批不要求真实联调,但代码路径要完整可读**; + - `SmsAuthService(redis, sender, *, settings)`: + - `request_code(phone, ip, device_id)`:号码格式校验(大陆手机号正则 `^1[3-9]\d{9}$`,写死并注释 V1 只做大陆)→ **先频控后发送**:同号 cooldown/hourly/daily、IP daily、设备 daily 全过才生成 6 位随机码(`secrets.randbelow`,非 random)→ 存 Redis `sms:code:{phone_hash}`(值 = sha256(code),**不存明文**,TTL 300s)+ 计数器自增 → sender 发送; + - 频控计数键按自然窗口(cooldown 用 TTL;hourly/daily 用 UTC+8 小时/日期桶,对齐 billing 的时区纪律); + - `verify(phone, code, ip, device_id)`:取码哈希比对(**恒定时间比较** `hmac.compare_digest`)→ 错误则 attempts+1(`sms:attempts:{phone_hash}`,达 5 删码并返回 40110)→ 成功删码(一次性)→ 由调用方建户; + - 全部日志打码:`masked_phone(phone) -> "138****1234"`。 +2. **路由**(auth_sms.py): + - `POST /auth/sms/send` body `{phone, device_id}`(ip 从 `Request.client.host` 取)→ 限流错误码透传; + - `POST /auth/sms/verify` body `{phone, code, device_id}` → 成功后 `get_or_create_by_external_identity(provider="phone", union_id=..., register_source="phone")` + `issue_session` → `ok(TokenPairResponse(...))`; + - 请求/响应模型定义在 sms_login.py 或新建 `packages/identity/sms_schemas.py`(二选一,不碰共享 schemas)。 +3. **刷量测试**(FR-ACC-14 验收原文"脚本化刷量测试证明拦截有效"):集成测试用循环模拟。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_sms_login.py`(fakeredis 或自写内存 dict 实现 Protocol 化的 redis 接口——参考 `InMemoryCostCounter` 的做法,不引新依赖): + 1. 号码正则(非法格式 400 类错误); + 2. 频控矩阵:cooldown 内二次 → 42910;1h 第 6 条 → 42910;24h 第 11 条 → 42911;IP 第 21 条 / 设备第 11 条 → 42911; + 3. 验证码:错误 4 次可再试、第 5 次后码作废(40110 且码已删)、过期(TTL 模拟)→ 40111、成功后码即失效(二次 verify 40111); + 4. 掩码函数;恒定时间比较(不 assert 时长,只 assert 行为)。 +- 集成 `tests/integration/test_auth_sms.py`(新文件,SMS_PROVIDER=fake,从响应前的日志拿不到码——**测试钩子**:fake sender 把码写进 Redis 测试键或直接用 fixture 替换 sender 为捕获型 fake,不要为测试开后门到生产代码): + 1. send → verify(fixture 捕获的码)→ 200 双令牌,新 access 可用; + 2. 同号再登录同 user_id(映射唯一); + 3. **刷量脚本化**:连发 11 次,第 11 次 42911;换 IP 维度再验一轮; + 4. 错误码 5 次作废路径。 + +## 5. 禁改清单 + +通用禁改之外:`packages/identity/{service,repo,schemas,tokens,huawei}.py`、`apps/api/routers/auth.py`、`packages/{kb,rules,retrieval}/**`。 + +## 6. 验收标准(FR-ACC-04 / FR-ACC-14) + +1. 验证码 6 位、5 分钟、一次性、失败 5 次作废——四点各有断言; +2. 三重频控全部生效且有脚本化刷量测试证明; +3. 号码与验证码在日志/Redis 均无明文(掩码 + 哈希); +4. 与华为通道零耦合(不 import huawei 模块); +5. 全部检查绿(mypy 范围同 T6 列表)。 + +## 7. 提交信息 + +``` +feat(identity): phone SMS login with triple rate limiting and anti-abuse + +SmsSender Protocol(fake/tencent)+ 验证码生命周期(哈希存储/一次性/ +5 次作废)+ 号码-IP-设备三重频控 + 掩码纪律(FR-ACC-04/14,TD-32; +真实发送待签名审核)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T12-极简运营后台.md b/docs/开发任务包/T12-极简运营后台.md new file mode 100644 index 0000000..fd573dd --- /dev/null +++ b/docs/开发任务包/T12-极简运营后台.md @@ -0,0 +1,72 @@ +# T12 · 极简运营后台 + +> 对应任务:`T-P2-09` 🔴 | 需求:`FR-ADM-01~05`、Sprint 2 出口「**顾问可独立完成内容维护、审核、发布与回滚,全程无工程介入**」| 分支:`task/t12-admin-console` | worktree:`/home/xsl/meida_backend-t12` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +`apps/admin/` 独立 FastAPI 应用(与 apps/api 平级,部署上分离):知识实体与规则的维护/审核/发布/回滚入口、素材登记与审核、知识缺口看板。**这是 M2 出口的成败标志**——顾问从此不找工程。 + +## 2. 已就绪的契约(协调提交 `9c18021` 已提供) + +### 配置与鉴权 + +- `settings.admin_token`(env `ADMIN_TOKEN`):**空字符串 = 全部 admin 端点 404**(等价未部署)。鉴权方式:请求头 `X-Admin-Token` 精确匹配(`hmac.compare_digest`);不匹配 → 40100(UNAUTHENTICATED)。 +- **apps 之间互不依赖**(import-linter):admin 不许 import `apps.api` 的任何东西——session 依赖、响应信封在 admin 包内自建(`ok`/`AppError` 来自 `packages.shared`,允许)。 + +### 可复用的服务层(先读再用,不改它们) + +- `packages/kb/service.py`:`KbService`(实体 CRUD/状态机/图谱/色板)+ **`KbAssetService`(新)**:`register_asset(payload) -> KbAsset`、`review_asset(asset_id, payload)`(`tryon_ready` 只能随 `approved` 打开,服务端已强制)、`list_assets(...)`;schemas:`AssetCreate/AssetReview/AssetOut` 已加。 +- `packages/rules/service.py`:规则 CRUD + 状态机 + `PublishedRules` 缓存 `invalidate()`。 +- `packages/retrieval/service.py`:模块级 `list_gaps(session, ...)`(hit_count 降序)+ **`invalidate_cache()`**(发布编排后调用——T14 会在背后实现真缓存,你只管调)。 +- 实体状态机:draft→reviewing→published→archived(`KbService` 内已有 transition 语义,读源码确认方法名;若服务层没有显式 `transition` 方法,通过 `update_entity` 的 status 字段走 `assert_transition` 校验——以代码为准)。 + +### 现状 + +`apps/admin/__init__.py`(占位注释 FR-ADM-01~05)——你填充;无其它骨架。 + +## 3. 实现要点 + +允许改动:`apps/admin/**`、测试文件。**不改任何 packages/**(服务层已齐;发现缺口写进偏差说明)。** + +1. **`apps/admin/app.py`**:`create_admin_app(settings) -> FastAPI`;`lifespan` 里建 DB engine 复用 `packages.shared.db`;挂 `RequestContextMiddleware`、统一异常处理(`packages.shared.handlers`);路由前缀 `/admin/api/v1`(与客户端 API 区分);docs 仅 local。 +2. **`apps/admin/deps.py`**:`admin_auth` 依赖(读 `X-Admin-Token`,空配置 → 全部路由在装配时不注册或请求时 404——选**装配时不注册**,404 语义更干净);session 依赖(仿 `apps/api/deps.session_dep`,不 import 它)。 +3. **路由**(每域一个 router 文件,全部经 `admin_auth`): + - **实体**:`GET/POST /kb/items`、`GET/PATCH /kb/items/{id}`、`POST /kb/items/{id}/transition`(body `{target: "reviewing"|"published"|"archived"}`,走服务层状态机;**发布成功后调用 `rules service invalidate()` + `retrieval invalidate_cache()`**——发布编排是后台的职责,这就是 T-P2-10 的"自然失效"入口)、`GET /kb/items/{id}/graph`; + - **规则**:`GET/POST /rules`、`PATCH /rules/{id}`、`POST /rules/{id}/transition`(发布同样触发双 invalidate); + - **素材**:`POST /assets`(登记)、`GET /assets`(entity/tryon_ready 过滤)、`POST /assets/{id}/review`(审核); + - **缺口看板**:`GET /kb/gaps`(limit/offset,直接返回 `list_gaps` 结果)。 +4. **模块 docstring**:V1 极简鉴权的边界(共享令牌、无审计日志字段、无角色)与升级路径(运营账号体系属 V2;操作留证当前靠 structlog request_id)。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_admin_app.py`:`ADMIN_TOKEN=""` 时所有路由 404(用 TestClient 打几个代表路径);令牌错误 → 40100;正确令牌 → 200(服务层用假 session 或只测装配层,不连库——若 router 直接依赖 DB session,把鉴权/404 逻辑测在装配层即可)。 +- 集成 `tests/integration/test_admin_console.py`(新文件,允许跑): + 1. 顾问全流程**不碰客户端 API**:建实体 → reviewing → published → 建规则 → 发布 → 断言规则可被 `PublishedRules` 缓存载入、检索新内容可见(简化断言:发布后 `invalidate` 被调用的副作用可观察——直接再查库即可); + 2. 非法跃迁 published→draft → 40900; + 3. 素材:登记(tryon_ready 恒 False)→ 审核 approved+tryon_ready=true → 列表过滤 `tryon_ready=true` 命中; + 4. 审核不通过时 tryon_ready=true → 400(服务层约束透传); + 5. 缺口:预插两行 kb_gap → `GET /kb/gaps` 按 hit_count 降序; + 6. 无令牌/错令牌 → 401。 + +## 5. 禁改清单 + +通用禁改之外:**全部 `packages/**`**、`apps/api/**`、所有已有测试文件。 + +## 6. 验收标准(T-P2-09) + +1. 实体/规则/素材/缺口四类操作全部有 HTTP 入口,鉴权生效(空 token 404、错 token 401); +2. 发布编排(publish → rules invalidate + retrieval invalidate_cache)落地; +3. 集成测试覆盖顾问全流程(无工程介入语义); +4. 全部检查绿(mypy 范围:`apps packages/shared ... packages/retrieval` 全列——照 T6 文档同口径,含你的 `apps/admin`)。 + +## 7. 提交信息 + +``` +feat(admin): minimal console for KB content operations + +apps/admin 独立应用:X-Admin-Token 鉴权(空=404)、实体/规则维护与状态机、 +发布编排(双 invalidate)、素材登记与审核、缺口看板(T-P2-09,FR-ADM-01~05)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T13-黄金集CI框架.md b/docs/开发任务包/T13-黄金集CI框架.md new file mode 100644 index 0000000..1f2b248 --- /dev/null +++ b/docs/开发任务包/T13-黄金集CI框架.md @@ -0,0 +1,71 @@ +# T13 · 黄金集回归框架接入 CI + +> 对应任务:`T-P2-11` 🔴 | 需求:`FR-KB-11`、`NFR-MAINT-04`(指标退化阻断合并)、架构 §17「黄金集是最重要的一道防线」| 分支:`task/t13-golden-ci` | worktree:`/home/xsl/meida_backend-t13` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +把黄金集从"散在单测里的固定样本"升级为**数据驱动的回归框架**:用例存 JSON、阈值存配置、CI 强制跑、**指标退化直接 fail**。V1 先落两个子集(规则黄金集、检索黄金集),Agent 问答与报告结构两个子集留占位(等 Sprint 3/4)。 + +## 2. 已就绪的契约(协调提交 `9c18021` 已提供) + +- **pytest marker `golden`** 已注册(`--strict-markers` 下可直接用);默认 addopts 排除 `golden`(与 integration 同待遇),`make golden` / CI 防线 5c 强制跑(env `MEIDA_RUN_INTEGRATION=1`)。 +- **规则黄金集现有资产**:`tests/unit/test_rules_golden.py` 有 8 条规则 + 11 组样本(硬禁忌压制、多规则叠加等)——**不许改它**;你的 JSON 用例集是独立交付(可参考其场景设计新写,不是搬运代码)。 +- **检索侧可复用**:`packages/retrieval/embedding.py` 的 `DeterministicEmbedder`(同文本同向量→黄金集可断言确定性结果)+ `packages/rules` 的 DSL/引擎。 + +### 黄金集规模口径(知识库文档 §7,唯一事实源) + +四子集共 **330 例**(检索 100 / Agent 问答 100 / 诊断 100 / 报告结构 30)。**本任务只搭框架 + 规则/检索各 10~15 例种子**,规模达标是内容轨 `T-P2-16` 的事——框架必须让"加用例=加 JSON 行"。 + +## 3. 实现要点 + +允许改动:`tests/golden/**`(新目录,归你全部)、**仅此一处**;不改 packages/apps/已有测试。 + +1. **目录结构**: + ``` + tests/golden/ + README.md # 用例格式说明 + 如何加用例 + 阈值怎么调 + thresholds.json # {"rules": {"pass_rate_min": 1.0}, "retrieval": {"pass_rate_min": 0.8, "first_hit_in_top3_min": 0.8}} + cases/ + rules_cases.json # 规则黄金集种子 + retrieval_cases.json # 检索黄金集种子 + runner.py # 用例加载 + 执行 + 指标计算 + 明细报告 + test_golden.py # pytest 入口(marker=golden,skipif 无 MEIDA_RUN_INTEGRATION) + ``` +2. **规则用例格式**(rules_cases.json):`{"name", "rules": [完整 Rule DSL 数组], "context": {...RuleContext}, "expect": {"must_recommend": [{"slot": "top", "attrs_include": {...}}], "must_avoid": [...], "hard_blocks_nonempty": bool, "forbidden_recommend": [...]}}`。判定:`evaluate()` 结果逐一核对 expect;一条不符 = 该用例 fail。 +3. **检索用例格式**(retrieval_cases.json):`{"name", "seed_entities": [{entity_type, name, attrs, status, chunks: [text]}], "query", "user_ctx": {...}, "expect": {"must_hit_names": [...], "must_miss_names": [...], "first_hit_in": "top1|top3"}`。执行:每个用例**先清自己 seed 的实体**(按 name 前缀 `golden:` 命名,跑前 DELETE 这些行,保证幂等重跑)→ DeterministicEmbedder 写 chunk → `hybrid_search` → 判定。`must_miss` 语义 = 硬过滤红线(禁忌/draft 实体绝不出现)。 +4. **指标与阻断**(runner.py): + - 每子集算 `pass_rate`(通过用例/总数);检索加算 `first_hit_in_top3_rate`; + - **规则子集阈值 = 1.0**(硬结论子集,一条不许退);检索 = 0.8 起步(阈值在 thresholds.json,顾问扩充用例后可调); + - fail 时输出**逐用例明细**(name + 期望 vs 实际的差异摘要)——CI 日志里要能直接看出哪条退化了; + - `runner.py` 是普通模块(不依赖 pytest),`test_golden.py` 只是薄壳调用——将来想在本地脚本或 admin 后台跑同一套,不用改框架。 +5. **README.md**:写明四子集规划(330 例口径)、当前覆盖(两子集 N 例)、加用例流程、"为什么规则阈值必须 1.0"(核心结论由规则产出,规则退化=产品根基退化)。 + +## 4. 测试要求(即框架本身) + +- `make golden`(或 `MEIDA_RUN_INTEGRATION=1 .venv/bin/pytest -m golden --no-cov`)全绿:两个种子子集通过率 100%(种子用例是手工校准过的,必须全对,否则框架自身有 bug); +- **阻断语义自证**:在本地临时改坏一条用例的 expect 验证会 fail 且输出明细(验证完改回来——提交里只有正确版本;这个自证过程写进提交信息,不写代码); +- 无 PG/Redis 时 `pytest -m golden` 全部 skip(不拖累本地 `make test`)。 + +## 5. 禁改清单 + +通用禁改之外:`packages/**`、`apps/**`、`tests/unit/**`、`tests/integration/**`、`pyproject.toml`(golden marker 已注册)、`Makefile`/CI(防线 5c 已挂)。**你的全部产出在 `tests/golden/**`。** + +## 6. 验收标准(T-P2-11) + +1. CI 防线 5c 能跑且通过(本地等价命令全绿); +2. 指标退化即 fail:人为退化会被拦截(自证过程留提交信息); +3. 加用例 = 加 JSON 行,零代码改动(README 有流程); +4. 规则子集含硬禁忌场景、检索子集含 must_miss(红线)场景; +5. 全部检查绿(tests 不在 mypy 范围;ruff 对 tests/golden 全绿)。 + +## 7. 提交信息 + +``` +test(golden): data-driven golden set framework wired into CI + +tests/golden:JSON 用例 + 阈值配置 + runner(规则 pass_rate=1.0、检索 0.8 +起步)+ pytest 壳;规则/检索两子集种子,Agent/报告子集占位(T-P2-11)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T14-检索缓存与失效.md b/docs/开发任务包/T14-检索缓存与失效.md new file mode 100644 index 0000000..1b90462 --- /dev/null +++ b/docs/开发任务包/T14-检索缓存与失效.md @@ -0,0 +1,65 @@ +# T14 · 检索结果缓存与发布失效 + +> 对应任务:`T-P2-10` | 需求:发布后检索缓存自然失效(Sprint 2 验收行)| 分支:`task/t14-retrieval-cache` | worktree:`/home/xsl/meida_backend-t14` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +给 `RetrievalService.search` 加**进程内结果缓存**(TTL + 容量上限 + 代数失效),把协调提交里的 `invalidate_cache()` 桩变成真实现。缓存解决的是"同一 query 高频重复检索"(Agent 多轮、黄金集回归);失效保证知识发布后**不吐旧结果**。 + +## 2. 已就绪的契约(协调提交 `9c18021` 已提供) + +- **配置**(config.py 已加,.env.example 已示例):`retrieval_cache_enabled=True`、`retrieval_cache_ttl_seconds=60`、`retrieval_cache_max_entries=512`。 +- **钩子已存在**:`packages/retrieval/service.py` 模块级 `invalidate_cache()`(当前只是 `repo.INVALIDATION_VERSION += 1`)与 `packages/retrieval/repo.py` 的 `INVALIDATION_VERSION = 1`——运营后台(T12)已经在调它。**你实现真缓存,不改钩子签名**。 +- `RetrievalService.__init__(session, *, embedding, weights, top_k)` 与 `from_settings(session, settings)`——缓存参数经 `from_settings` 读 Settings 注入(**不改 `__init__` 既有签名**,加带默认值的 keyword-only 参数)。 + +## 3. 实现要点 + +允许改动:`packages/retrieval/cache.py`(新建)、`packages/retrieval/service.py`(缓存接线)、测试文件。**不改 repo.py 的既有函数与 INVALIDATION_VERSION 语义**。 + +1. **`packages/retrieval/cache.py`**(纯逻辑,重点单测): + - `CacheKey`:`(version, query_text, body_shape, occasion, entity_types, rule_boost_hash, top_k)`——**全部影响结果的输入都必须进键**,缺一个就会串结果(rule_boost 是 dict[int,float],排序后序列化再哈希); + - `TtlCache`:容量上限用**简单 LRU**(`OrderedDict` 移动末尾 + 超限 popitem(last=False));TTL 用惰性过期(读时比对时间戳,过期视作 miss);**线程安全不需要**(asyncio 单循环,无 await 竞态窗口——注释写明这个前提,别加锁); + - 指标:`hits`/`misses`/`evictions` 计数器(Prometheus 可后续接,先暴露属性)+ `stats()` 方法。 +2. **service.py 接线**: + - `RetrievalService` 增加 `cache: TtlCache | None`(None = 禁用,行为与现在完全一致); + - `search()`:`cache` 非 None 且非 `degraded` 路径——命中直接返回缓存 `SearchOutcome`;未命中照常检索后**只在有结果时**写缓存(空结果不缓存:`record_gap` 每次都要跑,否则缺口计数被缓存吞掉); + - `from_settings` 按 Settings 装配缓存;**模块级 `invalidate_cache()`**:把 service 模块持有的共享缓存实例(或 repo.INVALIDATION_VERSION,二选一,选实现更干净的那个)作废——调用方(admin 发布编排)不需要知道细节。 +3. **正确性红线**(写进 cache.py docstring):缓存的是 `SearchOutcome`(frozen dataclass,不可变)——**绝不缓存可变对象引用**;命中路径不得复用 session(缓存命中时 DB 零交互)。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_retrieval_cache.py`(fake repo/embedding,不连库): + 1. 同参数二连查:第二次零 DB 调用(fake 计数断言)、结果相等; + 2. 任一键分量变化(query/occasion/body_shape/rule_boost/top_k)→ miss; + 3. TTL 过期 → miss(注入 clock); + 4. LRU 容量超限 → 最旧被逐、计数正确; + 5. 空结果不缓存(degraded 路径每次都触发 gap 记录 fake 调用); + 6. `invalidate_cache()` 后同参数 → miss(代数失效); + 7. cache=None → 行为与现在一致。 +- 集成 `tests/integration/test_retrieval_cache.py`(新文件): + 1. seed 实体 + chunk → search 命中 → **发布新实体(直接调 kb service 或 SQL 置 published)+ `invalidate_cache()`** → 再 search 能看到新实体(**这是 T-P2-10 的验收语义**); + 2. 不失效等 TTL 过期(TTL 配 1s,sleep 1.1s)→ 新实体同样可见(双保险)。 + +## 5. 禁改清单 + +通用禁改之外:`packages/retrieval/repo.py` 的既有函数、`packages/{kb,rules}/**`、`apps/**`、所有已有测试文件。T12 会调 `invalidate_cache()`——**签名不变**是你与它的契约。 + +## 6. 验收标准(T-P2-10) + +1. 同参重复检索零 DB 交互(单测断言); +2. 发布后新内容立即可见(invalidate 路径)且 TTL 到期自然可见(双路径集成测试); +3. 影响结果的输入全量进缓存键(rule_boost 哈希); +4. 空结果不缓存(gap 埋点不被吞); +5. 全部检查绿(mypy 范围照 T6 文档同口径 + `packages/retrieval`)。 + +## 7. 提交信息 + +``` +feat(retrieval): TTL+LRU result cache with generation-based invalidation + +TtlCache(全输入进键/惰性过期/容量逐出/命中零 DB)+ search 接线 +(空结果不缓存,保住 gap 埋点)+ invalidate_cache 真实现(T-P2-10)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T2-任务奖励领取.md b/docs/开发任务包/T2-任务奖励领取.md new file mode 100644 index 0000000..68c2879 --- /dev/null +++ b/docs/开发任务包/T2-任务奖励领取.md @@ -0,0 +1,98 @@ +# T2 · 任务奖励额度领取(claim) + +> 对应任务:`T-P1-26` 收尾件 | 需求:`FR-BILL-03`、架构 §9.3 | 分支:`task/t2-task-bonus` | worktree:`/home/xsl/meida_backend-t2` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +实现产品方案 §3.2 的「做任务换额度」闭环的服务端:`POST /quota/tasks/{task_key}/claim`。 +发放即到账、**发放幂等**(同一任务同一周期不重复发放,FR-BILL-03 验收原文)、受 `bl_quota_rule.task_bonus_max` 封顶。 + +## 2. 已就绪的契约(协调提交已提供) + +### 配置(`config.py` 已加字段,`.env.example` 已示例) + +```python +task_bonus_rules: dict[str, dict[str, Any]] # env: TASK_BONUS_RULES_JSON +``` +默认值(协调提交写死在 config.py,含义:task_key → {quota_key, amount, claim_period}): +```json +{ + "sign_in": {"quota_key": "agent_chat", "amount": 1, "claim_period": "day"}, + "profile_complete":{"quota_key": "agent_chat", "amount": 1, "claim_period": "lifetime"}, + "report_feedback": {"quota_key": "report_regen", "amount": 1, "claim_period": "day"}, + "share_report": {"quota_key": "tryon", "amount": 1, "claim_period": "day"}, + "invite_friend": {"quota_key": "tryon", "amount": 1, "claim_period": "day"} +} +``` +⚠️ `amount` 全部是**占位值 1**——具体数值待产品在产品方案 §3.2 补充后改 env,不改代码(FR-BILL-04 配置化)。代码注释里必须写明这一点。 + +### 错误码(`errors.py` 已加) + +- `TASK_BONUS_UNKNOWN = 40410`:404「任务不存在或暂未开放」; +- `TASK_BONUS_ALREADY_CLAIMED = 40910`:409「这个周期的任务奖励已领取」,hint「下个周期再来」; +- `TASK_BONUS_CAP_REACHED = 40310`:403「任务奖励已达上限」,hint「明天再来,或等额度自然恢复」。 +(注意:这些文案已按 FR-BILL-07 写好,**不得出现价格/购买/会员/升级字样**,你不需要也不要改 meta。) + +### 现有代码事实(先读再写) + +- `packages/domain/billing/quota.py`:`QuotaState(used, base_limit, bonus, hard_cap)`、`period_key(now, period_type, tz_offset_hours)`、`period_reset_at(...)`、`reserve_idempotency_key`/`refund_idempotency_key`;`PeriodType = Literal["day","month","lifetime"]`。 +- `packages/billing/service.py`:`BillingService(session, tz_offset_hours)`;`QuotaSnapshot`;`reserve/commit/refund/get_quota/list_ledger`;内部 `_load_rule(quota_key)`、`lock_or_create_quota(...)`(返回已行锁的 `Quota` 行,`limit_value` 是 base_limit 快照,`bonus_granted` 是本周期已获奖励)。 +- `packages/billing/repo.py`:`add_ledger`/`find_ledger_by_idempotency_key` 等(照现有模式补你需要的)。 +- `packages/shared/models.py`:`Ledger`(`bl_ledger`:`tx_id` PK、`delta`、`tx_type`、`status`、`idempotency_key` UNIQUE、`balance_after`、`quota_key`、`period_key`、`reason`、`ref_type`、`ref_id`)。 +- `apps/api/routers/quota.py`:现有 `GET /quota`、`GET /quota/ledger`,响应模式照抄。 + +## 3. 实现要点 + +允许改动:`packages/billing/service.py`、`packages/billing/repo.py`、`packages/billing/schemas.py`(新建)、`packages/domain/billing/quota.py`、`apps/api/routers/quota.py`、测试文件。 + +1. **纯域函数**(`packages/domain/billing/quota.py`,mypy strict): + ```python + @dataclass(frozen=True, slots=True) + class BonusDecision: ... + def grant_bonus(state: QuotaState, amount: int, task_bonus_max: int) -> QuotaDecision-like + ``` + 语义:`bonus + amount > task_bonus_max` → 不允许(封顶);否则返回 `replace(state, bonus=state.bonus + amount)`。`task_bonus_max` 来自 `bl_quota_rule`。 +2. **服务方法** `BillingService.claim_task_bonus(user_id, task_key, rules)`: + - task_key 不在 rules → `AppError(TASK_BONUS_UNKNOWN)`; + - 解析 `{quota_key, amount, claim_period}`;`rule = await self._load_rule(quota_key)`; + - `pk = period_key(now, claim_period, tz)`;幂等键 `f"grant:{user_id}:{task_key}:{pk}"`; + - 先查 `bl_ledger.idempotency_key` 已存在 → `TASK_BONUS_ALREADY_CLAIMED`(幂等快捷路径); + - `lock_or_create_quota`(同周期)→ `grant_bonus(...)` 封顶判定 → 不允许则 `TASK_BONUS_CAP_REACHED`(**不写任何流水**); + - 同一事务内:更新 `bl_quota.bonus_granted`,插入 `Ledger(tx_type="system_grant", delta=0, status="committed", balance_after=state.remaining, reason=task_key, ref_type="task_bonus", ref_id=task_key)`,提交; + - 并发兜底:`IntegrityError`(唯一键冲突)→ 回滚 → `TASK_BONUS_ALREADY_CLAIMED`。**`delta=0` 的理由写进注释**:system_grant 不改变 `used`,只提高有效额度,`balance_after` 仍按 remaining 语义记录(流水可重算的是 used,不是余额本身——与现有 reserve/commit 的记账口径一致)。 +3. **路由**(`apps/api/routers/quota.py` 追加):`POST /tasks/{task_key}/claim`(注意 router prefix 是 `/quota`,最终路径即架构 §9.3 的 `POST /quota/tasks/{task_key}/claim`)。请求无需 body;响应 `ok(TaskBonusClaimResponse(...))`,字段:`task_key/quota_key/amount/bonus_granted/remaining`。Pydantic 模型放 **`packages/billing/schemas.py`**(新建;apps/api/schemas.py 禁改)。 + - rules 从 settings 读:路由函数签名加 `settings: SettingsDep`,传 `dict(settings.task_bonus_rules)`。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_bonus_domain.py`:`grant_bonus` 的封顶边界(bonus+amount == max 允许、== max+1 拒绝)、amount≤0 抛 ValueError、不改变 used。 +- 单测 `tests/unit/test_task_bonus_rules.py`:settings 解析 `TASK_BONUS_RULES_JSON`(字符串 JSON / dict / 空值容错)——参考 config.py 的 `_parse_json_object` 模式,如果协调提交已把 `task_bonus_rules` 加进该 validator 的字段列表就不用测解析,直接测默认值形状(每个 entry 必有 quota_key/amount/claim_period,claim_period ∈ PeriodType)。 +- 集成测试 `tests/integration/test_task_bonus.py`(新文件,允许跑): + 1. dev-token 登录 → claim `sign_in` → 200,`GET /quota?quota_key=agent_chat` 的 `bonus_granted` = 1; + 2. 再 claim 同一 task → 40910; + 3. 把 Redis 无关、纯 DB 断言:`bl_ledger` 出现 `system_grant` 行且 `balance_after` = base+bonus; + 4. 封顶路径可用 `TASK_BONUS_RULES_JSON` 覆盖 amount 为 11(> task_bonus_max=10)→ claim → 40310,且无新流水。 + (settings 覆盖用 `monkeypatch.setenv + get_settings.cache_clear()`,结束恢复,参考 T1 文档的写法。) + +## 5. 禁改清单(通用禁改之外) + +`packages/llm/**`、`packages/agent/**`、`packages/identity/**`、`packages/analytics/**`、`packages/storage/**`、`apps/api/routers/{agent,auth,tasks,events,uploads}.py`、`apps/api/deps.py`、`tests/integration/test_vertical_slice.py`。 + +## 6. 验收标准(FR-BILL-03 原文) + +1. 「签到、完善资料、反馈、邀请、分享完成即到账」——五类 task_key 全部走通 claim; +2. **发放幂等**:同一任务同一周期重复 claim 返回 40910,账本无重复行; +3. 封顶:`bonus_granted` 永不超过 `bl_quota_rule.task_bonus_max`; +4. 全部检查绿(mypy 范围:`apps packages/shared packages/identity packages/billing packages/task packages/agent packages/compliance packages/analytics packages/storage` + `packages/domain` strict)。 + +## 7. 提交信息 + +``` +feat(billing): task bonus claim endpoint with idempotency and cap + +POST /quota/tasks/{task_key}/claim:配置驱动任务注册表、 +grant_bonus 纯域函数、ledger 幂等键防重、task_bonus_max 封顶(FR-BILL-03/04)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T3-埋点上报.md b/docs/开发任务包/T3-埋点上报.md new file mode 100644 index 0000000..e4b4ef9 --- /dev/null +++ b/docs/开发任务包/T3-埋点上报.md @@ -0,0 +1,84 @@ +# T3 · 客户端埋点批量上报 + +> 对应任务:`T-P1-10`(埋点事件字典/上报通道的服务端部分)| 需求:`FR-SYS-06`、架构 §9.3 | 分支:`task/t3-events` | worktree:`/home/xsl/meida_backend-t3` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +实现 `POST /events/batch`:客户端批量上报埋点事件,落 `ob_event` 表。这是 V1 全链路埋点(FR-SYS-06)与后续三张看板(TD-24)的数据入口。 + +## 2. 已就绪的契约(协调提交已提供) + +### 表(迁移 `0004` 已建,ORM 已加,**你不需要碰 models.py / migrations**) + +`ob_event`(架构 §6.2): +- `event_id` bigint PK(雪花)、`user_id` bigint NOT NULL、`event_name` varchar(64)、`props` JSONB、`client_ts` timestamptz NULL、`server_ts` timestamptz NOT NULL default now()、 +- 索引:`(event_name, server_ts)`、`(user_id, server_ts)`。 +- 架构原文"按月分区"——**V1 先普通表**(与 ob_ai_call 同样处理),代码注释写明"分区在数据量上来后再做,先不预支复杂度"。 + +### 配置(`config.py` 已加字段) + +```python +events_max_batch: int = 50 # env: EVENTS_MAX_BATCH +events_props_max_bytes: int = 8192 # env: EVENTS_PROPS_MAX_BYTES +``` + +### 错误码(`errors.py` 已加) + +- `EVENTS_BATCH_REJECTED = 40020`:400「埋点数据格式有误」,hint=null(对客户端静默降级即可,不打扰用户)。 + +### 骨架(协调提交已建,直接填充) + +- `packages/analytics/__init__.py`、`packages/analytics/README.md`、`packages/analytics/service.py`(空骨架) +- `apps/api/routers/events.py`(空骨架 router,**已注册进 main.py**,prefix `/events`) +- `apps/api/deps.py` 已有 `AnalyticsServiceDep`(构造 `AnalyticsService(session)`)——**deps.py 禁改,签名如不满足需要,用路由函数内自建服务对象的方式,不改 deps**。 + +### 分层位置 + +`packages.analytics` 被协调提交放进了 import-linter 的"共享能力层"(与 identity/billing/task 同层)——它只能 import `packages.shared`/`packages.domain`,**不得** import 其它业务包或 llm。 + +## 3. 实现要点 + +允许改动:`packages/analytics/**`、`apps/api/routers/events.py`、测试文件。 + +1. **`packages/analytics/schemas.py`**(新建): + - `EventIn`:`event_name: str`(`pattern=r"^[a-z][a-z0-9_.]{1,63}$"`)、`props: dict[str, Any] = {}`、`client_ts: datetime | None = None`; + - `EventBatchRequest`:`events: list[EventIn]`(`min_length=1`)、 + - `EventBatchResponse`:`accepted: int`。 + - **事件字典(event dictionary)**:V1 不做服务端枚举校验(客户端事件名集合由 TD-23 埋点字典维护,尚未定稿)——用正则约束形状即可,代码注释写明"字典校验等 TD-23 定稿后再收紧"。 +2. **`packages/analytics/service.py`**: + - `AnalyticsService(session)`:`async def record(user_id: int, batch: list[EventIn]) -> int`; + - 校验(fail-close,任一不过 → 整批拒绝 `AppError(EVENTS_BATCH_REJECTED)`,**不部分落库**):批量 ≤ `events_max_batch`;每条 props 序列化后 ≤ `events_props_max_bytes`(`len(json.dumps(props, ensure_ascii=False).encode())`);`client_ts` 若给出:不晚于 server_now+5min、不早于 7 天前; + - 组装 `ObEvent(event_id=next_id(), user_id=..., server_ts=datetime.now(UTC))` 逐条 add,`flush` 一次,返回条数;(`from packages.domain.ids import next_id`,参考 identity/service.py 用法。) + - **不含 PII 清洗**:props 里若有 `phone`/`token`/`authorization` 键 → 直接删除该键并 `logger.warning("events_pii_dropped", ...)`(NFR-SEC-03 日志脱敏的同族要求)。 +3. **`apps/api/routers/events.py`**: + - `POST /batch`,`user_id: CurrentUserId`,`analytics: AnalyticsServiceDep`(若 Dep 不可用则函数内 `AnalyticsService(session)` with `SessionDep`); + - `ok(EventBatchResponse(...))`;服务层抛的 `AppError` 自然走统一异常处理,不需要额外 catch。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_events_validation.py`(纯校验逻辑,不连库——把校验拆成模块级纯函数 `validate_batch(events, *, max_batch, max_bytes, now) -> list[ObEvent-like]` 便于无 DB 测试): + - 合法批量通过;超批量拒;props 超字节拒;event_name 正则(大写/数字开头/超长拒);client_ts 未来>5min 拒、7 天前拒;PII 键被剔除。 +- 集成测试 `tests/integration/test_events.py`(新文件,允许跑):dev-token → POST /v1/events/batch 两条事件 → 200 accepted=2;查库两行 `ob_event`;再 POST 非法批(event_name 大写)→ 40020 且**库里没有半条**。 + +## 5. 禁改清单(通用禁改之外) + +`packages/llm/**`、`packages/billing/**`、`packages/identity/**`、`packages/storage/**`、`packages/agent/**`、`apps/api/routers/{agent,auth,quota,tasks,uploads}.py`、`tests/integration/test_vertical_slice.py`。 + +## 6. 验收标准 + +1. `POST /api/v1/events/batch`(及 `/v1` 别名)可用,认证必需(无 token → 40100); +2. 整批原子:要么全落、要么全拒; +3. 校验规则全部有测试覆盖; +4. 全部检查绿(mypy 范围同 T2 列表)。 + +## 7. 提交信息 + +``` +feat(analytics): POST /events/batch with fail-close validation + +ob_event 落库通道:形状校验 + props 字节上限 + client_ts 合理性 + +PII 键剔除,整批原子(FR-SYS-06 服务端部分)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T4-刷新令牌与会话.md b/docs/开发任务包/T4-刷新令牌与会话.md new file mode 100644 index 0000000..aeb46d7 --- /dev/null +++ b/docs/开发任务包/T4-刷新令牌与会话.md @@ -0,0 +1,96 @@ +# T4 · 刷新令牌与服务端可撤销会话 + +> 对应任务:`T-P1-13`(双令牌会话)| 需求:`FR-ACC-05`、架构 §6.2/§9.1 | 分支:`task/t4-refresh-session` | worktree:`/home/xsl/meida_backend-t4` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +在现有 15 分钟 access JWT 之外,补齐 **refresh token**:opaque 令牌 + `id_session` 服务端可撤销 + 轮换(rotation)。端点:`POST /auth/refresh`、`POST /auth/logout`;dev-token 扩展为同时签发 refresh,使整条链路可测。 + +**明确不做**(写进代码注释,防止后人误判):华为账号登录(T-P1-12,等资质)、手机验证码(T-P1-12b,等短信通道)、游客/合并/注销、access token 的黑名单式即时吊销(JWT 无状态,V1 接受"access 最长 15 分钟残留";「登出后旧令牌立即失效」的验收对象是 refresh 令牌与会话,见下)。 + +## 2. 已就绪的契约(协调提交已提供) + +### 表(迁移 `0004` 已建,ORM 已加,禁改 models.py/migrations) + +`id_session`(架构 §6.2 原文列:`session_id, user_id, refresh_token_hash, expire_at, revoked`;协调提交落地为): +- **ORM 类名为 `UserSession`**(刻意不用 `Session`,避免与 SQLAlchemy 的 AsyncSession 混淆): +- `session_id` bigint PK(雪花)、`user_id` bigint NOT NULL、`refresh_token_hash` varchar(64) UNIQUE(sha256 hex)、`expire_at` timestamptz NOT NULL、`revoked` boolean NOT NULL default false、`revoked_at` timestamptz NULL、`created_at` timestamptz NOT NULL default now(); +- 索引:`(user_id)`。 + +### 配置(`config.py` 已加字段) + +```python +refresh_token_ttl_seconds: int = 2592000 # env: REFRESH_TOKEN_TTL_SECONDS,30 天 +``` + +### 错误码(`errors.py` 已加) + +- `REFRESH_TOKEN_INVALID = 40103`:401「登录状态无效」,hint「请重新登录」; +- `REFRESH_TOKEN_EXPIRED = 40104`:401「登录状态已过期」,hint「请重新登录」; +- `SESSION_REVOKED = 40105`:401「该会话已登出」,hint「请重新登录」。 + +### 现有代码事实(先读再写) + +- `packages/shared/security.py`:`create_access_token`/`decode_access_token`(JWT HS256,claims `sub/iat/exp/iss/typ`)——**不改此文件**,refresh 不是 JWT。 +- `packages/identity/service.py`:`IdentityService(session)`、`get_or_create_by_external_identity`、`get_user`(注销/禁用会抛 UNAUTHENTICATED)、`record_consent`;`DEV_PROVIDER = "dev"`。 +- `packages/identity/repo.py`:`get_user/find_identity/add_user/add_identity/add_consent/get_consent` 模式(照抄风格补 session 函数)。 +- `apps/api/routers/auth.py`:dev-token 端点(`DEV_AUTH_ENABLED && is_local` 双开关)。 +- `apps/api/deps.py`:`IdentityServiceDep`、`SettingsDep` 已有(deps.py 禁改,够用)。 + +## 3. 实现要点 + +允许改动:`packages/identity/tokens.py`(新建)、`packages/identity/repo.py`、`packages/identity/service.py`、`packages/identity/schemas.py`(新建)、`apps/api/routers/auth.py`、测试文件。 + +1. **`packages/identity/tokens.py`**(纯逻辑,无 IO,重点单测): + ```python + def generate_refresh_token() -> str # secrets.token_urlsafe(48) + def hash_refresh_token(token: str) -> str # hashlib.sha256(token.encode()).hexdigest() + ``` + 决策注释:**为什么 opaque 而不是 JWT**——refresh 的核心价值就是"服务端可单条撤销"(FR-ACC-05),无状态 JWT 做不到撤销;哈希落库而非明文,泄漏库不等于泄漏令牌。 +2. **repo**:`find_session_by_hash(session, token_hash)`、`add_session(session, Session(...))`、`revoke_session(session, session_id)`、`revoke_all_sessions(session, user_id)`(最后一个 V1 先备着,logout-all 属 FR 后续)。 +3. **service**(`IdentityService` 上追加方法): + - `async def issue_session(user_id) -> SessionIssued`(dataclass:`refresh_token: str`、`expires_at: datetime`、`session_id: int`):先 `await self.get_user(user_id)`(防已注销用户续签),生成/哈希/落库/flush; + - `async def rotate(refresh_token: str) -> SessionIssued`:查哈希 → 无 → `REFRESH_TOKEN_INVALID`;`revoked` → `SESSION_REVOKED`;`expire_at < now` → `REFRESH_TOKEN_EXPIRED`(并把该行 `revoked=true` 顺手落库);通过 → **旧行 revoked=true + 新行创建,同一事务**(轮换 = 旧 refresh 一次性);返回新 token; + - `async def revoke(refresh_token: str) -> bool`:查哈希 → 无/已撤销返回 False;否则 revoked=true。**重复使用已撤销的 refresh(重放检测)**:V1 只报 `SESSION_REVOKED` 并 `logger.warning("refresh_reuse_detected", user_id=...)`,不做家族连坐(注释写明这是 V2 硬化项)。 +4. **`packages/identity/schemas.py`**(新建;apps/api/schemas.py 禁改): + - `RefreshRequest(refresh_token: str)`、`LogoutRequest(refresh_token: str)`; + - `TokenPairResponse(access_token, refresh_token, expires_in, refresh_expires_in, user_id: str)`。 +5. **`apps/api/routers/auth.py`**: + - `POST /refresh`:`identity.rotate(...)` → `create_access_token(...)` → `ok(TokenPairResponse(...))`; + - `POST /logout`:`user_id: CurrentUserId` + `identity.revoke(...)` → `ok({"revoked": True/False})`(False 不报错——幂等登出); + - dev-token 端点扩展:同时 `issue_session`,响应改用 `TokenPairResponse`(**保留原字段名 `access_token/expires_in/user_id` 不变**,新增 `refresh_token/refresh_expires_in`——不破坏 smoke.sh 与现有测试对旧字段的读取;若现有测试断言整个 dict 形状,允许增量更新断言但不得删除旧字段)。 + - 注意:登出语义 = 撤销会话(refresh 失效)。access JWT 在 TTL 内仍有效的残留窗口,写进 auth.py 模块 docstring。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_refresh_tokens.py`:token 长度/字符域、哈希稳定性、哈希不可逆(≠原文)。 +- 集成测试 `tests/integration/test_auth_refresh.py`(新文件,允许跑,**自建 app fixture 参考 test_vertical_slice.py 的 FakeProvider 段,但 auth 用例不需要 LLM**): + 1. dev-token → 拿 `refresh_token`;`POST /v1/auth/refresh` → 200,新 access 可调 `GET /v1/quota`; + 2. **旧 refresh 二次使用** → 40105; + 3. `POST /v1/auth/logout` → 再 refresh → 40105; + 4. 伪造 refresh → 40103; + 5. 过期路径:构造 `REFRESH_TOKEN_TTL_SECONDS=0`(monkeypatch.setenv + `get_settings.cache_clear()`)→ refresh → 40104;结束恢复 cache。 + +## 5. 禁改清单(通用禁改之外) + +`packages/shared/security.py`、`packages/llm/**`、`packages/billing/**`、`packages/analytics/**`、`packages/storage/**`、`apps/api/routers/{agent,quota,tasks,events,uploads}.py`、`apps/api/deps.py`、`tests/integration/test_vertical_slice.py`。 + +## 6. 验收标准(FR-ACC-05) + +1. access/refresh 双令牌齐备:access 15 分钟 JWT(现状)+ refresh 30 天 opaque 可撤销; +2. **refresh 服务端可单条撤销**:logout 后该 refresh 立即失效; +3. 轮换生效:每次 refresh 换新 token,旧 token 一次性; +4. dev-token 链路向后兼容(旧响应字段不变); +5. 全部检查绿(mopy 范围同 T2 列表)。 + +## 7. 提交信息 + +``` +feat(identity): refresh token rotation with server-side revocable sessions + +id_session 落地:opaque refresh + sha256 哈希存储 + 一次性轮换 + +logout 撤销 + 重放检测告警(FR-ACC-05);dev-token 兼容性扩展。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T5-上传预签名.md b/docs/开发任务包/T5-上传预签名.md new file mode 100644 index 0000000..9908f70 --- /dev/null +++ b/docs/开发任务包/T5-上传预签名.md @@ -0,0 +1,104 @@ +# T5 · 图片直传预签名 URL + +> 对应任务:`T-P1-27` | 需求:`FR-SYS-03`、架构 §9.3 | 分支:`task/t5-upload-presign` | worktree:`/home/xsl/meida_backend-t5` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +实现 `POST /uploads/presign`:为客户端签发 COS 临时桶的 PUT 预签名 URL,**图片不经应用服务器**(FR-SYS-03 验收原文)。本任务只做「签发」,不做上传回调/校验图片内容(那是报告链路 T-P3 的事)。 + +## 2. 已就绪的契约(协调提交已提供) + +### 依赖(协调提交已 `uv add`,禁改 pyproject/uv.lock) + +`cos-python-sdk-v5`(腾讯云 COS 官方 SDK,模块名 `qcloud_cos`)。mypy 对它的缺失 stub 已在协调提交里配好 override。真实调用只需要 `CosS3Config` 与 `CosS3Client.get_presigned_url` / `get_presigned_download_url` 所在的 `CosS3Client`。 + +### 配置(`config.py` 已加字段,`.env.example` 已示例) + +```python +cos_region: str = "ap-shanghai" # env: COS_REGION +cos_secret_id: str = "" # env: COS_SECRET_ID +cos_secret_key: str = "" # env: COS_SECRET_KEY +cos_bucket_temp: str = "meida-temp" # env: COS_BUCKET_TEMP(临时桶,2h 生命周期 CR-02) +cos_bucket_user: str = "meida-user" # env: COS_BUCKET_USER +cos_bucket_kb: str = "meida-kb" # env: COS_BUCKET_KB +cos_bucket_report: str = "meida-report" # env: COS_BUCKET_REPORT +cos_presign_expire_seconds: int = 600 # env: COS_PRESIGN_EXPIRE_SECONDS +upload_max_bytes: int = 20_971_520 # env: UPLOAD_MAX_BYTES(20MB) +upload_allowed_types: str = "image/jpeg,image/png,image/webp" # env: UPLOAD_ALLOWED_TYPES +upload_scenes: dict[str, dict[str, Any]] = Field(..., alias="UPLOAD_SCENES_JSON") +# 默认:{"report_photo": {"bucket": "temp"}, "avatar": {"bucket": "temp"}} +``` +(字段名/别名以 config.py 协调提交的实际代码为准——**开工先读 config.py 核对**,文档若有出入以代码为准。) + +### 错误码(`errors.py` 已加) + +- `UPLOAD_REJECTED = 40010`:400「不支持的图片类型或超出大小限制」,hint「请上传 20MB 以内的 JPG/PNG/WebP 图片」。 + +### 骨架(协调提交已建) + +- `packages/storage/__init__.py`、`README.md`、`service.py`(空骨架); +- `apps/api/routers/uploads.py`(空骨架 router,prefix `/uploads`,已注册 main.py); +- `apps/api/deps.py` 已有 `StorageServiceDep`(**deps.py 禁改**;若构造参数不满足,在路由函数内用 `SettingsDep` 自建 service)。 + +### 分层位置 + +`packages.storage` 在 import-linter"共享能力层"。只准 import `packages.shared`/`packages.domain` 与 `qcloud_cos`/stdlib。 + +## 3. 实现要点 + +允许改动:`packages/storage/**`、`apps/api/routers/uploads.py`、测试文件。 + +1. **`packages/storage/schemas.py`**: + - `PresignRequest(scene: str, content_type: str, size_bytes: int)`(size ≥1); + - `PresignResponse(upload_url: str, object_key: str, expires_in: int, max_bytes: int, content_type: str)`。 +2. **`packages/storage/service.py`**: + - 定义 `Protocol CosSigner`:`def presign_put(bucket: str, key: str, expires: int, content_type: str) -> str`(**把 SDK 隔离在 Protocol 后面**,单测用 FakeSigner,绝不连网); + - `QcloudCosSigner`:包 `CosS3Client`(构造参数 region/secret_id/secret_key),`presign_put` 调 `client.get_presigned_url(Method.PUT, bucket, key, Expire=expires, Headers={"Content-Type": content_type})`(`from qcloud_cos import CosConfig, CosS3Client`;`Method` 从 `qcloud_cos.method` import——以实际 SDK 签名为准,若 Headers 参数名不同按 SDK 文档调整,**以能签出带 content-type 约束的 PUT URL 为准**); + - `StorageService(settings, signer: CosSigner | None = None)`:signer 为 None 时用 `QcloudCosSigner` 惰性构造(**只有真实请求才建 client**;测试必传 FakeSigner); + - `def build_presign(user_id: int, request: PresignRequest) -> PresignResponse`: + - scene 不在 `settings.upload_scenes` → `AppError(UPLOAD_REJECTED)`(message 写"不支持的上传场景"); + - content_type ∉ `settings.upload_allowed_types` 解析集合 → `UPLOAD_REJECTED`; + - size_bytes > `upload_max_bytes` → `UPLOAD_REJECTED`; + - 扩展名映射 `{image/jpeg: jpg, image/png: png, image/webp: webp}`(其它类型已被上一条拦下); + - object_key = `f"{scene}/{user_id}/{datetime:%Y%m%d}/{uuid4().hex}.{ext}"`(**这个 key 布局写死**:按场景分目录、按用户隔离、按日分组、uuid 防猜——注释说明这是 COS 侧做前缀审计与生命周期的基础); + - bucket = scenes 配置里的 `bucket`("temp"/"user"/"kb"/"report" → 对应 `settings.cos_bucket_*` 四字段映射); + - 调 signer 得 URL,返回 `PresignResponse(expires_in=settings.cos_presign_expire_seconds, max_bytes=settings.upload_max_bytes, ...)`。 +3. **`apps/api/routers/uploads.py`**: + - `POST /presign`,`user_id: CurrentUserId` + settings;`ok(PresignResponse(...))`; + - 模块 docstring 写明:真实联调需在 `.env` 配 COS 四件套;本地单测不依赖任何真实 COS。 + +## 4. 测试要求 + +单测 `tests/unit/test_upload_presign.py`(全部用 FakeSigner,断言 URL/key/参数,**零网络**): +- 合法请求 → key 布局正确(scene 前缀、user_id 段、日期段、扩展名)、expires_in/max_bytes 回显; +- 非法 content_type(如 `application/pdf`)→ 40010; +- 超大小 → 40010; +- 未知 scene → 40010; +- bucket 映射:`{"bucket":"temp"}` → 使用 `cos_bucket_temp` 配置值(FakeSigner 捕获 bucket 参数断言); +- `upload_allowed_types` 字符串解析(空串/带空格容错)。 + +不需要集成测试(无 DB 依赖)。 + +## 5. 禁改清单(通用禁改之外) + +`packages/llm/**`、`packages/billing/**`、`packages/identity/**`、`packages/analytics/**`、`apps/api/routers/{agent,auth,quota,tasks,events}.py`、`apps/api/deps.py`、`tests/integration/**`。 + +## 6. 验收标准(FR-SYS-03) + +1. `POST /api/v1/uploads/presign`(含 `/v1` 别名)可用,认证必需; +2. 类型/大小/场景三重服务端校验,错误码统一 40010; +3. key 布局可审计(scene/user/日期/uuid); +4. 单测全绿且无任何真实网络调用(FakeSigner 注入); +5. 全部检查绿(mypy 范围同 T2 列表)。 + +## 7. 提交信息 + +``` +feat(storage): COS presigned PUT URL endpoint for direct upload + +POST /uploads/presign:场景/类型/大小三重校验 + 可审计 object_key 布局 + +SDK 隔离在 CosSigner Protocol 后(FR-SYS-03,T-P1-27)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T6-知识库实体与图谱.md b/docs/开发任务包/T6-知识库实体与图谱.md new file mode 100644 index 0000000..1c0dc02 --- /dev/null +++ b/docs/开发任务包/T6-知识库实体与图谱.md @@ -0,0 +1,64 @@ +# T6 · 知识库·实体与图谱 + +> 对应任务:`T-P2-01`(12 类实体可存取)、`T-P2-05`(1~3 跳可解释路径)| 需求:`FR-KB-*`、架构 §6.2 kb 域、`TD-09` | 分支:`task/t6-kb-entity` | worktree:`/home/xsl/meida_backend-t6` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +知识库的实体底座:12 类实体统一表的 CRUD 与状态机、`kb_relation` 邻接表 + 递归 CTE 的 1~3 跳图谱查询(可解释路径)、`kb_color` 自建色板存取。这是知识库可运营(M2 出口)的第一块。 + +## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations) + +### 表(迁移 `0005` 已建,ORM 已加:`KbEntity`/`KbRelation`/`KbColor`,先读 `packages/shared/models.py` 核对列名) + +- `kb_entity`:`entity_id` PK 雪花、`entity_type` varchar(32)、`name` varchar(128)、`attrs` JSONB、`status` draft/reviewing/published/archived、`version` int、`source`、`license`、`updated_by`、`created_at/updated_at`;UNIQUE(entity_type,name)、Index(entity_type,status) +- `kb_relation`:`relation_id` PK、`from_id`/`to_id` bigint、`rel_type` varchar(64)、`weight` Numeric(4,3)、`confidence` Numeric(4,3)、`evidence` varchar(255);UNIQUE(from_id,rel_type,to_id)、Index(to_id) +- `kb_color`:`color_id` PK、`season_code` varchar(16)、`role` base/accent/avoid、`lab_l/lab_a/lab_b` Numeric(6,2)、`hex` varchar(7)、`name_cn` varchar(64);Index(season_code) + +### 12 类 entity_type(知识库文档 §3.1,写进 schemas 的 Literal 校验) + +`item / color / style / body_shape / face_hair / occasion / material / accessory / makeup / rule / outfit / trend` + +### 状态机(写进 service,非法跃迁抛 `AppError(STATE_CONFLICT)`) + +`draft → reviewing → published → archived`;`archived` 终态;`draft/reviewing → archived` 允许(撤稿);其余跃迁非法。每次**内容修改** version+1 且**不允许直接改 published 实体**——published 实体要改必须先回 draft(新版本走评审,这是知识库文档"发布回滚"的语义)。 + +### 路由骨架 + +`apps/api/routers/kb.py` 空路由(prefix `/kb`)已注册 main.py——你填充。 + +## 3. 实现要点 + +允许改动:`packages/kb/**`(service.py / repo.py / schemas.py / README.md)、`apps/api/routers/kb.py`、测试文件。**`packages/kb` 现只有 README + 空 `__init__`;`packages/rules`、`packages/retrieval` 归 T7/T8,禁碰。** + +1. **schemas.py**(Pydantic):`EntityCreate`(entity_type/name/attrs)、`EntityUpdate`(仅 draft/reviewing 可用)、`EntityOut`(含 status/version/时间戳)、`RelationCreate`、`GraphPath`(路径节点序列 + 每跳 rel_type)、`ColorOut`。**跨域响应模型放本包**,apps/api/schemas.py 禁改。 +2. **repo.py**:CRUD + `search_entities(entity_type, status, name_ilike, limit, offset)` + `traverse(from_id, max_hops)` —— 递归 CTE 用 `sqlalchemy.text()` 写原生 SQL(返回 hop 序列),1~3 跳,**防环**(CTE 里用 path 数组 + `NOT path @> ARRAY[to_id]` 判重)。参考架构 TD-09 的理由:浅层有界遍历,不需要图数据库。 +3. **service.py**:状态机校验、版本递增、relation 写入(自环/重复抛 `AppError(BAD_REQUEST)`)、色板 CRUD(role/hex 格式校验,`#RRGGBB`)。实体创建时 `entity_id=next_id()`(`packages.domain.ids`)。 +4. **路由**:`GET /kb/items`(分页 + entity_type/status 过滤)、`GET /kb/items/{id}`、`POST /kb/items`、`PATCH /kb/items/{id}`、`GET /kb/items/{id}/graph?max_hops=3`(1~3 跳,超界 422/400)。写接口**暂不挂鉴权以外的前置**(后台运营角色属 T-P2-09,先 CurrentUserId)。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_kb_entity.py`:状态机全跃迁矩阵(合法/非法)、12 类 Literal 校验、非法 entity_type 拒绝、色板 hex 校验。(纯逻辑部分;repo/service 层用集成覆盖。) +- 集成 `tests/integration/test_kb_entity.py`(新文件):建实体→reviewing→published;改 published 被拒;draft 改→version+1;建 relation→graph 2 跳返回可解释路径(含每跳 rel_type);自环拒绝;`GET /v1/kb/items` 过滤分页。 + +## 5. 禁改清单 + +通用禁改之外:`packages/rules/**`、`packages/retrieval/**`、`packages/identity/**`、`apps/api/routers/{auth,auth_sms}.py`、全部已有测试文件。 + +## 6. 验收标准(T-P2-01/05) + +1. 12 类实体可存取,`attrs` JSONB 差异化承载; +2. 状态机非法跃迁 100% 被拦(有测试矩阵证明); +3. 图谱 1~3 跳返回可解释路径且防环; +4. 全部检查绿(mypy 范围:`apps packages/shared packages/identity packages/billing packages/task packages/agent packages/compliance packages/analytics packages/storage packages/kb`)。 + +## 7. 提交信息 + +``` +feat(kb): entity store with lifecycle, graph traversal and color palette + +12 类实体统一表 + 状态机 + version 递增 + kb_relation 递归 CTE 1~3 跳 +可解释路径(防环)+ kb_color 色板(T-P2-01/05,TD-09/18)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T7-规则引擎.md b/docs/开发任务包/T7-规则引擎.md new file mode 100644 index 0000000..3ad2331 --- /dev/null +++ b/docs/开发任务包/T7-规则引擎.md @@ -0,0 +1,89 @@ +# T7 · 知识库·规则引擎 + +> 对应任务:`T-P2-02`(规则求值 p95 < 50ms;硬约束可拦截)| 需求:`FR-KB-02`、架构 §8.2 rules-engine、`TD-10` | 分支:`task/t7-rules-engine` | worktree:`/home/xsl/meida_backend-t7` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +自研数据驱动决策表(TD-10):`kb_rule` 的 JSON DSL 解析 + 纯域求值引擎。**核心结论由规则引擎产出,LLM 只负责表达**(防幻觉红线)——这是整个产品"专业可信"的根基,代码质量按"钱与正确性"标准写。 + +## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations) + +### 表 `kb_rule`(ORM `KbRule` 已加) + +`rule_id` PK 雪花、`rule_key` varchar(64) UNIQUE(如 `R-BODY-A-002`)、`category` varchar(32)、`version` int、`status` draft/published/archived、`priority` int、`conditions` JSONB、`effects` JSONB、`is_hard` bool、`confidence` Numeric(4,3)、`evidence` varchar(255)、`author`/`reviewer` varchar(64)、`published_at`;Index(status, priority)。 +(注:架构 §6.2 写的列名 `conclusions`,协调提交统一为 **`effects`**——与"决策表"术语一致,任务文档以 models.py 实际代码为准。) + +### DSL 规格(知识库文档 §3.2⑩ 原文结构) + +```json +{ + "rule_key": "R-BODY-A-002", + "category": "body_shape", + "priority": 90, + "conditions": { + "body_shape": "A型", + "features": { "include": ["溜肩"] }, + "goals": { "include": ["修饰溜肩"] } + }, + "effects": { + "recommend": [ { "slot": "top", "attrs": { "shoulder": ["垫肩","自然肩"], "sleeve": ["泡泡袖"] } } ], + "avoid": [ { "slot": "top", "attrs": { "shoulder": ["落肩"], "silhouette": ["修身"] } } ], + "explanation": "溜肩需要在肩部建立结构感……" + }, + "is_hard": false, + "confidence": 0.95 +} +``` + +### 优先级分组(知识库文档 §3.2⑩,语义:**数字越大越优先**) + +合规 100 / 用户禁忌 95 / 体型 90 / 色彩 85 / 场景 80 / 温度 75 / 风格 70 / 偏好 60 / 趋势 50。 + +### 分层位置 + +`packages/rules` 已在 import-linter 第 4 层(kb|retrieval|rules|memory|profile),现只有 README + 空 `__init__`。只准 import `packages.shared`/`packages.domain`。 + +## 3. 实现要点 + +允许改动:`packages/rules/**`、测试文件。 + +1. **`packages/rules/dsl.py`**——纯逻辑(无 IO,重点单测): + - `RuleCondition` / `RuleEffect` / `Rule` pydantic 模型(解析校验 DSL;`conditions` 支持的字段:等值匹配(str)、`include`/`exclude` 列表、`min`/`max` 数值区间;未识别的键**拒绝**而非忽略——静默忽略会让规则悄悄失效); + - `match(condition: RuleCondition, ctx: RuleContext) -> bool`:`RuleContext` 是 frozen dataclass(`body_shape/season/face_shape/temperature/features/goals/occasion/style/user_taboos…`,缺字段 = 不匹配该子句); + - 所有子句 AND 语义。 +2. **`packages/rules/engine.py`**——纯域求值: + - `evaluate(rules, ctx) -> RuleVerdict`:返回 `recommend/avoid/explanation 汇总 + rule_hits`(每条含 rule_key/priority/category/is_hard/confidence/explanation); + - 排序:priority 降序,同 priority 按 confidence 降序; + - **硬约束语义**:`is_hard=true` 且 avoid 命中 → `RuleVerdict.hard_blocks` 非空,调用方必须用它做硬过滤(禁忌违反 = 0 是发布红线);**用户禁忌(priority ≥95 的 avoid)永远压过专业推荐**——同 slot 冲突时用户禁忌胜出并从 recommend 中剔除冲突项; + - p95 < 50ms:求值是纯内存操作,规则量 400 条时远低于此;写一个 400 条规则 × 1000 次求值的基准单测(`time.perf_counter`,断言均值 < 5ms/千次的量级即可,CI 机器差异留裕量)。 +3. **`packages/rules/service.py` + `repo.py`**——DB 侧:按 status='published' 载入规则(内存缓存 + `invalidate()`;量级 400,直接整表载入),CRUD(draft→published 状态机,同 T6 语义),DSL 写入前用 pydantic 校验。 +4. **黄金集雏形**(架构 §1081 明确要求):`tests/unit/test_rules_golden.py` 固定 8~10 组「输入画像 → 期望 recommend/avoid/hard_blocks」样本,其中**必须包含**:一条硬禁忌场景(用户禁忌压过专业推荐的用例)、一条多规则叠加场景。 + +## 4. 测试要求 + +- 单测:DSL 解析(合法/非法结构/未识别键拒绝)、match 矩阵(等值/include/exclude/min-max/缺字段)、排序、硬约束剔除、用户禁忌优先、基准测试、黄金集。 +- 集成 `tests/integration/test_rules_engine.py`(新文件):插 3 条规则(含 1 条 is_hard)→ service 载入 → 求值命中与拦截正确;draft 规则不被载入。 + +## 5. 禁改清单 + +通用禁改之外:`packages/kb/**`(表共享但包不共享——**repo 只碰 kb_rule 表**,通过 ORM 模型访问,不改 T6 的文件)、`packages/retrieval/**`、`packages/identity/**`。 + +## 6. 验收标准(T-P2-02) + +1. 求值 p95 满足 400 条规则量级远低于 50ms(基准测试证明); +2. 硬约束可拦截(hard_blocks 语义 + 黄金集用例); +3. 用户禁忌永远压过专业推荐(黄金集用例); +4. DSL 非法结构 100% 拒绝(不会静默失效); +5. 全部检查绿(mypy 范围同 T6 列表 + `packages/rules`)。 + +## 7. 提交信息 + +``` +feat(rules): data-driven decision table engine with hard constraints + +kb_rule DSL 解析 + 纯域求值(优先级分组/硬约束/用户禁忌压过专业推荐) ++ 400 条基准 + 黄金集雏形(T-P2-02,TD-10)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T8-向量与混合检索.md b/docs/开发任务包/T8-向量与混合检索.md new file mode 100644 index 0000000..9b24c2f --- /dev/null +++ b/docs/开发任务包/T8-向量与混合检索.md @@ -0,0 +1,83 @@ +# T8 · 知识库·向量与混合检索 + +> 对应任务:`T-P2-03`(向量化可重跑/模型可切换)、`T-P2-04`(检索、硬过滤零漏)、`T-P2-06`(无结果 query 记录)| 需求:`FR-KB-03/04`、架构 §8.3、`TD-08` | 分支:`task/t8-retrieval` | worktree:`/home/xsl/meida_backend-t8` +> 先读《README.md(协作规程)》,遵守全部纪律。 + +## 1. 目标 + +pgvector 上的混合检索管线骨架:**硬过滤 → 向量召回 → 关键词召回 → RRF 融合重排**,加 `kb_chunk` 写入通道(向量 + tsvector 双索引)与 `kb_gap` 知识缺口埋点。检索是 Agent(M4-b)和报告(M3)的共同前置。 + +## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations) + +### 表(迁移 `0005` 已建,ORM `KbChunk`/`KbAsset`/`KbGap` 已加,先读 models.py 核对) + +- `kb_chunk`:`chunk_id` PK、`entity_id`、`chunk_type` varchar(32)、`text` text、`embedding` **vector(1024)**、`embedding_model` varchar(64)、`embedding_version` varchar(16)、`tsv` tsvector(GENERATED ALWAYS AS to_tsvector('simple', text) STORED——**写入时不要手工填 tsv**);HNSW 索引(cosine)+ GIN(tsv) +- `kb_asset`:`asset_id` PK、`entity_id`、`asset_type`、`cos_key` varchar(255)、`spec_check` JSONB、`tryon_ready` bool、`review_status`、`license`;Index(entity_id)、partial Index(tryon_ready) +- `kb_gap`:`gap_id` PK、`query_text`、`filter_ctx` JSONB、`hit_count` int、`first_seen_at`;Index(query_text) + +### 配置(config.py 已加,.env.example 已示例) + +```python +kb_vector_dimensions: int = 1024 # KB_VECTOR_DIMENSIONS +kb_embedding_model: str = "deterministic-v1" # KB_EMBEDDING_MODEL(当前默认=确定性假实现) +kb_embedding_device: str = "cpu" # KB_EMBEDDING_DEVICE +retrieval_top_k: int = 50 # RETRIEVAL_TOP_K +retrieval_rerank_weights: dict = {"vector":0.45,"keyword":0.2,"rule":0.25,"freshness":0.1} # RETRIEVAL_RERANK_WEIGHTS_JSON +``` +⚠️ 上批经验:带 alias 的 Settings 字段在**测试里按字段名构造会被静默忽略**——测试用 `monkeypatch.setenv` + `get_settings.cache_clear()`。 + +### 重排公式(架构 §8.3 唯一口径) + +`最终分 = 0.45×向量 + 0.2×关键词 + 0.25×规则权重 + 0.1×新鲜度`(权重可配,就是上面的 rerank_weights)。规则权重项本批的输入是 `rule_boost: dict[chunk_id, float]`(由调用方传入,规则引擎联动属后续任务,管线只认接口)。 + +### 分层位置 + +`packages/retrieval` 在 import-linter 第 4 层(与 rules 同层)。只准 import `packages.shared`/`packages.domain` 与已装依赖(pgvector 的 sqlalchemy 类型、numpy)。 + +## 3. 实现要点 + +允许改动:`packages/retrieval/**`、测试文件。**不建 HTTP 路由**(检索是内部能力,被 agent/report 的 service 调用)。 + +1. **`packages/retrieval/embedding.py`**: + - `EmbeddingProvider` Protocol:`async def embed(texts: list[str]) -> list[list[float]]`、属性 `model_name/version/dimensions`; + - `DeterministicEmbedder`:把文本 sha256 种子映射到**归一化**的 1024 维向量(同文本同向量、不同文本近似正交——用 seed 生成伪随机向量后 L2 归一化)。注释写明:这是测试/dev 的假实现,**真实 BGE-M3 ONNX 加载是后续任务**(配置里的 `kb_embedding_device` 是为它预留的); + - `build_embedding_provider(settings)` 工厂:按 `kb_embedding_model` 选择实现(当前只认 `deterministic-v1`,其它值抛 `AppError(CONFIG_ERROR)`)。 +2. **`packages/retrieval/repo.py`**: + - `upsert_chunks(session, entity_id, chunks, embedding)`:写 kb_chunk(embedding 用 `pgvector.sqlalchemy.Vector`;`embedding_model/version` 必填——**没有版本号的向量不可重跑**,这是 T-P2-03 的验收); + - `hybrid_search(session, query_vec, query_text, *, entity_types, occasion, body_shape, rule_boost, top_k)`:按架构 §8.3 的 CTE 结构实现(可用 `sqlalchemy.text()` 原生 SQL + 绑定参数,或 SQLAlchemy core;`embedding <=> :query_vec` 余弦距离、`ts_rank`、RRF:`1/(60+rank_vec) + 1/(60+rank_kw)`,再叠加权重与新鲜度 `(now-updated_at)` 归一化衰减); + - **硬过滤是第一公民**:`e.status='published'` + entity_types + occasion(`attrs->'occasions' ? :occasion`)+ 禁忌排除(`NOT (attrs->'avoid_body_shapes' ? :body_shape)`)——**硬过滤零漏**是 T-P2-04 红线,集成测试必须证明"禁忌实体即使向量最近也绝不出现"; + - `record_gap(session, query_text, filter_ctx)`:无结果时 upsert kb_gap(同 query_text + filter_ctx 则 `hit_count += 1`)。 +3. **`packages/retrieval/service.py`**:`RetrievalService.search(query_text, *, user_ctx) -> SearchOutcome`(编排 embed → hybrid_search →(结果为空时)record_gap);`SearchOutcome` 含 `results: list[ScoredChunk]`(chunk_id/entity_id/text/vec_score/kw_score/final_score)与 `degraded: bool`(无结果走了 gap 记录)。 + +## 4. 测试要求 + +- 单测 `tests/unit/test_embedding.py`:确定性(同文本同向量)、维度、L2 归一化、工厂的未知模型名拒绝。 +- 单测 `tests/unit/test_rerank.py`:RRF 融合 + 权重 + 新鲜度的数学正确性(构造已知分数断言最终序)。 +- 集成 `tests/integration/test_hybrid_retrieval.py`(新文件,用 DeterministicEmbedder,**不需要真实模型**): + 1. 建 3 个实体(1 个 published 带禁忌属性、1 个 draft、1 个 published 正常)+ 各挂 chunk → 检索命中正常实体、**draft 不出现、禁忌实体零出现**(T-P2-04 硬过滤零漏); + 2. 关键词命中路径(text 里放英文关键词,tsv 'simple' 分词); + 3. 无结果 query → kb_gap 出现一行、再查同 query → hit_count=2(T-P2-06); + 4. `tryon_ready=true` 的 kb_asset 可按 entity 查询(顺手验证 partial 索引可用)。 + +## 5. 禁改清单 + +通用禁改之外:`packages/kb/**`、`packages/rules/**`、`packages/identity/**`、所有路由文件。 + +## 6. 验收标准(T-P2-03/04/06) + +1. 向量写入带 `embedding_model/version`,换模型名即视为不同版本(可重跑判据); +2. 硬过滤零漏(集成断言禁忌/draft 实体绝不出现在结果); +3. RRF + 权重重排数学正确(单测); +4. 无结果 query 全量落 kb_gap 且可聚合排序(hit_count); +5. 全部检查绿(mypy 范围同 T6 列表 + `packages/retrieval`)。 + +## 7. 提交信息 + +``` +feat(retrieval): hybrid search over pgvector with hard filters and gap tracking + +DeterministicEmbedder(可换 BGE-M3 的接口位)+ kb_chunk 双索引写入 ++ 硬过滤→向量→关键词→RRF 重排管线 + kb_gap 缺口埋点(T-P2-03/04/06,TD-08)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T9-视觉算法SPIKE.md b/docs/开发任务包/T9-视觉算法SPIKE.md new file mode 100644 index 0000000..4c3e45f --- /dev/null +++ b/docs/开发任务包/T9-视觉算法SPIKE.md @@ -0,0 +1,76 @@ +# T9 · 算法验证 SPIKE·RTMPose 体型原型 + BiRefNet 抠图 + +> 对应任务:`T-P0-16`(RTMPose ONNX 单张耗时)、`T-P0-12`(BiRefNet 抠图质检 ≥90%)、`T-P0-10` 前置(体型测算原型)| 分支:`task/t9-vision-spike` | worktree:`/home/xsl/meida_backend-t9` +> 先读《README.md(协作规程)》,遵守全部纪律。**本任务是 SPIKE:产出是脚本 + 基准报告 + 结论,不是生产代码。** + +## 1. 目标 + +用本地可跑的最小代价回答三个 Sprint 1 硬门槛的**可行性问题**,结论写进报告(不达标要换方案,不要硬做): + +1. **RTMPose ONNX 在 CPU 上的单张推理耗时**(T-P0-16 验收:可接受即通过;开发机另有 RTX 5090,若 WSL 能用 GPU 也测一档,不行就只报 CPU——先探测 `nvidia-smi` 与 `onnxruntime` 的 CUDA 可用性,探测失败不折腾); +2. **BiRefNet ONNX 抠图的质量代理指标**(T-P0-12 的 ≥90% 质检通过率需要真实素材与人工标注,本 SPIKE 只建管线 + 代理指标 + 少量样图的主观核验,正式验收留给素材流水线); +3. **体型测算「掩膜宽度剖面法 + 关键点定高」原型**(TD-06):在合成人形图上跑通"关键点 → 定高 → 剖面宽度 → 肩/腰/臀比例 → H/A/V/O/X 初判",证明管线成立;**一致率 ≥80% 的正式验证需要人工标注数据(顾问轨),本任务不承诺**。 + +## 2. 已就绪的契约(协调提交已提供) + +- **依赖**:`spike` extra 已加入 pyproject(`onnxruntime`、`opencv-python-headless`、`numpy`),你的 worktree venv 已 `uv sync --extra spike` 装好。**GPU 版 onnxruntime 不装**(CUDA 匹配成本高,CPU 基准已满足 T-P0-16 验收口径)。 +- **目录**:`scripts/spikes/`(新建,归你)+ 模型缓存目录 `models/`(已在 `.gitignore`,**模型文件绝不入库**)。 +- **样图**:用脚本合成(OpenCV 画人形剪影/几何人体,见 §3),不使用任何真实人脸/人体数据(合规红线:测试不碰真实个人数据)。 + +## 3. 实现要点 + +允许改动:`scripts/spikes/**`、测试(可选,本任务以报告为验收)。其余全禁改。 + +### 3.1 模型获取(本批唯一允许联网的环节) + +- 优先 `HF_ENDPOINT=https://hf-mirror.com`(国内镜像)用 `huggingface_hub.snapshot_download` / `hf_hub_download` 下载 ONNX: + - RTMPose:`PINTO0309/PINTO_model_zoo` 里的 rtmpose(搜 `rtmpose` + onnx,选 **body 26/17 keypoint 的小模型**)或 `dhkim0115/rtmpose-onnx` 等社区导出;取不到就换 OpenMMLab 官方导出说明,**再不行降级:用 `opencv` 自带的 HOGDetector 作关键点占位并在报告声明**; + - BiRefNet:`zhengpeng7/birefnet` 的 ONNX 导出(或 `onnx-community/BiRefNet`);文件大(~1GB)下载慢的话选 **lite/general-lite** 变体,报告注明用的哪个; +- 下载脚本 `scripts/spikes/download_models.py`:带本地存在性检查 + 进度打印 + 失败重试一次;模型路径默认 `models/.onnx`,支持 CLI 参数覆盖; +- **超时纪律**:单个模型下载超过 ~10 分钟即放弃并走降级路线,把时间花在报告上。 + +### 3.2 `scripts/spikes/rtmpose_benchmark.py` + +- 加载 ONNX → 预处理(resize/归一化,按模型 meta)→ 推理 → 解码关键点(17 点 COCO 或 26 点,按模型); +- 输入:合成图 `scripts/spikes/assets/synthetic_person.png`(用 OpenCV 画:头圆 + 躯干梯形 + 四肢线段,肩/腰/臀位置可控——这也是 3.4 的输入); +- 耗时基准:预热 3 次 + 正式 20 次,报 **均值 / P95 / 单次最大**(`time.perf_counter`); +- 输出关键点坐标 JSON + 控制台表格。 + +### 3.3 `scripts/spikes/birefnet_matting.py` + +- 加载 ONNX → 推理 → 后处理(sigmoid + resize 回原尺寸 + 二值化阈值 0.5)→ 保存掩膜图与抠图预览到 `scripts/spikes/out/`(out/ 也 gitignore,报告里只留指标); +- 代理质检指标(T-P2-07 质检的雏形口径,写清楚这是代理不是正式验收): + - 前景像素占比(应在 15%~60% 合理区间); + - 最大连通域占前景比(>0.9 视为"主体单一完整"); + - 边缘羽化度(掩膜梯度带宽度均值); + - 抠图耗时(同 3.2 口径); +- 输入:合成人形图(与 3.2 同一张)+ 一张带渐变背景的变体。 + +### 3.4 `scripts/spikes/body_measure_prototype.py`(TD-06 原型) + +- 用 3.2 的关键点(或合成图直接已知的真值框)定肩线/腰线/臀线高度 → 在 3.3 的掩膜上取对应高度的**宽度剖面** → 肩宽/腰宽/臀宽比值; +- 规则初判:肩/臀比 >1.05 → V 型;<0.95 → A 型;肩≈腰≈臀 → H;腰远小于肩臀 → X;中段最大 → O(阈值写成常量并注明"待顾问校准"); +- 对合成图断言:**已知真值形状的分类正确**(合成图生成时就知道自己画的是 A 型还是 H 型——这就是最小可行的一致率验证)。 + +### 3.5 `scripts/spikes/REPORT.md`(**核心交付物**) + +结构:结论先行(三问三答,每问给"通过 / 有条件通过 / 不达标需换方案")→ 各基准数字表 → 用的模型与版本 → 降级与偏差 → 对 Sprint 1 正式验证的建议(需要什么数据、多少标注量)。 + +## 4. 验收标准 + +1. `download_models.py` + 三个 spike 脚本可重复执行(模型已缓存时离线可跑); +2. RTMPose CPU 耗时基准有数字(或降级路径有声明); +3. 体型原型在合成图上分类正确; +4. `REPORT.md` 三问三答齐全,结论可决策; +5. `ruff check` / `ruff format --check` 对 `scripts/spikes/**` 全绿;无 pytest 强制要求(脚本里的断言跑一遍即可,在报告里贴输出)。 + +## 5. 提交信息 + +``` +spike(vision): rtmpose latency, birefnet matting proxies, body-shape prototype + +T-P0-16/12/10 可行性验证:CPU 基准 + 代理质检指标 + 合成图体型剖面法 +原型 + REPORT.md 三问三答(Sprint 1 门槛的决策依据)。 + +偏差说明:<模型是否齐全、走了什么降级、写在这里> +``` diff --git a/docs/开发任务拆解.md b/docs/开发任务拆解.md index 8fee578..f4263e7 100644 --- a/docs/开发任务拆解.md +++ b/docs/开发任务拆解.md @@ -429,6 +429,7 @@ M2 知识库 M3 报告闭环 M4-a 换装/发型 │ | 日期 | 版本 | 变更 | | --- | --- | --- | +| 2026-08-14 | v1.5 | **两批多 agent 并行开发交付回写**(任务包见 `开发任务包/`,实测详情见《Sprint 执行计划》§9):`T-P1-25b` 四档熔断(阈值待 8-17 重算)、`T-P1-26` 收尾 claim、`T-P1-10/13/27` 交付、`T-P1-12/12b/12c` 登录双通道代码先行(联调待凭证)、`T-P2-01~06` 知识库三件套交付(BGE-M3 真模型待接)、`T-P0-16` ✅(2.8ms)、`T-P0-10/12` SPIKE 初步结论。**新风险登记:RTMPose 合成图置信度 0.003(域差异),真实照片重验为 `T-P0-10` 前置;BiRefNet CPU 3.1s/张需异步化** | | 2026-08-14 | v1.4 | 交叉评审回写:`T-P1-25b` 验收补「reserve 之后熔断」集成断言与日预算按新价重算;`T-P1-26` 明确种子必须与产品方案 §3.2 一致 | | 2026-08-13 | v1.3 | **后端脚手架落地后的回写**。新增两项由真机实测暴露、原 WBS 完全未覆盖的任务:**`T-P1-24a` 思考模式按场景显式配置** —— `deepseek-v4-flash` **默认开启**思考模式,实测同一单句问答开启 13.5s、关闭 1.8s,思考 token 还按输出价计费,漏配会同时打破 `NFR-PERF-02b` 与成本模型;**`T-P1-25c` 峰谷单价按调用时刻计价** —— DeepSeek 自 2026-08-17 起高峰单价为空闲时段 2 倍,用单一均价记账会让 `ob_ai_call` 与真实账单差出近一倍,而它是 V2 定价的唯一依据。另:`T-P1-01`/`02`/`06`/`07`/`24`/`25` 已在 `meida_backend` 仓库交付并实测通过,各任务的完成状态见《Sprint 执行计划》§1.1 | | 2026-08-13 | v1.2 | **文档一致性评审修订,补齐 9 项"需求已定但无任务认领"的缺口**。新增任务:`T-P1-12b` 短信验证码登录、`T-P1-12c` 短信防刷、`T-P1-16b` 清空我的照片、`T-P1-25b` **全局日成本熔断**(四档降级,验收含「配额净扣减 = 0」)、`T-P3-21` **报告结论认同/修正入口**、`T-P3-22` **全面报告预约留资**、`T-P5-17` 会员页留资、`T-P5-18` CLS 日志留存 ≥6 个月(`CR-11` 此前无人认领)、`T-P5-19` 熔断四档演练。其中 `T-P3-21`/`T-P3-22` 尤其关键 —— 前者是「结论认同率 > 70%、修正率 < 15%」的唯一数据来源,后者是 V1 唯一的商业化验证数据来源,**缺了会让 V1 白跑一遍**。另:**依赖图中的 M4 拆为 `M4-a` 换装/发型(可与 M3 并行)与 `M4-b` Agent 对话(须等 M2+M3)**,原图两处都标 M4 易误读为同一批工作;`T-P3-17` 注入预算 500→1400、`T-P4-03` 总输入 3700→4200;`T-P4-14` 明确输入校验须在预扣配额之前 | diff --git a/docs/文档评审报告-2026-08-13.md b/docs/文档评审报告-2026-08-13.md deleted file mode 100644 index 3781a0c..0000000 --- a/docs/文档评审报告-2026-08-13.md +++ /dev/null @@ -1,230 +0,0 @@ -# 「美搭」文档体系评审报告 - -> 评审日期:2026-08-13 | 评审范围:`docs/` 目录全部 11 份文档(README + 10 份内容文档) -> 评审方法:逐份通读 + 跨文档事实交叉比对(数字、枚举、编号、状态)+ 按《开发任务拆解》§9.3 的要求做「需求 → 任务」反查 -> 本文性质:评审反馈,不修改任何被评文档;问题定位到文档与章节,供修订时对照 - ---- - -## 0. 总体结论 - -**这套文档体系的骨架是好的,可以进入评审流程;但有一批"数字口径"类的事实性冲突需要先修掉,否则评审会上会卡在事实核对而不是方案讨论上。** - -值得肯定的地方(这些做得比多数项目文档好,应保持): - -- **四层结构与编号体系清晰**:FR/NFR/CR/TD/ADR/T/R 七套编号 + 「同一事实只在一处定义、其余引用编号」的纪律(README §4、§6),是这套文档能长期维护的前提。 -- **决策留痕完整**:每份文档有变更记录,技术选型报告 §8 有「与既有文档的冲突修正清单」,产品方案 §11 有已决策/待输入清单。本轮"备案完成、V1 不收费、素材自采"三个大转向在各文档间基本同步到位。 -- **验收标准可判定**:需求规格 §1.3 专门给出了"不可验收 vs 可验收"的对照写法,DoD(§1.4)把合规出口、配额三段式、埋点纳入完成定义。 -- **风险是真实践过的**:腾讯云人体关键点下线、RMBG-2.0 非商用许可、AI 标识元数据被 CDN 剥离,都是调研中实际撞出来的坑,文档如实记录并改了方案。 -- **对代理指标的诚实**:明确写出"预约 ≠ 付费"、V1 不收费意味着付费意愿无法被证伪(产品方案 §7.2.3、TD-28),没有自欺欺人。 - -但交叉比对发现:**v6.0/v1.1/v1.2 这轮大改版在"定性结论"上同步得很好,在"定量数字"上留下了不少残留矛盾**。下面按严重程度分三级列出。 - ---- - -## 1. P0:事实性冲突与错误(评审前必须修正) - -这类问题是"两个文档对同一事实给了不同答案",不修正则下游(开发、测试、财务)必有一方做错。 - -### P0-1 V1 成本封顶的数字算不平(最重要的一条) - -**位置**:产品方案 §5.2.1、§5.3.1;技术架构 §13 - -- 产品方案 §5.3.1:「月度 AI 总成本(MAU 15 万口径)≤ **¥12 万/月**」,「单免费用户月度 AI 成本 ≤ **¥8**」,并声称「月度成本上限 = MAU × 单用户配额成本上限,**是可以事前算准的确定值**」。架构 §13 复述了同一口径。 -- 但 15 万 × ¥8 = **¥120 万**,与 ¥12 万相差 10 倍。两个数字不可能同时成立。 -- 再验算 ¥8 本身:按配额表基础额度(每日 3 次换装 ×¥0.5 + 3 次发型 ×¥0.2 + 10 轮对话 ×¥0.05 ≈ ¥2.6/日),若用户每日用满,月成本约 **¥78**;若按硬上限(8+6+40 次)算是 ¥246/月。¥8 只有在"月均活跃约 3 天"的使用率假设下才成立,但这个假设**没有写在任何文档里**。 -- 后果:这是 V1 唯一的财务锚点,且文档把它定性为"由配额硬性决定的确定上限"。按文中公式算出来的真硬上限(百万级/月)与文中给出的 ¥12 万相差两个数量级,**"成本可事前算准"这个论证目前不成立**。 - -**建议**:把两个口径分开写清楚——① 真·硬上限 = MAU × 配额硬上限 × 单次成本(数字会很大,但它才是"确定值");② 预期成本 = 硬上限 × 使用率假设(把活跃天数/功能渗透率的假设显式写出来,并标注为假设)。¥12 万和 ¥8 按口径②重新推导或修正。 - -### P0-2 诊断维度数:技术架构文档仍写 34 项 - -**位置**:技术架构 §6.2(`pf_diagnosis` 行注释「34 个诊断维度」)、§6.3(「34 个诊断维度用 JSONB 存储」) - -TD-31 已决定维度由 34 减为 **32**(删除五行分布、幸运色),美学报告 §4、需求规格 FR-RPT-06、README §0.1/§5、任务拆解 T-P0-25/T-P3-06 均已同步为 32,**只有技术架构文档两处遗漏**。这是"本轮决策同步"的明确漏网之鱼。 - -### P0-3 性能指标至少四处互相矛盾 - -同一指标在不同文档给出不同数值,且需求规格 §4.1 并未被其他文档遵守为单一事实源: - -| 指标 | 需求规格 §4.1 / 架构 §12.1 | 产品方案 §8.3.1 | 模块文档 | 冲突 | -| --- | --- | --- | --- | --- | -| 初级报告 P95 | **< 90s**(NFR-PERF-03) | **< 60s** | 美学报告 §10.2:< 60s | 90 vs 60 | -| 全面报告 P95 | **< 180s**(NFR-PERF-04) | **< 5min(300s)** | 美学报告 §10.2:≤ 5min | 180 vs 300 | -| 换装出图 P95 | **< 40s**(NFR-PERF-05,不含排队) | **< 320s** | 产品方案 §2.1.1:前端超时 ≥ 320s | 40 vs 320 | -| Agent 整轮 P95 | **< 8s**(NFR-PERF-02) | — | 穿搭 Agent §7.1/§8.3:**< 15s** | 8 vs 15 | -| 系统可用性 | ≥ 99.5%(NFR-REL-01、架构 §12.1) | **> 99.9%** | — | 停机预算差 5 倍 | - -换装一条特别需要说明:40s 与 320s 可能分别是"改造后目标"与"当前接口现实",但没有任何文档写明这层关系,FR-TRY-01 的验收标准(p95 < 40s)按现有接口实测很可能直接不达标。 - -**建议**:性能指标只保留需求规格 §4.1 一处定义,其余文档改为引用;换装/发型补充"现状基线(T-P0-13/14 实测)→ 目标值"的两段式表述。 - -### P0-4 记忆注入的 token 预算有两套互相矛盾的口径 - -**位置**:产品方案 §2.4.2、用户系统 §3.8、穿搭 Agent §3.5.2 vs 需求规格 FR-MEM-04、架构 §7.2、穿搭 Agent §10.2、任务拆解 T-P3-17 - -- 口径 A(≤1400):画像 ≤250 + 偏好 ≤150 + 事件召回 ≤200 + 会话 ≤800(产品方案、用户系统、Agent §3.5.2 一致)。 -- 口径 B:架构 §7.2 与 Agent §10.2 的预算表为「画像摘要 ≤500、会话摘要 ≤400」(总输入 ≤3700 的分解);FR-MEM-04 验收标准写「注入 token ≤ 500」,T-P3-17 照抄。 -- 直接冲突点:**会话摘要 800 vs 400**;**记忆注入合计 1400 vs 500**。验收标准(FR-MEM-04)与需求原文(用户系统 §3.8)不一致,测试不知道该按哪个写。 - -**建议**:在架构 §7.2 定义唯一的预算分解(建议明确"记忆注入预算"与"总输入预算 3700"的包含关系),FR-MEM-04 与其余文档全部引用该处。 - -### P0-5 人脸原图保留期:2 小时 vs 24 小时 - -**位置**:合规方案 §2.1.2/§4.1、CR-02、架构 §7.1/§11.2、任务拆解 T-P1-27 = **≤ 2 小时**;产品方案 §7.1.3、美学报告 §9.1 = **≤ 24h 仅用于任务重试** - -这是合规红线参数,两套说法直接冲突。"24h 用于任务重试"与"处理后显式删除、桶级 2 小时兜底"是两种不同的设计(前者意味着原图要留存到任务结束)。按"下层优先"原则应以合规方案/CR-02 的 2 小时为准,修正产品方案与美学报告;若确实需要重试窗口,则合规方案要补充论证。 - -### P0-6 配额流水类型:两处写"两种",两处写"三种" - -**位置**:需求规格 §2.9 架构约束、产品方案 §3.3 = V1 只用 `system_grant` + `consume` **两种**;用户系统 §2.4.2、架构 §6.2 = `system_grant` / `consume` / `refund` **三种** - -`refund` 是预扣三段式(FR-BILL-05)的必需类型,需求规格与产品方案的表述属于漏写。按"表结构真相在架构"的原则,应以三种为准,修正需求规格 §2.9 与产品方案 §3.3。 - -### P0-7 美学报告文档内的价格残留(与同节注释自相矛盾) - -**位置**:美学报告 §2.5、§2.1 - -- §2.5 正文 bullet 仍写「预览卡片(模糊化目录截图 + 内容清单 + **预计价格**)」、埋点记录含「**点击价格档位**」;紧随其后的 ⚠️ 注释明确说这两项"已移除"。同一节内自相矛盾。 -- §2.1 分级总览表「价格」行仍写全面报告「**单次 ¥29.9**」,与 §2.5「全部为占位草案,待定」及"V1 全线不出现价格"的纪律(FR-BILL-07、TD-28)冲突。 - -**建议**:删除 §2.5 正文中的「预计价格」「点击价格档位」,§2.1 的 ¥29.9 改为"待定(V2 按真实数据定价)"。 - -### P0-8 全面报告"40+ 项维度"与 32 项维度字典矛盾 - -**位置**:产品方案 §2.2.1(「8 项核心 / **40+ 项完整**」)、美学报告 §2.1(同)vs 美学报告 §4 维度字典(**共 32 项**)、README §5 - -字典总共 32 项,全面报告不可能覆盖 40+ 项。这是五行模块移除(34→32)之前就遗留的旧口径。另外"初级 8 项核心"与字典中初级打 ✅ 的维度数(约 15 项)也对不上——建议把分级表改为"初级覆盖 X 项 / 全面覆盖全部 32 项"的准确口径,或明确"8 项"指的是首屏核心展示数。 - -### P0-9 语音输入在穿搭 Agent 文档中被列为 P0 - -**位置**:穿搭 Agent §2.2(P0 能力范围含「文字对话(**语音输入转文字**)」)、§2.1(能力总览「文字/语音多模态输入」标 P0) - -FR-AGT-17 与 TD-14 均明确语音输入为 **P1 / V2**(Core Speech Kit),本文 §9.1/§9.2 的阶段划分也把语音放在 Phase 2。§2.2 属于改版残留,与需求规格直接冲突。 - -### P0-10 画像/记忆/同意的数据结构与规则在架构与用户系统间不一致 - -**位置**:技术架构 §6.2、§8.4 vs 用户系统 §2.2、§3.3、§3.5 - -| 项 | 架构文档 | 用户系统文档 | -| --- | --- | --- | -| `pf_memory.status` 枚举 | active / pending_confirm / **archived / rejected** | active / pending_confirm / **decayed / overridden / deleted** | -| `pf_memory.source` 枚举 | explicit / implicit / report / behavior | explicit_user_input / user_setting / report_vision / self_reported / behavior_inferred / llm_extracted | -| 显式陈述初始置信度 | **0.95**(§8.4) | **0.90**(§3.5;产品方案 §2.4.2 同为 0.9) | -| 同意类型枚举 | terms / privacy / face / **minor** | 隐私政策 / 人脸 / **个性化推荐 / 推送通知** | -| 手机号存储 | `id_user` 字段清单中**无 phone**,`id_user_identity` 仅示例 huawei | 用户主表含 `phone`(加密存储) | - -其中同意类型枚举的偏差有合规后果:用户系统 §3.10 要求记忆写入建立在「个性化推荐」授权之上,但架构的 `id_consent` 枚举里没有这一项。建议以用户系统文档的语义为准修订架构 §6.2,并补齐手机号的存储位置说明。 - -### P0-11 准确率门槛两套数值 - -**位置**:FR-RPT-05 与 T-P0-10/11(体型 ≥80%、季型 ≥75%)vs 美学报告 §9.4、产品方案 §8.3.3(体型 ≥85%、季型 ≥80%);FR-AGT-02(意图识别 ≥85%)vs 穿搭 Agent §8.2(>90%) - -可能是"Phase 0 验证门槛"与"上线质量门槛"的区分,但没有任何文档写明这层关系。建议显式标注两档(如:验证门槛 80/75,上线门槛 85/80),否则 Phase 0 验收与上线验收会打架。 - ---- - -## 2. P1:覆盖缺口(需求 → 任务反查发现) - -按任务拆解 §9.3 的要求做了 P0/CR 需求反查,以下缺口会直接影响执行: - -### P1-1 手机号验证码登录:无开发任务、无短信通道选型(最实际的缺口) - -- FR-ACC-04 是 P0/V1(登录兜底通道),但任务拆解 Phase 1 §3.2(T-P1-11~18)**只有华为账号登录任务,没有手机号验证码任务**。 -- 更大范围看:技术选型 31 项决策中**没有短信服务商选型**;架构 §2 外部依赖表与系统上下文图均无短信通道;成本模型 §13 未计短信成本;NFR-SEC-05 防刷未提短信轰炸防护。 -- 建议:补一项短信通道选型决策(TD-32)+ 一个开发任务(验证码下发、校验、限频、防刷),并在外部依赖表中登记降级策略。 - -### P1-2 全面报告「预约留资」入口无 WBS 任务 - -产品方案 §2.2.3 与 §6.1.2 均要求 V1 做「全面报告预约入口 + 埋点」,**这是 V1 唯一的商业化验证指标(预约率 ≥8% 决策门槛)的数据来源**;FR-RPT-15 验收标准也含"V1 仅做预约留资"。但任务拆解 Phase 3(T-P3-01~20)没有对应任务。漏掉它,V1 上线后连"要不要做付费报告"都回答不了。 - -### P1-3 会员页静态展示 + 「上线提醒我」留资:无 FR、无任务 - -产品方案 §2.8(🅿️ 区)与 §3.4 均要求 V1 交付,但需求规格中无对应 FR 编号,WBS 无任务。建议补 FR-BILL-xx(或 FR-ACC-07 扩展)与 Phase 5 任务。 - -### P1-4 报告结论的反馈与修正入口无 FR - -产品方案 §8.1.2/§8.1.4 的核心质量指标——**报告结论认同率 >70%、结论修正率 <15%**——依赖"用户主动确认/修正"的采集入口(美学报告 §7.2「不准?修正一下」、§9.4 反馈入口)。需求规格中没有对应 FR(FR-RPT-10 的验收不含此项),WBS 中 T-P4-11 只覆盖 Agent 反馈。指标没有数据来源就等于没有指标。 - -### P1-5 游客配额未进入配额表,FR-ACC-02 验收不可判定 - -游客额度(1 次换装 / 1 次试戴 / 1 次报告 / 3 次对话)只出现在产品方案 §2.4.1 与用户系统 §2.1 的叙述中,**没有进入配额表**(产品方案 §3.2 / 用户系统 §2.4.1 均只有注册用户行),违反自定的"单一事实来源"纪律。同时 FR-ACC-02 的验收标准「体验流程中被允许的部分」不可判定,建议把游客额度补入配额表并在 FR-ACC-02 引用。 - -### P1-6 天气数据源完全未选型 - -Agent 的意图类别含「天气适配」(穿搭 Agent §3.2)、典型场景 §4.4 直接演示"明天 15 度有雨怎么穿"、知识库场景实体含温度带映射(§3.2 ⑥)、L0 城市字段的用途写明"天气定位"——但全部文档中**没有天气 API 的来源、选型与成本说明**。V1 的 Agent 要回答天气类问题就需要它。建议补一项选型决策(或明确 V1 天气问题降级为"让用户告知天气")。 - -### P1-7 「清空照片」入口无 FR - -用户系统 §2.6 与产品方案 §2.4.1 均承诺"清空照片"作为账号生命周期能力,需求规格无对应 FR(FR-ACC-09 注销、FR-ACC-10 导出之外缺少"清空照片"),合规检查清单 §5.3 也未覆盖。涉及人脸数据删除,建议补 FR 并纳入合规清单。 - -### P1-8 配额模型中 `bl_quota` 与 `bl_wallet`/`bl_ledger` 的关系未说清 - -架构 §6.2 同时存在周期配额表(`bl_quota` + `bl_quota_rule`)与余额账本(`bl_wallet` + `bl_ledger`),§7.4 校验公式为 `bl_quota.used < limit_value + task_bonus`——但 `task_bonus` 从哪里来(是 `bl_ledger` 中 system_grant 的周期聚合?)、`bl_wallet.balance` 在 V1 纯配额模式下代表什么,没有定义。两套机制并存但职责边界模糊,开发时会卡住。建议在架构 §7.4 补一段"V1 配额判定数据流"的明确说明。 - -### P1-9 两个编号/归属问题 - -- **TD-00 被引用但从未定义**:需求规格 FR-TRY-01 依赖列写「TD-00 改造」、架构 §4.4 写「(TD-00 前提)」,但技术选型报告的决策总表从 TD-01 开始。§0「已确认的项目前提」中的"换装/发型服务自有可控"应编号化为 TD-00。 -- **CR-11 日志留存 ≥6 个月**无明确落地任务(T-P1-08 只含日志脱敏,CLS 留存周期配置无人认领),建议在 T-P1-04 或 T-P1-08 的验收标准中补上。 - ---- - -## 3. P2:一致性与维护性问题(不阻塞评审,但应一并修) - -| # | 问题 | 位置 | 说明 | -| --- | --- | --- | --- | -| P2-1 | **五份文档头部版本号滞留 v1.0** | 需求规格、技术架构、技术选型、合规方案、任务拆解的页首 | 五份文档的变更记录均已升至 v1.1,README §8 也按 v1.1 引用,但页首仍写 v1.0。本轮新增的五份文档全部中招,建议统一刷新 | -| P2-2 | README §8 遗漏一份文档 | README §8「九份文档全部更新」 | 内容文档实为 **10 份**,表格 9 行漏了《穿搭 Agent-需求文档》(当天同样更新至 v1.2) | -| P2-3 | 文档状态口径不一 | README §7 vs 各文档页首/页尾 | README 写「产品需求已定稿」,但产品方案、美学报告、用户系统、知识库、Agent 五份文档自身状态均为「待评审」 | -| P2-4 | "点数/扣点/计费"措辞残留 | 合规 §2.4.1("不消耗点数/退点")、架构 `tk_task.points_reserved` 字段名、架构 §7.1("不重复扣点")、FR-TASK-05、TD-11、产品方案 §2.7("不重复计费")、TD-23 埋点清单含"点数消耗/付费页曝光/下单/支付成功" | V1 已统一为"配额"语义,这些 V2 词汇残留会造成实现期误解;TD-23 的 V2 事件建议标注版本 | -| P2-5 | 枚举计数错误 | 知识库 §3.1 ⑤「8 脸型映射」vs 详情与美学报告 §4 均为 **7 种脸型**;§3.2 ④「局部特征 12 项」实际列了 **13 项** | 小数字错误,但这类错误多了会损害文档可信度 | -| P2-6 | 知识库规模口径需单一事实源 | 单品:知识库 §3.1「冷启动 800~1500」、§9「长期 800 / V1 硬指标 300」、产品方案 §2.5.2「800 条(≥400 可试穿)」、§2.8「300 件」;规则:知识库 §3.1「800~1200」、§9 表「第一版 800」、§9 文末分期「V1 400」、产品方案 §2.5.2「800」、§2.8「400」 | 产品方案 §2.5.2 的"第一版目标量级"列与 §2.8 直接矛盾;知识库 §9 表格与其自己的分期建议矛盾。建议统一为:V1 = 规则 400 / 单品 300 tryon_ready / 方案 100,长期目标另列 | -| P2-7 | 黄金集规模口径需分列 | 知识库 §7「200 条」、美学报告 §9.4「100 例」、FR-KB-11「≥100」、FR-AGT-05「100 例」、FR-RPT-07「30 例」、产品方案 §6.1.2「200 条」、知识库 §5.1.1「100~200 例」 | 各数字可能对应不同子集(检索/规则/Agent/报告),但没有一处说清。建议在 FR-KB-11 或知识库 §7 定义各子集规模,其余引用 | -| P2-8 | 若干净期承诺与"按产能倒排"原则冲突 | 产品方案 §4.3.3「1-2 个月上线 MVP」、§6.1 标题「Phase 1:MVP(1-2 个月)」、§9.3「1 个月内…正式上线」 | 本轮已把排期原则改为"不按人日估算、按实测产能倒排"(知识库 §5.1 还专门删了"3~4 周"估算),这几处时间盒是改版前残留,建议改为以里程碑出口条件表述 | -| P2-9 | 其他小项 | ① 架构 §12.2 容量基线 1 万 DAU < 产品方案 §8.1.1 目标档 DAU 2 万,建议对齐或说明假设;② 穿搭 Agent §7.1「1000+ 并发对话」相对容量基线缺乏依据;③ FR-TASK-04「推送到达率 ≥90%」未定义分母(全体用户还是已授权用户,TD-17 指出通知授权默认关闭);④ 产品方案 §9.1.4 仍列「市场数据信源补齐」待办、§10.4.1 仍挂三条"待确认"行业报告,与 §4.1 已完成信源替换矛盾;⑤ 美学报告 §2.1「首次不限」与配额表「首次 1 次(终身)」措辞歧义;⑥ README §0.3「其余 20 项决策」与产品方案 §11.1 实际 21 行;⑦ 任务拆解 §1 依赖图中 M4 编号同时指"换装/发型"与"Agent 对话"两块,易误读;⑧ 需求规格 §1.3 示例(p95 < 90s)与产品方案 §2.2.1(≤60s)矛盾,同 P0-3 | — | - ---- - -## 4. 修订建议与处理顺序 - -**第一优先(评审会前)**:P0-1(成本口径)与 P0-3(性能指标)。这两条会被任何认真读文档的人第一时间发现,且都涉及"哪个文档是单一事实源"的问题——正好借修订把纪律落实: - -| 事实 | 建议的唯一定义位置 | 其余文档 | -| --- | --- | --- | -| 性能指标 | 需求规格 §4.1 | 产品方案 §8.3.1、各模块文档改为引用 | -| 配额数值(含游客) | 产品方案 §3.2 | 用户系统 §2.4.1 引用 | -| Token 预算 | 架构 §7.2 | FR-MEM-04、Agent §3.5.2/§10.2、用户系统 §3.8 引用 | -| 诊断维度数与字典 | 美学报告 §4 | 架构 §6.2/§6.3、README §5 引用 | -| 知识库规模目标 | 知识库 §9 | 产品方案 §2.5.2/§2.8 引用 | -| 黄金集规模 | 知识库 §7 或 FR-KB-11 | 其余引用 | -| 准确率门槛 | 美学报告 §9.4(区分验证档/上线档) | FR-RPT-05、产品方案 §8.3.3 引用 | -| 人脸原图保留期 | 合规方案 §2.1.2 / CR-02 | 产品方案 §7.1.3、美学报告 §9.1 引用 | - -**第二优先(进入开发前)**:P1-1(短信通道)、P1-2(预约留资任务)、P1-8(配额数据流)。这三项会直接卡住开发或让 V1 失去关键数据。 - -**第三优先(随下一轮文档更新)**:P0 其余项与全部 P2。建议按 README §6 的变更留痕规范逐份修订并登记。 - -**流程建议**:把"数字一致性走查"加入文档评审 checklist——本轮的问题高度集中在数字(维度数、预算数、成本数、性能数、规模数),人工通读很难避免,建议在每次大改版后专门做一次"同一数字全局搜索"(如搜 `34`、`500`、`1400`、`90s`、`300`)。 - ---- - -## 5. 附:评审覆盖清单 - -| 文档 | 版本(自称) | 实际应为 | 主要问题分布 | -| --- | --- | --- | --- | -| README | — | — | P2-2、P2-3、P2-8⑥ | -| 产品方案 | v6.0 | v6.0 ✓ | P0-1、P0-3、P0-4、P0-5、P0-6、P0-8、P2-4、P2-6、P2-8、P2-9 | -| 需求规格说明 | v1.0(页首滞留) | v1.1 | P0-3、P0-4、P0-6、P0-11、P1-1~P1-7、P2-1 | -| 美学报告-需求文档 | v1.2 | v1.2 ✓ | P0-5、P0-7、P0-8、P0-11、P2-9⑤ | -| 穿搭Agent-需求文档 | v1.2 | v1.2 ✓ | P0-3、P0-4、P0-9、P0-11、P2-9② | -| 用户系统与记忆系统-需求文档 | v1.2 | v1.2 ✓ | P0-4、P0-6、P0-10(作为正确口径基准) | -| 美学知识库-建设方案 | v1.2 | v1.2 ✓ | P2-5、P2-6、P2-7 | -| 技术架构文档 | v1.0(页首滞留) | v1.1 | P0-1、P0-2、P0-3、P0-4、P0-10、P1-8、P1-9、P2-1、P2-4、P2-9① | -| 技术选型与调研报告 | v1.0(页首滞留) | v1.1 | P1-1(缺短信选型)、P1-6(缺天气选型)、P1-9(TD-00)、P2-1、P2-4 | -| 合规与安全方案 | v1.0(页首滞留) | v1.1 | P0-5(作为正确口径基准)、P2-1、P2-4 | -| 开发任务拆解 | v1.0(页首滞留) | v1.1 | P1-1、P1-2、P1-3、P1-4、P1-9、P2-1、P2-9⑦ | - ---- - -**评审人**:AI 辅助评审 -**说明**:本报告仅基于文档文本的交叉比对,未验证文档与真实世界的一致性(如备案材料内容、接口实际表现)。P0-1 涉及的成本推算过程已在 §1 中给出算式,修订时可直接复算。 diff --git a/docs/项目整体评审报告-2026-08-14.md b/docs/项目整体评审报告-2026-08-14.md deleted file mode 100644 index a2b2bc3..0000000 --- a/docs/项目整体评审报告-2026-08-14.md +++ /dev/null @@ -1,212 +0,0 @@ -# 「美搭」项目整体评审报告 - -> 评审日期:2026-08-14 | 评审人:AI 辅助评审 -> 评审范围:**两个仓库全量** —— 文档仓库 `G:\meida`(13 份文档 + reference 资料)与后端仓库 `meida_backend`(WSL `/home/xsl/meida_backend`,HEAD `c97f8ae`,10 个提交) -> 评审方法:文档逐份通读 + 后端代码结构/核心模块审查 + **文档 ↔ 代码交叉核对**(表名、字段名、种子数据、API 路径、配额数值)+ 对《文档评审报告-2026-08-13》所列 P0 修复项的落地抽查 -> 本文性质:评审反馈,不修改任何被评文档与代码;问题定位到文档章节或代码文件,供修订时对照 - ---- - -## 0. 总体结论 - -**这个项目的整体质量显著高于同阶段的常见水平,且"自我认知"准确 —— 文档没有夸大、代码没有虚报。主要风险不在工程能力,而在三处:一个躺在工作区里的 API 密钥、一批"文档说改了但代码没改(或反之)"的漂移点、以及既定的内容产能关键路径。** - -分项结论: - -| 维度 | 评级 | 一句话 | -| --- | --- | --- | -| 文档体系 | **A-** | 四层结构 + 单一事实源纪律执行到位,上轮评审的 P0 修复**抽查全部落地**;遗留少量表名/仓库名漂移 | -| 后端架构与工程化 | **A** | import-linter 四契约、CI 六道防线、迁移往返、双密钥扫描 —— "用机器替 review 守纪律"的做法少见地完整 | -| 后端实现完成度 | **B(符合预期)** | 工程地基生产级 + 一条真花钱验证过的纵向切片;业务域 9 个包刻意占位,与 Sprint 计划的自我标注(🟡)一致,无虚报 | -| 文档 ↔ 代码一致性 | **C+** | **配额种子数据与唯一事实源不符、`points_reserved` 改名"假回写"** 两处必须修;另有若干小漂移 | -| 安全卫生 | **⚠️ 有一个 P0** | `reference/deepseek.txt` 存有 API 密钥,未被 .gitignore 覆盖,`git add -A` 即入库 | - ---- - -## 1. 项目现状全景 - -### 1.1 文档仓库(G:\meida) - -13 份文档构成四层体系(为什么做 → 做什么 → 怎么做 → 谁来做/何时做),7 套编号(FR/NFR/CR/TD/ADR/T/R),README §4.1 建立"单一事实源索引"锁定 13 类关键数字的唯一定义位置。关键业务前提已全部收口:备案已完成(转持续义务)、V1 完全不收费(配额为唯一成本闸门)、素材全部自采(成为新关键路径)、诊断维度 32 项、五行/命理永久不做。 - -### 1.2 后端仓库(meida_backend) - -| 层 | 状态 | -| --- | --- | -| `apps/`(api / worker / admin) | api ✅(10 个路由,1 条纵向切片)、worker ✅(ping 任务 + 4 队列)、admin 🟦 占位 | -| `packages/`(20 个包) | domain / llm / shared / identity / billing / task / agent ✅ 实现;compliance ⚠️ 仅接口(全 `NotImplementedError`);kb / memory / notify / profile / report / retrieval / rules / tryon / wardrobe 🟦 占位(README + 空 `__init__`) | -| `services/`(vision / matting / tryon) | 🟦 全占位 | -| 数据库 | 2 个迁移:0001 建 **8 张表** + pgvector 扩展(向量列未建);0002 给 `ob_ai_call` 加 `tokens_reasoning`/`rate_tier` | -| 测试 | 142 个测试函数(16 个单测文件 + 1 个集成切片);`scripts/smoke.sh` 真调 DeepSeek 端到端 | -| 工程化 | ruff + mypy(domain/llm strict)+ import-linter(4 契约)+ pytest + alembic 往返 + detect-secrets/gitleaks,CI 单 job 六道防线 | - -**已打通的真实链路**:`POST /api/v1/agent/chat` → JWT 校验 → 配额预扣(`bl_ledger` reserved 行)→ 真实调用 DeepSeek(场景路由 / 熔断器 / 峰谷计价 / 思考模式显式关闭)→ 成本行落 `ob_ai_call`(独立事务)→ 配额 commit / 失败自动 refund。 - ---- - -## 2. 文档体系评审 - -### 2.1 做得好的(应保持) - -1. **单一事实源纪律真的执行了,不只是口号**。抽查验证:FR-BILL 引用产品方案 §3.2 而不复述数值;用户系统 §2.4.1 明确"本节不再复述数值";架构 §7.2 token 预算收口 4200/1400 并写明修正原因(原表漏算近轮原文)。 -2. **上轮评审(2026-08-13)的修复可信**。抽查 5 项 P0:维度 34→32(架构 §6.2 已改)、可用性 99.9%→99.5%(全库无 99.9% 残留)、人脸保留期 24h→2h(美学报告 §9.1 显式标注废弃原因)、token 两套口径→4200/1400(含错因分析)、流水类型两种→三种(FR-BILL-02 原文更正并留痕)。**全部落地**。 -3. **风险记录是真实调研出来的**,不是模板:腾讯云人体关键点接口下线(2025-10-30)迫使自建 RTMPose、RMBG-2.0 CC BY-NC 许可禁用改 BiRefNet、AI 标识元数据会被 CDN/压缩剥离、DeepSeek 2026-08-17 峰谷涨价(空闲 ¥1.5/¥4.5、高峰 ¥3/¥9 每百万 token)——且都给出了应对与任务编号。 -4. **对代理指标的诚实**:明确"预约 ≠ 付费"、V1 不收费意味着付费意愿不可证伪、"配额封不住总成本"的自我纠错(架构 §13 改为三口径)。这类不自欺的表述是文档可信度的来源。 -5. **Sprint 计划对已交付部分的验收标注与代码实况相符**(🟡 标注诚实,见 §4.3 核对)。 - -### 2.2 遗留问题 - -| # | 级别 | 问题 | 位置 | 说明 | -| --- | --- | --- | --- | --- | -| D-1 | **P1** | **用户系统文档表名未与架构/代码对齐** | 用户系统 §2.2、§3.9 | 仍用 `user`/`user_consent`/`memory_audit` 表名叙述,`phone`/`huawei_open_id` 画在主表;而架构 §6.2 与实际代码是 `id_user`/`id_consent`/`pf_memory_event`,手机号在 `id_user_identity`(phone_enc + phone_hash)。上轮修了枚举与置信度(已对齐),**表名这层漏了** | -| D-2 | P2 | 架构 §16.1 仓库根目录写 `meida/`,实际为 `meida_backend` | 技术架构文档:999 | 其他文档(任务拆解、Sprint 计划)均已用 `meida_backend` | -| D-3 | P2 | Sprint 计划 §1.1 写"`0001` 建 **9** 张表",实际 **8** 张 | Sprint执行计划.md §1.1 | 迁移文件 `op.create_table` 计数 = 8 | -| D-4 | P2 | memory 是 profile 域子模块还是独立包,架构 §4.1 与 §16.1 口径不一 | 技术架构文档 | §4.1 列 11 个业务域(profile 含记忆),§16.1 的 packages 把 `profile/` 与 `memory/` 列为两个包;实际代码是两个独立包(与 §16.1 一致)。建议在 §4.1 补一句判别口径 | -| D-5 | 备注 | 鉴权细节(refresh token 轮换、JWT claims、服务间令牌签发)与 API 限流阈值在文档层仍偏薄 | 架构 §9/§11 | 属 T-P1-13 与压测(T-P5-06)范围,开发前需细化 | - ---- - -## 3. 后端代码评审 - -### 3.1 做得好的(生产级,保持) - -1. **架构纪律由机器强制,不靠自觉**:import-linter 四契约(分层单向、domain 禁框架/IO、业务包禁直连 provider、apps 互不依赖)进 CI;`packages/domain` 是纯算术(雪花 ID 含时钟回拨处理、配额三段式、任务状态机),mypy strict。 -2. **LLM 网关是全仓库最完整的一块,且踩点准确**: - - **峰谷计价赶在 2026-08-17 生效前实现**(`PeakAwareRateTable` 按调用时刻北京时间判档,`rate_tier` 与 `cost_cents` 同刻落库,事后不重算); - - **思考模式显式开关**(V4 默认开启,实测开启 13.5s / 关闭 1.8s,思考 token 按输出价计费——四个场景全部显式关闭); - - 成本行**独立事务落库**(请求回滚时成本行仍存活)+ Prometheus 双写; - - prompt 缓存前缀纪律由 `validate_cache_prefix()` 静态扫描强制(volatile token 检测),strict 模式直接抛错; - - 4xx 不可重试(避免在备用 provider 烧两次钱)、熔断器三态可注入 clock、结构化输出重试把模型回答带回修正。 -3. **配额三段式正确**:reserve/commit/refund + 幂等键派生 + `balance_after` 流水可重算对账;DeepSeek 超时自动退还配额(集成测试覆盖)。 -4. **错误码体系**:4xxxx 客户端 / 5xxxx 服务端 / **6xxxx 外部依赖**(把"我们的 bug"与"厂商故障"分开),配额耗尽文案经测试断言**不含价格字样**(FR-BILL-07 的验收在测试里真实生效)。 -5. **占位是显式的、带启用前置的**:智谱 provider 与 compliance 全部 `raise NotImplementedError` 并注明启用条件(备案覆盖核实、审核+水印+元数据齐备)。"宁可不实现也不半实现"(half-working moderation looks like a working one)这个判断正确。 - -### 3.2 代码问题与风险 - -| # | 级别 | 问题 | 位置 | 说明与建议 | -| --- | --- | --- | --- | --- | -| C-1 | **P1** | **CI 不跑集成切片** | `.github/workflows/ci.yml:88` | CI 的 pytest 步骤未设 `MEIDA_RUN_INTEGRATION=1`,而 pytest 默认 `-m 'not integration'`——**集成切片在 CI 里被静默跳过**。讽刺的是 CI 明明起了 pgvector + Redis service 容器(目前只服务于 alembic 往返)。建议在 CI 加一步 `MEIDA_RUN_INTEGRATION=1 uv run pytest -m integration`(容器已就位,零额外成本),否则纵向切片回归只靠本地 `make integration` | -| C-2 | **P1** | **四档熔断降级未实现,且当前熔断位置与 NFR-COST-02 的退款要求需验证** | `packages/llm/budget.py` | 当前只有单一日预算阈值(`LLM_DAILY_BUDGET_CENTS`)+ critical 场景豁免;60/80/90/100% 四档降级是 T-P1-25b 待补(Sprint 计划已列,属实)。需注意:**若 `DailyBudgetExceededError` 在配额 reserve 之后抛出,必须走 refund**——建议给 T-P1-25b 的验收加一条"被拒请求账本净变动 = 0"的集成断言(Sprint §1.2 已写此验收,落实现时别丢) | -| C-3 | **P1** | **日预算阈值未按 8-17 峰谷新价重算** | `.env.example`(`LLM_DAILY_BUDGET_CENTS=100000`) | Sprint §1.3 已自我提醒(输出侧涨 2.25~4.5 倍),此处登记为待办:**8-17 之后用实测 token 分布重算**,否则熔断会在错误位置触发 | -| C-4 | P2 | 集成测试仅 1 个文件 3 个用例,FakeProvider 替换真实 provider | `tests/integration/` | 真实 provider 行为(超时映射、429、reasoning token 解析)只有单测级的 respx mock 覆盖。可接受,但 C-1 修复后建议补"预算熔断 + 退款"与"幂等键重放"两个集成用例 | -| C-5 | P2 | `ob_ai_call` 无保留/归档策略 | 迁移 0001 | 成本明细表只增不减,长期需要分区或归档策略(V2 定价依赖它,不能简单清理)。可在架构 §6.2 补一句保留口径 | -| C-6 | 备注 | dev-token 双开关(`DEV_AUTH_ENABLED` && `is_local`)设计正确,但上线检查清单应含"确认 prod 无 dev-token 路由" | `apps/api/routers/auth.py` | 建议纳入 T-P5 合规检查清单 | - -### 3.3 与 Sprint 计划自我标注的核对(抽验) - -Sprint §1.1 的 🟡 标注与代码实况**逐条相符**:T-P1-03 确缺 COS mock、T-P1-08 确无 CLS 看板、T-P1-19 确只有 ping 骨架任务、T-P1-26 确无周期重置后台任务(设计上靠 `period_key` 自然重置,"缺后台"实际指任务奖励发放 `bonus` 递增链路未建)。**没有虚报完成度。** - ---- - -## 4. 文档 ↔ 代码一致性核对(本次评审的核心增量) - -上轮评审只做了"文档 ↔ 文档"比对;本轮首次把**实际代码**纳入核对。以下按严重程度排列。 - -### 4.1 🔴 必须修的漂移 - -#### X-1 配额种子数据与"唯一事实源"不符(P0) - -README §4.1 与产品方案 §3.2 均声称:**`bl_quota_rule` 的初始化数据一律引用产品方案 §3.2 配额表**。但迁移 `0001_initial_foundation.py` 的种子数据(约 207-235 行)与该表对照: - -| 能力 | 文档(产品方案 §3.2) | 代码种子(bl_quota_rule) | 差异 | -| --- | --- | --- | --- | -| AI 对话 | 每日 **10** 轮 / +10 / 硬上限 **40** | `agent_chat`: day / **20** / 10 / **50** | **base 是文档 2 倍,hard 多 10** | -| 换装 | 每日 3 / +3 / 硬上限 **8** | `tryon`: day / 3 / 3 / **10** | hard 不符(10 vs 8) | -| 报告 | 首次 **1 次(终身)**;重新生成每 30 天 1 次 / +1 / **每日 ≤ 2** | `report_basic`: **month** / 1 / 1 / **3** | 把"首次终身"与"每 30 天重新生成"两个口径揉成一个 key;周期口径(每日 ≤2 vs hard 3)不符 | -| 发型/发色试戴 | 每日 3 / +2 / ≤6 | **无此 quota_key** | 缺失 | -| 高清导出 | 每日 1 / — / ≤3 | **无此 quota_key** | 缺失 | -| 游客配额 | 换装 1 / 试戴 1 / 报告 1 / 对话 3(设备维度终身) | **无承载** | 缺失(设备维度计量链路未建,V1 骨架可接受,但应显式登记) | - -**处理建议**:二选一并留痕——① 按纪律以产品方案 §3.2 为准修正种子(现在表里没有生产数据,改动成本≈0);② 若 20/50 是开发期故意放宽,应通过 `bl_quota_rule` 的**运行时配置覆盖**实现(该表本就是配置表),并在文档变更记录登记理由,不能让种子数据成为第二套口径。 - -#### X-2 `points_reserved` → `quota_reserved` 是"假回写"(P0) - -架构文档 v1.3 变更记录与 README §8 均声称已把 `tk_task.points_reserved` 改名为 `quota_reserved`(清除 V2"点数"措辞残留,P2-4 修复项)。**但实际迁移与 ORM 仍是 `points_reserved`**(`migrations/versions/0001_initial_foundation.py:153`、`packages/shared/models.py`)。文档声称与实现相反——要么改代码(推荐,趁表里无数据),要么回退文档表述。 - -#### X-3 `bl_quota` 列名与架构文档不一致(P1) - -架构 §6.2/§7.4 明确:`bl_quota` 列为 `used`、**`bonus_granted`**(物化任务奖励,判定公式 `base_limit + bonus_granted`);实际代码列为 `used`、`limit_value`、**`bonus_value`**——名字不同、语义相近,且文档列清单里没有 `limit_value`、代码里没有文档写的列。任务奖励发放链路(T-P1-26 的剩余部分)实现时会直接撞上这处歧义。 - -### 4.2 🟡 核对通过的关键点(列出来给后续评审省时间) - -| 核对项 | 结论 | -| --- | --- | -| API 基础路径 `/api/v1`(规范)+ `/v1`(别名) | ✅ 代码用中间件实现别名,与架构 §9.1 v1.3 澄清一致 | -| 表名域前缀(id_/bl_/tk_/ob_) | ✅ 代码与架构 §6.2 一致(不一致的是用户系统文档,见 D-1) | -| 流水类型 `system_grant/consume/refund` 三种 | ✅ 代码与 FR-BILL-02 修正后口径一致 | -| `ob_ai_call` 四列(prompt 版本/思考 token/峰谷档位) | ✅ 0002 迁移 + ORM + recorder 均落地,与架构 v1.3 回写一致 | -| 峰谷时段(09-12/14-18 北京时间)与单价结构 | ✅ `cost.py`/`.env.example` 与 Sprint §1.3、架构 §13.2 一致 | -| 思考模式四场景显式关闭 | ✅ `routing.py` V1 全 `thinking=False`,与 T-P1-24a 一致 | -| 错误码文案不含价格字样(FR-BILL-07) | ✅ 有专项测试断言 | -| 日预算 key 按 UTC+8 日滚动 | ✅ 与 FR-BILL-01"跨时区按 UTC+8 统一"一致 | - ---- - -## 5. 🔴 安全问题(独立成节,优先级最高) - -**`G:\meida\reference\deepseek.txt` 是一个 35 字符的 DeepSeek API 密钥**(`sk-` 开头),当前状态: - -- 未被 git 跟踪,但**也未被 `.gitignore` 覆盖** —— 一次 `git add -A && git push` 就会把它推到远端; -- 该仓库已有 remote(origin)。 - -**立即处理**(按顺序): -1. 把该密钥从文件中移除,改放后端 `.env`(后端仓库的 `.gitignore` 已正确排除 `.env`,且 CI 有 detect-secrets + gitleaks 双扫——文档仓库没有这层防护); -2. 若该密钥已在任何地方暴露过,去 DeepSeek 控制台**作废重发**; -3. 在文档仓库加 `.gitignore`(至少覆盖 `reference/*.txt` 或明确列该文件); -4. 顺带清理:`reference/` 下的临时资料建议与正式文档分离,避免下次再混入敏感物。 - -这也暴露一个流程缺口:**文档仓库没有 pre-commit/密钥扫描**(后端仓库有)。文档仓库虽不含代码,但正是密钥、备案材料这类敏感物容易先落在文档区的地方。 - ---- - -## 6. 风险总览(按当前时点重排) - -| 风险 | 等级 | 现状与依据 | -| --- | --- | --- | -| 密钥泄漏(§5) | 🔴 立即 | 未处理即存在 | -| 配额/字段漂移(X-1/X-2/X-3) | 🔴 本周 | 表里尚无生产数据,修错成本每周上升 | -| 备案模型清单未核实(T-P0-01) | 🔴 既定 | README §0.2 第 1 项;影响双路由合法性 | -| 内容产能(顾问到岗 + 300 件自采) | 🔴 既定关键路径 | 无新技术判断,按 Sprint §6 倒排执行 | -| 8-17 峰谷涨价 | 🟡 已知 | 代码已实现峰谷计价;剩"重算日预算阈值"(C-3) | -| 集成回归缺口(C-1) | 🟡 | CI 容器已就位,修复成本低 | -| 四档熔断未实现(C-2) | 🟡 | T-P1-25b 已列,注意退款验收 | -| 算法验证门槛(体型/季型/脸型) | 🟡 既定 | Sprint 1 结论不达标要换方案,不是硬做 | - ---- - -## 7. 建议行动清单(按优先级) - -**立即(今天)** -- [ ] 处理 `reference/deepseek.txt` 密钥:移除 + 视暴露情况作废重发 + 补文档仓库 `.gitignore` - -**本周(Sprint 0 内)** -- [ ] 决策并执行 X-1:以产品方案 §3.2 修配额种子(含补 hairstyle/hd_export 两个 key),或以配置覆盖 + 文档留痕的方式登记偏差 -- [ ] X-2:`points_reserved` → `quota_reserved`(迁移 + ORM + repo 同步改,趁无数据) -- [ ] C-1:CI 增加 `MEIDA_RUN_INTEGRATION=1 pytest -m integration` 步骤 -- [ ] X-3/D-1:对齐 `bl_quota` 列名与用户系统文档表名(两处都是"文档 vs 代码"各改一半) - -**8-17 峰谷生效后** -- [ ] C-3:用实测 token 分布重算 `LLM_DAILY_BUDGET_CENTS` 与四档阈值 - -**随下一轮文档更新** -- [ ] D-2(仓库根目录名)、D-3(9→8 张表)、D-4(memory 包归属口径)、C-5(ob_ai_call 保留策略)、C-6(prod 无 dev-token 加入上线清单) - ---- - -## 附 A:评审覆盖清单 - -| 对象 | 方式 | 深度 | -| --- | --- | --- | -| docs/ 全部 13 份文档 | 通读(README/评审报告/Sprint/需求规格 §4-5/产品方案 §3.2 逐行;其余 8 份结构化摘要 + 关键节抽读) | 中-高 | -| 上轮评审 P0 修复项 | 抽查 5 项(34→32、99.9%、24h、4200/1400、三种流水) | 全部落地 | -| meida_backend 全部 10 提交 | 结构化探索 + 配置/迁移/网关/worker/测试逐模块 | 中-高 | -| 迁移种子数据、字段名、表数 | 直接读迁移文件逐行核对 | 高 | -| CI/测试门控 | 读 ci.yml + Makefile + 测试门控逻辑 | 高 | -| reference/ | 清点(发现密钥文件) | 高 | - -**未覆盖**(后续评审可补):后端单测断言质量逐条审读;`.env` 实际内容(仅看 example,避免接触真实密钥);鸿蒙端侧代码(尚无仓库);`reference/个人形象报告.pdf` 内容与美学报告维度字典的映射关系。 - ---- - -**评审人**:AI 辅助评审(ZCode) -**说明**:文档侧结论基于文本交叉比对;代码侧结论基于 HEAD `c97f8ae`。X-1 的配额数值差异存在"开发期有意放宽"的可能解释,需负责人确认意图后二选一处理。 diff --git a/docs/鸿蒙美搭-智能穿搭美学助手-产品方案.md b/docs/鸿蒙美搭-智能穿搭美学助手-产品方案.md index 54ba456..1f42084 100644 --- a/docs/鸿蒙美搭-智能穿搭美学助手-产品方案.md +++ b/docs/鸿蒙美搭-智能穿搭美学助手-产品方案.md @@ -1538,7 +1538,7 @@ V1 不接入支付,因此**本节对 V1 不产生任何影响**。但 V2 定 **作者**:产品团队 **状态**:待评审 **变更说明**: -- v6.1(2026-08-13):文档一致性评审修订(见《文档评审报告-2026-08-13》) +- v6.1(2026-08-13):文档一致性评审修订(2026-08-13) - **§5.2.1 / §5.3.1 成本口径彻底重写,并推翻一个错误论证**。原文称「月度成本上限 = MAU × 单用户配额成本上限,是可事前算准的确定值」,有两个漏洞:① 数字算不平(15 万 × ¥8 = ¥120 万 ≠ ¥12 万);② **更根本的是,按人头发配额封不住总成本** —— 配额约束单用户,而总成本 = MAU × 人均,MAU 无上限。现改为:单用户成本分**物理上限 ¥236 / 重度 ¥150 / 人均预期 ¥0.8** 三档;总成本 ¥7~12 万定性为**预期值(含使用率假设)而非硬上限**,并给出可复算的算式;**新增「全局日成本熔断」为唯一真正的封顶机制**(`NFR-COST-02`,四档自动降级,非仅告警) - **§8.3.1 性能指标改为引用 `NFR-PERF-*`**,不再自成一套。连带修正四处:初级报告 60s→90s、全面报告 5min→180s、换装改「现状基线 → 目标值」两段式、**可用性 99.9%→99.5%**(V1 单可用区无冗余,99.9% 在架构上无法兑现) - **§8.3.3 诊断准确率拆为「Phase 0 验证门槛 / 上线质量门槛」两档**,此前两套数值冲突的根源就是没区分这两档(两者的失败后果完全不同:前者触发换方案,后者触发维度降级)