2026-07-01 17:42:10 +08:00
# 执行进度记录
> 本文件是只追加的历史流水,用来记录任务执行过程、验证命令、阻塞点和关键决策。
> 当前目录、当前命令、下一个可领取任务等可覆盖快照,写入 [`docs/current-state.md`](docs/current-state.md)。
## 职责边界
- `docs/06-tasks.md` :任务看板,维护任务状态、依赖和验收要点。
- `progress.md` :历史流水,只追加记录每轮执行发生了什么。
- `docs/current-state.md` :当前快照,可覆盖更新仓库现实、可运行命令和下一步。
不要在本文重复维护当前目录结构、当前运行命令或下一个任务;这些信息以 `docs/current-state.md` 为准。
## 记录格式
每完成或中断一轮任务,在文件末尾追加一条记录:
```markdown
## 【YYYY-MM-DD】T-【编号】 【任务名】
- 状态:【DONE / BLOCKED / PARTIAL】
- 变更:【修改了哪些文件或模块】
- 验证:【运行的真实命令和结果】
- 阻塞:【如有,写明原因和需要谁决策】
- 决策:【如有,记录本轮确定的关键取舍】
- 下一步:【建议下一个任务 ID 或待确认事项】
```
## 执行记录
## 2026-06-29 文档初始化(非任务)
- 状态:DONE
- 变更:基于 `D:\github\harness_coding_docs` 模板,结合需求讨论生成 `cmhub` 全套 harness 文档(AGENTS/CLAUDE、docs/00– 06、api、routes、current-state、README、progress、init 脚本);通用流程文档(adoption/clean-state/method-map/evaluator/quality)从模板复制。
- 验证:仅文档,无代码可跑。
- 决策:技术栈 Django+DRF+django-admin;预付费点数模型(充值按汇率转点存本地,调用扣本地点数,不实时查支付系统);生成接口同步返回;充值由支付系统服务端回调入账(验签+幂等)。
- 下一步:领取 T-001 初始化 Django + DRF 骨架。支付系统接口文档待提供(T-304 前需要)。
## 2026-06-29 设计优化:可插拔供应商(别名 + 适配器)(非任务)
- 状态:DONE
- 变更:把「两个接口 + 后台配模型」的设计从简化版抬到可治理版,更新到多个文档:
- `04-architecture.md` : AI 调用层改为 Provider 适配器架构;新增 ModelAlias 表与 AiModel 的 capabilities/加密 key; PricingRule 改为按别名定价;CallRecord 增 alias + model_used;计费时序加入别名解析与能力校验;难点表补充抽象泄漏/供应商耦合/配置热生效/审计;项目结构加 `apps/ai/providers/` 。
- `api.md` :对外 `model` 字段明确为能力别名;新增 `parameters` 透传;响应加 `model_used` ;图片默认返回 URL;AI 模块合约改为别名解析 + Provider 适配器接口。
- `03-tech-stack.md` / `02-requirements.md` / `05-coding-rules.md` / `routes.md` / `06-tasks.md` :同步别名机制、密钥加密、配置审计、对象存储等决策与任务。
- 验证:仅文档;自检文档间链接与任务依赖一致(T-302 依赖更新为 T-104)。
- 决策:① 对外绑能力别名而非具体模型 SKU;② `api_type` 升级为显式 Provider 适配器层 + capabilities 声明 + parameters 透传;③ 供应商密钥加密存储 + 配置热生效 + 后台变更审计。按账号授权别名、按比例分流/故障转移列入 Backlog(接口预留,MVP 不实现)。
- 下一步:领取 T-001 初始化 Django + DRF 骨架(Phase 1 任务已重排为 T-101 适配器层 / T-102 别名解析 / T-103 审计 / T-104 跑通)。
## 2026-07-01 设计决策:桌面端全同步接入 + V2 异步预研(非任务)
- 状态:DONE
- 变更:把「桌面端不改、全同步接入 cmhub」的可用性结论与前提写入两个每轮必读文档:
- `03-tech-stack.md` 第二节「同步生成而非任务队列」决策下补:桌面端可不改交互骨架、只换 service 层 URL/密钥即可接入;同步可用的三前提;何时转 V2 异步的触发信号。
- `04-architecture.md` 第五节新增 `5.1 同步方案可用性结论` :超时链路层层放大对齐(点名 `timeout_seconds:0` 、Gunicorn 30s、Nginx 60s 三个默认值雷,建议统一 300s)、worker 数按峰值总并发预留、适用边界与 V2 触发条件。
- 验证:仅文档;`ai_models.json` 现存 `timeout_seconds: 0` ,迁入服务端时须改有限值(关联约束已写入 04 第五节)。
- 决策:① MVP 桌面端不改、`桌面端 → cmhub → 中转站` 全同步,多一跳不影响可用性,点数一致性更简单(一次请求闭环:预扣→同步调→成功/失败退点);② 同步可用的硬前提是「超时链路 + worker 容量」配对,否则「小量正常、上量假死」;③ 适用边界为单接入方小并发批量,V2 异步(队列)延后,但适配器接口与 `call_record` 三态需为异步预留口子。
- 安全提醒:`ai_models.json` 内三把 `sk-` 为明文真实密钥,视为已泄露,迁入时须加密存储(Fernet/KMS)并轮换;密钥不进桌面端。
- 下一步:不改变任务看板顺序,仍从 T-001 起步;V2 异步化触发条件见 03/04,暂不排期。
## 2026-07-01 设计决策:版本锁定 + 数据库选型(MySQL 8.4 独立实例)(非任务)
- 状态:DONE
- 变更:结合实际部署环境(一台 VPS,实测 15G 内存 / available 7.4G / 无 swap,已装 MySQL 5.7 供其他服务用),定稿版本与数据库选型,更新 `03-tech-stack.md` (技术栈表语言/框架/数据库三行 + 决策记录三条)、`04-architecture.md` (数据库条目 + 第五节并发扣点难点补 CHECK 版本注意)、`README.md` 技术栈行。
- 验证:仅文档。
- 决策:
- ① 框架/语言锁定 **Django 5.2 LTS + Python 3.12** ( `requires-python ">=3.12,<3.14"` );禁用已 EOL 的 Django 4.0/4.1;理由是安全/维护窗口,非性能(性能瓶颈在等上游+worker,见 5.1)。
- ② 数据库定 **MySQL 8.4 LTS, cmhub 专用独立实例** 。放弃复用 VPS 已有的 MySQL 5.7: 5.7 跑不了 Django 5.2(需 ≥8.0.11)、已 EOL、不支持 CHECK 约束;曾评估「坚持 5.7」会连锁把 Django 拖回 4.0(EOL)+Python≤3.10+cmbot 兼容风险,被否。内存宽裕(7.4G),单开独立实例与已有 5.7 隔离、互不影响。
- ③ 硬性约束:InnoDB + utf8mb4; CHECK 需 MySQL ≥8.0.16 才生效,扣点主防线是 `select_for_update` / `UPDATE ... WHERE balance>=N` ,不能只靠 CHECK; MySQL 默认隔离级别 REPEATABLE READ,计费按此语义验证;开发亦用 MySQL,不用 SQLite(会忽略 FOR UPDATE,测不出并发扣点)。
- 待办提醒:T-001 骨架落地时须选 MySQL 驱动(`mysqlclient` 或 `PyMySQL` )、`DATABASES` 配 `charset=utf8mb4` 、连接指向独立实例端口;机器建议补 2–4G swap;`03` 部署维度仍为「待定」,部署基线待后续定稿。
- 下一步:不改变任务看板顺序,仍从 T-001 起步(Django+DRF 骨架,按上述版本/数据库落地)。
## 2026-07-01 定位扩展:新增自助用户端(B2B → B2B+B2C)(非任务)
- 状态:DONE
- 变更:按用户新增需求(终端用户自助注册/扫码充值/API Key 管理/查记录),把项目从纯 B2B API 网关扩展为「自助用户端 + 计费 API + 运营后台」三合一,系统性更新文档:
- 定位/需求:`01-vision` 、`00-ai-start-here` 、`02-requirements` 、`project-brief` 、`project-onepager` (用户角色加注册用户;MVP 加用户端;移除"自助注册"非目标;注册不送点数)。
- 架构:`04-architecture` (系统结构加用户端;数据模型 Account→`User` +`UserWallet` +`ApiKey` ;两套认证;4.2 加自助扫码下单时序;难点加 API Key 哈希/注册滥用/Web-API worker 隔离/充错账户;项目结构 apps;架构纪律)。
- 合约/路由/规则:`api.md` (认证主体 User、`recharge/create` 转正扫码、API 只认 Key)、`routes.md` (用户端页面路由、admin 改 User/Wallet/ApiKey)、`03-tech-stack` (用户端形态 Django SSR+Bootstrap+allauth、鉴权、部署 worker 隔离)、`05-coding-rules` (范围、API Key 哈希、API 只认 Key、锁 wallet)。
- 任务:`06-tasks` (T-002 首迁移前定自定义 User; T-201 改 User/Wallet/ApiKey; T-203 锁 wallet; T-305 扫码下单;新增 Phase 4 用户端 T-501~504;里程碑加 M4 用户端)。
- `README` 、`current-state` 同步。
- 验证:仅文档。
- 决策:
- ① 前端形态选 **Django 模板 SSR 单体** ( +Bootstrap/allauth/crispy),不引前后端分离框架。
- ② 用户模型:`User` (auth 登录态) / `UserWallet` (点数余额,扣点锁 wallet、与 auth 解耦) / `ApiKey` (User 1:N, sha256 哈希存储、明文只显示一次)。
- ③ 认证分两套认同一 User:用户端 session+CSRF,对外 API 只挂 API Key(不挂 Session,防绕过计费)。
- ④ 充值:`recharge/create` 转正,用户端自助扫码下单 + 回调入账,订单绑定 user 防充错账户,金额 Decimal 向下取整。
- ⑤ **注册不送免费点数** (必须充值才有点数,降低薅羊毛);注册须邮箱验证、生成接口须限流。
- ⑥ 单体部署按路径把图片 API 与用户端页面分流到不同 worker 池。
- 待办提醒:**支付系统扫码下单 + 回调接口文档仍未提供**,是 T-304/T-305 充值的硬阻塞;自定义 User 必须在首次 migrate 前定义(Django 硬约束)。
- 下一步:任务仍从 T-001 起步;用户端任务见 Phase 4( T-501~504)。
## 2026-07-01 补全支付协议(参考同系统 PHP 实现,解除充值 blocker)(非任务)
- 状态:DONE
- 变更:从 Obsidian 笔记(虎观虾皮一键采购 `扫码支付购买流程-技术文档` + `扫码支付迁移到Django-实施指南` )提取同一支付系统的扫码支付协议,补进当前 Django 仓库文档:
- `api.md` :充值段重写为微信 V3 native + 支付宝当面付双回调(`/recharge/callback/wechat` 、`/alipay` ) + `create` ( pay_method、code_url/qr_code) + `status` 轮询;金额单位、验签、幂等、应答格式明确。
- `04-architecture.md` 4.2:补通道协议、库、金额单位、回调应答、`@csrf_exempt` 、主动查单兜底。
- `03-tech-stack.md` :充值对接行加库 `wechatpayv3` /`python-alipay-sdk` 。
- `routes.md` :拆微信/支付宝回调端点 + status。
- `06-tasks.md` :T-304 拆双回调+主动查单,T-305 加 weixin/alipay+轮询。
- `05-coding-rules.md` :回调 `@csrf_exempt` + 主动查单兜底。
- `current-state.md` : blocker 降级。
- 验证:仅文档。
- 决策:
- ① 路线确认:**继续当前 Django 仓库**, Obsidian 的 Go(Gin+GoAdmin) cmhub 方案仅作支付逻辑参考(用户拍板)。
- ② 支付通道:微信 V3 native(`wechatpayv3` ,金额**分**,回调 SDK 验签解密、`TRANSACTION.SUCCESS` 、应答 `{code:SUCCESS}` );支付宝当面付 `trade.precreate` ( `python-alipay-sdk` ,金额**元**, `verify` 、`TRADE_SUCCESS/FINISHED` 、应答 `success` )。
- ③ 二维码不含业务数据,靠 `out_trade_no` (=order_no) 在回调关联;回调 `@csrf_exempt` ;幂等 `select_for_update` +`status!=pending` ;须主动查单兜底。
- ④ 只取支付层,不引入 Obsidian 源里的套餐/会员有效期/邀请返佣业务(那是虎观助手专有);充值入账走本项目 `UserWallet` +`points_ledger` +`ExchangeRate` 。
- 待办提醒:**blocker 从「协议缺失」降级为「仅缺商户密钥/证书真实值」**,不阻塞开发,可先 mock。原 PHP 遗留坑(测试后门号、H5 金额写死、支付宝 debug 沙箱、明文密钥)不迁入。
- 下一步:任务仍从 T-001 起步;充值见 T-304/T-305(可 mock 先行)。
## 2026-07-01 吸收 AI 模型调用机制(来自 Go 方案 §16.6)(非任务)
- 状态:DONE
- 变更:从 Obsidian `Golang-Gin-GoAdmin-技术方案` §16.6 提取三个上游模型的真实调用机制(业务规则/上游契约,非技术栈),补进 `04-architecture.md` ( 3.1 + AI 层职责)、`api.md` (待确认 + image 改图必传)、`03-tech-stack.md` 、`06-tasks.md` ( T-101 验收)。
- 验证:仅文档。
- 决策:
- ① GPT-5.5( `chat` )标准 chat/completions; Nano Banana 2( `api_type=auto` )走 chat/completions 多模态返图、需自定义解析、可文/图生图;GPT Image 2( `images_edits` )走 images/edits 改图、原图必传。
- ② 两个图片模型**非标准** `images/generations` ,每模型独立 url+key;图片返回结构(URL/base64/位置)**首次对接抓真实响应再定解析**。
- ③ 只吸收 AI 模型机制,**不吸收** Go 方案的会员套餐/续期/折扣/邀请返佣/多端 JWT(属虎观采购业务或 Go 栈,非 cmhub 定位);鉴权继续用 API Key(比桌面端 JWT 更安全,不在桌面端存密码)。
- 下一步:T-101/T-102 实现适配器时按此机制,首次对接 `api.vectorengine.ai` 抓真实响应确认图片返回结构。
<!-- 后续从这里向下继续追加记录。 -->
## 2026-07-01 文档优化:定位一致性 + 充值 schema + 调用状态 + 环境配置(非任务)
- 状态:DONE
- 变更:按全栈落地前优先级修正文档:
- `docs/README.md` / `project-brief.md` / `project-onepager.md` :定位同步为「自助用户端 + 计费型 AI 能力网关 + 运营后台」三合一,并加入 `env.md` 导航。
- `docs/04-architecture.md` / `docs/api.md` / `docs/06-tasks.md` :充值订单改为下单时锁定 `exchange_rate` 与 `points_granted` ,回调入账使用订单值并校验金额;`call_record.status` 明确为 `pending -> success / failed` ,失败退点通过 `points_ledger(refund, ref_call_id)` 关联。
- `docs/env.md` :新增环境变量与配置清单,覆盖 Django、MySQL、AI 密钥加密、微信/支付宝、对象存储与上线检查。
- `docs/03-tech-stack.md` / `docs/current-state.md` / `docs/00-ai-start-here.md` / `docs/02-requirements.md` / 汇报文档:同步环境配置入口、当前快照与支付 blocker 降级口径。
- 验证:文档修改;用 `rg` 定位相关段落,未运行代码测试(仓库当前尚无代码)。
- 阻塞:无。
- 决策:充值汇率采用「下单锁定」而非「回调时读取当前汇率」,便于页面展示预计到账点数,也避免用户扫码后后台改价导致入账变化;调用记录不增加 `refunded` 状态,账务冲正由点数流水表达。
- 下一步:仍从 T-001 初始化 Django + DRF 骨架开始。
## 2026-07-01 文档优化:里程碑口径同步(非任务)
- 状态:DONE
- 变更:同步汇报文档里程碑到 `06-tasks.md` 的 M1-M5 口径:`project-brief.md` 增加 M4 用户端可用、M5 验收上线;`project-onepager.md` 进度行同步 M4/M5。
- 验证:文档修改;用 `rg` 定位里程碑旧口径。
- 阻塞:无。
- 决策:以 `06-tasks.md` 的里程碑为权威,汇报文档只做同口径摘要。
- 下一步:仍从 T-001 初始化 Django + DRF 骨架开始。
## 2026-07-01 文档优化:Phase 顺序口径同步(非任务)
- 状态:DONE
- 变更:更新 `docs/00-ai-start-here.md` 的优先路径,Phase 3/4/5 与 `docs/06-tasks.md` 对齐:Phase 3 为对外 API 与充值,Phase 4 为用户端,Phase 5 为后台与发布。
- 验证:文档修改;用 `rg` 对照 `00-ai-start-here.md` 与 `06-tasks.md` 的 Phase 标题。
- 阻塞:无。
- 决策:任务阶段顺序以 `06-tasks.md` 为权威,入口文档只做一致的导航摘要。
- 下一步:仍从 T-001 初始化 Django + DRF 骨架开始。
2026-07-01 18:01:23 +08:00
## 2026-07-01 T-001 初始化 Django + DRF 项目骨架
- 状态:DONE
- 变更:
- 新增 `manage.py` 、`config/` Django 工程骨架。
- 新增 `requirements.txt` ,锁定 Django 5.2 系列与 DRF 3.16 系列。
- `config/settings.py` 加入 `rest_framework` , `SECRET_KEY` / `DEBUG` / `ALLOWED_HOSTS` 支持环境变量,默认仅用于本地开发。
- `init.ps1` / `init.sh` 替换为真实命令;按用户要求使用系统 Python 3.12,不使用虚拟环境。Windows 用 `py -3.12` , Unix/WSL 用 `python3.12` 。
- 同步 `docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/current-state.md` 、`docs/06-tasks.md` 、`README.md` 。
- 验证:
- `py -3.12 -m pip install -r requirements.txt` : Django 5.2.15 / DRF 3.16.1 已安装。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test` :通过,当前 0 tests。
- `./init.ps1` :通过,完成依赖检查、`manage.py check` ,打印启动命令。
- `C:\Python312\python.exe manage.py runserver 127.0.0.1:8765 --noreload` :端口 smoke 通过(TCP 8765 可连接),随后已停止进程。
- `bash -n init.sh` :当前机器无可用 bash/WSL,无法本机验证 Unix 脚本。
- 阻塞:无。
- 决策:T-001 不接入 MySQL、不创建自定义 User、不创建 apps 目录;这些按任务边界留给 T-002。T-001 阶段暂保留 Django 默认 SQLite 配置用于框架启动检查,T-002 必须在首次迁移前改为自定义 User + MySQL 8.4 配置。
- 下一步:领取 T-002 建立 apps 目录、自定义 User 与配置。
2026-07-02 09:07:15 +08:00
## 2026-07-02 T-002 建立 apps 目录、自定义 User 与配置
- 状态:DONE
- 变更:
- 创建 `apps/` 包与 `apps/users|portal|billing|ai|api` 五个 Django app。
- `apps/users.models.User` 继承 `AbstractUser` ,新增 `payment_user_id` 、`status` 、`created_at` ,邮箱改为必填,`db_table="user"` ; `settings.AUTH_USER_MODEL="users.User"` 已设置。
- `settings.py` 增加根目录 `.env` 读取,数据库从 SQLite 切换到 MySQL,配置 `utf8mb4` 与严格 SQL 模式。
- 采用 `PyMySQL` 作为 MySQL 驱动,更新 `requirements.txt` 与 `config/__init__.py` 。
- 生成 `apps/users/migrations/0001_initial.py` 。
- 验证:
- `py -3.12 -m pip install -r requirements.txt` :通过,安装 PyMySQL 1.1.3。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations users` :通过,生成 `0001_initial.py` 。
- `py -3.12 manage.py migrate` :首次失败,MySQL 返回 `ALTER command denied to user 'test'@'61.141.174.182' for table 'django_content_type'` ;用户在宝塔/MySQL 授权后,清理失败迁移留下的空表并重新执行,最终通过。
- `SHOW GRANTS FOR CURRENT_USER()` :授权后显示 `GRANT ALL PRIVILEGES ON cmhub.* TO test@%` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test` :通过,当前 0 tests。
- MySQL 验证:存在自定义 `user` 表,含 `payment_user_id` / `status` / `created_at` ;未创建默认 `auth_user` 表;`django_migrations` 共 19 条。
- 阻塞:已解除。曾因 MySQL 用户缺少 `ALTER/INDEX/DROP` 等迁移权限受阻。
- 决策:使用 PyMySQL 作为 MySQL 驱动;T-002 只落 apps、自定义 User、settings 与 MySQL 初始迁移,不提前实现 UserWallet/ApiKey 等 T-201 内容。
- 下一步:领取 T-003 接通 django-admin 与最小测试。
2026-07-02 09:46:32 +08:00
## 2026-07-02 T-003 接通 django-admin 与最小测试
- 状态:PARTIAL
- 变更:
- `apps/users/admin.py` :用 `DjangoUserAdmin` 注册自定义 `User` ,后台列表展示 `username/email/status/is_staff/is_active/created_at` ,支持按 `status` 过滤、按 `username/email/payment_user_id` 搜索,`created_at` 只读。
- `apps/users/tests.py` :新增最小 admin smoke test,覆盖自定义 User 已注册到 admin、superuser 可登录 `/admin/` 。
- `config/settings.py` :本地默认 `ALLOWED_HOSTS` 增加 `testserver` ,匹配 Django test client。
- `docs/06-tasks.md` / `docs/current-state.md` :记录 T-003 当前权限阻塞。
- 验证:
- `py -3.12 manage.py check` :通过,0 issues。
- 手工创建临时 superuser 并用 Django test client 登录 `/admin/` :登录成功,`GET /admin/` 返回 200;临时用户已清理。
- `py -3.12 manage.py test` :未通过,Django 在创建 `test_cmhub` 测试库时报 `(1044, "Access denied for user 'test'@'%' to database 'test_cmhub'")` 。
- 进一步验证 `CREATE DATABASE IF NOT EXISTS test_cmhub ...` 同样返回 1044;当前授权仅覆盖 `cmhub.*` 。
- 阻塞:需要给 MySQL 用户增加测试库权限,建议优先只授权测试库范围:
- `GRANT ALL PRIVILEGES ON test_cmhub.* TO 'test'@'%';`
- `FLUSH PRIVILEGES;`
- 决策:不把 Django 测试改跑到业务库 `cmhub` ,避免测试清表/迁移影响真实开发数据;T-003 在标准 `manage.py test` 通过前不标 `DONE` 。
- 下一步:完成 MySQL 授权后重跑 `py -3.12 manage.py test` ,再跑 `py -3.12 manage.py check` / `./init.ps1` ,通过后把 T-003 标记 DONE。
## 2026-07-02 T-003 接通 django-admin 与最小测试(完成验收)
- 状态:DONE
- 变更:
- `requirements.txt` :新增 `cryptography>=42,<47` ,用于支持 PyMySQL 连接 MySQL 8 默认 `caching_sha2_password` 认证。
- `docs/06-tasks.md` : T-003 从 BLOCKED 改为 DONE。
- `docs/current-state.md` / `docs/03-tech-stack.md` :同步当前依赖、验证结果与下一步任务。
- 验证:
- `py -3.12 -m pip install -r requirements.txt` :通过,安装 `cryptography 46.0.7` 。
- `py -3.12 manage.py test` :通过,2 tests OK,测试库 `test_cmhub` 创建与销毁正常。
- `py -3.12 manage.py check` :通过,0 issues。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- 阻塞:无。
- 决策:保留独立 `test_cmhub` 测试库路径,标准测试不使用业务库 `cmhub` 。
- 下一步:领取 T-101 Provider 适配器层 + 移植 cmbot 调用。
2026-07-02 10:17:10 +08:00
## 2026-07-02 Phase 0 骨架审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-0-review.md` ; `06-tasks.md` 新增 T-004 修补任务;`docs/README.md` 导航登记;`current-state.md` 下一步改 T-004。
- 审核结论:**T-001~003 验收全部达标,质量高于骨架平均水准**。自定义 User 先于首次迁移、`AUTH_USER_MODEL` 已设、`PyMySQL install_as_MySQLdb()` 胶水到位、密钥全走 env 且 `.env` /`ai_models.json` 未被 git 跟踪、独立 `test_cmhub` 测试库——关键硬约束全对。
- 发现(详见 `phase-0-review.md` ):
- P1(建议现在改):`User.email` 未 `unique` ; `requires-python` 未落地、init 脚本无解释器版本断言。
- P2(规范性):`INSTALLED_APPS` 顺序(本地 app 排在 contrib 前,模板查找隐患);缺 `.env.example` 。
- P3(后续任务处理):REST_FRAMEWORK 认证配置(T-301)、`sql_mode` 完整性、生产 SECRET_KEY 保护(T-403)。
- 未本地复跑:审核机无 `python3.12` ,结论基于静态审查 + codex 验证记录(记录真实,含 MySQL 授权 / caching_sha2 修复过程)。
- 下一步:codex 领 T-004,按 `phase-0-review.md` 修 P1/P2,全绿留证后再进 T-101。
## 2026-07-02 T-004 Phase 0 骨架审核修补
- 状态:DONE
- 变更:
- 文档先行:更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/current-state.md` 、`docs/env.md` 、`docs/06-tasks.md` ,把 T-004、邮箱唯一约束、Python 版本断言、`.env.example` 和下一步 T-101 口径同步。
- `apps/users.models.User.email` 增加 `unique=True` ,并生成/应用 `apps/users/migrations/0002_alter_user_email.py` 。
- `config/settings.py` 调整 `INSTALLED_APPS` 顺序为 `django.contrib.*` → `rest_framework` → 本地 apps。
- `init.ps1` / `init.sh` 在安装依赖前校验 Python 版本必须满足 `>=3.12,<3.14` 。
- 新增 `.env.example` (仅占位符,无真实密钥)与 `pyproject.toml` (落地 `requires-python` )。
- 验证:
- 重复邮箱预检:业务库非空 email 无重复。
- `py -3.12 manage.py makemigrations users` :生成 `0002_alter_user_email.py` 。
- `py -3.12 manage.py migrate` :通过,应用 `users.0002_alter_user_email` 。
- 数据库索引验证:`user.email` 存在唯一索引 `user_email_54dc62b2_uniq` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test` :通过,2 tests OK。
- `./init.ps1` :通过,输出 Python 3.12.3 版本断言、依赖同步与基础检查。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `git diff --check` :通过,仅有 CRLF 提示。
- `bash -n init.sh` :当前 Windows 未安装 WSL/bash,无法本机验证 Unix 脚本语法。
- 阻塞:无。
- 决策:保留 `requirements.txt` 作为运行依赖来源,`pyproject.toml` 只承载 Python 版本元数据;`.env.example` 使用占位符,不提交 `.env` 。
- 下一步:领取 T-101 Provider 适配器层 + 移植 cmbot 调用。
2026-07-02 10:33:15 +08:00
## 2026-07-02 T-101 Provider 适配器层 + 移植 cmbot 调用
- 状态:DONE
- 变更:
- 新增 `apps/ai/providers/` : `ResolvedModel` 、`Provider` 协议、结果对象、错误类型、注册表、`chat` /`gemini` /`images` /`images_edits` 适配器。
- 从 `D:\chengma\cmbot\src\services\ai_text_service.py` / `ai_image_service.py` 移植纯 HTTP 与响应解析逻辑:URL 归一化、`api_type=auto` 识别、分辨率超时、chat/gemini payload、images/edits multipart、标题清洗、图片 data URL/base64/URL 解析。
- 服务端接口改为 bytes 输入,不依赖桌面端本地路径、GUI、线程或 Qt;HTTP session 可注入,便于测试。
- `requirements.txt` 新增 `requests>=2.32,<3` 。
- `apps/ai/tests.py` 新增 5 条 mock 单测,覆盖 provider 选择、chat 文本请求构造和标题解析、chat 多模态返图解析、images/edits multipart 请求构造和 base64 图片解析、能力不支持错误。
- 同步 `docs/api.md` 、`docs/03-tech-stack.md` 、`docs/current-state.md` 、`docs/06-tasks.md` 、`README.md` 、`docs/00-ai-start-here.md` 。
- 验证:
- 脱敏读取 `D:\chengma\cmbot\config\ai_models.json` :顶层为 `models` 列表,共 3 个模型;仅打印非密钥字段,未暴露真实 key。
- `py -3.12 -m pip install -r requirements.txt` :通过,安装 `requests 2.34.2` 及依赖。
- `py -3.12 manage.py test apps.ai` :通过,5 tests OK。
- `py -3.12 manage.py test` :通过,7 tests OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` :通过。
- `./init.ps1` :通过,依赖同步含 `requests` ,基础检查正常。
- 阻塞:无。
- 决策:T-101 不创建 AiModel/ModelAlias 数据表、不做别名解析数据库读取、不接计费;先用 `ResolvedModel` dataclass 承接后续 T-102 的数据库模型。
- 下一步:领取 T-102 AiModel + ModelAlias 模型 + 别名解析。
2026-07-02 11:07:44 +08:00
## 2026-07-02 T-102 AiModel + ModelAlias 模型 + 别名解析
- 状态:DONE
- 变更:
- 新增 `AiModel` / `ModelAlias` 数据模型与 `apps/ai/migrations/0001_initial.py` : `AiModel` 保存上游 url/model/api_type/capabilities/timeout/extra_body/is_active 与 Fernet 密文 `api_key_encrypted` ; `ModelAlias` 用 `operation_type + alias` 映射到具体模型,支持默认别名与启停。
- 新增 `apps/ai/security.py` : `AI_KEY_ENCRYPTION_KEY` 驱动 Fernet 加解密,密文带 `fernet:` 前缀;拒绝明文 fallback。
- 新增 `apps/ai/aliases.py` : `resolve_alias(operation_type, alias=None)` 每次查当前 active 配置,标题要求 `text` 能力,图片要求 `image` 能力,返回 provider 可用的 `ResolvedModel` 。
- 新增 `apps/ai/importers.py` 与 `import_ai_models` 管理命令:支持导入 `cmbot` 形状的 `ai_models.json` ,导入时加密 key、推断 capabilities,可选创建 `title-standard` / `image-standard` 默认别名。
- 完善 `apps/ai/admin.py` : admin 提供写入型 `api_key` 字段,密钥脱敏显示且不回显;缺少 Fernet 主密钥时以表单错误提示;`api_key_encrypted` 不暴露。
- 扩展 `apps/ai/tests.py` :覆盖密钥加密/解密、别名解析、能力不匹配、inactive alias、配置导入、admin 脱敏与缺主密钥错误;同步更新 README、入口文档、技术栈、架构、API、环境配置、任务看板和当前状态。
- 验证:
- `py -3.12 manage.py migrate` :通过,`ai.0001_initial` 已应用;`py -3.12 manage.py showmigrations ai` 显示 `[X] 0001_initial` 。
- `py -3.12 manage.py test apps.ai --noinput` :通过,16 tests OK。
- `py -3.12 manage.py test --noinput` :通过,18 tests OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` / `py -3.12 -m compileall apps\ai` :通过。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- 验证期间远程 MySQL `43.128.3.240:3306` 多次短暂超时,`Test-NetConnection 43.128.3.240 -Port 3306` 恢复后重试通过;失败点均为建库/事务前连接超时,不是断言失败。
- 阻塞:无。
- 决策:
- 供应商密钥存储采用 `cryptography` Fernet 应用层加密,主密钥只走 `AI_KEY_ENCRYPTION_KEY` ;不实现明文兼容。
- T-102 不自动导入真实 `D:\chengma\cmbot\config\ai_models.json` ,避免真实上游 key 被误暴露;后续由管理员在确认环境密钥后手动执行导入命令。
- 默认别名唯一性先由 model validation、admin 与导入器保证;MySQL partial unique 约束留待审计/后台完善阶段评估。
- 下一步:领取 T-103 配置变更审计。
2026-07-02 11:42:39 +08:00
## 2026-07-02 T-103 配置变更审计
- 状态:DONE
- 变更:
- 新增 `AiConfigAuditLog` 审计模型与 `apps/ai/migrations/0002_aiconfigauditlog.py` ,字段包括 `actor` 、`action` 、`target_type` 、`target_id` 、`target_repr` 、`changed_fields` 、`changes` 、`created_at` 。
- 新增 `apps/ai/audit.py` :对 `AiModel` / `ModelAlias` 做变更快照与字段 diff, `api_key_encrypted` 对外记录为 `api_key` ,只保留 `empty` / `set` 状态,不记录明文或 Fernet 密文。
- 更新 `apps/ai/admin.py` : `AiModelAdmin` / `ModelAliasAdmin` 的保存和删除 hook 自动写审计日志;新增 `AiConfigAuditLogAdmin` ,仅允许只读查看,不允许新增、修改、删除审计日志。
- 扩展 `apps/ai/tests.py` :新增 5 条审计测试,覆盖 AiModel 创建、密钥更新、ModelAlias 映射变更、删除审计、审计 admin 只读。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/06-tasks.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/current-state.md` 、汇报文档。
- 验证:
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py migrate` :通过,应用 `ai.0002_aiconfigauditlog` 。
- `py -3.12 manage.py showmigrations ai` :通过,`0001_initial` / `0002_aiconfigauditlog` 均为 `[X]` 。
- `py -3.12 manage.py test apps.ai.tests.AiConfigAuditAdminTests --noinput` :通过,5 tests OK。
- `py -3.12 manage.py test apps.ai --noinput` :通过,21 tests OK。
- `py -3.12 manage.py test --noinput --keepdb` :通过,23 tests OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅有 CRLF 提示。
- 标准 `py -3.12 manage.py test --noinput` 多次在远程 MySQL `43.128.3.240:3306` 创建/销毁测试库或测试中途重连时超时;失败点为 MySQL 连接/建库,不是断言失败。为降低远程建库压力,使用 `--keepdb` 完整测试已通过。
- 阻塞:无代码阻塞;远程 MySQL 偶发连接超时会影响标准测试建库稳定性。
- 决策:T-103 使用自建 `AiConfigAuditLog` 专表,不只依赖 django-admin `LogEntry` ;密钥审计只记录状态变化,不记录明文或密文。
- 下一步:领取 T-104 跑通一次真实/录制的标题或图片生成。
2026-07-02 13:58:34 +08:00
## 2026-07-02 T-104 跑通一次真实/录制的标题或图片生成
- 状态:DONE
- 变更:
- 新增 `smoke_ai_generation` 管理命令:支持 `title --recorded` ,在事务内临时创建 fake `AiModel` / `ModelAlias` ,通过 `resolve_alias()` 解析别名,再用录制的 chat/completions 响应走 `ChatCompletionsProvider.generate_text()` ,最后回滚临时配置,不写真实 key、不污染业务库。
- 扩展 `apps/ai/tests.py` :新增录制 smoke 命令测试,确认输出不包含 placeholder key / Bearer,且临时 `AiModel` / `ModelAlias` 不持久化。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、汇报文档。
- 验证:
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py showmigrations ai` :通过,`0001_initial` / `0002_aiconfigauditlog` 均为 `[X]` 。
- `py -3.12 manage.py shell -c "...AI_KEY_ENCRYPTION_KEY..."` :当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 。
- `py -3.12 manage.py shell -c "...AiModel/ModelAlias count..."` :业务库 `AiModel count=0` 、`ModelAlias count=0` 。
- `py -3.12 manage.py smoke_ai_generation title --recorded --prompt "为测试商品生成3个中文标题" --resolution 1K` :通过;`alias=t-104-recorded-title-c054d919` , `model_used=recorded-title-model` , `elapsed_ms=149` , `title_count=3` 。
- smoke 后再次查询业务库:`AiModel count=0` 、`ModelAlias count=0` ,确认临时配置已回滚。
- `py -3.12 manage.py test apps.ai.tests.AiGenerationSmokeCommandTests --noinput --keepdb` :通过,1 test OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` :通过。
- `py -3.12 manage.py test apps.ai --noinput --keepdb` :多次在远程 MySQL `43.128.3.240:3306` 连接/测试库初始化阶段超时;失败点为数据库连接,不是断言失败。
- 阻塞:真实上游 smoke 未执行,因为当前环境未配置 `AI_KEY_ENCRYPTION_KEY` ,且业务库没有 AiModel/ModelAlias。配置 Fernet 主密钥并导入模型后,可去掉 `--recorded` 用同一命令跑真实标题 smoke。
- 决策:T-104 采用“录制标题生成”作为验收路径,验证别名解析、Provider 选择、响应解析和同步耗时记录;真实图片同步耗时仍需在后续配置真实模型后补测。
- 下一步:领取 T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型。
2026-07-02 14:47:22 +08:00
## 2026-07-02 Phase 1 审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-1-review.md` ; `06-tasks.md` 新增 T-105 修补任务、T-104 状态标注「图片同步风险未退」;`docs/README.md` 导航登记;`current-state.md` 阶段/下一步改 T-105。
- 审核结论:**T-101~104 验收达标,代码质量高**。三模型机制忠实落地(chat / auto→chat 多模态返图 nano-banana2 / images_edits 改图 gpt-image-2);`timeout_seconds=0→按分辨率有限超时` 修对了桌面端「0=无限」的坑;Fernet 加密 + 拒绝明文 fallback + 缺主密钥 fail-closed + admin 写入型脱敏字段 + 审计只记 empty/set,密钥零泄露闭环;审计 admin 真只读、delete_queryset 也留痕;smoke 用 override_settings+atomic+rollback 不落库、不打印 Bearer/key。
- 发现(详见 `phase-1-review.md` ):
- P1-1(进 T-302 前必修,资金安全):`apps/ai/providers/openai_compatible.py:apply_extra_body` 无差别 `payload.update(parameters)` ,调用方透传可覆盖 `model` /`n` /`size` /`resolution` 等核心/计费字段,击穿「按别名计费」契约。须白名单化、核心字段服务端固定不可覆盖。
- P1-2(最高风险未退):T-104 只跑了 recorded **标题** ( `elapsed_ms=149` 无网络意义),Phase 1 存在的唯一理由「图片同步在 Gunicorn/nginx 下不超时」**未验证**。结论应改为「管路已验、图片同步风险未退」,并在依赖同步部署前(最迟 T-403)跑一次真实图片生成记录真实耗时。
- P2: `.env.example` 的 `AI_DEFAULT_CONNECT/READ_TIMEOUT_SECONDS` 代码未读取(死配置,接上或删除);`resolution_to_size` 未大小写归一(`"1k"` 漏命中);导入器默认别名 `image-standard` 与文档示例 `image-hd` 口径不一致。
- P3(登记):`Provider.capabilities()` 无调用点(半冗余);图生图 vs 改图都归 `operation_type=IMAGE` ,T-302 需把「缺原图」翻成 400 而非 500; CallRecord 写入禁 dump `result.raw` (含 base64 大图/敏感);provider 密钥无轮换(单 Fernet key)。
- 未本地复跑:审核机无 `python3.12` ,结论基于静态审查(providers/models/security/aliases/audit/importers/admin/tests/迁移/settings) + codex 验证记录(7→18→23 tests 递增、含真实 MySQL 建库超时的诚实记录,可信度高)。T-104 的 149ms 不作为图片同步可行性证据。
- 下一步:codex 领 T-105,按 `phase-1-review.md` 修 P1/P2; P1-1 须先于 T-302,全绿留证。
## 2026-07-02 T-105 Phase 1 AI 层审核修补
- 状态:DONE
- 变更:
- `apps/ai/providers/openai_compatible.py` : `parameters` / `extra_body` 改为白名单安全透传,`model` 、`messages` 、`n` 、`size` 、`stream` 等核心/计费字段不可被覆盖;`generationConfig` 仅允许安全子字段合并。
- `apps/ai/providers/utils.py` : `resolution_to_size()` 增加大小写归一,`"1k"` / `"512px"` 可正确映射。
- `apps/ai/importers.py` 与 `import_ai_models` 帮助文案:默认图片别名从 `image-standard` 对齐为 `image-hd` 。
- `smoke_ai_generation` 支持 `image --recorded` ,录制图片路径走别名解析 + Provider 解析 + 事务回滚,不打印 key/Bearer、不持久化临时配置;保留真实 image smoke 入口和 `--image-file` 。
- 扩展 `apps/ai/tests.py` :覆盖 provider 参数越权拦截、分辨率归一、默认别名、录制 image smoke。
- `.env.example` / `docs/env.md` 删除未被代码读取的 `AI_DEFAULT_CONNECT_TIMEOUT_SECONDS` / `AI_DEFAULT_READ_TIMEOUT_SECONDS` ,改为说明超时来自 `AiModel` 字段与 Provider 分辨率默认值。
- 同步更新 `docs/06-tasks.md` 、`docs/api.md` 、`docs/04-architecture.md` 、`docs/03-tech-stack.md` 、`README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` : T-105 完成,P1/P2 已修;P3 已挂到 T-201/T-302/T-403 或 Backlog。
- 验证:
- `py -3.12 -m py_compile apps\ai\providers\openai_compatible.py apps\ai\providers\utils.py apps\ai\importers.py apps\ai\management\commands\smoke_ai_generation.py apps\ai\tests.py` :通过。
- `py -3.12 manage.py test apps.ai.tests.ProviderUtilsTests apps.ai.tests.ChatCompletionsProviderTests apps.ai.tests.ImagesEditsProviderTests apps.ai.tests.AiModelsImportTests apps.ai.tests.AiGenerationSmokeCommandTests --noinput --keepdb` :通过,10 tests OK。
- `py -3.12 manage.py smoke_ai_generation image --recorded --prompt "生成测试图片" --resolution 1k` :通过;`alias=t-105-recorded-image-27c25b3a` , `model_used=recorded-image-model` , `elapsed_ms=71` , `image_bytes=20` 。
- `py -3.12 manage.py smoke_ai_generation title --recorded --prompt "为测试商品生成3个中文标题" --resolution 1K` :通过;`alias=t-104-recorded-title-ceec8dab` , `model_used=recorded-title-model` , `elapsed_ms=92` , `title_count=3` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test apps.ai --noinput --keepdb` :两次未作为绿灯;一次在连接远程 MySQL 测试库前超时,一次跑到 23 tests 后于 `AiModelAdminTests.setUpClass` 事务连接阶段超时。失败点均为远程 MySQL `43.128.3.240:3306` 连接/重连,不是断言失败。
- 阻塞:无代码阻塞。真实图片同步耗时仍未验证,已登记到 T-302/T-403;需要真实 `AI_KEY_ENCRYPTION_KEY` 、AiModel/ModelAlias 与上游 key 后执行。
- 决策:对外 `parameters` 不是任意直通上游,而是 provider 白名单安全参数;服务端固定字段优先,计费仍以能力别名/分辨率为准。未生效的全局 AI timeout 环境变量删除,避免运维误以为可通过 env 调整。
- 下一步:领取 T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型。
2026-07-02 15:06:00 +08:00
## 2026-07-02 T-201 User / UserWallet / ApiKey / PointsLedger / CallRecord 模型
- 状态:DONE
- 变更:
- `apps/users/models.py` :新增 `UserWallet` (一用户一钱包,`points_balance >= 0` DB 约束)与 `ApiKey` ( `key_hash` SHA-256、`key_prefix` 、active/revoked 状态、`create_for_user()` 只在创建时返回明文)。
- `apps/billing/models.py` :新增 `CallRecord` ( user/api_key/operation/alias/model_used/result_ref/result_summary/status/latency,不含 provider raw/base64 字段)与 `PointsLedger` ( recharge/consume/adjust/refund,非零 delta、`balance_after >= 0` ,手工调整需 reason)。
- 新增迁移:`users.0003_apikey_userwallet` 、`billing.0001_initial` ,并已应用到当前 MySQL。
- `apps/users/admin.py` :注册 `UserWallet` / `ApiKey` ,钱包余额只读,API Key 只显示 prefix 和 hash 摘要,不回显明文。
- `apps/billing/admin.py` :注册 `PointsLedger` / `CallRecord` 只读 admin,用于检索对账。
- `apps/billing/tests.py` :覆盖钱包非负约束、API Key 哈希与匹配、调用记录不含 raw 字段、流水约束与 admin 注册。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/06-tasks.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/current-state.md` 、`docs/project-brief.md` 。
- 验证:
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test apps.billing --noinput --keepdb` :通过,6 tests OK。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py migrate` :通过,应用 `users.0003_apikey_userwallet` / `billing.0001_initial` 。
- `py -3.12 manage.py test apps.users apps.billing --noinput --keepdb` :通过,8 tests OK。
- `py -3.12 manage.py showmigrations users billing` :通过,`users.0001/0002/0003` 与 `billing.0001` 均为 `[X]` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test --noinput --keepdb` :通过,34 tests OK。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:无。
- 决策:
- `UserWallet` / `ApiKey` 放在 `apps.users` , `PointsLedger` / `CallRecord` 放在 `apps.billing` ;T-201 只建数据结构,不实现扣点服务、计费规则、充值订单或 API 鉴权。
- `PointsLedger.ref_order_id` 在 RechargeOrder 模型落地前先保留为索引化数值引用,充值任务落地时再正式关联或补迁移。
- 下一步:领取 T-202 PricingRule / ExchangeRate 模型 + 计费计算。
2026-07-02 15:28:19 +08:00
## 2026-07-02 T-202 PricingRule / ExchangeRate 模型 + 计费计算
- 状态:DONE
- 变更:
- `apps/billing/models.py` :新增 `PricingRule` (按 `operation_type + alias + resolution` 唯一,`points_cost > 0` , active 开关)与 `ExchangeRate` ( `currency + effective_from` 当前汇率,`points_per_unit > 0` ),并归一化分辨率/币种。
- `apps/billing/pricing.py` :新增计费计算服务;定价优先精确分辨率,再回退空 `resolution` 默认价;缺规则抛 `NoPricingRuleError(code="no_pricing_rule")` ;充值点数按当前 active 汇率 `floor(amount * points_per_unit)` 计算。
- `apps/billing/admin.py` :注册 `PricingRule` / `ExchangeRate` ,运营可在 admin 配规则与汇率。
- `apps/billing/tests.py` :覆盖分辨率精确价优先、默认价回退、换底层 `ModelAlias.ai_model` 不影响别名价格、缺规则错误码、当前汇率选择与点数向下取整。
- 新增迁移 `billing.0002_exchangerate_pricingrule` ,已应用到当前 MySQL。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `py -3.12 -m py_compile apps\billing\models.py apps\billing\pricing.py apps\billing\admin.py apps\billing\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py migrate` :通过,应用 `billing.0002_exchangerate_pricingrule` 。
- `py -3.12 manage.py showmigrations billing` :通过,`billing.0001_initial` / `billing.0002_exchangerate_pricingrule` 均为 `[X]` 。
- `Test-NetConnection 43.128.3.240 -Port 3306` :通过,`TcpTestSucceeded=True` 。
- `py -3.12 manage.py test apps.billing --noinput --keepdb` :通过,10 tests OK。
- `py -3.12 manage.py test apps.users --noinput --keepdb` :通过,2 tests OK。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test --noinput --keepdb` :两次未作为绿灯;一次跑到 31 tests 后在 `apps.ai` 测试类建事务时远程 MySQL 连接超时,一次在测试库连接阶段被远程主机重置。`py -3.12 manage.py test apps.ai --noinput --keepdb` 也在远程连接重建时超时;已跑过测试无断言失败。失败点均为远程 MySQL `43.128.3.240:3306` 连接问题。
- 阻塞:无代码阻塞。远程 MySQL 对全量测试仍有间歇连接超时/重置;T-203 做并发扣点时需要继续使用 `--keepdb` 并必要时分 app 重试。
- 决策:
- 定价规则绑定能力别名字符串,不外键到具体 `AiModel` ;后台切换 `ModelAlias` 指向不改变价格。
- T-202 不实现扣点、退点、充值订单或 API 编排;这些留给 T-203/T-304/T-305/T-302。
- 下一步:领取 T-203 并发安全扣点 / 退点(billing 层)。
2026-07-02 16:33:31 +08:00
## 2026-07-02 T-203 并发安全扣点 / 退点(billing 层)
- 状态:DONE
- 变更:
- 新增 `apps/billing/services.py` : `precharge_call()` 在事务内 `select_for_update()` 锁 `UserWallet` 行,余额足够才预扣,写 `CallRecord(status=pending)` 与 `PointsLedger(consume, -N)` ;余额不足抛 `InsufficientPointsError(code="insufficient_points")` ,不写调用/流水。
- 新增 `mark_call_success()` :上游成功后只更新调用记录为 `success` 与结果摘要/耗时,不再改余额。
- 新增 `refund_call_points()` :失败退点锁 `CallRecord` ,同一调用只写一条 `refund` 流水并恢复余额;重复调用幂等,不重复加点;成功调用不能走失败退点。
- 扩展 `apps/billing/tests.py` :覆盖预扣写账、余额不足不写账、失败退点幂等、成功后拒绝失败退点、2 线程并发扣同一钱包不超扣不为负。测试数据改成唯一用户名/别名,避免远程 `test_cmhub` 断线残留造成重复键假失败。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` : Phase 2 完成,下一步 T-301。
- 验证:
- `py -3.12 -m py_compile apps\billing\services.py apps\billing\tests.py` :通过。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests --noinput --keepdb` :通过,4 tests OK。
- `py -3.12 manage.py test apps.billing.tests.ConcurrentDebitTests --noinput --keepdb` :通过,1 test OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test --noinput --keepdb` :未作为绿灯;发现 43 tests,跑到 42 tests 后远程 MySQL 连接超时,导致并发测试线程拿不到连接,测试库 flush/teardown 也断线。
- `py -3.12 manage.py test apps.billing --noinput --keepdb` / 不带 `--keepdb` :未作为绿灯;远程 MySQL 在 `PricingCalculationTests` 或测试库创建/销毁阶段超时/断线。T-203 新增的服务测试与并发测试已单独通过。
- 阻塞:无代码阻塞。远程 MySQL `43.128.3.240:3306` 对完整测试仍间歇超时/重置,并且失败后可能保留脏 `test_cmhub` ;后续建议优先用 `--keepdb` + 唯一测试数据分 app 验证,必要时清理测试库后再跑全量。
- 决策:
- T-203 只做 billing 层扣点/退点服务,不做 DRF 认证、生成接口编排、充值订单或支付回调。
- 失败退点以 `CallRecord` 行锁 + 已有 refund 流水检查实现幂等,不给 `CallRecord` 增加 refunded 状态。
- 下一步:领取 T-301 API Key 鉴权(DRF Authentication)。
## 2026-07-02 Phase 2 计费核心审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-2-review.md` ; `06-tasks.md` 新增 T-204 加固任务;`docs/README.md` 导航登记。
- 审核结论:**T-201~203 验收全部达标,质量为三阶段最高,零 P1**。§8 资金安全硬约束逐条核对通过——先扣后调/失败必退(precharge/mark_success/refund 三原语对齐 4.1 时序)、并发安全(事务 + `select_for_update` 锁 wallet + DB 级 `points_balance>=0` )、退点幂等(锁 CallRecord 行 + 查 REFUND 流水去重)、全程留痕(ledger/call_record admin 只读=只追加)、wallet 余额 admin 只读(后台无法绕过账本)、ApiKey sha256+hmac.compare_digest+prefix+hash 脱敏、计费按别名解耦模型 + 分辨率 wildcard、Decimal floor 计费、CallRecord 无 raw 字段、错误码 api.md 对齐(insufficient_points→402/no_pricing_rule→400)。
- 关键证据:`ConcurrentDebitTests` 用真线程 + Barrier 强制并发扣同一钱包,**在 MySQL `test_cmhub` 上单独跑过(1 test OK)**——`select_for_update` 真生效,不是 SQLite 假绿。DB 级 CHECK/Unique 约束全部落到迁移文件。
- 发现(详见 `phase-2-review.md` ):
- P1:无。
- P2-1:退点幂等缺 DB 级兜底(无「每 ref_call 最多一条 REFUND」约束,锁内安全但无 backstop);T-204 应加 MySQL 可落地的 `ref_call + change_type` 复合唯一约束 + 直写 IntegrityError 测试,不使用 MySQL 不支持的 partial unique / 条件唯一约束。
- P2-2:完整测试套件从未单次全绿(远程 MySQL 间歇超时/重置),靠分 app 子集通过佐证;建议在稳定 MySQL 上跑一次完整全绿留证,并标注「并发测试须在 MySQL 上跑,SQLite 会假绿」。
- P3(登记):手工调点服务未实现(模型层 reason 守卫已备好、wallet admin 已锁死=安全中间态)→ T-401;扣点热路径 wallet get_or_create 掩盖钱包缺失 → T-501;锁顺序约定;只追加靠 admin+无 update 保证(DB 无触发器);OperationType 双份枚举 DRY。
- 未本地复跑:审核机无 python3.12,结论基于静态审查(users/billing models、services、pricing、两 app admin、tests、全部迁移、settings) + codex 执行记录(T-201 34 tests / T-202 billing 10 tests / T-203 BillingServiceTests 4 + ConcurrentDebitTests 1 均单独在 MySQL 通过;完整套件因连接问题未单次全绿,失败点均为连接/建库超时非断言失败)。
- 下一步:codex 可领 T-204 做 P2 加固(无 P1,不阻塞 T-301)。
2026-07-02 16:59:02 +08:00
## 2026-07-02 T-204 Phase 2 计费核心审核加固
- 状态:BLOCKED( P2-1 已完成;P2-2 完整 `manage.py test` 单次全绿被远程 MySQL 连接稳定性阻塞)
- 变更:
- `apps/billing/models.py` :给 `PointsLedger` 增加 MySQL 可落地的 `UniqueConstraint(fields=("ref_call", "change_type"), name="unique_ledger_change_type_per_call")` ,作为同一调用重复写同类型流水的 DB 级兜底;没有使用 MySQL 不支持的 partial unique / 条件唯一约束。
- 新增迁移 `billing.0003_pointsledger_unique_ledger_change_type_per_call` ,已应用到当前 MySQL。
- `apps/billing/tests.py` :新增直写测试,确认同一 `CallRecord` 的一条 `CONSUME` + 一条 `REFUND` 可共存,第二条 `REFUND` 抛 `IntegrityError` 。
- `docs/03-tech-stack.md` :测试说明补充「涉及 `select_for_update` 的并发扣点测试必须在 MySQL 上跑,SQLite 会忽略行锁导致假绿」。
- `docs/06-tasks.md` / `docs/current-state.md` :记录 T-204 的 P2-1 完成、P2-2 因远程 MySQL 全量测试未单次全绿而阻塞;T-204 无 P1,不阻塞 T-301。
- 验证:
- `py -3.12 -m py_compile apps\billing\models.py apps\billing\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py migrate` :通过,应用 `billing.0003_pointsledger_unique_ledger_change_type_per_call` 。
- `py -3.12 manage.py showmigrations billing` :通过,`billing.0001/0002/0003` 均为 `[X]` 。
- `py -3.12 manage.py test apps.billing.tests.BillingCoreModelTests.test_points_ledger_allows_consume_and_refund_but_rejects_duplicate_refund --noinput --keepdb` :通过,1 test OK。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests apps.billing.tests.ConcurrentDebitTests --noinput --keepdb` :通过,5 tests OK。
- `py -3.12 manage.py test apps.billing --noinput --keepdb` :通过,16 tests OK。
- `Test-NetConnection 43.128.3.240 -Port 3306` :通过,`TcpTestSucceeded=True` 。
- `py -3.12 -m compileall apps` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test --noinput --keepdb` :两次未作为绿灯;一次发现 44 tests,跑到 42 tests 后 `apps.ai.tests.AiModelEncryptionTests` 建连接超时;一次跑到 34 tests 后 `apps.ai` 多个 TestCase 建连接超时。失败点均为远程 MySQL `43.128.3.240:3306` 连接超时。
- `py -3.12 manage.py test apps.users apps.billing --noinput --keepdb` :未作为绿灯;跑到 17/18 后 `ConcurrentDebitTests.setUp` 建连接超时。
- 阻塞:当前没有代码阻塞;P2-1 已完成且相关测试通过。P2-2 需要稳定 MySQL 环境完成一次完整全量测试单次绿灯,当前远程实例仍在多连接/长测试期间间歇超时。
- 下一步:若先处理 T-204 blocker,需要提供/切换稳定 MySQL 8.4 测试环境后重跑 `py -3.12 manage.py test --noinput` ;若接受 T-204 无 P1 且不阻塞后续,可进入 T-301 API Key 鉴权。
2026-07-02 17:29:26 +08:00
## 2026-07-02 T-204 blocker 处理:MySQL 测试稳定性与完整测试补跑
- 状态:DONE
- 变更:
- `config/settings.py` : MySQL `OPTIONS` 增加 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT` ,默认分别为 30 / 120 / 120 秒,避免远程测试库使用 PyMySQL 默认值导致连接稳定性误判。
- `.env.example` / `docs/env.md` / `docs/03-tech-stack.md` :同步登记 MySQL 客户端超时配置。
- `apps/billing/tests.py` : `ConcurrentDebitTests` 增加 `connections.close_all` cleanup,确保并发测试结束后 Django teardown 使用新连接,减少远程连接失效对 flush/post_migrate 的影响。
- `docs/06-tasks.md` / `docs/current-state.md` : T-204 从 `BLOCKED` 更新为 `DONE` ,下一步回到 T-301。
- 验证:
- `Test-NetConnection 43.128.3.240 -Port 3306` :通过,`TcpTestSucceeded=True` 。
- `py -3.12 manage.py shell -c "select version(), @@wait_timeout, @@interactive_timeout, @@connect_timeout, @@max_connections"` :通过;MySQL `8.4.8` , `max_connections=500` 。
- `py -3.12 -m py_compile config\settings.py apps\billing\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py test apps.users apps.billing --noinput --keepdb --verbosity 2` :通过,18 tests OK。
- `py -3.12 manage.py test apps.ai --noinput --keepdb --verbosity 2` :通过,26 tests OK。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :首次在测试库 setup/post_migrate 阶段遇到一次远程 MySQL read timeout,确认无残留 processlist 后立即重跑;第二次通过,44 tests OK,耗时 157.827s。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `./init.ps1` :通过,依赖同步与基础检查正常。
- 阻塞:无。远程 MySQL 仍可能偶发链路超时;后续测试不要并行跑共享同一 `test_cmhub` 的数据库测试,必要时用 `--keepdb` 串行重跑。
- 决策:T-204 的 P2-2 已补齐单次完整测试全绿证据;并发扣点测试仍必须在 MySQL 上跑,SQLite 不可作为验收依据。
- 下一步:领取 T-301 API Key 鉴权(DRF Authentication)。
2026-07-02 17:40:26 +08:00
## 2026-07-02 T-301 API Key 鉴权(DRF Authentication)
- 状态:DONE
- 变更:
- 新增 `apps/api/authentication.py` :实现 `ApiKeyAuthentication` ,只解析 `Authorization: Bearer <API_KEY>` ;对明文 Key 做 SHA-256 hash 后查 `ApiKey` ,成功后返回 `(user, api_key)` ,并更新 `last_used_at` 。
- 新增 `apps/api/errors.py` :提供统一错误响应结构 `{"error":{"code":...,"message":...}}` 。
- 更新 `apps/api/views.py` :新增 `ExternalApiView` ,外部 API 只挂 `ApiKeyAuthentication + IsAuthenticated` ,缺失/无效 Key 返回 `401 unauthorized` ,不接受 Web session。
- 更新 `apps/api/tests.py` :覆盖有效 Bearer Key、缺失 Key、无效 Key、错误鉴权 scheme、Key revoked、User disabled、Web session 不被外部 API 接受。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` : T-301 完成,下一步 T-302。
- 验证:
- `./init.ps1` :开工前通过;完成后再次通过。
- `py -3.12 -m py_compile apps\api\authentication.py apps\api\errors.py apps\api\views.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :通过,8 tests OK。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :通过,52 tests OK,耗时 54.905s。
- 阻塞:无。
- 决策:生成/余额等对外 API 后续统一继承 `ExternalApiView` ;充值下单/状态查询仍按用户端 session 流程另行实现,不使用该外部 API 基类。
- 下一步:领取 T-302 生成标题 / 图片接口。
2026-07-02 22:41:37 +08:00
## 2026-07-02 T-302 生成标题 / 图片接口
- 状态:DONE
- 变更:
- 新增 `apps/api/generation.py` :编排「别名解析 -> Provider 能力二次校验 -> 计费 -> 预扣 -> 调 Provider -> 成功确认 / 失败退点」,统一翻译 `insufficient_points` 、`no_pricing_rule` 、`model_not_allowed` 、`bad_request` 、`upstream_error` 。
- 新增 `apps/api/serializers.py` :校验 `POST /api/v1/generate/title` 与 `POST /api/v1/generate/image` 请求参数,仅把 `parameters` 字段传给 Provider,不把整包请求透传。
- 新增 `apps/api/storage.py` :图片结果用 Django `default_storage` 存到 `MEDIA_ROOT/generated/images/...` ,响应返回 `image_url` ,调用记录只写 URL / 摘要,不写 provider raw 或 base64。
- 新增 `apps/api/urls.py` 并接入 `config/urls.py` :开放 `/api/v1/generate/title` 与 `/api/v1/generate/image` 。
- `config/settings.py` :补 `MEDIA_ROOT` / `MEDIA_URL` 。
- `apps/ai/aliases.py` :新增 `resolve_model_alias()` ,用于默认别名场景拿到真实 alias 参与计费和调用记录;原 `resolve_alias()` 保持兼容。
- 扩展 `apps/api/tests.py` :覆盖标题成功扣点、图片存储返回 URL、余额不足不调上游、缺计费规则不扣点、别名能力不匹配、Provider 能力二次校验、上游失败退点、`AiCapabilityError` 返回 400 且退点。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 。
- 验证:
- `./init.ps1` :开工前通过。
- `py -3.12 -m py_compile apps\api\generation.py apps\api\serializers.py apps\api\storage.py apps\api\urls.py apps\api\views.py apps\api\tests.py apps\ai\aliases.py config\settings.py config\urls.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :通过,16 tests OK。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :通过,60 tests OK,耗时 133.953s。
- 阻塞:无。真实上游标题/图片接口未跑,原因是当前环境未配置可用 `AI_KEY_ENCRYPTION_KEY` + AiModel/ModelAlias + 真实上游 key;图片同步真实耗时风险仍保留到 T-403 部署前校准。
- 决策:MVP 图片结果先用本地 media 存储返回 URL,不引入 S3 依赖;后续生产对象存储可替换 `default_storage` 。
- 下一步:领取 T-303 余额查询接口。
2026-07-03 08:36:30 +08:00
## 2026-07-02 T-303 余额查询接口
- 状态:DONE
- 变更:
- `apps/billing/services.py` :新增 `BalanceSnapshot` 与 `get_balance_snapshot(user)` ,只读返回 `UserWallet.points_balance` 与 `PointsLedger.points_delta` 累加值,供余额接口和后续对账复用。
- `apps/api/views.py` :新增 `BalanceView` ,继承 `ExternalApiView` ,只接受 API Key 鉴权,返回当前用户名与 `points_balance` 。
- `apps/api/urls.py` :新增 `GET /api/v1/balance` 路由。
- `apps/api/tests.py` :新增余额接口测试,覆盖余额响应与流水累加一致、缺失钱包返回 0 且不创建钱包、Web session 不能调用外部余额接口。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` : T-303 完成,下一步 T-304。
- 验证:
- `./init.ps1` :开工前通过,依赖同步与 `manage.py check` 正常。
- `py -3.12 -m py_compile apps\billing\services.py apps\api\views.py apps\api\urls.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :通过,19 tests OK。
- `py -3.12 manage.py test apps.ai.tests.AiModelEncryptionTests --noinput --keepdb --verbosity 2` (设置 `MYSQL_CONNECT_TIMEOUT=90` 补跑失败类):通过,2 tests OK。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `Test-NetConnection 43.128.3.240 -Port 3306` :一次超时,后续通过,`TcpTestSucceeded=True` 。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :多次未作为绿灯;一次工具超时未取得明细,后续运行分别在 `apps.users.tests.AdminSmokeTests.setUpClass` / `apps.ai.tests.AiModelEncryptionTests.setUpClass` / `apps.billing.tests.ConcurrentDebitTests.setUp` 或测试库 setup 阶段遇到远程 MySQL 连接超时或 reset;已跑过的测试均无断言失败。
- 阻塞:T-303 功能与相关测试无阻塞;完整测试当前被远程 MySQL `43.128.3.240:3306` 连接稳定性阻塞,后续建议数据库连通稳定后重跑一次 `py -3.12 manage.py test --noinput --keepdb --verbosity 2` 。
- 决策:余额接口返回 `UserWallet.points_balance` 作为对外余额;测试用流水累加值校验账务场景一致性。接口只读,不在 API 层创建或修改钱包。
- 下一步:领取 T-304 充值回调(微信/支付宝验签 + 幂等入账)。
2026-07-03 09:07:21 +08:00
## 2026-07-03 T-304 充值回调(微信/支付宝验签 + 幂等入账)
- 状态:DONE
- 变更:
- `apps/billing/models.py` :新增 `RechargeOrder` ,保存 `order_no` 、用户、金额、通道、下单锁定汇率、预计/实际入账点数、订单状态、支付流水号与支付时间;新增 `points_ledger(ref_order_id, change_type)` 复合唯一约束,作为同一订单重复充值流水的 DB 兜底。
- `apps/billing/services.py` :新增 `RechargePayment` / `RechargeResult` 、`apply_recharge_payment()` 与 `query_and_apply_recharge_payment()` ;入账时锁订单,校验 pending、金额、通道,锁 `UserWallet` 加点,写 `PointsLedger(recharge)` ,重复回调幂等返回不重复加点。
- `apps/billing/payment_gateways.py` :新增微信 / 支付宝回调 verifier;`PAYMENT_CALLBACK_MODE=mock` 用 HMAC 模拟验签,`sdk` 模式走 `wechatpayv3` / `python-alipay-sdk` 入口,缺 SDK 或配置时失败不入账。
- `apps/api/views.py` / `apps/api/urls.py` :新增 `POST /api/v1/recharge/callback/wechat` 与 `/alipay` ,两个端点均无登录态、显式 `@csrf_exempt` ;微信成功返回 `{"code":"SUCCESS","message":"成功"}` ,支付宝成功返回纯文本 `success` 。
- `apps/billing/admin.py` :注册只读 `RechargeOrderAdmin` 。
- `apps/billing/tests.py` :覆盖订单 admin 注册、同一订单重复充值流水 DB 兜底、成功入账、重复回调幂等、金额不一致不入账、主动查单复用同一入账路径。
- `apps/api/tests.py` :覆盖微信/支付宝回调成功与重复回调幂等、mock 验签失败不入账、金额不一致不入账、开启 CSRF 检查时回调端点仍可被支付网关调用。
- 新增迁移 `apps/billing/migrations/0004_rechargeorder_and_more.py` ,已应用到当前 MySQL。
- 同步更新 `.env.example` 、`README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/env.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 。
- 验证:
- `./init.ps1` :开工前通过,依赖同步与 `manage.py check` 正常。
- `py -3.12 manage.py makemigrations billing` :生成 `billing.0004_rechargeorder_and_more` 。
- `py -3.12 -m py_compile apps\billing\models.py apps\billing\admin.py apps\billing\services.py apps\billing\payment_gateways.py apps\billing\tests.py apps\api\views.py apps\api\urls.py apps\api\tests.py config\settings.py` :通过。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 manage.py migrate` :通过,应用 `billing.0004_rechargeorder_and_more` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py showmigrations billing` :通过,`billing.0001` ~ `0004` 均为 `[X]` 。
- `py -3.12 manage.py test apps.billing apps.api --noinput --keepdb --verbosity 2` :通过,45 tests OK。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :未作为绿灯;本轮跑到 65/73 后,远程 MySQL 连接超时导致 `BillingCoreModelTests.setUpClass` 失败,并引发并发测试 barrier 失败。
- `$env:MYSQL_CONNECT_TIMEOUT='90'; py -3.12 manage.py test apps.billing.tests.BillingCoreModelTests apps.billing.tests.ConcurrentDebitTests --noinput --keepdb --verbosity 2` :业务用例均跑过并显示 ok;最终测试库 flush 阶段仍遇到远程 MySQL 连接超时,因此命令整体未作为绿灯。
- 阻塞:T-304 功能与相关测试无阻塞;完整测试仍被远程 MySQL `43.128.3.240:3306` 连接稳定性阻塞。生产真实支付还需要安装/配置微信、支付宝 SDK 与商户密钥/证书;当前 mock 仅用于本地/测试联调。
- 决策:T-304 只做回调验签与幂等入账;扫码下单、订单状态查询、前端轮询留给 T-305。`PointsLedger.ref_order_id` 本轮仍保持数值引用,配合复合唯一约束兜底防重复充值流水。
- 下一步:领取 T-305 扫码充值下单 + 轮询(create/status)。
2026-07-03 09:34:07 +08:00
## 2026-07-03 T-305 扫码充值下单 + 轮询(create/status)
- 状态:DONE
- 变更:
- `apps/billing/services.py` :新增 `create_recharge_order()` ,创建 pending 订单并绑定发起用户;下单时锁定当前 `ExchangeRate.points_per_unit` 与 `points_granted` ,再调用支付网关下单取二维码票据;支付平台下单失败时订单标记 `failed` 。
- `apps/billing/payment_gateways.py` :新增 `PaymentOrderCode` 、`create_payment_order()` 、微信/支付宝 mock 二维码生成、微信金额分转换、支付宝金额元格式化,以及 SDK 模式下单/查单入口;mock 查单明确返回不可用,等待回调或测试注入主动查单结果。
- `apps/api/views.py` / `apps/api/serializers.py` / `apps/api/urls.py` :新增 `POST /api/v1/recharge/create` 与 `GET /api/v1/recharge/status` ;两个端点走 `SessionAuthentication + IsAuthenticated` ,不继承外部 API Key 视图;create 受 CSRF 保护,status 只允许订单所属用户访问,并在 pending 时尝试主动查单补入账。
- `config/settings.py` / `.env.example` / `docs/env.md` :新增 `PAYMENT_QR_EXPIRES_MINUTES` ,仅作为二维码本地有效期提示,不自动把 pending 订单改 expired。
- `apps/billing/tests.py` :覆盖创建充值订单锁定汇率/点数/二维码票据、网关下单失败时订单标记 failed。
- `apps/api/tests.py` :覆盖 session 下单、API Key 不能调用充值下单、真实 session 客户端无 CSRF 被拒、支付宝 mock 二维码、状态查询本人可见/他人 404、主动查单补入账且不重复入账。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 。
- 验证:
- `./init.ps1` :开工前通过,依赖同步与基础检查正常。
- `py -3.12 -m py_compile apps\billing\payment_gateways.py apps\billing\services.py apps\billing\tests.py apps\api\serializers.py apps\api\views.py apps\api\urls.py apps\api\tests.py config\settings.py` :通过。
- `py -3.12 manage.py test apps.billing apps.api --noinput --keepdb --verbosity 2` :通过,53 tests OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :未作为绿灯;发现 81 tests,跑过 71 tests,已运行用例均无断言失败;远程 MySQL `43.128.3.240:3306` 连接超时导致 `BillingCoreModelTests.setUpClass` 与 `AdminSmokeTests.setUpClass` 失败。
- `$env:MYSQL_CONNECT_TIMEOUT='90'; py -3.12 manage.py test apps.billing.tests.BillingCoreModelTests apps.users.tests.AdminSmokeTests --noinput --keepdb --verbosity 2` :通过,10 tests OK。
- 补微信主动查单 SDK 入口后,`py -3.12 -m py_compile apps\billing\payment_gateways.py apps\billing\services.py apps\api\views.py apps\api\tests.py` :通过;`py -3.12 manage.py check` :通过。
- `py -3.12 manage.py test apps.api.tests.RechargeCreateStatusApiTests apps.billing.tests.BillingServiceTests.test_create_recharge_order_locks_exchange_rate_points_and_qr_code apps.billing.tests.BillingServiceTests.test_create_recharge_order_marks_order_failed_when_gateway_fails --noinput --keepdb --verbosity 2` :未作为绿灯;API 新增 6 tests OK 后,进入 billing 测试类时远程 MySQL 连接超时。
- `$env:MYSQL_CONNECT_TIMEOUT='90'; py -3.12 manage.py test apps.billing.tests.BillingServiceTests.test_create_recharge_order_locks_exchange_rate_points_and_qr_code apps.billing.tests.BillingServiceTests.test_create_recharge_order_marks_order_failed_when_gateway_fails --noinput --keepdb --verbosity 2` :通过,2 tests OK。
- 阻塞:T-305 功能与相关测试无阻塞;完整测试仍被远程 MySQL 连接稳定性阻塞。真实支付生产下单 / 查单仍需要安装 SDK 并提供微信、支付宝商户密钥/证书与公网 notify_url;当前 mock 用于本地/测试联调。
- 决策:状态轮询不因本地 `expires_at` 自动把 pending 订单改 expired,避免误挡延迟到达的真实支付回调;二维码过期只作为前端提示,支付结果仍以回调或主动查单入账为准。T-305 只提供服务端 create/status,实际用户端充值页面留给 T-504。
- 下一步:领取 T-501 注册 / 登录(allauth)。
2026-07-03 09:53:23 +08:00
## 2026-07-03 Phase 3 对外 API 与充值审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-3-review.md` ; `06-tasks.md` 新增 T-306 安全加固任务;`docs/README.md` 导航登记。
- 审核结论:**T-301~305 功能验收全部达标,测试覆盖为四阶段最全**。§8/4.2 逐条核对通过——对外只认 Key 不认 Session(含专测)、回调 @csrf_exempt + 验签前置(mock 用 HMAC-SHA256+compare_digest 真验签、mock/真实 SDK 双路径由 PAYMENT_CALLBACK_MODE 切换、SDK 惰性导入)、入账锁订单行幂等 + 金额校验 + 用订单锁定 points_granted + 锁钱包写 RECHARGE 流水、主动查单兜底复用同一入账、生成编排 4.1 全分支(余额不足不调上游/能力不符不扣点/上游失败先退点)均有测试;Portal 会话接口自动强制 CSRF(含专测);resolve_alias 保留向后兼容未破 smoke。
- 发现(详见 `phase-3-review.md` ):
- P1-1(上线前必修,安全):`download_image_input` 拉取调用方任意 `image_url` = SSRF;且在 precharge **之前**执行,0 点数用户即可无成本触发;无内网/回环/link-local 拦截、无大小上限、无重定向限制(可 30x/DNS rebinding 绕过)。须加协议白名单 + 网段拦截(含重定向后地址)+ 大小上限 + 测试。
- P2-1: settings 无全局 REST_FRAMEWORK 默认,DRF 默认认证是 Session+Basic;外部视图都显式覆盖为 ApiKey(当前安全),但将来新增视图忘设置会默默继承默认——建议设 DEFAULT_AUTHENTICATION_CLASSES=[] 强制显式声明。
- P2-2:生成/认证端点无限流(刷点/放大上游成本/Key 枚举风险),建议 ScopedRateThrottle。
- P2-3:充值金额只有 min 无 max,建议加单笔上限。
- P3(登记):充值流水 DB 兜底已存在(T-304 已加 `unique(ref_order_id, change_type)` ,并有重复充值流水 `IntegrityError` 测试),非待办;生成图片 /media 公网 URL 无 per-user 访问控制;T-305 后完整套件未再单次全绿(远程 MySQL 不稳,T-302 时有 60 tests 全绿);过期订单不自动置 expired(有意设计)。
- 未本地复跑:审核机无 python3.12,结论基于静态审查(api authentication/generation/views/serializers/storage/urls; billing services/payment_gateways/models/migrations; settings;两 app tests) + codex 执行记录(T-301 52 / T-302 60 tests 单次全绿;T-303~305 分 app 子集通过,完整套件受远程 MySQL 连接稳定性阻塞,失败点为连接超时非断言失败)。
- 口径校正:后续核对发现报告 P3 把充值流水 DB 兜底误列为待办;T-304 已在 `billing.0004_rechargeorder_and_more` 落地 `unique(ref_order_id, change_type)` ,并有重复充值流水 `IntegrityError` 测试。后续文档已改为“DB 兜底已存在,非待办”。
- 下一步:codex 领 T-306 做 P1 SSRF 修复 + P2 加固(P1 属上线前必须)。
## 2026-07-03 Phase 3 审核报告口径修正(文档)
- 状态:DONE
- 变更:
- 修正 `docs/phase-3-review.md` :P3 中充值流水 DB 兜底口径改为“已存在,非待办”,明确 T-306 不应重复增加同类迁移。
- 修正 `progress.md` 中同一处错误口径。
- 更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` :下一步从 T-501 改为 T-306。
- 更新 `docs/06-tasks.md` :T-306 验收口径明确 P3 只登记媒体访问控制、完整测试补跑和过期订单口径。
- 验证:文档口径修正,无代码变更;执行 `git diff --check` 作为格式检查。
- 下一步:领取 T-306 Phase 3 对外 API 安全加固。
2026-07-03 10:34:37 +08:00
## 2026-07-03 T-306 Phase 3 对外 API 安全加固
- 状态:DONE
- 变更:
- `apps/api/generation.py` :重写 `download_image_input()` 安全边界;`image_url` 只允许 `http` / `https` ,请求前解析域名并拒绝私有、回环、链路本地、保留、组播、未指定地址;手动跟随重定向并逐跳重校验;响应按 `IMAGE_URL_MAX_BYTES` 流式限长读取,超限 / 非图片 / 空内容均返回 `bad_request` ,且发生在预扣点前。
- `apps/api/throttles.py` / `apps/api/exceptions.py` :新增生成接口按 API Key / 用户限流、认证失败按 IP 限流;DRF 429 统一返回 `rate_limited` 错误结构。
- `apps/api/authentication.py` / `apps/api/views.py` :无效 / 畸形 / 缺失 API Key 进入认证失败限流;生成标题、生成图片视图挂生成限流。
- `config/settings.py` :新增全局 `REST_FRAMEWORK` 安全默认(默认空认证 + 默认 `IsAuthenticated` ,强制视图显式 opt-in)、限流 / `image_url` 下载 / 充值金额上限配置。
- `apps/api/serializers.py` : `RechargeCreateRequestSerializer` 增加 `RECHARGE_MAX_AMOUNT_CNY` 单笔金额上限校验。
- `apps/api/tests.py` :新增裸 `APIView` 不接受 Web session 默认认证、无效 Key 限流、生成接口限流、`image_url` 回环 / 云元数据 / 重定向到内网 / 超大响应拒绝且不扣点、合法公网图片下载、充值金额上限测试。
- 同步更新 `.env.example` 、`README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/env.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `py -3.12 -m py_compile config/settings.py apps/api/exceptions.py apps/api/generation.py apps/api/authentication.py apps/api/views.py apps/api/serializers.py apps/api/throttles.py apps/api/tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.api.tests.ApiKeyAuthenticationTests.test_invalid_api_key_failures_are_throttled_by_ip apps.api.tests.GenerateApiTests.test_generate_endpoint_is_throttled_by_api_key_without_extra_charge --noinput --keepdb --verbosity 2` :通过,2 tests OK。
- `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :通过,39 tests OK。
- `py -3.12 -m compileall apps config` :通过。
- `py -3.12 manage.py test apps.billing apps.api --noinput --keepdb --verbosity 2` :未作为绿灯;62 tests 跑到末尾,功能用例输出均为 ok,最后远程 MySQL 在并发扣点测试的 flush/重连阶段 reset 连接,报 `OperationalError (2013 Lost connection to MySQL server during query)` 。
- `$env:MYSQL_CONNECT_TIMEOUT='90'; py -3.12 manage.py test apps.billing.tests.ConcurrentDebitTests --noinput --keepdb --verbosity 2` :通过,1 test OK。
- `./init.ps1` :通过,Python 3.12.3、依赖已满足、`manage.py check` 0 issues。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:T-306 功能无阻塞;完整大套件仍受远程 MySQL 连接稳定性影响。生产真实支付仍需微信 / 支付宝商户密钥、证书与 SDK 配置;真实 AI 上游 smoke 仍需配置 `AI_KEY_ENCRYPTION_KEY` 并导入 AiModel/ModelAlias。
- 下一步:领取 T-501 注册 / 登录(allauth)。
2026-07-03 11:27:45 +08:00
## 2026-07-03 T-501 注册 / 登录(allauth)
- 状态:DONE
- 变更:
- `requirements.txt` :新增 `django-allauth>=65.18,<66` ,本机安装到 65.18.0。
- `config/settings.py` / `config/urls.py` :接入 `allauth` / `allauth.account` 、allauth authentication backend、`AccountMiddleware` 、邮箱验证 mandatory、登录/登出跳转、邮件 backend 配置、项目模板优先级、`accounts/` 路由与 portal 页面路由。
- `apps/portal/` :新增 `CmhubAccountAdapter` , allauth 注册成功后创建 `UserWallet(points_balance=0)` ,不写赠点流水;新增 `/signup` 、`/login` 、`/logout` 、`/dashboard` 路由,最小 dashboard 只读显示点数余额;新增 Bootstrap 模板与中文验证邮件模板。
- `apps/portal/tests.py` :覆盖注册创建未验证邮箱与 0 点钱包、无赠点流水、未验证邮箱不能登录、已验证用户登录/dashboard/登出、dashboard session 保护、登录 POST CSRF 保护。
- 同步更新 `.env.example` 、`README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/env.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 、`docs/phase-2-review.md` 。
- 验证:
- `./init.ps1` :开工前通过,依赖同步与 `manage.py check` 正常。
- `py -3.12 -m pip install -r requirements.txt` :通过,安装 `django-allauth 65.18.0` 。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 -m py_compile config\settings.py config\urls.py apps\portal\adapters.py apps\portal\urls.py apps\portal\views.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` :通过,5 tests OK。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py migrate --noinput` :通过,应用 `account.0001` ~ `account.0009` ; MySQL 对 allauth `account.EmailAddress` 条件唯一约束提示 `models.W036` ,第三方表的条件约束不会创建。
- `py -3.12 manage.py test apps.users apps.portal --noinput --keepdb --verbosity 2` :通过,7 tests OK。
- `py -3.12 -m compileall apps config` :通过。
- `py -3.12 manage.py shell -c "from django.test import Client; r=Client().get('/login'); print(r.status_code); print(r.content[:120])"` :通过,`/login` 返回 200;修正模板优先级后确认返回项目内 `portal/base.html` 模板。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :首次在 6 分钟工具超时内无结果;随后设置 `$env:MYSQL_CONNECT_TIMEOUT='90'` 、`$env:MYSQL_READ_TIMEOUT='180'` 、`$env:MYSQL_WRITE_TIMEOUT='180'` 后重跑通过,95 tests OK,234.743s;修正模板优先级后再次完整重跑通过,95 tests OK, 301.722s。
- 阻塞:T-501 功能无阻塞。生产邮箱验证需要配置真实邮件服务;当前默认 console backend 只适合本地开发。allauth 的 `EmailAddress` 条件唯一约束在 MySQL 上有第三方模型警告,但本项目账本唯一性由 `user.email` 唯一约束承担。
- 决策:不引入 crispy-forms; T-501 只需要简单 allauth 表单,MVP 先用 Django form + Bootstrap 模板。注册路径只初始化 0 点钱包,不创建 `PointsLedger` ,避免被误解为注册赠点。
- 下一步:领取 T-502 API Key 自助管理页。
2026-07-03 11:51:46 +08:00
## 2026-07-03 T-502 API Key 自助管理页
- 状态:DONE
- 变更:
- `apps/portal/forms.py` :新增 `ApiKeyCreateForm` ,用于用户端填写 Key 名称。
- `apps/portal/views.py` / `urls.py` :新增 `/apikeys` 列表 + 生成页,以及 `/apikeys/<id>/delete` 删除动作;生成复用 `ApiKey.create_for_user()` ,完整明文只放入 session 供重定向后的首个页面显示一次;删除写为 `revoked` ,不物理删除。
- `apps/portal/templates/portal/apikeys.html` / `base.html` / `dashboard.html` :新增 API Key 管理页、导航入口和 dashboard 入口;列表只显示 prefix、名称、状态和时间。
- `apps/portal/tests.py` :新增 6 条用户端 API Key 测试,覆盖登录保护、明文只显示一次、库内只存 hash、列表只见本人 prefix、CSRF 保护、删除后外部 API 返回 403。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `py -3.12 -m py_compile apps\portal\forms.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` :通过,11 tests OK。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` :通过,52 tests OK。
- `./init.ps1` :通过,Python 3.12.3、依赖已满足、`manage.py check` 0 issues。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:无。测试阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目账本邮箱唯一性由 `user.email` 唯一约束承担。
- 决策:T-502 原任务写“删除后该 Key 调用 401”,但项目架构和 T-301 已明确 `revoked` Key 返回 403、缺失/无效/不存在 Key 才返回 401;本轮将用户端“删除”定义为吊销 `revoked` ,保留审计与调用记录关联,并同步修正文档口径。
- 下一步:领取 T-503 个人中心 / 记录页。
2026-07-03 14:00:00 +08:00
## 2026-07-03 T-503 个人中心 / 记录页
- 状态:DONE
- 变更:
- `apps/portal/views.py` :新增只读账号汇总 helper、充值订单查询和消费流水查询;`/dashboard` 汇总剩余点数、充值总额、入账点数、累计扣点、退款和净消耗,并展示最近充值 / 消费记录。
- `apps/portal/urls.py` :新增 `/records/recharge` 与 `/records/usage` 。
- `apps/portal/templates/portal/dashboard.html` :扩展个人中心汇总与最近记录。
- `apps/portal/templates/portal/recharge_records.html` :新增充值记录页,按当前用户查询订单,汇总已支付订单金额与入账点数。
- `apps/portal/templates/portal/usage_records.html` :新增消费记录页,按当前用户查询 `consume` / `refund` 点数流水并关联调用信息。
- `apps/portal/templates/portal/base.html` :增加充值记录 / 消费记录导航入口。
- `apps/portal/tests.py` :新增 dashboard 汇总、充值记录、消费记录三类测试,覆盖数据与流水一致、记录仅见本人、未登录跳转。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `./init.ps1` :开工前通过,Python 3.12.3、依赖已满足、`manage.py check` 0 issues。
- `py -3.12 -m py_compile apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` :通过,14 tests OK。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` :通过,55 tests OK。
- `./init.ps1` :收尾通过,依赖已满足、`manage.py check` 0 issues。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:无。测试阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目账本邮箱唯一性由 `user.email` 唯一约束承担。
- 决策:T-503 页面只读,不写 `UserWallet.points_balance` ;余额用 `get_balance_snapshot()` ,充值总额按 paid `RechargeOrder.amount_money` 汇总,入账点数按 `PointsLedger(recharge)` 汇总,消费记录直接展示 `PointsLedger(consume/refund)` 并关联 `CallRecord` ,保证记录与流水同源。
- 下一步:领取 T-504 充值页(扫码 + 轮询到账)。
2026-07-03 14:49:45 +08:00
## 2026-07-03 T-504 充值页(扫码 + 轮询到账)
- 状态:DONE
- 变更:
- `apps/portal/forms.py` :新增 `RechargeCreateForm` ,校验充值金额、支付方式和 `RECHARGE_MAX_AMOUNT_CNY` 单笔上限。
- `apps/portal/views.py` / `urls.py` :新增 `/recharge` 用户端页面;GET 展示余额、充值表单、当前订单和最近充值;POST 复用 `create_recharge_order()` 创建 pending 订单并重定向到当前订单页,避免刷新重复下单。
- `apps/portal/templates/portal/recharge.html` :新增充值页,展示支付二维码票据,保留可复制票据兜底;浏览器轮询 `/api/v1/recharge/status` ,订单 paid 后刷新页面重新读取余额。
- `apps/portal/templates/portal/base.html` / `dashboard.html` :新增充值入口。
- `apps/portal/tests.py` :新增充值页测试,覆盖登录保护、创建订单不加点不写流水、当前订单仅见本人、金额上限和 CSRF 保护。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `./init.ps1` :开工前通过,Python 3.12.3、依赖已满足、`manage.py check` 0 issues。
- `py -3.12 -m py_compile apps\portal\forms.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` :通过,19 tests OK。
- `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` :首次因远程 MySQL 连接中断失败;拉长 MySQL 超时并在网络恢复后复跑通过,60 tests OK。
- `./init.ps1` :收尾通过,依赖已满足、`manage.py check` 0 issues。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:T-504 功能无阻塞。测试阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 警告;本项目账本邮箱唯一性由 `user.email` 唯一约束承担。真实支付仍需微信 / 支付宝商户密钥、证书、生产 SDK 依赖与公网回调地址。
- 决策:`/recharge` 页面 POST 直接复用 billing 层 `create_recharge_order()` 创建订单,JSON `POST /api/v1/recharge/create` 继续保留给用户端脚本或后续前端调用;页面本身不写钱包和流水,到账只认回调或主动查单入账后的本地订单状态。
- 下一步:领取 T-401 运营后台完善。
2026-07-03 15:14:17 +08:00
## 2026-07-03 Phase 4 用户端审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-4-review.md` ; `06-tasks.md` 新增 T-505 优化任务;`docs/README.md` 导航登记。
- 审核结论:**T-501~504 功能验收全部达标,测试覆盖极全,零 P1(无安全/资金/越权缺陷)**。逐条核对:注册建 0 点钱包且不写 PointsLedger(不送点数)、未验证邮箱不能登录、所有页面 LoginRequiredMixin + POST 带 CSRF、API Key 明文只显一次(session pop)且库内只存哈希、删除=软吊销且吊销后外部 API 返回 403、删他人 Key 返回 404( IDOR)、所有记录/订单按 user 过滤(仅见本人)、充值下单不加点不写流水、轮询 T-305 status 端点、到账经幂等回调/查单入账(页面只读、以回调为权威)——每条都有专测。T-501 有一次完整 95 tests 单次全绿。
- 发现(详见 `phase-4-review.md` ):
- P1:无。
- P2-1: base.html/recharge.html 从 jsdelivr 加载 Bootstrap/qrcode 且无 SRI/crossorigin——已登录/支付页的供应链风险 + jsdelivr 国内不稳(微信支付宝产品)。建议自托管静态资源(衔接 T-403 collectstatic)或加 integrity+crossorigin。
- P2-2:记录页 RECORDS_PAGE_SIZE=50 仅切片无真正分页,>50 条历史静默截断。建议 Paginator 或明确截断提示。
- P3(登记):API Key 数量无上限、充值下单无频率限制(→Backlog);生产必须配真实 SMTP 邮件后端否则邮箱验证发不出(→T-403);ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=True 邮件链接即登录(备忘);新 Key 明文短暂经 session 存储;allauth EmailAddress W036 cosmetic 警告(邮箱唯一性已由 User.email 承担)。
- 未本地复跑:审核机无 python3.12,结论基于静态审查(portal views/adapters/forms/urls/全部模板/tests; settings allauth 段)+ codex 执行记录(T-501~504 分 app 通过、T-501 一次 95 tests 单次全绿、T-504 完整 60 tests 通过,失败点均为远程 MySQL 连接超时非断言失败)。
- 下一步:领取 T-505 做 Phase 4 用户端审核优化;完成后再进入 T-401 运营后台完善。
## 2026-07-03 Phase 4 审核任务口径修正(文档)
- 状态:DONE
- 变更:统一新增 T-505 后的下一步口径:因任务看板规则要求领取第一个 `TODO` 且依赖均 `DONE` 的任务,T-505 作为 Phase 4 用户端审核优化先于 T-401 执行;同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 、`docs/06-tasks.md` 、`docs/phase-4-review.md` 与本执行流水。
- 验证:文档口径修正,无代码变更;执行 `git diff --check` 。
- 下一步:领取 T-505 Phase 4 用户端审核优化。
2026-07-03 16:02:29 +08:00
## 2026-07-03 T-505 Phase 4 用户端审核优化
- 状态:DONE
- 变更:
- `apps/portal/static/portal/vendor/` :新增本地自托管 `bootstrap/bootstrap.min.css` 与 `qrcode/qrcode.js` ,并随附许可证文件;`base.html` / `recharge.html` 改用 `{% static %}` ,移除 jsdelivr 运行时依赖。
- `apps/portal/views.py` :新增 `paginate_records()` ,充值记录与消费记录从固定 `[:50]` 切片改为 Django `Paginator` 分页。
- `apps/portal/templates/portal/includes/pagination.html` :新增分页片段;充值记录 / 消费记录页展示总数、每页数量、当前页和上一页 / 下一页。
- `apps/portal/tests.py` :新增本地静态资源断言、无 CDN 断言,以及充值记录 / 消费记录各 >50 条分页测试。
- `docs/06-tasks.md` : T-505 标记 DONE; P3 后续项登记到 T-403 或 Backlog:生产邮件配置、API Key 数量治理、充值下单频控、邮箱确认自动登录 / raw key session 策略复核、allauth W036 已知警告。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `./init.ps1` :开工前通过;收尾通过。
- `py -3.12 -m py_compile apps\portal\views.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` :通过,均找到本地 static 文件。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 -m compileall apps config` :通过。
- `rg -n "cdn\.jsdelivr|jsdelivr|qrcode@1\.5\.4" apps\portal\templates apps\portal\static` :无命中。
- `py -3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2` :通过,21 tests OK。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_records_paginate_more_than_page_size apps.portal.tests.PortalAccountFlowTests.test_usage_records_paginate_more_than_page_size --noinput --keepdb --verbosity 2` :通过,2 tests OK。
- 尝试 `py -3.12 manage.py test apps.users apps.portal apps.api --noinput --keepdb --verbosity 2` :未取得单次全绿;第一次在 `setUpClass` 前远程 MySQL 超时,第二次跑过 users + portal + 多数 API 后 MySQL 出现 `OperationalError 2003/2013` / 连接重置,已通过用例无断言失败。
- 尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :未取得单次全绿;36 个 API 测试已运行,多数通过,`BalanceApiTests.setUpClass` 因远程 MySQL `Packet sequence number wrong` 中断。`Test-NetConnection 43.128.3.240 -Port 3306` 后续显示端口可达,判断为远程 MySQL 间歇稳定性问题。
- 阻塞:T-505 功能无阻塞;完整大套件单次全绿仍受远程 MySQL 连接稳定性影响。
- 决策:T-505 仅处理审核报告 P2;P3 不在本轮实现,已登记到 T-403 或 Backlog。
- 下一步:领取 T-401 运营后台完善。
2026-07-03 16:06:34 +08:00
## 2026-07-03 T-505 复核(Claude Code review,非任务)
- 状态:DONE(复核完成,结论:达标)
- 对象:codex 按 `docs/phase-4-review.md` 完成的 T-505 修复。
- 结论:**达标**。P2-1 去 CDN: Bootstrap/qrcode 已自托管到 `apps/portal/static/portal/vendor/` + `{% static %}` ,无 jsdelivr 残留、`findstatic` 通过(开发环境层面达标)。P2-2 分页:`paginate_records` 返回 paginator/page_obj/is_paginated/records_total_count/records_page_size 全套,模板正确渲染总数与翻页;测试造 55 条验第 1/2 页且跨页仍不泄露他人订单(越权防护在分页下依然成立)。P3 已登记 T-403/Backlog。
- 复核发现的补充(非 T-505 失败,属部署衔接):自托管只做了 app 层,`settings` 仅有 `STATIC_URL` 、无 `STATIC_ROOT` /collectstatic/WhiteNoise, `DEBUG=False` 生产下 Django 不发静态文件→CSS 错版、二维码 404。**已把「生产静态文件 serving」登记到 T-403 验收要点**(与邮件后端、限流共享缓存并列)。
- 验证依据:静态审查 portal views/templates/tests + settings; codex 记录 portal 21 tests OK、分页 2 tests OK;完整套件仍受远程 MySQL 间歇断连影响(非断言失败)。
## 2026-07-03 T-105 复核(Claude Code review,非任务|补录)
- 说明:本条为补录——T-105 复核实际发生在 2026-07-02,当时只在对话中给出结论、未落 progress.md,现补齐以与「审核轮」对称。
- 结论:**达标**。P1-1 `parameters` /`extra_body` 白名单化落地,核心/计费字段(model/messages/stream/n/size)不可被覆盖,两条越权拦截专测(chat + images_edits)断言 model 不变、n 丢弃、temperature 生效。P2 全处理:`resolution_to_size` 大小写归一、`.env.example` 死配置 `AI_DEFAULT_*` 删除、默认别名对齐 `image-hd` 。T-104 结论改写为「图片同步风险未退」并挂 T-302/T-403, smoke 增 `image` 。
- 复核发现的残留(登记,非阻塞):codex 把可信的 `extra_body` 也锁进了与不可信 `parameters` 同一张白名单——偏安全(fail-closed)但偏离原设计的「extra_body 保留覆盖能力」,将来接需要特殊 body 字段的供应商需改代码。属方向正确的加严,登记备忘。
## 2026-07-03 T-306 复核(Claude Code review,非任务|补录)
- 说明:本条为补录——T-306 复核实际发生在 2026-07-03 稍早,当时只在对话中给出结论、未落 progress.md,现补齐。
- 结论:**达标,且实现超出文档要求**。P1-1 SSRF:协议白名单 + `getaddrinfo` 解析后逐 IP 校验(not_global/private/loopback/link_local/reserved/multicast/unspecified) + 处理 IPv6 映射 IPv4( `::ffff:169.254.169.254` ) + 重定向手动逐跳 re-validate(堵 30x 绕过)+ Content-Length 与流式双重大小上限;测试覆盖回环/云元数据/重定向内网/超大响应均「拒绝且不扣点」。P2-1 全局 `REST_FRAMEWORK` 默认空认证、P2-2 生成/认证限流、P2-3 充值金额上限均落地含测试。
- 复核发现的残留(已登记):① DNS rebinding TOCTOU(校验解析与 requests 各解析一次,未钉 IP)→ 已登记 Backlog;② DRF 限流依赖共享缓存,多 worker 下 LocMemCache 每进程失效 → 已登记 T-403。
2026-07-03 16:36:06 +08:00
## 2026-07-03 T-401 运营后台完善
- 状态:DONE
- 变更:
- `apps/billing/services.py` :新增 `adjust_wallet_points()` 、`WalletAdjustment` 和调点错误类型;手工调点必须非 0、必须有原因,在事务内锁 `UserWallet` ,拒绝扣成负数,并写 `PointsLedger(change_type=adjust)` 。
- `apps/users/admin.py` / `apps/users/templates/admin/users/userwallet/adjust_points.html` :新增钱包后台专用“手工调点”入口;钱包余额字段继续只读,提交表单后才调用计费层服务写余额和流水;用户列表显示钱包余额;API Key 后台保持 prefix/hash 摘要展示,不回显明文或完整 hash。
- `apps/billing/admin.py` :增强计费规则、汇率、充值订单、点数流水和调用记录的后台检索/列表配置;订单、流水、调用记录保持只读。
- `apps/billing/tests.py` / `apps/users/tests.py` :新增调点服务和 admin 测试,覆盖加点/扣点、原因必填、拒绝透支、后台调点写流水、直接提交钱包 change form 不改余额、API Key 后台不泄露明文和完整 hash。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/04-architecture.md` 、`docs/routes.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` 。
- 验证:
- `./init.ps1` :开工前通过;收尾通过。
- `py -3.12 -m py_compile apps\billing\services.py apps\billing\admin.py apps\billing\tests.py apps\users\admin.py apps\users\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `Test-NetConnection 43.128.3.240 -Port 3306` : `TcpTestSucceeded=True` 。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests apps.users.tests.UserAdminOperationTests --noinput --keepdb --verbosity 2` :通过,18 tests OK。
- `py -3.12 manage.py test apps.billing.tests.BillingCoreModelTests apps.billing.tests.PricingCalculationTests apps.billing.tests.BillingServiceTests apps.users.tests --noinput --keepdb --verbosity 2` :通过,32 tests OK。
- 尝试 `py -3.12 manage.py test apps.billing apps.users --noinput --keepdb --verbosity 2` :未取得单次全绿;运行中远程 MySQL 出现 `OperationalError 2003/2013` 、连接重置与事务中断,已通过用例无 T-401 断言失败。
- 尝试 `py -3.12 manage.py test apps.billing.tests.ConcurrentDebitTests --noinput --keepdb --verbosity 2` :测试体显示 `ok` ,但 teardown/flush 阶段远程 MySQL 再次连接超时,被 Django 记为 ERROR。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:T-401 功能无阻塞;完整大套件单次全绿仍受远程 MySQL 连接稳定性影响。
- 决策:运营手工调点不开放直接编辑 `UserWallet.points_balance` ,只通过 `adjust_wallet_points()` 统一落账;后台订单、流水、调用记录继续只读,避免运营后台成为账目篡改入口。
- 下一步:领取 T-402 完整验收 MVP。
2026-07-03 17:23:59 +08:00
## 2026-07-03 T-402 完整验收 MVP
- 状态:DONE
- 变更:
- 新增 `docs/mvp-acceptance.md` ,按 `docs/02-requirements.md` P0 项逐项记录 MVP 验收矩阵、测试证据、结论和已知限制。
- 修正 `docs/02-requirements.md` P0 口径:点数流水明确包含充值 / 消费 / 退款 / 运营调整;API Key 删除按实际实现描述为吊销,吊销后返回 403,缺失 / 无效 / 不存在 Key 返回 401。
- `docs/README.md` 增加 MVP 验收报告入口。
- `docs/06-tasks.md` 标记 T-402 DONE;同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` ,下一步改为 T-403 部署 / 运行文档。
- 验证:
- `./init.ps1` :开工前通过。
- `py -3.12 manage.py test --noinput --keepdb --verbosity 2` :发现 119 tests,已运行 98 tests 后远程 MySQL 在 `apps.portal.tests.PortalAccountFlowTests.setUpClass` 阶段连接重置;已通过用例无断言失败。
- 拆分运行同一套 119 条测试全部通过:`apps.portal` 21 tests OK; `apps.ai` 26 tests OK; `apps.api` 39 tests OK; `apps.billing apps.users` 33 tests OK。
- `py -3.12 manage.py smoke_ai_generation title --recorded` :通过,录制标题 smoke 成功。
- `py -3.12 manage.py smoke_ai_generation image --recorded` :通过,录制图片 smoke 成功。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` :通过,两个本地 static 文件均可发现。
- `py -3.12 -m compileall apps config` :通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 阻塞:T-402 MVP P0 验收无功能阻塞;单条完整 `manage.py test` 命令仍受远程 MySQL 间歇断连影响,后续部署验收建议使用稳定测试库或本地 MySQL 复跑。
- 已知限制:真实微信/支付宝商户凭证和生产 SDK 未接入;真实 AI 上游 smoke 尚未执行;图片同步真实耗时仍未验证,需在 T-403 部署 / 运行文档中明确超时配置和风险口径。
- 决策:T-402 不改业务代码,仅完成验收、口径修正和文档归档;MVP P0 结论为通过。
- 下一步:领取 T-403 部署 / 运行文档。
2026-07-03 17:53:49 +08:00
## 2026-07-03 T-403 部署 / 运行文档
- 状态:DONE
- 变更:
- 新增 `docs/deployment.md` :按 VPS / 宝塔 / Nginx / Gunicorn(gthread) / MySQL 8.4 写生产部署步骤,覆盖 `.env` 、迁移、`collectstatic` 、共享 cache、双 Gunicorn 池、Nginx 分流、上线检查和真实图片耗时验证口径。
- 新增 `requirements-production.txt` :Linux 生产运行在基础依赖外追加 Gunicorn,避免 Windows 本地开发强制安装生产 runner。
- `config/settings.py` :新增 `STATIC_ROOT` 、`STATIC_URL` 环境变量支持;新增 `CSRF_TRUSTED_ORIGINS` ;新增 `CACHES` 环境变量配置,生产可用 DatabaseCache / Redis / Memcached 等共享 cache;新增 HTTPS cookie、proxy SSL 与 HSTS 环境变量支持。
- `.env.example` / `docs/env.md` :同步新增生产静态、共享 cache、CSRF trusted origins、HTTPS cookie/proxy/HSTS 配置项。
- `docs/03-tech-stack.md` :部署方式定为 VPS / 宝塔 + Nginx + Gunicorn(gthread) + systemd,并记录生产安装、cache table、collectstatic 与 Gunicorn 命令。
- `docs/04-architecture.md` :修正部署分流路径为实际的 `/api/v1/generate/*` 。
- 同步更新 `README.md` 、`docs/00-ai-start-here.md` 、`docs/README.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` ; T-403 标记 DONE,计划内 MVP 任务暂无下一个 TODO。
- 验证:
- `./init.ps1` :开工前通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` :通过,两个本地 static 文件均可发现。
- `py -3.12 manage.py check --deploy` :当前开发 `.env` 下只报预期安全配置警告;临时注入生产型安全环境变量(含 HSTS includeSubDomains/preload)后通过,0 issues。
- `py -3.12 manage.py collectstatic --dry-run --noinput` :通过,预期收集 169 个 static 文件。
- `py -3.12 manage.py createcachetable --dry-run cmhub_cache` :通过,输出 MySQL cache 表 DDL。
- `py -3.12 -m compileall config` :通过。
- 尝试 `py -3.12 manage.py test apps.api --noinput --keepdb --verbosity 2` :未取得单次全绿;25 条用例通过,`GenerateApiTests` 14 条因远程 MySQL `OperationalError 2013` / 连接重置 / 事务中断被记 ERROR。
- `Test-NetConnection 43.128.3.240 -Port 3306` : `TcpTestSucceeded=True` 。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --noinput --keepdb --verbosity 2` :通过,14 tests OK。
- 阻塞:T-403 文档与配置支撑无功能阻塞;完整 API 套件单次运行仍受远程 MySQL 间歇断连影响。
- 已知限制:真实 AI 上游和真实图片耗时仍未验证,因当前环境未配置 `AI_KEY_ENCRYPTION_KEY` 且数据库无真实 AiModel/ModelAlias;真实微信/支付宝商户配置和 SDK 仍待生产环境提供。`deployment.md` 已明确未跑通真实图片 smoke 前不能声称生产图片链路已验证。
- 决策:生产限流共享 cache 的 MVP 推荐路径先用 Django `DatabaseCache` + MySQL cache 表,不引入 Redis 作为当前代码依赖;高并发后可切换 Redis/Memcached 并安装对应 backend。生产静态资源采用 `STATIC_ROOT + collectstatic + Nginx/宝塔托管` ,不在应用内引入 WhiteNoise。
- 下一步:计划内 MVP 任务已完成;进入 VPS 实际部署、真实邮件/支付/AI 配置和真实图片耗时验证,或从 Backlog 重新拆后续任务。
2026-07-04 10:14:16 +08:00
## 2026-07-04 Phase 6 T-601 可行性复核与文档口径修正(非任务)
- 状态:DONE
- 变更:复核 Claude Code 新增的 Phase 6 / T-601「可用别名发现」后,更新 `docs/06-tasks.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`README.md` 。T-601 路由统一为 `GET /api/v1/models` ;验收要点收紧为只列 `ModelAlias.is_active=True` 且 `ai_model.is_active=True` 的别名;实现不得调用 `AiModel.to_resolved_model()` 或任何会解密 provider key 的路径;响应与 portal 页面不得暴露具体 SKU、模型 URL、provider key、`api_key_encrypted` 、`extra_body` 等内部字段;模型列表成功请求不占用生成接口 `GenerateRateThrottle` 额度,只复用认证失败限流。
- 验证:文档修改;未运行代码测试。
- 决策:T-601 可行,属于 MVP 后低风险只读增强,不新增数据表;当前下一个可领取任务改为 T-601。真实邮件、支付、AI 模型和图片真实耗时验证仍是生产配置待办,不因 T-601 取消。
- 下一步:领取 T-601 可用别名发现。
## 2026-07-04 T-601 可用别名发现
- 状态:DONE
- 变更:
- 新增 `apps/ai/catalog.py` :集中生成公开模型目录,只读取 `ModelAlias` 、`AiModel` 的公开能力字段和 `PricingRule` ,只列 active 且具备对应能力的别名;按 `api_type` / URL 推导 `images_edits` 是否需要原图;缺定价返回 `pricing_status=unpriced` 与空价格列表。
- 新增 `GET /api/v1/models` :继承 `ExternalApiView` ,使用 API Key 鉴权;不挂 `GenerateRateThrottle` ,成功查询不占用生成接口额度;响应只含 `alias` 、`operation_type` 、`capabilities` 、`requires_image` 、`pricing_status` 、`prices` 。
- 新增 portal `/models` 只读「可用模型」页,并在导航加入入口;页面显示别名、操作类型、能力、是否需要原图和点数单价,缺定价显示「暂未定价」。
- 扩展 `apps/api/tests.py` 与 `apps/portal/tests.py` :覆盖 API Key 成功结构、缺失/无效 Key 拒绝、Web session 不能调用外部 API、portal 未登录跳转、页面不泄露底层 SKU / URL / key / `extra_body` 、缺定价别名显示暂未定价。
- 同步更新 `docs/api.md` 、`docs/routes.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/00-ai-start-here.md` 与 `README.md` ;T-601 标记 DONE,当前暂无新的已拆 `TODO` 。
- 验证:
- `.\init.ps1` :通过;Python 3.12.3,依赖均已满足,`manage.py check` 0 issues,并打印启动命令 `py -3.12 manage.py runserver` 。
- `py -3.12 -m py_compile apps\ai\catalog.py apps\api\views.py apps\api\urls.py apps\api\tests.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test apps.api.tests.ModelsCatalogApiTests apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` :5 条目标用例中 3 条 API 测试已通过;portal 测试未跑到断言,因远程 MySQL 连接 `43.128.3.240` 超时中断。
- `Test-NetConnection 43.128.3.240 -Port 3306` : `TcpTestSucceeded=True` 。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_models_page_requires_session_login apps.portal.tests.PortalAccountFlowTests.test_models_page_lists_public_aliases_prices_and_unpriced_state --keepdb --noinput --verbosity 2` :通过,2 tests OK。
- 阻塞:T-601 功能无阻塞;完整套件未单次重跑,远程 MySQL 仍存在间歇超时风险。测试/迁移阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的 `models.W036` 既有警告。
- 决策:模型目录接口是只读发现能力,不调用 `AiModel.to_resolved_model()` ,不依赖 `AI_KEY_ENCRYPTION_KEY` ,不解密或输出 provider key;未来接 `account_alias_permission` 时在同一 catalog 查询层按用户过滤。
- 下一步:暂无已拆 `TODO` ;建议先处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
2026-07-04 10:47:35 +08:00
## 2026-07-04 用户端顶部导航 active 状态修复
- 状态:DONE
- 变更:
- `apps/portal/templates/portal/base.html` :顶部导航改为基于 `request.resolver_match.url_name` 判断当前页面;当前页面入口使用 `btn-primary` 与 `aria-current="page"` ,非当前页面入口使用 `btn-outline-secondary` 。修复访问「可用模型」/「API Key」/记录页时「充值」仍固定深色高亮的问题。
- `apps/portal/tests.py` :新增导航 active 状态测试,覆盖 `/dashboard` 、`/recharge` 、`/apikeys` 、`/models` 、`/records/recharge` 、`/records/usage` 。
- `docs/routes.md` :将导航缺陷口径更新为已修复,并记录 active 状态规则。
- `docs/06-tasks.md` / `docs/current-state.md` :移除待修 Backlog 口径,更新当前状态与验证记录。
- 验证:
- `py -3.12 -m py_compile apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --keepdb --noinput --verbosity 2` :通过,1 test OK;测试阶段仍有 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 阻塞:无。
- 下一步:暂无已拆 `TODO` ;建议继续处理真实邮件、支付、AI 模型导入与真实图片耗时验证,或从 Backlog 拆新任务。
2026-07-04 10:14:16 +08:00
## 2026-07-04 T-601 复核(Claude Code review,非任务)
- 状态:DONE(复核完成,结论:达标,质量高)
- 对象:codex 完成的 T-601「可用别名发现」(`GET /api/v1/models` + portal 只读「可用模型」页)。
- 逐条核对全过:
- ① `ModelsView(ExternalApiView)` API Key 鉴权,未挂 `GenerateRateThrottle` (不占用生成限流额度,仅认证失败限流),`filter(is_active=True, ai_model__is_active=True)` 。
- ② 响应字段 alias/operation_type/capabilities/requires_image/pricing_status/prices[{resolution,points_cost}];缺价 → pricing_status=unpriced + prices=[],不报错,有专测。
- ③ **不走解密路径** : `apps/ai/catalog.py` 只读 capabilities/api_type/url( url 仅用于 `resolve_api_type` 推导 requires_image,不输出),全项目 `to_resolved_model()` 仅出现在 generation.py;清单不依赖 `AI_KEY_ENCRYPTION_KEY` 、不碰 `api_key_encrypted` 。
- ④ 不暴露 SKU/url/key/extra_body:测试用哨兵值(secret-sku / provider-secret.example / encrypted-provider-key / provider-extra-secret)断言响应 JSON 一个都搜不到,另断言字段名 api_key/api_key_encrypted/extra_body/url/model_used 缺席——比「断言不含字段」更强,能抓从别路径的泄漏。
- ⑤ portal `ModelCatalogView` ( session) + models.html(单价「X 点」/暂未定价/需原图)+ base.html「可用模型」导航入口。
- ⑥ 测试:API Key 成功结构、缺/无效/仅 session 拒绝、portal 未登录跳转、portal 页不泄露内部字段、unpriced 显示暂未定价——均覆盖。
- 加分:catalog 额外做能力校验过滤(别名指向模型无对应能力则不列,与 resolve_alias 口径一致);查询无 N+1( select_related + 批量 PricingRule);api.md 已同步 /v1/models 契约(补上上轮登记的缺口)。
- 小点(不用改):`prices` 按分辨率字符串排序,`512` 排在 `4K` 之后,纯展示层顺序,无功能影响。
- 保留:本轮完整套件未单次全绿(远程 MySQL 间歇超时),T-601 相关测试本身通过,非断言失败。
- 结论:达标,无 P1/P2,不需要修补任务。
2026-07-04 14:51:55 +08:00
## 2026-07-04 决策:django-admin 中文化(T-602 立项,先文档)
- 背景:运营反馈 admin 后台表名/字段名是英文,希望显示中文。按「需求先改文档再改代码」规矩,先立任务。
- 范围:本次只做**第 1-3 层(显示层,不动业务逻辑与 DB 结构)**:
- 第 1 层 `LANGUAGE_CODE='zh-hans'` + `USE_I18N=True` : Django 自带 admin 与内置 auth(用户/组/权限)界面中文化(无迁移,性价比最高)。
- 第 2 层 各 app `AppConfig.verbose_name` :中文分组名(无迁移)。
- 第 3 层 各模型 `Meta.verbose_name` /`verbose_name_plural` :中文表名(产出 `AlterModelOptions` **无 DB 变更**迁移)。
- 明确**不含第 4 层**字段级 `verbose_name` (字段列名暂保持英文),如需另立任务。
- 验收:`makemigrations` (仅 options)→ `migrate` → `check` /`test` 全绿 → `/admin/` 目视中文化,留证据。
- 决策依据:第 1 层一行配置即可中文化 Django 自身 + auth 约 60-70%;自建模型的表名靠第 2/3 层 verbose_name;字段级(第 4 层)体力大、单独评估。任务见 `06-tasks.md` T-602。
- 下一步:按 T-602 实现第 1-3 层。
## 2026-07-04 决策补充:admin 中文化第 4 层立为 T-603
- 承接上条:应要求把「第 4 层·字段级 `verbose_name` 」也纳入计划,立为独立任务 T-603(依赖 T-602,在 1-3 层之上做)。
- 范围:各模型业务字段加中文 `verbose_name` (如 `points_balance` →点数余额、`amount_money` →金额、`points_granted` →到账点数等),admin 自定义显示方法同步中文。
- 约束不变:**仅显示层**, `verbose_name` 只改展示、不改字段名与代码引用;产出 `AlterField` ( metadata-only,无 DB schema 变更)迁移。
- 拆两个任务的原因:第 4 层字段多、机械体力大,独立成 T-603 便于单独实现与复核;T-602(1-3 层)可先落地看效果。
- 下一步:T-602 先行,T-603 随后。
2026-07-04 16:27:45 +08:00
## 2026-07-04 T-602 django-admin 中文化(第 1-3 层)
- 状态:DONE
- 变更:
- 第 1 层:`LANGUAGE_CODE='zh-hans'` + `USE_I18N=True` (本就已在,Django 自带 admin 与内置 auth 已中文)。
- 第 2 层:5 个 app 加 `AppConfig.verbose_name` ——用户与钱包 / 计费与充值 / AI 模型配置 / 对外 API / 用户端。
- 第 3 层:11 个模型加 `Meta.verbose_name` /`verbose_name_plural` (用户/用户钱包/API 密钥/调用记录/计费规则/汇率/充值订单/点数流水/AI 模型/能力别名/AI 配置审计日志)。
- 生成 3 条迁移 `users.0004` / `billing.0005` / `ai.0003` ,共 11 个 `AlterModelOptions` , **仅 options、无 DB schema 变更**。
- 验证:
- `makemigrations` :仅生成 11 个 `AlterModelOptions` ,无 add/alter field、无 constraint/index/table 变更。
- `migrate` :用户在本地环境应用通过。
- 目视:用户本地运行最新代码,`/admin/` 可见中文表名。
- 说明:本任务为纯显示层改动(verbose_name),不涉及逻辑/字段/表结构,功能风险为零;未单独粘贴 `check` /`test` 输出,以迁移应用 + 目视中文化为验收证据。
- 约束保持:只改展示名,不改字段名、代码引用与 DB 结构。
- 提交:`4aceb3a` (11 文件:8 代码 + 3 迁移;未混入内容审核骨架 / `ACCOUNT_EMAIL_VERIFICATION` 测试改动)。
- 下一步:T-603 字段级中文(第 4 层)。
## 2026-07-04 T-603 django-admin 中文化(第 4 层·字段级)
- 状态:BLOCKED(字段中文化代码与迁移已完成;完整测试被工作区既有邮箱验证配置改动阻塞)
- 变更:
- `apps/users/models.py` / `admin.py` : `User` 、`UserWallet` 、`ApiKey` 业务字段补中文 `verbose_name` ,状态枚举显示值和 admin 自定义列标题同步中文。
- `apps/billing/models.py` / `admin.py` : `CallRecord` 、`PricingRule` 、`ExchangeRate` 、`RechargeOrder` 、`PointsLedger` 业务字段补中文 `verbose_name` ,支付方式/状态/流水类型等 choices 显示值同步中文。
- `apps/ai/models.py` / `admin.py` : `AiModel` 、`ModelAlias` 、`AiConfigAuditLog` 字段补中文 `verbose_name` ,admin 密钥状态、能力列、审计变更字段等显示同步中文。
- 生成 3 条迁移:`users.0005` / `billing.0006` / `ai.0004` ,均为字段 metadata 的 `AlterField` ; `sqlmigrate` 确认为 `(no-op)` ,无 DB schema 变更。
- 验证:
- `py -3.12 -m py_compile apps\users\models.py apps\users\admin.py apps\billing\models.py apps\billing\admin.py apps\ai\models.py apps\ai\admin.py` :通过。
- `py -3.12 manage.py makemigrations` :仅生成 `AlterField` 迁移。
- `rg -n "CreateModel|DeleteModel|AddField|RemoveField|Rename|AlterModel|AddIndex|RemoveIndex|AddConstraint|RemoveConstraint|RunPython|RunSQL" ...0005... ...0006... ...0004...` :无匹配,确认无建删表/字段/索引/约束/数据脚本。
- `py -3.12 manage.py sqlmigrate users 0005` / `billing 0006` / `ai 0004` :均为 `(no-op)` 。
- `py -3.12 manage.py migrate` :通过,应用 3 条迁移;仅出现既有 allauth `account.EmailAddress` MySQL `models.W036` 警告。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py check` :通过,0 issues。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test --keepdb --noinput --verbosity 2` :125 条中 123 条通过,2 条失败。
- 阻塞:
- 失败用例:`test_signup_creates_unverified_user_wallet_with_zero_points_and_no_ledger` 、`test_unverified_email_cannot_establish_login_session` 。
- 失败原因:工作区既有 `config/settings.py` 未提交改动将 `ACCOUNT_EMAIL_VERIFICATION` 从 `mandatory` 改为 `none` ,导致注册不发验证邮件、未验证用户也能建立登录 session;该文件不属于 T-603,本轮未纳入提交。
- 下一步:恢复 `ACCOUNT_EMAIL_VERIFICATION="mandatory"` 后,重跑上述失败用例和完整测试;全部通过后把 T-603 从 `BLOCKED` 改为 `DONE` 。
2026-07-04 16:43:57 +08:00
## 2026-07-04 线上 admin ExchangeRate 500 热修
- 状态:DONE
- 现象:线上访问 `https://cm.833729.com/admin/billing/exchangerate/` 返回 500;未登录 curl 为 302,已登录 admin 页面复现 500。
- 根因:Django admin 的 `date_hierarchy` 会对 DateTime 字段做按日期层级聚合;MySQL 后端在 `USE_TZ=True` 且 `TIME_ZONE=Asia/Shanghai` 时需要 MySQL 时区表支持 `CONVERT_TZ` 。生产 MySQL 当前未加载时区表,`ExchangeRateAdmin.date_hierarchy="effective_from"` 渲染时报 `ValueError: Database returned an invalid datetime value. Are time zone definitions for your database installed?`
- 变更:
- `apps/billing/admin.py` :移除 `PointsLedgerAdmin` 、`ExchangeRateAdmin` 、`RechargeOrderAdmin` 、`CallRecordAdmin` 的 DateTime `date_hierarchy` ,避免同类页面因 MySQL 时区表缺失 500;保留 `list_filter` 日期筛选。
- `apps/billing/tests.py` :新增 `BillingAdminTests.test_exchange_rate_changelist_renders_when_rates_exist` ,覆盖有汇率数据时 admin 列表页必须 200。
- 验证:
- `py -3.12 -m py_compile apps\billing\admin.py apps\billing\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.billing.tests.BillingAdminTests.test_exchange_rate_changelist_renders_when_rates_exist --keepdb --noinput --verbosity 2` :通过,1 test OK;测试阶段仍有既有 allauth MySQL `models.W036` 警告。
- 下一步:提交并部署后,在 VPS 上重启 `cmhub-web.service` ,用 Django test client 或浏览器确认 `/admin/billing/exchangerate/` 已恢复 200。
2026-07-06 08:56:30 +08:00
## 2026-07-04 Mock 支付二维码提示
- 状态:DONE
- 背景:线上 `PAYMENT_CALLBACK_MODE=mock` 时生成的微信二维码内容为 `weixin://wxpay/cmhub-mock...` ,该票据只用于联调,不会进入微信真实支付页;用户用微信扫码可能看到空白,容易误认为生产支付异常。
- 文档先行:
- `docs/api.md` :补充 mock 模式返回的是测试二维码票据,不能跳转真实微信/支付宝收银台;用户端充值页应展示醒目 mock 提示。
- `docs/04-architecture.md` :补充 `/recharge` 页面在 `PAYMENT_CALLBACK_MODE=mock` 时必须提示测试票据不可真实支付。
- 代码:
- `apps/portal/views.py` :向充值页模板传入 `payment_callback_mode` 与 `is_mock_payment_mode` 。
- `apps/portal/templates/portal/recharge.html` :待支付订单区域在 mock 模式下显示 warning 提示。
- `apps/portal/tests.py` :覆盖 mock 模式显示提示、sdk 模式不显示提示。
- 验证:
- `py -3.12 -m py_compile apps\portal\views.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_page_post_creates_pending_order_without_crediting_wallet_or_ledger apps.portal.tests.PortalAccountFlowTests.test_recharge_page_does_not_show_mock_notice_in_sdk_mode --keepdb --noinput --verbosity 2` :通过,2 tests OK;测试阶段仍有既有 allauth MySQL `models.W036` 警告。
## 2026-07-04 临时隐藏用户端支付宝充值入口
- 状态:DONE
- 背景:线上支付宝 `trade.precreate` 返回 `isv.illegal-client-ip` / `当前调用IP不在可信名单中` , VPS 出口 IP 已确认为 `43.128.3.240` ;在支付宝开放平台可信 IP 未配置前,用户端展示支付宝会持续创建失败订单。
- 变更:
- `apps/portal/forms.py` : `RechargeCreateForm.pay_method` 暂时只保留微信选项,页面不再渲染支付宝 radio,手工提交 `pay_method=alipay` 也会被表单拒绝。
- `apps/portal/tests.py` :覆盖充值页不显示支付宝、超过上限仍按微信校验、隐藏支付宝提交不创建订单。
- `docs/current-state.md` :记录支付宝临时下线原因和恢复条件;底层 API / 回调 / SDK 路径保留。
- 恢复条件:支付宝开放平台把 `43.128.3.240` 加入可信 IP 白名单并通过真实预下单 smoke 后,再把 `RechargeCreateForm` 的支付宝 choice 加回。
## 2026-07-06 支付正式模式热修收敛
- 状态:DONE
- 背景:生产已切到 `PAYMENT_CALLBACK_MODE=sdk` 并配置微信 / 支付宝真实参数后,微信 1 元下单先后暴露两个 SDK 兼容问题;支付宝预下单返回业务错误 `isv.illegal-client-ip` 。
- 变更:
- `requirements.txt` :新增 `wechatpayv3` / `python-alipay-sdk` ,并把 `cryptography` 上限放宽到 `<50` ,对齐真实支付 SDK 依赖。
- `.gitignore` :忽略 `.secrets/` / `secrets/` ,避免支付证书和私钥误入库。
- `apps/billing/payment_gateways.py` :微信 Native 下单显式传 `WeChatPayType.NATIVE` ;兼容 `wechatpayv3` 返回 `(status_code, json_string)` 的 tuple 响应,统一解析 `code_url` 与查单结果。
- `apps/api/views.py` :pending 充值订单主动查单遇到支付网关运行时异常时不再让状态接口 500,而是记录 warning 并保持当前订单状态返回 200。
- `apps/portal/forms.py` :支付宝可信 IP 未配置前,用户端充值表单只保留微信;手工提交 `pay_method=alipay` 会被表单拒绝。
- `docs/api.md` / `docs/04-architecture.md` / `docs/current-state.md` :同步 mock 提示、微信正式下单热修、支付宝临时下线和恢复条件。
- 验证:
- `py -3.12 -m py_compile apps\billing\payment_gateways.py apps\api\views.py apps\billing\tests.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests.test_wechat_sdk_native_order_passes_explicit_pay_type apps.billing.tests.BillingServiceTests.test_wechat_sdk_query_parses_tuple_json_response apps.api.tests.RechargeCreateStatusApiTests.test_recharge_status_keeps_pending_when_active_query_raises_unexpected_error --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- `py -3.12 -m py_compile apps\portal\forms.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_recharge_page_requires_login_and_shows_form apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_amount_above_configured_maximum apps.portal.tests.PortalAccountFlowTests.test_recharge_page_rejects_hidden_alipay_submit --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- 线上:同步热修后 `python3.12 manage.py check` 通过,`cmhub-web` / `cmhub-generate` 重启后均为 `active` ;修复后的微信 1 分诊断预支付请求返回 `weixin://wxpay/bizpayurl` 票据;用户端表单 choices 仅剩 `微信` ,支付宝提交无效。
- 后续:微信下单已通,但线上微信回调曾出现 `PaymentVerificationError` ,仍需修复并做真实支付到账闭环;支付宝需在开放平台把 VPS 出口 IP `43.128.3.240` 加入可信 IP 后再恢复页面选项。
2026-07-06 10:44:13 +08:00
## 2026-07-06 需求+方案:中文敏感词本地过滤(T-604,ahocorapy)
- 需求:对中文 prompt 做本地敏感词过滤,命中即在**预扣点之前**拦截(不扣点、不调上游、返回 400 content_blocked)。
- 定位(重要):本地词表 = **免费快筛 + 运营自定义黑名单** ,**不替代合规内容审核**。国内 AIGC 上线通常仍需有资质的云内容安全服务;云 API 作为后续可插拔槽位,不在 T-604 范围。
- 选型:匹配引擎用 ** `ahocorapy` (纯 Python Aho-Corasick,免编译,VPS 上 pip 一定装得上)**,放弃需编译的 `pyahocorasick` 。繁简归一化 `opencc` 可选。
- 方案要点:
1. 接现有 `apps/moderation/` 骨架(provider 接口 + `moderate_input` /`moderate_output_*` 钩子 + fail-closed),本任务把骨架一并转正提交;`MODERATION_ENABLED` 默认 False → no-op,现有测试不受影响。
2. `KeywordModerationProvider` :**先归一化再匹配**(全半角/去空白分隔标点/零宽/大小写/可选繁简)以抗低级绕过;ahocorapy 做子串匹配。
3. `SensitiveWord` 模型(word/category/action/is_active) + admin 维护 + 迁移。
4. **缓存 matcher** :启动/词库变更时构建自动机缓存内存,信号或版本号失效重建,绝不每请求重建;多 worker 各建各的(只读)。
5. 命中默认 block(脱敏/flag 后续可配);留痕只记分类/词 id,不落违规原文。
6. 可选 `seed_sensitive_words` 命令导开源词库打底,真实词库数据不进仓库。
- 依赖:现有 `apps/moderation/` 骨架(当前未提交),本任务转正;`requirements.txt` 加 `ahocorapy` 。
- 待确认(已给默认):本地是「补充」非「替代」云 API(推荐);命中动作默认 block;词库 admin 维护 + 可选 seed。
- 任务:`06-tasks.md` T-604。下一步:实现 T-604(并处理 T-603 的 `ACCOUNT_EMAIL_VERIFICATION` 阻塞)。
## 2026-07-06 决策:永久免邮箱验证(ACCOUNT_EMAIL_VERIFICATION="none")
- 决策:用户端注册**不做邮箱验证**,`ACCOUNT_EMAIL_VERIFICATION="none"` 为**长期既定策略**(不再考虑改回 mandatory)。注册填邮箱 + 密码即注册成功、可直接登录使用。
- 影响:
- 邮箱**仍必填且唯一**( `User.email` unique 约束不变),只是不验证。
- 注册流程**不再发验证邮件**——注册本身不再依赖 SMTP/邮件服务(找回密码等其它邮件功能如启用才需要)。
- 原假设 mandatory 的 2 条 portal 测试(`test_signup_creates_unverified_user_wallet_...` 、`test_unverified_email_cannot_establish_login_session` )需**按新策略更新**,不是恢复 mandatory。
- `settings.py` 现为 `"none"#"mandatory"` 的临时写法,需清理成干净的 `"none"` 。
- 防刷改由「注册限流 / 图形验证码」补位(邮箱验证不再承担这个作用)。
- 本决策为**全项目权威口径**: `02-requirements` /`05-coding-rules` /`api` /`04-architecture` /`03-tech-stack` /`routes` /`env` /`deployment` /`00-ai-start-here` 中所有「邮箱验证」表述以本决策为准,统一改为「免邮箱验证,邮箱仍唯一」,由 **T-605** 落实。
- T-603 解封路径随之改变:不再「恢复 mandatory」,而是等 T-605 更新测试后重跑标 DONE。
## 2026-07-06 发现+决策:用户端与后台共用会话 → 账号分离(方案 A)
- 发现:portal( `/` )、`/admin/` 、`/api/` 挂同一域名/同一 Django 工程,共用同一个 `sessionid` cookie; `/admin/` 不是另一套登录,只是在同一登录态上加 `is_staff=True` 门槛。所以 staff/superuser 账号登了 portal,去 `/admin/` 也是登录态(观察到的「用户端登录后台跟着登」根因)。属单体复用 Django auth 的必然结果,非 bug。
- 澄清:普通用户 `is_staff=False` 登 portal **进不了后台** ,无越权、无泄露。
- 决策:采用**方案 A·账号分离(零代码)**——终端用户一律 `is_staff=False` ;运营用**专用 admin superuser 只登 `/admin/` ,不与 portal 账号混用**。不采用「同账号双会话」(需 admin 独立子域名/独立 cookie,成本高、MVP 不值)。
- 加固(上线前,deployment):`/admin/` 加访问保护(改路径 / IP 白名单 / 反代 basic auth / 2FA),永不给终端用户 `is_staff` 。
- 文档落点:`04-architecture.md` (设计事实 + 方案 A)、`05-coding-rules.md` (「用户端与后台账号分离」规则)。属**运营/编码约定,无代码改动**。
## 2026-07-06 T-604 文档方案收紧:本地 prompt 敏感词过滤
- 状态:DONE(文档先行,未改代码)。
- 背景:复核 Claude Code 提出的“提示词敏感词检查”方案后,结论为方向合理,但原方案把本地关键词、云内容安全、输出审核和图片审核混在一起,且现有 `generation.py` 接入顺序存在“先下载 image_url 再审 prompt”的实现风险。
- 变更:
- 新增 `docs/moderation.md` ,作为 T-604 权威设计:T-604 只做输入 prompt 本地敏感词快筛;命中必须在扣点和调上游前拦截;云内容安全、输出审核、图片审核后续另做。
- 更新 `docs/06-tasks.md` : T-604 范围收紧为 keyword provider + `SensitiveWord` + matcher 缓存 + 共享 cache 版本号失效;明确 `ahocorapy` 只是候选依赖,编码前必须验证 PyPI 可用性和 API 形状。
- 更新 `docs/04-architecture.md` :新增内容安全层职责、`SensitiveWord` 配置数据、prompt 先审再下载图片/计费的时序,以及多 worker 下不能只依赖 Django signal。
- 更新 `docs/api.md` :补 `content_blocked` 错误码,明确命中不扣点、不写调用/流水、不调上游,也不得先下载 `image_url` 。
- 更新 `docs/env.md` :补 `MODERATION_ENABLED` 、`MODERATION_PROVIDER` 、`MODERATION_FAIL_CLOSED` 、`MODERATION_BLOCK_ON_REVIEW` 、`MODERATION_CACHE_VERSION_KEY` 等配置,并说明生产依赖共享 cache。
- 更新 `docs/05-coding-rules.md` 与 `docs/README.md` :加入内容安全顺序规则和文档导航。
- 决策:
- T-604 不启用输出审核;`MODERATION_REFUND_ON_OUTPUT_BLOCK` 不作为 T-604 交付项,避免配置存在但行为未实现。
- `SensitiveWord.action` MVP 只支持 `block` ,后续再扩展 `review` / `flag` 。
- 词库变更后用共享 cache 版本号让各 worker 懒重建 matcher,不能只靠 `post_save` / `post_delete` signal。
- 验证:文档修改;未运行代码测试。
- 下一步:实现 T-604 时先清理 `apps/moderation/__pycache__` ,把 `apps.moderation` 接入 `INSTALLED_APPS` ,再按 `docs/moderation.md` 落模型、admin、provider、缓存和测试。
## 2026-07-06 复核:codex 更新的 T-604 敏感词方案(Claude Code review,非任务)
- 对象:codex 审核并扩写的中文 prompt 敏感词方案(新增 `docs/moderation.md` ,更新 `04-architecture` /`api` /`05-coding-rules` /`06-tasks` /`env` /`README` )。
- 结论:**合理,且是明显改进,可放行**。
- codex 改对/改好:
- 范围收紧为「只审 prompt 文本」,砍掉 skeleton 里对输入图片做关键词匹配(无意义)。
- 请求时序:审 prompt 在下载 image 之前,命中即不下载图片/不别名/不计费/不调上游。
- 多 worker 缓存失效用共享 cache 版本号(`moderation:sensitive_words:version` ),点破 post_save signal 只在当前进程生效——与 T-403 限流共享 cache 口径一致。
- 「ahocorapy 编码前必须验证 PyPI 可用性和 API 形状」——先核实真实 API 的纪律。
- 诚实标注归一化误伤风险并要求正常文本测试;不暴露未实现的输出审核配置。
- 实现时须盯的一点(skeleton 与新规格冲突,非文档错):现有未提交的 `generation.py` skeleton 是「先 load_image_input 再 moderate_input(含 image)」+ 有 output 钩子;T-604 实现必须按新规格翻正——① prompt 审核挪到图片下载之前;② moderate_input 改纯 prompt;③ 去掉/休眠 output 钩子与未实现的退点配置。
- 小注意(codex 已自标、可接受):归一化误伤需正常文本测试兜底;每请求一次共享 cache 版本检查(DatabaseCache 为一次 DB 读)。
2026-07-06 14:02:48 +08:00
## 2026-07-06 T-605:落实免邮箱验证策略
- 状态:DONE。
- 目标:把 `ACCOUNT_EMAIL_VERIFICATION="none"` 作为长期既定策略清理落地,注册即可用、不发验证邮件,邮箱仍必填且唯一。
- 代码变更:
- `config/settings.py` : `ACCOUNT_EMAIL_VERIFICATION` 改为干净的 `"none"` ; `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=False` ,避免免验证策略下保留“点确认邮件自动登录”的误导配置。
- `apps/portal/tests.py` :更新注册 / 登录测试口径。注册测试改为断言不发验证邮件、创建 0 点钱包、不写赠点流水且注册后已登录;未验证邮箱用户可直接登录;普通登录用例名称去掉 `verified` 语义。
- 文档变更:
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/06-tasks.md` 、`docs/current-state.md` 、`docs/deployment.md` 、`docs/env.md` 、`docs/mvp-acceptance.md` 、`docs/project-brief.md` :同步“免邮箱验证、注册即可用,邮箱仍唯一”口径。
- 生产部署文档不再把真实邮件服务作为注册登录前置条件;邮件服务仅用于后续密码找回、通知或恢复邮箱验证等邮件能力。
- T-603 从“邮箱验证口径阻塞”解封,状态改为 `TODO` :字段中文化代码/迁移已完成,待稳定 MySQL 测试库补跑完整验证后再标 `DONE` 。
- 验证:
- `py -3.12 -m py_compile config\settings.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_signup_creates_user_wallet_with_zero_points_no_ledger_and_can_login apps.portal.tests.PortalAccountFlowTests.test_user_can_login_without_verified_email apps.portal.tests.PortalAccountFlowTests.test_user_can_login_view_dashboard_and_logout --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- `py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 2` :通过,26 tests OK。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 全量测试说明:
- `py -3.12 manage.py test --keepdb --noinput --verbosity 2` :已尝试,发现 131 tests,跑到 95 tests 后远程 MySQL `43.128.3.240` 连接超时 / WinError 10051,最终 22 errors。
- `Test-NetConnection 43.128.3.240 -Port 3306` :随后显示 `TcpTestSucceeded=True` ,说明端口间歇可达。
- 单独复跑失败起点 `apps.billing.tests.BillingServiceTests apps.billing.tests.BillingAdminTests apps.billing.tests.ConcurrentDebitTests` :17 条业务用例 OK;并发扣点用例本体打印 `ok` 后在测试库 flush 阶段再次因 MySQL 连接超时 / WinError 10060 记 ERROR。
- 结论:T-605 相关测试已通过,全量单次绿受远程 MySQL 长跑稳定性阻塞,不是本任务断言失败。
- 下一步:回到 T-603,在更稳定的 MySQL 测试库或网络条件下补跑完整验证,再决定是否标 `DONE` 。
2026-07-06 14:57:25 +08:00
## 2026-07-06 T-604:中文敏感词本地过滤
- 状态:DONE。
- 目标:按 `docs/moderation.md` 落地本地 keyword provider,只审核输入 prompt;命中必须在下载 `image_url` 、解析别名、计费、预扣点和上游调用前返回 `400 content_blocked` 。
- 依赖确认:编码前已验证 `ahocorapy==1.6.2` 可安装,API 使用 `KeywordTree.add()` / `finalize()` / `search_all()` , `search_all()` 返回 `(matched_keyword, index)` 。
- 代码变更:
- `config/settings.py` / `.env.example` / `requirements.txt` :接入 `apps.moderation` 、`MODERATION_*` 配置和 `ahocorapy` 依赖。
- `apps/moderation` :新增 `SensitiveWord` 模型/admin/迁移、归一化管线、共享 cache 版本号、signal 失效、keyword provider 与 prompt 审核 service;清理早期 Tencent stub 和输出审核骨架。
- `apps/api/generation.py` :生成 title/image 的入口改为 serializer 后先 `moderate_prompt()` ;命中直接 `content_blocked` ,未命中才读取图片并继续别名、计费、扣点、上游调用。
- `apps/api/tests.py` / `apps/moderation/tests.py` :覆盖 no-op、归一化、防绕过、词库版本重建、命中不下载图片/不扣点/不建记录/不调上游。
- 文档变更:
- `docs/moderation.md` :从设计口径更新为实现态,明确 T-604 已清理输出审核和云厂商 stub。
- `docs/06-tasks.md` : T-604 标为 DONE。
- `docs/current-state.md` :同步依赖、当前状态、配置基线、验证证据和任务看板。
- 验证:
- `py -3.12 -m py_compile config\settings.py apps\api\generation.py apps\api\tests.py apps\moderation\apps.py apps\moderation\models.py apps\moderation\normalization.py apps\moderation\policy.py apps\moderation\services.py apps\moderation\signals.py apps\moderation\versioning.py apps\moderation\providers\base.py apps\moderation\providers\keyword.py apps\moderation\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.moderation --keepdb --noinput --verbosity 2` :通过,7 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_blocked_prompt_returns_content_blocked_before_image_download_or_charge apps.api.tests.GenerateApiTests.test_disabled_moderation_does_not_block_matching_prompt --keepdb --noinput --verbosity 2` :通过,2 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` :通过,16 tests OK。
- 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告,本项目邮箱唯一性由 `user.email` 承担。
- 注意:
- 真实敏感词库数据不入仓库,需要运营在 admin 中维护或后续另做导入命令。
- 生产多 worker 必须使用共享 cache( DatabaseCache/Redis/Memcached)承载 `MODERATION_CACHE_VERSION_KEY` ,不能依赖 `LocMemCache` 做跨进程刷新。
- 下一个看板任务仍是 T-603:在稳定 MySQL 测试库上补跑完整验证并收尾。
2026-07-06 15:14:47 +08:00
## 2026-07-06 需求+决策:公开首页 + 客户端下载入口(T-606)
- 背景:域名根 `/` 现为「重定向到 /dashboard → 要登录」,匿名访客直接撞登录墙,无项目介绍、无客户端下载入口(线上没地方下载桌面端)。
- 决策:加**公开首页**(补「前门」,拉注册)+ **客户端下载入口** 。
- 分析结论:
- 首页是缺失的漏斗第一环(介绍→注册→下载→建 Key→用),成本低(一个 SSR 模板,契合单体),顺带修 `/` 撞登录墙的体验。
- 安装包是大二进制:**Django 不直接扛**,生产由 **Nginx 直接服务 media/下载文件** (与 T-403 static/media serving 一致)。
- **前期安装包放本服务器**( `MEDIA_ROOT` );`DownloadRelease.external_url` 字段预留,后续切对象存储/CDN 只改后台链接不改代码。
- 生产注意:下载 HTTPS + 页面展示 SHA256 校验;**代码签名**是下载型桌面软件的真实门槛(未签名被 SmartScreen/Gatekeeper 拦),首期先给「忽略警告」说明,签名后续补。
- 落点:`06-tasks.md` T-606(依赖 T-501);`02-requirements.md` 新增「公开首页 + 客户端下载」P1 功能行。
- ID 说明:T-605 已用于「免邮箱验证」,故本任务用 T-606。
- 下一步:实现 T-606(路由改造 + DownloadRelease 模型/admin/迁移 + 首页/下载 SSR 模板 + 测试)。
2026-07-06 15:34:30 +08:00
2026-07-06 15:45:48 +08:00
## 2026-07-06 变更:余额接口返回桌面端测试连接账号信息
- 背景:桌面端“测试连接”希望在余额点数外展示当前 API Key 所属账号。为兼容已有调用方,保留旧字段 `user` 与 `points_balance` ,新增 `account` 对象;按决策不返回邮箱。
- 文档先行:先更新 `docs/api.md` 的 `GET /api/v1/balance` 成功响应结构:
- `user` : 旧兼容字段,用户名。
- `points_balance` : 当前点数余额。
- `account.username` : 用户名。
- `account.display_name` : 用户姓名;未设置时回退用户名。
- 不返回 `email` 、数据库用户 ID 或其他敏感字段。
- 代码变更:
- `apps/api/views.py` : `BalanceView` 响应新增 `account` , `display_name = request.user.get_full_name() or username` 。
- `apps/api/tests.py` :更新 `BalanceApiTests` ,断言旧字段保留、新字段存在,且 `account` 不含 `email` / `id` 。
- 验证:
- `py -3.12 -m py_compile apps\api\views.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.api.tests.BalanceApiTests --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
2026-07-06 15:34:30 +08:00
## 2026-07-06 决策:T-606 首页视觉方向定为 v1「生成台」
- 背景:为 T-606(公开首页 + 客户端下载)出了两版 SVG 原型,写入 `prototypes/` :
- `cmhub-homepage-v1.svg` ——「生成台」:冷调纸白 + 靛蓝(生成)×琥珀(点数),Hero 为「商品图+一句话 → 标题+主图 + 点数计量」转化图,面向电商卖家、讲价值。
- `cmhub-homepage-v2.svg` ——「API 终端」:深色控制台 + 等宽字体,Hero 为真实 `curl → JSON` ,面向开发者。
- 决策:**首页采用 v1「生成台」方向**。理由:首页第一受众是自助注册的电商卖家,v1 的「看图就懂 + 讲价值」更利于转化;v2 的终端质感更适合后续「开发者/文档页」,可保留复用,非二选一。
- 落点:`06-tasks.md` T-606 验收要点已指向 `prototypes/cmhub-homepage-v1.svg` ,要求配色/间距/结构照此原型转 Django 模板(Bootstrap + 本地 static)。
- 说明:`prototypes/` 两个 SVG 为设计存档,未来实现 T-606 时以 v1 为准;v2 留作开发者页参考。
2026-07-06 16:11:01 +08:00
## 2026-07-06 品牌基线 brand.md + T-606 加 portal 视觉一致
- 背景:v1 首页精致,但现有 portal 是朴素 Bootstrap;只做漂亮首页会造成「落地页酷、控制台朴素」的割裂。且桌面端是独立项目,需要一份跨端的品牌单一真相源。
- 决策:一致的是**品牌 token**(色/字/logo/术语/语气),不是像素级 UI;各端布局按各自的活儿来。
- 落点:
- 新增 `docs/brand.md` :靛蓝=生成 / 琥珀=点数的配色、字体栈与阶梯、logo/图标、术语表、语气、**可复制的 `:root` CSS 变量**、应用范围(首页/portal/admin/桌面端)。取自 `prototypes/cmhub-homepage-v1.svg` 。
- `06-tasks.md` T-606 验收新增「视觉一致性」:抽共享 CSS 变量(`brand.css` ),**首页与现有 portal( dashboard/记录/充值/API Key)一起套同一套 token**,不是只做漂亮首页。
- `docs/README.md` 导航登记 brand.md。
- 桌面端 cmbot(独立项目):品牌层对齐(logo/主色/术语),UI 渐进靠拢、单独排期,不阻塞 T-606。
2026-07-06 16:51:18 +08:00
## 2026-07-06 线上缺陷:标题生成返回「上游模型配置不可用」
- 背景:线上已拉取最新代码 `7854b50` 并重启 `cmhub-web.service` / `cmhub-generate.service` ;用 `api_key.txt` 第二个 Key 按文档 demo 测试 `POST /api/v1/generate/title` 。
- 复现结果:
- `GET https://cm.833729.com/api/v1/balance` :成功,返回 `user=admin` 、`points_balance=1010` ,并包含最新代码新增的 `account.username/display_name` 。
- `GET https://cm.833729.com/api/v1/models` :成功,`title-standard` 存在,`operation_type=title` , `pricing_status=priced` ,默认单价 1 点。
- `POST https://cm.833729.com/api/v1/generate/title` :返回 `502` ,错误体为 `{"error":{"code":"upstream_error","message":"上游模型配置不可用"}}` 。
- 失败后复查余额仍为 1010,确认未扣点。
- 判定:API Key、base URL、请求 payload、能力别名和计费规则均正常;问题在服务端 `title-standard` 对应 `AiModel` 的运行配置,不应要求桌面端继续修改 payload。
- 解决方案:
- 检查线上 `.env` 的 `AI_KEY_ENCRYPTION_KEY` 是否存在、有效且与导入/保存 `AiModel.api_key_encrypted` 时一致。
- 在 django-admin 检查 `ModelAlias(title-standard)` 是否 active、`operation_type=title` 、指向 active 的 `AiModel` 。
- 检查对应 `AiModel` 的 `url` 、`model` 、`api_type` 、`capabilities` 和 API Key 是否完整;标题模型至少要有 `text` 能力。
- 若解密失败或 key 曾变更,用当前 `AI_KEY_ENCRYPTION_KEY` 重新保存 API Key,或重新执行 `import_ai_models` 。
- 修复后重启两个 systemd 服务,跑 `python3.12 manage.py smoke_ai_generation title` ,再用 API Key 验证 `/balance` 、`/models` 、`/generate/title` 。
- 文档变更:
- `docs/api.md` :补充 `upstream_error` + 「上游模型配置不可用」的判定口径。
- `docs/deployment.md` :新增 AI 上游配置故障处理步骤、shell 诊断命令和验收路径。
- `docs/current-state.md` :记录线上复现结果,并把真实 AI blocker 从“待验证”改为“已复现模型配置不可用,需修模型运行配置”。
2026-07-06 17:08:15 +08:00
## 2026-07-06 线上修复:重新导入 AI 模型密钥并跑通标题生成
- 根因确认:VPS 上 `AI_KEY_ENCRYPTION_KEY` 已配置且长度正常,`title-standard` 别名、`AiModel.url` 、`model` 、`api_type` 、`capabilities` 均存在,但 `model.to_resolved_model()` 抛 `AiKeyEncryptionError: AI api_key cannot be decrypted` 。说明数据库里的 `AiModel.api_key_encrypted` 是旧 Fernet key 加密或与当前线上 key 不匹配。
- 修复动作:
- 本机确认 `D:\chengma\cmhub\ai_models.json` 含 `GPT-5.5 文本` 、`GPT Image 2` 、`Nano Banana 2` 三个 vectorengine 模型配置,且每项均有 API Key;检查过程不打印 key。
- 在 VPS 创建 `/secure/cmhub` ,权限 `700` ;上传 `ai_models.json` 到 `/secure/cmhub/ai_models.json` ,文件权限 `600` ,不入 git。
- 执行 `python3.12 manage.py import_ai_models /secure/cmhub/ai_models.json` ,用当前线上 `AI_KEY_ENCRYPTION_KEY` 重新加密入库;结果 `created=1, updated=2, aliases=0` ,未改现有默认别名。
- 重新诊断 `title-standard` : `to_resolved_model()` 成功,`resolved_has_api_key=True` , `get_provider()` 成功,provider 为 `ChatCompletionsProvider` 。
- 重启 `cmhub-web.service` 与 `cmhub-generate.service` ,两者均 `active` 。
- 验证:
- `python3.12 manage.py check` :通过,0 issues。
- `python3.12 manage.py smoke_ai_generation title` :真实调用成功,`model_used=gpt-5.5` , `elapsed_ms=16865` , `title_count=3` 。
- 用 `api_key.txt` 第二个 Key 调线上 `/api/v1/balance` :修复前余额 1010。
- 调 `POST https://cm.833729.com/api/v1/generate/title` :成功返回 5 个中文标题,`alias=title-standard` , `model_used=gpt-5.5` , `points_cost=1` , `points_balance=1009` , `call_id=1` 。
- 再查 `/api/v1/balance` :余额 1009,确认真实扣点闭环正常。
- 结论:标题生成的「上游模型配置不可用」已修复。后续仍需补测图片生成真实耗时与微信支付回调到账闭环。
2026-07-06 17:38:56 +08:00
## 2026-07-06 T-603 django-admin 字段级中文化收尾
- 状态:DONE。
- 背景:T-603 字段级 `verbose_name` 代码和 no-op/metadata 迁移此前已完成,后续因邮箱验证策略和远程 MySQL 长跑稳定性问题暂未标 DONE;T-605 已解除邮箱验证口径阻塞。本轮由人工确认 `/admin/` 字段标签已中文化,作为目视验收依据。
- 文档变更:
- `docs/06-tasks.md` : T-603 从 TODO 标为 DONE。
- `docs/current-state.md` :Phase 6 当前状态、任务看板摘要、blocker 和下一个任务更新为 T-606。
- `docs/00-ai-start-here.md` / `docs/project-brief.md` :同步 T-603 已完成、下一步 T-606。
- 验证:
- 人工确认 `/admin/` 字段标签中文化:通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.users.tests apps.billing.tests.BillingAdminTests apps.ai.tests.AiModelAdminTests apps.ai.tests.AiConfigAuditAdminTests --keepdb --noinput --verbosity 2` :通过,15 tests OK。
- 测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告;本项目邮箱唯一性由 `user.email` 承担。
- 下一步:按任务看板进入 T-606 公开首页 + 客户端下载入口;生产侧另需补微信真实回调到账闭环与图片生成真实耗时验证。
2026-07-06 23:18:08 +08:00
## 2026-07-06 T-606 公开首页 + 客户端下载入口
- 状态:DONE。
- 代码变更:
- `apps/portal/models.py` / `apps/portal/migrations/0001_initial.py` :新增 `DownloadRelease` ,字段包含 `platform` 、`version` 、`file` 、`external_url` 、`sha256` 、`is_current` 、`release_notes` 与时间戳;表名 `download_release` ; `external_url` 优先于 `file.url` ;保存时在事务内把同平台其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。
- `apps/portal/admin.py` :注册客户端下载版本 admin,支持上传本地安装包或填写外部链接,列表展示平台、版本、当前状态、下载来源和 SHA256 摘要。
- `apps/portal/views.py` / `urls.py` : `/` 从重定向 `/dashboard` 改为 `HomeView` 公开首页;首页读取 Windows 当前发布版本。
- `apps/portal/templates/portal/home.html` :按 v1「生成台」方向落地首页,包含 Hero、四步上手、标题/主图能力卡、点数计费 band、客户端下载区和控制台/API 入口;无 current release 时显示「暂未发布」。
- `apps/portal/static/portal/brand.css` / `base.html` :抽共享品牌 token,首页与现有 dashboard / 记录 / 充值 / API Key / 可用模型页面共用同一套颜色、字体、按钮和面板样式;保留现有导航按钮 HTML,避免破坏 active 状态断言。
- `apps/portal/tests.py` :新增 `/` 匿名 200、不跳登录、下载当前版本展示、无当前版本提示、登录用户显示「进入控制台」、每平台只保留一个 current release 等测试。
- 文档变更:
- `docs/06-tasks.md` : T-606 标为 DONE。
- `docs/routes.md` :新增 `/` 公开首页和 `DownloadRelease` admin 职责。
- `docs/04-architecture.md` / `docs/api.md` :同步 `DownloadRelease` schema、首页/下载入口行为和生产媒体文件由 Nginx 直接服务口径。
- `docs/current-state.md` 、`docs/00-ai-start-here.md` 、`docs/project-brief.md` 、`README.md` :同步 Phase 6 当前状态,移除“下一步 T-606”旧口径。
- 验证:
- `.\init.ps1` :开工前通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `py -3.12 manage.py makemigrations portal` :生成 `apps/portal/migrations/0001_initial.py` 。
- `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\urls.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py migrate portal` :通过,`portal.0001_initial` 已应用到当前数据库;仅保留 allauth 既有 `models.W036` 警告。
- `py -3.12 manage.py shell -c "... Client().get('/') ..."` :通过,返回 `STATUS=200` ,首页标识 `cmhub AI 电商生成台` 与下载区标识均存在。
- `py -3.12 manage.py findstatic portal/brand.css portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1` :通过,均找到本地 static 文件。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only --noinput --verbosity 2` :重建测试库后通过,5 tests OK;测试期仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests --keepdb --noinput --verbosity 2` :已尝试,因远程 MySQL `43.128.3.240` 在测试库迁移阶段连接超时(2003 / WinError 10060)中断,不是断言失败;数据库恢复后需补跑 portal 整组。
- 下一步:看板当前无新的编号任务;建议优先确认真实支付回调到账闭环、上传/发布 Windows 客户端安装包,或补真实图片生成耗时验证并拆成新任务。
2026-07-07 08:33:49 +08:00
## 2026-07-06 决策:T-607 桌面端最新版本检查接口
- 状态:DONE(文档创建任务,未改代码)。
- 背景:T-606 已有公开首页和 `DownloadRelease` 后台发布配置,但桌面端如果要自动检查更新,不能依赖解析 HTML,需要稳定 JSON 合约。
- 变更:
- `docs/06-tasks.md` :新增 T-607「桌面端最新版本检查接口」,依赖 T-606,状态 TODO。
- `docs/api.md` :新增 `GET /api/v1/client/releases/latest?platform=windows` 合约、响应示例和安全约束。
- `docs/routes.md` :登记计划中的公开版本检查 API。
- `docs/02-requirements.md` :把桌面端版本检查 API 加入 P1 功能清单。
- `docs/04-architecture.md` :同步公开只读例外、数据来源和不得暴露本地路径的约束。
- `docs/current-state.md` 、`docs/00-ai-start-here.md` 、`docs/project-brief.md` 、`README.md` :同步下一项任务为 T-607。
- 决策:
- 版本检查接口公开匿名可访问,不需要 API Key,不读取用户、不扣点、不占用生成接口限流。
- 只返回 `DownloadRelease` 的公开发布元数据:`platform` 、`version` 、`download_url` 、`sha256` 、`release_notes` 、`published_at` 。
- `platform` 缺省为 `windows` ,支持 `windows` / `macos` / `linux` ;非法平台返回 `400 bad_request` 。
- 无当前版本或当前版本没有下载地址时返回 HTTP 200,结构为 `release:null` + `message:"暂未发布"` ,方便桌面端静默处理。
- `download_url` 优先使用 `external_url` ,否则用上传文件 URL 生成外部可访问绝对 URL;响应不得暴露 `MEDIA_ROOT` 、文件系统路径、后台 ID、用户信息、模型配置或密钥。
- 验证:文档修改;后续执行 T-607 时需补 `check` 、目标 API 测试和 `git diff --check` 证据。
- 下一步:领取 T-607,按文档实现公开版本检查 API。
## 2026-07-06 T-607 桌面端最新版本检查接口
- 状态:DONE。
- 代码变更:
- `apps/api/views.py` :新增 `ClientLatestReleaseView` ,显式空认证 + `AllowAny` ,公开匿名读取 `DownloadRelease(platform, is_current=True)` ,返回版本、下载地址、SHA256、发布说明和发布时间;无当前版本或无下载地址返回 `release:null` 。
- `apps/api/urls.py` :新增 `GET /api/v1/client/releases/latest` 路由。
- `apps/api/tests.py` :新增 `ClientLatestReleaseApiTests` ,覆盖匿名无 Key、Web session/无效 Authorization 不影响公开读取、当前版本结构、文件 URL 绝对化、`external_url` 优先、无 current、无下载地址、非法平台、macOS 平台和内部字段不泄露。
- 文档变更:
- `docs/06-tasks.md` : T-607 标为 DONE。
- `docs/api.md` / `docs/routes.md` / `docs/04-architecture.md` :从计划口径改为已实现口径。
- `docs/current-state.md` 、`docs/00-ai-start-here.md` 、`docs/project-brief.md` 、`README.md` :同步 Phase 6 当前状态、当前 API 列表和下一步口径。
- 验证:
- `.\init.ps1` :开工前通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `py -3.12 -m py_compile apps\api\views.py apps\api\urls.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` :通过,9 tests OK。
- `py -3.12 manage.py test apps.api.tests.ApiKeyAuthenticationTests apps.api.tests.BalanceApiTests apps.api.tests.ModelsCatalogApiTests apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` :通过,25 tests OK。
- `py -3.12 manage.py test apps.api --keepdb --noinput --verbosity 2` :首次运行 184 秒超时,没有拿到结论。
- `py -3.12 manage.py test apps.api --keepdb --noinput --verbosity 1` :重跑通过,54 tests OK;仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 决策:T-607 不继承 `ExternalApiView` ,避免要求 API Key;也不挂 `GenerateRateThrottle` ,避免版本检查占用生成额度。文件下载地址按请求生成绝对 URL;生产 HTTPS 由已有 proxy/HTTPS 配置保证。
- 下一步:看板当前无新的编号任务;建议继续按业务优先级拆真实支付回调到账闭环、客户端下载包发布或真实图片耗时验证。
2026-07-07 08:38:07 +08:00
## 2026-07-07 文档:桌面端版本检查接口对接 Demo
- 状态:DONE。
- 变更:
- 新增 `docs/client-release-api-integration.md` ,整理 `GET /api/v1/client/releases/latest` 的用途、生产/本地请求地址、参数、curl / PowerShell / Python / JavaScript 请求 demo、成功/暂未发布/错误响应、客户端版本比较、下载 SHA256 校验、服务端发布前置条件和排查清单。
- `docs/README.md` :登记新文档入口。
- `docs/api.md` :在版本检查接口段落补充对接文档链接。
- 验证:纯文档修改;`git diff --check` 通过。
- 下一步:按业务优先级继续处理真实支付回调到账闭环、客户端下载包发布或真实图片耗时验证。
2026-07-08 14:19:00 +08:00
## 2026-07-08 决策:T-608 新用户注册赠送 100 点试用点数
- 状态:DONE(文档决策与任务拆分,未改代码)。
- 背景:产品新增需求「用户注册送 100 点」。该需求涉及可消费点数,不能简单把注册后的钱包初始值从 0 改成 100;必须纳入 billing 账本、流水、幂等和防刷设计。
- 需求口径:
- 新用户注册成功后一次性赠送 100 点试用点数。
- 只对 T-608 上线后的新注册用户自动发放;历史用户是否补发需另行审批和单独批处理任务。
- 当前注册策略仍为免邮箱验证、注册即可用、邮箱必填且唯一;注册送点会提高刷号动机,实施时需补基础注册限流,并登记图形验证码 / 人机验证等上线前风控项。
- 技术口径:
- 注册赠点必须经 `apps.billing` 服务发放,不得在 allauth adapter / view / signal 中直接写 `UserWallet.points_balance` 。
- 新增 `PointsLedger.ChangeType.SIGNUP_BONUS` ,写 `PointsLedger(signup_bonus, +100, balance_after, reason="new_user_registration")` 。
- 采用 MySQL 可落地的数据库级幂等兜底,推荐 `SignupBonusGrant(user UNIQUE)` ;不使用 MySQL 不支持的 partial unique / 条件唯一约束,也不使用会误伤其他流水类型的 `PointsLedger(user, change_type)` 全局唯一。
- `grant_signup_bonus(user, points=100)` 负责锁/创建钱包、创建幂等标记、加点和写流水;重复调用或并发触发不得重复发放。
- 文档变更:
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/01-vision.md` 、`docs/02-requirements.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/05-coding-rules.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/brand.md` :同步新需求、账本规则、幂等与防刷要求。
- `docs/06-tasks.md` :新增 T-608「新用户注册赠送 100 点试用点数」,状态 TODO,并把复杂活动赠点 / 历史用户补发移回 Backlog。
- `docs/current-state.md` :记录 T-608 为下一个可领取任务,并明确当前代码仍是旧行为,尚未实现注册送点。
- `docs/mvp-acceptance.md` 、`docs/phase-4-review.md` 、`docs/README.md` :标注 T-501/T-402/Phase 4 审核中的“注册不送点”属于历史口径,已被 T-608 新需求变更。
- 验证:纯文档修改;后续执行 T-608 时需补迁移、目标测试、`check` 和 `git diff --check` 证据。
- 下一步:领取 T-608,先实现 billing 层注册赠点服务和幂等数据结构,再接 allauth adapter、页面文案和测试。
2026-07-08 15:13:23 +08:00
## 2026-07-08 T-608 新用户注册赠送 100 点试用点数落地
- 状态:DONE。
- 代码变更:
- `apps/billing/models.py` :新增 `PointsLedger.ChangeType.SIGNUP_BONUS` 和 `SignupBonusGrant(user UNIQUE)` 幂等标记模型,迁移为 `billing.0007_alter_pointsledger_change_type_signupbonusgrant` 。
- `apps/billing/services.py` :新增 `grant_signup_bonus(user, points=100)` ,事务内锁/创建钱包、创建幂等记录、加点、写 `PointsLedger(signup_bonus,+100,balance_after,reason="new_user_registration")` ;重复调用返回未发放结果,不重复加点。
- `apps/portal/adapters.py` :allauth 注册保存成功后只调用 billing 服务发放注册赠点,不在 portal 直接写 `points_balance` 。
- `apps/portal/views.py` 与模板:dashboard 统计充值点数 + 注册赠点为“获得点数”;`/records/usage` 改为点数记录,展示 `signup_bonus` / `consume` / `refund` 流水;公开首页和注册页同步「注册送 100 点」文案。
- `config/settings.py` :新增 `ACCOUNT_RATE_LIMITS["signup"]` ,从 `ACCOUNT_SIGNUP_RATE_LIMIT` 读取,默认 `20/m/ip` ,由 allauth signup rate limit 执行。
- `apps/billing/admin.py` :新增 `SignupBonusGrant` 只读 admin,便于运营检索注册赠点记录。
- 文档变更:
- `docs/06-tasks.md` :T-608 状态改为 DONE,防刷口径改为显式配置 `ACCOUNT_SIGNUP_RATE_LIMIT` ,图形验证码 / 人机验证保留为上线前增强项。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/03-tech-stack.md` 、`docs/04-architecture.md` 、`docs/api.md` 、`docs/routes.md` 、`docs/current-state.md` 、`docs/env.md` 、`docs/deployment.md` 、`docs/mvp-acceptance.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` :同步注册赠点已落地、点数记录口径、注册限流配置和当前状态。
- 验证:
- `py -3.12 -m py_compile config\settings.py apps\billing\models.py apps\billing\services.py apps\billing\admin.py apps\portal\adapters.py apps\portal\views.py apps\billing\tests.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py migrate billing --noinput` :通过,No migrations to apply。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests.test_grant_signup_bonus_creates_wallet_ledger_and_is_idempotent apps.billing.tests.BillingServiceTests.test_grant_signup_bonus_adds_to_existing_wallet_balance apps.billing.tests.ConcurrentSignupBonusTests --keepdb --noinput --verbosity 2` : 3 tests OK。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_signup_rate_limit_is_configured --keepdb --noinput --verbosity 2` : 1 test OK。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_signup_grants_100_points_writes_signup_bonus_ledger_and_can_login apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_dashboard_entry_for_authenticated_user apps.portal.tests.PortalAccountFlowTests.test_authenticated_nav_highlights_current_page_only apps.portal.tests.PortalAccountFlowTests.test_dashboard_shows_account_summary_recent_records_for_current_user_only apps.portal.tests.PortalAccountFlowTests.test_usage_records_require_login_and_only_show_current_user_usage --keepdb --noinput --verbosity 2` : 6 tests OK。
- `py -3.12 manage.py test apps.billing.tests.BillingCoreModelTests.test_billing_models_are_registered_in_admin --keepdb --noinput --verbosity 2` : 1 test OK。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 已知测试环境现象:一次 portal 目标测试启动阶段因远程 MySQL `43.128.3.240` 连接超时中断;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示端口可达,拆分重跑全部目标用例通过。测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 下一步:当前任务看板暂无新的编号 TODO;建议按业务优先级拆真实支付回调到账闭环、客户端下载包发布、真实图片耗时验证,或从 Backlog 创建新任务。
2026-07-08 15:19:12 +08:00
## 2026-07-08 决策:T-609 桌面端版本检查接口增加强制更新标记
- 状态:DONE(文档决策与任务拆分,未改代码)。
- 背景:桌面端请求 `GET https://cm.833729.com/api/v1/client/releases/latest?platform=windows` 时,需要知道当前版本是否强制升级。产品要求在 `release` 对象中增加布尔字段 `force_update` ,示例值为 `true` 。
- 技术口径:
- 在现有 `release` 对象上新增 `force_update` ,保持 `version` 、`download_url` 、`sha256` 、`release_notes` 、`published_at` 等既有字段不删除,避免破坏已接入客户端。
- 数据来源拟新增 `DownloadRelease.force_update` 布尔字段,默认 `false` , django-admin 可编辑。
- 无当前版本或当前版本无下载地址时继续返回 `release:null` ,不额外返回 `force_update` 。
- 接口仍公开匿名只读,不读取用户、不扣点、不暴露后台 ID、本地路径或内部状态。
- 文档变更:
- `docs/06-tasks.md` :新增 T-609「桌面端版本检查接口增加强制更新标记」,状态 TODO。
- `docs/00-ai-start-here.md` 、`docs/current-state.md` :记录 T-609 为下一个可领取任务。
- 验证:纯文档修改;后续执行 T-609 时需补迁移、目标测试、`check` 和 `git diff --check` 证据。
- 下一步:领取 T-609,先补 `DownloadRelease.force_update` 与迁移,再更新接口响应、admin、测试和文档。
2026-07-08 15:38:59 +08:00
## 2026-07-08 T-609 桌面端版本检查接口增加强制更新标记落地
- 状态:DONE。
- 代码变更:
- `apps/portal/models.py` : `DownloadRelease` 新增 `force_update` 布尔字段,默认 `false` ,迁移为 `portal.0002_downloadrelease_force_update` 。
- `apps/portal/admin.py` :客户端发布版本 admin 列表、筛选和编辑表单加入「强制更新」。
- `apps/api/views.py` : `GET /api/v1/client/releases/latest` 的 `release` 对象新增 `force_update` ,保持既有 `version` 、`download_url` 、`sha256` 、`release_notes` 、`published_at` 不变;无发布版本或无下载地址仍返回 `release:null` 。
- `apps/api/tests.py` :覆盖默认 `force_update=false` 、`force_update=true` 、未发布响应不返回顶层 `force_update` 、响应字段白名单和 admin 字段可见性。
- 文档变更:
- `docs/06-tasks.md` : T-609 状态改为 DONE。
- `docs/api.md` 、`docs/routes.md` 、`docs/04-architecture.md` :同步版本检查接口响应结构、字段说明与 `DownloadRelease.force_update` schema。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/project-brief.md` :同步 Phase 6 当前状态和下一步口径。
- 验证:
- `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\api\views.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py migrate portal --noinput` :已应用 `portal.0002_downloadrelease_force_update` 。
- `py -3.12 manage.py test apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` :重跑通过,11 tests OK。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_download_release_only_keeps_one_current_per_platform --keepdb --noinput --verbosity 2` :通过,2 tests OK。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 已知测试环境现象:一次 API 目标测试与 portal 目标测试并行启动时,API 测试在连接远程 MySQL `43.128.3.240` 建测试库阶段超时;随后 `Test-NetConnection 43.128.3.240 -Port 3306` 显示 `TcpTestSucceeded=True` ,单独重跑 API 目标测试通过。测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 下一步:当前看板无编号 TODO;建议按业务优先级拆真实支付回调到账闭环、配置并发布客户端下载包、真实图片耗时验证,或从 Backlog 创建新任务。
2026-07-08 16:01:31 +08:00
## 2026-07-08 需求+决策:T-610 首页导入模板下载入口
- 状态:DONE(文档决策与任务拆分,未改代码)。
- 背景:公开首页已有“下载客户端”入口,但桌面端导入商品数据需要 Excel 模板。产品要求在“下载客户端”右侧增加“下载导入模板”链接,并允许运营在后台更新模板文件。
- 全栈分析:
- 前端:该入口属于公开首页下载区的一部分,应与“下载客户端”并列但保持次级样式;移动端需要换行不重叠。
- 后端:不应硬编码静态文件路径;模板需要后台可维护,因此应新增独立 `ImportTemplate` 配置模型,而不是复用 `DownloadRelease` 。
- 存储:本地上传模板放 `MEDIA_ROOT/import_templates/` ,生产继续由 Nginx 直接服务 `/media/` ,避免文件下载占用 Gunicorn worker;保留 `external_url` 以便后续切 CDN / 对象存储。
- 安全:该资源公开匿名下载,不需要 API Key、不读取用户、不扣点;页面不得暴露 `MEDIA_ROOT` 或服务器本地路径;当前模板互斥用应用层事务处理,不使用 MySQL 不支持的条件唯一约束。
- 接口:不新增对外 JSON API,首页 SSR 直接渲染当前模板下载链接即可。
- 文档变更:
2026-07-08 16:29:48 +08:00
- `docs/06-tasks.md` :新增 T-610「首页导入模板下载入口」,并增加 M9 里程碑。
2026-07-08 16:01:31 +08:00
- `docs/02-requirements.md` :新增 P1「导入模板下载」功能行,游客权限加入公开模板下载入口。
- `docs/04-architecture.md` :新增 `ImportTemplate` 目标 schema 和首页 / admin / media 托管口径。
- `docs/routes.md` :更新 `/` 首页职责与 admin 管理项。
- `docs/api.md` :明确导入模板下载不新增对外 JSON API。
- `docs/00-ai-start-here.md` 、`docs/current-state.md` :记录 T-610 为下一个可领取任务。
- 验证:纯文档修改;`git diff --check` 通过。
- 下一步:领取 T-610,先实现 `ImportTemplate` 模型/admin/迁移,再更新 `HomeView` 与首页模板,补目标测试并验证迁移、check、portal 首页测试。
2026-07-08 16:29:48 +08:00
## 2026-07-08 实施:T-610 首页导入模板下载入口
- 状态:DONE。
- 代码变更:
- `apps/portal/models.py` :新增 `ImportTemplate` ,字段包含 `name` 、`file` 、`external_url` 、`sha256` 、`is_current` 、`notes` 、`created_at` 、`updated_at` ;保存当前模板时在事务内把其他 current 置为 false,不使用 MySQL 条件唯一约束。
- `apps/portal/admin.py` :注册导入模板后台,支持上传模板文件、填写外部下载地址、标记当前模板、查看 SHA256 与下载来源。
- `apps/portal/views.py` : `HomeView` 同时读取当前 Windows 客户端版本与当前导入模板;本地模板 `file.url` 会转成当前站点绝对 URL, `external_url` 优先。
- `apps/portal/templates/portal/home.html` :在首页 hero 与下载区的“下载客户端”旁展示“下载导入模板”;无当前模板或无下载地址时不展示,不影响首页 200。
- `apps/portal/migrations/0003_importtemplate.py` :新增 `import_template` 表。
- `apps/portal/tests.py` :补充首页模板链接、外部 URL 优先、本地文件绝对 URL、本地路径不泄露、当前模板互斥、admin 字段可见的目标测试。
- 文档变更:
- `docs/06-tasks.md` : T-610 标记为 DONE。
- `docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/04-architecture.md` 、`docs/routes.md` 、`docs/api.md` :同步 T-610 已落地口径。
- 验证:
- `py -3.12 manage.py makemigrations portal` :生成 `portal.0003_importtemplate` 。
- `py -3.12 -m py_compile apps\portal\models.py apps\portal\admin.py apps\portal\views.py apps\portal\tests.py` :通过。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_import_template_download_link apps.portal.tests.PortalAccountFlowTests.test_homepage_builds_absolute_import_template_file_url_without_local_path apps.portal.tests.PortalAccountFlowTests.test_homepage_prefers_import_template_external_url_over_file apps.portal.tests.PortalAccountFlowTests.test_import_template_only_keeps_one_current apps.portal.tests.PortalAccountFlowTests.test_import_template_admin_exposes_download_fields --keepdb --noinput --verbosity 2` :通过,6 tests OK,并在测试库应用 `portal.0003_importtemplate` 。
- `py -3.12 manage.py test apps.portal --keepdb --noinput --verbosity 1` :跑到后段时远程 MySQL `43.128.3.240` 连接被重置(2013 / WinError 10054);前 27 条已通过,后续 8 errors 为连接断开后的事务错误。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 下一步:提交后可部署到 185.216.248.75,执行迁移并在 django-admin 新增当前导入模板记录;配置模板后线上首页才会显示“下载导入模板”。
2026-07-08 16:55:41 +08:00
## 2026-07-08 需求+决策:T-611 用户端品牌名统一为“虾皮圈”
- 状态:DONE(文档决策与任务拆分,未改代码)。
- 背景:线上用户端页面仍把产品/项目名展示为 `cmhub` ,需要统一改成面向终端用户的品牌“虾皮圈”。
- 范围决策:
- 修改范围限定在用户端 portal 页面可见文案和浏览器标题,包括公开首页、顶部导航、注册/登录/登出、控制台、充值、API Key、可用模型、充值记录、点数记录等模板。
- 用户端文案中的“cmhub API Key”改为“虾皮圈 API Key”。
- 不改仓库名、Python 包名、Django app 名、数据库表名、环境变量、API 路径、域名、对外接口字段、后台内部模型名和技术文档里作为服务代号的 `cmhub` 。
- 文档变更:
2026-07-08 17:05:20 +08:00
- `docs/06-tasks.md` :新增 T-611「用户端品牌名统一为虾皮圈」,并增加 M10 里程碑。
2026-07-08 16:55:41 +08:00
- `docs/02-requirements.md` :新增 P1 用户端品牌展示需求。
- `docs/00-ai-start-here.md` 、`docs/current-state.md` :记录 T-611 为下一个可领取任务。
- 验收口径:更新 portal 相关断言,覆盖首页展示“虾皮圈”且不再展示旧标题 `cmhub AI 电商生成台` ;关键用户端页面渲染不含作为品牌展示的 `cmhub` ; `check` 、`makemigrations --check --dry-run` 、目标 portal 测试通过。
- 下一步:领取 T-611,先搜索 portal 模板与测试里的品牌文案,再做最小范围替换。
2026-07-08 17:05:20 +08:00
## 2026-07-08 实施:T-611 用户端品牌名统一为“虾皮圈”
- 状态:DONE。
- 代码变更:
- `apps/portal/templates/portal/base.html` :浏览器默认标题与顶部导航品牌从 `cmhub` 改为“虾皮圈”。
- `apps/portal/templates/portal/home.html` :首页 title、H1 与下载区“cmhub API Key”文案改为“虾皮圈”。
- `apps/portal/templates/account/*.html` 、`apps/portal/templates/portal/*.html` :注册、登录、退出、控制台、充值、API Key、可用模型、充值记录、点数记录等页面标题统一为“虾皮圈”。
- `apps/portal/templates/account/email/*` :allauth 邮件模板品牌同步为“虾皮圈”(当前免邮箱验证不会发送,但保留一致性)。
- `apps/portal/tests.py` :新增品牌断言,覆盖公开首页、注册/登录页和登录后的关键用户端页面;保留 CSS 类名、API Key 前缀、mock 支付票据、下载文件名里的内部 `cmhub` 不变。
- 文档变更:
- `docs/06-tasks.md` : T-611 标记为 DONE。
- `docs/00-ai-start-here.md` 、`docs/current-state.md` :同步 T-611 已完成与最新验证。
- 验证:
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_homepage_is_public_and_shows_anonymous_onboarding_without_release apps.portal.tests.PortalAccountFlowTests.test_homepage_shows_current_download_release apps.portal.tests.PortalAccountFlowTests.test_portal_pages_use_shopee_circle_branding --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- 下一步:如需线上生效,部署最新代码到 185.216.248.75 并重启服务;无数据库迁移需要执行。
2026-07-08 19:47:49 +08:00
## 2026-07-08 生图异步任务化任务口径修正(非任务)
- 状态:DONE。
- 背景:Claude Code 新增了 T-612~T-615 草案,用于处理生图慢、504 gateway timeout、客户端请求超时和旧同步接口迁移问题。复核线上实际运行后确认当前 `cmhub-generate` 已是 `gthread --workers 2 --threads 16 --timeout 900` ,并非 sync worker,因此原草案中“sync worker 池饥饿 → 默认切 gevent”的口径需要修正。
- 文档变更:
- `docs/06-tasks.md` :T-612 改为“上游硬截止 + 长请求池校准”,明确先保留已验证的 `gthread` 长请求池,gevent 仅作为兼容性验证后的替代方案;T-613 明确重构现有 `apps/api/generation.py` ,不得复制第二套扣点 / 退点逻辑;T-614 补充 `ImageGenerationTask` 、UUID `task_id` 、`Idempotency-Key` 去重与 payload 冲突、DB worker + systemd、公开 URL 配置、IDOR 防护和本期不暴露 cancel;T-615 补充旧同步 / 新异步路径结构化遥测与弃用条件。
- `docs/current-state.md` :把“当前无编号 TODO”更新为 T-612~T-615 待开始,下一步为 T-612。
- `README.md` 、`docs/00-ai-start-here.md` :同步 Phase 6 当前状态和下一步任务。
- 验证:
- `git diff --check -- README.md docs/00-ai-start-here.md docs/06-tasks.md docs/current-state.md` :通过,仅 Windows CRLF 提示。
- `Select-String` 确认 `docs/06-tasks.md` 中 T-612~T-615、M11 和 Backlog 已更新,`docs/current-state.md` 的下一个可领取任务为 T-612。
- `rg -n "sync worker|先 gevent|当前无编号 TODO|图片真实耗时验证" README.md docs/00-ai-start-here.md docs/06-tasks.md docs/current-state.md` :仅保留 T-612 中“当前是 `gthread` 而非 sync worker”的事实说明,未再出现“当前无编号 TODO”等旧口径。
- 阻塞:无。
- 决策:T-612 先做硬截止和现有 gthread 长请求池校准,不把 gevent 作为默认第一步;T-614 第一版用 DB 任务表 + management command worker + systemd,不引入 Celery / Redis;异步 worker 必须复用共享 core 和已预扣 `CallRecord` ,不得复制资金逻辑。
- 下一步:领取 T-612,先更新 `deployment.md` / `04-architecture.md` / `env.md` ,再落代码实现上游硬截止与退点测试。
## 2026-07-08 T-614 验收条件加固(非任务)
- 状态:DONE。
- 背景:复核 Claude Code 对 T-614 的新增验收条件后,确认“结果保留窗口”“僵任务回收 reaper”“worker 重复执行幂等”方向合理,但需要把重排队、租约和迟到 worker 的口径写得更严格。
- 变更:更新 `docs/06-tasks.md` 中 T-614:
- `ImageGenerationTask` 字段补充 `locked_at` 、`lease_expires_at` 、`heartbeat_at` 、`worker_id` 、`attempt_count` 。
- reaper 改为按 `lease_expires_at` / `heartbeat_at` 识别僵任务,默认 `running -> failed + 退点` ,不默认重排队;只有能证明尚未调上游时才允许后续任务设计重排队。
- worker 语义改为“允许至少一次执行,但账务和结果终态必须 exactly-once”,不得重复扣 / 退,不得覆盖已成功结果,也不得把已被 reaper 判失败并退点的任务改回成功。
- 环境变量口径补充 `IMAGE_TASK_RETENTION_HOURS` 、`GENERATED_IMAGE_RETENTION_HOURS` 、`IMAGE_TASK_REAPER_INTERVAL_SECONDS` 、`IMAGE_TASK_LEASE_SECONDS` 。
- 验收补充迟到 worker 场景:reaper 已判失败并退点后,迟到 worker 返回成功也不能改回 `succeeded` 、不能覆盖结果、不能再次改账。
- 验证:仅文档更新;提交前用 `git diff --check` 、`Select-String` 复核。
- 下一步:提交本轮文档;后续领取 T-612。
2026-07-08 20:14:33 +08:00
## 2026-07-08 实施:T-612 生图同步接口止血
- 状态:DONE。
- 代码变更:
- `config/settings.py` / `.env.example` :新增 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS` ,默认 180 秒。
- `apps/ai/providers/utils.py` :新增生图上游读取硬截止计算,实际读取超时为 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)` 。
- `apps/ai/providers/openai_compatible.py` :所有 `generate_image` Provider 的上游请求和上游返回图片 URL 下载改用生图硬截止;`generate_text` 继续使用原 `AiModel.timeout_seconds` / 分辨率默认超时,不受 T-612 截断。
- `apps/api/generation.py` : `requests.Timeout` 统一映射为 `upstream_timeout` ,HTTP 502,并复用既有失败退点路径。
- `apps/ai/tests.py` / `apps/api/tests.py` :新增生图超时截断、文本不受影响、上游超时后退回预扣点数并标记调用失败的目标测试。
- 文档变更:
- `docs/06-tasks.md` :T-612 标记为 DONE,下一个任务更新为 T-613。
- `docs/env.md` 、`docs/deployment.md` 、`docs/04-architecture.md` 、`docs/api.md` :同步环境变量、部署超时链、API 错误码和退款口径。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` :同步当前阶段、最新验证和下一步。
- 验证:
- `py -3.12 -m py_compile config\settings.py apps\ai\providers\utils.py apps\ai\providers\openai_compatible.py apps\api\generation.py apps\ai\tests.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.ai.tests.ProviderUtilsTests.test_image_request_timeout_caps_read_timeout_to_deadline apps.ai.tests.ChatCompletionsProviderTests.test_generate_image_caps_post_and_download_timeouts apps.ai.tests.ChatCompletionsProviderTests.test_generate_text_does_not_use_image_deadline --keepdb --noinput --verbosity 2` :首次发现 text/image 超时方法调用接反,修正后重跑通过,3 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_image_upstream_timeout_refunds_precharged_points_and_marks_call_failed --keepdb --noinput --verbosity 2` :通过。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` :通过,17 tests OK。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 已知测试环境现象:测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 决策:T-612 只给旧同步生图接口加硬截止和明确错误码,不引入 gevent,不改变文本生成超时口径;真正解决长耗时与客户端轮询体验仍放到 T-613~T-615。
- 下一步:领取 T-613,抽生成核心 service,为 T-614 生图异步任务化复用同一套审核、计费、上游和退点逻辑。
2026-07-08 21:24:59 +08:00
## 2026-07-08 实施:T-613 抽生成核心 service
- 状态:DONE。
- 代码变更:
- `apps/api/generation.py` :把旧同步生成编排抽成 `GenerationInput` 、`PreparedGeneration` 、`PrechargedGeneration` 、`GenerationResult` 与三段核心函数:`prepare_generation()` 、`precharge_generation()` 、`execute_precharged_generation()` ;旧 `generate_title_response()` / `generate_image_response()` 保留为兼容薄包装。
- `apps/api/generation.py` : `prepare_generation()` 负责 prompt 审核、图片输入处理、别名解析、Provider 选择和计费计算;`precharge_generation()` 只调用 billing 的 `precharge_call()` ; `execute_precharged_generation()` 复用已预扣 `CallRecord` 调上游,并通过既有 `mark_call_success()` / `refund_call_points()` 完成成功确认或失败退点。
- `apps/api/storage.py` : `save_generated_image()` 新增 `url_builder` 参数,核心 service 可生成外部 URL 而不依赖 DRF `Request` ;保留旧 `request` 参数兼容。
- `apps/api/views.py` : `GenerateImageView` 只把 `request.build_absolute_uri` 作为 URL 构建器传给核心 service,不再把整个 request 传入生成核心。
- `apps/api/tests.py` :新增核心 service 目标测试,覆盖无 DRF request 构建图片 URL,以及已预扣执行阶段上游失败后退点。
- 文档变更:
- `docs/06-tasks.md` :T-613 标记为 DONE,下一个任务为 T-614。
- `docs/04-architecture.md` 、`docs/api.md` :同步生成核心 service 的内部阶段与“后续异步 worker 必须复用同一套资金逻辑”的口径。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` :同步 Phase 6 当前状态、验证证据和下一步。
- 验证:
- `.\init.ps1` :开工前通过。
- `py -3.12 -m py_compile apps\api\generation.py apps\api\storage.py apps\api\views.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_generation_core_saves_image_with_url_builder_without_request apps.api.tests.GenerateApiTests.test_precharged_generation_stage_refunds_on_upstream_failure --keepdb --noinput --verbosity 2` :通过,2 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 1` :通过,19 tests OK。
- `.\init.ps1` :收尾验证通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 测试环境现象:
- 首次运行新增目标测试时,测试库 setup 阶段远程 MySQL `43.128.3.240` 连接超时;`Test-NetConnection 43.128.3.240 -Port 3306` 随后显示 `TcpTestSucceeded=True` ,重跑通过。
- 曾尝试 `py -3.12 manage.py test apps.api --keepdb --noinput --verbosity 1` ,命令 484 秒超时无断言结果;拆跑非生成 API 测试类时仍在测试库 setup 阶段出现 `OperationalError(2003)` 。本轮直接影响的 `GenerateApiTests` 已完整通过。
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 决策:T-613 不改旧同步接口成功 / 失败响应契约,不新增异步任务表或新路由;只把生成 pipeline 拆成后续 T-614 可复用的准备、预扣、已预扣执行三个阶段。
- 下一步:领取 T-614,新增生图异步提交 / 轮询接口,并复用 T-613 的核心阶段。
2026-07-08 22:08:48 +08:00
## 2026-07-08 实施:T-614 生图异步任务化接口
- 状态:DONE。
- 代码变更:
- `apps/api/models.py` / `apps/api/migrations/0001_initial.py` :新增 `ImageGenerationTask` ,公开 UUID `task_id` ,关联 `user` 、`api_key` 、已预扣 `CallRecord` ,记录状态、请求哈希、输入文件引用、结果 URL、错误、租约、心跳、worker 和尝试次数;幂等键使用 `(api_key, idempotency_key_hash)` 唯一约束,兼容 MySQL,不使用条件唯一约束。
- `apps/api/image_tasks.py` :新增异步任务 service,提交阶段同步审核 prompt、处理图片输入、预扣点并建 queued 任务;支持 `Idempotency-Key` 去重和 payload 冲突;worker 抢任务后复用 T-613 的 `execute_precharged_generation()` ,成功写稳定 URL,失败 / 超时走计费层幂等退款;reaper 处理租约或心跳过期的 running 任务。
- `apps/api/views.py` / `apps/api/urls.py` :新增 `POST /api/v1/generate/image/tasks` 与 `GET /api/v1/generate/image/tasks/{task_id}` ;旧 `POST /api/v1/generate/image` 保留。
- `apps/api/management/commands/run_image_tasks.py` :新增 DB worker 命令,支持循环处理、`--once` 和 `--reap-only` 。
- `config/settings.py` / `.env.example` :新增 `PUBLIC_BASE_URL` 、`MEDIA_PUBLIC_BASE_URL` 、`IMAGE_TASK_RETENTION_HOURS` 、`GENERATED_IMAGE_RETENTION_HOURS` 、`IMAGE_TASK_REAPER_INTERVAL_SECONDS` 、`IMAGE_TASK_LEASE_SECONDS` 。
- `apps/api/admin.py` :注册图片生成任务只读排障 admin。
- `apps/api/tests.py` :新增异步生图目标测试,覆盖审核拦截、余额不足、幂等、worker 成功、跨用户拒绝、失败退款、reaper 和迟到 worker 幂等。
- 文档变更:
- `docs/api.md` :新增异步提交 / 轮询接口合约、响应示例、错误码和幂等说明。
- `docs/routes.md` :新增两条异步生图 API 路由和 admin 管理项。
- `docs/04-architecture.md` :同步 `ImageGenerationTask` schema、异步计费时序、worker/reaper 语义和 exactly-once 账务终态。
- `docs/deployment.md` :新增 worker 运行 / systemd 示例、Nginx 对 `/api/v1/generate/image/tasks*` 的优先短请求分流、上线验证和检查项。
- `docs/env.md` 、`docs/03-tech-stack.md` 、`docs/02-requirements.md` :同步异步生图配置与“旧同步 + 新异步并存”口径。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` :同步当前阶段和下一步为 T-615。
- `docs/06-tasks.md` : T-614 标记为 DONE。
- 验证:
- `py -3.12 manage.py makemigrations api` :通过,生成 `apps/api/migrations/0001_initial.py` 。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests -v 2 --keepdb` :通过,28 tests OK。
- 测试环境现象:
- 首次运行 `py -3.12 manage.py test apps.api.tests.GenerateApiTests -v 2` 时,已有测试库 `test_cmhub` 触发交互式删除确认,非交互环境 EOF 中断;改用 `--keepdb` 后迁移并通过。
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 决策:
- T-614 不引入 Celery / Redis;第一版异步用 MySQL 任务表 + management command worker + systemd。
- 本期不暴露 cancel 路由;后续如需取消,仅允许取消 queued 并单独拆任务。
- `image_base64` 不进 DB 原文,提交阶段解码为输入文件引用;异步结果不透传上游临时链接,只返回 cmhub 托管 URL。
- 下一步:领取 T-615,给旧同步生图接口和新异步路径补结构化遥测,并形成旧同步接口弃用条件。
2026-07-08 22:44:17 +08:00
## 2026-07-08 实施:T-615 旧同步生图接口用量遥测 + 弃用口径
- 状态:DONE。
- 代码变更:
- 新增 `apps/api/telemetry.py` ,集中构造并写入 `cmhub.api.generation_usage` 结构化日志事件 `generation_route_usage` 。
- `apps/api/views.py` :旧同步 `POST /api/v1/generate/image` 与新异步提交 `POST /api/v1/generate/image/tasks` 在参数错误、业务错误和成功返回前写遥测日志;旧同步成功响应和异步提交响应契约不变。
- `apps/api/tests.py` :新增 3 条目标测试,覆盖旧同步成功日志、异步提交成功日志、异步余额不足错误日志,并断言日志只含白名单字段,不包含完整 API Key、prompt、`image_base64` 或 provider raw。
- 文档变更:
- `docs/04-architecture.md` 、`docs/api.md` 、`docs/deployment.md` :同步遥测字段、`X-Client-Version` 请求头建议、日志查看方式和旧同步接口弃用条件。
- `README.md` 、`docs/00-ai-start-here.md` 、`docs/current-state.md` 、`docs/project-brief.md` 、`docs/project-onepager.md` :同步 T-615 已完成和后续生产观察口径。
- `docs/06-tasks.md` : T-615 标记为 DONE。
- 验证:
- `py -3.12 -m py_compile apps\api\telemetry.py apps\api\views.py apps\api\tests.py` :通过。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_sync_image_usage_telemetry_logs_safe_client_version_and_key_identity apps.api.tests.GenerateApiTests.test_async_image_submit_usage_telemetry_logs_safe_success_event apps.api.tests.GenerateApiTests.test_async_image_submit_usage_telemetry_logs_error_code_without_sensitive_data --keepdb --noinput --verbosity 2` :通过,3 tests OK。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` :通过,31 tests OK。
- `.\init.ps1` :通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 测试环境现象:
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
2026-07-13 17:51:43 +08:00
2026-07-08 22:44:17 +08:00
- 决策:
- T-615 第一版只做日志遥测,不新增 admin 报表或统计表;如后续需要报表,单独评估数据量、索引和保留周期。
- 旧同步接口下线前必须继续保持成功响应兼容;待新版客户端默认异步、旧路调用归零或低于运营阈值一段时间后,再宣布 deprecated 并单独立下线任务。
- 下一步:部署后观察 `generation_route_usage` 日志;继续处理真实支付回调到账闭环、客户端下载包发布等业务事项。
2026-07-08 23:11:46 +08:00
## 2026-07-08 文档补充:T-614 异步生图实现取舍记录(非任务)
- 状态:DONE。
- 背景:复核 T-612~T-615 审核建议后确认两点实现行为需要文档化:worker 会复跑 `prepare_generation()` ; `image_url` 输入会在 submit 阶段下载。
- 变更:
- `docs/04-architecture.md` :补充 worker 复审 prompt、重解析别名 / Provider / 定价的当前取舍;明确账务仍使用已预扣 `CallRecord.points_cost` ,不重复扣点;队列积压时再做提交时模型配置快照。
- `docs/api.md` :在异步生图提交接口说明桌面端主链路推荐 `image_base64` , `image_url` 会在 submit 阶段下载并可能阻塞。
- `docs/deployment.md` :补充生产排查口径,submit 慢时先检查是否批量使用 `image_url` ;worker 重解析配置是短队列下偏安全取舍。
- `docs/06-tasks.md` :Backlog 新增“异步生图提交时模型配置快照”。
- `docs/current-state.md` :同步当前实现边界。
- 验证:仅文档更新;使用 `git diff --check` 检查格式。
- 决策:当前不改代码。`image_base64` 是桌面端主路径,submit 仍为短请求;`image_url` 阻塞属于边缘兼容路径。模型配置快照不作为热修,待队列积压或多模型价差扩大后单独立任务。
- 下一步:无需立即编码;继续按生产优先级处理支付回调闭环、客户端发布和线上遥测观察。
2026-07-09 11:57:06 +08:00
## 2026-07-09 热修:异步生图 worker 失败日志与扩容口径
- 状态:DONE。
- 背景:线上 admin 中多条生图任务停留在“待处理”。排查确认 `POST /api/v1/generate/image/tasks` 已返回 `202` ,任务进入 `queued` ;当前只有 1 个 `cmhub-image-worker` 串行消费,部分任务在上游 180 秒硬截止后 `upstream_timeout` ,导致后续 queued 积压。
- 代码变更:
- `apps/api/management/commands/run_image_tasks.py` :worker 每处理一个任务输出结构化 stdout 行 `event=image_task_processed task_id=... status=... alias=... duration_ms=...` ;失败 / 过期任务额外输出 `error_code` 。
- `apps/api/tests.py` :新增目标测试,覆盖失败任务日志包含 `task_id` 、别名、`error_code` 与 `duration_ms` ,且不输出 prompt。
- 文档变更:
- `docs/deployment.md` :补充 worker 任务日志、排障口径和扩容策略;明确不要直接扩到 100 个 worker,应按 2、4、8、16 逐级观察。
- `docs/current-state.md` :同步当前 worker 日志字段和扩容口径。
- 验证:
- `py -3.12 -m py_compile apps\api\management\commands\run_image_tasks.py apps\api\tests.py` :通过。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_run_image_tasks_logs_failed_task_alias_error_and_duration --keepdb --noinput --verbosity 2` :通过,1 test OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 1` :通过,32 tests OK。
- 决策:暂不把线上 `cmhub-image-worker` 直接扩到 100。100 个进程会同时放大 MySQL 连接、上游请求、图片下载和本地写文件压力;在上游已经出现 `upstream_timeout` 时,直接 100 并发更可能放大失败率。建议先用新日志观测后按 2、4、8、16 逐级扩容。
2026-07-09 14:17:22 +08:00
## 2026-07-09 文档登记:T-616 生图失败自动重试 2 次
- 状态:DONE(仅任务登记,未改代码)。
- 背景:线上 `api.vectorengine.ai` 部分生图任务在上游读取截止内返回 `Read timed out` ,现有异步任务会立即 `failed` 并退点。为降低偶发上游超时对用户的失败率,需要在异步 worker 层增加可控重试。
- 文档变更:
- `docs/06-tasks.md` :新增 T-616「生图失败自动重试 2 次」,状态 TODO;明确最多 3 次上游调用、只预扣一次、前两次临时性失败重排队不退点、最终失败才退点。
- `docs/current-state.md` :同步 T-616 为下一个可领取任务。
- 方案要点:
- 只重试 `upstream_timeout` 、网络连接错误、上游 502/503/504 等临时性错误。
- 不重试敏感词命中、点数不足、未定价、模型/别名配置不可用、参数错误、Provider 配置错误等确定性错误。
- 新增 `next_attempt_at` 与 `IMAGE_TASK_MAX_RETRIES` / `IMAGE_TASK_RETRY_BACKOFF_SECONDS` 等配置;worker 只抢到达重试时间的 queued 任务。
- 轮询响应可兼容性新增 `attempt_count` 、`max_attempts` 、`next_attempt_at` ;日志新增 attempt / retrying / next_attempt_at 字段。
- 验证:仅文档更新;提交前执行 `git diff --check` 。
2026-07-09 14:46:09 +08:00
## 2026-07-09 实施:T-616 生图失败自动重试 2 次
- 状态:DONE。
- 背景:线上异步生图已出现 `api.vectorengine.ai` 偶发 `Read timed out` 。原逻辑第一次 `upstream_timeout` 就把任务置 `failed` 并退点,批量生图成功率受上游短暂波动影响较大。
- 代码变更:
- `apps/api/models.py` / `apps/api/migrations/0002_imagegenerationtask_next_attempt_at_and_more.py` :给 `ImageGenerationTask` 增加 `next_attempt_at` ,并新增 `(status, next_attempt_at)` 索引,worker 只抢到达重试时间的 queued 任务。
- `config/settings.py` / `.env.example` :新增 `IMAGE_TASK_MAX_RETRIES=2` 、`IMAGE_TASK_RETRY_BACKOFF_SECONDS=10,30` 。
- `apps/api/generation.py` : `execute_precharged_generation()` 增加 `refund_on_failure` 参数;旧同步接口默认行为不变,异步 worker 可在上游失败时先不退款,由任务状态机决定重试或最终退款。
- `apps/api/image_tasks.py` :只对 `upstream_timeout` / `upstream_error` 自动重试;前两次临时性失败回到 `queued` 、写 `next_attempt_at` 、保留 `CallRecord.pending` 、不退点;不可重试错误或最终失败才幂等退款。轮询响应新增 `attempt_count` 、`max_attempts` 、`next_attempt_at` 。
- `apps/api/management/commands/run_image_tasks.py` : worker 日志增加 `attempt` 、`max_attempts` 、`retrying` 、`next_attempt_at` ,重试中 / 失败 / 过期任务输出 `error_code` 。
- `apps/api/admin.py` :后台图片任务列表 / 过滤 / 只读字段展示 `next_attempt_at` 。
- `apps/api/tests.py` :新增 T-616 目标测试,覆盖首次超时回队列不退点、两次超时后三次成功只扣一次、连续三次超时最终只退一次、非重试错误立即退款、worker 重试日志字段不泄露敏感信息;旧失败退款测试用 `IMAGE_TASK_MAX_RETRIES=0` 保留原验收。
- 文档变更:
- `docs/api.md` :异步提交 / 轮询响应示例新增 `attempt_count` 、`max_attempts` 、`next_attempt_at` ,说明 `queued + next_attempt_at` 为等待自动重试。
- `docs/04-architecture.md` :同步 `next_attempt_at` schema、索引、异步计费状态机和临时性失败重试语义。
- `docs/env.md` / `docs/deployment.md` :同步新增环境变量、最坏耗时估算和 worker 日志排障口径。
- `docs/current-state.md` :同步 T-616 已完成、最新验证和下一步。
- `docs/06-tasks.md` : T-616 标记为 DONE。
- 验证:
- `py -3.12 -m py_compile apps\api\generation.py apps\api\image_tasks.py apps\api\management\commands\run_image_tasks.py apps\api\models.py apps\api\admin.py apps\api\tests.py config\settings.py` :通过。
- `py -3.12 manage.py makemigrations api` :生成 `api.0002_imagegenerationtask_next_attempt_at_and_more` 。
- `py -3.12 manage.py migrate api --noinput` :通过,已应用 `api.0001_initial` 与 `api.0002_imagegenerationtask_next_attempt_at_and_more` 到当前开发库。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_async_image_retryable_timeout_requeues_without_refund_and_respects_backoff apps.api.tests.GenerateApiTests.test_async_image_retryable_timeouts_then_success_charges_once apps.api.tests.GenerateApiTests.test_async_image_retryable_timeouts_final_failure_refunds_once apps.api.tests.GenerateApiTests.test_async_image_non_retryable_provider_error_fails_immediately_and_refunds apps.api.tests.GenerateApiTests.test_run_image_tasks_logs_retrying_task_attempt_fields_without_sensitive_data --keepdb --noinput --verbosity 2` :通过,5 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests.test_async_image_submit_usage_telemetry_logs_safe_success_event apps.api.tests.GenerateApiTests.test_async_image_submit_usage_telemetry_logs_error_code_without_sensitive_data apps.api.tests.GenerateApiTests.test_async_image_blocked_prompt_creates_no_task_or_charge apps.api.tests.GenerateApiTests.test_async_image_insufficient_points_returns_402_without_task apps.api.tests.GenerateApiTests.test_async_image_idempotency_reuses_task_and_rejects_conflict apps.api.tests.GenerateApiTests.test_async_image_worker_success_and_poll_are_idempotent apps.api.tests.GenerateApiTests.test_async_image_poll_rejects_cross_user_access apps.api.tests.GenerateApiTests.test_async_image_worker_failure_refunds_precharged_points apps.api.tests.GenerateApiTests.test_run_image_tasks_logs_failed_task_alias_error_and_duration apps.api.tests.GenerateApiTests.test_async_image_reaper_fails_stale_running_task_and_refunds apps.api.tests.GenerateApiTests.test_async_image_duplicate_worker_does_not_double_charge_or_refund apps.api.tests.GenerateApiTests.test_async_image_late_worker_after_reaper_cannot_flip_failed_task --keepdb --noinput --verbosity 2` :通过,12 tests OK。
- `py -3.12 manage.py test apps.api.tests.GenerateApiTests --keepdb --noinput --verbosity 2` :通过,37 tests OK。
- `.\init.ps1` :通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 测试环境现象:
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
2026-07-13 17:51:43 +08:00
2026-07-09 14:46:09 +08:00
- 决策:
- T-616 第一版不重试敏感词、余额不足、未定价、模型 / 别名不可用、参数错误和 Provider 能力错误;这些属于确定性失败,立即失败并退款或在提交阶段不扣点。
- 重试期间不展示 `error` 给轮询客户端,客户端只需看到 `queued + next_attempt_at` 后继续轮询;最终 `failed` 才表示已退款。
2026-07-13 17:04:59 +08:00
## 2026-07-13 实施:T-617 桌面端版本检查接口增加文件大小字段
- 状态:DONE。
- 背景:桌面端请求 `GET /api/v1/client/releases/latest?platform=windows` 后,需要拿到安装包文件大小,下载完成后与本地文件大小比对,辅助发现下载不完整或文件异常。
- 代码变更:
- `apps/portal/models.py` / `apps/portal/migrations/0004_downloadrelease_size_bytes.py` : `DownloadRelease` 新增 `size_bytes` 可空正整数字段,单位字节,老发布记录可保持空值。
- `apps/portal/admin.py` :客户端下载版本 admin 的下载配置区新增 `size_bytes` ,列表页同步展示,便于运营填写和核对。
- `apps/api/views.py` : `GET /api/v1/client/releases/latest` 的 `release` 对象新增 `size_bytes` ;有值返回整数,未配置返回 `null` ;无当前版本或无下载地址时仍返回 `release:null` 。
- `apps/api/tests.py` :补充版本接口字段白名单、有值返回、空值返回、未发布不返回顶层字段和 admin 字段可见测试。
- 文档变更:
- `docs/api.md` :更新响应示例、字段说明和客户端校验建议。
- `docs/04-architecture.md` :同步 `DownloadRelease.size_bytes` schema、公开版本检查接口元数据口径。
- `docs/02-requirements.md` / `docs/routes.md` / `docs/06-tasks.md` / `docs/current-state.md` / `README.md` :同步 T-617 状态和桌面端文件大小校验口径。
- 验证:
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py test apps.api.tests.ClientLatestReleaseApiTests --keepdb --noinput --verbosity 2` :通过,12 tests OK。
- `py -3.12 manage.py migrate portal --noinput` :命令 120 秒超时,但随后 `py -3.12 manage.py showmigrations portal` 显示 `portal.0004_downloadrelease_size_bytes` 已应用;未发现残留 `manage.py migrate` 进程。
- 测试环境现象:
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
2026-07-13 17:51:43 +08:00
## 2026-07-13 文档登记:T-618 客户端发布版本后台必填文件校验元数据
- 状态:TODO(仅任务登记,未改代码)。
- 背景:T-617 已让版本检查接口返回 `release.size_bytes` ,但线上当前发布版本仍可能为空;如果 admin 发布客户端版本时漏填 `sha256` 或 `size_bytes` ,桌面端无法完整校验下载文件。
- 文档变更:
- `docs/06-tasks.md` :新增 T-618「客户端发布版本后台必填文件校验元数据」,要求 django-admin 中 `DownloadRelease.sha256` 和 `DownloadRelease.size_bytes` 必填。
- `docs/current-state.md` :把 T-618 设为下一个可领取任务。
- 方案口径:
- 第一版只做 admin 表单必填,不直接把数据库字段改成 `NOT NULL` ,避免历史记录或线上当前 `size_bytes=null` 造成迁移风险。
- 实现时建议给 `DownloadReleaseAdmin` 增加专用 `ModelForm` ,字段级要求 `sha256` 必填且保持 64 位十六进制校验,`size_bytes` 必填且 `>=1` 。
- 上线后必须补齐当前 Windows 发布版本的 `size_bytes` ,并验证 `/api/v1/client/releases/latest?platform=windows` 返回正整数。
- 验证:仅文档更新;提交前执行 `git diff --check` 。
## 2026-07-13 实施:T-618 客户端发布版本后台必填文件校验元数据
- 状态:DONE。
- 背景:T-617 已让版本检查接口返回 `release.size_bytes` ,但如果运营在 django-admin 发布客户端版本时漏填 `sha256` 或 `size_bytes` ,桌面端无法完整校验安装包。
- 代码变更:
- `apps/portal/admin.py` :新增 `DownloadReleaseAdminForm` 并挂到 `DownloadReleaseAdmin.form` ; admin 新增 / 编辑 `DownloadRelease` 时 `sha256` 与 `size_bytes` 必填,继续复用 `sha256` 64 位十六进制校验和 `size_bytes >= 1` 校验。
- `apps/portal/tests.py` :新增 admin 表单目标测试,覆盖缺 `sha256` / 缺 `size_bytes` 拒绝保存,以及合法 `sha256 + size_bytes` 可通过表单保存。
- 文档变更:
- `docs/06-tasks.md` :T-618 标记为 DONE,并把实现口径从建议改为已落地。
- `docs/api.md` :说明 T-618 起 admin 新增 / 编辑发布版本必须填写 `sha256` 和 `size_bytes` ,但 API 仍兼容历史空值。
- `docs/04-architecture.md` / `docs/routes.md` :同步客户端下载版本 admin 必填校验元数据口径。
- `README.md` / `docs/current-state.md` :同步 T-618 已完成、当前状态与验证结果。
- 验证:
- `.\init.ps1` :通过(Python 3.12.3,依赖已满足,`manage.py check` 0 issues,打印启动命令)。
- `py -3.12 manage.py check` :通过,0 issues。
- `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- `py -3.12 manage.py test apps.portal.tests.PortalAccountFlowTests.test_download_release_admin_requires_sha256_and_size_bytes apps.portal.tests.PortalAccountFlowTests.test_download_release_admin_saves_valid_release_metadata apps.api.tests.ClientLatestReleaseApiTests.test_latest_release_returns_null_size_bytes_when_not_configured apps.api.tests.ClientLatestReleaseApiTests.test_latest_release_is_public_without_api_key_and_returns_current_release --keepdb --noinput --verbosity 2` :通过,4 tests OK。
- 测试环境现象:
- 测试期仍保留 allauth `account.EmailAddress` 条件唯一约束在 MySQL 上不可创建的既有 `models.W036` 警告。
- 部署提醒:
- 代码部署后需要在生产 admin 补齐当前 Windows 发布版本的 `size_bytes` ,并验证 `GET /api/v1/client/releases/latest?platform=windows` 返回正整数 `release.size_bytes` 。
2026-07-16 11:38:17 +08:00
## 2026-07-16 文档登记:T-619 多张图片理解并返回文字
- 状态:TODO(仅任务登记,未改代码或数据库)。
- 背景:现有对外能力只有生成标题与生成图片;标题接口虽然可带单张图片,但返回语义固定为标题,不能作为通用的单图 / 多图理解接口。当前 Provider 已有部分 `vision` 能力声明,但生成核心、序列化器和公开操作类型仍只支持单个图片输入与 `title` / `image` 。
- 文档变更:
- `docs/06-tasks.md` :新增 T-619「多张图片理解并返回文字」,状态为 TODO,并登记 M15。
- `docs/current-state.md` / `docs/00-ai-start-here.md` :把 T-619 设为下一项可领取任务,并明确当前尚未实现。
- 方案口径:
- 新增独立 `vision` 操作与建议默认别名 `vision-standard` ,不复用标题别名,不向调用方暴露具体上游模型。
- 新增同步 `POST /api/v1/analyze/images` ;请求使用有序 `images` 列表,每项二选一提供 `image_url` 或 `image_base64` ,返回普通文字 `text` 。
- 第一版按一次请求固定扣点,并通过 `VISION_MAX_IMAGES=8` 、`VISION_MAX_IMAGE_BYTES=10485760` 、`VISION_MAX_TOTAL_BYTES=33554432` 限制图片数量、单图大小和总大小;只审核 prompt,不声称已做图片内容审核。
- 多图上游调用必须扩展既有 Provider 和 T-613 共享生成 core;图片 URL 必须复用现有 SSRF 防护,失败复用计费层幂等退点。
- 本任务不包含流式、异步 vision task、OCR 专用接口或结构化 JSON 输出;OCR、商品识别和图片对比先通过 prompt 表达。
- 验证:`./init.ps1` 通过,Python 3.12.3,依赖已满足,`manage.py check` 0 issues;任务登记完成后继续执行文档 diff 检查。
2026-07-16 14:13:19 +08:00
## 2026-07-16 开工:T-619 多张图片理解并返回文字
- 状态:DOING。
- 基线:执行 `./init.ps1` 通过,Python 3.12.3,依赖已满足,`manage.py check` 0 issues。
- 范围:按 `docs/06-tasks.md` 新增独立 `vision` 操作、多图理解同步接口、Provider 多图支持、固定单次计费、目录展示和输入资源限制;不改旧标题、生图和异步生图契约。
- 工作区说明:开工时存在用户侧 `.gitignore` 修改、已跟踪测试脚本 / 提示词删除和临时截图,均不属于 T-619,不恢复、不覆盖、不纳入本任务。
## 2026-07-16 实施:T-619 多张图片理解并返回文字
- 状态:DONE。
- 代码变更:
- `ModelAlias.OperationType` 、`CallRecord.OperationType` 与 `PricingRule` 可选操作新增 `vision` ,生成并应用 `ai.0005` / `billing.0008` 迁移。
- `apps.ai.aliases` 把能力要求统一为集合;`vision` 模型和 Provider 必须同时支持 `text` 、`vision` ,图片生成 / 编辑专用 Provider 不可误接。
- Provider 协议新增 `analyze_images()` 与 `MultimodalImage` ; Chat Completions、Gemini 按请求顺序发送多张图片并解析完整文字结果。
- 新增 `POST /api/v1/analyze/images` ,支持有序 URL/Base64 混合图片;prompt 审核先于图片读取,URL 复用现有 SSRF 防护;数量、单图和总大小受三个 `VISION_*` 环境变量限制。
- 多图理解复用 T-613 生成核心和 billing 预扣 / 成功确认 / 幂等退款;每次请求按 `vision + alias` 默认价固定扣一次,调用记录只保留最多 500 字文字摘要,不保存输入图片或 provider raw。
- `GET /api/v1/models` 与 portal 可用模型页支持展示能力匹配的 active `vision` 别名,并返回 / 显示 `requires_image=true` 与计费状态。
- 文档变更:同步 `README.md` 、`docs/00-ai-start-here.md` 、`02-requirements.md` 、`04-architecture.md` 、`06-tasks.md` 、`api.md` 、`current-state.md` 、`env.md` 、`routes.md` 与 `.env.example` ; T-619 标记为 DONE。
- 验证:
- `./init.ps1` 开工和收尾均通过;收尾为 Python 3.12.3、依赖已满足、Django system check 0 issues。
- `py -3.12 -m py_compile ...` : T-619 相关 Python 文件通过。
- `py -3.12 manage.py migrate --noinput` :通过,已应用 `ai.0005` 与 `billing.0008` 。
- `py -3.12 manage.py check` :通过,0 issues; `py -3.12 manage.py makemigrations --check --dry-run` :通过,No changes detected。
- Provider 专项:10 tests OK; T-619 API / 别名 / 目录专项:13 tests OK;旧标题 / 生图关键回归:5 tests OK。
- `apps.ai.tests + ModelsCatalogApiTests + GenerateApiTests + portal 模型页` 扩展回归:84 tests OK。
- `apps.users.tests + apps.billing.tests + apps.moderation.tests` 分组回归:46 tests OK。
- 完整单命令回归运行 604 秒后达到执行器超时,未取得最终结果;随后 `apps.api.tests` 全量 88 条测试重试两次,均在测试库初始化阶段因远程 MySQL `43.128.3.240` 握手超时中断,未进入断言。同期 TCP 3306 探测可达,按环境稳定性问题记录,不把该两次运行记为通过。
- `git diff --check` :通过,仅 Windows CRLF 提示。
- 生产启用提醒:部署代码并执行 migrate 后,需在 admin 配置同时具有 `text` 、`vision` 能力的 active `AiModel` 、`vision-standard` active 默认别名和 `operation_type=vision` 的默认 `PricingRule` ;本任务不写入真实模型、URL、密钥或价格。
- 工作区说明:用户侧 `.gitignore` 修改、`xiaxiuxiu.py` / `图生图提示词.txt` 删除及 `.tmp-t609-layout.png` 未跟踪文件保持原状,未作为 T-619 内容处理。
- 数据库恢复后的补充验证:
- 新增 `GenerateApiTests.test_analyze_images_supports_single_image_with_explicit_alias` ,补齐单图 + 指定别名成功矩阵;单独运行 1 test OK。
- API 鉴权 / 余额 / 模型目录 / 客户端版本分组:28 tests OK。
- API 充值回调 / 下单状态分组:13 tests OK。
- `apps.portal.tests` 全量:39 tests OK。
- 结合此前 `GenerateApiTests` 整类 47 tests OK 与新增单测,当前 API 89 条测试均有通过证据;所有应用已通过拆分方式完成完整回归。完整单命令超时仍作为远程 MySQL / 执行器稳定性现象保留,不再构成测试覆盖缺口。
2026-07-17 15:22:51 +08:00
## 2026-07-16 生产启用:T-619 多张图片理解
- 目标:`185.216.248.75` ,域名 `cm.833729.com` ,发布版本 `707941a` 。
- 备份:
- `/root/cmhub_deploy_backups/cmhub-before-t619-20260716174234.tar.gz` :排除 media/staticfiles 的代码与 `.env` , root-only。
- `/root/cmhub_deploy_backups/cmhub-config-before-t619-20260716174234.tar.gz` : Nginx、systemd、MySQL 8.4 配置,root-only。
- `/root/cmhub_deploy_backups/cmhub-db-before-t619-20260716174234.sql.gz` :MySQL 8.4 逻辑备份,已重新压缩并通过 `gzip -t` 。
- 部署:
- 本地 `main` 推送到远程后,以 `git archive 707941a` 生成发布包;SHA256 为 `ccf111ae126cf3274dfdc060d5ad742a13f565fd19cd781736614840668a15b9` 。
- 服务器无 Git 工作副本,使用 staging + `rsync --delete` 同步跟踪文件;明确排除 `.env*` 、media、staticfiles、`.secrets` 、`.well-known` 与部署标记。
- `manage.py check` 0 issues;生产已应用 `ai.0005` 、`billing.0008` 。仅保留 allauth 在 MySQL 条件唯一约束上的既有 `models.W036` 警告。
- `.env` 显式配置 `VISION_MAX_IMAGES=8` 、`VISION_MAX_IMAGE_BYTES=10485760` 、`VISION_MAX_TOTAL_BYTES=33554432` 。
- Nginx HTTP / HTTPS server 均新增 `/api/v1/analyze/images` 精确路由,转发到 `127.0.0.1:8012` ,连接超时 30 秒、发送/读取超时 900 秒;`client_max_body_size=168m` 保持不变。`nginx -t` 通过。
- 依次 reload Nginx、重启 `cmhub-generate` 、验证 8012,再重启 `cmhub-web` ; 66 个 `cmhub-image-worker@*.service` 未重启,避免打断排队生图任务。
- 运营配置:
- 复用 `AiModel(id=7, GPT-5.5 文本, api_type=chat)` ,其 active capabilities 为 `text + vision` 。
- 新建 active 默认别名 `vision-standard` ,并写 `AiConfigAuditLog` 。
- 新建 active 默认计费规则 `operation_type=vision + alias=vision-standard + resolution=""` ,价格为 1 点/次。
- 真实验收:
- 使用临时生产验收 API Key 提交两张相同截图,调用 `POST https://cm.833729.com/api/v1/analyze/images` 。
- 返回 HTTP 200、`model_used=gpt-5.5` 、`points_cost=1` 、`call_id=6096` 和非空中文理解文字。
- 余额 234→233; `CallRecord` 为 `vision/success` ; consume 1 条、refund 0 条;结果摘要长度 258,不保存输入图片。
- `cmhub-generate` access log 确认请求由长请求池处理;临时验收 Key 已吊销。
- 收尾状态:`cmhub-web` 、`cmhub-generate` 、Nginx、MySQL 8.4 均 active,无 failed systemd unit;公网首页返回 200。
## 2026-07-17 文档登记:T-620 图生图支持单图 / 多图主图与参考图
- 状态:TODO(仅任务登记,未改代码、数据库或线上配置)。
- 背景:当前 cmhub 图生图链路的请求模型、生成核心和 Provider 都只传递单张输入图;中转站 `/v1/images/edits` 已通过直连测试确认可以接收按顺序重复的 multipart `image` 字段并返回图片,但 cmhub 尚未把该能力暴露给调用方。业务需要约定第一张为主图,第二张及之后为参考图。
- 文档变更:
- `docs/06-tasks.md` :新增 T-620「图生图支持单图 / 多图主图与参考图」,状态为 TODO,并登记 M16。
- `docs/00-ai-start-here.md` 、`README.md` 、`docs/current-state.md` :将 T-620 登记为下一项任务。
- 方案口径:
- 同步 `/api/v1/generate/image` 与异步 `/api/v1/generate/image/tasks` 增加有序 `images` 列表,同时保留旧单个 `image_url` / `image_base64` 字段;新旧输入不能混用。
- cmhub 服务端固定注入主图 / 参考图规则,`images[0]` 为主图、`images[1:]` 为参考图;用户 prompt 仍先审核,固定规则随有效 prompt 发送给中转站。
- Provider 按协议传递完整有序图片集合:Chat 多段图片、JSON `image_urls` / `images` 数组、`images_edits` 重复 `image` multipart 字段;不得只发第一张或拼接图片。
- 异步任务新增有序输入文件存储,旧单输入任务保持读取兼容;一次请求只扣一次点,失败 / 重试 / 僵任务继续沿用现有幂等退点。
- 输入数量、单图和总大小、SSRF、敏感词、无定价等校验均在预扣前完成;不保存 base64、输入图片内容或 provider raw。
- 中转站直连验收证据:使用 `gpt-image-2` 、两张本地图片调用 `/v1/images/edits` , HTTP 200,约 73 秒返回 `data[0].b64_json` ,解码后图片约 2.04 MB;输出保存于工作区外的 `D:\chengma\cmhub-relay-tests\multi-images-edits-result.bin` 。该结果确认重复 `image` 字段可被接受,但主图 / 参考图语义仍由 T-620 的服务端固定 prompt 和真实两张明显不同图片验收确认。
- 验证:任务登记完成后执行文档 diff 检查;未运行代码测试,因为本轮未改实现。
2026-07-17 15:55:38 +08:00
## 2026-07-17 完成:T-620 图生图支持单图 / 多图主图与参考图
- 接口与输入:`POST /api/v1/generate/image` 、`POST /api/v1/generate/image/tasks` 新增有序 `images` 。每项必须且只能给出 `image_url` 或 `image_base64` ;旧单图字段继续兼容,但不得与 `images` 混用。图片数量、单图和总大小分别由 `IMAGE_MAX_INPUT_IMAGES=8` 、`IMAGE_MAX_INPUT_IMAGE_BYTES=10485760` 、`IMAGE_MAX_INPUT_TOTAL_BYTES=33554432` 控制。
- 角色语义:用户 prompt 先经既有敏感词审核;只要有输入图片,服务端才追加不可由调用方覆盖的规则,明确第 1 张为主商品图、第 2 张及之后仅为参考图。一次请求仍按既有 image 别名 / 分辨率预扣一次,校验、审核、SSRF、下载与解码都在预扣前;失败、重试与僵任务退款继续复用原有幂等链路。
- Provider: `MultimodalImage` 增加文件名,生成接口增加有序 `images` 参数。Chat Completions 按顺序生成图片内容段;Gemini 按顺序生成 inlineData; JSON 图片 Provider 发送有序 `image_urls` ; `images_edits` 多图使用重复的 multipart `image` 字段,单图仍保持原请求形状。
- 异步:新增 `api.0003_image_generation_task_input` 与 `ImageGenerationTaskInput(task, ordinal, image, mime_type, filename)` 。提交时只保存解码后的输入文件和非敏感元数据,不保存 base64 原文;worker 按序恢复所有输入。没有子记录时仍读取历史任务的 `input_image` 。
- 安全回归:完整 `GenerateApiTests` 首次运行 53 条时发现旧 `image_url` 会因新多图单图限制而绕过 `IMAGE_URL_MAX_BYTES` 。已修复为 URL 下载取既有 URL 上限与请求上下文单图上限中的较小值,恢复旧接口保护语义。
- 文档:同步更新 `README.md` 、`00-ai-start-here.md` 、`02-requirements.md` 、`04-architecture.md` 、`api.md` 、`routes.md` 、`env.md` 、`deployment.md` 、`current-state.md` 与任务看板,明确多图请求格式、主图 / 参考图语义、异步存储和 Nginx 请求体配置。
- 验证:`C:/Python312/python.exe -m compileall -q apps config` 、`manage.py check` 、`manage.py makemigrations --check --dry-run` 通过;迁移 `api.0003_image_generation_task_input` 已应用到当前开发库。Provider 测试 13 条通过;序列化器多图、空数组与混用边界通过;目标 API 6 条通过(同步多图、URL+base64 混合、混用拒绝、超限拒绝、异步存储 / worker 恢复、旧 URL 上限)。完整 53 条 `GenerateApiTests` 的修复后重跑受远程 MySQL 测试库耗时影响超过 5 分钟未完成,未出现新的失败输出;此前完整运行唯一失败已修复并由目标回归用例确认。中转站两图 `images/edits` 重复 multipart `image` 直连验收此前返回 HTTP 200 和有效图片,作为真实上游多文件传输证据。
2026-07-18 15:00:04 +08:00
## 2026-07-18 文档登记:T-621 注册赠点运营后台配置
- 背景:当前新用户赠点由 `grant_signup_bonus(points=100)` 的代码默认值决定,既不读取 `.env` ,也不能在 django-admin 调整。运营需要在不重启服务的前提下管理未来新注册用户的试用额度。
- 决策:新增数据库单例 `SignupBonusPolicy` ,默认启用并赠送 100 点;admin 只能编辑,不允许新增或删除。配置改动须写独立只读审计记录,避免将 Django `LogEntry` 作为唯一业务审计。
- 账务口径:赠点服务在同一事务内读取策略、锁钱包并创建既有 `SignupBonusGrant(user UNIQUE)` 。策略停用时仍写 `points_granted=0` 的决定记录、不写零额流水,防止注册回调重试或后续重新开启策略对同一用户补发;历史用户的已发点数、流水和余额不回算。
- 任务文档:`docs/06-tasks.md` 新增 T-621 和 M17; `docs/current-state.md` 更新为下一可领取任务。未改代码、数据库或线上配置。
## 2026-07-18 评审修订:T-621 卡片按设计评审优化
- 背景:对 T-621 卡片做实现前评审,发现并发锁语义、约束设计和若干实现细节可优化,已把结论落回 `docs/06-tasks.md` 的 T-621 描述。未改代码 / 数据库。
- 并发锁(重要):原措辞「配置修改和注册赠点行锁序列化」会导致对策略单例行 `pk=1` 加排他锁、把所有注册全局串行化。修订为:策略只做只读快照(不 `select_for_update` ),单用户幂等由 `SignupBonusGrant(user UNIQUE)` + 钱包行锁保证,不同用户注册不相互阻塞。
- 约束简化:`points` 由「启用 1..10000 / 停用 0..10000」两套条件约束,简化为 DB 统一 `0 <= points <= 10000` ,「启用需 >= 1」放表单 / `clean()` 层校验;停用时 `points` 不参与发放。
- 实现细节明确:`grant_signup_bonus()` 移除 / 忽略 `points` 入参(一律以策略为准,避免双真相源);停用 `points=0` 路径必须绕过 `_validate_positive_points` (其拒绝 `<= 0` ,否则连带回滚用户创建)。
- 可读性:`SignupBonusGrant` 现含赠 0 决定记录,`verbose_name` / 列表展示澄清为「注册赠点处理记录(含赠 0)」;`points` 上限 `10000` 在 `help_text` 注明口径来源。
- 测试补充:停用 `points=0` 不被正数校验误杀;不同用户并发注册不因策略读取相互串行 / 阻塞。
## 2026-07-18 决策:T-621 移入 Backlog,注册赠点调整为 10 点
- 决策:T-621「注册赠点运营后台配置」不再作为可领取任务,移入 `docs/06-tasks.md` 的 Backlog;不新增 `SignupBonusPolicy` 、审计表或 admin 配置,当前额度继续由代码默认值控制。
- 口径:后续新注册用户一次性赠送 **10 点** ;已注册用户的余额、注册赠点记录与流水不回算、不补发、不扣回。
- 实现:`grant_signup_bonus()` 默认值、`SignupBonusGrant.points_granted` 默认值、用户端注册/首页文案和相关 billing / portal 测试同步改为 10;新增 `billing.0009_signup_bonus_default_ten` ,仅更新后续插入记录的数据库默认值。
- 安全:仍复用既有 `SignupBonusGrant(user UNIQUE)` 数据库级幂等标记和 billing 事务写账;此次只调整额度,不改变注册接入、钱包锁、流水或 API 契约。
- 本地验证:`C:/Python312/python.exe manage.py migrate --noinput` 已应用 `billing.0009_signup_bonus_default_ten` ; `manage.py check` 0 issues; `makemigrations --check --dry-run` 无待生成迁移;`compileall` 通过。定向 MySQL 测试 5 条通过:服务默认发放 / 已有钱包累加 / 并发仅发一次 / 注册 adapter 写余额与流水 / 首页匿名文案。测试仅出现既有 allauth 条件唯一约束在 MySQL 上不创建的 `models.W036` 警告。
2026-07-18 15:09:22 +08:00
- 线上部署:已部署代码提交 `83fa541` 至 `185.216.248.75:/www/wwwroot/cmhub` ,同步时保留 `.env` 、媒体、静态文件和部署配置,并创建服务器侧配置/媒体备份。部署前确认无 `queued` / `running` 图片任务;`cmhub-web` 、`cmhub-generate` 及 66 个 `cmhub-image-worker@*` 均已重启并为 `active` ,无 cmhub / Nginx failed unit。线上已应用 `billing.0009_signup_bonus_default_ten` ,迁移记录为已登记,`manage.py check` 通过;运行时代码和模型字段默认值均为 10,HTTPS 首页返回 200。
2026-07-20 08:58:34 +08:00
## 2026-07-20 文档登记:T-622 图片生成任务后台图片缩略预览
- 状态:TODO,仅登记任务,未改代码、数据库、接口、计费、worker 或线上服务。
- 范围:在 django-admin 的 `ImageGenerationTask` 详情页复用已存的 `ImageGenerationTaskInput` (主图与参考图)及 `result_url` ,展示稳定尺寸缩略图;双击后以后台模态窗口查看大图。
- 边界:不改 `CallRecord` ,不为旧同步生图补建任务或补存输入图,不新增公开接口或媒体访问策略;列表页不加载缩略图,避免扩大查询和带宽开销。
## 2026-07-20 完成:T-622 图片生成任务后台图片缩略预览
- 实现:`ImageGenerationTaskAdmin` 使用专用 change form 模板,仅在详情页渲染缩略画廊。多图输入按 `ordinal` 标识第 1 张主图及后续参考图;无子输入时回退展示历史 `input_image` ;成功任务有 `result_url` 时展示生成结果。缩略图双击打开原生 `<dialog>` 大图预览,支持关闭按钮、点击遮罩和 `Escape` ;缩略图或大图加载失败均有前端降级提示。
- 边界:未修改模型、迁移、`CallRecord` 、API 契约、计费、worker 或媒体访问策略;任务列表不渲染缩略图。新增静态 CSS / JS 仅由任务详情模板加载。
- 验证:`C:/Python312/python.exe manage.py check` 通过;`manage.py test apps.api.tests.ImageGenerationTaskAdminTests --keepdb --noinput --verbosity 2` 通过,4 tests OK(多图+结果、历史单图、无图片任务、非 staff);`makemigrations --check --dry-run` 无待生成迁移;`findstatic` 找到 CSS / JS; `./init.ps1` 通过;`git diff --check` 通过。目标测试仅有既有 allauth MySQL 条件唯一约束 `models.W036` 警告。
- 线上部署:当前未提交的 T-622 工作区代码已同步到 `185.216.248.75:/www/wwwroot/cmhub` ,同步保留 `.env` 、媒体、静态文件与部署配置,并创建部署前配置/静态文件备份。线上 `manage.py check` 通过,`collectstatic --noinput` 新复制 7 个静态文件,`cmhub-web` 重启后为 `active` ; `cmhub-generate` 未重启,未影响 66 个生图 worker。已确认任务详情模板及 `staticfiles/api/admin/image-task-gallery.css` 、`image-task-gallery.js` 存在。
2026-07-20 08:59:12 +08:00
## 2026-07-20 文档登记:T-623 图片生成任务单图 / 多图筛选
- 状态:TODO,仅登记任务,未改代码、数据库、接口、计费、worker 或线上服务。
- 范围:在 django-admin 图片生成任务列表右侧增加“输入图片类型”筛选,按已有子输入图数量识别单图 / 多图,并兼容历史 `input_image` 单图任务;无输入图任务不归入这两类。
- 边界:使用数据库聚合筛选,不在 Python 遍历任务;不改任务详情缩略图、媒体文件、模型、迁移或对外接口。
2026-07-20 09:03:23 +08:00
## 2026-07-20 完成:T-623 图片生成任务单图 / 多图筛选
- 实现:`ImageInputTypeFilter(SimpleListFilter)` 加入 `ImageGenerationTaskAdmin.list_filter` 。筛选通过 `Count("input_images")` 生成数据库子查询:1 条子输入为新单图、2 条及以上为多图;无子输入但旧 `input_image` 有值的任务作为兼容单图;无输入任务不返回。结果仍为原 queryset,可与状态 / 日期过滤叠加,不在 Python 层遍历或去重。
- 验证:`C:/Python312/python.exe manage.py check` 通过;`manage.py test apps.api.tests.ImageGenerationTaskAdminTests --keepdb --noinput --verbosity 2` 通过,6 tests OK(T-622 画廊回归、新/旧单图、多图、无输入排除、状态组合、权限);`makemigrations --check --dry-run` 返回 No changes detected,期间开发库 `43.128.3.240` 出现一次既有连接超时 `WinError 10060` 警告;`./init.ps1` 通过;`git diff --check` 通过。
2026-07-21 09:27:05 +08:00
## 2026-07-21 完成:T-624 蝦皮圈设备登记与会话观测
- 实现:新增 `apps/licensing` ,通过 `ClientDevice` 、`DeviceSession` 、`DeviceBindingAudit` 记录设备、短期会话和审计;新增 `licensing.0001_initial` 。设备登记 `POST /api/v1/client/devices/register` 继续使用 API Key,返回仅本次展示的设备会话令牌;心跳 `POST /api/v1/client/devices/heartbeat` 使用 `X-Device-Session` 。设备标识使用服务端 pepper HMAC 摘要、安装公钥和会话令牌仅存 SHA-256 hash;重复登记复用设备且轮换会话,活跃写入默认按日节流。
- 兼容性:本任务没有修改现有生成、余额、模型目录、异步任务、点数账本或通用 API Key 鉴权。设备会话暂不参与生成授权,旧客户端不传设备头继续正常调用。
- 文档:同步 `api.md` 、`routes.md` 、`04-architecture.md` 、`env.md` 、`current-state.md` 和任务状态;新增生产需设置的 `DEVICE_IDENTIFIER_PEPPER` 、`DEVICE_SESSION_TTL_SECONDS` 、`DEVICE_ACTIVITY_UPDATE_SECONDS` 。
- 验证:本地执行 `py -3.12 manage.py migrate licensing --noinput` ,应用 `licensing.0001_initial` ; `py -3.12 manage.py test apps.licensing apps.api.tests.ApiKeyAuthenticationTests apps.api.tests.BalanceApiTests --keepdb --noinput --verbosity 2` 通过,21 tests OK; `py -3.12 manage.py makemigrations --check --dry-run` 无变化;`py -3.12 manage.py check` 通过;`./init.ps1` 通过。测试保留既有 allauth MySQL 条件唯一约束 `models.W036` 警告。
- 下一步:领取 T-625,将有效设备会话关联到调用记录和遥测,但继续保持只观测、不阻断。
2026-07-21 09:40:40 +08:00
## 2026-07-21 完成:T-625 蝦皮圈设备使用关联与迁移观测
- 实现:`CallRecord` 新增可空 `client_device` 外键和 `(client_device, created_at)` 索引,迁移为 `billing.0010_callrecord_client_device_and_more` 。标题、图片、异步图片提交和图片理解在 API Key 鉴权后可选解析 `X-Device-Session` ,仅把服务端验证的设备传给既有预扣服务写入调用记录;异步 worker 不重复解析会话,既有任务轮询保持 API Key 所属用户校验。
- 安全与兼容:未携带设备会话的调用保持既有响应、账务和任务语义。显式提供伪造或过期会话返回 `401 device_session_invalid` ,吊销设备或跨账号会话返回 `403` ,均发生在审核/预扣/上游调用前。`generation_route_usage` 只新增 `product_code` 、内部 `client_device_id` 和 `device_session_present` ,不记录原始设备 ID、公钥、令牌、API Key、prompt 或图片。
- 后台与文档:`CallRecordAdmin` 增加只读设备展示与筛选;同步 `api.md` 、`routes.md` 、`04-architecture.md` 、`current-state.md` 和任务看板,明确本期只做关联观测,不实施订阅授权拦截。
- 验证:本地已应用 billing 迁移;新增 4 条测试覆盖四个生成入口关联、无头兼容、伪造/跨账号会话拒绝、遥测脱敏和异步轮询。`py -3.12 manage.py test` 定向新增 4 条通过;随后 licensing 加关键标题/vision/同步图片/异步提交/遥测回归共 14 条通过;`manage.py check` 和 `makemigrations --check --dry-run` 通过。测试仅保留既有 allauth MySQL 条件唯一约束 `models.W036` 警告。
- 下一步:领取 T-626,建立套餐、权益、席位和审计的基础模型与后台,不接入生成授权或购买流程。
2026-07-21 09:51:29 +08:00
## 2026-07-21 完成:T-626 软件套餐、权益与设备席位基础模型
- 实现:`apps.licensing` 新增 `SoftwarePlan` 、`SoftwareEntitlement` 、`LicenseSeat` 、`LicenseEvent` ,迁移为 `licensing.0002_softwareentitlement_licenseseat_licenseevent_and_more` 。授予权益时复制套餐名称、价格、有效天数、设备数和宽限期,并预创建固定数量的席位;套餐后续编辑不会反向修改历史权益。
- 服务与后台:新增事务服务 `grant_software_entitlement()` 、`renew_software_entitlement()` 、`revoke_software_entitlement()` 、`assign_license_seat()` 、`release_license_seat()` 。每个人工操作均要求非空原因并写不可变授权事件;续期按 `max(now, expires_at)` 延长,席位分配锁权益和席位行。django-admin 支持套餐维护和权益专用授予/续期/撤销页,权益、席位、事件不允许直接编辑。
- 边界:本任务不创建购买页、软件订单、支付回调、存量迁移请求/设备凭证,也不改变通用生成 API 或点数账本。
- 验证:本地已应用 licensing 迁移。`py -3.12 manage.py test apps.licensing --keepdb --noinput --verbosity 2` 通过 14 条测试,包含套餐快照、续期、撤销、审计、后台权限和两个独立数据库连接并发抢占单席位;`manage.py check` 与 `makemigrations --check --dry-run` 通过。测试仅保留既有 allauth MySQL 条件唯一约束 `models.W036` 警告。
- 下一步:领取 T-627,实施存量迁移权益、网页登录确认和产品专用设备凭证,继续保持通用生成 API 兼容。
2026-07-21 10:06:51 +08:00
## 2026-07-21 完成:T-627 存量用户迁移权益、网页确认与设备凭证
- 实现:新增 `LegacyMigrationGrant` 、`MigrationRequest` 、`DeviceCredential` 与 `licensing.0003_alter_licenseevent_action_legacymigrationgrant_and_more` 。迁移资格只能由 admin 专用入口显式按用户和套餐授予,保存资格快照;不接注册流程,因此新注册用户不会自动获得迁移权益。
- 流程:客户端以旧 API Key + 当前 `X-Device-Session` 调用 `POST /api/v1/client/migration-requests` ,获得短时确认链接和一次性 `device_credential_token` 。数据库只保存待签发凭证 hash/前缀。portal 同账号确认后在事务内分配席位、签发绑定到迁移请求的凭证;重复确认与轮询返回既有状态,不重复占席或签发。用户可在 `/migration/devices` 自助解绑,服务会吊销凭证、释放席位并写授权事件。
- 安全与兼容:跨账号确认返回 403,缺设备会话返回 401,跨设备查询按 404 处理;确认 URL 不含凭证明文。旧生成、余额、模型目录、异步任务和点数账本未改,设备凭证尚未参与生成授权。
- 验证:本地已应用 licensing 迁移。T-627 迁移专项 3 条测试通过,覆盖资格边界、缺会话、跨账号、过期/重复、确认/轮询、凭证 hash 和自助解绑;此前 T-626 14 条 licensing 基础回归通过。`manage.py check` 与 `makemigrations --check --dry-run` 通过;测试仅保留既有 allauth MySQL 条件唯一约束 `models.W036` 警告。
- 下一步:领取 T-628,建立蝦皮圈专属调用入口与影子授权校验,不影响通用 `/api/v1/generate/*` 。
2026-07-21 10:13:26 +08:00
## 2026-07-21 完成:T-628 蝦皮圈专属授权入口与影子校验
- 实现:新增蝦皮圈专属 title、vision、异步图片提交和任务读取路由。提交路径复用已有视图、生成 core 与点数账本;通用 `/api/v1/generate/*` 未修改,已接受任务读取仍只按 API Key 所属用户检查。
- 授权:新增统一设备授权判定,检查当前设备会话、产品凭证、用户、设备、席位和权益有效期。专属入口只写 `cmhub.licensing.authorization` 脱敏事件,记录内部 ID、`would_reject` 和结果码,不记录凭证、API Key、prompt 或图片;本期不阻断请求。
- 配置:增加 `CMSHOPEE_AUTHORIZATION_SHADOW_MODE=true` ,当前仅作为影子部署口径与日志字段;正式执行阻断需单独任务、灰度和回滚验收。
- 验证:凭证正确、无设备、无凭证和过期的判定测试已覆盖;T-627 迁移专项 4 条回归、`manage.py check` 与 `makemigrations --check --dry-run` 通过。
- 下一步:领取 T-629,建立与点数充值订单隔离的软件套餐购买、续订订单和幂等权益入账。
2026-07-21 11:14:53 +08:00
## 2026-07-21 评审:T-624~T-628 静态代码复核
- 范围:`apps/licensing/` 全模块、`apps/api/views.py` 授权入口、`apps/api/telemetry.py` 、`apps/api/urls.py` 、`apps/licensing/admin.py` 。因当前 WSL 环境未安装 Python/Django,未能实际运行测试与 `check` ,本次为静态代码审查,卡片自述的测试通过情况未独立复验。
- 结论:五张卡与实现一致,判定达标。设备与公钥只存 HMAC/SHA256 摘要,会话与凭证只存 hash;重复登记复用设备并轮换旧会话;遥测字段为白名单,未泄露摘要、令牌、Key、prompt;401/403 映射与卡片一致;专属入口以 Mixin + 独立路由隔离,通用 `/api/v1/generate/*` 未受影响。
- 发现①(须在 T-629 前修):`renew_software_entitlement` 不刷新已签发 `DeviceCredential.expires_at` 。凭证有效期在签发时钉死为当时的 `grace_expires_at` ,续费后不随权益延长,用户会在旧宽限期后被判 `license_expired` 。当前影子模式不阻断故未暴露,切强制即影响付费用户。已写入 T-626 遗留与 T-629 前置修复。
- 发现②:`revoke_software_entitlement` 不吊销凭证也不释放席位,裁决层由 `is_usable_at()` 兜底,属数据残留非安全问题。已随①一并登记到 T-629。
- 发现③:`CMSHOPEE_AUTHORIZATION_SHADOW_MODE` 仅写入日志,无任何分支依据它拒绝请求。符合阶段 2 预期,但已在 T-628 卡片留档,提醒切强制需要改代码而非改配置。
## 2026-07-21 开始:T-629 软件套餐购买、续订订单与权益入账
- 先收紧任务口径:软件订单、支付回调与权益发放必须和 `RechargeOrder` / `PointsLedger` 隔离;支付交易号在软件订单域内唯一,并可追溯至目标权益和授权事件。
- 第一版只支持用户主动月度续订,不实现自动代扣,也不实现支付退款后的自动权益回收;退款与撤销走带订单号和原因的运营人工流程。自动回收须在未来以按订单权益周期/发放账本为基础单列任务,不能直接回滚累计到期时间。
- 实现顺序:先修 T-626 的凭证续期同步和撤销清理,再实现独立软件订单、独立回调/查单、portal 与 HTTP/并发回归测试。
2026-07-21 11:52:49 +08:00
## 2026-07-21 完成:T-629 软件套餐购买、续订订单与权益入账
- 实现:新增 `SoftwareOrder` 、`LicenseEvent(order_fulfilled)` 和 licensing `0004` 迁移;软件订单锁定套餐/金额快照,支付交易号按通道唯一,并关联目标权益、发放事件和发放时间。支付成功仅创建或续订 `SoftwareEntitlement` ,不写 `RechargeOrder` 、`UserWallet` 或 `PointsLedger` 。
- 支付:支付网关抽取无业务副作用的 `PaymentReceipt` ,充值继续将其交给 `apply_recharge_payment()` ;订阅改用独立的微信回调/状态路由和 `SOFTWARE_WECHAT_PAY_NOTIFY_URL` ,避免软件支付进入点数充值回调。portal 新增 `/subscription` , admin 新增只读软件套餐订单。
- T-626 修复:续期在事务内刷新全部未吊销设备凭证到新的宽限截止;撤销权益会吊销未吊销凭证、释放已占席位并分别写审计事件。
- 验证:`py -3.12 manage.py test apps.licensing.tests.SoftwareOrderServiceTests apps.licensing.tests.SoftwareOrderConcurrencyTests apps.licensing.tests.LegacyMigrationFlowTests apps.api.tests.SoftwareOrderCallbackApiTests --keepdb` 通过(12 tests);`py -3.12 manage.py test apps.billing.tests.BillingServiceTests apps.api.tests.RechargeCallbackApiTests apps.api.tests.RechargeCreateStatusApiTests --keepdb` 通过(31 tests);`py -3.12 manage.py check` 、`py -3.12 manage.py makemigrations --check --dry-run` 和 `compileall` 通过。两组均仅有既有 allauth MySQL 条件唯一约束 `models.W036` 警告。完整 `manage.py test --keepdb` 在远端 MySQL 测试库运行 10 分钟无失败输出后超时,未记为通过;线上真实微信付款验收仍未执行。
2026-07-21 16:21:43 +08:00
## 2026-07-21 规划:T-630 账号订阅授权与默认多设备模式
- 需求变更:保留软件会员订阅,取消卡密和固定电脑绑定。同一账号默认允许多台设备使用。
- 任务边界:授权只检查 API Key 对应用户的 `SoftwareEntitlement` ;设备登记/心跳/调用关联仅作观测和风控,历史设备凭证与迁移记录保留只读,不删除、不再作为新授权依据。
- 发布策略:新增 `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=false` ,先记录账号订阅影子结果;完成多设备与存量用户回归后,才在产品专属入口开启实际拒绝。