@lumenkey/cli 0.2.1 → 0.2.3
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 +21 -3
- package/dist/cli/copied.d.ts +7 -2
- package/dist/cli/copied.d.ts.map +1 -1
- package/dist/cli/copied.js +131 -79
- package/dist/cli/copied.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +25 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/core/atomic-config-file.d.ts +8 -0
- package/dist/core/atomic-config-file.d.ts.map +1 -0
- package/dist/core/atomic-config-file.js +63 -0
- package/dist/core/atomic-config-file.js.map +1 -0
- package/dist/core/client.d.ts +10 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +20 -4
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +2 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/download.d.ts +3 -0
- package/dist/core/download.d.ts.map +1 -0
- package/dist/core/download.js +107 -0
- package/dist/core/download.js.map +1 -0
- package/dist/core/error-result.d.ts +14 -0
- package/dist/core/error-result.d.ts.map +1 -0
- package/dist/core/error-result.js +27 -0
- package/dist/core/error-result.js.map +1 -0
- package/dist/core/generation-result.d.ts +19 -0
- package/dist/core/generation-result.d.ts.map +1 -0
- package/dist/core/generation-result.js +22 -0
- package/dist/core/generation-result.js.map +1 -0
- package/dist/core/locale.d.ts.map +1 -1
- package/dist/core/locale.js +34 -0
- package/dist/core/locale.js.map +1 -1
- package/dist/core/poller.d.ts +15 -5
- package/dist/core/poller.d.ts.map +1 -1
- package/dist/core/poller.js +45 -57
- package/dist/core/poller.js.map +1 -1
- package/dist/core/profiles.d.ts +5 -1
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +59 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/set-values.d.ts.map +1 -1
- package/dist/core/set-values.js +6 -1
- package/dist/core/set-values.js.map +1 -1
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +3 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/wait.d.ts +1 -0
- package/dist/core/wait.d.ts.map +1 -1
- package/dist/core/wait.js +2 -40
- package/dist/core/wait.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +50 -55
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ export LUMENKEY_API_KEY="lk_live_xxxxxxxx"
|
|
|
31
31
|
npx @lumenkey/cli login --key lk_live_xxxxxxxx
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
也可运行 `lumenkey login` 交互输入(不回显),或从标准输入传入一行密钥;不要把真实密钥写进命令历史。`login` 只保存显式输入的字段,不把本次进程的网关、超时等环境变量固化到配置。损坏的 `config.json` / `profiles.json` 会报错并保留原文件,请先备份再修正;不会当成空档案覆盖。POSIX 上保存文件权限为 0600,Windows 仍应由用户目录 ACL 保护。
|
|
35
|
+
|
|
36
|
+
配置必须使用独立的普通文件/目录。`config.json` 的符号链接、硬链接或 `.lumenkey` 链接目录会被拒绝并返回 `CONFIG_UNSAFE_PATH`,不是目录不可写;不会修改链接目标。请先备份,再使用普通的私有文件/目录,不要放宽权限绕过保护。
|
|
37
|
+
|
|
34
38
|
---
|
|
35
39
|
|
|
36
40
|
## 💻 命令行模式 (CLI Usage)
|
|
@@ -70,7 +74,19 @@ npx @lumenkey/cli status <jobId>
|
|
|
70
74
|
npx @lumenkey/cli --json status <jobId>
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
`status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json
|
|
77
|
+
`status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json`。失败任务仍返回原始任务详情和 `error.code`,同时以退出码 1 结束。网关请求失败等命令错误统一返回 `error`、`code`、`type`,有 HTTP 状态时另带 `httpStatus`。任务错误码可能为小写(例如 `provider_error`),请原样处理。
|
|
78
|
+
|
|
79
|
+
从 0.2.3 起,CLI `--json` 失败输出统一提供 `ok:false`、`code`、`message`;任务失败另带实际 `jobStatus`。`message` 是便于展示的原因,`code` 供脚本判断;旧 `error`、`status`、`failureCode`、`failureMessage` 字段保留。等待超时使用 `waitStatus:"TIMEOUT"`,兼容旧 `status:"TIMEOUT"`,不凭空推断任务失败。图片/视频非法 `--max-credits` 返回 `CREDIT_LIMIT_INVALID`,聊天保持 `CHAT_CREDIT_LIMIT_INVALID`。MCP 工具既有文本/任务结果结构不因此更换。
|
|
80
|
+
|
|
81
|
+
图片、视频、`tasks wait` 共用轮询逻辑,默认间隔 5 秒、等待 1800 秒。间隔必须大于 0,指定 25 秒不会被缩短到 10 秒。等待时长是两次请求之间检查的预算,不会中断已发出的单次查询;单次网关请求另受 `LUMENKEY_TIMEOUT_MS` 控制。超时带 `WAIT_TIMEOUT`、`timedOut: true` 和任务号;不取消任务、不代表退款。
|
|
82
|
+
|
|
83
|
+
CLI 的 `--interval` / `--timeout` 单位都是**秒**,MCP 的 `intervalMs` / `timeoutMs` 则是**毫秒**。CLI 小于 1 秒的正数间隔仍可使用,但 stderr 会提示 `POLL_INTERVAL_SHORT`;例如 `--interval 0.001` 是 1 毫秒,不是 1 秒。建议使用默认 5 秒;警告不会混入 JSON stdout。
|
|
84
|
+
|
|
85
|
+
Ctrl+C / SIGTERM 只停止本机等待或下载,不调用取消接口;分别以 130 / 143 退出并保留恢复查询信息。取消任务必须显式运行 `tasks cancel <jobId> --yes`(MCP 使用 `yes: true`)。
|
|
86
|
+
|
|
87
|
+
`--output` 下载不覆盖已有文件,单次限 120 秒、128 MiB。生成成功但保存失败时,仍返回 `status: "succeeded"`、`jobId`、`idempotencyKey` 和全部 `urls`,另带 `download: {ok:false,code,error}`;CLI 以 1 退出表示本机保存失败,MCP 保留生成成功和下载警告。请用结果地址另存,不要为下载失败重新提交付费生成。下载中断时也保留这些结果。
|
|
88
|
+
|
|
89
|
+
`--output` 必须是包含文件名的路径;目标为目录时返回 `DOWNLOAD_IS_DIRECTORY`,已有普通文件则返回 `DOWNLOAD_EXISTS`,不会覆盖或删除它们。
|
|
74
90
|
|
|
75
91
|
### 4. 查看账户算力余额
|
|
76
92
|
```bash
|
|
@@ -81,7 +97,7 @@ npx @lumenkey/cli balance
|
|
|
81
97
|
|
|
82
98
|
`LUMENKEY_BASE_URL`、config.json 的 baseUrl、当前档案 host 按顺序选择,最后才使用默认网关。所选地址会裁掉首尾空白并移除一个尾部 `/v1`;显式设置空字符串会报错,不会悄悄切回默认站点。`doctor` 默认只做本地检查,`--online` 才读取网关余额。
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
网关请求和产物下载共用 HTTP(S) 代理规则:`https_proxy`/`HTTPS_PROXY`、`http_proxy`/`HTTP_PROXY`、`all_proxy`/`ALL_PROXY`。HTTPS 优先专属 HTTPS 代理,再回落 HTTP、ALL;HTTP 优先 HTTP,再回落 ALL。对应的小写变量优先;`no_proxy`/`NO_PROXY` 支持显式旁路。ALL 是回落项,不覆盖专属代理。SOCKS 等未支持协议报 `PROXY_PROTOCOL_UNSUPPORTED`,不会静默直连;代理地址和其中的凭据不写入报错。产物请求不携带网关认证信息,也不跟随重定向。
|
|
85
101
|
|
|
86
102
|
代理地址只能包含源地址(可带代理认证信息),不能附加 `/gw` 等路径、查询或片段;无效时返回 `PROXY_URL_INVALID`。`NO_PROXY` 明确旁路的请求不使用该代理。
|
|
87
103
|
|
|
@@ -149,6 +165,8 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
|
|
|
149
165
|
| `lumenkey_tasks_wait` | 等待已有任务,不新建任务 |
|
|
150
166
|
| `lumenkey_tasks_cancel` | 传 `yes: true` 请求取消;不承诺退款 |
|
|
151
167
|
|
|
168
|
+
`lumenkey_generate_image` / `lumenkey_generate_video` 支持可选 `intervalMs`、`timeoutMs`,与 CLI 共用默认 5000 / 1800000 毫秒;省略 `confirm` 仍然只报价。等待超时后可用 `lumenkey_query_job` 或 `lumenkey_tasks_progress` 继续查询。IDE 自身也可能有工具调用时限,需独立设置;客户端不会因此承诺服务端任务已取消。
|
|
169
|
+
|
|
152
170
|
---
|
|
153
171
|
|
|
154
172
|
## 🏗️ 架构与底层协议
|
|
@@ -156,7 +174,7 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
|
|
|
156
174
|
遵循光钥华语站/国际站 OpenAPI 标准:
|
|
157
175
|
1. `POST /v1/generation-quotes`:获取单次生成报价与算力预扣凭据;
|
|
158
176
|
2. `POST /v1/generation-jobs`:附带 `Idempotency-Key` 幂等创建异步作业;
|
|
159
|
-
3. `GET /v1/generation-jobs/{jobId}
|
|
177
|
+
3. `GET /v1/generation-jobs/{jobId}`:按指定间隔轮询,只有可重试错误才退避;
|
|
160
178
|
4. `POST /v1/generation-jobs/{jobId}/cancel`:显式取消任务;等待超时不自动调用此接口。
|
|
161
179
|
|
|
162
180
|
---
|
package/dist/cli/copied.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function rawOption(names: string[], argv?: string[]): string | un
|
|
|
6
6
|
export declare function applyRaw(options: Record<string, unknown>, key: string, names: string[]): void;
|
|
7
7
|
export declare function emit(data: unknown, human: () => void, argv?: string[]): void;
|
|
8
8
|
export declare function rejectEmptyIdempotencyKey(): void;
|
|
9
|
-
export declare function parseMaxCredits(value: unknown, label?: string): number | undefined;
|
|
9
|
+
export declare function parseMaxCredits(value: unknown, label?: string, code?: string): number | undefined;
|
|
10
10
|
export declare function quoteCreditLimitError(label?: string): Error;
|
|
11
11
|
export declare function dryRunPreview(modelId: string, input: Record<string, unknown>, model: ModelItem, confirm?: unknown): Record<string, unknown>;
|
|
12
12
|
export declare function submitQuote(modelId: string, input: Record<string, unknown>, options: Record<string, unknown>): Promise<{
|
|
@@ -27,7 +27,7 @@ export declare function outputUrls(detail: {
|
|
|
27
27
|
export declare function parsePollOptions(options: {
|
|
28
28
|
interval?: unknown;
|
|
29
29
|
timeout?: unknown;
|
|
30
|
-
}): {
|
|
30
|
+
}, warnShort?: boolean): {
|
|
31
31
|
initialIntervalMs: number;
|
|
32
32
|
timeoutMs: number;
|
|
33
33
|
};
|
|
@@ -35,6 +35,11 @@ export declare function pollConfirmed(api: LumenKeyClient, jobId: string, option
|
|
|
35
35
|
interval?: unknown;
|
|
36
36
|
timeout?: unknown;
|
|
37
37
|
}): Promise<GenerationJobDetail>;
|
|
38
|
+
export declare function finishGeneration(api: LumenKeyClient, created: {
|
|
39
|
+
jobId: string;
|
|
40
|
+
idempotencyKey: string;
|
|
41
|
+
}, options: Record<string, unknown>): Promise<void>;
|
|
42
|
+
export declare function emitJobDetail(detail: GenerationJobDetail): void;
|
|
38
43
|
export declare function promptForKey(): Promise<string | undefined>;
|
|
39
44
|
export declare function readSecret(input?: NodeJS.ReadStream, output?: NodeJS.WritableStream): Promise<string>;
|
|
40
45
|
//# sourceMappingURL=copied.d.ts.map
|
package/dist/cli/copied.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,
|
|
1
|
+
{"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/copied.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { describeKey, loadConfig } from '../core/config.js';
|
|
4
|
-
import { LumenKeyClient, LumenKeyError, validateModelType } from '../core/client.js';
|
|
5
|
-
import { blankPromptSentence, errorSentence, isBlankPrompt, jobStatusText, missingModelSentence, quoteCapSentence, spendingCapSentence, ui
|
|
4
|
+
import { assertIdempotencyKey, LumenKeyClient, LumenKeyError, validateModelType } from '../core/client.js';
|
|
5
|
+
import { blankPromptSentence, errorSentence, isBlankPrompt, jobStatusText, missingModelSentence, quoteCapSentence, spendingCapSentence, 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
9
|
import { applyKeyValues, asPairs, modelParameterError, validateKnownEnums } from '../core/set-values.js';
|
|
10
10
|
import { createProfile, currentProfile, deleteProfile, listProfiles, ProfileMissingError, publicProfile, setNamedProfileFields, setProfile, setProfileFields, unsetProfile } from '../core/profiles.js';
|
|
11
11
|
import { waitForJob } from '../core/wait.js';
|
|
12
|
-
import { assertJobResult,
|
|
12
|
+
import { assertJobResult, pollJob } from '../core/poller.js';
|
|
13
|
+
import { generationResult } from '../core/generation-result.js';
|
|
14
|
+
import { packageVersion } from '../core/version.js';
|
|
15
|
+
import { errorResult } from '../core/error-result.js';
|
|
13
16
|
const COMPLETIONS = {
|
|
14
17
|
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`,
|
|
15
18
|
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`,
|
|
@@ -65,14 +68,14 @@ function propertiesOf(model) {
|
|
|
65
68
|
}
|
|
66
69
|
export function rejectEmptyIdempotencyKey() {
|
|
67
70
|
const rawKey = rawOption(['--idempotency-key']);
|
|
68
|
-
if (rawKey !== undefined
|
|
69
|
-
|
|
71
|
+
if (rawKey !== undefined)
|
|
72
|
+
assertIdempotencyKey(rawKey);
|
|
70
73
|
}
|
|
71
|
-
export function parseMaxCredits(value, label = '--max-credits') {
|
|
74
|
+
export function parseMaxCredits(value, label = '--max-credits', code = 'CREDIT_LIMIT_INVALID') {
|
|
72
75
|
if (value === undefined)
|
|
73
76
|
return undefined;
|
|
74
77
|
if ((typeof value !== 'string' && typeof value !== 'number') || !/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value))) {
|
|
75
|
-
throw new LumenKeyError(
|
|
78
|
+
throw new LumenKeyError(code, undefined, undefined, spendingCapSentence(label));
|
|
76
79
|
}
|
|
77
80
|
return Number(value);
|
|
78
81
|
}
|
|
@@ -111,8 +114,12 @@ export async function submitQuote(modelId, input, options) {
|
|
|
111
114
|
});
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
if (options.idempotencyKey !== undefined) {
|
|
118
|
+
if (typeof options.idempotencyKey !== 'string')
|
|
119
|
+
throw new LumenKeyError('IDEMPOTENCY_INPUT_INVALID');
|
|
120
|
+
assertIdempotencyKey(options.idempotencyKey);
|
|
121
|
+
}
|
|
122
|
+
const key = options.idempotencyKey === undefined ? crypto.randomUUID() : String(options.idempotencyKey);
|
|
116
123
|
console.error(ui('Submitting generation job...', '正在提交生成任务...'));
|
|
117
124
|
const projectId = options.projectId == null || options.projectId === '' ? undefined : String(options.projectId);
|
|
118
125
|
const job = await api.createJob({ quoteId: quote.quoteId, projectId, input }, key);
|
|
@@ -266,7 +273,7 @@ export function registerCopied(cli) {
|
|
|
266
273
|
throw new LumenKeyError('MODEL_REQUIRED');
|
|
267
274
|
if (isBlankPrompt(options.prompt))
|
|
268
275
|
throw new Error(blankPromptSentence('flag'));
|
|
269
|
-
parsePollOptions(options);
|
|
276
|
+
parsePollOptions(options, true);
|
|
270
277
|
client().requireKey();
|
|
271
278
|
parseMaxCredits(options.maxCredits);
|
|
272
279
|
rejectEmptyIdempotencyKey();
|
|
@@ -289,20 +296,7 @@ export function registerCopied(cli) {
|
|
|
289
296
|
const created = await submitQuote(String(options.model), built.input, { ...options, schemaProperties: built.properties, suppressEmit: wantsJson() && Boolean(options.confirm) });
|
|
290
297
|
if (!created)
|
|
291
298
|
return;
|
|
292
|
-
|
|
293
|
-
assertJobResult(detail, true);
|
|
294
|
-
const urls = outputUrls(detail);
|
|
295
|
-
const savedPath = urls[0] && typeof options.output === 'string' && options.output
|
|
296
|
-
? await downloadFile(urls[0], options.output) : undefined;
|
|
297
|
-
if (wantsJson()) {
|
|
298
|
-
console.log(JSON.stringify({ submitted: true, idempotencyKey: created.idempotencyKey, status: detail.status, urls, localFile: savedPath }));
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
for (const url of urls)
|
|
302
|
-
console.log(url);
|
|
303
|
-
if (savedPath) {
|
|
304
|
-
console.log(ui(`Saved to ${savedPath}`, `已保存到 ${savedPath}`));
|
|
305
|
-
}
|
|
299
|
+
await finishGeneration(client(), created, options);
|
|
306
300
|
});
|
|
307
301
|
cli.command('tasks <action> <jobId>', ui('tasks progress | wait | cancel', '查看进度、等待或取消任务'))
|
|
308
302
|
.example('lumenkey tasks progress <jobId>')
|
|
@@ -314,41 +308,20 @@ export function registerCopied(cli) {
|
|
|
314
308
|
.action(async (action, jobId, options) => {
|
|
315
309
|
if (action === 'progress') {
|
|
316
310
|
const detail = await client().getJob(jobId);
|
|
317
|
-
|
|
318
|
-
emit(detail, () => console.log(formatJobDetail(detail)));
|
|
311
|
+
emitJobDetail(detail);
|
|
319
312
|
return;
|
|
320
313
|
}
|
|
321
314
|
if (action === 'wait') {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (interval * 1000 > 2_147_483_647)
|
|
327
|
-
throw new Error(ui('--interval must be between 0 and 2147483 seconds.', '--interval 必须在 0 到 2147483 秒之间。'));
|
|
328
|
-
try {
|
|
329
|
-
const detail = await waitForJob(client(), jobId, { intervalMs: interval * 1000, timeoutMs: timeout * 1000 });
|
|
330
|
-
const failed = detail.error != null || ['failed', 'rejected', 'cancelled', 'expired'].includes(detail.status);
|
|
331
|
-
const body = { ...detail, ok: detail.status === 'succeeded' && !failed, timedOut: false };
|
|
332
|
-
emit(body, () => console.log(formatJobDetail(detail)));
|
|
333
|
-
if (!body.ok)
|
|
334
|
-
process.exitCode = 1;
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
if (err instanceof LumenKeyError && err.code === 'WAIT_TIMEOUT') {
|
|
339
|
-
const body = { status: 'TIMEOUT', ok: false, timedOut: true, jobId, code: 'WAIT_TIMEOUT' };
|
|
340
|
-
emit(body, () => console.error(new LumenKeyError('WAIT_TIMEOUT', undefined, undefined, waitTimeoutDetail(jobId, timeout * 1000)).message));
|
|
341
|
-
process.exitCode = 1;
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
throw err;
|
|
345
|
-
}
|
|
315
|
+
const timing = parsePollOptions(options, true);
|
|
316
|
+
const detail = await withCliWait(jobId, signal => waitForJob(client(), jobId, { intervalMs: timing.initialIntervalMs, timeoutMs: timing.timeoutMs, signal }));
|
|
317
|
+
emit({ ...detail, jobStatus: detail.status, ok: true, timedOut: false }, () => console.log(formatJobDetail(detail)));
|
|
318
|
+
return;
|
|
346
319
|
}
|
|
347
320
|
if (action === 'cancel') {
|
|
348
321
|
if (!options.yes)
|
|
349
|
-
throw new
|
|
322
|
+
throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
|
|
350
323
|
const result = await client().cancelJob(jobId);
|
|
351
|
-
emit(result, () => console.log(
|
|
324
|
+
emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, `已请求取消任务 ${jobId},最终结算由网关确认。`)));
|
|
352
325
|
return;
|
|
353
326
|
}
|
|
354
327
|
throw new Error(ui(`Unknown tasks command: ${action}`, `未知的 tasks 子命令:${action}`));
|
|
@@ -362,7 +335,7 @@ export function registerCopied(cli) {
|
|
|
362
335
|
if (extra.length || (value !== undefined && ['list', 'unset'].includes(action)))
|
|
363
336
|
throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
|
|
364
337
|
if (!['list', 'create', 'set', 'unset', 'key', 'host', 'delete'].includes(action)) {
|
|
365
|
-
throw new
|
|
338
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Unknown profile command: ${action}`, `未知的 profile 子命令:${action}`));
|
|
366
339
|
}
|
|
367
340
|
if (action === 'list') {
|
|
368
341
|
const listed = listProfiles();
|
|
@@ -375,6 +348,9 @@ export function registerCopied(cli) {
|
|
|
375
348
|
return;
|
|
376
349
|
}
|
|
377
350
|
if (action === 'key') {
|
|
351
|
+
const target = value ?? currentProfile().name;
|
|
352
|
+
if (!target || !listProfiles().profiles.some(row => row.profile === target))
|
|
353
|
+
throw new ProfileMissingError(target ?? ui('(active profile)', '(当前档案)'));
|
|
378
354
|
const key = await readProfileKey();
|
|
379
355
|
const name = value ? (setNamedProfileFields(value, { key }), value) : setProfileFields({ key });
|
|
380
356
|
emit({ profile: name, hasStoredToken: true }, () => console.log(ui(`Key set for profile ${name}.`, `已为档案 ${name} 设置密钥。`)));
|
|
@@ -383,13 +359,13 @@ export function registerCopied(cli) {
|
|
|
383
359
|
if (action === 'host' && value === undefined)
|
|
384
360
|
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Profile host needs a gateway address. Use profile host <address>.', 'profile host 需要网关地址。请使用 profile host <地址>。'));
|
|
385
361
|
if (action !== 'host' && !value)
|
|
386
|
-
throw new
|
|
362
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
|
|
387
363
|
if (value === undefined)
|
|
388
364
|
throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
|
|
389
365
|
if (action === 'create') {
|
|
390
366
|
const result = createProfile(value);
|
|
391
367
|
if (result === 'exists')
|
|
392
|
-
throw new
|
|
368
|
+
throw new LumenKeyError('PROFILE_EXISTS');
|
|
393
369
|
emit({ name: value, created: true }, () => console.log(ui(`Profile ${value} created.`, `已创建档案 ${value}。`)));
|
|
394
370
|
return;
|
|
395
371
|
}
|
|
@@ -426,7 +402,7 @@ export function registerCopied(cli) {
|
|
|
426
402
|
if (action === 'delete') {
|
|
427
403
|
const removed = deleteProfile(value);
|
|
428
404
|
if (!removed)
|
|
429
|
-
throw new
|
|
405
|
+
throw new ProfileMissingError(value);
|
|
430
406
|
emit({ deleted: true, name: value }, () => console.log(ui(`Profile ${value} deleted.`, `已删除档案 ${value}。`)));
|
|
431
407
|
return;
|
|
432
408
|
}
|
|
@@ -460,7 +436,7 @@ export function registerCopied(cli) {
|
|
|
460
436
|
: ui('Unknown auth profile command.', '未知的 auth profile 子命令。'));
|
|
461
437
|
}
|
|
462
438
|
if (!name)
|
|
463
|
-
throw new
|
|
439
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Auth profile ${action} needs a name.`, `auth profile ${action} 需要名称。`));
|
|
464
440
|
if (action === 'use') {
|
|
465
441
|
setProfile(name);
|
|
466
442
|
const record = currentProfile().record;
|
|
@@ -487,7 +463,7 @@ export function registerCopied(cli) {
|
|
|
487
463
|
throw new Error(ui('Refusing to delete auth profile without --yes.', '未加 --yes,拒绝删除认证档案。'));
|
|
488
464
|
const removed = deleteProfile(name);
|
|
489
465
|
if (!removed)
|
|
490
|
-
throw new
|
|
466
|
+
throw new ProfileMissingError(name);
|
|
491
467
|
emit({ deleted: true, name }, () => console.log(ui(`Auth profile ${name} deleted.`, `已删除认证档案 ${name}。`)));
|
|
492
468
|
return;
|
|
493
469
|
}
|
|
@@ -498,7 +474,7 @@ export function registerCopied(cli) {
|
|
|
498
474
|
.action(async (options) => {
|
|
499
475
|
const config = loadConfig();
|
|
500
476
|
const checks = [
|
|
501
|
-
{ name: 'cli', status: 'pass', version:
|
|
477
|
+
{ name: 'cli', status: 'pass', version: packageVersion },
|
|
502
478
|
{ name: 'runtime', status: 'pass', value: process.version },
|
|
503
479
|
{ name: 'platform', status: 'pass', value: `${process.platform} ${os.release()}` },
|
|
504
480
|
{ name: 'host', status: hostStatus(config), value: config.baseUrl },
|
|
@@ -515,7 +491,7 @@ export function registerCopied(cli) {
|
|
|
515
491
|
}
|
|
516
492
|
const ok = checks.every((check) => check.status !== 'fail');
|
|
517
493
|
emit({ ok, checks }, () => {
|
|
518
|
-
console.log(ui(
|
|
494
|
+
console.log(ui(`cli: ${packageVersion}`, `命令行:${packageVersion}`));
|
|
519
495
|
console.log(ui(`runtime: ${process.version}`, `运行时:${process.version}`));
|
|
520
496
|
console.log(ui(`platform: ${process.platform} ${os.release()}`, `平台:${process.platform} ${os.release()}`));
|
|
521
497
|
console.log(ui(`host: ${config.baseUrl}`, `网关:${config.baseUrl}`));
|
|
@@ -530,9 +506,9 @@ export function registerCopied(cli) {
|
|
|
530
506
|
});
|
|
531
507
|
cli.command('completion <shell>', ui('Print a shell completion script', '打印 shell 补全脚本'))
|
|
532
508
|
.action((shell) => {
|
|
533
|
-
const script = COMPLETIONS[shell];
|
|
509
|
+
const script = Object.hasOwn(COMPLETIONS, shell) ? COMPLETIONS[shell] : undefined;
|
|
534
510
|
if (!script)
|
|
535
|
-
throw new
|
|
511
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Shell must be bash, zsh, tcsh, or fish.', 'shell 只能是 bash、zsh、tcsh 或 fish。'));
|
|
536
512
|
console.log(script);
|
|
537
513
|
});
|
|
538
514
|
}
|
|
@@ -560,38 +536,114 @@ function coerceDeclared(spec, value, key) {
|
|
|
560
536
|
export function outputUrls(detail) {
|
|
561
537
|
return (detail.outputs ?? []).map((item) => item.url).filter((url) => typeof url === 'string' && url.length > 0);
|
|
562
538
|
}
|
|
563
|
-
export function parsePollOptions(options) {
|
|
539
|
+
export function parsePollOptions(options, warnShort = false) {
|
|
564
540
|
const interval = Number(options.interval ?? 5);
|
|
565
541
|
const timeout = Number(options.timeout ?? 1800);
|
|
566
|
-
if (!Number.isFinite(interval) || interval
|
|
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()) {
|
|
567
543
|
throw new LumenKeyError('WAIT_OPTIONS_INVALID');
|
|
568
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'));
|
|
569
547
|
return { initialIntervalMs: interval * 1000, timeoutMs: timeout * 1000 };
|
|
570
548
|
}
|
|
571
549
|
export async function pollConfirmed(api, jobId, options) {
|
|
572
550
|
const timing = parsePollOptions(options);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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);
|
|
577
566
|
try {
|
|
578
|
-
return await
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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;
|
|
582
577
|
}
|
|
583
578
|
finally {
|
|
584
|
-
process.off('SIGINT',
|
|
579
|
+
process.off('SIGINT', sigint);
|
|
580
|
+
process.off('SIGTERM', sigterm);
|
|
585
581
|
}
|
|
586
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
|
+
}
|
|
587
632
|
export async function promptForKey() {
|
|
588
|
-
if (
|
|
633
|
+
if (process.stdin.readableEnded || process.stdin.destroyed)
|
|
589
634
|
return undefined;
|
|
590
|
-
|
|
635
|
+
try {
|
|
636
|
+
return await readProfileKey();
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (!process.stdin.isTTY && error instanceof LumenKeyError && error.code === 'INPUT_CLOSED')
|
|
640
|
+
return undefined;
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
591
643
|
}
|
|
592
644
|
export async function readSecret(input = process.stdin, output = process.stderr) {
|
|
593
645
|
if (input.readableEnded || input.destroyed)
|
|
594
|
-
throw new
|
|
646
|
+
throw new LumenKeyError('INPUT_CLOSED');
|
|
595
647
|
if (!input.isTTY) {
|
|
596
648
|
const { createInterface } = await import('node:readline');
|
|
597
649
|
return new Promise((resolve, reject) => {
|
|
@@ -606,7 +658,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
|
|
|
606
658
|
resolve(line.trim());
|
|
607
659
|
});
|
|
608
660
|
reader.once('close', () => { if (!answered)
|
|
609
|
-
reject(new
|
|
661
|
+
reject(new LumenKeyError('INPUT_CLOSED')); });
|
|
610
662
|
reader.once('error', reject);
|
|
611
663
|
output.write(ui('Enter the key: ', '请输入密钥:'));
|
|
612
664
|
});
|
|
@@ -681,14 +733,14 @@ export async function readSecret(input = process.stdin, output = process.stderr)
|
|
|
681
733
|
}
|
|
682
734
|
async function ask(prompt) {
|
|
683
735
|
if (process.stdin.readableEnded || process.stdin.destroyed)
|
|
684
|
-
throw new
|
|
736
|
+
throw new LumenKeyError('INPUT_CLOSED');
|
|
685
737
|
const readline = await import('node:readline');
|
|
686
738
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
687
739
|
return await new Promise((resolve, reject) => {
|
|
688
740
|
let answered = false;
|
|
689
741
|
rl.once('line', (line) => { answered = true; rl.close(); resolve(line); });
|
|
690
742
|
rl.once('close', () => { if (!answered)
|
|
691
|
-
reject(new
|
|
743
|
+
reject(new LumenKeyError('INPUT_CLOSED')); });
|
|
692
744
|
rl.setPrompt(prompt);
|
|
693
745
|
rl.prompt();
|
|
694
746
|
});
|