@actalk/inkos-studio 1.6.3 → 1.7.0
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.
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +402 -113
- package/dist/api/server.js.map +1 -1
- package/dist/assets/FilmWizard-DMFBbaXo.js +1 -0
- package/dist/assets/FlowView-C1NMbKwK.js +7 -0
- package/dist/assets/{_baseUniq-B-ZYRDWx.js → _baseUniq-DSlQQdBD.js} +1 -1
- package/dist/assets/{arc-Ciw3FGtr.js → arc-Dns2Uhcf.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-mC_rz2X6.js → architectureDiagram-Q4EWVU46-C4o80QfP.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-D_GMBRzY.js → blockDiagram-DXYQGD6D-v8Jrngon.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CvmmAOg5.js → c4Diagram-AHTNJAMY-B1Fujpc3.js} +1 -1
- package/dist/assets/channel-DYtAlgfX.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BPr0k8sP.js → chunk-4BX2VUAB-BaFvLxj9.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-D-Y1Qj_T.js → chunk-4TB4RGXK-B-9uyAsr.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bug_ufgD.js → chunk-55IACEB6-DhZyrHsN.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-C7tD8L3k.js → chunk-EDXVE4YY-OD0vcvaC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BU7HNA1K.js → chunk-FMBD7UC4-Dp2GQRr5.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-DQ_CIv08.js → chunk-OYMX7WX6-D3r5Nwmb.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-HODyXosI.js → chunk-QZHKN3VN-DnYd3GKf.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-7Y8K_8MD.js → chunk-YZCP3GAM-DYexb8Gp.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DQWMsmEX.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DQWMsmEX.js +1 -0
- package/dist/assets/clone-Cax418xZ.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-D2kYbpZy.js → cose-bilkent-S5V4N54A-CVl0lUPg.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-BlyZQ0n0.js → dagre-KV5264BT-BjmDwv13.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-C3lJ6ObZ.js → diagram-5BDNPKRD-CPHkNiE-.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-hx-WGqJX.js → diagram-G4DWMVQ6-BhmoLr3O.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CdI5Z_ZJ.js → diagram-MMDJMWI5-D16xeq7f.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BmtYRsNe.js → diagram-TYMM5635-BaELRM0R.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-DPokxoLy.js → erDiagram-SMLLAGMA-B2aLymvv.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-DFnf6GAZ.js → flowDiagram-DWJPFMVM-DdKH_wgd.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-ymFoiBFD.js → ganttDiagram-T4ZO3ILL-B6bjg7qJ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BOAZqylM.js → gitGraphDiagram-UUTBAWPF-CY2CyY-W.js} +1 -1
- package/dist/assets/{graph-BMumGBmN.js → graph-C0NBXw-9.js} +1 -1
- package/dist/assets/{highlighted-body-OFNGDK62-DRX5PypI.js → highlighted-body-OFNGDK62-CkWd2EdN.js} +1 -1
- package/dist/assets/index-BMp83egq.css +1 -0
- package/dist/assets/{index-MiIZL4f3.js → index-Bx0bFhkr.js} +326 -316
- package/dist/assets/{infoDiagram-42DDH7IO-D0iCgAlu.js → infoDiagram-42DDH7IO-D0Q7gFDt.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-WtqHTEzb.js → ishikawaDiagram-UXIWVN3A-CrrH-P2c.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-DUlKzcQ-.js → journeyDiagram-VCZTEJTY-C0fvpM7W.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-COYae0oO.js → kanban-definition-6JOO6SKY-CMM-2rp8.js} +1 -1
- package/dist/assets/{layout-Et_h2gsr.js → layout-B2QGzq_V.js} +1 -1
- package/dist/assets/{linear-DY_vot7D.js → linear-BQ-iBxey.js} +1 -1
- package/dist/assets/{min-CImNjYIc.js → min-D505DuPN.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-zOJ7uKnl.js → mindmap-definition-QFDTVHPH-CYRf42pX.js} +2 -2
- package/dist/assets/{pieDiagram-DEJITSTG-BftexWKs.js → pieDiagram-DEJITSTG-CC8K2Mwc.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CHmM6MYD.js → quadrantDiagram-34T5L4WZ-DPopTEAi.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-eMzrziUe.js → requirementDiagram-MS252O5E-C3ewlVOB.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B-Bgmxmu.js → sankeyDiagram-XADWPNL6-B_OEE-2a.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-cFT-__W_.js → sequenceDiagram-FGHM5R23-DOcJot11.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CVnHwqCo.js → stateDiagram-FHFEXIEX-CxQZgNz4.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-D2VCh5-K.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CKbNRuf9.js → timeline-definition-GMOUNBTQ-ZC7OAV2y.js} +1 -1
- package/dist/assets/{value-IOfiG7E7.js → value-Cr6rlNd8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-BDoxftwQ.js → vennDiagram-DHZGUBPP-ClWvcTEE.js} +4 -4
- package/dist/assets/{wardley-RL74JXVD-5TXAieIJ.js → wardley-RL74JXVD-BGm2zW6Q.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-CFpD2tIy.js → wardleyDiagram-NUSXRM2D-BKVs8NMK.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-cP5C2vcw.js → xychartDiagram-5P7HB3ND-DTIdAaRC.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/dist/assets/FilmWizard-C_1W4acq.js +0 -1
- package/dist/assets/FlowView-DvrwPvo2.js +0 -7
- package/dist/assets/channel-F10GB4f_.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BqBb7BDW.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BqBb7BDW.js +0 -1
- package/dist/assets/clone-DOQmcAsj.js +0 -1
- package/dist/assets/index-BZMnkfN7.css +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-nhhJvKGq.js +0 -1
package/dist/api/server.js
CHANGED
|
@@ -3,54 +3,81 @@ import { cors } from "hono/cors";
|
|
|
3
3
|
import { streamSSE } from "hono/streaming";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { gzipSync } from "node:zlib";
|
|
6
|
-
import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, listBookSessions, loadBookSession, appendManualSessionMessages, createAndPersistBookSession, renameBookSession, deleteBookSession, migrateBookSession, SessionAlreadyMigratedError, abortAgentSession, runAgentSession, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, guessServiceFromBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, listModelsForService, isApiKeyOptionalForEndpoint, getAllEndpoints, probeModelsFromUpstream, fetchWithProxy, chatCompletion, buildExportArtifact, evaluateBookQuality, ConsolidatorAgent, DetectionConfigSchema, ResearchSearchConfigSchema, InputGovernanceModeSchema, GLOBAL_ENV_PATH, COVER_PROVIDER_PRESETS, createPlayDB, PlayStore, buildPlayEntityImagePrompt, buildPlaySceneImagePrompt, generatePlayImage, readPlayImageManifest, readPlayImageSettings, writePlayImageSettings, Scheduler, coverSecretKey, resolveCoverProviderPreset, SessionKindSchema, isExplicitWriteChapterCommand, isUsablePlayInitialScene, isWriteNextInstruction, normalizeActionSource as normalizeCoreActionSource, normalizeActionPayload as normalizeCoreActionPayload, normalizePlayMode as normalizeCorePlayMode, normalizeRequestedIntent as normalizeCoreRequestedIntent, normalizeSkillIdList as normalizeCoreSkillIdList, inferLanguage, createSkillRegistry, loadConfiguredCapabilitySkills, CapabilitySkillManifestSchema, getBuiltinPrompt, listBuiltinPromptPacks, listBuiltinPrompts, loadPromptPackPrompt, promptOverridePath, createGenerateCoverTool, createInteractiveFilmCreationTool, createPlayStartTool, createScriptCreationTool, createShortFictionRunTool, createStoryboardCreationTool, createSubAgentTool, createDraftStructureTool, createConnectChoiceTool, createRemoveNodeTool, filmLLMDepsFromClient, applyGraphDelta, loadStoryGraph, reviewStoryGraph, exportInk, buildPlayableHtml, analyzeEmotionalArcs, analyzePathDistribution, generateNodeImage, defaultNodeImageDeps, } from "@actalk/inkos-core";
|
|
6
|
+
import { StateManager, PipelineRunner, createLLMClient, createLogger, createInteractionToolsFromDeps, computeAnalytics, loadProjectConfig, loadProjectSession, processProjectInteractionRequest, resolveSessionActiveBook, listBookSessions, loadBookSession, appendManualSessionMessages, createAndPersistBookSession, renameBookSession, deleteBookSession, migrateBookSession, SessionAlreadyMigratedError, abortAgentSession, runAgentSession, resolveServicePreset, resolveServiceProviderFamily, resolveServiceModelsBaseUrl, guessServiceFromBaseUrl, resolveServiceModel, loadSecrets, saveSecrets, listModelsForService, isApiKeyOptionalForEndpoint, getAllEndpoints, probeModelsFromUpstream, fetchWithProxy, chatCompletion, buildExportArtifact, evaluateBookQuality, ConsolidatorAgent, DetectionConfigSchema, ResearchSearchConfigSchema, InputGovernanceModeSchema, GLOBAL_ENV_PATH, COVER_PROVIDER_PRESETS, createPlayDB, PlayStore, buildPlayEntityImagePrompt, buildPlaySceneImagePrompt, generatePlayImage, readPlayImageManifest, readPlayImageSettings, writePlayImageSettings, Scheduler, coverSecretKey, resolveCoverProviderPreset, SessionKindSchema, isExplicitWriteChapterCommand, isUsablePlayInitialScene, isWriteNextInstruction, normalizeActionSource as normalizeCoreActionSource, normalizeActionPayload as normalizeCoreActionPayload, normalizePlayMode as normalizeCorePlayMode, normalizeRequestedIntent as normalizeCoreRequestedIntent, normalizeSkillIdList as normalizeCoreSkillIdList, inferLanguage, createSkillRegistry, loadConfiguredCapabilitySkills, CapabilitySkillManifestSchema, getBuiltinPrompt, listBuiltinPromptPacks, listBuiltinPrompts, loadPromptPackPrompt, promptOverridePath, toPosixPath, createGenerateCoverTool, createInteractiveFilmCreationTool, createPlayStartTool, createScriptCreationTool, createShortFictionRunTool, createStoryboardCreationTool, createTranslationCreateTool, createSubAgentTool, createDraftStructureTool, createConnectChoiceTool, createRemoveNodeTool, createLLMTranslationModel, createTranslationProjectFromFile, loadTranslationChapter, loadTranslationManifest, runTranslationProject, writeTranslationExport, filmLLMDepsFromClient, applyGraphDelta, loadStoryGraph, reviewStoryGraph, exportInk, buildPlayableHtml, analyzeEmotionalArcs, analyzePathDistribution, generateNodeImage, defaultNodeImageDeps, } from "@actalk/inkos-core";
|
|
7
7
|
import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { isSafeBookId } from "./safety.js";
|
|
10
10
|
import { ApiError } from "./errors.js";
|
|
11
11
|
import { buildStudioBookConfig } from "./book-create.js";
|
|
12
|
-
|
|
12
|
+
function normalizeStudioLanguage(value) {
|
|
13
|
+
return value === "en" ? "en" : "zh";
|
|
14
|
+
}
|
|
15
|
+
function pick(lang, zh, en) {
|
|
16
|
+
return lang === "en" ? en : zh;
|
|
17
|
+
}
|
|
13
18
|
const PIPELINE_STAGES = {
|
|
14
19
|
writer: [
|
|
15
|
-
"准备章节输入", "
|
|
16
|
-
"
|
|
17
|
-
"
|
|
20
|
+
{ zh: "准备章节输入", en: "Prepare chapter input" },
|
|
21
|
+
{ zh: "撰写章节草稿", en: "Write chapter draft" },
|
|
22
|
+
{ zh: "落盘最终章节", en: "Save final chapter" },
|
|
23
|
+
{ zh: "生成最终真相文件", en: "Generate final truth files" },
|
|
24
|
+
{ zh: "校验真相文件变更", en: "Validate truth file changes" },
|
|
25
|
+
{ zh: "同步记忆索引", en: "Sync memory index" },
|
|
26
|
+
{ zh: "更新章节索引与快照", en: "Update chapter index and snapshot" },
|
|
18
27
|
],
|
|
19
28
|
architect: [
|
|
20
|
-
"生成基础设定", "
|
|
21
|
-
"
|
|
29
|
+
{ zh: "生成基础设定", en: "Generate foundation" },
|
|
30
|
+
{ zh: "保存书籍配置", en: "Save book config" },
|
|
31
|
+
{ zh: "写入基础设定文件", en: "Write foundation files" },
|
|
32
|
+
{ zh: "初始化控制文档", en: "Initialize control documents" },
|
|
33
|
+
{ zh: "创建初始快照", en: "Create initial snapshot" },
|
|
22
34
|
],
|
|
23
35
|
reviser: [
|
|
24
|
-
"加载修订上下文", "
|
|
25
|
-
"
|
|
36
|
+
{ zh: "加载修订上下文", en: "Load revision context" },
|
|
37
|
+
{ zh: "修订章节", en: "Revise chapter" },
|
|
38
|
+
{ zh: "落盘修订结果", en: "Save revision result" },
|
|
39
|
+
{ zh: "更新索引与快照", en: "Update index and snapshot" },
|
|
26
40
|
],
|
|
27
|
-
auditor: ["审计章节"],
|
|
41
|
+
auditor: [{ zh: "审计章节", en: "Audit chapter" }],
|
|
28
42
|
};
|
|
43
|
+
function pipelineStages(agent, lang = "zh") {
|
|
44
|
+
return PIPELINE_STAGES[agent]?.map((stage) => pick(lang, stage.zh, stage.en));
|
|
45
|
+
}
|
|
29
46
|
function attachmentDisposition(fileName) {
|
|
30
47
|
const safeAscii = fileName.replace(/[^A-Za-z0-9._-]+/g, "_") || "download";
|
|
31
48
|
return `attachment; filename="${safeAscii}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
|
|
32
49
|
}
|
|
33
50
|
const AGENT_LABELS = {
|
|
34
|
-
architect:
|
|
35
|
-
|
|
51
|
+
architect: { zh: "建书", en: "Book setup" },
|
|
52
|
+
writer: { zh: "写作", en: "Writing" },
|
|
53
|
+
auditor: { zh: "审计", en: "Audit" },
|
|
54
|
+
reviser: { zh: "修订", en: "Revision" },
|
|
55
|
+
exporter: { zh: "导出", en: "Export" },
|
|
36
56
|
};
|
|
37
57
|
const TOOL_LABELS = {
|
|
38
|
-
read:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
read: { zh: "读取文件", en: "Read file" },
|
|
59
|
+
edit: { zh: "编辑文件", en: "Edit file" },
|
|
60
|
+
grep: { zh: "搜索", en: "Search" },
|
|
61
|
+
ls: { zh: "列目录", en: "List directory" },
|
|
62
|
+
propose_action: { zh: "确认动作", en: "Confirm action" },
|
|
63
|
+
short_fiction_run: { zh: "短篇生产", en: "Short fiction" },
|
|
64
|
+
script_create: { zh: "剧本创作", en: "Script creation" },
|
|
65
|
+
storyboard_create: { zh: "分镜创作", en: "Storyboard creation" },
|
|
66
|
+
interactive_film_create: { zh: "互动影游", en: "Interactive film" },
|
|
67
|
+
translation_create: { zh: "翻译项目", en: "Translation" },
|
|
68
|
+
generate_cover: { zh: "生成封面", en: "Cover generation" },
|
|
69
|
+
play_edit: { zh: "编辑互动世界", en: "Edit interactive world" },
|
|
70
|
+
play_start: { zh: "启动互动世界", en: "Start interactive world" },
|
|
71
|
+
play_revise: { zh: "重做互动回合", en: "Redo interactive turn" },
|
|
72
|
+
play_step: { zh: "推进互动世界", en: "Advance interactive world" },
|
|
49
73
|
};
|
|
50
|
-
function resolveToolLabel(tool, agent) {
|
|
51
|
-
if (tool === "sub_agent" && agent)
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
function resolveToolLabel(tool, agent, lang = "zh") {
|
|
75
|
+
if (tool === "sub_agent" && agent) {
|
|
76
|
+
const label = AGENT_LABELS[agent];
|
|
77
|
+
return label ? pick(lang, label.zh, label.en) : agent;
|
|
78
|
+
}
|
|
79
|
+
const label = TOOL_LABELS[tool];
|
|
80
|
+
return label ? pick(lang, label.zh, label.en) : tool;
|
|
54
81
|
}
|
|
55
82
|
function summarizeResult(result) {
|
|
56
83
|
if (typeof result === "string")
|
|
@@ -189,8 +216,8 @@ function normalizeApiBookId(value, fieldName) {
|
|
|
189
216
|
}
|
|
190
217
|
return bookId;
|
|
191
218
|
}
|
|
192
|
-
function nonTextModelMessage(modelId) {
|
|
193
|
-
return `模型 ${modelId} 不适合文本聊天/写作。请在模型选择器中改用文本模型,例如 gemini-2.5-flash、gemini-2.5-pro 或对应服务的 chat
|
|
219
|
+
function nonTextModelMessage(modelId, lang = "zh") {
|
|
220
|
+
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.`);
|
|
194
221
|
}
|
|
195
222
|
function extractToolError(result) {
|
|
196
223
|
if (typeof result === "string")
|
|
@@ -354,6 +381,7 @@ function normalizeStudioSkillId(value, field = "skillId") {
|
|
|
354
381
|
const MAX_AGENT_ATTACHMENTS = 8;
|
|
355
382
|
const MAX_AGENT_ATTACHMENT_BYTES = 4 * 1024 * 1024;
|
|
356
383
|
const MAX_AGENT_ATTACHMENT_TEXT_CHARS = 120_000;
|
|
384
|
+
const MAX_TRANSLATION_UPLOAD_BYTES = 80 * 1024 * 1024;
|
|
357
385
|
function safeUploadFileName(value) {
|
|
358
386
|
const trimmed = value.trim().replace(/[/\\\0]/g, "_").replace(/\s+/g, " ");
|
|
359
387
|
const safe = trimmed.replace(/[^\p{L}\p{N}._ -]+/gu, "_").slice(0, 120).trim();
|
|
@@ -411,7 +439,7 @@ async function normalizeAgentAttachments(root, sessionId, value) {
|
|
|
411
439
|
const storedName = `${Date.now()}-${index + 1}-${filename}`;
|
|
412
440
|
const storedPath = join(uploadDir, storedName);
|
|
413
441
|
await writeFile(storedPath, parsed.buffer);
|
|
414
|
-
const relPath = relative(root, storedPath);
|
|
442
|
+
const relPath = toPosixPath(relative(root, storedPath));
|
|
415
443
|
if (mimeType.startsWith("image/")) {
|
|
416
444
|
out.push({
|
|
417
445
|
id: payload.id || `${Date.now()}-${index}`,
|
|
@@ -451,6 +479,26 @@ async function normalizeAgentAttachments(root, sessionId, value) {
|
|
|
451
479
|
}
|
|
452
480
|
return out;
|
|
453
481
|
}
|
|
482
|
+
async function storeTranslationUpload(root, payload) {
|
|
483
|
+
const filename = safeUploadFileName(payload.filename || "translation-source");
|
|
484
|
+
if (!payload.dataUrl) {
|
|
485
|
+
throw new ApiError(400, "INVALID_TRANSLATION_UPLOAD", "Translation upload is missing dataUrl");
|
|
486
|
+
}
|
|
487
|
+
const parsed = parseDataUrl(payload.dataUrl);
|
|
488
|
+
if (parsed.buffer.byteLength > MAX_TRANSLATION_UPLOAD_BYTES) {
|
|
489
|
+
throw new ApiError(413, "TRANSLATION_UPLOAD_TOO_LARGE", `${filename} exceeds ${MAX_TRANSLATION_UPLOAD_BYTES} bytes`);
|
|
490
|
+
}
|
|
491
|
+
const uploadDir = join(root, ".inkos", "uploads", "translation");
|
|
492
|
+
await mkdir(uploadDir, { recursive: true });
|
|
493
|
+
const storedName = `${Date.now()}-${filename}`;
|
|
494
|
+
const storedPath = join(uploadDir, storedName);
|
|
495
|
+
await writeFile(storedPath, parsed.buffer);
|
|
496
|
+
return {
|
|
497
|
+
storedPath: toPosixPath(relative(root, storedPath)),
|
|
498
|
+
size: parsed.buffer.byteLength,
|
|
499
|
+
mimeType: parsed.mimeType,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
454
502
|
function projectSkillsDir(root) {
|
|
455
503
|
return join(root, ".inkos", "skills");
|
|
456
504
|
}
|
|
@@ -554,7 +602,8 @@ async function toStudioPromptPackPrompt(root, prompt) {
|
|
|
554
602
|
content: loaded.content,
|
|
555
603
|
source: loaded.source,
|
|
556
604
|
overridden: loaded.source === "project",
|
|
557
|
-
|
|
605
|
+
// Windows 上 relative() 产生反斜杠,这个 path 会被前端展示/断言为 posix 相对路径
|
|
606
|
+
path: loaded.source === "project" ? toPosixPath(relative(root, overridePath)) : undefined,
|
|
558
607
|
};
|
|
559
608
|
}
|
|
560
609
|
function normalizeStudioPromptId(value) {
|
|
@@ -790,28 +839,30 @@ async function tryHandleExternalChatEdit(params) {
|
|
|
790
839
|
};
|
|
791
840
|
}
|
|
792
841
|
function validateAgentActionExecution(args) {
|
|
842
|
+
const lang = args.language ?? "zh";
|
|
793
843
|
const failedExec = args.collectedToolExecs.find(isLikelyFailedToolResult);
|
|
794
844
|
if (failedExec) {
|
|
795
|
-
|
|
845
|
+
const detail = failedExec.error ?? failedExec.result ?? pick(lang, "未知错误", "unknown error");
|
|
846
|
+
return pick(lang, `${failedExec.label} 执行失败:${detail}`, `${failedExec.label} failed: ${detail}`);
|
|
796
847
|
}
|
|
797
848
|
if (args.agentBookId
|
|
798
849
|
&& args.requestedIntent === "write_next"
|
|
799
850
|
&& !hasSuccessfulSubAgentExec(args.collectedToolExecs, "writer")) {
|
|
800
|
-
return "模型声称已完成下一章,但没有实际调用写作工具。请重试;如果仍失败,请检查模型是否支持工具调用。";
|
|
851
|
+
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.");
|
|
801
852
|
}
|
|
802
853
|
if (!args.agentBookId
|
|
803
854
|
&& args.requestedIntent === "create_book"
|
|
804
855
|
&& !hasSuccessfulSubAgentExec(args.collectedToolExecs, "architect")) {
|
|
805
|
-
return "已确认建书,但模型没有实际调用建书工具。请重试;如果仍失败,请检查模型是否支持工具调用。";
|
|
856
|
+
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.");
|
|
806
857
|
}
|
|
807
858
|
if (args.requestedIntent === "short_run" && !hasSuccessfulToolExec(args.collectedToolExecs, "short_fiction_run")) {
|
|
808
|
-
return "已确认生成短篇,但模型没有实际调用短篇生产工具。请重试;如果仍失败,请检查模型是否支持工具调用。";
|
|
859
|
+
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.");
|
|
809
860
|
}
|
|
810
861
|
if (args.requestedIntent === "play_start" && !hasSuccessfulToolExec(args.collectedToolExecs, "play_start")) {
|
|
811
|
-
return "已确认启动互动世界,但模型没有实际调用互动世界工具。请重试;如果仍失败,请检查模型是否支持工具调用。";
|
|
862
|
+
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.");
|
|
812
863
|
}
|
|
813
864
|
if (args.requestedIntent === "generate_cover" && !hasSuccessfulToolExec(args.collectedToolExecs, "generate_cover")) {
|
|
814
|
-
return "已确认生成封面,但模型没有实际调用封面工具。请重试;如果仍失败,请检查模型是否支持工具调用。";
|
|
865
|
+
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.");
|
|
815
866
|
}
|
|
816
867
|
return undefined;
|
|
817
868
|
}
|
|
@@ -819,6 +870,9 @@ function classifyAgentFailure(message) {
|
|
|
819
870
|
const text = message.trim();
|
|
820
871
|
if (!text)
|
|
821
872
|
return "unknown";
|
|
873
|
+
if (/BookWriteLockError|locked by an active InkOS write|BOOK_BUSY/i.test(text)) {
|
|
874
|
+
return "busy";
|
|
875
|
+
}
|
|
822
876
|
if (/API\s*返回|上游|upstream|Bad Gateway|temporarily unavailable|rate limit|quota|API Key|unauthorized|forbidden|无法连接到 API|fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|LLM returned empty response|Provider finish_reason|reasoning_content/i.test(text)) {
|
|
823
877
|
return "llm";
|
|
824
878
|
}
|
|
@@ -827,16 +881,29 @@ function classifyAgentFailure(message) {
|
|
|
827
881
|
}
|
|
828
882
|
return "unknown";
|
|
829
883
|
}
|
|
830
|
-
function formatAgentFailure(message) {
|
|
884
|
+
function formatAgentFailure(message, lang = "zh") {
|
|
831
885
|
const kind = classifyAgentFailure(message);
|
|
886
|
+
if (kind === "busy") {
|
|
887
|
+
return { code: "BOOK_BUSY", message, status: 409 };
|
|
888
|
+
}
|
|
832
889
|
if (kind === "llm") {
|
|
833
890
|
return { code: "AGENT_LLM_ERROR", message, status: 502 };
|
|
834
891
|
}
|
|
835
892
|
if (kind === "internal") {
|
|
836
|
-
return {
|
|
893
|
+
return {
|
|
894
|
+
code: "AGENT_INTERNAL_ERROR",
|
|
895
|
+
message: pick(lang, `InkOS 内部流程错误:${message}`, `InkOS internal pipeline error: ${message}`),
|
|
896
|
+
status: 500,
|
|
897
|
+
};
|
|
837
898
|
}
|
|
838
899
|
return { code: "AGENT_ERROR", message, status: 500 };
|
|
839
900
|
}
|
|
901
|
+
function formatAgentActionFailure(message, lang) {
|
|
902
|
+
const failure = formatAgentFailure(message, lang);
|
|
903
|
+
return failure.code === "BOOK_BUSY"
|
|
904
|
+
? { code: failure.code, message: failure.message, status: 409 }
|
|
905
|
+
: { code: "AGENT_ACTION_FAILED", message, status: 502 };
|
|
906
|
+
}
|
|
840
907
|
class ConfirmedActionExecutionError extends Error {
|
|
841
908
|
exec;
|
|
842
909
|
constructor(message, exec, cause) {
|
|
@@ -888,6 +955,7 @@ function isConfirmedProductionAction(args) {
|
|
|
888
955
|
|| args.requestedIntent === "script_create"
|
|
889
956
|
|| args.requestedIntent === "storyboard_create"
|
|
890
957
|
|| args.requestedIntent === "interactive_film_create"
|
|
958
|
+
|| args.requestedIntent === "translation_create"
|
|
891
959
|
|| args.requestedIntent === "play_start"
|
|
892
960
|
|| args.requestedIntent === "generate_cover"
|
|
893
961
|
|| args.requestedIntent === "draft_structure"
|
|
@@ -901,11 +969,12 @@ function requirePayloadText(value, message) {
|
|
|
901
969
|
}
|
|
902
970
|
return text;
|
|
903
971
|
}
|
|
904
|
-
function toolResultText(result) {
|
|
972
|
+
function toolResultText(result, lang = "zh") {
|
|
905
973
|
const text = extractToolError(result).trim();
|
|
906
|
-
return text || "已完成。";
|
|
974
|
+
return text || pick(lang, "已完成。", "Done.");
|
|
907
975
|
}
|
|
908
976
|
async function executeConfirmedProductionAction(args) {
|
|
977
|
+
const lang = args.language ?? "zh";
|
|
909
978
|
const id = `direct-${args.requestedIntent}-${Date.now().toString(36)}`;
|
|
910
979
|
const actionPayload = args.actionPayload;
|
|
911
980
|
let tool;
|
|
@@ -913,7 +982,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
913
982
|
let agent;
|
|
914
983
|
if (args.requestedIntent === "create_book") {
|
|
915
984
|
const payload = actionPayload?.createBook;
|
|
916
|
-
const title = requirePayloadText(payload?.title, "确认建书缺少书名,请重新生成确认卡。");
|
|
985
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认建书缺少书名,请重新生成确认卡。", "The book creation confirmation is missing a title. Regenerate the confirmation card."));
|
|
917
986
|
tool = createSubAgentTool(args.pipeline, null, args.root, { actionPayload });
|
|
918
987
|
agent = "architect";
|
|
919
988
|
params = {
|
|
@@ -931,7 +1000,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
931
1000
|
const payload = actionPayload?.shortRun;
|
|
932
1001
|
const direction = payload?.direction?.trim() || args.instruction.trim();
|
|
933
1002
|
if (!direction)
|
|
934
|
-
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", "确认短篇缺少方向,请重新生成确认卡。");
|
|
1003
|
+
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认短篇缺少方向,请重新生成确认卡。", "The short fiction confirmation is missing a direction. Regenerate the confirmation card."));
|
|
935
1004
|
tool = createShortFictionRunTool(args.pipeline, args.root, { actionPayload });
|
|
936
1005
|
params = {
|
|
937
1006
|
direction,
|
|
@@ -944,7 +1013,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
944
1013
|
}
|
|
945
1014
|
else if (args.requestedIntent === "generate_cover") {
|
|
946
1015
|
const payload = actionPayload?.generateCover;
|
|
947
|
-
const title = requirePayloadText(payload?.title, "确认生成封面缺少标题,请重新生成确认卡。");
|
|
1016
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认生成封面缺少标题,请重新生成确认卡。", "The cover generation confirmation is missing a title. Regenerate the confirmation card."));
|
|
948
1017
|
tool = createGenerateCoverTool(args.root, { actionPayload });
|
|
949
1018
|
params = {
|
|
950
1019
|
title,
|
|
@@ -956,7 +1025,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
956
1025
|
}
|
|
957
1026
|
else if (args.requestedIntent === "script_create") {
|
|
958
1027
|
const payload = actionPayload?.scriptCreate;
|
|
959
|
-
const title = requirePayloadText(payload?.title, "确认创建剧本缺少标题,请重新生成确认卡。");
|
|
1028
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认创建剧本缺少标题,请重新生成确认卡。", "The script creation confirmation is missing a title. Regenerate the confirmation card."));
|
|
960
1029
|
tool = createScriptCreationTool(args.pipeline, args.root, { actionPayload });
|
|
961
1030
|
params = {
|
|
962
1031
|
title,
|
|
@@ -974,7 +1043,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
974
1043
|
}
|
|
975
1044
|
else if (args.requestedIntent === "storyboard_create") {
|
|
976
1045
|
const payload = actionPayload?.storyboardCreate;
|
|
977
|
-
const title = requirePayloadText(payload?.title, "确认创建分镜缺少标题,请重新生成确认卡。");
|
|
1046
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认创建分镜缺少标题,请重新生成确认卡。", "The storyboard creation confirmation is missing a title. Regenerate the confirmation card."));
|
|
978
1047
|
tool = createStoryboardCreationTool(args.pipeline, args.root, { actionPayload });
|
|
979
1048
|
params = {
|
|
980
1049
|
title,
|
|
@@ -993,7 +1062,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
993
1062
|
}
|
|
994
1063
|
else if (args.requestedIntent === "interactive_film_create") {
|
|
995
1064
|
const payload = actionPayload?.interactiveFilmCreate;
|
|
996
|
-
const title = requirePayloadText(payload?.title, "确认创建互动影游缺少标题,请重新生成确认卡。");
|
|
1065
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认创建互动影游缺少标题,请重新生成确认卡。", "The interactive film confirmation is missing a title. Regenerate the confirmation card."));
|
|
997
1066
|
tool = createInteractiveFilmCreationTool(args.pipeline, args.root, { actionPayload });
|
|
998
1067
|
params = {
|
|
999
1068
|
title,
|
|
@@ -1011,9 +1080,23 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1011
1080
|
...(payload?.outDir ? { outDir: payload.outDir } : {}),
|
|
1012
1081
|
};
|
|
1013
1082
|
}
|
|
1083
|
+
else if (args.requestedIntent === "translation_create") {
|
|
1084
|
+
const payload = actionPayload?.translationCreate;
|
|
1085
|
+
const filePath = requirePayloadText(payload?.filePath, pick(lang, "确认创建翻译项目缺少文件路径,请重新生成确认卡。", "The translation confirmation is missing a file path. Regenerate the confirmation card."));
|
|
1086
|
+
const sourceLanguage = requirePayloadText(payload?.sourceLanguage, pick(lang, "确认创建翻译项目缺少源语言,请重新生成确认卡。", "The translation confirmation is missing a source language. Regenerate the confirmation card."));
|
|
1087
|
+
const targetLanguage = requirePayloadText(payload?.targetLanguage, pick(lang, "确认创建翻译项目缺少目标语言,请重新生成确认卡。", "The translation confirmation is missing a target language. Regenerate the confirmation card."));
|
|
1088
|
+
tool = createTranslationCreateTool(args.root, { actionPayload });
|
|
1089
|
+
params = {
|
|
1090
|
+
filePath,
|
|
1091
|
+
sourceLanguage,
|
|
1092
|
+
targetLanguage,
|
|
1093
|
+
...(payload?.title ? { title: payload.title } : {}),
|
|
1094
|
+
...(payload?.segmentMaxChars ? { segmentMaxChars: payload.segmentMaxChars } : {}),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1014
1097
|
else if (args.requestedIntent === "play_start") {
|
|
1015
1098
|
const payload = actionPayload?.playStart;
|
|
1016
|
-
const title = requirePayloadText(payload?.title, "确认启动互动世界缺少标题,请重新生成确认卡。");
|
|
1099
|
+
const title = requirePayloadText(payload?.title, pick(lang, "确认启动互动世界缺少标题,请重新生成确认卡。", "The interactive world start confirmation is missing a title. Regenerate the confirmation card."));
|
|
1017
1100
|
const fallbackScene = [payload?.premise, args.instruction].filter((part) => typeof part === "string" && part.trim().length > 0).join("\n\n");
|
|
1018
1101
|
const initialScene = isUsablePlayInitialScene(payload?.initialScene)
|
|
1019
1102
|
? payload?.initialScene?.trim()
|
|
@@ -1046,7 +1129,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1046
1129
|
throw new ApiError(400, "INVALID_ID", "interactive-film action requires a project id (bookId)");
|
|
1047
1130
|
const agentCtx = args.pipeline.createAgentContext("film-authoring", projectId);
|
|
1048
1131
|
const deps = filmLLMDepsFromClient(agentCtx.client, agentCtx.model);
|
|
1049
|
-
tool = createDraftStructureTool(args.root, projectId, deps);
|
|
1132
|
+
tool = createDraftStructureTool(args.root, projectId, deps, lang);
|
|
1050
1133
|
params = {
|
|
1051
1134
|
instruction: payload?.instruction?.trim() || args.instruction,
|
|
1052
1135
|
};
|
|
@@ -1054,7 +1137,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1054
1137
|
else if (args.requestedIntent === "connect_choice") {
|
|
1055
1138
|
const payload = actionPayload?.connectChoice;
|
|
1056
1139
|
if (!payload?.node) {
|
|
1057
|
-
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", "确认连接选择缺少节点数据,请重新生成确认卡。");
|
|
1140
|
+
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认连接选择缺少节点数据,请重新生成确认卡。", "The connect-choice confirmation is missing node data. Regenerate the confirmation card."));
|
|
1058
1141
|
}
|
|
1059
1142
|
const projectId = payload?.projectId ?? args.bookId;
|
|
1060
1143
|
if (!projectId)
|
|
@@ -1067,7 +1150,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1067
1150
|
else if (args.requestedIntent === "remove_node") {
|
|
1068
1151
|
const payload = actionPayload?.removeNode;
|
|
1069
1152
|
if (!payload?.nodeId) {
|
|
1070
|
-
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", "确认删除节点缺少 nodeId,请重新生成确认卡。");
|
|
1153
|
+
throw new ApiError(400, "CONFIRMED_ACTION_PAYLOAD_INCOMPLETE", pick(lang, "确认删除节点缺少 nodeId,请重新生成确认卡。", "The remove-node confirmation is missing a nodeId. Regenerate the confirmation card."));
|
|
1071
1154
|
}
|
|
1072
1155
|
const projectId = payload?.projectId ?? args.bookId;
|
|
1073
1156
|
if (!projectId)
|
|
@@ -1084,10 +1167,10 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1084
1167
|
id,
|
|
1085
1168
|
tool: tool.name,
|
|
1086
1169
|
agent,
|
|
1087
|
-
label: resolveToolLabel(tool.name, agent),
|
|
1170
|
+
label: resolveToolLabel(tool.name, agent, lang),
|
|
1088
1171
|
status: "running",
|
|
1089
1172
|
args: params,
|
|
1090
|
-
stages: agent ?
|
|
1173
|
+
stages: agent ? pipelineStages(agent, lang)?.map(label => ({ label, status: "pending" })) : undefined,
|
|
1091
1174
|
startedAt: Date.now(),
|
|
1092
1175
|
};
|
|
1093
1176
|
broadcast("tool:start", {
|
|
@@ -1107,7 +1190,7 @@ async function executeConfirmedProductionAction(args) {
|
|
|
1107
1190
|
});
|
|
1108
1191
|
exec.status = "completed";
|
|
1109
1192
|
exec.completedAt = Date.now();
|
|
1110
|
-
exec.result = toolResultText(result);
|
|
1193
|
+
exec.result = toolResultText(result, lang);
|
|
1111
1194
|
exec.details = result?.details;
|
|
1112
1195
|
exec.stages = exec.stages?.map(stage => ({ ...stage, status: "completed" }));
|
|
1113
1196
|
broadcast("tool:end", {
|
|
@@ -1337,6 +1420,34 @@ async function resolveBookChapterReviewMode(root, bookId, projectMode) {
|
|
|
1337
1420
|
return projectMode;
|
|
1338
1421
|
}
|
|
1339
1422
|
}
|
|
1423
|
+
function normalizeRevisionGate(gate) {
|
|
1424
|
+
return gate === "lenient" || gate === "always" ? gate : "strict";
|
|
1425
|
+
}
|
|
1426
|
+
function readProjectRevisionGate(config) {
|
|
1427
|
+
const writing = config.writing && typeof config.writing === "object" && !Array.isArray(config.writing)
|
|
1428
|
+
? config.writing
|
|
1429
|
+
: {};
|
|
1430
|
+
return normalizeRevisionGate(writing.revisionGate);
|
|
1431
|
+
}
|
|
1432
|
+
function readBookRevisionGate(rawBook) {
|
|
1433
|
+
const writing = rawBook.writing && typeof rawBook.writing === "object" && !Array.isArray(rawBook.writing)
|
|
1434
|
+
? rawBook.writing
|
|
1435
|
+
: undefined;
|
|
1436
|
+
if (!writing || writing.revisionGate !== "strict" && writing.revisionGate !== "lenient" && writing.revisionGate !== "always")
|
|
1437
|
+
return undefined;
|
|
1438
|
+
return writing.revisionGate;
|
|
1439
|
+
}
|
|
1440
|
+
async function resolveBookRevisionGate(root, bookId, projectGate) {
|
|
1441
|
+
if (!bookId || !isSafeBookId(bookId))
|
|
1442
|
+
return projectGate;
|
|
1443
|
+
try {
|
|
1444
|
+
const rawBook = await loadRawBookConfig(root, bookId);
|
|
1445
|
+
return readBookRevisionGate(rawBook) ?? projectGate;
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
return projectGate;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1340
1451
|
function unquoteEnvValue(value) {
|
|
1341
1452
|
const trimmed = value.trim();
|
|
1342
1453
|
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
@@ -1558,13 +1669,13 @@ function fallbackTextModelsForEndpoint(endpoint, preset) {
|
|
|
1558
1669
|
function shouldTrustStaticModelsWhenLiveListUnavailable(endpoint) {
|
|
1559
1670
|
return endpoint?.group === "aggregator";
|
|
1560
1671
|
}
|
|
1561
|
-
async function withTimeout(promise, timeoutMs, label) {
|
|
1672
|
+
async function withTimeout(promise, timeoutMs, label, lang = "zh") {
|
|
1562
1673
|
let timeout;
|
|
1563
1674
|
try {
|
|
1564
1675
|
return await Promise.race([
|
|
1565
1676
|
promise,
|
|
1566
1677
|
new Promise((_, reject) => {
|
|
1567
|
-
timeout = setTimeout(() => reject(new Error(`${label} 超时(${timeoutMs}ms
|
|
1678
|
+
timeout = setTimeout(() => reject(new Error(pick(lang, `${label} 超时(${timeoutMs}ms)`, `${label} timed out (${timeoutMs}ms)`))), timeoutMs);
|
|
1568
1679
|
}),
|
|
1569
1680
|
]);
|
|
1570
1681
|
}
|
|
@@ -1574,49 +1685,55 @@ async function withTimeout(promise, timeoutMs, label) {
|
|
|
1574
1685
|
}
|
|
1575
1686
|
}
|
|
1576
1687
|
function formatServiceProbeError(args) {
|
|
1688
|
+
const lang = args.language ?? "zh";
|
|
1577
1689
|
const rawDetail = args.error
|
|
1578
1690
|
.replace(/\n\s*\(baseUrl:[\s\S]*?\)$/m, "")
|
|
1579
1691
|
.trim();
|
|
1580
1692
|
const upstreamDetail = rawDetail.includes("上游详情:")
|
|
1581
1693
|
? rawDetail
|
|
1582
1694
|
: "";
|
|
1695
|
+
const protocol = args.apiFormat === "responses" ? "Responses" : "Chat / Completions";
|
|
1696
|
+
const streamSuffix = typeof args.stream === "boolean"
|
|
1697
|
+
? pick(lang, `,${args.stream ? "流式" : "非流式"}`, `, ${args.stream ? "streaming" : "non-streaming"}`)
|
|
1698
|
+
: "";
|
|
1583
1699
|
const context = [
|
|
1584
|
-
`服务商:${args.label ?? args.service}`,
|
|
1585
|
-
`测试模型:${args.model ?? "未确定"}`,
|
|
1586
|
-
`协议:${
|
|
1587
|
-
`Base URL:${args.baseUrl}`,
|
|
1700
|
+
pick(lang, `服务商:${args.label ?? args.service}`, `Service: ${args.label ?? args.service}`),
|
|
1701
|
+
pick(lang, `测试模型:${args.model ?? "未确定"}`, `Test model: ${args.model ?? "undetermined"}`),
|
|
1702
|
+
pick(lang, `协议:${protocol}${streamSuffix}`, `Protocol: ${protocol}${streamSuffix}`),
|
|
1703
|
+
pick(lang, `Base URL:${args.baseUrl}`, `Base URL: ${args.baseUrl}`),
|
|
1588
1704
|
].join("\n");
|
|
1705
|
+
const upstreamPrefix = (detail) => pick(lang, `\n上游返回:${detail}`, `\nUpstream response: ${detail}`);
|
|
1589
1706
|
if (args.service === "google") {
|
|
1590
1707
|
return [
|
|
1591
|
-
"Google Gemini 测试连接失败。",
|
|
1708
|
+
pick(lang, "Google Gemini 测试连接失败。", "Google Gemini connection test failed."),
|
|
1592
1709
|
context,
|
|
1593
1710
|
"",
|
|
1594
|
-
"请优先检查:",
|
|
1595
|
-
"1. API Key 是否来自 Google AI Studio 的 Gemini API key,而不是 OAuth、Vertex AI 或其它 Google 服务凭据。",
|
|
1596
|
-
"2. 该 key 所属项目是否已启用 Gemini API,并且没有被限制到其它 API、来源或服务。",
|
|
1597
|
-
"3. 当前地区/账号是否允许访问 Gemini API。",
|
|
1598
|
-
"4. 如果 key 曾经泄露,请在 AI Studio 重新生成后再保存。",
|
|
1599
|
-
upstreamDetail ?
|
|
1711
|
+
pick(lang, "请优先检查:", "Check these first:"),
|
|
1712
|
+
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."),
|
|
1713
|
+
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."),
|
|
1714
|
+
pick(lang, "3. 当前地区/账号是否允许访问 Gemini API。", "3. Your region/account is allowed to access the Gemini API."),
|
|
1715
|
+
pick(lang, "4. 如果 key 曾经泄露,请在 AI Studio 重新生成后再保存。", "4. If the key was ever leaked, regenerate it in AI Studio before saving."),
|
|
1716
|
+
upstreamDetail ? upstreamPrefix(upstreamDetail) : "",
|
|
1600
1717
|
].filter(Boolean).join("\n");
|
|
1601
1718
|
}
|
|
1602
1719
|
if (args.service === "moonshot" || args.service === "kimiCodingPlan" || args.service === "kimicode") {
|
|
1603
1720
|
return [
|
|
1604
|
-
`${args.label ?? args.service} 测试连接失败。`,
|
|
1721
|
+
pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`),
|
|
1605
1722
|
context,
|
|
1606
1723
|
"",
|
|
1607
|
-
"请优先检查模型是否可用,以及 kimi-k2.x 这类模型是否需要 temperature=1。",
|
|
1608
|
-
rawDetail ?
|
|
1724
|
+
pick(lang, "请优先检查模型是否可用,以及 kimi-k2.x 这类模型是否需要 temperature=1。", "Check first whether the model is available, and whether models like kimi-k2.x require temperature=1."),
|
|
1725
|
+
rawDetail ? upstreamPrefix(rawDetail) : "",
|
|
1609
1726
|
].filter(Boolean).join("\n");
|
|
1610
1727
|
}
|
|
1611
1728
|
return [
|
|
1612
|
-
`${args.label ?? args.service} 测试连接失败。`,
|
|
1729
|
+
pick(lang, `${args.label ?? args.service} 测试连接失败。`, `${args.label ?? args.service} connection test failed.`),
|
|
1613
1730
|
context,
|
|
1614
1731
|
"",
|
|
1615
|
-
"请检查 API Key、模型可用性、账号额度,以及协议类型是否匹配该服务商。",
|
|
1616
|
-
rawDetail ?
|
|
1732
|
+
pick(lang, "请检查 API Key、模型可用性、账号额度,以及协议类型是否匹配该服务商。", "Check the API Key, model availability, account quota, and whether the protocol type matches this service."),
|
|
1733
|
+
rawDetail ? upstreamPrefix(rawDetail) : "",
|
|
1617
1734
|
].filter(Boolean).join("\n");
|
|
1618
1735
|
}
|
|
1619
|
-
async function fetchModelsFromServiceBaseUrl(serviceId, baseUrl, apiKey, proxyUrl) {
|
|
1736
|
+
async function fetchModelsFromServiceBaseUrl(serviceId, baseUrl, apiKey, proxyUrl, lang = "zh") {
|
|
1620
1737
|
const endpoint = isCustomServiceId(serviceId)
|
|
1621
1738
|
? undefined
|
|
1622
1739
|
: getAllEndpoints().find((ep) => ep.id === serviceId);
|
|
@@ -1626,14 +1743,14 @@ async function fetchModelsFromServiceBaseUrl(serviceId, baseUrl, apiKey, proxyUr
|
|
|
1626
1743
|
const modelsUrl = modelsBaseUrl.replace(/\/$/, "") + "/models";
|
|
1627
1744
|
try {
|
|
1628
1745
|
const res = await fetchWithProxy(modelsUrl, {
|
|
1629
|
-
headers: buildBearerAuthHeaders(apiKey),
|
|
1746
|
+
headers: buildBearerAuthHeaders(apiKey, lang),
|
|
1630
1747
|
signal: AbortSignal.timeout(SERVICE_MODELS_PROBE_TIMEOUT_MS),
|
|
1631
1748
|
}, proxyUrl);
|
|
1632
1749
|
if (!res.ok) {
|
|
1633
1750
|
const body = await res.text().catch(() => "");
|
|
1634
1751
|
return {
|
|
1635
1752
|
models: [],
|
|
1636
|
-
error: `服务商返回 ${res.status}: ${body.slice(0, 200)}`,
|
|
1753
|
+
error: pick(lang, `服务商返回 ${res.status}: ${body.slice(0, 200)}`, `Service returned ${res.status}: ${body.slice(0, 200)}`),
|
|
1637
1754
|
authFailed: res.status === 401 || res.status === 403,
|
|
1638
1755
|
};
|
|
1639
1756
|
}
|
|
@@ -1649,16 +1766,17 @@ async function fetchModelsFromServiceBaseUrl(serviceId, baseUrl, apiKey, proxyUr
|
|
|
1649
1766
|
};
|
|
1650
1767
|
}
|
|
1651
1768
|
}
|
|
1652
|
-
function buildBearerAuthHeaders(apiKey) {
|
|
1769
|
+
function buildBearerAuthHeaders(apiKey, lang = "zh") {
|
|
1653
1770
|
const trimmed = apiKey?.trim() ?? "";
|
|
1654
1771
|
if (!trimmed)
|
|
1655
1772
|
return {};
|
|
1656
1773
|
if (!/^[\x20-\x7e]+$/.test(trimmed)) {
|
|
1657
|
-
throw new Error("API Key 只能包含英文、数字和常见 ASCII 符号,请检查是否误粘贴了中文说明。");
|
|
1774
|
+
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."));
|
|
1658
1775
|
}
|
|
1659
1776
|
return { Authorization: `Bearer ${trimmed}` };
|
|
1660
1777
|
}
|
|
1661
1778
|
async function probeServiceCapabilities(args) {
|
|
1779
|
+
const lang = args.language ?? "zh";
|
|
1662
1780
|
const rawConfig = await loadRawConfig(args.root).catch(() => ({}));
|
|
1663
1781
|
const llm = rawConfig.llm ?? {};
|
|
1664
1782
|
const envConfig = await readEnvConfigStatus(args.root);
|
|
@@ -1668,12 +1786,12 @@ async function probeServiceCapabilities(args) {
|
|
|
1668
1786
|
? envConfig.global.model
|
|
1669
1787
|
: null;
|
|
1670
1788
|
const baseService = isCustomServiceId(args.service) ? "custom" : args.service;
|
|
1671
|
-
const modelsResponse = await fetchModelsFromServiceBaseUrl(baseService, args.baseUrl, args.apiKey, args.proxyUrl);
|
|
1789
|
+
const modelsResponse = await fetchModelsFromServiceBaseUrl(baseService, args.baseUrl, args.apiKey, args.proxyUrl, lang);
|
|
1672
1790
|
if (modelsResponse.authFailed) {
|
|
1673
1791
|
return {
|
|
1674
1792
|
ok: false,
|
|
1675
1793
|
models: [],
|
|
1676
|
-
error: modelsResponse.error ?? "API Key 无效或无权访问模型列表。",
|
|
1794
|
+
error: modelsResponse.error ?? pick(lang, "API Key 无效或无权访问模型列表。", "API Key is invalid or has no access to the model list."),
|
|
1677
1795
|
};
|
|
1678
1796
|
}
|
|
1679
1797
|
const discoveredModels = modelsResponse.models;
|
|
@@ -1686,7 +1804,7 @@ async function probeServiceCapabilities(args) {
|
|
|
1686
1804
|
return {
|
|
1687
1805
|
ok: false,
|
|
1688
1806
|
models: discoveredModels,
|
|
1689
|
-
error: "模型列表可访问,但没有发现可用于文本对话的模型。",
|
|
1807
|
+
error: pick(lang, "模型列表可访问,但没有发现可用于文本对话的模型。", "The model list is reachable, but no model usable for text chat was found."),
|
|
1690
1808
|
};
|
|
1691
1809
|
}
|
|
1692
1810
|
return {
|
|
@@ -1745,10 +1863,10 @@ async function probeServiceCapabilities(args) {
|
|
|
1745
1863
|
return {
|
|
1746
1864
|
ok: false,
|
|
1747
1865
|
models: [],
|
|
1748
|
-
error: "无法自动确定模型,请先填写可用模型或提供支持 /models 的服务端点。",
|
|
1866
|
+
error: pick(lang, "无法自动确定模型,请先填写可用模型或提供支持 /models 的服务端点。", "Could not determine a model automatically. Fill in an available model first, or provide a service endpoint that supports /models."),
|
|
1749
1867
|
};
|
|
1750
1868
|
}
|
|
1751
|
-
let lastError = modelsResponse.error ?? "自动探测失败";
|
|
1869
|
+
let lastError = modelsResponse.error ?? pick(lang, "自动探测失败", "Automatic probing failed");
|
|
1752
1870
|
for (const model of modelCandidates) {
|
|
1753
1871
|
for (const plan of buildProbePlans(args.preferredApiFormat, args.preferredStream)) {
|
|
1754
1872
|
const client = createLLMClient({
|
|
@@ -1770,7 +1888,7 @@ async function probeServiceCapabilities(args) {
|
|
|
1770
1888
|
// A connectivity probe wants a fast pass/fail — never the transient
|
|
1771
1889
|
// retry+backoff, which would multiply the time when the upstream is
|
|
1772
1890
|
// rate-limiting (and make the diagnostics page hang).
|
|
1773
|
-
chatCompletion(client, model, [{ role: "user", content: "Reply with OK only." }], { maxTokens: 16, retry: false }), SERVICE_CHAT_PROBE_TIMEOUT_MS, "service connection test");
|
|
1891
|
+
chatCompletion(client, model, [{ role: "user", content: "Reply with OK only." }], { maxTokens: 16, retry: false }), SERVICE_CHAT_PROBE_TIMEOUT_MS, "service connection test", lang);
|
|
1774
1892
|
const models = discoveredModels.length > 0
|
|
1775
1893
|
? discoveredModels
|
|
1776
1894
|
: fallbackTextModelsForEndpoint(endpoint, preset);
|
|
@@ -1793,6 +1911,7 @@ async function probeServiceCapabilities(args) {
|
|
|
1793
1911
|
apiFormat: plan.apiFormat,
|
|
1794
1912
|
stream: plan.stream,
|
|
1795
1913
|
error: error instanceof Error ? error.message : String(error),
|
|
1914
|
+
language: lang,
|
|
1796
1915
|
});
|
|
1797
1916
|
}
|
|
1798
1917
|
}
|
|
@@ -1859,10 +1978,19 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
1859
1978
|
cachedConfig = freshConfig;
|
|
1860
1979
|
return freshConfig;
|
|
1861
1980
|
}
|
|
1981
|
+
// Read the project language fresh from inkos.json on every call, so a language
|
|
1982
|
+
// switch takes effect on the next request instead of being frozen at startup.
|
|
1983
|
+
// A missing/corrupt inkos.json means "no project language configured" -> zh.
|
|
1984
|
+
async function currentProjectLanguage() {
|
|
1985
|
+
const raw = await loadRawConfig(root).catch(() => ({}));
|
|
1986
|
+
return normalizeStudioLanguage(raw.language);
|
|
1987
|
+
}
|
|
1862
1988
|
async function buildPipelineConfig(overrides) {
|
|
1863
1989
|
const currentConfig = overrides?.currentConfig ?? await loadCurrentProjectConfig();
|
|
1864
1990
|
const projectReviewMode = readProjectChapterReviewMode(currentConfig);
|
|
1865
1991
|
const chapterReviewMode = await resolveBookChapterReviewMode(root, overrides?.bookIdForSettings, projectReviewMode);
|
|
1992
|
+
const projectRevisionGate = readProjectRevisionGate(currentConfig);
|
|
1993
|
+
const revisionGate = await resolveBookRevisionGate(root, overrides?.bookIdForSettings, projectRevisionGate);
|
|
1866
1994
|
const scopedSseSink = overrides?.sessionIdForSSE
|
|
1867
1995
|
? {
|
|
1868
1996
|
write(entry) {
|
|
@@ -1884,6 +2012,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
1884
2012
|
foundationReviewRetries: currentConfig.foundation?.reviewRetries ?? 2,
|
|
1885
2013
|
writingReviewRetries: currentConfig.writing?.reviewRetries ?? 1,
|
|
1886
2014
|
chapterReviewMode,
|
|
2015
|
+
revisionGate,
|
|
1887
2016
|
modelOverrides: currentConfig.modelOverrides,
|
|
1888
2017
|
notifyChannels: currentConfig.notify,
|
|
1889
2018
|
logger,
|
|
@@ -2386,7 +2515,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2386
2515
|
const env = await readEffectiveEnvConfigValues(root);
|
|
2387
2516
|
if (!env || !env.values.apiKey) {
|
|
2388
2517
|
return c.json({
|
|
2389
|
-
error: "未检测到可导入的 LLM 环境变量配置,或缺少 INKOS_LLM_API_KEY。",
|
|
2518
|
+
error: pick(await currentProjectLanguage(), "未检测到可导入的 LLM 环境变量配置,或缺少 INKOS_LLM_API_KEY。", "No importable LLM environment variable configuration was detected, or INKOS_LLM_API_KEY is missing."),
|
|
2390
2519
|
}, 400);
|
|
2391
2520
|
}
|
|
2392
2521
|
const config = await loadRawConfig(root);
|
|
@@ -2436,7 +2565,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2436
2565
|
}
|
|
2437
2566
|
if (body.configSource === "env") {
|
|
2438
2567
|
return c.json({
|
|
2439
|
-
error: "Studio 运行时不支持切换到 env;env 只在 CLI/daemon/部署运行时作为覆盖层使用。",
|
|
2568
|
+
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."),
|
|
2440
2569
|
}, 400);
|
|
2441
2570
|
}
|
|
2442
2571
|
if (body.configSource !== undefined) {
|
|
@@ -2510,7 +2639,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2510
2639
|
const body = await c.req.json();
|
|
2511
2640
|
const trimmedKey = body.apiKey?.trim() ?? "";
|
|
2512
2641
|
if (trimmedKey && !isHeaderSafeApiKey(trimmedKey)) {
|
|
2513
|
-
return c.json({
|
|
2642
|
+
return c.json({
|
|
2643
|
+
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."),
|
|
2644
|
+
}, 400);
|
|
2514
2645
|
}
|
|
2515
2646
|
const secrets = await loadSecrets(root);
|
|
2516
2647
|
const key = coverSecretKey(service);
|
|
@@ -2547,9 +2678,13 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2547
2678
|
app.post("/api/v1/services/:service/test", async (c) => {
|
|
2548
2679
|
const service = c.req.param("service");
|
|
2549
2680
|
const { apiKey, baseUrl, apiFormat, stream } = await c.req.json();
|
|
2681
|
+
const language = await currentProjectLanguage();
|
|
2550
2682
|
const resolvedBaseUrl = await resolveConfiguredServiceBaseUrl(root, service, baseUrl);
|
|
2551
2683
|
if (!resolvedBaseUrl) {
|
|
2552
|
-
return c.json({
|
|
2684
|
+
return c.json({
|
|
2685
|
+
ok: false,
|
|
2686
|
+
error: pick(language, `未知服务商: ${service}`, `Unknown service: ${service}`),
|
|
2687
|
+
}, 400);
|
|
2553
2688
|
}
|
|
2554
2689
|
const baseService = isCustomServiceId(service) ? "custom" : service;
|
|
2555
2690
|
const apiKeyOptional = isApiKeyOptionalForEndpoint({
|
|
@@ -2559,7 +2694,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2559
2694
|
if (!apiKey?.trim() && !apiKeyOptional) {
|
|
2560
2695
|
return c.json({
|
|
2561
2696
|
ok: false,
|
|
2562
|
-
error: "API Key 不能为空",
|
|
2697
|
+
error: pick(language, "API Key 不能为空", "API Key must not be empty"),
|
|
2563
2698
|
}, 400);
|
|
2564
2699
|
}
|
|
2565
2700
|
const rawConfig = await loadRawConfig(root).catch(() => ({}));
|
|
@@ -2572,17 +2707,19 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2572
2707
|
preferredApiFormat: apiFormat,
|
|
2573
2708
|
preferredStream: stream,
|
|
2574
2709
|
proxyUrl: typeof llm.proxyUrl === "string" ? llm.proxyUrl : undefined,
|
|
2710
|
+
language,
|
|
2575
2711
|
});
|
|
2576
2712
|
// B12: 升级响应 shape 为 { probe, chat, ... },同时保留老字段供 UI 过渡期兼容
|
|
2713
|
+
const connectionFailed = pick(language, "连接失败", "Connection failed");
|
|
2577
2714
|
const probeStatus = {
|
|
2578
2715
|
ok: probe.ok,
|
|
2579
2716
|
models: probe.models?.length ?? 0,
|
|
2580
|
-
...(probe.ok ? {} : { error: probe.error ??
|
|
2717
|
+
...(probe.ok ? {} : { error: probe.error ?? connectionFailed }),
|
|
2581
2718
|
};
|
|
2582
2719
|
if (!probe.ok) {
|
|
2583
2720
|
return c.json({
|
|
2584
2721
|
ok: false,
|
|
2585
|
-
error: probe.error ??
|
|
2722
|
+
error: probe.error ?? connectionFailed,
|
|
2586
2723
|
probe: probeStatus,
|
|
2587
2724
|
chat: null,
|
|
2588
2725
|
}, 400);
|
|
@@ -2612,7 +2749,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
2612
2749
|
if (!isHeaderSafeApiKey(trimmedKey)) {
|
|
2613
2750
|
return c.json({
|
|
2614
2751
|
ok: false,
|
|
2615
|
-
error: "API Key 只能包含可放进 HTTP Authorization header 的非空白 ASCII 字符;请不要粘贴连接失败提示或诊断文本。",
|
|
2752
|
+
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."),
|
|
2616
2753
|
}, 400);
|
|
2617
2754
|
}
|
|
2618
2755
|
secrets.services[service] = { apiKey: trimmedKey };
|
|
@@ -3260,8 +3397,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3260
3397
|
if (!sessionId?.trim()) {
|
|
3261
3398
|
throw new ApiError(400, "SESSION_ID_REQUIRED", "sessionId is required");
|
|
3262
3399
|
}
|
|
3400
|
+
const language = await currentProjectLanguage();
|
|
3263
3401
|
if (reqModel && !isTextChatModelId(reqModel)) {
|
|
3264
|
-
const message = nonTextModelMessage(reqModel);
|
|
3402
|
+
const message = nonTextModelMessage(reqModel, language);
|
|
3265
3403
|
return c.json({ error: message, response: message }, 400);
|
|
3266
3404
|
}
|
|
3267
3405
|
const actionSource = normalizeStudioActionSource(reqActionSource);
|
|
@@ -3368,8 +3506,8 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3368
3506
|
const msg = e?.message ?? String(e);
|
|
3369
3507
|
if (/API key/i.test(msg)) {
|
|
3370
3508
|
return c.json({
|
|
3371
|
-
error: `请先为 ${reqService} 配置 API Key`,
|
|
3372
|
-
response: `请先在模型配置中为 ${reqService} 填写 API Key,然后再试。`,
|
|
3509
|
+
error: pick(language, `请先为 ${reqService} 配置 API Key`, `Configure an API Key for ${reqService} first`),
|
|
3510
|
+
response: pick(language, `请先在模型配置中为 ${reqService} 填写 API Key,然后再试。`, `Fill in an API Key for ${reqService} in the model settings, then try again.`),
|
|
3373
3511
|
}, 400);
|
|
3374
3512
|
}
|
|
3375
3513
|
throw e;
|
|
@@ -3463,6 +3601,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3463
3601
|
instruction,
|
|
3464
3602
|
requestedIntent,
|
|
3465
3603
|
actionPayload,
|
|
3604
|
+
language,
|
|
3466
3605
|
...(playMode ? { playMode } : {}),
|
|
3467
3606
|
});
|
|
3468
3607
|
let createdBookId = null;
|
|
@@ -3489,7 +3628,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3489
3628
|
});
|
|
3490
3629
|
}
|
|
3491
3630
|
}
|
|
3492
|
-
const responseText = exec.result ?? "已完成。";
|
|
3631
|
+
const responseText = exec.result ?? pick(language, "已完成。", "Done.");
|
|
3493
3632
|
const responseForUser = suppressManualTextForTool(exec) ? "" : responseText;
|
|
3494
3633
|
await appendManualSessionMessages(root, bookSession.sessionId, [
|
|
3495
3634
|
manualToolAssistantMessage(responseText, exec, configuredEntry?.service ?? reqService ?? config.llm.provider, reqModel ?? config.llm.model),
|
|
@@ -3508,6 +3647,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3508
3647
|
}
|
|
3509
3648
|
catch (error) {
|
|
3510
3649
|
const message = error instanceof Error ? error.message : String(error);
|
|
3650
|
+
const failure = formatAgentActionFailure(message, language);
|
|
3511
3651
|
if (pendingBookId) {
|
|
3512
3652
|
bookCreateStatus.set(pendingBookId, { status: "error", error: message });
|
|
3513
3653
|
broadcast("book:error", { bookId: pendingBookId, sessionId: streamSessionId, error: message });
|
|
@@ -3520,9 +3660,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3520
3660
|
}
|
|
3521
3661
|
broadcast("agent:error", { instruction, activeBookId: agentBookId, sessionId: bookSession.sessionId, sessionKind, error: message });
|
|
3522
3662
|
return c.json({
|
|
3523
|
-
error: { code:
|
|
3524
|
-
response: message,
|
|
3525
|
-
},
|
|
3663
|
+
error: { code: failure.code, message: failure.message },
|
|
3664
|
+
response: failure.message,
|
|
3665
|
+
}, failure.status);
|
|
3526
3666
|
}
|
|
3527
3667
|
}
|
|
3528
3668
|
if (shouldRunDirectWriteNext({ instruction, agentBookId, sessionKind, actionSource, requestedIntent })) {
|
|
@@ -3537,12 +3677,12 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3537
3677
|
id: toolCallId,
|
|
3538
3678
|
tool: "sub_agent",
|
|
3539
3679
|
args: toolArgs,
|
|
3540
|
-
stages:
|
|
3680
|
+
stages: pipelineStages("writer", language),
|
|
3541
3681
|
});
|
|
3542
3682
|
try {
|
|
3543
3683
|
const writeResult = await pipeline.writeNextChapter(directWriteBookId);
|
|
3544
3684
|
const writeNeedsReview = Boolean(writeResult.status && writeResult.status !== "ready-for-review");
|
|
3545
|
-
const
|
|
3685
|
+
const zhResponseText = writeNeedsReview
|
|
3546
3686
|
? [
|
|
3547
3687
|
`已为 ${directWriteBookId} 写出第 ${writeResult.chapterNumber} 章`,
|
|
3548
3688
|
writeResult.title ? `《${writeResult.title}》` : "",
|
|
@@ -3553,6 +3693,13 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3553
3693
|
writeResult.title ? `《${writeResult.title}》` : "",
|
|
3554
3694
|
`,字数 ${writeResult.wordCount},状态 ${writeResult.status}。`,
|
|
3555
3695
|
].join("");
|
|
3696
|
+
const enChapterRef = writeResult.title
|
|
3697
|
+
? `chapter ${writeResult.chapterNumber} "${writeResult.title}"`
|
|
3698
|
+
: `chapter ${writeResult.chapterNumber}`;
|
|
3699
|
+
const enResponseText = writeNeedsReview
|
|
3700
|
+
? `Wrote ${enChapterRef} for ${directWriteBookId}: ${writeResult.wordCount} words, but the review did not pass (status: ${writeResult.status}). Manual review is required before continuing.`
|
|
3701
|
+
: `Completed ${enChapterRef} for ${directWriteBookId}: ${writeResult.wordCount} words, status ${writeResult.status}.`;
|
|
3702
|
+
const responseText = pick(language, zhResponseText, enResponseText);
|
|
3556
3703
|
const toolResult = {
|
|
3557
3704
|
content: [{ type: "text", text: responseText }],
|
|
3558
3705
|
details: {
|
|
@@ -3576,7 +3723,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3576
3723
|
id: toolCallId,
|
|
3577
3724
|
tool: "sub_agent",
|
|
3578
3725
|
agent: "writer",
|
|
3579
|
-
label: resolveToolLabel("sub_agent", "writer"),
|
|
3726
|
+
label: resolveToolLabel("sub_agent", "writer", language),
|
|
3580
3727
|
status: writeNeedsReview ? "error" : "completed",
|
|
3581
3728
|
args: toolArgs,
|
|
3582
3729
|
result: responseText,
|
|
@@ -3600,12 +3747,13 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3600
3747
|
}
|
|
3601
3748
|
catch (error) {
|
|
3602
3749
|
const message = error instanceof Error ? error.message : String(error);
|
|
3750
|
+
const failure = formatAgentActionFailure(message, language);
|
|
3603
3751
|
const toolResult = { content: [{ type: "text", text: message }] };
|
|
3604
3752
|
const exec = {
|
|
3605
3753
|
id: toolCallId,
|
|
3606
3754
|
tool: "sub_agent",
|
|
3607
3755
|
agent: "writer",
|
|
3608
|
-
label: resolveToolLabel("sub_agent", "writer"),
|
|
3756
|
+
label: resolveToolLabel("sub_agent", "writer", language),
|
|
3609
3757
|
status: "error",
|
|
3610
3758
|
args: toolArgs,
|
|
3611
3759
|
error: message,
|
|
@@ -3625,9 +3773,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3625
3773
|
await refreshBookSessionFromTranscript().catch(() => undefined);
|
|
3626
3774
|
broadcast("agent:error", { instruction, activeBookId: agentBookId, sessionId: bookSession.sessionId, sessionKind, error: message });
|
|
3627
3775
|
return c.json({
|
|
3628
|
-
error: { code:
|
|
3629
|
-
response: message,
|
|
3630
|
-
},
|
|
3776
|
+
error: { code: failure.code, message: failure.message },
|
|
3777
|
+
response: failure.message,
|
|
3778
|
+
}, failure.status);
|
|
3631
3779
|
}
|
|
3632
3780
|
}
|
|
3633
3781
|
// The surface agent should speak the user's language, not just the project default.
|
|
@@ -3681,12 +3829,12 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3681
3829
|
if (event.type === "tool_execution_start") {
|
|
3682
3830
|
const args = event.args;
|
|
3683
3831
|
const agent = event.toolName === "sub_agent" ? args?.agent : undefined;
|
|
3684
|
-
const stages = agent ? (
|
|
3832
|
+
const stages = agent ? (pipelineStages(agent, language) ?? []) : [];
|
|
3685
3833
|
collectedToolExecs.push({
|
|
3686
3834
|
id: event.toolCallId,
|
|
3687
3835
|
tool: event.toolName,
|
|
3688
3836
|
agent,
|
|
3689
|
-
label: resolveToolLabel(event.toolName, agent),
|
|
3837
|
+
label: resolveToolLabel(event.toolName, agent, language),
|
|
3690
3838
|
status: "running",
|
|
3691
3839
|
args,
|
|
3692
3840
|
stages: stages.length > 0
|
|
@@ -3759,6 +3907,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3759
3907
|
agentBookId,
|
|
3760
3908
|
requestedIntent,
|
|
3761
3909
|
collectedToolExecs,
|
|
3910
|
+
language,
|
|
3762
3911
|
});
|
|
3763
3912
|
if (actionExecutionError) {
|
|
3764
3913
|
return c.json({
|
|
@@ -3821,7 +3970,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3821
3970
|
if (resolveCreatedBookIdFromToolExecs(collectedToolExecs)) {
|
|
3822
3971
|
await finalizeCreatedBook();
|
|
3823
3972
|
}
|
|
3824
|
-
const failure = formatAgentFailure(result.errorMessage);
|
|
3973
|
+
const failure = formatAgentFailure(result.errorMessage, language);
|
|
3825
3974
|
return c.json({
|
|
3826
3975
|
error: { code: failure.code, message: failure.message },
|
|
3827
3976
|
response: failure.message,
|
|
@@ -3832,6 +3981,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3832
3981
|
agentBookId,
|
|
3833
3982
|
requestedIntent,
|
|
3834
3983
|
collectedToolExecs,
|
|
3984
|
+
language,
|
|
3835
3985
|
});
|
|
3836
3986
|
if (actionExecutionError) {
|
|
3837
3987
|
return c.json({
|
|
@@ -3853,7 +4003,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3853
4003
|
},
|
|
3854
4004
|
});
|
|
3855
4005
|
}
|
|
3856
|
-
const emptyMessage = "模型未返回文本内容。请检查协议类型(chat/responses)、流式开关或上游服务兼容性。";
|
|
4006
|
+
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.");
|
|
3857
4007
|
if (resolveCreatedBookIdFromToolExecs(collectedToolExecs)) {
|
|
3858
4008
|
await finalizeCreatedBook();
|
|
3859
4009
|
}
|
|
@@ -3888,11 +4038,14 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3888
4038
|
// Agent busy — return 429 with user-friendly message
|
|
3889
4039
|
if (/already processing|prompt.*queue/i.test(msg)) {
|
|
3890
4040
|
return c.json({
|
|
3891
|
-
error: {
|
|
3892
|
-
|
|
4041
|
+
error: {
|
|
4042
|
+
code: "AGENT_BUSY",
|
|
4043
|
+
message: pick(language, "正在处理中,请等待当前操作完成", "Still processing. Wait for the current operation to finish"),
|
|
4044
|
+
},
|
|
4045
|
+
response: pick(language, "正在处理中,请等待当前操作完成后再发送。", "Still processing. Wait for the current operation to finish before sending again."),
|
|
3893
4046
|
}, 429);
|
|
3894
4047
|
}
|
|
3895
|
-
const failure = formatAgentFailure(msg);
|
|
4048
|
+
const failure = formatAgentFailure(msg, language);
|
|
3896
4049
|
return c.json({ error: { code: failure.code, message: failure.message } }, failure.status);
|
|
3897
4050
|
}
|
|
3898
4051
|
});
|
|
@@ -3963,6 +4116,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
3963
4116
|
return c.json({ error: "Chapter not found" }, 404);
|
|
3964
4117
|
const pipeline = new PipelineRunner(await buildPipelineConfig({
|
|
3965
4118
|
externalContext: body.brief,
|
|
4119
|
+
bookIdForSettings: id,
|
|
3966
4120
|
}));
|
|
3967
4121
|
const normalizedMode = body.mode ?? "spot-fix";
|
|
3968
4122
|
const result = await pipeline.reviseDraft(id, chapterNum, normalizedMode);
|
|
@@ -4731,6 +4885,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
4731
4885
|
preferredStream: currentConfig.llm.stream,
|
|
4732
4886
|
preferredModel: currentConfig.llm.model,
|
|
4733
4887
|
proxyUrl: currentConfig.llm.proxyUrl,
|
|
4888
|
+
language: normalizeStudioLanguage(currentConfig.language),
|
|
4734
4889
|
}), DOCTOR_LLM_PROBE_BUDGET_MS, "doctor llm probe");
|
|
4735
4890
|
checks.llmConnected = probe.ok;
|
|
4736
4891
|
}
|
|
@@ -4762,6 +4917,140 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
|
|
|
4762
4917
|
films.sort((a, b) => a.title.localeCompare(b.title, "zh"));
|
|
4763
4918
|
return c.json({ films });
|
|
4764
4919
|
});
|
|
4920
|
+
app.get("/api/v1/translations", async (c) => {
|
|
4921
|
+
const translationsDir = join(root, "translations");
|
|
4922
|
+
let entries = [];
|
|
4923
|
+
try {
|
|
4924
|
+
const dirents = await readdir(translationsDir, { withFileTypes: true });
|
|
4925
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4926
|
+
}
|
|
4927
|
+
catch (err) {
|
|
4928
|
+
if (err.code !== "ENOENT")
|
|
4929
|
+
throw err;
|
|
4930
|
+
}
|
|
4931
|
+
const translations = [];
|
|
4932
|
+
for (const projectId of entries) {
|
|
4933
|
+
if (!isSafeBookId(projectId))
|
|
4934
|
+
continue;
|
|
4935
|
+
try {
|
|
4936
|
+
const manifest = await loadTranslationManifest(root, projectId);
|
|
4937
|
+
translations.push({
|
|
4938
|
+
projectId,
|
|
4939
|
+
title: manifest.title,
|
|
4940
|
+
sourceLanguage: manifest.sourceLanguage,
|
|
4941
|
+
targetLanguage: manifest.targetLanguage,
|
|
4942
|
+
chapters: manifest.chapters.length,
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
catch {
|
|
4946
|
+
// Skip dirs without a valid translation manifest.
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
translations.sort((a, b) => b.projectId.localeCompare(a.projectId));
|
|
4950
|
+
return c.json({ translations });
|
|
4951
|
+
});
|
|
4952
|
+
app.post("/api/v1/translations/upload", async (c) => {
|
|
4953
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4954
|
+
const result = await storeTranslationUpload(root, body);
|
|
4955
|
+
return c.json(result);
|
|
4956
|
+
});
|
|
4957
|
+
app.post("/api/v1/translations/create", async (c) => {
|
|
4958
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4959
|
+
if (!body.filePath?.trim()) {
|
|
4960
|
+
return c.json({ error: { code: "MISSING_FILE_PATH", message: "filePath is required" } }, 400);
|
|
4961
|
+
}
|
|
4962
|
+
if (!body.sourceLanguage?.trim() || !body.targetLanguage?.trim()) {
|
|
4963
|
+
return c.json({ error: { code: "MISSING_LANGUAGES", message: "sourceLanguage and targetLanguage are required" } }, 400);
|
|
4964
|
+
}
|
|
4965
|
+
const result = await createTranslationProjectFromFile(root, {
|
|
4966
|
+
filePath: body.filePath,
|
|
4967
|
+
sourceLanguage: body.sourceLanguage,
|
|
4968
|
+
targetLanguage: body.targetLanguage,
|
|
4969
|
+
title: body.title,
|
|
4970
|
+
segmentMaxChars: body.segmentMaxChars,
|
|
4971
|
+
});
|
|
4972
|
+
return c.json({
|
|
4973
|
+
...result,
|
|
4974
|
+
projectId: result.manifest.id,
|
|
4975
|
+
title: result.manifest.title,
|
|
4976
|
+
});
|
|
4977
|
+
});
|
|
4978
|
+
app.get("/api/v1/translations/:id", async (c) => {
|
|
4979
|
+
const id = c.req.param("id");
|
|
4980
|
+
if (!isSafeBookId(id)) {
|
|
4981
|
+
return c.json({ error: { code: "INVALID_ID", message: `invalid translation id: ${id}` } }, 400);
|
|
4982
|
+
}
|
|
4983
|
+
try {
|
|
4984
|
+
const manifest = await loadTranslationManifest(root, id);
|
|
4985
|
+
const reportPath = join(root, "translations", id, "review-report.md");
|
|
4986
|
+
const report = await readFile(reportPath, "utf-8").catch(() => "");
|
|
4987
|
+
const chapters = await Promise.all(manifest.chapters.map(async (chapter) => {
|
|
4988
|
+
const source = await loadTranslationChapter(root, chapter.sourcePath);
|
|
4989
|
+
const translated = await loadTranslationChapter(root, chapter.translatedPath).catch(() => ({
|
|
4990
|
+
...source,
|
|
4991
|
+
segments: [],
|
|
4992
|
+
}));
|
|
4993
|
+
const targets = new Map(translated.segments.map((segment) => [segment.index, segment]));
|
|
4994
|
+
return {
|
|
4995
|
+
number: chapter.number,
|
|
4996
|
+
title: chapter.title,
|
|
4997
|
+
status: chapter.status,
|
|
4998
|
+
segments: source.segments.map((segment) => ({
|
|
4999
|
+
index: segment.index,
|
|
5000
|
+
source: segment.source,
|
|
5001
|
+
target: targets.get(segment.index)?.target ?? "",
|
|
5002
|
+
notes: targets.get(segment.index)?.notes ?? "",
|
|
5003
|
+
})),
|
|
5004
|
+
};
|
|
5005
|
+
}));
|
|
5006
|
+
return c.json({ manifest, report, chapters });
|
|
5007
|
+
}
|
|
5008
|
+
catch (error) {
|
|
5009
|
+
if (error.code === "ENOENT") {
|
|
5010
|
+
return c.json({ error: { code: "NOT_FOUND", message: `translation project not found for ${id}` } }, 404);
|
|
5011
|
+
}
|
|
5012
|
+
throw error;
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
app.post("/api/v1/translations/:id/run", async (c) => {
|
|
5016
|
+
const id = c.req.param("id");
|
|
5017
|
+
if (!isSafeBookId(id)) {
|
|
5018
|
+
return c.json({ error: { code: "INVALID_ID", message: `invalid translation id: ${id}` } }, 400);
|
|
5019
|
+
}
|
|
5020
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5021
|
+
try {
|
|
5022
|
+
const currentConfig = await loadCurrentProjectConfig();
|
|
5023
|
+
const model = createLLMTranslationModel({
|
|
5024
|
+
client: createLLMClient(currentConfig.llm),
|
|
5025
|
+
model: currentConfig.llm.model,
|
|
5026
|
+
maxTokens: body.maxTokens,
|
|
5027
|
+
});
|
|
5028
|
+
const result = await runTranslationProject(root, id, {
|
|
5029
|
+
model,
|
|
5030
|
+
batchSize: body.batchSize,
|
|
5031
|
+
});
|
|
5032
|
+
return c.json(result);
|
|
5033
|
+
}
|
|
5034
|
+
catch (error) {
|
|
5035
|
+
if (error instanceof ApiError)
|
|
5036
|
+
throw error;
|
|
5037
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5038
|
+
const isUpstream = /API|LLM|provider|upstream|temporarily unavailable|rate limit|quota|fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|503|502|504/i.test(message);
|
|
5039
|
+
throw new ApiError(isUpstream ? 502 : 500, "TRANSLATION_RUN_FAILED", message || "Translation run failed.");
|
|
5040
|
+
}
|
|
5041
|
+
});
|
|
5042
|
+
app.post("/api/v1/translations/:id/export", async (c) => {
|
|
5043
|
+
const id = c.req.param("id");
|
|
5044
|
+
if (!isSafeBookId(id)) {
|
|
5045
|
+
return c.json({ error: { code: "INVALID_ID", message: `invalid translation id: ${id}` } }, 400);
|
|
5046
|
+
}
|
|
5047
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5048
|
+
const result = await writeTranslationExport(root, id, {
|
|
5049
|
+
format: body.format ?? "md",
|
|
5050
|
+
outputPath: body.outputPath,
|
|
5051
|
+
});
|
|
5052
|
+
return c.json(result);
|
|
5053
|
+
});
|
|
4765
5054
|
app.post("/api/v1/projects/:id/story-graph/delta", async (c) => {
|
|
4766
5055
|
const id = c.req.param("id");
|
|
4767
5056
|
if (!isSafeBookId(id)) {
|