# 2026-06-10 全项目审查修复计划 > **状态(2026-06-11 截断):** 本计划随 v7 绞杀式收敛(`docs/architecture/story-repo-spec-2026-06-10.md`)截断收口。 > - **已完成并保留**:Phase 0 全部(Task 1-6,正文数据安全)+ Task 7——这是 v6 用户数据与 v7 迁移器读取的地基。 > - **作废**:Task 8-24(Phase 1 数据链)、Task 26-27、Task 29-34——目标模块(SQLite 投影、event log、v6 提示词、dashboard、CLI 样板)在 v7 中整体删除,不再修缮。 > - **例外保留为独立候选**:Task 25(嵌入默认出网,隐私问题,若 v6 分支再发维护版则必修)、Task 28(CI 加固,仓库层面,v7 继续复用,可随时单独做)。 > - 分支 `fix/audit-2026-06-10` 以 Phase 0 + Task 7 收口合入 master,作为 v6 最后一批数据安全维护。 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修复 2026-06-10 深度审查发现的全部高/中危问题:数据丢失路径、数据链不一致、skill 流程死锁、隐私出网默认值与守卫绕过。 **Architecture:** 按伤害优先级分四个阶段(P0 数据安全 → P1 数据链与流程 → P2 安全隐私 → P3 质量卫生)。每个阶段独立可交付、全绿后再进下一阶段。修复以"先写探针测试复现问题 → 修复 → 验证"为节奏;过时的文案级断言按《测试是探针不是约束》原则直接改写。 **Tech Stack:** Python 3.10+ / pytest(Windows 下设 `PYTHONUTF8=1`)/ SQLite / FastAPI。 **审查报告来源:** 本计划对应 2026-06-10 会话中六个维度的审查结论(数据链、提示词、代码质量、dashboard+hooks、数据安全+CI、仓库卫生+残余模块)。 **运行测试:** `python -m pytest`(根目录 pytest.ini 已配置 testpaths 与 cov-fail-under=90)。 --- ## Phase 0 — P0 数据安全(正文是不可再生数据) ### Task 1: backup_manager 备份失败不得报告成功 **Files:** - Modify: `webnovel-writer/scripts/backup_manager.py:150-166, 228-254` - Test: `webnovel-writer/scripts/tests/test_backup_manager.py` - [x] **Step 1: 写失败测试**:在 tmp git 仓库中故意不配置 `user.name/user.email`(`git config --local --unset` 或 `-c user.useConfigOnly=true`),调用 `backup()`,断言返回失败且输出包含"备份失败"、不产生 `ch{N}` tag。 - [x] **Step 2: 运行确认现状是假成功**(当前会打印 ✅ 并打 tag 在旧 HEAD)。 - [x] **Step 3: 修复 `_run_git_command`**:`check=False` 分支改为返回 `(result.returncode == 0, stdout, stderr)`;调用方据真实退出码判断。"nothing to commit" 改为从 stdout/stderr 文本判断(当前 `:233` 的 `if not success and "nothing to commit"` 是永假死代码,一并删除重写): ```python def _run_git_command(self, args, check=True): result = subprocess.run( ["git", *args], cwd=self.project_root, capture_output=True, text=True, encoding="utf-8", ) ok = result.returncode == 0 if check and not ok: raise BackupError(f"git {' '.join(args)} 失败: {result.stderr.strip()}") return ok, result.stdout, result.stderr ``` - [x] **Step 4: `backup()` 中 commit 失败时中止**:不打 tag、返回非零、输出含修复指引(提示运行 `git config user.name/user.email`);"nothing to commit" 视为成功但提示"本章无变更"。 - [x] **Step 5: 跑全部 backup 测试通过后提交** `fix: backup reports real git failures and aborts tagging`。 ### Task 2: rollback 改为前滚式恢复,去掉 detached HEAD 与硬编码 master **Files:** - Modify: `webnovel-writer/scripts/backup_manager.py:294-307` - Test: `webnovel-writer/scripts/tests/test_backup_manager.py` - [x] **Step 1: 写测试**:建 tmp 仓库(默认分支命名为 `main`),打两个 ch tag,回滚到 ch1 后断言:(a) 仍在原分支(`git symbolic-ref HEAD` 成功且为 main);(b) 工作区内容等于 ch1;(c) `git log` 多出一个"rollback"提交(历史不丢)。 - [x] **Step 2: 实现前滚式回滚**: ```python def rollback(self, chapter: int) -> bool: tag = f"ch{chapter}" ok, _, _ = self._run_git_command(["rev-parse", "--verify", tag], check=False) if not ok: print(f"❌ 备份点 {tag} 不存在"); return False ok, _, err = self._run_git_command(["checkout", tag, "--", "."], check=False) if not ok: print(f"❌ 回滚失败: {err}"); return False self._run_git_command(["add", "-A"], check=False) ok, _, err = self._run_git_command( ["commit", "-m", f"rollback: 恢复到 {tag} 备份点"], check=False) # 工作区与 tag 相同则 commit 报 nothing to commit,视为成功 return True ``` - [x] **Step 3: 删除所有 `checkout master` 硬编码**;任何需要分支名的地方用 `git symbolic-ref --short HEAD` 探测。 - [x] **Step 4: 测试通过后提交** `fix: rollback is forward-only, never detaches HEAD`。 ### Task 3: 无 Git 时的降级备份必须覆盖正文,或醒目声明没有 **Files:** - Modify: `webnovel-writer/scripts/backup_manager.py:175-195` - Test: `webnovel-writer/scripts/tests/test_backup_manager.py` - [x] **Step 1: 写测试**:模拟 git 不可用(monkeypatch `_git_available` 为 False),项目含 `正文/第0001章-x.md`,调用 `backup()` 后断言备份目录里存在该正文文件副本。 - [x] **Step 2: 实现**:降级路径把 `正文/`、`大纲/`、`设定集/`、`.webnovel/state.json` 全部 `shutil.copytree/copy2` 进 `.webnovel/backups/snapshot_ch{N}_{ts}/`;输出明确列出备份了什么。保留按数量滚动清理(最多 10 个 snapshot)。 - [x] **Step 3: 提交** `fix: degraded backup covers manuscript files`。 ### Task 4: init 重跑不得静默覆盖损坏的 state.json **Files:** - Modify: `webnovel-writer/scripts/init_project.py:294-300,366` - Test: `webnovel-writer/scripts/data_modules/tests/test_init_project_pruning.py` - [x] **Step 1: 写测试**:项目里放一个非法 JSON 的 state.json,重跑 init,断言 (a) 生成 `state.corrupt_*.json` 副本且内容等于原损坏文本;(b) 输出包含警告。 - [x] **Step 2: 实现**:捕获 `json.JSONDecodeError` 时先 `shutil.copy2(state_path, state_path.with_name(f"state.corrupt_{ts}.json"))` 再重建,打印"⚠️ 原 state.json 已损坏,已另存为 ... 供手工抢救"。 - [x] **Step 3: 提交** `fix: preserve corrupt state.json before rebuilding`。 ### Task 5: 迁移脚本带错不精简、写回原子化 **Files:** - Modify: `webnovel-writer/scripts/data_modules/migrate_state_to_sqlite.py:235-258` - Test: `webnovel-writer/scripts/data_modules/tests/test_migrate_state_to_sqlite.py` - [x] **Step 1: 写测试**:构造一条会迁移失败的实体(如非法类型触发 `stats["errors"] += 1`),跑迁移,断言 state.json 中 `entities_v3` 字段仍在、CLI 退出码非 0。 - [x] **Step 2: 实现**:`if stats["errors"]: 跳过步骤5精简,输出"存在迁移错误,已保留原字段"`;步骤 5 的裸 `open('w')+json.dump` 改为 `security_utils.atomic_write_json(state_path, state, use_lock=True)`。 - [x] **Step 3: 提交** `fix: migration never prunes state on partial failure`。 ### Task 6: archive_manager 原子写 + 恢复顺序反转 **Files:** - Modify: `webnovel-writer/scripts/archive_manager.py:125-128, 494-508` - Test: `webnovel-writer/scripts/data_modules/tests/test_archive_manager.py` - [x] **Step 1: `save_archive` 改用 `atomic_write_json`**(归档是数据被移出 state 后的唯一副本)。 - [x] **Step 2: `restore_character` 顺序反转**:先恢复 SQLite,确认成功后才从归档 JSON 删除该角色;SQLite 失败时归档保持原样并返回错误。写测试:monkeypatch SQLite 恢复抛异常,断言归档文件未被修改。 - [x] **Step 3: 提交** `fix: archive writes atomic, restore is delete-last`。 --- ## Phase 1 — P1 数据链一致性与流程死锁 ### Task 7: SQLite 同步失败必须可见 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_manager.py:393-416, 450-451, 606-609` - Test: `webnovel-writer/scripts/data_modules/tests/test_state_manager_extra.py` - [x] `_sync_to_sqlite` 失败时:`save_state` 返回值携带 `sqlite_sync_ok=False`;`process-chapter` CLI 据此 `emit_error`(退出码非 0),错误信息提示运行 `webnovel.py projections retry --chapter N` 补偿。测试:monkeypatch `_sync_pending_patches_to_sqlite` 抛异常,断言 CLI 退出非 0 且 stdout JSON 含补偿指引。 - [x] 提交 `fix: surface sqlite sync failures in process-chapter`。 ### Task 8: get_state_changes / get_relationships 走 SQLite 回退 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_manager.py:972-977, 1005-1013` - Test: `webnovel-writer/scripts/data_modules/tests/test_state_manager_extra.py` - [ ] 仿照 `get_entity` 的 SQLite-first 模式:先查 `self._sql_state_manager.get_entity_state_changes / get_recent_relationships`,无结果再回退内存。测试:用一个新建 StateManager 实例(模拟跨进程)读取此前保存的 state_changes,断言非空。 - [ ] 同步把 `record_state_change`(:953-966)改为只进 `_pending_state_changes`,删除向 `self._state["state_changes"]` 的追加。 - [ ] 提交 `fix: state change reads hit sqlite, not stale memory`。 ### Task 9: 事件镜像按章先删后插 **Files:** - Modify: `webnovel-writer/scripts/data_modules/event_log_store.py:109-146` - Test: `webnovel-writer/scripts/data_modules/tests/test_event_log_store.py` - [ ] 测试:同章先写 events A,再整体覆盖写 events B(不同 event_id),断言 `story_events` 表里只剩 B。实现:`_write_sqlite_mirror` 在同一事务内 `DELETE FROM story_events WHERE chapter = ?` 后再 INSERT(JSON 文件是该章事实源)。 - [ ] 提交 `fix: event mirror mirrors, not accumulates`。 ### Task 10: 投影 writer 复用 chapter_status 单调状态机 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_projection_writer.py:59-65, 95` - Test: `webnovel-writer/scripts/data_modules/tests/test_projection_writers.py` - [ ] 把 `StateManager.set_chapter_status` 的 rank 比较逻辑提取为模块级函数 `should_transition(old, new) -> bool`(同文件或 schemas.py),两处共用。测试:先投影 accepted commit 再重放历史 rejected commit,断言状态仍是 `chapter_committed`。 - [ ] 提交 `fix: projection respects chapter status monotonicity`。 ### Task 11: total_words 统一为投影重算口径 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_manager.py:280-285` - Test: 改写涉及 `update_progress` 增量口径的既有断言(探针原则) - [ ] `update_progress` 不再累加 `total_words`,只更新 `current_chapter/last_updated`;字数一律由 `StateProjectionWriter` 全量重算。grep 全仓库 `total_words` 的写入点确认只剩投影一处。 - [ ] 提交 `fix: single source of truth for total_words`。 ### Task 12: add_entity 别名并入 pending 事务 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_manager.py:839-854` - Test: `webnovel-writer/scripts/data_modules/tests/test_state_manager_extra.py` - [ ] 别名写入改为追加到 `_pending_alias_entries`,统一在 `_sync_pending_patches_to_sqlite` 落库。测试:`add_entity` 后不调 `save_state` 直接查 SQLite,断言别名尚未落库;`save_state` 后断言已落库。 - [ ] 提交 `fix: alias writes go through pending patch transaction`。 ### Task 13: SQLite 连接统一 WAL + busy_timeout + 批量事务 **Files:** - Modify: `webnovel-writer/scripts/data_modules/index_manager.py:626-634`(`_get_conn`) - Test: 现有测试回归即可 - [ ] `_get_conn` 中执行 `PRAGMA journal_mode=WAL; PRAGMA busy_timeout=8000`。投影路径上把"每方法一次 commit"合并为单连接单事务(`IndexProjectionWriter.apply` 持有一个连接传入各写方法)。 - [ ] 提交 `perf: WAL + single transaction per projection`。 ### Task 14: projection_log 持锁追加 + compact **Files:** - Modify: `webnovel-writer/scripts/data_modules/projection_log.py:101-119` - Test: `webnovel-writer/scripts/data_modules/tests/test_projection_log.py` - [ ] 追加前持 `FileLock(path + ".lock")`;新增 `compact_projection_log(project_root, keep_per_chapter=3)` 函数并挂到 `webnovel.py projections compact` 子命令。测试:写 5 条同章 run 后 compact,断言只剩 3 条且为最新。 - [ ] 提交 `fix: projection log locked appends + compact command`。 ### Task 15: legacy 提交路径加护栏 **Files:** - Modify: `webnovel-writer/scripts/data_modules/memory_contract_adapter.py:71-120, 147-156` - Test: `webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py` - [ ] `_commit_chapter_legacy` 入口检测:该章已存在 accepted commit 文件时拒绝执行并报错"该章已走 Story System 主链,禁止 legacy 双写"。docstring 标注 deprecated。 - [ ] `chapter_commit_service.py:182-188`:amend proposal 的持久化挪到投影成功之后(或写入时带 `projection_run_id`,投影失败的 run 对应提案在 `projections retry` 成功前不进 pending 列表)。测试:投影全失败时断言 override_ledger 无新增 pending 提案。 - [ ] 提交 `fix: legacy commit path refuses to double-write mainline chapters`。 ### Task 16: 清理危险死代码 **Files:** - Modify: `webnovel-writer/scripts/data_modules/state_manager.py:708-711` - [ ] 删除 `_save_state`(全仓库无调用方、绕过 pending 合并语义)。grep 确认无引用后提交 `chore: remove dangerous dead _save_state`。 ### Task 17: write SKILL blocking 死锁解除 **Files:** - Modify: `webnovel-writer/skills/webnovel-write/SKILL.md`(L162 Step 3、L245、L327-331) - Test: `webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py`(若有相关断言联动改) - [ ] Step 3 改为:blocking 定点修复后**必须重跑 review-pipeline 重新生成 review_results.json**(清零已修复项),然后才进 Step 4;并补"作者裁决保留当前版本"出口:引用 `references/review/blocking-override-guidelines.md`,写明用 override ledger 命令记录后 commit 可带 `--override-ref` 通过。两条路径都要与 `chapter_commit_service.py:45` 的 `rejected = bool(review.blocking_count)` 判定自洽(override 路径需确认 service 支持,不支持则在 service 增加 override_ref 豁免逻辑——先读 `chapter_commit_service.py` 与 `override_ledger_service.py` 确认现有机制再动笔)。 - [ ] 提交 `fix(skill): unblock the blocking-fix path in write flow`。 ### Task 18: 提示词中的错误命令修正 **Files:** - Modify: `webnovel-writer/skills/webnovel-query/SKILL.md:78`、`webnovel-writer/agents/reviewer.md:30` - [ ] `memory-contract query-rules --chapter {n}` → `--domain {domain}`(对照 `memory_cli.py:90` 实参);reviewer.md 的 `index get-state-changes --limit 20` 补必填 `--entity "{entity_id}"`。逐条在本地实际执行一次验证不报 argparse 错。 - [ ] 提交 `fix(skill): correct CLI invocations in query skill and reviewer agent`。 ### Task 19: 统一入口与 chapter_commit 参数口径对齐 **Files:** - Modify: `webnovel-writer/scripts/data_modules/webnovel.py:554-559` 或 `webnovel-writer/scripts/chapter_commit.py:23-26` - Test: `webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py` - [ ] 统一为 required(推荐,强制契约):`webnovel.py` 侧四个参数加 `required=True`,缺参时报错发生在统一入口层、信息面向作者。测试:缺 `--review-result` 调用,断言错误信息含参数名与示例。 - [ ] 提交 `fix: align chapter-commit arg contract between entrypoints`。 ### Task 20: 修复 genre-profiles 死链(题材画像复活) **Files:** - Modify: `webnovel-writer/scripts/data_modules/config.py`(新增 `references_dir` 解析)、`webnovel-writer/scripts/data_modules/context_manager.py:337-338`、`webnovel-writer/scripts/data_modules/memory_contract_adapter.py:245` - Test: `webnovel-writer/scripts/data_modules/tests/test_context_manager.py` - [ ] config 新增解析顺序:`{project_root}/.claude/references/`(用户覆盖)→ `{plugin_root}/references/`(默认,由 `Path(__file__).resolve().parents[2] / "references"` 推导)。两处读取点改用 `config.references_dir / "genre-profiles.md"`。 - [ ] 测试改造:现有手搓 `.claude/references` 的测试保留(验证覆盖优先级),新增"无项目级文件时回退插件目录"的测试。 - [ ] 提交 `fix: genre profiles resolve to plugin references by default`。 ### Task 21: story_system_engine base_context 必空 bug **Files:** - Modify: `webnovel-writer/scripts/data_modules/story_system_engine.py:125, 412-423` - Test: `webnovel-writer/scripts/data_modules/tests/test_story_system_engine.py` - [ ] `_apply_reasoning` 的早退分支也为每行设置 `_priority_rank`:base_context 来源行设 0、dynamic 行设 1(保持 base 优先),保证 `build()` 的 `< 999` 过滤不再误杀。测试:用没有裁决规则行的题材跑 `build()`,断言 `master_setting.base_context` 非空。 - [ ] 提交 `fix: base_context survives the no-reasoning path`。 ### Task 22: extract_chapter_context 载荷去重 **Files:** - Modify: `webnovel-writer/scripts/extract_chapter_context.py:321-354` - Test: `webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py` - [ ] 顶层 `outline`/`previous_summaries`/`state_summary` 与 `core.*` 二选一:保留 `core.*`(ContextManager 已排序),顶层字段删除;消费方(context-agent.md 中引用了哪些键先 grep 确认)同步更新。测试断言 payload 中大纲文本只出现一次。 - [ ] 提交 `fix: dedupe chapter context payload`。 ### Task 23: 伏笔紧急度量纲统一 **Files:** - Modify: `webnovel-writer/skills/webnovel-query/references/advanced/foreshadowing.md` - [ ] 公式改为 0-100 量纲:`紧急度 = min(100, (已过章节/目标章节) × 层级权重 × 33.3)` 或直接改为与 `urgency_utils` 的 high/medium/low≈100/60/20 对齐的阈值表;删除与"核心 50-300 章"矛盾的"核心 >50 章未回收即 Critical"行,改为"超过该伏笔自身 target 章节数即 Critical"。示例数值同步重算。 - [ ] 提交 `fix(ref): foreshadowing urgency uses runtime 0-100 scale`。 ### Task 24: 散件正确性修复打包 **Files:** - Modify: `webnovel-writer/scripts/data_modules/rag_adapter.py:1477, 615-649`、`api_client.py`(9 处 print)、`knowledge_query.py:17,46`、`webnovel.py:127, 583`、`update_state.py:344-351,609-611`、`config.py:30-48,60`、`context_manager.py:790-829` - Test: 各对应测试文件 - [ ] rag_adapter `index-chapter` 用 `adapter.config.project_root` 替代未定义 `config`;`vector_search` 行解包 `chapter` 改名 `row_chapter`。 - [ ] api_client 全部 `[WARN]/[ERR]/[WARMUP]` 改 `file=sys.stderr`。 - [ ] knowledge_query 连接前 `is_file()` 检查,缺失时输出含修复建议的友好错误。 - [ ] `webnovel.py` `int(e.code or 0)` 对非 int code 打印后返回 1;`knowledge` subparsers 加 `required=True`。 - [ ] update_state `update_strand_tracker` 失败累计并以非零退出。 - [ ] config `.env` 值 `strip().strip("\"'")`;`_load_dotenv` 从模块导入时移到 `from_project_root` 显式调用。 - [ ] context_manager CLI 失败路径 `sys.exit(1)`。 - [ ] 每修一处先在对应测试文件加探针测试。完成后提交 `fix: batch correctness fixes from audit`。 --- ## Phase 2 — P2 安全与隐私 ### Task 25: 嵌入出网需要显式配置 **Files:** - Modify: `webnovel-writer/scripts/data_modules/api_client.py`(embed/rerank 调用入口)、`vector_projection_writer.py:236-246` - Test: `webnovel-writer/scripts/data_modules/tests/test_vector_projection_writer.py` - Docs: `webnovel-writer/README.md`、`docs/guides/rag-and-config.md` - [ ] `EMBED_API_KEY` 为空时 vector 投影直接返回 `{"status": "skipped", "reason": "no_api_key"}`,不发任何 HTTP 请求。测试:key 为空 + monkeypatch aiohttp 断言零请求。 - [ ] 文档新增"数据出网说明"小节:明确写出默认端点、发送内容(摘要/场景/事件文本)、如何关闭。 - [ ] 提交 `fix: no network egress without explicit api key`。 ### Task 26: 写守卫 hook 加固 **Files:** - Modify: `webnovel-writer/hooks/guard_runtime_write.py:61-67, 101-110` - Test: `webnovel-writer/scripts/tests/test_hooks.py` - [ ] 正则修复:`\b(>|out-file|...)` 中 `>` 单独处理(`(?:^|\s)>{1,2}(?:\s|$)|\b(out-file|set-content|add-content|copy-item|move-item|cp|mv|rm|tee|sed|python|python3)\b`);测试用例覆盖 `echo x > .webnovel/state.json`、`mv a .webnovel/state.json`、`tee .webnovel/index.db`。 - [ ] `_deny` 的 JSON 决策输出改打 stdout(退出码 2 保留),`systemMessage` 才能被宿主解析。 - [ ] 提交 `fix(hooks): close redirect/unix-command bypass in write guard`。 ### Task 27: dashboard 最小防护 **Files:** - Modify: `webnovel-writer/dashboard/app.py`、`server.py` - Test: `webnovel-writer/scripts/tests/test_dashboard_security.py` - [ ] 加 `TrustedHostMiddleware(allowed_hosts=["localhost", "127.0.0.1"])`(防 DNS rebinding);`--host` 非回环地址时打印醒目警告"整个项目将对网络可见";支持 `WEBNOVEL_DASHBOARD_TOKEN` 环境变量,设置后所有 `/api/*` 校验 `Authorization: Bearer`。测试:伪 Host 头请求返回 400;带错误 token 返回 401。 - [ ] 顺手修 `app.py:175` `_inspect_vector_db` 连接泄漏(包 `closing()`)。 - [ ] 提交 `fix(dashboard): trusted host + optional token auth`。 ### Task 28: CI 加固 **Files:** - Modify: `.github/workflows/plugin-version.yml`、`.github/workflows/plugin-release.yml:43-51, 108` - [ ] plugin-version.yml 顶部加 `permissions: contents: read`。 - [ ] release 的 `workflow_dispatch` version 输入加 `[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1` 前置校验;`softprops/action-gh-release@v2` pin 到具体 commit SHA;`git ls-remote` 区分查询失败(退出码非 0 且非"未找到")与 tag 不存在。 - [ ] 注意:不要动 README 版本表相关检查(CI 硬约束)。提交 `ci: least privilege + input validation + pinned action`。 ### Task 29: 首次运行体验 **Files:** - Modify: `webnovel-writer/skills/webnovel-init/SKILL.md`(Step 0 预检)、`webnovel-writer/hooks/hooks.json` - Test: 手动验证 - [ ] init SKILL 的 Step 0 增加一行指令:先运行 `python -X utf8 "{plugin_root}/scripts/webnovel.py" doctor --format json`,存在 blocker(缺 pydantic/aiohttp 等)时向作者展示一键安装命令 `python -m pip install -r "{plugin_root}/scripts/requirements.txt"` 并等待完成再继续(doctor 已有 `python.import.*` 检查,无需新代码)。 - [ ] hooks.json 保持 `python`,但 `session_start.py` 在 `webnovel.py` 调用失败(FileNotFoundError/非零退出)时输出一行"⚠️ Python 环境异常,运行 /webnovel-doctor 检查"——守卫 hook 的 fail-open 风险在 doctor 报告中体现(新增 check:`shutil.which("python")` 探测)。 - [ ] 提交 `feat: dependency preflight in init + doctor python check`。 --- ## Phase 3 — P3 质量与卫生(可与 Phase 2 并行) ### Task 30: 提示词重复下沉 **Files:** - Create: `webnovel-writer/references/shared/author-report-contract.md` - Modify: `webnovel-writer/skills/{webnovel-init,webnovel-plan,webnovel-write,webnovel-review}/SKILL.md`、`webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py`、`webnovel-writer/references/index/reference-loading-map.md` - [ ] "作者友好过程提示与恢复契约"+"最终报告契约"两节(4 个 skill 重复约 60-70 行)抽到共享文件,各 SKILL 留一行引用 + stage 差异参数。SubagentRun JSON 模板同理只在共享文件保留一份。`test_prompt_integrity.py` 的文案断言按探针原则改为断言引用行存在。loading-map 登记新文件。SKILL 改动遵循"只写指令"原则——不留解释性注释。 - [ ] 提交 `refactor(skill): hoist author report contract to shared reference`。 ### Task 31: 提示词过期内容清理 **Files:** - Modify: `webnovel-writer/templates/genres/*.md`(34 个 XML 实体段 + 30 个 Pack 编号)、`webnovel-writer/references/index/reference-loading-map.md`、`webnovel-writer/references/genre-profiles.md`、`webnovel-writer/references/shared/core-constraints.md:6-7`、`webnovel-writer/references/review-schema.md:3`、`webnovel-writer/references/review/blocking-override-guidelines.md:3,8,39`、`webnovel-writer/skills/webnovel-write/references/style-adapter.md:9`、`webnovel-writer/skills/webnovel-write/references/anti-ai-guide.md` - [ ] 批量删 genre 模板的 `` 扩展段与悬空 Pack 编号行(脚本化 sed/python 批改后抽查 3 个文件)。 - [ ] loading-map 对照 8 个 SKILL 重新核账步骤号;genre-profiles 为缺失题材补段或在 §2 头部写明 fallback 规则(命中失败 → 使用 shuangwen 基线段);§3 删除 Checkers/`project.genre` 旧引用。 - [ ] anti-ai-guide 三方矛盾裁决:保留文件、头部"加载时机"改为"润色阶段按需",与 polish-guide 重叠词条合并,登记进 loading-map 非直接调用表。 - [ ] 各文件头部步骤号修正(core-constraints/review-schema/blocking-override/style-adapter)。 - [ ] 提交 `docs(ref): purge stale protocol fragments and fix loading map drift`。 ### Task 32: CLI 样板抽取 **Files:** - Create: `webnovel-writer/scripts/data_modules/cli_runtime.py` - Modify: `entity_linker.py`、`index_manager.py`、`rag_adapter.py`、`state_manager.py`、`sql_state_manager.py`、`style_sampler.py` 各 `main()`,`webnovel.py:306-381` - [ ] 提供 `resolve_config(args) -> DataModulesConfig`(封装 normalize_global_project_root + resolve_project_root + from_project_root)与 `run_cli(fn)` 装饰器(统一 emit_success/emit_error + 退出码)。六个入口迁移;`webnovel.py` 的 run-ledger/run-log 复抄段改为转发。现有 CLI 测试全量回归。 - [ ] 提交 `refactor: extract cli_runtime, dedupe six entrypoints`。 ### Task 33: 性能修复 **Files:** - Modify: `webnovel-writer/scripts/data_modules/rag_adapter.py:583-650, 721-758`、`webnovel-writer/scripts/status_reporter.py:370-460` - [ ] vector 直连路径复用 hybrid 的 `vector_full_scan_max_vectors` 预筛;bm25 命中 chunk 改 `WHERE chunk_id IN (...)` 一次取回;status_reporter 开头一次性 `SELECT * FROM entities/chapters` 建 dict,删除循环内单查。用现有测试回归 + 千章模拟数据手测对比耗时(可选)。 - [ ] 提交 `perf: kill N+1 queries in rag and status reporter`。 ### Task 34: 仓库与测试卫生 **Files:** - Modify: `webnovel-writer/scripts/data_modules/tests/test_story_system_engine.py`(或其 fixture)、`sitecustomize.py`、`.github/workflows/plugin-version.yml`、根 `requirements.txt` 三份、`webnovel-writer/scripts/update_state.py:159-178`、`webnovel-writer/scripts/data_modules/summary_projection_writer.py:20` - [ ] 找到向仓库根写 `.tmp_story_system_engine/case_*` 的测试,改用 pytest `tmp_path`;删除根目录现存 231 个残留目录。 - [ ] `sitecustomize.py`:移出仓库(或改名 `sitecustomize.py.example` + README 说明),避免影响分发用户。 - [ ] CI 增加 dist 同步校验 step:`npm ci && npm run build` 后 `git diff --exit-code webnovel-writer/dashboard/frontend/dist`(或对比构建哈希),防止前端源码与 dist 漂移。 - [ ] requirements 关键依赖加上界(`fastapi>=0.110,<1`、`pydantic>=2,<3` 等);确认 dashboard 的 httpx 是否仅测试用,是则移到测试依赖。 - [ ] update_state 备份文件按数量滚动清理(保留最近 20 个);summary 投影写改 tmp+replace。 - [ ] 提交 `chore: test/repo hygiene batch`。 --- ## 验收清单(整体) - [ ] `python -m pytest`(PYTHONUTF8=1)全绿,覆盖率 ≥90% 不回退 - [ ] 手动冒烟:新建 tmp 项目跑 init → 写一章 → review → chapter-commit → projections retry → dashboard 启动 - [ ] `git grep -n "checkout master"` 在 scripts 下零命中 - [ ] 无 key 环境跑一次 chapter-commit,抓包/日志确认零出网请求 - [ ] README 版本表未被改动(CI 硬约束)