feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)

- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点
- 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件
- 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板
- 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE
- bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试
- M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug
- 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
This commit is contained in:
Yaojia Wang
2026-06-18 14:21:17 +02:00
parent 68f194a043
commit 5fb7bfb1de
74 changed files with 6529 additions and 126 deletions

View File

@@ -1,17 +1,44 @@
"""ww-agents内置 Agent 声明AgentSpec+ 结构化输出 schemaC6
对齐 ARCH §5.1AgentSpec/ §5.4I/O 契约)/ §6.1(冲突分类)。
对齐 ARCH §5.1AgentSpec/ §5.4I/O 契约)/ §6.1(冲突分类)/ §6.2 §6.4(伏笔/节奏)
"""
from __future__ import annotations
from .schemas import Conflict, ConflictType, ContinuityReview
from .specs import AgentSpec, continuity_spec
from .schemas import (
Conflict,
ConflictType,
ContinuityReview,
ForeshadowReview,
ForeshadowSuggestion,
ForeshadowWindow,
OutlineChapter,
OutlineResult,
PaceIssue,
PaceReview,
)
from .specs import (
AgentSpec,
continuity_spec,
foreshadow_spec,
outliner_spec,
pace_spec,
)
__all__ = [
"AgentSpec",
"Conflict",
"ConflictType",
"ContinuityReview",
"ForeshadowReview",
"ForeshadowSuggestion",
"ForeshadowWindow",
"OutlineChapter",
"OutlineResult",
"PaceIssue",
"PaceReview",
"continuity_spec",
"foreshadow_spec",
"outliner_spec",
"pace_spec",
]

View File

@@ -44,3 +44,106 @@ class ContinuityReview(BaseModel):
default_factory=list,
description="检出的一致性冲突;无冲突时为空列表",
)
# ---- 大纲 Agent 结构化输出ARCH §5.4 outliner 行 / §6.2 伏笔窗口)----
class ForeshadowWindow(BaseModel):
"""伏笔回收窗口关联本章与某条伏笔ARCH §6.2)。
排大纲时把伏笔的「埋设章 → 期望回收区间」绑到具体章节;接近回收窗口的章节
提示作者推进/收束。下界/上界可缺省(仅埋设、尚未排定回收时间)。
"""
code: str = Field(description="伏笔编码(关联 foreshadow.code")
plant_chapter: int | None = Field(
default=None, description="埋设章号(伏笔在本章/此章首次出现)"
)
expected_close_from: int | None = Field(default=None, description="期望回收区间下界(章号)")
expected_close_to: int | None = Field(default=None, description="期望回收区间上界(章号)")
class OutlineChapter(BaseModel):
"""单章大纲:章号 + 节拍清单 + 关联伏笔窗口。"""
no: int = Field(description="章号")
beats: list[str] = Field(default_factory=list, description="本章节拍/情节要点(有序)")
foreshadow_windows: list[ForeshadowWindow] = Field(
default_factory=list,
description="本章关联的伏笔回收窗口;无则空列表",
)
class OutlineResult(BaseModel):
"""大纲结构化产出分卷分章的章节大纲清单ARCH §5.4 writes=outline
声明式 writes不变量 #3本结构是排大纲的纯产物落 `outline` 表经验收/
端点T3.5),不在 agent 节点直接写库。
"""
chapters: list[OutlineChapter] = Field(
default_factory=list,
description="分章大纲;无则空列表",
)
# ---- 伏笔续审 Agent 结构化输出ARCH §5.4 foreshadow-analyst 行 / §6.2----
class ForeshadowSuggestion(BaseModel):
"""单条伏笔建议:本章新埋线 或 疑似回收ARCH §6.2)。
只读建议——作者在验收裁决里确认后才登记/改状态(不变量 #3本审不写库。
`code` 可缺(新埋线尚无编码时由作者命名);`where` 给本章定位。
"""
code: str | None = Field(
default=None, description="伏笔编码(关联已登记 foreshadow.code新埋可缺"
)
title: str = Field(description="伏笔标题/一句话描述")
where: str | None = Field(default=None, description="本章定位:哪一段埋下/疑似回收")
note: str | None = Field(default=None, description="建议说明(埋设要点 / 回收理由)")
class ForeshadowReview(BaseModel):
"""伏笔续审结构化产出:本章新埋 + 疑似回收两组建议ARCH §5.4 / §6.2)。
只读建议清单(不变量 #3登记/状态变更经验收裁决(作者确认)。
"""
planted: list[ForeshadowSuggestion] = Field(
default_factory=list,
description="本章疑似新埋的伏笔;无则空列表",
)
resolved: list[ForeshadowSuggestion] = Field(
default_factory=list,
description="本章疑似回收/收束的伏笔;无则空列表",
)
# ---- 节奏续审 Agent 结构化输出ARCH §5.4 pace-checker 行 / §6.4----
class PaceIssue(BaseModel):
"""单条节奏问题:注水段定位 + 原因ARCH §6.4)。"""
where: str = Field(description="本章定位:哪一段疑似注水/拖沓")
reason: str = Field(description="判定理由(信息密度低 / 重复 / 偏题等)")
class PaceReview(BaseModel):
"""节奏续审结构化产出:注水段 + 章末钩子 + 爽点节拍图ARCH §5.4 / §6.4)。
只读(不变量 #3。`beat_map` 是逐段节拍强度序列(整数),供前端 ▁▃▅ 可视化。
"""
water: list[PaceIssue] = Field(
default_factory=list,
description="疑似注水/拖沓段;无则空列表",
)
hook: bool = Field(default=False, description="章末是否有有效钩子(悬念/反转/期待)")
beat_map: list[int] = Field(
default_factory=list,
description="逐段爽点节拍强度序列(整数,供前端节拍图可视化)",
)

View File

@@ -12,7 +12,12 @@ from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from ww_llm_gateway.types import Tier
from .schemas import ContinuityReview
from .schemas import (
ContinuityReview,
ForeshadowReview,
OutlineResult,
PaceReview,
)
class AgentSpec(BaseModel):
@@ -63,3 +68,99 @@ continuity_spec = AgentSpec(
writes=[], # 只读(不变量 #3
scope="builtin",
)
OUTLINER_SYSTEM_PROMPT = """你是长篇连载小说的「大纲架构师」。职责:依据作品立意、\
人物与世界观,排出分卷分章的章节大纲,并为每条伏笔标注回收窗口。
输入材料:
- 作品设定projects题材、立意、主线、卖点
- 已登记伏笔foreshadow编码、标题、埋设/期望回收线索);
- 主要人物characters与世界观实体world_entities
产出要求(每章):
- 章号有序、按卷推进;
- beats本章核心节拍/情节要点,推动主线、服务人物弧光;
- foreshadow_windows本章关联的伏笔回收窗口——给出伏笔编码、埋设章号、\
期望回收区间(下界/上界章号)。把每条伏笔的「埋设 → 回收」绑到具体章节。
纪律:
- 接近某伏笔的回收窗口时,在对应章 beats 里安排推进/收束该伏笔的情节,避免伏笔悬置过久;
- 你只产结构化大纲(章节 + 节拍 + 伏笔窗口),**不改稿、不写库**(落 outline 表经验收/端点);
- 伏笔编码须与已登记 foreshadow.code 对应,便于后续按窗口提示与校验。"""
outliner_spec = AgentSpec(
name="outliner",
tier="analyst", # 不变量 #2只声明档位不写 model
system_prompt=OUTLINER_SYSTEM_PROMPT,
input_schema=None, # 注入材料为序列化文本(设定/伏笔/人物/世界观),非结构化入参
output_schema=OutlineResult,
reads=["projects", "foreshadow", "characters", "world_entities"],
writes=["outline"], # 声明式(经验收/T3.5 才真写库,不变量 #3
scope="builtin",
)
FORESHADOW_SYSTEM_PROMPT = """你是长篇连载小说的「伏笔续审」foreshadow-analyst\
职责:把本章草稿与作品已登记伏笔逐项比对,找出本章**新埋的伏笔**与**疑似回收/收束**\
的伏笔,产出结构化建议清单。
比对依据(注入材料):
- 已登记伏笔foreshadow编码、标题、状态、期望回收窗口
- 本章草稿正文。
产出两组建议:
- planted新埋本章疑似首次埋下的伏笔——给出标题、本章定位、要点说明\
若与某已登记编码相关则填 code否则留空由作者命名。
- resolved回收本章疑似回收/收束已登记伏笔——给出对应 code、本章定位、回收理由。
纪律:
- 你**只读、只产建议**,不改稿、不写库、不直接登记或改伏笔状态——\
登记/状态变更经作者在验收时裁决确认(不变量 #3 / #4
- 只报有据可依的;无则两组都返回空列表,不要臆造。
- 引用具体(伏笔编码、本章段落定位),便于作者就地确认。"""
foreshadow_spec = AgentSpec(
name="foreshadow",
tier="analyst", # 不变量 #2只声明档位不写 model
system_prompt=FORESHADOW_SYSTEM_PROMPT,
input_schema=None, # 注入材料为序列化文本(已登记伏笔 + 草稿),非结构化入参
output_schema=ForeshadowReview,
reads=["foreshadow"],
writes=[], # 只读(不变量 #3
scope="builtin",
)
PACE_SYSTEM_PROMPT = """你是长篇连载小说的「节奏续审」pace-checker。职责按题材的\
节奏模板审本章草稿,找出注水段、判定章末钩子有无、给出爽点节拍图。
题材节奏模板genre 级规则,注入材料里带本作题材的具体模板;无则用网文通用基线):
- 黄金三章:开篇前三章须快速立爽点/钩子/代入感,信息密度高、少铺垫慢热;
- 章末钩子:每章结尾应留悬念/反转/期待,驱动追更;
- 爽点密度:按题材基线维持爽点节拍,避免长段平淡注水。
产出:
- water注水段逐段审标出信息密度低/重复/偏题/拖沓的段落,给本章定位与原因;
- hook章末钩子本章结尾是否存在有效钩子true/false
- beat_map节拍图把本章按段切分给每段一个爽点强度整数如 05\
形成逐段强度序列,供前端 ▁▃▅ 可视化节奏起伏。
纪律:
- 你**只读、只报节奏诊断**,不改稿、不写库(不变量 #3
- 依据题材模板判定,不臆造;无注水段则 water 为空列表。
- beat_map 长度应与切分段数一致、顺序即正文顺序,便于前端对齐渲染。"""
pace_spec = AgentSpec(
name="pace",
tier="light", # 不变量 #2只声明档位不写 model节奏审用轻量档
system_prompt=PACE_SYSTEM_PROMPT,
input_schema=None, # 注入材料为序列化文本(题材模板 + 草稿),非结构化入参
output_schema=PaceReview,
reads=["rules"], # genre 模板 DSL 经 rulesgenre 级)注入,复用 review_context 规则合并
writes=[], # 只读(不变量 #3
scope="builtin",
)