@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.
- package/README.md +36 -2
- package/dist/cli/copied.js +86 -85
- package/dist/cli/index.js +56 -44
- package/dist/cli/render.js +3 -2
- package/dist/core/asset-stream.d.ts +16 -0
- package/dist/core/asset-stream.js +64 -0
- package/dist/core/atomic-config-file.js +8 -7
- package/dist/core/chinese-messages.d.ts +4 -0
- package/dist/core/chinese-messages.js +907 -0
- package/dist/core/chinese-script.d.ts +7 -0
- package/dist/core/chinese-script.js +28 -0
- package/dist/core/client.js +2 -1
- package/dist/core/config-dir.js +2 -1
- package/dist/core/config.js +7 -6
- package/dist/core/download-root.d.ts +5 -0
- package/dist/core/download-root.js +64 -0
- package/dist/core/download.js +15 -12
- package/dist/core/generation-input.js +3 -2
- package/dist/core/generation.d.ts +8 -1
- package/dist/core/generation.js +13 -10
- package/dist/core/job-display.js +7 -6
- package/dist/core/locale.js +163 -148
- package/dist/core/outcome.js +6 -4
- package/dist/core/output-path.d.ts +3 -1
- package/dist/core/output-path.js +5 -2
- package/dist/core/params.js +26 -25
- package/dist/core/profiles.js +5 -4
- package/dist/core/set-values.d.ts +0 -1
- package/dist/core/set-values.js +7 -7
- package/dist/core/submission-journal.js +31 -16
- package/dist/core/version.js +2 -3
- package/dist/mcp/server.js +30 -29
- package/dist/studio/asset-integrity-hash.d.ts +28 -0
- package/dist/studio/asset-integrity-hash.js +93 -0
- package/dist/studio/asset-integrity-queue.d.ts +24 -0
- package/dist/studio/asset-integrity-queue.js +94 -0
- package/dist/studio/asset-store.d.ts +52 -0
- package/dist/studio/asset-store.js +352 -0
- package/dist/studio/canvas/LICENSES.txt +1590 -0
- package/dist/studio/canvas/dependencies.json +862 -0
- package/dist/studio/canvas/director-source-manifest.json +47 -0
- package/dist/studio/canvas/editor.css +239 -0
- package/dist/studio/canvas/editor.js +4613 -0
- package/dist/studio/canvas/integration-source-manifest.json +384 -0
- package/dist/studio/canvas/source-manifest.json +240 -0
- package/dist/studio/canvas-store.d.ts +1254 -0
- package/dist/studio/canvas-store.js +205 -0
- package/dist/studio/capture-placement.d.ts +11 -0
- package/dist/studio/capture-placement.js +85 -0
- package/dist/studio/cloud-contract.d.ts +128 -0
- package/dist/studio/cloud-contract.js +229 -0
- package/dist/studio/cloud-service.d.ts +178 -0
- package/dist/studio/cloud-service.js +202 -0
- package/dist/studio/director-captures.d.ts +59 -0
- package/dist/studio/director-captures.js +141 -0
- package/dist/studio/director-contract.d.ts +1489 -0
- package/dist/studio/director-contract.js +288 -0
- package/dist/studio/director-png.d.ts +6 -0
- package/dist/studio/director-png.js +41 -0
- package/dist/studio/director-relations.d.ts +40 -0
- package/dist/studio/director-relations.js +103 -0
- package/dist/studio/document-authority.d.ts +7 -0
- package/dist/studio/document-authority.js +50 -0
- package/dist/studio/draft-store.d.ts +95 -0
- package/dist/studio/draft-store.js +100 -0
- package/dist/studio/errors.d.ts +85 -0
- package/dist/studio/errors.js +94 -0
- package/dist/studio/launch.d.ts +5 -0
- package/dist/studio/launch.js +31 -0
- package/dist/studio/media.d.ts +9 -0
- package/dist/studio/media.js +50 -0
- package/dist/studio/output-save-contract.d.ts +1321 -0
- package/dist/studio/output-save-contract.js +26 -0
- package/dist/studio/output-save-journal.d.ts +971 -0
- package/dist/studio/output-save-journal.js +287 -0
- package/dist/studio/output-save-recovery.d.ts +18 -0
- package/dist/studio/output-save-recovery.js +161 -0
- package/dist/studio/output-save-service.d.ts +436 -0
- package/dist/studio/output-save-service.js +251 -0
- package/dist/studio/output-save-worker.d.ts +40 -0
- package/dist/studio/output-save-worker.js +360 -0
- package/dist/studio/paths.d.ts +6 -0
- package/dist/studio/paths.js +68 -0
- package/dist/studio/run-journal.d.ts +993 -0
- package/dist/studio/run-journal.js +125 -0
- package/dist/studio/server.d.ts +17 -0
- package/dist/studio/server.js +440 -0
- package/dist/studio/session.d.ts +6 -0
- package/dist/studio/session.js +26 -0
- package/dist/studio/shell.d.ts +3 -0
- package/dist/studio/shell.js +23 -0
- package/dist/studio/style.d.ts +2 -0
- package/dist/studio/style.js +29 -0
- package/dist/studio/types.d.ts +26 -0
- package/dist/studio/types.js +2 -0
- package/dist/studio/web/app.d.ts +2 -0
- package/dist/studio/web/app.js +662 -0
- package/dist/studio/web/asset-controller.d.ts +37 -0
- package/dist/studio/web/asset-controller.js +191 -0
- package/dist/studio/web/asset-model.d.ts +29 -0
- package/dist/studio/web/asset-model.js +51 -0
- package/dist/studio/web/controller.d.ts +113 -0
- package/dist/studio/web/controller.js +401 -0
- package/dist/studio/web/messages.d.ts +2 -0
- package/dist/studio/web/messages.js +74 -0
- package/dist/studio/web/model.d.ts +75 -0
- package/dist/studio/web/model.js +182 -0
- package/dist/studio/web/save-controller.d.ts +85 -0
- package/dist/studio/web/save-controller.js +495 -0
- package/dist/studio/web/save-model.d.ts +63 -0
- package/dist/studio/web/save-model.js +64 -0
- package/dist/studio/web/save-queue.d.ts +7 -0
- package/dist/studio/web/save-queue.js +62 -0
- package/dist/studio/web/save-view.d.ts +9 -0
- package/dist/studio/web/save-view.js +19 -0
- package/dist/studio/writer-lock.d.ts +5 -0
- package/dist/studio/writer-lock.js +88 -0
- package/package.json +11 -5
- package/dist/cli/copied.d.ts.map +0 -1
- package/dist/cli/copied.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/render.d.ts.map +0 -1
- package/dist/cli/render.js.map +0 -1
- package/dist/core/atomic-config-file.d.ts.map +0 -1
- package/dist/core/atomic-config-file.js.map +0 -1
- package/dist/core/chat.d.ts.map +0 -1
- package/dist/core/chat.js.map +0 -1
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/client.js.map +0 -1
- package/dist/core/config-dir.d.ts.map +0 -1
- package/dist/core/config-dir.js.map +0 -1
- package/dist/core/config-error.d.ts.map +0 -1
- package/dist/core/config-error.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/credit-cap.d.ts.map +0 -1
- package/dist/core/credit-cap.js.map +0 -1
- package/dist/core/download.d.ts.map +0 -1
- package/dist/core/download.js.map +0 -1
- package/dist/core/error-result.d.ts.map +0 -1
- package/dist/core/error-result.js.map +0 -1
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js.map +0 -1
- package/dist/core/generation-input.d.ts.map +0 -1
- package/dist/core/generation-input.js.map +0 -1
- package/dist/core/generation-result.d.ts.map +0 -1
- package/dist/core/generation-result.js.map +0 -1
- package/dist/core/generation.d.ts.map +0 -1
- package/dist/core/generation.js.map +0 -1
- package/dist/core/job-display.d.ts.map +0 -1
- package/dist/core/job-display.js.map +0 -1
- package/dist/core/locale.d.ts.map +0 -1
- package/dist/core/locale.js.map +0 -1
- package/dist/core/origin.d.ts.map +0 -1
- package/dist/core/origin.js.map +0 -1
- package/dist/core/outcome.d.ts.map +0 -1
- package/dist/core/outcome.js.map +0 -1
- package/dist/core/output-path.d.ts.map +0 -1
- package/dist/core/output-path.js.map +0 -1
- package/dist/core/params.d.ts.map +0 -1
- package/dist/core/params.js.map +0 -1
- package/dist/core/poller.d.ts.map +0 -1
- package/dist/core/poller.js.map +0 -1
- package/dist/core/profiles.d.ts.map +0 -1
- package/dist/core/profiles.js.map +0 -1
- package/dist/core/set-values.d.ts.map +0 -1
- package/dist/core/set-values.js.map +0 -1
- package/dist/core/submission-journal.d.ts.map +0 -1
- package/dist/core/submission-journal.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js.map +0 -1
- package/dist/core/wait.d.ts.map +0 -1
- package/dist/core/wait.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcp/render.d.ts.map +0 -1
- package/dist/mcp/render.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- 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
|
package/dist/core/client.js
CHANGED
|
@@ -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.`,
|
|
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) {
|
package/dist/core/config-dir.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -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.',
|
|
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.',
|
|
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.',
|
|
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.`,
|
|
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.',
|
|
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.',
|
|
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
|
package/dist/core/download.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.',
|
|
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
|
package/dist/core/generation.js
CHANGED
|
@@ -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'), {
|
|
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.`,
|
|
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.`,
|
|
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.',
|
|
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}`,
|
|
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.`,
|
|
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')}.`,
|
|
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.',
|
|
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.`,
|
|
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,
|
package/dist/core/job-display.js
CHANGED
|
@@ -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}`,
|
|
6
|
-
ui(`Model: ${detail.modelId ?? 'not reported'}`,
|
|
7
|
-
ui(`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}`,
|
|
11
|
+
lines.push(ui(`Credits: ${detail.credits}`, (zhText("m46") + (detail.credits) + "")));
|
|
11
12
|
if (detail.updatedAt)
|
|
12
|
-
lines.push(ui(`Updated: ${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}`,
|
|
16
|
+
lines.push(ui(`Result: ${output.url}`, (zhText("m44") + (output.url) + "")));
|
|
16
17
|
}
|
|
17
18
|
return lines.join('\n');
|
|
18
19
|
}
|