zapmyco 0.6.0 → 0.8.0

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.
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { D as eventBus, F as __require, M as APP_NAME, N as SESSION_DIR_NAME, O as buildSkillSnapshot, P as __VERSION__, a as SubAgentManager, c as logger, g as ZapmycoErrorCode, k as loadSkills, m as WebError, s as configureLogger, t as loadConfig, v as createLlmBasedAgent } from "../loader-CyGlMdl7.mjs";
2
+ import { B as APP_NAME, E as createLlmBasedAgent, F as eventBus, H as VERSION, I as buildSkillSnapshot, L as loadSkills, S as WebError, U as __require, V as SESSION_DIR_NAME, g as logger, h as configureLogger, i as AgentLlmFacade, n as loadConfig, p as SubAgentManager, t as HOME_CONFIG_PATH, w as ZapmycoErrorCode } from "../loader-C_55Y7Q_.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
- import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
6
6
  import * as os from "node:os";
7
- import { homedir } from "node:os";
7
+ import { homedir, tmpdir } from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
10
10
  import { EventEmitter } from "node:events";
11
+ import { getModels, getProviders } from "@mariozechner/pi-ai";
11
12
  import chalk, { Chalk } from "chalk";
12
13
  import { Command } from "commander";
13
- import { getModel } from "@mariozechner/pi-ai";
14
- import { CombinedAutocompleteProvider, Container, Editor, Key, ProcessTerminal, TUI, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
15
- import { spawn } from "node:child_process";
14
+ import { spawn, spawnSync } from "node:child_process";
15
+ import { CombinedAutocompleteProvider, Container, Editor, Input, Key, ProcessTerminal, SelectList, TUI, getKeybindings, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
16
+ import i18next from "i18next";
16
17
  import TurndownService from "turndown";
17
18
  import { lookup } from "node:dns/promises";
18
19
  import { Client } from "@modelcontextprotocol/sdk/client";
@@ -140,14 +141,73 @@ function createClearCommand() {
140
141
  //#endregion
141
142
  //#region src/cli/repl/commands/config-cmd.ts
142
143
  /**
144
+ * /config 命令
145
+ *
146
+ * 查看和修改当前配置信息。
147
+ */
148
+ /** 判断 key 是否可能触发原型污染 */
149
+ function isPrototypePollutionKey(key) {
150
+ return key === "__proto__" || key === "constructor" || key === "prototype";
151
+ }
152
+ function getByDotPath(obj, path) {
153
+ const keys = path.split(".");
154
+ let current = obj;
155
+ for (const key of keys) {
156
+ if (current === null || current === void 0 || typeof current !== "object") return;
157
+ if (isPrototypePollutionKey(key)) return;
158
+ current = current[key];
159
+ }
160
+ return current;
161
+ }
162
+ /**
163
+ * 通过 dot-path 设置嵌套对象属性(自动创建中间对象)
164
+ */
165
+ function setByDotPath(obj, path, value) {
166
+ const keys = path.split(".");
167
+ let current = obj;
168
+ for (let i = 0; i < keys.length - 1; i++) {
169
+ const key = keys[i];
170
+ if (isPrototypePollutionKey(key)) return;
171
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
172
+ current = current[key];
173
+ }
174
+ const lastKey = keys[keys.length - 1];
175
+ if (isPrototypePollutionKey(lastKey)) return;
176
+ current[lastKey] = value;
177
+ }
178
+ /**
179
+ * 更新 settings.json 中指定 dot-path 的值
180
+ */
181
+ function updateSettingsFile(path, value) {
182
+ try {
183
+ const raw = readFileSync(HOME_CONFIG_PATH, "utf-8");
184
+ const config = JSON.parse(raw);
185
+ let parsedValue = value;
186
+ try {
187
+ parsedValue = JSON.parse(value);
188
+ } catch {}
189
+ setByDotPath(config, path, parsedValue);
190
+ writeFileSync(HOME_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
191
+ return {
192
+ success: true,
193
+ message: ""
194
+ };
195
+ } catch (err) {
196
+ return {
197
+ success: false,
198
+ message: err instanceof Error ? err.message : String(err)
199
+ };
200
+ }
201
+ }
202
+ /**
143
203
  * 创建 config 命令定义
144
204
  */
145
205
  function createConfigCommand() {
146
206
  return {
147
207
  name: "config",
148
208
  aliases: ["cfg"],
149
- description: "查看配置 [show | get <key>]",
150
- usage: "/config [show | get <key>]",
209
+ description: "查看或修改配置 [show | get <key> | set <key> <value>]",
210
+ usage: "/config [show | get <key> | set <key> <value>]",
151
211
  handler(args, session) {
152
212
  const config = session.config;
153
213
  const renderer = session.getRenderer();
@@ -161,9 +221,9 @@ function createConfigCommand() {
161
221
  if (value !== void 0) {
162
222
  const displayValue = args[1].toLowerCase().includes("apikey") || args[1].toLowerCase().includes("api_key") ? "***已配置***" : JSON.stringify(value, null, 2);
163
223
  session.appendOutput([
164
- ``,
224
+ "",
165
225
  ` ${args[1]}: ${displayValue}`,
166
- ``
226
+ ""
167
227
  ]);
168
228
  } else session.appendOutput([
169
229
  "",
@@ -173,28 +233,40 @@ function createConfigCommand() {
173
233
  ]);
174
234
  return;
175
235
  }
236
+ if (args[0] === "set" && args[1] && args[2] !== void 0) {
237
+ const key = args[1];
238
+ const value = args.slice(2).join(" ");
239
+ const result = updateSettingsFile(key, value);
240
+ if (result.success) {
241
+ let parsedValue = value;
242
+ try {
243
+ parsedValue = JSON.parse(value);
244
+ } catch {}
245
+ setByDotPath(session.config, key, parsedValue);
246
+ session.applyConfigUpdate(key);
247
+ const displayValue = key.toLowerCase().includes("apikey") || key.toLowerCase().includes("api_key") ? "***已配置***" : value;
248
+ session.appendOutput([
249
+ "",
250
+ ` ✅ 配置已更新: ${key} = ${displayValue}`,
251
+ ` 已持久化到 ${HOME_CONFIG_PATH}`,
252
+ ""
253
+ ]);
254
+ } else session.appendOutput([
255
+ "",
256
+ ` ❌ 配置更新失败: ${result.message}`,
257
+ ""
258
+ ]);
259
+ return;
260
+ }
176
261
  session.appendOutput([
177
262
  "",
178
- " 用法: /config [show | get <key>]",
263
+ " 用法: /config [show | get <key> | set <key> <value>]",
264
+ " 示例: /config set llm.providers.deepseek.apiKey sk-xxx",
179
265
  ""
180
266
  ]);
181
267
  }
182
268
  };
183
269
  }
184
- /**
185
- * 通过 dot-path 获取嵌套对象属性
186
- *
187
- * 例如: getByPath(config, 'llm.provider') → config.llm.provider
188
- */
189
- function getByDotPath(obj, path) {
190
- const keys = path.split(".");
191
- let current = obj;
192
- for (const key of keys) {
193
- if (current === null || current === void 0 || typeof current !== "object") return;
194
- current = current[key];
195
- }
196
- return current;
197
- }
198
270
 
199
271
  //#endregion
200
272
  //#region src/cli/repl/commands/help.ts
@@ -294,6 +366,1263 @@ function createQuitCommand() {
294
366
  };
295
367
  }
296
368
 
369
+ //#endregion
370
+ //#region src/i18n/locales/en.json
371
+ var en_default = {
372
+ output: {
373
+ "welcome": "Welcome back!",
374
+ "error": {
375
+ "executionFailed": "Execution failed:",
376
+ "detail": "Details:"
377
+ },
378
+ "result": {
379
+ "title": "Execution Complete",
380
+ "goal": "Goal:",
381
+ "status": "Status:",
382
+ "success": "Success",
383
+ "partialSuccess": "Partial Success",
384
+ "failed": "Failed",
385
+ "duration": "Duration:",
386
+ "token": "Token:",
387
+ "cost": "Cost:",
388
+ "taskBreakdown": "Task Breakdown",
389
+ "subtaskCount": "subtasks",
390
+ "artifacts": "Artifacts:",
391
+ "suggestions": "Suggestions:"
392
+ },
393
+ "taskGraph": {
394
+ "title": "Task Breakdown Overview",
395
+ "total": "{{count}} subtasks, {{layers}} parallel layers",
396
+ "layer": "Layer {{index}} (parallel):"
397
+ },
398
+ "agents": {
399
+ "title": "Registered Agents",
400
+ "empty": "No registered agents",
401
+ "id": "ID",
402
+ "status": "Status",
403
+ "load": "Load",
404
+ "capability": "Capabilities"
405
+ },
406
+ "config": {
407
+ "title": "Current Configuration",
408
+ "llm": "LLM:",
409
+ "defaultModel": "Default Model:",
410
+ "provider": "Provider:",
411
+ "modelId": "Model ID:",
412
+ "inputType": "Input Type:",
413
+ "apiKey": "API Key:",
414
+ "apiKeyConfigured": "***Configured***",
415
+ "apiKeyNotConfigured": "(Not configured)",
416
+ "apiFormat": "API Format:",
417
+ "scheduler": "Scheduler:",
418
+ "maxConcurrency": "Max Concurrency:",
419
+ "maxPerAgent": "Max Per Agent:",
420
+ "taskTimeout": "Task Timeout:",
421
+ "maxRetries": "Max Retries:",
422
+ "cli": "CLI:",
423
+ "colorEnabled": "Enabled",
424
+ "colorDisabled": "Disabled",
425
+ "debugEnabled": "Enabled",
426
+ "debugDisabled": "Disabled",
427
+ "outputFormat": "Output Format:",
428
+ "uiLanguage": "UI Language:",
429
+ "agents": "Agents:",
430
+ "minutes": "min"
431
+ },
432
+ "history": {
433
+ "title": "Session History",
434
+ "empty": "No history yet"
435
+ },
436
+ "status": {
437
+ "title": "Session Status",
438
+ "idle": "Idle",
439
+ "executing": "Executing",
440
+ "closing": "Closing",
441
+ "state": "State:",
442
+ "totalRequests": "Total Requests:",
443
+ "success": "Success:",
444
+ "failure": "Failure:",
445
+ "tokenConsumption": "Token Usage:",
446
+ "totalCost": "Total Cost:"
447
+ }
448
+ },
449
+ dialog: {
450
+ "footer": {
451
+ "search": "Type to search · ↑↓ Navigate · Enter Confirm · Esc Cancel",
452
+ "normal": "k/j ↑↓ Navigate · / Search · Enter Select · Esc/q Quit · BS/h Back",
453
+ "searchNarrow": "Enter Confirm · Esc Cancel",
454
+ "normalNarrow": "↑↓=k/j / Enter Esc/q BS/h",
455
+ "textInput": "Enter to confirm · Esc to cancel"
456
+ },
457
+ "configView": {
458
+ "narrow": "q/Esc Back",
459
+ "wide": "q/Esc Back · Enter/BS/h Back to Menu"
460
+ }
461
+ },
462
+ settings: {
463
+ "mainMenu": {
464
+ "defaultModel": "Default Model",
465
+ "manageProviders": "Manage Providers",
466
+ "viewConfig": "View Config",
467
+ "language": "Language / 语言",
468
+ "notConfigured": "not configured",
469
+ "nConfigured": "{{count}} configured",
470
+ "noneConfigured": "none configured",
471
+ "displayFullConfig": "Display full configuration details"
472
+ },
473
+ "apiKeyConfig": {
474
+ "useEnvVar": "Use env var",
475
+ "useEnvVarDesc": "Recommended — more secure",
476
+ "enterManually": "Enter manually",
477
+ "enterManuallyDesc": "Type the key directly (stored in plaintext in settings.json)",
478
+ "clear": "Clear",
479
+ "clearDesc": "Remove the configured key"
480
+ },
481
+ "providerActions": {
482
+ "configureApiKey": "Configure API Key",
483
+ "selectModel": "Select Model",
484
+ "baseUrl": "Base URL",
485
+ "setAsDefault": "Set as Default"
486
+ },
487
+ "providerEntry": {
488
+ "keyConfigured": "key configured",
489
+ "noKey": "no key",
490
+ "addProvider": "Add Provider",
491
+ "addProviderDesc": "Select from a list of known providers",
492
+ "apiFormat": "API format: {{format}}",
493
+ "openaiCompatible": "OpenAI compatible"
494
+ },
495
+ "addProvider": {
496
+ "yes": "Yes, configure API Key now",
497
+ "yesDesc": "Go to API Key settings",
498
+ "later": "Later",
499
+ "laterDesc": "Return to main menu"
500
+ },
501
+ "messages": {
502
+ "apiKeyCleared": "[ok] Cleared API key for {{provider}}",
503
+ "apiKeySetToEnv": "[ok] Set {{provider}} API key to env var ${ {{envVar}} }",
504
+ "envVarNote": "Make sure {{envVar}} is set in your shell",
505
+ "apiKeyConfigured": "[ok] Configured API key for {{provider}}",
506
+ "modelSelected": "[ok] Selected model: {{model}}",
507
+ "providerAdded": "[ok] Added provider: {{provider}}",
508
+ "baseUrlReset": "[ok] Reset {{provider}} Base URL to default",
509
+ "baseUrlSet": "[ok] Set {{provider}} Base URL: {{url}}",
510
+ "defaultModelSet": "[ok] Default model set to: {{model}}",
511
+ "languageSet": "[ok] Language set to: {{locale}}",
512
+ "restartRequired": "Some changes may require a session restart to take full effect.",
513
+ "noModels": "has no available model list",
514
+ "configureManually": "Configure models manually in settings.json or check the provider name",
515
+ "alreadyExists": "already exists, use the provider entry to configure it",
516
+ "configureFirst": "Configure a model first before setting it as default",
517
+ "checkApiKeyHelpPrefix": " Hint: check"
518
+ },
519
+ "cliMode": {
520
+ "usage": "Usage:",
521
+ "settingsUsage": "/settings — Open interactive configuration menu",
522
+ "listProvidersUsage": "/settings list-providers — List all known providers",
523
+ "listModelsUsage": "/settings list-models <name> — List available models for a provider",
524
+ "knownProviders": "Known providers:",
525
+ "noModelsAvailable": "No models available from pi-ai registry",
526
+ "hintConfigureFirst": "Hint: use /settings to configure this provider first",
527
+ "availableModels": "available models:"
528
+ },
529
+ "modelSelector": { "notConfigured": "Not configured - Enter to set API Key" }
530
+ },
531
+ session: {
532
+ "welcome": "ZapMyco: Welcome back!",
533
+ "errorPrefix": "[Error]",
534
+ "agentErrorMessage": "Agent execution failed (no detailed error information)",
535
+ "noContentError": "Model returned no content, please check your API Key configuration",
536
+ "displayName": "Zapmyco AI Assistant",
537
+ "editorNotFound": "Editor not found: {{cmd}}, please set $EDITOR environment variable",
538
+ "editorFailed": "Failed to open editor: {{message}}",
539
+ "executing": "Executing...",
540
+ "setEnvVarHint": "Set environment variable: export {{envVar}}=<your-api-key>",
541
+ "useConfigHint": "Or use in REPL: /config set llm.providers.{{provider}}.apiKey <your-key>"
542
+ }
543
+ };
544
+
545
+ //#endregion
546
+ //#region src/i18n/locales/zh-CN.json
547
+ var zh_CN_default = {
548
+ output: {
549
+ "welcome": "欢迎回来!",
550
+ "error": {
551
+ "executionFailed": "执行失败:",
552
+ "detail": "详情:"
553
+ },
554
+ "result": {
555
+ "title": "执行完成",
556
+ "goal": "目标:",
557
+ "status": "状态:",
558
+ "success": "成功",
559
+ "partialSuccess": "部分成功",
560
+ "failed": "失败",
561
+ "duration": "耗时:",
562
+ "token": "Token:",
563
+ "cost": "成本:",
564
+ "taskBreakdown": "任务拆分",
565
+ "subtaskCount": "个子任务",
566
+ "artifacts": "制品:",
567
+ "suggestions": "建议:"
568
+ },
569
+ "taskGraph": {
570
+ "title": "任务拆分概览",
571
+ "total": "共 {{count}} 个子任务,{{layers}} 层并行",
572
+ "layer": "第 {{index}} 层 (可并行):"
573
+ },
574
+ "agents": {
575
+ "title": "已注册 Agent",
576
+ "empty": "暂无已注册的 Agent",
577
+ "id": "ID",
578
+ "status": "状态",
579
+ "load": "负载",
580
+ "capability": "能力"
581
+ },
582
+ "config": {
583
+ "title": "当前配置",
584
+ "llm": "LLM:",
585
+ "defaultModel": "默认模型:",
586
+ "provider": "提供商:",
587
+ "modelId": "模型 ID:",
588
+ "inputType": "输入类型:",
589
+ "apiKey": "API Key:",
590
+ "apiKeyConfigured": "***已配置***",
591
+ "apiKeyNotConfigured": "(未配置)",
592
+ "apiFormat": "API 格式:",
593
+ "scheduler": "调度器:",
594
+ "maxConcurrency": "最大并行:",
595
+ "maxPerAgent": "单 Agent 最大并发:",
596
+ "taskTimeout": "任务超时:",
597
+ "maxRetries": "最大重试:",
598
+ "cli": "CLI:",
599
+ "colorEnabled": "开启",
600
+ "colorDisabled": "关闭",
601
+ "debugEnabled": "开启",
602
+ "debugDisabled": "关闭",
603
+ "outputFormat": "输出格式:",
604
+ "uiLanguage": "UI 语言:",
605
+ "agents": "Agents:",
606
+ "minutes": "分钟"
607
+ },
608
+ "history": {
609
+ "title": "会话历史",
610
+ "empty": "暂无历史记录"
611
+ },
612
+ "status": {
613
+ "title": "会话状态",
614
+ "idle": "空闲",
615
+ "executing": "执行中",
616
+ "closing": "关闭中",
617
+ "state": "状态:",
618
+ "totalRequests": "总请求数:",
619
+ "success": "成功:",
620
+ "failure": "失败:",
621
+ "tokenConsumption": "Token 消耗:",
622
+ "totalCost": "总成本:"
623
+ }
624
+ },
625
+ dialog: {
626
+ "footer": {
627
+ "search": "输入文字搜索 · ↑↓ 导航 · Enter 确认 · Esc 取消",
628
+ "normal": "k/j ↑↓ 导航 · / 搜索 · Enter 选择 · Esc/q 退出 · BS/h 返回",
629
+ "searchNarrow": "Enter 确认 · Esc 取消",
630
+ "normalNarrow": "↑↓=k/j / Enter Esc/q BS/h",
631
+ "textInput": "Enter to confirm · Esc to cancel"
632
+ },
633
+ "configView": {
634
+ "narrow": "q/Esc 返回",
635
+ "wide": "q/Esc 返回 · Enter/BS/h 返回菜单"
636
+ }
637
+ },
638
+ settings: {
639
+ "mainMenu": {
640
+ "defaultModel": "默认模型",
641
+ "manageProviders": "管理提供商",
642
+ "viewConfig": "查看配置",
643
+ "language": "Language / 语言",
644
+ "notConfigured": "未配置",
645
+ "nConfigured": "已配置 {{count}} 个",
646
+ "noneConfigured": "未配置",
647
+ "displayFullConfig": "显示完整配置详情"
648
+ },
649
+ "apiKeyConfig": {
650
+ "useEnvVar": "使用环境变量",
651
+ "useEnvVarDesc": "推荐 — 更安全",
652
+ "enterManually": "手动输入",
653
+ "enterManuallyDesc": "直接输入密钥(以明文存储在 settings.json 中)",
654
+ "clear": "清除",
655
+ "clearDesc": "移除已配置的密钥"
656
+ },
657
+ "providerActions": {
658
+ "configureApiKey": "配置 API Key",
659
+ "selectModel": "选择模型",
660
+ "baseUrl": "Base URL",
661
+ "setAsDefault": "设为默认"
662
+ },
663
+ "providerEntry": {
664
+ "keyConfigured": "已配置密钥",
665
+ "noKey": "无密钥",
666
+ "addProvider": "添加提供商",
667
+ "addProviderDesc": "从已知提供商列表中选择",
668
+ "apiFormat": "API 格式: {{format}}",
669
+ "openaiCompatible": "OpenAI 兼容"
670
+ },
671
+ "addProvider": {
672
+ "yes": "是,立即配置 API Key",
673
+ "yesDesc": "前往 API Key 设置",
674
+ "later": "稍后",
675
+ "laterDesc": "返回主菜单"
676
+ },
677
+ "messages": {
678
+ "apiKeyCleared": "[ok] 已清除 {{provider}} 的 API Key",
679
+ "apiKeySetToEnv": "[ok] {{provider}} 的 API Key 已设置为环境变量 ${ {{envVar}} }",
680
+ "envVarNote": "请确保 {{envVar}} 已在 shell 中设置",
681
+ "apiKeyConfigured": "[ok] 已配置 {{provider}} 的 API Key",
682
+ "modelSelected": "[ok] 已选择模型: {{model}}",
683
+ "providerAdded": "[ok] 已添加提供商: {{provider}}",
684
+ "baseUrlReset": "[ok] 已重置 {{provider}} 的 Base URL 为默认值",
685
+ "baseUrlSet": "[ok] 已设置 {{provider}} 的 Base URL: {{url}}",
686
+ "defaultModelSet": "[ok] 默认模型已设置为: {{model}}",
687
+ "languageSet": "[ok] 语言已设置为: {{locale}}",
688
+ "restartRequired": "部分更改可能需要重启会话才能完全生效。",
689
+ "noModels": "没有可用的模型列表",
690
+ "configureManually": "请在 settings.json 中手动配置模型或检查提供商名称",
691
+ "alreadyExists": "已存在,请使用该提供商条目进行配置",
692
+ "configureFirst": "请先配置模型再设为默认",
693
+ "checkApiKeyHelpPrefix": " 提示: 请检查"
694
+ },
695
+ "cliMode": {
696
+ "usage": "用法:",
697
+ "settingsUsage": "/settings — 打开交互式配置菜单",
698
+ "listProvidersUsage": "/settings list-providers — 列出所有已知提供商",
699
+ "listModelsUsage": "/settings list-models <name> — 列出提供商可用的模型",
700
+ "knownProviders": "已知提供商:",
701
+ "noModelsAvailable": "pi-ai 注册表中没有可用的模型",
702
+ "hintConfigureFirst": "提示: 使用 /settings 先配置此提供商",
703
+ "availableModels": "可用模型:"
704
+ },
705
+ "modelSelector": { "notConfigured": "未配置 - Enter 设置 API Key" }
706
+ },
707
+ session: {
708
+ "welcome": "ZapMyco: 欢迎回来!",
709
+ "errorPrefix": "[错误]",
710
+ "agentErrorMessage": "Agent 执行失败(无详细错误信息)",
711
+ "noContentError": "模型未返回任何内容,请检查 API Key 配置",
712
+ "displayName": "Zapmyco AI 助手",
713
+ "editorNotFound": "未找到编辑器: {{cmd}},请设置 $EDITOR 环境变量",
714
+ "editorFailed": "打开编辑器失败: {{message}}",
715
+ "executing": "正在执行...",
716
+ "setEnvVarHint": "请设置环境变量: export {{envVar}}=<your-api-key>",
717
+ "useConfigHint": "或在 REPL 中使用: /config set llm.providers.{{provider}}.apiKey <your-key>"
718
+ }
719
+ };
720
+
721
+ //#endregion
722
+ //#region src/i18n/index.ts
723
+ /**
724
+ * i18next 国际化模块
725
+ *
726
+ * 使用 i18next 实现国际化支持。
727
+ * 通过传入静态 resources 实现同步初始化,无需 async/await。
728
+ * 翻译文件为标准 JSON 格式,未来 React Web 界面可复用。
729
+ */
730
+ i18next.init({
731
+ lng: "zh-CN",
732
+ fallbackLng: "zh-CN",
733
+ resources: {
734
+ "zh-CN": { translation: zh_CN_default },
735
+ en: { translation: en_default }
736
+ },
737
+ initAsync: false,
738
+ interpolation: {
739
+ escapeValue: false,
740
+ prefix: "{{",
741
+ suffix: "}}"
742
+ },
743
+ returnNull: false,
744
+ returnEmptyString: true
745
+ });
746
+ const { t } = i18next;
747
+ /**
748
+ * 设置当前语言
749
+ */
750
+ function setLocale(locale) {
751
+ i18next.changeLanguage(locale).catch(() => {});
752
+ }
753
+
754
+ //#endregion
755
+ //#region src/cli/repl/components/dialogs.ts
756
+ /**
757
+ * Shared dialog components for TUI overlays
758
+ *
759
+ * Extracted from settings-cmd.ts for reuse across the REPL.
760
+ * Provides SelectList and TextInput overlay dialogs.
761
+ */
762
+ /** Overlay layout options for menus */
763
+ const OVERLAY_OPTIONS = {
764
+ width: "100%",
765
+ anchor: "top-left",
766
+ margin: {
767
+ top: 1,
768
+ bottom: 1
769
+ }
770
+ };
771
+ const SELECT_THEME = {
772
+ selectedPrefix: (text) => chalk.green(`❯ ${text}`),
773
+ selectedText: (text) => chalk.green.bold(text),
774
+ description: (text) => chalk.gray(text),
775
+ scrollInfo: (text) => chalk.gray(text),
776
+ noMatch: (text) => chalk.red(text)
777
+ };
778
+ /**
779
+ * Wraps SelectList with a footer hint showing available keybindings.
780
+ * This lets users discover navigation keys without relying on terminal conventions.
781
+ */
782
+ var SelectListWithFooter = class {
783
+ selectList;
784
+ tui;
785
+ filterText = "";
786
+ isFiltering = false;
787
+ /** Callbacks stored at wrapper level, forwarded through inner SelectList */
788
+ onSelect;
789
+ onCancel;
790
+ /** Called when user presses q/esc — exits settings entirely */
791
+ onExit;
792
+ /** Called when user presses backspace/h — goes back one level */
793
+ onBack;
794
+ constructor(tui, items, maxVisible, theme) {
795
+ this.tui = tui;
796
+ this.selectList = new SelectList(items, maxVisible, theme);
797
+ this.selectList.onSelect = (item) => {
798
+ this.onSelect?.(item);
799
+ };
800
+ this.selectList.onCancel = () => {
801
+ this.onCancel?.();
802
+ };
803
+ }
804
+ handleInput(data) {
805
+ if (this.isFiltering) if (data.length === 1 && data >= " " && data <= "~") {
806
+ this.filterText += data;
807
+ this.applyFilter();
808
+ } else if (data === "backspace" || data === "" || data === "\b") if (this.filterText.length > 0) {
809
+ this.filterText = this.filterText.slice(0, -1);
810
+ this.applyFilter();
811
+ } else {
812
+ this.isFiltering = false;
813
+ this.resetFilteredItems();
814
+ this.selectList.invalidate();
815
+ }
816
+ else if (data === "escape") {
817
+ this.isFiltering = false;
818
+ this.filterText = "";
819
+ this.resetFilteredItems();
820
+ this.selectList.invalidate();
821
+ } else if (data === "enter") {
822
+ this.isFiltering = false;
823
+ this.selectList.handleInput("enter");
824
+ } else this.selectList.handleInput(data);
825
+ else if (data === "/") {
826
+ this.isFiltering = true;
827
+ this.filterText = "";
828
+ this.selectList.invalidate();
829
+ } else if (data === "escape" || data === "q") this.onExit?.();
830
+ else if (data === "backspace" || data === "" || data === "\b" || data === "h") this.onBack?.();
831
+ else this.selectList.handleInput(data);
832
+ }
833
+ /** Apply current filter text to the SelectList (uses includes, not startsWith) */
834
+ applyFilter() {
835
+ const sl = this.selectList;
836
+ if (!this.filterText) sl.filteredItems = [...sl.items];
837
+ else {
838
+ const lower = this.filterText.toLowerCase();
839
+ sl.filteredItems = sl.items.filter((item) => item.value.toLowerCase().includes(lower));
840
+ }
841
+ sl.selectedIndex = 0;
842
+ this.selectList.invalidate();
843
+ }
844
+ /** Restore SelectList to show all unfiltered items */
845
+ resetFilteredItems() {
846
+ const sl = this.selectList;
847
+ sl.filteredItems = [...sl.items];
848
+ sl.selectedIndex = 0;
849
+ }
850
+ invalidate() {
851
+ this.selectList.invalidate();
852
+ }
853
+ render(width) {
854
+ const lines = [];
855
+ if (this.isFiltering) {
856
+ lines.push("");
857
+ lines.push(chalk.cyan(` /${this.filterText}█`));
858
+ lines.push(chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
859
+ lines.push("");
860
+ }
861
+ const listLines = this.selectList.render(width);
862
+ lines.push(...listLines);
863
+ const termHeight = this.tui.terminal.rows;
864
+ const padding = Math.max(0, termHeight - 1 - lines.length - 3);
865
+ for (let i = 0; i < padding; i++) lines.push("");
866
+ if (width >= 50) {
867
+ lines.push(chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
868
+ if (this.isFiltering) lines.push(chalk.gray(` ${t("dialog.footer.search")}`));
869
+ else lines.push(chalk.gray(` ${t("dialog.footer.normal")}`));
870
+ } else if (this.isFiltering) lines.push(chalk.gray(` ${t("dialog.footer.searchNarrow")}`));
871
+ else lines.push(chalk.gray(` ${t("dialog.footer.normalNarrow")}`));
872
+ lines.push("");
873
+ return lines;
874
+ }
875
+ };
876
+ /**
877
+ * Show a SelectList overlay and wait for user selection
878
+ * @returns The selected item, or null if cancelled
879
+ */
880
+ function showSelectList(tui, items, options) {
881
+ return new Promise((resolve) => {
882
+ const list = new SelectListWithFooter(tui, items, options?.maxVisible ?? 10, SELECT_THEME);
883
+ let handle = null;
884
+ list.onSelect = (item) => {
885
+ handle?.hide();
886
+ resolve(item);
887
+ };
888
+ list.onCancel = () => {
889
+ handle?.hide();
890
+ resolve(null);
891
+ };
892
+ list.onExit = () => {
893
+ handle?.hide();
894
+ resolve(null);
895
+ options?.onExit?.();
896
+ };
897
+ list.onBack = () => {
898
+ handle?.hide();
899
+ resolve(null);
900
+ };
901
+ handle = tui.showOverlay(list, OVERLAY_OPTIONS);
902
+ });
903
+ }
904
+ var TextInputComponent = class {
905
+ input;
906
+ label;
907
+ constructor(label, initialValue, placeholder, onSubmit, onCancel) {
908
+ this.label = label;
909
+ this.input = new Input();
910
+ if (initialValue) this.input.setValue(initialValue);
911
+ this.input.onSubmit = (value) => {
912
+ onSubmit(value === placeholder ? initialValue : value);
913
+ };
914
+ this.input.onEscape = () => {
915
+ onCancel();
916
+ };
917
+ }
918
+ get focused() {
919
+ return this.input.focused;
920
+ }
921
+ set focused(v) {
922
+ this.input.focused = v;
923
+ }
924
+ handleInput(data) {
925
+ this.input.handleInput(data);
926
+ }
927
+ invalidate() {
928
+ this.input.invalidate();
929
+ }
930
+ render(width) {
931
+ const c = chalk;
932
+ return [
933
+ "",
934
+ c.bold(` ${this.label}`),
935
+ "",
936
+ c.gray(` ${"─".repeat(Math.min(width - 4, 50))}`),
937
+ ` ${this.input.render(width - 4)[0] ?? ""}`,
938
+ c.gray(` ${"─".repeat(Math.min(width - 4, 50))}`),
939
+ "",
940
+ c.gray(` ${t("dialog.footer.textInput")}`),
941
+ ""
942
+ ];
943
+ }
944
+ };
945
+ /**
946
+ * Show a text input overlay
947
+ * @returns The entered text, or null if cancelled
948
+ */
949
+ function showTextInput(tui, label, initialValue, placeholder) {
950
+ return new Promise((resolve) => {
951
+ let handle = null;
952
+ const component = new TextInputComponent(label, initialValue, placeholder ?? "", (value) => {
953
+ handle?.hide();
954
+ resolve(value);
955
+ }, () => {
956
+ handle?.hide();
957
+ resolve(null);
958
+ });
959
+ handle = tui.showOverlay(component, {
960
+ width: "60%",
961
+ minWidth: 50,
962
+ maxHeight: 12,
963
+ anchor: "top-left"
964
+ });
965
+ });
966
+ }
967
+ /**
968
+ * A simple overlay component that displays configuration text
969
+ * and dismisses on q/Esc/Enter/BS/h.
970
+ */
971
+ var ConfigViewComponent = class {
972
+ lines;
973
+ onDismiss;
974
+ constructor(config, renderer) {
975
+ this.lines = renderer.renderConfig(config);
976
+ }
977
+ /** Set the dismiss callback from showConfigView */
978
+ setDismissCallback(cb) {
979
+ this.onDismiss = cb;
980
+ }
981
+ handleInput(data) {
982
+ if (data === "escape" || data === "q" || data === "enter" || data === "backspace" || data === "" || data === "\b" || data === "h") this.onDismiss?.();
983
+ }
984
+ invalidate() {}
985
+ render(width) {
986
+ if (width < 50) return [
987
+ ...this.lines,
988
+ "",
989
+ chalk.gray(` ${t("dialog.configView.narrow")}`)
990
+ ];
991
+ return [
992
+ ...this.lines,
993
+ "",
994
+ chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`),
995
+ chalk.gray(` ${t("dialog.configView.wide")}`),
996
+ ""
997
+ ];
998
+ }
999
+ };
1000
+ /**
1001
+ * Show the current configuration as a dismissible overlay.
1002
+ * Returns when the user dismisses it.
1003
+ */
1004
+ function showConfigView(tui, config, renderer) {
1005
+ return new Promise((resolve) => {
1006
+ const component = new ConfigViewComponent(config, renderer);
1007
+ const handle = tui.showOverlay(component, {
1008
+ width: "100%",
1009
+ anchor: "top-left",
1010
+ margin: {
1011
+ top: 1,
1012
+ bottom: 1
1013
+ }
1014
+ });
1015
+ component.setDismissCallback(() => {
1016
+ handle.hide();
1017
+ resolve();
1018
+ });
1019
+ });
1020
+ }
1021
+
1022
+ //#endregion
1023
+ //#region src/cli/repl/config-utils.ts
1024
+ /**
1025
+ * Configuration file utilities
1026
+ *
1027
+ * Shared read/write helpers for ~/.zapmyco/settings.json,
1028
+ * used by settings-cmd.ts and session.ts.
1029
+ */
1030
+ /** Read settings.json and return a mutable object */
1031
+ function readSettings() {
1032
+ try {
1033
+ return JSON.parse(readFileSync(HOME_CONFIG_PATH, "utf-8"));
1034
+ } catch {
1035
+ return {};
1036
+ }
1037
+ }
1038
+ /** Write back to settings.json */
1039
+ function writeSettings(settings) {
1040
+ writeFileSync(HOME_CONFIG_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1041
+ }
1042
+ /** Safely set a nested property (prototype-chain safe) */
1043
+ function _setByDotPath(obj, path, value) {
1044
+ const keys = path.split(".");
1045
+ let current = obj;
1046
+ for (let i = 0; i < keys.length - 1; i++) {
1047
+ const key = keys[i];
1048
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return;
1049
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
1050
+ current = current[key];
1051
+ }
1052
+ const lastKey = keys[keys.length - 1];
1053
+ if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
1054
+ current[lastKey] = value;
1055
+ }
1056
+ /** Get a nested property value via dot-path */
1057
+ function _getByDotPath(obj, path) {
1058
+ const keys = path.split(".");
1059
+ let current = obj;
1060
+ for (const key of keys) {
1061
+ if (current === null || current === void 0 || typeof current !== "object") return;
1062
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return void 0;
1063
+ current = current[key];
1064
+ }
1065
+ return current;
1066
+ }
1067
+
1068
+ //#endregion
1069
+ //#region src/cli/repl/commands/settings-cmd.ts
1070
+ /**
1071
+ * /settings command — interactive configuration menu
1072
+ *
1073
+ * TUI overlay-based graphical configuration interface:
1074
+ * - View and change the default model
1075
+ * - Configure API keys, models, and base URLs for existing providers
1076
+ * - Add new providers from a curated list
1077
+ * - All changes sync to ~/.zapmyco/settings.json in real-time
1078
+ */
1079
+ /** Curated list of known providers (sorted by popularity) */
1080
+ const KNOWN_PROVIDERS = [
1081
+ {
1082
+ id: "anthropic",
1083
+ label: "Anthropic",
1084
+ apiFormat: "anthropic"
1085
+ },
1086
+ {
1087
+ id: "openai",
1088
+ label: "OpenAI"
1089
+ },
1090
+ {
1091
+ id: "deepseek",
1092
+ label: "DeepSeek"
1093
+ },
1094
+ {
1095
+ id: "google",
1096
+ label: "Google (Gemini)"
1097
+ },
1098
+ {
1099
+ id: "mistral",
1100
+ label: "Mistral AI"
1101
+ },
1102
+ {
1103
+ id: "xai",
1104
+ label: "xAI (Grok)"
1105
+ },
1106
+ {
1107
+ id: "groq",
1108
+ label: "Groq"
1109
+ },
1110
+ {
1111
+ id: "openrouter",
1112
+ label: "OpenRouter"
1113
+ },
1114
+ {
1115
+ id: "cerebras",
1116
+ label: "Cerebras"
1117
+ },
1118
+ {
1119
+ id: "fireworks",
1120
+ label: "Fireworks AI"
1121
+ },
1122
+ {
1123
+ id: "github-copilot",
1124
+ label: "GitHub Copilot"
1125
+ },
1126
+ {
1127
+ id: "huggingface",
1128
+ label: "Hugging Face"
1129
+ },
1130
+ {
1131
+ id: "moonshotai",
1132
+ label: "Moonshot AI (Kimi)"
1133
+ },
1134
+ {
1135
+ id: "cloudflare-workers-ai",
1136
+ label: "Cloudflare Workers AI"
1137
+ },
1138
+ {
1139
+ id: "amazon-bedrock",
1140
+ label: "Amazon Bedrock"
1141
+ },
1142
+ {
1143
+ id: "zai",
1144
+ label: "ZAI"
1145
+ },
1146
+ {
1147
+ id: "minimax",
1148
+ label: "MiniMax"
1149
+ },
1150
+ {
1151
+ id: "opencode",
1152
+ label: "OpenCode"
1153
+ }
1154
+ ];
1155
+ /** Supported language locales */
1156
+ const SUPPORTED_LOCALES = [{
1157
+ value: "zh-CN",
1158
+ label: "简体中文",
1159
+ description: "Chinese (Simplified)"
1160
+ }, {
1161
+ value: "en",
1162
+ label: "English",
1163
+ description: "English"
1164
+ }];
1165
+ /** Set a dot-path value, persist to disk and hot-reload agent */
1166
+ function setConfigValue(session, dotPath, value) {
1167
+ const settings = readSettings();
1168
+ _setByDotPath(settings, dotPath, value);
1169
+ writeSettings(settings);
1170
+ _setByDotPath(session.config, dotPath, value);
1171
+ session.applyConfigUpdate(dotPath);
1172
+ }
1173
+ /** Check if a provider has an API key configured */
1174
+ function hasApiKey(config, providerName) {
1175
+ const key = _getByDotPath(config, `llm.providers.${providerName}.apiKey`);
1176
+ if (!key) return false;
1177
+ const keyStr = String(key);
1178
+ return keyStr.length > 0 && keyStr !== "${}";
1179
+ }
1180
+ /** Get available model IDs for a provider (from config or pi-ai) */
1181
+ function getProviderModels(config, providerName) {
1182
+ const models = _getByDotPath(config, `llm.providers.${providerName}.models`);
1183
+ if (models && typeof models === "object" && Object.keys(models).length > 0) return Object.keys(models);
1184
+ try {
1185
+ const piModels = getModels(providerName);
1186
+ if (piModels && piModels.length > 0) return piModels.map((m) => m.id);
1187
+ } catch {}
1188
+ return [];
1189
+ }
1190
+ /**
1191
+ * Create the /settings command definition
1192
+ */
1193
+ function createSettingsCommand() {
1194
+ return {
1195
+ name: "settings",
1196
+ aliases: ["set"],
1197
+ description: "Interactive configuration menu — manage model providers and API keys",
1198
+ usage: "/settings [list-providers | list-models <provider>]",
1199
+ async handler(args, session) {
1200
+ const tui = session.getTui();
1201
+ const config = readSettings();
1202
+ if (args.length > 0) {
1203
+ await handleCommandLine(args, session, tui, config);
1204
+ return;
1205
+ }
1206
+ await handleInteractiveMode(tui, session, config);
1207
+ }
1208
+ };
1209
+ }
1210
+ /**
1211
+ * CLI mode — quick operations via arguments
1212
+ */
1213
+ async function handleCommandLine(args, session, _tui, config) {
1214
+ switch (args[0]) {
1215
+ case "list-providers": {
1216
+ const providers = _getByDotPath(config, "llm.providers");
1217
+ const names = providers ? Object.keys(providers) : [];
1218
+ const lines = ["", t("settings.cliMode.knownProviders")];
1219
+ for (const p of KNOWN_PROVIDERS) {
1220
+ const configured = names.includes(p.id) ? " ✓" : " ";
1221
+ lines.push(` ${configured} ${p.label} (${p.id})`);
1222
+ }
1223
+ session.appendOutput(lines);
1224
+ return;
1225
+ }
1226
+ case "list-models": {
1227
+ if (!args[1]) {
1228
+ session.appendOutput([
1229
+ "",
1230
+ `${t("settings.cliMode.usage")} /settings list-models <provider>`,
1231
+ ""
1232
+ ]);
1233
+ return;
1234
+ }
1235
+ const modelIds = getProviderModels(config, args[1]);
1236
+ if (modelIds.length === 0) {
1237
+ session.appendOutput([
1238
+ "",
1239
+ `Provider "${args[1]}" ${t("settings.messages.noModels")}`,
1240
+ t("settings.cliMode.hintConfigureFirst"),
1241
+ ""
1242
+ ]);
1243
+ return;
1244
+ }
1245
+ session.appendOutput([
1246
+ "",
1247
+ `${args[1]} ${t("settings.cliMode.availableModels")}`,
1248
+ ...modelIds.map((id) => ` - ${id}`),
1249
+ ""
1250
+ ]);
1251
+ return;
1252
+ }
1253
+ default: session.appendOutput([
1254
+ "",
1255
+ t("settings.cliMode.usage"),
1256
+ ` ${t("settings.cliMode.settingsUsage")}`,
1257
+ ` ${t("settings.cliMode.listProvidersUsage")}`,
1258
+ ` ${t("settings.cliMode.listModelsUsage")}`,
1259
+ ""
1260
+ ]);
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Interactive menu mode — main flow
1265
+ */
1266
+ async function handleInteractiveMode(tui, session, config) {
1267
+ const state = { current: config };
1268
+ /**
1269
+ * Handle API Key configuration
1270
+ */
1271
+ const handleApiKeyConfig = async (providerName, _currentKey) => {
1272
+ const envVarName = `${providerName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
1273
+ const choice = await showSelectList(tui, [
1274
+ {
1275
+ value: "env",
1276
+ label: `${t("settings.apiKeyConfig.useEnvVar")} \${${envVarName}}`,
1277
+ description: t("settings.apiKeyConfig.useEnvVarDesc")
1278
+ },
1279
+ {
1280
+ value: "manual",
1281
+ label: t("settings.apiKeyConfig.enterManually"),
1282
+ description: t("settings.apiKeyConfig.enterManuallyDesc")
1283
+ },
1284
+ {
1285
+ value: "clear",
1286
+ label: t("settings.apiKeyConfig.clear"),
1287
+ description: t("settings.apiKeyConfig.clearDesc")
1288
+ }
1289
+ ], { onExit: exitAll });
1290
+ if (!choice) return;
1291
+ if (choice.value === "clear") {
1292
+ setConfigValue(session, `llm.providers.${providerName}.apiKey`, "");
1293
+ session.appendOutput([
1294
+ "",
1295
+ ` ${t("settings.messages.apiKeyCleared", { provider: providerName })}`,
1296
+ ""
1297
+ ]);
1298
+ } else if (choice.value === "env") {
1299
+ setConfigValue(session, `llm.providers.${providerName}.apiKey`, `\${${envVarName}}`);
1300
+ session.appendOutput([
1301
+ "",
1302
+ ` ${t("settings.messages.apiKeySetToEnv", {
1303
+ provider: providerName,
1304
+ envVar: envVarName
1305
+ })}`,
1306
+ ` ${t("settings.messages.envVarNote", { envVar: envVarName })}`,
1307
+ ""
1308
+ ]);
1309
+ } else if (choice.value === "manual") {
1310
+ const key = await showTextInput(tui, `Enter ${providerName} API Key`, "", "sk-...");
1311
+ if (key && key.length > 0) {
1312
+ setConfigValue(session, `llm.providers.${providerName}.apiKey`, key);
1313
+ session.appendOutput([
1314
+ "",
1315
+ ` ${t("settings.messages.apiKeyConfigured", { provider: providerName })}`,
1316
+ ""
1317
+ ]);
1318
+ }
1319
+ }
1320
+ };
1321
+ /**
1322
+ * Handle Base URL configuration
1323
+ */
1324
+ const handleBaseUrlConfig = async (providerName, _currentUrl) => {
1325
+ const url = await showTextInput(tui, `Enter ${providerName} Base URL`, "", "https://api.example.com");
1326
+ if (url !== null) {
1327
+ const configPath = `llm.providers.${providerName}.baseUrl`;
1328
+ if (url.length === 0) {
1329
+ const settings = readSettings();
1330
+ _setByDotPath(settings, configPath, void 0);
1331
+ const parent = _getByDotPath(settings, `llm.providers.${providerName}`);
1332
+ if (parent) delete parent.baseUrl;
1333
+ writeSettings(settings);
1334
+ session.appendOutput([
1335
+ "",
1336
+ ` ${t("settings.messages.baseUrlReset", { provider: providerName })}`,
1337
+ ""
1338
+ ]);
1339
+ } else {
1340
+ setConfigValue(session, configPath, url);
1341
+ session.appendOutput([
1342
+ "",
1343
+ ` ${t("settings.messages.baseUrlSet", {
1344
+ provider: providerName,
1345
+ url
1346
+ })}`,
1347
+ ""
1348
+ ]);
1349
+ }
1350
+ }
1351
+ };
1352
+ /**
1353
+ * Handle model selection
1354
+ */
1355
+ const handleModelSelect = async (providerName) => {
1356
+ const modelIds = getProviderModels(state.current, providerName);
1357
+ if (modelIds.length === 0) {
1358
+ session.appendOutput([
1359
+ "",
1360
+ ` ${providerName} ${t("settings.messages.noModels")}`,
1361
+ ` ${t("settings.messages.configureManually")}`,
1362
+ ""
1363
+ ]);
1364
+ return;
1365
+ }
1366
+ const selected = await showSelectList(tui, modelIds.map((id) => ({
1367
+ value: id,
1368
+ label: id,
1369
+ description: ""
1370
+ })), { onExit: exitAll });
1371
+ if (selected && selected.value) {
1372
+ const modelId = selected.value;
1373
+ const settings = readSettings();
1374
+ _setByDotPath(settings, `llm.providers.${providerName}.models.${modelId}`, { id: modelId });
1375
+ writeSettings(settings);
1376
+ _setByDotPath(session.config, `llm.providers.${providerName}.models`, _getByDotPath(settings, `llm.providers.${providerName}.models`) ?? {});
1377
+ session.appendOutput([
1378
+ "",
1379
+ ` ${t("settings.messages.modelSelected", { model: `${providerName}/${modelId}` })}`,
1380
+ ""
1381
+ ]);
1382
+ }
1383
+ };
1384
+ /**
1385
+ * Handle setting a provider's model as the default
1386
+ */
1387
+ const handleSetDefault = async (providerName) => {
1388
+ const modelIds = getProviderModels(state.current, providerName);
1389
+ if (modelIds.length === 0) {
1390
+ session.appendOutput([
1391
+ "",
1392
+ ` ${t("settings.messages.configureFirst")}`,
1393
+ ""
1394
+ ]);
1395
+ return;
1396
+ }
1397
+ const modelKey = `${providerName}/${modelIds[0]}`;
1398
+ setConfigValue(session, "llm.defaultModel", modelKey);
1399
+ session.appendOutput([
1400
+ "",
1401
+ ` ${t("settings.messages.defaultModelSet", { model: modelKey })}`,
1402
+ ""
1403
+ ]);
1404
+ };
1405
+ let running = true;
1406
+ const exitAll = () => {
1407
+ running = false;
1408
+ };
1409
+ while (running) {
1410
+ state.current = readSettings();
1411
+ const providers = _getByDotPath(state.current, "llm.providers") ?? {};
1412
+ const providerCount = Object.keys(providers).length;
1413
+ const choice = await showSelectList(tui, [
1414
+ {
1415
+ value: "default-model",
1416
+ label: t("settings.mainMenu.defaultModel"),
1417
+ description: String(_getByDotPath(state.current, "llm.defaultModel") ?? t("settings.mainMenu.notConfigured"))
1418
+ },
1419
+ {
1420
+ value: "manage-providers",
1421
+ label: t("settings.mainMenu.manageProviders"),
1422
+ description: providerCount > 0 ? t("settings.mainMenu.nConfigured", { count: providerCount }) : t("settings.mainMenu.noneConfigured")
1423
+ },
1424
+ {
1425
+ value: "view-config",
1426
+ label: t("settings.mainMenu.viewConfig"),
1427
+ description: t("settings.mainMenu.displayFullConfig")
1428
+ },
1429
+ {
1430
+ value: "language",
1431
+ label: t("settings.mainMenu.language"),
1432
+ description: String(_getByDotPath(state.current, "locale") ?? "zh-CN")
1433
+ }
1434
+ ], { onExit: exitAll });
1435
+ if (!choice) {
1436
+ running = false;
1437
+ break;
1438
+ }
1439
+ const value = choice.value;
1440
+ if (value === "default-model") {
1441
+ const configuredProviders = _getByDotPath(state.current, "llm.providers");
1442
+ const allProviders = getProviders();
1443
+ const enabledItems = [];
1444
+ const disabledItems = [];
1445
+ for (const providerName of allProviders) {
1446
+ const models = getProviderModels(state.current, providerName);
1447
+ if (models.length === 0) continue;
1448
+ const isEnabled = configuredProviders !== void 0 && providerName in configuredProviders && hasApiKey(state.current, providerName);
1449
+ for (const modelId of models) {
1450
+ const key = `${providerName}/${modelId}`;
1451
+ if (isEnabled) enabledItems.push({
1452
+ value: key,
1453
+ label: key,
1454
+ description: ""
1455
+ });
1456
+ else disabledItems.push({
1457
+ value: key,
1458
+ label: chalk.gray(key),
1459
+ description: chalk.gray(t("settings.modelSelector.notConfigured"))
1460
+ });
1461
+ }
1462
+ }
1463
+ const modelItems = [...enabledItems, ...disabledItems];
1464
+ if (modelItems.length === 0) {
1465
+ session.appendOutput([
1466
+ "",
1467
+ ` ${t("settings.cliMode.noModelsAvailable")}`,
1468
+ ""
1469
+ ]);
1470
+ continue;
1471
+ }
1472
+ const selected = await showSelectList(tui, modelItems, { onExit: exitAll });
1473
+ if (!selected || !selected.value) continue;
1474
+ const selectedKey = selected.value;
1475
+ const slashIndex = selectedKey.indexOf("/");
1476
+ const providerName = selectedKey.slice(0, slashIndex);
1477
+ if (configuredProviders !== void 0 && providerName in configuredProviders && hasApiKey(state.current, providerName)) {
1478
+ setConfigValue(session, "llm.defaultModel", selectedKey);
1479
+ session.appendOutput([
1480
+ "",
1481
+ ` ${t("settings.messages.defaultModelSet", { model: selectedKey })}`,
1482
+ ""
1483
+ ]);
1484
+ } else {
1485
+ if (!configuredProviders || !(providerName in configuredProviders)) {
1486
+ const known = KNOWN_PROVIDERS.find((p) => p.id === providerName);
1487
+ const newProvider = { apiKey: "" };
1488
+ if (known?.apiFormat) newProvider.apiFormat = known.apiFormat;
1489
+ const settings = readSettings();
1490
+ _setByDotPath(settings, `llm.providers.${providerName}`, newProvider);
1491
+ writeSettings(settings);
1492
+ _setByDotPath(session.config, `llm.providers.${providerName}`, newProvider);
1493
+ state.current = readSettings();
1494
+ }
1495
+ await handleApiKeyConfig(providerName, "");
1496
+ state.current = readSettings();
1497
+ if (hasApiKey(state.current, providerName)) {
1498
+ setConfigValue(session, "llm.defaultModel", selectedKey);
1499
+ session.appendOutput([
1500
+ "",
1501
+ ` ${t("settings.messages.defaultModelSet", { model: selectedKey })}`,
1502
+ ""
1503
+ ]);
1504
+ }
1505
+ }
1506
+ } else if (value === "manage-providers") {
1507
+ const providerChoice = await showSelectList(tui, [...Object.entries(providers).map(([name]) => {
1508
+ const hasK = hasApiKey(state.current, name);
1509
+ return {
1510
+ value: `provider:${name}`,
1511
+ label: name,
1512
+ description: hasK ? t("settings.providerEntry.keyConfigured") : t("settings.providerEntry.noKey")
1513
+ };
1514
+ }), {
1515
+ value: "add-provider",
1516
+ label: t("settings.providerEntry.addProvider"),
1517
+ description: t("settings.providerEntry.addProviderDesc")
1518
+ }], { onExit: exitAll });
1519
+ if (!providerChoice) continue;
1520
+ const providerValue = providerChoice.value;
1521
+ if (providerValue.startsWith("provider:")) {
1522
+ const providerName = providerValue.slice(9);
1523
+ const action = await showSelectList(tui, [
1524
+ {
1525
+ value: "api-key",
1526
+ label: t("settings.providerActions.configureApiKey"),
1527
+ description: ""
1528
+ },
1529
+ {
1530
+ value: "model",
1531
+ label: t("settings.providerActions.selectModel"),
1532
+ description: ""
1533
+ },
1534
+ {
1535
+ value: "base-url",
1536
+ label: t("settings.providerActions.baseUrl"),
1537
+ description: ""
1538
+ },
1539
+ {
1540
+ value: "set-default",
1541
+ label: t("settings.providerActions.setAsDefault"),
1542
+ description: ""
1543
+ }
1544
+ ], { onExit: exitAll });
1545
+ if (!action) continue;
1546
+ switch (action.value) {
1547
+ case "api-key":
1548
+ await handleApiKeyConfig(providerName, String(_getByDotPath(state.current, `llm.providers.${providerName}.apiKey`) ?? ""));
1549
+ break;
1550
+ case "model":
1551
+ await handleModelSelect(providerName);
1552
+ break;
1553
+ case "base-url":
1554
+ await handleBaseUrlConfig(providerName, String(_getByDotPath(state.current, `llm.providers.${providerName}.baseUrl`) ?? ""));
1555
+ break;
1556
+ case "set-default":
1557
+ await handleSetDefault(providerName);
1558
+ break;
1559
+ }
1560
+ } else if (providerValue === "add-provider") {
1561
+ const selected = await showSelectList(tui, KNOWN_PROVIDERS.map((p) => ({
1562
+ value: p.id,
1563
+ label: `${p.label} (${p.id})`,
1564
+ description: p.apiFormat ? t("settings.providerEntry.apiFormat", { format: p.apiFormat }) : t("settings.providerEntry.openaiCompatible")
1565
+ })), {
1566
+ maxVisible: 12,
1567
+ onExit: exitAll
1568
+ });
1569
+ if (!selected || !selected.value) continue;
1570
+ const providerName = selected.value;
1571
+ const existingProviders = _getByDotPath(state.current, "llm.providers");
1572
+ if (existingProviders && providerName in existingProviders) {
1573
+ session.appendOutput([
1574
+ "",
1575
+ ` ${providerName} ${t("settings.messages.alreadyExists")}`,
1576
+ ""
1577
+ ]);
1578
+ continue;
1579
+ }
1580
+ const known = KNOWN_PROVIDERS.find((p) => p.id === providerName);
1581
+ const newProvider = { apiKey: "" };
1582
+ if (known?.apiFormat) newProvider.apiFormat = known.apiFormat;
1583
+ const settings = readSettings();
1584
+ _setByDotPath(settings, `llm.providers.${providerName}`, newProvider);
1585
+ writeSettings(settings);
1586
+ _setByDotPath(session.config, `llm.providers.${providerName}`, newProvider);
1587
+ session.appendOutput([
1588
+ "",
1589
+ ` ${t("settings.messages.providerAdded", { provider: providerName })}`,
1590
+ ""
1591
+ ]);
1592
+ if ((await showSelectList(tui, [{
1593
+ value: "yes",
1594
+ label: t("settings.addProvider.yes"),
1595
+ description: t("settings.addProvider.yesDesc")
1596
+ }, {
1597
+ value: "no",
1598
+ label: t("settings.addProvider.later"),
1599
+ description: t("settings.addProvider.laterDesc")
1600
+ }], { onExit: exitAll }))?.value === "yes") await handleApiKeyConfig(providerName, "");
1601
+ }
1602
+ } else if (value === "view-config") {
1603
+ const renderer = session.getRenderer();
1604
+ await showConfigView(tui, session.config, renderer);
1605
+ } else if (value === "language") {
1606
+ const currentLocale = String(_getByDotPath(state.current, "locale") ?? "zh-CN");
1607
+ const selected = await showSelectList(tui, SUPPORTED_LOCALES.map((loc) => ({
1608
+ ...loc,
1609
+ label: loc.value === currentLocale ? `${loc.label} ✓` : loc.label
1610
+ })), { onExit: exitAll });
1611
+ if (!selected || !selected.value) continue;
1612
+ if (selected.value !== currentLocale) {
1613
+ setConfigValue(session, "locale", selected.value);
1614
+ setLocale(selected.value);
1615
+ session.appendOutput([
1616
+ "",
1617
+ ` ${t("settings.messages.languageSet", { locale: selected.value })}`,
1618
+ ` ${t("settings.messages.restartRequired")}`,
1619
+ ""
1620
+ ]);
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1625
+
297
1626
  //#endregion
298
1627
  //#region src/cli/repl/commands/status.ts
299
1628
  /**
@@ -321,6 +1650,7 @@ function createStatusCommand() {
321
1650
  * 继承自 pi-tui 的 Editor,添加 zapmyco 特有的快捷键处理:
322
1651
  * - Ctrl+C: 取消任务 / 二次退出
323
1652
  * - Ctrl+D: 退出
1653
+ * - Ctrl+O: 打开外部编辑器编辑输入
324
1654
  * - Escape: 取消当前输入
325
1655
  *
326
1656
  * 同时 override render() 以:
@@ -357,6 +1687,8 @@ var ZapmycoEditor = class extends Editor {
357
1687
  onCtrlC;
358
1688
  /** Ctrl+D 回调 */
359
1689
  onCtrlD;
1690
+ /** Ctrl+O 回调(打开外部编辑器) */
1691
+ onOpenEditor;
360
1692
  /** 是否正在执行(用于显示 loading) */
361
1693
  #executing = false;
362
1694
  /** 是否显示 spinner(执行期间禁用输入但不一定显示 spinner) */
@@ -378,6 +1710,10 @@ var ZapmycoEditor = class extends Editor {
378
1710
  if (this.getText().length === 0 && this.onCtrlD) this.onCtrlD();
379
1711
  return;
380
1712
  }
1713
+ if (matchesKey(data, Key.ctrl("o")) && this.onOpenEditor) {
1714
+ this.onOpenEditor();
1715
+ return;
1716
+ }
381
1717
  super.handleInput(data);
382
1718
  }
383
1719
  /**
@@ -1286,7 +2622,7 @@ var OutputFormatter = class {
1286
2622
  "",
1287
2623
  ` 🍄 ${c.bold(`zapmyco@${version}`)}`,
1288
2624
  "",
1289
- " 欢迎回来!",
2625
+ ` ${t("output.welcome")}`,
1290
2626
  "",
1291
2627
  c.gray("─".repeat(90)),
1292
2628
  ""
@@ -1299,8 +2635,8 @@ var OutputFormatter = class {
1299
2635
  const zapmycoError = error;
1300
2636
  if (zapmycoError.code) {
1301
2637
  lines.push(`${c.red.bold(` ✗ [${zapmycoError.code}]`)} ${error.message}`);
1302
- if (zapmycoError.context && Object.keys(zapmycoError.context).length > 0) lines.push(c.gray(` 详情: ${JSON.stringify(zapmycoError.context)}`));
1303
- } else lines.push(`${c.red.bold("执行失败:")} ${error.message}`);
2638
+ if (zapmycoError.context && Object.keys(zapmycoError.context).length > 0) lines.push(c.gray(` ${t("output.error.detail")} ${JSON.stringify(zapmycoError.context)}`));
2639
+ } else lines.push(`${c.red.bold(`${t("output.error.executionFailed")}`)} ${error.message}`);
1304
2640
  lines.push("");
1305
2641
  return lines;
1306
2642
  }
@@ -1311,16 +2647,16 @@ var OutputFormatter = class {
1311
2647
  const lines = [
1312
2648
  "",
1313
2649
  c.gray(" ┌────────────────────────────────────────────┐"),
1314
- ` │ ${statusIcon} ${c.bold("执行完成")}`,
2650
+ ` │ ${statusIcon} ${c.bold(t("output.result.title"))}`,
1315
2651
  c.gray(" ├────────────────────────────────────────────┤"),
1316
- ` │ ${c.gray("目标:")} ${result.summary.slice(0, 40)}`,
1317
- ` │ ${c.gray("状态:")} ${result.overallStatus === "success" ? c.green("成功") : result.overallStatus === "partial-failure" ? c.yellow("部分成功") : c.red("失败")}`,
1318
- ` │ ${c.gray("耗时:")} ${(result.totalDuration / 1e3).toFixed(1)}s · ${c.gray("Token:")} ${result.totalTokenUsage.totalTokens.toLocaleString()}`,
1319
- ` │ ${c.gray("成本:")} $${result.totalTokenUsage.estimatedCostUsd.toFixed(4)}`
2652
+ ` │ ${c.gray(t("output.result.goal"))} ${result.summary.slice(0, 40)}`,
2653
+ ` │ ${c.gray(t("output.result.status"))} ${result.overallStatus === "success" ? c.green(t("output.result.success")) : result.overallStatus === "partial-failure" ? c.yellow(t("output.result.partialSuccess")) : c.red(t("output.result.failed"))}`,
2654
+ ` │ ${c.gray(t("output.result.duration"))} ${(result.totalDuration / 1e3).toFixed(1)}s · ${c.gray(t("output.result.token"))} ${result.totalTokenUsage.totalTokens.toLocaleString()}`,
2655
+ ` │ ${c.gray(t("output.result.cost"))} $${result.totalTokenUsage.estimatedCostUsd.toFixed(4)}`
1320
2656
  ];
1321
2657
  if (result.taskResults.length > 0) {
1322
2658
  lines.push(c.gray(" ├────────────────────────────────────────────┤"));
1323
- lines.push(` │ ${c.bold("任务拆分")} (${result.taskResults.length} 个子任务):`);
2659
+ lines.push(` │ ${c.bold(t("output.result.taskBreakdown"))} (${result.taskResults.length} ${t("output.result.subtaskCount")}):`);
1324
2660
  for (const tr of result.taskResults) {
1325
2661
  const icon = tr.status === "success" ? c.green("✓") : tr.status === "partial" ? c.yellow("~") : c.red("✗");
1326
2662
  lines.push(` │ ${icon} ${tr.taskId.slice(0, 12)}...`);
@@ -1328,7 +2664,7 @@ var OutputFormatter = class {
1328
2664
  }
1329
2665
  if (result.allArtifacts.length > 0) {
1330
2666
  lines.push(c.gray(" ├────────────────────────────────────────────┤"));
1331
- lines.push(` │ ${c.bold("制品:")}`);
2667
+ lines.push(` │ ${c.bold(t("output.result.artifacts"))}`);
1332
2668
  for (const artifact of result.allArtifacts) {
1333
2669
  const icon = artifact.type === "pull-request" ? "🔗" : "📄";
1334
2670
  lines.push(` │ ${icon} ${artifact.description} (${artifact.reference})`);
@@ -1336,7 +2672,7 @@ var OutputFormatter = class {
1336
2672
  }
1337
2673
  if (result.nextSteps && result.nextSteps.length > 0) {
1338
2674
  lines.push(c.gray(" ├────────────────────────────────────────────┤"));
1339
- lines.push(` │ ${c.bold("建议:")}`);
2675
+ lines.push(` │ ${c.bold(t("output.result.suggestions"))}`);
1340
2676
  for (let i = 0; i < result.nextSteps.length; i++) lines.push(` │ ${i + 1}. ${result.nextSteps[i]}`);
1341
2677
  }
1342
2678
  lines.push(c.gray(" └────────────────────────────────────────────┘"));
@@ -1348,14 +2684,17 @@ var OutputFormatter = class {
1348
2684
  const c = this.getColor();
1349
2685
  const lines = [
1350
2686
  "",
1351
- c.bold(" 📋 任务拆分概览"),
1352
- c.gray(` ${graph.nodes.size} 个子任务,${graph.layers.length} 层并行`),
2687
+ c.bold(` 📋 ${t("output.taskGraph.title")}`),
2688
+ c.gray(` ${t("output.taskGraph.total", {
2689
+ count: graph.nodes.size,
2690
+ layers: graph.layers.length
2691
+ })}`),
1353
2692
  ""
1354
2693
  ];
1355
2694
  for (let layerIdx = 0; layerIdx < graph.layers.length; layerIdx++) {
1356
2695
  const layer = graph.layers[layerIdx];
1357
2696
  if (!layer) continue;
1358
- lines.push(c.gray(` ${layerIdx + 1} 层 (可并行):`));
2697
+ lines.push(c.gray(` ${t("output.taskGraph.layer", { index: layerIdx + 1 })}`));
1359
2698
  for (const taskId of layer) {
1360
2699
  const task = graph.nodes.get(taskId);
1361
2700
  if (task) {
@@ -1372,15 +2711,15 @@ var OutputFormatter = class {
1372
2711
  const c = this.getColor();
1373
2712
  const lines = [
1374
2713
  "",
1375
- c.bold(" 🤖 已注册 Agent"),
2714
+ c.bold(` 🤖 ${t("output.agents.title")}`),
1376
2715
  ""
1377
2716
  ];
1378
2717
  if (agents.length === 0) {
1379
- lines.push(c.gray(" 暂无已注册的 Agent"));
2718
+ lines.push(c.gray(` ${t("output.agents.empty")}`));
1380
2719
  lines.push("");
1381
2720
  return lines;
1382
2721
  }
1383
- lines.push(` ${c.bold("ID").padEnd(20)} ${c.bold("状态").padEnd(10)} ${c.bold("负载").padEnd(8)} ${c.bold("能力")}`);
2722
+ lines.push(` ${c.bold(t("output.agents.id")).padEnd(20)} ${c.bold(t("output.agents.status")).padEnd(10)} ${c.bold(t("output.agents.load")).padEnd(8)} ${c.bold(t("output.agents.capability"))}`);
1384
2723
  lines.push(c.gray(` ${"─".repeat(60)}`));
1385
2724
  for (const agent of agents) {
1386
2725
  const statusDot = agent.status === "online" ? c.green("●") : agent.status === "busy" ? c.yellow("●") : c.gray("○");
@@ -1395,28 +2734,33 @@ var OutputFormatter = class {
1395
2734
  const c = this.getColor();
1396
2735
  const lines = [
1397
2736
  "",
1398
- c.bold(" ⚙️ 当前配置"),
2737
+ c.bold(` ⚙️ ${t("output.config.title")}`),
1399
2738
  "",
1400
- c.bold(" LLM:")
2739
+ c.bold(` ${t("output.config.llm")}`)
1401
2740
  ];
1402
- lines.push(` 默认模型: ${config.llm.defaultModel}`);
1403
- const defaultModelConfig = config.llm.models[config.llm.defaultModel];
1404
- if (defaultModelConfig) {
1405
- lines.push(` 提供商: ${defaultModelConfig.provider}`);
1406
- lines.push(` 模型 ID: ${defaultModelConfig.modelId}`);
1407
- }
1408
- const auth = config.llm.providers[defaultModelConfig?.provider ?? "anthropic"];
1409
- lines.push(` API Key: ${auth?.apiKey ? c.gray("***已配置***") : c.red("(未配置)")}`);
1410
- lines.push(c.bold(" 调度器:"));
1411
- lines.push(` 最大并行: ${config.scheduler.maxConcurrency}`);
1412
- lines.push(` Agent 最大并发: ${config.scheduler.maxPerAgent}`);
1413
- lines.push(` 任务超时: ${(config.scheduler.taskTimeoutMs / 1e3 / 60).toFixed(0)} 分钟`);
1414
- lines.push(` 最大重试: ${config.scheduler.maxRetries}`);
1415
- lines.push(c.bold(" CLI:"));
1416
- lines.push(` 颜色输出: ${config.cli.color ? c.green("开启") : c.gray("关闭")}`);
1417
- lines.push(` 调试模式: ${config.cli.debug ? c.green("开启") : c.gray("关闭")}`);
1418
- lines.push(` 输出格式: ${config.cli.outputFormat}`);
1419
- lines.push(c.bold(" Agents:"));
2741
+ lines.push(` ${t("output.config.defaultModel")} ${config.llm.defaultModel}`);
2742
+ const defaultModelKey = config.llm.defaultModel;
2743
+ const slashIdx = defaultModelKey.indexOf("/");
2744
+ const defaultProvider = slashIdx > 0 ? defaultModelKey.slice(0, slashIdx) : "anthropic";
2745
+ const defaultModelName = slashIdx > 0 ? defaultModelKey.slice(slashIdx + 1) : defaultModelKey;
2746
+ const providerConfig = config.llm.providers[defaultProvider];
2747
+ const modelConfig = providerConfig?.models?.[defaultModelName];
2748
+ lines.push(` ${t("output.config.provider")} ${defaultProvider}`);
2749
+ lines.push(` ${t("output.config.modelId")} ${modelConfig?.id ?? defaultModelName}`);
2750
+ if (modelConfig?.input && modelConfig.input.length > 0) lines.push(` ${t("output.config.inputType")} ${modelConfig.input.join(", ")}`);
2751
+ lines.push(` ${t("output.config.apiKey")} ${providerConfig?.apiKey ? c.gray(t("output.config.apiKeyConfigured")) : c.red(t("output.config.apiKeyNotConfigured"))}`);
2752
+ if (providerConfig?.apiFormat) lines.push(` ${t("output.config.apiFormat")} ${providerConfig.apiFormat}`);
2753
+ lines.push(c.bold(` ${t("output.config.scheduler")}`));
2754
+ lines.push(` ${t("output.config.maxConcurrency")} ${config.scheduler.maxConcurrency}`);
2755
+ lines.push(` ${t("output.config.maxPerAgent")} ${config.scheduler.maxPerAgent}`);
2756
+ lines.push(` ${t("output.config.taskTimeout")} ${(config.scheduler.taskTimeoutMs / 1e3 / 60).toFixed(0)} ${t("output.config.minutes")}`);
2757
+ lines.push(` ${t("output.config.maxRetries")} ${config.scheduler.maxRetries}`);
2758
+ lines.push(c.bold(` ${t("output.config.cli")}`));
2759
+ lines.push(` ${t("output.config.colorEnabled")}: ${config.cli.color ? c.green(t("output.config.colorEnabled")) : c.gray(t("output.config.colorDisabled"))}`);
2760
+ lines.push(` ${t("output.config.debugEnabled")}: ${config.cli.debug ? c.green(t("output.config.debugEnabled")) : c.gray(t("output.config.debugDisabled"))}`);
2761
+ lines.push(` ${t("output.config.outputFormat")} ${config.cli.outputFormat}`);
2762
+ lines.push(` ${t("output.config.uiLanguage")} ${config.locale ?? "zh-CN"}`);
2763
+ lines.push(c.bold(` ${t("output.config.agents")}`));
1420
2764
  for (const agent of config.agents) {
1421
2765
  const statusIcon = agent.enabled ? c.green("✓") : c.gray("✗");
1422
2766
  lines.push(` ${statusIcon} ${agent.id}`);
@@ -1429,11 +2773,11 @@ var OutputFormatter = class {
1429
2773
  const c = this.getColor();
1430
2774
  const lines = [
1431
2775
  "",
1432
- c.bold(" 📜 会话历史"),
2776
+ c.bold(` 📜 ${t("output.history.title")}`),
1433
2777
  ""
1434
2778
  ];
1435
2779
  if (entries.length === 0) {
1436
- lines.push(c.gray(" 暂无历史记录"));
2780
+ lines.push(c.gray(` ${t("output.history.empty")}`));
1437
2781
  lines.push("");
1438
2782
  return lines;
1439
2783
  }
@@ -1452,16 +2796,16 @@ var OutputFormatter = class {
1452
2796
  const c = this.getColor();
1453
2797
  const lines = [
1454
2798
  "",
1455
- c.bold(" 📊 会话状态"),
2799
+ c.bold(` 📊 ${t("output.status.title")}`),
1456
2800
  ""
1457
2801
  ];
1458
- const stateLabel = stats.state === "idle" ? c.green("空闲") : stats.state === "executing" ? c.magenta("执行中") : c.gray("关闭中");
1459
- lines.push(` 状态: ${stateLabel}`);
1460
- lines.push(` 总请求数: ${stats.totalRequests}`);
1461
- lines.push(` 成功: ${c.green(String(stats.successCount))}`);
1462
- lines.push(` 失败: ${stats.failureCount > 0 ? c.red(String(stats.failureCount)) : String(stats.failureCount)}`);
1463
- lines.push(` Token 消耗: ${stats.totalTokens.toLocaleString()}`);
1464
- lines.push(` 总成本: $${stats.totalCostUsd.toFixed(4)}`);
2802
+ const stateLabel = stats.state === "idle" ? c.green(t("output.status.idle")) : stats.state === "executing" ? c.magenta(t("output.status.executing")) : c.gray(t("output.status.closing"));
2803
+ lines.push(` ${t("output.status.state").padEnd(10)} ${stateLabel}`);
2804
+ lines.push(` ${t("output.status.totalRequests").padEnd(10)} ${stats.totalRequests}`);
2805
+ lines.push(` ${t("output.status.success").padEnd(10)} ${c.green(String(stats.successCount))}`);
2806
+ lines.push(` ${t("output.status.failure").padEnd(10)} ${stats.failureCount > 0 ? c.red(String(stats.failureCount)) : String(stats.failureCount)}`);
2807
+ lines.push(` ${t("output.status.tokenConsumption").padEnd(10)} ${stats.totalTokens.toLocaleString()}`);
2808
+ lines.push(` ${t("output.status.totalCost").padEnd(10)} $${stats.totalCostUsd.toFixed(4)}`);
1465
2809
  lines.push("");
1466
2810
  return lines;
1467
2811
  }
@@ -6613,26 +7957,32 @@ var TaskStore = class {
6613
7957
  };
6614
7958
 
6615
7959
  //#endregion
6616
- //#region src/llm/pi-ai-provider.ts
7960
+ //#region src/cli/repl/session.ts
6617
7961
  /**
6618
- * 解析模型标识符
7962
+ * REPL 会话核心(pi-tui 版)
6619
7963
  *
6620
- * 支持格式:provider/modelId(如 anthropic/claude-sonnet-4-20250514)
6621
- * 导出供其他模块复用(如 REPL Session 为 Agent 解析 Model 对象)
7964
+ * 使用 @mariozechner/pi-tui 框架替代 readline,
7965
+ * 实现完整的 TUI 交互式 REPL:
7966
+ * - Editor 组件自带上下边框
7967
+ * - 差量渲染,无闪烁
7968
+ * - 组件化布局,可扩展
6622
7969
  */
6623
- function parseModelKey(key) {
6624
- const slashIndex = key.indexOf("/");
6625
- if (slashIndex <= 0 || slashIndex >= key.length - 1) return null;
6626
- return {
6627
- provider: key.slice(0, slashIndex),
6628
- modelId: key.slice(slashIndex + 1)
6629
- };
6630
- }
6631
-
6632
- //#endregion
6633
- //#region src/cli/repl/session.ts
6634
7970
  const log = logger.child("repl:session");
6635
7971
  /**
7972
+ * 检查错误消息是否匹配 "No API key for provider",返回解决指引行
7973
+ */
7974
+ function getApiKeyErrorHelp(errorMessage) {
7975
+ const match = errorMessage.match(/No API key for provider: (\w+)/);
7976
+ if (!match) return [];
7977
+ const providerName = match[1];
7978
+ const envVarName = `${providerName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
7979
+ return [
7980
+ "",
7981
+ chalk.yellow(` ${t("session.setEnvVarHint", { envVar: envVarName })}`),
7982
+ chalk.yellow(` ${t("session.useConfigHint", { provider: providerName })}`)
7983
+ ];
7984
+ }
7985
+ /**
6636
7986
  * 输出区域组件
6637
7987
  *
6638
7988
  * 管理所有输出内容的行缓冲,实现 pi-tui 的 render 接口。
@@ -6717,6 +8067,12 @@ var ReplSession = class {
6717
8067
  const theme = createTheme(this.options.color);
6718
8068
  const terminal = new ProcessTerminal();
6719
8069
  this.tui = new TUI(terminal);
8070
+ getKeybindings().setUserBindings({
8071
+ "tui.select.up": ["up", "k"],
8072
+ "tui.select.down": ["down", "j"],
8073
+ "tui.select.cancel": ["escape", "h"],
8074
+ "tui.select.confirm": ["enter", "l"]
8075
+ });
6720
8076
  this.outputArea = new OutputArea();
6721
8077
  this.editor = new ZapmycoEditor(this.tui, theme.editorTheme);
6722
8078
  const root = new Container();
@@ -6758,7 +8114,8 @@ var ReplSession = class {
6758
8114
  async start() {
6759
8115
  this._state = "idle";
6760
8116
  this.updateStatsState();
6761
- this.outputArea.append(["ZapMyco: 欢迎回来!", ""]);
8117
+ setLocale(this.config.locale ?? "zh-CN");
8118
+ this.outputArea.append([t("session.welcome"), ""]);
6762
8119
  this.tui.start();
6763
8120
  }
6764
8121
  /** 优雅关闭会话 */
@@ -6768,6 +8125,7 @@ var ReplSession = class {
6768
8125
  this.updateStatsState();
6769
8126
  log.info("REPL 关闭", { reason: reason ?? "未知" });
6770
8127
  this.cancelCurrentTask();
8128
+ this.editor.setExecuting(false);
6771
8129
  eventBus.emit("system:shutdown", { reason });
6772
8130
  if (this.cronScheduler) {
6773
8131
  this.cronScheduler.stop();
@@ -6778,11 +8136,16 @@ var ReplSession = class {
6778
8136
  this.mcpManager = null;
6779
8137
  }
6780
8138
  this.tui.stop();
8139
+ process.exit(0);
6781
8140
  }
6782
8141
  /** 获取渲染器引用 */
6783
8142
  getRenderer() {
6784
8143
  return this.renderer;
6785
8144
  }
8145
+ /** 获取 TUI 实例(用于显示 overlay 菜单) */
8146
+ getTui() {
8147
+ return this.tui;
8148
+ }
6786
8149
  /** 获取历史存储引用 */
6787
8150
  getHistoryStore() {
6788
8151
  return this.history;
@@ -6813,6 +8176,12 @@ var ReplSession = class {
6813
8176
  let historyEntry;
6814
8177
  const taskId = `task-${Date.now()}`;
6815
8178
  const ZAPMYCO_PREFIX = "ZapMyco: ";
8179
+ const THINKING_PREFIX = " 💭 ";
8180
+ const colorEnabled = this.options.color;
8181
+ const userStyle = (s) => colorEnabled ? chalk.bold.cyan(s) : s;
8182
+ const responseStyle = (s) => s;
8183
+ const toolStyle = (s) => colorEnabled ? chalk.yellow(s) : s;
8184
+ const thinkingStyle = (s) => colorEnabled ? chalk.gray(s) : s;
6816
8185
  let spinnerActive = true;
6817
8186
  let spinnerInterval;
6818
8187
  try {
@@ -6828,37 +8197,64 @@ var ReplSession = class {
6828
8197
  goalId: `goal-${startTime}`,
6829
8198
  rawInput
6830
8199
  });
6831
- this.outputArea.append([`Me: ${rawInput}`, ZAPMYCO_PREFIX + LOADING_FRAMES[0]]);
8200
+ this.outputArea.append([userStyle(`Me: ${rawInput}`), responseStyle(ZAPMYCO_PREFIX + LOADING_FRAMES[0])]);
6832
8201
  let spinnerFrame = 0;
6833
8202
  spinnerActive = true;
6834
8203
  spinnerInterval = setInterval(() => {
6835
8204
  if (!spinnerActive) return;
6836
8205
  spinnerFrame = (spinnerFrame + 1) % LOADING_FRAMES.length;
6837
- this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + LOADING_FRAMES[spinnerFrame]);
8206
+ this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + LOADING_FRAMES[spinnerFrame]));
6838
8207
  this.tui.requestRender();
6839
8208
  }, 100);
6840
8209
  let firstOutputReceived = false;
8210
+ let outputAccumulator = "";
8211
+ let thinkingAccumulator = "";
8212
+ let streamMode = "response";
6841
8213
  const outputHandler = (event) => {
6842
- if (event.taskId === taskId) {
6843
- if (!firstOutputReceived) {
6844
- firstOutputReceived = true;
6845
- spinnerActive = false;
6846
- clearInterval(spinnerInterval);
6847
- this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + event.text);
6848
- } else this.outputArea.appendText(event.text);
6849
- this.tui.requestRender();
8214
+ if (event.taskId !== taskId || !event.text) return;
8215
+ if (!firstOutputReceived) {
8216
+ firstOutputReceived = true;
8217
+ spinnerActive = false;
8218
+ clearInterval(spinnerInterval);
8219
+ streamMode = "response";
8220
+ thinkingAccumulator = "";
8221
+ outputAccumulator = event.text;
8222
+ this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputAccumulator));
8223
+ } else if (streamMode !== "response") {
8224
+ streamMode = "response";
8225
+ thinkingAccumulator = "";
8226
+ outputAccumulator = event.text;
8227
+ this.outputArea.append([responseStyle(ZAPMYCO_PREFIX + outputAccumulator)]);
8228
+ } else {
8229
+ outputAccumulator += event.text;
8230
+ this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputAccumulator));
6850
8231
  }
8232
+ this.tui.requestRender();
8233
+ };
8234
+ const thinkingHandler = (event) => {
8235
+ if (event.taskId !== taskId || !event.text) return;
8236
+ if (streamMode !== "thinking") {
8237
+ streamMode = "thinking";
8238
+ outputAccumulator = "";
8239
+ thinkingAccumulator = event.text;
8240
+ this.outputArea.append([thinkingStyle(THINKING_PREFIX + thinkingAccumulator)]);
8241
+ } else {
8242
+ thinkingAccumulator += event.text;
8243
+ this.outputArea.replaceLastLine(thinkingStyle(THINKING_PREFIX + thinkingAccumulator));
8244
+ }
8245
+ this.tui.requestRender();
6851
8246
  };
6852
8247
  const errorHandler = (event) => {
6853
8248
  if (event.taskId === taskId) log.error("Agent 执行中收到 error 事件", { error: event.error.message });
6854
8249
  };
6855
8250
  const progressHandler = (event) => {
6856
8251
  if (event.taskId === taskId && event.percent === 0) {
6857
- this.outputArea.append([` → ${event.message}`]);
8252
+ this.outputArea.append([toolStyle(` → ${event.message}`)]);
6858
8253
  this.tui.requestRender();
6859
8254
  }
6860
8255
  };
6861
8256
  this.agent.on(this.agent.EVENT_OUTPUT, outputHandler);
8257
+ this.agent.on(this.agent.EVENT_THINKING, thinkingHandler);
6862
8258
  this.agent.on(this.agent.EVENT_ERROR, errorHandler);
6863
8259
  this.agent.on(this.agent.EVENT_PROGRESS, progressHandler);
6864
8260
  this.currentTaskId = taskId;
@@ -6876,6 +8272,7 @@ var ReplSession = class {
6876
8272
  }
6877
8273
  });
6878
8274
  this.agent.off(this.agent.EVENT_OUTPUT, outputHandler);
8275
+ this.agent.off(this.agent.EVENT_THINKING, thinkingHandler);
6879
8276
  this.agent.off(this.agent.EVENT_ERROR, errorHandler);
6880
8277
  this.agent.off(this.agent.EVENT_PROGRESS, progressHandler);
6881
8278
  log.debug("Agent 执行完成", {
@@ -6888,11 +8285,50 @@ var ReplSession = class {
6888
8285
  if (spinnerActive) {
6889
8286
  spinnerActive = false;
6890
8287
  clearInterval(spinnerInterval);
6891
- if (outputText) this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + outputText);
8288
+ if (outputText) this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputText));
8289
+ else if (taskResult.status !== "success") {
8290
+ const errorMsg = taskResult.error?.message ?? t("session.agentErrorMessage");
8291
+ this.outputArea.replaceLastLine(chalk.red(`ZapMyco: ${t("session.errorPrefix")} ${errorMsg}`));
8292
+ const helpLines = getApiKeyErrorHelp(errorMsg);
8293
+ if (helpLines.length > 0) {
8294
+ this.outputArea.append(helpLines);
8295
+ const providerMatch = errorMsg.match(/No API key for provider: (\w+)/);
8296
+ if (providerMatch) {
8297
+ const providerName = providerMatch[1];
8298
+ this.outputArea.append([""]);
8299
+ if ((await showSelectList(this.tui, [{
8300
+ value: "yes",
8301
+ label: "好的,我来输入 API Key",
8302
+ description: `直接输入 ${providerName} 的 API Key,立即配置并重试`
8303
+ }, {
8304
+ value: "no",
8305
+ label: "稍后再说",
8306
+ description: "回到对话"
8307
+ }], { title: `需要配置 ${providerName} 的 API Key` }))?.value === "yes") {
8308
+ const apiKey = await showTextInput(this.tui, `请输入 ${providerName} 的 API Key:`, "", "sk-...");
8309
+ if (apiKey && apiKey.length > 0) {
8310
+ const dotPath = `llm.providers.${providerName}.apiKey`;
8311
+ const settings = readSettings();
8312
+ _setByDotPath(settings, dotPath, apiKey);
8313
+ writeSettings(settings);
8314
+ _setByDotPath(this.config, dotPath, apiKey);
8315
+ this.applyConfigUpdate(dotPath);
8316
+ this.outputArea.append([
8317
+ "",
8318
+ chalk.green(`已配置 ${providerName} 的 API Key,正在重试...`),
8319
+ ""
8320
+ ]);
8321
+ return await this.executeGoal(rawInput);
8322
+ }
8323
+ }
8324
+ this.outputArea.append([""]);
8325
+ }
8326
+ }
8327
+ } else this.outputArea.replaceLastLine(chalk.red(`ZapMyco: ${t("session.errorPrefix")} ${t("session.noContentError")}`));
6892
8328
  }
6893
8329
  if (taskResult.status !== "success") {
6894
- const errorMsg = taskResult.error?.message ?? "Agent 执行失败(无详细错误信息)";
6895
- this.outputArea.appendText(`[错误] ${errorMsg}`);
8330
+ const errorMsg = taskResult.error?.message ?? t("session.agentErrorMessage");
8331
+ if (!spinnerActive || outputText) this.outputArea.appendText(`${t("session.errorPrefix")} ${errorMsg}`);
6896
8332
  log.error("Agent 执行返回 failure", {
6897
8333
  taskId,
6898
8334
  error: taskResult.error,
@@ -6946,7 +8382,42 @@ var ReplSession = class {
6946
8382
  goalId: `goal-${startTime}`,
6947
8383
  error: err
6948
8384
  });
6949
- this.outputArea.replaceLastLine(`ZapMyco: [错误] ${err.message}`);
8385
+ this.outputArea.replaceLastLine(responseStyle(`${ZAPMYCO_PREFIX}${t("session.errorPrefix")} ${err.message}`));
8386
+ const helpLines = getApiKeyErrorHelp(err.message);
8387
+ if (helpLines.length > 0) {
8388
+ this.outputArea.append(helpLines);
8389
+ const providerMatch = err.message.match(/No API key for provider: (\w+)/);
8390
+ if (providerMatch) {
8391
+ const providerName = providerMatch[1];
8392
+ this.outputArea.append([""]);
8393
+ if ((await showSelectList(this.tui, [{
8394
+ value: "yes",
8395
+ label: "好的,我来输入 API Key",
8396
+ description: `直接输入 ${providerName} 的 API Key,立即配置并重试`
8397
+ }, {
8398
+ value: "no",
8399
+ label: "稍后再说",
8400
+ description: "回到对话"
8401
+ }], { title: `需要配置 ${providerName} 的 API Key` }))?.value === "yes") {
8402
+ const apiKey = await showTextInput(this.tui, `请输入 ${providerName} 的 API Key:`, "", "sk-...");
8403
+ if (apiKey && apiKey.length > 0) {
8404
+ const dotPath = `llm.providers.${providerName}.apiKey`;
8405
+ const settings = readSettings();
8406
+ _setByDotPath(settings, dotPath, apiKey);
8407
+ writeSettings(settings);
8408
+ _setByDotPath(this.config, dotPath, apiKey);
8409
+ this.applyConfigUpdate(dotPath);
8410
+ this.outputArea.append([
8411
+ "",
8412
+ chalk.green(`已配置 ${providerName} 的 API Key,正在重试...`),
8413
+ ""
8414
+ ]);
8415
+ return await this.executeGoal(rawInput);
8416
+ }
8417
+ }
8418
+ this.outputArea.append([""]);
8419
+ }
8420
+ }
6950
8421
  const errorLines = this.renderer.renderError(err).slice(1);
6951
8422
  if (errorLines.length > 0) this.outputArea.append(errorLines);
6952
8423
  const duration = Date.now() - startTime;
@@ -7012,6 +8483,7 @@ var ReplSession = class {
7012
8483
  this.registry.register(createConfigCommand());
7013
8484
  this.registry.register(createAgentsCommand());
7014
8485
  this.registry.register(createStatusCommand());
8486
+ this.registry.register(createSettingsCommand());
7015
8487
  this.buildAutocompleteProvider();
7016
8488
  }
7017
8489
  /** 构建并设置 autocomplete provider,将命令注册表中的命令接入 pi-tui 补全系统 */
@@ -7042,7 +8514,7 @@ var ReplSession = class {
7042
8514
  createReplAgent() {
7043
8515
  const agent = createLlmBasedAgent({
7044
8516
  agentId: "repl-chat-agent",
7045
- displayName: "Zapmyco AI 助手",
8517
+ displayName: t("session.displayName"),
7046
8518
  capabilities: [{
7047
8519
  id: "chat",
7048
8520
  name: "对话",
@@ -7051,39 +8523,25 @@ var ReplSession = class {
7051
8523
  }],
7052
8524
  runtimeConfig: this.config.agentRuntime ?? {}
7053
8525
  });
7054
- agent.innerAgent.state.model = this.resolveModelForAgent();
7055
- agent.innerAgent.getApiKey = (_provider) => {
7056
- const modelKey = this.config.llm.defaultModel;
7057
- const providerName = this.config.llm.models[modelKey]?.provider;
7058
- if (providerName) return this.config.llm.providers[providerName]?.apiKey;
7059
- };
7060
- return agent;
7061
- }
7062
- /**
7063
- * 为 Agent 解析 pi-ai Model 对象
7064
- *
7065
- * 复用 PiAiProvider 的模型解析逻辑(parseModelKey + getModel),
7066
- * 但不依赖 chat/chatStream 方法。
7067
- */
7068
- resolveModelForAgent() {
7069
- const modelKey = this.config.llm.defaultModel;
7070
- const parsed = parseModelKey(modelKey);
7071
- if (!parsed) throw new Error(`无效的模型标识符: ${modelKey}`);
7072
- const modelConfig = this.config.llm.models[modelKey];
7073
- const provider = modelConfig?.provider ?? parsed.provider;
7074
- const modelId = modelConfig?.modelId ?? parsed.modelId;
7075
- const baseModelId = provider === "anthropic" ? "claude-sonnet-4-20250514" : modelId;
7076
- let model;
7077
- try {
7078
- model = getModel(provider, baseModelId);
7079
- } catch {
7080
- model = getModel("anthropic", "claude-sonnet-4-20250514");
8526
+ const facade = new AgentLlmFacade(this.config.llm);
8527
+ agent.innerAgent.state.model = facade.resolvePiModel();
8528
+ agent.innerAgent.getApiKey = facade.createGetApiKeyFn();
8529
+ agent.llmFacade = facade;
8530
+ const defaultModelInfo = facade.getModelInfo();
8531
+ if (defaultModelInfo) {
8532
+ if (!facade.getApiKey(defaultModelInfo.provider)) {
8533
+ const providerName = defaultModelInfo.provider;
8534
+ const envVar = providerName.toUpperCase() + "_API_KEY";
8535
+ this.outputArea.append([
8536
+ chalk.red(`[!] 提供商 "${providerName}" 没有配置 API Key`),
8537
+ chalk.yellow(` 请设置环境变量: export ${envVar}=<your-key>`),
8538
+ chalk.yellow(` 或在 REPL 中使用: /config set llm.providers.${providerName}.apiKey <your-key>`),
8539
+ ""
8540
+ ]);
8541
+ log.warn("默认提供商缺少 API Key", { provider: providerName });
8542
+ }
7081
8543
  }
7082
- if (!model) throw new Error(`无法初始化模型 ${modelKey}:pi-ai 返回了无效的模型对象`);
7083
- model.name = modelKey;
7084
- model.id = modelId;
7085
- if (modelConfig?.baseUrl) model.baseUrl = modelConfig.baseUrl;
7086
- return model;
8544
+ return agent;
7087
8545
  }
7088
8546
  /**
7089
8547
  * 注册 REPL 场景下的基础工具
@@ -7173,6 +8631,8 @@ var ReplSession = class {
7173
8631
  }
7174
8632
  ctrlCPressCount++;
7175
8633
  if (ctrlCPressCount >= 2) {
8634
+ clearTimeout(ctrlCTimer);
8635
+ ctrlCTimer = void 0;
7176
8636
  this.shutdown("用户连续按下 Ctrl+C");
7177
8637
  return;
7178
8638
  }
@@ -7189,6 +8649,7 @@ var ReplSession = class {
7189
8649
  this.editor.onCtrlD = () => {
7190
8650
  this.shutdown("收到 EOF (Ctrl+D)");
7191
8651
  };
8652
+ this.editor.onOpenEditor = () => this.openInEditor();
7192
8653
  }
7193
8654
  /** 设置信号处理 */
7194
8655
  setupSignalHandlers() {
@@ -7211,6 +8672,71 @@ var ReplSession = class {
7211
8672
  this.currentTaskAbort = null;
7212
8673
  }
7213
8674
  }
8675
+ /**
8676
+ * 打开外部编辑器(vim / $EDITOR)编辑当前输入内容
8677
+ *
8678
+ * 流程:
8679
+ * 1. 将编辑器当前文本写入临时文件
8680
+ * 2. 暂停 TUI(恢复终端 cooked 模式)
8681
+ * 3. 启动外部编辑器,用户编辑并保存退出
8682
+ * 4. 读取编辑后的内容并更新编辑器
8683
+ * 5. 恢复 TUI 并重绘
8684
+ */
8685
+ openInEditor() {
8686
+ const tmpFile = join(tmpdir(), "zapmyco-editor-input.txt");
8687
+ let tuiStopped = false;
8688
+ try {
8689
+ const currentText = this.editor.getExpandedText();
8690
+ writeFileSync(tmpFile, currentText, "utf-8");
8691
+ this.tui.stop();
8692
+ tuiStopped = true;
8693
+ const editorCmd = process.env.VISUAL || process.env.EDITOR || "vim";
8694
+ const result = spawnSync(editorCmd, [tmpFile], { stdio: "inherit" });
8695
+ const newText = readFileSync(tmpFile, "utf-8");
8696
+ if (newText !== currentText) this.editor.setText(newText);
8697
+ if (result.error) {
8698
+ const err = result.error;
8699
+ if (err.code === "ENOENT") this.outputArea.append([
8700
+ "",
8701
+ t("session.editorNotFound", { cmd: editorCmd }),
8702
+ ""
8703
+ ]);
8704
+ else this.outputArea.append([
8705
+ "",
8706
+ `编辑器启动失败: ${err.message}`,
8707
+ ""
8708
+ ]);
8709
+ }
8710
+ } catch (err) {
8711
+ const message = err instanceof Error ? err.message : String(err);
8712
+ this.outputArea.append([
8713
+ "",
8714
+ t("session.editorFailed", { message }),
8715
+ ""
8716
+ ]);
8717
+ } finally {
8718
+ if (tuiStopped) {
8719
+ this.tui.start();
8720
+ this.tui.requestRender(true);
8721
+ }
8722
+ try {
8723
+ unlinkSync(tmpFile);
8724
+ } catch {}
8725
+ }
8726
+ }
8727
+ /**
8728
+ * 应用配置更新到运行中的 Agent(无需重启)
8729
+ *
8730
+ * 当前处理以 "llm." 开头的配置变更,重新创建 AgentLlmFacade
8731
+ * 并注入到运行中的 Agent 实例,使新 Key/模型立即生效。
8732
+ */
8733
+ applyConfigUpdate(key) {
8734
+ if (!key.startsWith("llm.")) return;
8735
+ const newFacade = new AgentLlmFacade(this.config.llm);
8736
+ this.agent.innerAgent.state.model = newFacade.resolvePiModel();
8737
+ this.agent.innerAgent.getApiKey = newFacade.createGetApiKeyFn();
8738
+ this.agent.llmFacade = newFacade;
8739
+ }
7214
8740
  /** 更新统计中的状态字段 */
7215
8741
  updateStatsState() {
7216
8742
  this.stats.state = this._state;
@@ -7255,7 +8781,7 @@ async function startRepl() {
7255
8781
  * zapmyco version 显示版本号
7256
8782
  */
7257
8783
  const program = new Command();
7258
- program.name(APP_NAME).description("AI 原生并行任务编排系统 -- AI 总管").version(__VERSION__, "-v, --version", "显示版本号").helpOption("-h, --help", "显示帮助信息");
8784
+ program.name(APP_NAME).description("AI 原生并行任务编排系统 -- AI 总管").version(VERSION, "-v, --version", "显示版本号").helpOption("-h, --help", "显示帮助信息");
7259
8785
  program.action(async () => {
7260
8786
  try {
7261
8787
  await startRepl();