feat(backend): 整章再沟通/重写 SSE 端点——喂当前草稿+意见+完整记忆注入流式重写一版
WFW-8 整章级再沟通(后端)。新增 POST /projects/{id}/chapters/{no}/rewrite(SSE):
- rewrite.md 教条(品类无关内核,非 SPEC,仿 write_craft 经 load_prompt 读盘);
- rewrite_node:build_rewrite_request 纯函数(system=[rewrite教条,stable_core] cache 前缀;
input=近况+当前草稿+作者意见,断点后不入缓存前缀——守不变量 #9)+ stream_chapter_rewrite;
- RewriteStreamRequest{feedback,prior_draft}(带 max_length 上界);端点复用 assemble 记忆注入
+ normalize_deltas SSE,只读不写库(HITL,新版停前端接受才落,不变量 #3),是工具非写节点
(不破坏 章=f(outline,state) 纯函数,不变量 #7),项目不存在触网关前 404。
门禁绿:ruff / mypy 224 files / pytest(新增 test_rewrite_node 5 例 + 更新 doctrine 一一对应)/
alembic 无漂移。OpenAPI 含新路径 + schema(前端待 gen:api)。
This commit is contained in:
@@ -37,6 +37,7 @@ from ww_core.orchestrator import (
|
||||
normalize_deltas,
|
||||
normalize_review,
|
||||
stream_chapter_draft,
|
||||
stream_chapter_rewrite,
|
||||
)
|
||||
from ww_db import get_session
|
||||
from ww_llm_gateway import Gateway
|
||||
@@ -63,6 +64,7 @@ from ww_api.schemas.projects import (
|
||||
ReviewHistoryItem,
|
||||
ReviewHistoryResponse,
|
||||
ReviewRequest,
|
||||
RewriteStreamRequest,
|
||||
)
|
||||
from ww_api.services.accept_service import (
|
||||
AcceptOutcome,
|
||||
@@ -310,6 +312,64 @@ async def stream_draft(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{project_id}/chapters/{chapter_no}/rewrite")
|
||||
async def stream_rewrite(
|
||||
project_id: uuid.UUID,
|
||||
chapter_no: int,
|
||||
request: Request,
|
||||
repos: MemoryReposDep,
|
||||
gateway: GatewayDep,
|
||||
injection_repo: InjectionRepoDep,
|
||||
project_repo: ProjectRepoDep,
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
body: RewriteStreamRequest,
|
||||
) -> StreamingResponse:
|
||||
"""整章再沟通/重写:组装记忆 + 当前草稿 + 作者意见 → 网关流 → 归一 SSE → event-stream。
|
||||
|
||||
HITL:新版停前端/草稿,接受才落库(不变量 #3);rewrite 是工具非写节点,不破坏章
|
||||
纯函数性(不变量 #7)。项目不存在 → 404(触网关前 fail-fast,同 draft,QA C1)。
|
||||
"""
|
||||
if await project_repo.get(STUB_OWNER_ID, project_id) is None:
|
||||
raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
override = await injection_repo.get(project_id, chapter_no)
|
||||
context = await assemble(repos, project_id, chapter_no, override=override)
|
||||
log.info(
|
||||
"rewrite_stream_start",
|
||||
project_id=str(project_id),
|
||||
chapter_no=chapter_no,
|
||||
request_id=request_id,
|
||||
stable_core_len=len(context.stable_core),
|
||||
volatile_len=len(context.volatile),
|
||||
prior_draft_len=len(body.prior_draft),
|
||||
feedback_len=len(body.feedback),
|
||||
)
|
||||
|
||||
deltas = stream_chapter_rewrite(
|
||||
gateway,
|
||||
stable_core=context.stable_core,
|
||||
volatile=context.volatile,
|
||||
prior_draft=body.prior_draft,
|
||||
feedback=body.feedback,
|
||||
user_id=STUB_OWNER_ID,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
async def _frames() -> AsyncIterator[str]:
|
||||
async for event in normalize_deltas(deltas, request_id=request_id):
|
||||
yield _encode_sse(event)
|
||||
try:
|
||||
await session.commit()
|
||||
except Exception: # noqa: BLE001 — 流已发完,commit 失败不能再改响应;至少记错误。
|
||||
log.error("sse_commit_failed", request_id=request_id, endpoint="rewrite")
|
||||
|
||||
return StreamingResponse(
|
||||
_frames(),
|
||||
media_type="text/event-stream",
|
||||
headers={"cache-control": "no-cache", "x-accel-buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{project_id}/chapters/{chapter_no}/draft")
|
||||
async def save_draft(
|
||||
project_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user