Files
cmhub/docs/06-tasks.md
T

50 KiB
Raw Blame History

任务看板(Tasks)

把 MVP 拆成小步、可独立交付的任务,让 AI 一步一步开发,避免一次生成整个项目。

使用规则

  1. 一次只做一个任务:每轮只领取一个状态为 TODO、且依赖均已 DONE 的任务,取最靠前的那个。
  2. 做完即停:完成该任务、自测通过、把状态改成 DONE 后,停下来汇报。
  3. 不跳步:依赖未完成的任务不能开工。
  4. 完成定义:以 编码规则 的验证清单为准。
  5. passing 需证据:标记 DONE 前,必须在 ../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
T-003 接通 django-admin 与最小测试 T-001 createsuperuser 后能登录 /admin/;manage.py test 可运行(至少 1 条占位测试通过) DONE
T-004 Phase 0 骨架审核修补 T-003 按 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 修 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 处理 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 处理 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 处理 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 实施。范围收紧: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 抽出共享 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 新用户注册赠送 100 点试用点数 T-501, T-203, T-401 把 2026-07-08 的产品口径落地为账本安全实现。业务口径:只对 T-608 上线后的新注册用户自动发放 100 点;历史用户是否补发不在本任务内,需单独审批和批处理任务。数据与服务:新增 PointsLedger.ChangeType.SIGNUP_BONUS(展示为注册赠点)和 MySQL 兼容的数据库级幂等兜底 SignupBonusGrant(user UNIQUE),不要用 partial unique / 条件唯一约束;新增 apps.billing.services.grant_signup_bonus(user, points=100),在事务内锁/创建钱包、发放点数、写 PointsLedger(signup_bonus,+100,balance_after,reason),重复调用或并发触发不得重复发放。接入点:allauth adapter 注册成功后调用 billing 服务,不得在 portal 直接写 points_balance;失败要清晰暴露并保持注册 / 账务一致性。页面与文案:公开首页、注册页、dashboard/记录页等用户端文案同步「注册送 100 点」;点数记录页能展示注册赠点正向流水;admin 可检索该流水和注册赠点记录。防刷:免邮箱验证策略下显式配置 ACCOUNT_SIGNUP_RATE_LIMIT(默认 20/m/ip),由 allauth signup rate limit 执行;图形验证码 / 人机验证仍作为上线前风控增强项。测试:注册后余额为 100 且有一条 signup_bonus 流水;重复调用服务不重复加点;并发触发不重复发放;现有用户不会自动补发;dashboard / 点数记录显示一致;check/迁移/目标测试通过并在 ../progress.md 留证据 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` 留证据
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 |

里程碑

  • 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)。

待办池(Backlog)

  • 异步生成(任务队列 + 轮询/回调) → 已立项 T-612~T-615(生图异步任务化:先硬截止 + 长请求池校准止血,再抽共享 core,最后新增 /tasks 提交+轮询接口与旧同步接口共存、旧路遥测后弃用)。
  • 按账号授权可用别名(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/路由),对每次重定向同样处理。属安全深水区,规模化/公网大流量前处理;上线前安全检查应至少标注此残留。