@kienmatu/inkos-studio 0.1.3 → 3.3.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.
Files changed (69) hide show
  1. package/dist/api/server.d.ts.map +1 -1
  2. package/dist/api/server.js +192 -10
  3. package/dist/api/server.js.map +1 -1
  4. package/dist/api/task-store.d.ts +1 -0
  5. package/dist/api/task-store.d.ts.map +1 -1
  6. package/dist/api/task-store.js +3 -0
  7. package/dist/api/task-store.js.map +1 -1
  8. package/dist/assets/{FilmWizard-BAeDX_rI.js → FilmWizard-DsSZPo5A.js} +1 -1
  9. package/dist/assets/{FlowView-C6n6r8pJ.js → FlowView-DtborsGc.js} +1 -1
  10. package/dist/assets/{_baseUniq-CHNUfn5h.js → _baseUniq-BEIj9tcE.js} +1 -1
  11. package/dist/assets/{arc-CdPV8k-0.js → arc-C7Rm_CAk.js} +1 -1
  12. package/dist/assets/{architectureDiagram-Q4EWVU46-oodkcVSQ.js → architectureDiagram-Q4EWVU46-DlmfKm7b.js} +1 -1
  13. package/dist/assets/{blockDiagram-DXYQGD6D-CIbxhPHs.js → blockDiagram-DXYQGD6D-DwWcyB5e.js} +1 -1
  14. package/dist/assets/{c4Diagram-AHTNJAMY-CxIpgCiP.js → c4Diagram-AHTNJAMY-DJr7ig4J.js} +1 -1
  15. package/dist/assets/channel-D3RJhFng.js +1 -0
  16. package/dist/assets/{chunk-4BX2VUAB-Cf70tfVa.js → chunk-4BX2VUAB-BEbkhxPj.js} +1 -1
  17. package/dist/assets/{chunk-4TB4RGXK-uFQAF256.js → chunk-4TB4RGXK-CKlayWkT.js} +1 -1
  18. package/dist/assets/{chunk-55IACEB6-4u7WcTds.js → chunk-55IACEB6-Do_NAv20.js} +1 -1
  19. package/dist/assets/{chunk-EDXVE4YY-DKvxdCCA.js → chunk-EDXVE4YY-CIaC2_XQ.js} +1 -1
  20. package/dist/assets/{chunk-FMBD7UC4-DA6v8xvJ.js → chunk-FMBD7UC4-DK2fFb-S.js} +1 -1
  21. package/dist/assets/{chunk-OYMX7WX6-DwLlaWl9.js → chunk-OYMX7WX6-DOk8ZO7Q.js} +1 -1
  22. package/dist/assets/{chunk-QZHKN3VN-CFgB8UWK.js → chunk-QZHKN3VN-CWQlxTIX.js} +1 -1
  23. package/dist/assets/{chunk-YZCP3GAM-JgBXvZgf.js → chunk-YZCP3GAM-BKMKNDs_.js} +1 -1
  24. package/dist/assets/classDiagram-6PBFFD2Q-Ctnvqnpw.js +1 -0
  25. package/dist/assets/classDiagram-v2-HSJHXN6E-Ctnvqnpw.js +1 -0
  26. package/dist/assets/clone-BdOaHqQL.js +1 -0
  27. package/dist/assets/{cose-bilkent-S5V4N54A-DKMKqdAL.js → cose-bilkent-S5V4N54A-CtsSt-S-.js} +1 -1
  28. package/dist/assets/{dagre-KV5264BT-DuZ4zm4B.js → dagre-KV5264BT-zROnUgJY.js} +1 -1
  29. package/dist/assets/{diagram-5BDNPKRD-CoA3HYsp.js → diagram-5BDNPKRD-Ct6aFDWr.js} +1 -1
  30. package/dist/assets/{diagram-G4DWMVQ6-DVnnKNaz.js → diagram-G4DWMVQ6-B2bwAps6.js} +1 -1
  31. package/dist/assets/{diagram-MMDJMWI5-iB_Pzv_O.js → diagram-MMDJMWI5-CbwZreHT.js} +1 -1
  32. package/dist/assets/{diagram-TYMM5635-CJ1VtRnc.js → diagram-TYMM5635-gKzWLxCO.js} +1 -1
  33. package/dist/assets/{erDiagram-SMLLAGMA-SWAf-S26.js → erDiagram-SMLLAGMA-DipB25lI.js} +1 -1
  34. package/dist/assets/{flowDiagram-DWJPFMVM-bjSm1F6q.js → flowDiagram-DWJPFMVM-C9VuPnc4.js} +1 -1
  35. package/dist/assets/{ganttDiagram-T4ZO3ILL-B3eioyUQ.js → ganttDiagram-T4ZO3ILL-B2dTjD_8.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DGaIKwEh.js → gitGraphDiagram-UUTBAWPF-BZXYA0Ec.js} +1 -1
  37. package/dist/assets/{graph-zC0IkLW4.js → graph-iDCrfqrI.js} +1 -1
  38. package/dist/assets/{highlighted-body-OFNGDK62-CkE4PgdB.js → highlighted-body-OFNGDK62-CP2BM2l8.js} +1 -1
  39. package/dist/assets/index-CTu6WYGu.css +1 -0
  40. package/dist/assets/{index-9mF0_S-1.js → index-PEL5ijWf.js} +298 -293
  41. package/dist/assets/{infoDiagram-42DDH7IO-z7BfWhD6.js → infoDiagram-42DDH7IO-Bd7gtY2E.js} +1 -1
  42. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DgkoM8pX.js → ishikawaDiagram-UXIWVN3A-HMp96hG2.js} +1 -1
  43. package/dist/assets/{journeyDiagram-VCZTEJTY-7szsjnaK.js → journeyDiagram-VCZTEJTY-D36jcF3p.js} +1 -1
  44. package/dist/assets/{kanban-definition-6JOO6SKY-CN_iYP62.js → kanban-definition-6JOO6SKY-C4wSbzoj.js} +1 -1
  45. package/dist/assets/{layout-B1_IEbhE.js → layout-DqXRWJEx.js} +1 -1
  46. package/dist/assets/{linear-DgZKeCD_.js → linear-QlXzAe7A.js} +1 -1
  47. package/dist/assets/{min-BGmTIgk7.js → min-iaA6vGbj.js} +1 -1
  48. package/dist/assets/{mindmap-definition-QFDTVHPH-CaZaCUVQ.js → mindmap-definition-QFDTVHPH-B-PCe4LP.js} +1 -1
  49. package/dist/assets/{pieDiagram-DEJITSTG-Bai_9_pu.js → pieDiagram-DEJITSTG-DRSPzSKd.js} +1 -1
  50. package/dist/assets/{quadrantDiagram-34T5L4WZ-CgKE3-Ag.js → quadrantDiagram-34T5L4WZ-D-c8JbLY.js} +1 -1
  51. package/dist/assets/{requirementDiagram-MS252O5E-CiP3d8R4.js → requirementDiagram-MS252O5E-CdOoLbvR.js} +1 -1
  52. package/dist/assets/{sankeyDiagram-XADWPNL6-RIeASvdJ.js → sankeyDiagram-XADWPNL6-CQDxnTGB.js} +1 -1
  53. package/dist/assets/{sequenceDiagram-FGHM5R23-Buf7D5PQ.js → sequenceDiagram-FGHM5R23-SZbPsgFb.js} +1 -1
  54. package/dist/assets/{stateDiagram-FHFEXIEX-5uYn-_xo.js → stateDiagram-FHFEXIEX-uFY2njzT.js} +1 -1
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BJCQ3uMo.js +1 -0
  56. package/dist/assets/{timeline-definition-GMOUNBTQ-fcTegT1X.js → timeline-definition-GMOUNBTQ-n_oY30Kg.js} +1 -1
  57. package/dist/assets/{value-CCp47p1v.js → value-hJ3k-Vla.js} +1 -1
  58. package/dist/assets/{vennDiagram-DHZGUBPP-BUY_I73j.js → vennDiagram-DHZGUBPP-B9VAsWw3.js} +1 -1
  59. package/dist/assets/{wardley-RL74JXVD-CfGeUcvr.js → wardley-RL74JXVD-DPynk1O-.js} +1 -1
  60. package/dist/assets/{wardleyDiagram-NUSXRM2D-0A6kvF9i.js → wardleyDiagram-NUSXRM2D-BxHtrGEZ.js} +1 -1
  61. package/dist/assets/{xychartDiagram-5P7HB3ND-C2L50m3n.js → xychartDiagram-5P7HB3ND-B3Hi8GO_.js} +1 -1
  62. package/dist/index.html +2 -2
  63. package/package.json +2 -2
  64. package/dist/assets/channel-BudrdgJ_.js +0 -1
  65. package/dist/assets/classDiagram-6PBFFD2Q-CNYOE6Pk.js +0 -1
  66. package/dist/assets/classDiagram-v2-HSJHXN6E-CNYOE6Pk.js +0 -1
  67. package/dist/assets/clone-CbLIuAlV.js +0 -1
  68. package/dist/assets/index-B9yC-RFq.css +0 -1
  69. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CiNPYrtS.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,OAAO,EAsHL,KAAK,aAAa,EAIlB,KAAK,aAAa,EAMnB,MAAM,sBAAsB,CAAC;AAkB9B,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAItE;AAGD,wBAAgB,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGtF;AAu3ED,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE;IAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,aAAa,CAAA;CAAO,8EAq/H7I;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,OAAO,EAuHL,KAAK,aAAa,EAMlB,KAAK,aAAa,EAOnB,MAAM,sBAAsB,CAAC;AAkB9B,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAItE;AAGD,wBAAgB,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGtF;AAwgFD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE;IAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,aAAa,CAAA;CAAO,8EAsjI7I;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,SAAO,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -4,7 +4,7 @@ import { streamSSE } from "hono/streaming";
4
4
  import { serve } from "@hono/node-server";
5
5
  import { gzipSync } from "node:zlib";
6
6
  import { randomUUID } from "node:crypto";
7
- 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, runWorkerAgent, buildExportArtifact, evaluateBookQuality, ConsolidatorAgent, DetectionConfigSchema, ResearchSearchConfigSchema, GLOBAL_ENV_PATH, COVER_PROVIDER_PRESETS, createPlayDB, PlayStore, buildPlayEntityImagePrompt, buildPlaySceneImagePrompt, generatePlayImage, readPlayImageManifest, readPlayImageSettings, writePlayImageSettings, Scheduler, coverSecretKey, normalizeCoverBaseUrl, resolveCoverProviderPreset, SessionKindSchema, normalizeActionSource as normalizeCoreActionSource, normalizeActionPayload as normalizeCoreActionPayload, normalizePlayMode as normalizeCorePlayMode, normalizeRequestedIntent as normalizeCoreRequestedIntent, normalizeSkillIdList as normalizeCoreSkillIdList, inferLanguage, toWritingLanguage, ingestMaterial, createSkillRegistry, loadAvailableAgentSkills, activatedSkillIds, mergeActivatedSkillGuidance, resolveProductionSkillActivations, parseAgentSkillDocument, getBuiltinPrompt, listBuiltinPromptPacks, listBuiltinPrompts, loadPromptPackPrompt, promptOverridePath, toPosixPath, createGenerateCoverTool, createInteractiveFilmCreationTool, createPlayStartTool, createScriptCreationTool, createShortFictionRunTool, createStoryboardCreationTool, createTranslationCreateTool, createFanficBookTool, createContinuationImportTool, createSpinoffBookTool, createImitationBookTool, createSubAgentTool, createDraftStructureTool, createConnectChoiceTool, createRemoveNodeTool, createLLMTranslationModel, deleteLatestChapter, executeEditTransaction, listChapterVersions, readChapterPlanDocument, readChapterUserBrief, readChapterVersion, saveChapterUserBrief, createTranslationProjectFromFile, loadTranslationChapter, loadTranslationManifest, runTranslationProject, writeTranslationExport, filmLLMDepsFromClient, applyGraphDelta, loadStoryGraph, reviewStoryGraph, exportInk, buildPlayableHtml, analyzeEmotionalArcs, analyzePathDistribution, generateNodeImage, defaultNodeImageDeps, } from "@kienmatu/inkos-core";
7
+ 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, runWorkerAgent, buildExportArtifact, createProductionRunSnapshot, evaluateBookQuality, ConsolidatorAgent, DetectionConfigSchema, ResearchSearchConfigSchema, GLOBAL_ENV_PATH, COVER_PROVIDER_PRESETS, createPlayDB, PlayStore, buildPlayEntityImagePrompt, buildPlaySceneImagePrompt, generatePlayImage, readPlayImageManifest, readPlayImageSettings, writePlayImageSettings, Scheduler, coverSecretKey, normalizeCoverBaseUrl, resolveCoverProviderPreset, SessionKindSchema, normalizeActionSource as normalizeCoreActionSource, normalizeActionPayload as normalizeCoreActionPayload, normalizePlayMode as normalizeCorePlayMode, normalizeRequestedIntent as normalizeCoreRequestedIntent, normalizeSkillIdList as normalizeCoreSkillIdList, inferLanguage, toWritingLanguage, ingestMaterial, createSkillRegistry, loadAvailableAgentSkills, activatedSkillIds, mergeActivatedSkillGuidance, resolveProductionSkillActivations, parseAgentSkillDocument, getBuiltinPrompt, listBuiltinPromptPacks, listBuiltinPrompts, loadPromptPackPrompt, promptOverridePath, toPosixPath, createGenerateCoverTool, createInteractiveFilmCreationTool, createPlayStartTool, createScriptCreationTool, createShortFictionRunTool, createStoryboardCreationTool, createTranslationCreateTool, createFanficBookTool, createContinuationImportTool, createSpinoffBookTool, createImitationBookTool, createSubAgentTool, createDraftStructureTool, createConnectChoiceTool, createRemoveNodeTool, createLLMTranslationModel, deleteLatestChapter, executeEditTransaction, listChapterVersions, readChapterPlanDocument, readChapterUserBrief, readChapterVersion, saveChapterUserBrief, createTranslationProjectFromFile, loadTranslationChapter, loadTranslationManifest, runTranslationProject, writeTranslationExport, filmLLMDepsFromClient, applyGraphDelta, loadStoryGraph, reviewStoryGraph, exportInk, buildPlayableHtml, analyzeEmotionalArcs, analyzePathDistribution, generateNodeImage, defaultNodeImageDeps, writeProductionRunSnapshot, } from "@kienmatu/inkos-core";
8
8
  import { isConfirmedProductionAction } from "../shared/confirmed-production.js";
9
9
  import { summarizeToolResult } from "../shared/tool-result.js";
10
10
  import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -879,6 +879,7 @@ function toolResultText(result, lang = "vi") {
879
879
  }
880
880
  async function executeConfirmedProductionAction(args) {
881
881
  const lang = args.language ?? "vi";
882
+ const uiLang = args.uiLanguage ?? lang;
882
883
  const id = args.taskId;
883
884
  const actionPayload = args.actionPayload;
884
885
  const configuredSkills = await loadAvailableAgentSkills({ projectRoot: args.root });
@@ -950,7 +951,7 @@ async function executeConfirmedProductionAction(args) {
950
951
  else if (args.requestedIntent === "generate_cover") {
951
952
  const payload = actionPayload?.generateCover;
952
953
  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."));
953
- tool = createGenerateCoverTool(args.root, { actionPayload });
954
+ tool = createGenerateCoverTool(args.root, { actionPayload, language: toWritingLanguage(lang) });
954
955
  params = {
955
956
  title,
956
957
  ...(payload?.intro ? { intro: payload.intro } : {}),
@@ -1204,10 +1205,10 @@ async function executeConfirmedProductionAction(args) {
1204
1205
  id,
1205
1206
  tool: tool.name,
1206
1207
  agent,
1207
- label: resolveToolLabel(tool.name, agent, lang),
1208
+ label: resolveToolLabel(tool.name, agent, uiLang),
1208
1209
  status: "running",
1209
1210
  args: params,
1210
- stages: agent ? pipelineStages(agent, lang)?.map(label => ({ label, status: "pending" })) : undefined,
1211
+ stages: agent ? pipelineStages(agent, uiLang)?.map(label => ({ label, status: "pending" })) : undefined,
1211
1212
  startedAt: Date.now(),
1212
1213
  };
1213
1214
  await args.onTaskChange(exec);
@@ -1266,6 +1267,124 @@ async function executeConfirmedProductionAction(args) {
1266
1267
  throw new ConfirmedActionExecutionError(message, exec, error);
1267
1268
  }
1268
1269
  }
1270
+ function parseProductionObservation(value) {
1271
+ if (!value || typeof value !== "object" || Array.isArray(value))
1272
+ return null;
1273
+ const record = value;
1274
+ if (typeof record.metric !== "string"
1275
+ || !["info", "warning", "blocking"].includes(String(record.severity))
1276
+ || typeof record.repairable !== "boolean"
1277
+ || (record.evidence !== undefined && typeof record.evidence !== "string")) {
1278
+ return null;
1279
+ }
1280
+ return {
1281
+ metric: record.metric,
1282
+ expected: record.expected,
1283
+ actual: record.actual,
1284
+ severity: record.severity,
1285
+ ...(typeof record.evidence === "string" ? { evidence: record.evidence } : {}),
1286
+ repairable: record.repairable,
1287
+ };
1288
+ }
1289
+ function parseStudioShortRun(value, expectedStoryId) {
1290
+ if (!value || typeof value !== "object" || Array.isArray(value))
1291
+ return null;
1292
+ const record = value;
1293
+ const status = record.status;
1294
+ const artifacts = Array.isArray(record.artifacts) && record.artifacts.every((item) => typeof item === "string")
1295
+ ? record.artifacts
1296
+ : null;
1297
+ const observations = Array.isArray(record.observations)
1298
+ ? record.observations.map(parseProductionObservation)
1299
+ : null;
1300
+ if (record.version !== 1
1301
+ || record.kind !== "short-fiction"
1302
+ || record.id !== expectedStoryId
1303
+ || (status !== "complete" && status !== "needs-review")
1304
+ || typeof record.stage !== "string"
1305
+ || !artifacts
1306
+ || !observations
1307
+ || observations.some((observation) => observation === null)
1308
+ || typeof record.updatedAt !== "string"
1309
+ || (record.model !== undefined && typeof record.model !== "string")
1310
+ || (record.skillIds !== undefined && (!Array.isArray(record.skillIds) || !record.skillIds.every((item) => typeof item === "string")))
1311
+ || (record.resumeCursor !== undefined && typeof record.resumeCursor !== "string")
1312
+ || (record.error !== undefined && typeof record.error !== "string")) {
1313
+ return null;
1314
+ }
1315
+ return {
1316
+ version: 1,
1317
+ kind: "short-fiction",
1318
+ id: expectedStoryId,
1319
+ status,
1320
+ stage: record.stage,
1321
+ artifacts,
1322
+ observations: observations,
1323
+ ...(typeof record.model === "string" ? { model: record.model } : {}),
1324
+ ...(Array.isArray(record.skillIds) ? { skillIds: record.skillIds } : {}),
1325
+ ...(typeof record.resumeCursor === "string" ? { resumeCursor: record.resumeCursor } : {}),
1326
+ ...(typeof record.error === "string" ? { error: record.error } : {}),
1327
+ updatedAt: record.updatedAt,
1328
+ };
1329
+ }
1330
+ function parseVisibleShortRunIndex(value, expectedStoryId) {
1331
+ if (!value || typeof value !== "object" || Array.isArray(value))
1332
+ return null;
1333
+ const record = value;
1334
+ const status = record.status;
1335
+ if (record.version !== 1
1336
+ || record.kind !== "short-fiction"
1337
+ || record.id !== expectedStoryId
1338
+ || (status !== "complete" && status !== "needs-review")
1339
+ || typeof record.updatedAt !== "string") {
1340
+ return null;
1341
+ }
1342
+ return { storyId: expectedStoryId, status, updatedAt: record.updatedAt };
1343
+ }
1344
+ function parseShortStoryTitle(value) {
1345
+ if (!value || typeof value !== "object" || Array.isArray(value))
1346
+ return null;
1347
+ const storyTitle = value.storyTitle;
1348
+ return typeof storyTitle === "string" && storyTitle.trim() ? storyTitle.trim() : null;
1349
+ }
1350
+ async function loadStudioShortList(root) {
1351
+ const shortsDir = join(root, "shorts");
1352
+ let storyIds = [];
1353
+ try {
1354
+ const entries = await readdir(shortsDir, { withFileTypes: true });
1355
+ storyIds = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1356
+ }
1357
+ catch (error) {
1358
+ if (error.code !== "ENOENT")
1359
+ throw error;
1360
+ }
1361
+ const shorts = [];
1362
+ for (const storyId of storyIds) {
1363
+ if (!isSafeBookId(storyId))
1364
+ continue;
1365
+ const shortDir = join(shortsDir, storyId);
1366
+ try {
1367
+ const status = JSON.parse(await readFile(join(shortDir, "status.json"), "utf-8"));
1368
+ const run = parseVisibleShortRunIndex(status, storyId);
1369
+ if (!run)
1370
+ continue;
1371
+ const story = JSON.parse(await readFile(join(shortDir, "final", "short-story.json"), "utf-8"));
1372
+ const title = parseShortStoryTitle(story) ?? storyId;
1373
+ await readFile(join(shortDir, "final", "full.md"), "utf-8");
1374
+ shorts.push({
1375
+ storyId,
1376
+ title,
1377
+ status: run.status,
1378
+ finalMarkdownPath: toPosixPath(join("shorts", storyId, "final", "full.md")),
1379
+ updatedAt: run.updatedAt,
1380
+ });
1381
+ }
1382
+ catch {
1383
+ // A malformed or incomplete directory is not a completed short work.
1384
+ }
1385
+ }
1386
+ return shorts;
1387
+ }
1269
1388
  const subscribers = new Set();
1270
1389
  const bookCreateStatus = new Map();
1271
1390
  // 内存缓存:service -> 模型列表 + 更新时间戳;避免每次 sidebar 挂载时都打真实 LLM /models
@@ -2010,6 +2129,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2010
2129
  const state = new StateManager(root);
2011
2130
  let cachedConfig = initialConfig;
2012
2131
  const activeConfirmedTasks = new Map();
2132
+ const confirmedTaskRevisions = new Map();
2013
2133
  // 确认式生产任务的单任务名额(sessionId → taskId)。原来的检查是"await 读快照
2014
2134
  // → 之后才 set controller"的 check-then-act:两个并发确认请求都能通过检查,
2015
2135
  // 双任务同时启动、快照互相覆盖。这里在任何 await 之前同步占位,占位失败的
@@ -2033,8 +2153,11 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2033
2153
  const persistConfirmedTask = async (sessionId, requestedIntent, exec, sourceRequestId) => {
2034
2154
  if (deletedSessionIds.has(sessionId))
2035
2155
  return;
2156
+ const revision = (confirmedTaskRevisions.get(exec.id) ?? 0) + 1;
2157
+ confirmedTaskRevisions.set(exec.id, revision);
2036
2158
  const snapshot = {
2037
2159
  version: 1,
2160
+ revision,
2038
2161
  sessionId,
2039
2162
  ...(sourceRequestId ? { sourceRequestId } : {}),
2040
2163
  requestedIntent,
@@ -2045,7 +2168,10 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2045
2168
  ...(exec.logs ? { logs: [...exec.logs] } : {}),
2046
2169
  },
2047
2170
  };
2171
+ // Publish the same durable snapshot that refresh recovery reads, so an
2172
+ // already-open task card sees milestone logs without requiring a reload.
2048
2173
  await saveStudioTaskSnapshot(root, snapshot);
2174
+ broadcast("task:snapshot", snapshot);
2049
2175
  };
2050
2176
  const loadReconciledTaskSnapshot = async (sessionId) => {
2051
2177
  const task = await loadStudioTaskSnapshot(root, sessionId);
@@ -2174,6 +2300,9 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2174
2300
  }
2175
2301
  : sseSink;
2176
2302
  const logger = createLogger({ tag: "studio", sinks: [scopedSseSink, consoleSink] });
2303
+ // Progress lines are read by the person watching Studio, so they follow the
2304
+ // UI language rather than the book's writing language.
2305
+ const logLanguage = await currentProjectLanguage();
2177
2306
  return {
2178
2307
  client: overrides?.client ?? createLLMClient(currentConfig.llm),
2179
2308
  model: overrides?.model ?? currentConfig.llm.model,
@@ -2186,6 +2315,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
2186
2315
  modelOverrides: currentConfig.modelOverrides,
2187
2316
  notifyChannels: currentConfig.notify,
2188
2317
  logger,
2318
+ logLanguage,
2189
2319
  onContextCompression: (event) => {
2190
2320
  broadcast("context:compression", {
2191
2321
  ...(overrides?.sessionIdForSSE ? { sessionId: overrides.sessionIdForSSE } : {}),
@@ -3189,13 +3319,29 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
3189
3319
  id: `custom:${s.name ?? "Custom"}`,
3190
3320
  baseUrl: s.baseUrl ?? "",
3191
3321
  label: s.name ?? "Custom",
3322
+ configuredModels: s.models ?? [],
3192
3323
  }))
3193
- .filter((s) => s.baseUrl && Boolean(secrets.services[s.id]?.apiKey));
3194
- const groups = await Promise.all(customs.map(async (s) => ({
3195
- service: s.id,
3196
- label: s.label,
3197
- models: filterTextChatModels(await probeModelsFromUpstream(s.baseUrl, secrets.services[s.id].apiKey, 10_000)),
3198
- })));
3324
+ .filter((s) => {
3325
+ if (!s.baseUrl)
3326
+ return false;
3327
+ if (secrets.services[s.id]?.apiKey)
3328
+ return true;
3329
+ // Local/self-hosted endpoints are usable without a key.
3330
+ return isApiKeyOptionalForEndpoint({
3331
+ provider: resolveServiceProviderFamily("custom") ?? "openai",
3332
+ baseUrl: s.baseUrl,
3333
+ });
3334
+ });
3335
+ const groups = await Promise.all(customs.map(async (s) => {
3336
+ const probed = filterTextChatModels(await probeModelsFromUpstream(s.baseUrl, secrets.services[s.id]?.apiKey ?? "", 10_000));
3337
+ // The probe swallows every failure (timeout, offline upstream, bad
3338
+ // gateway) and returns []. Without the configured ids as a fallback a
3339
+ // single slow reload empties the model picker, so the UI drops back to
3340
+ // "set up models" even though the service is configured.
3341
+ const models = mergeServiceModelIds(probed.map((model) => model.id), s.configuredModels)
3342
+ .map((id) => probed.find((model) => model.id.toLowerCase() === id.toLowerCase()) ?? { id, name: id });
3343
+ return { service: s.id, label: s.label, models };
3344
+ }));
3199
3345
  return c.json({ groups });
3200
3346
  });
3201
3347
  app.get("/api/v1/services/:service/models", async (c) => {
@@ -4025,6 +4171,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4025
4171
  requestedSkills,
4026
4172
  disabledSkills,
4027
4173
  language: surfaceLanguage,
4174
+ uiLanguage: language,
4028
4175
  taskId,
4029
4176
  sourceRequestId,
4030
4177
  signal: taskController.signal,
@@ -4108,6 +4255,7 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
4108
4255
  }
4109
4256
  finally {
4110
4257
  activeConfirmedTasks.delete(taskId);
4258
+ confirmedTaskRevisions.delete(taskId);
4111
4259
  reservedProductionSessions.delete(reservedSessionId);
4112
4260
  }
4113
4261
  }
@@ -5313,6 +5461,40 @@ export function createStudioServer(initialConfig, root, overrides = {}) {
5313
5461
  films.sort((a, b) => a.title.localeCompare(b.title, "zh"));
5314
5462
  return c.json({ films });
5315
5463
  });
5464
+ app.get("/api/v1/shorts", async (c) => {
5465
+ const shorts = await loadStudioShortList(root);
5466
+ return c.json({ shorts });
5467
+ });
5468
+ app.post("/api/v1/shorts/:id/complete", async (c) => {
5469
+ const storyId = c.req.param("id");
5470
+ if (!isSafeBookId(storyId)) {
5471
+ return c.json({ error: "Invalid short id" }, 400);
5472
+ }
5473
+ const runPath = join("shorts", storyId, "status.json");
5474
+ let raw;
5475
+ try {
5476
+ raw = JSON.parse(await readFile(join(root, runPath), "utf-8"));
5477
+ }
5478
+ catch {
5479
+ return c.json({ error: "Short not found" }, 404);
5480
+ }
5481
+ const run = parseStudioShortRun(raw, storyId);
5482
+ if (!run) {
5483
+ return c.json({ error: "Short status is invalid" }, 409);
5484
+ }
5485
+ if (run.status === "needs-review") {
5486
+ await writeProductionRunSnapshot({
5487
+ rootDir: root,
5488
+ runPath,
5489
+ run: createProductionRunSnapshot({
5490
+ ...run,
5491
+ status: "complete",
5492
+ updatedAt: new Date().toISOString(),
5493
+ }),
5494
+ });
5495
+ }
5496
+ return c.json({ ok: true, storyId, status: "complete" });
5497
+ });
5316
5498
  app.get("/api/v1/translations", async (c) => {
5317
5499
  const translationsDir = join(root, "translations");
5318
5500
  let entries = [];