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

feat(core): context-transform 检测旧书时注入升级提示

createBookContextTransform 在读 truth files 之前先用 isNewLayoutBook(bookDir)
判断当前书的架构稿是否已经是 Phase 5 段落式。如果还是旧的条目式格式,
把一段 UPGRADE_HINT 拼到注入的 user message body 前面,提示 LLM 可以调
sub_agent(architect, { revise: true, ... }) 做一次升级——同时强调不要在
作者没明确同意前主动触发。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 6 месяцев назад
Родитель
Сommit
4a8c2cc60d

+ 26 - 0
packages/core/src/__tests__/context-transform.test.ts

@@ -79,4 +79,30 @@ describe("createBookContextTransform", () => {
     const result = await transform(original);
     const result = await transform(original);
     expect(result).toBe(original);
     expect(result).toBe(original);
   });
   });
+
+  it("injects upgrade hint when book is legacy layout (no outline/story_frame.md)", async () => {
+    const transform = createBookContextTransform(bookId, projectRoot);
+    const result = await transform([
+      { role: "user" as const, content: "写下一章", timestamp: Date.now() },
+    ]);
+
+    const injected = result[0] as { role: string; content: string };
+    expect(injected.content).toContain("旧的条目式格式");
+    expect(injected.content).toContain("sub_agent(architect, { revise: true");
+  });
+
+  it("does NOT inject upgrade hint when book is Phase 5 layout", async () => {
+    const outlineDir = join(projectRoot, "books", bookId, "story", "outline");
+    await mkdir(outlineDir, { recursive: true });
+    await writeFile(join(outlineDir, "story_frame.md"), "## 主题\n段落式内容");
+
+    const transform = createBookContextTransform(bookId, projectRoot);
+    const result = await transform([
+      { role: "user" as const, content: "写下一章", timestamp: Date.now() },
+    ]);
+
+    const injected = result[0] as { role: string; content: string };
+    expect(injected.content).not.toContain("旧的条目式格式");
+    expect(injected.content).not.toContain("revise: true");
+  });
 });
 });

+ 14 - 2
packages/core/src/agent/context-transform.ts

@@ -2,6 +2,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
 import type { UserMessage } from "@mariozechner/pi-ai";
 import type { UserMessage } from "@mariozechner/pi-ai";
 import { readdir, readFile } from "node:fs/promises";
 import { readdir, readFile } from "node:fs/promises";
 import { join } from "node:path";
 import { join } from "node:path";
+import { isNewLayoutBook } from "../utils/outline-paths.js";
 
 
 /** Files read in this order; anything else in story/ comes after, sorted alphabetically. */
 /** Files read in this order; anything else in story/ comes after, sorted alphabetically. */
 const PRIORITY_FILES = [
 const PRIORITY_FILES = [
@@ -11,6 +12,12 @@ const PRIORITY_FILES = [
   "current_focus.md",
   "current_focus.md",
 ];
 ];
 
 
+const UPGRADE_HINT =
+  "[提示] 当前这本书的架构稿是旧的条目式格式(story_bible.md / volume_outline.md / character_matrix.md)。" +
+  "如果作者有意愿升级成段落式架构稿 + 一人一卡的角色目录(outline/story_frame.md + outline/volume_map.md + roles/)," +
+  "可以调用 `sub_agent(architect, { revise: true, bookId, feedback: \"把架构稿从条目式升级成段落式架构稿,并把角色矩阵拆成 roles 目录一人一卡\" })`。" +
+  "升级只改架构稿,不动已写的章节。在作者没明确同意前不要主动触发。";
+
 export function createBookContextTransform(
 export function createBookContextTransform(
   bookId: string | null,
   bookId: string | null,
   projectRoot: string,
   projectRoot: string,
@@ -19,14 +26,19 @@ export function createBookContextTransform(
     return async (messages) => messages;
     return async (messages) => messages;
   }
   }
 
 
-  const storyDir = join(projectRoot, "books", bookId, "story");
+  const bookDir = join(projectRoot, "books", bookId);
+  const storyDir = join(bookDir, "story");
 
 
   return async (messages) => {
   return async (messages) => {
     const sections = await readTruthFiles(storyDir);
     const sections = await readTruthFiles(storyDir);
     if (sections.length === 0) return messages;
     if (sections.length === 0) return messages;
 
 
+    const isNew = await isNewLayoutBook(bookDir);
+    const hintBlock = isNew ? "" : `\n\n${UPGRADE_HINT}`;
+
     const body =
     const body =
-      "[以下是当前书籍的真相文件,每次对话时自动从磁盘读取注入。请基于这些内容进行创作和判断。]\n\n" +
+      "[以下是当前书籍的真相文件,每次对话时自动从磁盘读取注入。请基于这些内容进行创作和判断。]" +
+      hintBlock + "\n\n" +
       sections.map((s) => `=== ${s.name} ===\n${s.content}`).join("\n\n");
       sections.map((s) => `=== ${s.name} ===\n${s.content}`).join("\n\n");
 
 
     const injected: UserMessage = {
     const injected: UserMessage = {