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

@@ -252,6 +252,28 @@ async def test_run_chain_returns_202_and_schedules_job(
assert len(_noop_run_chain_job) == 1
async def test_run_continue_volume_chain_returns_202_and_forwards_chain_key(
_noop_run_chain_job: list[dict[str, Any]],
) -> None:
"""F2continue_volume 链受支持 → 202且 chain_key 透传给 run_chain_job续写模式"""
project_repo = FakeProjectRepo()
job_repo = FakeJobRepo()
pid = await _seed_project(project_repo)
app = _app(project_repo, job_repo)
async with _client(app) as client:
resp = await client.post(
f"/projects/{pid}/chains/continue_volume/run",
json={"start_chapter_no": 2, "count": 2},
)
assert resp.status_code == 202
body = resp.json()
assert body["chain_key"] == "continue_volume"
assert len(_noop_run_chain_job) == 1
assert _noop_run_chain_job[0]["kwargs"]["chain_key"] == "continue_volume"
async def test_run_chain_unknown_key_returns_404(
_noop_run_chain_job: list[dict[str, Any]],
) -> None:

View File

@@ -59,8 +59,9 @@ log = get_logger("ww.api.chain")
router = APIRouter(prefix="/projects", tags=["chain"])
# 本期唯一内置链§1.3/§3。未知 key → 404系统边界fail fast
SUPPORTED_CHAINS = frozenset({"draft_volume"})
# 内置链§1.3/§3draft_volume=从 start 章按记忆量产continue_volume=每章以上一章已验收
# 正文末尾作前文引子续写F2。未知 key → 404系统边界fail fast
SUPPORTED_CHAINS = frozenset({"draft_volume", "continue_volume"})
ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
JobRepoDep = Annotated[JobRepo, Depends(get_job_repo)]