@lumenkey/cli 0.3.0 → 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +51 -2
  2. package/dist/cli/copied.js +86 -85
  3. package/dist/cli/index.js +56 -44
  4. package/dist/cli/render.js +5 -1
  5. package/dist/core/asset-stream.d.ts +16 -0
  6. package/dist/core/asset-stream.js +64 -0
  7. package/dist/core/atomic-config-file.js +8 -7
  8. package/dist/core/chinese-messages.d.ts +4 -0
  9. package/dist/core/chinese-messages.js +907 -0
  10. package/dist/core/chinese-script.d.ts +7 -0
  11. package/dist/core/chinese-script.js +28 -0
  12. package/dist/core/client.js +2 -1
  13. package/dist/core/config-dir.js +2 -1
  14. package/dist/core/config.js +7 -6
  15. package/dist/core/download-root.d.ts +5 -0
  16. package/dist/core/download-root.js +64 -0
  17. package/dist/core/download.js +15 -12
  18. package/dist/core/generation-input.js +3 -2
  19. package/dist/core/generation.d.ts +8 -1
  20. package/dist/core/generation.js +13 -10
  21. package/dist/core/job-display.js +7 -6
  22. package/dist/core/locale.d.ts +2 -0
  23. package/dist/core/locale.js +172 -142
  24. package/dist/core/outcome.d.ts +6 -0
  25. package/dist/core/outcome.js +17 -4
  26. package/dist/core/output-path.d.ts +3 -1
  27. package/dist/core/output-path.js +5 -2
  28. package/dist/core/params.js +26 -25
  29. package/dist/core/profiles.js +5 -4
  30. package/dist/core/set-values.d.ts +0 -1
  31. package/dist/core/set-values.js +7 -7
  32. package/dist/core/submission-journal.js +43 -17
  33. package/dist/core/version.js +2 -3
  34. package/dist/mcp/server.js +30 -29
  35. package/dist/studio/asset-integrity-hash.d.ts +28 -0
  36. package/dist/studio/asset-integrity-hash.js +93 -0
  37. package/dist/studio/asset-integrity-queue.d.ts +24 -0
  38. package/dist/studio/asset-integrity-queue.js +94 -0
  39. package/dist/studio/asset-store.d.ts +52 -0
  40. package/dist/studio/asset-store.js +352 -0
  41. package/dist/studio/canvas/LICENSES.txt +1590 -0
  42. package/dist/studio/canvas/dependencies.json +862 -0
  43. package/dist/studio/canvas/director-source-manifest.json +47 -0
  44. package/dist/studio/canvas/editor.css +239 -0
  45. package/dist/studio/canvas/editor.js +4613 -0
  46. package/dist/studio/canvas/integration-source-manifest.json +384 -0
  47. package/dist/studio/canvas/source-manifest.json +240 -0
  48. package/dist/studio/canvas-store.d.ts +1254 -0
  49. package/dist/studio/canvas-store.js +205 -0
  50. package/dist/studio/capture-placement.d.ts +11 -0
  51. package/dist/studio/capture-placement.js +85 -0
  52. package/dist/studio/cloud-contract.d.ts +128 -0
  53. package/dist/studio/cloud-contract.js +229 -0
  54. package/dist/studio/cloud-service.d.ts +178 -0
  55. package/dist/studio/cloud-service.js +202 -0
  56. package/dist/studio/director-captures.d.ts +59 -0
  57. package/dist/studio/director-captures.js +141 -0
  58. package/dist/studio/director-contract.d.ts +1489 -0
  59. package/dist/studio/director-contract.js +288 -0
  60. package/dist/studio/director-png.d.ts +6 -0
  61. package/dist/studio/director-png.js +41 -0
  62. package/dist/studio/director-relations.d.ts +40 -0
  63. package/dist/studio/director-relations.js +103 -0
  64. package/dist/studio/document-authority.d.ts +7 -0
  65. package/dist/studio/document-authority.js +50 -0
  66. package/dist/studio/draft-store.d.ts +95 -0
  67. package/dist/studio/draft-store.js +100 -0
  68. package/dist/studio/errors.d.ts +85 -0
  69. package/dist/studio/errors.js +94 -0
  70. package/dist/studio/launch.d.ts +5 -0
  71. package/dist/studio/launch.js +31 -0
  72. package/dist/studio/media.d.ts +9 -0
  73. package/dist/studio/media.js +50 -0
  74. package/dist/studio/output-save-contract.d.ts +1321 -0
  75. package/dist/studio/output-save-contract.js +26 -0
  76. package/dist/studio/output-save-journal.d.ts +971 -0
  77. package/dist/studio/output-save-journal.js +287 -0
  78. package/dist/studio/output-save-recovery.d.ts +18 -0
  79. package/dist/studio/output-save-recovery.js +161 -0
  80. package/dist/studio/output-save-service.d.ts +436 -0
  81. package/dist/studio/output-save-service.js +251 -0
  82. package/dist/studio/output-save-worker.d.ts +40 -0
  83. package/dist/studio/output-save-worker.js +360 -0
  84. package/dist/studio/paths.d.ts +6 -0
  85. package/dist/studio/paths.js +68 -0
  86. package/dist/studio/run-journal.d.ts +993 -0
  87. package/dist/studio/run-journal.js +125 -0
  88. package/dist/studio/server.d.ts +17 -0
  89. package/dist/studio/server.js +440 -0
  90. package/dist/studio/session.d.ts +6 -0
  91. package/dist/studio/session.js +26 -0
  92. package/dist/studio/shell.d.ts +3 -0
  93. package/dist/studio/shell.js +23 -0
  94. package/dist/studio/style.d.ts +2 -0
  95. package/dist/studio/style.js +29 -0
  96. package/dist/studio/types.d.ts +26 -0
  97. package/dist/studio/types.js +2 -0
  98. package/dist/studio/web/app.d.ts +2 -0
  99. package/dist/studio/web/app.js +662 -0
  100. package/dist/studio/web/asset-controller.d.ts +37 -0
  101. package/dist/studio/web/asset-controller.js +191 -0
  102. package/dist/studio/web/asset-model.d.ts +29 -0
  103. package/dist/studio/web/asset-model.js +51 -0
  104. package/dist/studio/web/controller.d.ts +113 -0
  105. package/dist/studio/web/controller.js +401 -0
  106. package/dist/studio/web/messages.d.ts +2 -0
  107. package/dist/studio/web/messages.js +74 -0
  108. package/dist/studio/web/model.d.ts +75 -0
  109. package/dist/studio/web/model.js +182 -0
  110. package/dist/studio/web/save-controller.d.ts +85 -0
  111. package/dist/studio/web/save-controller.js +495 -0
  112. package/dist/studio/web/save-model.d.ts +63 -0
  113. package/dist/studio/web/save-model.js +64 -0
  114. package/dist/studio/web/save-queue.d.ts +7 -0
  115. package/dist/studio/web/save-queue.js +62 -0
  116. package/dist/studio/web/save-view.d.ts +9 -0
  117. package/dist/studio/web/save-view.js +19 -0
  118. package/dist/studio/writer-lock.d.ts +5 -0
  119. package/dist/studio/writer-lock.js +88 -0
  120. package/package.json +11 -5
  121. package/dist/cli/copied.d.ts.map +0 -1
  122. package/dist/cli/copied.js.map +0 -1
  123. package/dist/cli/index.d.ts.map +0 -1
  124. package/dist/cli/index.js.map +0 -1
  125. package/dist/cli/render.d.ts.map +0 -1
  126. package/dist/cli/render.js.map +0 -1
  127. package/dist/core/atomic-config-file.d.ts.map +0 -1
  128. package/dist/core/atomic-config-file.js.map +0 -1
  129. package/dist/core/chat.d.ts.map +0 -1
  130. package/dist/core/chat.js.map +0 -1
  131. package/dist/core/client.d.ts.map +0 -1
  132. package/dist/core/client.js.map +0 -1
  133. package/dist/core/config-dir.d.ts.map +0 -1
  134. package/dist/core/config-dir.js.map +0 -1
  135. package/dist/core/config-error.d.ts.map +0 -1
  136. package/dist/core/config-error.js.map +0 -1
  137. package/dist/core/config.d.ts.map +0 -1
  138. package/dist/core/config.js.map +0 -1
  139. package/dist/core/credit-cap.d.ts.map +0 -1
  140. package/dist/core/credit-cap.js.map +0 -1
  141. package/dist/core/download.d.ts.map +0 -1
  142. package/dist/core/download.js.map +0 -1
  143. package/dist/core/error-result.d.ts.map +0 -1
  144. package/dist/core/error-result.js.map +0 -1
  145. package/dist/core/explain.d.ts.map +0 -1
  146. package/dist/core/explain.js.map +0 -1
  147. package/dist/core/generation-input.d.ts.map +0 -1
  148. package/dist/core/generation-input.js.map +0 -1
  149. package/dist/core/generation-result.d.ts.map +0 -1
  150. package/dist/core/generation-result.js.map +0 -1
  151. package/dist/core/generation.d.ts.map +0 -1
  152. package/dist/core/generation.js.map +0 -1
  153. package/dist/core/job-display.d.ts.map +0 -1
  154. package/dist/core/job-display.js.map +0 -1
  155. package/dist/core/locale.d.ts.map +0 -1
  156. package/dist/core/locale.js.map +0 -1
  157. package/dist/core/origin.d.ts.map +0 -1
  158. package/dist/core/origin.js.map +0 -1
  159. package/dist/core/outcome.d.ts.map +0 -1
  160. package/dist/core/outcome.js.map +0 -1
  161. package/dist/core/output-path.d.ts.map +0 -1
  162. package/dist/core/output-path.js.map +0 -1
  163. package/dist/core/params.d.ts.map +0 -1
  164. package/dist/core/params.js.map +0 -1
  165. package/dist/core/poller.d.ts.map +0 -1
  166. package/dist/core/poller.js.map +0 -1
  167. package/dist/core/profiles.d.ts.map +0 -1
  168. package/dist/core/profiles.js.map +0 -1
  169. package/dist/core/set-values.d.ts.map +0 -1
  170. package/dist/core/set-values.js.map +0 -1
  171. package/dist/core/submission-journal.d.ts.map +0 -1
  172. package/dist/core/submission-journal.js.map +0 -1
  173. package/dist/core/types.d.ts.map +0 -1
  174. package/dist/core/types.js.map +0 -1
  175. package/dist/core/version.d.ts.map +0 -1
  176. package/dist/core/version.js.map +0 -1
  177. package/dist/core/wait.d.ts.map +0 -1
  178. package/dist/core/wait.js.map +0 -1
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js.map +0 -1
  181. package/dist/mcp/render.d.ts.map +0 -1
  182. package/dist/mcp/render.js.map +0 -1
  183. package/dist/mcp/server.d.ts.map +0 -1
  184. package/dist/mcp/server.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lumenkey/cli
2
2
 
3
- 本版:**0.3.0**(CLI/MCP);下文 0.2.3 均为已发布旧版的行为对照。
3
+ 本候选版:**0.4.0-rc.1**(CLI、MCP、本机 Studio、画布与 3D 导演台);下文 0.2.3 均为旧版行为对照。候选版不代表已发布。
4
4
 
5
5
  > **光钥 (LumenKey) 官方 CLI 命令行工具 & 智能体 MCP (Model Context Protocol) 统一服务包**
6
6
  > 一套代码同构支持:**人类终端命令行 (CLI)** 与 **AI Agent (Cursor / Claude Code / Codex / Antigravity) 原生工具挂载 (MCP Server)**。
@@ -88,6 +88,27 @@ CLI `--json` 与 MCP 工具结果使用**同一个结果对象**(MCP 的 `cont
88
88
 
89
89
  为兼容 0.2.x 脚本,`error`、`status`、`failureCode`、`failureMessage`、`timedOut` 继续输出,但已标记弃用:`status` 在任务失败时是任务状态、在等待超时时是 `"TIMEOUT"`,新脚本请改用 `jobStatus` / `waitStatus`。图片/视频非法 `--max-credits` 返回 `CREDIT_LIMIT_INVALID`,聊天保持 `CHAT_CREDIT_LIMIT_INVALID`;MCP 参数校验失败也返回同一结果对象(例如 `INPUT_INVALID`、`IDEMPOTENCY_KEY_REQUIRED`),不再是 SDK 的 `-32602` 文本。
90
90
 
91
+ | 兼容字段 | 含义 | 新消费者 |
92
+ | --- | --- | --- |
93
+ | `success` | 生成成功结果上的旧布尔字段,与 `ok` 同义;已弃用,并非每种结果均有此字段。 | 一律读 `ok`。 |
94
+
95
+ `submission:"unknown"` 的 `message` 是简短安全提示;完整的旧版 `error` 字符串保持兼容,不应再用它解析恢复信息。新消费者读下列字段:
96
+
97
+ | 字段 | 内容 |
98
+ | --- | --- |
99
+ | `idempotencyKey`、`quoteId` | 原提交的幂等键及报价 ID,不得为了重试而换键或参数。 |
100
+ | `recovery` | `rerun`(可续传)、`resume-command`(需显式恢复)、`expired`(已过期)、`ledger`(核对任务与余额,不在此重发)。 |
101
+ | `recoveryCommand` | 只有可安全续传时才有的 **argv 数组**,例如 `["lumenkey","tasks","resume","--key=<原幂等键>"]`。第一项是程序,其余是参数;不拼成 shell 字符串、不用 `eval`。MCP 可改调用 `lumenkey_resume_submission` 并传同一 `idempotencyKey`。 |
102
+ | `resumeDeadline` | 本机记录仍存在时的 23 小时截止时间,ISO UTC;不是新报价有效期,也不是延长服务端幂等期限。 |
103
+
104
+ 供人阅读的短 `message` 及 CLI 错误显示使用**执行机本机时区**,附 `±HH:MM` 偏移;例如 `2026-10-16 17:47 (-07:00)`。机器字段 `resumeDeadline` 保持 UTC。已过期、记录丢失或幂等冲突时不提供 `recoveryCommand`;先核对任务列表与余额,不能靠换键重提来解决 unknown。
105
+
106
+ 过期的待确认提交统一返回 `code: "SUBMISSION_RECORD_EXPIRED"`、`recovery: "expired"` 和原记录的 `resumeDeadline`,CLI 与 MCP 相同。提前拒绝时 `submission: "none"` 仅说明这次没有重发,不证明历史提交未受理;若请求发出后才超过窗口,仍保留 `submission: "unknown"`。两种情况都不提供 `recoveryCommand`,不自动创建新任务。
107
+
108
+ 内部待确认记录的绝对路径不会出现在 `message`、`detail`、`error`、通知或 stderr,损坏记录用 `record:<短哈希>` 定位。配置文件与用户指定的下载目标仍可通过明确的 `path` 字段及兼容的配置错误文本显示,便于用户修正自己的路径;下载底层异常不会直接透传。该例外不适用于内部提交记录。
109
+
110
+ 中文简繁跟随设备:`LC_ALL` → `LC_MESSAGES` → `LANG` → Windows UI 语言 → Intl locale;`zh-TW`、`zh-HK`、`zh-MO`、`zh-Hant` 使用繁体,其余回落简体。可用 `LUMENKEY_ZH_SCRIPT=Hans|Hant` 明确覆写。此设置只选择 CLI/MCP 内置文案表,不转换用户输入、路径、模型回复或错误码;`--lang en` / `LUMENKEY_LANG=en` 仍选择英文。
111
+
91
112
  **提交后的真金字段。** 只要请求了 `--confirm`,无论成功失败,结果都带 `submission`;一旦拿到报价就带 `quoteId`,一旦决定了幂等键就带 `idempotencyKey`(报价之前的失败——参数错误、没有密钥、目录不可写——只带 `submission:"none"`,没有键和报价)。`submission`:`"created"` 已创建任务(带 `jobId`)、`"none"` 什么都没创建(请求没发出,或网关返回了格式正确的 4xx)、`"unknown"` 请求已发出但没有拿到格式正确的回答(连接中断、客户端超时、任何 5xx、2xx 但响应不合约)。旧的布尔 `submitted` 只在成功结果上保留(报价 `false`、已建单 `true`),已标记弃用,不会出现在失败结果里。`"unknown"` 时网关可能已受理并冻结算力点:**不要换键重提**。客户端在 POST 前把这次提交(幂等键、quoteId、projectId、模型与输入、状态、网关地址与密钥的 SHA-256 指纹,不含密钥本身)原子写入 `~/.lumenkey/pending-submissions/`;记录只对创建它的账号和网关有效:换档案后不会自动接续别人的记录,显式带别人的键会本地返回 `SUBMISSION_CREDENTIALS_MISMATCH` 且不发请求、不动记录;同一账号换了密钥也不再匹配(记录保留并在 24 小时后过期)。观察到 `"unknown"` 后记录改为 unknown 状态。之后**用相同参数再次运行同一命令/同一工具调用**(不带键也可以):客户端找到模型与输入完全相同、状态为 unknown 的最早记录,原样重发它的请求体而不是重新报价,结果带 `resumed: true`;显式带 `--idempotency-key <同一个键>`(MCP `idempotencyKey`)则直接重发该键的记录,参数不同返回 `SUBMISSION_RETRY_MISMATCH`,不发任何请求。仍在运行的进程的 in-flight 记录不会被自动接续(stderr 会提示它的键),所以并行提交多个相同生成仍会得到多个任务;被杀死进程留下的本机记录会被接续。**恢复动词**:`lumenkey tasks resume <幂等键>`(MCP `lumenkey_resume_submission`)把记录的请求体原样重发——不读目录、不重新报价、不接受任何生成参数——遵守同样的账号绑定、23 小时窗口和重放分类,返回同样的结果对象并带 `resumed: true`;模型下架或目录变更后仍可用它找回已有任务。显式带一个新键而本机已有相同输入的未确认记录时,不会用新键发送任何内容,而是返回 `SUBMISSION_PENDING_IDENTICAL`(`submission:"none"`):结果里 `idempotencyKey` 仍是你提供的键,`pendingIdempotencyKey`、`pendingQuoteId`、`pendingRecordedAt`、`pendingAgeSeconds` 指出阻塞的那条记录,文案指引先运行 `tasks resume <那个键>`。在那条记录处理完或记录后 23 小时过期之前,无法创建第二个相同的任务(显式键的语义不会被覆盖)。`tasks` 的各子命令只接受自己的选项(`progress` 无,`wait` 为 `--interval/--timeout`,`cancel` 为 `--yes`,`resume` 为 `-o/--interval/--timeout/--key`),其他选项返回 `INPUT_INVALID`;以 `-` 开头的键用 `tasks resume --key=<键>` 或 `tasks resume -- <键>` 传入。同模型但输入不同的未确认记录、其他主机的记录、过期的记录都会在 stderr 和 `detail` 里点名,不会被静默跳过。重发时只有网关在建单步骤内给出的明确拒绝(如 `QUOTE_NOT_AVAILABLE`、`INSUFFICIENT_CREDITS`)才算 `"none"` 并删除记录;鉴权、限流等其他 4xx 不能说明第一次有没有建单,结果保持 `"unknown"`、记录保留。首次提交时也只有带业务码的 4xx 才算 `"none"`;没有业务码的 4xx(例如代理返回的 HTML)同样是 `"unknown"`。记录可接续的窗口是 23 小时(比网关的 24 小时幂等记录短一小时):过期记录不再自动接续,显式带它的键会本地返回 `SUBMISSION_RECORD_EXPIRED` 且不发请求;过期记录保留到 48 小时以便报告,之后清理。`"unknown"` 结果里的恢复建议按磁盘上的真实状态给出:记录已标记为 unknown → 直接重跑即接续;记录仍是 in-flight(标记失败)→ 需显式带键;记录已不存在 → 核对任务列表与余额。被杀死的进程留下的 in-flight 记录(本机、进程已不存在)会被相同的重跑接续;仍在运行的进程的记录不会;其他主机的记录会在 stderr 和 `detail` 中提示但不接续。
92
113
 
93
114
  **写不进记录时不提交任务。** 在报价之前就会探测记录目录是否可写,失败返回 `SUBMISSION_JOURNAL_UNWRITABLE`(带目录与原因),不会买下报价。这意味着 0.2.3 在只读 HOME(容器、CI、`HOME=/nonexistent`)下仍可 `--confirm`,而本版本会拒绝;请设置 `LUMENKEY_JOURNAL_DIR=<可写目录>` 把待确认记录放到别处(目录以 0700 创建),记录不可关闭。任务创建成功后记录删不掉只会写进 `detail`,不会改变结果。`--json --confirm` 下任务一创建就向 stderr 写一行 `{"event":"job_created","jobId":…,"idempotencyKey":…,"quoteId":…}`,stdout 仍是单个 JSON 文档;人类模式在任务号旁打印幂等键。
@@ -104,7 +125,11 @@ Ctrl+C / SIGTERM 只停止本机等待或下载,不调用取消接口;分别
104
125
 
105
126
  `--output` 下载不覆盖已有文件,单次限 120 秒、128 MiB;目标目录里超过 1 小时的残留 `.part` 文件会被清理。生成成功但保存失败时,仍返回 `status: "succeeded"`、`jobId`、`idempotencyKey` 和全部 `urls`,另带 `download: {ok:false,code,error}`;CLI 以 1 退出表示本机保存失败,MCP 保留生成成功和下载警告。请用结果地址另存,不要为下载失败重新提交付费生成。下载中断时也保留这些结果。
106
127
 
107
- `--output` 必须是包含文件名的路径;目标为目录(包括指向目录的符号链接/junction)时返回 `DOWNLOAD_IS_DIRECTORY`,已有普通文件(或指向文件的链接)则返回 `DOWNLOAD_EXISTS`,不会覆盖、删除或穿过链接写入。目标目录链只在结果开始落盘时创建,保存失败会删掉本次创建的空目录;不支持硬链接的文件系统(exFAT、部分 SMB)自动改用独占复制,仍不覆盖。两个入口都把开头的 `~/` 展开为主目录(不展开 `~user`);CLI 的相对路径按当前目录解析,MCP 的 `downloadPath` 必须是绝对路径或 `~/` 开头,否则在报价前返回 `DOWNLOAD_PATH_NOT_ABSOLUTE`(MCP 服务的工作目录不属于调用方)。下载目录白名单留待后续版本。
128
+ `--output` 必须是包含文件名的路径;目标为目录(包括指向目录的符号链接/junction)时返回 `DOWNLOAD_IS_DIRECTORY`,已有普通文件(或指向文件的链接)则返回 `DOWNLOAD_EXISTS`,不会覆盖、删除或穿过链接写入。目标目录链只在结果开始落盘时创建,保存失败会删掉本次创建的空目录;不支持硬链接的文件系统(exFAT、部分 SMB)自动改用独占复制,仍不覆盖。两个入口都把开头的 `~/` 展开为主目录(不展开 `~user`);CLI 的相对路径按当前目录解析,MCP 的 `downloadPath` 必须是绝对路径或 `~/` 开头,否则在报价前返回 `DOWNLOAD_PATH_NOT_ABSOLUTE`(MCP 服务的工作目录不属于调用方)。
129
+
130
+ 可设置绝对目录 `LUMENKEY_DOWNLOAD_ROOT` 限制 CLI/MCP 下载位置;该配置为相对路径时返回 `DOWNLOAD_ROOT_INVALID`,请修正环境变量,而非更换输出文件名。越界或任何 `..` 路径段返回 `DOWNLOAD_OUTSIDE_ROOT`,无法验证、子目录符号链接/junction 或硬链接目标返回 `DOWNLOAD_UNSAFE_PATH`。配置根目录本身按 realpath 规范化,根内子路径不能通过链接逃逸;报价前及写入/发布前复查。未设置时沿用调用方选择的目标父目录作为默认范围,**不默认限制在 cwd**;IDE 管理员需要固定白名单时应明确设置此变量。此限制不代替操作系统权限隔离,不能对抗具有同等文件系统写权限的进程在检查与系统调用之间恶意替换目录。
131
+
132
+ 行为变更:即使未设置 `LUMENKEY_DOWNLOAD_ROOT`,也不再接受带 `..` 路径段的下载路径(包括 `~/a/../file`);请传不含上级跳转的目标路径。为兼容脚本仍使用 `DOWNLOAD_OUTSIDE_ROOT`,这里表示拒绝上级跳转,不表示需要增设白名单。链接/junction/硬链接**目标**不会被覆盖或穿过写入;依检查阶段会返回 `DOWNLOAD_EXISTS`、`DOWNLOAD_IS_DIRECTORY` 或 `DOWNLOAD_UNSAFE_PATH`。未配置根时,其默认目标父目录按 realpath 处理,不代表任意祖先目录都禁止包含链接;要约束根内子目录,请显式配置固定根。
108
133
 
109
134
  `~/.lumenkey` 在首次写入时以 0700 创建,已存在但权限更宽时会被收紧(config.json、profiles.json、待确认提交记录共用同一写入路径;Windows 不应用权限位)。配置路径为符号链接或硬链接时,`config.json` 与 `profiles.json` 都返回 `CONFIG_UNSAFE_PATH`(0.2.3 对 profiles 返回 `PROFILE_UNREADABLE`,这是有意的契约变更);路径存在但不是普通文件(目录、FIFO、套接字、设备)或 `.lumenkey` 不是目录时返回 `CONFIG_NOT_A_FILE`,带 `path` 与 `detail`。
110
135
 
@@ -202,3 +227,27 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
202
227
 
203
228
  ## 📄 License
204
229
  MIT © 2026 LumenKey Team.
230
+
231
+ ## 本机 Studio(开发候选,尚未发布)
232
+
233
+ 在此源码包构建后运行:
234
+
235
+ ```powershell
236
+ node bin/lumenkey.js studio --output-dir "D:\MyAssets"
237
+ ```
238
+
239
+ 服务只在本机启动,并打开本地工作台;可将已有 PNG/JPEG/WebP/MP4/WebM 保存到指定目录的 `LumenKey` 子目录、查看与读取素材。关闭网页或重启后,素材与已保存草稿仍从磁盘清单恢复。图像/视频创作可显式连接已有 CLI 配置,按目录报价和确认提交。
240
+
241
+ 省略 `--output-dir` 可以启动,但不能保存;不自动创建项目。`--no-open` 用于无浏览器诊断,打印的地址没有会话授权,交互使用请去掉该参数重新启动。退出按 Ctrl+C;同一保存目录同时只允许一个服务。
242
+
243
+ 启动、本机素材与草稿不需要云端 Key,也不自动调用云端。开发候选页面已接入云端目录、余额、报价、明确确认生成和任务查询;只有点击“连接云端”才读取已有 CLI 配置。页面按目录展示参数,留空使用平台默认,不写死模型、价格、时长或分辨率。报价不会自动下单,提交结果未知时会阻止再次下单;不宣称退款。详见 `docs/studio/runtime-contract-v1.md`。
244
+
245
+ “保存草稿”把当前图像/视频编辑分别写入所选目录的 `LumenKey/drafts.json`,浏览器刷新或服务重启后可恢复。未保存编辑在关闭时提醒;强制结束浏览器无法保证保留未保存内容。草稿与任务输入均为明文,应保存在本人可控目录。目录被另一窗口更新时会提示冲突,不自动覆盖。
246
+
247
+ “画布”与普通创作在同一个 Studio 内:可新建画布、编辑节点与连线、打组/解组、排列、引用本机素材、撤销重做、使用快捷键和保存。保存写入 `LumenKey/canvases.json`,刷新或服务重启后恢复;未完成的参数输入也保留为草稿。离开未保存画布时可保存、留下或放弃,遇到冲突不会自动覆盖。画布的“获取报价”和“确认提交”是两个明确动作,顶栏自动执行仍未开放;查询状态不会自动下载结果。
248
+
249
+ 生成任务的“云端完成”不等于“已保存到本机”。媒体结果在任务页明确选择“保存到本机”,受本机目录授权、输出身份和保存回执约束;核验结果与历史保存事实分别显示。画布中可打开任务与手动保存入口,不会另起一套远程下载/自动导入流程。远程文字结果自动回填及站位 JSON 自动应用尚未接入受保护的本机读取链路;不要把“查询成功”当成已写回节点。
250
+
251
+ 本地 3D 导演台提供人物/物件、机位、路径、时间线、PNG 和 WebM 导出,并与同一画布文件、素材和历史关联。WebGL、录制格式和设备性能受浏览器支持限制;Windows 合成验收不代表 Mac/Linux 已验收。角色造型室仍隐藏;手机虚拟相机、全景 AI、远程 ONNX 和多段成片拼接未开放。营销/充值、自动更新、参考素材上传也不在这版的已完成能力内。
252
+
253
+ 任务记录在本机 `LumenKey/runs.json`,包含提示词但不包含 API Key,当前不加密,请选择本人可控目录。原有 CLI 和 MCP 继续独立使用。本候选版默认打包包含 Studio、画布和导演台,不需要另外下载前端源码。0.4.0-rc.1 尚待审核,不代表已发布;已发布的 0.3.1 仍是 CLI/MCP 包。
@@ -1,3 +1,4 @@
1
+ import { zhText } from '../core/chinese-messages.js';
1
2
  import os from 'node:os';
2
3
  import pc from 'picocolors';
3
4
  import { describeKey, loadConfig } from '../core/config.js';
@@ -43,7 +44,7 @@ export function applyRaw(options, key, names) {
43
44
  if (raw !== undefined)
44
45
  options[key] = raw;
45
46
  else if (options[key] !== undefined && typeof options[key] !== 'string') {
46
- throw new Error(ui(`${names[0]} could not be read as text. Use ${names[0]} <value>.`, `${names[0]} 无法按文本读取。请使用 ${names[0]} <值>。`));
47
+ throw new Error(ui(`${names[0]} could not be read as text. Use ${names[0]} <value>.`, ("" + (names[0]) + zhText("m359") + (names[0]) + zhText("m360"))));
47
48
  }
48
49
  }
49
50
  /** Plain data commands (catalog, balance, profiles) print their payload; outcomes go through renderOutcome. */
@@ -82,7 +83,7 @@ export async function withCliSignals(run) {
82
83
  }
83
84
  }
84
85
  function progressLine(status) {
85
- process.stderr.write(ui(` Status: ${jobStatusText(status)}\n`, ` 状态:${jobStatusText(status)}\n`));
86
+ process.stderr.write(ui(` Status: ${jobStatusText(status)}\n`, (zhText("m358") + (jobStatusText(status)) + "\n")));
86
87
  }
87
88
  /** CLI view of generation progress. In --json mode the job identity goes to stderr the moment it exists. */
88
89
  export function cliGenerationEvents() {
@@ -91,24 +92,24 @@ export function cliGenerationEvents() {
91
92
  switch (event.type) {
92
93
  case 'quoting':
93
94
  if (!json && event.kind === 'video')
94
- console.log(pc.cyan(ui(`\n🎬 Requesting quote for model: ${pc.bold(event.modelId)}...`, `\n🎬 正在为模型 ${pc.bold(event.modelId)} 获取报价...`)));
95
+ console.log(pc.cyan(ui(`\n🎬 Requesting quote for model: ${pc.bold(event.modelId)}...`, (zhText("m356") + (pc.bold(event.modelId)) + zhText("m357")))));
95
96
  return;
96
97
  case 'submitting':
97
98
  console.error(event.resubmit
98
- ? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}) instead of creating a new job...`, `正在重发一次之前结果未确认的提交(幂等键 ${event.idempotencyKey},报价 ${event.quoteId}),不会新建任务...`)
99
- : ui('Submitting generation job...', '正在提交生成任务...'));
99
+ ? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}) instead of creating a new job...`, (zhText("m354") + (event.idempotencyKey) + zhText("m34") + (event.quoteId) + zhText("m355")))
100
+ : ui('Submitting generation job...', zhText("m353")));
100
101
  return;
101
102
  case 'job_created':
102
103
  if (json)
103
104
  console.error(JSON.stringify({ event: 'job_created', jobId: event.jobId, idempotencyKey: event.idempotencyKey, quoteId: event.quoteId }));
104
105
  else
105
- console.log(ui(`Job created: ${event.jobId} (idempotency key: ${event.idempotencyKey})`, `任务已创建:${event.jobId}(幂等键:${event.idempotencyKey})`));
106
+ console.log(ui(`Job created: ${event.jobId} (idempotency key: ${event.idempotencyKey})`, (zhText("m351") + (event.jobId) + zhText("m352") + (event.idempotencyKey) + ")")));
106
107
  return;
107
108
  case 'progress':
108
109
  progressLine(event.detail.status);
109
110
  return;
110
111
  case 'notice':
111
- console.error(ui(`Note: ${event.text}`, `注意:${event.text}`));
112
+ console.error(ui(`Note: ${event.text}`, (zhText("m350") + (event.text) + "")));
112
113
  return;
113
114
  default:
114
115
  return;
@@ -121,19 +122,19 @@ export function printGeneration(result) {
121
122
  return;
122
123
  }
123
124
  if ('requiresConfirmation' in result) {
124
- console.log(ui(`Quote ID: ${result.quoteId} (${result.credits} credits)`, `报价编号:${result.quoteId}(${result.credits} 算力点)`));
125
- console.log(ui('Quote only. No job submitted.', '只完成报价,没有提交任务。'));
125
+ console.log(ui(`Quote ID: ${result.quoteId} (${result.credits} credits)`, (zhText("m348") + (result.quoteId) + "(" + (result.credits) + zhText("m349"))));
126
+ console.log(ui('Quote only. No job submitted.', zhText("m347")));
126
127
  return;
127
128
  }
128
129
  const done = result;
129
- console.log(ui('Generation succeeded!', '生成成功!'));
130
- console.log(ui(`Task: ${done.jobId}`, `任务:${done.jobId}`));
130
+ console.log(ui('Generation succeeded!', zhText("m346")));
131
+ console.log(ui(`Task: ${done.jobId}`, (zhText("m50") + (done.jobId) + "")));
131
132
  for (const url of done.urls)
132
133
  console.log(url);
133
134
  if (done.localFile)
134
- console.log(ui(`Saved to ${done.localFile}`, `已保存到 ${done.localFile}`));
135
+ console.log(ui(`Saved to ${done.localFile}`, (zhText("m345") + (done.localFile) + "")));
135
136
  if (done.download?.ok === false)
136
- console.error(ui(`Local save failed: ${done.download.error} (${done.download.code}). The generation result remains available above.`, `本机保存失败:${done.download.error}(${done.download.code})。生成结果仍可通过以上地址获取。`));
137
+ console.error(ui(`Local save failed: ${done.download.error} (${done.download.code}). The generation result remains available above.`, (zhText("m343") + (done.download.error) + "(" + (done.download.code) + zhText("m344"))));
137
138
  }
138
139
  /** Shared by `video` and `image generate`: options come from the core spec, values go through the core normalizer, one use case, one renderer. */
139
140
  export async function runCliGeneration(kind, spec, options) {
@@ -165,51 +166,51 @@ export function registerOptions(command, spec) {
165
166
  }
166
167
  function printProfiles(listed) {
167
168
  if (!listed.profiles.length) {
168
- console.log(ui('No profiles configured. Create one with: lumenkey profile create <name>', '还没有档案。创建一个:lumenkey profile create <name>'));
169
+ console.log(ui('No profiles configured. Create one with: lumenkey profile create <name>', zhText("m342")));
169
170
  return;
170
171
  }
171
172
  for (const row of listed.profiles)
172
173
  console.log(`${row.isSet ? '* ' : ' '}${row.profile} ${row.settings.join(', ')}`);
173
- console.log(ui('* = active profile; fields shown are configured settings, not their values.', '* 表示当前档案;列出的是已配置的项,不是它们的值。'));
174
+ console.log(ui('* = active profile; fields shown are configured settings, not their values.', zhText("m341")));
174
175
  }
175
176
  function printModel(model) {
176
177
  console.log(`${model.id} — ${model.displayName || model.name || model.id}`);
177
178
  for (const param of explainModel(model).params) {
178
- const choices = param.options.length ? `; ${ui('allowed', '允许')}: ${param.options.map(value => JSON.stringify(value)).join(', ')}` : '';
179
- const defaultValue = param.default === null ? '' : `; ${ui('default', '默认')}: ${JSON.stringify(param.default)}`;
180
- console.log(`${param.name}: ${param.required ? ui('required', '必填') : ui('optional', '可选')}${choices}${defaultValue}`);
179
+ const choices = param.options.length ? `; ${ui('allowed', zhText("m340"))}: ${param.options.map(value => JSON.stringify(value)).join(', ')}` : '';
180
+ const defaultValue = param.default === null ? '' : `; ${ui('default', zhText("m339"))}: ${JSON.stringify(param.default)}`;
181
+ console.log(`${param.name}: ${param.required ? ui('required', zhText("m338")) : ui('optional', zhText("m337"))}${choices}${defaultValue}`);
181
182
  }
182
- const prices = model.pricing?.map(price => `${price.credits} ${ui('credits', '算力点')}/${price.unit}`).join(', ');
183
- console.log(`${ui('Price', '价格')}: ${prices || ui('not reported in catalog', '目录未给出价格')}`);
183
+ const prices = model.pricing?.map(price => `${price.credits} ${ui('credits', zhText("m336"))}/${price.unit}`).join(', ');
184
+ console.log(`${ui('Price', zhText("m335"))}: ${prices || ui('not reported in catalog', zhText("m334"))}`);
184
185
  }
185
186
  export function registerCopied(cli) {
186
- cli.option('--json', ui('Emit machine-readable JSON', '输出机器可读的 JSON'));
187
- cli.command('model <action> [name]', ui('model search | get | explain', '搜索、查看或解释模型'))
187
+ cli.option('--json', ui('Emit machine-readable JSON', zhText("m333")));
188
+ cli.command('model <action> [name]', ui('model search | get | explain', zhText("m332")))
188
189
  .example('lumenkey model search wan --type video')
189
190
  .example('lumenkey model get minimax-h3')
190
191
  .example('lumenkey --json model explain minimax-h3')
191
- .option('--type <type>', ui('Filter or disambiguate by catalog type', '按目录类型筛选或消除歧义'))
192
+ .option('--type <type>', ui('Filter or disambiguate by catalog type', zhText("m331")))
192
193
  .action(async (action, name, options) => {
193
194
  applyRaw(options, 'type', ['--type']);
194
195
  validateModelType(options.type);
195
196
  if (!['search', 'get', 'explain'].includes(action))
196
- throw new Error(ui(`Unknown model command: ${action}`, `未知的 model 子命令:${action}`));
197
+ throw new Error(ui(`Unknown model command: ${action}`, (zhText("m326") + (action) + "")));
197
198
  if (action === 'search') {
198
199
  const models = findModels(await client().listModels(), name || '', options.type);
199
200
  emit({ query: name || '', count: models.length, models }, () => {
200
201
  for (const model of models)
201
202
  console.log(model.id);
202
203
  if (models.length === 0)
203
- console.log(ui('No models available for this request.', '这次请求没有可用模型。'));
204
+ console.log(ui('No models available for this request.', zhText("m330")));
204
205
  });
205
206
  return;
206
207
  }
207
208
  if (!name)
208
- throw new Error(ui('Model name is required. Try: model search', '必须提供模型名称。可先运行:model search'));
209
+ throw new Error(ui('Model name is required. Try: model search', zhText("m329")));
209
210
  if (action === 'get') {
210
211
  const exact = findModels(await client().listModels(), '').filter((model) => model.id === name || (model.displayName || model.name || '').toLowerCase() === name.toLowerCase());
211
212
  if (exact.length !== 1)
212
- throw exact.length === 0 ? new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(name)) : new Error(ui(`More than one model is named ${name}. Run: lumenkey model search ${name}`, `有多个模型叫 ${name}。请运行:lumenkey model search ${name}`));
213
+ throw exact.length === 0 ? new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(name)) : new Error(ui(`More than one model is named ${name}. Run: lumenkey model search ${name}`, (zhText("m327") + (name) + zhText("m328") + (name) + "")));
213
214
  emit(exact[0], () => printModel(exact[0]));
214
215
  return;
215
216
  }
@@ -218,36 +219,36 @@ export function registerCopied(cli) {
218
219
  const exact = all.filter((model) => model.id === name || (model.displayName || model.name || '').toLowerCase() === name.toLowerCase());
219
220
  const chosen = exact.length === 1 ? exact[0] : findModels(all, name, options.type).length === 1 ? findModels(all, name, options.type)[0] : undefined;
220
221
  if (!chosen)
221
- throw exact.length === 0 ? new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(name)) : new Error(ui(`More than one model is named ${name}. Run: lumenkey model search ${name}`, `有多个模型叫 ${name}。请运行:lumenkey model search ${name}`));
222
+ throw exact.length === 0 ? new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(name)) : new Error(ui(`More than one model is named ${name}. Run: lumenkey model search ${name}`, (zhText("m327") + (name) + zhText("m328") + (name) + "")));
222
223
  const explained = explainModel(chosen);
223
224
  emit(explained, () => printModel(chosen));
224
225
  return;
225
226
  }
226
- throw new Error(ui(`Unknown model command: ${action}`, `未知的 model 子命令:${action}`));
227
+ throw new Error(ui(`Unknown model command: ${action}`, (zhText("m326") + (action) + "")));
227
228
  });
228
- registerOptions(cli.command('image <action>', ui('image generate', '生成图片'))
229
+ registerOptions(cli.command('image <action>', ui('image generate', zhText("m325")))
229
230
  .example('lumenkey image generate -m wan2.7-image -p "red circle"')
230
231
  .example('lumenkey image generate -m wan2.7-image -p "red circle" --set seed=2 --dry-run'), imageParams)
231
232
  .action(async (action, options) => {
232
233
  if (action !== 'generate')
233
- throw new Error(ui(`Unknown image command: ${action}`, `未知的 image 子命令:${action}`));
234
+ throw new Error(ui(`Unknown image command: ${action}`, (zhText("m324") + (action) + "")));
234
235
  await runCliGeneration('image', imageParams, options);
235
236
  });
236
- registerOptions(cli.command('tasks <action> [jobId]', ui('tasks progress | wait | cancel | resume', '查看进度、等待、取消任务,或重发一次记录的提交'))
237
+ registerOptions(cli.command('tasks <action> [jobId]', ui('tasks progress | wait | cancel | resume', zhText("m323")))
237
238
  .example('lumenkey tasks progress <jobId>')
238
239
  .example('lumenkey tasks wait <jobId> --interval 5 --timeout 1800')
239
240
  .example('lumenkey tasks cancel <jobId> --yes')
240
241
  .example('lumenkey tasks resume <idempotencyKey>')
241
242
  .example('lumenkey tasks resume --key=<idempotencyKey> (for a key that begins with -)'), resumeParams)
242
- .option('--yes', ui('Confirm cancellation', '确认取消'))
243
- .option('--key <idempotencyKey>', ui('The idempotency key for tasks resume; use --key=<key> or -- <key> when the key begins with -', 'tasks resume 的幂等键;键以 - 开头时用 --key=<键> 或 -- <键>'))
243
+ .option('--yes', ui('Confirm cancellation', zhText("m322")))
244
+ .option('--key <idempotencyKey>', ui('The idempotency key for tasks resume; use --key=<key> or -- <key> when the key begins with -', zhText("m321")))
244
245
  .action(async (action, positional, options) => {
245
246
  // Each subcommand owns its options; any other option is as unknown here as on the MCP surface.
246
247
  rejectForeignTaskOptions(action);
247
248
  applyRaw(options, 'key', ['--key']);
248
249
  const jobId = positional ?? firstDoubleDashArgument(options) ?? (typeof options.key === 'string' ? options.key : undefined);
249
250
  if (jobId === undefined)
250
- throw new UsageArgumentError(ui(`missing required args for command \`tasks ${action}\``, `命令 \`tasks ${action}\` 缺少必填参数。`));
251
+ throw new UsageArgumentError(ui(`missing required args for command \`tasks ${action}\``, (zhText("m319") + (action) + zhText("m320"))));
251
252
  if (action === 'progress') {
252
253
  renderOutcome(await readJob(client(), jobId), ok => console.log(formatJobDetail(ok.job)));
253
254
  return;
@@ -276,12 +277,12 @@ export function registerCopied(cli) {
276
277
  if (options.yes !== true)
277
278
  throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
278
279
  const result = await client().cancelJob(jobId);
279
- emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, `已请求取消任务 ${jobId},最终结算由网关确认。`)));
280
+ emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, (zhText("m317") + (jobId) + zhText("m318")))));
280
281
  return;
281
282
  }
282
- throw new Error(ui(`Unknown tasks command: ${action}`, `未知的 tasks 子命令:${action}`));
283
+ throw new Error(ui(`Unknown tasks command: ${action}`, (zhText("m316") + (action) + "")));
283
284
  });
284
- cli.command('profile <action> [value] [...extra]', ui('profile list | create | set | unset | key | host | delete', '列出、创建、切换或删除档案'))
285
+ cli.command('profile <action> [value] [...extra]', ui('profile list | create | set | unset | key | host | delete', zhText("m315")))
285
286
  .example('lumenkey profile create work')
286
287
  .example('lumenkey profile key')
287
288
  .example('lumenkey profile host https://123outlets.com')
@@ -290,7 +291,7 @@ export function registerCopied(cli) {
290
291
  if (extra.length || (value !== undefined && ['list', 'unset'].includes(action)))
291
292
  throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
292
293
  if (!['list', 'create', 'set', 'unset', 'key', 'host', 'delete'].includes(action)) {
293
- throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Unknown profile command: ${action}`, `未知的 profile 子命令:${action}`));
294
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Unknown profile command: ${action}`, (zhText("m303") + (action) + "")));
294
295
  }
295
296
  if (action === 'list') {
296
297
  const listed = listProfiles();
@@ -299,29 +300,29 @@ export function registerCopied(cli) {
299
300
  }
300
301
  if (action === 'unset') {
301
302
  unsetProfile();
302
- emit({ current: null }, () => console.log(ui('Default profile unset.', '已取消默认档案。')));
303
+ emit({ current: null }, () => console.log(ui('Default profile unset.', zhText("m314"))));
303
304
  return;
304
305
  }
305
306
  if (action === 'key') {
306
307
  const target = value ?? currentProfile().name;
307
308
  if (!target || !listProfiles().profiles.some(row => row.profile === target))
308
- throw new ProfileMissingError(target ?? ui('(active profile)', '(当前档案)'));
309
+ throw new ProfileMissingError(target ?? ui('(active profile)', zhText("m313")));
309
310
  const key = await readProfileKey();
310
311
  const name = value ? (setNamedProfileFields(value, { key }), value) : setProfileFields({ key });
311
- emit({ profile: name, hasStoredToken: true }, () => console.log(ui(`Key set for profile ${name}.`, `已为档案 ${name} 设置密钥。`)));
312
+ emit({ profile: name, hasStoredToken: true }, () => console.log(ui(`Key set for profile ${name}.`, (zhText("m311") + (name) + zhText("m312")))));
312
313
  return;
313
314
  }
314
315
  if (action === 'host' && value === undefined)
315
- throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Profile host needs a gateway address. Use profile host <address>.', 'profile host 需要网关地址。请使用 profile host <地址>。'));
316
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Profile host needs a gateway address. Use profile host <address>.', zhText("m310")));
316
317
  if (action !== 'host' && !value)
317
- throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
318
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Profile ${action} needs a name.`, ("profile " + (action) + zhText("m295"))));
318
319
  if (value === undefined)
319
320
  throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
320
321
  if (action === 'create') {
321
322
  const result = createProfile(value);
322
323
  if (result === 'exists')
323
324
  throw new LumenKeyError('PROFILE_EXISTS');
324
- emit({ name: value, created: true }, () => console.log(ui(`Profile ${value} created.`, `已创建档案 ${value}。`)));
325
+ emit({ name: value, created: true }, () => console.log(ui(`Profile ${value} created.`, (zhText("m309") + (value) + "。"))));
325
326
  return;
326
327
  }
327
328
  if (action === 'set') {
@@ -332,14 +333,14 @@ export function registerCopied(cli) {
332
333
  if (!(err instanceof ProfileMissingError))
333
334
  throw err;
334
335
  console.error(err.message);
335
- const answer = (await ask(ui('Would you like to create it? (y/N): ', '要创建它吗?(y/N): '))).trim().toLowerCase();
336
+ const answer = (await ask(ui('Would you like to create it? (y/N): ', zhText("m308")))).trim().toLowerCase();
336
337
  if (answer !== 'y' && answer !== 'yes') {
337
- emit({ current: null, created: false }, () => console.log(ui('Profile creation cancelled.', '已取消创建档案。')));
338
+ emit({ current: null, created: false }, () => console.log(ui('Profile creation cancelled.', zhText("m307"))));
338
339
  return;
339
340
  }
340
341
  createProfile(value);
341
342
  }
342
- emit({ current: value }, () => console.log(ui(`Default profile set to ${value}.`, `默认档案已设为 ${value}。`)));
343
+ emit({ current: value }, () => console.log(ui(`Default profile set to ${value}.`, (zhText("m306") + (value) + "。"))));
343
344
  return;
344
345
  }
345
346
  if (action === 'host') {
@@ -351,34 +352,34 @@ export function registerCopied(cli) {
351
352
  throw new LumenKeyError('BASE_URL_INVALID');
352
353
  }
353
354
  const name = setProfileFields({ host: value });
354
- emit({ profile: name, host: value }, () => console.log(ui(`Host set to ${value}.`, `网关已设为 ${value}。`)));
355
+ emit({ profile: name, host: value }, () => console.log(ui(`Host set to ${value}.`, (zhText("m305") + (value) + "。"))));
355
356
  return;
356
357
  }
357
358
  if (action === 'delete') {
358
359
  const removed = deleteProfile(value);
359
360
  if (!removed)
360
361
  throw new ProfileMissingError(value);
361
- emit({ deleted: true, name: value }, () => console.log(ui(`Profile ${value} deleted.`, `已删除档案 ${value}。`)));
362
+ emit({ deleted: true, name: value }, () => console.log(ui(`Profile ${value} deleted.`, (zhText("m304") + (value) + "。"))));
362
363
  return;
363
364
  }
364
- throw new Error(ui(`Unknown profile command: ${action}`, `未知的 profile 子命令:${action}`));
365
+ throw new Error(ui(`Unknown profile command: ${action}`, (zhText("m303") + (action) + "")));
365
366
  });
366
- cli.command('auth <group> [action] [name]', ui('auth status | profile list | use | save | delete', '查看状态或管理登录档案'))
367
+ cli.command('auth <group> [action] [name]', ui('auth status | profile list | use | save | delete', zhText("m302")))
367
368
  .example('lumenkey auth profile list')
368
369
  .example('lumenkey auth profile save work')
369
370
  .example('lumenkey auth profile use work')
370
371
  .example('lumenkey auth profile delete work --yes')
371
- .option('--yes', ui('Confirm deletion', '确认删除'))
372
- .option('--store-token', ui('Store the environment key after a balance check', '余额检查通过后保存环境变量中的密钥'))
373
- .option('--no-activate', ui('Do not make the saved profile active', '保存后不把它设为当前档案'))
372
+ .option('--yes', ui('Confirm deletion', zhText("m301")))
373
+ .option('--store-token', ui('Store the environment key after a balance check', zhText("m300")))
374
+ .option('--no-activate', ui('Do not make the saved profile active', zhText("m299")))
374
375
  .action(async (group, action, name, options) => {
375
376
  if (group === 'status') {
376
377
  const body = describeKey();
377
- emit(body, () => console.log(body.keyConfigured ? ui(`key: ${body.keySource}`, `密钥:${body.keySource}`) : errorSentence('API_KEY_NOT_CONFIGURED')));
378
+ emit(body, () => console.log(body.keyConfigured ? ui(`key: ${body.keySource}`, (zhText("m298") + (body.keySource) + "")) : errorSentence('API_KEY_NOT_CONFIGURED')));
378
379
  return;
379
380
  }
380
381
  if (group !== 'profile')
381
- throw new Error(ui(`Unknown auth command: ${group}`, `未知的 auth 子命令:${group}`));
382
+ throw new Error(ui(`Unknown auth command: ${group}`, (zhText("m297") + (group) + "")));
382
383
  if (action === 'list') {
383
384
  const data = listProfiles();
384
385
  const body = { active: data.current ?? null, profiles: data.profiles.map((row) => ({ name: row.profile, settings: row.settings })) };
@@ -387,15 +388,15 @@ export function registerCopied(cli) {
387
388
  }
388
389
  if (!action || !['use', 'save', 'delete'].includes(action)) {
389
390
  throw new Error(action
390
- ? ui(`Unknown auth profile command: ${action}`, `未知的 auth profile 子命令:${action}`)
391
- : ui('Unknown auth profile command.', '未知的 auth profile 子命令。'));
391
+ ? ui(`Unknown auth profile command: ${action}`, (zhText("m290") + (action) + ""))
392
+ : ui('Unknown auth profile command.', zhText("m296")));
392
393
  }
393
394
  if (!name)
394
- throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Auth profile ${action} needs a name.`, `auth profile ${action} 需要名称。`));
395
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Auth profile ${action} needs a name.`, ("auth profile " + (action) + zhText("m295"))));
395
396
  if (action === 'use') {
396
397
  setProfile(name);
397
398
  const record = currentProfile().record;
398
- emit({ name, hasStoredToken: Boolean(record?.key), account: publicProfile(record) }, () => console.log(ui(`Profile ${name} activated.`, `已启用档案 ${name}。`)));
399
+ emit({ name, hasStoredToken: Boolean(record?.key), account: publicProfile(record) }, () => console.log(ui(`Profile ${name} activated.`, (zhText("m294") + (name) + "。"))));
399
400
  return;
400
401
  }
401
402
  if (action === 'save') {
@@ -410,22 +411,22 @@ export function registerCopied(cli) {
410
411
  setNamedProfileFields(name, { key, host: process.env.LUMENKEY_BASE_URL });
411
412
  }
412
413
  const record = listProfiles().profiles.find((row) => row.profile === name);
413
- emit({ name, active: activate, storedToken: Boolean(options.storeToken), settings: record?.settings ?? [] }, () => console.log(ui(`Auth profile ${name} saved.`, `已保存认证档案 ${name}。`)));
414
+ emit({ name, active: activate, storedToken: Boolean(options.storeToken), settings: record?.settings ?? [] }, () => console.log(ui(`Auth profile ${name} saved.`, (zhText("m293") + (name) + "。"))));
414
415
  return;
415
416
  }
416
417
  if (action === 'delete') {
417
418
  if (!options.yes)
418
- throw new Error(ui('Refusing to delete auth profile without --yes.', '未加 --yes,拒绝删除认证档案。'));
419
+ throw new Error(ui('Refusing to delete auth profile without --yes.', zhText("m292")));
419
420
  const removed = deleteProfile(name);
420
421
  if (!removed)
421
422
  throw new ProfileMissingError(name);
422
- emit({ deleted: true, name }, () => console.log(ui(`Auth profile ${name} deleted.`, `已删除认证档案 ${name}。`)));
423
+ emit({ deleted: true, name }, () => console.log(ui(`Auth profile ${name} deleted.`, (zhText("m291") + (name) + "。"))));
423
424
  return;
424
425
  }
425
- throw new Error(ui(`Unknown auth profile command: ${action}`, `未知的 auth profile 子命令:${action}`));
426
+ throw new Error(ui(`Unknown auth profile command: ${action}`, (zhText("m290") + (action) + "")));
426
427
  });
427
- cli.command('doctor', ui('Local environment summary. --online checks the key by reading balance', '本地环境摘要。--online 会读取余额来检查密钥'))
428
- .option('--online', ui('Verify the configured key', '检查已配置的密钥'))
428
+ cli.command('doctor', ui('Local environment summary. --online checks the key by reading balance', zhText("m289")))
429
+ .option('--online', ui('Verify the configured key', zhText("m288")))
429
430
  .action(async (options) => {
430
431
  const config = loadConfig();
431
432
  const checks = [
@@ -446,24 +447,24 @@ export function registerCopied(cli) {
446
447
  }
447
448
  const ok = checks.every((check) => check.status !== 'fail');
448
449
  emit({ ok, checks }, () => {
449
- console.log(ui(`cli: ${packageVersion}`, `命令行:${packageVersion}`));
450
- console.log(ui(`runtime: ${process.version}`, `运行时:${process.version}`));
451
- console.log(ui(`platform: ${process.platform} ${os.release()}`, `平台:${process.platform} ${os.release()}`));
452
- console.log(ui(`host: ${config.baseUrl}`, `网关:${config.baseUrl}`));
453
- console.log(config.apiKey ? ui('key: configured', '密钥:已配置') : ui('key: not configured', '密钥:未配置'));
450
+ console.log(ui(`cli: ${packageVersion}`, (zhText("m287") + (packageVersion) + "")));
451
+ console.log(ui(`runtime: ${process.version}`, (zhText("m286") + (process.version) + "")));
452
+ console.log(ui(`platform: ${process.platform} ${os.release()}`, (zhText("m285") + (process.platform) + " " + (os.release()) + "")));
453
+ console.log(ui(`host: ${config.baseUrl}`, (zhText("m284") + (config.baseUrl) + "")));
454
+ console.log(config.apiKey ? ui('key: configured', zhText("m283")) : ui('key: not configured', zhText("m282")));
454
455
  if (options.online) {
455
456
  const check = checks.find(row => row.name === 'onlineAuth');
456
- console.log(`${ui('onlineAuth', '在线鉴权')}: ${check?.status}${check?.message ? ` — ${check.message}` : ''}`);
457
+ console.log(`${ui('onlineAuth', zhText("m281"))}: ${check?.status}${check?.message ? ` — ${check.message}` : ''}`);
457
458
  }
458
459
  });
459
460
  if (!ok)
460
461
  process.exitCode = 1;
461
462
  });
462
- cli.command('completion <shell>', ui('Print a shell completion script', '打印 shell 补全脚本'))
463
+ cli.command('completion <shell>', ui('Print a shell completion script', zhText("m280")))
463
464
  .action((shell) => {
464
465
  const script = Object.hasOwn(COMPLETIONS, shell) ? COMPLETIONS[shell] : undefined;
465
466
  if (!script)
466
- throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Shell must be bash, zsh, tcsh, or fish.', 'shell 只能是 bash、zsh、tcsh 或 fish。'));
467
+ throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Shell must be bash, zsh, tcsh, or fish.', zhText("m279")));
467
468
  console.log(script);
468
469
  });
469
470
  }
@@ -483,7 +484,7 @@ function rejectForeignTaskOptions(action) {
483
484
  break;
484
485
  const name = arg.split('=')[0];
485
486
  if (all.includes(name) && !owned.has(name)) {
486
- throw new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown option ${JSON.stringify(name)} for tasks ${action}.`, `tasks ${action} 没有选项 ${JSON.stringify(name)}。`));
487
+ throw new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown option ${JSON.stringify(name)} for tasks ${action}.`, ("tasks " + (action) + zhText("m278") + (JSON.stringify(name)) + "。")));
487
488
  }
488
489
  }
489
490
  }
@@ -529,14 +530,14 @@ export async function readSecret(input = process.stdin, output = process.stderr)
529
530
  answered = true;
530
531
  reader.close();
531
532
  if (!line.trim())
532
- reject(new Error(ui('Enter the key: empty.', '输入密钥不能为空。')));
533
+ reject(new Error(ui('Enter the key: empty.', zhText("m276"))));
533
534
  else
534
535
  resolve(line.trim());
535
536
  });
536
537
  reader.once('close', () => { if (!answered)
537
538
  reject(new LumenKeyError('INPUT_CLOSED')); });
538
539
  reader.once('error', reject);
539
- output.write(ui('Enter the key: ', '请输入密钥:'));
540
+ output.write(ui('Enter the key: ', zhText("m272")));
540
541
  });
541
542
  }
542
543
  const wasRaw = Boolean(input.isRaw);
@@ -558,7 +559,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
558
559
  input.setRawMode(wasRaw);
559
560
  }
560
561
  catch (restoreError) {
561
- error = restoreError instanceof Error ? restoreError : new Error(ui('Could not restore terminal mode.', '无法恢复终端模式。'));
562
+ error = restoreError instanceof Error ? restoreError : new Error(ui('Could not restore terminal mode.', zhText("m277")));
562
563
  }
563
564
  if (wasPaused)
564
565
  input.pause();
@@ -566,12 +567,12 @@ export async function readSecret(input = process.stdin, output = process.stderr)
566
567
  if (error)
567
568
  reject(error);
568
569
  else if (!value.trim())
569
- reject(new Error(ui('Enter the key: empty.', '输入密钥不能为空。')));
570
+ reject(new Error(ui('Enter the key: empty.', zhText("m276"))));
570
571
  else
571
572
  resolve(value.trim());
572
573
  value = '';
573
574
  };
574
- const onEnd = () => finish(new Error(ui('stdin closed before an answer was given.', '输入密钥前标准输入已关闭。')));
575
+ const onEnd = () => finish(new Error(ui('stdin closed before an answer was given.', zhText("m275"))));
575
576
  const onError = (error) => finish(error);
576
577
  const onData = (chunk) => {
577
578
  for (const char of typeof chunk === 'string' ? chunk : decoder.write(chunk)) {
@@ -580,7 +581,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
580
581
  return;
581
582
  }
582
583
  if (char === '\u0003' || char === '\u0004') {
583
- finish(new Error(ui('Key entry cancelled.', '已取消输入密钥。')));
584
+ finish(new Error(ui('Key entry cancelled.', zhText("m274"))));
584
585
  return;
585
586
  }
586
587
  if (char === '\u007f' || char === '\b')
@@ -588,7 +589,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
588
589
  else if (char >= ' ')
589
590
  value += char;
590
591
  if (value.length > 8192) {
591
- finish(new Error(ui('Key input is too long.', '密钥输入过长。')));
592
+ finish(new Error(ui('Key input is too long.', zhText("m273"))));
592
593
  return;
593
594
  }
594
595
  }
@@ -599,11 +600,11 @@ export async function readSecret(input = process.stdin, output = process.stderr)
599
600
  input.once('end', onEnd);
600
601
  input.once('close', onEnd);
601
602
  input.once('error', onError);
602
- output.write(ui('Enter the key: ', '请输入密钥:'));
603
+ output.write(ui('Enter the key: ', zhText("m272")));
603
604
  input.resume();
604
605
  }
605
606
  catch (error) {
606
- finish(error instanceof Error ? error : new Error(ui('Could not disable terminal echo.', '无法关闭终端回显。')));
607
+ finish(error instanceof Error ? error : new Error(ui('Could not disable terminal echo.', zhText("m271"))));
607
608
  }
608
609
  });
609
610
  }