@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.
Files changed (58) hide show
  1. package/README.md +21 -3
  2. package/dist/cli/copied.d.ts +7 -2
  3. package/dist/cli/copied.d.ts.map +1 -1
  4. package/dist/cli/copied.js +131 -79
  5. package/dist/cli/copied.js.map +1 -1
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +25 -28
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/core/atomic-config-file.d.ts +8 -0
  10. package/dist/core/atomic-config-file.d.ts.map +1 -0
  11. package/dist/core/atomic-config-file.js +63 -0
  12. package/dist/core/atomic-config-file.js.map +1 -0
  13. package/dist/core/client.d.ts +10 -1
  14. package/dist/core/client.d.ts.map +1 -1
  15. package/dist/core/client.js +20 -4
  16. package/dist/core/client.js.map +1 -1
  17. package/dist/core/config.d.ts +2 -2
  18. package/dist/core/config.d.ts.map +1 -1
  19. package/dist/core/config.js +22 -4
  20. package/dist/core/config.js.map +1 -1
  21. package/dist/core/download.d.ts +3 -0
  22. package/dist/core/download.d.ts.map +1 -0
  23. package/dist/core/download.js +107 -0
  24. package/dist/core/download.js.map +1 -0
  25. package/dist/core/error-result.d.ts +14 -0
  26. package/dist/core/error-result.d.ts.map +1 -0
  27. package/dist/core/error-result.js +27 -0
  28. package/dist/core/error-result.js.map +1 -0
  29. package/dist/core/generation-result.d.ts +19 -0
  30. package/dist/core/generation-result.d.ts.map +1 -0
  31. package/dist/core/generation-result.js +22 -0
  32. package/dist/core/generation-result.js.map +1 -0
  33. package/dist/core/locale.d.ts.map +1 -1
  34. package/dist/core/locale.js +34 -0
  35. package/dist/core/locale.js.map +1 -1
  36. package/dist/core/poller.d.ts +15 -5
  37. package/dist/core/poller.d.ts.map +1 -1
  38. package/dist/core/poller.js +45 -57
  39. package/dist/core/poller.js.map +1 -1
  40. package/dist/core/profiles.d.ts +5 -1
  41. package/dist/core/profiles.d.ts.map +1 -1
  42. package/dist/core/profiles.js +59 -12
  43. package/dist/core/profiles.js.map +1 -1
  44. package/dist/core/set-values.d.ts.map +1 -1
  45. package/dist/core/set-values.js +6 -1
  46. package/dist/core/set-values.js.map +1 -1
  47. package/dist/core/version.d.ts +2 -0
  48. package/dist/core/version.d.ts.map +1 -0
  49. package/dist/core/version.js +3 -0
  50. package/dist/core/version.js.map +1 -0
  51. package/dist/core/wait.d.ts +1 -0
  52. package/dist/core/wait.d.ts.map +1 -1
  53. package/dist/core/wait.js +2 -40
  54. package/dist/core/wait.js.map +1 -1
  55. package/dist/mcp/server.d.ts.map +1 -1
  56. package/dist/mcp/server.js +50 -55
  57. package/dist/mcp/server.js.map +1 -1
  58. 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`。等待超时以退出码 1 结束,不会自动取消任务,也不代表已退款。`tasks wait --json` 保留 `status: "TIMEOUT"`、`ok: false`、`timedOut: true`,同时带 `code: "WAIT_TIMEOUT"`。
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
- 网关请求支持 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`,不会静默直连;代理地址和其中的凭据不写入报错。
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
  ---
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"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"}
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,EAAwB,cAAc,EAAoC,MAAM,mBAAmB,CAAC;AAY3G,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AASvE,wBAAgB,SAAS,CAAC,IAAI,WAAe,GAAG,OAAO,CAEtD;AAOD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,WAAe,GAAG,MAAM,GAAG,SAAS,CAYlF;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAS7F;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,WAAe,GAAG,IAAI,CAMhF;AAYD,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAkB,EAAE,IAAI,SAAyB,GAAG,MAAM,GAAG,SAAS,CAM1H;AAED,wBAAgB,qBAAqB,CAAC,KAAK,SAAkB,GAAG,KAAK,CAEpE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS3I;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CA4BnL;AA8BD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC,CAiBnO;AAsBD,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAkS7C;AAsBD,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,MAAM,EAAE,CAElF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,SAAS,UAAQ,GAAG;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAWxJ;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAMxJ;AAuBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB/J;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAa/D;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIhE;AAED,wBAAsB,UAAU,CAC9B,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,MAAM,GAAE,MAAM,CAAC,cAA+B,GAC7C,OAAO,CAAC,MAAM,CAAC,CA6DjB"}
@@ -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, waitTimeoutDetail } from '../core/locale.js';
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, downloadFile, pollJob } from '../core/poller.js';
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 && !rawKey.trim())
69
- throw new Error(ui('--idempotency-key must not be empty.', '幂等键不能为空。'));
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('CHAT_CREDIT_LIMIT_INVALID', undefined, undefined, spendingCapSentence(label));
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
- const provided = options.idempotencyKey == null ? '' : String(options.idempotencyKey).trim();
115
- const key = provided || crypto.randomUUID();
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
- const detail = await pollConfirmed(client(), created.jobId, options);
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
- assertJobResult(detail, detail.status === 'succeeded');
318
- emit(detail, () => console.log(formatJobDetail(detail)));
311
+ emitJobDetail(detail);
319
312
  return;
320
313
  }
321
314
  if (action === 'wait') {
322
- const interval = Number(options.interval);
323
- const timeout = Number(options.timeout);
324
- if (!Number.isFinite(interval) || interval < 0 || !Number.isFinite(timeout) || timeout < 0)
325
- throw new Error(ui('--interval and --timeout must be non-negative numbers.', '--interval 和 --timeout 必须是非负数。'));
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 Error(ui('Refusing to cancel without --yes.', '未加 --yes,拒绝取消。'));
322
+ throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
350
323
  const result = await client().cancelJob(jobId);
351
- emit(result, () => console.log(JSON.stringify(result)));
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 Error(ui(`Unknown profile command: ${action}`, `未知的 profile 子命令:${action}`));
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 Error(ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
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 Error(ui(`Profile ${value} already exists.`, `档案 ${value} 已存在。`));
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 Error(ui(`Profile ${value} does not exist.`, `档案 ${value} 不存在。`));
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 Error(ui(`Auth profile ${action} needs a name.`, `auth profile ${action} 需要名称。`));
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 Error(ui(`Profile ${name} does not exist.`, `档案 ${name} 不存在。`));
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: '0.2.1' },
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('cli: 0.2.0', '命令行:0.2.0'));
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 Error(ui('Shell must be bash, zsh, tcsh, or fish.', 'shell 只能是 bash、zsh、tcsh 或 fish。'));
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 < 0 || interval * 1000 > 2_147_483_647 || !Number.isFinite(timeout) || timeout < 0 || timeout * 1000 > Number.MAX_SAFE_INTEGER - Date.now()) {
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
- const onSigint = () => {
574
- api.cancelJob(jobId).catch(() => undefined).finally(() => process.exit(130));
575
- };
576
- process.once('SIGINT', onSigint);
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 pollJob(api, jobId, {
579
- ...timing,
580
- onProgress: (row) => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
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', onSigint);
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 (!process.stdin.isTTY || process.stdin.readableEnded || process.stdin.destroyed)
633
+ if (process.stdin.readableEnded || process.stdin.destroyed)
589
634
  return undefined;
590
- return readProfileKey();
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 Error(ui('stdin closed before an answer was given.', '输入密钥前标准输入已关闭。'));
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 Error(ui('stdin closed before an answer was given.', '输入密钥前标准输入已关闭。'))); });
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 Error('stdin closed before an answer was given.');
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 Error('stdin closed before an answer was given.')); });
743
+ reject(new LumenKeyError('INPUT_CLOSED')); });
692
744
  rl.setPrompt(prompt);
693
745
  rl.prompt();
694
746
  });