@lumenkey/cli 0.2.3 → 0.3.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 +43 -7
- package/dist/cli/copied.d.ts +19 -36
- package/dist/cli/copied.d.ts.map +1 -1
- package/dist/cli/copied.js +159 -283
- package/dist/cli/copied.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +86 -136
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/render.d.ts +11 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +38 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/core/atomic-config-file.d.ts +21 -2
- package/dist/core/atomic-config-file.d.ts.map +1 -1
- package/dist/core/atomic-config-file.js +66 -10
- package/dist/core/atomic-config-file.js.map +1 -1
- package/dist/core/chat.d.ts +13 -0
- package/dist/core/chat.d.ts.map +1 -0
- package/dist/core/chat.js +31 -0
- package/dist/core/chat.js.map +1 -0
- package/dist/core/client.d.ts +19 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +50 -11
- package/dist/core/client.js.map +1 -1
- package/dist/core/config-dir.d.ts.map +1 -1
- package/dist/core/config-dir.js +3 -4
- package/dist/core/config-dir.js.map +1 -1
- package/dist/core/config-error.d.ts +14 -0
- package/dist/core/config-error.d.ts.map +1 -0
- package/dist/core/config-error.js +20 -0
- package/dist/core/config-error.js.map +1 -0
- package/dist/core/config.d.ts +1 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -39
- package/dist/core/config.js.map +1 -1
- package/dist/core/credit-cap.d.ts +5 -0
- package/dist/core/credit-cap.d.ts.map +1 -0
- package/dist/core/credit-cap.js +15 -0
- package/dist/core/credit-cap.js.map +1 -0
- package/dist/core/download.d.ts.map +1 -1
- package/dist/core/download.js +105 -16
- package/dist/core/download.js.map +1 -1
- package/dist/core/error-result.d.ts +4 -13
- package/dist/core/error-result.d.ts.map +1 -1
- package/dist/core/error-result.js +4 -24
- package/dist/core/error-result.js.map +1 -1
- package/dist/core/generation-input.d.ts +33 -0
- package/dist/core/generation-input.d.ts.map +1 -0
- package/dist/core/generation-input.js +145 -0
- package/dist/core/generation-input.js.map +1 -0
- package/dist/core/generation-result.d.ts +17 -14
- package/dist/core/generation-result.d.ts.map +1 -1
- package/dist/core/generation-result.js +3 -4
- package/dist/core/generation-result.js.map +1 -1
- package/dist/core/generation.d.ts +124 -0
- package/dist/core/generation.d.ts.map +1 -0
- package/dist/core/generation.js +316 -0
- package/dist/core/generation.js.map +1 -0
- package/dist/core/locale.d.ts +21 -0
- package/dist/core/locale.d.ts.map +1 -1
- package/dist/core/locale.js +84 -4
- package/dist/core/locale.js.map +1 -1
- package/dist/core/outcome.d.ts +134 -0
- package/dist/core/outcome.d.ts.map +1 -0
- package/dist/core/outcome.js +112 -0
- package/dist/core/outcome.js.map +1 -0
- package/dist/core/output-path.d.ts +10 -0
- package/dist/core/output-path.d.ts.map +1 -0
- package/dist/core/output-path.js +15 -0
- package/dist/core/output-path.js.map +1 -0
- package/dist/core/params.d.ts +68 -0
- package/dist/core/params.d.ts.map +1 -0
- package/dist/core/params.js +227 -0
- package/dist/core/params.js.map +1 -0
- package/dist/core/poller.d.ts +6 -1
- package/dist/core/poller.d.ts.map +1 -1
- package/dist/core/poller.js +17 -8
- package/dist/core/poller.js.map +1 -1
- package/dist/core/profiles.d.ts +4 -2
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +20 -9
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/submission-journal.d.ts +106 -0
- package/dist/core/submission-journal.d.ts.map +1 -0
- package/dist/core/submission-journal.js +340 -0
- package/dist/core/submission-journal.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/render.d.ts +11 -0
- package/dist/mcp/render.d.ts.map +1 -0
- package/dist/mcp/render.js +17 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +95 -337
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -1
package/dist/cli/copied.js
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
1
|
import os from 'node:os';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
3
|
import { describeKey, loadConfig } from '../core/config.js';
|
|
4
4
|
import { assertIdempotencyKey, LumenKeyClient, LumenKeyError, validateModelType } from '../core/client.js';
|
|
5
|
-
import {
|
|
5
|
+
import { errorSentence, jobStatusText, missingModelSentence, ui } from '../core/locale.js';
|
|
6
6
|
import { formatJobDetail } from '../core/job-display.js';
|
|
7
7
|
import { normalizeGatewayOrigin } from '../core/origin.js';
|
|
8
8
|
import { explainModel, findModels } from '../core/explain.js';
|
|
9
|
-
import { applyKeyValues, asPairs, modelParameterError, validateKnownEnums } from '../core/set-values.js';
|
|
10
9
|
import { createProfile, currentProfile, deleteProfile, listProfiles, ProfileMissingError, publicProfile, setNamedProfileFields, setProfile, setProfileFields, unsetProfile } from '../core/profiles.js';
|
|
11
|
-
import { waitForJob } from '../core/wait.js';
|
|
12
|
-
import { assertJobResult, pollJob } from '../core/poller.js';
|
|
13
|
-
import { generationResult } from '../core/generation-result.js';
|
|
14
10
|
import { packageVersion } from '../core/version.js';
|
|
15
|
-
import {
|
|
11
|
+
import { argsFromCac, cacKey, cacOptions, cliSpellings, forSurface, generationArgs, imageParams, resumeArgs, resumeParams, waitArgs } from '../core/params.js';
|
|
12
|
+
import { readJob, resumeSubmission, runGeneration, waitJob } from '../core/generation.js';
|
|
13
|
+
import { renderOutcome, wantsJson } from './render.js';
|
|
14
|
+
export { wantsJson } from './render.js';
|
|
16
15
|
const COMPLETIONS = {
|
|
17
16
|
bash: `_lumenkey() {\n local cur prev\n cur="\${COMP_WORDS[COMP_CWORD]}"\n prev="\${COMP_WORDS[COMP_CWORD-1]}"\n case "$prev" in\n model) COMPREPLY=( $(compgen -W "search get explain" -- "$cur") ) ;;\n image) COMPREPLY=( $(compgen -W "generate" -- "$cur") ) ;;\n tasks) COMPREPLY=( $(compgen -W "progress wait cancel" -- "$cur") ) ;;\n profile) COMPREPLY=( $(compgen -W "list create set unset key host delete" -- "$cur") ) ;;\n auth) COMPREPLY=( $(compgen -W "profile" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "login models video status balance chat mcp model image tasks profile auth doctor completion" -- "$cur") ) ;;\n esac\n}\ncomplete -F _lumenkey lumenkey\n`,
|
|
18
17
|
zsh: `#compdef lumenkey\n_lumenkey() {\n if (( CURRENT == 2 )); then\n compadd login models video status balance chat mcp model image tasks profile auth doctor completion\n elif [[ \${words[2]} == model ]]; then\n compadd search get explain\n elif [[ \${words[2]} == image ]]; then\n compadd generate\n elif [[ \${words[2]} == tasks ]]; then\n compadd progress wait cancel\n fi\n}\ncompdef _lumenkey lumenkey\n`,
|
|
19
18
|
tcsh: `complete lumenkey 'p/1/(login models video status balance chat mcp model image tasks profile auth doctor completion)/' 'n/model/(search get explain)/' 'n/image/(generate)/' 'n/tasks/(progress wait cancel)/'\n`,
|
|
20
19
|
fish: `complete -c lumenkey -n "__fish_use_subcommand" -a "login models video status balance chat mcp model image tasks profile auth doctor completion"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from model" -a "search get explain"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from image" -a "generate"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from tasks" -a "progress wait cancel"\n`,
|
|
21
20
|
};
|
|
22
|
-
export function wantsJson(argv = process.argv) {
|
|
23
|
-
return argv.includes('--json');
|
|
24
|
-
}
|
|
25
21
|
function spellings(name) {
|
|
26
22
|
const camel = name.replace(/([a-z])-([a-z])/g, (_, a, b) => a + b.toUpperCase());
|
|
27
23
|
return camel === name ? [name] : [name, camel];
|
|
@@ -50,6 +46,7 @@ export function applyRaw(options, key, names) {
|
|
|
50
46
|
throw new Error(ui(`${names[0]} could not be read as text. Use ${names[0]} <value>.`, `${names[0]} 无法按文本读取。请使用 ${names[0]} <值>。`));
|
|
51
47
|
}
|
|
52
48
|
}
|
|
49
|
+
/** Plain data commands (catalog, balance, profiles) print their payload; outcomes go through renderOutcome. */
|
|
53
50
|
export function emit(data, human, argv = process.argv) {
|
|
54
51
|
if (wantsJson(argv)) {
|
|
55
52
|
console.log(JSON.stringify(data));
|
|
@@ -60,124 +57,111 @@ export function emit(data, human, argv = process.argv) {
|
|
|
60
57
|
function client() {
|
|
61
58
|
return new LumenKeyClient(loadConfig());
|
|
62
59
|
}
|
|
63
|
-
function propertiesOf(model) {
|
|
64
|
-
const properties = model.inputSchema?.properties;
|
|
65
|
-
if (!properties || typeof properties !== 'object' || Array.isArray(properties))
|
|
66
|
-
throw new LumenKeyError('MODEL_INPUT_SCHEMA_INVALID');
|
|
67
|
-
return properties;
|
|
68
|
-
}
|
|
69
60
|
export function rejectEmptyIdempotencyKey() {
|
|
70
61
|
const rawKey = rawOption(['--idempotency-key']);
|
|
71
62
|
if (rawKey !== undefined)
|
|
72
63
|
assertIdempotencyKey(rawKey);
|
|
73
64
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
/** Install SIGINT/SIGTERM handling around a wait: abort the core operation and keep the conventional exit code. */
|
|
66
|
+
export async function withCliSignals(run) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const interrupt = (exitCode) => { if (!controller.signal.aborted) {
|
|
69
|
+
process.exitCode = exitCode;
|
|
70
|
+
controller.abort();
|
|
71
|
+
} };
|
|
72
|
+
const sigint = () => interrupt(130);
|
|
73
|
+
const sigterm = () => interrupt(143);
|
|
74
|
+
process.on('SIGINT', sigint);
|
|
75
|
+
process.on('SIGTERM', sigterm);
|
|
76
|
+
try {
|
|
77
|
+
return await run(controller.signal);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
process.off('SIGINT', sigint);
|
|
81
|
+
process.off('SIGTERM', sigterm);
|
|
79
82
|
}
|
|
80
|
-
return Number(value);
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
function progressLine(status) {
|
|
85
|
+
process.stderr.write(ui(` Status: ${jobStatusText(status)}\n`, ` 状态:${jobStatusText(status)}\n`));
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
/** CLI view of generation progress. In --json mode the job identity goes to stderr the moment it exists. */
|
|
88
|
+
export function cliGenerationEvents() {
|
|
89
|
+
const json = wantsJson();
|
|
90
|
+
return event => {
|
|
91
|
+
switch (event.type) {
|
|
92
|
+
case 'quoting':
|
|
93
|
+
if (!json && event.kind === 'video')
|
|
94
|
+
console.log(pc.cyan(ui(`\n🎬 Requesting quote for model: ${pc.bold(event.modelId)}...`, `\n🎬 正在为模型 ${pc.bold(event.modelId)} 获取报价...`)));
|
|
95
|
+
return;
|
|
96
|
+
case 'submitting':
|
|
97
|
+
console.error(event.resubmit
|
|
98
|
+
? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}) instead of creating a new job...`, `正在重发一次之前结果未确认的提交(幂等键 ${event.idempotencyKey},报价 ${event.quoteId}),不会新建任务...`)
|
|
99
|
+
: ui('Submitting generation job...', '正在提交生成任务...'));
|
|
100
|
+
return;
|
|
101
|
+
case 'job_created':
|
|
102
|
+
if (json)
|
|
103
|
+
console.error(JSON.stringify({ event: 'job_created', jobId: event.jobId, idempotencyKey: event.idempotencyKey, quoteId: event.quoteId }));
|
|
104
|
+
else
|
|
105
|
+
console.log(ui(`Job created: ${event.jobId} (idempotency key: ${event.idempotencyKey})`, `任务已创建:${event.jobId}(幂等键:${event.idempotencyKey})`));
|
|
106
|
+
return;
|
|
107
|
+
case 'progress':
|
|
108
|
+
progressLine(event.detail.status);
|
|
109
|
+
return;
|
|
110
|
+
case 'notice':
|
|
111
|
+
console.error(ui(`Note: ${event.text}`, `注意:${event.text}`));
|
|
112
|
+
return;
|
|
113
|
+
default:
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
95
117
|
}
|
|
96
|
-
export
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const maxCredits = parseMaxCredits(options.maxCredits);
|
|
100
|
-
rejectEmptyIdempotencyKey();
|
|
101
|
-
const rawKey = rawOption(['--idempotency-key']);
|
|
102
|
-
if (rawKey !== undefined)
|
|
103
|
-
options.idempotencyKey = rawKey;
|
|
104
|
-
const schemaProperties = options.schemaProperties;
|
|
105
|
-
if (schemaProperties && typeof schemaProperties === 'object' && !Array.isArray(schemaProperties))
|
|
106
|
-
validateKnownEnums(schemaProperties, input);
|
|
107
|
-
const quote = await api.getQuote({ modelId, input });
|
|
108
|
-
if (maxCredits !== undefined && quote.credits > maxCredits)
|
|
109
|
-
throw quoteCreditLimitError();
|
|
110
|
-
if (!options.confirm) {
|
|
111
|
-
emit({ ...quote, submitted: false }, () => {
|
|
112
|
-
console.log(ui(`Quote ID: ${quote.quoteId} (${quote.credits} credits)`, `报价编号:${quote.quoteId}(${quote.credits} 算力点)`));
|
|
113
|
-
console.log(ui('Quote only. No job submitted.', '只完成报价,没有提交任务。'));
|
|
114
|
-
});
|
|
118
|
+
export function printGeneration(result) {
|
|
119
|
+
if ('dryRun' in result) {
|
|
120
|
+
console.log(JSON.stringify(result));
|
|
115
121
|
return;
|
|
116
122
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
assertIdempotencyKey(options.idempotencyKey);
|
|
121
|
-
}
|
|
122
|
-
const key = options.idempotencyKey === undefined ? crypto.randomUUID() : String(options.idempotencyKey);
|
|
123
|
-
console.error(ui('Submitting generation job...', '正在提交生成任务...'));
|
|
124
|
-
const projectId = options.projectId == null || options.projectId === '' ? undefined : String(options.projectId);
|
|
125
|
-
const job = await api.createJob({ quoteId: quote.quoteId, projectId, input }, key);
|
|
126
|
-
if (!options.suppressEmit)
|
|
127
|
-
emit({ ...job, idempotencyKey: key, submitted: true }, () => console.log(ui(`Job created: ${job.jobId}`, `任务已创建:${job.jobId}`)));
|
|
128
|
-
return { jobId: job.jobId, idempotencyKey: key };
|
|
129
|
-
}
|
|
130
|
-
function hasDeclared(properties, key) {
|
|
131
|
-
return Object.prototype.hasOwnProperty.call(properties, key);
|
|
132
|
-
}
|
|
133
|
-
function assignAspect(properties, input, fields) {
|
|
134
|
-
const aspect = fields.aspect_ratio;
|
|
135
|
-
const ratio = fields.ratio;
|
|
136
|
-
if (aspect === undefined && ratio === undefined)
|
|
123
|
+
if ('requiresConfirmation' in result) {
|
|
124
|
+
console.log(ui(`Quote ID: ${result.quoteId} (${result.credits} credits)`, `报价编号:${result.quoteId}(${result.credits} 算力点)`));
|
|
125
|
+
console.log(ui('Quote only. No job submitted.', '只完成报价,没有提交任务。'));
|
|
137
126
|
return;
|
|
138
|
-
const hasAspect = hasDeclared(properties, 'aspect_ratio');
|
|
139
|
-
const hasRatio = hasDeclared(properties, 'ratio');
|
|
140
|
-
if (!hasAspect && !hasRatio)
|
|
141
|
-
throw modelParameterError('MODEL_PARAM_UNKNOWN', 'aspect_ratio');
|
|
142
|
-
if (hasAspect && aspect !== undefined)
|
|
143
|
-
input.aspect_ratio = coerceDeclared(properties.aspect_ratio, aspect, 'aspect_ratio');
|
|
144
|
-
if (hasRatio && ratio !== undefined)
|
|
145
|
-
input.ratio = coerceDeclared(properties.ratio, ratio, 'ratio');
|
|
146
|
-
if (input.aspect_ratio === undefined && input.ratio === undefined) {
|
|
147
|
-
const name = hasAspect ? 'aspect_ratio' : 'ratio';
|
|
148
|
-
const value = aspect !== undefined ? aspect : ratio;
|
|
149
|
-
input[name] = coerceDeclared(properties[name], value, name);
|
|
150
127
|
}
|
|
128
|
+
const done = result;
|
|
129
|
+
console.log(ui('Generation succeeded!', '生成成功!'));
|
|
130
|
+
console.log(ui(`Task: ${done.jobId}`, `任务:${done.jobId}`));
|
|
131
|
+
for (const url of done.urls)
|
|
132
|
+
console.log(url);
|
|
133
|
+
if (done.localFile)
|
|
134
|
+
console.log(ui(`Saved to ${done.localFile}`, `已保存到 ${done.localFile}`));
|
|
135
|
+
if (done.download?.ok === false)
|
|
136
|
+
console.error(ui(`Local save failed: ${done.download.error} (${done.download.code}). The generation result remains available above.`, `本机保存失败:${done.download.error}(${done.download.code})。生成结果仍可通过以上地址获取。`));
|
|
151
137
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const spec
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const properties = propertiesOf(model);
|
|
168
|
-
const defined = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
169
|
-
const input = {};
|
|
170
|
-
assignAspect(properties, input, defined);
|
|
171
|
-
for (const [key, value] of Object.entries(defined)) {
|
|
172
|
-
if (key === 'aspect_ratio' || key === 'ratio')
|
|
173
|
-
continue;
|
|
174
|
-
assignDeclared(properties, input, key, value);
|
|
138
|
+
/** Shared by `video` and `image generate`: options come from the core spec, values go through the core normalizer, one use case, one renderer. */
|
|
139
|
+
export async function runCliGeneration(kind, spec, options) {
|
|
140
|
+
// Re-read string options from raw argv so CAC's numeric coercion never changes a value such as a prompt of "007".
|
|
141
|
+
for (const param of forSurface(spec, 'cli'))
|
|
142
|
+
if (param.flag && param.kind === 'string')
|
|
143
|
+
applyRaw(options, cacKey(param.flag), cliSpellings(param));
|
|
144
|
+
rejectEmptyIdempotencyKey();
|
|
145
|
+
const request = generationArgs(kind, argsFromCac(spec, options), 'cli');
|
|
146
|
+
// CAC folds --aspect-ratio and --ratio together. Retain the supplied field spelling so the
|
|
147
|
+
// shared core can reject two different aliases (including an alias supplied through --set).
|
|
148
|
+
const aspect = rawOption(['--aspect-ratio']);
|
|
149
|
+
if (aspect !== undefined) {
|
|
150
|
+
request.fields.aspect_ratio = aspect;
|
|
151
|
+
if (rawOption(['--ratio']) === undefined)
|
|
152
|
+
delete request.fields.ratio;
|
|
175
153
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
154
|
+
const api = client();
|
|
155
|
+
const outcome = await withCliSignals(signal => runGeneration(api, { ...request, signal, onEvent: cliGenerationEvents() }));
|
|
156
|
+
renderOutcome(outcome, printGeneration);
|
|
157
|
+
if (outcome.ok && 'download' in outcome && outcome.download?.ok === false && process.exitCode !== 130 && process.exitCode !== 143)
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
}
|
|
160
|
+
/** Register every CLI option of a spec on a command. */
|
|
161
|
+
export function registerOptions(command, spec) {
|
|
162
|
+
for (const [flags, description, config] of cacOptions(spec))
|
|
163
|
+
command = command.option(flags, description, config);
|
|
164
|
+
return command;
|
|
181
165
|
}
|
|
182
166
|
function printProfiles(listed) {
|
|
183
167
|
if (!listed.profiles.length) {
|
|
@@ -241,84 +225,55 @@ export function registerCopied(cli) {
|
|
|
241
225
|
}
|
|
242
226
|
throw new Error(ui(`Unknown model command: ${action}`, `未知的 model 子命令:${action}`));
|
|
243
227
|
});
|
|
244
|
-
cli.command('image <action>', ui('image generate', '生成图片'))
|
|
228
|
+
registerOptions(cli.command('image <action>', ui('image generate', '生成图片'))
|
|
245
229
|
.example('lumenkey image generate -m wan2.7-image -p "red circle"')
|
|
246
|
-
.example('lumenkey image generate -m wan2.7-image -p "red circle" --set seed=2 --dry-run')
|
|
247
|
-
.option('-m, --model <modelId>', ui('Model id', '模型 ID'))
|
|
248
|
-
.option('-p, --prompt <prompt>', ui('Prompt', '提示词'))
|
|
249
|
-
.option('--resolution <res>', ui('Resolution, only if the catalog declares it', '分辨率,仅当目录声明了该字段'))
|
|
250
|
-
.option('--aspect-ratio <ratio>', ui('Aspect ratio, only if the catalog declares it', '宽高比,仅当目录声明了该字段'))
|
|
251
|
-
.option('--count <count>', ui('Count, only if the catalog declares it', '数量,仅当目录声明了该字段'))
|
|
252
|
-
.option('--image <image>', ui('Reference image, only if the catalog declares it', '参考图,仅当目录声明了该字段'))
|
|
253
|
-
.option('-s, --set <pair>', ui('Extra key=value. Repeatable', '额外的键=值,可重复'))
|
|
254
|
-
.option('--dry-run', ui('Print the projected input and do not quote', '只打印将提交的输入,不报价'))
|
|
255
|
-
.option('--confirm', ui('Submit this quote', '提交这次报价'))
|
|
256
|
-
.option('--idempotency-key <key>', ui('Stable operation key', '稳定的幂等键'))
|
|
257
|
-
.option('--max-credits <credits>', ui('Reject quotes above this limit', '报价超过该上限时拒绝'))
|
|
258
|
-
.option('-o, --output <outputPath>', ui('Save the first result to this path. Omit it to print the URLs only', '把第一个结果保存到该路径;省略则只打印地址'))
|
|
259
|
-
.option('--interval <seconds>', ui('Poll interval seconds after confirm', '确认后的轮询间隔(秒)'), { default: 5 })
|
|
260
|
-
.option('--timeout <seconds>', ui('Poll timeout seconds after confirm', '确认后的轮询超时(秒)'), { default: 1800 })
|
|
230
|
+
.example('lumenkey image generate -m wan2.7-image -p "red circle" --set seed=2 --dry-run'), imageParams)
|
|
261
231
|
.action(async (action, options) => {
|
|
262
232
|
if (action !== 'generate')
|
|
263
233
|
throw new Error(ui(`Unknown image command: ${action}`, `未知的 image 子命令:${action}`));
|
|
264
|
-
|
|
265
|
-
applyRaw(options, 'prompt', ['--prompt', '-p']);
|
|
266
|
-
applyRaw(options, 'resolution', ['--resolution']);
|
|
267
|
-
applyRaw(options, 'aspectRatio', ['--aspect-ratio']);
|
|
268
|
-
applyRaw(options, 'count', ['--count']);
|
|
269
|
-
applyRaw(options, 'image', ['--image']);
|
|
270
|
-
applyRaw(options, 'idempotencyKey', ['--idempotency-key']);
|
|
271
|
-
applyRaw(options, 'output', ['--output', '-o']);
|
|
272
|
-
if (!options.model)
|
|
273
|
-
throw new LumenKeyError('MODEL_REQUIRED');
|
|
274
|
-
if (isBlankPrompt(options.prompt))
|
|
275
|
-
throw new Error(blankPromptSentence('flag'));
|
|
276
|
-
parsePollOptions(options, true);
|
|
277
|
-
client().requireKey();
|
|
278
|
-
parseMaxCredits(options.maxCredits);
|
|
279
|
-
rejectEmptyIdempotencyKey();
|
|
280
|
-
const model = (await client().listModels()).find((item) => item.id === options.model);
|
|
281
|
-
if (!model)
|
|
282
|
-
throw new LumenKeyError('MODEL_NOT_IN_CATALOG');
|
|
283
|
-
const built = await buildGenerationInput(String(options.model), {
|
|
284
|
-
prompt: String(options.prompt),
|
|
285
|
-
resolution: options.resolution,
|
|
286
|
-
aspect_ratio: options.aspectRatio,
|
|
287
|
-
ratio: options.aspectRatio,
|
|
288
|
-
count: options.count,
|
|
289
|
-
image: options.image,
|
|
290
|
-
}, asPairs(options.set), model);
|
|
291
|
-
if (options.dryRun) {
|
|
292
|
-
const preview = dryRunPreview(String(options.model), built.input, model, options.confirm);
|
|
293
|
-
emit(preview, () => console.log(JSON.stringify(preview)));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const created = await submitQuote(String(options.model), built.input, { ...options, schemaProperties: built.properties, suppressEmit: wantsJson() && Boolean(options.confirm) });
|
|
297
|
-
if (!created)
|
|
298
|
-
return;
|
|
299
|
-
await finishGeneration(client(), created, options);
|
|
234
|
+
await runCliGeneration('image', imageParams, options);
|
|
300
235
|
});
|
|
301
|
-
cli.command('tasks <action>
|
|
236
|
+
registerOptions(cli.command('tasks <action> [jobId]', ui('tasks progress | wait | cancel | resume', '查看进度、等待、取消任务,或重发一次记录的提交'))
|
|
302
237
|
.example('lumenkey tasks progress <jobId>')
|
|
303
238
|
.example('lumenkey tasks wait <jobId> --interval 5 --timeout 1800')
|
|
304
239
|
.example('lumenkey tasks cancel <jobId> --yes')
|
|
305
|
-
.
|
|
306
|
-
.
|
|
240
|
+
.example('lumenkey tasks resume <idempotencyKey>')
|
|
241
|
+
.example('lumenkey tasks resume --key=<idempotencyKey> (for a key that begins with -)'), resumeParams)
|
|
307
242
|
.option('--yes', ui('Confirm cancellation', '确认取消'))
|
|
308
|
-
.
|
|
243
|
+
.option('--key <idempotencyKey>', ui('The idempotency key for tasks resume; use --key=<key> or -- <key> when the key begins with -', 'tasks resume 的幂等键;键以 - 开头时用 --key=<键> 或 -- <键>'))
|
|
244
|
+
.action(async (action, positional, options) => {
|
|
245
|
+
// Each subcommand owns its options; any other option is as unknown here as on the MCP surface.
|
|
246
|
+
rejectForeignTaskOptions(action);
|
|
247
|
+
applyRaw(options, 'key', ['--key']);
|
|
248
|
+
const jobId = positional ?? firstDoubleDashArgument(options) ?? (typeof options.key === 'string' ? options.key : undefined);
|
|
249
|
+
if (jobId === undefined)
|
|
250
|
+
throw new UsageArgumentError(ui(`missing required args for command \`tasks ${action}\``, `命令 \`tasks ${action}\` 缺少必填参数。`));
|
|
309
251
|
if (action === 'progress') {
|
|
310
|
-
|
|
311
|
-
emitJobDetail(detail);
|
|
252
|
+
renderOutcome(await readJob(client(), jobId), ok => console.log(formatJobDetail(ok.job)));
|
|
312
253
|
return;
|
|
313
254
|
}
|
|
314
255
|
if (action === 'wait') {
|
|
315
|
-
const timing =
|
|
316
|
-
const
|
|
317
|
-
|
|
256
|
+
const timing = waitArgs({ intervalMs: options.interval, timeoutMs: options.timeout }, 'cli');
|
|
257
|
+
const api = client();
|
|
258
|
+
const outcome = await withCliSignals(signal => waitJob(api, jobId, { ...timing, signal }));
|
|
259
|
+
renderOutcome(outcome, ok => console.log(formatJobDetail(ok.job)));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (action === 'resume') {
|
|
263
|
+
// Re-send a recorded submission verbatim: no model, prompt or catalog involved; the positional is the idempotency key.
|
|
264
|
+
for (const param of forSurface(resumeParams, 'cli'))
|
|
265
|
+
if (param.flag && param.kind === 'string')
|
|
266
|
+
applyRaw(options, cacKey(param.flag), cliSpellings(param));
|
|
267
|
+
const request = resumeArgs({ ...argsFromCac(resumeParams, options), idempotencyKey: jobId }, 'cli');
|
|
268
|
+
const api = client();
|
|
269
|
+
const outcome = await withCliSignals(signal => resumeSubmission(api, { ...request, signal, onEvent: cliGenerationEvents() }));
|
|
270
|
+
renderOutcome(outcome, printGeneration);
|
|
271
|
+
if (outcome.ok && outcome.download?.ok === false && process.exitCode !== 130 && process.exitCode !== 143)
|
|
272
|
+
process.exitCode = 1;
|
|
318
273
|
return;
|
|
319
274
|
}
|
|
320
275
|
if (action === 'cancel') {
|
|
321
|
-
if (
|
|
276
|
+
if (options.yes !== true)
|
|
322
277
|
throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
|
|
323
278
|
const result = await client().cancelJob(jobId);
|
|
324
279
|
emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, `已请求取消任务 ${jobId},最终结算由网关确认。`)));
|
|
@@ -512,6 +467,35 @@ export function registerCopied(cli) {
|
|
|
512
467
|
console.log(script);
|
|
513
468
|
});
|
|
514
469
|
}
|
|
470
|
+
/** The options each tasks subcommand owns, by CLI spelling; everything else present on the command line is an unknown option for it. */
|
|
471
|
+
const TASK_OPTIONS = {
|
|
472
|
+
progress: [],
|
|
473
|
+
wait: ['--interval', '--timeout'],
|
|
474
|
+
cancel: ['--yes'],
|
|
475
|
+
resume: ['--output', '-o', '--interval', '--timeout', '--key'],
|
|
476
|
+
};
|
|
477
|
+
function rejectForeignTaskOptions(action) {
|
|
478
|
+
const owned = new Set(TASK_OPTIONS[action] ?? []);
|
|
479
|
+
const all = ['--interval', '--timeout', '--output', '-o', '--yes', '--key'];
|
|
480
|
+
for (let i = 2; i < process.argv.length; i += 1) {
|
|
481
|
+
const arg = process.argv[i];
|
|
482
|
+
if (arg === '--')
|
|
483
|
+
break;
|
|
484
|
+
const name = arg.split('=')[0];
|
|
485
|
+
if (all.includes(name) && !owned.has(name)) {
|
|
486
|
+
throw new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown option ${JSON.stringify(name)} for tasks ${action}.`, `tasks ${action} 没有选项 ${JSON.stringify(name)}。`));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** CAC collects everything after `--` here; a key that begins with `-` can be passed that way. */
|
|
491
|
+
function firstDoubleDashArgument(options) {
|
|
492
|
+
const rest = options['--'];
|
|
493
|
+
return Array.isArray(rest) && typeof rest[0] === 'string' ? rest[0] : undefined;
|
|
494
|
+
}
|
|
495
|
+
/** A missing positional, phrased like CAC's own message so existing readers of it keep working. */
|
|
496
|
+
class UsageArgumentError extends LumenKeyError {
|
|
497
|
+
constructor(line) { super('CLI_ARGUMENTS_INVALID', undefined, undefined, line); this.name = 'UsageError'; this.message = line; }
|
|
498
|
+
}
|
|
515
499
|
function hostStatus(config) {
|
|
516
500
|
try {
|
|
517
501
|
new LumenKeyClient({ baseUrl: config.baseUrl, timeoutMs: config.timeoutMs, apiKey: config.apiKey });
|
|
@@ -521,114 +505,6 @@ function hostStatus(config) {
|
|
|
521
505
|
return 'fail';
|
|
522
506
|
}
|
|
523
507
|
}
|
|
524
|
-
function coerceDeclared(spec, value, key) {
|
|
525
|
-
const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
|
|
526
|
-
if (type === 'integer' || type === 'number') {
|
|
527
|
-
const parsed = Number(value);
|
|
528
|
-
if (!Number.isFinite(parsed))
|
|
529
|
-
throw modelParameterError('MODEL_PARAM_INVALID', key);
|
|
530
|
-
return type === 'integer' ? Math.trunc(parsed) : parsed;
|
|
531
|
-
}
|
|
532
|
-
if (type === 'string')
|
|
533
|
-
return String(value);
|
|
534
|
-
return value;
|
|
535
|
-
}
|
|
536
|
-
export function outputUrls(detail) {
|
|
537
|
-
return (detail.outputs ?? []).map((item) => item.url).filter((url) => typeof url === 'string' && url.length > 0);
|
|
538
|
-
}
|
|
539
|
-
export function parsePollOptions(options, warnShort = false) {
|
|
540
|
-
const interval = Number(options.interval ?? 5);
|
|
541
|
-
const timeout = Number(options.timeout ?? 1800);
|
|
542
|
-
if (!Number.isFinite(interval) || interval <= 0 || interval * 1000 > 2_147_483_647 || !Number.isFinite(timeout) || timeout < 0 || timeout * 1000 > Number.MAX_SAFE_INTEGER - Date.now()) {
|
|
543
|
-
throw new LumenKeyError('WAIT_OPTIONS_INVALID');
|
|
544
|
-
}
|
|
545
|
-
if (warnShort && interval < 1)
|
|
546
|
-
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'));
|
|
547
|
-
return { initialIntervalMs: interval * 1000, timeoutMs: timeout * 1000 };
|
|
548
|
-
}
|
|
549
|
-
export async function pollConfirmed(api, jobId, options) {
|
|
550
|
-
const timing = parsePollOptions(options);
|
|
551
|
-
return withCliWait(jobId, signal => pollJob(api, jobId, {
|
|
552
|
-
...timing, signal,
|
|
553
|
-
onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
|
|
554
|
-
}));
|
|
555
|
-
}
|
|
556
|
-
async function withCliWait(jobId, wait) {
|
|
557
|
-
const controller = new AbortController();
|
|
558
|
-
const interrupt = (exitCode) => { if (!controller.signal.aborted) {
|
|
559
|
-
process.exitCode = exitCode;
|
|
560
|
-
controller.abort();
|
|
561
|
-
} };
|
|
562
|
-
const sigint = () => interrupt(130);
|
|
563
|
-
const sigterm = () => interrupt(143);
|
|
564
|
-
process.on('SIGINT', sigint);
|
|
565
|
-
process.on('SIGTERM', sigterm);
|
|
566
|
-
try {
|
|
567
|
-
return await wait(controller.signal);
|
|
568
|
-
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
|
|
571
|
-
Object.assign(failure, { jobId });
|
|
572
|
-
if (failure instanceof LumenKeyError && failure.code === 'WAIT_TIMEOUT')
|
|
573
|
-
Object.assign(failure, { timedOut: true });
|
|
574
|
-
if (controller.signal.aborted)
|
|
575
|
-
failure.message += ui(` Resume with: lumenkey status ${jobId}`, ` 可继续查询:lumenkey status ${jobId}`);
|
|
576
|
-
throw failure;
|
|
577
|
-
}
|
|
578
|
-
finally {
|
|
579
|
-
process.off('SIGINT', sigint);
|
|
580
|
-
process.off('SIGTERM', sigterm);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
export async function finishGeneration(api, created, options) {
|
|
584
|
-
try {
|
|
585
|
-
const timing = parsePollOptions(options);
|
|
586
|
-
const result = await withCliWait(created.jobId, async (signal) => {
|
|
587
|
-
const detail = await pollJob(api, created.jobId, { ...timing, signal,
|
|
588
|
-
onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
|
|
589
|
-
});
|
|
590
|
-
return generationResult(detail, created.idempotencyKey, typeof options.output === 'string' ? options.output : undefined, signal);
|
|
591
|
-
});
|
|
592
|
-
emit(result, () => {
|
|
593
|
-
console.log(ui('Generation succeeded!', '生成成功!'));
|
|
594
|
-
console.log(ui(`Task: ${result.jobId}`, `任务:${result.jobId}`));
|
|
595
|
-
for (const url of result.urls)
|
|
596
|
-
console.log(url);
|
|
597
|
-
if (result.localFile)
|
|
598
|
-
console.log(ui(`Saved to ${result.localFile}`, `已保存到 ${result.localFile}`));
|
|
599
|
-
if (result.download?.ok === false)
|
|
600
|
-
console.error(ui(`Local save failed: ${result.download.error} (${result.download.code}). The generation result remains available above.`, `本机保存失败:${result.download.error}(${result.download.code})。生成结果仍可通过以上地址获取。`));
|
|
601
|
-
});
|
|
602
|
-
if (result.download?.ok === false && process.exitCode !== 130 && process.exitCode !== 143)
|
|
603
|
-
process.exitCode = 1;
|
|
604
|
-
}
|
|
605
|
-
catch (error) {
|
|
606
|
-
const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
|
|
607
|
-
Object.assign(failure, created);
|
|
608
|
-
throw failure;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
export function emitJobDetail(detail) {
|
|
612
|
-
let failure;
|
|
613
|
-
try {
|
|
614
|
-
assertJobResult(detail, detail.status === 'succeeded');
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
failure = error instanceof Error ? error : new LumenKeyError('JOB_TERMINAL_FAILURE');
|
|
618
|
-
}
|
|
619
|
-
const failureResult = failure ? errorResult(failure) : undefined;
|
|
620
|
-
const result = { ...detail, jobStatus: detail.status, ...(failureResult ? {
|
|
621
|
-
ok: false, code: failureResult.code, message: failureResult.message,
|
|
622
|
-
failureCode: failureResult.code, failureMessage: failureResult.message,
|
|
623
|
-
} : {}) };
|
|
624
|
-
emit(result, () => {
|
|
625
|
-
console.log(formatJobDetail(detail));
|
|
626
|
-
if (failure)
|
|
627
|
-
console.error(failure.message);
|
|
628
|
-
});
|
|
629
|
-
if (failure)
|
|
630
|
-
process.exitCode = 1;
|
|
631
|
-
}
|
|
632
508
|
export async function promptForKey() {
|
|
633
509
|
if (process.stdin.readableEnded || process.stdin.destroyed)
|
|
634
510
|
return undefined;
|