feat(ux): T4-b 后端 — draft 端点接收本章指令 directive 直通 assemble(volatile)

assemble/_build_volatile 加 directive 参数(仅入 volatile,守缓存前缀不变量 #9);stream_draft 加可选 body {directive} 透传;不持久化、无迁移;directive 长度入 log 不记原文。新 DraftStreamRequest schema + 重生成 TS 客户端。后端门禁绿: ruff/mypy/pytest。契约记 memory/contracts.md。
This commit is contained in:
Yaojia Wang
2026-06-20 18:25:00 +02:00
parent 8779530806
commit 2155193cdc
7 changed files with 94 additions and 5 deletions

View File

@@ -196,6 +196,33 @@ async def test_draft_stream_yields_sse_tokens_and_done() -> None:
assert gateway.requests[0].tier == "writer"
@pytest.mark.asyncio
async def test_draft_stream_threads_directive_into_volatile() -> None:
gateway = FakeWriterGateway(chunks=["正文"])
client, _, _, _ = _make_client(gateway=gateway)
pid = uuid.uuid4()
async with client:
resp = await client.post(
f"/projects/{pid}/chapters/1/draft", json={"directive": "多写战斗"}
)
assert resp.status_code == 200
assert len(gateway.requests) == 1
# 指令直通 assemble→volatilewrite 节点把 volatile 放进 LlmRequest.input
assert "多写战斗" in str(gateway.requests[0].input)
@pytest.mark.asyncio
async def test_draft_stream_backward_compatible_without_body() -> None:
gateway = FakeWriterGateway(chunks=["正文"])
client, _, _, _ = _make_client(gateway=gateway)
pid = uuid.uuid4()
async with client:
resp = await client.post(f"/projects/{pid}/chapters/1/draft")
assert resp.status_code == 200
assert len(gateway.requests) == 1
assert "本章指令" not in str(gateway.requests[0].input)
@pytest.mark.asyncio
async def test_draft_stream_maps_error_to_sse_error_event() -> None:
from ww_shared import AppError

View File

@@ -53,6 +53,7 @@ from ww_api.schemas.projects import (
AcceptResponse,
DraftResponse,
DraftSaveRequest,
DraftStreamRequest,
DraftView,
ProjectCreateRequest,
ProjectListResponse,
@@ -240,11 +241,17 @@ async def stream_draft(
gateway: GatewayDep,
injection_repo: InjectionRepoDep,
session: Annotated[AsyncSession, Depends(get_session)],
body: DraftStreamRequest | None = None,
) -> StreamingResponse:
"""流式写章草稿:组装记忆(含作者注入覆盖)→ 网关流 → 归一 SSE 事件 → text/event-stream。"""
"""流式写章草稿:组装记忆(含作者注入覆盖 + 本章指令)→ 网关流 → 归一 SSE event-stream。
`body.directive`可选T4-b是临时本章指令直通 assemble→volatile不持久化
无 body 的旧调用方仍可用(向后兼容)。
"""
request_id = getattr(request.state, "request_id", None)
directive = body.directive if body else None
override = await injection_repo.get(project_id, chapter_no)
context = await assemble(repos, project_id, chapter_no, override=override)
context = await assemble(repos, project_id, chapter_no, override=override, directive=directive)
log.info(
"draft_stream_start",
project_id=str(project_id),
@@ -252,6 +259,7 @@ async def stream_draft(
request_id=request_id,
stable_core_len=len(context.stable_core),
volatile_len=len(context.volatile),
directive_len=len(body.directive) if body and body.directive else 0,
)
deltas = stream_chapter_draft(

View File

@@ -42,6 +42,16 @@ class ProjectListResponse(BaseModel):
projects: list[ProjectResponse] = Field(default_factory=list)
class DraftStreamRequest(BaseModel):
"""POST /projects/:id/chapters/:no/draft本章生成的可选输入T4-b
`directive` = 作者本章指令(覆盖/补充大纲节拍)。临时输入,不持久化、无迁移;
后端只入 volatile守缓存前缀不变量 #9。无 body 时退化为纯按大纲生成(向后兼容)。
"""
directive: str | None = None
class DraftSaveRequest(BaseModel):
"""PUT /projects/:id/chapters/:no/draft自动保存草稿正文。"""

View File

@@ -142,7 +142,10 @@ export interface paths {
put: operations["save_draft_projects__project_id__chapters__chapter_no__draft_put"];
/**
* Stream Draft
* @description 流式写章草稿:组装记忆(含作者注入覆盖)→ 网关流 → 归一 SSE 事件 → text/event-stream。
* @description 流式写章草稿:组装记忆(含作者注入覆盖 + 本章指令)→ 网关流 → 归一 SSE event-stream。
*
* `body.directive`可选T4-b是临时本章指令直通 assemble→volatile不持久化
* 无 body 的旧调用方仍可用(向后兼容)。
*/
post: operations["stream_draft_projects__project_id__chapters__chapter_no__draft_post"];
delete?: never;
@@ -839,6 +842,17 @@ export interface components {
/** Text */
text: string;
};
/**
* DraftStreamRequest
* @description POST /projects/:id/chapters/:no/draft本章生成的可选输入T4-b
*
* `directive` = 作者本章指令(覆盖/补充大纲节拍)。临时输入,不持久化、无迁移;
* 后端只入 volatile守缓存前缀不变量 #9。无 body 时退化为纯按大纲生成(向后兼容)。
*/
DraftStreamRequest: {
/** Directive */
directive?: string | null;
};
/**
* DraftView
* @description GET .../draft回灌已保存草稿含正文供工作台重载编辑器
@@ -1848,7 +1862,11 @@ export interface operations {
};
cookie?: never;
};
requestBody?: never;
requestBody?: {
content: {
"application/json": components["schemas"]["DraftStreamRequest"] | null;
};
};
responses: {
/** @description Successful Response */
200: {