feat(chain): F2 续写式链 continue_volume——上一章 accepted 正文作前文引子

- core write_chapter 支持续写模式:chain_key==continue_volume 时经注入的
  chapter_repo.latest_accepted 读上一章已验收终稿,build_continuation_context
  改写 volatile(仅断点后块,守不变量 #9;core 不 import apps/api)。
- ChapterDraftRepo 协议加 latest_accepted;draft_volume 行为不变(无回归)。
- apps/api SUPPORTED_CHAINS 加 continue_volume;run 端点透传 chain_key 选模式
  (chain_runner 经 initial_chain_state 把 chain_key 落入 ChainState)。
- 单测(mock 网关+MemorySaver+fake session):续写第二章请求含第一章正文;
  draft_volume 不注入前文;端点 202 透传 chain_key。守不变量 #1/#5。
This commit is contained in:
Yaojia Wang
2026-06-23 20:09:08 +02:00
parent 61398a1452
commit 18aa87d751
4 changed files with 157 additions and 10 deletions

View File

@@ -20,6 +20,7 @@ from ww_agents import AgentSpec
from .._protocols import GatewayRun
from ..collect import collect_reviews
from ..generation_node import build_continuation_context
from ..review_node import build_review_context, run_review
from ..state import ChapterState
from ..write_node import build_write_request
@@ -27,6 +28,9 @@ from .state import ChainState
log = structlog.get_logger(__name__)
#: 续写式链 key写章以上一章已验收正文末尾作前文引子F2
CHAIN_CONTINUE_VOLUME = "continue_volume"
# ---- 注入缝(图工厂经默认参绑定,单测直接注 fake----
#: `session -> Repo`:节点自建短事务里从 session 造 repo仿端点依赖工厂
@@ -62,7 +66,10 @@ class DraftView(Protocol):
class ChapterDraftRepo(Protocol):
"""章草稿 repo 最小依赖write 落稿 `save_draft` + review 重读 `get_draft`(只 flush"""
"""章草稿 repo 最小依赖write 落稿 `save_draft` + review 重读 `get_draft`(只 flush
续写式链(`continue_volume`)写章另需 `latest_accepted`——读上一章已验收终稿正文作前文引子。
"""
async def save_draft(
self, project_id: uuid.UUID, chapter_no: int, *, text: str, volume: int = 1
@@ -70,6 +77,8 @@ class ChapterDraftRepo(Protocol):
async def get_draft(self, project_id: uuid.UUID, chapter_no: int) -> DraftView | None: ...
async def latest_accepted(self, project_id: uuid.UUID, chapter_no: int) -> DraftView | None: ...
class ReviewRecordRepo(Protocol):
"""review/accept 节点对审稿 repo 的最小依赖collect 留痕 + accept 重读最近审稿。"""
@@ -126,20 +135,30 @@ async def write_chapter(
project_id = state["project_id"]
chapter_no = state["current_chapter_no"]
user_id = state["user_id"]
is_continuation = state.get("chain_key") == CHAIN_CONTINUE_VOLUME
async with session_factory() as session:
gateway = await gateway_builder(session)
repos = memory_repos_factory(session)
context = await assemble(repos, project_id, chapter_no)
chapter_repo = chapter_repo_factory(session)
# 续写模式volatile 改写为「设定 + 上一章已验收正文末尾」(不变量 #9仅改断点后块
# stable_core 仍进缓存前缀)。前文经注入的 chapter_repo 重读core 不 import apps/api
if is_continuation:
prior_text = await _read_prior_accepted(chapter_repo, project_id, chapter_no)
volatile = build_continuation_context(
project_context=context.volatile, prior_text=prior_text
)
else:
volatile = context.volatile
req = build_write_request(
stable_core=context.stable_core,
volatile=context.volatile,
volatile=volatile,
user_id=user_id,
project_id=project_id,
)
# 收集版:非流式,一次 run 拿全文(链批量量产不走 SSE
collect_req = req.model_copy(update={"stream": False})
resp = await gateway.run(collect_req)
chapter_repo = chapter_repo_factory(session)
await chapter_repo.save_draft(project_id, chapter_no, text=resp.text)
await _commit(session)
log.info(
@@ -295,6 +314,19 @@ async def _read_draft(
return view.content if view is not None else ""
async def _read_prior_accepted(
chapter_repo: ChapterDraftRepo, project_id: uuid.UUID, chapter_no: int
) -> str:
"""续写式链读上一章chapter_no-1已验收终稿正文作前文引子不变量 #1/#4
第一章(前一章号 < 1或前一章未验收 → 空串,由 `build_continuation_context` 降级占位。
"""
if chapter_no <= 1:
return ""
view = await chapter_repo.latest_accepted(project_id, chapter_no - 1)
return view.content if view is not None else ""
async def _commit(session: CommitSession) -> None:
"""提交短事务session 满足 `CommitSession`:只需 `commit()`)。"""
await session.commit()
@@ -307,6 +339,7 @@ def has_conflicts(state: ChainState) -> bool:
__all__ = [
"CHAIN_CONTINUE_VOLUME",
"AcceptChapterOp",
"AssembleContext",
"AssembleFn",