# Webnovel Write Batch

> 连续写作多章。当用户要求多章写作时必须使用此 skill。 ## 触发条件 - "连续写N章"、"写第X-Y章"、"批量写X-Y章"、"一次写N章" - "重写第X-Y章"、"修改第X-Y章" - "多章"、"多章节" ## 区分规则 - 单章 → webnovel-write - 多章 → 必须使用本 skill

- Skill: `lujih/webnovel-write-batch` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lujih/webnovel-write-batch`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lujih/webnovel-write-batch/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lujih (https://skillmd.com/u/lujih)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lujih/webnovel-write-batch

---


# 批量写作

## ⛔ 硬规则（Anti-Laziness）

以下规则针对 AI 在长循环中的已知偷懒模式。**逐章执行前必须重现此清单。**

| # | 禁止 | 正确做法 |
|---|------|---------|
| 1 | 口头描述代替 `Agent()` 调用 | 必须使用 Agent 工具调用 context-agent / chapter-writer-agent / reviewer / data-agent |
| 2 | 跳过审查 | 每章必须运行 reviewer Agent + review-pipeline。blocking=false 也不能跳过 |
| 3 | 用 Read 工具代替验证命令 | 每步后必须运行 bash 验证命令（test -s / ls / python -c） |
| 4 | "稍后更新" batch_state | 每章完成后立即用 python -c 写 JSON，写完后重新读取验证 |
| 5 | 章数多了开始跳步/简化流程 | 每章必须完整执行本 skill 的 9 步，不得缩减。3 章后强制暂停 |
| 6 | 子代理失败后假装成功 | 每个 Agent() 调用后检查输出文件是否存在且非空。失败重试 1 次→仍失败则停止 |
| 7 | 审查和润色合并执行 | chapter-writer-agent 先写正文 → reviewer 审查 → 有 blocking 则回传修复。禁止在审查前自行修改 |
| 8 | 用简化版代替完整单章流程 | 每章 = 完整闭环。chapter-writer-agent 在干净上下文中执行创作，质量等同于单章 |

## 环境设置

```bash
export SCRIPTS_DIR="${PWD}/.opencode/scripts"
export SKILL_ROOT="${PWD}/.opencode/skills/webnovel-write"
test -d "${SCRIPTS_DIR}" || { echo "错误: 未找到 ${SCRIPTS_DIR}，请确保当前目录是 webnovel-writer 仓库根目录"; exit 1; }

export PYTHONUTF8=1

export PROJECT_ROOT="$(python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PWD}" where)"
# 归一化为正斜杠，避免路径中的 \b \n 等在 python -c 中被转义
export PROJECT_ROOT="${PROJECT_ROOT//\\//}"
test -n "$PROJECT_ROOT" && test -f "${PROJECT_ROOT}/.webnovel/state.json" || { echo "错误: PROJECT_ROOT 解析失败，请用 --project-root 显式指定"; exit 1; }
```

## Step 0: 解析章节范围

- "写第9-15章" → S=9, E=15
- "连续写3章" → **扫描文件系统获取最新章节号**（同单章 Step 0），S=最新章+1, E=S+2
- "批量写5章" → 同上，E=S+4

**禁止依赖 state.json 的 current_chapter 或对话记忆来确定起始章号。** 章节文件是唯一真源。

```bash
LATEST=$(python -c "
import re
from pathlib import Path
text_dir = Path('${PROJECT_ROOT}') / '正文'
nums = []
for f in text_dir.rglob('第*章*.md'):
    m = re.match(r'第0*(\d+)章', f.name)
    if m:
        nums.append(int(m.group(1)))
print(max(nums) if nums else 0)
")
echo "最新章节: 第${LATEST}章"
```

若无法解析，询问用户明确范围。上限 3 章（可通过 `--force` 参数绕过，上限 5 章）。

## Step 0.5: 断点恢复

```bash
BATCH_STATE="${PROJECT_ROOT}/.webnovel/batch_state.json"

if [ -f "$BATCH_STATE" ]; then
  STATUS=$(python -c "import json; print(json.load(open('$BATCH_STATE',encoding='utf-8-sig')).get('status',''))")
  if [ "$STATUS" = "running" ]; then
    echo "检测到未完成的批量任务"
    python -c "
import json
s = json.load(open('$BATCH_STATE', encoding='utf-8-sig'))
print(f'  已完成: 第{s[\"completed_chapters\"]}章')
print(f'  待恢复: 第{s[\"current_chapter\"]}章')
"
    # 询问用户：继续 / 重新开始 / 放弃
  elif [ "$STATUS" = "completed" ]; then
    echo "上一次批量任务已完成。"
    # 询问是否开始新批次
  fi
fi
```

## Step 0.6: 初始化 batch_state（新任务）

```bash
if [ ! -f "$BATCH_STATE" ] || [ "$STATUS" != "running" ]; then
  python -c "
import json, datetime
s = {
  'task_id': f'batch_{datetime.datetime.now(datetime.timezone.utc).strftime(\"%Y%m%d_%H%M%S\")}',
  'range': {'start': $S, 'end': $E},
  'status': 'paused',
  'current_chapter': $S,
  'completed_chapters': [],
  'failed_chapters': [],
  'chapter_results': {},
  'created_at': datetime.datetime.now(datetime.timezone.utc).isoformat()
}
open('$BATCH_STATE', 'w', encoding='utf-8').write(json.dumps(s, ensure_ascii=False, indent=2))
"
fi
```

---

## 批前一次性验证

在进入逐章循环前执行一次 preflight，后续各章不再重复。

```bash
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" preflight
```

---

## 逐章循环

> **每章 = 9 步完整闭环。创作在独立 subagent 干净上下文中执行。**

### 逐章检查清单（每章开始前重现）

**步骤映射：** 0=环境变量验证, A=上章完整性, 1=刷新合同, 1b=结构自检, 2=context-agent, 3=写作, 4=审查, 5=review-pipeline, 6=修复轮, 7=data-agent, 8=commit+验证, 9=更新状态

```
□ 0. 环境变量验证
□ A. 上章完整性检查（N > S 时）
□ 1. 刷新合同树
□ 1b. 结构自检
□ 2. Agent(context-agent) → 写作任务书
□ 3. Agent(chapter-writer-agent) → 起草+润色
□ 4. Agent(reviewer) → 审查结果
□ 5. review-pipeline → 判定 blocking
□ 6. blocking? → 提取反馈 → 回到 3（最多 2 轮）
□ 7. Agent(data-agent) → 事实提取
□ 8. chapter-commit + 验证投影 + Git 备份
□ 9. 更新 batch_state + 进度反馈
```

每步执行前打印进度标识：

```bash
echo "[Ch{N} Step {M}/9] {step_name}..."
```

---

### Step 0: 环境变量验证（每章开始前强制）

```bash
test -n "$PROJECT_ROOT" || { echo "❌ PROJECT_ROOT 未设置"; exit 1; }
test -n "$SCRIPTS_DIR" || { echo "❌ SCRIPTS_DIR 未设置"; exit 1; }
test -d "$PROJECT_ROOT" || { echo "❌ PROJECT_ROOT 目录不存在: $PROJECT_ROOT"; exit 1; }
test -d "${PROJECT_ROOT}/.webnovel" || { echo "❌ ${PROJECT_ROOT}/.webnovel 不存在，PROJECT_ROOT 可能不正确"; exit 1; }
echo "✅ PROJECT_ROOT=${PROJECT_ROOT}"
```

**状态门**: 若 batch_state.status 为 "paused"，输出进度摘要并等待用户输入 "继续" 才将 status 改为 "running"。

---

### Step A: 上章完整性检查（N > S 时强制）

```bash
if [ "$N" -gt "$S" ]; then
  PREV=$((N - 1))

  CHAPTER_FILE=$(python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" chapter-path --chapter $PREV)
  if [ ! -s "${PROJECT_ROOT}/${CHAPTER_FILE}" ]; then
    echo "❌ 第${PREV}章文件缺失。立即停止。"
    exit 1
  fi

  IN_BATCH=$(python -c "import json; s=json.load(open('$BATCH_STATE',encoding='utf-8-sig')); print($PREV in s.get('completed_chapters',[]))")
  if [ "$IN_BATCH" != "True" ]; then
    echo "❌ 第${PREV}章未在 batch_state 中标记完成。立即停止。"
    exit 1
  fi

  echo "✅ 第${PREV}章完整性检查通过"
fi
```

---

### Step 1: 刷新合同树

```bash
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" placeholder-scan --format text
```

genre 从 `.webnovel/state.json` 的初始化配置快照读取。调用 story-system 前必须先从详细大纲解析真实本章目标，禁止传 `{章纲目标}`、`第N章章纲目标` 等占位 query。

```bash
# 用 skill_runner 传递 CJK，genre 自动从 state.json 读取，goal 从 stdin 传入
echo "${CHAPTER_GOAL}" | python -X utf8 "${SCRIPTS_DIR}/skill_runner.py" story-system \
  --project-root "${PROJECT_ROOT}" --chapter {N}
if [ $? -ne 0 ]; then
  echo "❌ story-system 合同刷新失败，阻断流程"
  exit 1
fi
```

必备文件：`MASTER_SETTING.json`、`volume_{NNN}.json`、`chapter_{NNN}.review.json`。缺失则阻断。

`chapter_{NNN}.json` 必须优先检查顶层 `chapter_directive`。`chapter_focus` 只能来自 `chapter_directive.goal` 或真实 query，不得从 `dynamic_context` 的参考摘要继承。

---

### 准备：结构自检

```bash
# 从章纲提取 intended_strand（统一小写，避免大小写不匹配）
INTENDED_STRAND=$(python -c "
import json
contract_file = '${PROJECT_ROOT}/.story-system/chapters/chapter_$(printf '%03d' {N}).json'
try:
    d = json.load(open(contract_file))
    s = d.get('chapter_directive', {}).get('strand', '')
    print(s.strip().lower())
except: pass
")

python -X utf8 "${SCRIPTS_DIR}/skill_runner.py" check-structural \
  --project-root "${PROJECT_ROOT}" --chapter {N} --intended-strand "${INTENDED_STRAND}" --format json \
  --output "${PROJECT_ROOT}/.webnovel/tmp/structural_check.json"
```

```bash
python -c "
import json, sys
d = json.load(open('${PROJECT_ROOT}/.webnovel/tmp/structural_check.json'))
if not d.get('passed'):
    print('❌ 结构自检未通过，停止流程')
    for c in d['checks']:
        if c['severity'] == 'blocking' and not c['passed']:
            print(f'  BLOCKING: {c[\"name\"]}: {c[\"detail\"]}')
            print(f'  FIX: {c[\"fix\"]}')
    sys.exit(1)
" || exit 1
# (use $? check for PowerShell compatibility)
```

---

### Step 2: Agent(context-agent) → 写作任务书

**必须使用 Agent 工具调用 context-agent，不得由主流程自行整理。**

```text
Agent(
  subagent_type: "context-agent",
  prompt: "chapter={N}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}; storage_path=${PROJECT_ROOT}/.webnovel; state_file=${PROJECT_ROOT}/.webnovel/state.json（projection/read-model，仅兼容读取）。先 research，再按 本章硬性约束→CBN/CPNs/CEN→本章禁区→风格指引→dynamic_context补充参考 的顺序输出五段写作任务书。"
)
```

验证：任务书非空且包含"硬性约束"或"CBN"字样。失败或返回空→重试 1 次→仍失败则停止。

---

### Step 3: Agent(chapter-writer-agent) → 起草+润色

**核心变更：原来主线程的起草(Step 3)和润色(Step 5)合并为一次 Agent 调用，在干净上下文中完成完整创作闭环。**

传入 prompt 结构：

```text
Agent(
  subagent_type: "chapter-writer-agent",
  prompt: """
【章节】第{N}章
【字数】2000-2500 字

【写作任务书】
{context-agent 产出的完整任务书}

【章纲约束】
{从 chapter_{NNN}.json 提取的 chapter_directive.goal / time_anchor / countdown / chapter_end_open_question / must_cover_nodes / forbidden_zones}

【润色指南】
- 风格适配：与 MASTER_SETTING 保持一致的人称/视角/叙事距离
- 排版：标题格式 `## 第{NNNN}章 标题`，段落间空行，对话分行
- Anti-AI 终检：消除"不是...而是..."句式、段落首尾总结句、冗余"突然/忽然"、机械动作罗列、直接情感告知

【审查反馈】
（首轮为空。修复轮时填入：）
- [{category}] {description} (位置: {location})
"""
)
```

**验证：**

```bash
CHAPTER_PATH=$(python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" chapter-path --chapter {N})
CHAPTER_FILE="${PROJECT_ROOT}/${CHAPTER_PATH}"

test -s "$CHAPTER_FILE" || { echo "❌ 章节文件为空"; exit 1; }

WORDS=$(python -c "
import re
t = open('$CHAPTER_FILE', encoding='utf-8').read()
print(len(re.findall(r'[一-鿿]', t)))
")
echo "字数: $WORDS"
if [ "$WORDS" -lt 1500 ]; then
  echo "⚠️ 字数不足 1500"
fi
```

字数不足→重试 chapter-writer-agent（提示字数不足）。仍不足→标记 warning 继续。

---

### Step 4: Agent(reviewer) → 审查

**必须使用 Agent 工具调用 reviewer，不得由主流程伪造审查 JSON。**

```text
Agent(
  subagent_type: "reviewer",
  prompt: "chapter={N}; chapter_file=${CHAPTER_FILE}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}; REVIEW_OUTPUT=${PROJECT_ROOT}/.webnovel/tmp/review_results.json。

【自检系统状态 - 审查时需额外关注】
$(echo "$CHECK_RESULT" | python -c "
import json,sys
d=json.load(sys.stdin)
warnings=[c for c in d['checks'] if c['severity']=='warning' and not c['passed']]
if warnings:
    for w in warnings:
        print(f'- {w[\"name\"]}: {w[\"detail\"]}')
else:
    print('（无异常）')
")

严格输出 reviewer schema JSON，并保存到 ${PROJECT_ROOT}/.webnovel/tmp/review_results.json。"
)
```

---

### Step 5: review-pipeline

```bash
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" review-pipeline \
  --chapter {N} \
  --review-results "${PROJECT_ROOT}/.webnovel/tmp/review_results.json" \
  --metrics-out "${PROJECT_ROOT}/.webnovel/tmp/review_metrics.json" \
  --report-file "审查报告/第{N}章审查报告.md" \
  --save-metrics
```

验证审查结果：

```bash
python -c "
import json
d = json.load(open('${PROJECT_ROOT}/.webnovel/tmp/review_results.json'))
assert isinstance(d, dict), 'review_results 不是 JSON 对象'
blocking_count = len([i for i in d.get('issues',[]) if i.get('severity')=='blocking'])
print(f'blocking: {blocking_count}, score: {d.get(\"score\",\"unknown\")}')
"
```

---

### Step 6: 修复轮

```
blocking_count > 0 → 进入修复轮（最多 2 轮）

修复轮流程：
  round = 0
  while blocking_count > 0 and round < 2:
    1. 从 review_results.json 提取 blocking issues 文本：
       python -c "import json; d=json.load(open('${PROJECT_ROOT}/.webnovel/tmp/review_results.json')); [print(f\"- [{i.get('category','')}] {i.get('description','')} (位置: {i.get('location','')})\") for i in d.get('issues',[]) if i.get('severity')=='blocking']"

    2. Agent(chapter-writer-agent) 修复模式
       将提取的 issue 列表填入 prompt 的【审查反馈】部分
       prompt 明确指出："修复模式：只修改上述审查反馈指出的具体问题，不大面积重写。保留未涉及部分的原文。"

    python -X utf8 "${SCRIPTS_DIR}/skill_runner.py" clean-tmp --project-root "${PROJECT_ROOT}"

    3. Agent(reviewer) 重新审查
       → 覆盖更新 review_results.json

    4. review-pipeline 重新判定 blocking

    5. round += 1

  if blocking_count > 0:
    标记本章 failed，记录 blocking issues 到 batch_state，继续下一章
  else:
    阻塞已清除，继续 Step 7
```

---

### Step 7: Agent(data-agent) → 事实提取

**必须使用 Agent 工具调用 data-agent，不得跳过或手动模拟。**

```bash
# 清空旧 tmp 文件，保留 review_results.json（chapter-commit 仍需）
python -X utf8 "${SCRIPTS_DIR}/skill_runner.py" clean-tmp --project-root "${PROJECT_ROOT}" --keep review_results.json
```

```text
Agent(
  subagent_type: "data-agent",
  prompt: "chapter={N}; chapter_file=${CHAPTER_FILE}; project_root=${PROJECT_ROOT}; scripts_dir=${SCRIPTS_DIR}。从正文提取事实，生成 .webnovel/tmp/ 下的 fulfillment_result.json、disambiguation_result.json、extraction_result.json；不直接写 state/index/summaries/memory。"
)
```

验证输出文件：

```bash
for f in fulfillment_result.json disambiguation_result.json extraction_result.json; do
  FP="${PROJECT_ROOT}/.webnovel/tmp/${f}"
  if [ ! -s "$FP" ]; then
    echo "❌ 缺失: $f"
  else
    echo "✅ $f ($(wc -c < $FP) bytes)"
  fi
done
```

任一缺失→重试 Agent 调用 1 次。仍缺失→标记 failed 并停止。

---

### Step 8: chapter-commit + 验证投影 + Git 备份

```bash
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" chapter-commit \
  --chapter {N} \
  --review-result "${PROJECT_ROOT}/.webnovel/tmp/review_results.json" \
  --fulfillment-result "${PROJECT_ROOT}/.webnovel/tmp/fulfillment_result.json" \
  --disambiguation-result "${PROJECT_ROOT}/.webnovel/tmp/disambiguation_result.json" \
  --extraction-result "${PROJECT_ROOT}/.webnovel/tmp/extraction_result.json"
```

自动判定：blocking_count>0 或 missed_nodes 非空 或 pending 非空 → rejected，否则 accepted。

验证投影：projection_status 五项（state/index/summary/memory/vector）全部 done 或 skipped。

失败隔离：commit 未生成→重跑 2 次。projection 失败→只补跑失败项。不回退 Step 1-7。

#### 写后校验

```bash
python -X utf8 "${SCRIPTS_DIR}/skill_runner.py" verify-chapter-files \
  --project-root "${PROJECT_ROOT}" --chapter {N} \
  || { echo "❌ 写后校验失败"; exit 1; }
```

```bash
python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" backup \
  --chapter {N} \
  --chapter-title "{title}"
```

备份必须以解析后的 `PROJECT_ROOT` 为准，禁止从工作区父目录执行裸全量 Git add。

---

### Step 9: 更新 batch_state + 进度反馈

```bash
python -c "
import json, pathlib, datetime

# 读取审查得分
review_path = '${PROJECT_ROOT}/.webnovel/tmp/review_results.json'
score = json.load(open(review_path)).get('score', 0)

# 更新 batch_state
p = pathlib.Path('$BATCH_STATE')
s = json.loads(p.read_text())
if $N not in s['completed_chapters']:
    s['completed_chapters'].append($N)
s['current_chapter'] = $N + 1
s['chapter_results'][str($N)] = {
    'status': 'success',
    'score': score,
    'words': $WORDS,
    'completed_at': datetime.datetime.now(datetime.timezone.utc).isoformat()
}
p.write_text(json.dumps(s, ensure_ascii=False, indent=2))

# 重新读取验证（磁盘级校验）
s2 = json.loads(p.read_text())
assert $N in s2['completed_chapters'], '写入验证失败'

# 跨章完整性校验：范围内所有已完成的章都必须在 completed_chapters 中
contiguous = list(range($S, $N + 1))
actual = sorted(s2['completed_chapters'])
gaps = [ch for ch in contiguous if ch not in actual]
if gaps:
    raise AssertionError(f'batch_state 不完整！缺失章节: {gaps}')
total_in_batch = $E - $S + 1
done = len(actual)
print(f'✅ batch_state 已验证 ({done}/{total_in_batch} 章已记录)')
"
```

更新失败→重试 3 次。仍失败→停止。

> 若跨章校验发现缺失章节，说明前面某章的 Step 9 静默失败。必须补写缺失章到 completed_chapters 后再继续，不得忽略。

```
✅ 第{N}章完成 | 审查: {SCORE}/100 | 字数: {WORDS} | 进度: {N-S+1}/{E-S+1}
```

---

## 分批暂停点（每 3 章）

```
当 (N - S + 1) % 3 == 0 且 N != E:
  ═══════════════════════════════════════
  🚦 已完成第 {S}-{N} 章（共 {N-S+1} 章）

  章节摘要：
  第{S}章 ✅ score:{xx} words:{xxxx}
  ...

  batch_state 完整性: ✅

  确认下一步：
  - "继续" 撰写第 {N+1}-{min(N+3, E)} 章
  - "停止" 保存进度并退出
  ═══════════════════════════════════════

若用户设置 AUTO_CONTINUE=1，跳过暂停直接继续。
```

---

## 循环完成：汇总报告

```
═══════════════════════════════════════
📊 批量写作完成

| 章节 | 状态 | 得分 | 字数 |
|------|------|------|------|
| ... | ... | ... | ... |

总计: {count} 章 | 成功: {success} | 失败: {failed}
平均得分: {avg}
═══════════════════════════════════════
```

更新 batch_state status = "completed"。

---

## 失败处理速查

| 场景 | 处理 | 阻断 |
|------|------|------|
| 环境变量未设置/PROJECT_ROOT 错误 | 立即停止 | 阻断 |
| 上章完整性失败 | 立即停止 | 阻断 |
| preflight 失败（批前） | 修复环境→重试1次→停止 | 阻断 |
| 合同树刷新失败 | 检查缺失文件→修复→重试1次→停止 | 阻断 |
| context-agent 失败 | 重试1次→停止 | 阻断 |
| chapter-writer-agent 失败 | 重试1次→停止 | 阻断 |
| 字数不足 | 重试 chapter-writer-agent | 章内 |
| reviewer blocking(1-2轮) | 提取反馈→回传 chapter-writer-agent→重审 | 章内 |
| reviewer blocking(3轮) | 标记failed→继续 | 否 |
| data-agent 失败 | 重试1次→标记failed并停止 | 阻断 |
| chapter-commit 失败 | 修复→重试(3次)→停止 | 阻断 |
| projection 失败 | 只补跑失败项 | 章内 |
| batch_state 更新失败 | 重试3次→停止 | 阻断 |

