zapmyco 0.6.0 → 0.7.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 +1186 -93
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +446 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-CyGlMdl7.mjs → loader-B5_elj6d.mjs} +856 -29
- package/dist/loader-B5_elj6d.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/loader-CyGlMdl7.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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-B5_elj6d.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 { 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
16
|
import TurndownService from "turndown";
|
|
17
17
|
import { lookup } from "node:dns/promises";
|
|
18
18
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
@@ -140,14 +140,73 @@ function createClearCommand() {
|
|
|
140
140
|
//#endregion
|
|
141
141
|
//#region src/cli/repl/commands/config-cmd.ts
|
|
142
142
|
/**
|
|
143
|
+
* /config 命令
|
|
144
|
+
*
|
|
145
|
+
* 查看和修改当前配置信息。
|
|
146
|
+
*/
|
|
147
|
+
/** 判断 key 是否可能触发原型污染 */
|
|
148
|
+
function isPrototypePollutionKey(key) {
|
|
149
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
150
|
+
}
|
|
151
|
+
function getByDotPath(obj, path) {
|
|
152
|
+
const keys = path.split(".");
|
|
153
|
+
let current = obj;
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
156
|
+
if (isPrototypePollutionKey(key)) return;
|
|
157
|
+
current = current[key];
|
|
158
|
+
}
|
|
159
|
+
return current;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 通过 dot-path 设置嵌套对象属性(自动创建中间对象)
|
|
163
|
+
*/
|
|
164
|
+
function setByDotPath(obj, path, value) {
|
|
165
|
+
const keys = path.split(".");
|
|
166
|
+
let current = obj;
|
|
167
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
168
|
+
const key = keys[i];
|
|
169
|
+
if (isPrototypePollutionKey(key)) return;
|
|
170
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
171
|
+
current = current[key];
|
|
172
|
+
}
|
|
173
|
+
const lastKey = keys[keys.length - 1];
|
|
174
|
+
if (isPrototypePollutionKey(lastKey)) return;
|
|
175
|
+
current[lastKey] = value;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 更新 settings.json 中指定 dot-path 的值
|
|
179
|
+
*/
|
|
180
|
+
function updateSettingsFile(path, value) {
|
|
181
|
+
try {
|
|
182
|
+
const raw = readFileSync(HOME_CONFIG_PATH, "utf-8");
|
|
183
|
+
const config = JSON.parse(raw);
|
|
184
|
+
let parsedValue = value;
|
|
185
|
+
try {
|
|
186
|
+
parsedValue = JSON.parse(value);
|
|
187
|
+
} catch {}
|
|
188
|
+
setByDotPath(config, path, parsedValue);
|
|
189
|
+
writeFileSync(HOME_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
190
|
+
return {
|
|
191
|
+
success: true,
|
|
192
|
+
message: ""
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
message: err instanceof Error ? err.message : String(err)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
143
202
|
* 创建 config 命令定义
|
|
144
203
|
*/
|
|
145
204
|
function createConfigCommand() {
|
|
146
205
|
return {
|
|
147
206
|
name: "config",
|
|
148
207
|
aliases: ["cfg"],
|
|
149
|
-
description: "
|
|
150
|
-
usage: "/config [show | get <key>]",
|
|
208
|
+
description: "查看或修改配置 [show | get <key> | set <key> <value>]",
|
|
209
|
+
usage: "/config [show | get <key> | set <key> <value>]",
|
|
151
210
|
handler(args, session) {
|
|
152
211
|
const config = session.config;
|
|
153
212
|
const renderer = session.getRenderer();
|
|
@@ -161,9 +220,9 @@ function createConfigCommand() {
|
|
|
161
220
|
if (value !== void 0) {
|
|
162
221
|
const displayValue = args[1].toLowerCase().includes("apikey") || args[1].toLowerCase().includes("api_key") ? "***已配置***" : JSON.stringify(value, null, 2);
|
|
163
222
|
session.appendOutput([
|
|
164
|
-
|
|
223
|
+
"",
|
|
165
224
|
` ${args[1]}: ${displayValue}`,
|
|
166
|
-
|
|
225
|
+
""
|
|
167
226
|
]);
|
|
168
227
|
} else session.appendOutput([
|
|
169
228
|
"",
|
|
@@ -173,28 +232,40 @@ function createConfigCommand() {
|
|
|
173
232
|
]);
|
|
174
233
|
return;
|
|
175
234
|
}
|
|
235
|
+
if (args[0] === "set" && args[1] && args[2] !== void 0) {
|
|
236
|
+
const key = args[1];
|
|
237
|
+
const value = args.slice(2).join(" ");
|
|
238
|
+
const result = updateSettingsFile(key, value);
|
|
239
|
+
if (result.success) {
|
|
240
|
+
let parsedValue = value;
|
|
241
|
+
try {
|
|
242
|
+
parsedValue = JSON.parse(value);
|
|
243
|
+
} catch {}
|
|
244
|
+
setByDotPath(session.config, key, parsedValue);
|
|
245
|
+
session.applyConfigUpdate(key);
|
|
246
|
+
const displayValue = key.toLowerCase().includes("apikey") || key.toLowerCase().includes("api_key") ? "***已配置***" : value;
|
|
247
|
+
session.appendOutput([
|
|
248
|
+
"",
|
|
249
|
+
` ✅ 配置已更新: ${key} = ${displayValue}`,
|
|
250
|
+
` 已持久化到 ${HOME_CONFIG_PATH}`,
|
|
251
|
+
""
|
|
252
|
+
]);
|
|
253
|
+
} else session.appendOutput([
|
|
254
|
+
"",
|
|
255
|
+
` ❌ 配置更新失败: ${result.message}`,
|
|
256
|
+
""
|
|
257
|
+
]);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
176
260
|
session.appendOutput([
|
|
177
261
|
"",
|
|
178
|
-
" 用法: /config [show | get <key>]",
|
|
262
|
+
" 用法: /config [show | get <key> | set <key> <value>]",
|
|
263
|
+
" 示例: /config set llm.providers.deepseek.apiKey sk-xxx",
|
|
179
264
|
""
|
|
180
265
|
]);
|
|
181
266
|
}
|
|
182
267
|
};
|
|
183
268
|
}
|
|
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
269
|
|
|
199
270
|
//#endregion
|
|
200
271
|
//#region src/cli/repl/commands/help.ts
|
|
@@ -294,6 +365,840 @@ function createQuitCommand() {
|
|
|
294
365
|
};
|
|
295
366
|
}
|
|
296
367
|
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/cli/repl/components/dialogs.ts
|
|
370
|
+
/**
|
|
371
|
+
* Shared dialog components for TUI overlays
|
|
372
|
+
*
|
|
373
|
+
* Extracted from settings-cmd.ts for reuse across the REPL.
|
|
374
|
+
* Provides SelectList and TextInput overlay dialogs.
|
|
375
|
+
*/
|
|
376
|
+
/** Overlay layout options for menus */
|
|
377
|
+
const OVERLAY_OPTIONS = {
|
|
378
|
+
width: "100%",
|
|
379
|
+
anchor: "top-left",
|
|
380
|
+
margin: {
|
|
381
|
+
top: 1,
|
|
382
|
+
bottom: 1
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const SELECT_THEME = {
|
|
386
|
+
selectedPrefix: (text) => chalk.green(`❯ ${text}`),
|
|
387
|
+
selectedText: (text) => chalk.green.bold(text),
|
|
388
|
+
description: (text) => chalk.gray(text),
|
|
389
|
+
scrollInfo: (text) => chalk.gray(text),
|
|
390
|
+
noMatch: (text) => chalk.red(text)
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Wraps SelectList with a footer hint showing available keybindings.
|
|
394
|
+
* This lets users discover navigation keys without relying on terminal conventions.
|
|
395
|
+
*/
|
|
396
|
+
var SelectListWithFooter = class {
|
|
397
|
+
selectList;
|
|
398
|
+
tui;
|
|
399
|
+
filterText = "";
|
|
400
|
+
isFiltering = false;
|
|
401
|
+
/** Callbacks stored at wrapper level, forwarded through inner SelectList */
|
|
402
|
+
onSelect;
|
|
403
|
+
onCancel;
|
|
404
|
+
/** Called when user presses q/esc — exits settings entirely */
|
|
405
|
+
onExit;
|
|
406
|
+
/** Called when user presses backspace/h — goes back one level */
|
|
407
|
+
onBack;
|
|
408
|
+
constructor(tui, items, maxVisible, theme) {
|
|
409
|
+
this.tui = tui;
|
|
410
|
+
this.selectList = new SelectList(items, maxVisible, theme);
|
|
411
|
+
this.selectList.onSelect = (item) => {
|
|
412
|
+
this.onSelect?.(item);
|
|
413
|
+
};
|
|
414
|
+
this.selectList.onCancel = () => {
|
|
415
|
+
this.onCancel?.();
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
handleInput(data) {
|
|
419
|
+
if (this.isFiltering) if (data.length === 1 && data >= " " && data <= "~") {
|
|
420
|
+
this.filterText += data;
|
|
421
|
+
this.applyFilter();
|
|
422
|
+
} else if (data === "backspace" || data === "" || data === "\b") if (this.filterText.length > 0) {
|
|
423
|
+
this.filterText = this.filterText.slice(0, -1);
|
|
424
|
+
this.applyFilter();
|
|
425
|
+
} else {
|
|
426
|
+
this.isFiltering = false;
|
|
427
|
+
this.resetFilteredItems();
|
|
428
|
+
this.selectList.invalidate();
|
|
429
|
+
}
|
|
430
|
+
else if (data === "escape") {
|
|
431
|
+
this.isFiltering = false;
|
|
432
|
+
this.filterText = "";
|
|
433
|
+
this.resetFilteredItems();
|
|
434
|
+
this.selectList.invalidate();
|
|
435
|
+
} else if (data === "enter") {
|
|
436
|
+
this.isFiltering = false;
|
|
437
|
+
this.selectList.handleInput("enter");
|
|
438
|
+
} else this.selectList.handleInput(data);
|
|
439
|
+
else if (data === "/") {
|
|
440
|
+
this.isFiltering = true;
|
|
441
|
+
this.filterText = "";
|
|
442
|
+
this.selectList.invalidate();
|
|
443
|
+
} else if (data === "escape" || data === "q") this.onExit?.();
|
|
444
|
+
else if (data === "backspace" || data === "" || data === "\b" || data === "h") this.onBack?.();
|
|
445
|
+
else this.selectList.handleInput(data);
|
|
446
|
+
}
|
|
447
|
+
/** Apply current filter text to the SelectList (uses includes, not startsWith) */
|
|
448
|
+
applyFilter() {
|
|
449
|
+
const sl = this.selectList;
|
|
450
|
+
if (!this.filterText) sl.filteredItems = [...sl.items];
|
|
451
|
+
else {
|
|
452
|
+
const lower = this.filterText.toLowerCase();
|
|
453
|
+
sl.filteredItems = sl.items.filter((item) => item.value.toLowerCase().includes(lower));
|
|
454
|
+
}
|
|
455
|
+
sl.selectedIndex = 0;
|
|
456
|
+
this.selectList.invalidate();
|
|
457
|
+
}
|
|
458
|
+
/** Restore SelectList to show all unfiltered items */
|
|
459
|
+
resetFilteredItems() {
|
|
460
|
+
const sl = this.selectList;
|
|
461
|
+
sl.filteredItems = [...sl.items];
|
|
462
|
+
sl.selectedIndex = 0;
|
|
463
|
+
}
|
|
464
|
+
invalidate() {
|
|
465
|
+
this.selectList.invalidate();
|
|
466
|
+
}
|
|
467
|
+
render(width) {
|
|
468
|
+
const lines = [];
|
|
469
|
+
if (this.isFiltering) {
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push(chalk.cyan(` /${this.filterText}█`));
|
|
472
|
+
lines.push(chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
473
|
+
lines.push("");
|
|
474
|
+
}
|
|
475
|
+
const listLines = this.selectList.render(width);
|
|
476
|
+
lines.push(...listLines);
|
|
477
|
+
const termHeight = this.tui.terminal.rows;
|
|
478
|
+
const padding = Math.max(0, termHeight - 1 - lines.length - 3);
|
|
479
|
+
for (let i = 0; i < padding; i++) lines.push("");
|
|
480
|
+
if (width >= 50) {
|
|
481
|
+
lines.push(chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
482
|
+
if (this.isFiltering) lines.push(chalk.gray(" 输入文字搜索 · ↑↓ 导航 · Enter 确认 · Esc 取消"));
|
|
483
|
+
else lines.push(chalk.gray(" k/j ↑↓ 导航 · / 搜索 · Enter 选择 · Esc/q 退出 · BS/h 返回"));
|
|
484
|
+
} else if (this.isFiltering) lines.push(chalk.gray(" Enter 确认 · Esc 取消"));
|
|
485
|
+
else lines.push(chalk.gray(" ↑↓=k/j / Enter Esc/q BS/h"));
|
|
486
|
+
lines.push("");
|
|
487
|
+
return lines;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Show a SelectList overlay and wait for user selection
|
|
492
|
+
* @returns The selected item, or null if cancelled
|
|
493
|
+
*/
|
|
494
|
+
function showSelectList(tui, items, options) {
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
const list = new SelectListWithFooter(tui, items, options?.maxVisible ?? 10, SELECT_THEME);
|
|
497
|
+
let handle = null;
|
|
498
|
+
list.onSelect = (item) => {
|
|
499
|
+
handle?.hide();
|
|
500
|
+
resolve(item);
|
|
501
|
+
};
|
|
502
|
+
list.onCancel = () => {
|
|
503
|
+
handle?.hide();
|
|
504
|
+
resolve(null);
|
|
505
|
+
};
|
|
506
|
+
list.onExit = () => {
|
|
507
|
+
handle?.hide();
|
|
508
|
+
resolve(null);
|
|
509
|
+
options?.onExit?.();
|
|
510
|
+
};
|
|
511
|
+
list.onBack = () => {
|
|
512
|
+
handle?.hide();
|
|
513
|
+
resolve(null);
|
|
514
|
+
};
|
|
515
|
+
handle = tui.showOverlay(list, OVERLAY_OPTIONS);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
var TextInputComponent = class {
|
|
519
|
+
input;
|
|
520
|
+
label;
|
|
521
|
+
constructor(label, initialValue, placeholder, onSubmit, onCancel) {
|
|
522
|
+
this.label = label;
|
|
523
|
+
this.input = new Input();
|
|
524
|
+
if (initialValue) this.input.setValue(initialValue);
|
|
525
|
+
this.input.onSubmit = (value) => {
|
|
526
|
+
onSubmit(value === placeholder ? initialValue : value);
|
|
527
|
+
};
|
|
528
|
+
this.input.onEscape = () => {
|
|
529
|
+
onCancel();
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
get focused() {
|
|
533
|
+
return this.input.focused;
|
|
534
|
+
}
|
|
535
|
+
set focused(v) {
|
|
536
|
+
this.input.focused = v;
|
|
537
|
+
}
|
|
538
|
+
handleInput(data) {
|
|
539
|
+
this.input.handleInput(data);
|
|
540
|
+
}
|
|
541
|
+
invalidate() {
|
|
542
|
+
this.input.invalidate();
|
|
543
|
+
}
|
|
544
|
+
render(width) {
|
|
545
|
+
const c = chalk;
|
|
546
|
+
return [
|
|
547
|
+
"",
|
|
548
|
+
c.bold(` ${this.label}`),
|
|
549
|
+
"",
|
|
550
|
+
c.gray(` ${"─".repeat(Math.min(width - 4, 50))}`),
|
|
551
|
+
` ${this.input.render(width - 4)[0] ?? ""}`,
|
|
552
|
+
c.gray(` ${"─".repeat(Math.min(width - 4, 50))}`),
|
|
553
|
+
"",
|
|
554
|
+
c.gray(" Enter to confirm · Esc to cancel"),
|
|
555
|
+
""
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
/**
|
|
560
|
+
* Show a text input overlay
|
|
561
|
+
* @returns The entered text, or null if cancelled
|
|
562
|
+
*/
|
|
563
|
+
function showTextInput(tui, label, initialValue, placeholder) {
|
|
564
|
+
return new Promise((resolve) => {
|
|
565
|
+
let handle = null;
|
|
566
|
+
const component = new TextInputComponent(label, initialValue, placeholder ?? "", (value) => {
|
|
567
|
+
handle?.hide();
|
|
568
|
+
resolve(value);
|
|
569
|
+
}, () => {
|
|
570
|
+
handle?.hide();
|
|
571
|
+
resolve(null);
|
|
572
|
+
});
|
|
573
|
+
handle = tui.showOverlay(component, {
|
|
574
|
+
width: "60%",
|
|
575
|
+
minWidth: 50,
|
|
576
|
+
maxHeight: 12,
|
|
577
|
+
anchor: "top-left"
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* A simple overlay component that displays configuration text
|
|
583
|
+
* and dismisses on q/Esc/Enter/BS/h.
|
|
584
|
+
*/
|
|
585
|
+
var ConfigViewComponent = class {
|
|
586
|
+
lines;
|
|
587
|
+
onDismiss;
|
|
588
|
+
constructor(config, renderer) {
|
|
589
|
+
this.lines = renderer.renderConfig(config);
|
|
590
|
+
}
|
|
591
|
+
/** Set the dismiss callback from showConfigView */
|
|
592
|
+
setDismissCallback(cb) {
|
|
593
|
+
this.onDismiss = cb;
|
|
594
|
+
}
|
|
595
|
+
handleInput(data) {
|
|
596
|
+
if (data === "escape" || data === "q" || data === "enter" || data === "backspace" || data === "" || data === "\b" || data === "h") this.onDismiss?.();
|
|
597
|
+
}
|
|
598
|
+
invalidate() {}
|
|
599
|
+
render(width) {
|
|
600
|
+
if (width < 50) return [
|
|
601
|
+
...this.lines,
|
|
602
|
+
"",
|
|
603
|
+
chalk.gray(" q/Esc 返回")
|
|
604
|
+
];
|
|
605
|
+
return [
|
|
606
|
+
...this.lines,
|
|
607
|
+
"",
|
|
608
|
+
chalk.gray(` ${"─".repeat(Math.max(0, width - 4))}`),
|
|
609
|
+
chalk.gray(" q/Esc 返回 · Enter/BS/h 返回菜单"),
|
|
610
|
+
""
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* Show the current configuration as a dismissible overlay.
|
|
616
|
+
* Returns when the user dismisses it.
|
|
617
|
+
*/
|
|
618
|
+
function showConfigView(tui, config, renderer) {
|
|
619
|
+
return new Promise((resolve) => {
|
|
620
|
+
const component = new ConfigViewComponent(config, renderer);
|
|
621
|
+
const handle = tui.showOverlay(component, {
|
|
622
|
+
width: "100%",
|
|
623
|
+
anchor: "top-left",
|
|
624
|
+
margin: {
|
|
625
|
+
top: 1,
|
|
626
|
+
bottom: 1
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
component.setDismissCallback(() => {
|
|
630
|
+
handle.hide();
|
|
631
|
+
resolve();
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/cli/repl/config-utils.ts
|
|
638
|
+
/**
|
|
639
|
+
* Configuration file utilities
|
|
640
|
+
*
|
|
641
|
+
* Shared read/write helpers for ~/.zapmyco/settings.json,
|
|
642
|
+
* used by settings-cmd.ts and session.ts.
|
|
643
|
+
*/
|
|
644
|
+
/** Read settings.json and return a mutable object */
|
|
645
|
+
function readSettings() {
|
|
646
|
+
try {
|
|
647
|
+
return JSON.parse(readFileSync(HOME_CONFIG_PATH, "utf-8"));
|
|
648
|
+
} catch {
|
|
649
|
+
return {};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/** Write back to settings.json */
|
|
653
|
+
function writeSettings(settings) {
|
|
654
|
+
writeFileSync(HOME_CONFIG_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
655
|
+
}
|
|
656
|
+
/** Safely set a nested property (prototype-chain safe) */
|
|
657
|
+
function _setByDotPath(obj, path, value) {
|
|
658
|
+
const keys = path.split(".");
|
|
659
|
+
let current = obj;
|
|
660
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
661
|
+
const key = keys[i];
|
|
662
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return;
|
|
663
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
664
|
+
current = current[key];
|
|
665
|
+
}
|
|
666
|
+
const lastKey = keys[keys.length - 1];
|
|
667
|
+
if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") return;
|
|
668
|
+
current[lastKey] = value;
|
|
669
|
+
}
|
|
670
|
+
/** Get a nested property value via dot-path */
|
|
671
|
+
function _getByDotPath(obj, path) {
|
|
672
|
+
const keys = path.split(".");
|
|
673
|
+
let current = obj;
|
|
674
|
+
for (const key of keys) {
|
|
675
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
676
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return void 0;
|
|
677
|
+
current = current[key];
|
|
678
|
+
}
|
|
679
|
+
return current;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/cli/repl/commands/settings-cmd.ts
|
|
684
|
+
/**
|
|
685
|
+
* /settings command — interactive configuration menu
|
|
686
|
+
*
|
|
687
|
+
* TUI overlay-based graphical configuration interface:
|
|
688
|
+
* - View and change the default model
|
|
689
|
+
* - Configure API keys, models, and base URLs for existing providers
|
|
690
|
+
* - Add new providers from a curated list
|
|
691
|
+
* - All changes sync to ~/.zapmyco/settings.json in real-time
|
|
692
|
+
*/
|
|
693
|
+
/** Curated list of known providers (sorted by popularity) */
|
|
694
|
+
const KNOWN_PROVIDERS = [
|
|
695
|
+
{
|
|
696
|
+
id: "anthropic",
|
|
697
|
+
label: "Anthropic",
|
|
698
|
+
apiFormat: "anthropic"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: "openai",
|
|
702
|
+
label: "OpenAI"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: "deepseek",
|
|
706
|
+
label: "DeepSeek"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: "google",
|
|
710
|
+
label: "Google (Gemini)"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
id: "mistral",
|
|
714
|
+
label: "Mistral AI"
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: "xai",
|
|
718
|
+
label: "xAI (Grok)"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: "groq",
|
|
722
|
+
label: "Groq"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
id: "openrouter",
|
|
726
|
+
label: "OpenRouter"
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
id: "cerebras",
|
|
730
|
+
label: "Cerebras"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
id: "fireworks",
|
|
734
|
+
label: "Fireworks AI"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
id: "github-copilot",
|
|
738
|
+
label: "GitHub Copilot"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: "huggingface",
|
|
742
|
+
label: "Hugging Face"
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
id: "moonshotai",
|
|
746
|
+
label: "Moonshot AI (Kimi)"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: "cloudflare-workers-ai",
|
|
750
|
+
label: "Cloudflare Workers AI"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
id: "amazon-bedrock",
|
|
754
|
+
label: "Amazon Bedrock"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
id: "zai",
|
|
758
|
+
label: "ZAI"
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
id: "minimax",
|
|
762
|
+
label: "MiniMax"
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
id: "opencode",
|
|
766
|
+
label: "OpenCode"
|
|
767
|
+
}
|
|
768
|
+
];
|
|
769
|
+
/** Set a dot-path value, persist to disk and hot-reload agent */
|
|
770
|
+
function setConfigValue(session, dotPath, value) {
|
|
771
|
+
const settings = readSettings();
|
|
772
|
+
_setByDotPath(settings, dotPath, value);
|
|
773
|
+
writeSettings(settings);
|
|
774
|
+
_setByDotPath(session.config, dotPath, value);
|
|
775
|
+
session.applyConfigUpdate(dotPath);
|
|
776
|
+
}
|
|
777
|
+
/** Check if a provider has an API key configured */
|
|
778
|
+
function hasApiKey(config, providerName) {
|
|
779
|
+
const key = _getByDotPath(config, `llm.providers.${providerName}.apiKey`);
|
|
780
|
+
if (!key) return false;
|
|
781
|
+
const keyStr = String(key);
|
|
782
|
+
return keyStr.length > 0 && keyStr !== "${}";
|
|
783
|
+
}
|
|
784
|
+
/** Get available model IDs for a provider (from config or pi-ai) */
|
|
785
|
+
function getProviderModels(config, providerName) {
|
|
786
|
+
const models = _getByDotPath(config, `llm.providers.${providerName}.models`);
|
|
787
|
+
if (models && typeof models === "object" && Object.keys(models).length > 0) return Object.keys(models);
|
|
788
|
+
try {
|
|
789
|
+
const piModels = getModels(providerName);
|
|
790
|
+
if (piModels && piModels.length > 0) return piModels.map((m) => m.id);
|
|
791
|
+
} catch {}
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Create the /settings command definition
|
|
796
|
+
*/
|
|
797
|
+
function createSettingsCommand() {
|
|
798
|
+
return {
|
|
799
|
+
name: "settings",
|
|
800
|
+
aliases: ["set"],
|
|
801
|
+
description: "Interactive configuration menu — manage model providers and API keys",
|
|
802
|
+
usage: "/settings [list-providers | list-models <provider>]",
|
|
803
|
+
async handler(args, session) {
|
|
804
|
+
const tui = session.getTui();
|
|
805
|
+
const config = readSettings();
|
|
806
|
+
if (args.length > 0) {
|
|
807
|
+
await handleCommandLine(args, session, tui, config);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
await handleInteractiveMode(tui, session, config);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* CLI mode — quick operations via arguments
|
|
816
|
+
*/
|
|
817
|
+
async function handleCommandLine(args, session, _tui, config) {
|
|
818
|
+
switch (args[0]) {
|
|
819
|
+
case "list-providers": {
|
|
820
|
+
const providers = _getByDotPath(config, "llm.providers");
|
|
821
|
+
const names = providers ? Object.keys(providers) : [];
|
|
822
|
+
const lines = ["", "Known providers:"];
|
|
823
|
+
for (const p of KNOWN_PROVIDERS) {
|
|
824
|
+
const configured = names.includes(p.id) ? " ✓" : " ";
|
|
825
|
+
lines.push(` ${configured} ${p.label} (${p.id})`);
|
|
826
|
+
}
|
|
827
|
+
session.appendOutput(lines);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
case "list-models": {
|
|
831
|
+
if (!args[1]) {
|
|
832
|
+
session.appendOutput([
|
|
833
|
+
"",
|
|
834
|
+
"Usage: /settings list-models <provider>",
|
|
835
|
+
""
|
|
836
|
+
]);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const modelIds = getProviderModels(config, args[1]);
|
|
840
|
+
if (modelIds.length === 0) {
|
|
841
|
+
session.appendOutput([
|
|
842
|
+
"",
|
|
843
|
+
`Provider "${args[1]}" has no known models`,
|
|
844
|
+
"Hint: use /settings to configure this provider first",
|
|
845
|
+
""
|
|
846
|
+
]);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
session.appendOutput([
|
|
850
|
+
"",
|
|
851
|
+
`${args[1]} available models:`,
|
|
852
|
+
...modelIds.map((id) => ` - ${id}`),
|
|
853
|
+
""
|
|
854
|
+
]);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
default: session.appendOutput([
|
|
858
|
+
"",
|
|
859
|
+
"Usage:",
|
|
860
|
+
" /settings — Open interactive configuration menu",
|
|
861
|
+
" /settings list-providers — List all known providers",
|
|
862
|
+
" /settings list-models <name> — List available models for a provider",
|
|
863
|
+
""
|
|
864
|
+
]);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Interactive menu mode — main flow
|
|
869
|
+
*/
|
|
870
|
+
async function handleInteractiveMode(tui, session, config) {
|
|
871
|
+
const state = { current: config };
|
|
872
|
+
/**
|
|
873
|
+
* Handle API Key configuration
|
|
874
|
+
*/
|
|
875
|
+
const handleApiKeyConfig = async (providerName, _currentKey) => {
|
|
876
|
+
const envVarName = `${providerName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
877
|
+
const choice = await showSelectList(tui, [
|
|
878
|
+
{
|
|
879
|
+
value: "env",
|
|
880
|
+
label: `Use env var ${"${" + envVarName + "}"}`,
|
|
881
|
+
description: "Recommended — more secure"
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
value: "manual",
|
|
885
|
+
label: "Enter manually",
|
|
886
|
+
description: "Type the key directly (stored in plaintext in settings.json)"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
value: "clear",
|
|
890
|
+
label: "Clear",
|
|
891
|
+
description: "Remove the configured key"
|
|
892
|
+
}
|
|
893
|
+
], { onExit: exitAll });
|
|
894
|
+
if (!choice) return;
|
|
895
|
+
if (choice.value === "clear") {
|
|
896
|
+
setConfigValue(session, `llm.providers.${providerName}.apiKey`, "");
|
|
897
|
+
session.appendOutput([
|
|
898
|
+
"",
|
|
899
|
+
` [ok] Cleared API key for ${providerName}`,
|
|
900
|
+
""
|
|
901
|
+
]);
|
|
902
|
+
} else if (choice.value === "env") {
|
|
903
|
+
setConfigValue(session, `llm.providers.${providerName}.apiKey`, `\${${envVarName}}`);
|
|
904
|
+
session.appendOutput([
|
|
905
|
+
"",
|
|
906
|
+
` [ok] Set ${providerName} API key to env var \${${envVarName}}`,
|
|
907
|
+
` Make sure ${envVarName} is set in your shell`,
|
|
908
|
+
""
|
|
909
|
+
]);
|
|
910
|
+
} else if (choice.value === "manual") {
|
|
911
|
+
const key = await showTextInput(tui, `Enter ${providerName} API Key`, "", "sk-...");
|
|
912
|
+
if (key && key.length > 0) {
|
|
913
|
+
setConfigValue(session, `llm.providers.${providerName}.apiKey`, key);
|
|
914
|
+
session.appendOutput([
|
|
915
|
+
"",
|
|
916
|
+
` [ok] Configured API key for ${providerName}`,
|
|
917
|
+
""
|
|
918
|
+
]);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
/**
|
|
923
|
+
* Handle Base URL configuration
|
|
924
|
+
*/
|
|
925
|
+
const handleBaseUrlConfig = async (providerName, _currentUrl) => {
|
|
926
|
+
const url = await showTextInput(tui, `Enter ${providerName} Base URL`, "", "https://api.example.com");
|
|
927
|
+
if (url !== null) {
|
|
928
|
+
const configPath = `llm.providers.${providerName}.baseUrl`;
|
|
929
|
+
if (url.length === 0) {
|
|
930
|
+
const settings = readSettings();
|
|
931
|
+
_setByDotPath(settings, configPath, void 0);
|
|
932
|
+
const parent = _getByDotPath(settings, `llm.providers.${providerName}`);
|
|
933
|
+
if (parent) delete parent.baseUrl;
|
|
934
|
+
writeSettings(settings);
|
|
935
|
+
session.appendOutput([
|
|
936
|
+
"",
|
|
937
|
+
` [ok] Reset ${providerName} Base URL to default`,
|
|
938
|
+
""
|
|
939
|
+
]);
|
|
940
|
+
} else {
|
|
941
|
+
setConfigValue(session, configPath, url);
|
|
942
|
+
session.appendOutput([
|
|
943
|
+
"",
|
|
944
|
+
` [ok] Set ${providerName} Base URL: ${url}`,
|
|
945
|
+
""
|
|
946
|
+
]);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
/**
|
|
951
|
+
* Handle model selection
|
|
952
|
+
*/
|
|
953
|
+
const handleModelSelect = async (providerName) => {
|
|
954
|
+
const modelIds = getProviderModels(state.current, providerName);
|
|
955
|
+
if (modelIds.length === 0) {
|
|
956
|
+
session.appendOutput([
|
|
957
|
+
"",
|
|
958
|
+
` ${providerName} has no available model list`,
|
|
959
|
+
" Configure models manually in settings.json or check the provider name",
|
|
960
|
+
""
|
|
961
|
+
]);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const selected = await showSelectList(tui, modelIds.map((id) => ({
|
|
965
|
+
value: id,
|
|
966
|
+
label: id,
|
|
967
|
+
description: ""
|
|
968
|
+
})), { onExit: exitAll });
|
|
969
|
+
if (selected && selected.value) {
|
|
970
|
+
const modelId = selected.value;
|
|
971
|
+
const settings = readSettings();
|
|
972
|
+
_setByDotPath(settings, `llm.providers.${providerName}.models.${modelId}`, { id: modelId });
|
|
973
|
+
writeSettings(settings);
|
|
974
|
+
_setByDotPath(session.config, `llm.providers.${providerName}.models`, _getByDotPath(settings, `llm.providers.${providerName}.models`) ?? {});
|
|
975
|
+
session.appendOutput([
|
|
976
|
+
"",
|
|
977
|
+
` [ok] Selected model: ${providerName}/${modelId}`,
|
|
978
|
+
""
|
|
979
|
+
]);
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
/**
|
|
983
|
+
* Handle setting a provider's model as the default
|
|
984
|
+
*/
|
|
985
|
+
const handleSetDefault = async (providerName) => {
|
|
986
|
+
const modelIds = getProviderModels(state.current, providerName);
|
|
987
|
+
if (modelIds.length === 0) {
|
|
988
|
+
session.appendOutput([
|
|
989
|
+
"",
|
|
990
|
+
" Configure a model first before setting it as default",
|
|
991
|
+
""
|
|
992
|
+
]);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const modelKey = `${providerName}/${modelIds[0]}`;
|
|
996
|
+
setConfigValue(session, "llm.defaultModel", modelKey);
|
|
997
|
+
session.appendOutput([
|
|
998
|
+
"",
|
|
999
|
+
` [ok] Default model set to: ${modelKey}`,
|
|
1000
|
+
""
|
|
1001
|
+
]);
|
|
1002
|
+
};
|
|
1003
|
+
let running = true;
|
|
1004
|
+
const exitAll = () => {
|
|
1005
|
+
running = false;
|
|
1006
|
+
};
|
|
1007
|
+
while (running) {
|
|
1008
|
+
state.current = readSettings();
|
|
1009
|
+
const providers = _getByDotPath(state.current, "llm.providers") ?? {};
|
|
1010
|
+
const providerCount = Object.keys(providers).length;
|
|
1011
|
+
const choice = await showSelectList(tui, [
|
|
1012
|
+
{
|
|
1013
|
+
value: "default-model",
|
|
1014
|
+
label: "Default Model",
|
|
1015
|
+
description: String(_getByDotPath(state.current, "llm.defaultModel") ?? "not configured")
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
value: "manage-providers",
|
|
1019
|
+
label: "Manage Providers",
|
|
1020
|
+
description: providerCount > 0 ? `${providerCount} configured` : "none configured"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
value: "view-config",
|
|
1024
|
+
label: "View Config",
|
|
1025
|
+
description: "Display full configuration details"
|
|
1026
|
+
}
|
|
1027
|
+
], { onExit: exitAll });
|
|
1028
|
+
if (!choice) {
|
|
1029
|
+
running = false;
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
const value = choice.value;
|
|
1033
|
+
if (value === "default-model") {
|
|
1034
|
+
const configuredProviders = _getByDotPath(state.current, "llm.providers");
|
|
1035
|
+
const allProviders = getProviders();
|
|
1036
|
+
const enabledItems = [];
|
|
1037
|
+
const disabledItems = [];
|
|
1038
|
+
for (const providerName of allProviders) {
|
|
1039
|
+
const models = getProviderModels(state.current, providerName);
|
|
1040
|
+
if (models.length === 0) continue;
|
|
1041
|
+
const isEnabled = configuredProviders !== void 0 && providerName in configuredProviders && hasApiKey(state.current, providerName);
|
|
1042
|
+
for (const modelId of models) {
|
|
1043
|
+
const key = `${providerName}/${modelId}`;
|
|
1044
|
+
if (isEnabled) enabledItems.push({
|
|
1045
|
+
value: key,
|
|
1046
|
+
label: key,
|
|
1047
|
+
description: ""
|
|
1048
|
+
});
|
|
1049
|
+
else disabledItems.push({
|
|
1050
|
+
value: key,
|
|
1051
|
+
label: chalk.gray(key),
|
|
1052
|
+
description: chalk.gray("未配置 - Enter 设置 API Key")
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const modelItems = [...enabledItems, ...disabledItems];
|
|
1057
|
+
if (modelItems.length === 0) {
|
|
1058
|
+
session.appendOutput([
|
|
1059
|
+
"",
|
|
1060
|
+
" No models available from pi-ai registry",
|
|
1061
|
+
""
|
|
1062
|
+
]);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const selected = await showSelectList(tui, modelItems, { onExit: exitAll });
|
|
1066
|
+
if (!selected || !selected.value) continue;
|
|
1067
|
+
const selectedKey = selected.value;
|
|
1068
|
+
const slashIndex = selectedKey.indexOf("/");
|
|
1069
|
+
const providerName = selectedKey.slice(0, slashIndex);
|
|
1070
|
+
if (configuredProviders !== void 0 && providerName in configuredProviders && hasApiKey(state.current, providerName)) {
|
|
1071
|
+
setConfigValue(session, "llm.defaultModel", selectedKey);
|
|
1072
|
+
session.appendOutput([
|
|
1073
|
+
"",
|
|
1074
|
+
` [ok] Default model set to: ${selectedKey}`,
|
|
1075
|
+
""
|
|
1076
|
+
]);
|
|
1077
|
+
} else {
|
|
1078
|
+
if (!configuredProviders || !(providerName in configuredProviders)) {
|
|
1079
|
+
const known = KNOWN_PROVIDERS.find((p) => p.id === providerName);
|
|
1080
|
+
const newProvider = { apiKey: "" };
|
|
1081
|
+
if (known?.apiFormat) newProvider.apiFormat = known.apiFormat;
|
|
1082
|
+
const settings = readSettings();
|
|
1083
|
+
_setByDotPath(settings, `llm.providers.${providerName}`, newProvider);
|
|
1084
|
+
writeSettings(settings);
|
|
1085
|
+
_setByDotPath(session.config, `llm.providers.${providerName}`, newProvider);
|
|
1086
|
+
state.current = readSettings();
|
|
1087
|
+
}
|
|
1088
|
+
await handleApiKeyConfig(providerName, "");
|
|
1089
|
+
state.current = readSettings();
|
|
1090
|
+
if (hasApiKey(state.current, providerName)) {
|
|
1091
|
+
setConfigValue(session, "llm.defaultModel", selectedKey);
|
|
1092
|
+
session.appendOutput([
|
|
1093
|
+
"",
|
|
1094
|
+
` [ok] Default model set to: ${selectedKey}`,
|
|
1095
|
+
""
|
|
1096
|
+
]);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} else if (value === "manage-providers") {
|
|
1100
|
+
const providerChoice = await showSelectList(tui, [...Object.entries(providers).map(([name]) => {
|
|
1101
|
+
const hasK = hasApiKey(state.current, name);
|
|
1102
|
+
return {
|
|
1103
|
+
value: `provider:${name}`,
|
|
1104
|
+
label: name,
|
|
1105
|
+
description: hasK ? "key configured" : "no key"
|
|
1106
|
+
};
|
|
1107
|
+
}), {
|
|
1108
|
+
value: "add-provider",
|
|
1109
|
+
label: "Add Provider",
|
|
1110
|
+
description: "Select from a list of known providers"
|
|
1111
|
+
}], { onExit: exitAll });
|
|
1112
|
+
if (!providerChoice) continue;
|
|
1113
|
+
const providerValue = providerChoice.value;
|
|
1114
|
+
if (providerValue.startsWith("provider:")) {
|
|
1115
|
+
const providerName = providerValue.slice(9);
|
|
1116
|
+
const action = await showSelectList(tui, [
|
|
1117
|
+
{
|
|
1118
|
+
value: "api-key",
|
|
1119
|
+
label: "Configure API Key",
|
|
1120
|
+
description: ""
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
value: "model",
|
|
1124
|
+
label: "Select Model",
|
|
1125
|
+
description: ""
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
value: "base-url",
|
|
1129
|
+
label: "Base URL",
|
|
1130
|
+
description: ""
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
value: "set-default",
|
|
1134
|
+
label: "Set as Default",
|
|
1135
|
+
description: ""
|
|
1136
|
+
}
|
|
1137
|
+
], { onExit: exitAll });
|
|
1138
|
+
if (!action) continue;
|
|
1139
|
+
switch (action.value) {
|
|
1140
|
+
case "api-key":
|
|
1141
|
+
await handleApiKeyConfig(providerName, String(_getByDotPath(state.current, `llm.providers.${providerName}.apiKey`) ?? ""));
|
|
1142
|
+
break;
|
|
1143
|
+
case "model":
|
|
1144
|
+
await handleModelSelect(providerName);
|
|
1145
|
+
break;
|
|
1146
|
+
case "base-url":
|
|
1147
|
+
await handleBaseUrlConfig(providerName, String(_getByDotPath(state.current, `llm.providers.${providerName}.baseUrl`) ?? ""));
|
|
1148
|
+
break;
|
|
1149
|
+
case "set-default":
|
|
1150
|
+
await handleSetDefault(providerName);
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
} else if (providerValue === "add-provider") {
|
|
1154
|
+
const selected = await showSelectList(tui, KNOWN_PROVIDERS.map((p) => ({
|
|
1155
|
+
value: p.id,
|
|
1156
|
+
label: `${p.label} (${p.id})`,
|
|
1157
|
+
description: p.apiFormat ? `API format: ${p.apiFormat}` : "OpenAI compatible"
|
|
1158
|
+
})), {
|
|
1159
|
+
maxVisible: 12,
|
|
1160
|
+
onExit: exitAll
|
|
1161
|
+
});
|
|
1162
|
+
if (!selected || !selected.value) continue;
|
|
1163
|
+
const providerName = selected.value;
|
|
1164
|
+
const existingProviders = _getByDotPath(state.current, "llm.providers");
|
|
1165
|
+
if (existingProviders && providerName in existingProviders) {
|
|
1166
|
+
session.appendOutput([
|
|
1167
|
+
"",
|
|
1168
|
+
` ${providerName} already exists, use the provider entry to configure it`,
|
|
1169
|
+
""
|
|
1170
|
+
]);
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const known = KNOWN_PROVIDERS.find((p) => p.id === providerName);
|
|
1174
|
+
const newProvider = { apiKey: "" };
|
|
1175
|
+
if (known?.apiFormat) newProvider.apiFormat = known.apiFormat;
|
|
1176
|
+
const settings = readSettings();
|
|
1177
|
+
_setByDotPath(settings, `llm.providers.${providerName}`, newProvider);
|
|
1178
|
+
writeSettings(settings);
|
|
1179
|
+
_setByDotPath(session.config, `llm.providers.${providerName}`, newProvider);
|
|
1180
|
+
session.appendOutput([
|
|
1181
|
+
"",
|
|
1182
|
+
` [ok] Added provider: ${providerName}`,
|
|
1183
|
+
""
|
|
1184
|
+
]);
|
|
1185
|
+
if ((await showSelectList(tui, [{
|
|
1186
|
+
value: "yes",
|
|
1187
|
+
label: "Yes, configure API Key now",
|
|
1188
|
+
description: "Go to API Key settings"
|
|
1189
|
+
}, {
|
|
1190
|
+
value: "no",
|
|
1191
|
+
label: "Later",
|
|
1192
|
+
description: "Return to main menu"
|
|
1193
|
+
}], { onExit: exitAll }))?.value === "yes") await handleApiKeyConfig(providerName, "");
|
|
1194
|
+
}
|
|
1195
|
+
} else if (value === "view-config") {
|
|
1196
|
+
const renderer = session.getRenderer();
|
|
1197
|
+
await showConfigView(tui, session.config, renderer);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
297
1202
|
//#endregion
|
|
298
1203
|
//#region src/cli/repl/commands/status.ts
|
|
299
1204
|
/**
|
|
@@ -321,6 +1226,7 @@ function createStatusCommand() {
|
|
|
321
1226
|
* 继承自 pi-tui 的 Editor,添加 zapmyco 特有的快捷键处理:
|
|
322
1227
|
* - Ctrl+C: 取消任务 / 二次退出
|
|
323
1228
|
* - Ctrl+D: 退出
|
|
1229
|
+
* - Ctrl+O: 打开外部编辑器编辑输入
|
|
324
1230
|
* - Escape: 取消当前输入
|
|
325
1231
|
*
|
|
326
1232
|
* 同时 override render() 以:
|
|
@@ -357,6 +1263,8 @@ var ZapmycoEditor = class extends Editor {
|
|
|
357
1263
|
onCtrlC;
|
|
358
1264
|
/** Ctrl+D 回调 */
|
|
359
1265
|
onCtrlD;
|
|
1266
|
+
/** Ctrl+O 回调(打开外部编辑器) */
|
|
1267
|
+
onOpenEditor;
|
|
360
1268
|
/** 是否正在执行(用于显示 loading) */
|
|
361
1269
|
#executing = false;
|
|
362
1270
|
/** 是否显示 spinner(执行期间禁用输入但不一定显示 spinner) */
|
|
@@ -378,6 +1286,10 @@ var ZapmycoEditor = class extends Editor {
|
|
|
378
1286
|
if (this.getText().length === 0 && this.onCtrlD) this.onCtrlD();
|
|
379
1287
|
return;
|
|
380
1288
|
}
|
|
1289
|
+
if (matchesKey(data, Key.ctrl("o")) && this.onOpenEditor) {
|
|
1290
|
+
this.onOpenEditor();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
381
1293
|
super.handleInput(data);
|
|
382
1294
|
}
|
|
383
1295
|
/**
|
|
@@ -1400,13 +2312,17 @@ var OutputFormatter = class {
|
|
|
1400
2312
|
c.bold(" LLM:")
|
|
1401
2313
|
];
|
|
1402
2314
|
lines.push(` 默认模型: ${config.llm.defaultModel}`);
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
lines.push(`
|
|
2315
|
+
const defaultModelKey = config.llm.defaultModel;
|
|
2316
|
+
const slashIdx = defaultModelKey.indexOf("/");
|
|
2317
|
+
const defaultProvider = slashIdx > 0 ? defaultModelKey.slice(0, slashIdx) : "anthropic";
|
|
2318
|
+
const defaultModelName = slashIdx > 0 ? defaultModelKey.slice(slashIdx + 1) : defaultModelKey;
|
|
2319
|
+
const providerConfig = config.llm.providers[defaultProvider];
|
|
2320
|
+
const modelConfig = providerConfig?.models?.[defaultModelName];
|
|
2321
|
+
lines.push(` 提供商: ${defaultProvider}`);
|
|
2322
|
+
lines.push(` 模型 ID: ${modelConfig?.id ?? defaultModelName}`);
|
|
2323
|
+
if (modelConfig?.input && modelConfig.input.length > 0) lines.push(` 输入类型: ${modelConfig.input.join(", ")}`);
|
|
2324
|
+
lines.push(` API Key: ${providerConfig?.apiKey ? c.gray("***已配置***") : c.red("(未配置)")}`);
|
|
2325
|
+
if (providerConfig?.apiFormat) lines.push(` API 格式: ${providerConfig.apiFormat}`);
|
|
1410
2326
|
lines.push(c.bold(" 调度器:"));
|
|
1411
2327
|
lines.push(` 最大并行: ${config.scheduler.maxConcurrency}`);
|
|
1412
2328
|
lines.push(` 单 Agent 最大并发: ${config.scheduler.maxPerAgent}`);
|
|
@@ -6613,26 +7529,32 @@ var TaskStore = class {
|
|
|
6613
7529
|
};
|
|
6614
7530
|
|
|
6615
7531
|
//#endregion
|
|
6616
|
-
//#region src/
|
|
7532
|
+
//#region src/cli/repl/session.ts
|
|
6617
7533
|
/**
|
|
6618
|
-
*
|
|
7534
|
+
* REPL 会话核心(pi-tui 版)
|
|
6619
7535
|
*
|
|
6620
|
-
*
|
|
6621
|
-
*
|
|
7536
|
+
* 使用 @mariozechner/pi-tui 框架替代 readline,
|
|
7537
|
+
* 实现完整的 TUI 交互式 REPL:
|
|
7538
|
+
* - Editor 组件自带上下边框
|
|
7539
|
+
* - 差量渲染,无闪烁
|
|
7540
|
+
* - 组件化布局,可扩展
|
|
6622
7541
|
*/
|
|
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
7542
|
const log = logger.child("repl:session");
|
|
6635
7543
|
/**
|
|
7544
|
+
* 检查错误消息是否匹配 "No API key for provider",返回解决指引行
|
|
7545
|
+
*/
|
|
7546
|
+
function getApiKeyErrorHelp(errorMessage) {
|
|
7547
|
+
const match = errorMessage.match(/No API key for provider: (\w+)/);
|
|
7548
|
+
if (!match) return [];
|
|
7549
|
+
const providerName = match[1];
|
|
7550
|
+
const envVarName = `${providerName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
7551
|
+
return [
|
|
7552
|
+
"",
|
|
7553
|
+
chalk.yellow(` 请设置环境变量: export ${envVarName}=<your-api-key>`),
|
|
7554
|
+
chalk.yellow(` 或在 REPL 中使用: /config set llm.providers.${providerName}.apiKey <your-key>`)
|
|
7555
|
+
];
|
|
7556
|
+
}
|
|
7557
|
+
/**
|
|
6636
7558
|
* 输出区域组件
|
|
6637
7559
|
*
|
|
6638
7560
|
* 管理所有输出内容的行缓冲,实现 pi-tui 的 render 接口。
|
|
@@ -6717,6 +7639,12 @@ var ReplSession = class {
|
|
|
6717
7639
|
const theme = createTheme(this.options.color);
|
|
6718
7640
|
const terminal = new ProcessTerminal();
|
|
6719
7641
|
this.tui = new TUI(terminal);
|
|
7642
|
+
getKeybindings().setUserBindings({
|
|
7643
|
+
"tui.select.up": ["up", "k"],
|
|
7644
|
+
"tui.select.down": ["down", "j"],
|
|
7645
|
+
"tui.select.cancel": ["escape", "h"],
|
|
7646
|
+
"tui.select.confirm": ["enter", "l"]
|
|
7647
|
+
});
|
|
6720
7648
|
this.outputArea = new OutputArea();
|
|
6721
7649
|
this.editor = new ZapmycoEditor(this.tui, theme.editorTheme);
|
|
6722
7650
|
const root = new Container();
|
|
@@ -6783,6 +7711,10 @@ var ReplSession = class {
|
|
|
6783
7711
|
getRenderer() {
|
|
6784
7712
|
return this.renderer;
|
|
6785
7713
|
}
|
|
7714
|
+
/** 获取 TUI 实例(用于显示 overlay 菜单) */
|
|
7715
|
+
getTui() {
|
|
7716
|
+
return this.tui;
|
|
7717
|
+
}
|
|
6786
7718
|
/** 获取历史存储引用 */
|
|
6787
7719
|
getHistoryStore() {
|
|
6788
7720
|
return this.history;
|
|
@@ -6813,6 +7745,12 @@ var ReplSession = class {
|
|
|
6813
7745
|
let historyEntry;
|
|
6814
7746
|
const taskId = `task-${Date.now()}`;
|
|
6815
7747
|
const ZAPMYCO_PREFIX = "ZapMyco: ";
|
|
7748
|
+
const THINKING_PREFIX = " 💭 ";
|
|
7749
|
+
const colorEnabled = this.options.color;
|
|
7750
|
+
const userStyle = (s) => colorEnabled ? chalk.bold.cyan(s) : s;
|
|
7751
|
+
const responseStyle = (s) => s;
|
|
7752
|
+
const toolStyle = (s) => colorEnabled ? chalk.yellow(s) : s;
|
|
7753
|
+
const thinkingStyle = (s) => colorEnabled ? chalk.gray(s) : s;
|
|
6816
7754
|
let spinnerActive = true;
|
|
6817
7755
|
let spinnerInterval;
|
|
6818
7756
|
try {
|
|
@@ -6828,37 +7766,64 @@ var ReplSession = class {
|
|
|
6828
7766
|
goalId: `goal-${startTime}`,
|
|
6829
7767
|
rawInput
|
|
6830
7768
|
});
|
|
6831
|
-
this.outputArea.append([`Me: ${rawInput}
|
|
7769
|
+
this.outputArea.append([userStyle(`Me: ${rawInput}`), responseStyle(ZAPMYCO_PREFIX + LOADING_FRAMES[0])]);
|
|
6832
7770
|
let spinnerFrame = 0;
|
|
6833
7771
|
spinnerActive = true;
|
|
6834
7772
|
spinnerInterval = setInterval(() => {
|
|
6835
7773
|
if (!spinnerActive) return;
|
|
6836
7774
|
spinnerFrame = (spinnerFrame + 1) % LOADING_FRAMES.length;
|
|
6837
|
-
this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + LOADING_FRAMES[spinnerFrame]);
|
|
7775
|
+
this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + LOADING_FRAMES[spinnerFrame]));
|
|
6838
7776
|
this.tui.requestRender();
|
|
6839
7777
|
}, 100);
|
|
6840
7778
|
let firstOutputReceived = false;
|
|
7779
|
+
let outputAccumulator = "";
|
|
7780
|
+
let thinkingAccumulator = "";
|
|
7781
|
+
let streamMode = "response";
|
|
6841
7782
|
const outputHandler = (event) => {
|
|
6842
|
-
if (event.taskId
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
7783
|
+
if (event.taskId !== taskId || !event.text) return;
|
|
7784
|
+
if (!firstOutputReceived) {
|
|
7785
|
+
firstOutputReceived = true;
|
|
7786
|
+
spinnerActive = false;
|
|
7787
|
+
clearInterval(spinnerInterval);
|
|
7788
|
+
streamMode = "response";
|
|
7789
|
+
thinkingAccumulator = "";
|
|
7790
|
+
outputAccumulator = event.text;
|
|
7791
|
+
this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputAccumulator));
|
|
7792
|
+
} else if (streamMode !== "response") {
|
|
7793
|
+
streamMode = "response";
|
|
7794
|
+
thinkingAccumulator = "";
|
|
7795
|
+
outputAccumulator = event.text;
|
|
7796
|
+
this.outputArea.append([responseStyle(ZAPMYCO_PREFIX + outputAccumulator)]);
|
|
7797
|
+
} else {
|
|
7798
|
+
outputAccumulator += event.text;
|
|
7799
|
+
this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputAccumulator));
|
|
6850
7800
|
}
|
|
7801
|
+
this.tui.requestRender();
|
|
7802
|
+
};
|
|
7803
|
+
const thinkingHandler = (event) => {
|
|
7804
|
+
if (event.taskId !== taskId || !event.text) return;
|
|
7805
|
+
if (streamMode !== "thinking") {
|
|
7806
|
+
streamMode = "thinking";
|
|
7807
|
+
outputAccumulator = "";
|
|
7808
|
+
thinkingAccumulator = event.text;
|
|
7809
|
+
this.outputArea.append([thinkingStyle(THINKING_PREFIX + thinkingAccumulator)]);
|
|
7810
|
+
} else {
|
|
7811
|
+
thinkingAccumulator += event.text;
|
|
7812
|
+
this.outputArea.replaceLastLine(thinkingStyle(THINKING_PREFIX + thinkingAccumulator));
|
|
7813
|
+
}
|
|
7814
|
+
this.tui.requestRender();
|
|
6851
7815
|
};
|
|
6852
7816
|
const errorHandler = (event) => {
|
|
6853
7817
|
if (event.taskId === taskId) log.error("Agent 执行中收到 error 事件", { error: event.error.message });
|
|
6854
7818
|
};
|
|
6855
7819
|
const progressHandler = (event) => {
|
|
6856
7820
|
if (event.taskId === taskId && event.percent === 0) {
|
|
6857
|
-
this.outputArea.append([` → ${event.message}`]);
|
|
7821
|
+
this.outputArea.append([toolStyle(` → ${event.message}`)]);
|
|
6858
7822
|
this.tui.requestRender();
|
|
6859
7823
|
}
|
|
6860
7824
|
};
|
|
6861
7825
|
this.agent.on(this.agent.EVENT_OUTPUT, outputHandler);
|
|
7826
|
+
this.agent.on(this.agent.EVENT_THINKING, thinkingHandler);
|
|
6862
7827
|
this.agent.on(this.agent.EVENT_ERROR, errorHandler);
|
|
6863
7828
|
this.agent.on(this.agent.EVENT_PROGRESS, progressHandler);
|
|
6864
7829
|
this.currentTaskId = taskId;
|
|
@@ -6876,6 +7841,7 @@ var ReplSession = class {
|
|
|
6876
7841
|
}
|
|
6877
7842
|
});
|
|
6878
7843
|
this.agent.off(this.agent.EVENT_OUTPUT, outputHandler);
|
|
7844
|
+
this.agent.off(this.agent.EVENT_THINKING, thinkingHandler);
|
|
6879
7845
|
this.agent.off(this.agent.EVENT_ERROR, errorHandler);
|
|
6880
7846
|
this.agent.off(this.agent.EVENT_PROGRESS, progressHandler);
|
|
6881
7847
|
log.debug("Agent 执行完成", {
|
|
@@ -6888,11 +7854,50 @@ var ReplSession = class {
|
|
|
6888
7854
|
if (spinnerActive) {
|
|
6889
7855
|
spinnerActive = false;
|
|
6890
7856
|
clearInterval(spinnerInterval);
|
|
6891
|
-
if (outputText) this.outputArea.replaceLastLine(ZAPMYCO_PREFIX + outputText);
|
|
7857
|
+
if (outputText) this.outputArea.replaceLastLine(responseStyle(ZAPMYCO_PREFIX + outputText));
|
|
7858
|
+
else if (taskResult.status !== "success") {
|
|
7859
|
+
const errorMsg = taskResult.error?.message ?? "Agent 执行失败(无详细错误信息)";
|
|
7860
|
+
this.outputArea.replaceLastLine(chalk.red(`ZapMyco: [错误] ${errorMsg}`));
|
|
7861
|
+
const helpLines = getApiKeyErrorHelp(errorMsg);
|
|
7862
|
+
if (helpLines.length > 0) {
|
|
7863
|
+
this.outputArea.append(helpLines);
|
|
7864
|
+
const providerMatch = errorMsg.match(/No API key for provider: (\w+)/);
|
|
7865
|
+
if (providerMatch) {
|
|
7866
|
+
const providerName = providerMatch[1];
|
|
7867
|
+
this.outputArea.append([""]);
|
|
7868
|
+
if ((await showSelectList(this.tui, [{
|
|
7869
|
+
value: "yes",
|
|
7870
|
+
label: "好的,我来输入 API Key",
|
|
7871
|
+
description: `直接输入 ${providerName} 的 API Key,立即配置并重试`
|
|
7872
|
+
}, {
|
|
7873
|
+
value: "no",
|
|
7874
|
+
label: "稍后再说",
|
|
7875
|
+
description: "回到对话"
|
|
7876
|
+
}], { title: `需要配置 ${providerName} 的 API Key` }))?.value === "yes") {
|
|
7877
|
+
const apiKey = await showTextInput(this.tui, `请输入 ${providerName} 的 API Key:`, "", "sk-...");
|
|
7878
|
+
if (apiKey && apiKey.length > 0) {
|
|
7879
|
+
const dotPath = `llm.providers.${providerName}.apiKey`;
|
|
7880
|
+
const settings = readSettings();
|
|
7881
|
+
_setByDotPath(settings, dotPath, apiKey);
|
|
7882
|
+
writeSettings(settings);
|
|
7883
|
+
_setByDotPath(this.config, dotPath, apiKey);
|
|
7884
|
+
this.applyConfigUpdate(dotPath);
|
|
7885
|
+
this.outputArea.append([
|
|
7886
|
+
"",
|
|
7887
|
+
chalk.green(`已配置 ${providerName} 的 API Key,正在重试...`),
|
|
7888
|
+
""
|
|
7889
|
+
]);
|
|
7890
|
+
return await this.executeGoal(rawInput);
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
this.outputArea.append([""]);
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
} else this.outputArea.replaceLastLine(chalk.red("ZapMyco: [错误] 模型未返回任何内容,请检查 API Key 配置"));
|
|
6892
7897
|
}
|
|
6893
7898
|
if (taskResult.status !== "success") {
|
|
6894
7899
|
const errorMsg = taskResult.error?.message ?? "Agent 执行失败(无详细错误信息)";
|
|
6895
|
-
this.outputArea.appendText(`[错误] ${errorMsg}`);
|
|
7900
|
+
if (!spinnerActive || outputText) this.outputArea.appendText(`[错误] ${errorMsg}`);
|
|
6896
7901
|
log.error("Agent 执行返回 failure", {
|
|
6897
7902
|
taskId,
|
|
6898
7903
|
error: taskResult.error,
|
|
@@ -6946,7 +7951,42 @@ var ReplSession = class {
|
|
|
6946
7951
|
goalId: `goal-${startTime}`,
|
|
6947
7952
|
error: err
|
|
6948
7953
|
});
|
|
6949
|
-
this.outputArea.replaceLastLine(
|
|
7954
|
+
this.outputArea.replaceLastLine(responseStyle(`${ZAPMYCO_PREFIX}[错误] ${err.message}`));
|
|
7955
|
+
const helpLines = getApiKeyErrorHelp(err.message);
|
|
7956
|
+
if (helpLines.length > 0) {
|
|
7957
|
+
this.outputArea.append(helpLines);
|
|
7958
|
+
const providerMatch = err.message.match(/No API key for provider: (\w+)/);
|
|
7959
|
+
if (providerMatch) {
|
|
7960
|
+
const providerName = providerMatch[1];
|
|
7961
|
+
this.outputArea.append([""]);
|
|
7962
|
+
if ((await showSelectList(this.tui, [{
|
|
7963
|
+
value: "yes",
|
|
7964
|
+
label: "好的,我来输入 API Key",
|
|
7965
|
+
description: `直接输入 ${providerName} 的 API Key,立即配置并重试`
|
|
7966
|
+
}, {
|
|
7967
|
+
value: "no",
|
|
7968
|
+
label: "稍后再说",
|
|
7969
|
+
description: "回到对话"
|
|
7970
|
+
}], { title: `需要配置 ${providerName} 的 API Key` }))?.value === "yes") {
|
|
7971
|
+
const apiKey = await showTextInput(this.tui, `请输入 ${providerName} 的 API Key:`, "", "sk-...");
|
|
7972
|
+
if (apiKey && apiKey.length > 0) {
|
|
7973
|
+
const dotPath = `llm.providers.${providerName}.apiKey`;
|
|
7974
|
+
const settings = readSettings();
|
|
7975
|
+
_setByDotPath(settings, dotPath, apiKey);
|
|
7976
|
+
writeSettings(settings);
|
|
7977
|
+
_setByDotPath(this.config, dotPath, apiKey);
|
|
7978
|
+
this.applyConfigUpdate(dotPath);
|
|
7979
|
+
this.outputArea.append([
|
|
7980
|
+
"",
|
|
7981
|
+
chalk.green(`已配置 ${providerName} 的 API Key,正在重试...`),
|
|
7982
|
+
""
|
|
7983
|
+
]);
|
|
7984
|
+
return await this.executeGoal(rawInput);
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
this.outputArea.append([""]);
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
6950
7990
|
const errorLines = this.renderer.renderError(err).slice(1);
|
|
6951
7991
|
if (errorLines.length > 0) this.outputArea.append(errorLines);
|
|
6952
7992
|
const duration = Date.now() - startTime;
|
|
@@ -7012,6 +8052,7 @@ var ReplSession = class {
|
|
|
7012
8052
|
this.registry.register(createConfigCommand());
|
|
7013
8053
|
this.registry.register(createAgentsCommand());
|
|
7014
8054
|
this.registry.register(createStatusCommand());
|
|
8055
|
+
this.registry.register(createSettingsCommand());
|
|
7015
8056
|
this.buildAutocompleteProvider();
|
|
7016
8057
|
}
|
|
7017
8058
|
/** 构建并设置 autocomplete provider,将命令注册表中的命令接入 pi-tui 补全系统 */
|
|
@@ -7051,39 +8092,25 @@ var ReplSession = class {
|
|
|
7051
8092
|
}],
|
|
7052
8093
|
runtimeConfig: this.config.agentRuntime ?? {}
|
|
7053
8094
|
});
|
|
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");
|
|
8095
|
+
const facade = new AgentLlmFacade(this.config.llm);
|
|
8096
|
+
agent.innerAgent.state.model = facade.resolvePiModel();
|
|
8097
|
+
agent.innerAgent.getApiKey = facade.createGetApiKeyFn();
|
|
8098
|
+
agent.llmFacade = facade;
|
|
8099
|
+
const defaultModelInfo = facade.getModelInfo();
|
|
8100
|
+
if (defaultModelInfo) {
|
|
8101
|
+
if (!facade.getApiKey(defaultModelInfo.provider)) {
|
|
8102
|
+
const providerName = defaultModelInfo.provider;
|
|
8103
|
+
const envVar = providerName.toUpperCase() + "_API_KEY";
|
|
8104
|
+
this.outputArea.append([
|
|
8105
|
+
chalk.red(`[!] 提供商 "${providerName}" 没有配置 API Key`),
|
|
8106
|
+
chalk.yellow(` 请设置环境变量: export ${envVar}=<your-key>`),
|
|
8107
|
+
chalk.yellow(` 或在 REPL 中使用: /config set llm.providers.${providerName}.apiKey <your-key>`),
|
|
8108
|
+
""
|
|
8109
|
+
]);
|
|
8110
|
+
log.warn("默认提供商缺少 API Key", { provider: providerName });
|
|
8111
|
+
}
|
|
7081
8112
|
}
|
|
7082
|
-
|
|
7083
|
-
model.name = modelKey;
|
|
7084
|
-
model.id = modelId;
|
|
7085
|
-
if (modelConfig?.baseUrl) model.baseUrl = modelConfig.baseUrl;
|
|
7086
|
-
return model;
|
|
8113
|
+
return agent;
|
|
7087
8114
|
}
|
|
7088
8115
|
/**
|
|
7089
8116
|
* 注册 REPL 场景下的基础工具
|
|
@@ -7189,6 +8216,7 @@ var ReplSession = class {
|
|
|
7189
8216
|
this.editor.onCtrlD = () => {
|
|
7190
8217
|
this.shutdown("收到 EOF (Ctrl+D)");
|
|
7191
8218
|
};
|
|
8219
|
+
this.editor.onOpenEditor = () => this.openInEditor();
|
|
7192
8220
|
}
|
|
7193
8221
|
/** 设置信号处理 */
|
|
7194
8222
|
setupSignalHandlers() {
|
|
@@ -7211,6 +8239,71 @@ var ReplSession = class {
|
|
|
7211
8239
|
this.currentTaskAbort = null;
|
|
7212
8240
|
}
|
|
7213
8241
|
}
|
|
8242
|
+
/**
|
|
8243
|
+
* 打开外部编辑器(vim / $EDITOR)编辑当前输入内容
|
|
8244
|
+
*
|
|
8245
|
+
* 流程:
|
|
8246
|
+
* 1. 将编辑器当前文本写入临时文件
|
|
8247
|
+
* 2. 暂停 TUI(恢复终端 cooked 模式)
|
|
8248
|
+
* 3. 启动外部编辑器,用户编辑并保存退出
|
|
8249
|
+
* 4. 读取编辑后的内容并更新编辑器
|
|
8250
|
+
* 5. 恢复 TUI 并重绘
|
|
8251
|
+
*/
|
|
8252
|
+
openInEditor() {
|
|
8253
|
+
const tmpFile = join(tmpdir(), "zapmyco-editor-input.txt");
|
|
8254
|
+
let tuiStopped = false;
|
|
8255
|
+
try {
|
|
8256
|
+
const currentText = this.editor.getExpandedText();
|
|
8257
|
+
writeFileSync(tmpFile, currentText, "utf-8");
|
|
8258
|
+
this.tui.stop();
|
|
8259
|
+
tuiStopped = true;
|
|
8260
|
+
const editorCmd = process.env.VISUAL || process.env.EDITOR || "vim";
|
|
8261
|
+
const result = spawnSync(editorCmd, [tmpFile], { stdio: "inherit" });
|
|
8262
|
+
const newText = readFileSync(tmpFile, "utf-8");
|
|
8263
|
+
if (newText !== currentText) this.editor.setText(newText);
|
|
8264
|
+
if (result.error) {
|
|
8265
|
+
const err = result.error;
|
|
8266
|
+
if (err.code === "ENOENT") this.outputArea.append([
|
|
8267
|
+
"",
|
|
8268
|
+
`未找到编辑器: ${editorCmd},请设置 $EDITOR 环境变量`,
|
|
8269
|
+
""
|
|
8270
|
+
]);
|
|
8271
|
+
else this.outputArea.append([
|
|
8272
|
+
"",
|
|
8273
|
+
`编辑器启动失败: ${err.message}`,
|
|
8274
|
+
""
|
|
8275
|
+
]);
|
|
8276
|
+
}
|
|
8277
|
+
} catch (err) {
|
|
8278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8279
|
+
this.outputArea.append([
|
|
8280
|
+
"",
|
|
8281
|
+
`打开编辑器失败: ${message}`,
|
|
8282
|
+
""
|
|
8283
|
+
]);
|
|
8284
|
+
} finally {
|
|
8285
|
+
if (tuiStopped) {
|
|
8286
|
+
this.tui.start();
|
|
8287
|
+
this.tui.requestRender(true);
|
|
8288
|
+
}
|
|
8289
|
+
try {
|
|
8290
|
+
unlinkSync(tmpFile);
|
|
8291
|
+
} catch {}
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
/**
|
|
8295
|
+
* 应用配置更新到运行中的 Agent(无需重启)
|
|
8296
|
+
*
|
|
8297
|
+
* 当前处理以 "llm." 开头的配置变更,重新创建 AgentLlmFacade
|
|
8298
|
+
* 并注入到运行中的 Agent 实例,使新 Key/模型立即生效。
|
|
8299
|
+
*/
|
|
8300
|
+
applyConfigUpdate(key) {
|
|
8301
|
+
if (!key.startsWith("llm.")) return;
|
|
8302
|
+
const newFacade = new AgentLlmFacade(this.config.llm);
|
|
8303
|
+
this.agent.innerAgent.state.model = newFacade.resolvePiModel();
|
|
8304
|
+
this.agent.innerAgent.getApiKey = newFacade.createGetApiKeyFn();
|
|
8305
|
+
this.agent.llmFacade = newFacade;
|
|
8306
|
+
}
|
|
7214
8307
|
/** 更新统计中的状态字段 */
|
|
7215
8308
|
updateStatsState() {
|
|
7216
8309
|
this.stats.state = this._state;
|