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` + 单一 `chunyu_project`)、nginx SPA 回退、 - `scripts/check_routes.mjs`(C-01 路由回归)。 -- 缺口:蜘蛛抓不到任何工具页正文;无 sitemap/robots;无单页 meta。 - -## 选型 -独立 Node 后处理脚本(`scripts/seo_prerender.mjs`,零新依赖),不引入 `vite-plugin-prerender`/puppeteer: -构建命令不变(`build` 链尾追加),失败即中断构建(`&&` 链),产出直接进 `dist/`。 - -## 改动(`chunyu_project_react/`) -1. `scripts/seo_prerender.mjs`(新建):工具清单与后端 `seed_tools.py` 同源(16 启用工具, - url_path/name/description 对齐);每工具页直出 `dist/<url>/index.html`(独立 title/description/ - keywords/canonical/OG + 静态正文 h1+说明+相关工具链 + `#root` 占位 + `location.replace` 跳转, - React hydrate 后照常接管);聚合页 `/`(`dist/index.seo.html`,不覆盖 SPA 外壳)、`/utility`、`/top`; - `sitemap.xml`(19 URL)+ `robots.txt`(放行蜘蛛、屏蔽 profile/wallet/console/messages/chat、指向 sitemap)。 -2. `scripts/check_seo.mjs`(新建,任务卡验收脚本):工具页存在/唯一 title/非空 description/ - 静态正文/OG、聚合页、sitemap 覆盖 19 URL、robots 口径、SPA 外壳完好、清单 16 条对齐。 -3. `package.json`:`build` 链尾 `&& node scripts/seo_prerender.mjs`;新增 `seo` / `seo:check` 快捷命令。 -4. `nginx.conf`:`location = /` 优先 `index.seo.html`;工具页正则 location 优先 `try_files` 静态 HTML - (1h 缓存);登录态页(/post、/profile 等)不命中,落 SPA 回退。 - -## 边界遵守 -- 无 Next.js 迁移;工具页业务零改;登录态页(profile/wallet/console/messages/chat)robots + nginx 双屏蔽。 - -## 验证 -```bash -pnpm build -# ✓ built in ~12s(含 tsc -b);[seo] prerender done: 19 html + sitemap(19 urls) + robots.txt in 15ms -node scripts/check_seo.mjs -# SEO CHECK ALL GREEN (16 tools + 3 pages) —— 逐页 PASS 明细见终端输出 -curl 等价:dist/utility/json-formatter/index.html → <title>JSON格式化 - 免费在线工具 | 可乐开发工具箱,含

正文 -nginx:本地无二进制/docker 未起,改用 location 自查——预渲染块在 SPA 回退前、 - 16 工具+聚合命中 SEO、/post/1 与 /profile 走 SPA、大括号平衡(node 自查脚本全对) -``` -构建耗时:C-06 轮 14.99s → 本轮 11.87s(机器波动范围内),SEO 脚本本身仅 +15ms。 - -## 状态 -done。遗留:`--base` 当前为 `https://tools.example.com` 占位符——部署时带真实域名重跑 -`node scripts/seo_prerender.mjs --dist dist --base https://真实域名` 刷新 sitemap/OG/canonical; -浏览器“首屏先现+React 接管”需线上 nginx 联调时录屏补证据。 +# PROGRESS — C-05 · 工具页 SEO 预渲染 + +- 状态:done + +> 日期 2026-09-13 · 范围:`chunyu_project_react`(构建层脚本+nginx.conf)+ 校验脚本 + +## 现状 vs 差距(接手时) +- 已有:Vite SPA(`dist/index.html` 纯空 `#root` + 单一 `chunyu_project`)、nginx SPA 回退、 + `scripts/check_routes.mjs`(C-01 路由回归)。 +- 缺口:蜘蛛抓不到任何工具页正文;无 sitemap/robots;无单页 meta。 + +## 选型 +独立 Node 后处理脚本(`scripts/seo_prerender.mjs`,零新依赖),不引入 `vite-plugin-prerender`/puppeteer: +构建命令不变(`build` 链尾追加),失败即中断构建(`&&` 链),产出直接进 `dist/`。 + +## 改动(`chunyu_project_react/`) +1. `scripts/seo_prerender.mjs`(新建):工具清单与后端 `seed_tools.py` 同源(16 启用工具, + url_path/name/description 对齐);每工具页直出 `dist/<url>/index.html`(独立 title/description/ + keywords/canonical/OG + 静态正文 h1+说明+相关工具链 + `#root` 占位 + `location.replace` 跳转, + React hydrate 后照常接管);聚合页 `/`(`dist/index.seo.html`,不覆盖 SPA 外壳)、`/utility`、`/top`; + `sitemap.xml`(19 URL)+ `robots.txt`(放行蜘蛛、屏蔽 profile/wallet/console/messages/chat、指向 sitemap)。 +2. `scripts/check_seo.mjs`(新建,任务卡验收脚本):工具页存在/唯一 title/非空 description/ + 静态正文/OG、聚合页、sitemap 覆盖 19 URL、robots 口径、SPA 外壳完好、清单 16 条对齐。 +3. `package.json`:`build` 链尾 `&& node scripts/seo_prerender.mjs`;新增 `seo` / `seo:check` 快捷命令。 +4. `nginx.conf`:`location = /` 优先 `index.seo.html`;工具页正则 location 优先 `try_files` 静态 HTML + (1h 缓存);登录态页(/post、/profile 等)不命中,落 SPA 回退。 + +## 边界遵守 +- 无 Next.js 迁移;工具页业务零改;登录态页(profile/wallet/console/messages/chat)robots + nginx 双屏蔽。 + +## 验证 +```bash +pnpm build +# ✓ built in ~12s(含 tsc -b);[seo] prerender done: 19 html + sitemap(19 urls) + robots.txt in 15ms +node scripts/check_seo.mjs +# SEO CHECK ALL GREEN (16 tools + 3 pages) —— 逐页 PASS 明细见终端输出 +curl 等价:dist/utility/json-formatter/index.html → <title>JSON格式化 - 免费在线工具 | 可乐开发工具箱,含

正文 +nginx:本地无二进制/docker 未起,改用 location 自查——预渲染块在 SPA 回退前、 + 16 工具+聚合命中 SEO、/post/1 与 /profile 走 SPA、大括号平衡(node 自查脚本全对) +``` +构建耗时:C-06 轮 14.99s → 本轮 11.87s(机器波动范围内),SEO 脚本本身仅 +15ms。 + +## 状态 +done。遗留:`--base` 当前为 `https://tools.example.com` 占位符——部署时带真实域名重跑 +`node scripts/seo_prerender.mjs --dist dist --base https://真实域名` 刷新 sitemap/OG/canonical; +浏览器“首屏先现+React 接管”需线上 nginx 联调时录屏补证据。 diff --git a/PLANNING/PROGRESS_C-06.md b/PLANNING/PROGRESS_C-06.md index a2b4ee5..8fa1eb5 100644 --- a/PLANNING/PROGRESS_C-06.md +++ b/PLANNING/PROGRESS_C-06.md @@ -1,53 +1,55 @@ -# PROGRESS_C-06 · 关注 feed + 首页签到卡 + 学习数据钩子 - -> 日期 2026-09-13 · 范围:`chunyu_project/article+learn+user`(后端只加字段/修bug/加用例,不改契约)+ `chunyu_project_react` - -## 现状 vs 差距(接手时) -- 已有:`Follow` 模型 + `POST /user/follow//` + `GET /api/article/list?feed=following`(后端)+ - 详情页关注按钮(`ArticleDetail.handleFollow`)+ 桌面端 `Home` 签到卡(含未登录引导态)+ - 课程 `students_count`(ChapterRead 去重)双端显示。 -- 缺口:article/learn **零用例**;列表接口缺 `author_user_id/is_following`(卡上无法关注); - 列表无关注空态;`HomeMobile` 无签到条;无 `total_minutes` 字段; - **`get_author_name` 在 ASGI 下同步碰 `obj.author` 外键 → 课程列表 500**(本轮真抓到的 bug)。 - -## 改动 -后端(`chunyu_project/`): -1. `article/serializers.py`:`ArticleListSerializer` 加 `author_user_id` + `is_following`(登录态关注判定)。 -2. `learn/serializers.py`: - - `get_author_name` 改 `sync_to_async` 线程取数(修课程列表 500 真 bug,list/detail 双处); - - 新增 `parse_duration_minutes`(中文“30分钟”/纯数字解析,不可解析返回 None 不伪造)+ - `total_minutes` 字段(list/detail 双序列化器,可解析章节求和)。 -3. `article/tests/`(新建包 + `test_following_feed.py` 7 条):仅关注作者可见/未关注空态/ - 双关注全可见/取关移除/匿名空态/普通列表不受影响/草稿不进 feed。 -4. `learn/tests/`(新建包 + `test_c06_hooks.py` 8 条):签到双签 400+积分不变+单记录/ - status 已签/匿名 401;聚合 2 用户交叉去重=2/零阅读=0/detail 对齐; - total_minutes 求和 75/全不可解析 null。 - -前端(`chunyu_project_react/src/`): -5. `pages/Articles/Articles.tsx`:`Article` 接口加 `author_user_id/is_following`; - `handleToggleFollow`(关注切换 + feed=5 下取关即时移除);grid/list 双卡作者行加关注按钮; - 空态(关注态 vs 普通态两套文案)。 -6. `pages/Articles/Articles.css`:`.articles-follow-btn` + `.articles-empty`(含 dark)。 -7. `locales/{en,zh,zh-TW,ja,ko,ru}.json`:`articles.empty.{title,hint,followingTitle,followingHint}`。 -8. `pages/Home/HomeMobile.tsx` + `.css`:移动签到条(状态/周进度/一键签到/未登录引导,与桌面同口径)。 -9. `pages/Learn/Learn.tsx` + `LearnMobile.tsx`:`total_minutes` 映射 + 有值才显示时长徽标; - `locales` 六语言加 `learn.minutes`;Learn 加缺失的 `ClockCircleOutlined` 引入。 - -## 口径说明 -- `total_minutes` 只对可解析 duration 求和;存量中文字符串解析不出 → null → 前端不显示。 -- 签到幂等:第二次 400(`今日已签到`),积分不变(单测锁定)。 -- 未动:推荐算法(无)、Follow/签到字段与契约(无)、弹窗动画(无)。 - -## 验证 -```bash -DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../dev_db.sqlite3" python manage.py test article learn tool -v 1 -# Ran 30 tests … OK(15 存量 + 15 新增;中途一次 sqlite 锁表抖动,重跑即绿) -tsc --noEmit -p chunyu_project_react/tsconfig.json # 零报错 -pnpm build # ✓ built in 14.99s -``` -手工等价:A 关注 B → `feed=following` 只见 B 文(单测同等价 7 条);连签两次 → 第二次 400 -积分不变(单测锁定)。 - -## 状态 -done。遗留:user 滑块验证码既有红(3 失败+9 错误,与本任务无关,未动); -dev_db.sqlite3 曾缺 `aitool` 表已补迁移(`migrate aitool`),验证残留已清零。 +# PROGRESS — C-06 · 关注 feed + 首页签到卡 + 学习数据钩子 + +- 状态:done + +> 日期 2026-09-13 · 范围:`chunyu_project/article+learn+user`(后端只加字段/修bug/加用例,不改契约)+ `chunyu_project_react` + +## 现状 vs 差距(接手时) +- 已有:`Follow` 模型 + `POST /user/follow//` + `GET /api/article/list?feed=following`(后端)+ + 详情页关注按钮(`ArticleDetail.handleFollow`)+ 桌面端 `Home` 签到卡(含未登录引导态)+ + 课程 `students_count`(ChapterRead 去重)双端显示。 +- 缺口:article/learn **零用例**;列表接口缺 `author_user_id/is_following`(卡上无法关注); + 列表无关注空态;`HomeMobile` 无签到条;无 `total_minutes` 字段; + **`get_author_name` 在 ASGI 下同步碰 `obj.author` 外键 → 课程列表 500**(本轮真抓到的 bug)。 + +## 改动 +后端(`chunyu_project/`): +1. `article/serializers.py`:`ArticleListSerializer` 加 `author_user_id` + `is_following`(登录态关注判定)。 +2. `learn/serializers.py`: + - `get_author_name` 改 `sync_to_async` 线程取数(修课程列表 500 真 bug,list/detail 双处); + - 新增 `parse_duration_minutes`(中文“30分钟”/纯数字解析,不可解析返回 None 不伪造)+ + `total_minutes` 字段(list/detail 双序列化器,可解析章节求和)。 +3. `article/tests/`(新建包 + `test_following_feed.py` 7 条):仅关注作者可见/未关注空态/ + 双关注全可见/取关移除/匿名空态/普通列表不受影响/草稿不进 feed。 +4. `learn/tests/`(新建包 + `test_c06_hooks.py` 8 条):签到双签 400+积分不变+单记录/ + status 已签/匿名 401;聚合 2 用户交叉去重=2/零阅读=0/detail 对齐; + total_minutes 求和 75/全不可解析 null。 + +前端(`chunyu_project_react/src/`): +5. `pages/Articles/Articles.tsx`:`Article` 接口加 `author_user_id/is_following`; + `handleToggleFollow`(关注切换 + feed=5 下取关即时移除);grid/list 双卡作者行加关注按钮; + 空态(关注态 vs 普通态两套文案)。 +6. `pages/Articles/Articles.css`:`.articles-follow-btn` + `.articles-empty`(含 dark)。 +7. `locales/{en,zh,zh-TW,ja,ko,ru}.json`:`articles.empty.{title,hint,followingTitle,followingHint}`。 +8. `pages/Home/HomeMobile.tsx` + `.css`:移动签到条(状态/周进度/一键签到/未登录引导,与桌面同口径)。 +9. `pages/Learn/Learn.tsx` + `LearnMobile.tsx`:`total_minutes` 映射 + 有值才显示时长徽标; + `locales` 六语言加 `learn.minutes`;Learn 加缺失的 `ClockCircleOutlined` 引入。 + +## 口径说明 +- `total_minutes` 只对可解析 duration 求和;存量中文字符串解析不出 → null → 前端不显示。 +- 签到幂等:第二次 400(`今日已签到`),积分不变(单测锁定)。 +- 未动:推荐算法(无)、Follow/签到字段与契约(无)、弹窗动画(无)。 + +## 验证 +```bash +DB_ENGINE=django.db.backends.sqlite3 DB_NAME=".../dev_db.sqlite3" python manage.py test article learn tool -v 1 +# Ran 30 tests … OK(15 存量 + 15 新增;中途一次 sqlite 锁表抖动,重跑即绿) +tsc --noEmit -p chunyu_project_react/tsconfig.json # 零报错 +pnpm build # ✓ built in 14.99s +``` +手工等价:A 关注 B → `feed=following` 只见 B 文(单测同等价 7 条);连签两次 → 第二次 400 +积分不变(单测锁定)。 + +## 状态 +done。遗留:user 滑块验证码既有红(3 失败+9 错误,与本任务无关,未动); +dev_db.sqlite3 曾缺 `aitool` 表已补迁移(`migrate aitool`),验证残留已清零。 diff --git a/PLANNING/PROGRESS_D-01.md b/PLANNING/PROGRESS_D-01.md index 4408c90..5ee44d3 100644 --- a/PLANNING/PROGRESS_D-01.md +++ b/PLANNING/PROGRESS_D-01.md @@ -1,60 +1,62 @@ -# PROGRESS_D-01 · 用户-租户 membership + 跨租户切换 - -> 日期 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(既有旁路,未动)。 +# 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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 literal 0 HcmV?d00001 diff --git a/docs/e2e-proof/C-03-timeout-proof.png b/docs/e2e-proof/C-03-timeout-proof.png new file mode 100644 index 0000000000000000000000000000000000000000..b7425f67f2aadd5629eef106a0615f1f9a105cf6 GIT binary patch literal 50254 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�@v=qg9Ewoz2SE8=sUgRbF{seKzb%pHHB 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 literal 0 HcmV?d00001 diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..ad84243 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,36 @@ +# infra 使用说明(I-02 · 本仓 CI + 备份) + +本目录只收本仓(vscode-workbench)基建,不碰各项目仓、不含任何密钥。 + +## backup.sh(备份) + +```bash +bash infra/backup.sh +# → backups/planning-backup-YYYYMMDD-HHMMSS.tar.gz + .sha256 +``` + +- 内容:`PLANNING/` + `docs/e2e-proof/`(任务资产 + 验证证据)。 +- 不含:`(服务器数据)`、`docker.env`、任何 key(见 `.gitignore`,`backups/` 不入库)。 +- 保留:最近 7 个归档自动轮转。 + +## restore.sh(恢复演练) + +```bash +bash infra/restore.sh backups/planning-backup-.tar.gz +# → sha256 校验 → 解到临时目录 → 断言 00/02 + C-03 证据存在 → 清理 +``` + +2026-09-14 实测:`planning-backup-20260914-034448.tar.gz` → `restore: OK`。 + +## CI + +- `.gitea/workflows/planning-ci.yml`:push/PR 触发,跑 `tasks.py list/status/collect` + + `pytest tools/test_ci_smoke.py`(3 用例)。无私有凭据,Gitea Actions 与 GitHub Actions 均可直接用。 +- 四项目 CI 现状:DSP 已有 `.github/workflows/ci.yml`(backend+web+android); + chunyu/english-drill/dealerhub 暂无 workflow——由各仓自行落地,本仓不代写。 + +## 回滚 + +- 本仓基建全是新增文件,回滚即删:`infra/`、`.gitea/`、`PLANNING/tools/test_ci_smoke.py`、 + `docs/e2e-proof/`、`PROGRESS_S-02/I-02`。 +- 备份归档在本地 `backups/`(git 忽略),删仓前拷一份即走。 diff --git a/infra/backup.sh b/infra/backup.sh new file mode 100644 index 0000000..fcf9698 --- /dev/null +++ b/infra/backup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# I-02 本仓备份脚本:打包 PLANNING(任务资产)+ docs/e2e-proof(验证证据)→ 带 sha256 的归档。 +# 不含任何密钥:只收 PLANNING + docs/e2e-proof;(服务器数据)/.env/docker.env 一律不碰。 +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +STAMP="$(date +%Y%m%d-%H%M%S)" +OUT_DIR="$ROOT/backups" +mkdir -p "$OUT_DIR" +ARCHIVE="$OUT_DIR/planning-backup-$STAMP.tar.gz" +tar -czf "$ARCHIVE" -C "$ROOT" PLANNING docs/e2e-proof +sha256sum "$ARCHIVE" > "$ARCHIVE.sha256" +# 保留策略:只留最近 7 个归档 +ls -1t "$OUT_DIR"/planning-backup-*.tar.gz 2>/dev/null | tail -n +8 | xargs -r rm -f +ls -1t "$OUT_DIR"/planning-backup-*.tar.gz.sha256 2>/dev/null | tail -n +8 | xargs -r rm -f +echo "backup: $ARCHIVE" +cat "$ARCHIVE.sha256" diff --git a/infra/restore.sh b/infra/restore.sh new file mode 100644 index 0000000..c82cdda --- /dev/null +++ b/infra/restore.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# I-02 本仓恢复演练:校验归档 → 解到临时目录 → 断言关键文件存在 → 清理。 +set -euo pipefail +ARCHIVE="${1:?用法: restore.sh }" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +sha256sum -c "$ARCHIVE.sha256" +tar -xzf "$ARCHIVE" -C "$TMP" +test -f "$TMP/PLANNING/02-任务总表.md" +test -f "$TMP/PLANNING/00-全局路线图.md" +test -f "$TMP/docs/e2e-proof/C-03-timeout-proof.png" +echo "restore: OK ($ARCHIVE → $TMP)" +ls "$TMP/PLANNING" | head -5