- 伏笔账本:纯函数状态机(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
88 lines
3.4 KiB
Python
88 lines
3.4 KiB
Python
"""大纲生成的注入上下文组装(确定性序列化文本;ARCH §5.4 outliner reads / §6.2)。
|
||
|
||
把 projects 设定 + 已登记伏笔 + 人物 + 世界观确定性序列化成纯文本喂给 outliner——
|
||
**无时间戳/无 UUID**(缓存前缀稳定,不变量 #9),逐项排序保证同输入同输出(可单测)。
|
||
outliner 据此排出分章节拍 + 伏笔回收窗口(关联本章与伏笔,§6.2)。
|
||
|
||
注:reads=["projects","foreshadow","characters","world_entities"](C6 outliner_spec),
|
||
本组装恰好覆盖这四源,不读 digest/outline(排大纲是从设定生成、非续写既有章)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Sequence
|
||
|
||
from ww_core.domain import ForeshadowLedgerView, ProjectView
|
||
from ww_core.domain.repositories import CharacterView, WorldEntityView
|
||
|
||
|
||
def _project_block(project: ProjectView) -> str:
|
||
lines = [f"标题:{project.title}"]
|
||
if project.genre:
|
||
lines.append(f"类型:{project.genre}")
|
||
if project.logline:
|
||
lines.append(f"一句话简介:{project.logline}")
|
||
if project.premise:
|
||
lines.append(f"前提:{project.premise}")
|
||
if project.theme:
|
||
lines.append(f"主题:{project.theme}")
|
||
if project.selling_points:
|
||
lines.append("卖点:" + "、".join(project.selling_points))
|
||
if project.structure:
|
||
lines.append(f"结构:{project.structure}")
|
||
return "【作品设定】\n" + "\n".join(lines)
|
||
|
||
|
||
def _foreshadow_block(foreshadow: Sequence[ForeshadowLedgerView]) -> str:
|
||
if not foreshadow:
|
||
return "【已登记伏笔】\n(无)"
|
||
rows = sorted(foreshadow, key=lambda f: f.code)
|
||
lines = []
|
||
for f in rows:
|
||
window = ""
|
||
if f.expected_close_from is not None or f.expected_close_to is not None:
|
||
lo = f.expected_close_from if f.expected_close_from is not None else "?"
|
||
hi = f.expected_close_to if f.expected_close_to is not None else "?"
|
||
window = f"(回收窗口 {lo}-{hi})"
|
||
plant = f"埋设第{f.planted_at}章 " if f.planted_at is not None else ""
|
||
lines.append(f"- [{f.code}] {f.title} {plant}{window}".rstrip())
|
||
return "【已登记伏笔】\n" + "\n".join(lines)
|
||
|
||
|
||
def _characters_block(characters: Sequence[CharacterView]) -> str:
|
||
if not characters:
|
||
return "【人物】\n(无)"
|
||
rows = sorted(characters, key=lambda c: c.name)
|
||
lines = []
|
||
for c in rows:
|
||
role = f"({c.role})" if c.role else ""
|
||
motive = f" 动机:{c.motive}" if c.motive else ""
|
||
lines.append(f"- {c.name}{role}{motive}".rstrip())
|
||
return "【人物】\n" + "\n".join(lines)
|
||
|
||
|
||
def _world_block(entities: Sequence[WorldEntityView]) -> str:
|
||
if not entities:
|
||
return "【世界观】\n(无)"
|
||
rows = sorted(entities, key=lambda e: (e.type, e.name))
|
||
lines = [f"- [{e.type}] {e.name}" for e in rows]
|
||
return "【世界观】\n" + "\n".join(lines)
|
||
|
||
|
||
def build_outline_context(
|
||
*,
|
||
project: ProjectView,
|
||
foreshadow: Sequence[ForeshadowLedgerView],
|
||
characters: Sequence[CharacterView],
|
||
world_entities: Sequence[WorldEntityView],
|
||
) -> str:
|
||
"""组装 outliner 注入上下文(确定性文本,纯函数)。"""
|
||
return "\n\n".join(
|
||
[
|
||
_project_block(project),
|
||
_foreshadow_block(foreshadow),
|
||
_characters_block(characters),
|
||
_world_block(world_entities),
|
||
]
|
||
)
|