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"}