Quellcode durchsuchen

feat(interaction): carry foundation drafts into book creation

Ma vor 7 Monaten
Ursprung
Commit
378acafb7a

+ 4 - 1
packages/cli/src/__tests__/tui-slash-autocomplete.test.ts

@@ -11,7 +11,8 @@ describe("tui slash autocomplete", () => {
     expect(getSlashSuggestions("/st", SLASH_COMMANDS)).toEqual(["/status"]);
     expect(getSlashSuggestions("/st", SLASH_COMMANDS)).toEqual(["/status"]);
     expect(getSlashSuggestions("/w", SLASH_COMMANDS)).toEqual(["/write"]);
     expect(getSlashSuggestions("/w", SLASH_COMMANDS)).toEqual(["/write"]);
     expect(getSlashSuggestions("/o", SLASH_COMMANDS)).toEqual(["/open <book>"]);
     expect(getSlashSuggestions("/o", SLASH_COMMANDS)).toEqual(["/open <book>"]);
-    expect(getSlashSuggestions("/d", SLASH_COMMANDS)).toEqual(["/depth <light|normal|deep>"]);
+    expect(getSlashSuggestions("/d", SLASH_COMMANDS)).toEqual(["/draft", "/discard", "/depth <light|normal|deep>"]);
+    expect(getSlashSuggestions("/cr", SLASH_COMMANDS)).toEqual(["/create"]);
   });
   });
 
 
   it("does not suggest anything for non-slash input", () => {
   it("does not suggest anything for non-slash input", () => {
@@ -29,5 +30,7 @@ describe("tui slash autocomplete", () => {
     expect(applySlashSuggestion("/st", ["/status"], 0)).toBe("/status");
     expect(applySlashSuggestion("/st", ["/status"], 0)).toBe("/status");
     expect(applySlashSuggestion("/o", ["/open <book>"], 0)).toBe("/open ");
     expect(applySlashSuggestion("/o", ["/open <book>"], 0)).toBe("/open ");
     expect(applySlashSuggestion("/d", ["/depth <light|normal|deep>"], 0)).toBe("/depth ");
     expect(applySlashSuggestion("/d", ["/depth <light|normal|deep>"], 0)).toBe("/depth ");
+    expect(applySlashSuggestion("/dr", ["/draft"], 0)).toBe("/draft");
+    expect(applySlashSuggestion("/cr", ["/create"], 0)).toBe("/create");
   });
   });
 });
 });

+ 4 - 4
packages/cli/src/tui/i18n.ts

@@ -85,13 +85,13 @@ const ZH_CN: TuiCopy = {
   composer: {
   composer: {
     placeholder: "告诉 InkOS 要写什么、修改什么,或解释什么…",
     placeholder: "告诉 InkOS 要写什么、修改什么,或解释什么…",
     emptyConversation: "先告诉 InkOS 你要做什么。",
     emptyConversation: "先告诉 InkOS 你要做什么。",
-    helper: "回车发送 • /write • /books • /open • /mode • /depth • /help",
+    helper: "回车发送 • /new • /draft • /create • /write • /books • /open • /mode • /depth • /help",
     submitting: "处理中…",
     submitting: "处理中…",
     failed: "上次请求失败",
     failed: "上次请求失败",
     ready: "就绪",
     ready: "就绪",
   },
   },
   notes: {
   notes: {
-    help: "可用命令:/write、/books、/open、/mode、/rewrite、/focus、/truth、/rename、/replace、/export、/status、/clear、/depth、/quit。也支持直接输入自然语言。",
+    help: "可用命令:/new、/draft、/create、/discard、/write、/books、/open、/mode、/rewrite、/focus、/truth、/rename、/replace、/export、/status、/clear、/depth、/quit。也支持直接输入自然语言。",
     status: (stage, mode) => `当前状态:${stage}(${mode})。`,
     status: (stage, mode) => `当前状态:${stage}(${mode})。`,
     config: "当前 Ink 仪表盘里还不支持交互式 /config。请使用 inkos config set-global。",
     config: "当前 Ink 仪表盘里还不支持交互式 /config。请使用 inkos config set-global。",
     depthSet: (depthLabel) => `思考深度已切换为 ${depthLabel}。`,
     depthSet: (depthLabel) => `思考深度已切换为 ${depthLabel}。`,
@@ -165,13 +165,13 @@ const EN: TuiCopy = {
   composer: {
   composer: {
     placeholder: "Ask InkOS to write, revise, or explain…",
     placeholder: "Ask InkOS to write, revise, or explain…",
     emptyConversation: "Start by asking InkOS what to do.",
     emptyConversation: "Start by asking InkOS what to do.",
-    helper: "Enter to send • /write • /books • /open • /mode • /depth • /help",
+    helper: "Enter to send • /new • /draft • /create • /write • /books • /open • /mode • /depth • /help",
     submitting: "Submitting…",
     submitting: "Submitting…",
     failed: "Last request failed",
     failed: "Last request failed",
     ready: "Ready",
     ready: "Ready",
   },
   },
   notes: {
   notes: {
-    help: "Commands: /write, /books, /open, /mode, /rewrite, /focus, /truth, /rename, /replace, /export, /status, /clear, /depth, /quit. Natural language still works.",
+    help: "Commands: /new, /draft, /create, /discard, /write, /books, /open, /mode, /rewrite, /focus, /truth, /rename, /replace, /export, /status, /clear, /depth, /quit. Natural language still works.",
     status: (stage, mode) => `Status: ${stage} (${mode}).`,
     status: (stage, mode) => `Status: ${stage} (${mode}).`,
     config: "Interactive /config is not available inside the Ink dashboard yet. Use inkos config set-global.",
     config: "Interactive /config is not available inside the Ink dashboard yet. Use inkos config set-global.",
     depthSet: (depthLabel) => `Thinking depth set to ${depthLabel}.`,
     depthSet: (depthLabel) => `Thinking depth set to ${depthLabel}.`,

+ 4 - 0
packages/cli/src/tui/slash-autocomplete.ts

@@ -1,4 +1,8 @@
 export const SLASH_COMMANDS = [
 export const SLASH_COMMANDS = [
+  "/new <idea>",
+  "/draft",
+  "/create",
+  "/discard",
   "/write",
   "/write",
   "/books",
   "/books",
   "/open <book>",
   "/open <book>",

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

@@ -135,6 +135,9 @@ describe("interaction natural-language router", () => {
     expect(routeNaturalLanguageIntent("/create", { hasCreationDraft: true })).toEqual({
     expect(routeNaturalLanguageIntent("/create", { hasCreationDraft: true })).toEqual({
       intent: "create_book",
       intent: "create_book",
     });
     });
+    expect(routeNaturalLanguageIntent("/draft", { hasCreationDraft: true })).toEqual({
+      intent: "show_book_draft",
+    });
     expect(routeNaturalLanguageIntent("/discard", { hasCreationDraft: true })).toEqual({
     expect(routeNaturalLanguageIntent("/discard", { hasCreationDraft: true })).toEqual({
       intent: "discard_book_draft",
       intent: "discard_book_draft",
     });
     });

+ 31 - 0
packages/core/src/__tests__/interaction-runtime.test.ts

@@ -134,6 +134,37 @@ describe("interaction runtime", () => {
     expect(result.responseText).toContain("已丢弃");
     expect(result.responseText).toContain("已丢弃");
   });
   });
 
 
+  it("renders the current creation draft when show_book_draft is requested", async () => {
+    const result = await runInteractionRequest({
+      session: InteractionSessionSchema.parse({
+        sessionId: "session-show-draft",
+        projectRoot: "/tmp/project",
+        automationMode: "semi",
+        creationDraft: {
+          concept: "港风商战悬疑,主角从灰产洗白。",
+          title: "夜港账本",
+          genre: "urban",
+          worldPremise: "近未来港口城,账本牵出多方势力。",
+          protagonist: "林砚,水货账房出身,擅长记账和看人。",
+          conflictCore: "洗白与旧债回潮的对撞。",
+          nextQuestion: "卷一先查账还是先砸场?",
+          missingFields: ["targetChapters"],
+          readyToCreate: false,
+        },
+        messages: [],
+        events: [],
+      }),
+      request: {
+        intent: "show_book_draft",
+      },
+      tools: makeTools(),
+    });
+
+    expect(result.responseText).toContain("夜港账本");
+    expect(result.responseText).toContain("近未来港口城");
+    expect(result.responseText).toContain("卷一先查账还是先砸场");
+  });
+
   it("routes export_book through the shared export tool", async () => {
   it("routes export_book through the shared export tool", async () => {
     const exportBook = vi.fn(async () => ({
     const exportBook = vi.fn(async () => ({
       outputPath: "/tmp/project/exports/harbor.md",
       outputPath: "/tmp/project/exports/harbor.md",

+ 70 - 0
packages/core/src/__tests__/interaction-tools.test.ts

@@ -180,4 +180,74 @@ describe("interaction tools", () => {
     await expect(readFile(join(projectRoot, "books", "harbor", "story", "author_intent.md"), "utf-8"))
     await expect(readFile(join(projectRoot, "books", "harbor", "story", "author_intent.md"), "utf-8"))
       .resolves.toContain("harbor mystery");
       .resolves.toContain("harbor mystery");
   });
   });
+
+  it("forwards foundation draft fields into shared book creation", async () => {
+    const pipeline = {
+      initBook: vi.fn(async () => undefined),
+      writeNextChapter: vi.fn(async () => ({
+        chapterNumber: 1,
+        title: "Draft",
+        wordCount: 1000,
+        revised: false,
+        status: "ready-for-review" as const,
+        auditResult: { passed: true, issues: [], summary: "ok" },
+      })),
+      reviseDraft: vi.fn(async () => ({
+        chapterNumber: 3,
+        wordCount: 1200,
+        fixedIssues: [],
+        applied: true,
+        status: "ready-for-review" as const,
+      })),
+    };
+    const state = {
+      ensureControlDocuments: vi.fn(async () => {}),
+      bookDir: vi.fn((bookId: string) => join(projectRoot, "books", bookId)),
+      loadBookConfig: vi.fn(async () => ({
+        id: "night-harbor",
+        title: "Night Harbor",
+        platform: "other" as const,
+        genre: "urban",
+        status: "outlining" as const,
+        targetChapters: 120,
+        chapterWordCount: 2800,
+        createdAt: "2026-04-10T00:00:00.000Z",
+        updatedAt: "2026-04-10T00:00:00.000Z",
+      })),
+      loadChapterIndex: vi.fn(async () => []),
+      saveChapterIndex: vi.fn(async () => undefined),
+      listBooks: vi.fn(async () => []),
+    };
+
+    const tools = createInteractionToolsFromDeps(pipeline, state);
+    await tools.createBook?.({
+      title: "Night Harbor",
+      genre: "urban",
+      platform: "tomato",
+      chapterWordCount: 2800,
+      targetChapters: 120,
+      blurb: "一个做灰产生意的人,准备在夜港洗白,却先被旧账拖回去。",
+      worldPremise: "近未来架空香港,港口账本牵出多方势力。",
+      protagonist: "林砚,水货账房出身,聪明克制,不轻易信人。",
+      conflictCore: "洗白与旧债回潮的对撞。",
+      volumeOutline: "卷一先查账,再暴露港口旧案。",
+      authorIntent: "# 作者意图\n\n写成冷硬、克制、利益驱动的商战悬疑。\n",
+      currentFocus: "# 当前聚焦\n\n先把旧账线和港口势力网立住。\n",
+    });
+
+    expect(pipeline.initBook).toHaveBeenCalledWith(
+      expect.objectContaining({
+        title: "Night Harbor",
+        genre: "urban",
+        platform: "tomato",
+        targetChapters: 120,
+        chapterWordCount: 2800,
+      }),
+      expect.objectContaining({
+        externalContext: expect.stringContaining("近未来架空香港"),
+        authorIntent: expect.stringContaining("冷硬、克制"),
+        currentFocus: expect.stringContaining("旧账线"),
+      }),
+    );
+  });
 });
 });

+ 61 - 0
packages/core/src/__tests__/pipeline-runner.test.ts

@@ -348,6 +348,67 @@ describe("PipelineRunner", () => {
     }
     }
   });
   });
 
 
+  it("applies creation-draft overrides while initializing a book", async () => {
+    const root = await mkdtemp(join(tmpdir(), "inkos-init-book-overrides-"));
+    const bookId = "override-book";
+    const book: BookConfig = {
+      id: bookId,
+      title: "Override Book",
+      platform: "tomato",
+      genre: "xuanhuan",
+      status: "outlining",
+      targetChapters: 20,
+      chapterWordCount: 2800,
+      createdAt: "2026-04-13T00:00:00.000Z",
+      updatedAt: "2026-04-13T00:00:00.000Z",
+    };
+
+    const runner = new PipelineRunner({
+      client: {
+        provider: "openai",
+        apiFormat: "chat",
+        stream: false,
+        defaults: {
+          temperature: 0.7,
+          maxTokens: 4096,
+          thinkingBudget: 0, maxTokensCap: null,
+        },
+      } as ConstructorParameters<typeof PipelineRunner>[0]["client"],
+      model: "test-model",
+      projectRoot: root,
+    });
+
+    const generateFoundationSpy = vi.spyOn(ArchitectAgent.prototype, "generateFoundation").mockResolvedValue({
+      storyBible: "# Story Bible\n",
+      volumeOutline: "# Volume Outline\n",
+      bookRules: "---\nversion: \"1.0\"\n---\n\n# Book Rules\n",
+      currentState: "# Current State\n",
+      pendingHooks: "# Pending Hooks\n",
+    });
+
+    try {
+      await runner.initBook(book, {
+        externalContext: "世界观重点:近未来港口城,账本与旧案牵出多方势力。",
+        authorIntent: "# 作者意图\n\n写成冷硬、克制、利益驱动的商战悬疑。\n",
+        currentFocus: "# 当前聚焦\n\n先把旧账线和港口势力网立住。\n",
+      });
+
+      expect(generateFoundationSpy).toHaveBeenCalledWith(
+        book,
+        expect.stringContaining("近未来港口城"),
+        undefined,
+      );
+
+      const storyDir = join(root, "books", bookId, "story");
+      await expect(readFile(join(storyDir, "author_intent.md"), "utf-8"))
+        .resolves.toContain("冷硬、克制、利益驱动");
+      await expect(readFile(join(storyDir, "current_focus.md"), "utf-8"))
+        .resolves.toContain("旧账线和港口势力网");
+    } finally {
+      await rm(root, { recursive: true, force: true });
+    }
+  });
+
   it("feeds foundation review feedback into the regeneration call after a rejection", async () => {
   it("feeds foundation review feedback into the regeneration call after a rejection", async () => {
     const { root, runner, bookId } = await createRunnerFixture();
     const { root, runner, bookId } = await createRunnerFixture();
     const reviewer = new FoundationReviewerAgent({
     const reviewer = new FoundationReviewerAgent({

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

@@ -3,6 +3,7 @@ import { AutomationModeSchema } from "./modes.js";
 
 
 export const InteractionIntentTypeSchema = z.enum([
 export const InteractionIntentTypeSchema = z.enum([
   "develop_book",
   "develop_book",
+  "show_book_draft",
   "create_book",
   "create_book",
   "discard_book_draft",
   "discard_book_draft",
   "list_books",
   "list_books",
@@ -37,6 +38,16 @@ export const InteractionRequestSchema = z.object({
   language: z.enum(["zh", "en"]).optional(),
   language: z.enum(["zh", "en"]).optional(),
   chapterWordCount: z.number().int().min(1).optional(),
   chapterWordCount: z.number().int().min(1).optional(),
   targetChapters: z.number().int().min(1).optional(),
   targetChapters: z.number().int().min(1).optional(),
+  blurb: z.string().min(1).optional(),
+  worldPremise: z.string().min(1).optional(),
+  settingNotes: z.string().min(1).optional(),
+  protagonist: z.string().min(1).optional(),
+  supportingCast: z.string().min(1).optional(),
+  conflictCore: z.string().min(1).optional(),
+  volumeOutline: z.string().min(1).optional(),
+  constraints: z.string().min(1).optional(),
+  authorIntent: z.string().min(1).optional(),
+  currentFocus: z.string().min(1).optional(),
   fileName: z.string().min(1).optional(),
   fileName: z.string().min(1).optional(),
   format: z.enum(["txt", "md", "epub"]).optional(),
   format: z.enum(["txt", "md", "epub"]).optional(),
   approvedOnly: z.boolean().optional(),
   approvedOnly: z.boolean().optional(),

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

@@ -55,6 +55,12 @@ export function routeNaturalLanguageIntent(
     };
     };
   }
   }
 
 
+  if (/^\/draft$/i.test(trimmed)) {
+    return {
+      intent: "show_book_draft",
+    };
+  }
+
   if (/^\/discard$/i.test(trimmed)) {
   if (/^\/discard$/i.test(trimmed)) {
     return {
     return {
       intent: "discard_book_draft",
       intent: "discard_book_draft",

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

@@ -16,7 +16,14 @@ import type { InteractionRuntimeTools } from "./runtime.js";
 import type { BookCreationDraft } from "./session.js";
 import type { BookCreationDraft } from "./session.js";
 
 
 type PipelineLike = Pick<PipelineRunner, "writeNextChapter" | "reviseDraft"> & {
 type PipelineLike = Pick<PipelineRunner, "writeNextChapter" | "reviseDraft"> & {
-  readonly initBook?: PipelineRunner["initBook"];
+  readonly initBook?: (
+    book: BookConfig,
+    options?: {
+      readonly externalContext?: string;
+      readonly authorIntent?: string;
+      readonly currentFocus?: string;
+    },
+  ) => Promise<void>;
 };
 };
 type StateLike = Pick<StateManager, "ensureControlDocuments" | "bookDir" | "loadBookConfig" | "loadChapterIndex" | "saveChapterIndex" | "listBooks">;
 type StateLike = Pick<StateManager, "ensureControlDocuments" | "bookDir" | "loadBookConfig" | "loadChapterIndex" | "saveChapterIndex" | "listBooks">;
 type InstrumentablePipelineLike = PipelineLike & {
 type InstrumentablePipelineLike = PipelineLike & {
@@ -147,6 +154,34 @@ function buildBookConfig(input: {
   };
   };
 }
 }
 
 
+function buildCreationExternalContext(input: {
+  readonly blurb?: string;
+  readonly worldPremise?: string;
+  readonly settingNotes?: string;
+  readonly protagonist?: string;
+  readonly supportingCast?: string;
+  readonly conflictCore?: string;
+  readonly volumeOutline?: string;
+  readonly constraints?: string;
+}): string | undefined {
+  const sections = [
+    input.worldPremise ? `## 世界观与核心设定\n${input.worldPremise}` : undefined,
+    input.settingNotes ? `## 补充设定\n${input.settingNotes}` : undefined,
+    input.protagonist ? `## 主角设定\n${input.protagonist}` : undefined,
+    input.supportingCast ? `## 关键角色与势力\n${input.supportingCast}` : undefined,
+    input.conflictCore ? `## 核心冲突\n${input.conflictCore}` : undefined,
+    input.volumeOutline ? `## 卷纲方向\n${input.volumeOutline}` : undefined,
+    input.blurb ? `## 简介卖点\n${input.blurb}` : undefined,
+    input.constraints ? `## 创作约束\n${input.constraints}` : undefined,
+  ].filter((section): section is string => Boolean(section?.trim()));
+
+  if (sections.length === 0) {
+    return undefined;
+  }
+
+  return sections.join("\n\n");
+}
+
 async function exportBookToPath(state: StateLike, bookId: string, options: {
 async function exportBookToPath(state: StateLike, bookId: string, options: {
   readonly format?: "txt" | "md" | "epub";
   readonly format?: "txt" | "md" | "epub";
   readonly approvedOnly?: boolean;
   readonly approvedOnly?: boolean;
@@ -445,8 +480,9 @@ export function createInteractionToolsFromDeps(
               "Ask at most one sharp next question.",
               "Ask at most one sharp next question.",
               "Default to concise Chinese unless the draft language is clearly English.",
               "Default to concise Chinese unless the draft language is clearly English.",
               "Return JSON only with keys assistantReply and draft.",
               "Return JSON only with keys assistantReply and draft.",
-              "draft must include concept and may include title, genre, platform, language, targetChapters, chapterWordCount, blurb, authorIntent, currentFocus, nextQuestion, missingFields, readyToCreate.",
-              "Be conservative: only mark readyToCreate=true when the draft already has a workable title, genre, targetChapters, and chapterWordCount.",
+              "draft must include concept and may include title, genre, platform, language, targetChapters, chapterWordCount, blurb, worldPremise, settingNotes, protagonist, supportingCast, conflictCore, volumeOutline, constraints, authorIntent, currentFocus, nextQuestion, missingFields, readyToCreate.",
+              "Help the user decide and revise worldview, setting, protagonist, supporting cast, core conflict, blurb, and volume direction.",
+              "Be conservative: only mark readyToCreate=true when the draft already has a workable title, genre, targetChapters, chapterWordCount, and enough setting/conflict detail to generate a foundation.",
             ].join(" "),
             ].join(" "),
           },
           },
           {
           {
@@ -479,7 +515,11 @@ export function createInteractionToolsFromDeps(
       if (!pipeline.initBook) {
       if (!pipeline.initBook) {
         throw new Error("Pipeline does not support shared book creation.");
         throw new Error("Pipeline does not support shared book creation.");
       }
       }
-      await pipeline.initBook(book);
+      await pipeline.initBook(book, {
+        externalContext: buildCreationExternalContext(input),
+        authorIntent: input.authorIntent,
+        currentFocus: input.currentFocus,
+      });
       return {
       return {
         bookId: book.id,
         bookId: book.id,
         title: book.title,
         title: book.title,

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

@@ -28,6 +28,16 @@ export interface InteractionRuntimeTools {
     readonly language?: "zh" | "en";
     readonly language?: "zh" | "en";
     readonly chapterWordCount?: number;
     readonly chapterWordCount?: number;
     readonly targetChapters?: number;
     readonly targetChapters?: number;
+    readonly blurb?: string;
+    readonly worldPremise?: string;
+    readonly settingNotes?: string;
+    readonly protagonist?: string;
+    readonly supportingCast?: string;
+    readonly conflictCore?: string;
+    readonly volumeOutline?: string;
+    readonly constraints?: string;
+    readonly authorIntent?: string;
+    readonly currentFocus?: string;
   }) => Promise<unknown>;
   }) => Promise<unknown>;
   readonly exportBook?: (bookId: string, options: {
   readonly exportBook?: (bookId: string, options: {
     readonly format?: "txt" | "md" | "epub";
     readonly format?: "txt" | "md" | "epub";
@@ -118,6 +128,38 @@ function localizeMode(mode: AutomationMode, language: RuntimeLanguage): string {
   }[mode] ?? mode;
   }[mode] ?? mode;
 }
 }
 
 
+function renderCreationDraft(
+  draft: NonNullable<InteractionSession["creationDraft"]>,
+  language: RuntimeLanguage,
+): string {
+  const lines = language === "en"
+    ? [
+        "# Current Book Draft",
+        draft.title ? `- Title: ${draft.title}` : undefined,
+        draft.genre ? `- Genre: ${draft.genre}` : undefined,
+        draft.platform ? `- Platform: ${draft.platform}` : undefined,
+        draft.worldPremise ? `- World: ${draft.worldPremise}` : undefined,
+        draft.protagonist ? `- Protagonist: ${draft.protagonist}` : undefined,
+        draft.conflictCore ? `- Core Conflict: ${draft.conflictCore}` : undefined,
+        draft.volumeOutline ? `- Volume Direction: ${draft.volumeOutline}` : undefined,
+        draft.blurb ? `- Blurb: ${draft.blurb}` : undefined,
+        draft.nextQuestion ? `- Next: ${draft.nextQuestion}` : undefined,
+      ]
+    : [
+        "# 当前创作草案",
+        draft.title ? `- 书名:${draft.title}` : undefined,
+        draft.genre ? `- 题材:${draft.genre}` : undefined,
+        draft.platform ? `- 平台:${draft.platform}` : undefined,
+        draft.worldPremise ? `- 世界观:${draft.worldPremise}` : undefined,
+        draft.protagonist ? `- 主角:${draft.protagonist}` : undefined,
+        draft.conflictCore ? `- 核心冲突:${draft.conflictCore}` : undefined,
+        draft.volumeOutline ? `- 卷纲方向:${draft.volumeOutline}` : undefined,
+        draft.blurb ? `- 简介:${draft.blurb}` : undefined,
+        draft.nextQuestion ? `- 下一步:${draft.nextQuestion}` : undefined,
+      ];
+  return lines.filter(Boolean).join("\n");
+}
+
 function buildTaskStartedState(
 function buildTaskStartedState(
   session: InteractionSession,
   session: InteractionSession,
   request: InteractionRequest,
   request: InteractionRequest,
@@ -384,6 +426,21 @@ export async function runInteractionRequest(params: {
         details: metadata.details,
         details: metadata.details,
       };
       };
     }
     }
+    case "show_book_draft": {
+      if (!session.creationDraft) {
+        return {
+          session: markCompleted(session),
+          responseText: localize(language, {
+            zh: "当前还没有创作草案。先告诉我你想写什么,再逐步把书收出来。",
+            en: "There is no active book draft yet. Start by telling me what you want to write.",
+          }),
+        };
+      }
+      return {
+        session: markCompleted(session),
+        responseText: renderCreationDraft(session.creationDraft, language),
+      };
+    }
     case "create_book": {
     case "create_book": {
       if (!params.tools.createBook) {
       if (!params.tools.createBook) {
         throw new Error(localize(language, {
         throw new Error(localize(language, {
@@ -406,6 +463,16 @@ export async function runInteractionRequest(params: {
         language: request.language ?? effectiveDraft?.language,
         language: request.language ?? effectiveDraft?.language,
         chapterWordCount: request.chapterWordCount ?? effectiveDraft?.chapterWordCount,
         chapterWordCount: request.chapterWordCount ?? effectiveDraft?.chapterWordCount,
         targetChapters: request.targetChapters ?? effectiveDraft?.targetChapters,
         targetChapters: request.targetChapters ?? effectiveDraft?.targetChapters,
+        blurb: request.blurb ?? effectiveDraft?.blurb,
+        worldPremise: request.worldPremise ?? effectiveDraft?.worldPremise,
+        settingNotes: request.settingNotes ?? effectiveDraft?.settingNotes,
+        protagonist: request.protagonist ?? effectiveDraft?.protagonist,
+        supportingCast: request.supportingCast ?? effectiveDraft?.supportingCast,
+        conflictCore: request.conflictCore ?? effectiveDraft?.conflictCore,
+        volumeOutline: request.volumeOutline ?? effectiveDraft?.volumeOutline,
+        constraints: request.constraints ?? effectiveDraft?.constraints,
+        authorIntent: request.authorIntent ?? effectiveDraft?.authorIntent,
+        currentFocus: request.currentFocus ?? effectiveDraft?.currentFocus,
       });
       });
       const metadata = extractToolMetadata(toolResult);
       const metadata = extractToolMetadata(toolResult);
       const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult
       const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult

+ 7 - 0
packages/core/src/interaction/session.ts

@@ -28,6 +28,13 @@ export const BookCreationDraftSchema = z.object({
   targetChapters: z.number().int().min(1).optional(),
   targetChapters: z.number().int().min(1).optional(),
   chapterWordCount: z.number().int().min(1).optional(),
   chapterWordCount: z.number().int().min(1).optional(),
   blurb: z.string().min(1).optional(),
   blurb: z.string().min(1).optional(),
+  worldPremise: z.string().min(1).optional(),
+  settingNotes: z.string().min(1).optional(),
+  protagonist: z.string().min(1).optional(),
+  supportingCast: z.string().min(1).optional(),
+  conflictCore: z.string().min(1).optional(),
+  volumeOutline: z.string().min(1).optional(),
+  constraints: z.string().min(1).optional(),
   authorIntent: z.string().min(1).optional(),
   authorIntent: z.string().min(1).optional(),
   currentFocus: z.string().min(1).optional(),
   currentFocus: z.string().min(1).optional(),
   nextQuestion: z.string().min(1).optional(),
   nextQuestion: z.string().min(1).optional(),

+ 16 - 3
packages/core/src/pipeline/runner.ts

@@ -171,6 +171,12 @@ export interface ImportChaptersResult {
   readonly nextChapter: number;
   readonly nextChapter: number;
 }
 }
 
 
+export interface InitBookOptions {
+  readonly externalContext?: string;
+  readonly authorIntent?: string;
+  readonly currentFocus?: string;
+}
+
 export class PipelineRunner {
 export class PipelineRunner {
   private readonly state: StateManager;
   private readonly state: StateManager;
   private readonly config: PipelineConfig;
   private readonly config: PipelineConfig;
@@ -436,7 +442,7 @@ export class PipelineRunner {
     return radar.scan();
     return radar.scan();
   }
   }
 
 
-  async initBook(book: BookConfig): Promise<void> {
+  async initBook(book: BookConfig, options: InitBookOptions = {}): Promise<void> {
     const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
     const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
     const bookDir = this.state.bookDir(book.id);
     const bookDir = this.state.bookDir(book.id);
     const stagingBookDir = join(
     const stagingBookDir = join(
@@ -452,7 +458,7 @@ export class PipelineRunner {
     const foundation = await this.generateAndReviewFoundation({
     const foundation = await this.generateAndReviewFoundation({
       generate: (reviewFeedback) => architect.generateFoundation(
       generate: (reviewFeedback) => architect.generateFoundation(
         book,
         book,
-        this.config.externalContext,
+        options.externalContext ?? this.config.externalContext,
         reviewFeedback,
         reviewFeedback,
       ),
       ),
       reviewer,
       reviewer,
@@ -476,8 +482,15 @@ export class PipelineRunner {
       await this.state.ensureControlDocumentsAt(
       await this.state.ensureControlDocumentsAt(
         stagingBookDir,
         stagingBookDir,
         book.language ?? gp.language,
         book.language ?? gp.language,
-        this.config.externalContext,
+        options.authorIntent ?? this.config.externalContext,
       );
       );
+      if (options.currentFocus?.trim()) {
+        await writeFile(
+          join(stagingBookDir, "story", "current_focus.md"),
+          options.currentFocus.trimEnd() + "\n",
+          "utf-8",
+        );
+      }
 
 
       await this.state.saveChapterIndexAt(stagingBookDir, []);
       await this.state.saveChapterIndexAt(stagingBookDir, []);