@actalk/inkos-core 1.6.3 → 1.7.0-canary.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/agent/agent-session.d.ts.map +1 -1
  2. package/dist/agent/agent-session.js +21 -11
  3. package/dist/agent/agent-session.js.map +1 -1
  4. package/dist/agent/agent-system-prompt.d.ts.map +1 -1
  5. package/dist/agent/agent-system-prompt.js +14 -8
  6. package/dist/agent/agent-system-prompt.js.map +1 -1
  7. package/dist/agent/agent-tools.d.ts +34 -2
  8. package/dist/agent/agent-tools.d.ts.map +1 -1
  9. package/dist/agent/agent-tools.js +236 -54
  10. package/dist/agent/agent-tools.js.map +1 -1
  11. package/dist/agent/chapter-import-source.d.ts +15 -0
  12. package/dist/agent/chapter-import-source.d.ts.map +1 -0
  13. package/dist/agent/chapter-import-source.js +41 -0
  14. package/dist/agent/chapter-import-source.js.map +1 -0
  15. package/dist/agent/film-authoring-tools.d.ts +5 -3
  16. package/dist/agent/film-authoring-tools.d.ts.map +1 -1
  17. package/dist/agent/film-authoring-tools.js +34 -19
  18. package/dist/agent/film-authoring-tools.js.map +1 -1
  19. package/dist/agent/index.d.ts +1 -1
  20. package/dist/agent/index.d.ts.map +1 -1
  21. package/dist/agent/index.js +1 -1
  22. package/dist/agent/index.js.map +1 -1
  23. package/dist/agents/base.d.ts +1 -0
  24. package/dist/agents/base.d.ts.map +1 -1
  25. package/dist/agents/base.js +2 -0
  26. package/dist/agents/base.js.map +1 -1
  27. package/dist/agents/script-storyboard.d.ts +3 -0
  28. package/dist/agents/script-storyboard.d.ts.map +1 -1
  29. package/dist/agents/script-storyboard.js +221 -14
  30. package/dist/agents/script-storyboard.js.map +1 -1
  31. package/dist/agents/short-fiction.d.ts +13 -2
  32. package/dist/agents/short-fiction.d.ts.map +1 -1
  33. package/dist/agents/short-fiction.js +77 -43
  34. package/dist/agents/short-fiction.js.map +1 -1
  35. package/dist/index.d.ts +8 -5
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +10 -8
  38. package/dist/index.js.map +1 -1
  39. package/dist/interaction/action-envelope.d.ts +53 -1
  40. package/dist/interaction/action-envelope.d.ts.map +1 -1
  41. package/dist/interaction/action-envelope.js +9 -0
  42. package/dist/interaction/action-envelope.js.map +1 -1
  43. package/dist/interaction/intents.d.ts +2 -2
  44. package/dist/interaction/project-control.d.ts +1 -1
  45. package/dist/interactive-film/generate.d.ts +1 -0
  46. package/dist/interactive-film/generate.d.ts.map +1 -1
  47. package/dist/interactive-film/generate.js +15 -5
  48. package/dist/interactive-film/generate.js.map +1 -1
  49. package/dist/llm/provider.d.ts +1 -0
  50. package/dist/llm/provider.d.ts.map +1 -1
  51. package/dist/llm/provider.js +64 -18
  52. package/dist/llm/provider.js.map +1 -1
  53. package/dist/llm/providers/endpoints/openrouter.d.ts.map +1 -1
  54. package/dist/llm/providers/endpoints/openrouter.js +3 -2
  55. package/dist/llm/providers/endpoints/openrouter.js.map +1 -1
  56. package/dist/llm/think-tag-stripper.d.ts +15 -0
  57. package/dist/llm/think-tag-stripper.d.ts.map +1 -0
  58. package/dist/llm/think-tag-stripper.js +60 -0
  59. package/dist/llm/think-tag-stripper.js.map +1 -0
  60. package/dist/materials/ingest.d.ts.map +1 -1
  61. package/dist/materials/ingest.js +4 -3
  62. package/dist/materials/ingest.js.map +1 -1
  63. package/dist/materials/retrieve.d.ts.map +1 -1
  64. package/dist/materials/retrieve.js +3 -1
  65. package/dist/materials/retrieve.js.map +1 -1
  66. package/dist/models/book.d.ts +28 -0
  67. package/dist/models/book.d.ts.map +1 -1
  68. package/dist/models/book.js +22 -0
  69. package/dist/models/book.js.map +1 -1
  70. package/dist/models/project.d.ts +40 -0
  71. package/dist/models/project.d.ts.map +1 -1
  72. package/dist/models/project.js +5 -0
  73. package/dist/models/project.js.map +1 -1
  74. package/dist/notify/dispatcher.d.ts.map +1 -1
  75. package/dist/notify/dispatcher.js +8 -5
  76. package/dist/notify/dispatcher.js.map +1 -1
  77. package/dist/notify/feishu.d.ts +2 -1
  78. package/dist/notify/feishu.d.ts.map +1 -1
  79. package/dist/notify/feishu.js +12 -6
  80. package/dist/notify/feishu.js.map +1 -1
  81. package/dist/notify/format.d.ts +12 -0
  82. package/dist/notify/format.d.ts.map +1 -0
  83. package/dist/notify/format.js +11 -0
  84. package/dist/notify/format.js.map +1 -0
  85. package/dist/notify/telegram.d.ts +2 -1
  86. package/dist/notify/telegram.d.ts.map +1 -1
  87. package/dist/notify/telegram.js +3 -2
  88. package/dist/notify/telegram.js.map +1 -1
  89. package/dist/notify/wechat-work.d.ts +2 -1
  90. package/dist/notify/wechat-work.d.ts.map +1 -1
  91. package/dist/notify/wechat-work.js +5 -5
  92. package/dist/notify/wechat-work.js.map +1 -1
  93. package/dist/pipeline/runner.d.ts +13 -1
  94. package/dist/pipeline/runner.d.ts.map +1 -1
  95. package/dist/pipeline/runner.js +41 -7
  96. package/dist/pipeline/runner.js.map +1 -1
  97. package/dist/pipeline/script-storyboard-runner.d.ts +3 -0
  98. package/dist/pipeline/script-storyboard-runner.d.ts.map +1 -1
  99. package/dist/pipeline/script-storyboard-runner.js +86 -43
  100. package/dist/pipeline/script-storyboard-runner.js.map +1 -1
  101. package/dist/pipeline/short-fiction-runner.d.ts +3 -1
  102. package/dist/pipeline/short-fiction-runner.d.ts.map +1 -1
  103. package/dist/pipeline/short-fiction-runner.js +80 -33
  104. package/dist/pipeline/short-fiction-runner.js.map +1 -1
  105. package/dist/play/play-runner.d.ts +8 -0
  106. package/dist/play/play-runner.d.ts.map +1 -1
  107. package/dist/play/play-runner.js +14 -0
  108. package/dist/play/play-runner.js.map +1 -1
  109. package/dist/prompts/short-fiction.d.ts +14 -13
  110. package/dist/prompts/short-fiction.d.ts.map +1 -1
  111. package/dist/prompts/short-fiction.js +253 -14
  112. package/dist/prompts/short-fiction.js.map +1 -1
  113. package/dist/state/manager.d.ts +16 -2
  114. package/dist/state/manager.d.ts.map +1 -1
  115. package/dist/state/manager.js +218 -31
  116. package/dist/state/manager.js.map +1 -1
  117. package/dist/translation/epub.d.ts +10 -0
  118. package/dist/translation/epub.d.ts.map +1 -0
  119. package/dist/translation/epub.js +86 -0
  120. package/dist/translation/epub.js.map +1 -0
  121. package/dist/translation/export.d.ts +6 -0
  122. package/dist/translation/export.d.ts.map +1 -0
  123. package/dist/translation/export.js +68 -0
  124. package/dist/translation/export.js.map +1 -0
  125. package/dist/translation/index.d.ts +9 -0
  126. package/dist/translation/index.d.ts.map +1 -0
  127. package/dist/translation/index.js +9 -0
  128. package/dist/translation/index.js.map +1 -0
  129. package/dist/translation/llm-model.d.ts +8 -0
  130. package/dist/translation/llm-model.d.ts.map +1 -0
  131. package/dist/translation/llm-model.js +134 -0
  132. package/dist/translation/llm-model.js.map +1 -0
  133. package/dist/translation/project.d.ts +3 -0
  134. package/dist/translation/project.d.ts.map +1 -0
  135. package/dist/translation/project.js +76 -0
  136. package/dist/translation/project.js.map +1 -0
  137. package/dist/translation/run-store.d.ts +11 -0
  138. package/dist/translation/run-store.d.ts.map +1 -0
  139. package/dist/translation/run-store.js +53 -0
  140. package/dist/translation/run-store.js.map +1 -0
  141. package/dist/translation/runner.d.ts +6 -0
  142. package/dist/translation/runner.d.ts.map +1 -0
  143. package/dist/translation/runner.js +88 -0
  144. package/dist/translation/runner.js.map +1 -0
  145. package/dist/translation/source.d.ts +12 -0
  146. package/dist/translation/source.d.ts.map +1 -0
  147. package/dist/translation/source.js +66 -0
  148. package/dist/translation/source.js.map +1 -0
  149. package/dist/translation/text.d.ts +10 -0
  150. package/dist/translation/text.d.ts.map +1 -0
  151. package/dist/translation/text.js +77 -0
  152. package/dist/translation/text.js.map +1 -0
  153. package/dist/translation/types.d.ts +96 -0
  154. package/dist/translation/types.d.ts.map +1 -0
  155. package/dist/translation/types.js +2 -0
  156. package/dist/translation/types.js.map +1 -0
  157. package/dist/utils/effective-llm-config.js +19 -1
  158. package/dist/utils/effective-llm-config.js.map +1 -1
  159. package/dist/utils/posix-path.d.ts +2 -0
  160. package/dist/utils/posix-path.d.ts.map +1 -0
  161. package/dist/utils/posix-path.js +7 -0
  162. package/dist/utils/posix-path.js.map +1 -0
  163. package/package.json +4 -3
@@ -12,9 +12,11 @@ import { safeChildPath } from "../utils/path-safety.js";
12
12
  import { normalizePlatformId, normalizePlatformOrOther } from "../models/book.js";
13
13
  import { generateShortFictionCover, runShortFictionProduction } from "../pipeline/short-fiction-runner.js";
14
14
  import { runInteractiveFilmCreation, runScriptCreation, runStoryboardCreation } from "../pipeline/script-storyboard-runner.js";
15
+ import { createTranslationProjectFromFile } from "../translation/index.js";
15
16
  import { runResearchReport } from "../agents/researcher.js";
16
17
  import { ingestMaterial } from "../materials/ingest.js";
17
18
  import { retrieveMaterials } from "../materials/retrieve.js";
19
+ import { loadChaptersFromPath } from "./chapter-import-source.js";
18
20
  import { createPlayDB } from "../play/play-db-factory.js";
19
21
  import { PlayRunner } from "../play/play-runner.js";
20
22
  import { PlayStore } from "../play/play-store.js";
@@ -49,6 +51,10 @@ function createDeterministicInteractionTools(pipeline, projectRoot) {
49
51
  function closePlayDB(db) {
50
52
  db.close?.();
51
53
  }
54
+ // runnerFactory 注入的测试替身没有 close 方法,可选调用兜底。
55
+ function closePlayRunner(runner) {
56
+ runner.close?.();
57
+ }
52
58
  function safePlayId(value, fallback) {
53
59
  const raw = (value?.trim() || fallback).slice(0, 80);
54
60
  if (!raw || raw === "." || raw === ".." || raw.includes("/") || raw.includes("\\") || raw.includes("\0")) {
@@ -98,6 +104,7 @@ const ProposeActionParams = Type.Object({
98
104
  Type.Literal("script_create"),
99
105
  Type.Literal("storyboard_create"),
100
106
  Type.Literal("interactive_film_create"),
107
+ Type.Literal("translation_create"),
101
108
  Type.Literal("draft_structure"),
102
109
  Type.Literal("connect_choice"),
103
110
  Type.Literal("remove_node"),
@@ -229,6 +236,13 @@ const ProposeActionParams = Type.Object({
229
236
  projectId: Type.Optional(Type.String({ description: "Optional output id under interactive-films/." })),
230
237
  outDir: Type.Optional(Type.String({ description: "Optional project-relative output directory. Default interactive-films/." })),
231
238
  }, { description: "Structured execution args for action=interactive_film_create." })),
239
+ translationCreate: Type.Optional(Type.Object({
240
+ filePath: Type.Optional(Type.String({ description: "Project-relative EPUB/PDF/TXT/Markdown source file path to translate." })),
241
+ sourceLanguage: Type.Optional(Type.String({ description: "Source language as a human-readable name, e.g. Auto detect, Japanese, English, Chinese (Simplified), 繁体中文(台湾). Do not require ISO abbreviations." })),
242
+ targetLanguage: Type.Optional(Type.String({ description: "Target language as a human-readable name, e.g. Chinese (Simplified), English, Japanese, Korean, Brazilian Portuguese. Do not require ISO abbreviations." })),
243
+ title: Type.Optional(Type.String({ description: "Optional translation project title." })),
244
+ segmentMaxChars: Type.Optional(Type.Number({ description: "Optional long-paragraph split threshold." })),
245
+ }, { description: "Structured execution args for action=translation_create." })),
232
246
  });
233
247
  function proposedActionSessionKind(action) {
234
248
  if (action === "create_book")
@@ -241,6 +255,8 @@ function proposedActionSessionKind(action) {
241
255
  return "storyboard";
242
256
  if (action === "interactive_film_create")
243
257
  return "interactive-film";
258
+ if (action === "translation_create")
259
+ return "chat";
244
260
  if (action === "draft_structure" || action === "connect_choice" || action === "remove_node")
245
261
  return "interactive-film-authoring";
246
262
  if (action === "fanfic_init" || action === "continuation_import" || action === "spinoff_create" || action === "style_imitation")
@@ -282,6 +298,8 @@ function proposedActionFallbackTitle(action, isZh) {
282
298
  return isZh ? "创建分镜" : "Create storyboard";
283
299
  case "interactive_film_create":
284
300
  return isZh ? "创建互动影游" : "Create interactive film";
301
+ case "translation_create":
302
+ return isZh ? "创建翻译项目" : "Create translation project";
285
303
  case "draft_structure":
286
304
  return isZh ? "生成故事结构" : "Draft story structure";
287
305
  case "connect_choice":
@@ -392,6 +410,11 @@ function proposedActionPayload(params) {
392
410
  if (interactiveFilmCreate)
393
411
  payload.interactiveFilmCreate = interactiveFilmCreate;
394
412
  }
413
+ if (params.action === "translation_create") {
414
+ const translationCreate = compactObject(params.translationCreate);
415
+ if (translationCreate)
416
+ payload.translationCreate = translationCreate;
417
+ }
395
418
  return Object.keys(payload).length > 0 ? payload : undefined;
396
419
  }
397
420
  function validateProposedActionPayload(payload) {
@@ -432,6 +455,12 @@ function assertExecutableProposedAction(params, payload) {
432
455
  }
433
456
  if (params.action === "interactive_film_create") {
434
457
  requireProposedText(payload?.interactiveFilmCreate?.title, "interactiveFilmCreate.title");
458
+ return;
459
+ }
460
+ if (params.action === "translation_create") {
461
+ requireProposedText(payload?.translationCreate?.filePath, "translationCreate.filePath");
462
+ requireProposedText(payload?.translationCreate?.sourceLanguage, "translationCreate.sourceLanguage");
463
+ requireProposedText(payload?.translationCreate?.targetLanguage, "translationCreate.targetLanguage");
435
464
  }
436
465
  }
437
466
  export function createProposeActionTool(language = "zh", options = {}) {
@@ -562,7 +591,16 @@ function prepareSubAgentArguments(args) {
562
591
  }
563
592
  return prepared;
564
593
  }
594
+ function runPipelineWithAbortSignal(pipeline, signal, task) {
595
+ // PipelineRunner owns the async cancellation scope. The fallback keeps
596
+ // lightweight embedders/test doubles source-compatible.
597
+ const runner = pipeline;
598
+ return runner.runWithAbortSignal
599
+ ? runner.runWithAbortSignal(signal, task)
600
+ : task();
601
+ }
565
602
  export function createSubAgentTool(pipeline, activeBookId, projectRoot, options = {}) {
603
+ const sessionIsZh = (options.language ?? "zh") !== "en";
566
604
  return {
567
605
  name: "sub_agent",
568
606
  description: options.architectCreateOnly
@@ -586,7 +624,9 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
586
624
  return textResult("No active book. Only the architect agent can create a book from this session.");
587
625
  }
588
626
  if (activeBookId && agent === "architect" && !revise) {
589
- return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。");
627
+ return textResult(sessionIsZh
628
+ ? "当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。"
629
+ : "This session already has a book, so no new book is needed. To create a new book, go back to the home page first.");
590
630
  }
591
631
  switch (agent) {
592
632
  case "architect": {
@@ -597,9 +637,11 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
597
637
  }
598
638
  const targetBookId = resolveToolBookId("architect", bookId, activeBookId);
599
639
  progress(`Revising foundation for "${targetBookId}"...`);
600
- await pipeline.reviseFoundation(targetBookId, feedback ?? instruction);
640
+ await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.reviseFoundation(targetBookId, feedback ?? instruction));
601
641
  progress(`Foundation revised for "${targetBookId}".`);
602
- return textResult(`Book "${targetBookId}" 架构稿已按要求重写。原书的条目式架构稿已备份到 story/.backup-phase4-<时间戳>/。`);
642
+ return textResult(sessionIsZh
643
+ ? `Book "${targetBookId}" 架构稿已按要求重写。原书的条目式架构稿已备份到 story/.backup-phase4-<时间戳>/。`
644
+ : `Book "${targetBookId}" foundation has been rewritten as requested. The previous itemized foundation was backed up to story/.backup-phase4-<timestamp>/.`);
603
645
  }
604
646
  const confirmedTitle = createBookPayload?.title?.trim();
605
647
  const resolvedTitle = confirmedTitle || title?.trim();
@@ -614,7 +656,7 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
614
656
  const now = new Date().toISOString();
615
657
  const resolvedLanguage = createBookPayload?.language ?? language ?? inferLanguage(instruction);
616
658
  progress(`Starting architect for book "${id}"...`);
617
- await pipeline.initBook({
659
+ await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.initBook({
618
660
  id,
619
661
  title: resolvedTitle,
620
662
  genre: createBookPayload?.genre ?? genre ?? "general",
@@ -625,14 +667,14 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
625
667
  chapterWordCount: createBookPayload?.chapterWordCount ?? chapterWordCount ?? defaultChapterLength(resolvedLanguage),
626
668
  createdAt: now,
627
669
  updatedAt: now,
628
- }, { externalContext: instruction });
670
+ }, { externalContext: instruction }));
629
671
  progress(`Architect finished — book "${id}" foundation created.`);
630
672
  return textResult(`Book "${resolvedTitle}" (${id}) initialised successfully. Foundation files are ready.`, { kind: "book_created", bookId: id, title: resolvedTitle });
631
673
  }
632
674
  case "writer": {
633
675
  const targetBookId = resolveToolBookId("writer", bookId, activeBookId);
634
676
  progress(`Writing next chapter for "${targetBookId}"...`);
635
- const result = await pipeline.writeNextChapter(targetBookId, chapterWordCount);
677
+ const result = await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.writeNextChapter(targetBookId, chapterWordCount));
636
678
  progress(`Writer finished chapter for "${targetBookId}".`);
637
679
  const resultStatus = result.status;
638
680
  const wordCount = result.wordCount ?? "unknown";
@@ -653,7 +695,7 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
653
695
  case "auditor": {
654
696
  const targetBookId = resolveToolBookId("auditor", bookId, activeBookId);
655
697
  progress(`Auditing chapter ${chapterNumber ?? "latest"} for "${targetBookId}"...`);
656
- const audit = await pipeline.auditDraft(targetBookId, chapterNumber);
698
+ const audit = await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.auditDraft(targetBookId, chapterNumber));
657
699
  progress(`Audit complete for "${targetBookId}".`);
658
700
  const issueLines = (audit.issues ?? [])
659
701
  .map((i) => `[${i.severity}] ${i.description}`)
@@ -665,7 +707,7 @@ export function createSubAgentTool(pipeline, activeBookId, projectRoot, options
665
707
  const targetBookId = resolveToolBookId("reviser", bookId, activeBookId);
666
708
  const resolvedMode = mode ?? "spot-fix";
667
709
  progress(`Revising "${targetBookId}" chapter ${chapterNumber ?? "latest"} in ${resolvedMode} mode...`);
668
- const result = await pipeline.reviseDraft(targetBookId, chapterNumber, resolvedMode, instruction);
710
+ const result = await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.reviseDraft(targetBookId, chapterNumber, resolvedMode, instruction));
669
711
  const applied = result.applied !== false;
670
712
  const resultChapter = result.chapterNumber ?? chapterNumber;
671
713
  const details = {
@@ -952,6 +994,76 @@ export function createRetrieveMaterialTool(projectRoot) {
952
994
  },
953
995
  };
954
996
  }
997
+ // ---------------------------------------------------------------------------
998
+ // 5. Chapter Import Tool (import_chapters)
999
+ // ---------------------------------------------------------------------------
1000
+ const ImportChaptersParams = Type.Object({
1001
+ bookId: Type.Optional(Type.String({
1002
+ description: "Target book ID to import into. In active-book sessions, omit it to use the current active book; if provided, it must match the active book. In general chat there is no active book, so it is required and must be an existing book.",
1003
+ })),
1004
+ sourcePath: Type.String({
1005
+ description: "Local path of the chapter source: either the stored_path from the Uploaded Files block (project-relative, e.g. .inkos/uploads/<session>/novel.txt) or an absolute path on this machine that the user provided. A directory imports each .md/.txt file as one chapter in filename order; a single file is split into chapters automatically by heading lines.",
1006
+ }),
1007
+ splitPattern: Type.Optional(Type.String({
1008
+ description: "Single-file mode only: custom JavaScript regex source matching chapter heading lines. Omit to use the default pattern, which matches \"第X章/第X回\" and \"Chapter N\" headings.",
1009
+ })),
1010
+ resumeFrom: Type.Optional(Type.Number({
1011
+ description: "Resume an interrupted import from chapter N (1-based). Required when the book already has chapters: replay starts at chapter N and earlier chapters are kept. Omit for a fresh import into an empty book.",
1012
+ })),
1013
+ importMode: Type.Optional(Type.Union([
1014
+ Type.Literal("continuation"),
1015
+ Type.Literal("series"),
1016
+ ], {
1017
+ description: "continuation (default): the book picks up exactly where the imported text left off, no new spacetime. series: shared universe but an independent new story, so a new spacetime is generated.",
1018
+ })),
1019
+ });
1020
+ export function createImportChaptersTool(pipeline, activeBookId, projectRoot) {
1021
+ return {
1022
+ name: "import_chapters",
1023
+ description: "Import an existing novel's chapters from a local file or directory into an InkOS book as real chapters (not reference material). " +
1024
+ "InkOS reverse-engineers foundation/truth files from the imported text and replays every chapter to rebuild story state, so the book can be continued afterwards. " +
1025
+ "Use ingest_material instead when the user only wants to archive reference material without touching book chapters.",
1026
+ label: "Import Chapters",
1027
+ parameters: ImportChaptersParams,
1028
+ async execute(_toolCallId, params, _signal, onUpdate) {
1029
+ const targetBookId = resolveToolBookId("import_chapters", params.bookId, activeBookId);
1030
+ const state = new StateManager(projectRoot);
1031
+ const existingChapterCount = (await state.getNextChapterNumber(targetBookId)) - 1;
1032
+ if (existingChapterCount > 0 && params.resumeFrom === undefined) {
1033
+ throw new Error(`Book "${targetBookId}" already has ${existingChapterCount} chapter(s). ` +
1034
+ `Pass resumeFrom=<n> to resume/append from chapter n, or ask the user to clear the existing chapters first.`);
1035
+ }
1036
+ const resolvedSourcePath = isAbsolute(params.sourcePath)
1037
+ ? params.sourcePath
1038
+ : resolve(projectRoot, params.sourcePath);
1039
+ onUpdate?.(textResult(`Reading chapters from ${resolvedSourcePath}...`));
1040
+ const chapters = await loadChaptersFromPath(resolvedSourcePath, params.splitPattern);
1041
+ onUpdate?.(textResult(`Found ${chapters.length} chapter(s); importing into "${targetBookId}"...`));
1042
+ const result = await runPipelineWithAbortSignal(pipeline, _signal, () => pipeline.importChapters({
1043
+ bookId: targetBookId,
1044
+ chapters,
1045
+ resumeFrom: params.resumeFrom,
1046
+ importMode: params.importMode,
1047
+ }));
1048
+ const regeneratedFoundation = (params.resumeFrom ?? 1) === 1;
1049
+ return textResult([
1050
+ `Imported ${result.importedCount} chapter(s) into book "${result.bookId}".`,
1051
+ `Total imported length: ${result.totalWords}. Next chapter to write: ${result.nextChapter}.`,
1052
+ regeneratedFoundation
1053
+ ? "Foundation and truth files were reverse-engineered from the imported text; chapter files and the chapter index were rebuilt by sequential replay."
1054
+ : `Resumed replay from chapter ${params.resumeFrom}; earlier chapters and the existing foundation were kept.`,
1055
+ `The book can now be continued with sub_agent(agent="writer") in the book session.`,
1056
+ ].join("\n"), {
1057
+ kind: "chapters_imported",
1058
+ bookId: result.bookId,
1059
+ importedCount: result.importedCount,
1060
+ totalWords: result.totalWords,
1061
+ nextChapter: result.nextChapter,
1062
+ importMode: params.importMode ?? "continuation",
1063
+ });
1064
+ },
1065
+ };
1066
+ }
955
1067
  function slugResearchTopic(topic) {
956
1068
  const slug = topic
957
1069
  .normalize("NFKC")
@@ -987,7 +1099,7 @@ const ShortFictionRunParams = Type.Object({
987
1099
  description: "Target complete short chapter count, 12-18. Default 12.",
988
1100
  })),
989
1101
  charsPerChapter: Type.Optional(Type.Number({
990
- description: "Target Chinese characters per chapter, 900-1200. Default 1000. Do not use total story length here.",
1102
+ description: "Per-chapter length in the story language's native unit: 900-1200 Chinese characters (default 1000) for zh, or 600-800 English words (default 650) for en. Do not use total story length here.",
991
1103
  })),
992
1104
  cover: Type.Optional(Type.Boolean({
993
1105
  description: "Whether to attempt cover image generation after synopsis and cover prompt. Default true; use false if the user only wants text assets.",
@@ -1034,6 +1146,7 @@ export function createShortFictionRunTool(pipeline, projectRoot, options = {}) {
1034
1146
  storyId: shortPayload?.storyId ?? params.storyId,
1035
1147
  chapterCount: shortPayload?.chapters ?? params.chapters,
1036
1148
  charsPerChapter: shortPayload?.charsPerChapter ?? params.charsPerChapter,
1149
+ language: options.language,
1037
1150
  cover: shortPayload?.cover ?? params.cover,
1038
1151
  coverBaseUrl: params.coverBaseUrl,
1039
1152
  coverEndpoint: params.coverEndpoint,
@@ -1072,7 +1185,53 @@ function summarizeCoverGenerationError(error) {
1072
1185
  return text.slice(0, 300);
1073
1186
  }
1074
1187
  // ---------------------------------------------------------------------------
1075
- // 3. Script and Storyboard tools
1188
+ // 3. Translation tool
1189
+ // ---------------------------------------------------------------------------
1190
+ const TranslationCreateParams = Type.Object({
1191
+ filePath: Type.String({
1192
+ description: "Project-relative EPUB/PDF/TXT/Markdown source file path to translate.",
1193
+ }),
1194
+ sourceLanguage: Type.String({
1195
+ description: "Source language as a human-readable name, e.g. Auto detect, Japanese, English, Chinese (Simplified), 繁体中文(台湾). Do not require ISO abbreviations.",
1196
+ }),
1197
+ targetLanguage: Type.String({
1198
+ description: "Target language as a human-readable name, e.g. Chinese (Simplified), English, Japanese, Korean, Brazilian Portuguese. Do not require ISO abbreviations.",
1199
+ }),
1200
+ title: Type.Optional(Type.String({
1201
+ description: "Optional translation project title.",
1202
+ })),
1203
+ segmentMaxChars: Type.Optional(Type.Number({
1204
+ description: "Optional max chars per segment before splitting long paragraphs.",
1205
+ })),
1206
+ });
1207
+ export function createTranslationCreateTool(projectRoot, options = {}) {
1208
+ return {
1209
+ name: "translation_create",
1210
+ description: "Create an InkOS translation project from an EPUB/PDF/TXT/Markdown file. " +
1211
+ "This only ingests and segments the source; running the actual translation is a separate long task.",
1212
+ label: "Translation",
1213
+ parameters: TranslationCreateParams,
1214
+ async execute(_toolCallId, params) {
1215
+ const payload = options.actionPayload?.translationCreate;
1216
+ const result = await createTranslationProjectFromFile(projectRoot, {
1217
+ filePath: payload?.filePath ?? params.filePath,
1218
+ sourceLanguage: payload?.sourceLanguage ?? params.sourceLanguage,
1219
+ targetLanguage: payload?.targetLanguage ?? params.targetLanguage,
1220
+ title: payload?.title ?? params.title,
1221
+ segmentMaxChars: payload?.segmentMaxChars ?? params.segmentMaxChars,
1222
+ });
1223
+ return textResult([
1224
+ `Translation project "${result.manifest.title}" created.`,
1225
+ `ID: ${result.manifest.id}`,
1226
+ `Source: ${result.manifest.source.kind} ${result.manifest.sourceLanguage} -> ${result.manifest.targetLanguage}`,
1227
+ `Chapters: ${result.manifest.chapters.length}`,
1228
+ `Manifest: ${result.manifestPath}`,
1229
+ ].join("\n"), { kind: "translation_project_created", ...result });
1230
+ },
1231
+ };
1232
+ }
1233
+ // ---------------------------------------------------------------------------
1234
+ // 4. Script and Storyboard tools
1076
1235
  // ---------------------------------------------------------------------------
1077
1236
  const ScriptCreateParams = Type.Object({
1078
1237
  title: Type.String({
@@ -1135,6 +1294,7 @@ export function createScriptCreationTool(pipeline, projectRoot, options = {}) {
1135
1294
  requirements: payload?.requirements ?? params.requirements,
1136
1295
  episodeCount: payload?.episodeCount ?? params.episodeCount,
1137
1296
  episodeDuration: payload?.episodeDuration ?? params.episodeDuration,
1297
+ language: options.language,
1138
1298
  projectId: payload?.projectId ?? params.projectId,
1139
1299
  outDir: payload?.outDir ?? params.outDir,
1140
1300
  onProgress: progress,
@@ -1208,6 +1368,7 @@ export function createStoryboardCreationTool(pipeline, projectRoot, options = {}
1208
1368
  aspectRatio: payload?.aspectRatio ?? params.aspectRatio,
1209
1369
  granularity: payload?.granularity ?? params.granularity,
1210
1370
  maxShots: payload?.maxShots ?? params.maxShots,
1371
+ language: options.language,
1211
1372
  projectId: payload?.projectId ?? params.projectId,
1212
1373
  outDir: payload?.outDir ?? params.outDir,
1213
1374
  onProgress: progress,
@@ -1287,6 +1448,7 @@ export function createInteractiveFilmCreationTool(pipeline, projectRoot, options
1287
1448
  episodeDuration: payload?.episodeDuration ?? params.episodeDuration,
1288
1449
  budget: payload?.budget ?? params.budget,
1289
1450
  referenceMode: payload?.referenceMode ?? params.referenceMode,
1451
+ language: options.language,
1290
1452
  projectId: payload?.projectId ?? params.projectId,
1291
1453
  outDir: payload?.outDir ?? params.outDir,
1292
1454
  onProgress: progress,
@@ -1588,7 +1750,7 @@ const PlayEditParams = Type.Object({
1588
1750
  description: "Short human-readable note summarizing what changed.",
1589
1751
  })),
1590
1752
  });
1591
- export function createPlayEditTool(projectRoot, sessionId) {
1753
+ export function createPlayEditTool(projectRoot, sessionId, language = "zh") {
1592
1754
  return {
1593
1755
  name: "play_edit",
1594
1756
  description: "Persistently edit the active InkOS Play world card, visual contract, player persona, or entity/role cards without advancing time or narrating a turn. " +
@@ -1601,8 +1763,11 @@ export function createPlayEditTool(projectRoot, sessionId) {
1601
1763
  const runId = "main";
1602
1764
  const world = await store.loadWorld(worldId);
1603
1765
  if (!world) {
1604
- return textResult("还没有可编辑的互动世界。先用 play_start 开一局。");
1766
+ return textResult(language === "en"
1767
+ ? "There is no interactive world to edit yet. Start one with play_start first."
1768
+ : "还没有可编辑的互动世界。先用 play_start 开一局。");
1605
1769
  }
1770
+ const isZh = (world.language ?? "zh") !== "en";
1606
1771
  const patch = {};
1607
1772
  const nextWorldContract = mergeContract(world.worldContract, params.worldContract, params.worldContractReplacements, params.worldContractAppend);
1608
1773
  const nextVisualContract = mergeContract(world.visualContract, params.visualContract, params.visualContractReplacements, params.visualContractAppend);
@@ -1626,9 +1791,9 @@ export function createPlayEditTool(projectRoot, sessionId) {
1626
1791
  upsertPlayEditEntity(db, {
1627
1792
  id: "actor_player",
1628
1793
  type: "actor",
1629
- label: existingPlayer?.label ?? "玩家",
1794
+ label: existingPlayer?.label ?? (isZh ? "玩家" : "Player"),
1630
1795
  summary: playerPersona,
1631
- status: "已更新",
1796
+ status: isZh ? "已更新" : "Updated",
1632
1797
  });
1633
1798
  updatedEntities += 1;
1634
1799
  }
@@ -1645,7 +1810,7 @@ export function createPlayEditTool(projectRoot, sessionId) {
1645
1810
  premise: updatedWorld.premise,
1646
1811
  graphEditedAt: new Date().toISOString(),
1647
1812
  });
1648
- return textResult(params.note?.trim() || "互动世界设定已更新。", {
1813
+ return textResult(params.note?.trim() || (isZh ? "互动世界设定已更新。" : "Interactive world settings updated."), {
1649
1814
  kind: "play_world_updated",
1650
1815
  worldId,
1651
1816
  runId,
@@ -1680,7 +1845,9 @@ export function createPlayStepTool(pipeline, projectRoot, sessionId, options = {
1680
1845
  const runId = "main";
1681
1846
  const world = await store.loadWorld(worldId);
1682
1847
  if (!world) {
1683
- return textResult("还没有可推进的互动世界。先用 play_start 开一局。");
1848
+ return textResult(options.language === "en"
1849
+ ? "There is no interactive world to advance yet. Start one with play_start first."
1850
+ : "还没有可推进的互动世界。先用 play_start 开一局。");
1684
1851
  }
1685
1852
  const target = { worldId, runId, world };
1686
1853
  onUpdate?.(textResult(`Advancing "${target.worldId}" / "${target.runId}"...`));
@@ -1714,6 +1881,9 @@ export function createPlayStepTool(pipeline, projectRoot, sessionId, options = {
1714
1881
  error: err instanceof Error ? err.message : String(err),
1715
1882
  });
1716
1883
  }
1884
+ finally {
1885
+ closePlayRunner(runner);
1886
+ }
1717
1887
  const db = createPlayDB(store.runDir(target.worldId, target.runId));
1718
1888
  let graph;
1719
1889
  try {
@@ -1751,8 +1921,11 @@ export function createPlayReviseTool(pipeline, projectRoot, sessionId, options =
1751
1921
  const runId = "main";
1752
1922
  const world = await store.loadWorld(worldId);
1753
1923
  if (!world) {
1754
- return textResult("还没有可重做的互动世界。先用 play_start 开一局。");
1924
+ return textResult(options.language === "en"
1925
+ ? "There is no interactive world to redo yet. Start one with play_start first."
1926
+ : "还没有可重做的互动世界。先用 play_start 开一局。");
1755
1927
  }
1928
+ const isZh = (world.language ?? "zh") !== "en";
1756
1929
  const ctx = pipeline.createAgentContext("play");
1757
1930
  const runner = options.runnerFactory?.({ projectRoot, worldId, runId, ctx }) ?? new PlayRunner({
1758
1931
  projectRoot,
@@ -1760,46 +1933,55 @@ export function createPlayReviseTool(pipeline, projectRoot, sessionId, options =
1760
1933
  runId,
1761
1934
  ctx,
1762
1935
  });
1763
- if (params.action === "restore_variant") {
1764
- const turn = params.turn;
1765
- const variantId = params.variantId?.trim();
1766
- if (typeof turn !== "number" || !Number.isFinite(turn) || !variantId) {
1767
- return textResult("恢复版本需要 turn 和 variantId。");
1768
- }
1769
- onUpdate?.(textResult(`Restoring play variant "${variantId}"...`));
1770
- const restored = await runner.restoreVariant({
1771
- turn: Math.trunc(turn),
1772
- variantId,
1773
- });
1774
- return textResult(restored.sceneText || "已切换到指定互动回合版本。", {
1775
- kind: "play_variant_restored",
1776
- worldId,
1777
- runId,
1778
- title: world.title,
1779
- turn: restored.turn,
1780
- variantId: restored.variantId,
1781
- sceneText: restored.sceneText,
1782
- });
1783
- }
1784
- const replacement = params.action === "edit_last_input" ? params.input?.trim() : undefined;
1785
- if (params.action === "edit_last_input" && !replacement) {
1786
- return textResult("编辑上一条玩家动作需要提供新的 input。");
1787
- }
1788
- onUpdate?.(textResult(params.action === "edit_last_input" ? "Replaying edited play turn..." : "Regenerating last play turn..."));
1789
1936
  let replay;
1937
+ // finally 关闭 runner 自建的 play.db 连接:句柄不关闭时 Windows 上无法删除数据库文件。
1790
1938
  try {
1791
- replay = await runner.regenerateLastTurn(replacement);
1939
+ if (params.action === "restore_variant") {
1940
+ const turn = params.turn;
1941
+ const variantId = params.variantId?.trim();
1942
+ if (typeof turn !== "number" || !Number.isFinite(turn) || !variantId) {
1943
+ return textResult(isZh
1944
+ ? "恢复版本需要 turn 和 variantId。"
1945
+ : "Restoring a variant requires both turn and variantId.");
1946
+ }
1947
+ onUpdate?.(textResult(`Restoring play variant "${variantId}"...`));
1948
+ const restored = await runner.restoreVariant({
1949
+ turn: Math.trunc(turn),
1950
+ variantId,
1951
+ });
1952
+ return textResult(restored.sceneText || (isZh ? "已切换到指定互动回合版本。" : "Switched to the requested play turn variant."), {
1953
+ kind: "play_variant_restored",
1954
+ worldId,
1955
+ runId,
1956
+ title: world.title,
1957
+ turn: restored.turn,
1958
+ variantId: restored.variantId,
1959
+ sceneText: restored.sceneText,
1960
+ });
1961
+ }
1962
+ const replacement = params.action === "edit_last_input" ? params.input?.trim() : undefined;
1963
+ if (params.action === "edit_last_input" && !replacement) {
1964
+ return textResult(isZh
1965
+ ? "编辑上一条玩家动作需要提供新的 input。"
1966
+ : "Editing the previous player action requires a new input.");
1967
+ }
1968
+ onUpdate?.(textResult(params.action === "edit_last_input" ? "Replaying edited play turn..." : "Regenerating last play turn..."));
1969
+ try {
1970
+ replay = await runner.regenerateLastTurn(replacement);
1971
+ }
1972
+ catch (err) {
1973
+ return textResult(isZh
1974
+ ? "(上一回合暂时不能安全重做。继续输入新的动作,我会从当前状态推进。)"
1975
+ : "(The previous turn cannot be safely regenerated yet. Enter a new action and I will continue from the current state.)", {
1976
+ kind: "play_revise_failed",
1977
+ worldId,
1978
+ runId,
1979
+ error: err instanceof Error ? err.message : String(err),
1980
+ });
1981
+ }
1792
1982
  }
1793
- catch (err) {
1794
- const isZh = (world.language ?? "zh") !== "en";
1795
- return textResult(isZh
1796
- ? "(上一回合暂时不能安全重做。继续输入新的动作,我会从当前状态推进。)"
1797
- : "(The previous turn cannot be safely regenerated yet. Enter a new action and I will continue from the current state.)", {
1798
- kind: "play_revise_failed",
1799
- worldId,
1800
- runId,
1801
- error: err instanceof Error ? err.message : String(err),
1802
- });
1983
+ finally {
1984
+ closePlayRunner(runner);
1803
1985
  }
1804
1986
  const db = createPlayDB(store.runDir(worldId, runId));
1805
1987
  let graph;