Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,90 @@ RERANK_API_KEY=your_rerank_api_key
/webnovel-review 1-5 # 审查第 1-5 章
```

## 命令总览(更新)

> 你可以只记住两层命令:
> 1) Claude Code 插件命令(`/webnovel-*`)
> 2) 统一 CLI 命令(`python .../webnovel.py <subcommand>`)

### A. Claude Code 插件命令(面向日常创作)

| 命令 | 作用 | 常见用法 |
|---|---|---|
| `/webnovel-init` | 初始化书项目目录、设定模板、状态文件 | `/webnovel-init` |
| `/webnovel-plan [卷号]` | 生成卷级规划和章节大纲 | `/webnovel-plan 1` |
| `/webnovel-write [章号]` | 执行完整写作链:上下文→起草→审查→提交 | `/webnovel-write 35` |
| `/webnovel-review [范围]` | 对已有章节做质量审查 | `/webnovel-review 31-35` |
| `/webnovel-query [关键词]` | 查询角色/伏笔/状态等运行时信息 | `/webnovel-query 萧炎` |
| `/webnovel-learn [内容]` | 抽取经验写入项目记忆 | `/webnovel-learn "这章危机钩有效"` |
| `/webnovel-dashboard` | 启动只读可视化面板 | `/webnovel-dashboard` |

### B. 统一 CLI 子命令(面向运维与自动化)

统一入口:

```bash
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" <子命令> [参数]
```

| 子命令 | 作用 |
|---|---|
| `preflight` | 校验脚本、项目根、主链健康状态 |
| `where` / `use` | 查看/绑定当前项目根目录 |
| `story-system` | 生成或刷新 Story System 合同(master/volume/chapter/review) |
| `review-pipeline` | 处理 reviewer JSON,生成报告并写审查指标 |
| `chapter-commit` | 提交章节事实并触发 projection(state/index/summary/memory/vector) |
| `orchestrate` | 批量自动编排(write/heal/nightly),减少手工逐条命令执行 |
| `rag` | 向量检索与索引管理(如按章索引、统计) |
| `index/state/entity/context/style` | 各类数据模块运维入口 |
| `status/update-state/backup/archive` | 状态巡检、手工更新、备份与归档 |
| `memory` / `memory-contract` / `project-memory` | 长期记忆查询、合同管理、项目记忆管理 |
| `story-events` | 查询章节事件或查看事件链健康 |
| `extract-context` | 提取指定章节上下文 |
| `master-outline-sync` | 卷规划后写回总纲锚点 |

`orchestrate` 示例:

```bash
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate write --chapters 1-20 --auto-vector-heal
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate heal --chapters 1-200 --json-report-out ".webnovel/reports/heal.json"
```

实体脏数据(如拼音/英文 snake_case)扫描与标记:

```bash
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" entity-clean --mark-invalid --format json --chapter 100
```

### Claude Code 插件内一键修复(推荐)

如果你在 **Claude Code 对话框内** 操作,优先用插件命令(不需要手动拼 Python 命令):

```bash
/webnovel-review 1-100
```

先对 1-100 章做一次总审查,确认阻断项和偏纲章段。

然后在 Claude Code 中执行(同一项目会话内):

```bash
/webnovel-write 100
```

用于重跑第 100 章完整链(上下文→起草/修复→审查→提交→投影)。
如果你要批量修复多章,建议在 Claude Code 中让助手按章循环执行 `/webnovel-write N`,每 10 章做一次 `/webnovel-review A-B` 复核。

坏章定点修复示例(15/91/92/94):

```bash
/webnovel-write 15
/webnovel-write 91
/webnovel-write 92
/webnovel-write 94
/webnovel-review 1-100
```

### 6) 可视化面板(可选)

```bash
Expand Down
105 changes: 105 additions & 0 deletions docs/guides/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,111 @@ python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJE
| `backup` | 备份管理 |
| `archive` | 归档管理 |
| `extract-context` | 提取章节上下文(`--chapter N --format json`) |
| `orchestrate` | 批量自动编排(`write/heal/nightly`),按章节范围自动执行检查与修复 |

`orchestrate` 示例:

```bash
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate write --chapters 1-20 --auto-vector-heal
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate heal --chapters 1-200 --fail-fast
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate nightly --chapters 50-80 --json-report-out ".webnovel/reports/nightly.json"
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate autofix --bad-chapters 15,91,92,94 --auto-vector-heal --entity-clean --sync-outline-volumes 2,3 --json-report-out ".webnovel/reports/autofix.json"
```

### 100 章后偏纲的完整修复链(推荐顺序)

当你已经写到 100 章并发现“正文偏离大纲”,建议按下列顺序执行,保证大纲/向量/关系链/实体/事件都被重建与校验:

```bash
# 0) 环境与主链预检
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" preflight --format json

# 1) 批量修复链(审查 + commit + projection + 向量补偿)
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate heal --chapters 1-100 --auto-vector-heal --json-report-out ".webnovel/reports/heal-1-100.json"

# 2) 事件链健康(伏笔追踪/关系链基础)
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" story-events --health

# 3) 实体脏数据扫描并标记(拼音/英文 snake_case)
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" entity-clean --mark-invalid --format json --chapter 100

# 4) 关键观测(实体、关系、审查趋势)
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" index get-core-entities
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" index get-relationship-graph --format json
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" index get-review-trend-stats --last-n 20
```

说明:
- `orchestrate heal` 负责批量修复执行链。
- `story-events --health` 用于检查事件链断裂与健康状态。
- `entity-clean` 会抓出类似 `old_book_knock_mark` 这类脏实体并写入 `invalid_facts` 待处理。

### Claude Code 插件方式:检查与修复(无需手动找命令)

在 Claude Code 里建议按以下节奏执行:

```bash
/webnovel-review 1-100
```

先出总审查报告,定位偏纲、设定冲突、时间线冲突。

```bash
/webnovel-write 91
/webnovel-write 92
/webnovel-write 93
...
/webnovel-write 100
```

对问题章节逐章重跑完整链路(包含审查与提交流程),每批修复完成后再复查:

```bash
/webnovel-review 91-100
```

如需检查运行时数据一致性,再补一条:

```bash
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate heal --chapters 91-100 --auto-vector-heal
```

### 自动修复建议(指定坏章,如 15/91/92/94)

你这个想法非常好,推荐用“**坏章列表自动修复 + 全书连贯复核 + 大纲反写**”三段式:

1) 先审查定位:

```bash
/webnovel-review 1-100
```

2) 在 Claude Code 中按坏章列表循环触发子 agent(示例):

```bash
/webnovel-write 15
/webnovel-write 91
/webnovel-write 92
/webnovel-write 94
```

> `webnovel-write` 会自动调用 context/reviewer/data-agent 子链做修复与重提交。

3) 修完后做全书连贯复核(防止局部修复引发跨章断裂):

```bash
/webnovel-review 1-100
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" story-events --health
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" orchestrate heal --chapters 1-100 --auto-vector-heal --json-report-out ".webnovel/reports/continuity-1-100.json"
```

4) 最后做“大纲反写/校准”,保证后续生成不受旧偏差影响:

```bash
# 先确认受影响卷号,然后逐卷执行
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" master-outline-sync --volume 2 --format json
python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" master-outline-sync --volume 3 --format json
```

### 长期记忆子命令

Expand Down
45 changes: 45 additions & 0 deletions webnovel-writer/scripts/data_modules/webnovel.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,23 @@ def main() -> None:
p_review_pipeline.add_argument("--report-file", default="", help="审查报告路径")
p_review_pipeline.add_argument("--save-metrics", action="store_true", help="直接写入 index.db")


p_orchestrate = sub.add_parser("orchestrate", help="批量自动编排写作/修复流程")
p_orchestrate.add_argument("mode", choices=["write", "heal", "nightly", "autofix"], help="运行模式")
p_orchestrate.add_argument("--chapters", default="1", help="章节范围,如 1-50")
p_orchestrate.add_argument("--bad-chapters", default="", help="坏章列表,如 15,91,92,94")
p_orchestrate.add_argument("--fail-fast", action="store_true", help="遇到错误即停止")
p_orchestrate.add_argument("--auto-vector-heal", action="store_true", help="自动补偿向量索引")
p_orchestrate.add_argument("--entity-clean", action="store_true", help="autofix 收尾时扫描实体脏数据")
p_orchestrate.add_argument("--sync-outline-volumes", default="", help="autofix 后回写总纲卷号,如 2,3")
p_orchestrate.add_argument("--json-report-out", default="", help="输出批处理 JSON 报告")


p_entity_clean = sub.add_parser("entity-clean", help="扫描并标记实体脏数据(拼音/英文 snake_case)")
p_entity_clean.add_argument("--format", choices=["json", "text"], default="json")
p_entity_clean.add_argument("--mark-invalid", action="store_true", help="写入 invalid_facts 待处理项")
p_entity_clean.add_argument("--chapter", type=int, default=None, help="可选:标记所属章节")

p_placeholder_scan = sub.add_parser("placeholder-scan", help="扫描大纲/设定集未补齐占位")
p_placeholder_scan.add_argument("--format", choices=["json", "text"], default="json", help="输出格式")

Expand Down Expand Up @@ -462,6 +479,34 @@ def main() -> None:
raise SystemExit(_run_script("memory_cli.py", [*forward_args, *rest]))
if tool == "project-memory":
raise SystemExit(_run_script("project_memory.py", [*forward_args, *rest]))
if tool == "entity-clean":
return_args = [*forward_args, "--format", str(args.format)]
if args.mark_invalid:
return_args.append("--mark-invalid")
if args.chapter is not None:
return_args.extend(["--chapter", str(args.chapter)])
raise SystemExit(_run_script("entity_cleanup.py", return_args))

if tool == "orchestrate":
return_args = [
*forward_args,
str(args.mode),
"--chapters", str(args.chapters),
]
if args.bad_chapters:
return_args.extend(["--bad-chapters", str(args.bad_chapters)])
if args.fail_fast:
return_args.append("--fail-fast")
if args.auto_vector_heal:
return_args.append("--auto-vector-heal")
if args.entity_clean:
return_args.append("--entity-clean")
if args.sync_outline_volumes:
return_args.extend(["--sync-outline-volumes", str(args.sync_outline_volumes)])
if args.json_report_out:
return_args.extend(["--json-report-out", str(args.json_report_out)])
raise SystemExit(_run_script("orchestrate.py", return_args))

if tool == "review-pipeline":
return_args = [
*forward_args,
Expand Down
76 changes: 76 additions & 0 deletions webnovel-writer/scripts/entity_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import argparse
import json
import re
import sqlite3
from pathlib import Path

from data_modules.config import DataModulesConfig

ASCII_SNAKE = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+){1,}$")


def _looks_dirty(entity_id: str, canonical_name: str) -> bool:
eid = (entity_id or "").strip().lower()
name = (canonical_name or "").strip().lower()
return bool(ASCII_SNAKE.match(eid) or ASCII_SNAKE.match(name))


def main() -> None:
parser = argparse.ArgumentParser(description="Cleanup dirty entities (snake_case pinyin/english)")
parser.add_argument("--project-root", required=True)
parser.add_argument("--format", choices=["json", "text"], default="json")
parser.add_argument("--mark-invalid", action="store_true", help="write invalid_facts rows for dirty entities")
parser.add_argument("--chapter", type=int, default=None)
args = parser.parse_args()

config = DataModulesConfig.from_project_root(Path(args.project_root))
db_path = Path(config.index_db)
rows = []
with sqlite3.connect(str(db_path)) as conn:
cur = conn.cursor()
cur.execute("SELECT id, type, canonical_name FROM entities")
all_rows = cur.fetchall()
for entity_id, entity_type, canonical_name in all_rows:
if _looks_dirty(str(entity_id), str(canonical_name or "")):
rows.append({"id": entity_id, "type": entity_type, "canonical_name": canonical_name})

marked = 0
if args.mark_invalid and rows:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS invalid_facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_type TEXT NOT NULL,
source_id TEXT NOT NULL,
reason TEXT NOT NULL,
marked_by TEXT,
chapter INTEGER,
status TEXT DEFAULT 'pending',
marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP
)
"""
)
for item in rows:
cur.execute(
"INSERT INTO invalid_facts (source_type, source_id, reason, marked_by, chapter, status) VALUES (?, ?, ?, ?, ?, 'pending')",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Insert invalid facts using the project schema

On initialized projects, invalid_facts is already created by index_manager with chapter_discovered and confirmed_at, not chapter/resolved_at; because CREATE TABLE IF NOT EXISTS leaves that table unchanged, entity-clean --mark-invalid fails with no column named chapter as soon as it finds a dirty entity. The insert should use the existing schema (or the shared mark_invalid_fact helper) so the advertised cleanup command works on real project databases.

Useful? React with 👍 / 👎.

("entity", item["id"], "dirty_ascii_snake_entity", "entity_cleanup", args.chapter),
)
marked += 1
conn.commit()

payload = {"dirty_entities": rows, "count": len(rows), "marked_invalid": marked}
if args.format == "json":
print(json.dumps(payload, ensure_ascii=False, indent=2))
else:
print(f"dirty_entities={len(rows)} marked_invalid={marked}")
for item in rows:
print(f"- {item['id']} ({item['canonical_name']})")


if __name__ == "__main__":
main()
Loading