From 8c7d4e21692791e8082b1c77d626f8765f7867bd Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Tue, 23 Jun 2026 19:38:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(scope-b):=20=E4=BF=AE=E8=AF=84=E5=AE=A1=204?= =?UTF-8?q?=20HIGH=20+=20=E5=90=8C=E6=AD=A5=20T6=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=95=B0(15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. T6 e2e 工具数同步:list 断言 11→15(含 continue/expand/de-ai/teardown), 测试名 ...eleven→fifteen;保留 legacy/ingestable 意图。 2. teardown 书名(body.kind)前置入原文 → 进 text_input context(拆书 prompt 期待书名)。 3. text_input 工具(扩写/降AI/拆书)空原文 → 422 VALIDATION,不让 LLM 空跑。 4. 链 UI:busy 守卫加 awaiting,裁决期间禁止重复发起链。 测试:扩 teardown 断言书名入 context + 新增 expand 空原文 422;后端 642 passed, 前端 294 passed,两门禁全绿。 --- apps/api/tests/test_toolbox_endpoints.py | 25 ++++++++++++++++++++++++ apps/api/ww_api/routers/toolbox.py | 15 +++++++++++++- apps/web/components/chain/ChainPage.tsx | 4 +++- tests/test_t6_toolbox_e2e.py | 12 ++++++++---- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/apps/api/tests/test_toolbox_endpoints.py b/apps/api/tests/test_toolbox_endpoints.py index 6ee1590..06d839d 100644 --- a/apps/api/tests/test_toolbox_endpoints.py +++ b/apps/api/tests/test_toolbox_endpoints.py @@ -492,6 +492,31 @@ async def test_generate_teardown_returns_structured() -> None: assert body["output_kind"] == "BookTeardownResult" assert body["preview"]["themes"] == ["逆袭"] assert body["preview"]["structure"] == "黄金三章立钩" + # 书名(body.kind)前置入原文 → 进了喂网关的 context(拆书 prompt 期待书名)。 + assert gateway.last_input is not None + assert "书名:某爆款" in gateway.last_input + assert "样本章节正文" in gateway.last_input + + +@pytest.mark.asyncio +async def test_generate_text_input_empty_text_422() -> None: + # text_input 工具(扩写/降AI/拆书)原文为空/空白 → 422,不触达网关(不让 LLM 空跑)。 + repo = FakeProjectRepo() + pid = await _seed_project(repo) + gateway = _CaptureGateway(PolishResult(text="不应被调用。")) + app, session, *_ = _make_app(project_repo=repo, gateway=gateway) + + async with _client(app) as client: + resp = await client.post( + f"/projects/{pid}/skills/expand/generate", + json={"text": " ", "brief": "加强描写"}, + ) + + assert resp.status_code == 422 + assert resp.json()["error"]["code"] == ErrorCode.VALIDATION + # 校验在生成前拦下:网关未被调用、未 commit。 + assert gateway.last_input is None + assert session.commits == 0 # ---- POST .../ingest(world_entities gate)---- diff --git a/apps/api/ww_api/routers/toolbox.py b/apps/api/ww_api/routers/toolbox.py index 8a08fde..a4d3ef4 100644 --- a/apps/api/ww_api/routers/toolbox.py +++ b/apps/api/ww_api/routers/toolbox.py @@ -192,6 +192,7 @@ async def generate_with_tool( world_context = "" beats: list[str] = [] prior_text = "" + text = body.text or "" if tool.context_strategy == "with_world": world_context = await _world_context(memory, project_id) elif tool.context_strategy == "with_outline_chapter": @@ -202,6 +203,18 @@ async def generate_with_tool( chapter_no = body.chapter_no or 1 prior_text = await _prior_chapter_text(chapter_repo, project_id, chapter_no) beats = await _chapter_beats(outline_repo, project_id, chapter_no) + elif tool.context_strategy == "text_input": + # text_input 工具(扩写/降AI/拆书)必须有原文样本——空原文会让 LLM 空跑(守输入校验纪律)。 + if not text.strip(): + raise AppError( + ErrorCode.VALIDATION, + f"工具 {tool_key} 需要原文输入(text 不可为空)", + {"tool_key": tool_key}, + ) + # 拆书的书名(body.kind)前置入原文,供拆解对照(teardown system prompt 期待书名)。 + kind = (body.kind or "").strip() + if kind: + text = f"书名:{kind}\n\n{text}" context = build_toolbox_context( tool.context_strategy, @@ -211,7 +224,7 @@ async def generate_with_tool( chapter_no=body.chapter_no, beats=beats, prior_text=prior_text, - text=body.text or "", + text=text, ) gateway = await build_gateway(spec.tier) diff --git a/apps/web/components/chain/ChainPage.tsx b/apps/web/components/chain/ChainPage.tsx index 9b00f63..0ba9178 100644 --- a/apps/web/components/chain/ChainPage.tsx +++ b/apps/web/components/chain/ChainPage.tsx @@ -77,7 +77,9 @@ export function ChainPage({ project }: ChainPageProps) { const showProgress = jobId !== null && result !== null; // 链运行中/等待裁决时禁止重复发起。 - const busy = starting || (jobId !== null && phase === "running"); + const busy = + starting || + (jobId !== null && (phase === "running" || phase === "awaiting")); return ( None: assert needle not in blob, f"sensitive marker leaked into response: {needle!r}" -async def test_t6_list_toolbox_returns_all_eleven_tools( +async def test_t6_list_toolbox_returns_all_fifteen_tools( e2e_sm: async_sessionmaker[AsyncSession], ) -> None: - """用例 1:GET /skills/toolbox 返回全 11 key;legacy 3 带 legacy_route,新 8 走通用执行器。""" + """用例 1:GET /skills/toolbox 返回全 15 key;legacy 3 带 legacy_route,新 12 走通用执行器。""" from ww_api.main import create_app app = create_app() @@ -256,7 +256,7 @@ async def test_t6_list_toolbox_returns_all_eleven_tools( body = resp.json() tools = {t["key"]: t for t in body["tools"]} - # 全 11 key(legacy 3 + 新 8)。 + # 全 15 key(legacy 3 + 新 8 + 竞品快赢 4:续写/扩写/降AI率/拆书)。 assert set(tools) == { "worldbuilding", "character", @@ -269,6 +269,10 @@ async def test_t6_list_toolbox_returns_all_eleven_tools( "glossary", "opening", "fine-outline", + "continue", + "expand", + "de-ai", + "teardown", } # legacy(spec=None):is_legacy=true + 带 legacy_route,不可经通用执行入库。 legacy_keys = {"worldbuilding", "character", "outline"}