@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
@@ -0,0 +1,7 @@
1
+ export type ChineseScript = 'Hans' | 'Hant';
2
+ /** No credentials or files are read. Explicit script overrides deployment/device detection. */
3
+ export declare function resolveChineseScript(env?: NodeJS.ProcessEnv, device?: {
4
+ intl?: string;
5
+ windows?: string;
6
+ }): ChineseScript;
7
+ //# sourceMappingURL=chinese-script.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ let windowsLocale;
3
+ function windowsUiLocale() {
4
+ if (process.platform !== 'win32')
5
+ return undefined;
6
+ if (windowsLocale !== undefined)
7
+ return windowsLocale;
8
+ try {
9
+ windowsLocale = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '[Console]::Write([Globalization.CultureInfo]::CurrentUICulture.Name)'], { encoding: 'utf8', timeout: 1500, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
10
+ }
11
+ catch {
12
+ windowsLocale = '';
13
+ }
14
+ return windowsLocale;
15
+ }
16
+ /** No credentials or files are read. Explicit script overrides deployment/device detection. */
17
+ export function resolveChineseScript(env = process.env, device) {
18
+ const override = env.LUMENKEY_ZH_SCRIPT?.toLowerCase();
19
+ if (override === 'hant' || override === 'traditional')
20
+ return 'Hant';
21
+ if (override === 'hans' || override === 'simplified')
22
+ return 'Hans';
23
+ const explicitLocale = env.LUMENKEY_LANG && /(?:hant|hans|tw|hk|mo|cn)/i.test(env.LUMENKEY_LANG) ? env.LUMENKEY_LANG : undefined;
24
+ const locale = explicitLocale || env.LC_ALL || env.LC_MESSAGES || env.LANG
25
+ || (device ? device.windows || device.intl : windowsUiLocale() || Intl.DateTimeFormat().resolvedOptions().locale) || '';
26
+ return /^zh(?:[-_].*)?$/i.test(locale.split(/[.@]/)[0]) && /(?:^|[-_])(?:hant|tw|hk|mo)(?:$|[-_])/i.test(locale.split(/[.@]/)[0]) ? 'Hant' : 'Hans';
27
+ }
28
+ //# sourceMappingURL=chinese-script.js.map
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { ProxyAgent, fetch as undiciFetch } from 'undici';
3
4
  import { billingSentence, blankPromptSentence, chatFailureSettlementAdvice, chatRetryAdvice, errorSentence, hasErrorSentence, internalUpstreamError, isBlankPrompt, langFlag, proxyTransportDetail, textCatalogMissDetail, transportFailureDetail, ui } from './locale.js';
@@ -185,7 +186,7 @@ export function assetFetch(url, init = {}) {
185
186
  }
186
187
  export function validateModelType(type) {
187
188
  if (type !== undefined && !['video', 'image', 'text', 'audio'].includes(type)) {
188
- throw new Error(ui(`Invalid model type ${JSON.stringify(type)}. Expected video, image, text or audio.`, `模型类型 ${JSON.stringify(type)} 无效。只能是 video、image、text 或 audio。`));
189
+ throw new Error(ui(`Invalid model type ${JSON.stringify(type)}. Expected video, image, text or audio.`, (zhText("m7") + (JSON.stringify(type)) + zhText("m8"))));
189
190
  }
190
191
  }
191
192
  export function validateJobId(jobId) {
@@ -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 { ui } from './locale.js';
@@ -13,7 +14,7 @@ export function assertConfigDirReadable(targetPath) {
13
14
  if (code === 'ENOENT' || code === 'ENOTDIR')
14
15
  return;
15
16
  if (code === 'EACCES' || code === 'EPERM') {
16
- throw new ConfigError('CONFIG_UNREADABLE', targetPath, ui('The config directory could not be read.', '配置目录无法读取。'));
17
+ throw new ConfigError('CONFIG_UNREADABLE', targetPath, ui('The config directory could not be read.', zhText("m9")));
17
18
  }
18
19
  throw error;
19
20
  }
@@ -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';
@@ -49,17 +50,17 @@ function readStoredConfig(configPath) {
49
50
  throw configFailure(error, configPath);
50
51
  }
51
52
  if (text.trim() === '') {
52
- throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json is empty.', 'config.json 是空文件。'));
53
+ throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json is empty.', zhText("m15")));
53
54
  }
54
55
  let parsed;
55
56
  try {
56
57
  parsed = JSON.parse(text);
57
58
  }
58
59
  catch {
59
- throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json is not valid JSON.', 'config.json 不是有效的 JSON。'));
60
+ throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json is not valid JSON.', zhText("m14")));
60
61
  }
61
62
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
62
- throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json must be an object.', 'config.json 必须是对象。'));
63
+ throw new ConfigError('CONFIG_INVALID', configPath, ui('config.json must be an object.', zhText("m13")));
63
64
  }
64
65
  return validateStoredConfig(parsed, configPath);
65
66
  }
@@ -67,13 +68,13 @@ function validateStoredConfig(record, configPath) {
67
68
  const reasons = [];
68
69
  for (const key of ['apiKey', 'baseUrl', 'projectId']) {
69
70
  if (record[key] !== undefined && typeof record[key] !== 'string') {
70
- reasons.push(ui(`${key} must be a string.`, `${key} 必须是字符串。`));
71
+ reasons.push(ui(`${key} must be a string.`, ("" + (key) + zhText("m12"))));
71
72
  }
72
73
  }
73
74
  if (reasons.length > 0)
74
75
  throw new ConfigError('CONFIG_INVALID', configPath, reasons.join(' '));
75
76
  if (record.timeoutMs !== undefined && (typeof record.timeoutMs !== 'number' || !Number.isSafeInteger(record.timeoutMs) || record.timeoutMs <= 0)) {
76
- throw new ConfigError('CONFIG_INVALID', configPath, ui('timeoutMs must be a positive integer.', 'timeoutMs 必须是正整数。'));
77
+ throw new ConfigError('CONFIG_INVALID', configPath, ui('timeoutMs must be a positive integer.', zhText("m11")));
77
78
  }
78
79
  return {
79
80
  apiKey: record.apiKey,
@@ -98,7 +99,7 @@ export function loadConfig() {
98
99
  undefined;
99
100
  const envTimeout = Number(process.env.LUMENKEY_TIMEOUT_MS);
100
101
  if (process.env.LUMENKEY_TIMEOUT_MS !== undefined && (!Number.isSafeInteger(envTimeout) || envTimeout <= 0)) {
101
- throw new ConfigError('CONFIG_INVALID', configPath, ui('LUMENKEY_TIMEOUT_MS must be a positive integer.', 'LUMENKEY_TIMEOUT_MS 必须是正整数。'));
102
+ throw new ConfigError('CONFIG_INVALID', configPath, ui('LUMENKEY_TIMEOUT_MS must be a positive integer.', zhText("m10")));
102
103
  }
103
104
  const timeoutMs = Number.isSafeInteger(envTimeout) && envTimeout > 0
104
105
  ? envTimeout
@@ -0,0 +1,5 @@
1
+ /** A configured root is authoritative. Without it, retain the caller's existing destination-directory scope. */
2
+ export declare function assertDownloadPath(raw: string, policy?: {
3
+ configuredRoot: string | undefined;
4
+ }): string;
5
+ //# sourceMappingURL=download-root.d.ts.map
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { LumenKeyError } from './client.js';
4
+ function inside(root, target) {
5
+ const relative = path.relative(root, target);
6
+ return !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep);
7
+ }
8
+ function canonicalMissing(target) {
9
+ let cursor = target;
10
+ const missing = [];
11
+ for (;;) {
12
+ try {
13
+ return path.join(fs.realpathSync(cursor), ...missing);
14
+ }
15
+ catch (error) {
16
+ if (error.code !== 'ENOENT')
17
+ throw error;
18
+ const parent = path.dirname(cursor);
19
+ if (parent === cursor)
20
+ throw error;
21
+ missing.unshift(path.basename(cursor));
22
+ cursor = parent;
23
+ }
24
+ }
25
+ }
26
+ /** A configured root is authoritative. Without it, retain the caller's existing destination-directory scope. */
27
+ export function assertDownloadPath(raw, policy) {
28
+ if (raw.split(/[\\/]/).includes('..'))
29
+ throw new LumenKeyError('DOWNLOAD_OUTSIDE_ROOT');
30
+ const target = path.resolve(raw);
31
+ const configured = policy ? policy.configuredRoot?.trim() : process.env.LUMENKEY_DOWNLOAD_ROOT?.trim();
32
+ if (configured && !path.isAbsolute(configured))
33
+ throw new LumenKeyError('DOWNLOAD_ROOT_INVALID');
34
+ const root = configured ? path.resolve(configured) : path.dirname(target);
35
+ if (!inside(root, target))
36
+ throw new LumenKeyError('DOWNLOAD_OUTSIDE_ROOT');
37
+ try {
38
+ const canonicalRoot = canonicalMissing(root);
39
+ let cursor = root;
40
+ for (const part of path.relative(root, target).split(path.sep).filter(Boolean)) {
41
+ cursor = path.join(cursor, part);
42
+ let stat;
43
+ try {
44
+ stat = fs.lstatSync(cursor);
45
+ }
46
+ catch (error) {
47
+ if (error.code === 'ENOENT')
48
+ continue;
49
+ throw error;
50
+ }
51
+ if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink > 1))
52
+ throw new LumenKeyError('DOWNLOAD_UNSAFE_PATH');
53
+ }
54
+ if (!inside(canonicalRoot, canonicalMissing(target)))
55
+ throw new LumenKeyError('DOWNLOAD_OUTSIDE_ROOT');
56
+ return target;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof LumenKeyError)
60
+ throw error;
61
+ throw new LumenKeyError('DOWNLOAD_UNSAFE_PATH');
62
+ }
63
+ }
64
+ //# sourceMappingURL=download-root.js.map
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
- import { Readable, Transform } from 'node:stream';
5
- import { pipeline } from 'node:stream/promises';
4
+ import { streamAssetBodyToFile } from './asset-stream.js';
6
5
  import { assetFetch, LumenKeyError } from './client.js';
6
+ import { assertDownloadPath } from './download-root.js';
7
7
  /**
8
8
  * What currently sits at the destination. A link is followed only to classify it (a link to a
9
9
  * directory is a directory, a link to a file or a dangling link is an occupied path); publishing
@@ -106,7 +106,9 @@ export async function downloadFile(url, outputPath, overwrite = false, callerSig
106
106
  try {
107
107
  resolved = path.resolve(outputPath);
108
108
  }
109
- catch {
109
+ catch (error) {
110
+ if (error instanceof LumenKeyError)
111
+ throw error;
110
112
  throw new LumenKeyError('DOWNLOAD_WRITE_ERROR');
111
113
  }
112
114
  const temporary = `${resolved}.${crypto.randomUUID()}.part`;
@@ -127,6 +129,7 @@ export async function downloadFile(url, outputPath, overwrite = false, callerSig
127
129
  throw new LumenKeyError('DOWNLOAD_IS_DIRECTORY');
128
130
  if (existing === 'occupied' && !overwrite)
129
131
  throw new LumenKeyError('DOWNLOAD_EXISTS');
132
+ assertDownloadPath(outputPath);
130
133
  stage = 'network';
131
134
  const fetched = await assetFetch(url, { redirect: 'error', signal });
132
135
  dispose = fetched.dispose;
@@ -141,21 +144,21 @@ export async function downloadFile(url, outputPath, overwrite = false, callerSig
141
144
  stage = 'write';
142
145
  // Only now is a directory needed; anything created here is removed again if the save fails.
143
146
  createdDirectories = await ensureParentDirectories(path.dirname(resolved));
147
+ assertDownloadPath(resolved);
144
148
  await sweepStaleParts(path.dirname(resolved));
145
- const handle = await fs.promises.open(temporary, 'wx', 0o600);
149
+ const handle = await fs.promises.open(temporary, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
146
150
  ownedTemporary = true;
147
- let bytes = 0;
148
- const source = Readable.fromWeb(response.body);
149
- // Distinguish body transport failures from filesystem failures.
150
- source.on('error', () => { stage = 'network'; });
151
- await pipeline(source, new Transform({ transform(chunk, _encoding, callback) {
152
- bytes += chunk.length;
153
- callback(bytes > limit ? new LumenKeyError('DOWNLOAD_TOO_LARGE') : null, chunk);
154
- } }), handle.createWriteStream(), { signal });
151
+ try {
152
+ await streamAssetBodyToFile(response.body, handle, { limit, signal, onReadError: () => { stage = 'network'; } });
153
+ }
154
+ finally {
155
+ await handle.close();
156
+ }
155
157
  stage = 'write';
156
158
  if (callerSignal?.aborted)
157
159
  throw new LumenKeyError('DOWNLOAD_INTERRUPTED');
158
160
  publishing = true;
161
+ assertDownloadPath(resolved);
159
162
  if (overwrite)
160
163
  await fs.promises.rename(temporary, resolved);
161
164
  else
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import { LumenKeyError } from './client.js';
2
3
  import { isBlankPrompt, promptRequiredSentence, ui } from './locale.js';
3
4
  import { applyKeyValues, modelParameterError, validateKnownEnums } from './set-values.js';
@@ -114,7 +115,7 @@ export async function buildGenerationInput(client, modelId, fields, pairs, known
114
115
  const extra = applyKeyValues({}, pairs);
115
116
  for (const [key, value] of [...Object.entries(defined), ...Object.entries(extra)]) {
116
117
  if (value === null)
117
- throw new LumenKeyError('MODEL_PARAM_INVALID', undefined, undefined, `${modelParameterError('MODEL_PARAM_INVALID', key).publicMessage} ${ui('To use the default, omit this field.', '要用默认值就省略该字段。')}`);
118
+ throw new LumenKeyError('MODEL_PARAM_INVALID', undefined, undefined, `${modelParameterError('MODEL_PARAM_INVALID', key).publicMessage} ${ui('To use the default, omit this field.', zhText("m17"))}`);
118
119
  }
119
120
  assertNoParameterAliasConflict([...Object.keys(defined), ...Object.keys(extra)]);
120
121
  const model = known?.id === modelId ? known : (await client.listModels()).find((item) => item.id === modelId);
@@ -140,6 +141,6 @@ export function dryRunPreview(modelId, input, model, confirm) {
140
141
  const properties = propertiesOf(model);
141
142
  const defaults = scalarDefaults(properties);
142
143
  return { dryRun: true, defaultsApplied: true, modelId, input, schemaDefaults: defaults, required: model.inputSchema?.required ?? [],
143
- ...(confirm ? { confirmIgnored: true, notice: ui('--confirm has no effect with --dry-run. No job submitted.', '--confirm 在 --dry-run 下不会生效。没有提交任务。') } : {}) };
144
+ ...(confirm ? { confirmIgnored: true, notice: ui('--confirm has no effect with --dry-run. No job submitted.', zhText("m16")) } : {}) };
144
145
  }
145
146
  //# sourceMappingURL=generation-input.js.map
@@ -1,7 +1,7 @@
1
1
  import { LumenKeyClient } from './client.js';
2
2
  import { type GenerationSuccess } from './generation-result.js';
3
3
  import { type DryRunPreview } from './generation-input.js';
4
- import { type JobSuccess, type Outcome, type QuoteSuccess, type Success, type WaitSuccess } from './outcome.js';
4
+ import { type JobSuccess, type Outcome, type QuoteSuccess, type Submission, type Success, type WaitSuccess } from './outcome.js';
5
5
  import { type OutputPathPolicy } from './output-path.js';
6
6
  import type { GenerationJobDetail, GenerationQuoteResponse } from './types.js';
7
7
  export type GenerationEvent = {
@@ -91,6 +91,13 @@ export type GenerationOutcome = Outcome<DryRunPreview | QuoteSuccess | Generatio
91
91
  * proves nothing about the first attempt; the submission stays `unknown` and the record is kept.
92
92
  */
93
93
  export declare const REPLAY_SETTLING_CODES: readonly string[];
94
+ /**
95
+ * What a failed job POST proves. First attempt: the server writes record, hold and job in one
96
+ * transaction, so a business-coded 4xx (IDEMPOTENCY_CONFLICT included: *this* attempt created
97
+ * nothing) means `none`; transport loss, timeout, malformed 2xx and every 5xx mean `unknown`.
98
+ * Replay of a recorded submission: only REPLAY_SETTLING_CODES settle it as `none`.
99
+ */
100
+ export declare function classifyCreateFailure(error: unknown, replay: boolean): Submission;
94
101
  /**
95
102
  * quote -> confirm -> create job -> poll -> finish, for every surface. Never throws: every
96
103
  * exit is an Outcome, and once the job POST has been attempted every exit names the
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import crypto from 'node:crypto';
2
3
  import { assertIdempotencyKey, LumenKeyError, validateJobId } from './client.js';
3
4
  import { assertWaitOptions, pollJob } from './poller.js';
@@ -43,7 +44,7 @@ export const REPLAY_SETTLING_CODES = Object.freeze([
43
44
  * nothing) means `none`; transport loss, timeout, malformed 2xx and every 5xx mean `unknown`.
44
45
  * Replay of a recorded submission: only REPLAY_SETTLING_CODES settle it as `none`.
45
46
  */
46
- function classifyCreateFailure(error, replay) {
47
+ export function classifyCreateFailure(error, replay) {
47
48
  if (!(error instanceof LumenKeyError))
48
49
  return 'unknown';
49
50
  if (replay)
@@ -150,10 +151,12 @@ function assertOwnRecord(record, identity, suppliedKey) {
150
151
  /** Past the resumable window the gateway may have dropped its record; a replay could be misread as "never created". */
151
152
  function assertNotExpired(record) {
152
153
  if (isExpired(record))
153
- throw Object.assign(new LumenKeyError('SUBMISSION_RECORD_EXPIRED'), { idempotencyKey: record.idempotencyKey, quoteId: record.quoteId });
154
+ throw Object.assign(new LumenKeyError('SUBMISSION_RECORD_EXPIRED'), {
155
+ idempotencyKey: record.idempotencyKey, quoteId: record.quoteId, recordedAt: record.createdAt,
156
+ });
154
157
  }
155
158
  function pendingRecordRemainsNote(pending) {
156
- return ui(`A pending submission with idempotency key ${pending.idempotencyKey} (quote ${pending.quoteId}, ${pending.credits} credits) is still recorded and was not re-sent; raise the cap or check your job list and balance.`, `幂等键 ${pending.idempotencyKey} 的待确认提交(报价 ${pending.quoteId},${pending.credits} 算力点)仍有记录且未重发;请提高上限,或核对任务列表与余额。`);
159
+ return ui(`A pending submission with idempotency key ${pending.idempotencyKey} (quote ${pending.quoteId}, ${pending.credits} credits) is still recorded and was not re-sent; raise the cap or check your job list and balance.`, (zhText("m39") + (pending.idempotencyKey) + zhText("m42") + (pending.quoteId) + "," + (pending.credits) + zhText("m43")));
157
160
  }
158
161
  /**
159
162
  * quote -> confirm -> create job -> poll -> finish, for every surface. Never throws: every
@@ -196,14 +199,14 @@ export async function runGeneration(client, req) {
196
199
  const taken = takePendingSubmission(req.idempotencyKey, req.home);
197
200
  pending = taken.record;
198
201
  if (taken.discarded)
199
- note(ui(`An earlier submission recorded under idempotency key ${req.idempotencyKey} ${ageText(taken.discarded)} is older than 48 hours and can no longer be verified from here; its record was discarded. Check your job list and balance if that submission matters before continuing under this key.`, `幂等键 ${req.idempotencyKey} 下${ageText(taken.discarded)}记录的一次提交已超过 48 小时,无法再从这里核实,记录已丢弃。若那次提交重要,请先核对任务列表与余额,再继续使用该键。`));
202
+ note(ui(`An earlier submission recorded under idempotency key ${req.idempotencyKey} ${ageText(taken.discarded)} is older than 48 hours and can no longer be verified from here; its record was discarded. Check your job list and balance if that submission matters before continuing under this key.`, (zhText("m39") + (req.idempotencyKey) + zhText("m40") + (ageText(taken.discarded)) + zhText("m41"))));
200
203
  }
201
204
  const changed = findInputPolicyConflict(identity, req.modelId, built.input, built.callerInput, built.defaults, req.home, built.legacyCallerInput);
202
205
  if (changed) {
203
206
  // No quote, no POST, no rewritten journal. The explicit recovery verb alone replays the
204
207
  // historical body, and keeps its existing sender binding and 23-hour safety window.
205
- const advice = isExpired(changed) ? ui('Its replay window has expired; check your job list and balance instead of retrying it.', '该记录已超过安全重放窗口,请核对任务列表和余额,不要重新提交。') : ui(`Use ${resumeCommandText(changed.idempotencyKey, 'en')} to recover the recorded body verbatim.`, `请用 ${resumeCommandText(changed.idempotencyKey, 'zh')} 原样恢复已有提交。`);
206
- throw Object.assign(new LumenKeyError('SUBMISSION_INPUT_POLICY_CHANGED', undefined, undefined, ui(`Catalog defaults or client input rules changed an unconfirmed submission (key ${changed.idempotencyKey}, quote ${changed.quoteId}). No new quote or job was sent. ${advice}`, `目录默认值或客户端入参规则使待确认提交的输入发生变化(键 ${changed.idempotencyKey},报价 ${changed.quoteId})。没有发送新报价或新任务。${advice}`)), { ...(req.idempotencyKey !== undefined ? { idempotencyKey: req.idempotencyKey } : {}), pendingIdempotencyKey: changed.idempotencyKey,
208
+ const advice = isExpired(changed) ? ui('Its replay window has expired; check your job list and balance instead of retrying it.', zhText("m38")) : ui(`Use ${resumeCommandText(changed.idempotencyKey, 'en')} to recover the recorded body verbatim.`, (zhText("m36") + (resumeCommandText(changed.idempotencyKey, 'zh')) + zhText("m37")));
209
+ throw Object.assign(new LumenKeyError('SUBMISSION_INPUT_POLICY_CHANGED', undefined, undefined, ui(`Catalog defaults or client input rules changed an unconfirmed submission (key ${changed.idempotencyKey}, quote ${changed.quoteId}). No new quote or job was sent. ${advice}`, (zhText("m33") + (changed.idempotencyKey) + zhText("m34") + (changed.quoteId) + zhText("m35") + (advice) + ""))), { ...(req.idempotencyKey !== undefined ? { idempotencyKey: req.idempotencyKey } : {}), pendingIdempotencyKey: changed.idempotencyKey,
207
210
  pendingQuoteId: changed.quoteId, pendingRecordedAt: changed.createdAt, pendingAgeSeconds: Math.floor(ageMs(changed) / 1000) });
208
211
  }
209
212
  if (!pending) {
@@ -214,7 +217,7 @@ export async function runGeneration(client, req) {
214
217
  // An explicit key means "this is a distinct request": the client neither overrides it nor creates a second
215
218
  // identical job while an identical submission is unconfirmed. Refuse, and name the pending record structurally.
216
219
  const pendingRecord = lookup.record;
217
- throw Object.assign(new LumenKeyError('SUBMISSION_PENDING_IDENTICAL', undefined, undefined, ui(`An identical submission is already recorded and unconfirmed under idempotency key ${pendingRecord.idempotencyKey} (quote ${pendingRecord.quoteId}, recorded ${ageText(pendingRecord)}). Nothing was sent under key ${req.idempotencyKey}. Settle it first with ${resumeCommandText(pendingRecord.idempotencyKey, 'en')}; a second identical job cannot be created until then, or until the record expires 23 hours after it was recorded.`, `已有一次相同的提交以幂等键 ${pendingRecord.idempotencyKey} 记录且结果未确认(报价 ${pendingRecord.quoteId},记录于${ageText(pendingRecord)})。没有用键 ${req.idempotencyKey} 发送任何内容。请先运行 ${resumeCommandText(pendingRecord.idempotencyKey, 'zh')} 处理它;在它处理完或记录后 23 小时过期之前,无法创建第二个相同的任务。`)), { idempotencyKey: req.idempotencyKey, pendingIdempotencyKey: pendingRecord.idempotencyKey, pendingQuoteId: pendingRecord.quoteId, pendingRecordedAt: pendingRecord.createdAt, pendingAgeSeconds: Math.floor(ageMs(pendingRecord) / 1000) });
220
+ throw Object.assign(new LumenKeyError('SUBMISSION_PENDING_IDENTICAL', undefined, undefined, ui(`An identical submission is already recorded and unconfirmed under idempotency key ${pendingRecord.idempotencyKey} (quote ${pendingRecord.quoteId}, recorded ${ageText(pendingRecord)}). Nothing was sent under key ${req.idempotencyKey}. Settle it first with ${resumeCommandText(pendingRecord.idempotencyKey, 'en')}; a second identical job cannot be created until then, or until the record expires 23 hours after it was recorded.`, (zhText("m27") + (pendingRecord.idempotencyKey) + zhText("m28") + (pendingRecord.quoteId) + zhText("m29") + (ageText(pendingRecord)) + zhText("m30") + (req.idempotencyKey) + zhText("m31") + (resumeCommandText(pendingRecord.idempotencyKey, 'zh')) + zhText("m32")))), { idempotencyKey: req.idempotencyKey, pendingIdempotencyKey: pendingRecord.idempotencyKey, pendingQuoteId: pendingRecord.quoteId, pendingRecordedAt: pendingRecord.createdAt, pendingAgeSeconds: Math.floor(ageMs(pendingRecord) / 1000) });
218
221
  }
219
222
  pending = lookup.record;
220
223
  }
@@ -229,7 +232,7 @@ export async function runGeneration(client, req) {
229
232
  let input;
230
233
  if (pending) {
231
234
  if (pending.modelId !== req.modelId || canonical(pending.input) !== canonical(built.input)) {
232
- throw Object.assign(new LumenKeyError('SUBMISSION_RETRY_MISMATCH', undefined, undefined, ui(`This idempotency key belongs to a pending submission with a different model or input. Nothing was submitted. Recorded: model ${pending.modelId}, input ${canonical(pending.input)}. Given: model ${req.modelId}, input ${canonical(built.input)}. To re-send the recorded submission as recorded (for example after a catalog change), run ${resumeCommandText(pending.idempotencyKey, 'en')}.`, `该幂等键对应一条待确认的提交,但模型或输入与这次不同。没有提交任何任务。记录:模型 ${pending.modelId},输入 ${canonical(pending.input)}。这次:模型 ${req.modelId},输入 ${canonical(built.input)}。要原样重发记录的提交(例如目录变更后),请运行 ${resumeCommandText(pending.idempotencyKey, 'zh')}。`)), { idempotencyKey: pending.idempotencyKey, quoteId: pending.quoteId });
235
+ throw Object.assign(new LumenKeyError('SUBMISSION_RETRY_MISMATCH', undefined, undefined, ui(`This idempotency key belongs to a pending submission with a different model or input. Nothing was submitted. Recorded: model ${pending.modelId}, input ${canonical(pending.input)}. Given: model ${req.modelId}, input ${canonical(built.input)}. To re-send the recorded submission as recorded (for example after a catalog change), run ${resumeCommandText(pending.idempotencyKey, 'en')}.`, (zhText("m23") + (pending.modelId) + zhText("m24") + (canonical(pending.input)) + zhText("m25") + (req.modelId) + zhText("m24") + (canonical(built.input)) + zhText("m26") + (resumeCommandText(pending.idempotencyKey, 'zh')) + "。"))), { idempotencyKey: pending.idempotencyKey, quoteId: pending.quoteId });
233
236
  }
234
237
  ({ quoteId, credits, projectId, input } = pending);
235
238
  if (req.maxCredits !== undefined && credits > req.maxCredits) {
@@ -277,14 +280,14 @@ export async function resumeSubmission(client, req) {
277
280
  const taken = takePendingSubmission(req.idempotencyKey, req.home);
278
281
  if (!taken.record) {
279
282
  throw Object.assign(new LumenKeyError('SUBMISSION_RECORD_NOT_FOUND', undefined, undefined, taken.discarded
280
- ? ui(`${ui('No pending submission is recorded under this idempotency key for this account and gateway.', '这个账号和网关下没有以该幂等键记录的待确认提交。')} An earlier record under this key (${ageText(taken.discarded)}) was older than 48 hours and was discarded; check your job list and balance.`, `这个账号和网关下没有以该幂等键记录的待确认提交。该键下更早的记录(${ageText(taken.discarded)})已超过 48 小时并被丢弃;请核对任务列表与余额。`)
283
+ ? ui(`${ui('No pending submission is recorded under this idempotency key for this account and gateway.', zhText("m22"))} An earlier record under this key (${ageText(taken.discarded)}) was older than 48 hours and was discarded; check your job list and balance.`, (zhText("m20") + (ageText(taken.discarded)) + zhText("m21")))
281
284
  : undefined), { idempotencyKey: req.idempotencyKey });
282
285
  }
283
286
  const record = taken.record;
284
287
  assertOwnRecord(record, identity, req.idempotencyKey);
285
288
  assertNotExpired(record);
286
289
  if (record.state === 'in-flight' && record.hostname === os.hostname() && record.pid !== process.pid && processAlive(record.pid)) {
287
- journal.note(ui(`The process that recorded this submission (pid ${record.pid}) is still alive; resuming anyway. The same key and body are re-sent, so the gateway replays or refuses and never creates a second job.`, `记录这次提交的进程(pid ${record.pid})仍在运行;仍然继续接续。重发的是同一个键和请求体,网关只会回放或拒绝,不会创建第二个任务。`));
290
+ journal.note(ui(`The process that recorded this submission (pid ${record.pid}) is still alive; resuming anyway. The same key and body are re-sent, so the gateway replays or refuses and never creates a second job.`, (zhText("m18") + (record.pid) + zhText("m19"))));
288
291
  }
289
292
  return await submitAndFinish(client, {
290
293
  kind: record.kind, modelId: record.modelId, quoteId: record.quoteId, credits: record.credits, projectId: record.projectId, input: record.input, pending: record,
@@ -1,18 +1,19 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import { jobStatusText, ui } from './locale.js';
2
3
  /** Human display only. Machine consumers continue to receive the original DTO. */
3
4
  export function formatJobDetail(detail) {
4
5
  const lines = [
5
- ui(`Task: ${detail.jobId}`, `任务:${detail.jobId}`),
6
- ui(`Model: ${detail.modelId ?? 'not reported'}`, `模型:${detail.modelId ?? '未返回'}`),
7
- ui(`Status: ${jobStatusText(detail.status)}`, `状态:${jobStatusText(detail.status)}`),
6
+ ui(`Task: ${detail.jobId}`, (zhText("m50") + (detail.jobId) + "")),
7
+ ui(`Model: ${detail.modelId ?? 'not reported'}`, (zhText("m48") + (detail.modelId ?? zhText("m49")) + "")),
8
+ ui(`Status: ${jobStatusText(detail.status)}`, (zhText("m47") + (jobStatusText(detail.status)) + "")),
8
9
  ];
9
10
  if (detail.credits !== undefined)
10
- lines.push(ui(`Credits: ${detail.credits}`, `算力点:${detail.credits}`));
11
+ lines.push(ui(`Credits: ${detail.credits}`, (zhText("m46") + (detail.credits) + "")));
11
12
  if (detail.updatedAt)
12
- lines.push(ui(`Updated: ${detail.updatedAt}`, `更新时间:${detail.updatedAt}`));
13
+ lines.push(ui(`Updated: ${detail.updatedAt}`, (zhText("m45") + (detail.updatedAt) + "")));
13
14
  for (const output of detail.outputs ?? []) {
14
15
  if (typeof output.url === 'string' && output.url)
15
- lines.push(ui(`Result: ${output.url}`, `结果:${output.url}`));
16
+ lines.push(ui(`Result: ${output.url}`, (zhText("m44") + (output.url) + "")));
16
17
  }
17
18
  return lines.join('\n');
18
19
  }