Przeglądaj źródła

fix(core): require explicit title for agent book creation

Ma 6 miesięcy temu
rodzic
commit
3813cbbf80

+ 2 - 0
packages/core/src/__tests__/agent-system-prompt.test.ts

@@ -13,6 +13,7 @@ describe("buildAgentSystemPrompt", () => {
       expect(prompt).toContain("核心冲突");
       expect(prompt).toContain("architect");
       expect(prompt).toContain("sub_agent");
+      expect(prompt).toContain("title");
     });
 
     it("English prompt includes info collection workflow", () => {
@@ -22,6 +23,7 @@ describe("buildAgentSystemPrompt", () => {
       expect(prompt).toContain("Genre");
       expect(prompt).toContain("Protagonist");
       expect(prompt).toContain("Core conflict");
+      expect(prompt).toContain("title");
     });
 
     it("Chinese prompt forbids emoji", () => {

+ 41 - 0
packages/core/src/__tests__/agent-tools.test.ts

@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
 import { join } from "node:path";
 import { StateManager } from "../state/manager.js";
 import {
+  createSubAgentTool,
   createPatchChapterTextTool,
   createRenameEntityTool,
   createReviseChapterTool,
@@ -119,4 +120,44 @@ describe("agent deterministic writing tools", () => {
       }),
     ]);
   });
+
+  it("requires an explicit title when the architect sub-agent creates a book", async () => {
+    const pipeline = {
+      initBook: vi.fn(async () => undefined),
+    };
+    const tool = createSubAgentTool(pipeline as never, null);
+
+    const result = await tool.execute("tool-5", {
+      agent: "architect",
+      instruction: "写一本港风商战小说",
+    });
+
+    expect(result.content[0]?.type).toBe("text");
+    if (result.content[0]?.type === "text") {
+      expect(result.content[0].text).toContain("title is required");
+    }
+    expect(pipeline.initBook).not.toHaveBeenCalled();
+  });
+
+  it("passes the explicit architect title straight into initBook", async () => {
+    const pipeline = {
+      initBook: vi.fn(async () => undefined),
+    };
+    const tool = createSubAgentTool(pipeline as never, null);
+
+    await tool.execute("tool-6", {
+      agent: "architect",
+      title: "夜港账本",
+      instruction: "写一本港风商战小说",
+    });
+
+    expect(pipeline.initBook).toHaveBeenCalledWith(
+      expect.objectContaining({
+        title: "夜港账本",
+      }),
+      expect.objectContaining({
+        externalContext: "写一本港风商战小说",
+      }),
+    );
+  });
 });

+ 2 - 0
packages/core/src/agent/agent-system-prompt.ts

@@ -16,6 +16,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
    - 写作语言(中文/English)
 
 2. **确认建书**(调用阶段)— 当信息足够时,调用 sub_agent 工具委托 architect 子智能体建书:
+   - 必须显式传入 "title" 参数,不能留空
    - instruction 中包含收集到的所有信息(题材、世界观、主角、冲突等)
    - architect 会生成完整的 foundation(世界观设定、卷纲规划、叙事规则等)
 
@@ -45,6 +46,7 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
    - Writing language
 
 2. **Create book** — When you have enough info, call the sub_agent tool with agent="architect":
+   - Pass the explicit "title" parameter; do not leave it empty
    - Include all collected info in the instruction
    - The architect will generate the complete foundation
 

+ 18 - 4
packages/core/src/agent/agent-tools.ts

@@ -59,8 +59,18 @@ const SubAgentParams = Type.Object({
   ]),
   instruction: Type.String({ description: "Natural language instruction from the main Agent" }),
   bookId: Type.Optional(Type.String({ description: "Book ID — required for all agents except architect" })),
+  title: Type.Optional(Type.String({ description: "Architect only: explicit book title. Required when creating a book." })),
 });
 
+function deriveBookIdFromTitle(title: string): string {
+  return title
+    .toLowerCase()
+    .replace(/[^a-z0-9\u4e00-\u9fff]/g, "-")
+    .replace(/-+/g, "-")
+    .replace(/^-+|-+$/g, "")
+    .slice(0, 30);
+}
+
 export function createSubAgentTool(
   pipeline: PipelineRunner,
   activeBookId: string | null,
@@ -80,7 +90,7 @@ export function createSubAgentTool(
       _signal?: AbortSignal,
       onUpdate?: AgentToolUpdateCallback,
     ): Promise<AgentToolResult<undefined>> {
-      const { agent, instruction, bookId } = params;
+      const { agent, instruction, bookId, title } = params;
 
       const progress = (msg: string) => {
         onUpdate?.(textResult(msg));
@@ -93,14 +103,18 @@ export function createSubAgentTool(
             if (activeBookId) {
               return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。");
             }
-            const id = bookId || `book-${Date.now().toString(36)}`;
+            const resolvedTitle = title?.trim();
+            if (!resolvedTitle) {
+              return textResult('Error: title is required for the architect agent.');
+            }
+            const id = bookId || deriveBookIdFromTitle(resolvedTitle) || `book-${Date.now().toString(36)}`;
             progress(`Starting architect for book "${id}"...`);
             await pipeline.initBook(
-              { id, genre: "general", title: "", language: "zh" } as any,
+              { id, genre: "general", title: resolvedTitle, language: "zh" } as any,
               { externalContext: instruction },
             );
             progress(`Architect finished — book "${id}" foundation created.`);
-            return textResult(`Book "${id}" initialised successfully. Foundation files are ready.`);
+            return textResult(`Book "${resolvedTitle}" (${id}) initialised successfully. Foundation files are ready.`);
           }
 
           case "writer": {