Răsfoiți Sursa

fix(core): align architect/writer/auditor/reviser with pipeline params

- architect: pass full BookConfig (title, genre, platform, language,
  targetChapters, chapterWordCount, status, timestamps) instead of
  hardcoded stub
- writer: forward chapterWordCount to pipeline.writeNextChapter
- auditor: return per-issue severity/description and pass/fail status
  instead of bare issue count
- reviser: use explicit mode param instead of regex-based detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 6 luni în urmă
părinte
comite
86e8d0a6f8

+ 107 - 1
packages/core/src/__tests__/agent-tools-params.test.ts

@@ -1,4 +1,4 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect, vi, beforeEach } from "vitest";
 import { createSubAgentTool } from "../agent/agent-tools.js";
 
 describe("SubAgentParams schema", () => {
@@ -42,3 +42,109 @@ describe("SubAgentParams schema", () => {
     expect(props.format.description).toMatch(/exporter/i);
   });
 });
+
+describe("architect agent — BookConfig construction", () => {
+  let initBookMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+
+  beforeEach(() => {
+    initBookMock = vi.fn(async () => {});
+    const mockPipeline = { initBook: initBookMock } as any;
+    tool = createSubAgentTool(mockPipeline, null);
+  });
+
+  it("passes complete BookConfig with schema params", async () => {
+    await tool.execute("tc1", {
+      agent: "architect",
+      instruction: "Create a xuanhuan novel",
+      title: "天道独行",
+      genre: "xuanhuan",
+      platform: "tomato",
+      language: "zh",
+      targetChapters: 100,
+      chapterWordCount: 4000,
+    });
+    expect(initBookMock).toHaveBeenCalledOnce();
+    const [bookConfig, options] = initBookMock.mock.calls[0];
+    expect(bookConfig.title).toBe("天道独行");
+    expect(bookConfig.genre).toBe("xuanhuan");
+    expect(bookConfig.platform).toBe("tomato");
+    expect(bookConfig.language).toBe("zh");
+    expect(bookConfig.targetChapters).toBe(100);
+    expect(bookConfig.chapterWordCount).toBe(4000);
+    expect(bookConfig.status).toBe("outlining");
+    expect(bookConfig.createdAt).toBeDefined();
+    expect(options.externalContext).toBe("Create a xuanhuan novel");
+  });
+
+  it("uses defaults when optional params are omitted", async () => {
+    await tool.execute("tc2", { agent: "architect", instruction: "Create a book" });
+    const [bookConfig] = initBookMock.mock.calls[0];
+    expect(bookConfig.genre).toBe("general");
+    expect(bookConfig.platform).toBe("other");
+    expect(bookConfig.language).toBe("zh");
+    expect(bookConfig.targetChapters).toBe(200);
+    expect(bookConfig.chapterWordCount).toBe(3000);
+  });
+});
+
+describe("writer agent — wordCount passthrough", () => {
+  let writeNextChapterMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+
+  beforeEach(() => {
+    writeNextChapterMock = vi.fn(async () => ({ wordCount: 3000 }));
+    const mockPipeline = { writeNextChapter: writeNextChapterMock } as any;
+    tool = createSubAgentTool(mockPipeline, "existing-book");
+  });
+
+  it("passes chapterWordCount as wordCount", async () => {
+    await tool.execute("tc1", { agent: "writer", instruction: "Write", bookId: "my-book", chapterWordCount: 5000 });
+    expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", 5000);
+  });
+
+  it("passes undefined when chapterWordCount omitted", async () => {
+    await tool.execute("tc2", { agent: "writer", instruction: "Write", bookId: "my-book" });
+    expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", undefined);
+  });
+});
+
+describe("auditor agent — rich return value", () => {
+  it("returns issue details with severity", async () => {
+    const auditDraftMock = vi.fn(async () => ({
+      chapterNumber: 3, passed: false,
+      issues: [
+        { severity: "warning", description: "Pacing too fast" },
+        { severity: "critical", description: "Name inconsistency" },
+      ],
+    }));
+    const tool = createSubAgentTool({ auditDraft: auditDraftMock } as any, "book");
+    const result = await tool.execute("tc1", { agent: "auditor", instruction: "Audit", bookId: "my-book", chapterNumber: 3 });
+    const text = result.content[0].text;
+    expect(text).toContain("FAILED");
+    expect(text).toContain("2 issue(s)");
+    expect(text).toContain("[warning]");
+    expect(text).toContain("[critical]");
+    expect(text).toContain("Pacing too fast");
+  });
+});
+
+describe("reviser agent — mode field", () => {
+  let reviseDraftMock: ReturnType<typeof vi.fn>;
+  let tool: ReturnType<typeof createSubAgentTool>;
+
+  beforeEach(() => {
+    reviseDraftMock = vi.fn(async () => ({}));
+    tool = createSubAgentTool({ reviseDraft: reviseDraftMock } as any, "book");
+  });
+
+  it("uses mode param directly", async () => {
+    await tool.execute("tc1", { agent: "reviser", instruction: "Fix", bookId: "my-book", chapterNumber: 5, mode: "anti-detect" });
+    expect(reviseDraftMock).toHaveBeenCalledWith("my-book", 5, "anti-detect");
+  });
+
+  it("defaults to spot-fix", async () => {
+    await tool.execute("tc2", { agent: "reviser", instruction: "Fix", bookId: "my-book" });
+    expect(reviseDraftMock).toHaveBeenCalledWith("my-book", undefined, "spot-fix");
+  });
+});

+ 24 - 19
packages/core/src/agent/agent-tools.ts

@@ -96,14 +96,25 @@ export function createSubAgentTool(pipeline: PipelineRunner, activeBookId: strin
       try {
         switch (agent) {
           case "architect": {
-            // architect 只在没有书的时候可用(建书流程)
             if (activeBookId) {
               return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。");
             }
             const id = bookId || `book-${Date.now().toString(36)}`;
+            const now = new Date().toISOString();
             progress(`Starting architect for book "${id}"...`);
             await pipeline.initBook(
-              { id, genre: "general", title: "", language: "zh" } as any,
+              {
+                id,
+                title: title ?? "",
+                genre: genre ?? "general",
+                platform: (platform ?? "other") as any,
+                language: (language ?? "zh") as any,
+                status: "outlining" as any,
+                targetChapters: targetChapters ?? 200,
+                chapterWordCount: chapterWordCount ?? 3000,
+                createdAt: now,
+                updatedAt: now,
+              },
               { externalContext: instruction },
             );
             progress(`Architect finished — book "${id}" foundation created.`);
@@ -113,11 +124,10 @@ export function createSubAgentTool(pipeline: PipelineRunner, activeBookId: strin
           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}". ` +
-              `Word count: ${(result as any).wordCount ?? "unknown"}.`,
+              `Chapter written for "${bookId}". Word count: ${(result as any).wordCount ?? "unknown"}.`,
             );
           }
 
@@ -126,27 +136,22 @@ export function createSubAgentTool(pipeline: PipelineRunner, activeBookId: strin
             progress(`Auditing chapter ${chapterNumber ?? "latest"} for "${bookId}"...`);
             const audit = await pipeline.auditDraft(bookId, chapterNumber);
             progress(`Audit complete for "${bookId}".`);
-            const issueCount = audit.issues?.length ?? 0;
+            const issueLines = (audit.issues ?? [])
+              .map((i: any) => `[${i.severity}] ${i.description}`)
+              .join("\n");
             return textResult(
-              `Audit complete for "${bookId}": ${issueCount} issue(s) found. ` +
-              `Chapter ${audit.chapterNumber}.`,
+              `Audit chapter ${audit.chapterNumber}: ${audit.passed ? "PASSED" : "FAILED"}, ${(audit.issues ?? []).length} issue(s).` +
+              (issueLines ? `\n${issueLines}` : ""),
             );
           }
 
           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)
-              ? "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);
+            const resolvedMode: ReviseMode = (mode as ReviseMode) ?? "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": {