diff --git a/docs/tasks/T-705.md b/docs/tasks/T-705.md new file mode 100644 index 0000000..9c62574 --- /dev/null +++ b/docs/tasks/T-705.md @@ -0,0 +1,109 @@ +--- +id: T-705 +title: cmhub 远程订阅策略开关与分类门禁提示 +phase: 8 +deps: [T-700, T-701] +status: BLOCKED +created: 2026-07-28 +--- + +# T-705 cmhub 远程订阅策略开关与分类门禁提示 + +## 问题 / 背景 + +当前客户端通过本地常量控制启动订阅检测和强制门禁,cmhub 无法在灰度上线、接口维护或异常期间远程切换。每次调整都需要重新修改并打包客户端。现有强制模式已经能限制业务 Tab、拦截新提交,并对缺少 Key 和套餐过期提供窗口,但未开通套餐、套餐撤销、账号禁用、Key 无效和接口异常仍缺少与原因匹配的独立窗口。 + +不能把远程开关放进订阅状态接口,否则客户端必须先请求该接口才能知道是否应跳过它。客户端门禁也不能成为服务端授权的唯一安全边界,cmhub 产品接口仍必须独立校验 API Key 和套餐。 + +## 外部前置条件 + +cmhub 先按 Obsidian《cmshopee-客户端订阅策略接口契约》交付并冻结启动策略字段、兼容规则和测试响应;未交付前本任务保持 `BLOCKED`,不得猜测字段或先写临时解析。 + +最低要求是在客户端启动时已经请求的 HTTPS 版本接口中增加可选 `client_policy`: + +```json +{ + "client_policy": { + "policy_version": 1, + "subscription_check_enabled": true, + "subscription_enforcement_enabled": true, + "updated_at": "2026-07-28T10:00:00+08:00" + } +} +``` + +cmhub 必须保证:`subscription_check_enabled=false` 时服务端产品接口的订阅策略与客户端关闭策略一致;无论客户端门禁是否开启,付费授权最终仍由服务端执行。 + +## 方案 + +### 1. 远程双开关 + +- `check=false`:跳过 `SubscriptionCheckWorker`,不显示会员激活/过期/异常窗口,不锁定业务 Tab;生成前订阅预检直接放行,设置页重新检测入口显示“服务端暂未启用会员检测”并禁用。 +- `check=true, enforcement=false`:执行真实查询并展示账号、套餐、有效期和观察状态,但不禁用业务功能、不拦截新提交、不弹强制处理窗口。 +- `check=true, enforcement=true`:保持现有强制门禁;仅 `active/grace/legacy` 放行业务功能。 +- `check=false, enforcement=true` 属于非法组合,客户端归一化为两者均关闭并写脱敏诊断,不允许在没有检测结果时强制锁定。 + +### 2. 启动策略获取与兼容 + +- 优先复用启动版本接口返回的 `client_policy`,不得为了读取开关再调用订阅状态接口,也不增加独立同步阻塞请求。 +- 缓存最近一次结构合法的非敏感策略和取得时间到 `data/config/` 独立文件;不包含 API Key、账号、套餐或订阅结果。 +- 当前响应有效时覆盖缓存;接口不可达、字段缺失或非法时使用最近一次有效缓存。无有效缓存时回退为“检测开启、强制关闭”的观察模式,避免控制面异常锁死正常用户;cmhub 产品接口继续作为最终授权边界。 +- 旧 cmhub 响应不含 `client_policy` 时按上述兼容规则处理,不影响启动版本检查和强制升级。 +- 只在启动时确定本次运行策略;服务端开关变更下一次启动生效。设置保存和“重新检测会员状态”只重查套餐,不偷偷改变本次策略。 + +### 3. 分类窗口 + +- `not_configured`:复用会员 API Key 激活窗口。 +- `required`:新增“尚未开通会员套餐”,提供“前往会员中心”“重新检测”“退出程序”。 +- `expired`:保留现有“会员套餐已过期”窗口及会员中心入口。 +- `revoked`:新增“会员套餐已失效”,不得使用含糊的技术错误。 +- `account_disabled`:新增“会员账号当前不可用”,提示前往会员中心或联系处理。 +- `key_invalid`:打开可重新填写 API Key 的激活窗口并聚焦输入框,不使用泛化订阅窗口。 +- `unavailable`:显示“暂时无法验证会员状态”,提供“重新检测”“打开设置”“退出程序”;不得误导用户未付费或要求重复订阅。 +- 同一运行期间同一状态只显示一次;状态恢复为允许后再次进入无效状态才可重新提示。重复检测、保存设置和陈旧 worker 结果不得造成弹窗循环。 +- 会员中心地址继续使用 `subscription.safe_manage_url()` 的 HTTPS 安全结果;无有效地址时禁用入口并显示中文原因。 + +### 4. 门禁边界 + +- 强制模式查询期间和明确无效状态只保留“设置”Tab;AI生成、AI帮写和商品套图的新提交继续经过主窗口统一预检。 +- 不改变已经运行任务的协作取消策略,不删除本地结果、不修改 SQLite 业务数据、不关闭 Chrome。 +- cmhub 托管生成接口仍必须服务端校验套餐;自定义网关和客户端本地功能是否受门禁影响沿用当前产品规则,本任务不扩张授权范围。 + +## 验收要点 + +- [ ] 服务端双开关三种合法组合分别得到关闭、观察、强制三种稳定行为,非法组合不会锁死客户端。 +- [ ] 明确关闭时启动不请求订阅状态;观察模式查询但不限制;强制模式保持现有门禁。 +- [ ] 策略响应、缓存、旧响应、接口失败和无缓存回退均有测试,不记录敏感信息。 +- [ ] `required/expired/revoked/account_disabled/key_invalid/unavailable/not_configured` 使用对应中文窗口或激活流程,不把技术异常说成未订阅。 +- [ ] 分类窗口按状态转换去重,会员中心 URL 安全校验、重新检测、设置和退出操作可用。 +- [ ] 服务端仍是授权最终来源;修改客户端开关不能绕过 cmhub 生成接口套餐校验。 +- [ ] 不修改 Chrome、CDP、蝦皮采集、封面上传、拖拽或线上更新逻辑。 + +## 测试与文档 + +- 扩展 `tests/test_update_check.py`:策略字段解析、非法组合、旧响应和策略透传。 +- 新增或扩展订阅策略缓存测试:有效缓存、损坏缓存、接口失败和无缓存观察回退。 +- 扩展 `tests/test_gui.py`:关闭/观察/强制模式、七类状态窗口、去重、重检和门禁。 +- 扩展 `tests/test_subscription.py`:状态与窗口动作映射所需的纯逻辑契约。 +- 更新 `docs/04-architecture.md`、`docs/api.md`、`docs/routes.md` 和打包/发布说明。 + +## 验证 + +```bash +py -3.10 -m unittest tests.test_update_check tests.test_subscription tests.test_gui +py -3.10 -m unittest discover -s tests +py -3.10 -m ruff check app tests main.py +py -3.10 -m compileall app main.py +git diff --check +``` + +## 边界(不改什么) + +- 不把客户端开关当成服务端授权凭据。 +- 不在订阅状态接口中返回“是否调用本接口”的开关。 +- 不新增同步启动阻塞请求,不把网络故障等同于未付费。 +- 不提交 API Key、账号、套餐响应、配置、数据库、日志或业务数据。 + +## 执行记录 + +- 2026-07-28:完成客户端任务定义;等待 cmhub 启动策略接口契约与测试响应交付后解除阻塞。