@kienmatu/inkos-studio 0.1.0 → 0.1.2

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 (66) hide show
  1. package/dist/api/server.d.ts.map +1 -1
  2. package/dist/api/server.js +134 -122
  3. package/dist/api/server.js.map +1 -1
  4. package/dist/assets/FilmWizard-CuCB-1xV.js +1 -0
  5. package/dist/assets/{FlowView-CZ7fxX3n.js → FlowView-rMLtzFZK.js} +3 -3
  6. package/dist/assets/{_baseUniq-DhsR81PW.js → _baseUniq-xbTfIKo7.js} +1 -1
  7. package/dist/assets/{arc-Ca3YI7OI.js → arc-BJ0Jn4qy.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-DDwYWwqm.js → architectureDiagram-Q4EWVU46-B54hBS8X.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-kqPOxZ2q.js → blockDiagram-DXYQGD6D-BrQpuNBu.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DBL27B73.js → c4Diagram-AHTNJAMY-nN4kJiPC.js} +1 -1
  11. package/dist/assets/channel-BMnRcRpv.js +1 -0
  12. package/dist/assets/{chunk-4BX2VUAB-BBF1yv__.js → chunk-4BX2VUAB-DnzQoX6p.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-DEDtFRAF.js → chunk-4TB4RGXK-PeEeEWP2.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DMKAeQw_.js → chunk-55IACEB6-BZHFzH6I.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-DMqU-4EI.js → chunk-EDXVE4YY-B06PJNq-.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-CZTsFDcK.js → chunk-FMBD7UC4-Cny1qCNu.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-CenEcrGy.js → chunk-OYMX7WX6-MqHFCczv.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-DXpv8Mti.js → chunk-QZHKN3VN-Bu8oSEuV.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-DLM5M52k.js → chunk-YZCP3GAM-DW9WLjg7.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-BpMdbRYm.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-BpMdbRYm.js +1 -0
  22. package/dist/assets/clone-Bfx4_ecn.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-DUMnmCfq.js → cose-bilkent-S5V4N54A-BFF3VF0W.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-DCU2j_Xn.js → dagre-KV5264BT-7wO_SFt1.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-CNmXjIem.js → diagram-5BDNPKRD-DpdLNbTp.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-BJB7dwH0.js → diagram-G4DWMVQ6-B2Xd9yJj.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-5OEVcoMB.js → diagram-MMDJMWI5-BcJABwZb.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-BH2d24yw.js → diagram-TYMM5635-ByagqgDJ.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-DhzfgmWY.js → erDiagram-SMLLAGMA-zfdClUWU.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-Dq699o3O.js → flowDiagram-DWJPFMVM-Cign1ovE.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-D2bwmYj5.js → ganttDiagram-T4ZO3ILL-zHFblbgr.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BkH_ILeo.js → gitGraphDiagram-UUTBAWPF-DgUNvxqj.js} +1 -1
  33. package/dist/assets/{graph-BiqUEdJM.js → graph-CT_N4MvP.js} +1 -1
  34. package/dist/assets/{highlighted-body-OFNGDK62-BD0SXLUv.js → highlighted-body-OFNGDK62-CbTQmRda.js} +1 -1
  35. package/dist/assets/{index-CRZhDmRu.js → index-C5AvWGUU.js} +253 -253
  36. package/dist/assets/index-DaFEDFsq.css +1 -0
  37. package/dist/assets/{infoDiagram-42DDH7IO-BVDONA_E.js → infoDiagram-42DDH7IO-B1j6wnrX.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-UXIWVN3A-CBELCuuM.js → ishikawaDiagram-UXIWVN3A-CmTERA2Q.js} +1 -1
  39. package/dist/assets/{journeyDiagram-VCZTEJTY-CpXgU8Ln.js → journeyDiagram-VCZTEJTY-lXMvtopT.js} +1 -1
  40. package/dist/assets/{kanban-definition-6JOO6SKY-DmjKOkdI.js → kanban-definition-6JOO6SKY-D2nd3IRf.js} +1 -1
  41. package/dist/assets/{layout-DXBAgkSC.js → layout-Dwdg17w2.js} +1 -1
  42. package/dist/assets/{linear-D5tXMFlB.js → linear-BGLXP-7J.js} +1 -1
  43. package/dist/assets/{min-Cd4glxaN.js → min-CWmMAm2D.js} +1 -1
  44. package/dist/assets/{mindmap-definition-QFDTVHPH-DTq5iIBu.js → mindmap-definition-QFDTVHPH-Bc_KwUJk.js} +1 -1
  45. package/dist/assets/{pieDiagram-DEJITSTG-Diz1Rvfr.js → pieDiagram-DEJITSTG-Dk4TEXCU.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-34T5L4WZ-58cr5yzB.js → quadrantDiagram-34T5L4WZ-DjpGwBeJ.js} +1 -1
  47. package/dist/assets/{requirementDiagram-MS252O5E-DDbIc9eT.js → requirementDiagram-MS252O5E-D-KTxT1S.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-XADWPNL6-Cdx6EKcx.js → sankeyDiagram-XADWPNL6-CAAThfhf.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-FGHM5R23-KfheY5t3.js → sequenceDiagram-FGHM5R23-EhYXipit.js} +1 -1
  50. package/dist/assets/{stateDiagram-FHFEXIEX-C5Ob61Ab.js → stateDiagram-FHFEXIEX-DlwRa19l.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DWOlnZT2.js +1 -0
  52. package/dist/assets/{timeline-definition-GMOUNBTQ-FSj6jaSw.js → timeline-definition-GMOUNBTQ-CnNt36mp.js} +1 -1
  53. package/dist/assets/{value-1MrA2_VI.js → value-BUuNnoEe.js} +1 -1
  54. package/dist/assets/{vennDiagram-DHZGUBPP-BQW3LO_W.js → vennDiagram-DHZGUBPP-CONWdidS.js} +1 -1
  55. package/dist/assets/{wardley-RL74JXVD-D1hNSINM.js → wardley-RL74JXVD-Dx3T9_uF.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-NUSXRM2D-SCZLQyge.js → wardleyDiagram-NUSXRM2D-Brxifhti.js} +1 -1
  57. package/dist/assets/{xychartDiagram-5P7HB3ND-DhvOA02s.js → xychartDiagram-5P7HB3ND-DkX9QW6S.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/package.json +2 -2
  60. package/dist/assets/FilmWizard-DW5wLwAY.js +0 -1
  61. package/dist/assets/channel-yY1GNn4w.js +0 -1
  62. package/dist/assets/classDiagram-6PBFFD2Q-BPxDssBj.js +0 -1
  63. package/dist/assets/classDiagram-v2-HSJHXN6E-BPxDssBj.js +0 -1
  64. package/dist/assets/clone-C9djMHZG.js +0 -1
  65. package/dist/assets/index-CbBLpAjo.css +0 -1
  66. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CcmSvc6O.js +0 -1
@@ -28,28 +28,28 @@ export function pick(lang, zh, en, vi) {
28
28
  }
29
29
  const PIPELINE_STAGES = {
30
30
  writer: [
31
- { zh: "准备章节输入", en: "Prepare chapter input" },
32
- { zh: "撰写章节草稿", en: "Write chapter draft" },
33
- { zh: "落盘最终章节", en: "Save final chapter" },
34
- { zh: "生成最终真相文件", en: "Generate final truth files" },
35
- { zh: "校验真相文件变更", en: "Validate truth file changes" },
36
- { zh: "同步记忆索引", en: "Sync memory index" },
37
- { zh: "更新章节索引与快照", en: "Update chapter index and snapshot" },
31
+ { zh: "准备章节输入", en: "Prepare chapter input", vi: "Chuẩn bị dữ liệu chương" },
32
+ { zh: "撰写章节草稿", en: "Write chapter draft", vi: "Viết bản nháp chương" },
33
+ { zh: "落盘最终章节", en: "Save final chapter", vi: "Lưu chương hoàn chỉnh" },
34
+ { zh: "生成最终真相文件", en: "Generate final truth files", vi: "Tạo tệp truth cuối cùng" },
35
+ { zh: "校验真相文件变更", en: "Validate truth file changes", vi: "Kiểm tra thay đổi tệp truth" },
36
+ { zh: "同步记忆索引", en: "Sync memory index", vi: "Đồng bộ chỉ mục bộ nhớ" },
37
+ { zh: "更新章节索引与快照", en: "Update chapter index and snapshot", vi: "Cập nhật chỉ mục và snapshot chương" },
38
38
  ],
39
39
  architect: [
40
- { zh: "生成基础设定", en: "Generate foundation" },
41
- { zh: "保存书籍配置", en: "Save book config" },
42
- { zh: "写入基础设定文件", en: "Write foundation files" },
43
- { zh: "初始化控制文档", en: "Initialize control documents" },
44
- { zh: "创建初始快照", en: "Create initial snapshot" },
40
+ { zh: "生成基础设定", en: "Generate foundation", vi: "Tạo thiết lập nền tảng" },
41
+ { zh: "保存书籍配置", en: "Save book config", vi: "Lưu cấu hình sách" },
42
+ { zh: "写入基础设定文件", en: "Write foundation files", vi: "Ghi tệp thiết lập nền tảng" },
43
+ { zh: "初始化控制文档", en: "Initialize control documents", vi: "Khởi tạo tài liệu kiểm soát" },
44
+ { zh: "创建初始快照", en: "Create initial snapshot", vi: "Tạo snapshot ban đầu" },
45
45
  ],
46
46
  reviser: [
47
- { zh: "加载修订上下文", en: "Load revision context" },
48
- { zh: "修订章节", en: "Revise chapter" },
49
- { zh: "落盘修订结果", en: "Save revision result" },
50
- { zh: "更新索引与快照", en: "Update index and snapshot" },
47
+ { zh: "加载修订上下文", en: "Load revision context", vi: "Tải ngữ cảnh chỉnh sửa" },
48
+ { zh: "修订章节", en: "Revise chapter", vi: "Chỉnh sửa chương" },
49
+ { zh: "落盘修订结果", en: "Save revision result", vi: "Lưu kết quả chỉnh sửa" },
50
+ { zh: "更新索引与快照", en: "Update index and snapshot", vi: "Cập nhật chỉ mục và snapshot" },
51
51
  ],
52
- auditor: [{ zh: "审计章节", en: "Audit chapter" }],
52
+ auditor: [{ zh: "审计章节", en: "Audit chapter", vi: "Kiểm duyệt chương" }],
53
53
  };
54
54
  function pipelineStages(agent, lang = "vi") {
55
55
  return PIPELINE_STAGES[agent]?.map((stage) => pick(lang, stage.zh, stage.en, stage.vi));
@@ -59,35 +59,35 @@ function attachmentDisposition(fileName) {
59
59
  return `attachment; filename="${safeAscii}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
60
60
  }
61
61
  const AGENT_LABELS = {
62
- architect: { zh: "建书", en: "Book setup" },
63
- writer: { zh: "写作", en: "Writing" },
64
- auditor: { zh: "审计", en: "Audit" },
65
- reviser: { zh: "修订", en: "Revision" },
66
- exporter: { zh: "导出", en: "Export" },
62
+ architect: { zh: "建书", en: "Book setup", vi: "Khởi tạo sách" },
63
+ writer: { zh: "写作", en: "Writing", vi: "Viết" },
64
+ auditor: { zh: "审计", en: "Audit", vi: "Kiểm duyệt" },
65
+ reviser: { zh: "修订", en: "Revision", vi: "Chỉnh sửa" },
66
+ exporter: { zh: "导出", en: "Export", vi: "Xuất" },
67
67
  };
68
68
  const TOOL_LABELS = {
69
- read: { zh: "读取文件", en: "Read file" },
70
- edit: { zh: "编辑文件", en: "Edit file" },
71
- grep: { zh: "搜索", en: "Search" },
72
- ls: { zh: "列目录", en: "List directory" },
73
- propose_action: { zh: "确认动作", en: "Confirm action" },
74
- short_fiction_run: { zh: "短篇生产", en: "Short fiction" },
75
- script_create: { zh: "剧本创作", en: "Script creation" },
76
- storyboard_create: { zh: "分镜创作", en: "Storyboard creation" },
77
- interactive_film_create: { zh: "互动影游", en: "Interactive film" },
78
- translation_create: { zh: "翻译项目", en: "Translation" },
79
- fanfic_create: { zh: "同人创作", en: "Fanfiction" },
80
- continuation_import: { zh: "导入续写", en: "Continuation import" },
81
- spinoff_create: { zh: "番外创作", en: "Side story" },
82
- imitation_create: { zh: "仿写创作", en: "Style imitation" },
83
- generate_cover: { zh: "生成封面", en: "Cover generation" },
84
- play_edit: { zh: "编辑互动世界", en: "Edit interactive world" },
85
- play_start: { zh: "启动互动世界", en: "Start interactive world" },
86
- play_revise: { zh: "重做互动回合", en: "Redo interactive turn" },
87
- play_step: { zh: "推进互动世界", en: "Advance interactive world" },
88
- create_narrative_forecast: { zh: "剧情多线推演", en: "Narrative forecast" },
89
- get_narrative_forecast: { zh: "核验剧情推演", en: "Recheck forecast" },
90
- select_narrative_branch: { zh: "采用候选分支", en: "Select candidate branch" },
69
+ read: { zh: "读取文件", en: "Read file", vi: "Đọc tệp" },
70
+ edit: { zh: "编辑文件", en: "Edit file", vi: "Chỉnh sửa tệp" },
71
+ grep: { zh: "搜索", en: "Search", vi: "Tìm kiếm" },
72
+ ls: { zh: "列目录", en: "List directory", vi: "Liệt kê thư mục" },
73
+ propose_action: { zh: "确认动作", en: "Confirm action", vi: "Xác nhận hành động" },
74
+ short_fiction_run: { zh: "短篇生产", en: "Short fiction", vi: "Truyện ngắn" },
75
+ script_create: { zh: "剧本创作", en: "Script creation", vi: "Sáng tác kịch bản" },
76
+ storyboard_create: { zh: "分镜创作", en: "Storyboard creation", vi: "Sáng tác phân cảnh" },
77
+ interactive_film_create: { zh: "互动影游", en: "Interactive film", vi: "Phim tương tác" },
78
+ translation_create: { zh: "翻译项目", en: "Translation", vi: "Dự án dịch thuật" },
79
+ fanfic_create: { zh: "同人创作", en: "Fanfiction", vi: "Sáng tác đồng nhân" },
80
+ continuation_import: { zh: "导入续写", en: "Continuation import", vi: "Nhập truyện viết tiếp" },
81
+ spinoff_create: { zh: "番外创作", en: "Side story", vi: "Sáng tác ngoại truyện" },
82
+ imitation_create: { zh: "仿写创作", en: "Style imitation", vi: "Sáng tác phỏng bút" },
83
+ generate_cover: { zh: "生成封面", en: "Cover generation", vi: "Tạo bìa sách" },
84
+ play_edit: { zh: "编辑互动世界", en: "Edit interactive world", vi: "Chỉnh sửa thế giới tương tác" },
85
+ play_start: { zh: "启动互动世界", en: "Start interactive world", vi: "Khởi động thế giới tương tác" },
86
+ play_revise: { zh: "重做互动回合", en: "Redo interactive turn", vi: "Làm lại lượt tương tác" },
87
+ play_step: { zh: "推进互动世界", en: "Advance interactive world", vi: "Tiếp tục thế giới tương tác" },
88
+ create_narrative_forecast: { zh: "剧情多线推演", en: "Narrative forecast", vi: "Dự đoán cốt truyện" },
89
+ get_narrative_forecast: { zh: "核验剧情推演", en: "Recheck forecast", vi: "Kiểm tra lại dự đoán" },
90
+ select_narrative_branch: { zh: "采用候选分支", en: "Select candidate branch", vi: "Chọn nhánh đề xuất" },
91
91
  };
92
92
  function resolveToolLabel(tool, agent, lang = "vi") {
93
93
  if (tool === "sub_agent" && agent) {
@@ -102,8 +102,8 @@ function formatTaskElapsed(ms, lang) {
102
102
  const minutes = Math.floor(totalSeconds / 60);
103
103
  const seconds = totalSeconds % 60;
104
104
  if (minutes === 0)
105
- return pick(lang, `${seconds} 秒`, `${seconds}s`);
106
- return pick(lang, `${minutes} 分 ${seconds} 秒`, `${minutes}m ${seconds}s`);
105
+ return pick(lang, `${seconds} 秒`, `${seconds}s`, `${seconds} giây`);
106
+ return pick(lang, `${minutes} 分 ${seconds} 秒`, `${minutes}m ${seconds}s`, `${minutes} phút ${seconds} giây`);
107
107
  }
108
108
  /**
109
109
  * 把正在后台运行的生产任务状态渲染成一段系统提示词附录,注入聊天 agent 的上下文。
@@ -113,11 +113,11 @@ function buildRunningTaskContextBlock(task, lang) {
113
113
  const exec = task.execution;
114
114
  const elapsed = formatTaskElapsed(Date.now() - exec.startedAt, lang);
115
115
  const status = exec.status === "processing"
116
- ? pick(lang, "处理中", "processing")
117
- : pick(lang, "运行中", "running");
116
+ ? pick(lang, "处理中", "processing", "đang xử lý")
117
+ : pick(lang, "运行中", "running", "đang chạy");
118
118
  const logsTail = (exec.logs ?? []).slice(-3);
119
119
  const logsBlock = logsTail.length > 0
120
- ? `\n${pick(lang, "- 最近日志:", "- Recent logs:")}\n${logsTail.map((line) => ` - ${line}`).join("\n")}`
120
+ ? `\n${pick(lang, "- 最近日志:", "- Recent logs:", "- Nhật ký gần đây:")}\n${logsTail.map((line) => ` - ${line}`).join("\n")}`
121
121
  : "";
122
122
  return pick(lang, [
123
123
  "## 后台任务状态",
@@ -133,6 +133,13 @@ function buildRunningTaskContextBlock(task, lang) {
133
133
  `- Status: ${status}`,
134
134
  `- Elapsed: ${elapsed}${logsBlock}`,
135
135
  "The task runs independently in the background; this chat turn does not interrupt it. When the user asks about its progress, answer truthfully from the information above. Do not start another production task of the same kind, and do not claim that no task is running. Production tools are temporarily unavailable and will be restored when the task finishes.",
136
+ ].join("\n"), [
137
+ "## Trạng thái tác vụ nền",
138
+ "Phiên này có một tác vụ sản xuất đang chạy nền:",
139
+ `- Tác vụ: ${exec.label} (${exec.tool})`,
140
+ `- Trạng thái: ${status}`,
141
+ `- Thời gian đã chạy: ${elapsed}${logsBlock}`,
142
+ "Tác vụ chạy độc lập ở nền; lượt trò chuyện này không làm gián đoạn nó. Khi người dùng hỏi về tiến độ, hãy trả lời trung thực dựa trên thông tin trên. Không khởi chạy thêm tác vụ sản xuất cùng loại, và không nói rằng không có tác vụ nào đang chạy. Các công cụ sản xuất tạm thời không khả dụng và sẽ được khôi phục khi tác vụ kết thúc.",
136
143
  ].join("\n"));
137
144
  }
138
145
  function compareServiceListItems(left, right) {
@@ -261,7 +268,7 @@ function normalizeApiBookId(value, fieldName) {
261
268
  return bookId;
262
269
  }
263
270
  function nonTextModelMessage(modelId, lang = "vi") {
264
- return pick(lang, `模型 ${modelId} 不适合文本聊天/写作。请在模型选择器中改用文本模型,例如 gemini-2.5-flash、gemini-2.5-pro 或对应服务的 chat 模型。`, `Model ${modelId} is not suitable for text chat/writing. Pick a text model in the model selector, e.g. gemini-2.5-flash, gemini-2.5-pro, or the service's chat model.`);
271
+ return pick(lang, `模型 ${modelId} 不适合文本聊天/写作。请在模型选择器中改用文本模型,例如 gemini-2.5-flash、gemini-2.5-pro 或对应服务的 chat 模型。`, `Model ${modelId} is not suitable for text chat/writing. Pick a text model in the model selector, e.g. gemini-2.5-flash, gemini-2.5-pro, or the service's chat model.`, `Model ${modelId} không phù hợp để chat/viết văn bản. Hãy chọn model văn bản khác trong bộ chọn model, ví dụ gemini-2.5-flash, gemini-2.5-pro, hoặc model chat của dịch vụ tương ứng.`);
265
272
  }
266
273
  function extractToolError(result) {
267
274
  return summarizeToolResult(result, 500);
@@ -748,27 +755,27 @@ function validateAgentActionExecution(args) {
748
755
  const lang = args.language ?? "vi";
749
756
  const failedExec = args.collectedToolExecs.find(isLikelyFailedToolResult);
750
757
  if (failedExec) {
751
- const detail = failedExec.error ?? failedExec.result ?? pick(lang, "未知错误", "unknown error");
752
- return pick(lang, `${failedExec.label} 执行失败:${detail}`, `${failedExec.label} failed: ${detail}`);
758
+ const detail = failedExec.error ?? failedExec.result ?? pick(lang, "未知错误", "unknown error", "lỗi không xác định");
759
+ return pick(lang, `${failedExec.label} 执行失败:${detail}`, `${failedExec.label} failed: ${detail}`, `${failedExec.label} thất bại: ${detail}`);
753
760
  }
754
761
  if (args.agentBookId
755
762
  && args.requestedIntent === "write_next"
756
763
  && !hasSuccessfulSubAgentExec(args.collectedToolExecs, "writer")) {
757
- return pick(lang, "模型声称已完成下一章,但没有实际调用写作工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "The model claimed the next chapter is done, but it never called the writing tool. Retry; if it keeps failing, check whether the model supports tool calls.");
764
+ return pick(lang, "模型声称已完成下一章,但没有实际调用写作工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "The model claimed the next chapter is done, but it never called the writing tool. Retry; if it keeps failing, check whether the model supports tool calls.", "Model báo đã hoàn thành chương tiếp theo, nhưng thực tế không gọi tool viết. Hãy thử lại; nếu vẫn thất bại, kiểm tra xem model có hỗ trợ gọi tool không.");
758
765
  }
759
766
  if (!args.agentBookId
760
767
  && args.requestedIntent === "create_book"
761
768
  && !hasSuccessfulSubAgentExec(args.collectedToolExecs, "architect")) {
762
- return pick(lang, "已确认建书,但模型没有实际调用建书工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Book creation was confirmed, but the model never called the book setup tool. Retry; if it keeps failing, check whether the model supports tool calls.");
769
+ return pick(lang, "已确认建书,但模型没有实际调用建书工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Book creation was confirmed, but the model never called the book setup tool. Retry; if it keeps failing, check whether the model supports tool calls.", "Đã xác nhận khởi tạo sách, nhưng model không gọi tool khởi tạo sách. Hãy thử lại; nếu vẫn thất bại, kiểm tra xem model có hỗ trợ gọi tool không.");
763
770
  }
764
771
  if (args.requestedIntent === "short_run" && !hasSuccessfulToolExec(args.collectedToolExecs, "short_fiction_run")) {
765
- return pick(lang, "已确认生成短篇,但模型没有实际调用短篇生产工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Short fiction was confirmed, but the model never called the short fiction tool. Retry; if it keeps failing, check whether the model supports tool calls.");
772
+ return pick(lang, "已确认生成短篇,但模型没有实际调用短篇生产工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Short fiction was confirmed, but the model never called the short fiction tool. Retry; if it keeps failing, check whether the model supports tool calls.", "Đã xác nhận tạo truyện ngắn, nhưng model không gọi tool sản xuất truyện ngắn. Hãy thử lại; nếu vẫn thất bại, kiểm tra xem model có hỗ trợ gọi tool không.");
766
773
  }
767
774
  if (args.requestedIntent === "play_start" && !hasSuccessfulToolExec(args.collectedToolExecs, "play_start")) {
768
- return pick(lang, "已确认启动互动世界,但模型没有实际调用互动世界工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Starting the interactive world was confirmed, but the model never called the interactive world tool. Retry; if it keeps failing, check whether the model supports tool calls.");
775
+ return pick(lang, "已确认启动互动世界,但模型没有实际调用互动世界工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Starting the interactive world was confirmed, but the model never called the interactive world tool. Retry; if it keeps failing, check whether the model supports tool calls.", "Đã xác nhận khởi động thế giới tương tác, nhưng model không gọi tool thế giới tương tác. Hãy thử lại; nếu vẫn thất bại, kiểm tra xem model có hỗ trợ gọi tool không.");
769
776
  }
770
777
  if (args.requestedIntent === "generate_cover" && !hasSuccessfulToolExec(args.collectedToolExecs, "generate_cover")) {
771
- return pick(lang, "已确认生成封面,但模型没有实际调用封面工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Cover generation was confirmed, but the model never called the cover tool. Retry; if it keeps failing, check whether the model supports tool calls.");
778
+ return pick(lang, "已确认生成封面,但模型没有实际调用封面工具。请重试;如果仍失败,请检查模型是否支持工具调用。", "Cover generation was confirmed, but the model never called the cover tool. Retry; if it keeps failing, check whether the model supports tool calls.", "Đã xác nhận tạo bìa sách, nhưng model không gọi tool tạo bìa. Hãy thử lại; nếu vẫn thất bại, kiểm tra xem model có hỗ trợ gọi tool không.");
772
779
  }
773
780
  return undefined;
774
781
  }
@@ -798,7 +805,7 @@ function formatAgentFailure(message, lang = "vi") {
798
805
  if (kind === "internal") {
799
806
  return {
800
807
  code: "AGENT_INTERNAL_ERROR",
801
- message: pick(lang, `InkOS 内部流程错误:${message}`, `InkOS internal pipeline error: ${message}`),
808
+ message: pick(lang, `InkOS 内部流程错误:${message}`, `InkOS internal pipeline error: ${message}`, `Lỗi quy trình nội bộ InkOS: ${message}`),
802
809
  status: 500,
803
810
  };
804
811
  }
@@ -868,7 +875,7 @@ function requirePayloadText(value, message) {
868
875
  }
869
876
  function toolResultText(result, lang = "vi") {
870
877
  const text = extractToolError(result).trim();
871
- return text || pick(lang, "已完成。", "Done.");
878
+ return text || pick(lang, "已完成。", "Done.", "Đã hoàn thành.");
872
879
  }
873
880
  async function executeConfirmedProductionAction(args) {
874
881
  const lang = args.language ?? "vi";
@@ -886,7 +893,7 @@ async function executeConfirmedProductionAction(args) {
886
893
  let agent;
887
894
  if (args.requestedIntent === "create_book") {
888
895
  const payload = actionPayload?.createBook;
889
- const title = requirePayloadText(payload?.title, pick(lang, "确认建书缺少书名,请重新生成确认卡。", "The book creation confirmation is missing a title. Regenerate the confirmation card."));
896
+ const title = requirePayloadText(payload?.title, pick(lang, "确认建书缺少书名,请重新生成确认卡。", "The book creation confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận khởi tạo sách thiếu tên sách. Vui lòng tạo lại thẻ xác nhận."));
890
897
  tool = createSubAgentTool(args.pipeline, null, args.root, {
891
898
  actionPayload,
892
899
  language: toWritingLanguage(lang),
@@ -908,7 +915,7 @@ async function executeConfirmedProductionAction(args) {
908
915
  const payload = actionPayload?.shortRun;
909
916
  const direction = payload?.direction?.trim() || args.instruction.trim();
910
917
  if (!direction)
911
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认短篇缺少方向,请重新生成确认卡。", "The short fiction confirmation is missing a direction. Regenerate the confirmation card."));
918
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认短篇缺少方向,请重新生成确认卡。", "The short fiction confirmation is missing a direction. Regenerate the confirmation card.", "Xác nhận truyện ngắn thiếu định hướng. Vui lòng tạo lại thẻ xác nhận."));
912
919
  tool = createShortFictionRunTool(args.pipeline, args.root, {
913
920
  actionPayload,
914
921
  language: toWritingLanguage(lang),
@@ -925,7 +932,7 @@ async function executeConfirmedProductionAction(args) {
925
932
  }
926
933
  else if (args.requestedIntent === "write_next") {
927
934
  if (!args.bookId) {
928
- throw new ApiError(400, "BOOK_ID_REQUIRED", pick(lang, "写下一章需要先打开一本书。", "Writing the next chapter requires an active book."));
935
+ throw new ApiError(400, "BOOK_ID_REQUIRED", pick(lang, "写下一章需要先打开一本书。", "Writing the next chapter requires an active book.", "Viết chương tiếp theo cần mở một cuốn sách trước."));
929
936
  }
930
937
  const chapterCount = actionPayload?.writeNext?.chapterCount ?? 1;
931
938
  tool = createSubAgentTool(args.pipeline, args.bookId, args.root, {
@@ -942,7 +949,7 @@ async function executeConfirmedProductionAction(args) {
942
949
  }
943
950
  else if (args.requestedIntent === "generate_cover") {
944
951
  const payload = actionPayload?.generateCover;
945
- const title = requirePayloadText(payload?.title, pick(lang, "确认生成封面缺少标题,请重新生成确认卡。", "The cover generation confirmation is missing a title. Regenerate the confirmation card."));
952
+ const title = requirePayloadText(payload?.title, pick(lang, "确认生成封面缺少标题,请重新生成确认卡。", "The cover generation confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận tạo bìa sách thiếu tiêu đề. Vui lòng tạo lại thẻ xác nhận."));
946
953
  tool = createGenerateCoverTool(args.root, { actionPayload });
947
954
  params = {
948
955
  title,
@@ -954,7 +961,7 @@ async function executeConfirmedProductionAction(args) {
954
961
  }
955
962
  else if (args.requestedIntent === "script_create") {
956
963
  const payload = actionPayload?.scriptCreate;
957
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建剧本缺少标题,请重新生成确认卡。", "The script creation confirmation is missing a title. Regenerate the confirmation card."));
964
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建剧本缺少标题,请重新生成确认卡。", "The script creation confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận tạo kịch bản thiếu tiêu đề. Vui lòng tạo lại thẻ xác nhận."));
958
965
  tool = createScriptCreationTool(args.pipeline, args.root, {
959
966
  actionPayload,
960
967
  language: toWritingLanguage(lang),
@@ -976,7 +983,7 @@ async function executeConfirmedProductionAction(args) {
976
983
  }
977
984
  else if (args.requestedIntent === "storyboard_create") {
978
985
  const payload = actionPayload?.storyboardCreate;
979
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建分镜缺少标题,请重新生成确认卡。", "The storyboard creation confirmation is missing a title. Regenerate the confirmation card."));
986
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建分镜缺少标题,请重新生成确认卡。", "The storyboard creation confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận tạo phân cảnh thiếu tiêu đề. Vui lòng tạo lại thẻ xác nhận."));
980
987
  tool = createStoryboardCreationTool(args.pipeline, args.root, {
981
988
  actionPayload,
982
989
  language: toWritingLanguage(lang),
@@ -999,7 +1006,7 @@ async function executeConfirmedProductionAction(args) {
999
1006
  }
1000
1007
  else if (args.requestedIntent === "interactive_film_create") {
1001
1008
  const payload = actionPayload?.interactiveFilmCreate;
1002
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建互动影游缺少标题,请重新生成确认卡。", "The interactive film confirmation is missing a title. Regenerate the confirmation card."));
1009
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建互动影游缺少标题,请重新生成确认卡。", "The interactive film confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận tạo phim tương tác thiếu tiêu đề. Vui lòng tạo lại thẻ xác nhận."));
1003
1010
  tool = createInteractiveFilmCreationTool(args.pipeline, args.root, {
1004
1011
  actionPayload,
1005
1012
  language: toWritingLanguage(lang),
@@ -1023,9 +1030,9 @@ async function executeConfirmedProductionAction(args) {
1023
1030
  }
1024
1031
  else if (args.requestedIntent === "translation_create") {
1025
1032
  const payload = actionPayload?.translationCreate;
1026
- const filePath = requirePayloadText(payload?.filePath, pick(lang, "确认创建翻译项目缺少文件路径,请重新生成确认卡。", "The translation confirmation is missing a file path. Regenerate the confirmation card."));
1027
- const sourceLanguage = requirePayloadText(payload?.sourceLanguage, pick(lang, "确认创建翻译项目缺少源语言,请重新生成确认卡。", "The translation confirmation is missing a source language. Regenerate the confirmation card."));
1028
- const targetLanguage = requirePayloadText(payload?.targetLanguage, pick(lang, "确认创建翻译项目缺少目标语言,请重新生成确认卡。", "The translation confirmation is missing a target language. Regenerate the confirmation card."));
1033
+ const filePath = requirePayloadText(payload?.filePath, pick(lang, "确认创建翻译项目缺少文件路径,请重新生成确认卡。", "The translation confirmation is missing a file path. Regenerate the confirmation card.", "Xác nhận tạo dự án dịch thuật thiếu đường dẫn tệp. Vui lòng tạo lại thẻ xác nhận."));
1034
+ const sourceLanguage = requirePayloadText(payload?.sourceLanguage, pick(lang, "确认创建翻译项目缺少源语言,请重新生成确认卡。", "The translation confirmation is missing a source language. Regenerate the confirmation card.", "Xác nhận tạo dự án dịch thuật thiếu ngôn ngữ nguồn. Vui lòng tạo lại thẻ xác nhận."));
1035
+ const targetLanguage = requirePayloadText(payload?.targetLanguage, pick(lang, "确认创建翻译项目缺少目标语言,请重新生成确认卡。", "The translation confirmation is missing a target language. Regenerate the confirmation card.", "Xác nhận tạo dự án dịch thuật thiếu ngôn ngữ đích. Vui lòng tạo lại thẻ xác nhận."));
1029
1036
  tool = createTranslationCreateTool(args.root, { actionPayload });
1030
1037
  params = {
1031
1038
  filePath,
@@ -1037,9 +1044,9 @@ async function executeConfirmedProductionAction(args) {
1037
1044
  }
1038
1045
  else if (args.requestedIntent === "fanfic_init") {
1039
1046
  const payload = actionPayload?.fanficCreate;
1040
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建同人缺少书名,请补充后重新确认。", "The fanfiction confirmation is missing a title."));
1047
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建同人缺少书名,请补充后重新确认。", "The fanfiction confirmation is missing a title.", "Xác nhận tạo truyện đồng nhân thiếu tên sách. Vui lòng bổ sung rồi xác nhận lại."));
1041
1048
  if (!payload?.sourceText?.trim() && !payload?.sourcePath?.trim()) {
1042
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "创建同人需要原作资料或上传文件。", "Fanfiction creation requires source material or an uploaded file."));
1049
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "创建同人需要原作资料或上传文件。", "Fanfiction creation requires source material or an uploaded file.", "Tạo truyện đồng nhân cần tài liệu gốc hoặc tệp đã tải lên."));
1043
1050
  }
1044
1051
  tool = createFanficBookTool(args.pipeline, args.root, {
1045
1052
  defaultSkills: productionSkills("longWriting"),
@@ -1059,10 +1066,10 @@ async function executeConfirmedProductionAction(args) {
1059
1066
  }
1060
1067
  else if (args.requestedIntent === "continuation_import") {
1061
1068
  const payload = actionPayload?.continuationImport;
1062
- const sourcePath = requirePayloadText(payload?.sourcePath, pick(lang, "导入续写需要上传文件或章节目录。", "Continuation import requires an uploaded file or chapter directory."));
1069
+ const sourcePath = requirePayloadText(payload?.sourcePath, pick(lang, "导入续写需要上传文件或章节目录。", "Continuation import requires an uploaded file or chapter directory.", "Nhập truyện viết tiếp cần tệp đã tải lên hoặc thư mục chương."));
1063
1070
  const targetBookId = payload?.bookId ?? args.bookId ?? undefined;
1064
1071
  if (!targetBookId && !payload?.title?.trim()) {
1065
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "导入续写需要选择已有书籍或填写新书名。", "Continuation import requires an existing book or a new title."));
1072
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "导入续写需要选择已有书籍或填写新书名。", "Continuation import requires an existing book or a new title.", "Nhập truyện viết tiếp cần chọn sách đã có hoặc nhập tên sách mới."));
1066
1073
  }
1067
1074
  tool = createContinuationImportTool(args.pipeline, args.bookId, args.root, {
1068
1075
  defaultSkills: productionSkills("longWriting"),
@@ -1082,8 +1089,8 @@ async function executeConfirmedProductionAction(args) {
1082
1089
  }
1083
1090
  else if (args.requestedIntent === "spinoff_create") {
1084
1091
  const payload = actionPayload?.spinoffCreate;
1085
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建番外缺少书名。", "The side-story confirmation is missing a title."));
1086
- const parentBookId = requirePayloadText(payload?.parentBookId ?? args.bookId ?? undefined, pick(lang, "创建番外需要指定正传书籍。", "Side-story creation requires a parent book."));
1092
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建番外缺少书名。", "The side-story confirmation is missing a title.", "Xác nhận tạo ngoại truyện thiếu tên sách."));
1093
+ const parentBookId = requirePayloadText(payload?.parentBookId ?? args.bookId ?? undefined, pick(lang, "创建番外需要指定正传书籍。", "Side-story creation requires a parent book.", "Tạo ngoại truyện cần chỉ định sách chính."));
1087
1094
  tool = createSpinoffBookTool(args.pipeline, args.root, {
1088
1095
  defaultSkills: productionSkills("longWriting"),
1089
1096
  });
@@ -1100,10 +1107,10 @@ async function executeConfirmedProductionAction(args) {
1100
1107
  }
1101
1108
  else if (args.requestedIntent === "style_imitation") {
1102
1109
  const payload = actionPayload?.imitationCreate;
1103
- const title = requirePayloadText(payload?.title, pick(lang, "确认创建仿写缺少书名。", "The imitation confirmation is missing a title."));
1104
- const storyIdea = requirePayloadText(payload?.storyIdea, pick(lang, "仿写需要一个原创故事方向。", "Style imitation requires an original story idea."));
1110
+ const title = requirePayloadText(payload?.title, pick(lang, "确认创建仿写缺少书名。", "The imitation confirmation is missing a title.", "Xác nhận tạo tác phẩm phỏng bút thiếu tên sách."));
1111
+ const storyIdea = requirePayloadText(payload?.storyIdea, pick(lang, "仿写需要一个原创故事方向。", "Style imitation requires an original story idea.", "Phỏng bút cần một định hướng câu chuyện gốc."));
1105
1112
  if (!payload?.referenceText?.trim() && !payload?.referencePath?.trim()) {
1106
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "仿写需要参考文本或上传文件。", "Style imitation requires reference text or an uploaded file."));
1113
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "仿写需要参考文本或上传文件。", "Style imitation requires reference text or an uploaded file.", "Phỏng bút cần văn bản tham khảo hoặc tệp đã tải lên."));
1107
1114
  }
1108
1115
  tool = createImitationBookTool(args.pipeline, args.root, {
1109
1116
  defaultSkills: productionSkills("longWriting"),
@@ -1123,7 +1130,7 @@ async function executeConfirmedProductionAction(args) {
1123
1130
  }
1124
1131
  else if (args.requestedIntent === "play_start") {
1125
1132
  const payload = actionPayload?.playStart;
1126
- const title = requirePayloadText(payload?.title, pick(lang, "确认启动互动世界缺少标题,请重新生成确认卡。", "The interactive world start confirmation is missing a title. Regenerate the confirmation card."));
1133
+ const title = requirePayloadText(payload?.title, pick(lang, "确认启动互动世界缺少标题,请重新生成确认卡。", "The interactive world start confirmation is missing a title. Regenerate the confirmation card.", "Xác nhận khởi động thế giới tương tác thiếu tiêu đề. Vui lòng tạo lại thẻ xác nhận."));
1127
1134
  const fallbackScene = [payload?.premise, args.instruction].filter((part) => typeof part === "string" && part.trim().length > 0).join("\n\n");
1128
1135
  const initialScene = payload?.initialScene?.trim() || fallbackScene.trim();
1129
1136
  const confirmedActionPayload = actionPayload
@@ -1167,7 +1174,7 @@ async function executeConfirmedProductionAction(args) {
1167
1174
  else if (args.requestedIntent === "connect_choice") {
1168
1175
  const payload = actionPayload?.connectChoice;
1169
1176
  if (!payload?.node) {
1170
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认连接选择缺少节点数据,请重新生成确认卡。", "The connect-choice confirmation is missing node data. Regenerate the confirmation card."));
1177
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认连接选择缺少节点数据,请重新生成确认卡。", "The connect-choice confirmation is missing node data. Regenerate the confirmation card.", "Xác nhận nối lựa chọn thiếu dữ liệu node. Vui lòng tạo lại thẻ xác nhận."));
1171
1178
  }
1172
1179
  const projectId = payload?.projectId ?? args.bookId;
1173
1180
  if (!projectId)
@@ -1180,7 +1187,7 @@ async function executeConfirmedProductionAction(args) {
1180
1187
  else if (args.requestedIntent === "remove_node") {
1181
1188
  const payload = actionPayload?.removeNode;
1182
1189
  if (!payload?.nodeId) {
1183
- throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认删除节点缺少 nodeId,请重新生成确认卡。", "The remove-node confirmation is missing a nodeId. Regenerate the confirmation card."));
1190
+ throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认删除节点缺少 nodeId,请重新生成确认卡。", "The remove-node confirmation is missing a nodeId. Regenerate the confirmation card.", "Xác nhận xóa node thiếu nodeId. Vui lòng tạo lại thẻ xác nhận."));
1184
1191
  }
1185
1192
  const projectId = payload?.projectId ?? args.bookId;
1186
1193
  if (!projectId)
@@ -1750,7 +1757,7 @@ async function withTimeout(promise, timeoutMs, label, lang = "vi") {
1750
1757
  return await Promise.race([
1751
1758
  promise,
1752
1759
  new Promise((_, reject) => {
1753
- timeout = setTimeout(() => reject(new Error(pick(lang, `${label} 超时(${timeoutMs}ms)`, `${label} timed out (${timeoutMs}ms)`))), timeoutMs);
1760
+ timeout = setTimeout(() => reject(new Error(pick(lang, `${label} 超时(${timeoutMs}ms)`, `${label} timed out (${timeoutMs}ms)`, `${label} đã hết thời gian chờ (${timeoutMs}ms)`))), timeoutMs);
1754
1761
  }),
1755
1762
  ]);
1756
1763
  }
@@ -1769,42 +1776,42 @@ function formatServiceProbeError(args) {
1769
1776
  : "";
1770
1777
  const protocol = args.apiFormat === "responses" ? "Responses" : "Chat / Completions";
1771
1778
  const streamSuffix = typeof args.stream === "boolean"
1772
- ? pick(lang, `,${args.stream ? "流式" : "非流式"}`, `, ${args.stream ? "streaming" : "non-streaming"}`)
1779
+ ? pick(lang, `,${args.stream ? "流式" : "非流式"}`, `, ${args.stream ? "streaming" : "non-streaming"}`, `, ${args.stream ? "streaming" : "không streaming"}`)
1773
1780
  : "";
1774
1781
  const context = [
1775
- pick(lang, `服务商:${args.label ?? args.service}`, `Service: ${args.label ?? args.service}`),
1776
- pick(lang, `测试模型:${args.model ?? "未确定"}`, `Test model: ${args.model ?? "undetermined"}`),
1777
- pick(lang, `协议:${protocol}${streamSuffix}`, `Protocol: ${protocol}${streamSuffix}`),
1778
- pick(lang, `Base URL:${args.baseUrl}`, `Base URL: ${args.baseUrl}`),
1782
+ pick(lang, `服务商:${args.label ?? args.service}`, `Service: ${args.label ?? args.service}`, `Dịch vụ: ${args.label ?? args.service}`),
1783
+ pick(lang, `测试模型:${args.model ?? "未确定"}`, `Test model: ${args.model ?? "undetermined"}`, `Model kiểm tra: ${args.model ?? "chưa xác định"}`),
1784
+ pick(lang, `协议:${protocol}${streamSuffix}`, `Protocol: ${protocol}${streamSuffix}`, `Giao thức: ${protocol}${streamSuffix}`),
1785
+ pick(lang, `Base URL:${args.baseUrl}`, `Base URL: ${args.baseUrl}`, `Base URL: ${args.baseUrl}`),
1779
1786
  ].join("\n");
1780
- const upstreamPrefix = (detail) => pick(lang, `\n上游返回:${detail}`, `\nUpstream response: ${detail}`);
1787
+ const upstreamPrefix = (detail) => pick(lang, `\n上游返回:${detail}`, `\nUpstream response: ${detail}`, `\nPhản hồi upstream: ${detail}`);
1781
1788
  if (args.service === "google") {
1782
1789
  return [
1783
- pick(lang, "Google Gemini 测试连接失败。", "Google Gemini connection test failed."),
1790
+ pick(lang, "Google Gemini 测试连接失败。", "Google Gemini connection test failed.", "Kiểm tra kết nối Google Gemini thất bại."),
1784
1791
  context,
1785
1792
  "",
1786
- pick(lang, "请优先检查:", "Check these first:"),
1787
- pick(lang, "1. API Key 是否来自 Google AI Studio 的 Gemini API key,而不是 OAuth、Vertex AI 或其它 Google 服务凭据。", "1. The API Key is a Gemini API key from Google AI Studio, not an OAuth, Vertex AI, or other Google service credential."),
1788
- pick(lang, "2. 该 key 所属项目是否已启用 Gemini API,并且没有被限制到其它 API、来源或服务。", "2. The key's project has the Gemini API enabled and is not restricted to other APIs, origins, or services."),
1789
- pick(lang, "3. 当前地区/账号是否允许访问 Gemini API。", "3. Your region/account is allowed to access the Gemini API."),
1790
- pick(lang, "4. 如果 key 曾经泄露,请在 AI Studio 重新生成后再保存。", "4. If the key was ever leaked, regenerate it in AI Studio before saving."),
1793
+ pick(lang, "请优先检查:", "Check these first:", "Hãy kiểm tra trước:"),
1794
+ pick(lang, "1. API Key 是否来自 Google AI Studio 的 Gemini API key,而不是 OAuth、Vertex AI 或其它 Google 服务凭据。", "1. The API Key is a Gemini API key from Google AI Studio, not an OAuth, Vertex AI, or other Google service credential.", "1. API Key có phải là Gemini API key từ Google AI Studio không, không phải OAuth, Vertex AI, hay thông tin xác thực dịch vụ Google khác."),
1795
+ pick(lang, "2. 该 key 所属项目是否已启用 Gemini API,并且没有被限制到其它 API、来源或服务。", "2. The key's project has the Gemini API enabled and is not restricted to other APIs, origins, or services.", "2. Dự án chứa key này đã bật Gemini API chưa, và không bị giới hạn cho API, nguồn hoặc dịch vụ khác."),
1796
+ pick(lang, "3. 当前地区/账号是否允许访问 Gemini API。", "3. Your region/account is allowed to access the Gemini API.", "3. Khu vực/tài khoản hiện tại có được phép truy cập Gemini API không."),
1797
+ pick(lang, "4. 如果 key 曾经泄露,请在 AI Studio 重新生成后再保存。", "4. If the key was ever leaked, regenerate it in AI Studio before saving.", "4. Nếu key từng bị lộ, hãy tạo lại trong AI Studio rồi mới lưu."),
1791
1798
  upstreamDetail ? upstreamPrefix(upstreamDetail) : "",
1792
1799
  ].filter(Boolean).join("\n");
1793
1800
  }
1794
1801
  if (args.service === "moonshot" || args.service === "kimiCodingPlan" || args.service === "kimicode") {
1795
1802
  return [
1796
- pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`),
1803
+ pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`, `Kiểm tra kết nối ${args.label ?? args.service} thất bại.`),
1797
1804
  context,
1798
1805
  "",
1799
- pick(lang, "请优先检查模型是否可用,以及 kimi-k2.x 这类模型是否需要 temperature=1。", "Check first whether the model is available, and whether models like kimi-k2.x require temperature=1."),
1806
+ pick(lang, "请优先检查模型是否可用,以及 kimi-k2.x 这类模型是否需要 temperature=1。", "Check first whether the model is available, and whether models like kimi-k2.x require temperature=1.", "Hãy kiểm tra trước xem model có khả dụng không, và các model như kimi-k2.x có cần temperature=1 không."),
1800
1807
  rawDetail ? upstreamPrefix(rawDetail) : "",
1801
1808
  ].filter(Boolean).join("\n");
1802
1809
  }
1803
1810
  return [
1804
- pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`),
1811
+ pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`, `Kiểm tra kết nối ${args.label ?? args.service} thất bại.`),
1805
1812
  context,
1806
1813
  "",
1807
- pick(lang, "请检查 API Key、模型可用性、账号额度,以及协议类型是否匹配该服务商。", "Check the API Key, model availability, account quota, and whether the protocol type matches this service."),
1814
+ pick(lang, "请检查 API Key、模型可用性、账号额度,以及协议类型是否匹配该服务商。", "Check the API Key, model availability, account quota, and whether the protocol type matches this service.", "Hãy kiểm tra API Key, tính khả dụng của model, hạn mức tài khoản, và giao thức có khớp với dịch vụ này không."),
1808
1815
  rawDetail ? upstreamPrefix(rawDetail) : "",
1809
1816
  ].filter(Boolean).join("\n");
1810
1817
  }
@@ -1825,7 +1832,7 @@ async function fetchModelsFromServiceBaseUrl(serviceId, baseUrl, apiKey, proxyUr
1825
1832
  const body = await res.text().catch(() => "");
1826
1833
  return {
1827
1834
  models: [],
1828
- error: pick(lang, `服务商返回 ${res.status}: ${body.slice(0, 200)}`, `Service returned ${res.status}: ${body.slice(0, 200)}`),
1835
+ error: pick(lang, `服务商返回 ${res.status}: ${body.slice(0, 200)}`, `Service returned ${res.status}: ${body.slice(0, 200)}`, `Dịch vụ trả về ${res.status}: ${body.slice(0, 200)}`),
1829
1836
  authFailed: res.status === 401 || res.status === 403,
1830
1837
  };
1831
1838
  }
@@ -1846,7 +1853,7 @@ function buildBearerAuthHeaders(apiKey, lang = "vi") {
1846
1853
  if (!trimmed)
1847
1854
  return {};
1848
1855
  if (!/^[\x20-\x7e]+$/.test(trimmed)) {
1849
- throw new Error(pick(lang, "API Key 只能包含英文、数字和常见 ASCII 符号,请检查是否误粘贴了中文说明。", "API Key may only contain ASCII letters, digits, and common symbols. Check whether you pasted explanatory text by mistake."));
1856
+ throw new Error(pick(lang, "API Key 只能包含英文、数字和常见 ASCII 符号,请检查是否误粘贴了中文说明。", "API Key may only contain ASCII letters, digits, and common symbols. Check whether you pasted explanatory text by mistake.", "API Key chỉ được chứa chữ cái, số và ký hiệu ASCII thông thường. Hãy kiểm tra xem có dán nhầm văn bản giải thích không."));
1850
1857
  }
1851
1858
  return { Authorization: `Bearer ${trimmed}` };
1852
1859
  }
@@ -1866,7 +1873,7 @@ async function probeServiceCapabilities(args) {
1866
1873
  return {
1867
1874
  ok: false,
1868
1875
  models: [],
1869
- error: modelsResponse.error ?? pick(lang, "API Key 无效或无权访问模型列表。", "API Key is invalid or has no access to the model list."),
1876
+ error: modelsResponse.error ?? pick(lang, "API Key 无效或无权访问模型列表。", "API Key is invalid or has no access to the model list.", "API Key không hợp lệ hoặc không có quyền truy cập danh sách model."),
1870
1877
  };
1871
1878
  }
1872
1879
  const discoveredModels = modelsResponse.models;
@@ -1879,7 +1886,7 @@ async function probeServiceCapabilities(args) {
1879
1886
  return {
1880
1887
  ok: false,
1881
1888
  models: discoveredModels,
1882
- error: pick(lang, "模型列表可访问,但没有发现可用于文本对话的模型。", "The model list is reachable, but no model usable for text chat was found."),
1889
+ error: pick(lang, "模型列表可访问,但没有发现可用于文本对话的模型。", "The model list is reachable, but no model usable for text chat was found.", "Danh sách model truy cập được, nhưng không tìm thấy model nào dùng được cho chat văn bản."),
1883
1890
  };
1884
1891
  }
1885
1892
  return {
@@ -1938,10 +1945,10 @@ async function probeServiceCapabilities(args) {
1938
1945
  return {
1939
1946
  ok: false,
1940
1947
  models: [],
1941
- error: pick(lang, "无法自动确定模型,请先填写可用模型或提供支持 /models 的服务端点。", "Could not determine a model automatically. Fill in an available model first, or provide a service endpoint that supports /models."),
1948
+ error: pick(lang, "无法自动确定模型,请先填写可用模型或提供支持 /models 的服务端点。", "Could not determine a model automatically. Fill in an available model first, or provide a service endpoint that supports /models.", "Không thể tự động xác định model. Hãy điền model khả dụng trước, hoặc cung cấp endpoint dịch vụ hỗ trợ /models."),
1942
1949
  };
1943
1950
  }
1944
- let lastError = modelsResponse.error ?? pick(lang, "自动探测失败", "Automatic probing failed");
1951
+ let lastError = modelsResponse.error ?? pick(lang, "自动探测失败", "Automatic probing failed", "Dò tự động thất bại");
1945
1952
  for (const model of modelCandidates) {
1946
1953
  for (const plan of buildProbePlans(args.preferredApiFormat, args.preferredStream)) {
1947
1954
  const client = createLLMClient({
@@ -2059,7 +2066,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2059
2066
  execution: {
2060
2067
  ...task.execution,
2061
2068
  status: "error",
2062
- error: pick(lang, "任务已中断:Studio 服务在任务运行期间重启,任务未能继续。请重新发起。", "Task interrupted: the Studio server restarted while this task was running. Please start it again."),
2069
+ error: pick(lang, "任务已中断:Studio 服务在任务运行期间重启,任务未能继续。请重新发起。", "Task interrupted: the Studio server restarted while this task was running. Please start it again.", "Tác vụ bị gián đoạn: máy chủ Studio đã khởi động lại trong khi tác vụ đang chạy nên không thể tiếp tục. Vui lòng khởi chạy lại."),
2063
2070
  completedAt,
2064
2071
  },
2065
2072
  };
@@ -2871,7 +2878,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2871
2878
  const env = await readEffectiveEnvConfigValues(root);
2872
2879
  if (!env || !env.values.apiKey) {
2873
2880
  return c.json({
2874
- error: pick(await currentProjectLanguage(), "未检测到可导入的 LLM 环境变量配置,或缺少 INKOS_LLM_API_KEY。", "No importable LLM environment variable configuration was detected, or INKOS_LLM_API_KEY is missing."),
2881
+ error: pick(await currentProjectLanguage(), "未检测到可导入的 LLM 环境变量配置,或缺少 INKOS_LLM_API_KEY。", "No importable LLM environment variable configuration was detected, or INKOS_LLM_API_KEY is missing.", "Không phát hiện cấu hình biến môi trường LLM để nhập, hoặc thiếu INKOS_LLM_API_KEY."),
2875
2882
  }, 400);
2876
2883
  }
2877
2884
  const config = await loadRawConfig(root);
@@ -2921,7 +2928,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2921
2928
  }
2922
2929
  if (body.configSource === "env") {
2923
2930
  return c.json({
2924
- error: pick(await currentProjectLanguage(), "Studio 运行时不支持切换到 env;env 只在 CLI/daemon/部署运行时作为覆盖层使用。", "The Studio runtime does not support switching to env; env only acts as an override layer in the CLI/daemon/deployment runtimes."),
2931
+ error: pick(await currentProjectLanguage(), "Studio 运行时不支持切换到 env;env 只在 CLI/daemon/部署运行时作为覆盖层使用。", "The Studio runtime does not support switching to env; env only acts as an override layer in the CLI/daemon/deployment runtimes.", "Runtime Studio không hỗ trợ chuyển sang env; env chỉ hoạt động như lớp ghi đè trong runtime CLI/daemon/deployment."),
2925
2932
  }, 400);
2926
2933
  }
2927
2934
  if (body.configSource !== undefined) {
@@ -2974,7 +2981,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2974
2981
  const baseUrl = normalizeCoverBaseUrl(requestedBaseUrl);
2975
2982
  if (requestedBaseUrl && !baseUrl) {
2976
2983
  return c.json({
2977
- error: pick(await currentProjectLanguage(), "封面 Base URL 必须是有效的 HTTP(S) 地址,且不能包含账号、查询参数或锚点。", "Cover Base URL must be a valid HTTP(S) URL without credentials, query parameters, or fragments."),
2984
+ error: pick(await currentProjectLanguage(), "封面 Base URL 必须是有效的 HTTP(S) 地址,且不能包含账号、查询参数或锚点。", "Cover Base URL must be a valid HTTP(S) URL without credentials, query parameters, or fragments.", "Base URL bìa sách phải là địa chỉ HTTP(S) hợp lệ, không chứa thông tin đăng nhập, tham số truy vấn hoặc fragment."),
2978
2985
  }, 400);
2979
2986
  }
2980
2987
  const config = await loadRawConfig(root);
@@ -3005,7 +3012,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3005
3012
  const trimmedKey = body.apiKey?.trim() ?? "";
3006
3013
  if (trimmedKey && !isHeaderSafeApiKey(trimmedKey)) {
3007
3014
  return c.json({
3008
- error: pick(await currentProjectLanguage(), "API Key 包含不能放入 HTTP Authorization header 的字符,请只粘贴原始密钥。", "API Key contains characters that cannot go into an HTTP Authorization header. Paste only the raw key."),
3015
+ error: pick(await currentProjectLanguage(), "API Key 包含不能放入 HTTP Authorization header 的字符,请只粘贴原始密钥。", "API Key contains characters that cannot go into an HTTP Authorization header. Paste only the raw key.", "API Key chứa ký tự không thể đặt vào HTTP Authorization header. Vui lòng chỉ dán key gốc."),
3009
3016
  }, 400);
3010
3017
  }
3011
3018
  const secrets = await loadSecrets(root);
@@ -3048,7 +3055,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3048
3055
  if (!resolvedBaseUrl) {
3049
3056
  return c.json({
3050
3057
  ok: false,
3051
- error: pick(language, `未知服务商: ${service}`, `Unknown service: ${service}`),
3058
+ error: pick(language, `未知服务商: ${service}`, `Unknown service: ${service}`, `Dịch vụ không xác định: ${service}`),
3052
3059
  }, 400);
3053
3060
  }
3054
3061
  const baseService = isCustomServiceId(service) ? "custom" : service;
@@ -3059,7 +3066,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3059
3066
  if (!apiKey?.trim() && !apiKeyOptional) {
3060
3067
  return c.json({
3061
3068
  ok: false,
3062
- error: pick(language, "API Key 不能为空", "API Key must not be empty"),
3069
+ error: pick(language, "API Key 不能为空", "API Key must not be empty", "API Key không được để trống"),
3063
3070
  }, 400);
3064
3071
  }
3065
3072
  const rawConfig = await loadRawConfig(root).catch(() => ({}));
@@ -3075,7 +3082,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3075
3082
  language,
3076
3083
  });
3077
3084
  // B12: 升级响应 shape 为 { probe, chat, ... },同时保留老字段供 UI 过渡期兼容
3078
- const connectionFailed = pick(language, "连接失败", "Connection failed");
3085
+ const connectionFailed = pick(language, "连接失败", "Connection failed", "Kết nối thất bại");
3079
3086
  const probeStatus = {
3080
3087
  ok: probe.ok,
3081
3088
  models: probe.models?.length ?? 0,
@@ -3114,7 +3121,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3114
3121
  if (!isHeaderSafeApiKey(trimmedKey)) {
3115
3122
  return c.json({
3116
3123
  ok: false,
3117
- error: pick(await currentProjectLanguage(), "API Key 只能包含可放进 HTTP Authorization header 的非空白 ASCII 字符;请不要粘贴连接失败提示或诊断文本。", "API Key may only contain non-whitespace ASCII characters that fit in an HTTP Authorization header; do not paste connection failure hints or diagnostic text."),
3124
+ error: pick(await currentProjectLanguage(), "API Key 只能包含可放进 HTTP Authorization header 的非空白 ASCII 字符;请不要粘贴连接失败提示或诊断文本。", "API Key may only contain non-whitespace ASCII characters that fit in an HTTP Authorization header; do not paste connection failure hints or diagnostic text.", "API Key chỉ được chứa ký tự ASCII không khoảng trắng phù hợp với HTTP Authorization header; không dán gợi ý lỗi kết nối hay văn bản chẩn đoán."),
3118
3125
  }, 400);
3119
3126
  }
3120
3127
  secrets.services[service] = { apiKey: trimmedKey };
@@ -3872,8 +3879,8 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3872
3879
  const msg = e?.message ?? String(e);
3873
3880
  if (/API key/i.test(msg)) {
3874
3881
  return c.json({
3875
- error: pick(language, `请先为 ${reqService} 配置 API Key`, `Configure an API Key for ${reqService} first`),
3876
- response: pick(language, `请先在模型配置中为 ${reqService} 填写 API Key,然后再试。`, `Fill in an API Key for ${reqService} in the model settings, then try again.`),
3882
+ error: pick(language, `请先为 ${reqService} 配置 API Key`, `Configure an API Key for ${reqService} first`, `Vui lòng cấu hình API Key cho ${reqService} trước`),
3883
+ response: pick(language, `请先在模型配置中为 ${reqService} 填写 API Key,然后再试。`, `Fill in an API Key for ${reqService} in the model settings, then try again.`, `Vui lòng điền API Key cho ${reqService} trong cấu hình model rồi thử lại.`),
3877
3884
  }, 400);
3878
3885
  }
3879
3886
  throw e;
@@ -3958,7 +3965,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3958
3965
  }));
3959
3966
  if (confirmedIntent && confirmedTaskId) {
3960
3967
  const productionTaskBusyResponse = () => {
3961
- const message = pick(surfaceLanguage, "当前会话已有一个生产任务在运行,请等它完成,或先用停止按钮结束它,再发起新任务。", "A production task is already running in this session. Wait for it to finish, or stop it first, then start a new task.");
3968
+ const message = pick(surfaceLanguage, "当前会话已有一个生产任务在运行,请等它完成,或先用停止按钮结束它,再发起新任务。", "A production task is already running in this session. Wait for it to finish, or stop it first, then start a new task.", "Phiên này đã có một tác vụ sản xuất đang chạy. Vui lòng đợi hoàn tất, hoặc dùng nút dừng để kết thúc trước khi bắt đầu tác vụ mới.");
3962
3969
  return c.json({
3963
3970
  error: { code: "PRODUCTION_TASK_ALREADY_RUNNING", message },
3964
3971
  response: message,
@@ -4029,7 +4036,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4029
4036
  createdBookId = resolveCreatedBookIdFromToolExecs([exec]);
4030
4037
  if (createdBookId) {
4031
4038
  if (!await completeBookExists(join(root, "books", createdBookId))) {
4032
- const message = pick(surfaceLanguage, "创作工具返回了建书结果,但磁盘上的书籍工件不完整。", "The creation tool returned a book result, but the on-disk book artifact is incomplete.");
4039
+ const message = pick(surfaceLanguage, "创作工具返回了建书结果,但磁盘上的书籍工件不完整。", "The creation tool returned a book result, but the on-disk book artifact is incomplete.", "Công cụ sáng tác đã trả về kết quả khởi tạo sách, nhưng tệp tin sách trên đĩa không đầy đủ.");
4033
4040
  bookCreateStatus.set(createdBookId, { status: "error", error: message });
4034
4041
  broadcast("book:error", { bookId: createdBookId, sessionId: bookSession.sessionId, error: message });
4035
4042
  throw new ApiError(500, "BOOK_CREATION_INCOMPLETE", message);
@@ -4054,7 +4061,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4054
4061
  });
4055
4062
  }
4056
4063
  }
4057
- const responseText = exec.result ?? pick(surfaceLanguage, "已完成。", "Done.");
4064
+ const responseText = exec.result ?? pick(surfaceLanguage, "已完成。", "Done.", "Đã hoàn thành.");
4058
4065
  const responseForUser = suppressManualTextForTool(exec) ? "" : responseText;
4059
4066
  // 指令已在任务开始时写入 transcript,这里只补助手工具消息。
4060
4067
  await appendSessionMessagesUnlessDeleted(root, bookSession.sessionId, [
@@ -4331,7 +4338,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4331
4338
  details: { toolExecutions: collectedToolExecs },
4332
4339
  });
4333
4340
  }
4334
- const emptyMessage = pick(language, "模型未返回文本内容。请检查协议类型(chat/responses)、流式开关或上游服务兼容性。", "The model returned no text content. Check the protocol type (chat/responses), the streaming switch, or upstream service compatibility.");
4341
+ const emptyMessage = pick(language, "模型未返回文本内容。请检查协议类型(chat/responses)、流式开关或上游服务兼容性。", "The model returned no text content. Check the protocol type (chat/responses), the streaming switch, or upstream service compatibility.", "Model không trả về nội dung văn bản. Hãy kiểm tra loại giao thức (chat/responses), chế độ streaming, hoặc khả năng tương thích của dịch vụ upstream.");
4335
4342
  if (resolveCreatedBookIdFromToolExecs(collectedToolExecs)) {
4336
4343
  await finalizeCreatedBook();
4337
4344
  }
@@ -4368,9 +4375,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4368
4375
  return c.json({
4369
4376
  error: {
4370
4377
  code: "AGENT_BUSY",
4371
- message: pick(language, "正在处理中,请等待当前操作完成", "Still processing. Wait for the current operation to finish"),
4378
+ message: pick(language, "正在处理中,请等待当前操作完成", "Still processing. Wait for the current operation to finish", "Đang xử lý, vui lòng chờ thao tác hiện tại hoàn tất"),
4372
4379
  },
4373
- response: pick(language, "正在处理中,请等待当前操作完成后再发送。", "Still processing. Wait for the current operation to finish before sending again."),
4380
+ response: pick(language, "正在处理中,请等待当前操作完成后再发送。", "Still processing. Wait for the current operation to finish before sending again.", "Đang xử lý, vui lòng chờ thao tác hiện tại hoàn tất rồi gửi lại."),
4374
4381
  }, 429);
4375
4382
  }
4376
4383
  const failure = formatAgentFailure(msg, language);
@@ -4955,7 +4962,12 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4955
4962
  return c.json({ error: "text is required" }, 400);
4956
4963
  try {
4957
4964
  const { analyzeStyle } = await import("@kienmatu/inkos-core");
4958
- const profile = analyzeStyle(text, sourceName ?? "unknown");
4965
+ // Infer the language from the sample itself. Without this the analyzer
4966
+ // defaults to "zh", which splits sentences on 。!?, counts characters
4967
+ // instead of words, takes the first TWO CHARACTERS as an opening pattern,
4968
+ // and labels counts with 次/处 — so an English sample came back as one
4969
+ // giant sentence with patterns like "Br...(15次)".
4970
+ const profile = analyzeStyle(text, sourceName ?? "unknown", inferLanguage(text));
4959
4971
  return c.json(profile);
4960
4972
  }
4961
4973
  catch (e) {