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.
- package/dist/cli/index.mjs +1663 -137
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +451 -34
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-CyGlMdl7.mjs → loader-C_55Y7Q_.mjs} +858 -31
- package/dist/loader-C_55Y7Q_.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/loader-CyGlMdl7.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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 {
|
|
14
|
-
import { CombinedAutocompleteProvider, Container, Editor, Key, ProcessTerminal, TUI, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
15
|
-
import
|
|
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: "
|
|
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(`
|
|
1303
|
-
} else lines.push(`${c.red.bold(
|
|
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("
|
|
1317
|
-
` │ ${c.gray("
|
|
1318
|
-
` │ ${c.gray("
|
|
1319
|
-
` │ ${c.gray("
|
|
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("
|
|
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(`
|
|
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(`
|
|
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(
|
|
2714
|
+
c.bold(` 🤖 ${t("output.agents.title")}`),
|
|
1376
2715
|
""
|
|
1377
2716
|
];
|
|
1378
2717
|
if (agents.length === 0) {
|
|
1379
|
-
lines.push(c.gray(
|
|
2718
|
+
lines.push(c.gray(` ${t("output.agents.empty")}`));
|
|
1380
2719
|
lines.push("");
|
|
1381
2720
|
return lines;
|
|
1382
2721
|
}
|
|
1383
|
-
lines.push(` ${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(
|
|
2739
|
+
c.bold(` ${t("output.config.llm")}`)
|
|
1401
2740
|
];
|
|
1402
|
-
lines.push(`
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
lines.push(`
|
|
1410
|
-
lines.push(
|
|
1411
|
-
lines.push(`
|
|
1412
|
-
lines.push(`
|
|
1413
|
-
lines.push(`
|
|
1414
|
-
lines.push(`
|
|
1415
|
-
lines.push(
|
|
1416
|
-
lines.push(`
|
|
1417
|
-
lines.push(`
|
|
1418
|
-
lines.push(`
|
|
1419
|
-
lines.push(c.bold(
|
|
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("
|
|
1459
|
-
lines.push(`
|
|
1460
|
-
lines.push(`
|
|
1461
|
-
lines.push(`
|
|
1462
|
-
lines.push(`
|
|
1463
|
-
lines.push(`
|
|
1464
|
-
lines.push(`
|
|
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/
|
|
7960
|
+
//#region src/cli/repl/session.ts
|
|
6617
7961
|
/**
|
|
6618
|
-
*
|
|
7962
|
+
* REPL 会话核心(pi-tui 版)
|
|
6619
7963
|
*
|
|
6620
|
-
*
|
|
6621
|
-
*
|
|
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.
|
|
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}
|
|
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
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
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 ?? "
|
|
6895
|
-
this.outputArea.appendText(
|
|
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(
|
|
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: "
|
|
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
|
-
|
|
7055
|
-
agent.innerAgent.
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
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
|
-
|
|
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(
|
|
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();
|