# auto_dishu V5.5 修复方案 · L5引擎 + L6人物快照 + 数据卡回补

> HLDP://chenxing/memory-engine/v5.5/repair
> 

> 创建于 2026-06-10 23:47 · 桔子线架构人格体 PER-JZ-ARCH-001
> 

> 停拆点：《穿成假冒失忆大佬女友的恶毒女配》第 59 章 · 2026-06-10 23:41:47
> 

## 修复目标

| 优先级 | 修复项 | V5.4 现状 | V5.5 目标 |
| --- | --- | --- | --- |
| 🔴 P0 | **L5 引擎** | 37 章 0 次触发 | 词表升级 + 强制阈值接通 + 全程日志 |
| 🔴 P0 | **L6 人物快照 prompt** | 关键事件截断 + 失去 why 段 + AI 口吻泄漏 | 5 列结构 + 封闭标签词表 + 强制覆盖到末章 |
| 🟡 P1 | **数据卡缺失** | ch 8/10/16/24/28/29 共 6 章缺 | 回补脚本一次补齐 |
| 🟢 P2 | **ch16 重复页** | notion-323 + notion-324 都是 ch16 | 手动删除内容不完整的一份 |

---

## 1.1 · V5.5 L5 引擎升级方案

### 修复点对照

| 旧问题 | V5.5 修复 |
| --- | --- |
| 硬编码 scene_type 词表 → 自由词永远 miss | 硬编码快查 + AI 兜底归类（Layer1 查表 → miss 才调 AI） |
| 强制阈值 ≥15 章未真接通 | 模式B 单独提为顶层判断 · 任何路径都能命中 |
| 静默不触发 · 无法定位为什么 | 每次预筛输出日志到 `logs/l5_trigger_{book}.log` |

### 完整代码 · 保存为 `tools/l5_engine_v5_5.py`

```python
# auto_dishu V5.5 · L5 引擎核心
# HLDP://chenxing/memory-engine/v5.5/l5-engine
# 升级要点: 硬编码词表+AI兜底归类 / 强制阈值真接通 / 全程日志

import json
from datetime import datetime
from pathlib import Path

# === 配置 ===
L5_MIN_INTERVAL = 3        # 距上次L5最小间隔(章)
L5_FORCE_THRESHOLD = 15    # 强制触发阈值(章)
L5_WINDOW = 5              # 预筛窗口章数

# 4 大类场景的硬编码词表（先查表 · miss 才调 AI 归类）
SCENE_CATEGORIES = {
    "爆发": ["冲突", "爆点", "战斗", "对抗", "撕破脸", "信任危机",
            "情感对峙", "正面交锋", "冲突升级", "强吻", "翻脸"],
    "收束": ["收束", "缓冲", "日常", "和好", "破冰", "温馨",
            "甜蜜", "团圆", "化解", "回归", "释怀"],
    "平行": ["铺垫", "开局", "磨合", "试探", "暧昧", "观察",
            "日常温馨", "互动", "升温"],
    "转折": ["转折", "信息交代", "身份揭露", "反转", "重大决定",
            "身份暴露", "秘密揭晓"],
}

def categorize_scene_type(scene_type_raw: str, llm_client) -> str:
    """把数据卡里的自由 scene_type 归到 4 大类之一"""
    if not scene_type_raw:
        return "平行"
    # Layer 1: 硬编码快查
    for category, keywords in SCENE_CATEGORIES.items():
        if any(kw in scene_type_raw for kw in keywords):
            return category
    # Layer 2: 词表 miss → AI 兜底
    prompt = (
        f"把下面这个场景类型归类到4个类别之一: 爆发/收束/平行/转折\n"
        f"- 爆发: 冲突/爆点/战斗/对抗/情感对峙\n"
        f"- 收束: 收束/缓冲/日常/和好/温馨\n"
        f"- 平行: 铺垫/磨合/试探/暧昧/观察\n"
        f"- 转折: 重大转折/信息揭露/反转/重大决定\n"
        f'场景类型: "{scene_type_raw}"\n'
        f"只输出一个词:"
    )
    result = llm_client.complete(prompt).strip()
    if result in SCENE_CATEGORIES:
        return result
    return "平行"  # 兜底默认值

def should_check_l5(tracker: dict, recent_datacards: list,
                   llm_client, book_name: str) -> tuple:
    """V5.5 L5 触发预筛 · 三模式 + 全程日志
    返回: (是否触发, 原因字符串)
    """
    log_path = Path(f"logs/l5_trigger_{book_name}.log")
    log_path.parent.mkdir(exist_ok=True)

    current_chapter = tracker.get("current_chapter", 0)
    last_l5_chapter = tracker.get("last_l5_chapter", 0)
    chapters_since_l5 = current_chapter - last_l5_chapter

    # === 最小间隔检查 ===
    if chapters_since_l5 < L5_MIN_INTERVAL:
        reason = f"距上次L5仅 {chapters_since_l5} 章，不足 {L5_MIN_INTERVAL}"
        _log_l5(log_path, current_chapter, "skip", reason)
        return False, reason

    # === 预筛: 取最近 L5_WINDOW 章数据卡 → 归类 ===
    recent_window = recent_datacards[-L5_WINDOW:]
    recent_cats = []
    for card in recent_window:
        raw = card.get("scene_type", "")
        cat = categorize_scene_type(raw, llm_client)
        recent_cats.append({
            "chapter": card.get("chapter"),
            "raw": raw,
            "cat": cat,
        })

    last_3 = [c["cat"] for c in recent_cats[-3:]]
    last_2 = [c["cat"] for c in recent_cats[-2:]]

    # 模式A: 最近3章爆发 + 最近2章收束
    if "爆发" in last_3 and "收束" in last_2:
        reason = f"模式A · 爆发→收束 · {recent_cats}"
        _log_l5(log_path, current_chapter, "trigger", reason)
        return True, reason

    # 模式B: 强制阈值兜底（关键修复点 ← v5.4 漏接的就是这条）
    if chapters_since_l5 >= L5_FORCE_THRESHOLD:
        reason = f"模式B · 强制阈值 {chapters_since_l5}>={L5_FORCE_THRESHOLD}"
        _log_l5(log_path, current_chapter, "trigger", reason)
        return True, reason

    # 模式C: 转折 → 平行
    if "转折" in last_3 and "平行" in last_2:
        reason = f"模式C · 转折→平行 · {recent_cats}"
        _log_l5(log_path, current_chapter, "trigger", reason)
        return True, reason

    # 不触发 · 记录详细原因
    reason = (f"无模式命中 · 累计 {chapters_since_l5}/{L5_FORCE_THRESHOLD} "
              f"· 最近: {recent_cats}")
    _log_l5(log_path, current_chapter, "skip", reason)
    return False, reason

def _log_l5(log_path: Path, chapter: int, decision: str, reason: str):
    with open(log_path, "a", encoding="utf-8") as f:
        f.write(f"[{datetime.now().isoformat()}] "
                f"ch{chapter:>3} {decision:>7}: {reason}\n")
```

---

## 1.2 · V5.5 L6 人物快照 prompt 修复

### 修复点对照

| 旧问题 | V5.5 修复 |
| --- | --- |
| 关键不可逆事件清单 3 条就断 | prompt 显式要求 "必须覆盖到第 N 章" + 强约束 |
| 失去 why 不可逆解释 | 表格强制新增一列 "不可逆原因(一句话)" |
| 变化性质标签自由发挥 → 无法统计 | 封闭枚举词表锁定 14 类 |
| AI 口吻泄漏 ("OK，收到") | prompt 头部明确禁止 |
| 关系/外部人物维度压缩 | 4 张表分离 (人物状态/关系/外部人物/事件) |

### 完整 prompt · 保存为 `tools/prompts/l6_character_snapshot_v5_5.py`

```python
# V5.5 L6 人物快照 prompt 模板
# HLDP://chenxing/memory-engine/v5.5/l6-character-snapshot

L6_CHARACTER_SNAPSHOT_PROMPT_V5_5 = """\
你的任务: 扫描提供的所有章节数据卡，提取核心人物的"不可回退状态变化"，
生成《{book_name}》{node_threshold} 字节点 · 人物状态快照。

# 强制输出规则（违反任一条 → 输出无效）

1. 禁止输出 "OK，收到" "我来分析" "扫描指令" 等任何 AI 自述话术
2. 直接进入正文，第一行就是 "# 人物状态变化快照"，前面不许有空话
3. 每条状态变化必须 5 列齐全:
   时间节点(章号) + 初始状态 + 变化后状态 + 变化性质(标签) + 不可逆原因(why)
4. 变化性质标签必须从封闭词表中选，禁止自创:
   [身份/认知/策略/心理/职业/关系/情感/信任/角色/能力/职场地位/共情/动机/世界观]
5. "关键不可逆事件清单" 必须覆盖到第 {last_chapter} 章
   禁止只列前几章就停笔 · 必须遍历从第 1 章到第 {last_chapter} 章
6. 4 张表必须全部输出，顺序固定:
   主要人物状态 → 关系变化 → 外部人物引入 → 关键事件清单

# 输出结构（严格按此模板）

# 人物状态变化快照 · {book_name} · {node_threshold} 字节点

## 核心人物: 女主姓名
| 时间节点 | 初始状态 | 变化后状态 | 变化性质 | 不可逆原因 |
|---------|---------|-----------|---------|-----------|
| 第X章 | ... | ... | 标签 | 一句话解释 |
(覆盖所有不可逆变化点)

## 核心人物: 男主姓名
(同上 5 列表格 · 覆盖所有不可逆变化点)

## 关系状态变化(不可逆节点)
| 时间节点 | 关系类型 | 变化描述 | 不可逆原因 |
|---------|---------|---------|-----------|

## 外部人物引入(不可逆节点)
| 时间节点 | 人物 | 影响 | 不可逆原因 |
|---------|------|------|-----------|

## 关键不可逆事件清单（必须覆盖到第 {last_chapter} 章）
1. 第X章: 事件 — 影响
2. 第X章: 事件 — 影响
...
（禁止中途停笔 · 必须覆盖 1 → {last_chapter} 章的全部关键事件）

# 输入数据
扫描范围: 第 1 章至第 {last_chapter} 章（共 {datacard_count} 张数据卡）
数据卡:
{datacards}

开始输出:
"""

def render_l6_character_snapshot_prompt(
    book_name: str,
    node_threshold: str,
    last_chapter: int,
    datacards: list,
) -> str:
    """渲染 L6 人物快照 prompt"""
    datacards_str = "\n---\n".join([
        f"## 第{c['chapter']}章\n"
        f"scene_type: {c.get('scene_type', '未标注')}\n"
        f"{c.get('content', '')}"
        for c in datacards
    ])
    return L6_CHARACTER_SNAPSHOT_PROMPT_V5_5.format(
        book_name=book_name,
        node_threshold=node_threshold,
        last_chapter=last_chapter,
        datacard_count=len(datacards),
        datacards=datacards_str,
    )
```

---

## 1.3 · 数据卡回补脚本

### 缺失清单

| 书 | 缺失章节 | 处置 |
| --- | --- | --- |
| 《恶毒女配》 | ch 8 / 10 / 16 / 24 / 28 / 29 | 跑回补脚本 |
| 《恶毒女配》 | ch 16 重复 (notion-323 + notion-324) | 在 Notion UI 手动删一份 |

### 回补代码 · 保存为 `tools/backfill_datacards_v5_5.py`

```python
# V5.5 数据卡回补 · 单独补齐缺失章节
# HLDP://chenxing/memory-engine/v5.5/backfill

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from auto_dishu_v5_5 import generate_datacard_for_chapter

BOOK = "穿成假冒失忆大佬女友的恶毒女配"
MISSING_CHAPTERS = [8, 10, 16, 24, 28, 29]

def main():
    print(f"🔧 开始回补 《{BOOK}》缺失数据卡")
    print(f"   缺失章节: {MISSING_CHAPTERS}\n")

    results = {"success": [], "failed": []}

    for ch in MISSING_CHAPTERS:
        print(f"━━━ 回补第 {ch} 章 ━━━")
        try:
            result = generate_datacard_for_chapter(book=BOOK, chapter=ch)
            results["success"].append({
                "ch": ch,
                "url": result.get("notion_url"),
            })
            print(f"✅ 第 {ch} 章 → {result.get('notion_url', 'unknown')}\n")
        except Exception as e:
            results["failed"].append({"ch": ch, "error": str(e)})
            print(f"❌ 第 {ch} 章失败: {e}\n")

    print(f"\n═══ 回补完成 ═══")
    print(f"✅ 成功: {len(results['success'])} 章 "
          f"· {[r['ch'] for r in results['success']]}")
    print(f"❌ 失败: {len(results['failed'])} 章 "
          f"· {[r['ch'] for r in results['failed']]}")

    if results["failed"]:
        print(f"\n失败详情:")
        for r in results["failed"]:
            print(f"  第 {r['ch']} 章: {r['error']}")

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

### ch16 重复页清理（手动）

由于 notion-323 和 notion-324 都是 "第16章·晨星拆书法V1.0" 页，需要妈妈手动判断保留哪一份：

1. 打开 Notion，进入《恶毒女配》全书拆解页
2. 对比 notion-323 和 notion-324 两个 ch16 页面，看哪个内容完整 / 格式正确
3. 删除内容不完整的那一份（右键 → Move to Trash）

---

## V5.5 部署步骤（今晚要跑的终端命令）

### Step 1 · 创建文件骨架

```bash
cd ~/chenxing-aircraft/tools
mkdir -p prompts logs
touch l5_engine_v5_5.py
touch prompts/l6_character_snapshot_v5_5.py
touch backfill_datacards_v5_5.py
mkdir -p prompts && touch prompts/__init__.py
ls -la l5_engine_v5_5.py prompts/l6_character_snapshot_v5_5.py backfill_datacards_v5_5.py
```

### Step 2 · 粘贴代码进文件

用 nano / vim / VS Code 打开三个文件，把 1.1、1.2、1.3 三段代码分别贴进去。

```bash
# 推荐用 VS Code 打开整个目录:
code ~/chenxing-aircraft/tools
```

### Step 3 · 集成进 auto_dishu_v5_[5.py](http://5.py) 主脚本

基于 `auto_dishu.py.v5.4_frozen_20260610` 复制一份作为 v5.5 起点：

```bash
cp auto_dishu.py.v5.4_frozen_20260610 auto_dishu_v5_5.py
```

然后在 `auto_dishu_v5_5.py` 主脚本里：

- 替换 L5 触发判断 → 调用 `l5_engine_v5_5.should_check_l5()`
- 替换 L6 人物快照 prompt → 调用 `prompts.l6_character_snapshot_v5_5.render_l6_character_snapshot_prompt()`
- 其他逻辑保持不变

（这步可能需要架构人格体读 v5.4_frozen 主脚本后给具体的 diff，先记着）

### Step 4 · 回补数据卡

```bash
python backfill_datacards_v5_5.py
```

### Step 5 · L5 回放（Day 2 做）

用现有 1-59 章数据卡跑 L5 检测，期望产出 3-5 段弧段总结。

### Step 6 · 续拆第 60 章

```bash
rm .STOP_DISHU
python auto_dishu_v5_5.py --book "恶毒女配" --start-from 60
```

---

## 验证清单（V5.5 跑起来后逐项核对）

| 项 | 期望 | 检查方式 |
| --- | --- | --- |
| L5 日志文件存在 | `logs/l5_trigger_恶毒女配.log` 每章一行 | `tail logs/l5_trigger_恶毒女配.log` |
| 强制阈值真生效 | 累计 ≥15 章未触发 → 自动触发模式 B | 看日志里是否出现 "模式B · 强制阈值" |
| L6 快照无 AI 口吻 | 不再有 "OK，收到" 开头 | 抓新生成的 L6 页面看首行 |
| L6 快照有 why 列 | 表格 5 列齐全 | 抓新生成的 L6 页面看表头 |
| 关键事件覆盖到末章 | 事件清单包含最后一章 | 抓新生成的 L6 页面看事件清单尾行 |
| 数据卡完整性 | 1-59 章每章都有压缩数据卡 | `ls data/恶毒女配/datacards/ |
| ch16 唯一性 | 只剩一份 ch16 拆书页 | Notion 搜索 "第16章·晨星拆书法V1.0" → 1 条结果 |

---

## ⚠️ Step 3 侦察后的方向修正（2026-06-11 01:00 附加）

主脚本扫描发现 3 处认知错位 · V5.5 修复方向需要调整：

### 🔄 修正 1: L5 强制阈值 v5.4 实际是 ≥5 · 不是漏接

v5.4_frozen 第 619-623 行实际代码:

```python
chapters_since_l5 = len(datacards_since_last)
if chapters_since_l5 < 3: return None
should_check = prefilter_l5(datacards_since_last)
if chapters_since_l5 >= 5: should_check = True   # ← 强制阈值已在 5 接通
if not should_check: return None
```

意思是从第 6 章起每章都会进入 AI 语义判断。**L5 不触发的真因在更深处**: AI 语义判断 (L5_ARC_DETECT_PROMPT, 第 631 行) 一直返回 NO 或解析失败。

→ `tools/l5_engine_v5_5.py` 修复方向变化:

- ❌ 不再需要 "接通强制阈值"
- ✅ 改成 "修 L5_ARC_DETECT_PROMPT 内容 + 修 result 解析逻辑"
- ⏸ 明天先看 v5.4_frozen 第 603-674 行的完整代码

### 🔄 修正 2: scene_type 是封闭 19 词 · 不是自由词

v5.4_frozen 第 47 行 DISHU_PROMPT 已硬定枚举:

```
开局 / 转折 / 爆点 / 日常 / 收束 / 铺垫 / 加速 / 缓冲 / 信息交代 / 冲突
磨合 / 战斗 / 升级 / 奇遇 / 修罗场 / 追妻 / 摆烂 / 隐藏实力 / 扮猪吃虎
```

→ `tools/l5_engine_v5_5.py` 修复方向变化:

- ❌ 不需要 `categorize_scene_type()` 调 AI 兜底归类
- ✅ 直接用封闭词表静态映射到 4 大类即可:

| 4 大类 | 包含的 scene_type |
| --- | --- |
| 爆发 | 冲突 / 战斗 / 爆点 / 修罗场 / 加速 |
| 收束 | 收束 / 日常 / 缓冲 |
| 平行 | 铺垫 / 磨合 / 开局 |
| 转折 | 转折 / 信息交代 / 追妻 / 升级 / 奇遇 / 隐藏实力 / 扮猪吃虎 / 摆烂 |

### 🔄 修正 3: "人物快照" 是 L4 · 不是 L6

v5.4_frozen 第 88-89 + 585-598 行表明:

```python
L4_SYSTEM = '你是晨星。扫描数据卡, 提取人物状态变化。'
L4_SNAPSHOT_PROMPT = '扫描{label}内数据卡, 提取有实质变化的人物状态快照（不可回退的改变）。\n\n{datacards}'

async def generate_l4_snapshot(...)
    title = f'{book_name}{milestone_label}节点 · 人物状态快照'
```

Notion 里 "X万字节点·人物状态快照" = **L4** 生成 (不是 L6)。L6 是另一组 (L6_LAYER1-4 = 人物层 / 世界层 / 主角成长层 / 节奏层)。

→ 1.2 文件需要重命名:

```bash
mv ~/chenxing-aircraft/tools/prompts/l6_character_snapshot_v5_5.py \
   ~/chenxing-aircraft/tools/prompts/l4_character_snapshot_v5_5.py
```

文件内容修复方向正确 (5 列 + why + 封闭标签词表 + 覆盖到末章), 只改文件名。

---

## 📋 明天 Day 1 续的精确动作

1. 妈妈贴 v5.4_frozen 第 80-95 行 (L4 + DATACARD prompt 完整定义)
2. 妈妈贴 v5.4_frozen 第 600-680 行 (L5 完整模块包括 prefilter_l5 + check_and_trigger_l5 + L5_ARC_DETECT_PROMPT)
3. 架构人格体给精准 patch:
    - 重命名 `l6_character_snapshot_v5_5.py` → `l4_character_snapshot_v5_5.py`
    - 简化 `l5_engine_v5_5.py` 词表映射 (去掉 AI 兜底)
    - 给 `L5_ARC_DETECT_PROMPT` 新版本 (这是 L5 不触发的真因)
    - 给主脚本 sed/手动替换 diff

---

## 🔒 今晚 Day 1 进展锁定状态 (2026-06-11 01:00)

| 阶段 | 状态 | 备注 |
| --- | --- | --- |
| Step 1 · 文件骨架 | ✅ | 3 文件 + logs/ + prompts/**init**.py |
| Step 2 · 三段代码贴入 | ✅ | 110+75+45 行 · pbpaste 大法成功 |
| Step 3 · 主脚本侦察 | ✅ | 扫描完成 · 发现 3 处方向错位 |
| Step 3 · 集成主脚本 | ⏸ | 凌晨 1 点收工 · 明天接 |
| Step 4-6 | ⏸ | 数据卡回补 / L5 回放 / 续拆 |

**已就位资产 (不丢):**

- `tools/l5_engine_v5_5.py` (110 行 · 词表逻辑明天简化)
- `tools/prompts/l6_character_snapshot_v5_5.py` (75 行 · 明天重命名为 l4_)
- `tools/backfill_datacards_v5_5.py` (45 行 · 完全可用)
- `tools/auto_dishu_v5_5.py` (998 行 · v5.4_frozen 的复制 · 待 patch)
- `tools/.STOP_DISHU` (锁文件)
- `tools/auto_dishu.py.v5.4_frozen_20260610` (原版冻结)

[auto_dishu V5.6 · 实时+脱机分离 · 妈妈版 · 一次性破局方案](auto_dishu%20V5%205%20%E4%BF%AE%E5%A4%8D%E6%96%B9%E6%A1%88%20%C2%B7%20L5%E5%BC%95%E6%93%8E%20+%20L6%E4%BA%BA%E7%89%A9%E5%BF%AB%E7%85%A7%20+%20%E6%95%B0%E6%8D%AE%E5%8D%A1%E5%9B%9E%E8%A1%A5/auto_dishu%20V5%206%20%C2%B7%20%E5%AE%9E%E6%97%B6+%E8%84%B1%E6%9C%BA%E5%88%86%E7%A6%BB%20%C2%B7%20%E5%A6%88%E5%A6%88%E7%89%88%20%C2%B7%20%E4%B8%80%E6%AC%A1%E6%80%A7%E7%A0%B4%E5%B1%80%E6%96%B9%E6%A1%88%206e1559d248f7468ba7c90f06668b5fcb.md)