@lumenkey/cli 0.3.1 → 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +36 -2
  2. package/dist/cli/copied.js +86 -85
  3. package/dist/cli/index.js +56 -44
  4. package/dist/cli/render.js +3 -2
  5. package/dist/core/asset-stream.d.ts +16 -0
  6. package/dist/core/asset-stream.js +64 -0
  7. package/dist/core/atomic-config-file.js +8 -7
  8. package/dist/core/chinese-messages.d.ts +4 -0
  9. package/dist/core/chinese-messages.js +907 -0
  10. package/dist/core/chinese-script.d.ts +7 -0
  11. package/dist/core/chinese-script.js +28 -0
  12. package/dist/core/client.js +2 -1
  13. package/dist/core/config-dir.js +2 -1
  14. package/dist/core/config.js +7 -6
  15. package/dist/core/download-root.d.ts +5 -0
  16. package/dist/core/download-root.js +64 -0
  17. package/dist/core/download.js +15 -12
  18. package/dist/core/generation-input.js +3 -2
  19. package/dist/core/generation.d.ts +8 -1
  20. package/dist/core/generation.js +13 -10
  21. package/dist/core/job-display.js +7 -6
  22. package/dist/core/locale.js +163 -148
  23. package/dist/core/outcome.js +6 -4
  24. package/dist/core/output-path.d.ts +3 -1
  25. package/dist/core/output-path.js +5 -2
  26. package/dist/core/params.js +26 -25
  27. package/dist/core/profiles.js +5 -4
  28. package/dist/core/set-values.d.ts +0 -1
  29. package/dist/core/set-values.js +7 -7
  30. package/dist/core/submission-journal.js +31 -16
  31. package/dist/core/version.js +2 -3
  32. package/dist/mcp/server.js +30 -29
  33. package/dist/studio/asset-integrity-hash.d.ts +28 -0
  34. package/dist/studio/asset-integrity-hash.js +93 -0
  35. package/dist/studio/asset-integrity-queue.d.ts +24 -0
  36. package/dist/studio/asset-integrity-queue.js +94 -0
  37. package/dist/studio/asset-store.d.ts +52 -0
  38. package/dist/studio/asset-store.js +352 -0
  39. package/dist/studio/canvas/LICENSES.txt +1590 -0
  40. package/dist/studio/canvas/dependencies.json +862 -0
  41. package/dist/studio/canvas/director-source-manifest.json +47 -0
  42. package/dist/studio/canvas/editor.css +239 -0
  43. package/dist/studio/canvas/editor.js +4613 -0
  44. package/dist/studio/canvas/integration-source-manifest.json +384 -0
  45. package/dist/studio/canvas/source-manifest.json +240 -0
  46. package/dist/studio/canvas-store.d.ts +1254 -0
  47. package/dist/studio/canvas-store.js +205 -0
  48. package/dist/studio/capture-placement.d.ts +11 -0
  49. package/dist/studio/capture-placement.js +85 -0
  50. package/dist/studio/cloud-contract.d.ts +128 -0
  51. package/dist/studio/cloud-contract.js +229 -0
  52. package/dist/studio/cloud-service.d.ts +178 -0
  53. package/dist/studio/cloud-service.js +202 -0
  54. package/dist/studio/director-captures.d.ts +59 -0
  55. package/dist/studio/director-captures.js +141 -0
  56. package/dist/studio/director-contract.d.ts +1489 -0
  57. package/dist/studio/director-contract.js +288 -0
  58. package/dist/studio/director-png.d.ts +6 -0
  59. package/dist/studio/director-png.js +41 -0
  60. package/dist/studio/director-relations.d.ts +40 -0
  61. package/dist/studio/director-relations.js +103 -0
  62. package/dist/studio/document-authority.d.ts +7 -0
  63. package/dist/studio/document-authority.js +50 -0
  64. package/dist/studio/draft-store.d.ts +95 -0
  65. package/dist/studio/draft-store.js +100 -0
  66. package/dist/studio/errors.d.ts +85 -0
  67. package/dist/studio/errors.js +94 -0
  68. package/dist/studio/launch.d.ts +5 -0
  69. package/dist/studio/launch.js +31 -0
  70. package/dist/studio/media.d.ts +9 -0
  71. package/dist/studio/media.js +50 -0
  72. package/dist/studio/output-save-contract.d.ts +1321 -0
  73. package/dist/studio/output-save-contract.js +26 -0
  74. package/dist/studio/output-save-journal.d.ts +971 -0
  75. package/dist/studio/output-save-journal.js +287 -0
  76. package/dist/studio/output-save-recovery.d.ts +18 -0
  77. package/dist/studio/output-save-recovery.js +161 -0
  78. package/dist/studio/output-save-service.d.ts +436 -0
  79. package/dist/studio/output-save-service.js +251 -0
  80. package/dist/studio/output-save-worker.d.ts +40 -0
  81. package/dist/studio/output-save-worker.js +360 -0
  82. package/dist/studio/paths.d.ts +6 -0
  83. package/dist/studio/paths.js +68 -0
  84. package/dist/studio/run-journal.d.ts +993 -0
  85. package/dist/studio/run-journal.js +125 -0
  86. package/dist/studio/server.d.ts +17 -0
  87. package/dist/studio/server.js +440 -0
  88. package/dist/studio/session.d.ts +6 -0
  89. package/dist/studio/session.js +26 -0
  90. package/dist/studio/shell.d.ts +3 -0
  91. package/dist/studio/shell.js +23 -0
  92. package/dist/studio/style.d.ts +2 -0
  93. package/dist/studio/style.js +29 -0
  94. package/dist/studio/types.d.ts +26 -0
  95. package/dist/studio/types.js +2 -0
  96. package/dist/studio/web/app.d.ts +2 -0
  97. package/dist/studio/web/app.js +662 -0
  98. package/dist/studio/web/asset-controller.d.ts +37 -0
  99. package/dist/studio/web/asset-controller.js +191 -0
  100. package/dist/studio/web/asset-model.d.ts +29 -0
  101. package/dist/studio/web/asset-model.js +51 -0
  102. package/dist/studio/web/controller.d.ts +113 -0
  103. package/dist/studio/web/controller.js +401 -0
  104. package/dist/studio/web/messages.d.ts +2 -0
  105. package/dist/studio/web/messages.js +74 -0
  106. package/dist/studio/web/model.d.ts +75 -0
  107. package/dist/studio/web/model.js +182 -0
  108. package/dist/studio/web/save-controller.d.ts +85 -0
  109. package/dist/studio/web/save-controller.js +495 -0
  110. package/dist/studio/web/save-model.d.ts +63 -0
  111. package/dist/studio/web/save-model.js +64 -0
  112. package/dist/studio/web/save-queue.d.ts +7 -0
  113. package/dist/studio/web/save-queue.js +62 -0
  114. package/dist/studio/web/save-view.d.ts +9 -0
  115. package/dist/studio/web/save-view.js +19 -0
  116. package/dist/studio/writer-lock.d.ts +5 -0
  117. package/dist/studio/writer-lock.js +88 -0
  118. package/package.json +11 -5
  119. package/dist/cli/copied.d.ts.map +0 -1
  120. package/dist/cli/copied.js.map +0 -1
  121. package/dist/cli/index.d.ts.map +0 -1
  122. package/dist/cli/index.js.map +0 -1
  123. package/dist/cli/render.d.ts.map +0 -1
  124. package/dist/cli/render.js.map +0 -1
  125. package/dist/core/atomic-config-file.d.ts.map +0 -1
  126. package/dist/core/atomic-config-file.js.map +0 -1
  127. package/dist/core/chat.d.ts.map +0 -1
  128. package/dist/core/chat.js.map +0 -1
  129. package/dist/core/client.d.ts.map +0 -1
  130. package/dist/core/client.js.map +0 -1
  131. package/dist/core/config-dir.d.ts.map +0 -1
  132. package/dist/core/config-dir.js.map +0 -1
  133. package/dist/core/config-error.d.ts.map +0 -1
  134. package/dist/core/config-error.js.map +0 -1
  135. package/dist/core/config.d.ts.map +0 -1
  136. package/dist/core/config.js.map +0 -1
  137. package/dist/core/credit-cap.d.ts.map +0 -1
  138. package/dist/core/credit-cap.js.map +0 -1
  139. package/dist/core/download.d.ts.map +0 -1
  140. package/dist/core/download.js.map +0 -1
  141. package/dist/core/error-result.d.ts.map +0 -1
  142. package/dist/core/error-result.js.map +0 -1
  143. package/dist/core/explain.d.ts.map +0 -1
  144. package/dist/core/explain.js.map +0 -1
  145. package/dist/core/generation-input.d.ts.map +0 -1
  146. package/dist/core/generation-input.js.map +0 -1
  147. package/dist/core/generation-result.d.ts.map +0 -1
  148. package/dist/core/generation-result.js.map +0 -1
  149. package/dist/core/generation.d.ts.map +0 -1
  150. package/dist/core/generation.js.map +0 -1
  151. package/dist/core/job-display.d.ts.map +0 -1
  152. package/dist/core/job-display.js.map +0 -1
  153. package/dist/core/locale.d.ts.map +0 -1
  154. package/dist/core/locale.js.map +0 -1
  155. package/dist/core/origin.d.ts.map +0 -1
  156. package/dist/core/origin.js.map +0 -1
  157. package/dist/core/outcome.d.ts.map +0 -1
  158. package/dist/core/outcome.js.map +0 -1
  159. package/dist/core/output-path.d.ts.map +0 -1
  160. package/dist/core/output-path.js.map +0 -1
  161. package/dist/core/params.d.ts.map +0 -1
  162. package/dist/core/params.js.map +0 -1
  163. package/dist/core/poller.d.ts.map +0 -1
  164. package/dist/core/poller.js.map +0 -1
  165. package/dist/core/profiles.d.ts.map +0 -1
  166. package/dist/core/profiles.js.map +0 -1
  167. package/dist/core/set-values.d.ts.map +0 -1
  168. package/dist/core/set-values.js.map +0 -1
  169. package/dist/core/submission-journal.d.ts.map +0 -1
  170. package/dist/core/submission-journal.js.map +0 -1
  171. package/dist/core/types.d.ts.map +0 -1
  172. package/dist/core/types.js.map +0 -1
  173. package/dist/core/version.d.ts.map +0 -1
  174. package/dist/core/version.js.map +0 -1
  175. package/dist/core/wait.d.ts.map +0 -1
  176. package/dist/core/wait.js.map +0 -1
  177. package/dist/index.d.ts.map +0 -1
  178. package/dist/index.js.map +0 -1
  179. package/dist/mcp/render.d.ts.map +0 -1
  180. package/dist/mcp/render.js.map +0 -1
  181. package/dist/mcp/server.d.ts.map +0 -1
  182. package/dist/mcp/server.js.map +0 -1
@@ -1,48 +1,49 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import { z } from 'zod';
2
3
  import { LumenKeyError } from './client.js';
3
4
  import { parseCreditCap } from './credit-cap.js';
4
5
  import { asPairs, modelParameterError } from './set-values.js';
5
6
  import { isBlankPrompt, promptRequiredSentence, ui } from './locale.js';
6
7
  const text = (en, zh) => ({ en, zh });
7
- const modelId = { name: 'modelId', kind: 'string', required: true, flag: '--model', aliases: ['-m'], placeholder: '<modelId>', describe: text('Model ID', '模型 ID') };
8
- const prompt = { name: 'prompt', kind: 'string', required: true, flag: '--prompt', aliases: ['-p'], placeholder: '<prompt>', describe: text('Prompt for the model', '发给模型的提示词') };
9
- const confirm = { name: 'confirm', kind: 'boolean', flag: '--confirm', describe: text('Explicitly submit this billable generation; otherwise quote only', '确认提交这次计费生成;否则只报价') };
10
- const dryRun = { name: 'dryRun', kind: 'boolean', flag: '--dry-run', only: 'cli', describe: text('Print the projected input and do not quote', '只打印将提交的输入,不报价') };
11
- const idempotencyKey = { name: 'idempotencyKey', kind: 'string', flag: '--idempotency-key', placeholder: '<key>', describe: text('Optional. Generated when omitted. Reuse it only to recover a submission whose outcome was unknown; the recorded body is re-sent unchanged', '可选,省略时自动生成。只在提交结果未知时复用,记录的请求体会原样重发') };
12
- const maxCredits = { name: 'maxCredits', kind: 'number', constraint: 'integer-nonnegative', flag: '--max-credits', placeholder: '<credits>', describe: text('Reject quotes above this spending cap (non-negative integer)', '报价超过该花费上限时拒绝(非负整数)') };
13
- const projectId = { name: 'projectId', kind: 'string', only: 'mcp', describe: text('Leave empty. The client fills this.', '请留空,由客户端填入。') };
14
- const downloadPath = { name: 'downloadPath', kind: 'string', flag: '--output', aliases: ['-o'], placeholder: '<path>', describe: text('Save the first result to this file path (absolute or ~/...). Omit it to receive the URLs only', '把第一个结果保存到该文件路径(绝对路径或 ~/ 开头);省略则只返回地址') };
15
- const set = { name: 'set', kind: 'string[]', flag: '--set', aliases: ['-s'], placeholder: '<pair>', describe: text('Extra key=value declared by the model schema. Repeatable', '模型参数结构声明的额外 key=value,可重复') };
16
- const intervalMs = { name: 'intervalMs', kind: 'number', constraint: 'positive', flag: '--interval', placeholder: '<seconds>', default: 5000, describe: text('Polling interval; defaults to 5 seconds', '轮询间隔;默认 5 秒') };
17
- const timeoutMs = { name: 'timeoutMs', kind: 'number', constraint: 'nonnegative', flag: '--timeout', placeholder: '<seconds>', default: 1_800_000, describe: text('Wait timeout; defaults to 1800 seconds. Does not cancel the task', '等待超时;默认 1800 秒,超时不取消任务') };
18
- const resolution = { name: 'resolution', kind: 'string', flag: '--resolution', aliases: ['-r'], placeholder: '<res>', describe: text('Resolution, only if the catalog declares it', '分辨率,仅当目录声明了该字段') };
19
- const ratio = { name: 'ratio', kind: 'string', flag: '--aspect-ratio', aliases: ['--ratio'], placeholder: '<ratio>', describe: text('Aspect ratio, only if the catalog declares aspect_ratio or ratio', '宽高比,仅当目录声明了 aspect_ratio 或 ratio') };
20
- const count = { name: 'count', kind: 'numeric', flag: '--count', placeholder: '<count>', describe: text('Count, only if the catalog declares it; validated against the catalog type', '数量,仅当目录声明了该字段;按目录类型校验') };
8
+ const modelId = { name: 'modelId', kind: 'string', required: true, flag: '--model', aliases: ['-m'], placeholder: '<modelId>', describe: text('Model ID', zhText("m238")) };
9
+ const prompt = { name: 'prompt', kind: 'string', required: true, flag: '--prompt', aliases: ['-p'], placeholder: '<prompt>', describe: text('Prompt for the model', zhText("m237")) };
10
+ const confirm = { name: 'confirm', kind: 'boolean', flag: '--confirm', describe: text('Explicitly submit this billable generation; otherwise quote only', zhText("m236")) };
11
+ const dryRun = { name: 'dryRun', kind: 'boolean', flag: '--dry-run', only: 'cli', describe: text('Print the projected input and do not quote', zhText("m235")) };
12
+ const idempotencyKey = { name: 'idempotencyKey', kind: 'string', flag: '--idempotency-key', placeholder: '<key>', describe: text('Optional. Generated when omitted. Reuse it only to recover a submission whose outcome was unknown; the recorded body is re-sent unchanged', zhText("m234")) };
13
+ const maxCredits = { name: 'maxCredits', kind: 'number', constraint: 'integer-nonnegative', flag: '--max-credits', placeholder: '<credits>', describe: text('Reject quotes above this spending cap (non-negative integer)', zhText("m233")) };
14
+ const projectId = { name: 'projectId', kind: 'string', only: 'mcp', describe: text('Leave empty. The client fills this.', zhText("m232")) };
15
+ const downloadPath = { name: 'downloadPath', kind: 'string', flag: '--output', aliases: ['-o'], placeholder: '<path>', describe: text('Save the first result to this file path (absolute or ~/...). Omit it to receive the URLs only', zhText("m231")) };
16
+ const set = { name: 'set', kind: 'string[]', flag: '--set', aliases: ['-s'], placeholder: '<pair>', describe: text('Extra key=value declared by the model schema. Repeatable', zhText("m230")) };
17
+ const intervalMs = { name: 'intervalMs', kind: 'number', constraint: 'positive', flag: '--interval', placeholder: '<seconds>', default: 5000, describe: text('Polling interval; defaults to 5 seconds', zhText("m229")) };
18
+ const timeoutMs = { name: 'timeoutMs', kind: 'number', constraint: 'nonnegative', flag: '--timeout', placeholder: '<seconds>', default: 1_800_000, describe: text('Wait timeout; defaults to 1800 seconds. Does not cancel the task', zhText("m228")) };
19
+ const resolution = { name: 'resolution', kind: 'string', flag: '--resolution', aliases: ['-r'], placeholder: '<res>', describe: text('Resolution, only if the catalog declares it', zhText("m227")) };
20
+ const ratio = { name: 'ratio', kind: 'string', flag: '--aspect-ratio', aliases: ['--ratio'], placeholder: '<ratio>', describe: text('Aspect ratio, only if the catalog declares aspect_ratio or ratio', zhText("m226")) };
21
+ const count = { name: 'count', kind: 'numeric', flag: '--count', placeholder: '<count>', describe: text('Count, only if the catalog declares it; validated against the catalog type', zhText("m225")) };
21
22
  export const videoParams = [
22
23
  modelId, prompt,
23
- { name: 'imageUrl', kind: 'string', flag: '--image', aliases: ['-i'], placeholder: '<imageUrl>', describe: text('Source image URL for image-to-video, only if the catalog declares it', '图生视频的源图地址,仅当目录声明了该字段') },
24
+ { name: 'imageUrl', kind: 'string', flag: '--image', aliases: ['-i'], placeholder: '<imageUrl>', describe: text('Source image URL for image-to-video, only if the catalog declares it', zhText("m224")) },
24
25
  // No numeric constraint on duration: the catalog decides what a valid duration is (an explicit 0 is passed through, as in 0.2.3).
25
- { name: 'duration', kind: 'number', flag: '--duration', aliases: ['-d'], placeholder: '<seconds>', describe: text('Video duration in seconds; omitted scalar catalog defaults are explicitly sent', '视频时长(秒);省略时会显式发送目录中的标量默认值') },
26
+ { name: 'duration', kind: 'number', flag: '--duration', aliases: ['-d'], placeholder: '<seconds>', describe: text('Video duration in seconds; omitted scalar catalog defaults are explicitly sent', zhText("m223")) },
26
27
  resolution, ratio, count, set, dryRun, confirm, idempotencyKey, maxCredits, projectId, downloadPath, intervalMs, timeoutMs,
27
28
  ];
28
29
  export const imageParams = [
29
30
  modelId, prompt,
30
- { name: 'imageUrl', kind: 'string', flag: '--image', placeholder: '<imageUrl>', describe: text('Reference image URL, only if the catalog declares it', '参考图地址,仅当目录声明了该字段') },
31
+ { name: 'imageUrl', kind: 'string', flag: '--image', placeholder: '<imageUrl>', describe: text('Reference image URL, only if the catalog declares it', zhText("m222")) },
31
32
  { ...resolution, aliases: undefined }, ratio,
32
- { name: 'size', kind: 'string', flag: '--size', placeholder: '<size>', describe: text('Legacy size, only for models whose catalog schema declares size', '旧版尺寸参数,仅用于目录声明了 size 的模型') },
33
+ { name: 'size', kind: 'string', flag: '--size', placeholder: '<size>', describe: text('Legacy size, only for models whose catalog schema declares size', zhText("m221")) },
33
34
  count, set, dryRun, confirm, idempotencyKey, maxCredits, projectId, downloadPath, intervalMs, timeoutMs,
34
35
  ];
35
36
  export const chatParams = [
36
- { ...modelId, required: false, default: 'qwen3.7-flash', describe: text('Model ID (e.g. qwen3.7-flash, qwen3.8-max, grok-4.6)', '模型 ID(例如 qwen3.7-flash、qwen3.8-max、grok-4.6)') },
37
- { ...prompt, describe: text('User message to send to the model', '发送给模型的用户消息') },
38
- { name: 'systemPrompt', kind: 'string', flag: '--system-prompt', placeholder: '<prompt>', describe: text('Optional system instruction; a blank one is not sent', '可选的系统指令;空白时不发送') },
39
- { ...maxCredits, describe: text('Reject before inference if the required reservation exceeds this spending cap (non-negative integer)', '所需预授权超过该花费上限时在推理前拒绝(非负整数)') },
40
- { ...idempotencyKey, describe: text('Reuse only for an identical request; after a timeout, retry the identical request with the key that was reported', '仅为同一次相同请求复用;超时后用结果里报告的键原样重试') },
37
+ { ...modelId, required: false, default: 'qwen3.7-flash', describe: text('Model ID (e.g. qwen3.7-flash, qwen3.8-max, grok-4.6)', zhText("m220")) },
38
+ { ...prompt, describe: text('User message to send to the model', zhText("m219")) },
39
+ { name: 'systemPrompt', kind: 'string', flag: '--system-prompt', placeholder: '<prompt>', describe: text('Optional system instruction; a blank one is not sent', zhText("m218")) },
40
+ { ...maxCredits, describe: text('Reject before inference if the required reservation exceeds this spending cap (non-negative integer)', zhText("m217")) },
41
+ { ...idempotencyKey, describe: text('Reuse only for an identical request; after a timeout, retry the identical request with the key that was reported', zhText("m216")) },
41
42
  ];
42
43
  export const waitParams = [intervalMs, timeoutMs];
43
44
  /** `tasks resume <key>` / `lumenkey_resume_submission`: the key plus wait and output options; no generation argument. */
44
45
  export const resumeParams = [
45
- { ...idempotencyKey, required: true, only: 'mcp', describe: text('The idempotency key of the recorded submission to re-send verbatim', '要原样重发的已记录提交的幂等键') },
46
+ { ...idempotencyKey, required: true, only: 'mcp', describe: text('The idempotency key of the recorded submission to re-send verbatim', zhText("m215")) },
46
47
  downloadPath, intervalMs, timeoutMs,
47
48
  ];
48
49
  export function forSurface(spec, surface) {
@@ -126,7 +127,7 @@ export function waitMs(value, surface, fallbackMs) {
126
127
  }
127
128
  function shortIntervalWarning(intervalMs, surface) {
128
129
  if (surface === 'cli' && intervalMs > 0 && intervalMs < 1000) {
129
- process.stderr.write(ui('Warning [POLL_INTERVAL_SHORT]: --interval uses seconds. Values below 1 second may cause excessive polling; consider --interval 5.\n', '警告 [POLL_INTERVAL_SHORT]:--interval 单位为秒。小于 1 秒可能导致过于频繁的查询;建议使用 --interval 5。\n'));
130
+ process.stderr.write(ui('Warning [POLL_INTERVAL_SHORT]: --interval uses seconds. Values below 1 second may cause excessive polling; consider --interval 5.\n', zhText("m214")));
130
131
  }
131
132
  }
132
133
  function countValue(value) {
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import os from 'node:os';
@@ -25,7 +26,7 @@ export class ProfileError extends LumenKeyError {
25
26
  }
26
27
  export class ProfileMissingError extends ProfileError {
27
28
  constructor(name) {
28
- super('PROFILE_MISSING', ui(`Profile ${name} does not exist.`, `档案 ${name} 不存在。`));
29
+ super('PROFILE_MISSING', ui(`Profile ${name} does not exist.`, (zhText("m242") + (name) + zhText("m243"))));
29
30
  }
30
31
  }
31
32
  function filePath(home = os.homedir()) {
@@ -52,18 +53,18 @@ function object(value) {
52
53
  }
53
54
  function validName(name) {
54
55
  if (!name.trim() || Object.prototype.hasOwnProperty.call(Object.prototype, name)) {
55
- throw new ProfileError('PROFILE_INVALID', ui('Invalid profile name.', '档案名称无效。'));
56
+ throw new ProfileError('PROFILE_INVALID', ui('Invalid profile name.', zhText("m241")));
56
57
  }
57
58
  }
58
59
  function validate(value) {
59
60
  if (!object(value) || !object(value.profiles) || (value.current !== undefined && typeof value.current !== 'string')) {
60
- throw new ProfileError('PROFILE_INVALID', ui('profiles.json must contain a profiles object.', 'profiles.json 必须包含 profiles 对象。'));
61
+ throw new ProfileError('PROFILE_INVALID', ui('profiles.json must contain a profiles object.', zhText("m240")));
61
62
  }
62
63
  const profiles = Object.create(null);
63
64
  for (const [name, record] of Object.entries(value.profiles)) {
64
65
  validName(name);
65
66
  if (!object(record) || ['key', 'host', 'projectId'].some(key => record[key] !== undefined && typeof record[key] !== 'string')) {
66
- throw new ProfileError('PROFILE_INVALID', ui('Profile records must be objects with string fields.', '档案记录必须是包含字符串字段的对象。'));
67
+ throw new ProfileError('PROFILE_INVALID', ui('Profile records must be objects with string fields.', zhText("m239")));
67
68
  }
68
69
  profiles[name] = Object.fromEntries(['key', 'host', 'projectId'].filter(key => record[key] !== undefined).map(key => [key, record[key]]));
69
70
  }
@@ -1,4 +1,3 @@
1
- /** Pixmax `_parse_value` plus fal's first `=` / `:=` split and `[index]` / `[]` paths. */
2
1
  import { LumenKeyError } from './client.js';
3
2
  /** Identify a parameter without echoing its value, a whole pair, or a payload. */
4
3
  export declare function modelParameterError(code: 'MODEL_PARAM_INVALID' | 'MODEL_PARAM_UNKNOWN', key: string): LumenKeyError;
@@ -1,19 +1,19 @@
1
- /** Pixmax `_parse_value` plus fal's first `=` / `:=` split and `[index]` / `[]` paths. */
1
+ import { zhText } from '../core/chinese-messages.js';
2
2
  import { LumenKeyError } from './client.js';
3
3
  import { ui } from './locale.js';
4
4
  /** Identify a parameter without echoing its value, a whole pair, or a payload. */
5
5
  export function modelParameterError(code, key) {
6
6
  if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key)) {
7
7
  return new LumenKeyError(code, undefined, undefined, code === 'MODEL_PARAM_UNKNOWN'
8
- ? ui('This model does not support the supplied parameter.', '该模型不支持所提供的参数。')
9
- : ui('The supplied parameter is invalid. Check lumenkey model explain.', '所提供的参数值无效。请运行 lumenkey model explain 查看允许的参数。'));
8
+ ? ui('This model does not support the supplied parameter.', zhText("m250"))
9
+ : ui('The supplied parameter is invalid. Check lumenkey model explain.', zhText("m249")));
10
10
  }
11
11
  const label = key === 'aspect_ratio' || key === 'ratio'
12
- ? ui('aspect ratio', '宽高比')
12
+ ? ui('aspect ratio', zhText("m248"))
13
13
  : key;
14
14
  const detail = code === 'MODEL_PARAM_UNKNOWN'
15
- ? ui(`This model has no ${label} parameter.`, `该模型不支持 ${label} 参数。`)
16
- : ui(`Invalid value for ${label}. Check this model's allowed parameters with lumenkey model explain.`, `${label} 参数值无效。请运行 lumenkey model explain 查看该模型允许的参数。`);
15
+ ? ui(`This model has no ${label} parameter.`, (zhText("m246") + (label) + zhText("m247")))
16
+ : ui(`Invalid value for ${label}. Check this model's allowed parameters with lumenkey model explain.`, ("" + (label) + zhText("m245")));
17
17
  return new LumenKeyError(code, undefined, undefined, detail);
18
18
  }
19
19
  export function parseValue(value) {
@@ -49,7 +49,7 @@ export function parsePath(key) {
49
49
  if (!name)
50
50
  throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID');
51
51
  if (name === '__proto__' || name === 'constructor' || name === 'prototype')
52
- throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID', undefined, undefined, ui('Unsafe key in key=value option.', 'key=value 参数不能使用不安全的字段名。'));
52
+ throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID', undefined, undefined, ui('Unsafe key in key=value option.', zhText("m244")));
53
53
  steps.push({ kind: 'key', name });
54
54
  };
55
55
  while (i < key.length) {
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -68,13 +69,27 @@ function pastRetention(entry, now = Date.now()) {
68
69
  }
69
70
  export function ageText(entry) {
70
71
  const hours = Math.floor(ageMs(entry) / 3600000);
71
- return hours >= 1 ? ui(`${hours} hours ago`, `${hours} 小时前`) : ui(`${Math.max(1, Math.floor(ageMs(entry) / 60000))} minutes ago`, `${Math.max(1, Math.floor(ageMs(entry) / 60000))} 分钟前`);
72
+ return hours >= 1 ? ui(`${hours} hours ago`, ("" + (hours) + zhText("m270"))) : ui(`${Math.max(1, Math.floor(ageMs(entry) / 60000))} minutes ago`, ("" + (Math.max(1, Math.floor(ageMs(entry) / 60000))) + zhText("m269")));
72
73
  }
73
74
  function journalError(code, file, error, detail) {
74
- return Object.assign(new LumenKeyError(code, undefined, undefined, detail), { path: file, cause: error });
75
+ return Object.assign(new LumenKeyError(code, undefined, undefined, detail), { recordId: recordIdentifier(file), cause: error });
76
+ }
77
+ function recordIdentifier(file) {
78
+ return `record:${crypto.createHash('sha256').update(path.basename(file)).digest('hex').slice(0, 12)}`;
75
79
  }
76
80
  function reasonOf(error) {
77
- return error?.code ?? (error instanceof Error ? error.message : String(error));
81
+ const code = error?.code;
82
+ return typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,79}$/.test(code) ? code : 'FILESYSTEM_ERROR';
83
+ }
84
+ function assertJournalReadable(file) {
85
+ try {
86
+ assertConfigDirReadable(file);
87
+ }
88
+ catch (error) {
89
+ if (error instanceof LumenKeyError)
90
+ throw Object.assign(new LumenKeyError(error.code, error.status, error.retryAfterMs, error.publicMessage), { recordId: recordIdentifier(file) });
91
+ throw journalError('SUBMISSION_JOURNAL_INVALID', file, error, errorSentence('SUBMISSION_JOURNAL_INVALID'));
92
+ }
78
93
  }
79
94
  /**
80
95
  * Prove the journal can be written before anything billable happens, so a refused submission
@@ -91,7 +106,7 @@ export function assertJournalWritable(home) {
91
106
  fs.unlinkSync(probe);
92
107
  }
93
108
  catch (error) {
94
- throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', dir, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`No job was submitted. Directory: ${dir} (${reasonOf(error)}). Set LUMENKEY_JOURNAL_DIR to a writable directory to keep pending records elsewhere.`, `没有提交任务。目录:${dir}(${reasonOf(error)})。可设置 LUMENKEY_JOURNAL_DIR 把待确认记录放到可写目录。`)}`);
109
+ throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', dir, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`No job was submitted. The pending-submissions journal directory is unavailable (${reasonOf(error)}). Set LUMENKEY_JOURNAL_DIR to a writable directory to keep pending records elsewhere.`, (zhText("m267") + (reasonOf(error)) + zhText("m268")))}`);
95
110
  }
96
111
  }
97
112
  function write(entry, home) {
@@ -101,7 +116,7 @@ function write(entry, home) {
101
116
  writeAtomicConfigFile(file, entry);
102
117
  }
103
118
  catch (error) {
104
- throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`Directory: ${path.dirname(file)} (${reasonOf(error)}).`, `目录:${path.dirname(file)}(${reasonOf(error)})。`)}`);
119
+ throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`Record ${recordIdentifier(file)} (${reasonOf(error)}).`, (zhText("m266") + (recordIdentifier(file)) + "(" + (reasonOf(error)) + ")。"))}`);
105
120
  }
106
121
  }
107
122
  /** Write before the POST. Any failure here must stop the submission: a job without a record cannot be recovered. */
@@ -111,7 +126,7 @@ export function rememberPendingSubmission(entry, home) {
111
126
  }
112
127
  catch (error) {
113
128
  if (error instanceof LumenKeyError)
114
- error.publicMessage = `${error.publicMessage} ${ui('No job was submitted.', '没有提交任务。')}`;
129
+ error.publicMessage = `${error.publicMessage} ${ui('No job was submitted.', zhText("m265"))}`;
115
130
  throw error;
116
131
  }
117
132
  sweep(home);
@@ -119,9 +134,9 @@ export function rememberPendingSubmission(entry, home) {
119
134
  /** Read a record by key without any expiry side effect. */
120
135
  function readRaw(idempotencyKey, home) {
121
136
  const file = entryPath(idempotencyKey, home);
122
- assertConfigDirReadable(file);
123
137
  let entry;
124
138
  try {
139
+ assertConfigDirReadable(file);
125
140
  assertSafeConfigFile(file);
126
141
  if (!fs.existsSync(file))
127
142
  return undefined;
@@ -129,7 +144,7 @@ function readRaw(idempotencyKey, home) {
129
144
  }
130
145
  catch (error) {
131
146
  if (error instanceof LumenKeyError)
132
- throw Object.assign(error, { path: file });
147
+ throw Object.assign(new LumenKeyError(error.code, error.status, error.retryAfterMs, error.publicMessage), { recordId: recordIdentifier(file) });
133
148
  throw journalError('SUBMISSION_JOURNAL_INVALID', file, error, errorSentence('SUBMISSION_JOURNAL_INVALID'));
134
149
  }
135
150
  if (entry.idempotencyKey !== idempotencyKey)
@@ -165,7 +180,7 @@ export function forgetPendingSubmission(idempotencyKey, quoteId, home) {
165
180
  catch (error) {
166
181
  if (error.code === 'ENOENT')
167
182
  return false;
168
- throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, ui(`The pending record ${file} could not be removed (${reasonOf(error)}); it will be swept after 48 hours.`, `待确认记录 ${file} 无法删除(${reasonOf(error)}),将在 48 小时后清理。`));
183
+ throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, ui(`The pending record ${recordIdentifier(file)} could not be removed (${reasonOf(error)}); it will be swept after 48 hours.`, (zhText("m262") + (recordIdentifier(file)) + zhText("m263") + (reasonOf(error)) + zhText("m264"))));
169
184
  }
170
185
  }
171
186
  /**
@@ -240,7 +255,7 @@ export function isInputPolicyConflict(entry, input, callerInput, defaults, legac
240
255
  }
241
256
  /** Before lookup: retain the replay guard for 23h, then report expiry; sweep after 48h. */
242
257
  export function findInputPolicyConflict(identity, modelId, input, callerInput, defaults, home, legacyCallerInput) {
243
- assertConfigDirReadable(path.join(journalDir(home), 'probe'));
258
+ assertJournalReadable(path.join(journalDir(home), 'probe'));
244
259
  sweep(home);
245
260
  return readAll(home).entries
246
261
  .filter(entry => sameSender(entry, identity) && entry.modelId === modelId && !isExpired(entry) && isInputPolicyConflict(entry, input, callerInput, defaults, legacyCallerInput))
@@ -265,21 +280,21 @@ export function processAlive(pid) {
265
280
  * are invisible here.
266
281
  */
267
282
  export function findResumablePendingSubmission(identity, modelId, canonicalInput, home) {
268
- assertConfigDirReadable(path.join(journalDir(home), 'probe'));
283
+ assertJournalReadable(path.join(journalDir(home), 'probe'));
269
284
  const { entries, damaged } = readAll(home);
270
- const notices = damaged.map(file => ui(`${errorSentence('SUBMISSION_JOURNAL_INVALID')} (SUBMISSION_JOURNAL_INVALID) ${file}`, `${errorSentence('SUBMISSION_JOURNAL_INVALID')}(SUBMISSION_JOURNAL_INVALID)${file}`));
285
+ const notices = damaged.map(file => ui(`${errorSentence('SUBMISSION_JOURNAL_INVALID')} (SUBMISSION_JOURNAL_INVALID) ${recordIdentifier(file)}`, `${errorSentence('SUBMISSION_JOURNAL_INVALID')}(SUBMISSION_JOURNAL_INVALID)${recordIdentifier(file)}`));
271
286
  const candidates = [];
272
287
  for (const entry of entries) {
273
288
  if (!sameSender(entry, identity) || entry.modelId !== modelId || pastRetention(entry))
274
289
  continue;
275
290
  if (isExpired(entry)) {
276
- notices.push(ui(`An unconfirmed submission recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) has expired past its replay window; check job history before continuing. It was not re-sent.`, `有一条未确认的旧请求已过可续传时限,请先查任务历史(幂等键 ${entry.idempotencyKey},报价 ${entry.quoteId},记录于${ageText(entry)})。没有重发旧请求。`));
291
+ notices.push(ui(`An unconfirmed submission recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) has expired past its replay window; check job history before continuing. It was not re-sent.`, (zhText("m260") + (entry.idempotencyKey) + zhText("m34") + (entry.quoteId) + zhText("m29") + (ageText(entry)) + zhText("m261"))));
277
292
  continue;
278
293
  }
279
294
  const sameInput = canonical(entry.input) === canonicalInput;
280
295
  if (!sameInput) {
281
296
  if (entry.state === 'unknown' || (entry.hostname === os.hostname() && !processAlive(entry.pid)))
282
- notices.push(ui(`A submission for this model with a different input was recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) and is still unconfirmed; it was not resumed because the input differs. If it is the one you meant (for example after a catalog change), run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, `有一次同模型但输入不同的提交记录于${ageText(entry)}(幂等键 ${entry.idempotencyKey},报价 ${entry.quoteId}),结果仍未确认;因输入不同没有接续。若那正是你要的(例如目录变更后),请运行 lumenkey tasks resume ${entry.idempotencyKey}(MCP:lumenkey_resume_submission)。`));
297
+ notices.push(ui(`A submission for this model with a different input was recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) and is still unconfirmed; it was not resumed because the input differs. If it is the one you meant (for example after a catalog change), run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, (zhText("m257") + (ageText(entry)) + zhText("m258") + (entry.idempotencyKey) + zhText("m34") + (entry.quoteId) + zhText("m259") + (entry.idempotencyKey) + "(MCP:lumenkey_resume_submission)。")));
283
298
  continue;
284
299
  }
285
300
  if (entry.state === 'unknown') {
@@ -287,11 +302,11 @@ export function findResumablePendingSubmission(identity, modelId, canonicalInput
287
302
  continue;
288
303
  }
289
304
  if (entry.hostname !== os.hostname()) {
290
- notices.push(ui(`An identical submission is still in flight from host ${entry.hostname} (idempotency key ${entry.idempotencyKey}); it was not resumed from here.`, `主机 ${entry.hostname} 上有一次相同的提交仍在进行(幂等键 ${entry.idempotencyKey}),没有从这里接续。`));
305
+ notices.push(ui(`An identical submission is still in flight from host ${entry.hostname} (idempotency key ${entry.idempotencyKey}); it was not resumed from here.`, (zhText("m254") + (entry.hostname) + zhText("m255") + (entry.idempotencyKey) + zhText("m256"))));
291
306
  continue;
292
307
  }
293
308
  if (processAlive(entry.pid)) {
294
- notices.push(ui(`An identical submission is still in flight in process ${entry.pid} on this host (idempotency key ${entry.idempotencyKey}); it was not resumed. If that process is gone, run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, `本机进程 ${entry.pid} 中有一次相同的提交仍在进行(幂等键 ${entry.idempotencyKey}),没有接续。若该进程已不存在,请运行 lumenkey tasks resume ${entry.idempotencyKey}(MCP:lumenkey_resume_submission)。`));
309
+ notices.push(ui(`An identical submission is still in flight in process ${entry.pid} on this host (idempotency key ${entry.idempotencyKey}); it was not resumed. If that process is gone, run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, (zhText("m251") + (entry.pid) + zhText("m252") + (entry.idempotencyKey) + zhText("m253") + (entry.idempotencyKey) + "(MCP:lumenkey_resume_submission)。")));
295
310
  continue;
296
311
  }
297
312
  candidates.push(entry);
@@ -1,3 +1,2 @@
1
- import fs from 'node:fs';
2
- export const packageVersion = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
3
- //# sourceMappingURL=version.js.map
1
+ // Generated from package.json by stamp-build-version.mjs.
2
+ export const packageVersion = "0.4.0-rc.1";
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
4
  import { z } from 'zod';
@@ -16,18 +17,18 @@ function log(line) {
16
17
  }
17
18
  /** MCP view of generation progress; the same core events the CLI renders, on stderr. */
18
19
  function mcpGenerationEvents() {
19
- const nounOf = (kind) => kind === 'video' ? ui('video', '视频') : ui('image', '图片');
20
+ const nounOf = (kind) => kind === 'video' ? ui('video', zhText("m442")) : ui('image', zhText("m441"));
20
21
  return event => {
21
22
  switch (event.type) {
22
- case 'quoting': return log(ui(`Quoting ${nounOf(event.kind)} generation for ${event.modelId}...`, `正在获取 ${event.modelId} 的${nounOf(event.kind)}生成报价...`));
23
+ case 'quoting': return log(ui(`Quoting ${nounOf(event.kind)} generation for ${event.modelId}...`, (zhText("m438") + (event.modelId) + zhText("m439") + (nounOf(event.kind)) + zhText("m440"))));
23
24
  case 'submitting': return log(event.resubmit
24
- ? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}, ${event.credits} credits) instead of creating a new job...`, `正在重发一次之前结果未确认的提交(幂等键 ${event.idempotencyKey},报价编号 ${event.quoteId},${event.credits} 算力点),不会新建任务...`)
25
- : ui(`Submitting job with quote ${event.quoteId} (${event.credits} credits)...`, `正在提交任务,报价编号:${event.quoteId}(${event.credits} 算力点)...`));
26
- case 'job_created': return log(ui(`Job created: ${event.jobId} (idempotency key ${event.idempotencyKey}, quote ${event.quoteId})`, `任务已创建:${event.jobId}(幂等键 ${event.idempotencyKey},报价编号 ${event.quoteId})`));
27
- case 'polling': return log(ui(`Polling job ${event.jobId}...`, `正在查询任务 ${event.jobId} 的进度...`));
28
- case 'progress': return log(ui(`Job ${event.detail.jobId} status: ${event.detail.status}`, `任务 ${event.detail.jobId} 状态:${jobStatusText(event.detail.status)}`));
29
- case 'downloading': return log(ui(`Downloading ${nounOf(event.kind)}...`, `正在下载${nounOf(event.kind)}...`));
30
- case 'notice': return log(ui(`Note: ${event.text}`, `注意:${event.text}`));
25
+ ? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}, ${event.credits} credits) instead of creating a new job...`, (zhText("m354") + (event.idempotencyKey) + zhText("m434") + (event.quoteId) + "," + (event.credits) + zhText("m437")))
26
+ : ui(`Submitting job with quote ${event.quoteId} (${event.credits} credits)...`, (zhText("m435") + (event.quoteId) + "(" + (event.credits) + zhText("m436"))));
27
+ case 'job_created': return log(ui(`Job created: ${event.jobId} (idempotency key ${event.idempotencyKey}, quote ${event.quoteId})`, (zhText("m351") + (event.jobId) + zhText("m258") + (event.idempotencyKey) + zhText("m434") + (event.quoteId) + ")")));
28
+ case 'polling': return log(ui(`Polling job ${event.jobId}...`, (zhText("m432") + (event.jobId) + zhText("m433"))));
29
+ case 'progress': return log(ui(`Job ${event.detail.jobId} status: ${event.detail.status}`, (zhText("m430") + (event.detail.jobId) + zhText("m431") + (jobStatusText(event.detail.status)) + "")));
30
+ case 'downloading': return log(ui(`Downloading ${nounOf(event.kind)}...`, (zhText("m429") + (nounOf(event.kind)) + "...")));
31
+ case 'notice': return log(ui(`Note: ${event.text}`, (zhText("m350") + (event.text) + "")));
31
32
  default: return;
32
33
  }
33
34
  };
@@ -54,7 +55,7 @@ function inputError(toolName, issues, args) {
54
55
  if (field === 'duration')
55
56
  return new LumenKeyError('VIDEO_DURATION_INVALID');
56
57
  if (issue.code === 'unrecognized_keys') {
57
- return new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown argument: ${issue.keys.join(', ')}.`, `未知参数:${issue.keys.join('、')}。`));
58
+ return new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown argument: ${issue.keys.join(', ')}.`, (zhText("m428") + (issue.keys.join('、')) + "。")));
58
59
  }
59
60
  }
60
61
  return new LumenKeyError('INPUT_INVALID', undefined, undefined, issues.map(issue => `${issue.path.join('.') || 'arguments'}: ${issue.message}`).join(' '));
@@ -91,42 +92,42 @@ export function createMcpServer() {
91
92
  }
92
93
  });
93
94
  }
94
- tool('lumenkey_list_models', ui('List all available AI models (video, image, text, audio) from LumenKey catalog with schemas and pricing', '读取光钥模型目录,包含视频、图片、文本、音频的参数结构与价格。'), {
95
- type: z.enum(['video', 'image', 'text', 'audio']).optional().describe(ui('Filter models by type', '按模型类型筛选')),
95
+ tool('lumenkey_list_models', ui('List all available AI models (video, image, text, audio) from LumenKey catalog with schemas and pricing', zhText("m427")), {
96
+ type: z.enum(['video', 'image', 'text', 'audio']).optional().describe(ui('Filter models by type', zhText("m426"))),
96
97
  }, async ({ type }) => {
97
98
  const models = await api().listModels(type);
98
99
  return dataResult({ count: models.length, models });
99
100
  });
100
- tool('lumenkey_generate_video', ui('Quote a LumenKey video. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', '获取光钥视频报价;未提供的目录标量默认值会显式写入 input;只有 confirm 为 true 时才提交计费生成。参数别名冲突在本地拒绝。'), zodShape(videoParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('video', args, 'mcp'), onEvent: mcpGenerationEvents() })));
101
- tool('lumenkey_generate_image', ui('Quote a LumenKey image. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', '获取光钥图片报价;未提供的目录标量默认值会显式写入 input;只有 confirm 为 true 时才提交计费生成。参数别名冲突在本地拒绝。'), zodShape(imageParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('image', args, 'mcp'), onEvent: mcpGenerationEvents() })));
102
- tool('lumenkey_query_job', ui('Query the status of an asynchronous generation task by its jobId', '按任务 ID 查询已有异步生成任务的状态。'), { jobId: z.string().describe(ui('The task ID to query', '要查询的任务 ID')) }, async ({ jobId }) => toolResult(await readJob(api(), jobId)));
103
- tool('lumenkey_check_balance', ui('Query current LumenKey account credits balance (available credits, reserved credits, and updatedAt timestamp)', '查询光钥账户的可用算力点、冻结算力点和更新时间。'), {}, async () => dataResult(await api().getBalance()));
104
- tool('lumenkey_chat', ui('Run billable LLM inference. Every successful call may incur credits; reuse idempotencyKey only for an identical request.', '发起计费文本推理;成功调用可能消耗算力点,仅为同一次相同请求复用幂等键。'), zodShape(chatParams), async (args) => {
101
+ tool('lumenkey_generate_video', ui('Quote a LumenKey video. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', zhText("m425")), zodShape(videoParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('video', args, 'mcp'), onEvent: mcpGenerationEvents() })));
102
+ tool('lumenkey_generate_image', ui('Quote a LumenKey image. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', zhText("m424")), zodShape(imageParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('image', args, 'mcp'), onEvent: mcpGenerationEvents() })));
103
+ tool('lumenkey_query_job', ui('Query the status of an asynchronous generation task by its jobId', zhText("m423")), { jobId: z.string().describe(ui('The task ID to query', zhText("m417"))) }, async ({ jobId }) => toolResult(await readJob(api(), jobId)));
104
+ tool('lumenkey_check_balance', ui('Query current LumenKey account credits balance (available credits, reserved credits, and updatedAt timestamp)', zhText("m422")), {}, async () => dataResult(await api().getBalance()));
105
+ tool('lumenkey_chat', ui('Run billable LLM inference. Every successful call may incur credits; reuse idempotencyKey only for an identical request.', zhText("m421")), zodShape(chatParams), async (args) => {
105
106
  const outcome = await runChat(api(), chatArgs(args, 'mcp'));
106
107
  return toolResult(outcome, outcome.ok ? outcome.text : undefined);
107
108
  });
108
- tool('lumenkey_explain_model', ui('Explain one model: its parameters, which are required, and which values are allowed.', '说明指定模型的参数、必填字段与允许的值。'), {
109
- modelId: z.string().describe(ui('The model ID to explain', '要解释的模型 ID')),
109
+ tool('lumenkey_explain_model', ui('Explain one model: its parameters, which are required, and which values are allowed.', zhText("m420")), {
110
+ modelId: z.string().describe(ui('The model ID to explain', zhText("m419"))),
110
111
  }, async ({ modelId }) => {
111
112
  const model = (await api().listModels()).find((item) => item.id === modelId);
112
113
  if (!model)
113
114
  throw new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(modelId));
114
115
  return dataResult(explainModel(model));
115
116
  });
116
- tool('lumenkey_tasks_progress', ui('Read one existing generation job', '读取一条已有生成任务的进度。'), {
117
- jobId: z.string().describe(ui('The task ID to query', '要查询的任务 ID')),
117
+ tool('lumenkey_tasks_progress', ui('Read one existing generation job', zhText("m418")), {
118
+ jobId: z.string().describe(ui('The task ID to query', zhText("m417"))),
118
119
  }, async ({ jobId }) => toolResult(await readJob(api(), jobId)));
119
- tool('lumenkey_tasks_wait', ui('Wait for an existing job without creating one', '等待已有生成任务,不创建新任务。'), {
120
- jobId: z.string().describe(ui('The existing task ID to wait for', '要等待的已有任务 ID')),
120
+ tool('lumenkey_tasks_wait', ui('Wait for an existing job without creating one', zhText("m416")), {
121
+ jobId: z.string().describe(ui('The existing task ID to wait for', zhText("m415"))),
121
122
  ...zodShape(waitParams),
122
123
  }, async ({ jobId, ...rest }) => toolResult(await waitJob(api(), jobId, waitArgs(rest, 'mcp'))));
123
- tool('lumenkey_resume_submission', ui('Re-send a recorded submission verbatim by its idempotency key (recovery after an unknown outcome). The gateway replays the earlier job or creates it once; nothing is quoted anew and no generation argument is accepted.', '按幂等键原样重发一次已记录的提交(结果未知后的恢复)。网关会回放之前的任务或只创建一次;不会重新报价,也不接受任何生成参数。'), zodShape(resumeParams), async (args) => toolResult(await resumeSubmission(api(), { ...resumeArgs(args, 'mcp'), onEvent: mcpGenerationEvents() })));
124
- tool('lumenkey_tasks_cancel', ui('Request cancellation with yes: true. Cancellation is not a refund; held credits are released or charged only when gateway settlement completes.', '取消已有任务必须传 yes: true。取消不等于退款;冻结算力点是否释放或扣除由网关最终结算决定。'), {
125
- jobId: z.string().describe(ui('The existing task ID to cancel', '要取消的任务 ID')),
126
- yes: z.boolean({ invalid_type_error: ui('Cancellation requires yes: true.', '取消任务必须传入 yes: true。') }).optional().describe(ui('Must be true to authorize cancellation; cancellation is not a refund', '必须为 true 才授权取消;取消不等于退款')),
124
+ tool('lumenkey_resume_submission', ui('Re-send a recorded submission verbatim by its idempotency key (recovery after an unknown outcome). The gateway replays the earlier job or creates it once; nothing is quoted anew and no generation argument is accepted.', zhText("m414")), zodShape(resumeParams), async (args) => toolResult(await resumeSubmission(api(), { ...resumeArgs(args, 'mcp'), onEvent: mcpGenerationEvents() })));
125
+ tool('lumenkey_tasks_cancel', ui('Request cancellation with yes: true. Cancellation is not a refund; held credits are released or charged only when gateway settlement completes.', zhText("m413")), {
126
+ jobId: z.string().describe(ui('The existing task ID to cancel', zhText("m412"))),
127
+ yes: z.boolean({ invalid_type_error: ui('Cancellation requires yes: true.', zhText("m410")) }).optional().describe(ui('Must be true to authorize cancellation; cancellation is not a refund', zhText("m411"))),
127
128
  }, async ({ jobId, yes }) => {
128
129
  if (yes !== true)
129
- throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED', undefined, undefined, ui('Cancellation requires yes: true.', '取消任务必须传入 yes: true。'));
130
+ throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED', undefined, undefined, ui('Cancellation requires yes: true.', zhText("m410")));
130
131
  return dataResult(await api().cancelJob(jobId));
131
132
  });
132
133
  return server;
@@ -135,6 +136,6 @@ export async function runMcpStdio() {
135
136
  const server = createMcpServer();
136
137
  const transport = new StdioServerTransport();
137
138
  await server.connect(transport);
138
- log(ui('Server running on stdio transport', '服务已启动,使用 stdio 传输'));
139
+ log(ui('Server running on stdio transport', zhText("m409")));
139
140
  }
140
141
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,28 @@
1
+ import type { StudioRoot, StudioAsset } from './types.js';
2
+ export type IntegrityFileFact = 'NOT_CHECKED' | 'ABSENT' | 'PARTIAL' | 'VERIFIED' | 'MISSING' | 'MODIFIED' | 'UNSAFE' | 'UNKNOWN';
3
+ export interface AssetIntegrity {
4
+ file: IntegrityFileFact;
5
+ checkedAt: string | null;
6
+ integrityCheck: 'IDLE' | 'PENDING';
7
+ }
8
+ export interface IntegrityResult {
9
+ file: 'VERIFIED' | 'MISSING' | 'MODIFIED' | 'UNSAFE' | 'UNKNOWN';
10
+ checkedAt: string | null;
11
+ signature?: string;
12
+ errorCode?: 'SAVE_FILE_MISSING' | 'SAVE_FILE_MODIFIED' | 'SAVE_FILE_UNSAFE' | 'SAVE_FILE_READ_FAILED';
13
+ }
14
+ /** Not a serialized flag: only a completed real hash can mint this process-local proof. */
15
+ export declare function isVerifiedHashResult(assetId: string, value: unknown): value is IntegrityResult;
16
+ /** Bounded stat path: no hash, no user file mutation. The identity comes from an opened handle. */
17
+ export declare function inspectStudioAsset(root: StudioRoot, asset: StudioAsset): Promise<IntegrityResult>;
18
+ /** The only producer of VERIFIED. Full chunked bytes and identity are checked on
19
+ * the SAME open regular-file handle before and after reading. No network/repair. */
20
+ export declare function hashStudioAsset(root: StudioRoot, asset: StudioAsset, options: {
21
+ maxBytes: number;
22
+ expectedSignature?: string;
23
+ signal?: AbortSignal;
24
+ now?: () => number;
25
+ beforeRead?(): void | Promise<void>;
26
+ afterChunk?(): void | Promise<void>;
27
+ }): Promise<IntegrityResult>;
28
+ //# sourceMappingURL=asset-integrity-hash.d.ts.map
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, open } from 'node:fs/promises';
3
+ import { assertStudioRoot, resolveStudioAssetPath } from './paths.js';
4
+ import { hasFsCode, StudioError } from './errors.js';
5
+ const proofs = new WeakMap();
6
+ /** Not a serialized flag: only a completed real hash can mint this process-local proof. */
7
+ export function isVerifiedHashResult(assetId, value) {
8
+ return typeof value === 'object' && value !== null && proofs.get(value) === `${assetId}:${JSON.stringify(value)}`;
9
+ }
10
+ function signature(s) {
11
+ return JSON.stringify([String(s.dev), String(s.ino), s.size, s.mtimeMs, s.ctimeMs, s.birthtimeMs, s.nlink]);
12
+ }
13
+ function regular(s) { return s.isFile() && !s.isSymbolicLink() && s.nlink === 1 && s.ino !== 0; }
14
+ function failed(file) {
15
+ return { file, checkedAt: null, errorCode: { MISSING: 'SAVE_FILE_MISSING', MODIFIED: 'SAVE_FILE_MODIFIED', UNSAFE: 'SAVE_FILE_UNSAFE', UNKNOWN: 'SAVE_FILE_READ_FAILED' }[file] };
16
+ }
17
+ function failure(error) {
18
+ return failed(hasFsCode(error, 'ENOENT') ? 'MISSING' : error instanceof StudioError && error.code === 'STUDIO_PATH_UNSAFE' ? 'UNSAFE' : 'UNKNOWN');
19
+ }
20
+ /** Bounded stat path: no hash, no user file mutation. The identity comes from an opened handle. */
21
+ export async function inspectStudioAsset(root, asset) {
22
+ try {
23
+ const file = await resolveStudioAssetPath(root, asset);
24
+ const handle = await open(file, 'r');
25
+ try {
26
+ const current = await handle.stat();
27
+ const named = await lstat(file);
28
+ await assertStudioRoot(root);
29
+ if (!regular(current) || !regular(named) || signature(current) !== signature(named))
30
+ return failed('UNSAFE');
31
+ if (current.size !== asset.byteLength)
32
+ return failed('MODIFIED');
33
+ return { file: 'UNKNOWN', checkedAt: null, signature: signature(current) };
34
+ }
35
+ finally {
36
+ await handle.close();
37
+ }
38
+ }
39
+ catch (error) {
40
+ return failure(error);
41
+ }
42
+ }
43
+ /** The only producer of VERIFIED. Full chunked bytes and identity are checked on
44
+ * the SAME open regular-file handle before and after reading. No network/repair. */
45
+ export async function hashStudioAsset(root, asset, options) {
46
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
47
+ throw new StudioError('STUDIO_INPUT_INVALID');
48
+ try {
49
+ const file = await resolveStudioAssetPath(root, asset);
50
+ const handle = await open(file, 'r');
51
+ try {
52
+ const before = await handle.stat();
53
+ const named = await lstat(file);
54
+ if (!regular(before) || !regular(named) || signature(before) !== signature(named))
55
+ return failed('UNSAFE');
56
+ const beforeSignature = signature(before);
57
+ if (before.size !== asset.byteLength || before.size > options.maxBytes || (options.expectedSignature !== undefined && options.expectedSignature !== beforeSignature))
58
+ return failed('MODIFIED');
59
+ const hash = createHash('sha256');
60
+ const buffer = Buffer.alloc(64 * 1024);
61
+ let position = 0;
62
+ for (;;) {
63
+ options.signal?.throwIfAborted();
64
+ await options.beforeRead?.();
65
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, asset.byteLength + 1 - position), position);
66
+ if (!bytesRead)
67
+ break;
68
+ position += bytesRead;
69
+ if (position > options.maxBytes || position > asset.byteLength)
70
+ return failed('MODIFIED');
71
+ hash.update(buffer.subarray(0, bytesRead));
72
+ await options.afterChunk?.();
73
+ }
74
+ const after = await handle.stat();
75
+ const afterNamed = await lstat(file);
76
+ await assertStudioRoot(root);
77
+ if (!regular(after) || !regular(afterNamed))
78
+ return failed('UNSAFE');
79
+ if (signature(after) !== beforeSignature || signature(afterNamed) !== beforeSignature || position !== asset.byteLength || hash.digest('hex') !== asset.sha256)
80
+ return failed('MODIFIED');
81
+ const result = Object.freeze({ file: 'VERIFIED', checkedAt: new Date((options.now ?? Date.now)()).toISOString(), signature: beforeSignature });
82
+ proofs.set(result, `${asset.assetId}:${JSON.stringify(result)}`);
83
+ return result;
84
+ }
85
+ finally {
86
+ await handle.close();
87
+ }
88
+ }
89
+ catch (error) {
90
+ return failure(error);
91
+ }
92
+ }
93
+ //# sourceMappingURL=asset-integrity-hash.js.map
@@ -0,0 +1,24 @@
1
+ import { type AssetIntegrity } from './asset-integrity-hash.js';
2
+ import type { StudioSaveAuthority } from './output-save-journal.js';
3
+ import type { StudioAsset, StudioRoot } from './types.js';
4
+ export interface AssetIntegrityQueue {
5
+ cached(assetIds: readonly string[]): Promise<Map<string, AssetIntegrity>>;
6
+ observe(asset: StudioAsset): Promise<AssetIntegrity>;
7
+ idle(): Promise<void>;
8
+ close(): Promise<void>;
9
+ }
10
+ /** Root-owned local observer. No network, repair, HTTP hooks or file mutation.
11
+ * Hashing runs outside the authority's short commit queue. A failed completion
12
+ * publication makes later reads fail closed, rather than advertising stale success. */
13
+ export declare function createAssetIntegrityQueue(root: StudioRoot, authority: StudioSaveAuthority, options: {
14
+ maxBytes: number;
15
+ now?: () => number;
16
+ beforeHash?(asset: StudioAsset): void | Promise<void>;
17
+ afterHash?(asset: StudioAsset): void | Promise<void>;
18
+ onInspect?(): void;
19
+ hashOptions?: {
20
+ beforeRead?(): void | Promise<void>;
21
+ afterChunk?(): void | Promise<void>;
22
+ };
23
+ }): AssetIntegrityQueue;
24
+ //# sourceMappingURL=asset-integrity-queue.d.ts.map