From 5b3850c191416f4a4e12cc9dc06a88831602097a Mon Sep 17 00:00:00 2001 From: root <1291454411@qq.com> Date: Mon, 14 Sep 2026 03:46:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(M4):=20S-02=E9=83=A8=E7=BD=B2=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF=E5=A4=8D=E6=A0=B8+I-02=E6=9C=AC=E4=BB=93CI=E4=B8=8E?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E6=BC=94=E7=BB=83+=E5=82=A8=E5=A4=87?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=EF=BC=9BM3=E9=81=97=E7=95=99C-03/S-01/C-04?= =?UTF-8?q?=E8=AF=81=E6=8D=AE=E8=A1=A5=E9=BD=90=EF=BC=9B12=E9=A1=B9PROGRES?= =?UTF-8?q?S=E5=BD=92=E5=B1=9E+collect=E8=A7=A3=E6=9E=90=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/planning-ci.yml | 27 ++++++ .gitignore | 1 + PLANNING/PROGRESS_C-02.md | 86 +++++++++--------- PLANNING/PROGRESS_C-03.md | 81 +++++++++-------- PLANNING/PROGRESS_C-04.md | 96 ++++++++++---------- PLANNING/PROGRESS_C-05.md | 90 +++++++++---------- PLANNING/PROGRESS_C-06.md | 108 ++++++++++++----------- PLANNING/PROGRESS_D-01.md | 122 +++++++++++++------------- PLANNING/PROGRESS_D-02.md | 64 +++++++------- PLANNING/PROGRESS_E-03.md | 62 ++++++------- PLANNING/PROGRESS_E-04.md | 66 +++++++------- PLANNING/PROGRESS_E-05.md | 82 ++++++++--------- PLANNING/PROGRESS_I-01.md | 84 +++++++++--------- PLANNING/PROGRESS_I-02.md | 66 ++++++++++++++ PLANNING/PROGRESS_RESERVE_REVIEW.md | 45 ++++++++++ PLANNING/PROGRESS_S-01.md | 94 ++++++++++---------- PLANNING/PROGRESS_S-02.md | 56 ++++++++++++ PLANNING/tools/test_ci_smoke.py | 31 +++++++ docs/e2e-proof/C-03-python-proof.png | Bin 0 -> 58623 bytes docs/e2e-proof/C-03-timeout-proof.png | Bin 0 -> 50254 bytes infra/README.md | 36 ++++++++ infra/backup.sh | 16 ++++ infra/restore.sh | 13 +++ 23 files changed, 827 insertions(+), 499 deletions(-) create mode 100644 .gitea/workflows/planning-ci.yml create mode 100644 PLANNING/PROGRESS_I-02.md create mode 100644 PLANNING/PROGRESS_RESERVE_REVIEW.md create mode 100644 PLANNING/PROGRESS_S-02.md create mode 100644 PLANNING/tools/test_ci_smoke.py create mode 100644 docs/e2e-proof/C-03-python-proof.png create mode 100644 docs/e2e-proof/C-03-timeout-proof.png create mode 100644 infra/README.md create mode 100644 infra/backup.sh create mode 100644 infra/restore.sh diff --git a/.gitea/workflows/planning-ci.yml b/.gitea/workflows/planning-ci.yml new file mode 100644 index 0000000..f7efaf7 --- /dev/null +++ b/.gitea/workflows/planning-ci.yml @@ -0,0 +1,27 @@ +name: vscode-planning-ci + +# I-02 · 本仓 CI(Gitea Actions / GitHub Actions 兼容:纯 push/pull_request 触发,无私有凭据)。 +on: + push: + pull_request: + +jobs: + planning-smoke: + runs-on: ubuntu-latest + defaults: + run: + working-directory: PLANNING + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: 调度器只读命令 + run: | + python tasks.py list + python tasks.py status + python tasks.py collect + - name: CI 烟雾测试 + run: | + pip install pytest -q + python -m pytest tools/test_ci_smoke.py -q diff --git a/.gitignore b/.gitignore index 8a0b63b..eb4d9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ chunyu_project_react/ node_modules/ shots/ docker.env +backups/ diff --git a/PLANNING/PROGRESS_C-02.md b/PLANNING/PROGRESS_C-02.md index 33d8eb0..8e21cac 100644 --- a/PLANNING/PROGRESS_C-02.md +++ b/PLANNING/PROGRESS_C-02.md @@ -1,42 +1,44 @@ -# PROGRESS_C-02 · 工具热榜 + 最近使用 - -> 日期 2026-09-12/13 · 范围:可乐工具(`chunyu_project/tool` + `chunyu_project_react`) - -## 现状 vs 差距(接手时) -- 已有:`ToolUsageRecord` 模型+迁移、`ToolTopView`(仅 `period=total|week`)、`increment` 双写、Home 热榜横条(`period=total`)。 -- 缺口:无 `GET /api/tool/recent`;榜单缺 `range=all|week` 口径与 `rank/weekly_count` 字段;无 `/top` 页面;首页无空态/周榜徽标;零聚合单测。 - -## 改动 -后端(`chunyu_project/tool/`): -1. `views/tool_manage_view.py`:`ToolTopView` 支持 `range=all|week`(兼容旧 `period=total|week`), - 返回项统一含 `usage_count/weekly_count/weekly_usage/rank`;新增 `ToolRecentView` - (登录,去重按 `-created_at,-id` 倒序)。 -2. `views/__init__.py`、`urls.py`:挂载 `path('recent/', ToolRecentView)`。 -3. `management/commands/seed_tools.py`:新增 `AI 工具` 分类(给 C-04 用)+ `AI 图片放大` / - `代码在线运行` 两个工具种子。 -4. `tests/test_tool_top.py`(新建,9 条):总榜排序+rank、period 别名、周聚合、 - 7 天窗口边界(±60s)、空周回退总榜、匿名/登录双写、recent 401、recent 去重排序。 - -前端(`chunyu_project_react/src/`): -5. `pages/Top/`(新建 `Top.tsx`+`Top.css`):`/top` 总榜/周榜切换 + 排名 + “本周 XX 人在用”。 -6. `App.tsx`:`Top` 懒加载路由 `/top`。 -7. `utils/request.ts`:`tool.getTop` 沿用 + 新增 `tool.getRecent`。 -8. `pages/Home/Home.tsx`:热榜改 `range=all`,查看全部指向 `/top`,加空态 + - 周榜徽标;`Home.css` 加 `.home-utility-card-weekly`、`.home-top-empty`。 - -## 口径说明 -- 存量 `Tool.usage_count` 继续为累计口径(不清零、不重算);周榜用 `ToolUsageRecord` - 近 7 天聚合;两者差异为预期(明细只从双写上线日起算),文档以此为准。 - -## 验证 -```bash -DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../chunyu_project/dev_db.sqlite3" \ -python ".../chunyu_project/manage.py" test tool -v 1 -# Ran 15 tests … OK(6 存量 + 9 新增;含一次 recent 排序稳定性修复:order_by 加 -id 兜底) -tsc --noEmit -p chunyu_project_react/tsconfig.json # 零报错 -vite build # 成功;Top/Upscaler/CodeRunner/pyodide-runner 均为独立 chunk -``` -手工:同工具连调 3 次 increment → `GET /tool/top/?range=week` weekly_count=3(单测已覆盖同等价)。 - -## 状态 -done。遗留:`/top` 页 i18n 文案为中文硬编码(多语言 key 后续补)。 +# PROGRESS — C-02 · 工具热榜 + 最近使用 + +- 状态:done + +> 日期 2026-09-12/13 · 范围:可乐工具(`chunyu_project/tool` + `chunyu_project_react`) + +## 现状 vs 差距(接手时) +- 已有:`ToolUsageRecord` 模型+迁移、`ToolTopView`(仅 `period=total|week`)、`increment` 双写、Home 热榜横条(`period=total`)。 +- 缺口:无 `GET /api/tool/recent`;榜单缺 `range=all|week` 口径与 `rank/weekly_count` 字段;无 `/top` 页面;首页无空态/周榜徽标;零聚合单测。 + +## 改动 +后端(`chunyu_project/tool/`): +1. `views/tool_manage_view.py`:`ToolTopView` 支持 `range=all|week`(兼容旧 `period=total|week`), + 返回项统一含 `usage_count/weekly_count/weekly_usage/rank`;新增 `ToolRecentView` + (登录,去重按 `-created_at,-id` 倒序)。 +2. `views/__init__.py`、`urls.py`:挂载 `path('recent/', ToolRecentView)`。 +3. `management/commands/seed_tools.py`:新增 `AI 工具` 分类(给 C-04 用)+ `AI 图片放大` / + `代码在线运行` 两个工具种子。 +4. `tests/test_tool_top.py`(新建,9 条):总榜排序+rank、period 别名、周聚合、 + 7 天窗口边界(±60s)、空周回退总榜、匿名/登录双写、recent 401、recent 去重排序。 + +前端(`chunyu_project_react/src/`): +5. `pages/Top/`(新建 `Top.tsx`+`Top.css`):`/top` 总榜/周榜切换 + 排名 + “本周 XX 人在用”。 +6. `App.tsx`:`Top` 懒加载路由 `/top`。 +7. `utils/request.ts`:`tool.getTop` 沿用 + 新增 `tool.getRecent`。 +8. `pages/Home/Home.tsx`:热榜改 `range=all`,查看全部指向 `/top`,加空态 + + 周榜徽标;`Home.css` 加 `.home-utility-card-weekly`、`.home-top-empty`。 + +## 口径说明 +- 存量 `Tool.usage_count` 继续为累计口径(不清零、不重算);周榜用 `ToolUsageRecord` + 近 7 天聚合;两者差异为预期(明细只从双写上线日起算),文档以此为准。 + +## 验证 +```bash +DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../chunyu_project/dev_db.sqlite3" \ +python ".../chunyu_project/manage.py" test tool -v 1 +# Ran 15 tests … OK(6 存量 + 9 新增;含一次 recent 排序稳定性修复:order_by 加 -id 兜底) +tsc --noEmit -p chunyu_project_react/tsconfig.json # 零报错 +vite build # 成功;Top/Upscaler/CodeRunner/pyodide-runner 均为独立 chunk +``` +手工:同工具连调 3 次 increment → `GET /tool/top/?range=week` weekly_count=3(单测已覆盖同等价)。 + +## 状态 +done。遗留:`/top` 页 i18n 文案为中文硬编码(多语言 key 后续补)。 diff --git a/PLANNING/PROGRESS_C-03.md b/PLANNING/PROGRESS_C-03.md index 87a3de8..5958e6f 100644 --- a/PLANNING/PROGRESS_C-03.md +++ b/PLANNING/PROGRESS_C-03.md @@ -1,38 +1,43 @@ -# PROGRESS_C-03 · 在线代码运行器(JS+Python,学练一体) - -> 日期 2026-09-13 · 范围:`chunyu_project_react`(纯前端沙箱,零服务端执行) - -## 现状 vs 差距(接手时) -- 已有:`CodeRunner.tsx`(iframe sandbox + `new Function` + 15s 超时)、`CodeRunnerPage`(`/utility/code-runner`)、 - CourseLearn “在线练习” Tab(无章节装载)。 -- 缺口 vs 任务卡:非 Worker 执行、超时 15s(要求 3s)、Pyodide 打进主包(无懒加载)、 - 章节代码不能一键装载。 - -## 改动(`src/components/CodeRunner/` + CourseLearn) -1. `worker-js.ts`(新建):JS 执行 Worker(Blob 内联)。Worker 内封堵 - `importScripts/fetch/XHR/WebSocket/EventSource`;主线程无 `eval`/`new Function`; - `RUN_TIMEOUT_MS = 3000`,超时主线程 `terminate()`。 -2. `pyodide-runner.ts`(新建):Pyodide 0.26.4(jsdelivr 锁定版本)懒加载单例, - `import()` 动态引入(独立 chunk `pyodide-runner-*`),默认断网(不 loadPackage)。 -3. `CodeRunner.tsx`(重写):Worker 执行 JS(3s terminate)/ Pyodide 执行 Python - (超时丢弃迟到结果);输出上限 500 行;状态行(运行中/超时/沙箱说明); - `extractFirstCodeBlock()` 导出供章节装载。 -4. `CourseLearn.tsx`:章节 md 载入时抽首个 ``` 代码块 → “随堂运行” Tab 显示 - “装载本章代码”条,一键装入运行器;`CourseLearn.css` 加 `.practice-snippet-bar`。 - -## 安全自查(任务卡要求交 gpt-5.6-luna 审查——本环境无该模型,做同等自查) -- [x] worker 内无 DOM 访问(Worker 线程天然无 DOM) -- [x] 主线程无 `eval`/`new Function`(grep 验证:仅 worker 源码字符串内含 indirect eval) -- [x] Pyodide 不加载额外包(无 loadPackage 调用) -- [x] 超时 3s:JS terminate / Python 丢弃迟到结果 -- [x] 无服务端执行面(后端零改动) - -## 验证 -```bash -tsc --noEmit -p tsconfig.json # 零报错 -vite build # 成功;CodeRunner-*.js / CodeRunnerPage-*.js / pyodide-runner-*.js 独立 chunk,主包未吸入 Pyodide -``` -手工待补(需浏览器):`while(1){}` 3 秒终止且页不冻;中文输出;Pyodide 首载 loading/断网降级。 - -## 状态 -done(代码+自查完成;浏览器手工三例留待联调时录屏补证据)。 +# PROGRESS — C-03 · 在线代码运行器(JS+Python,学练一体) + +- 状态:done + +> 日期 2026-09-13 · 范围:`chunyu_project_react`(纯前端沙箱,零服务端执行) + +## 现状 vs 差距(接手时) +- 已有:`CodeRunner.tsx`(iframe sandbox + `new Function` + 15s 超时)、`CodeRunnerPage`(`/utility/code-runner`)、 + CourseLearn “在线练习” Tab(无章节装载)。 +- 缺口 vs 任务卡:非 Worker 执行、超时 15s(要求 3s)、Pyodide 打进主包(无懒加载)、 + 章节代码不能一键装载。 + +## 改动(`src/components/CodeRunner/` + CourseLearn) +1. `worker-js.ts`(新建):JS 执行 Worker(Blob 内联)。Worker 内封堵 + `importScripts/fetch/XHR/WebSocket/EventSource`;主线程无 `eval`/`new Function`; + `RUN_TIMEOUT_MS = 3000`,超时主线程 `terminate()`。 +2. `pyodide-runner.ts`(新建):Pyodide 0.26.4(jsdelivr 锁定版本)懒加载单例, + `import()` 动态引入(独立 chunk `pyodide-runner-*`),默认断网(不 loadPackage)。 +3. `CodeRunner.tsx`(重写):Worker 执行 JS(3s terminate)/ Pyodide 执行 Python + (超时丢弃迟到结果);输出上限 500 行;状态行(运行中/超时/沙箱说明); + `extractFirstCodeBlock()` 导出供章节装载。 +4. `CourseLearn.tsx`:章节 md 载入时抽首个 ``` 代码块 → “随堂运行” Tab 显示 + “装载本章代码”条,一键装入运行器;`CourseLearn.css` 加 `.practice-snippet-bar`。 + +## 安全自查(任务卡要求交 gpt-5.6-luna 审查——本环境无该模型,做同等自查) +- [x] worker 内无 DOM 访问(Worker 线程天然无 DOM) +- [x] 主线程无 `eval`/`new Function`(grep 验证:仅 worker 源码字符串内含 indirect eval) +- [x] Pyodide 不加载额外包(无 loadPackage 调用) +- [x] 超时 3s:JS terminate / Python 丢弃迟到结果 +- [x] 无服务端执行面(后端零改动) + +## 验证 +```bash +tsc --noEmit -p tsconfig.json # 零报错 +vite build # 成功;CodeRunner-*.js / CodeRunnerPage-*.js / pyodide-runner-*.js 独立 chunk,主包未吸入 Pyodide +``` +手工已补(2026-09-14,`vite preview` + Playwright,`/utility/code-runner`): +JS 默认用例中文输出正常;死循环 `while(1){}` → `[Error] 执行超时(>3s),已终止`,页不冻 +(证据 `docs/e2e-proof/C-03-timeout-proof.png`);Python 默认用例 → 中文 3 行 + `sum = 6` +(证据 `docs/e2e-proof/C-03-python-proof.png`)。 + +## 状态 +done(代码+自查完成;浏览器手工三例留待联调时录屏补证据)。 diff --git a/PLANNING/PROGRESS_C-04.md b/PLANNING/PROGRESS_C-04.md index f5d4f66..57d9a52 100644 --- a/PLANNING/PROGRESS_C-04.md +++ b/PLANNING/PROGRESS_C-04.md @@ -1,44 +1,52 @@ -# PROGRESS_C-04 · AI 工具(图片放大最小闭环 + 钱包方案) - -> 日期 2026-09-13 · 范围:`chunyu_project/aitool`(新建)+ `chunyu_project_react` - -## 选型 -四件套中选**图片放大**做其一最小闭环:Pillow 本地 LANCZOS 2x/4x, -零 key、零外部依赖——mock 链路即生产链路,不被真实 key 阻塞。 - -## 改动 -后端(`chunyu_project/aitool/`,新建 app,已注册 settings + `api/aitool/` 路由): -1. `models.py`:`AIToolRecord`(user/tool/status/cost/scale/input/output/file/created_at)。 -2. `providers.py`:`BaseProvider` + `PillowProvider`(2x/4x,10MB/4096px 上限)+ - `HTTPProvider` 预留(`AITOOL_API_BASE/KEY` 环境变量,未配则 503 口径错误,绝不落库)。 -3. `views/upscale_view.py`:`POST /api/aitool/upscale`(登录)——事务+行锁扣积分 - (`AITOOL_UPSCALE_COST`,默认 2)→ 处理 → 成功落盘 `media/aitool/upscale/` + - FileResponse(`X-AITool-Cost/Balance/File` 头)/ 失败自动回滚(earn 流水 + - refunded 记录);402 `INSUFFICIENT_POINTS` 余额不足口径; - `GET /api/aitool/records` 本人流水。缓存装饰器用项目惯例 `async_never_cache_dispatch` - (`never_cache` 会炸异步视图,已踩坑修复)。 -4. `migrations/0001_initial.py`、`admin.py`、`tests/test_upscale.py`(7 条): - 成功链路(尺寸 2x/4x 断言)、余额不足 402、非法 scale、坏图回滚(积分不变+earn 流水)、 - records、匿名 401。 - -前端: -5. `pages/ImageUpscaler/`(新建):上传/2x|4x/预览/下载/扣费确认 + 402 引导签到。 -6. `utils/request.ts`:`aitool.upscale`(blob)+ `getRecords`;`App.tsx` 路由 - `/utility/image-upscaler`;seed 挂 `AI 工具` 分类 + 金币标注(描述含“2 积分/次”)。 -7. `docker.env.example`:`AITOOL_PROVIDER/AITOOL_UPSCALE_COST/API_BASE/API_KEY` 说明。 - -## 钱包方案(与钱包打通仅做方案不落地计费) -- 落地:积分(`PointTransaction` spend/earn,事务 + `select_for_update`,并发安全)。 -- 不落地:y币/现金计费、异步队列(同步+超时控制,量大再评审 Celery)、本地大模型 - (rembg/Real-ESRGAN 留 Phase 2)。key 仅环境变量,grep 全仓无残留(除读取代码与示例注释)。 - -## 验证 -```bash -DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../dev_db.sqlite3" \ -python ".../manage.py" test tool aitool -v 1 -# Ran 22 tests … OK(tool 15 + aitool 7) -tsc --noEmit # 零报错;vite build 成功,ImageUpscaler 独立 chunk -``` - -## 状态 -done。遗留:真实网关联调(配 key 切 `AITOOL_PROVIDER=http` 即切);抠图/证件照/生图三件套待排期。 +# PROGRESS — C-04 · AI 工具(图片放大最小闭环 + 钱包方案) + +- 状态:done + +> 日期 2026-09-13 · 范围:`chunyu_project/aitool`(新建)+ `chunyu_project_react` + +## 选型 +四件套中选**图片放大**做其一最小闭环:Pillow 本地 LANCZOS 2x/4x, +零 key、零外部依赖——mock 链路即生产链路,不被真实 key 阻塞。 + +## 改动 +后端(`chunyu_project/aitool/`,新建 app,已注册 settings + `api/aitool/` 路由): +1. `models.py`:`AIToolRecord`(user/tool/status/cost/scale/input/output/file/created_at)。 +2. `providers.py`:`BaseProvider` + `PillowProvider`(2x/4x,10MB/4096px 上限)+ + `HTTPProvider` 预留(`AITOOL_API_BASE/KEY` 环境变量,未配则 503 口径错误,绝不落库)。 +3. `views/upscale_view.py`:`POST /api/aitool/upscale`(登录)——事务+行锁扣积分 + (`AITOOL_UPSCALE_COST`,默认 2)→ 处理 → 成功落盘 `media/aitool/upscale/` + + FileResponse(`X-AITool-Cost/Balance/File` 头)/ 失败自动回滚(earn 流水 + + refunded 记录);402 `INSUFFICIENT_POINTS` 余额不足口径; + `GET /api/aitool/records` 本人流水。缓存装饰器用项目惯例 `async_never_cache_dispatch` + (`never_cache` 会炸异步视图,已踩坑修复)。 +4. `migrations/0001_initial.py`、`admin.py`、`tests/test_upscale.py`(7 条): + 成功链路(尺寸 2x/4x 断言)、余额不足 402、非法 scale、坏图回滚(积分不变+earn 流水)、 + records、匿名 401。 + +前端: +5. `pages/ImageUpscaler/`(新建):上传/2x|4x/预览/下载/扣费确认 + 402 引导签到。 +6. `utils/request.ts`:`aitool.upscale`(blob)+ `getRecords`;`App.tsx` 路由 + `/utility/image-upscaler`;seed 挂 `AI 工具` 分类 + 金币标注(描述含“2 积分/次”)。 +7. `docker.env.example`:`AITOOL_PROVIDER/AITOOL_UPSCALE_COST/API_BASE/API_KEY` 说明。 + +## 钱包方案(与钱包打通仅做方案不落地计费) +- 落地:积分(`PointTransaction` spend/earn,事务 + `select_for_update`,并发安全)。 +- 不落地:y币/现金计费、异步队列(同步+超时控制,量大再评审 Celery)、本地大模型 + (rembg/Real-ESRGAN 留 Phase 2)。key 仅环境变量,grep 全仓无残留(除读取代码与示例注释)。 + +## 验证 +```bash +DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../dev_db.sqlite3" \ +python ".../manage.py" test tool aitool -v 1 +# Ran 22 tests … OK(tool 15 + aitool 7) +tsc --noEmit # 零报错;vite build 成功,ImageUpscaler 独立 chunk +AITOOL_PROVIDER=http python -c "get_provider().upscale(...)" +# provider: HTTPProvider,base/key 为空 → RuntimeError: AITOOL_API_BASE / AITOOL_API_KEY 未配置 +# (即视图层 503 口径的触发源;未配 key 时绝不落库,已验证) +python manage.py test aitool -v 1 # Ran 7 tests … OK(2026-09-14 复跑) +grep -rn 真实 key 正则 aitool/*.py → 零命中(仅 API_KEY/API_BASE 读取与注释) +``` + +## 状态 +done。网关切换已验证(未配 key → 503 口径,不落库);有 key 环境配 `AITOOL_PROVIDER=http` + +`AITOOL_API_BASE/KEY` 即切真实网关。遗留:抠图/证件照/生图三件套待排期。 diff --git a/PLANNING/PROGRESS_C-05.md b/PLANNING/PROGRESS_C-05.md index 02e31e6..913e7f1 100644 --- a/PLANNING/PROGRESS_C-05.md +++ b/PLANNING/PROGRESS_C-05.md @@ -1,44 +1,46 @@ -# PROGRESS_C-05 · 工具页 SEO 预渲染 - -> 日期 2026-09-13 · 范围:`chunyu_project_react`(构建层脚本+nginx.conf)+ 校验脚本 - -## 现状 vs 差距(接手时) -- 已有:Vite SPA(`dist/index.html` 纯空 `#root` + 单一 `
/switch/` 校验 membership → 返回 `{code,name,role}` +
- 写审计(`tenant_switch`);前端换 `localStorage.tenant` + 整页 reload(会话隔离无脏数据)。
-- 兼容策略:存量 membership 不动;最后一个 owner 禁止移除/降级/禁用(服务端事务内强保)。
-
-## 现状 vs 差距(接手时)
-- 已有:membership 模型 + 迁移回填 + `TenantMembershipPermission` 服务端强制(iter6,8 用例)+
- 注册建 owner(P0-3)+ 前端 localStorage 租户头。
-- 缺口:无切换端点、无成员管理接口、无前端切换器/成员页/无权限页。
-
-## 改动
-后端(`backend/apps/core/` + `backend/tests/`):
-1. `tenants.py`(新建):`my_tenants`(我的租户+role)、`switch_tenant`(校验+审计)、
- `tenant_members`(GET 列表 / POST 邀请:admin 仅 member,新用户≥8位密码事务创建)、
- `tenant_member_detail`(PATCH 改角色/禁用 / DELETE 移除:admin 不动 owner/admin,
- 移除仅 owner,最后一个 owner 三禁)。
-2. `urls.py`:四条路由挂 `core_urlpatterns`(即 `/api/v1/tenants/...`)。
-3. `tests/test_tenant_switch.py`(新建,12 条):切换 200+role/无权 403/未知 400/mine 双租户/
- member 邀 403/owner 邀 201+被邀可切/admin 邀 owner 403/admin 动 owner 403/
- 跨租户成员页 403/删最后 owner 400/owner 删 member/禁用后资源 403。
-
-前端(`frontend/src/`):
-4. `api/tenants.js`(新建):mine/switchTo/members/invite/updateMember/removeMember。
-5. `pages/Members.vue`(新建):成员表(角色/状态标签)+ 邀请 dialog(新老用户兼容)+
- 改角色/禁用启用/移除(owner 专属按钮)+ 403 跳无权限页。
-6. `pages/NoPermission.vue`(新建):无权限页 + 我的租户一键进入。
-7. `layouts/MainLayout.vue`:租户 tag → 下拉切换器(`mine()` 列表,切后整页 reload)+
- “成员”按钮 + 菜单加“租户成员”;`router/index.js` 加 `/members` + `/no-permission`。
-
-## 边界遵守
-- 无计费/套餐;单据/库存业务零改;权限全在服务端(前端只做展示与跳转)。
-
-## 验证
-```bash
-pytest tests/test_tenant_switch.py tests/test_membership.py # 20 passed(12 新增 + 8 存量)
-pytest -p no:cacheprovider # 524 passed, 4 skipped(512 基线 + 12 新增)
-npm test # 17 pass / 0 fail
-npm run build # ✓ built in 6.13s
-manage.py makemigrations --check --dry-run # No changes detected
-manage.py check # 0 issues
-```
-
-## 状态
-done。遗留:切换端到端(前端下拉 → switch → reload 无脏数据)需 dev 服务联调时录屏补证据;
-demo token 流不走 membership(既有旁路,未动)。
+# PROGRESS — D-01 · 用户-租户 membership + 跨租户切换
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `gj/dealerhub` 目录(后端 core + 前端 tenants/members/切换器)
+
+## 设计(阶段一,任务卡要求先评审再动手——本轮设计与实现一致,评审记录在此)
+- 模型(存量复用):`TenantMembership(user, tenant, role[owner|admin|member], is_active, joined_at)`,
+ 唯一约束 `(user, tenant)`;存量回填(0002 迁移:超管=全租户,普通=default)iter6 已验证。
+- 授权矩阵:
+ | 角色 | 切租户/看成员 | 邀请 | 改角色/禁用 | 移除 |
+ |---|---|---|---|---|
+ | owner | ✅ | ✅(任意角色) | ✅ | ✅(专属) |
+ | admin | ✅ | ✅(仅 member) | ✅(仅 member) | ❌ |
+ | member | ✅(自己有 membership 的) | ❌ | ❌ | ❌ |
+ | 非成员 | 403 | 403 | 403 | 403 |
+ | 未知租户 | 400(参数错误,非 403) | 400 | 400 | 400 |
+ | 超管 | 直通(bootstrap 契约) | 直通 | 直通 | 直通 |
+- 切换流程:`POST /api/v1/tenants//switch/` 校验 membership → 返回 `{code,name,role}` +
+ 写审计(`tenant_switch`);前端换 `localStorage.tenant` + 整页 reload(会话隔离无脏数据)。
+- 兼容策略:存量 membership 不动;最后一个 owner 禁止移除/降级/禁用(服务端事务内强保)。
+
+## 现状 vs 差距(接手时)
+- 已有:membership 模型 + 迁移回填 + `TenantMembershipPermission` 服务端强制(iter6,8 用例)+
+ 注册建 owner(P0-3)+ 前端 localStorage 租户头。
+- 缺口:无切换端点、无成员管理接口、无前端切换器/成员页/无权限页。
+
+## 改动
+后端(`backend/apps/core/` + `backend/tests/`):
+1. `tenants.py`(新建):`my_tenants`(我的租户+role)、`switch_tenant`(校验+审计)、
+ `tenant_members`(GET 列表 / POST 邀请:admin 仅 member,新用户≥8位密码事务创建)、
+ `tenant_member_detail`(PATCH 改角色/禁用 / DELETE 移除:admin 不动 owner/admin,
+ 移除仅 owner,最后一个 owner 三禁)。
+2. `urls.py`:四条路由挂 `core_urlpatterns`(即 `/api/v1/tenants/...`)。
+3. `tests/test_tenant_switch.py`(新建,12 条):切换 200+role/无权 403/未知 400/mine 双租户/
+ member 邀 403/owner 邀 201+被邀可切/admin 邀 owner 403/admin 动 owner 403/
+ 跨租户成员页 403/删最后 owner 400/owner 删 member/禁用后资源 403。
+
+前端(`frontend/src/`):
+4. `api/tenants.js`(新建):mine/switchTo/members/invite/updateMember/removeMember。
+5. `pages/Members.vue`(新建):成员表(角色/状态标签)+ 邀请 dialog(新老用户兼容)+
+ 改角色/禁用启用/移除(owner 专属按钮)+ 403 跳无权限页。
+6. `pages/NoPermission.vue`(新建):无权限页 + 我的租户一键进入。
+7. `layouts/MainLayout.vue`:租户 tag → 下拉切换器(`mine()` 列表,切后整页 reload)+
+ “成员”按钮 + 菜单加“租户成员”;`router/index.js` 加 `/members` + `/no-permission`。
+
+## 边界遵守
+- 无计费/套餐;单据/库存业务零改;权限全在服务端(前端只做展示与跳转)。
+
+## 验证
+```bash
+pytest tests/test_tenant_switch.py tests/test_membership.py # 20 passed(12 新增 + 8 存量)
+pytest -p no:cacheprovider # 524 passed, 4 skipped(512 基线 + 12 新增)
+npm test # 17 pass / 0 fail
+npm run build # ✓ built in 6.13s
+manage.py makemigrations --check --dry-run # No changes detected
+manage.py check # 0 issues
+```
+
+## 状态
+done。遗留:切换端到端(前端下拉 → switch → reload 无脏数据)需 dev 服务联调时录屏补证据;
+demo token 流不走 membership(既有旁路,未动)。
diff --git a/PLANNING/PROGRESS_D-02.md b/PLANNING/PROGRESS_D-02.md
index eb47496..0e4e654 100644
--- a/PLANNING/PROGRESS_D-02.md
+++ b/PLANNING/PROGRESS_D-02.md
@@ -1,31 +1,33 @@
-# PROGRESS_D-02 · PG 并发测试环境(激活 4 个 skipped)
-
-> 日期 2026-09-13 · 范围:只改 `gj/dealerhub` 目录(新建 `backend/docs/pgtest.md` 一文件,零代码变更)
-
-## 现状 vs 差距(接手时)
-- 已有:`config/settings/pgtest.py`(PG DSN,环境变量覆盖)+ `scripts/setup_pg.py`(建库+迁移)+
- `tests/test_concurrency.py` 4 个 `@pytest.mark.postgres` 真并发用例 +
- `conftest.py` 自动跳过守卫 + `pytest.ini` marker 注册。
-- 缺口(任务卡验收项):`backend/docs/pgtest.md` 缺失;4 用例需在本机真实跑通贴输出。
-
-## 改动
-1. `backend/docs/pgtest.md`(新建):为什么必须真 PG、环境准备一条命令、运行一条命令
- (含 Windows Git Bash 实测命令)、隔离保证、3 个常见坑、本轮实测数字。
-
-## 验证(本机 PG 16,127.0.0.1:5433,全部实跑)
-```bash
-python scripts/setup_pg.py
-# ✓ dealerhub / dealerhub_test 幂等创建 + 开发库迁移完成
-DJANGO_SETTINGS_MODULE=config.settings.pgtest python -m pytest tests/test_concurrency.py -p no:cacheprovider -m postgres
-# 4 passed, 11 deselected in 11.20s
-DJANGO_SETTINGS_MODULE=config.settings.pgtest python -m pytest -p no:cacheprovider
-# 516 passed in 100.25s(0 skipped)
-python -m pytest tests/test_concurrency.py -p no:cacheprovider -q # 默认 SQLite
-# 4 skipped(守卫提示语正确),默认行为不变
-python manage.py makemigrations --check --dry-run # No changes detected
-python manage.py check # 0 issues
-```
-
-## 状态
-done。遗留:teardown 阶段 `其他用户正在使用数据库` 警告(并发线程 PG 会话残留,
-pytest 自动处理,不影响结果,文档已收录为常见坑 #2)。
+# PROGRESS — D-02 · PG 并发测试环境(激活 4 个 skipped)
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `gj/dealerhub` 目录(新建 `backend/docs/pgtest.md` 一文件,零代码变更)
+
+## 现状 vs 差距(接手时)
+- 已有:`config/settings/pgtest.py`(PG DSN,环境变量覆盖)+ `scripts/setup_pg.py`(建库+迁移)+
+ `tests/test_concurrency.py` 4 个 `@pytest.mark.postgres` 真并发用例 +
+ `conftest.py` 自动跳过守卫 + `pytest.ini` marker 注册。
+- 缺口(任务卡验收项):`backend/docs/pgtest.md` 缺失;4 用例需在本机真实跑通贴输出。
+
+## 改动
+1. `backend/docs/pgtest.md`(新建):为什么必须真 PG、环境准备一条命令、运行一条命令
+ (含 Windows Git Bash 实测命令)、隔离保证、3 个常见坑、本轮实测数字。
+
+## 验证(本机 PG 16,127.0.0.1:5433,全部实跑)
+```bash
+python scripts/setup_pg.py
+# ✓ dealerhub / dealerhub_test 幂等创建 + 开发库迁移完成
+DJANGO_SETTINGS_MODULE=config.settings.pgtest python -m pytest tests/test_concurrency.py -p no:cacheprovider -m postgres
+# 4 passed, 11 deselected in 11.20s
+DJANGO_SETTINGS_MODULE=config.settings.pgtest python -m pytest -p no:cacheprovider
+# 516 passed in 100.25s(0 skipped)
+python -m pytest tests/test_concurrency.py -p no:cacheprovider -q # 默认 SQLite
+# 4 skipped(守卫提示语正确),默认行为不变
+python manage.py makemigrations --check --dry-run # No changes detected
+python manage.py check # 0 issues
+```
+
+## 状态
+done。遗留:teardown 阶段 `其他用户正在使用数据库` 警告(并发线程 PG 会话残留,
+pytest 自动处理,不影响结果,文档已收录为常见坑 #2)。
diff --git a/PLANNING/PROGRESS_E-03.md b/PLANNING/PROGRESS_E-03.md
index 3eb1510..a777eb2 100644
--- a/PLANNING/PROGRESS_E-03.md
+++ b/PLANNING/PROGRESS_E-03.md
@@ -1,30 +1,32 @@
-# PROGRESS_E-03 · 每日一题 + 全站正确率
-
-> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(后端 core + 前端组件 + tests/e2e)
-
-## 现状 vs 差距(接手时)
-- 已有:`DailyChallengeView`(sha256(日期)%题数确定性选题、幂等、实时聚合)、
- `DailyChallengeCard`(参与数展示)、`ChallengeAttempt(user,date)` 唯一约束。
-- 缺口:E2E 零覆盖;前端提交后不展示全站正确率(“答完看看全站正确率”是空话)。
-
-## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
-1. `tests/e2e/cases_challenge.py`(新建,6 条,group=challenge):
- 同日多用户同题 / 匿名 GET 可见+POST 401 / 2 用户 1 对 1 错聚合 50% /
- 重复提交幂等(参与数不涨+`idempotent` 标记+已答态回显)/ 缺 answer 400 / 三次 GET 稳定同题。
-2. `tests/e2e/run_e2e.py`:注册 `cases_challenge`。
-3. `frontend/src/components/DailyChallengeCard.tsx`:提交后用返回的
- `participants_today/correct_rate` 刷新头部与结果区(“全站正确率 X%”)。
-
-## 验证
-```bash
-python "C:/.../english-drill/tests/e2e/run_e2e.py" -k challenge
-# 合计 6 用例 | 通过 6 | 断言失败 0 | 异常 0
-python "C:/.../english-drill/tests/e2e/run_e2e.py"
-# 合计 124 用例 | 通过 124 | 断言失败 0 | 异常 0(含本轮新增 13)
-tsc --noEmit -p frontend/tsconfig.json # 零报错
-```
-跨日轮换:算法为纯函数(sha256 日期取模),E2E 以“同日稳定同题”锁定确定性;
-跨日用例需 mock 日期,未做(Django timezone mock 在 ASGI 进程内不可靠,诚实记录)。
-
-## 状态
-done。遗留:跨日轮换 mock 用例;推送/提醒(任务卡明确本轮不做)。
+# PROGRESS — E-03 · 每日一题 + 全站正确率
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(后端 core + 前端组件 + tests/e2e)
+
+## 现状 vs 差距(接手时)
+- 已有:`DailyChallengeView`(sha256(日期)%题数确定性选题、幂等、实时聚合)、
+ `DailyChallengeCard`(参与数展示)、`ChallengeAttempt(user,date)` 唯一约束。
+- 缺口:E2E 零覆盖;前端提交后不展示全站正确率(“答完看看全站正确率”是空话)。
+
+## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
+1. `tests/e2e/cases_challenge.py`(新建,6 条,group=challenge):
+ 同日多用户同题 / 匿名 GET 可见+POST 401 / 2 用户 1 对 1 错聚合 50% /
+ 重复提交幂等(参与数不涨+`idempotent` 标记+已答态回显)/ 缺 answer 400 / 三次 GET 稳定同题。
+2. `tests/e2e/run_e2e.py`:注册 `cases_challenge`。
+3. `frontend/src/components/DailyChallengeCard.tsx`:提交后用返回的
+ `participants_today/correct_rate` 刷新头部与结果区(“全站正确率 X%”)。
+
+## 验证
+```bash
+python "C:/.../english-drill/tests/e2e/run_e2e.py" -k challenge
+# 合计 6 用例 | 通过 6 | 断言失败 0 | 异常 0
+python "C:/.../english-drill/tests/e2e/run_e2e.py"
+# 合计 124 用例 | 通过 124 | 断言失败 0 | 异常 0(含本轮新增 13)
+tsc --noEmit -p frontend/tsconfig.json # 零报错
+```
+跨日轮换:算法为纯函数(sha256 日期取模),E2E 以“同日稳定同题”锁定确定性;
+跨日用例需 mock 日期,未做(Django timezone mock 在 ASGI 进程内不可靠,诚实记录)。
+
+## 状态
+done。遗留:跨日轮换 mock 用例;推送/提醒(任务卡明确本轮不做)。
diff --git a/PLANNING/PROGRESS_E-04.md b/PLANNING/PROGRESS_E-04.md
index f966df8..15d5c13 100644
--- a/PLANNING/PROGRESS_E-04.md
+++ b/PLANNING/PROGRESS_E-04.md
@@ -1,32 +1,34 @@
-# PROGRESS_E-04 · 三步引导 + 考试倒计时 + 每日目标
-
-> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(仅新增 tests/e2e 用例 + 注册)
-
-## 现状 vs 差距(接手时)
-- 已有:`UserProfile(exam_date/daily_word_target/onboarding_completed/goal_book_id)`、
- `GoalsView/OnboardingView`(校验:日期 YYYY-MM-DD、词量 1-200、`days_to_exam=max(0)`)、
- `OnboardingPage` 三步页、首页 goal-summary 倒计时条。
-- 缺口:E2E 零覆盖。后端/前端实现完整,本轮只补回归。
-
-## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
-1. `tests/e2e/cases_onboarding.py`(新建,7 条,group=onboarding):
- 新用户默认(20 词/未完成/无日期)/ 三步提交闭环(240 天倒计时)/
- 倒计时精确(1/30/100 天 + 过去日期钳制 0,与 `_goal_payload` max(0) 口径对齐)/
- 改目标后计划新词 ≤ 目标 / 老用户空 exam_date 全链路(目标+统计+计划)/
- 非法输入 400x3 / 未登录 401x2。
-2. `tests/e2e/run_e2e.py`:注册 `cases_onboarding`。
-
-## 口径校准(诚实记录)
-- 过去日期倒计时:初版用例断言 `<= 0`,跑前核对后端为 `max(0, days)`,已改断言为 `== 0`。
-- “修改目标后计划随之变化”:计划接口返回结构为 `cards_new/cards` 混合,用例取三键和并断言 `≤ 目标`。
-
-## 验证
-```bash
-python "C:/.../english-drill/tests/e2e/run_e2e.py" -k onboarding
-# 合计 7 用例 | 通过 7 | 断言失败 0 | 异常 0
-python "C:/.../english-drill/tests/e2e/run_e2e.py"
-# 合计 124 用例 | 通过 124 | 断言失败 0 | 异常 0(无回归)
-```
-
-## 状态
-done。遗留:无(复杂规划算法任务卡明确不做)。
+# PROGRESS — E-04 · 三步引导 + 考试倒计时 + 每日目标
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(仅新增 tests/e2e 用例 + 注册)
+
+## 现状 vs 差距(接手时)
+- 已有:`UserProfile(exam_date/daily_word_target/onboarding_completed/goal_book_id)`、
+ `GoalsView/OnboardingView`(校验:日期 YYYY-MM-DD、词量 1-200、`days_to_exam=max(0)`)、
+ `OnboardingPage` 三步页、首页 goal-summary 倒计时条。
+- 缺口:E2E 零覆盖。后端/前端实现完整,本轮只补回归。
+
+## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
+1. `tests/e2e/cases_onboarding.py`(新建,7 条,group=onboarding):
+ 新用户默认(20 词/未完成/无日期)/ 三步提交闭环(240 天倒计时)/
+ 倒计时精确(1/30/100 天 + 过去日期钳制 0,与 `_goal_payload` max(0) 口径对齐)/
+ 改目标后计划新词 ≤ 目标 / 老用户空 exam_date 全链路(目标+统计+计划)/
+ 非法输入 400x3 / 未登录 401x2。
+2. `tests/e2e/run_e2e.py`:注册 `cases_onboarding`。
+
+## 口径校准(诚实记录)
+- 过去日期倒计时:初版用例断言 `<= 0`,跑前核对后端为 `max(0, days)`,已改断言为 `== 0`。
+- “修改目标后计划随之变化”:计划接口返回结构为 `cards_new/cards` 混合,用例取三键和并断言 `≤ 目标`。
+
+## 验证
+```bash
+python "C:/.../english-drill/tests/e2e/run_e2e.py" -k onboarding
+# 合计 7 用例 | 通过 7 | 断言失败 0 | 异常 0
+python "C:/.../english-drill/tests/e2e/run_e2e.py"
+# 合计 124 用例 | 通过 124 | 断言失败 0 | 异常 0(无回归)
+```
+
+## 状态
+done。遗留:无(复杂规划算法任务卡明确不做)。
diff --git a/PLANNING/PROGRESS_E-05.md b/PLANNING/PROGRESS_E-05.md
index 96ef742..e50916e 100644
--- a/PLANNING/PROGRESS_E-05.md
+++ b/PLANNING/PROGRESS_E-05.md
@@ -1,40 +1,42 @@
-# PROGRESS_E-05 · 公开内容页 SEO(词/题/词书)
-
-> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(后端 core/apps.content/config + 前端 pages/App + tests/e2e)
-
-## 选型
-**Django 模板直出**(与 `views_web.spa` 同栈),理由:后端已有 async 视图 + 完整 ORM,
-三条路由直出 HTML 零构建依赖;不做全站 SSR 迁移,不动登录内页面。
-
-## 现状 vs 差距(接手时)
-- 已有:440 word 卡 + 519 题 + 5 词书已入库;`public_question` 白名单(无答案);
- E2E 124 用例全绿;前端全登录内路由。
-- 缺口:整站登录后可见(首屏登录墙 E-02 已拆,但内容页仍需登录),440 词零长尾入口。
-
-## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
-1. `backend/core/views_seo.py`(新建):`SeoWordView`(/word/\,大小写归一,重词 order_by id 确定性),
- `SeoQuestionView`(/question/\,public_question 白名单,**不含答案/解析**),
- `SeoBookView`(/book/\,词数/章节概览),`seo_sitemap`(421 词 + 519 题 + 5 词书 = 945 URL,
- loc 站内相对路径,部署层拼域名),`seo_robots`。每页独立 title/description/OG/canonical + 注册引导(from=word-/question-/book-)。
-2. `backend/config/urls.py`:五条路由挂在 **SPA 兜底之前**。
-3. `backend/apps/content/views.py` + `config/urls.py`:`WordLookupView`(`GET /api/content/word/`,
- 公开白名单:释义/音标/例句/同课相关词,无用户数据)。
-4. 前端:`PublicWordPage/PublicQuestionPage/PublicBookPage`(调已有 API + 注册引导,
- 单题页主动剥离 answer/explanation);`App.tsx` 加 `/word/:front /question/:qid /book/:bid` 公开路由。
-5. `tests/e2e/cases_seo.py`(新建,7 条,group=seo)+ `run_e2e.py` 注册:
- 单词正文/大小写/404/单题无泄露(5 项白名单审查)/词书概览/查词白名单/sitemap 覆盖数。
-
-## 验证
-```bash
-python "C:/.../english-drill/tests/e2e/run_e2e.py" -k seo
-# 合计 7 用例 | 通过 7 | 断言失败 0 | 异常 0(sitemap word=421 q=519 book=5)
-python "C:/.../english-drill/tests/e2e/run_e2e.py"
-# 合计 133 用例 | 通过 133 | 断言失败 0 | 异常 0(124 存量 + 7 seo + 2 波动恢复,无回归)
-tsc --noEmit -p frontend/tsconfig.json # 零报错(修 SkeletonBlock h prop + BankDetail unknown 中转)
-curl 等价:/word/abandon → abandon 是什么意思… + 释义正文(E2E 同等价覆盖)
-```
-踩坑:harness `hit()` 默认按 JSON 解析——直出 HTML/XML 用例改原生 resp 断状态码 + 正文关键字。
-
-## 状态
-done。遗留:sitemap loc 为站内相对路径(部署 nginx 按真实域名拼 canonical,见 deploy/nginx.conf 接线时处理);
-词面 440 卡去重 421(19 个跨词书重词,重词页 order_by id 取首个,诚实记录)。
+# PROGRESS — E-05 · 公开内容页 SEO(词/题/词书)
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `english-drill` 目录(后端 core/apps.content/config + 前端 pages/App + tests/e2e)
+
+## 选型
+**Django 模板直出**(与 `views_web.spa` 同栈),理由:后端已有 async 视图 + 完整 ORM,
+三条路由直出 HTML 零构建依赖;不做全站 SSR 迁移,不动登录内页面。
+
+## 现状 vs 差距(接手时)
+- 已有:440 word 卡 + 519 题 + 5 词书已入库;`public_question` 白名单(无答案);
+ E2E 124 用例全绿;前端全登录内路由。
+- 缺口:整站登录后可见(首屏登录墙 E-02 已拆,但内容页仍需登录),440 词零长尾入口。
+
+## 改动(全部在 `C:/Users/12914/Desktop/english-drill/` 内)
+1. `backend/core/views_seo.py`(新建):`SeoWordView`(/word/\,大小写归一,重词 order_by id 确定性),
+ `SeoQuestionView`(/question/\,public_question 白名单,**不含答案/解析**),
+ `SeoBookView`(/book/\,词数/章节概览),`seo_sitemap`(421 词 + 519 题 + 5 词书 = 945 URL,
+ loc 站内相对路径,部署层拼域名),`seo_robots`。每页独立 title/description/OG/canonical + 注册引导(from=word-/question-/book-)。
+2. `backend/config/urls.py`:五条路由挂在 **SPA 兜底之前**。
+3. `backend/apps/content/views.py` + `config/urls.py`:`WordLookupView`(`GET /api/content/word/`,
+ 公开白名单:释义/音标/例句/同课相关词,无用户数据)。
+4. 前端:`PublicWordPage/PublicQuestionPage/PublicBookPage`(调已有 API + 注册引导,
+ 单题页主动剥离 answer/explanation);`App.tsx` 加 `/word/:front /question/:qid /book/:bid` 公开路由。
+5. `tests/e2e/cases_seo.py`(新建,7 条,group=seo)+ `run_e2e.py` 注册:
+ 单词正文/大小写/404/单题无泄露(5 项白名单审查)/词书概览/查词白名单/sitemap 覆盖数。
+
+## 验证
+```bash
+python "C:/.../english-drill/tests/e2e/run_e2e.py" -k seo
+# 合计 7 用例 | 通过 7 | 断言失败 0 | 异常 0(sitemap word=421 q=519 book=5)
+python "C:/.../english-drill/tests/e2e/run_e2e.py"
+# 合计 133 用例 | 通过 133 | 断言失败 0 | 异常 0(124 存量 + 7 seo + 2 波动恢复,无回归)
+tsc --noEmit -p frontend/tsconfig.json # 零报错(修 SkeletonBlock h prop + BankDetail unknown 中转)
+curl 等价:/word/abandon → abandon 是什么意思… + 释义正文(E2E 同等价覆盖)
+```
+踩坑:harness `hit()` 默认按 JSON 解析——直出 HTML/XML 用例改原生 resp 断状态码 + 正文关键字。
+
+## 状态
+done。遗留:sitemap loc 为站内相对路径(部署 nginx 按真实域名拼 canonical,见 deploy/nginx.conf 接线时处理);
+词面 440 卡去重 421(19 个跨词书重词,重词页 order_by id 取首个,诚实记录)。
diff --git a/PLANNING/PROGRESS_I-01.md b/PLANNING/PROGRESS_I-01.md
index 6974baf..bf2bc2d 100644
--- a/PLANNING/PROGRESS_I-01.md
+++ b/PLANNING/PROGRESS_I-01.md
@@ -1,41 +1,43 @@
-# PROGRESS_I-01 · 模型能力自动探测(每晚,只报告不写配置)
-
-> 日期 2026-09-13/14 · 范围:只改 `dsh` 目录(新建 `tools/probe_models.py` + 报告目录)
-
-## 现状 vs 差距(接手时)
-- 已有:2026-09-06 手动探测结论(DSH模型能力测试报告.md:b/glm-5.3-flash 与 b/qwen3.8-flash
- 思考✅视觉✅)+ `~/.dsh/model-probe-last.json` 单次快照 + settings.yaml 能力声明注释。
-- 缺口:无自动巡检;无 diff 告警;手动一次即忘,漂移无人知。
-
-## 改动(全部在 `C:/Users/12914/Desktop/dsh/` 内)
-1. `tools/probe_models.py`(新建,单文件,标准库 + urllib,无 httpx 依赖):
- - 只读 `~/.dsh/settings.yaml`(全脚本两个打开点,均为 `mode='r'`;`--check-readonly` 自证无写入路径);
- 无 PyYAML 时降级为最小缩进解析(provider/api/apiKeyEnv/baseURL/models/input/compat/reasoningEfforts)。
- - 枚举 10 分组 23 模型;三组探测:基线(最小对话 200+非空)/ 思考(按 thinkingFormat 发
- enable_thinking+reasoning_effort,验 reasoning 字段 + 推理题答案 6)/ 视觉(内嵌 64px 纯红 PNG 问主色)。
- - openai-responses(ginka)与 openai-completions 双协议;超时 30s/请求;单模型异常全部捕获,
- 失败隔离不中断全量;无 key 分组标记 skip(不判 fail);报告不打印 key。
-2. 产出:`tools/probe-reports/probe-YYYY-MM-DD.json`(机读:verdict/diff)+
- 同名 `.md`(人读:表格 + 与上次 diff + 失败明细 + 配置建议)。
- 告警方向:报告首行——能力变化列 diff,无变化/失败数异常即标红。
-3. 运行:`--all` 全量 / `--provider b` 单组 / `--all --compare` 对比打印;
- Windows 计划任务示例(每晚 02:00 schtasks)写进脚本 docstring。
-
-## 验证
-```bash
-python tools/probe_models.py --check-readonly
-# readonly-check: PASS(settings 打开点仅 mode='r',无写入路径)
-python tools/probe_models.py --all
-# 报告:tools/probe-reports/probe-2026-09-14.json + .md
-# 总 23 · 通过 0 · 失败 0 · 全跳过 23(本 shell 无 provider key,全部 skip: 无 key——预期内,
-# key 在 DSH 进程环境;DSH 宿主机/计划任务环境下有 key 即实测)
-链路自验证(本地 OpenAI 兼容桩 127.0.0.1:18731):
-# baseline True / thinking True / vision True / 无-reasoning判fail / 未声明判skip / 不可达判fail(2s超时)
-# PROBE CHAIN ALL GREEN
-```
-对照 09-06 手动结论:b/glm-5.3-flash 与 b/qwen3.8-flash 的思考(zai + high)+ 视觉(image 声明)
-口径与脚本探测逻辑一致(同参数、同答案 6、同红色判定);有 key 环境跑出的 verdict 可直接对照。
-
-## 状态
-done(最小闭环)。遗留阻塞:本 shell 无 10 个 `*_API_KEY`,全量实测 verdict 需在有 key 的环境
-(DSH 宿主机计划任务)跑一条 `--all` 即出首份非 skip 报告;`--compare` 的 diff 基线即本次报告。
+# PROGRESS — I-01 · 模型能力自动探测(每晚,只报告不写配置)
+
+- 状态:done
+
+> 日期 2026-09-13/14 · 范围:只改 `dsh` 目录(新建 `tools/probe_models.py` + 报告目录)
+
+## 现状 vs 差距(接手时)
+- 已有:2026-09-06 手动探测结论(DSH模型能力测试报告.md:b/glm-5.3-flash 与 b/qwen3.8-flash
+ 思考✅视觉✅)+ `~/.dsh/model-probe-last.json` 单次快照 + settings.yaml 能力声明注释。
+- 缺口:无自动巡检;无 diff 告警;手动一次即忘,漂移无人知。
+
+## 改动(全部在 `C:/Users/12914/Desktop/dsh/` 内)
+1. `tools/probe_models.py`(新建,单文件,标准库 + urllib,无 httpx 依赖):
+ - 只读 `~/.dsh/settings.yaml`(全脚本两个打开点,均为 `mode='r'`;`--check-readonly` 自证无写入路径);
+ 无 PyYAML 时降级为最小缩进解析(provider/api/apiKeyEnv/baseURL/models/input/compat/reasoningEfforts)。
+ - 枚举 10 分组 23 模型;三组探测:基线(最小对话 200+非空)/ 思考(按 thinkingFormat 发
+ enable_thinking+reasoning_effort,验 reasoning 字段 + 推理题答案 6)/ 视觉(内嵌 64px 纯红 PNG 问主色)。
+ - openai-responses(ginka)与 openai-completions 双协议;超时 30s/请求;单模型异常全部捕获,
+ 失败隔离不中断全量;无 key 分组标记 skip(不判 fail);报告不打印 key。
+2. 产出:`tools/probe-reports/probe-YYYY-MM-DD.json`(机读:verdict/diff)+
+ 同名 `.md`(人读:表格 + 与上次 diff + 失败明细 + 配置建议)。
+ 告警方向:报告首行——能力变化列 diff,无变化/失败数异常即标红。
+3. 运行:`--all` 全量 / `--provider b` 单组 / `--all --compare` 对比打印;
+ Windows 计划任务示例(每晚 02:00 schtasks)写进脚本 docstring。
+
+## 验证
+```bash
+python tools/probe_models.py --check-readonly
+# readonly-check: PASS(settings 打开点仅 mode='r',无写入路径)
+python tools/probe_models.py --all
+# 报告:tools/probe-reports/probe-2026-09-14.json + .md
+# 总 23 · 通过 0 · 失败 0 · 全跳过 23(本 shell 无 provider key,全部 skip: 无 key——预期内,
+# key 在 DSH 进程环境;DSH 宿主机/计划任务环境下有 key 即实测)
+链路自验证(本地 OpenAI 兼容桩 127.0.0.1:18731):
+# baseline True / thinking True / vision True / 无-reasoning判fail / 未声明判skip / 不可达判fail(2s超时)
+# PROBE CHAIN ALL GREEN
+```
+对照 09-06 手动结论:b/glm-5.3-flash 与 b/qwen3.8-flash 的思考(zai + high)+ 视觉(image 声明)
+口径与脚本探测逻辑一致(同参数、同答案 6、同红色判定);有 key 环境跑出的 verdict 可直接对照。
+
+## 状态
+done(最小闭环)。遗留阻塞:本 shell 无 10 个 `*_API_KEY`,全量实测 verdict 需在有 key 的环境
+(DSH 宿主机计划任务)跑一条 `--all` 即出首份非 skip 报告;`--compare` 的 diff 基线即本次报告。
diff --git a/PLANNING/PROGRESS_I-02.md b/PLANNING/PROGRESS_I-02.md
new file mode 100644
index 0000000..24768f6
--- /dev/null
+++ b/PLANNING/PROGRESS_I-02.md
@@ -0,0 +1,66 @@
+# PROGRESS — I-02 Gitea CI + 定时备份演练(本仓最小闭环 + 四仓现状)
+
+- 状态:done(本仓闭环;四仓 CI/备份由各仓自行落地,现状如实记录)
+- 执行模型:muse-spark-1.3-contributor(AGI 自驱 M4)
+- 日期:2026-09-14
+- 分支/提交:未提交(本仓只新增下述文件)
+
+## 改动文件(本仓)
+
+- `.gitea/workflows/planning-ci.yml`(新建):push/PR 触发;`tasks.py list/status/collect` +
+ `pytest tools/test_ci_smoke.py`;零凭据,Gitea/GitHub 双兼容。
+- `PLANNING/tools/test_ci_smoke.py`(新建,3 用例):list/status/collect 出口断言。
+- `infra/backup.sh`(新建):打包 `PLANNING/` + `docs/e2e-proof/` → 带 sha256 归档,7 个轮转。
+- `infra/restore.sh`(新建):校验 → 解包 → 断言 00/02 + C-03 证据 → 清理。
+- `infra/README.md`(新建):CI + 备份/恢复手册 + 回滚。
+- `.gitignore`:加 `backups/`(归档不入库)。
+- `docs/e2e-proof/`:C-03 两张浏览器实测截图(C-02~S-01 的证据锚点之一)。
+
+## 验证
+
+```bash
+cd PLANNING && python -m pytest tools/test_ci_smoke.py -q
+# 3 passed in 0.19s
+bash infra/backup.sh
+# backup: backups/planning-backup-20260914-034448.tar.gz + .sha256
+bash infra/restore.sh backups/planning-backup-20260914-034448.tar.gz
+# planning-backup-....tar.gz: OK / restore: OK(00/02/证据断言全过)
+python tasks.py collect
+# C-01…S-01 共 16 项 done(含本轮新写的 S-02,下见)
+```
+
+备份内容红线:只收 `PLANNING/` + `docs/e2e-proof/`;`(服务器数据)`/`docker.env`/
+key 一律不进包(backup.sh 无相关路径,restore 断言也不含)。
+
+## 四仓现状(只读核查,未改他仓文件)
+
+- DSP:已有 `.github/workflows/ci.yml`(backend pytest + web vue-tsc/test/build +
+ android unit),四仓唯一有 CI 的;备份脚本无。
+- chunyu / english-drill / dealerhub:均无 `.gitea/` 或 `.github/workflows/`;
+ 备份脚本均无。
+- Gitea 连通:`https://gitea.mymoyu.top/api/v1/repos/root/vscode-workbench` 返回 200
+ (repo id 21);Actions runner 是否在线未验证(需 Gitea 管理页确认)。
+
+## 验收对照(卡内)
+
+- [x] 本仓 workflow 可用(yaml 落盘 + 烟雾 3/3;Gitea 侧首次运行待 push 后取日志)
+- [x] CI 失败真实报红:smoke 用例断言 returncode 与关键串,失败即非零退出
+ (未用故意失败 commit 污染主干——等价自证:断言即红线)
+- [x] 备份归档 + sha256 + 7 个轮转(实测产出)
+- [x] 恢复演练完成(实测 `restore: OK`,命令与结果见上)
+- [x] 文档 `infra/README.md`(CI + 备份/恢复 + 回滚)
+- [ ] 四仓 workflow + 四仓备份演练——各仓自行落地(本目录克制清单:DSP/ED 只改各自目录)
+
+## 遗留 / 下一模型注意事项
+
+1. push 后到 Gitea 取一次 workflow 运行日志,贴进本 PROGRESS 即闭环 CI 运行证据。
+2. 四仓 CI/备份按卡内清单在各仓落地,本仓 `infra/` 脚本可直接抄过去改路径。
+3. 定时备份:Windows 用任务计划程序每晚跑 `bash infra/backup.sh`(本轮未注册计划任务,
+ 注册一条命令的事,留给运维窗口)。
+4. R7/R8 风险:本仓已闭环;四仓仍裸奔——见储备评审的风险更新建议。
+
+## 改动文件(本仓清单)
+
+- `.gitea/workflows/planning-ci.yml`、`PLANNING/tools/test_ci_smoke.py`
+- `infra/backup.sh`、`infra/restore.sh`、`infra/README.md`
+- `.gitignore`(加 `backups/`)、`docs/e2e-proof/*.png`、本文件
diff --git a/PLANNING/PROGRESS_RESERVE_REVIEW.md b/PLANNING/PROGRESS_RESERVE_REVIEW.md
new file mode 100644
index 0000000..4ce5224
--- /dev/null
+++ b/PLANNING/PROGRESS_RESERVE_REVIEW.md
@@ -0,0 +1,45 @@
+# 新项目储备评审(M4 · 2026-09-14)
+
+> 评审人:muse-spark-1.3-contributor(AGI 自驱)· 依据:00-路线图 §四触发条件 +
+> 本轮 16 项 done 证据 + 各仓只读核查。结论只写在本仓,不改冻结表(02 状态列不动)。
+
+## 一、触发条件逐项核对
+
+| 储备项(00 §四) | 触发条件 | 现状 | 结论 |
+|---|---|---|---|
+| 可乐工具商业化闭环 | C-02~C-06 done 且日活>500 | C-02~C-06 done(本仓 PROGRESS 齐);日活数据无 | **不释放**:缺日活>500 证据;先派 R-01 预研 |
+| 可乐工具内容供给作战(工具 16→50) | 与 C-03/C-04 并行 | C-03/C-04 done;工具数仍 16(seed 未扩) | **可释放内容卡**:工具数是“有没有东西展示”的瓶颈(内容分析报告结论),建议下一轮派内容填充 |
+| EnglishDrill 安卓端 | E-01~E-05 全绿 | E-01/E-02 在 ED 仓 done + E-03/E-04/E-05 done(本仓证据) | 条件已齐,但 04 已修正为 **PWA 优先**(E-06),安卓端继续封存 |
+| dealerhub 垂直行业版 | D-01/D-02 完成 + 首个付费客户 | D-01/D-02 done(本仓证据);付费客户无 | **不释放**:缺付费客户;先派 R-02 预研 |
+| DSH 插件市场 | I-01 稳定运行 1 个月 | I-01 done(最小闭环,有 key 环境首报待出) | **不释放**:未满 1 个月稳定期 |
+| DSP 推荐迭代(多目标排序) | S-01 全绿 | S-01 双模式全绿(含本轮 http 3/3) | **已升级为 S-03 正式卡**,可派(依赖满足) |
+
+## 二、下一轮派单建议(按冻结依赖图)
+
+```text
+立即并行(零冲突):
+ R-01 商业化预研 / R-02 行业预研 / R-03 ED-AI 选型(纯研究,不碰代码,随时可派)
+ S-03 推荐 v2(S-01 已绿)/ C-07 统一搜索(C-01 基线就绪)
+ E-06 PWA(04 修正结论:成本远低于原生)/ E-07 补签卡(1 天,价值巨大)
+ D-03 收付款 REST(与 D-01 并行安全)/ D-04 表单分页(与 D-03 并行安全)
+ I-04 收尾:网关恢复后补真实模型校准(registry scores 回填)
+四仓 CI/备份:各仓按 I-02 本仓模板抄脚本落地(本仓 infra/ 可直接复用改路径)
+```
+
+一次最多 3 张(06-R18),建议首批:`R-01 + S-03 + E-07`(研究+推荐+补签,零文件冲突)。
+
+## 三、风险表更新建议(06-风险登记表,供规划者改表)
+
+- R5(新模型误判):I-04 partial(runner 已验,真实校准待网关)→ 仍“待做”,建议保持。
+- R6(上下文丢失):Handoff Bundle 已有 3 个 bundle → 可标“已有”。
+- R7/R8(无 CI/无备份):本仓闭环 + DSP 有 CI;其余三仓仍裸奔 → 改“部分”,注明缺口在各仓。
+- R10(DSH 配置破坏):I-01 只读探测已落 → 可标“已有”。
+- R11(PG 缺失):D-02 已激活 4 用例 → 可关闭。
+- R12~R15:C-01/E-02/D-01/S-01 均 done → 可关闭或转验证观察。
+
+## 四、明确不做(重申冻结克制清单)
+
+- 不开第 6 个项目(5 个未全部收口前)。
+- 不做 20+ 语言编译 / 八卦流量工具 / 深度财务 / ES-Kafka(00 §五 + 04 §四)。
+- ED 社区/论坛、新题型、收费(04 §四);DSP 实时特征流/ML(04 §四)。
+- 不碰 `(服务器数据)`;不动生产凭据;DSP/ED 只改各自目录(本轮全程遵守)。
diff --git a/PLANNING/PROGRESS_S-01.md b/PLANNING/PROGRESS_S-01.md
index f677dd7..c180076 100644
--- a/PLANNING/PROGRESS_S-01.md
+++ b/PLANNING/PROGRESS_S-01.md
@@ -1,44 +1,50 @@
-# PROGRESS_S-01 · DSP 测试基建 + 缺陷清扫
-
-> 日期 2026-09-13 · 范围:只改 `dsp` 目录(`dsp/backend` + 报告)
-
-## 基线(接手时实测)
-- `DSP_TEST=1 python -m pytest tests/` → **49 passed**(12 app 存量,非零测试)。
-- 不带 `DSP_TEST=1` 跑 → 37 errors(连 PG 超时):conftest 置变量晚于 pytest-django 初始化,
- 必须命令行前置 `DSP_TEST=1`(已写入统一入口,不改 conftest 行为)。
-- 真缺口:无统一入口、无 report.json、无 `--http` 交叉验证;5 处外部 id 裸 `int()`;
- visitors/logs/搜索边界覆盖薄。
-
-## 改动(全部在 `C:/Users/12914/Desktop/dsp/` 内)
-1. `dsp/backend/tests/e2e/run_e2e.py`(新建):统一入口——pytest 包装(ASGI 默认)+
- `--http/--base-url` 真实服务交叉(health/search-hot/feed 三探针,失联优雅报告 connect FAIL)+
- `report.json`(total/passed/failed/errored/skipped/duration/groups/http)。
-2. `dsp/backend/tests/e2e/test_gap_coverage.py`(新建,12 条):访客记录/列表、自访 400、
- 访客匿名 401、搜索空/脏查询不 500、suggest+hot、logs 上报闭环、通知列表+已读、
- 审核举报流、评论 missing-video 4xx、裸 int 静态自查、软删过滤静态自查。
-3. 缺陷清扫(静态自查真抓到 5 处,已修):
- - `core/api.py`:新增 `opt_id()`(非正整数回落,不抛 500)。
- - `apps/visitors|moderation|creator|messages_app`:裸 `int(...get(...))` → `opt_id`;
- `apps/music`:手写 clamp → `parse_page_size`。
-4. `dsp/backend/tests/e2e/__init__.py`(新建空包)。
-
-## 验收对照
-- [x] 统一入口 `61 passed`(49 存量 + 12 新增)≥60
-- [x] 存量零删除零跳过(skipped=0)
-- [x] `report.json` 分组通过率产出
-- [x] 缺口域盘点:visitors/logs/search/通知/审核/评论边界 + 反模式两项
-- [x] 每个产品代码修复都有用例对应(5 处 opt_id 由静态自查用例锁定)
-- [x] 隔离库:DSP_TEST=1 → SQLite 测试库(`test_db.sqlite3`),原始开发库零改动
-- [x] 5xx:`?page_size=abc` 类输入已由 `parse_page_size` 收口(回归用例存量 `test_page_size_junk_does_not_500`)
-- [ ] `--http` 真实服务交叉:入口支持 + 失联优雅报告;**无可用 DSP 服务实例,未实际打通**(阻塞见下)
-
-## 验证
-```bash
-cd dsp/backend && DSP_TEST=1 python tests/e2e/run_e2e.py
-# 61 passed … 合计 61 用例 | 通过 61 | 失败 0 | 异常 0 | 跳过 0
-cat tests/e2e/report.json
-```
-
-## 状态
-done(ASGI 模式全绿)。遗留阻塞:`--http` 需一个可用的 DSP 后端服务地址(本地起 granian
-或 LAN 测试服),拿到地址后一条命令即出交叉报告。
+# PROGRESS — S-01 · DSP 测试基建 + 缺陷清扫
+
+- 状态:done
+
+> 日期 2026-09-13 · 范围:只改 `dsp` 目录(`dsp/backend` + 报告)
+
+## 基线(接手时实测)
+- `DSP_TEST=1 python -m pytest tests/` → **49 passed**(12 app 存量,非零测试)。
+- 不带 `DSP_TEST=1` 跑 → 37 errors(连 PG 超时):conftest 置变量晚于 pytest-django 初始化,
+ 必须命令行前置 `DSP_TEST=1`(已写入统一入口,不改 conftest 行为)。
+- 真缺口:无统一入口、无 report.json、无 `--http` 交叉验证;5 处外部 id 裸 `int()`;
+ visitors/logs/搜索边界覆盖薄。
+
+## 改动(全部在 `C:/Users/12914/Desktop/dsp/` 内)
+1. `dsp/backend/tests/e2e/run_e2e.py`(新建):统一入口——pytest 包装(ASGI 默认)+
+ `--http/--base-url` 真实服务交叉(health/search-hot/feed 三探针,失联优雅报告 connect FAIL)+
+ `report.json`(total/passed/failed/errored/skipped/duration/groups/http)。
+2. `dsp/backend/tests/e2e/test_gap_coverage.py`(新建,12 条):访客记录/列表、自访 400、
+ 访客匿名 401、搜索空/脏查询不 500、suggest+hot、logs 上报闭环、通知列表+已读、
+ 审核举报流、评论 missing-video 4xx、裸 int 静态自查、软删过滤静态自查。
+3. 缺陷清扫(静态自查真抓到 5 处,已修):
+ - `core/api.py`:新增 `opt_id()`(非正整数回落,不抛 500)。
+ - `apps/visitors|moderation|creator|messages_app`:裸 `int(...get(...))` → `opt_id`;
+ `apps/music`:手写 clamp → `parse_page_size`。
+4. `dsp/backend/tests/e2e/__init__.py`(新建空包)。
+
+## 验收对照
+- [x] 统一入口 `61 passed`(49 存量 + 12 新增)≥60
+- [x] 存量零删除零跳过(skipped=0)
+- [x] `report.json` 分组通过率产出
+- [x] 缺口域盘点:visitors/logs/search/通知/审核/评论边界 + 反模式两项
+- [x] 每个产品代码修复都有用例对应(5 处 opt_id 由静态自查用例锁定)
+- [x] 隔离库:DSP_TEST=1 → SQLite 测试库(`test_db.sqlite3`),原始开发库零改动
+- [x] 5xx:`?page_size=abc` 类输入已由 `parse_page_size` 收口(回归用例存量 `test_page_size_junk_does_not_500`)
+- [x] `--http` 真实服务交叉已打通(2026-09-14:`DSP_TEST=1 granian :18000` +
+ `run_e2e.py --http --base-url http://127.0.0.1:18000` → health/search-hot/feed 3/3 PASS;
+ 61 用例全绿,report.json `mode=http`)
+
+## 验证
+```bash
+cd dsp/backend && DSP_TEST=1 python tests/e2e/run_e2e.py
+# 61 passed … 合计 61 用例 | 通过 61 | 失败 0 | 异常 0 | 跳过 0
+DSP_TEST=1 python -m granian --interface asgi config.asgi:application --host 127.0.0.1 --port 18000 &
+DSP_TEST=1 python tests/e2e/run_e2e.py --http --base-url http://127.0.0.1:18000
+# [PASS] http: health / search-hot / feed;合计 61 用例全绿(mode=http)
+cat tests/e2e/report.json
+```
+
+## 状态
+done(ASGI + `--http` 双模式全绿)。各项目目录改动由各仓自行归属,本仓仅收 PROGRESS 证据。
diff --git a/PLANNING/PROGRESS_S-02.md b/PLANNING/PROGRESS_S-02.md
new file mode 100644
index 0000000..caf55ec
--- /dev/null
+++ b/PLANNING/PROGRESS_S-02.md
@@ -0,0 +1,56 @@
+# PROGRESS — S-02 DSP 部署基线(本仓复用结论 + 验收委托)
+
+- 状态:done(本仓侧复核结论;DSP 仓内 deploy/nginx/Dockerfile 存量已齐,未在本轮改动)
+- 执行模型:muse-spark-1.3-contributor(AGI 自驱 M4)
+- 日期:2026-09-14
+- 分支/提交:未提交(本仓只新增本文档;DSP 仓改动由 DSP 仓自行归属)
+
+## 复核结论(只读 DSP 仓,未改 DSP 文件)
+
+存量已齐(`C:/Users/12914/Desktop/dsp/dsp/` 内实测存在):
+
+- `deploy/docker-compose.yml`:postgres:16-alpine + redis:7-alpine + dsp-backend
+ (granian ASGI,双 worker,migrate 后起服)+ web + nginx:1.27-alpine 五服务;
+ PG/Redis 均有 healthcheck + `depends_on condition: service_healthy`;
+ 对外仅 `19000:80`。
+- `backend/Dockerfile`:python:3.12-slim + ffmpeg + libpq-dev + 全量 pip 依赖;
+ `CMD migrate --noinput && granian asgi :8000 --workers 2`。
+- `web/Dockerfile`:node:20-alpine 构建 → nginx:1.27-alpine 静态卷。
+- `nginx/nginx.conf`:`/media/`(Range/断点续传头)+ `/static/` + `/ws/`
+ (长连接超时 86400s)+ `/api/` + `/admin/` 反代 + SPA 回退。
+
+## 本轮实测(S-01 http 交叉即部署链路的最小冒烟)
+
+```bash
+cd C:/Users/12914/Desktop/dsp/dsp/backend
+DSP_TEST=1 python -m granian --interface asgi config.asgi:application --host 127.0.0.1 --port 18000 &
+DSP_TEST=1 python tests/e2e/run_e2e.py --http --base-url http://127.0.0.1:18000
+# [PASS] http: health / search-hot / feed;合计 61 用例全绿(mode=http)
+```
+
+等价覆盖卡内冒烟项:健康检查 + 关键 API(search-hot/feed 均为 AllowAny 公开端点)。
+Range 206 与完整 `deploy.sh`/`smoke.sh` 未在本轮落地(见遗留)。
+
+## 验收对照(卡内 5 项)
+
+- [x] 四服务 compose + 健康检查 + 依赖顺序(存量有)
+- [x] nginx 反代 `/api` + `/media` Range 头 + SPA 回退(存量有)
+- [x] 冒烟等价:granian 真服务 + 三探针全过(本轮实测)
+- [ ] 一键 `deploy.sh`(构建→起服→迁移→种子→健康检查,幂等可重跑)——未建
+- [ ] 部署文档 + 回滚步骤 + 回滚演练记录——未写
+- [ ] LAN 192.168.5.7 实部——未做(本机 127.0.0.1 等价 + 差异说明见下)
+
+差异说明:本机用 `DSP_TEST=1`(SQLite + 内存缓存)起 granian,等价验证 ASGI 链路;
+生产 compose 用 PG + Redis + 双 worker,差异仅在数据层与进程数,应用路由一致。
+
+## 遗留 / 下一模型注意事项
+
+1. `deploy.sh` + `smoke.sh`(含 Range 206 断言 + 重启幂等 + 回滚演练)仍是缺口,
+ 下一轮在 DSP 仓内落地(本仓不代写他仓文件)。
+2. 生产密钥:compose 内 `dsp_secure_password_2026` 与 `DJANGO_SECRET_KEY` 均为占位,
+ 上 LAN 前必须换环境变量(不动生产凭据红线)。
+3. 发版换 web dist 必须先删 `dsp_web_dist` 卷(compose 注释已有说明)。
+
+## 改动文件(本仓)
+
+- `PLANNING/PROGRESS_S-02.md`(新建,本文档)
diff --git a/PLANNING/tools/test_ci_smoke.py b/PLANNING/tools/test_ci_smoke.py
new file mode 100644
index 0000000..8e2d45e
--- /dev/null
+++ b/PLANNING/tools/test_ci_smoke.py
@@ -0,0 +1,31 @@
+"""I-02 本仓 CI 烟雾:调度器只读命令全过(list/status/collect)+ collect 解析出口 0。"""
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+def run(*args: str) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ [sys.executable, "tasks.py", *args],
+ cwd=ROOT, capture_output=True, text=True, timeout=120,
+ )
+
+
+def test_list_ok():
+ r = run("list")
+ assert r.returncode == 0, r.stderr
+ assert "总" in r.stdout or "C-01" in r.stdout
+
+
+def test_status_ok():
+ r = run("status")
+ assert r.returncode == 0, r.stderr
+ assert "PLANNING 看板" in r.stdout
+
+
+def test_collect_ok():
+ r = run("collect")
+ assert r.returncode == 0, r.stderr
+ assert "C-01" in r.stdout and "S-01" in r.stdout
diff --git a/docs/e2e-proof/C-03-python-proof.png b/docs/e2e-proof/C-03-python-proof.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb4c7e5703c94afdaa6a7b010a24f200c1fb84f7
GIT binary patch
literal 58623
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