150 lines
59 KiB
Markdown
150 lines
59 KiB
Markdown
# 任务看板(Tasks)
|
||
|
||
> 把 MVP 拆成小步、可独立交付的任务,让 AI 一步一步开发,避免一次生成整个项目。
|
||
|
||
## 使用规则
|
||
|
||
1. **一次只做一个任务**:每轮只领取一个状态为 `TODO`、且依赖均已 `DONE` 的任务,取最靠前的那个。
|
||
2. **做完即停**:完成该任务、自测通过、把状态改成 `DONE` 后,停下来汇报。
|
||
3. **不跳步**:依赖未完成的任务不能开工。
|
||
4. **完成定义**:以 [编码规则](05-coding-rules.md) 的验证清单为准。
|
||
5. **passing 需证据**:标记 `DONE` 前,必须在 [`../progress.md`](../progress.md) 记录跑过的验证命令和结果。
|
||
6. **动手前**先读 `00-ai-start-here.md`、`05-coding-rules.md` 和 `current-state.md`。
|
||
7. **完成后**同步任务状态到本文,执行记录追加到 `../progress.md`,覆盖更新 `current-state.md`,过一遍 `clean-state-checklist.md`。
|
||
|
||
## 状态图例
|
||
|
||
`TODO` 待开始 · `DOING` 进行中(同一时间最多 1 个)· `DONE` 已完成并验收 · `BLOCKED` 受阻(注明原因)
|
||
|
||
---
|
||
|
||
## Phase 0 · 地基
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-001 | 初始化 Django + DRF 项目骨架 | - | `manage.py` 可运行;`runserver` 起得来;用真实命令替换 `init.sh`/`init.ps1` 与 `00-ai-start-here.md`/`03-tech-stack.md`/`current-state.md` 的占位命令 | DONE |
|
||
| T-002 | 建立 apps 目录、自定义 User 与配置 | T-001 | 按 `04-architecture.md` 建 `apps/users|portal|billing|ai|api`;**首次迁移前定义自定义 `User` 模型(设 `AUTH_USER_MODEL`)**;settings 用环境变量读密钥、配 MySQL(utf8mb4),无明文密钥 | DONE |
|
||
| T-003 | 接通 django-admin 与最小测试 | T-001 | `createsuperuser` 后能登录 `/admin/`;`manage.py test` 可运行(至少 1 条占位测试通过) | DONE |
|
||
| T-004 | Phase 0 骨架审核修补 | T-003 | 按 [`phase-0-review.md`](phase-0-review.md) 修 **P1**(`User.email` 加 `unique`、`requires-python` 落地 + init 校验解释器版本)与 **P2**(`INSTALLED_APPS` 顺序、补 `.env.example`);同步修正根 `README.md` 当前状态;`check`/`test`/`init` 重新全绿并在 `../progress.md` 留证据。**建议先于 T-101 完成**(P1-1 趁 user 表空成本最低) | DONE |
|
||
|
||
## Phase 1 · 最高风险验证(AI 调用 + 可插拔供应商)
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-101 | Provider 适配器层 + 移植 cmbot 调用 | T-002 | 定义 `Provider` 接口(`capabilities`/`generate_text`/`generate_image`),按 `api_type` 注册;把 `ai_text_service.py`/`ai_image_service.py` 搬进 `apps/ai/providers/` 并去除桌面依赖;**注意 3 模型机制不同(chat / chat 多模态返图 nano-banana2 / images_edits 改图 gpt-image-2,见 `04` 3.1),两图片模型非标准生成需分别解析、首次对接抓真实响应**;mock 上游单测验证解析与适配器选取 | DONE |
|
||
| T-102 | AiModel + ModelAlias 模型 + 别名解析 | T-101 | AiModel 含 `capabilities`、`api_key` **加密存储**(admin 脱敏不回显);ModelAlias 映射别名→模型;`resolve_alias()` 能解析并按能力校验;配置迁移自 `ai_models.json`;后台改配置运行时热生效 | DONE |
|
||
| T-103 | 配置变更审计 | T-102 | AiModel/ModelAlias/密钥的后台变更留痕(谁、何时、改了什么);可在 admin 查看 | DONE |
|
||
| T-104 | 跑通一次真实/录制的标题或图片生成 | T-102 | 用别名 + 最小输入跑通一次生成,结论写入 `progress.md`(含耗时,验证图片同步可行性与超时配置) | DONE(管路已验;**图片同步耗时风险未退,已挂到 T-302/T-403,见 phase-1-review P1-2**) |
|
||
| T-105 | Phase 1 AI 层审核修补 | T-104 | 按 [`phase-1-review.md`](phase-1-review.md) 修 **P1**(`parameters`/`extra_body` 透传白名单化,核心/计费字段不可被调用方覆盖,含单测;T-104 结论改写为「图片同步风险未退」并把该未决风险显式登记到 T-302/T-403)与 **P2**(`.env.example` 的 `AI_DEFAULT_*` 超时键接上或删除、`resolution_to_size` 大小写归一、默认别名口径对齐);`check`/`test`/`init` 全绿并在 `../progress.md` 留证据。**P1-1 建议先于 T-302 完成**(否则易顺手 `parameters=request.data` 造成越权计费) | DONE |
|
||
|
||
## Phase 2 · 计费核心
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-201 | User / UserWallet / ApiKey / PointsLedger / CallRecord 模型 | T-002 | 表结构符合 `04-architecture.md`;`UserWallet.points_balance>=0` 约束;ApiKey **哈希存储**(key_hash+key_prefix,明文只创建时返回);CallRecord 含 `user`/`api_key`/`alias`/`model_used`;调用结果只存 `result_ref`/摘要,不 dump provider `raw`、base64 图片或敏感上游字段;admin 注册 | DONE |
|
||
| T-202 | PricingRule / ExchangeRate 模型 + 计费计算 | T-201, T-102 | **按「操作 + 能力别名(+ 可选分辨率)」定价**;换底层模型不影响计费;缺规则返回 `no_pricing_rule` | DONE |
|
||
| T-203 | 并发安全扣点 / 退点(billing 层) | T-201 | 锁 `UserWallet` 行或 F() 原子扣减;并发测试不超扣、不为负;失败退点写流水;含测试 | DONE |
|
||
| T-204 | Phase 2 计费核心审核加固 | T-203 | 按 [`phase-2-review.md`](phase-2-review.md) 处理 **P2**(`points_ledger` 加 MySQL 可落地的 `ref_call + change_type` 复合唯一约束,作为「每 `ref_call` 最多一条 REFUND」DB 兜底,**不要使用 MySQL 不支持的 partial unique / 条件唯一约束**;补直写 `IntegrityError` 测试,并确认同一调用的 CONSUME 与 REFUND 可共存;在稳定 MySQL 上跑一次完整 `test` 全绿留证,并在测试文档标注「并发测试须在 MySQL 上跑,SQLite 会假绿」);P3 已挂到 T-401/T-501 或备忘。**无 P1**,不阻塞 T-301;`check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
|
||
|
||
## Phase 3 · 对外 API 与充值
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-301 | API Key 鉴权(DRF Authentication) | T-201 | 对请求 Key 哈希比对定位 ApiKey→User;**只挂 Key 认证、不挂 Session**;无效/缺失 401;用户或 Key 禁用 403 | DONE |
|
||
| T-302 | 生成标题 / 图片接口 | T-104, T-203, T-301 | 按 `api.md` 实现;请求传**能力别名**+ `parameters`,但 `parameters` 只能经 Provider 白名单透传,核心/计费字段不可被覆盖;编排「别名解析+能力校验→预扣→调上游→成功确认/失败退点→写记录」;评估 `Provider.capabilities()` 与模型声明能力的二次校验;`images_edits` 缺原图 / `AiCapabilityError` 翻译为 400,不落 500;调用记录不保存 provider `raw` / base64;点数不足返回 402;含测试;建议随本任务或 T-403 前跑一次真实图片生成并记录真实耗时 | DONE |
|
||
| T-303 | 余额查询接口 | T-301 | 返回余额等于流水累加;含测试 | DONE |
|
||
| T-304 | 充值回调(微信/支付宝验签 + 幂等入账) | T-202 | 两端点 `@csrf_exempt`;微信 SDK 验签解密、支付宝 SDK verify;验签失败不入账;同一 order_no 重复回调只入账一次;校验回调金额与订单金额一致;使用订单创建时锁定的 `points_granted` 锁 wallet 入账写流水;补主动查单兜底;含幂等测试 | DONE |
|
||
| T-305 | 扫码充值下单 + 轮询(create/status) | T-304 | 支持 weixin(native,金额分)/alipay(precreate,金额元);建 pending 订单绑定 user,并在下单时锁定汇率/预计点数→取 code_url/qr_code→前端渲染 + 轮询 status;缺商户密钥时 mock | DONE |
|
||
| T-306 | Phase 3 对外 API 安全加固 | T-305 | 按 [`phase-3-review.md`](phase-3-review.md) 处理 **P1**(`download_image_input` SSRF:协议白名单 + 私有/回环/链路本地网段拦截含重定向后地址 + 响应大小上限,含内网/元数据 URL 被拒测试)与 **P2**(全局 `REST_FRAMEWORK` 安全默认认证 + 生成/认证端点限流 + 充值金额上限);P3 仅登记媒体访问控制、完整测试补跑和过期订单口径,充值流水 DB 兜底已在 T-304 完成不要重复迁移。**P1-1 属上线前必须**;`check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
|
||
|
||
## Phase 4 · 用户端(Django 模板 SSR)
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-501 | 注册 / 登录(allauth) | T-201 | 自助注册(**免邮箱验证、注册即可用;邮箱仍必填且唯一**)、登录、登出;注册后钱包点数为 0(不送点数);session + CSRF。**策略变更见 T-605 与 2026-07-06 决策** | DONE |
|
||
| T-502 | API Key 自助管理页 | T-501, T-301 | 登录用户生成/删除 Key;明文只显示一次、库内只存哈希;列表只显示 prefix;删除即吊销,吊销后该 Key 调用 403(无效/不存在 Key 仍为 401) | DONE |
|
||
| T-503 | 个人中心 / 记录页 | T-501, T-203 | 剩余点数、充值总额、充值记录、消费(调用)记录;数据与流水一致;仅见本人 | DONE |
|
||
| T-504 | 充值页(扫码 + 轮询到账) | T-501, T-305 | 发起充值→展示二维码→轮询订单状态→到账后余额刷新;到账以回调为权威 | DONE |
|
||
| T-505 | Phase 4 用户端审核优化 | T-504 | 按 [`phase-4-review.md`](phase-4-review.md) 处理 **P2**(P2-1 Bootstrap/qrcode 自托管或加 SRI+crossorigin,断网 jsdelivr 后充值页仍可用;P2-2 记录页真正分页或明确截断提示,含 >50 条测试);P3 登记到 T-401/T-403 或 Backlog。**无 P1,但按任务看板领取规则应先完成 T-505,再进入 T-401**;`check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
|
||
|
||
## Phase 5 · 后台与发布
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-401 | 运营后台完善 | T-302, T-304 | admin 可管理用户/钱包/ApiKey(脱敏)、配规则/汇率、检索充值订单/流水/调用记录;手工调点带原因并经计费层写流水 | DONE |
|
||
| T-402 | 完整验收 MVP | T-401, T-505 | `02-requirements.md` 的 P0 验收全部通过(含用户端注册/充值/API Key/记录) | DONE |
|
||
| T-403 | 部署 / 运行文档 | T-402 | 新环境可按文档运行;明确图片接口超时配置;上线前必须引用一次真实图片生成耗时来设置 Gunicorn `--timeout`、Nginx `proxy_read_timeout`、客户端 read timeout;若尚无真实耗时,部署文档必须显式标注图片同步风险未退,不得声称已验证;当前注册策略为免邮箱验证,注册登录不依赖 `DJANGO_EMAIL_BACKEND`,如后续启用密码找回/通知/邮箱验证再配置真实邮件服务;**生产静态文件 serving**:T-505 已把 Bootstrap/qrcode 自托管到 `apps/portal/static/`,但 `settings` 目前只有 `STATIC_URL`,`DEBUG=False` 下 Django 不发静态文件——必须配置 `STATIC_ROOT` + `collectstatic` + WhiteNoise 或 Nginx 托管 `/static/`,否则用户端 CSS 错版、充值二维码 404(P2-1 的国内可用性目标在生产失效);**DRF 限流依赖共享缓存**:多 Gunicorn worker 下默认 `LocMemCache` 是每进程的,会让 `generate`/`api_auth_failure` 限流按 worker 各算一份(实际速率≈worker 数×配置值),部署必须配置 Redis/Memcached 等共享 `CACHES` 后端并在文档说明,否则限流形同虚设 | DONE |
|
||
|
||
## Phase 6 · 增强(MVP 后)
|
||
|
||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||
| --- | --- | --- | --- | --- |
|
||
| T-601 | 可用别名发现(portal 别名页 + `GET /api/v1/models`) | T-202, T-301, T-503 | 让调用方能自助发现「能填哪些 `model` 能力别名」。① 新增 `GET /api/v1/models`(**API Key 鉴权,继承 `ExternalApiView`,只复用认证失败限流;不要占用生成接口的 `GenerateRateThrottle` 额度**),仅返回 `ModelAlias.is_active=True` 且 `ai_model.is_active=True` 的可调用别名;② 响应每项只含对外安全字段:`alias`、`operation_type`、`capabilities`、`requires_image`(按 `api_type`/provider 规则推导,如 `images_edits=true`)、`pricing_status`、`prices[{resolution, points_cost}]`,缺 `PricingRule` 时标「暂未定价」而非报错;③ **实现时不得调用 `AiModel.to_resolved_model()` 或任何会解密 provider key 的路径**,列表接口不依赖 `AI_KEY_ENCRYPTION_KEY`,也不读取/输出 `api_key_encrypted`;④ **不暴露具体 SKU/模型名/url/key/provider 原始配置**,测试必须断言响应不含 `ai_model.model`、`ai_model.url`、`api_key`、`api_key_encrypted`、`extra_body` 等内部字段;⑤ portal 加只读「可用模型」页(session 鉴权),同字段、人类可读、单价以点数展示,并在导航中加入入口;⑥ 含测试:API Key 成功返回结构、缺/无效 Key 拒绝、Web session 不能调用外部 API、portal 未登录跳转、页面不泄露 SKU/URL/Key、缺定价别名显示暂未定价。**标注**:未来接 `account_alias_permission`(见 Backlog)后,本清单改为「只列该用户被授权的别名」,接口与页面均按 `user` 过滤 | DONE |
|
||
| T-602 | django-admin 中文化(第 1-3 层) | - | 让运营后台表名/分组/框架文字显示中文,**仅显示层、不改业务逻辑与 DB 结构**。**第 1 层**:`config/settings.py` 设 `LANGUAGE_CODE='zh-hans'` + `USE_I18N=True`,使 Django 自带 admin 界面与内置 `auth`(用户/组/权限)中文化(无迁移)。**第 2 层**:各 app `AppConfig.verbose_name` 设中文分组名(`users`/`billing`/`ai`/`api`/`portal`),无迁移。**第 3 层**:各模型 `Meta.verbose_name`/`verbose_name_plural` 设中文表名(`User`/`UserWallet`/`ApiKey`/`PointsLedger`/`CallRecord`/`PricingRule`/`ExchangeRate`/`RechargeOrder`/`AiModel`/`ModelAlias`/`AiConfigAuditLog`)。**不含第 4 层字段级 `verbose_name`(字段列名保持英文,见 T-603)**。第 3 层会产出 `AlterModelOptions` 等**无 DB 变更**迁移;验收:`makemigrations`(仅 options 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 目视中文化,并在 `../progress.md` 留证据 | DONE |
|
||
| T-603 | django-admin 中文化(第 4 层·字段级) | T-602 | 给各模型**字段**加中文 `verbose_name`,使 admin 列表/编辑页的字段标签显示中文,在 T-602(1-3 层)之上做。范围覆盖 `users`/`billing`/`ai`(及 portal 相关)模型的业务字段,例如 `points_balance`→点数余额、`key_prefix`→Key 前缀、`points_delta`→点数变动、`balance_after`→变动后余额、`amount_money`→金额、`exchange_rate`→汇率、`points_granted`→到账点数、`api_type`/`capabilities`/`is_active`/`created_at` 等;`admin.py` 里自定义显示方法(`@admin.display(description=...)`)同步中文。**仅显示层**:`verbose_name` 只改展示,**不改字段名、不改代码引用、英文字段名保持不变**;产出的是 `AlterField`(metadata-only,**无 DB schema 变更**)迁移。验收:`makemigrations`(仅 AlterField,无 schema 变更)→ `migrate` → `check` 0 issues → `test` 全绿 → `/admin/` 字段标签目视中文,并在 `../progress.md` 留证据。已人工确认 admin 字段中文化,目标后台回归测试通过 | DONE |
|
||
| T-605 | 落实「免邮箱验证」策略 | T-501 | 把 `ACCOUNT_EMAIL_VERIFICATION="none"`(注册即可用、不发验证邮件、邮箱仍必填且唯一)作为**既定策略**清理落地:① `config/settings.py` 把 `ACCOUNT_EMAIL_VERIFICATION = "none"#"mandatory"` 改为干净的 `"none"`(去行内注释),并把免验证下无实际意义的 `ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION` 设为 `False`;② 更新 `apps/portal/tests.py` 里假设 mandatory 的 2 条测试(signup 不再依赖验证邮件、未验证也可直接登录),改为断言「注册后可直接登录」;③ 同步全项目文档口径(`02-requirements`/`05-coding-rules`/`api`/`04-architecture`/`03-tech-stack`/`routes`/`env`/`deployment`/`00-ai-start-here` 中「邮箱验证」→「免邮箱验证,邮箱仍唯一」)。验收:`check` 通过,`apps.portal` 26 tests OK;全量 `manage.py test` 已尝试,跑到 95/131 后因远程 MySQL 连接超时失败(WinError 10051/10060),非本任务断言失败,详见 `../progress.md` | DONE |
|
||
| T-604 | 中文敏感词本地过滤(本地 keyword provider) | T-302, T-401 | 按 [`moderation.md`](moderation.md) 实施。**范围收紧**:T-604 只做输入 prompt 的本地敏感词快筛,不做云内容安全、不做输出审核、不做图片审核。**关键时序**:serializer 后先审 prompt,命中即 `400 content_blocked`;不得先下载 `image_url`,不得预扣点,不写 `CallRecord` / `PointsLedger`,不调上游。**核心实现**:新增 `apps.moderation`、`SensitiveWord` 模型/admin/迁移、keyword provider、归一化管线、Aho-Corasick matcher;`ahocorapy` 作为候选依赖,编码前必须验证 PyPI 可用性和 API 形状。**缓存**:matcher 进程内缓存,词库变更用共享 cache 版本号失效,不能只靠 `post_save` signal;生产依赖共享 cache。**配置**:`MODERATION_ENABLED=false` 默认 no-op;启用时 `MODERATION_PROVIDER=keyword`;MVP `SensitiveWord.action` 只支持 `block`。**验收**:`check`/`test` 全绿,覆盖 no-op、命中拦截且不扣点/不建记录/不调上游/不下载图片、归一化防绕过、词库变更后 matcher 重建;真实词库数据不进仓库,`__pycache__` 不进 Git | DONE |
|
||
| T-606 | 公开首页 + 客户端下载入口 | T-501 | 给网站补「前门」并提供桌面端下载。**路由改造**:`/` 从「重定向到 `/dashboard`」改为**公开首页**(匿名可访问、不跳登录);已登录用户显示「进入控制台」,匿名显示「注册/登录 + 下载客户端」。**首页内容**(SSR 模板):项目一句话介绍(生成标题/图片、按点数计费)+ 三步上手(注册→充值→建 API Key→桌面端填 Key)+ 下载入口 + 文档链接。**`DownloadRelease` 模型**:`platform`(如 windows)、`version`、`file`(FileField,**前期存 `MEDIA_ROOT`、服务器托管**) + `external_url`(URLField 可选,**后续切对象存储/CDN 用,有则优先**)、`sha256`、`is_current`(每平台仅一个当前版本)、`release_notes`、时间戳;admin 可上传安装包并标记当前版本;迁移。**下载区块**:展示当前 release 的版本、下载按钮、**SHA256 校验值**、可选 release notes;无 current release 时优雅提示「暂未发布」。**托管策略**:**前期安装包放本服务器**(生产由 **Nginx 直接服务 media/下载文件、不走 Django**,与 T-403 static/media serving 一致,大文件不占 gunicorn worker);`external_url` 预留,后续切对象存储只改后台链接不改代码。**安全**:下载走 HTTPS,页面展示 SHA256 供校验;**代码签名**作为决策登记——未签名 Windows 安装包会被 SmartScreen 拦「未知发布者」、macOS 被 Gatekeeper 拦,首页先给「如何忽略警告」说明,正式签名后续补(挂 Backlog / deployment)。**含测试**:`/` 匿名 200 不跳登录、下载区展示当前 release、无 current release 优雅处理、已登录用户显示「进入控制台」。**视觉原型(已定 v1)**:按 `prototypes/cmhub-homepage-v1.svg` 落地——「生成台」方向:靛蓝=生成 / 琥珀=点数;Hero 为「商品图 + 一句话 → 吸睛标题 + 生成主图 + 点数计量」转化图;四步上手 01–04、两张能力卡、深色计费 band、下载区(版本/SHA256/未签名提示);配色、间距、结构照此原型转成 Django 模板(Bootstrap + 本地 static)。**视觉一致性(brand token)**:按 [`brand.md`](brand.md) 抽出共享 CSS 变量(如 `apps/portal/static/portal/brand.css`),**首页与现有 portal 页面(dashboard/记录/充值/API Key)一起套用同一套 token**(把配色/字体变量灌进现有 Bootstrap,各页只引用变量不散写 hex),确保落地页与登录后控制台风格一致——**不是只做漂亮首页**。验收 `check`/`test`/`init` 全绿并在 `../progress.md` 留证据 | DONE |
|
||
| T-607 | 桌面端最新版本检查接口 | T-606 | 给桌面端自动检查更新提供只读 JSON 合约。**新增路由**:`GET /api/v1/client/releases/latest?platform=windows`,公开匿名可访问,**不需要 API Key、不读取用户、不扣点、不占用生成接口限流**;只返回发布元数据,不返回本地文件系统路径、后台 ID、内部状态或任何用户数据。**请求参数**:`platform` 支持 `windows` / `macos` / `linux`,缺省按 `windows`;非法平台返回 `400 bad_request`。**响应结构**:有当前版本时返回 `{platform, release:{version, download_url, sha256, release_notes, published_at}}`;无当前版本或当前版本没有下载地址时返回 `{platform, release:null, message:"暂未发布"}` 且 HTTP 200,方便客户端安静处理。**数据来源**:复用 `DownloadRelease`,只查 `platform + is_current=True`;`download_url` 继续按 `external_url` 优先,否则由 `file.url` 生成绝对 HTTPS URL;`published_at` 可先使用 `updated_at`。**缓存与安全**:可加短 TTL 公共缓存(如 60 秒);生产下载仍走 HTTPS + SHA256 校验;不得把 `MEDIA_ROOT` 或服务器路径暴露给客户端。**含测试**:匿名无 Key 可访问;当前 release 返回完整结构和绝对下载 URL;无 current release 返回 `release:null`;非法 platform 返回 400;`external_url` 优先于 `file`;响应不含本地路径、模型/密钥/用户字段;`check`/目标测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-608 | 新用户注册赠送试用点数(当前 10 点) | T-501, T-203, T-401 | 已完成:仅对 T-608 上线后的新注册用户,经 `apps.billing.services.grant_signup_bonus(user, points=10)` 一次性发放 10 点;历史用户不补发。服务在事务内锁/创建钱包、写 `SignupBonusGrant(user UNIQUE)` 幂等记录和 `PointsLedger(signup_bonus,+10,balance_after,reason)`,重复调用或并发触发不重复发放;allauth adapter 只调用 billing 服务,不直接改余额。公开首页、注册页、dashboard/点数记录页展示当前 10 点,admin 可检索记录;免邮箱验证下由 `ACCOUNT_SIGNUP_RATE_LIMIT=20/m/ip` 限制注册频率。已覆盖注册余额/流水、重复调用、并发幂等、页面文案和后台检索;当前默认值由 `billing.0009_signup_bonus_default_ten` 与服务默认参数共同保证。 | DONE |
|
||
| T-609 | 桌面端版本检查接口增加强制更新标记 | T-607 | 在 `GET /api/v1/client/releases/latest?platform=windows` 的当前版本响应中,为 `release` 对象新增布尔字段 `force_update`,用于桌面端判断是否必须升级。**接口兼容**:只新增字段,不删除既有 `version` / `download_url` / `sha256` / `release_notes` / `published_at` 字段;无当前版本或无下载地址时仍返回 `release:null`,不返回 `force_update`。**数据来源**:在 `DownloadRelease` 增加 `force_update` 布尔字段,默认 `false`;django-admin 可编辑并在列表展示。**示例**:`{platform:"windows", release:{version:"0.1.1", download_url:"...0.1.1.zip", release_notes:"优化了ai模块的生图的功能", force_update:true}}`。**安全**:接口仍公开匿名只读,不读取用户、不扣点、不暴露后台 ID、本地文件路径或内部状态。**文档**:同步 `api.md`、`routes.md`、`04-architecture.md`、`current-state.md`、版本接口对接文档口径。**测试**:覆盖 `force_update=true` / 默认 `false`、无 current release 不返回该字段、响应字段白名单更新、admin 字段可见;`makemigrations` / `migrate` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-610 | 首页导入模板下载入口 | T-606 | 在公开首页“下载客户端”按钮右侧新增“下载导入模板”链接,让用户下载桌面端导入商品数据使用的 Excel 模板。**产品口径**:模板下载是公开资源,匿名可见;不需要登录、不需要 API Key、不扣点、不占用生成接口限流。**后台配置**:新增独立 `ImportTemplate`(不要复用 `DownloadRelease`,两者生命周期不同),字段建议包含 `name`、`file`(FileField,`MEDIA_ROOT/import_templates/`)、`external_url`(可选,有则优先,后续切 CDN/对象存储)、`sha256`(可选)、`is_current`、`notes`、`created_at`、`updated_at`;admin 可上传/更新模板并标记当前模板;每次保存 current 时把其他 current 置为 false,避免使用 MySQL 不支持的条件唯一约束。**首页行为**:`HomeView` 同时读取 Windows 当前 `DownloadRelease` 和当前 `ImportTemplate`;有当前模板且有可下载地址时,在“下载客户端”右侧展示“下载导入模板”链接;无模板时不显示该链接或显示不可用提示,不能影响客户端下载入口和首页 200。**托管策略**:本地上传模板复用生产 Nginx `/media/` 静态托管,不经 Django/Gunicorn 传大文件;`external_url` 优先于 `file.url`,响应/页面不得暴露本地 `MEDIA_ROOT` 或服务器文件路径。**UI**:保持 T-606 下载区现有布局,桌面端按钮为主按钮,导入模板为次级链接/按钮;移动端需换行不重叠。**文档**:同步 `02-requirements.md`、`04-architecture.md`、`routes.md`、`api.md`、`current-state.md`。**测试**:覆盖首页有模板时展示链接且 href 为绝对/可访问 media URL,`external_url` 优先,本地路径不泄露,无模板时首页正常且不显示错误;admin 字段可见;`makemigrations` / `migrate` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-611 | 用户端品牌名统一为“虾皮圈” | T-606 | 把用户端页面上对外展示的项目/产品名从 `cmhub` 统一改为“虾皮圈”。**范围**:只改终端用户可见的 portal 页面文案和浏览器标题,包括公开首页、顶部导航品牌、注册/登录/登出页、控制台、充值、API Key、可用模型、充值记录、点数记录等模板中作为品牌/产品名出现的 `cmhub`;用户端文案里的“cmhub API Key”应改为“虾皮圈 API Key”。**不改**:仓库名、Python 包名、Django app 名、数据库表名、环境变量、API 路径、域名、对外接口字段、后台内部模型名和技术文档里指代服务代号的 `cmhub`。**文档**:同步 `02-requirements.md`、`00-ai-start-here.md`、`current-state.md` 与 `../progress.md`。**测试**:更新 portal 相关断言,覆盖首页展示“虾皮圈”且不再展示旧标题 `cmhub AI 电商生成台`;关键用户端页面渲染不含作为品牌展示的 `cmhub`;`check`、`makemigrations --check --dry-run`、目标 portal 测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-612 | 生图同步接口止血(上游硬截止 + 长请求池校准) | T-403 | **先行止血,不改成功响应契约**,保护存量老客户端的旧同步路。**真实现状**:线上已按路径分流到独立 Gunicorn 长请求池,当前是 `gthread` 而非 sync worker;P0 风险是同步生图长请求仍会占用生成池线程,上游慢 / 卡死时导致生成池排队、客户端写入或读取超时,并可能拖慢同池生文接口。**硬截止优先**:新增可配置上游生图硬截止(建议 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS`,默认 180s 左右,生产按真实 smoke 校准),Provider 实际读取超时取 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`;撞截止必须走现有失败退点路径,保证不超扣、不扣成负数。**Worker 策略**:默认继续使用已验证的 `gthread` 长请求池并校准 `workers/threads/timeout`;只有在本地和生产预演证明 `requests`、PyMySQL、Django cache、支付 SDK 在 monkey-patch 下无问题时,才允许把 `gevent` 作为替代方案写入部署文档,不能把 gevent 当成默认第一步。**接口兼容**:`POST /api/v1/generate/image` 的成功响应字段保持不变;失败响应外层结构保持 `{error:{code,message}}`,如新增 `upstream_timeout` 错误码必须同步 `api.md` 并保持旧客户端可按失败处理。**文档**:同步 `deployment.md`(硬截止、`AiModel.timeout_seconds`、Gunicorn `--timeout`、Nginx `proxy_read_timeout`、客户端 read timeout 的外层 >= 内层关系)、`04-architecture.md` 和 `env.md`。**验收**:模拟上游超时能退点并返回结构化错误;压测下卡死上游不会拖垮 `/balance`、`/models`,生文排队受控;线上配置仍保留 web 池和 generate 池分离;`check`/目标测试/`init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-613 | 抽生成核心 service(计费+审核+上游共享 core) | T-612, T-302, T-604 | 为异步化铺路,**重构现有 `apps/api/generation.py`,不是另写第二套生成逻辑**。把「别名解析+能力校验 → 审核(T-604) → 图片输入处理 → 计费计算 → 预扣(precharge) → 调上游 → 保存结果 → 成功确认/失败退点」整理为不依赖 DRF `Request` / `Response` 的核心 service,并为 T-614 拆出可复用的阶段:同步旧接口可一口气执行完整 pipeline,异步 worker 可复用“已预扣 call_record 的执行与确认 / 退点”阶段,**严禁复制第二套扣点/退点逻辑**。**HTTP 解耦**:核心 service 不直接构造 DRF `Response`,错误用领域异常表达;图片保存不能强依赖 `request.build_absolute_uri()`,需通过 URL 构建器或公开基础 URL 生成结果 URL。**兼容约束**:旧 `POST /api/v1/generate/title|image` 的字段、HTTP 状态码和错误语义保持不变;不要求 JSON 字段顺序逐字节一致。**验收**:T-302/T-604 既有断言不降低标准即通过;覆盖旧同步接口成功、上游失败退点、敏感词拦截不扣点、图片 URL 保存;`check`/目标测试/`init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-614 | 生图异步任务化接口(提交+轮询,新增不动旧接口) | T-613, T-203, T-301 | 新增任务化接口与旧同步接口**共存**(expand-contract 并行变更),新版桌面端走新路、老版本零感知。跨仓契约参考 cmshopee 侧设计(Obsidian「生图接口异步任务化-提交轮询方案」评审修订 v2),**落地口径以本任务 + `api.md` 为准**。**新增路由**:`POST /api/v1/generate/image/tasks`(提交后返回 `202` + `task_id`)、`GET /api/v1/generate/image/tasks/{task_id}`(轮询取状态和结果);本期不暴露 cancel 路由,若后续要做取消单独拆任务且只允许取消 `queued`。**任务模型**:新增 `ImageGenerationTask`,公开 `task_id` 用 UUID,不暴露自增 ID;字段至少包含 `user`、`api_key`、`call_record`、`status(queued/running/succeeded/failed/expired)`、`idempotency_key`、`request_hash`、必要的请求快照 / 输入引用、`result_url`、`error_code`、`error_message`、`started_at`、`finished_at`、`expires_at`、`locked_at`、`lease_expires_at`、`heartbeat_at`、`worker_id`、`attempt_count`。不得把 provider raw、密钥或超大 base64 原文长期存 DB;`image_base64` 应先解码后落临时文件 / 存储引用,`image_url` submit 阶段至少做协议与公网地址校验,实际下载可在 worker 内执行,失败走退点。**提交段**:审核(T-604)同步执行,命中 BLOCK 直接 `400 content_blocked`,不建 task、不扣点、不调上游;submit 时预扣(沿用现有 `precharge`),余额不足 `402`,堵住「余额只够 1 张却提交 100 个 task」;建任务后立即返回 `202`。**幂等**:支持 `Idempotency-Key`,按 `api_key + operation + key` 去重;同 key 同 payload 返回同一 `task_id` 且不重复预扣,同 key 不同 payload 返回 `409 idempotency_conflict`。**后台 worker**:优先用 DB 任务表 + management command worker + systemd 托管,MySQL 8.4 可用 `select_for_update(skip_locked)` 抢任务;暂不引入 Celery/Redis,`django-q`/`huey` 只有在 DB worker 不够时再单独评估。worker 必须复用 T-613 共享 core 和已预扣的 `CallRecord`,成功保留扣点并返回 cmhub 托管 URL,失败/撞硬截止必须退点。**结果 URL**:异步 worker 没有 request,必须新增 `PUBLIC_BASE_URL` 或 `MEDIA_PUBLIC_BASE_URL` 等配置来生成绝对 URL;不得透传上游临时链接。**查询段**:只读、短超时,必须校验 `task.user == api_key.user`(防 IDOR,跨用户返回 404 或 403);`succeeded` 幂等重取返回同一 URL;过期任务 / 图片 GC 口径写入文档。**保留窗口**:task_id 持久化的目的是扛客户端重启,窗口须覆盖桌面端现实停机(如关一晚),**任务元数据保留 ≥24h、结果图保留更久(如 24–72h,可配,别硬编码 6h)**,避免「点已扣、图被 GC」。**租约与僵任务回收(reaper,必做)**:worker 抢任务时写 `worker_id`、`locked_at`、`lease_expires_at`,运行中周期性更新 `heartbeat_at`;如果 worker 用 `select_for_update(skip_locked)` 抢任务后崩溃,行锁随连接释放但 `status` 会永远停在 `running`——**点数已预扣却永不退、客户端轮询到自己超时**。reaper 按 `lease_expires_at` / `heartbeat_at`(兜底 `started_at`)识别僵任务,默认把超时 `running` 判 `failed` + **退点(幂等)**,不默认重排队;只有能证明任务尚未调上游(如明确 `stage=not_started`)时才允许后续任务设计重排队。**worker 至少一次执行,账务和结果 exactly-once**:worker 可能重复执行同一任务,但「上游调用确认 / 退点 / 写结果」这些终态动作必须幂等;重复执行不得重复扣/退,不得覆盖已 `succeeded` 结果,也不得把 reaper 已判 `failed` 且已退点的任务改回 `succeeded`。**配置**:新增或同步 `IMAGE_TASK_RETENTION_HOURS`、`GENERATED_IMAGE_RETENTION_HOURS`、`IMAGE_TASK_REAPER_INTERVAL_SECONDS`、`IMAGE_TASK_LEASE_SECONDS` 等环境变量口径。**文档**:同步 `api.md`(新契约、状态机、错误码、幂等)、`routes.md`、`04-architecture.md`(异步计费时序)、`deployment.md`(worker 进程托管)、`env.md`。**验收**:审核命中不建 task/不扣点/不调上游;预扣余额不足 402;同 Idempotency-Key 去重且 payload 冲突 409;worker 成功后重复 GET 同一 URL;跨用户 GET 被拒;失败/超时退点不超扣;**模拟 worker 崩溃留下 `running` 僵任务 → reaper 判失败并退点、不超扣、客户端下次 GET 得到 `failed`**;**worker 重复执行同一任务幂等(不重复扣/退、不覆盖已 `succeeded` 结果)**;**reaper 已把任务判 `failed` 并退点后,迟到 worker 返回成功也不能改回 `succeeded`、不能覆盖结果、不能再次改账**;旧生文与旧同步生图不回归;`check`/目标测试/`init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-615 | 旧同步生图接口用量遥测 + 弃用口径 | T-614 | 给旧同步接口装可观测、定弃用退出条件,避免永久双维护。**最小遥测**:旧 `POST /api/v1/generate/image` 和新 `/tasks` 路径都写结构化日志 / 计数,至少含 route_type(sync/async)、api_key 前缀或 ID、user_id、client version(若请求头提供,如 `X-Client-Version`)、alias、status、latency_ms、error_code;日志不得包含 API Key 明文、prompt 全文、图片 base64 或 provider raw。**查看方式**:先用日志查询即可;若要 admin 报表需单独评估数据量和索引。**弃用口径**:在 `deployment.md`/`04-architecture.md` 记录迁移计划:新版桌面端默认走异步接口 → 观察旧路调用量和错误率 → 旧路调用归零或低于阈值一段时间 → 宣布 deprecate → 另立任务下线;旧同步接口下线前必须保留成功响应兼容。**验收**:能按 client version / api_key 看到旧路与新路用量;弃用条件成文;不泄露敏感数据;`check`/目标测试/`init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-616 | 生图失败自动重试 2 次 | T-614, T-615, T-203 | 给异步生图 worker 增加临时性上游失败自动重试,降低 `api.vectorengine.ai` 偶发 `Read timed out` 对用户的失败率。**业务口径**:“2 次重试”定义为第 1 次正常执行 + 失败后最多再重试 2 次,即最多 3 次上游调用;提交阶段仍只预扣一次。**只重试临时性错误**:`upstream_timeout`、网络连接错误、上游 502/503/504 等;不重试 `content_blocked`、`insufficient_points`、`no_pricing_rule`、模型/别名配置不可用、参数错误、Provider 配置错误等确定性错误。**数据结构**:复用现有 `attempt_count` 表示已开始执行次数;新增 `next_attempt_at`(允许下次抢占时间)和必要的 metadata-only admin 展示;如需记录最后一次错误,继续使用 `error_code` / `error_message`,不得保存 provider raw。**状态机**:`queued -> running(attempt_count+1)`;若成功则 `succeeded` 并确认已预扣调用;若可重试失败且 `attempt_count < 3`,任务回到 `queued`、设置 `next_attempt_at=now+backoff`、保留 `CallRecord.pending`、不退点;若不可重试或已到最终次数,才 `failed` 并调用计费层幂等退点。**worker 抢任务**:只抢 `status=queued` 且 `next_attempt_at IS NULL OR next_attempt_at <= now` 的任务;避免 66 worker 立即反复打爆上游。**配置**:新增 `IMAGE_TASK_MAX_RETRIES=2`、`IMAGE_TASK_RETRY_BACKOFF_SECONDS=10,30`(或等价配置),同步 `.env.example`、`env.md`、`deployment.md`;生产要提醒最坏耗时约 `上游硬截止 * 3 + backoff`,桌面端轮询超时需覆盖。**接口兼容**:轮询响应可新增可选字段 `attempt_count`、`max_attempts`、`next_attempt_at`,但不得删除既有字段;旧客户端忽略新字段仍可工作。**日志**:扩展 `event=image_task_processed`,包含 `attempt`、`max_attempts`、`retrying`、`next_attempt_at`、`duration_ms`、`error_code`,不得记录 prompt、base64、provider raw 或密钥。**账务验收**:第 1/2 次 `upstream_timeout` 回到 queued 且不退点;第 3 次成功只扣一次并成功确认;连续 3 次失败只退一次;非重试错误立即失败并只退一次;幂等提交仍返回同一 task,不重复预扣;reaper 与迟到 worker 不得把已最终失败/已退款任务改回成功。**测试**:覆盖上述账务、状态机、backoff 抢占、轮询新增字段兼容、worker 日志字段和敏感信息不泄露;`makemigrations` / `migrate` / `check` / 目标测试 / `init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
|
||
| T-617 | 桌面端版本检查接口增加文件大小字段 | T-607, T-609 | 在 `GET /api/v1/client/releases/latest?platform=windows` 的当前版本响应中,为 `release` 对象新增 `size_bytes` 字段,用于桌面端下载后比对文件大小。**接口兼容**:只新增字段,不删除既有 `version` / `download_url` / `sha256` / `release_notes` / `force_update` / `published_at`;无当前版本或无下载地址时仍返回 `release:null`,不返回顶层 `size_bytes`。**数据来源**:`DownloadRelease` 新增 `size_bytes` 可空正整数字段,单位字节,django-admin 可填写并在列表展示;老数据为空时接口返回 `null`,客户端只在值为正整数时做大小校验。**安全**:接口仍公开匿名只读,不读取用户、不扣点、不暴露后台 ID、本地文件路径或内部状态。**文档**:同步 `api.md`、`routes.md`、`04-architecture.md`、`02-requirements.md`、`current-state.md`。**测试**:覆盖有值返回整数、未配置返回 `null`、未发布响应不返回顶层字段、响应字段白名单更新、admin 字段可见;`makemigrations --check` / `check` / 目标测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-618 | 客户端发布版本后台必填文件校验元数据 | T-617 | 把 django-admin 里 `DownloadRelease` 的 `sha256` 和 `size_bytes` 改为必填,避免运营发布客户端安装包时漏填校验元数据,导致桌面端无法完整校验下载文件。**范围**:本任务只要求 admin 后台保存时必填;为兼容历史数据和现有 API 合约,第一步不直接把数据库字段改成 `NOT NULL`,`size_bytes` 仍允许旧记录为空,API 暂保持可返回 `null`;若后续要数据库级强约束,需先单独回填全量历史发布记录再做迁移。**实现**:`DownloadReleaseAdmin` 已挂专用 `ModelForm`,将 `sha256`、`size_bytes` 设为 required,并保留 `sha256` 64 位十六进制校验、`size_bytes >= 1` 校验;admin 新增 / 编辑保存时未填会显示字段级错误,不写库。**生产数据**:上线后必须补齐当前 `windows` 发布版本的 `size_bytes`,再验证 `/api/v1/client/releases/latest?platform=windows` 返回正整数 `release.size_bytes`;已有 `sha256` 继续保留并核对。**兼容性**:不删除既有 API 字段,不改变无当前版本时的 `release:null`;非 admin 的历史数据、迁移和只读接口不应因空值直接 500。**文档**:同步 `api.md`、`04-architecture.md`、`routes.md`、`current-state.md` 和 `progress.md` 的发布校验口径。**测试**:已覆盖 admin 表单缺 `sha256` / 缺 `size_bytes` 拒绝保存、合法 `sha256 + size_bytes` 可保存、API 仍能读取历史空值记录、当前版本补齐后接口返回正整数;`check` / `makemigrations --check` / 目标测试通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-619 | 多张图片理解并返回文字 | T-613, T-601, T-306 | 新增独立的多模态图片理解能力,不复用“生成标题”语义。**操作与别名**:在 `ModelAlias.OperationType`、`CallRecord.OperationType` 和别名解析中新增 `vision`;建议首个数据库别名为 `vision-standard`,但不得在接口中暴露具体供应商模型名。`vision` 别名指向的 `AiModel` 及 Provider 必须同时具备 `vision` 与文字输出能力,图片生成 / 编辑专用 Provider 不得误接。**新增接口**:`POST /api/v1/analyze/images`,继承 `ExternalApiView`、使用 API Key 鉴权和生成限流,同步返回 `{text, alias, model_used, points_cost, points_balance, call_id}`;不改变 `/api/v1/generate/title`、`/api/v1/generate/image` 及异步生图接口。**请求结构**:`prompt` 必填,`model` 可选,`images` 为有序列表且至少 1 张;每项必须且只能提供一个 `image_url` 或 `image_base64`,允许 URL/base64 混合。新增 `VISION_MAX_IMAGES=8`、`VISION_MAX_IMAGE_BYTES=10485760`、`VISION_MAX_TOTAL_BYTES=33554432` 三个可配置上限,分别控制单次图片数量、单图解码后字节数和总字节数;超限、空列表、格式错误或同项双来源返回 `400 bad_request`,不扣点、不调上游。**安全时序**:serializer 后先按 T-604 审核 prompt,再下载 URL / 解码 base64;每个 URL 必须复用 T-306 的协议、公网地址、重定向逐跳校验、超时和响应大小保护,不另写弱化下载器。第一版只审核文字 prompt,不声称已做图片内容审核。**Provider**:复用 `apps/ai/providers` 现有 HTTP 适配层,增加向 Chat Completions / Gemini 发送多张图片并保序的兼容能力;不得复制第二套上游调用实现。现有单图标题 / 生图调用签名与行为保持兼容。**计费与留痕**:复用 T-613 共享生成 core 与 billing 的预扣 / 成功确认 / 幂等退点;第一版按 `operation_type=vision + alias` 的默认 `PricingRule` 对一次请求固定扣点,不按图片张数重复扣费,图片数量上限用于控制成本;上游失败必须退点。`CallRecord` 记录 `vision`、别名、实际模型、点数、耗时和结果摘要,不保存输入图片、base64、provider raw 或完整上游响应。**目录与运营配置**:`GET /api/v1/models` 和 portal 可用模型页按 T-601 既有语义列出 active、能力匹配的 `vision` 别名并显示 `requires_image=true`;有定价时返回 `priced`,缺定价时仍返回 `unpriced`,不得改变现有目录兼容行为。代码部署 / migrate 后由运营在 admin 配置支持视觉理解的 `AiModel`、`vision-standard` 默认别名和计费规则,不通过数据迁移写入真实上游配置或密钥。**范围边界**:本任务只做同步文字结果,不做流式输出、异步 vision task、OCR 专用接口、结构化 JSON schema、图片内容审核,也不为 OCR / 商品识别 / 图片对比各建别名;这些用途先由 prompt 表达,只有底层模型或价格确实不同时再增加别名。**文档**:实现时同步 `02-requirements.md`、`04-architecture.md`、`api.md`、`routes.md`、`env.md`、`current-state.md` 和 `progress.md`。**测试**:覆盖单图 / 多图 / 混合来源及顺序、默认 / 指定别名、模型与 Provider 能力拒绝、图片数量与大小限制、SSRF、敏感词先拦截、成功仅扣一次、上游失败仅退一次、调用记录不保存图片 / raw、模型目录安全字段、旧标题 / 生图接口回归;`makemigrations` / `migrate` / `check` / 目标测试 / `init` 通过并在 `../progress.md` 留证据 | DONE |
|
||
| T-620 | 图生图支持单图 / 多图主图与参考图 | T-613, T-614, T-616, T-619 | 已完成:同步 / 异步图生图支持有序 `images`,旧单图字段继续兼容且禁止混用;服务端固定注入首图主商品、后续参考图规则。新增 `IMAGE_MAX_INPUT_IMAGES` / `IMAGE_MAX_INPUT_IMAGE_BYTES` / `IMAGE_MAX_INPUT_TOTAL_BYTES`,图片 URL 同时受新单图限制与既有 `IMAGE_URL_MAX_BYTES` 的较小值保护。Provider 已验证 Chat 多段、JSON `image_urls` 与 `images_edits` 重复 multipart `image` 的顺序传递;异步任务新增 `ImageGenerationTaskInput` 有序输入表,旧 `input_image` 读取保持兼容。已通过 `check`、迁移一致性、13 条 Provider 测试、序列化器边界校验与 6 条目标 API 测试(同步多图 / URL+base64 混合 / 互斥输入 / 超限 / 异步恢复 / URL 上限);完整 `GenerateApiTests` 曾跑出 53 条且发现并修复 URL 上限回归,远程 MySQL 测试库后续整类重跑超过 5 分钟未完成。此前已使用真实中转站 `gpt-image-2` 对两张图片重复 multipart `image` 调用获得 HTTP 200 与图片结果,确认上游多文件传输契约。详见 `progress.md`。 | DONE |
|
||
| T-622 | 图片生成任务后台图片缩略预览 | T-614, T-620 | 已完成:django-admin 的 `ImageGenerationTask` 详情页新增输入图 / 结果图缩略画廊;输入按 `ordinal` 标为主图和参考图,兼容历史 `input_image` 单图;成功任务以 `result_url` 展示生成结果。双击缩略图弹出后台 `<dialog>` 大图预览,支持关闭按钮、遮罩和 `Escape`;图片加载失败显示降级提示。未改 `CallRecord`、对外 API、计费、worker、迁移、媒体访问策略或列表页查询。已通过 4 条目标 admin 测试、`check`、迁移一致性、静态资源发现和 `init.ps1`。 | DONE |
|
||
| T-623 | 图片生成任务单图 / 多图筛选 | T-614, T-620 | 已完成:`ImageGenerationTaskAdmin` 的列表右侧新增“输入图片类型”筛选,提供“单图生图”和“多图生图”。数据库聚合计数识别 1 条子输入的单图、2 条及以上的多图;无子输入且历史 `input_image` 非空的任务兼容归单图;无输入图任务不归类。筛选可与既有状态 / 日期过滤叠加,结果不重复;未改模型、迁移、任务状态、计费、API、worker、媒体文件或详情页缩略图。已通过 6 条目标 admin 测试、`check`、迁移一致性和 `init.ps1`。 | DONE |
|
||
| T-624 | 蝦皮圈设备登记与会话观测 | T-301, T-401 | 已完成:新增 `apps.licensing`、`ClientDevice` / `DeviceSession` / `DeviceBindingAudit` 与 `licensing.0001_initial`。设备登记用 API Key 鉴权、心跳用短期 `X-Device-Session`;重复登记不重复建设备并轮换旧会话,设备/公钥/会话只存摘要或 hash,活跃时间默认每日节流。admin 只读展示设备、会话和审计摘要,不回显敏感值。现有生成、余额、模型目录、异步任务和通用 API Key 语义未改变。已通过本地迁移、21 条目标回归测试、`check`、迁移一致性和 `init.ps1`。 | DONE |
|
||
| T-625 | 蝦皮圈设备使用关联与迁移观测 | T-624, T-613, T-614, T-615 | 已完成阶段 1 关联观测:有效 `X-Device-Session` 仅在标题、同步图片、异步图片提交、vision 的预扣 `CallRecord` 写入服务端解析的可空 `client_device`;无头旧调用保持原返回、扣点、提交和轮询。新增 `billing.0010_callrecord_client_device_and_more` 与 `(client_device, created_at)` 索引;admin 支持展示 / 按设备筛选。`generation_route_usage` 新增 `product_code`、内部 `client_device_id`、`device_session_present` 白名单字段,不记录设备摘要、公钥、令牌、Key、prompt 或图片。伪造/过期会话在预扣前返回 401,吊销/跨账号会话返回 403;仍不做订阅授权拦截。已通过 4 条新增入口/兼容/安全/遥测测试及 14 条 licensing/生成回归、迁移、`check` 和一致性验证。 | DONE |
|
||
| T-626 | 软件套餐、权益与设备席位基础模型 | T-624, T-401 | 已完成阶段 2 基础模型:新增 `SoftwarePlan`、`SoftwareEntitlement`、`LicenseSeat`、`LicenseEvent` 和 `licensing.0002_softwareentitlement_licenseseat_licenseevent_and_more`。授予时复制套餐名称/价格/时长/设备数/宽限期快照并预创建固定席位;`(entitlement, seat_number)` 唯一。服务层以事务锁权益和席位执行授予、续期、撤销、绑定与解绑,所有人工操作原因必填并写事件;续期从 `max(now, expires_at)` 起算,不触碰钱包。admin 提供套餐维护及权益授予/续期/撤销专用入口,权益/席位/事件只读。未接购买、支付、迁移凭证或生成授权。已通过套餐快照、续期、撤销、审计、后台权限及双连接单席位并发测试、迁移和 `check`。**评审遗留(2026-07-21,须在 T-629 强制前修复)**:① `renew_software_entitlement` 只更新权益自身字段,不刷新该权益下已签发 `DeviceCredential.expires_at`(签发时钉死为旧 `grace_expires_at`),续费用户在旧宽限期后会被判 `license_expired`;现有续期测试只断言权益未覆盖凭证。② `revoke_software_entitlement` 不吊销凭证也不释放席位,裁决层由 `is_usable_at()` 兜底不构成安全问题,但留下 `revoked_at=null` 凭证与占用席位的数据残留。 | DONE |
|
||
| T-627 | 存量用户迁移权益、网页确认与设备凭证 | T-624, T-626, T-501 | 已完成存量迁移闭环:新增 `LegacyMigrationGrant`、短时 `MigrationRequest` 和 `DeviceCredential`,均只保存 hash/摘要;admin 通过显式用户+套餐授予迁移资格和快照,新注册用户无自动路径。客户端用旧 API Key + 当前设备会话申请迁移并一次性取得凭证明文;同账号 portal 确认后事务内绑定席位、按 hash 签发凭证,重复确认/轮询不重复占位或签发。用户可自助解绑,凭证吊销并释放席位且写审计。新增迁移 API、portal 确认/设备页;旧生成 API 完全不变。已覆盖资格、缺会话、跨账号、过期、重复确认、凭证 hash、状态轮询与解绑;迁移 / `check` / 迁移一致性通过。 | DONE |
|
||
| T-628 | 蝦皮圈专属授权入口与影子校验 | T-625, T-626, T-627, T-613, T-614 | 已完成:新增 `/api/v1/cmshopee/` 的 title、vision、异步图片提交和已接受任务读取,均复用原 API / 计费 core。统一授权判定检查设备会话、凭证、用户、产品、席位和权益,返回 `device_not_bound` / `device_mismatch` / `license_required` / `license_expired`;当前仅写脱敏 `would_reject` 日志,不阻断专属或通用调用。通用 `/api/v1/generate/*` 未改,任务读取不追加订阅校验;`CMSHOPEE_AUTHORIZATION_SHADOW_MODE=true` 为部署口径。已验证正确 / 缺失 / 过期凭证判定、迁移流程回归、`check` 和迁移一致性。**评审说明(2026-07-21)**:`CMSHOPEE_AUTHORIZATION_SHADOW_MODE` 当前仅被写入日志事件,没有任何分支依据它决定放行或拒绝,属阶段 2 预期行为;须留档提醒——切换强制拦截不是改环境变量即可,需要在专属入口显式加入拒绝分支并同步部署说明,避免运维误判为纯配置开关。 | DONE |
|
||
| T-629 | 软件套餐购买、续订订单与权益入账 | T-626, T-627, T-628, T-304, T-305 | **已完成。** 新增与 `RechargeOrder`、`PointsLedger` 完全分离的 `SoftwareOrder` 和 `licensing.0004_alter_licenseevent_action_softwareorder`。订单锁定产品、套餐、金额、币种、时长、设备数、宽限期和通道快照,保存关联权益、唯一 `order_fulfilled` 事件和发放时间;`order_no` 与 `(pay_method, payment_txn_no)` 受数据库唯一约束,已支付订单收到不同交易号一律拒绝。微信支付仅复用通用验签、下单、查单适配,新增独立 `SOFTWARE_WECHAT_PAY_NOTIFY_URL`、`POST /api/v1/software-orders/callback/wechat` 与 `GET /api/v1/software-orders/status`,未调用 `apply_recharge_payment()`、未写 `UserWallet` / `PointsLedger`。portal 新增 `/subscription` 套餐购买/续订、状态和最近订单,admin 新增只读软件套餐订单检索。成功回调/查单锁订单后首次创建权益或续订同套餐权益;同订单重复/并发回调最多发放一次,金额/通道/验签不一致不发权益。支付票据到期转 `expired`,下单失败转 `failed`。第一版仅微信主动月度续订,不做自动代扣、退款发起或自动退款回收;已支付退款与权益撤销由运营按订单人工处理。**同时修复 T-626 遗留**:续期同步刷新未吊销凭证到期时间,撤销权益逐项吊销凭证、释放席位并写审计。已通过 12 条 licensing/API 目标测试(含双连接并发回调)和 31 条既有充值回归;`check`、`makemigrations --check --dry-run`、编译检查通过。全量测试在远端 MySQL 测试库运行 10 分钟未返回且无失败输出,未将其记为通过。 | DONE |
|
||
| T-630 | 账号订阅授权与默认多设备模式 | T-628, T-629 | **已完成。** 将蝦皮圈产品专属授权从“设备会话 + 设备凭证”调整为“API Key 对应账号 + 软件订阅权益”。同一账号默认允许多台电脑使用,不设置 `LicenseSeat` 设备数量硬限制;`ClientDevice` / `DeviceSession` 仅保留为可选观测和风控数据。新增 `GET /api/v1/cmshopee/subscription/status`,返回账号对应产品的订阅状态、套餐和到期时间;新增 `subscription_required` / `subscription_expired` 错误码。授权服务只查询用户 `SoftwareEntitlement`,不再读取 `DeviceCredential`。`CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=false` 时只写账号订阅影子日志并保持兼容,开启后产品专属生成入口才拒绝无订阅用户;通用 `/api/v1/generate/*`、充值、点数和历史迁移接口不变。保留旧设备/卡密数据只读,不删除历史记录,不再签发新设备凭证;过期设备会话也不会阻断账号订阅授权。已补账号无订阅/有效订阅状态、强制拦截和多设备上下文回归测试;`check`、编译检查和目标 API 测试通过。 | DONE |
|
||
|
||
## 里程碑
|
||
|
||
- M1:Django + admin 骨架可运行、自定义 User 就位(T-003)。
|
||
- M2:服务端跑通一次 AI 生成(T-104)。
|
||
- M3:计费 + 对外接口 + 充值闭环 + 上线前安全加固(T-306)。
|
||
- M4:用户端(注册/充值/API Key/记录)可用(T-504)。
|
||
- M5:运营后台 + MVP 验收 + 可部署(T-403)。
|
||
- M6:可用别名发现(T-601)。
|
||
- M7:注册试用额度安全落地(T-608)。
|
||
- M8:客户端版本强制更新标记(T-609)。
|
||
- M9:首页导入模板下载入口(T-610)。
|
||
- M10:用户端品牌名统一为“虾皮圈”(T-611)。
|
||
- M11:生图异步任务化(先止血、后共享 core、再新老接口共存并观察旧路用量,T-612~T-615)。
|
||
- M12:生图失败自动重试(T-616)。
|
||
- M13:桌面端版本文件大小校验元数据(T-617)。
|
||
- M14:客户端发布版本后台必填校验元数据(T-618)。
|
||
- M15:多张图片理解并返回文字(T-619)。
|
||
- M16:图生图支持单图 / 多图主图与参考图(T-620)。
|
||
- M17:图片生成任务后台图片缩略预览(T-622)。
|
||
- M18:图片生成任务单图 / 多图筛选(T-623)。
|
||
- M19:蝦皮圈设备授权与订阅迁移(T-624~T-629)。
|
||
|
||
## 待办池(Backlog)
|
||
|
||
- ~~异步生成(任务队列 + 轮询/回调)~~ → 已立项 **T-612~T-615**(生图异步任务化:先硬截止 + 长请求池校准止血,再抽共享 core,最后新增 `/tasks` 提交+轮询接口与旧同步接口共存、旧路遥测后弃用)。
|
||
- **T-621 注册赠点运营后台配置**:将当前固定 10 点改为 admin 可管理的单例策略,包含策略审计、停用时的 0 点决定记录及不追溯历史用户。该项涉及账本与后台配置,现移入 Backlog,暂不实施。
|
||
- 按账号授权可用别名(`account_alias_permission`),防止调用未授权/昂贵模型。
|
||
- 别名按比例分流到多个模型(灰度 / A/B / 故障转移);供应商 A 故障自动切 B。
|
||
- 异步生图提交时模型配置快照:当前 worker 会复跑 `prepare_generation()`,重新审核 prompt、解析别名 / Provider / 定价,但账务使用已预扣 `CallRecord.points_cost`。短队列下可接受;若后续队列积压或后台频繁切换别名,应在 submit 阶段快照 `ai_model_id` / `model_used` / 必要 Provider 配置,worker 使用提交时模型执行,同时保留 prompt 复审。
|
||
- 复杂活动赠点、邀请奖励、历史用户批量补发(需单独审批、风控、批处理与流水留痕)。
|
||
- 用量统计报表。
|
||
- 退款对账自动化、API Key 轮换、Provider key 轮换(MultiFernet / 双 key 迁移)、限流细化。
|
||
- 用户端 API Key 数量治理:限制每用户 active Key 数量(如 10 个)、提供轮换与过期策略,避免无限建 Key。
|
||
- 充值下单风控:限制每用户 pending 订单数量或创建频率,避免反复点击刷出大量未支付订单并放大支付网关调用。
|
||
- 用户端登录与凭证安全复核:评估新 API Key 明文短暂进入 session 的替代方案。
|
||
- allauth `account.EmailAddress` 的 MySQL `models.W036` 条件唯一约束警告为已知 cosmetic 警告;邮箱唯一性由 `user.email` 约束承担,后续升级 allauth / 数据库兼容性时复核。
|
||
- **SSRF DNS rebinding 加固(`image_url` 下载)**:当前 `download_image_input` 校验时 `getaddrinfo` 解析一次、`requests.get` 又独立解析一次,两次之间存在 TOCTOU(恶意 DNS 可先返公网 IP 过校验、再返内网 IP 供实际请求)。彻底堵需把连接**钉死到已校验的 IP**(自定义 `HTTPAdapter`/`connection` 连已验证 IP + 保留 Host 头做 TLS/路由),对每次重定向同样处理。属安全深水区,规模化/公网大流量前处理;上线前安全检查应至少标注此残留。
|