@lumenkey/cli 0.2.0 → 0.2.2

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 (59) hide show
  1. package/README.md +21 -3
  2. package/dist/cli/copied.d.ts +12 -0
  3. package/dist/cli/copied.d.ts.map +1 -1
  4. package/dist/cli/copied.js +143 -93
  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 +23 -27
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/core/atomic-config-file.d.ts +5 -0
  10. package/dist/core/atomic-config-file.d.ts.map +1 -0
  11. package/dist/core/atomic-config-file.js +54 -0
  12. package/dist/core/atomic-config-file.js.map +1 -0
  13. package/dist/core/client.d.ts +9 -1
  14. package/dist/core/client.d.ts.map +1 -1
  15. package/dist/core/client.js +32 -8
  16. package/dist/core/client.js.map +1 -1
  17. package/dist/core/config.d.ts.map +1 -1
  18. package/dist/core/config.js +19 -4
  19. package/dist/core/config.js.map +1 -1
  20. package/dist/core/download.d.ts +3 -0
  21. package/dist/core/download.d.ts.map +1 -0
  22. package/dist/core/download.js +100 -0
  23. package/dist/core/download.js.map +1 -0
  24. package/dist/core/error-result.d.ts +14 -0
  25. package/dist/core/error-result.d.ts.map +1 -0
  26. package/dist/core/error-result.js +25 -0
  27. package/dist/core/error-result.js.map +1 -0
  28. package/dist/core/generation-result.d.ts +18 -0
  29. package/dist/core/generation-result.d.ts.map +1 -0
  30. package/dist/core/generation-result.js +22 -0
  31. package/dist/core/generation-result.js.map +1 -0
  32. package/dist/core/locale.d.ts +1 -0
  33. package/dist/core/locale.d.ts.map +1 -1
  34. package/dist/core/locale.js +41 -0
  35. package/dist/core/locale.js.map +1 -1
  36. package/dist/core/poller.d.ts +13 -4
  37. package/dist/core/poller.d.ts.map +1 -1
  38. package/dist/core/poller.js +46 -53
  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 +3 -0
  45. package/dist/core/set-values.d.ts.map +1 -1
  46. package/dist/core/set-values.js +40 -14
  47. package/dist/core/set-values.js.map +1 -1
  48. package/dist/core/version.d.ts +2 -0
  49. package/dist/core/version.d.ts.map +1 -0
  50. package/dist/core/version.js +3 -0
  51. package/dist/core/version.js.map +1 -0
  52. package/dist/core/wait.d.ts +1 -0
  53. package/dist/core/wait.d.ts.map +1 -1
  54. package/dist/core/wait.js +2 -33
  55. package/dist/core/wait.js.map +1 -1
  56. package/dist/mcp/server.d.ts.map +1 -1
  57. package/dist/mcp/server.js +96 -94
  58. package/dist/mcp/server.js.map +1 -1
  59. package/package.json +10 -2
package/README.md CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  ## 📦 极速上手 (Quick Start)
18
18
 
19
+ 运行环境:Node.js **20.18.1 或更新**(建议使用当前 Node LTS)。CLI 与 MCP 需使用同一已安装版本;升级后请重启 IDE 的 MCP 进程。
20
+
19
21
  ### 1. 认证配置
20
22
 
21
23
  可通过环境变量或配置命令设置您的 API Key:
@@ -29,6 +31,8 @@ export LUMENKEY_API_KEY="lk_live_xxxxxxxx"
29
31
  npx @lumenkey/cli login --key lk_live_xxxxxxxx
30
32
  ```
31
33
 
34
+ 也可运行 `lumenkey login` 交互输入(不回显),或从标准输入传入一行密钥;不要把真实密钥写进命令历史。`login` 只保存显式输入的字段,不把本次进程的网关、超时等环境变量固化到配置。损坏的 `config.json` / `profiles.json` 会报错并保留原文件,请先备份再修正;不会当成空档案覆盖。POSIX 上保存文件权限为 0600,Windows 仍应由用户目录 ACL 保护。
35
+
32
36
  ---
33
37
 
34
38
  ## 💻 命令行模式 (CLI Usage)
@@ -42,6 +46,8 @@ npx @lumenkey/cli models
42
46
  npx @lumenkey/cli models --type video
43
47
  ```
44
48
 
49
+ 目录价格用于浏览模型,不等于具体参数及账户条件下的最终报价。图像/视频请先不带 `--confirm` 报价,以服务端返回的 `credits` 为准;客户端不自行改价或补算账户折扣。目录参数以 `lumenkey model explain <modelId>` 为准,错误值不会原样写入报错。
50
+
45
51
  ### 2. 视频先报价,确认后生成
46
52
  不加 `--confirm` 只报价,不提交生成任务。参数与默认值由所选模型目录决定;不要把某个模型的规格套用到其他模型。
47
53
 
@@ -66,7 +72,13 @@ npx @lumenkey/cli status <jobId>
66
72
  npx @lumenkey/cli --json status <jobId>
67
73
  ```
68
74
 
69
- `status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json`。等待超时以退出码 1 结束,不会自动取消任务,也不代表已退款。`tasks wait --json` 保留 `status: "TIMEOUT"`、`ok: false`、`timedOut: true`,同时带 `code: "WAIT_TIMEOUT"`。
75
+ `status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json`。失败任务仍返回原始任务详情和 `error.code`,同时以退出码 1 结束。网关请求失败等命令错误统一返回 `error`、`code`、`type`,有 HTTP 状态时另带 `httpStatus`。任务错误码可能为小写(例如 `provider_error`),请原样处理。
76
+
77
+ 图片、视频、`tasks wait` 共用轮询逻辑,默认间隔 5 秒、等待 1800 秒。间隔必须大于 0,指定 25 秒不会被缩短到 10 秒。等待时长是两次请求之间检查的预算,不会中断已发出的单次查询;单次网关请求另受 `LUMENKEY_TIMEOUT_MS` 控制。超时带 `WAIT_TIMEOUT`、`timedOut: true` 和任务号;不取消任务、不代表退款。
78
+
79
+ Ctrl+C / SIGTERM 只停止本机等待或下载,不调用取消接口;分别以 130 / 143 退出并保留恢复查询信息。取消任务必须显式运行 `tasks cancel <jobId> --yes`(MCP 使用 `yes: true`)。
80
+
81
+ `--output` 下载不覆盖已有文件,单次限 120 秒、128 MiB。生成成功但保存失败时,仍返回 `status: "succeeded"`、`jobId`、`idempotencyKey` 和全部 `urls`,另带 `download: {ok:false,code,error}`;CLI 以 1 退出表示本机保存失败,MCP 保留生成成功和下载警告。请用结果地址另存,不要为下载失败重新提交付费生成。下载中断时也保留这些结果。
70
82
 
71
83
  ### 4. 查看账户算力余额
72
84
  ```bash
@@ -77,7 +89,11 @@ npx @lumenkey/cli balance
77
89
 
78
90
  `LUMENKEY_BASE_URL`、config.json 的 baseUrl、当前档案 host 按顺序选择,最后才使用默认网关。所选地址会裁掉首尾空白并移除一个尾部 `/v1`;显式设置空字符串会报错,不会悄悄切回默认站点。`doctor` 默认只做本地检查,`--online` 才读取网关余额。
79
91
 
80
- 网关请求支持 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`,不会静默直连;代理地址和其中的凭据不写入报错。
92
+ 网关请求和产物下载共用 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`,不会静默直连;代理地址和其中的凭据不写入报错。产物请求不携带网关认证信息,也不跟随重定向。
93
+
94
+ 代理地址只能包含源地址(可带代理认证信息),不能附加 `/gw` 等路径、查询或片段;无效时返回 `PROXY_URL_INVALID`。`NO_PROXY` 明确旁路的请求不使用该代理。
95
+
96
+ 聊天返回 HTTP 5xx 且没有 `billing` 时,客户端会明确提示结算状态未知、算力点可能仍被冻结;不会承诺已释放、扣费为零或自动重试。服务端带回的 `SETTLED` / `RELEASED` / `PENDING` 仍是结算依据。目录查询失败不会被误写成聊天冻结。
81
97
 
82
98
  ---
83
99
 
@@ -141,6 +157,8 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
141
157
  | `lumenkey_tasks_wait` | 等待已有任务,不新建任务 |
142
158
  | `lumenkey_tasks_cancel` | 传 `yes: true` 请求取消;不承诺退款 |
143
159
 
160
+ `lumenkey_generate_image` / `lumenkey_generate_video` 支持可选 `intervalMs`、`timeoutMs`,与 CLI 共用默认 5000 / 1800000 毫秒;省略 `confirm` 仍然只报价。等待超时后可用 `lumenkey_query_job` 或 `lumenkey_tasks_progress` 继续查询。IDE 自身也可能有工具调用时限,需独立设置;客户端不会因此承诺服务端任务已取消。
161
+
144
162
  ---
145
163
 
146
164
  ## 🏗️ 架构与底层协议
@@ -148,7 +166,7 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
148
166
  遵循光钥华语站/国际站 OpenAPI 标准:
149
167
  1. `POST /v1/generation-quotes`:获取单次生成报价与算力预扣凭据;
150
168
  2. `POST /v1/generation-jobs`:附带 `Idempotency-Key` 幂等创建异步作业;
151
- 3. `GET /v1/generation-jobs/{jobId}`:指数退避轮询任务状态;
169
+ 3. `GET /v1/generation-jobs/{jobId}`:按指定间隔轮询,只有可重试错误才退避;
152
170
  4. `POST /v1/generation-jobs/{jobId}/cancel`:显式取消任务;等待超时不自动调用此接口。
153
171
 
154
172
  ---
@@ -24,10 +24,22 @@ export declare function outputUrls(detail: {
24
24
  url?: string;
25
25
  }>;
26
26
  }): string[];
27
+ export declare function parsePollOptions(options: {
28
+ interval?: unknown;
29
+ timeout?: unknown;
30
+ }): {
31
+ initialIntervalMs: number;
32
+ timeoutMs: number;
33
+ };
27
34
  export declare function pollConfirmed(api: LumenKeyClient, jobId: string, options: {
28
35
  interval?: unknown;
29
36
  timeout?: unknown;
30
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;
31
43
  export declare function promptForKey(): Promise<string | undefined>;
32
44
  export declare function readSecret(input?: NodeJS.ReadStream, output?: NodeJS.WritableStream): Promise<string>;
33
45
  //# sourceMappingURL=copied.d.ts.map
@@ -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,EAAE,cAAc,EAAoC,MAAM,mBAAmB,CAAC;AASrF,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,GAAG,MAAM,GAAG,SAAS,CAM3F;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,CAyBnL;AAmCD,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,CAwT7C;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,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,CAmBxJ;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGhE;AAED,wBAAsB,UAAU,CAC9B,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,MAAM,GAAE,MAAM,CAAC,cAA+B,GAC7C,OAAO,CAAC,MAAM,CAAC,CA6DjB"}
1
+ {"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"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"}
@@ -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
- import { applyKeyValues, asPairs, validateKnownEnums } from '../core/set-values.js';
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,8 +68,8 @@ 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
74
  export function parseMaxCredits(value, label = '--max-credits') {
72
75
  if (value === undefined)
@@ -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);
@@ -120,11 +127,6 @@ export async function submitQuote(modelId, input, options) {
120
127
  emit({ ...job, idempotencyKey: key, submitted: true }, () => console.log(ui(`Job created: ${job.jobId}`, `任务已创建:${job.jobId}`)));
121
128
  return { jobId: job.jobId, idempotencyKey: key };
122
129
  }
123
- function parameterLabel(key) {
124
- if (key === 'aspect_ratio' || key === 'ratio')
125
- return 'aspect ratio';
126
- return key;
127
- }
128
130
  function hasDeclared(properties, key) {
129
131
  return Object.prototype.hasOwnProperty.call(properties, key);
130
132
  }
@@ -136,24 +138,24 @@ function assignAspect(properties, input, fields) {
136
138
  const hasAspect = hasDeclared(properties, 'aspect_ratio');
137
139
  const hasRatio = hasDeclared(properties, 'ratio');
138
140
  if (!hasAspect && !hasRatio)
139
- throw new Error(`This model has no ${parameterLabel('aspect_ratio')} parameter.`);
141
+ throw modelParameterError('MODEL_PARAM_UNKNOWN', 'aspect_ratio');
140
142
  if (hasAspect && aspect !== undefined)
141
- input.aspect_ratio = coerceDeclared(properties.aspect_ratio, aspect);
143
+ input.aspect_ratio = coerceDeclared(properties.aspect_ratio, aspect, 'aspect_ratio');
142
144
  if (hasRatio && ratio !== undefined)
143
- input.ratio = coerceDeclared(properties.ratio, ratio);
145
+ input.ratio = coerceDeclared(properties.ratio, ratio, 'ratio');
144
146
  if (input.aspect_ratio === undefined && input.ratio === undefined) {
145
147
  const name = hasAspect ? 'aspect_ratio' : 'ratio';
146
148
  const value = aspect !== undefined ? aspect : ratio;
147
- input[name] = coerceDeclared(properties[name], value);
149
+ input[name] = coerceDeclared(properties[name], value, name);
148
150
  }
149
151
  }
150
152
  function assignDeclared(properties, input, key, value) {
151
153
  if (!hasDeclared(properties, key))
152
- throw new Error(`This model has no ${parameterLabel(key)} parameter.`);
154
+ throw modelParameterError('MODEL_PARAM_UNKNOWN', key);
153
155
  const spec = properties[key];
154
156
  const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
155
157
  const scalar = value === null || value === undefined || typeof value !== 'object';
156
- input[key] = scalar && (type === 'integer' || type === 'number' || type === 'string') ? coerceDeclared(spec, value) : value;
158
+ input[key] = scalar && (type === 'integer' || type === 'number' || type === 'string') ? coerceDeclared(spec, value, key) : value;
157
159
  }
158
160
  export async function buildGenerationInput(modelId, fields, pairs, known) {
159
161
  client().requireKey();
@@ -161,7 +163,7 @@ export async function buildGenerationInput(modelId, fields, pairs, known) {
161
163
  throw new Error(blankPromptSentence('flag'));
162
164
  const model = known?.id === modelId ? known : (await client().listModels()).find((item) => item.id === modelId);
163
165
  if (!model)
164
- throw new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, `Model ${JSON.stringify(modelId)} is not in your available catalog. Use an exact model ID.`);
166
+ throw new LumenKeyError('MODEL_NOT_IN_CATALOG');
165
167
  const properties = propertiesOf(model);
166
168
  const defined = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
167
169
  const input = {};
@@ -271,6 +273,7 @@ export function registerCopied(cli) {
271
273
  throw new LumenKeyError('MODEL_REQUIRED');
272
274
  if (isBlankPrompt(options.prompt))
273
275
  throw new Error(blankPromptSentence('flag'));
276
+ parsePollOptions(options);
274
277
  client().requireKey();
275
278
  parseMaxCredits(options.maxCredits);
276
279
  rejectEmptyIdempotencyKey();
@@ -293,19 +296,7 @@ export function registerCopied(cli) {
293
296
  const created = await submitQuote(String(options.model), built.input, { ...options, schemaProperties: built.properties, suppressEmit: wantsJson() && Boolean(options.confirm) });
294
297
  if (!created)
295
298
  return;
296
- const detail = await pollConfirmed(client(), created.jobId, options);
297
- assertJobResult(detail, true);
298
- const urls = outputUrls(detail);
299
- if (wantsJson()) {
300
- console.log(JSON.stringify({ submitted: true, idempotencyKey: created.idempotencyKey, status: detail.status, urls }));
301
- return;
302
- }
303
- for (const url of urls)
304
- console.log(url);
305
- if (urls[0] && typeof options.output === 'string' && options.output) {
306
- const savedPath = await downloadFile(urls[0], options.output);
307
- console.log(ui(`Saved to ${savedPath}`, `已保存到 ${savedPath}`));
308
- }
299
+ await finishGeneration(client(), created, options);
309
300
  });
310
301
  cli.command('tasks <action> <jobId>', ui('tasks progress | wait | cancel', '查看进度、等待或取消任务'))
311
302
  .example('lumenkey tasks progress <jobId>')
@@ -317,41 +308,20 @@ export function registerCopied(cli) {
317
308
  .action(async (action, jobId, options) => {
318
309
  if (action === 'progress') {
319
310
  const detail = await client().getJob(jobId);
320
- assertJobResult(detail, detail.status === 'succeeded');
321
- emit(detail, () => console.log(formatJobDetail(detail)));
311
+ emitJobDetail(detail);
322
312
  return;
323
313
  }
324
314
  if (action === 'wait') {
325
- const interval = Number(options.interval);
326
- const timeout = Number(options.timeout);
327
- if (!Number.isFinite(interval) || interval < 0 || !Number.isFinite(timeout) || timeout < 0)
328
- throw new Error(ui('--interval and --timeout must be non-negative numbers.', '--interval 和 --timeout 必须是非负数。'));
329
- if (interval * 1000 > 2_147_483_647)
330
- throw new Error(ui('--interval must be between 0 and 2147483 seconds.', '--interval 必须在 0 到 2147483 秒之间。'));
331
- try {
332
- const detail = await waitForJob(client(), jobId, { intervalMs: interval * 1000, timeoutMs: timeout * 1000 });
333
- const failed = detail.error != null || ['failed', 'rejected', 'cancelled', 'expired'].includes(detail.status);
334
- const body = { ...detail, ok: detail.status === 'succeeded' && !failed, timedOut: false };
335
- emit(body, () => console.log(formatJobDetail(detail)));
336
- if (!body.ok)
337
- process.exitCode = 1;
338
- return;
339
- }
340
- catch (err) {
341
- if (err instanceof LumenKeyError && err.code === 'WAIT_TIMEOUT') {
342
- const body = { status: 'TIMEOUT', ok: false, timedOut: true, jobId, code: 'WAIT_TIMEOUT' };
343
- emit(body, () => console.error(new LumenKeyError('WAIT_TIMEOUT', undefined, undefined, waitTimeoutDetail(jobId, timeout * 1000)).message));
344
- process.exitCode = 1;
345
- return;
346
- }
347
- throw err;
348
- }
315
+ const timing = parsePollOptions(options);
316
+ const detail = await withCliWait(jobId, signal => waitForJob(client(), jobId, { intervalMs: timing.initialIntervalMs, timeoutMs: timing.timeoutMs, signal }));
317
+ emit({ ...detail, ok: true, timedOut: false }, () => console.log(formatJobDetail(detail)));
318
+ return;
349
319
  }
350
320
  if (action === 'cancel') {
351
321
  if (!options.yes)
352
- throw new Error(ui('Refusing to cancel without --yes.', '未加 --yes,拒绝取消。'));
322
+ throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
353
323
  const result = await client().cancelJob(jobId);
354
- emit(result, () => console.log(JSON.stringify(result)));
324
+ emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, `已请求取消任务 ${jobId},最终结算由网关确认。`)));
355
325
  return;
356
326
  }
357
327
  throw new Error(ui(`Unknown tasks command: ${action}`, `未知的 tasks 子命令:${action}`));
@@ -365,7 +335,7 @@ export function registerCopied(cli) {
365
335
  if (extra.length || (value !== undefined && ['list', 'unset'].includes(action)))
366
336
  throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
367
337
  if (!['list', 'create', 'set', 'unset', 'key', 'host', 'delete'].includes(action)) {
368
- 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}`));
369
339
  }
370
340
  if (action === 'list') {
371
341
  const listed = listProfiles();
@@ -378,17 +348,24 @@ export function registerCopied(cli) {
378
348
  return;
379
349
  }
380
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)', '(当前档案)'));
381
354
  const key = await readProfileKey();
382
355
  const name = value ? (setNamedProfileFields(value, { key }), value) : setProfileFields({ key });
383
356
  emit({ profile: name, hasStoredToken: true }, () => console.log(ui(`Key set for profile ${name}.`, `已为档案 ${name} 设置密钥。`)));
384
357
  return;
385
358
  }
386
- if (!value)
387
- throw new Error(ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
359
+ if (action === 'host' && value === undefined)
360
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Profile host needs a gateway address. Use profile host <address>.', 'profile host 需要网关地址。请使用 profile host <地址>。'));
361
+ if (action !== 'host' && !value)
362
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
363
+ if (value === undefined)
364
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
388
365
  if (action === 'create') {
389
366
  const result = createProfile(value);
390
367
  if (result === 'exists')
391
- throw new Error(ui(`Profile ${value} already exists.`, `档案 ${value} 已存在。`));
368
+ throw new LumenKeyError('PROFILE_EXISTS');
392
369
  emit({ name: value, created: true }, () => console.log(ui(`Profile ${value} created.`, `已创建档案 ${value}。`)));
393
370
  return;
394
371
  }
@@ -425,7 +402,7 @@ export function registerCopied(cli) {
425
402
  if (action === 'delete') {
426
403
  const removed = deleteProfile(value);
427
404
  if (!removed)
428
- throw new Error(ui(`Profile ${value} does not exist.`, `档案 ${value} 不存在。`));
405
+ throw new ProfileMissingError(value);
429
406
  emit({ deleted: true, name: value }, () => console.log(ui(`Profile ${value} deleted.`, `已删除档案 ${value}。`)));
430
407
  return;
431
408
  }
@@ -459,7 +436,7 @@ export function registerCopied(cli) {
459
436
  : ui('Unknown auth profile command.', '未知的 auth profile 子命令。'));
460
437
  }
461
438
  if (!name)
462
- 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} 需要名称。`));
463
440
  if (action === 'use') {
464
441
  setProfile(name);
465
442
  const record = currentProfile().record;
@@ -486,7 +463,7 @@ export function registerCopied(cli) {
486
463
  throw new Error(ui('Refusing to delete auth profile without --yes.', '未加 --yes,拒绝删除认证档案。'));
487
464
  const removed = deleteProfile(name);
488
465
  if (!removed)
489
- throw new Error(ui(`Profile ${name} does not exist.`, `档案 ${name} 不存在。`));
466
+ throw new ProfileMissingError(name);
490
467
  emit({ deleted: true, name }, () => console.log(ui(`Auth profile ${name} deleted.`, `已删除认证档案 ${name}。`)));
491
468
  return;
492
469
  }
@@ -497,7 +474,7 @@ export function registerCopied(cli) {
497
474
  .action(async (options) => {
498
475
  const config = loadConfig();
499
476
  const checks = [
500
- { name: 'cli', status: 'pass', version: '0.2.0' },
477
+ { name: 'cli', status: 'pass', version: packageVersion },
501
478
  { name: 'runtime', status: 'pass', value: process.version },
502
479
  { name: 'platform', status: 'pass', value: `${process.platform} ${os.release()}` },
503
480
  { name: 'host', status: hostStatus(config), value: config.baseUrl },
@@ -514,7 +491,7 @@ export function registerCopied(cli) {
514
491
  }
515
492
  const ok = checks.every((check) => check.status !== 'fail');
516
493
  emit({ ok, checks }, () => {
517
- console.log(ui('cli: 0.2.0', '命令行:0.2.0'));
494
+ console.log(ui(`cli: ${packageVersion}`, `命令行:${packageVersion}`));
518
495
  console.log(ui(`runtime: ${process.version}`, `运行时:${process.version}`));
519
496
  console.log(ui(`platform: ${process.platform} ${os.release()}`, `平台:${process.platform} ${os.release()}`));
520
497
  console.log(ui(`host: ${config.baseUrl}`, `网关:${config.baseUrl}`));
@@ -529,9 +506,9 @@ export function registerCopied(cli) {
529
506
  });
530
507
  cli.command('completion <shell>', ui('Print a shell completion script', '打印 shell 补全脚本'))
531
508
  .action((shell) => {
532
- const script = COMPLETIONS[shell];
509
+ const script = Object.hasOwn(COMPLETIONS, shell) ? COMPLETIONS[shell] : undefined;
533
510
  if (!script)
534
- 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。'));
535
512
  console.log(script);
536
513
  });
537
514
  }
@@ -544,12 +521,12 @@ function hostStatus(config) {
544
521
  return 'fail';
545
522
  }
546
523
  }
547
- function coerceDeclared(spec, value) {
524
+ function coerceDeclared(spec, value, key) {
548
525
  const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
549
526
  if (type === 'integer' || type === 'number') {
550
527
  const parsed = Number(value);
551
528
  if (!Number.isFinite(parsed))
552
- throw new Error(`Invalid number: ${String(value)}`);
529
+ throw modelParameterError('MODEL_PARAM_INVALID', key);
553
530
  return type === 'integer' ? Math.trunc(parsed) : parsed;
554
531
  }
555
532
  if (type === 'string')
@@ -559,35 +536,108 @@ function coerceDeclared(spec, value) {
559
536
  export function outputUrls(detail) {
560
537
  return (detail.outputs ?? []).map((item) => item.url).filter((url) => typeof url === 'string' && url.length > 0);
561
538
  }
562
- export async function pollConfirmed(api, jobId, options) {
539
+ export function parsePollOptions(options) {
563
540
  const interval = Number(options.interval ?? 5);
564
541
  const timeout = Number(options.timeout ?? 1800);
565
- if (!Number.isFinite(interval) || interval < 0 || interval * 1000 > 2_147_483_647 || !Number.isFinite(timeout) || timeout < 0) {
566
- throw new Error(ui('--interval and --timeout must be non-negative numbers.', '--interval 和 --timeout 必须是非负数。'));
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');
567
544
  }
568
- const onSigint = () => {
569
- api.cancelJob(jobId).catch(() => undefined).finally(() => process.exit(130));
570
- };
571
- process.once('SIGINT', onSigint);
545
+ return { initialIntervalMs: interval * 1000, timeoutMs: timeout * 1000 };
546
+ }
547
+ export async function pollConfirmed(api, jobId, options) {
548
+ const timing = parsePollOptions(options);
549
+ return withCliWait(jobId, signal => pollJob(api, jobId, {
550
+ ...timing, signal,
551
+ onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
552
+ }));
553
+ }
554
+ async function withCliWait(jobId, wait) {
555
+ const controller = new AbortController();
556
+ const interrupt = (exitCode) => { if (!controller.signal.aborted) {
557
+ process.exitCode = exitCode;
558
+ controller.abort();
559
+ } };
560
+ const sigint = () => interrupt(130);
561
+ const sigterm = () => interrupt(143);
562
+ process.on('SIGINT', sigint);
563
+ process.on('SIGTERM', sigterm);
572
564
  try {
573
- return await pollJob(api, jobId, {
574
- initialIntervalMs: interval * 1000,
575
- timeoutMs: timeout * 1000,
576
- onProgress: (row) => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
577
- });
565
+ return await wait(controller.signal);
566
+ }
567
+ catch (error) {
568
+ const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
569
+ Object.assign(failure, { jobId });
570
+ if (failure instanceof LumenKeyError && failure.code === 'WAIT_TIMEOUT')
571
+ Object.assign(failure, { timedOut: true });
572
+ if (controller.signal.aborted)
573
+ failure.message += ui(` Resume with: lumenkey status ${jobId}`, ` 可继续查询:lumenkey status ${jobId}`);
574
+ throw failure;
578
575
  }
579
576
  finally {
580
- process.off('SIGINT', onSigint);
577
+ process.off('SIGINT', sigint);
578
+ process.off('SIGTERM', sigterm);
579
+ }
580
+ }
581
+ export async function finishGeneration(api, created, options) {
582
+ try {
583
+ const timing = parsePollOptions(options);
584
+ const result = await withCliWait(created.jobId, async (signal) => {
585
+ const detail = await pollJob(api, created.jobId, { ...timing, signal,
586
+ onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
587
+ });
588
+ return generationResult(detail, created.idempotencyKey, typeof options.output === 'string' ? options.output : undefined, signal);
589
+ });
590
+ emit(result, () => {
591
+ console.log(ui('Generation succeeded!', '生成成功!'));
592
+ console.log(ui(`Task: ${result.jobId}`, `任务:${result.jobId}`));
593
+ for (const url of result.urls)
594
+ console.log(url);
595
+ if (result.localFile)
596
+ console.log(ui(`Saved to ${result.localFile}`, `已保存到 ${result.localFile}`));
597
+ if (result.download?.ok === false)
598
+ console.error(ui(`Local save failed: ${result.download.error} (${result.download.code}). The generation result remains available above.`, `本机保存失败:${result.download.error}(${result.download.code})。生成结果仍可通过以上地址获取。`));
599
+ });
600
+ if (result.download?.ok === false && process.exitCode !== 130 && process.exitCode !== 143)
601
+ process.exitCode = 1;
602
+ }
603
+ catch (error) {
604
+ const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
605
+ Object.assign(failure, created);
606
+ throw failure;
581
607
  }
582
608
  }
609
+ export function emitJobDetail(detail) {
610
+ let failure;
611
+ try {
612
+ assertJobResult(detail, detail.status === 'succeeded');
613
+ }
614
+ catch (error) {
615
+ failure = error instanceof Error ? error : new LumenKeyError('JOB_TERMINAL_FAILURE');
616
+ }
617
+ const result = failure ? { ...detail, failureCode: errorResult(failure).code, failureMessage: failure.message } : detail;
618
+ emit(result, () => {
619
+ console.log(formatJobDetail(detail));
620
+ if (failure)
621
+ console.error(failure.message);
622
+ });
623
+ if (failure)
624
+ process.exitCode = 1;
625
+ }
583
626
  export async function promptForKey() {
584
- if (!process.stdin.isTTY || process.stdin.readableEnded || process.stdin.destroyed)
627
+ if (process.stdin.readableEnded || process.stdin.destroyed)
585
628
  return undefined;
586
- return readProfileKey();
629
+ try {
630
+ return await readProfileKey();
631
+ }
632
+ catch (error) {
633
+ if (!process.stdin.isTTY && error instanceof LumenKeyError && error.code === 'INPUT_CLOSED')
634
+ return undefined;
635
+ throw error;
636
+ }
587
637
  }
588
638
  export async function readSecret(input = process.stdin, output = process.stderr) {
589
639
  if (input.readableEnded || input.destroyed)
590
- throw new Error(ui('stdin closed before an answer was given.', '输入密钥前标准输入已关闭。'));
640
+ throw new LumenKeyError('INPUT_CLOSED');
591
641
  if (!input.isTTY) {
592
642
  const { createInterface } = await import('node:readline');
593
643
  return new Promise((resolve, reject) => {
@@ -602,7 +652,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
602
652
  resolve(line.trim());
603
653
  });
604
654
  reader.once('close', () => { if (!answered)
605
- reject(new Error(ui('stdin closed before an answer was given.', '输入密钥前标准输入已关闭。'))); });
655
+ reject(new LumenKeyError('INPUT_CLOSED')); });
606
656
  reader.once('error', reject);
607
657
  output.write(ui('Enter the key: ', '请输入密钥:'));
608
658
  });
@@ -677,14 +727,14 @@ export async function readSecret(input = process.stdin, output = process.stderr)
677
727
  }
678
728
  async function ask(prompt) {
679
729
  if (process.stdin.readableEnded || process.stdin.destroyed)
680
- throw new Error('stdin closed before an answer was given.');
730
+ throw new LumenKeyError('INPUT_CLOSED');
681
731
  const readline = await import('node:readline');
682
732
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
683
733
  return await new Promise((resolve, reject) => {
684
734
  let answered = false;
685
735
  rl.once('line', (line) => { answered = true; rl.close(); resolve(line); });
686
736
  rl.once('close', () => { if (!answered)
687
- reject(new Error('stdin closed before an answer was given.')); });
737
+ reject(new LumenKeyError('INPUT_CLOSED')); });
688
738
  rl.setPrompt(prompt);
689
739
  rl.prompt();
690
740
  });