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

feat(interaction): support truth-file edit actions

Ma 7 месяцев назад
Родитель
Сommit
12740d2d7d

+ 26 - 0
packages/cli/src/__tests__/tui-control-flow.test.ts

@@ -29,6 +29,7 @@ describe("tui control flow", () => {
       reviseDraft: vi.fn(async () => ({ ok: true })),
       updateCurrentFocus: vi.fn(async () => ({ ok: true })),
       updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
     };
 
     const result = await processTuiInput(projectRoot, "continue", tools);
@@ -51,6 +52,7 @@ describe("tui control flow", () => {
       reviseDraft: vi.fn(async () => ({ ok: true })),
       updateCurrentFocus: vi.fn(async () => ({ ok: true })),
       updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
     };
 
     await processTuiInput(projectRoot, "把 focus 拉回旧案线", tools);
@@ -69,10 +71,34 @@ describe("tui control flow", () => {
       reviseDraft: vi.fn(async () => ({ ok: true })),
       updateCurrentFocus: vi.fn(async () => ({ ok: true })),
       updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
     };
 
     const result = await processTuiInput(projectRoot, "切换到全自动", tools);
 
     expect(result.session.automationMode).toBe("auto");
   });
+
+  it("routes truth-file edits through the shared control flow", async () => {
+    await persistProjectSession(projectRoot, {
+      ...createProjectSession(projectRoot),
+      activeBookId: "harbor",
+    });
+
+    const tools = {
+      writeNextChapter: vi.fn(async () => ({ ok: true })),
+      reviseDraft: vi.fn(async () => ({ ok: true })),
+      updateCurrentFocus: vi.fn(async () => ({ ok: true })),
+      updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
+    };
+
+    await processTuiInput(projectRoot, "/truth current_focus.md Bring it back", tools);
+
+    expect(tools.writeTruthFile).toHaveBeenCalledWith(
+      "harbor",
+      "current_focus.md",
+      "Bring it back",
+    );
+  });
 });

+ 6 - 0
packages/core/src/__tests__/interaction-nl-router.test.ts

@@ -92,5 +92,11 @@ describe("interaction natural-language router", () => {
       bookId: "harbor",
       instruction: "bring it back to the old case",
     });
+    expect(routeNaturalLanguageIntent("/truth current_focus.md Bring it back", { activeBookId: "harbor" })).toEqual({
+      intent: "edit_truth",
+      bookId: "harbor",
+      fileName: "current_focus.md",
+      instruction: "Bring it back",
+    });
   });
 });

+ 43 - 1
packages/core/src/__tests__/interaction-runtime.test.ts

@@ -14,6 +14,7 @@ describe("interaction runtime", () => {
     const reviseDraft = vi.fn();
     const updateCurrentFocus = vi.fn();
     const updateAuthorIntent = vi.fn();
+    const writeTruthFile = vi.fn();
 
     const session = InteractionSessionSchema.parse({
       sessionId: "session-1",
@@ -26,7 +27,7 @@ describe("interaction runtime", () => {
     const result = await runInteractionRequest({
       session,
       request: { intent: "write_next", bookId: "harbor" },
-      tools: { writeNextChapter, reviseDraft, updateCurrentFocus, updateAuthorIntent },
+      tools: { writeNextChapter, reviseDraft, updateCurrentFocus, updateAuthorIntent, writeTruthFile },
     });
 
     expect(writeNextChapter).toHaveBeenCalledWith("harbor");
@@ -61,6 +62,7 @@ describe("interaction runtime", () => {
         reviseDraft,
         updateCurrentFocus: vi.fn(),
         updateAuthorIntent: vi.fn(),
+        writeTruthFile: vi.fn(),
       },
     });
 
@@ -90,6 +92,7 @@ describe("interaction runtime", () => {
         reviseDraft,
         updateCurrentFocus: vi.fn(),
         updateAuthorIntent: vi.fn(),
+        writeTruthFile: vi.fn(),
       },
     });
 
@@ -117,6 +120,7 @@ describe("interaction runtime", () => {
         reviseDraft: vi.fn(),
         updateCurrentFocus,
         updateAuthorIntent: vi.fn(),
+        writeTruthFile: vi.fn(),
       },
     });
 
@@ -126,11 +130,46 @@ describe("interaction runtime", () => {
     );
   });
 
+  it("routes edit_truth to the truth-file updater", async () => {
+    const writeTruthFile = vi.fn(async () => ({ ok: true }));
+
+    await runInteractionRequest({
+      session: InteractionSessionSchema.parse({
+        sessionId: "session-4b",
+        projectRoot: "/tmp/project",
+        activeBookId: "harbor",
+        automationMode: "semi",
+        messages: [],
+        events: [],
+      }),
+      request: {
+        intent: "edit_truth",
+        bookId: "harbor",
+        fileName: "current_focus.md",
+        instruction: "# Current Focus\n\nBring the story back to the old harbor debt line.\n",
+      },
+      tools: {
+        writeNextChapter: vi.fn(),
+        reviseDraft: vi.fn(),
+        updateCurrentFocus: vi.fn(),
+        updateAuthorIntent: vi.fn(),
+        writeTruthFile,
+      },
+    });
+
+    expect(writeTruthFile).toHaveBeenCalledWith(
+      "harbor",
+      "current_focus.md",
+      "# Current Focus\n\nBring the story back to the old harbor debt line.\n",
+    );
+  });
+
   it("updates automation mode without invoking pipeline tools", async () => {
     const writeNextChapter = vi.fn();
     const reviseDraft = vi.fn();
     const updateCurrentFocus = vi.fn();
     const updateAuthorIntent = vi.fn();
+    const writeTruthFile = vi.fn();
 
     const result = await runInteractionRequest({
       session: InteractionSessionSchema.parse({
@@ -146,6 +185,7 @@ describe("interaction runtime", () => {
         reviseDraft,
         updateCurrentFocus,
         updateAuthorIntent,
+        writeTruthFile,
       },
     });
 
@@ -172,6 +212,7 @@ describe("interaction runtime", () => {
         reviseDraft: vi.fn(),
         updateCurrentFocus: vi.fn(),
         updateAuthorIntent: vi.fn(),
+        writeTruthFile: vi.fn(),
       },
     });
 
@@ -202,6 +243,7 @@ describe("interaction runtime", () => {
         reviseDraft: vi.fn(),
         updateCurrentFocus: vi.fn(),
         updateAuthorIntent: vi.fn(),
+        writeTruthFile: vi.fn(),
       },
     });
 

+ 2 - 0
packages/core/src/__tests__/project-interaction.test.ts

@@ -32,6 +32,7 @@ describe("project interaction control", () => {
       reviseDraft: vi.fn(async () => ({ ok: true })),
       updateCurrentFocus: vi.fn(async () => ({ ok: true })),
       updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
     };
 
     const result = await processProjectInteractionInput({
@@ -59,6 +60,7 @@ describe("project interaction control", () => {
         reviseDraft: vi.fn(async () => ({ ok: true })),
         updateCurrentFocus: vi.fn(async () => ({ ok: true })),
         updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+        writeTruthFile: vi.fn(async () => ({ ok: true })),
       },
     });
 

+ 1 - 0
packages/core/src/interaction/intents.ts

@@ -25,6 +25,7 @@ export const InteractionRequestSchema = z.object({
   intent: InteractionIntentTypeSchema,
   bookId: z.string().min(1).optional(),
   chapterNumber: z.number().int().min(1).optional(),
+  fileName: z.string().min(1).optional(),
   instruction: z.string().min(1).optional(),
   mode: AutomationModeSchema.optional(),
 });

+ 10 - 0
packages/core/src/interaction/nl-router.ts

@@ -80,6 +80,16 @@ export function routeNaturalLanguageIntent(
     };
   }
 
+  const slashTruth = trimmed.match(/^\/truth\s+([^\s]+)\s+([\s\S]+)$/i);
+  if (slashTruth) {
+    return {
+      intent: "edit_truth",
+      ...(bookId ? { bookId } : {}),
+      fileName: slashTruth[1]!.trim(),
+      instruction: slashTruth[2]!.trim(),
+    };
+  }
+
   const rewriteMatch = trimmed.match(/(?:rewrite chapter|重写第)\s*(\d+)\s*(?:章)?/i);
   if (rewriteMatch) {
     return {

+ 4 - 0
packages/core/src/interaction/project-tools.ts

@@ -21,5 +21,9 @@ export function createInteractionToolsFromDeps(
       await state.ensureControlDocuments(bookId);
       await writeFile(join(state.bookDir(bookId), "story", "author_intent.md"), content, "utf-8");
     },
+    writeTruthFile: async (bookId, fileName, content) => {
+      await state.ensureControlDocuments(bookId);
+      await writeFile(join(state.bookDir(bookId), "story", fileName), content, "utf-8");
+    },
   };
 }

+ 17 - 0
packages/core/src/interaction/runtime.ts

@@ -11,6 +11,7 @@ export interface InteractionRuntimeTools {
   readonly reviseDraft: (bookId: string, chapterNumber: number, mode: ReviseMode) => Promise<unknown>;
   readonly updateCurrentFocus: (bookId: string, content: string) => Promise<unknown>;
   readonly updateAuthorIntent: (bookId: string, content: string) => Promise<unknown>;
+  readonly writeTruthFile: (bookId: string, fileName: string, content: string) => Promise<unknown>;
 }
 
 export interface InteractionRuntimeResult {
@@ -118,6 +119,22 @@ export async function runInteractionRequest(params: {
         responseText: `Updated author intent for ${bookId}.`,
       };
     }
+    case "edit_truth": {
+      const bookId = request.bookId ?? session.activeBookId;
+      if (!bookId) {
+        throw new Error("No active book is bound to the interaction session.");
+      }
+      if (!request.fileName || !request.instruction) {
+        throw new Error("Truth-file edit requires a file name and content.");
+      }
+      await params.tools.writeTruthFile(bookId, request.fileName, request.instruction);
+      session = bindActiveBook(session, bookId);
+      const completed = markCompleted(session);
+      return {
+        session: addEvent(completed, "task.completed", "completed", `Updated ${request.fileName} for ${bookId}.`),
+        responseText: `Updated ${request.fileName} for ${bookId}.`,
+      };
+    }
     case "switch_mode":
       session = markCompleted(session);
       return {