Просмотр исходного кода

feat(core): minimally extend sub_agent control params

Ma 6 месяцев назад
Родитель
Сommit
bca9529c3d

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

@@ -51,6 +51,9 @@ describe("buildAgentSystemPrompt", () => {
       expect(prompt).toContain("writer");
       expect(prompt).toContain("auditor");
       expect(prompt).toContain("reviser");
+      expect(prompt).toContain("chapterWordCount");
+      expect(prompt).toContain("mode");
+      expect(prompt).toContain("approvedOnly");
       expect(prompt).toContain("read");
       expect(prompt).toContain("revise_chapter");
       expect(prompt).toContain("write_truth_file");

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

@@ -160,4 +160,64 @@ describe("agent deterministic writing tools", () => {
       }),
     );
   });
+
+  it("passes chapterWordCount through the writer sub-agent", async () => {
+    const pipeline = {
+      writeNextChapter: vi.fn(async () => ({
+        chapterNumber: 4,
+        wordCount: 2600,
+      })),
+    };
+    const tool = createSubAgentTool(pipeline as never, "harbor");
+
+    await tool.execute("tool-7", {
+      agent: "writer",
+      bookId: "harbor",
+      chapterWordCount: 2600,
+      instruction: "继续写,控制在 2600 字",
+    } as any);
+
+    expect(pipeline.writeNextChapter).toHaveBeenCalledWith("harbor", 2600);
+  });
+
+  it("prefers explicit reviser mode over instruction guessing", async () => {
+    const pipeline = {
+      reviseDraft: vi.fn(async () => ({
+        chapterNumber: 3,
+        wordCount: 120,
+        fixedIssues: [],
+        applied: true,
+        status: "ready-for-review" as const,
+      })),
+    };
+    const tool = createSubAgentTool(pipeline as never, "harbor");
+
+    await tool.execute("tool-8", {
+      agent: "reviser",
+      bookId: "harbor",
+      chapterNumber: 3,
+      mode: "spot-fix",
+      instruction: "重写第3章",
+    } as any);
+
+    expect(pipeline.reviseDraft).toHaveBeenCalledWith("harbor", 3, "spot-fix");
+  });
+
+  it("uses explicit exporter params instead of guessing from instruction", async () => {
+    const pipeline = {};
+    const tool = createSubAgentTool(pipeline as never, "harbor", root);
+
+    const result = await tool.execute("tool-9", {
+      agent: "exporter",
+      bookId: "harbor",
+      format: "md",
+      approvedOnly: false,
+      instruction: "导出成 epub",
+    } as any);
+
+    expect(result.content[0]?.type).toBe("text");
+    if (result.content[0]?.type === "text") {
+      expect(result.content[0].text).toContain(".md");
+    }
+  });
 });

+ 12 - 6
packages/core/src/agent/agent-system-prompt.ts

@@ -71,11 +71,14 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 ## 可用工具
 
 - **sub_agent** — 委托子智能体执行重操作:
-  - agent="writer" 写下一章
+  - agent="writer" 写下一章(支持 chapterWordCount)
   - agent="auditor" 审计章节质量(可指定 chapterNumber)
-  - agent="reviser" 修订章节(可指定 chapterNumber 和修订模式)
-  - agent="exporter" 导出书籍
+  - agent="reviser" 修订章节(可指定 chapterNumber 和 mode)
+  - agent="exporter" 导出书籍(可指定 format 和 approvedOnly)
   - **chapterNumber 参数**:auditor 和 reviser 支持指定章节号,不指定则默认最新章节
+  - **chapterWordCount 参数**:writer 可指定本章目标字数
+  - **mode 参数**:reviser 可显式指定修订模式
+  - **format / approvedOnly 参数**:exporter 可显式指定导出格式和是否仅导出已通过章节
 - **read** — 读取书籍的设定文件或章节内容
 - **revise_chapter** — 对已有章节做精修/重写/返工
 - **write_truth_file** — 整文件覆盖真相文件(story_bible、volume_outline、book_rules、current_focus 等)
@@ -120,11 +123,14 @@ export function buildAgentSystemPrompt(bookId: string | null, language: string):
 ## Available Tools
 
 - **sub_agent** — Delegate to sub-agents:
-  - agent="writer" for writing next chapter
+  - agent="writer" for writing next chapter (supports chapterWordCount)
   - agent="auditor" for chapter quality audit (supports chapterNumber)
-  - agent="reviser" for chapter revision (supports chapterNumber + mode via instruction)
-  - agent="exporter" for book export
+  - agent="reviser" for chapter revision (supports chapterNumber and explicit mode)
+  - agent="exporter" for book export (supports format and approvedOnly)
   - **chapterNumber param**: auditor and reviser accept an explicit chapter number; omit for latest
+  - **chapterWordCount param**: writer can override the chapter target word count
+  - **mode param**: reviser can explicitly choose the revision mode
+  - **format / approvedOnly params**: exporter can explicitly choose the export format and approval filter
 - **read** — Read truth files or chapter content
 - **revise_chapter** — Rewrite or polish an existing chapter
 - **write_truth_file** — Replace a canonical truth file in story/

+ 25 - 12
packages/core/src/agent/agent-tools.ts

@@ -61,6 +61,20 @@ const SubAgentParams = Type.Object({
   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." })),
   chapterNumber: Type.Optional(Type.Number({ description: "Target chapter number for auditor/reviser. Omit to use the latest chapter." })),
+  chapterWordCount: Type.Optional(Type.Number({ description: "Writer only: explicit chapterWordCount override." })),
+  mode: Type.Optional(Type.Union([
+    Type.Literal("spot-fix"),
+    Type.Literal("polish"),
+    Type.Literal("rewrite"),
+    Type.Literal("rework"),
+    Type.Literal("anti-detect"),
+  ])),
+  format: Type.Optional(Type.Union([
+    Type.Literal("txt"),
+    Type.Literal("md"),
+    Type.Literal("epub"),
+  ])),
+  approvedOnly: Type.Optional(Type.Boolean({ description: "Exporter only: export approved chapters only." })),
 });
 
 function deriveBookIdFromTitle(title: string): string {
@@ -91,7 +105,7 @@ export function createSubAgentTool(
       _signal?: AbortSignal,
       onUpdate?: AgentToolUpdateCallback,
     ): Promise<AgentToolResult<undefined>> {
-      const { agent, instruction, bookId, title, chapterNumber } = params;
+      const { agent, instruction, bookId, title, chapterNumber, chapterWordCount, mode, format, approvedOnly } = params;
 
       const progress = (msg: string) => {
         onUpdate?.(textResult(msg));
@@ -121,7 +135,7 @@ export function createSubAgentTool(
           case "writer": {
             if (!bookId) return textResult("Error: bookId is required for the writer agent.");
             progress(`Writing next chapter for "${bookId}"...`);
-            const result = await pipeline.writeNextChapter(bookId);
+            const result = await pipeline.writeNextChapter(bookId, chapterWordCount);
             progress(`Writer finished chapter for "${bookId}".`);
             return textResult(
               `Chapter written for "${bookId}". ` +
@@ -143,33 +157,32 @@ export function createSubAgentTool(
 
           case "reviser": {
             if (!bookId) return textResult("Error: bookId is required for the reviser agent.");
-            // Detect revision mode from instruction keywords
-            const mode: ReviseMode = /rewrite|改写|重写/.test(instruction)
+            const resolvedMode: ReviseMode = mode ?? (/rewrite|改写|重写/.test(instruction)
               ? "rewrite"
               : /polish|润色/.test(instruction)
                 ? "polish"
                 : /rework|返工/.test(instruction)
                   ? "rework"
-                  : "spot-fix";
-            progress(`Revising "${bookId}" chapter ${chapterNumber ?? "latest"} in ${mode} mode...`);
-            await pipeline.reviseDraft(bookId, chapterNumber, mode);
+                  : "spot-fix");
+            progress(`Revising "${bookId}" chapter ${chapterNumber ?? "latest"} in ${resolvedMode} mode...`);
+            await pipeline.reviseDraft(bookId, chapterNumber, resolvedMode);
             progress(`Revision complete for "${bookId}".`);
-            return textResult(`Revision (${mode}) complete for "${bookId}" chapter ${chapterNumber ?? "latest"}.`);
+            return textResult(`Revision (${resolvedMode}) complete for "${bookId}" chapter ${chapterNumber ?? "latest"}.`);
           }
 
           case "exporter": {
             if (!bookId) return textResult("Error: bookId is required for the exporter agent.");
             if (!projectRoot) return textResult("Error: exporter requires projectRoot.");
-            const inferredFormat = /epub/i.test(instruction)
+            const inferredFormat = format ?? (/epub/i.test(instruction)
               ? "epub"
               : /markdown|\bmd\b/i.test(instruction)
                 ? "md"
-                : "txt";
-            const approvedOnly = /approved|已通过|通过章节/.test(instruction);
+                : "txt");
+            const exportApprovedOnly = approvedOnly ?? /approved|已通过|通过章节/.test(instruction);
             const state = new StateManager(projectRoot);
             const result = await writeExportArtifact(state, bookId, {
               format: inferredFormat,
-              approvedOnly,
+              approvedOnly: exportApprovedOnly,
             });
             return textResult(
               `Exported "${bookId}": ${result.chaptersExported} chapters, ${result.totalWords} words → ${result.outputPath}`,