Files
writer-work-flow/apps/api/ww_api/services/outline_context.py
Yaojia Wang 5fb7bfb1de 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
2026-06-18 14:21:17 +02:00

88 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""大纲生成的注入上下文组装确定性序列化文本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),
]
)