zapmyco 0.9.0 → 0.11.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 +3748 -495
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +146 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{loader-Dtu0hhLu.mjs → loader-CosX5iDC.mjs} +144 -9
- package/dist/loader-CosX5iDC.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/loader-Dtu0hhLu.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as configureLogger, H as SESSION_DIR_NAME, I as ZapmycoErrorCode, L as buildSkillSnapshot, O as logger, P as WebError, R as loadSkills, T as DEFAULT_COMPACTION_CONFIG, U as VERSION, V as APP_NAME, W as __require, h as createLlmBasedAgent, i as AgentLlmFacade, n as loadConfig, p as SubAgentManager, t as HOME_CONFIG_PATH, w as eventBus } from "../loader-
|
|
2
|
+
import { D as configureLogger, H as SESSION_DIR_NAME, I as ZapmycoErrorCode, L as buildSkillSnapshot, O as logger, P as WebError, R as loadSkills, T as DEFAULT_COMPACTION_CONFIG, U as VERSION, V as APP_NAME, W as __require, h as createLlmBasedAgent, i as AgentLlmFacade, n as loadConfig, p as SubAgentManager, t as HOME_CONFIG_PATH, w as eventBus } from "../loader-CosX5iDC.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, 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
7
|
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
9
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
10
10
|
import { EventEmitter } from "node:events";
|
|
11
11
|
import { getModels, getProviders } from "@mariozechner/pi-ai";
|
|
12
12
|
import chalk, { Chalk } from "chalk";
|
|
13
13
|
import { Command } from "commander";
|
|
14
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
15
15
|
import { CombinedAutocompleteProvider, Container, Editor, Input, Key, ProcessTerminal, SelectList, TUI, getKeybindings, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
16
16
|
import i18next from "i18next";
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
18
|
import TurndownService from "turndown";
|
|
18
19
|
import { lookup } from "node:dns/promises";
|
|
20
|
+
import { promisify } from "node:util";
|
|
19
21
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
20
22
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
21
23
|
|
|
22
24
|
//#region src/cli/repl/command-registry.ts
|
|
23
|
-
const log$
|
|
25
|
+
const log$21 = logger.child("repl:command-registry");
|
|
24
26
|
/**
|
|
25
27
|
* 命令注册表
|
|
26
28
|
*/
|
|
27
29
|
var CommandRegistry = class {
|
|
30
|
+
session;
|
|
28
31
|
commands = /* @__PURE__ */ new Map();
|
|
29
32
|
aliasMap = /* @__PURE__ */ new Map();
|
|
30
33
|
constructor(session) {
|
|
@@ -35,7 +38,7 @@ var CommandRegistry = class {
|
|
|
35
38
|
*/
|
|
36
39
|
register(cmd) {
|
|
37
40
|
const canonicalName = cmd.name.toLowerCase();
|
|
38
|
-
if (this.commands.has(canonicalName)) log$
|
|
41
|
+
if (this.commands.has(canonicalName)) log$21.warn(`命令 "${canonicalName}" 已存在,将被覆盖`);
|
|
39
42
|
this.commands.set(canonicalName, cmd);
|
|
40
43
|
for (const alias of cmd.aliases) {
|
|
41
44
|
const lowerAlias = alias.toLowerCase();
|
|
@@ -63,7 +66,7 @@ var CommandRegistry = class {
|
|
|
63
66
|
*/
|
|
64
67
|
async dispatch(parsed) {
|
|
65
68
|
if (parsed.kind !== "command") {
|
|
66
|
-
log$
|
|
69
|
+
log$21.warn("dispatch 收到了非 command 类型的输入");
|
|
67
70
|
return;
|
|
68
71
|
}
|
|
69
72
|
const cmd = this.getCommand(parsed.name);
|
|
@@ -75,7 +78,7 @@ var CommandRegistry = class {
|
|
|
75
78
|
await cmd.handler(parsed.args, this.session);
|
|
76
79
|
} catch (error) {
|
|
77
80
|
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
-
log$
|
|
81
|
+
log$21.error(`命令 /${cmd.name} 执行出错`, {}, error);
|
|
79
82
|
console.log(`\n 命令执行出错: ${message}\n`);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
@@ -444,7 +447,7 @@ const noColor = {
|
|
|
444
447
|
|
|
445
448
|
//#endregion
|
|
446
449
|
//#region src/core/agent-team/agent-instance-manager.ts
|
|
447
|
-
const log$
|
|
450
|
+
const log$20 = logger.child("agent-instance-manager");
|
|
448
451
|
/**
|
|
449
452
|
* Agent 实例状态转换表
|
|
450
453
|
*
|
|
@@ -508,7 +511,7 @@ var AgentInstanceManager = class {
|
|
|
508
511
|
const parent = this.instances.get(parentInstanceId);
|
|
509
512
|
if (parent) parent.childInstanceIds.push(instance.instanceId);
|
|
510
513
|
}
|
|
511
|
-
log$
|
|
514
|
+
log$20.debug("注册 Agent 实例", {
|
|
512
515
|
instanceId: instance.instanceId,
|
|
513
516
|
typeId: definition.typeId,
|
|
514
517
|
depth,
|
|
@@ -526,12 +529,12 @@ var AgentInstanceManager = class {
|
|
|
526
529
|
transition(instanceId, newStatus) {
|
|
527
530
|
const instance = this.instances.get(instanceId);
|
|
528
531
|
if (!instance) {
|
|
529
|
-
log$
|
|
532
|
+
log$20.warn("状态转换失败:实例不存在", { instanceId });
|
|
530
533
|
return false;
|
|
531
534
|
}
|
|
532
535
|
const allowed = VALID_TRANSITIONS[instance.status];
|
|
533
536
|
if (!allowed.includes(newStatus)) {
|
|
534
|
-
log$
|
|
537
|
+
log$20.warn("状态转换拒绝", {
|
|
535
538
|
instanceId,
|
|
536
539
|
from: instance.status,
|
|
537
540
|
to: newStatus,
|
|
@@ -540,7 +543,7 @@ var AgentInstanceManager = class {
|
|
|
540
543
|
return false;
|
|
541
544
|
}
|
|
542
545
|
instance.status = newStatus;
|
|
543
|
-
log$
|
|
546
|
+
log$20.debug("Agent 实例状态转换", {
|
|
544
547
|
instanceId,
|
|
545
548
|
typeId: instance.typeId,
|
|
546
549
|
from: instance.status,
|
|
@@ -641,7 +644,7 @@ var AgentInstanceManager = class {
|
|
|
641
644
|
instance.agent.removeAllListeners();
|
|
642
645
|
instance.agent.systemPromptOverride = null;
|
|
643
646
|
this.instances.delete(instanceId);
|
|
644
|
-
log$
|
|
647
|
+
log$20.debug("Agent 实例已清理", { instanceId });
|
|
645
648
|
}
|
|
646
649
|
/**
|
|
647
650
|
* 清理所有终态的实例
|
|
@@ -923,6 +926,7 @@ const plannerType = {
|
|
|
923
926
|
"- **务实优先**:选择最简单可行的方案,避免过度设计",
|
|
924
927
|
"- **分阶段交付**:将方案拆解为增量可交付的阶段",
|
|
925
928
|
"- **可派生子 Agent**:需要调研时 spawn researcher 并行搜索",
|
|
929
|
+
"- **交互式确认**:遇到需要用户决策的方向性问题时,使用 AskUserQuestion 工具获取用户偏好。不要自行假设用户的需求",
|
|
926
930
|
"",
|
|
927
931
|
`## 工作目录\n${ctx.workdir}`
|
|
928
932
|
];
|
|
@@ -1083,7 +1087,7 @@ const BUILTIN_AGENT_TYPES = [
|
|
|
1083
1087
|
*
|
|
1084
1088
|
* @module core/agent-team
|
|
1085
1089
|
*/
|
|
1086
|
-
const log$
|
|
1090
|
+
const log$19 = logger.child("agent-type-registry");
|
|
1087
1091
|
/**
|
|
1088
1092
|
* Agent 类型注册中心
|
|
1089
1093
|
*
|
|
@@ -1107,14 +1111,14 @@ var AgentTypeRegistry = class {
|
|
|
1107
1111
|
*/
|
|
1108
1112
|
register(definition) {
|
|
1109
1113
|
const existing = this.types.get(definition.typeId);
|
|
1110
|
-
if (existing) log$
|
|
1114
|
+
if (existing) log$19.info("覆盖已注册的 Agent 类型", {
|
|
1111
1115
|
typeId: definition.typeId,
|
|
1112
1116
|
oldSource: existing.source,
|
|
1113
1117
|
newSource: definition.source
|
|
1114
1118
|
});
|
|
1115
1119
|
this.types.set(definition.typeId, definition);
|
|
1116
1120
|
this.refreshCache();
|
|
1117
|
-
log$
|
|
1121
|
+
log$19.debug("注册 Agent 类型", {
|
|
1118
1122
|
typeId: definition.typeId,
|
|
1119
1123
|
source: definition.source
|
|
1120
1124
|
});
|
|
@@ -1127,7 +1131,7 @@ var AgentTypeRegistry = class {
|
|
|
1127
1131
|
registerAll(definitions) {
|
|
1128
1132
|
for (const def of definitions) this.types.set(def.typeId, def);
|
|
1129
1133
|
this.refreshCache();
|
|
1130
|
-
log$
|
|
1134
|
+
log$19.info("批量注册 Agent 类型", { count: definitions.length });
|
|
1131
1135
|
}
|
|
1132
1136
|
/**
|
|
1133
1137
|
* 注销 Agent 类型
|
|
@@ -1139,7 +1143,7 @@ var AgentTypeRegistry = class {
|
|
|
1139
1143
|
const result = this.types.delete(typeId);
|
|
1140
1144
|
if (result) {
|
|
1141
1145
|
this.refreshCache();
|
|
1142
|
-
log$
|
|
1146
|
+
log$19.debug("注销 Agent 类型", { typeId });
|
|
1143
1147
|
}
|
|
1144
1148
|
return result;
|
|
1145
1149
|
}
|
|
@@ -1226,7 +1230,7 @@ var AgentTypeRegistry = class {
|
|
|
1226
1230
|
loadBuiltinTypes() {
|
|
1227
1231
|
for (const def of BUILTIN_AGENT_TYPES) this.types.set(def.typeId, def);
|
|
1228
1232
|
this.refreshCache();
|
|
1229
|
-
log$
|
|
1233
|
+
log$19.info("加载内置 Agent 类型", { count: BUILTIN_AGENT_TYPES.length });
|
|
1230
1234
|
}
|
|
1231
1235
|
/**
|
|
1232
1236
|
* 刷新可见类型缓存
|
|
@@ -2028,7 +2032,7 @@ function setLocale(locale) {
|
|
|
2028
2032
|
* Provides SelectList and TextInput overlay dialogs.
|
|
2029
2033
|
*/
|
|
2030
2034
|
/** Overlay layout options for menus */
|
|
2031
|
-
const OVERLAY_OPTIONS = {
|
|
2035
|
+
const OVERLAY_OPTIONS$1 = {
|
|
2032
2036
|
width: "100%",
|
|
2033
2037
|
anchor: "top-left",
|
|
2034
2038
|
margin: {
|
|
@@ -2166,7 +2170,7 @@ function showSelectList(tui, items, options) {
|
|
|
2166
2170
|
handle?.hide();
|
|
2167
2171
|
resolve(null);
|
|
2168
2172
|
};
|
|
2169
|
-
handle = tui.showOverlay(list, OVERLAY_OPTIONS);
|
|
2173
|
+
handle = tui.showOverlay(list, OVERLAY_OPTIONS$1);
|
|
2170
2174
|
});
|
|
2171
2175
|
}
|
|
2172
2176
|
var TextInputComponent = class {
|
|
@@ -3430,7 +3434,7 @@ const CRON_CONSTANTS = {
|
|
|
3430
3434
|
*
|
|
3431
3435
|
* @module cli/repl/cron/cron-scheduler
|
|
3432
3436
|
*/
|
|
3433
|
-
const log$
|
|
3437
|
+
const log$18 = logger.child("cron:scheduler");
|
|
3434
3438
|
var CronScheduler = class extends EventEmitter {
|
|
3435
3439
|
store;
|
|
3436
3440
|
jobs = [];
|
|
@@ -3451,7 +3455,7 @@ var CronScheduler = class extends EventEmitter {
|
|
|
3451
3455
|
if (this.running) return;
|
|
3452
3456
|
const loadedJobs = await this.store.load();
|
|
3453
3457
|
this.jobs = loadedJobs;
|
|
3454
|
-
log$
|
|
3458
|
+
log$18.info(`调度器启动,加载 ${loadedJobs.length} 个 durable 任务`);
|
|
3455
3459
|
await this.handleMissedJobs();
|
|
3456
3460
|
this.checkAutoExpiry();
|
|
3457
3461
|
this.running = true;
|
|
@@ -3467,7 +3471,7 @@ var CronScheduler = class extends EventEmitter {
|
|
|
3467
3471
|
clearInterval(this.timer);
|
|
3468
3472
|
this.timer = null;
|
|
3469
3473
|
}
|
|
3470
|
-
log$
|
|
3474
|
+
log$18.info("调度器已停止");
|
|
3471
3475
|
}
|
|
3472
3476
|
/** 添加任务 */
|
|
3473
3477
|
async addJob(job) {
|
|
@@ -3479,7 +3483,7 @@ var CronScheduler = class extends EventEmitter {
|
|
|
3479
3483
|
this.jobs.push(job);
|
|
3480
3484
|
await this.store.persist(this.jobs);
|
|
3481
3485
|
} else this.sessionJobs.push(job);
|
|
3482
|
-
log$
|
|
3486
|
+
log$18.info("任务已添加", {
|
|
3483
3487
|
id: job.id,
|
|
3484
3488
|
cron: job.cron,
|
|
3485
3489
|
durable: job.durable
|
|
@@ -3609,7 +3613,7 @@ var CronScheduler = class extends EventEmitter {
|
|
|
3609
3613
|
}, delay);
|
|
3610
3614
|
}
|
|
3611
3615
|
if (toDelete.length > 0) {
|
|
3612
|
-
log$
|
|
3616
|
+
log$18.info(`跳过 ${toDelete.length} 个错过的一次性任务(超出补发上限)`);
|
|
3613
3617
|
this.emit("missed-overflow", {
|
|
3614
3618
|
count: toDelete.length,
|
|
3615
3619
|
jobIds: toDelete.map((m) => m.id)
|
|
@@ -3627,7 +3631,7 @@ var CronScheduler = class extends EventEmitter {
|
|
|
3627
3631
|
job.lastFiredAt = now;
|
|
3628
3632
|
job.fireCount++;
|
|
3629
3633
|
this.removeJob(job.id);
|
|
3630
|
-
log$
|
|
3634
|
+
log$18.info("任务已过期并触发最后一次", { id: job.id });
|
|
3631
3635
|
}
|
|
3632
3636
|
}
|
|
3633
3637
|
}
|
|
@@ -3688,7 +3692,7 @@ function applyOneShotJitter(jobId, rawNext) {
|
|
|
3688
3692
|
*
|
|
3689
3693
|
* @module cli/repl/cron/cron-store
|
|
3690
3694
|
*/
|
|
3691
|
-
const log$
|
|
3695
|
+
const log$17 = logger.child("cron:store");
|
|
3692
3696
|
const STORE_FILE = join(join(homedir(), ".zapmyco", "cron"), "scheduled_tasks.json");
|
|
3693
3697
|
var CronStore = class {
|
|
3694
3698
|
filePath;
|
|
@@ -3712,13 +3716,13 @@ var CronStore = class {
|
|
|
3712
3716
|
const raw = await readFile(this.filePath, "utf-8");
|
|
3713
3717
|
const data = JSON.parse(raw);
|
|
3714
3718
|
if (!Array.isArray(data)) {
|
|
3715
|
-
log$
|
|
3719
|
+
log$17.warn("存储文件格式无效(非数组),将使用空列表");
|
|
3716
3720
|
return [];
|
|
3717
3721
|
}
|
|
3718
3722
|
return this.validateJobs(data);
|
|
3719
3723
|
} catch (err) {
|
|
3720
3724
|
if (err.code === "ENOENT") return [];
|
|
3721
|
-
log$
|
|
3725
|
+
log$17.warn("加载定时任务文件失败,将使用空列表", { error: err instanceof Error ? err.message : String(err) });
|
|
3722
3726
|
return [];
|
|
3723
3727
|
}
|
|
3724
3728
|
}
|
|
@@ -3765,7 +3769,7 @@ var CronStore = class {
|
|
|
3765
3769
|
if (typeof obj.maxFires === "number") job.maxFires = obj.maxFires;
|
|
3766
3770
|
valid.push(job);
|
|
3767
3771
|
}
|
|
3768
|
-
if (valid.length < raw.length) log$
|
|
3772
|
+
if (valid.length < raw.length) log$17.warn(`跳过 ${raw.length - valid.length} 个无效任务条目`);
|
|
3769
3773
|
return valid;
|
|
3770
3774
|
}
|
|
3771
3775
|
};
|
|
@@ -3783,7 +3787,7 @@ function getCronStore() {
|
|
|
3783
3787
|
* 基于内存的环形缓冲区,记录 REPL 会话中的用户输入和执行结果。
|
|
3784
3788
|
* 支持文件持久化到 ~/.zapmyco/history.json,跨会话恢复。
|
|
3785
3789
|
*/
|
|
3786
|
-
const log$
|
|
3790
|
+
const log$16 = logger.child("history:store");
|
|
3787
3791
|
/** 默认最大历史条数 */
|
|
3788
3792
|
const DEFAULT_MAX_SIZE = 100;
|
|
3789
3793
|
/** 历史文件存储路径 */
|
|
@@ -3842,13 +3846,13 @@ var HistoryStore = class {
|
|
|
3842
3846
|
if (Array.isArray(data.entries)) {
|
|
3843
3847
|
this.entries = data.entries.slice(-this.maxSize);
|
|
3844
3848
|
this.nextId = typeof data.nextId === "number" ? data.nextId : 1;
|
|
3845
|
-
log$
|
|
3849
|
+
log$16.debug("历史记录已加载", {
|
|
3846
3850
|
count: this.entries.length,
|
|
3847
3851
|
nextId: this.nextId
|
|
3848
3852
|
});
|
|
3849
3853
|
}
|
|
3850
3854
|
} catch {
|
|
3851
|
-
log$
|
|
3855
|
+
log$16.debug("无历史文件或加载失败,使用空历史");
|
|
3852
3856
|
}
|
|
3853
3857
|
}
|
|
3854
3858
|
/** 持久化历史记录到文件 */
|
|
@@ -3861,7 +3865,7 @@ var HistoryStore = class {
|
|
|
3861
3865
|
}, null, 2);
|
|
3862
3866
|
writeFileSync(this.filePath, data, "utf-8");
|
|
3863
3867
|
} catch (err) {
|
|
3864
|
-
log$
|
|
3868
|
+
log$16.warn("历史记录保存失败", { error: err instanceof Error ? err.message : String(err) });
|
|
3865
3869
|
}
|
|
3866
3870
|
}
|
|
3867
3871
|
};
|
|
@@ -3961,6 +3965,432 @@ var InputParser = class {
|
|
|
3961
3965
|
}
|
|
3962
3966
|
};
|
|
3963
3967
|
|
|
3968
|
+
//#endregion
|
|
3969
|
+
//#region src/cli/repl/components/ask-user-question.ts
|
|
3970
|
+
const OVERLAY_OPTIONS = {
|
|
3971
|
+
width: "100%",
|
|
3972
|
+
anchor: "top-left",
|
|
3973
|
+
margin: {
|
|
3974
|
+
top: 1,
|
|
3975
|
+
bottom: 1
|
|
3976
|
+
}
|
|
3977
|
+
};
|
|
3978
|
+
/** 截断文本到指定宽度 */
|
|
3979
|
+
function truncate(text, maxLen) {
|
|
3980
|
+
if (text.length <= maxLen) return text;
|
|
3981
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
3982
|
+
}
|
|
3983
|
+
var AskUserQuestionComponent = class {
|
|
3984
|
+
tui;
|
|
3985
|
+
questions;
|
|
3986
|
+
onResolve;
|
|
3987
|
+
onCancel;
|
|
3988
|
+
phase = "answering";
|
|
3989
|
+
currentQuestionIndex = 0;
|
|
3990
|
+
questionStates;
|
|
3991
|
+
selectedOptionIndex = 0;
|
|
3992
|
+
otherInputValue = "";
|
|
3993
|
+
showPreview = false;
|
|
3994
|
+
constructor(tui, params, onResolve, onCancel) {
|
|
3995
|
+
this.tui = tui;
|
|
3996
|
+
this.questions = params.questions;
|
|
3997
|
+
this.onResolve = onResolve;
|
|
3998
|
+
this.onCancel = onCancel;
|
|
3999
|
+
this.questionStates = this.questions.map(() => ({
|
|
4000
|
+
selectedLabels: [],
|
|
4001
|
+
otherText: ""
|
|
4002
|
+
}));
|
|
4003
|
+
const hasPreview = this.questions.some((q) => q.options.some((o) => o.preview));
|
|
4004
|
+
this.showPreview = hasPreview;
|
|
4005
|
+
}
|
|
4006
|
+
/** 安全获取当前问题 */
|
|
4007
|
+
getCurrentQuestion() {
|
|
4008
|
+
return this.questions[this.currentQuestionIndex];
|
|
4009
|
+
}
|
|
4010
|
+
/** 安全获取当前问题的状态 */
|
|
4011
|
+
getCurrentState() {
|
|
4012
|
+
return this.questionStates[this.currentQuestionIndex];
|
|
4013
|
+
}
|
|
4014
|
+
handleInput(data) {
|
|
4015
|
+
if (data === "escape" || data === "q") {
|
|
4016
|
+
if (this.phase === "other_input") {
|
|
4017
|
+
this.phase = "answering";
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
if (this.phase === "reviewing") {
|
|
4021
|
+
this.phase = "answering";
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
this.onCancel?.();
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
switch (this.phase) {
|
|
4028
|
+
case "answering":
|
|
4029
|
+
this.handleAnsweringInput(data);
|
|
4030
|
+
break;
|
|
4031
|
+
case "other_input":
|
|
4032
|
+
this.handleOtherInput(data);
|
|
4033
|
+
break;
|
|
4034
|
+
case "reviewing":
|
|
4035
|
+
this.handleReviewingInput(data);
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
handleAnsweringInput(data) {
|
|
4040
|
+
const q = this.getCurrentQuestion();
|
|
4041
|
+
const state = this.getCurrentState();
|
|
4042
|
+
const optionCount = q.options.length;
|
|
4043
|
+
if (data >= "1" && data <= "9") {
|
|
4044
|
+
const idx = Number.parseInt(data, 10) - 1;
|
|
4045
|
+
if (idx < optionCount) {
|
|
4046
|
+
this.selectOption(idx);
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
if (idx === optionCount) {
|
|
4050
|
+
this.enterOtherInput();
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
switch (data) {
|
|
4056
|
+
case "up":
|
|
4057
|
+
case "k":
|
|
4058
|
+
if (this.selectedOptionIndex > 0) this.selectedOptionIndex--;
|
|
4059
|
+
return;
|
|
4060
|
+
case "down":
|
|
4061
|
+
case "j":
|
|
4062
|
+
if (this.selectedOptionIndex < optionCount) this.selectedOptionIndex++;
|
|
4063
|
+
return;
|
|
4064
|
+
case " ":
|
|
4065
|
+
if (q.multiSelect && this.selectedOptionIndex < optionCount) this.toggleOption(this.selectedOptionIndex);
|
|
4066
|
+
return;
|
|
4067
|
+
case "tab":
|
|
4068
|
+
this.advanceQuestion();
|
|
4069
|
+
return;
|
|
4070
|
+
case "shift+tab":
|
|
4071
|
+
if (this.currentQuestionIndex > 0) {
|
|
4072
|
+
this.currentQuestionIndex--;
|
|
4073
|
+
this.selectedOptionIndex = 0;
|
|
4074
|
+
}
|
|
4075
|
+
return;
|
|
4076
|
+
case "enter":
|
|
4077
|
+
if (q.multiSelect) {
|
|
4078
|
+
if (state.selectedLabels.length > 0) this.advanceQuestion();
|
|
4079
|
+
} else if (this.selectedOptionIndex < optionCount) {
|
|
4080
|
+
this.selectOption(this.selectedOptionIndex);
|
|
4081
|
+
this.advanceQuestion();
|
|
4082
|
+
} else if (this.selectedOptionIndex === optionCount) this.enterOtherInput();
|
|
4083
|
+
return;
|
|
4084
|
+
case "o":
|
|
4085
|
+
this.enterOtherInput();
|
|
4086
|
+
return;
|
|
4087
|
+
case "p":
|
|
4088
|
+
this.showPreview = !this.showPreview;
|
|
4089
|
+
return;
|
|
4090
|
+
case "h":
|
|
4091
|
+
case "backspace":
|
|
4092
|
+
if (this.currentQuestionIndex > 0) {
|
|
4093
|
+
this.currentQuestionIndex--;
|
|
4094
|
+
this.selectedOptionIndex = 0;
|
|
4095
|
+
}
|
|
4096
|
+
return;
|
|
4097
|
+
default: break;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
handleOtherInput(data) {
|
|
4101
|
+
if (data === "enter") {
|
|
4102
|
+
const trimmed = this.otherInputValue.trim();
|
|
4103
|
+
if (trimmed) {
|
|
4104
|
+
const state = this.getCurrentState();
|
|
4105
|
+
state.selectedLabels = [trimmed];
|
|
4106
|
+
state.otherText = trimmed;
|
|
4107
|
+
}
|
|
4108
|
+
this.otherInputValue = "";
|
|
4109
|
+
this.phase = "answering";
|
|
4110
|
+
this.advanceQuestion();
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
if (data === "escape") {
|
|
4114
|
+
this.otherInputValue = "";
|
|
4115
|
+
this.phase = "answering";
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
if (data === "backspace" || data === "" || data === "\b") {
|
|
4119
|
+
this.otherInputValue = this.otherInputValue.slice(0, -1);
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
if (data.length === 1 && data >= " " && data <= "~") this.otherInputValue += data;
|
|
4123
|
+
}
|
|
4124
|
+
handleReviewingInput(data) {
|
|
4125
|
+
if (data === "enter") {
|
|
4126
|
+
this.submitAnswers();
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
if (data === "escape") {
|
|
4130
|
+
this.phase = "answering";
|
|
4131
|
+
this.currentQuestionIndex = this.questions.length - 1;
|
|
4132
|
+
this.selectedOptionIndex = 0;
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
selectOption(index) {
|
|
4137
|
+
const state = this.getCurrentState();
|
|
4138
|
+
const option = this.getCurrentQuestion().options[index];
|
|
4139
|
+
if (this.getCurrentQuestion().multiSelect) this.toggleOption(index);
|
|
4140
|
+
else state.selectedLabels = [option.label];
|
|
4141
|
+
}
|
|
4142
|
+
toggleOption(index) {
|
|
4143
|
+
const state = this.getCurrentState();
|
|
4144
|
+
const option = this.getCurrentQuestion().options[index];
|
|
4145
|
+
const idx = state.selectedLabels.indexOf(option.label);
|
|
4146
|
+
if (idx >= 0) state.selectedLabels.splice(idx, 1);
|
|
4147
|
+
else state.selectedLabels.push(option.label);
|
|
4148
|
+
}
|
|
4149
|
+
enterOtherInput() {
|
|
4150
|
+
this.otherInputValue = this.getCurrentState().otherText;
|
|
4151
|
+
this.phase = "other_input";
|
|
4152
|
+
}
|
|
4153
|
+
advanceQuestion() {
|
|
4154
|
+
if (this.currentQuestionIndex < this.questions.length - 1) {
|
|
4155
|
+
this.currentQuestionIndex++;
|
|
4156
|
+
this.selectedOptionIndex = 0;
|
|
4157
|
+
} else this.phase = "reviewing";
|
|
4158
|
+
}
|
|
4159
|
+
submitAnswers() {
|
|
4160
|
+
const answers = {};
|
|
4161
|
+
const annotations = {};
|
|
4162
|
+
for (let i = 0; i < this.questions.length; i++) {
|
|
4163
|
+
const q = this.questions[i];
|
|
4164
|
+
const state = this.questionStates[i];
|
|
4165
|
+
if (q.multiSelect) answers[q.question] = [...state.selectedLabels];
|
|
4166
|
+
else answers[q.question] = state.selectedLabels[0] ?? "";
|
|
4167
|
+
if (state.otherText) annotations[q.question] = { notes: `自定义: ${state.otherText}` };
|
|
4168
|
+
}
|
|
4169
|
+
this.onResolve?.({
|
|
4170
|
+
questions: this.questions,
|
|
4171
|
+
answers,
|
|
4172
|
+
annotations: Object.keys(annotations).length > 0 ? annotations : void 0
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
invalidate() {}
|
|
4176
|
+
render(width) {
|
|
4177
|
+
switch (this.phase) {
|
|
4178
|
+
case "reviewing": return this.renderReview(width);
|
|
4179
|
+
case "other_input": return this.renderOtherInput(width);
|
|
4180
|
+
default: return this.renderQuestion(width);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
renderQuestion(fullWidth) {
|
|
4184
|
+
const c = chalk;
|
|
4185
|
+
const showPreviewPanel = this.showPreview && this.questions.some((q) => q.options.some((o) => o.preview));
|
|
4186
|
+
const mainWidth = showPreviewPanel ? Math.floor(fullWidth * .58) : fullWidth;
|
|
4187
|
+
const previewWidth = showPreviewPanel ? fullWidth - mainWidth - 1 : 0;
|
|
4188
|
+
const mainLines = this.renderMainPanel(mainWidth);
|
|
4189
|
+
let previewLines = [];
|
|
4190
|
+
if (showPreviewPanel) previewLines = this.renderPreviewPanel(previewWidth);
|
|
4191
|
+
const maxLines = Math.max(mainLines.length, previewLines.length);
|
|
4192
|
+
const result = [];
|
|
4193
|
+
for (let i = 0; i < maxLines; i++) {
|
|
4194
|
+
const left = i < mainLines.length ? mainLines[i] : " ".repeat(mainWidth);
|
|
4195
|
+
const right = i < previewLines.length ? previewLines[i] : "";
|
|
4196
|
+
const separator = showPreviewPanel ? c.gray("│") : "";
|
|
4197
|
+
result.push(`${left}${separator}${right}`);
|
|
4198
|
+
}
|
|
4199
|
+
return result;
|
|
4200
|
+
}
|
|
4201
|
+
renderMainPanel(width) {
|
|
4202
|
+
const c = chalk;
|
|
4203
|
+
const lines = [];
|
|
4204
|
+
lines.push("");
|
|
4205
|
+
lines.push(this.renderTabs(width));
|
|
4206
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4207
|
+
lines.push("");
|
|
4208
|
+
const q = this.getCurrentQuestion();
|
|
4209
|
+
const wrappedQuestion = this.wrapText(q.question, width - 4);
|
|
4210
|
+
for (const line of wrappedQuestion) lines.push(c.bold(` ${line}`));
|
|
4211
|
+
lines.push("");
|
|
4212
|
+
const state = this.getCurrentState();
|
|
4213
|
+
for (let i = 0; i < q.options.length; i++) {
|
|
4214
|
+
const opt = q.options[i];
|
|
4215
|
+
const isSelected = q.multiSelect ? state.selectedLabels.includes(opt.label) : state.selectedLabels[0] === opt.label;
|
|
4216
|
+
const isFocused = this.selectedOptionIndex === i;
|
|
4217
|
+
const prefix = `${i + 1}`;
|
|
4218
|
+
const checkbox = isSelected ? c.green("●") : "○";
|
|
4219
|
+
const label = isFocused ? c.cyan.bold(opt.label) : c.bold(opt.label);
|
|
4220
|
+
const desc = c.gray(` ${opt.description}`);
|
|
4221
|
+
const line = ` ${c.gray(prefix)} ${checkbox} ${label}`;
|
|
4222
|
+
lines.push(line);
|
|
4223
|
+
if (isFocused && opt.description) lines.push(` ${desc}`);
|
|
4224
|
+
}
|
|
4225
|
+
const otherIdx = q.options.length;
|
|
4226
|
+
const isOtherFocused = this.selectedOptionIndex === otherIdx;
|
|
4227
|
+
const otherLabel = isOtherFocused ? c.cyan.bold("其他 (自定义)") : c.gray("其他 (自定义)");
|
|
4228
|
+
const otherPrefix = `${otherIdx + 1}`;
|
|
4229
|
+
lines.push(` ${c.gray(otherPrefix)} ${otherLabel}`);
|
|
4230
|
+
if (isOtherFocused) lines.push(` ${c.gray("输入自定义答案")}`);
|
|
4231
|
+
lines.push("");
|
|
4232
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4233
|
+
const termHeight = this.tui.terminal.rows;
|
|
4234
|
+
const padding = Math.max(0, termHeight - 1 - lines.length - 3);
|
|
4235
|
+
for (let i = 0; i < padding; i++) lines.push("");
|
|
4236
|
+
if (q.multiSelect) lines.push(c.gray(` ${this.buildMultiFooter()}`));
|
|
4237
|
+
else lines.push(c.gray(` ${this.buildSingleFooter()}`));
|
|
4238
|
+
lines.push("");
|
|
4239
|
+
return lines;
|
|
4240
|
+
}
|
|
4241
|
+
renderTabs(width) {
|
|
4242
|
+
const c = chalk;
|
|
4243
|
+
const parts = [];
|
|
4244
|
+
for (let i = 0; i < this.questions.length; i++) {
|
|
4245
|
+
const q = this.questions[i];
|
|
4246
|
+
const isAnswered = this.questionStates[i].selectedLabels.length > 0;
|
|
4247
|
+
const isCurrent = i === this.currentQuestionIndex;
|
|
4248
|
+
const header = truncate(q.header, 10);
|
|
4249
|
+
const indicator = isAnswered ? c.green("✓") : c.gray("○");
|
|
4250
|
+
let tab;
|
|
4251
|
+
if (isCurrent) tab = c.bgCyan.black(` ${indicator} ${header} `);
|
|
4252
|
+
else if (isAnswered) tab = c.green(` ${indicator} ${header} `);
|
|
4253
|
+
else tab = c.gray(` ${indicator} ${header} `);
|
|
4254
|
+
parts.push(tab);
|
|
4255
|
+
}
|
|
4256
|
+
const submitTab = this.phase === "reviewing" ? c.bgGreen.black(" 提交 ") : c.gray(" 提交 ");
|
|
4257
|
+
parts.push(submitTab);
|
|
4258
|
+
return ` ${parts.join(" ")}`;
|
|
4259
|
+
}
|
|
4260
|
+
renderPreviewPanel(width) {
|
|
4261
|
+
const c = chalk;
|
|
4262
|
+
const lines = [];
|
|
4263
|
+
if (width < 15) return lines;
|
|
4264
|
+
lines.push(c.bold(" 预览"));
|
|
4265
|
+
lines.push(c.gray("─".repeat(Math.max(0, width - 2))));
|
|
4266
|
+
const q = this.getCurrentQuestion();
|
|
4267
|
+
if (this.selectedOptionIndex < q.options.length) {
|
|
4268
|
+
const opt = q.options[this.selectedOptionIndex];
|
|
4269
|
+
if (opt.preview) {
|
|
4270
|
+
const previewLines = opt.preview.split("\n");
|
|
4271
|
+
const maxPreviewLines = 20;
|
|
4272
|
+
for (const line of previewLines.slice(0, maxPreviewLines)) lines.push(` ${c.gray(truncate(line, width - 2))}`);
|
|
4273
|
+
if (previewLines.length > maxPreviewLines) lines.push(c.gray(` ... 还有 ${previewLines.length - maxPreviewLines} 行`));
|
|
4274
|
+
} else lines.push(c.gray(" (无预览)"));
|
|
4275
|
+
} else lines.push(c.gray(" (无预览)"));
|
|
4276
|
+
const maxLines = 22;
|
|
4277
|
+
while (lines.length < maxLines) lines.push("");
|
|
4278
|
+
return lines;
|
|
4279
|
+
}
|
|
4280
|
+
renderOtherInput(width) {
|
|
4281
|
+
const c = chalk;
|
|
4282
|
+
const q = this.getCurrentQuestion();
|
|
4283
|
+
const lines = [];
|
|
4284
|
+
lines.push("");
|
|
4285
|
+
lines.push(this.renderTabs(width));
|
|
4286
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4287
|
+
lines.push("");
|
|
4288
|
+
lines.push(c.bold(` ${q.question}`));
|
|
4289
|
+
lines.push("");
|
|
4290
|
+
lines.push(` 输入自定义答案:`);
|
|
4291
|
+
lines.push("");
|
|
4292
|
+
lines.push(c.yellow(` > ${this.otherInputValue}█`));
|
|
4293
|
+
lines.push("");
|
|
4294
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4295
|
+
lines.push(c.gray(" Enter 确认 · Esc 返回 · 输入自定义文本"));
|
|
4296
|
+
lines.push("");
|
|
4297
|
+
return lines;
|
|
4298
|
+
}
|
|
4299
|
+
renderReview(width) {
|
|
4300
|
+
const c = chalk;
|
|
4301
|
+
const lines = [];
|
|
4302
|
+
lines.push("");
|
|
4303
|
+
lines.push(c.bold(" 确认你的答案"));
|
|
4304
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4305
|
+
lines.push("");
|
|
4306
|
+
for (let i = 0; i < this.questions.length; i++) {
|
|
4307
|
+
const q = this.questions[i];
|
|
4308
|
+
const state = this.questionStates[i];
|
|
4309
|
+
const answer = q.multiSelect ? state.selectedLabels.join(", ") : state.selectedLabels[0] ?? c.gray("(未回答)");
|
|
4310
|
+
const header = truncate(q.header, 15);
|
|
4311
|
+
lines.push(` ${c.bold(header)}: ${answer}`);
|
|
4312
|
+
}
|
|
4313
|
+
lines.push("");
|
|
4314
|
+
lines.push(c.gray(` ${"─".repeat(Math.max(0, width - 4))}`));
|
|
4315
|
+
lines.push(c.gray(" Enter 提交答案 · Esc 返回修改"));
|
|
4316
|
+
lines.push("");
|
|
4317
|
+
return lines;
|
|
4318
|
+
}
|
|
4319
|
+
buildSingleFooter() {
|
|
4320
|
+
const parts = [];
|
|
4321
|
+
const q = this.getCurrentQuestion();
|
|
4322
|
+
parts.push(`1-${q.options.length} 选择`);
|
|
4323
|
+
parts.push("k/j ↑↓ 导航");
|
|
4324
|
+
if (q.options.some((o) => o.preview)) parts.push("p 切换预览");
|
|
4325
|
+
if (this.questions.length > 1) parts.push("Tab 下一题");
|
|
4326
|
+
parts.push("Enter 确认");
|
|
4327
|
+
parts.push("o 自定义");
|
|
4328
|
+
parts.push("Esc 取消");
|
|
4329
|
+
return parts.join(" · ");
|
|
4330
|
+
}
|
|
4331
|
+
buildMultiFooter() {
|
|
4332
|
+
const parts = [];
|
|
4333
|
+
const q = this.getCurrentQuestion();
|
|
4334
|
+
parts.push(`1-${q.options.length} 切换`);
|
|
4335
|
+
parts.push("Space 选择");
|
|
4336
|
+
parts.push("k/j ↑↓ 导航");
|
|
4337
|
+
if (q.options.some((o) => o.preview)) parts.push("p 切换预览");
|
|
4338
|
+
if (this.questions.length > 1) parts.push("Tab 下一题");
|
|
4339
|
+
parts.push("Enter 确认选择");
|
|
4340
|
+
parts.push("o 自定义");
|
|
4341
|
+
parts.push("Esc 取消");
|
|
4342
|
+
return parts.join(" · ");
|
|
4343
|
+
}
|
|
4344
|
+
wrapText(text, maxWidth) {
|
|
4345
|
+
if (text.length <= maxWidth) return [text];
|
|
4346
|
+
const lines = [];
|
|
4347
|
+
let remaining = text;
|
|
4348
|
+
while (remaining.length > maxWidth) {
|
|
4349
|
+
let breakAt = maxWidth;
|
|
4350
|
+
const lastSpace = remaining.lastIndexOf(" ", maxWidth);
|
|
4351
|
+
if (lastSpace > maxWidth / 2) breakAt = lastSpace;
|
|
4352
|
+
lines.push(remaining.slice(0, breakAt));
|
|
4353
|
+
remaining = remaining.slice(breakAt).trim();
|
|
4354
|
+
}
|
|
4355
|
+
if (remaining) lines.push(remaining);
|
|
4356
|
+
return lines;
|
|
4357
|
+
}
|
|
4358
|
+
};
|
|
4359
|
+
/**
|
|
4360
|
+
* 显示 AskUserQuestion 对话框
|
|
4361
|
+
*
|
|
4362
|
+
* @param tui - TUI 实例
|
|
4363
|
+
* @param params - 问题参数
|
|
4364
|
+
* @returns 用户回答结果,取消时 reject
|
|
4365
|
+
*/
|
|
4366
|
+
function showAskUserQuestionDialog(tui, params) {
|
|
4367
|
+
return new Promise((resolve, reject) => {
|
|
4368
|
+
let handle = null;
|
|
4369
|
+
const component = new AskUserQuestionComponent(tui, params, (result) => {
|
|
4370
|
+
handle?.hide();
|
|
4371
|
+
resolve(result);
|
|
4372
|
+
}, () => {
|
|
4373
|
+
handle?.hide();
|
|
4374
|
+
reject(/* @__PURE__ */ new Error("用户取消了提问"));
|
|
4375
|
+
});
|
|
4376
|
+
handle = tui.showOverlay(component, OVERLAY_OPTIONS);
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
//#endregion
|
|
4381
|
+
//#region src/cli/repl/question-provider.ts
|
|
4382
|
+
/**
|
|
4383
|
+
* 创建基于 TUI 的问题提供者
|
|
4384
|
+
*
|
|
4385
|
+
* @param tui - TUI 实例
|
|
4386
|
+
* @returns QuestionProvider 实现
|
|
4387
|
+
*/
|
|
4388
|
+
function createTuiQuestionProvider(tui) {
|
|
4389
|
+
return { async showQuestions(params) {
|
|
4390
|
+
return showAskUserQuestionDialog(tui, params);
|
|
4391
|
+
} };
|
|
4392
|
+
}
|
|
4393
|
+
|
|
3964
4394
|
//#endregion
|
|
3965
4395
|
//#region src/cli/repl/components/output-area.ts
|
|
3966
4396
|
/**
|
|
@@ -3975,6 +4405,7 @@ var InputParser = class {
|
|
|
3975
4405
|
* 从原 Renderer 中提取的纯格式化逻辑,不依赖 console.log。
|
|
3976
4406
|
*/
|
|
3977
4407
|
var OutputFormatter = class {
|
|
4408
|
+
color;
|
|
3978
4409
|
c;
|
|
3979
4410
|
cDisabled;
|
|
3980
4411
|
constructor(color) {
|
|
@@ -4292,133 +4723,1186 @@ var Renderer = class {
|
|
|
4292
4723
|
};
|
|
4293
4724
|
|
|
4294
4725
|
//#endregion
|
|
4295
|
-
//#region src/
|
|
4726
|
+
//#region src/security/tool-guard.ts
|
|
4296
4727
|
/**
|
|
4297
|
-
*
|
|
4728
|
+
* 工具守卫 — 代理模式包装 ToolRegistration
|
|
4298
4729
|
*
|
|
4299
|
-
*
|
|
4300
|
-
*
|
|
4730
|
+
* 为每个 ToolRegistration 的 execute 函数添加安全管道:
|
|
4731
|
+
* 1. PermissionEngine.evaluate() → 获取安全决策
|
|
4732
|
+
* 2. DENY → 抛出 SecurityBlockedError
|
|
4733
|
+
* 3. ASK → ApprovalManager.requestApproval()
|
|
4734
|
+
* 4. ALLOW → 执行原 execute()
|
|
4735
|
+
*
|
|
4736
|
+
* 代理模式:保留原 ToolRegistration 所有属性,
|
|
4737
|
+
* 仅替换 execute 函数为带安全检查的版本。
|
|
4738
|
+
*
|
|
4739
|
+
* ## ToolGuardContext
|
|
4740
|
+
*
|
|
4741
|
+
* 通过 AsyncLocalStorage 传递运行时上下文,控制特殊场景下的行为:
|
|
4742
|
+
* - isBackgroundAgent: 后台 Agent 遇 ASK 自动降级为 DENY(无用户可交互)
|
|
4743
|
+
* - planMode: Plan Mode 下限制工具可用性
|
|
4744
|
+
* - worktreePath: 工作树上下文
|
|
4745
|
+
*
|
|
4746
|
+
* @module security/tool-guard
|
|
4301
4747
|
*/
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
}
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4748
|
+
const log$15 = logger.child("tool-guard");
|
|
4749
|
+
const toolGuardCtxStore = new AsyncLocalStorage();
|
|
4750
|
+
/**
|
|
4751
|
+
* 获取当前 ToolGuard 上下文
|
|
4752
|
+
*/
|
|
4753
|
+
function getToolGuardContext() {
|
|
4754
|
+
return toolGuardCtxStore.getStore();
|
|
4755
|
+
}
|
|
4756
|
+
/**
|
|
4757
|
+
* 在指定上下文中执行回调
|
|
4758
|
+
*
|
|
4759
|
+
* 用于在后台 Agent / Plan Mode 等场景下包装工具执行。
|
|
4760
|
+
*/
|
|
4761
|
+
async function runWithToolGuardContext(context, fn) {
|
|
4762
|
+
return toolGuardCtxStore.run(context, fn);
|
|
4763
|
+
}
|
|
4764
|
+
/**
|
|
4765
|
+
* 安全阻止错误
|
|
4766
|
+
*
|
|
4767
|
+
* 当工具调用被权限引擎拒绝时抛出。
|
|
4768
|
+
* 调用方(agent-adapter / session)应捕获此错误
|
|
4769
|
+
* 并转换为 LLM 友好的错误反馈。
|
|
4770
|
+
*/
|
|
4771
|
+
var SecurityBlockedError = class extends Error {
|
|
4772
|
+
toolId;
|
|
4773
|
+
risk;
|
|
4774
|
+
reason;
|
|
4775
|
+
constructor(message, toolId, risk, reason) {
|
|
4776
|
+
super(message);
|
|
4777
|
+
this.name = "SecurityBlockedError";
|
|
4778
|
+
this.toolId = toolId;
|
|
4779
|
+
this.risk = risk;
|
|
4780
|
+
this.reason = reason;
|
|
4781
|
+
}
|
|
4782
|
+
};
|
|
4783
|
+
var ToolGuard = class {
|
|
4784
|
+
engine;
|
|
4785
|
+
approvalManager;
|
|
4786
|
+
store;
|
|
4787
|
+
sessionId;
|
|
4788
|
+
auditLogger;
|
|
4789
|
+
constructor(engine, approvalManager, store, sessionId, auditLogger) {
|
|
4790
|
+
this.engine = engine;
|
|
4791
|
+
this.approvalManager = approvalManager;
|
|
4792
|
+
this.store = store;
|
|
4793
|
+
this.sessionId = sessionId ?? `session-${Date.now()}`;
|
|
4794
|
+
this.auditLogger = auditLogger;
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* 包装单个 ToolRegistration
|
|
4798
|
+
*
|
|
4799
|
+
* 返回新 ToolRegistration,原对象不变。
|
|
4800
|
+
* execute 被替换为带安全检查的版本。
|
|
4801
|
+
*/
|
|
4802
|
+
wrap(registration) {
|
|
4803
|
+
const originalExecute = registration.execute;
|
|
4804
|
+
const toolId = registration.id;
|
|
4805
|
+
const toolLabel = registration.label;
|
|
4806
|
+
const guardedExecute = async (toolCallId, params, signal, onUpdate) => {
|
|
4807
|
+
const decision = this.engine.evaluate(toolId, params);
|
|
4808
|
+
if (decision.action === "deny") {
|
|
4809
|
+
const reason = decision.reason ?? `工具 ${toolId} 已被安全策略阻止`;
|
|
4810
|
+
log$15.warn("工具调用被阻止", {
|
|
4811
|
+
toolId,
|
|
4812
|
+
risk: decision.risk,
|
|
4813
|
+
reason
|
|
4814
|
+
});
|
|
4815
|
+
eventBus.emit("security:blocked", {
|
|
4816
|
+
toolId,
|
|
4817
|
+
risk: decision.risk,
|
|
4818
|
+
reason,
|
|
4819
|
+
params
|
|
4820
|
+
});
|
|
4821
|
+
this.auditLogger?.log({
|
|
4822
|
+
action: "BLOCK",
|
|
4823
|
+
toolId,
|
|
4824
|
+
risk: decision.risk,
|
|
4825
|
+
reason,
|
|
4826
|
+
params,
|
|
4827
|
+
...decision.matchedRule ? { matchedRule: decision.matchedRule } : {}
|
|
4828
|
+
});
|
|
4829
|
+
throw new SecurityBlockedError(reason, toolId, decision.risk, reason);
|
|
4830
|
+
}
|
|
4831
|
+
if (decision.action === "ask") {
|
|
4832
|
+
if (getToolGuardContext()?.isBackgroundAgent) {
|
|
4833
|
+
const reason = `后台 Agent 不允许交互式审批,工具 ${toolId} 被自动拒绝`;
|
|
4834
|
+
log$15.info("后台 Agent ASK 降级为 DENY", {
|
|
4835
|
+
toolId,
|
|
4836
|
+
risk: decision.risk
|
|
4837
|
+
});
|
|
4838
|
+
eventBus.emit("security:blocked", {
|
|
4839
|
+
toolId,
|
|
4840
|
+
risk: decision.risk,
|
|
4841
|
+
reason,
|
|
4842
|
+
params
|
|
4843
|
+
});
|
|
4844
|
+
this.auditLogger?.log({
|
|
4845
|
+
action: "BLOCK",
|
|
4846
|
+
toolId,
|
|
4847
|
+
risk: decision.risk,
|
|
4848
|
+
reason,
|
|
4849
|
+
params
|
|
4850
|
+
});
|
|
4851
|
+
throw new SecurityBlockedError(reason, toolId, decision.risk, reason);
|
|
4852
|
+
}
|
|
4853
|
+
this.auditLogger?.log({
|
|
4854
|
+
action: "APPROVAL_REQUESTED",
|
|
4855
|
+
toolId,
|
|
4856
|
+
risk: decision.risk,
|
|
4857
|
+
params,
|
|
4858
|
+
...decision.reason ? { reason: decision.reason } : {}
|
|
4859
|
+
});
|
|
4860
|
+
const approvalResponse = await this.approvalManager.requestApproval({
|
|
4861
|
+
toolId,
|
|
4862
|
+
toolLabel,
|
|
4863
|
+
params,
|
|
4864
|
+
risk: decision.risk,
|
|
4865
|
+
reason: decision.reason ?? `工具 ${toolId} 需要审批`,
|
|
4866
|
+
sessionId: this.sessionId
|
|
4867
|
+
});
|
|
4868
|
+
if (!approvalResponse.approved) {
|
|
4869
|
+
const reason = `用户拒绝了工具 ${toolId} 的执行请求`;
|
|
4870
|
+
log$15.info("用户拒绝工具执行", { toolId });
|
|
4871
|
+
this.auditLogger?.log({
|
|
4872
|
+
action: "APPROVAL_DENIED",
|
|
4873
|
+
toolId,
|
|
4874
|
+
risk: decision.risk,
|
|
4875
|
+
reason
|
|
4876
|
+
});
|
|
4877
|
+
throw new SecurityBlockedError(reason, toolId, decision.risk, reason);
|
|
4878
|
+
}
|
|
4879
|
+
if (approvalResponse.scope === "session") this.store.addSessionApproval(toolId);
|
|
4880
|
+
else if (approvalResponse.scope === "always") this.store.addPersistentApproval(toolId);
|
|
4881
|
+
this.auditLogger?.log({
|
|
4882
|
+
action: "APPROVAL_GRANTED",
|
|
4883
|
+
toolId,
|
|
4884
|
+
risk: decision.risk,
|
|
4885
|
+
...approvalResponse.scope ? { scope: approvalResponse.scope } : {},
|
|
4886
|
+
...decision.reason ? { reason: decision.reason } : {}
|
|
4887
|
+
});
|
|
4888
|
+
log$15.debug("审批通过,执行工具", {
|
|
4889
|
+
toolId,
|
|
4890
|
+
scope: approvalResponse.scope
|
|
4891
|
+
});
|
|
4892
|
+
} else this.auditLogger?.log({
|
|
4893
|
+
action: "ALLOW",
|
|
4894
|
+
toolId,
|
|
4895
|
+
risk: decision.risk,
|
|
4896
|
+
params,
|
|
4897
|
+
...decision.matchedRule ? { matchedRule: decision.matchedRule } : {}
|
|
4898
|
+
});
|
|
4899
|
+
return originalExecute(toolCallId, params, signal, onUpdate);
|
|
4900
|
+
};
|
|
4901
|
+
return {
|
|
4902
|
+
...registration,
|
|
4903
|
+
execute: guardedExecute
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* 批量包装所有工具
|
|
4908
|
+
*/
|
|
4909
|
+
wrapAll(registrations) {
|
|
4910
|
+
return registrations.map((reg) => this.wrap(reg));
|
|
4911
|
+
}
|
|
4912
|
+
};
|
|
4913
|
+
/**
|
|
4914
|
+
* 从 ToolRegistration 数组构建 ToolInfoResolver
|
|
4915
|
+
*
|
|
4916
|
+
* 供 PermissionEngine 使用,将 toolId 映射到其安全信息。
|
|
4917
|
+
*/
|
|
4918
|
+
function createToolInfoResolver(registrations) {
|
|
4919
|
+
const map = /* @__PURE__ */ new Map();
|
|
4920
|
+
for (const reg of registrations) map.set(reg.id, reg);
|
|
4921
|
+
return (toolId) => {
|
|
4922
|
+
const reg = map.get(toolId);
|
|
4923
|
+
if (!reg) return void 0;
|
|
4924
|
+
return {
|
|
4925
|
+
checkPermission: reg.checkPermission,
|
|
4926
|
+
defaultRisk: reg.defaultRisk
|
|
4927
|
+
};
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
//#endregion
|
|
4932
|
+
//#region src/core/agent-team/agent-background-store.ts
|
|
4933
|
+
/**
|
|
4934
|
+
* 后台任务持久化存储
|
|
4935
|
+
*
|
|
4936
|
+
* 复用 TaskStore 的内存+JSON 双写模式。
|
|
4937
|
+
* 存储路径:~/.zapmyco/background-tasks/<cwd-hash>.json
|
|
4938
|
+
*
|
|
4939
|
+
* @module core/agent-team
|
|
4940
|
+
*/
|
|
4941
|
+
const log$14 = logger.child("background-store");
|
|
4942
|
+
/**
|
|
4943
|
+
* 后台任务持久化存储
|
|
4944
|
+
*
|
|
4945
|
+
* 提供跨会话的后台任务状态持久化。
|
|
4946
|
+
* 内存 Map + JSON 文件双写,每次变更自动同步到磁盘。
|
|
4947
|
+
*/
|
|
4948
|
+
var BackgroundTaskStore = class {
|
|
4949
|
+
tasks = /* @__PURE__ */ new Map();
|
|
4950
|
+
filePath;
|
|
4951
|
+
constructor(cwd) {
|
|
4952
|
+
const dir = join(homedir(), ".zapmyco", "background-tasks");
|
|
4953
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
4954
|
+
const hash = createHash("md5").update(cwd ?? process.cwd()).digest("hex").slice(0, 16);
|
|
4955
|
+
this.filePath = join(dir, `${hash}.json`);
|
|
4956
|
+
}
|
|
4957
|
+
/** 获取存储文件路径 */
|
|
4958
|
+
get storagePath() {
|
|
4959
|
+
return this.filePath;
|
|
4960
|
+
}
|
|
4961
|
+
/**
|
|
4962
|
+
* 从磁盘加载(覆盖内存)
|
|
4963
|
+
*/
|
|
4964
|
+
load() {
|
|
4965
|
+
try {
|
|
4966
|
+
if (existsSync(this.filePath)) {
|
|
4967
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
4968
|
+
const entries = JSON.parse(raw);
|
|
4969
|
+
this.tasks.clear();
|
|
4970
|
+
for (const entry of entries) this.tasks.set(entry.taskId, entry);
|
|
4971
|
+
log$14.debug("后台任务已加载", {
|
|
4972
|
+
count: entries.length,
|
|
4973
|
+
path: this.filePath
|
|
4974
|
+
});
|
|
4975
|
+
return entries;
|
|
4976
|
+
}
|
|
4977
|
+
} catch (err) {
|
|
4978
|
+
log$14.warn("后台任务加载失败", {
|
|
4979
|
+
error: String(err),
|
|
4980
|
+
path: this.filePath
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return [];
|
|
4984
|
+
}
|
|
4985
|
+
/**
|
|
4986
|
+
* 获取指定任务
|
|
4987
|
+
*/
|
|
4988
|
+
get(taskId) {
|
|
4989
|
+
return this.tasks.get(taskId);
|
|
4990
|
+
}
|
|
4991
|
+
/**
|
|
4992
|
+
* 获取所有任务
|
|
4993
|
+
*/
|
|
4994
|
+
listAll() {
|
|
4995
|
+
return Array.from(this.tasks.values());
|
|
4996
|
+
}
|
|
4997
|
+
/**
|
|
4998
|
+
* 获取活跃(未完结)的任务
|
|
4999
|
+
*/
|
|
5000
|
+
listActive() {
|
|
5001
|
+
return this.listAll().filter((t) => t.status === "pending" || t.status === "running");
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* 保存或更新任务
|
|
5005
|
+
*/
|
|
5006
|
+
save(entry) {
|
|
5007
|
+
this.tasks.set(entry.taskId, entry);
|
|
5008
|
+
this.persist();
|
|
5009
|
+
}
|
|
5010
|
+
/**
|
|
5011
|
+
* 更新任务状态
|
|
5012
|
+
*/
|
|
5013
|
+
updateStatus(taskId, status, extra) {
|
|
5014
|
+
const entry = this.tasks.get(taskId);
|
|
5015
|
+
if (!entry) return false;
|
|
5016
|
+
Object.assign(entry, {
|
|
5017
|
+
status,
|
|
5018
|
+
...extra
|
|
5019
|
+
});
|
|
5020
|
+
this.persist();
|
|
5021
|
+
return true;
|
|
5022
|
+
}
|
|
5023
|
+
/**
|
|
5024
|
+
* 删除任务
|
|
5025
|
+
*/
|
|
5026
|
+
remove(taskId) {
|
|
5027
|
+
const deleted = this.tasks.delete(taskId);
|
|
5028
|
+
if (deleted) this.persist();
|
|
5029
|
+
return deleted;
|
|
5030
|
+
}
|
|
5031
|
+
/** 同步到磁盘 */
|
|
5032
|
+
persist() {
|
|
5033
|
+
try {
|
|
5034
|
+
const entries = Array.from(this.tasks.values());
|
|
5035
|
+
writeFileSync(this.filePath, JSON.stringify(entries, null, 2), "utf-8");
|
|
5036
|
+
} catch (err) {
|
|
5037
|
+
log$14.error("后台任务持久化失败", {
|
|
5038
|
+
error: String(err),
|
|
5039
|
+
path: this.filePath
|
|
5040
|
+
});
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
/** 恢复时清理卡死的 running 任务 */
|
|
5044
|
+
cleanStale(maxRunningMs = 7200 * 1e3) {
|
|
5045
|
+
let cleaned = 0;
|
|
5046
|
+
const now = Date.now();
|
|
5047
|
+
const stale = [];
|
|
5048
|
+
for (const [id, entry] of this.tasks) {
|
|
5049
|
+
if (entry.status === "running" && now - entry.createdAt > maxRunningMs) stale.push(id);
|
|
5050
|
+
if (entry.status === "pending" && now - entry.createdAt > maxRunningMs) stale.push(id);
|
|
5051
|
+
}
|
|
5052
|
+
for (const id of stale) {
|
|
5053
|
+
const entry = this.tasks.get(id);
|
|
5054
|
+
if (entry) {
|
|
5055
|
+
entry.status = entry.status === "running" ? "failed" : "cancelled";
|
|
5056
|
+
entry.error = entry.status === "failed" ? "任务超时丢失(跨会话恢复)" : "任务在 pending 状态超时,无法恢复";
|
|
5057
|
+
entry.completedAt = now;
|
|
5058
|
+
cleaned++;
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
if (cleaned > 0) {
|
|
5062
|
+
this.persist();
|
|
5063
|
+
log$14.info("清理过期后台任务", { cleaned });
|
|
5064
|
+
}
|
|
5065
|
+
return cleaned;
|
|
5066
|
+
}
|
|
5067
|
+
};
|
|
5068
|
+
|
|
5069
|
+
//#endregion
|
|
5070
|
+
//#region src/core/agent-team/agent-message-bus.ts
|
|
5071
|
+
/**
|
|
5072
|
+
* Agent 消息总线
|
|
5073
|
+
*
|
|
5074
|
+
* 内存中的 Agent 间消息路由系统。
|
|
5075
|
+
* 基于 EventEmitter 实现 publish/subscribe 模式,
|
|
5076
|
+
* 消息同时投递到目标 AgentInstance 的 inbox。
|
|
5077
|
+
*
|
|
5078
|
+
* @module core/agent-team
|
|
5079
|
+
*/
|
|
5080
|
+
const log$13 = logger.child("agent-message-bus");
|
|
5081
|
+
/**
|
|
5082
|
+
* Agent 消息总线(单例)
|
|
5083
|
+
*
|
|
5084
|
+
* 负责 Agent 间消息的路由和投递:
|
|
5085
|
+
* - publish(): 将消息投递到目标 inbox + 触发订阅回调
|
|
5086
|
+
* - subscribe(): 注册消息监听器
|
|
5087
|
+
* - drainInbox(): 取出并清空 Agent 收件箱
|
|
5088
|
+
*/
|
|
5089
|
+
var AgentMessageBus = class {
|
|
5090
|
+
emitter = new EventEmitter();
|
|
5091
|
+
messageCounter = 0;
|
|
5092
|
+
/**
|
|
5093
|
+
* 发布消息
|
|
5094
|
+
*
|
|
5095
|
+
* 1. 生成 messageId(若未提供)
|
|
5096
|
+
* 2. 将消息推入目标 AgentInstance.inbox(通过 AgentInstanceManager)
|
|
5097
|
+
* 3. 触发目标 Agent 的订阅回调
|
|
5098
|
+
*
|
|
5099
|
+
* @param fromAgentId - 发送方实例 ID
|
|
5100
|
+
* @param toAgentId - 接收方实例 ID
|
|
5101
|
+
* @param message - 消息内容(不含 messageId/fromAgentId/timestamp)
|
|
5102
|
+
* @returns 完整的 AgentMessage(含生成的 messageId 和 timestamp)
|
|
5103
|
+
*/
|
|
5104
|
+
publish(fromAgentId, toAgentId, message) {
|
|
5105
|
+
const fullMessage = {
|
|
5106
|
+
...message,
|
|
5107
|
+
messageId: this.generateMessageId(),
|
|
5108
|
+
fromAgentId,
|
|
5109
|
+
toAgentId,
|
|
5110
|
+
timestamp: Date.now()
|
|
5111
|
+
};
|
|
5112
|
+
const targetInstance = getAgentInstanceManager().get(toAgentId);
|
|
5113
|
+
if (targetInstance) targetInstance.inbox.push(fullMessage);
|
|
5114
|
+
else log$13.warn("目标 Agent 实例不存在,消息丢弃", {
|
|
5115
|
+
toAgentId,
|
|
5116
|
+
messageId: fullMessage.messageId
|
|
5117
|
+
});
|
|
5118
|
+
this.emitter.emit(`msg:${toAgentId}`, fullMessage);
|
|
5119
|
+
log$13.debug("消息已投递", {
|
|
5120
|
+
from: fromAgentId,
|
|
5121
|
+
to: toAgentId,
|
|
5122
|
+
type: fullMessage.type,
|
|
5123
|
+
messageId: fullMessage.messageId
|
|
5124
|
+
});
|
|
5125
|
+
return fullMessage;
|
|
5126
|
+
}
|
|
5127
|
+
/**
|
|
5128
|
+
* 订阅指定 Agent 的消息
|
|
5129
|
+
*
|
|
5130
|
+
* 当有新消息投递到该 Agent 时,回调被触发。
|
|
5131
|
+
*
|
|
5132
|
+
* @param agentId - 要监听的 Agent 实例 ID
|
|
5133
|
+
* @param callback - 消息到达时的回调
|
|
5134
|
+
*/
|
|
5135
|
+
subscribe(agentId, callback) {
|
|
5136
|
+
this.emitter.on(`msg:${agentId}`, callback);
|
|
5137
|
+
}
|
|
5138
|
+
/**
|
|
5139
|
+
* 取消订阅
|
|
5140
|
+
*
|
|
5141
|
+
* @param agentId - Agent 实例 ID
|
|
5142
|
+
* @param callback - 要移除的回调
|
|
5143
|
+
*/
|
|
5144
|
+
unsubscribe(agentId, callback) {
|
|
5145
|
+
this.emitter.off(`msg:${agentId}`, callback);
|
|
5146
|
+
}
|
|
5147
|
+
/**
|
|
5148
|
+
* 取出并清空指定 Agent 的收件箱
|
|
5149
|
+
*
|
|
5150
|
+
* @param agentId - Agent 实例 ID
|
|
5151
|
+
* @returns 收件箱中的所有消息(按投递时间排序)
|
|
5152
|
+
*/
|
|
5153
|
+
drainInbox(agentId) {
|
|
5154
|
+
const instance = getAgentInstanceManager().get(agentId);
|
|
5155
|
+
if (!instance || instance.inbox.length === 0) return [];
|
|
5156
|
+
const messages = [...instance.inbox];
|
|
5157
|
+
instance.inbox = [];
|
|
5158
|
+
return messages;
|
|
5159
|
+
}
|
|
5160
|
+
/**
|
|
5161
|
+
* 获取收件箱中的消息数量(不清空)
|
|
5162
|
+
*
|
|
5163
|
+
* @param agentId - Agent 实例 ID
|
|
5164
|
+
*/
|
|
5165
|
+
inboxCount(agentId) {
|
|
5166
|
+
return getAgentInstanceManager().get(agentId)?.inbox.length ?? 0;
|
|
5167
|
+
}
|
|
5168
|
+
/** 获取当前活跃的订阅数量 */
|
|
5169
|
+
get subscriptionCount() {
|
|
5170
|
+
return this.emitter.listenerCount("msg:");
|
|
5171
|
+
}
|
|
5172
|
+
/** 生成唯一消息 ID */
|
|
5173
|
+
generateMessageId() {
|
|
5174
|
+
this.messageCounter++;
|
|
5175
|
+
return `msg-${Date.now()}-${this.messageCounter}`;
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
/** 全局单例引用 */
|
|
5179
|
+
let _messageBus = null;
|
|
5180
|
+
/**
|
|
5181
|
+
* 获取 AgentMessageBus 单例
|
|
5182
|
+
*/
|
|
5183
|
+
function getAgentMessageBus() {
|
|
5184
|
+
if (!_messageBus) _messageBus = new AgentMessageBus();
|
|
5185
|
+
return _messageBus;
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
//#endregion
|
|
5189
|
+
//#region src/core/agent-team/agent-background-manager.ts
|
|
5190
|
+
/**
|
|
5191
|
+
* 后台 Agent 管理器
|
|
5192
|
+
*
|
|
5193
|
+
* 管理后台异步 Agent 的完整生命周期:
|
|
5194
|
+
* - fire-and-forget 启动(立即返回 taskId,Agent 在后台执行)
|
|
5195
|
+
* - 进度跟踪
|
|
5196
|
+
* - 结果收集
|
|
5197
|
+
* - 完成后通过 AgentMessageBus 通知父 Agent
|
|
5198
|
+
* - 超时自动取消
|
|
5199
|
+
*
|
|
5200
|
+
* @module core/agent-team
|
|
5201
|
+
*/
|
|
5202
|
+
const log$12 = logger.child("background-agent-manager");
|
|
5203
|
+
/**
|
|
5204
|
+
* 后台 Agent 管理器(单例)
|
|
5205
|
+
*/
|
|
5206
|
+
var BackgroundAgentManager = class {
|
|
5207
|
+
runtime = /* @__PURE__ */ new Map();
|
|
5208
|
+
store;
|
|
5209
|
+
orchestrator = null;
|
|
5210
|
+
defaultTimeoutMs = 1800 * 1e3;
|
|
5211
|
+
constructor() {
|
|
5212
|
+
this.store = new BackgroundTaskStore();
|
|
5213
|
+
}
|
|
5214
|
+
/** 注入 AgentOrchestrator(用于创建 Agent) */
|
|
5215
|
+
setOrchestrator(orchestrator) {
|
|
5216
|
+
this.orchestrator = orchestrator;
|
|
5217
|
+
}
|
|
5218
|
+
/** 获取持久化存储(供外部查询) */
|
|
5219
|
+
getStore() {
|
|
5220
|
+
return this.store;
|
|
5221
|
+
}
|
|
5222
|
+
/**
|
|
5223
|
+
* 异步启动一个 Agent
|
|
5224
|
+
*
|
|
5225
|
+
* fire-and-forget 模式:
|
|
5226
|
+
* 1. 立即返回 { taskId, instanceId }
|
|
5227
|
+
* 2. Agent 在后台执行
|
|
5228
|
+
* 3. 完成后自动通过 AgentMessageBus 通知父 Agent
|
|
5229
|
+
*
|
|
5230
|
+
* @param params - 异步 Agent 参数
|
|
5231
|
+
* @returns 任务标识
|
|
5232
|
+
*/
|
|
5233
|
+
async executeAsync(params) {
|
|
5234
|
+
if (!this.orchestrator) throw new Error("BackgroundAgentManager 未注入 AgentOrchestrator");
|
|
5235
|
+
const taskId = `bg-${params.typeId}-${Date.now()}`;
|
|
5236
|
+
const abortController = new AbortController();
|
|
5237
|
+
const timeoutMs = params.timeoutMs ?? this.defaultTimeoutMs;
|
|
5238
|
+
const runtime = {
|
|
5239
|
+
taskId,
|
|
5240
|
+
instanceId: "",
|
|
5241
|
+
typeId: params.typeId,
|
|
5242
|
+
description: params.description,
|
|
5243
|
+
status: "pending",
|
|
5244
|
+
createdAt: Date.now(),
|
|
5245
|
+
abortController,
|
|
5246
|
+
parentInstanceId: params.parentInstanceId
|
|
5247
|
+
};
|
|
5248
|
+
this.runtime.set(taskId, runtime);
|
|
5249
|
+
this.store.save({
|
|
5250
|
+
taskId,
|
|
5251
|
+
instanceId: "",
|
|
5252
|
+
typeId: params.typeId,
|
|
5253
|
+
description: params.description,
|
|
5254
|
+
status: "pending",
|
|
5255
|
+
createdAt: runtime.createdAt,
|
|
5256
|
+
parentAgentId: params.parentInstanceId
|
|
5257
|
+
});
|
|
5258
|
+
this.runAsync(taskId, params, abortController, timeoutMs).catch((err) => {
|
|
5259
|
+
log$12.error("后台 Agent 意外崩溃", {
|
|
5260
|
+
taskId,
|
|
5261
|
+
error: String(err)
|
|
5262
|
+
});
|
|
5263
|
+
this.failTask(taskId, `意外错误: ${err instanceof Error ? err.message : String(err)}`);
|
|
5264
|
+
});
|
|
5265
|
+
return {
|
|
5266
|
+
taskId,
|
|
5267
|
+
instanceId: runtime.instanceId || "pending"
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
* 实际执行后台 Agent 的逻辑
|
|
5272
|
+
*/
|
|
5273
|
+
async runAsync(taskId, params, abortController, timeoutMs) {
|
|
5274
|
+
const runtime = this.runtime.get(taskId);
|
|
5275
|
+
if (!runtime) return;
|
|
5276
|
+
const orchestrator = this.orchestrator;
|
|
5277
|
+
const messageBus = getAgentMessageBus();
|
|
5278
|
+
const timeoutHandle = setTimeout(() => {
|
|
5279
|
+
log$12.warn("后台 Agent 超时,自动取消", {
|
|
5280
|
+
taskId,
|
|
5281
|
+
timeoutMs
|
|
5282
|
+
});
|
|
5283
|
+
abortController.abort();
|
|
5284
|
+
}, timeoutMs);
|
|
5285
|
+
try {
|
|
5286
|
+
const workerOptions = {
|
|
5287
|
+
taskId,
|
|
5288
|
+
timeoutMs,
|
|
5289
|
+
inheritContext: params.inheritContext ?? false,
|
|
5290
|
+
context: params.context,
|
|
5291
|
+
parentInstanceId: params.parentInstanceId,
|
|
5292
|
+
isolation: params.isolation,
|
|
5293
|
+
wrapExecute: (execute) => runWithToolGuardContext({ isBackgroundAgent: true }, () => execute())
|
|
5294
|
+
};
|
|
5295
|
+
const result = await orchestrator.spawnWorker(params.typeId, params.description, workerOptions);
|
|
5296
|
+
runtime.instanceId = result.instanceId;
|
|
5297
|
+
runtime.status = result.status === "success" ? "completed" : "failed";
|
|
5298
|
+
runtime.completedAt = Date.now();
|
|
5299
|
+
if (result.error) runtime.error = result.error.message;
|
|
5300
|
+
this.store.updateStatus(taskId, runtime.status, {
|
|
5301
|
+
instanceId: result.instanceId,
|
|
5302
|
+
completedAt: runtime.completedAt,
|
|
5303
|
+
result: result.output ?? void 0,
|
|
5304
|
+
error: runtime.error
|
|
5305
|
+
});
|
|
5306
|
+
if (params.parentInstanceId) {
|
|
5307
|
+
const payload = JSON.stringify({
|
|
5308
|
+
taskId,
|
|
5309
|
+
instanceId: result.instanceId,
|
|
5310
|
+
typeId: params.typeId,
|
|
5311
|
+
status: result.status,
|
|
5312
|
+
summary: result.output,
|
|
5313
|
+
duration: result.duration,
|
|
5314
|
+
tokenUsage: result.tokenUsage,
|
|
5315
|
+
error: result.error
|
|
5316
|
+
});
|
|
5317
|
+
messageBus.publish(result.instanceId, params.parentInstanceId, {
|
|
5318
|
+
type: "task_result",
|
|
5319
|
+
payload,
|
|
5320
|
+
taskId,
|
|
5321
|
+
requiresResponse: false
|
|
5322
|
+
});
|
|
5323
|
+
log$12.info("后台 Agent 完成通知已发送", {
|
|
5324
|
+
taskId,
|
|
5325
|
+
instanceId: result.instanceId,
|
|
5326
|
+
parentId: params.parentInstanceId,
|
|
5327
|
+
status: result.status
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
} catch (err) {
|
|
5331
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5332
|
+
if (abortController.signal.aborted) this.failTask(taskId, `任务超时(${timeoutMs / 1e3}秒)`);
|
|
5333
|
+
else this.failTask(taskId, message);
|
|
5334
|
+
} finally {
|
|
5335
|
+
clearTimeout(timeoutHandle);
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
/** 标记任务失败 */
|
|
5339
|
+
failTask(taskId, error) {
|
|
5340
|
+
const runtime = this.runtime.get(taskId);
|
|
5341
|
+
if (runtime) {
|
|
5342
|
+
runtime.status = "failed";
|
|
5343
|
+
runtime.error = error;
|
|
5344
|
+
runtime.completedAt = Date.now();
|
|
5345
|
+
}
|
|
5346
|
+
this.store.updateStatus(taskId, "failed", {
|
|
5347
|
+
completedAt: Date.now(),
|
|
5348
|
+
error
|
|
5349
|
+
});
|
|
5350
|
+
log$12.warn("后台 Agent 失败", {
|
|
5351
|
+
taskId,
|
|
5352
|
+
error
|
|
5353
|
+
});
|
|
5354
|
+
}
|
|
5355
|
+
/**
|
|
5356
|
+
* 获取后台任务状态
|
|
5357
|
+
*/
|
|
5358
|
+
getTask(taskId) {
|
|
5359
|
+
return this.runtime.get(taskId);
|
|
5360
|
+
}
|
|
5361
|
+
/**
|
|
5362
|
+
* 列出所有活跃(未完结)的后台任务
|
|
5363
|
+
*/
|
|
5364
|
+
listActive() {
|
|
5365
|
+
return Array.from(this.runtime.values()).filter((t) => t.status === "pending" || t.status === "running");
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* 列出所有后台任务
|
|
5369
|
+
*/
|
|
5370
|
+
listAll() {
|
|
5371
|
+
return Array.from(this.runtime.values());
|
|
5372
|
+
}
|
|
5373
|
+
/**
|
|
5374
|
+
* 取消后台任务
|
|
5375
|
+
*/
|
|
5376
|
+
async cancel(taskId) {
|
|
5377
|
+
const runtime = this.runtime.get(taskId);
|
|
5378
|
+
if (!runtime) return false;
|
|
5379
|
+
if (runtime.status === "completed" || runtime.status === "failed" || runtime.status === "cancelled") return false;
|
|
5380
|
+
runtime.abortController.abort();
|
|
5381
|
+
runtime.status = "cancelled";
|
|
5382
|
+
runtime.completedAt = Date.now();
|
|
5383
|
+
this.store.updateStatus(taskId, "cancelled", { completedAt: runtime.completedAt });
|
|
5384
|
+
if (runtime.instanceId) try {
|
|
5385
|
+
await getAgentInstanceManager().cancel(runtime.instanceId);
|
|
5386
|
+
} catch {}
|
|
5387
|
+
log$12.info("后台 Agent 已取消", { taskId });
|
|
5388
|
+
return true;
|
|
5389
|
+
}
|
|
5390
|
+
/**
|
|
5391
|
+
* 从持久化存储恢复(跨会话)
|
|
5392
|
+
*/
|
|
5393
|
+
restore() {
|
|
5394
|
+
this.store.load();
|
|
5395
|
+
const stale = this.store.cleanStale();
|
|
5396
|
+
if (stale > 0) log$12.info("跨会话恢复:清理了过期后台任务", { count: stale });
|
|
5397
|
+
const active = this.store.listActive();
|
|
5398
|
+
for (const entry of active) this.store.updateStatus(entry.taskId, "failed", {
|
|
5399
|
+
completedAt: Date.now(),
|
|
5400
|
+
error: "会话终止导致任务丢失"
|
|
5401
|
+
});
|
|
5402
|
+
if (active.length > 0) log$12.info("跨会话恢复:标记活跃任务为 failed", { count: active.length });
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
/** 全局单例 */
|
|
5406
|
+
let globalBackgroundManager = null;
|
|
5407
|
+
/** 获取 BackgroundAgentManager 单例 */
|
|
5408
|
+
function getBackgroundAgentManager() {
|
|
5409
|
+
if (!globalBackgroundManager) globalBackgroundManager = new BackgroundAgentManager();
|
|
5410
|
+
return globalBackgroundManager;
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
//#endregion
|
|
5414
|
+
//#region src/cli/repl/tools/agent-tool.ts
|
|
5415
|
+
/**
|
|
5416
|
+
* 创建 AgentTool 工具注册
|
|
5417
|
+
*
|
|
5418
|
+
* @param orchestrator - AgentOrchestrator 实例
|
|
5419
|
+
* @returns ToolRegistration
|
|
5420
|
+
*/
|
|
5421
|
+
function createAgentTool(orchestrator) {
|
|
5422
|
+
const registry = getAgentTypeRegistry();
|
|
5423
|
+
return {
|
|
5424
|
+
id: "AgentTool",
|
|
5425
|
+
label: "创建 Agent",
|
|
5426
|
+
defaultRisk: "high",
|
|
5427
|
+
description: [
|
|
5428
|
+
"创建特定类型的子 Agent 来执行独立任务。支持按 Agent 类型创建(推荐)或批量匿名创建(兼容旧版)。",
|
|
5429
|
+
"",
|
|
5430
|
+
"### 何时使用此工具",
|
|
5431
|
+
"1. 需要分解复杂任务为多个独立子任务时",
|
|
5432
|
+
"2. 需要特定类型的 Agent(研究员/编码助手/审查员/规划师)时",
|
|
5433
|
+
"3. 多个子任务之间没有顺序依赖关系时可以并行创建",
|
|
5434
|
+
"",
|
|
5435
|
+
"### 何时不使用此工具",
|
|
5436
|
+
"- 只有 1 个简单任务时(直接执行即可)",
|
|
5437
|
+
"- 任务之间有严格的顺序依赖(必须串行执行)",
|
|
5438
|
+
"- 任务非常简单(如读取单个文件)",
|
|
5439
|
+
"",
|
|
5440
|
+
"### 可用的 Agent 类型",
|
|
5441
|
+
registry.list().map((t) => `- **${t.typeId}**: ${t.whenToUse}`).join("\n"),
|
|
5442
|
+
"",
|
|
5443
|
+
"### 参数说明",
|
|
5444
|
+
"- **subagent_type**(推荐): Agent 类型 ID,如 researcher/coder/reviewer/planner/general-purpose",
|
|
5445
|
+
"- **description**: 发给子 Agent 的详细任务指令",
|
|
5446
|
+
"- **run_in_background**: 是否后台运行(默认 false)。设为 true 时立即返回 taskId,Agent 在后台执行,完成后自动通知",
|
|
5447
|
+
"- **inherit_context**: 是否继承父级上下文",
|
|
5448
|
+
"- **agents**(已废弃,兼容旧版): 批量匿名子 Agent 列表",
|
|
5449
|
+
"- **context**: 可选背景摘要",
|
|
5450
|
+
"",
|
|
5451
|
+
"### 使用流程",
|
|
5452
|
+
"1. 分析任务,确定需要的 Agent 类型",
|
|
5453
|
+
"2. 为每个 Agent 编写详细的任务指令",
|
|
5454
|
+
"3. 调用本工具创建 Agent(可多次调用创建多个不同类型的 Agent)",
|
|
5455
|
+
"4. 同步模式:等待结果返回后整合汇总",
|
|
5456
|
+
"5. 异步模式:立即返回 taskId,完成后通过收件箱接收通知"
|
|
5457
|
+
].join("\n"),
|
|
5458
|
+
parameters: {
|
|
5459
|
+
type: "object",
|
|
5460
|
+
properties: {
|
|
5461
|
+
subagent_type: {
|
|
5462
|
+
type: "string",
|
|
5463
|
+
description: `Agent 类型 ID。可用类型: ${registry.list().map((t) => t.typeId).join(", ")}`
|
|
5464
|
+
},
|
|
5465
|
+
description: {
|
|
5466
|
+
type: "string",
|
|
5467
|
+
description: "发给子 Agent 的详细任务指令"
|
|
5468
|
+
},
|
|
5469
|
+
run_in_background: {
|
|
5470
|
+
type: "boolean",
|
|
5471
|
+
description: "是否后台运行(默认 false)。设为 true 时 fire-and-forget,Agent 后台执行完成后自动通知父 Agent",
|
|
5472
|
+
default: false
|
|
5473
|
+
},
|
|
5474
|
+
inherit_context: {
|
|
5475
|
+
type: "boolean",
|
|
5476
|
+
description: "是否继承父级上下文",
|
|
5477
|
+
default: false
|
|
5478
|
+
},
|
|
5479
|
+
agents: {
|
|
5480
|
+
type: "array",
|
|
5481
|
+
items: {
|
|
5482
|
+
type: "object",
|
|
5483
|
+
properties: {
|
|
5484
|
+
id: {
|
|
5485
|
+
type: "string",
|
|
5486
|
+
description: "子任务唯一标识"
|
|
5487
|
+
},
|
|
5488
|
+
description: {
|
|
5489
|
+
type: "string",
|
|
5490
|
+
description: "发给子 Agent 的详细任务指令"
|
|
5491
|
+
},
|
|
5492
|
+
allowedTools: {
|
|
5493
|
+
type: "array",
|
|
5494
|
+
items: { type: "string" },
|
|
5495
|
+
description: "可选工具白名单"
|
|
5496
|
+
}
|
|
5497
|
+
},
|
|
5498
|
+
required: ["id", "description"]
|
|
5499
|
+
},
|
|
5500
|
+
description: "[已废弃] 批量匿名子 Agent 列表,请使用 subagent_type 参数"
|
|
5501
|
+
},
|
|
5502
|
+
context: {
|
|
5503
|
+
type: "string",
|
|
5504
|
+
description: "可选背景摘要,注入给每个子 Agent"
|
|
5505
|
+
},
|
|
5506
|
+
isolation: {
|
|
5507
|
+
type: "string",
|
|
5508
|
+
enum: ["worktree"],
|
|
5509
|
+
description: "隔离模式。设为 \"worktree\" 时 Agent 在独立 git worktree 中运行,修改不影响主工作区"
|
|
5510
|
+
}
|
|
5511
|
+
},
|
|
5512
|
+
required: ["description"]
|
|
5513
|
+
},
|
|
5514
|
+
execute: async (_toolCallId, params) => {
|
|
5515
|
+
const p = params;
|
|
5516
|
+
if (p.run_in_background && p.subagent_type && !p.agents) {
|
|
5517
|
+
const { taskId, instanceId } = await getBackgroundAgentManager().executeAsync({
|
|
5518
|
+
typeId: p.subagent_type,
|
|
5519
|
+
description: p.description,
|
|
5520
|
+
context: p.context,
|
|
5521
|
+
inheritContext: p.inherit_context,
|
|
5522
|
+
...p.isolation ? { isolation: p.isolation } : {}
|
|
5523
|
+
});
|
|
5524
|
+
return {
|
|
5525
|
+
content: [{
|
|
5526
|
+
type: "text",
|
|
5527
|
+
text: [
|
|
5528
|
+
`🚀 **${p.subagent_type}** 已作为后台任务启动`,
|
|
5529
|
+
"",
|
|
5530
|
+
`- 任务 ID: \`${taskId}\``,
|
|
5531
|
+
`- 实例 ID: \`${instanceId}\``,
|
|
5532
|
+
`- 状态: 后台运行中`,
|
|
5533
|
+
"",
|
|
5534
|
+
"Agent 完成后将自动通知。可使用 BackgroundTask 工具查询状态。"
|
|
5535
|
+
].join("\n")
|
|
5536
|
+
}],
|
|
5537
|
+
details: {
|
|
5538
|
+
taskId,
|
|
5539
|
+
instanceId,
|
|
5540
|
+
status: "async_launched"
|
|
5541
|
+
}
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
if (p.run_in_background && !p.subagent_type) return { content: [{
|
|
5545
|
+
type: "text",
|
|
5546
|
+
text: "后台运行模式需要指定 subagent_type 参数。请选择一种 Agent 类型。"
|
|
5547
|
+
}] };
|
|
5548
|
+
if (p.subagent_type && !p.agents) {
|
|
5549
|
+
const result = await orchestrator.spawnWorker(p.subagent_type, p.description, {
|
|
5550
|
+
...p.context != null ? { context: p.context } : {},
|
|
5551
|
+
...p.inherit_context != null ? { inheritContext: p.inherit_context } : {},
|
|
5552
|
+
...p.isolation ? { isolation: p.isolation } : {}
|
|
5553
|
+
});
|
|
5554
|
+
return {
|
|
5555
|
+
content: [{
|
|
5556
|
+
type: "text",
|
|
5557
|
+
text: result.status === "success" ? `✅ **${result.typeId}** 执行成功 (${(result.duration / 1e3).toFixed(1)}s)\n\n${result.output ?? "(无输出)"}` : `❌ **${result.typeId}** 执行失败: ${result.error?.message ?? "未知错误"}`
|
|
5558
|
+
}],
|
|
5559
|
+
details: result
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
if (p.agents && p.agents.length > 0) {
|
|
5563
|
+
const results = await orchestrator.spawnFlat(p.agents, p.context);
|
|
5564
|
+
return {
|
|
5565
|
+
content: [{
|
|
5566
|
+
type: "text",
|
|
5567
|
+
text: results.summary
|
|
5568
|
+
}],
|
|
5569
|
+
details: results
|
|
5570
|
+
};
|
|
5571
|
+
}
|
|
5572
|
+
return { content: [{
|
|
5573
|
+
type: "text",
|
|
5574
|
+
text: "请指定 subagent_type(推荐的 Agent 类型)或 agents(已废弃的批量参数)。"
|
|
5575
|
+
}] };
|
|
5576
|
+
}
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
//#endregion
|
|
5581
|
+
//#region src/core/question/types.ts
|
|
5582
|
+
/** 创建 Deferred */
|
|
5583
|
+
function createDeferred() {
|
|
5584
|
+
let resolve;
|
|
5585
|
+
let reject;
|
|
5586
|
+
let isSettled = false;
|
|
5587
|
+
return {
|
|
5588
|
+
promise: new Promise((res, rej) => {
|
|
5589
|
+
resolve = (value) => {
|
|
5590
|
+
if (!isSettled) {
|
|
5591
|
+
isSettled = true;
|
|
5592
|
+
res(value);
|
|
5593
|
+
}
|
|
5594
|
+
};
|
|
5595
|
+
reject = (error) => {
|
|
5596
|
+
if (!isSettled) {
|
|
5597
|
+
isSettled = true;
|
|
5598
|
+
rej(error);
|
|
5599
|
+
}
|
|
5600
|
+
};
|
|
5601
|
+
}),
|
|
5602
|
+
resolve,
|
|
5603
|
+
reject,
|
|
5604
|
+
isSettled
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
//#endregion
|
|
5609
|
+
//#region src/core/question/question-manager.ts
|
|
5610
|
+
/**
|
|
5611
|
+
* 问题管理器
|
|
5612
|
+
*
|
|
5613
|
+
* 基于 Provider 模式实现异步阻塞提问。
|
|
5614
|
+
* TUI 层通过 setProvider() 注入提问 UI 实现,
|
|
5615
|
+
* Agent 工具执行流通过 ask() 被阻塞等待用户回答。
|
|
5616
|
+
*
|
|
5617
|
+
* 在 headless 模式(无 provider)下自动报错。
|
|
5618
|
+
*
|
|
5619
|
+
* @module core/question/question-manager
|
|
5620
|
+
*/
|
|
5621
|
+
const log$11 = logger.child("question-manager");
|
|
5622
|
+
/** 问题超时时间(5 分钟) */
|
|
5623
|
+
const QUESTION_TIMEOUT_MS = 300 * 1e3;
|
|
5624
|
+
var QuestionManager = class {
|
|
5625
|
+
provider = null;
|
|
5626
|
+
pending = /* @__PURE__ */ new Map();
|
|
5627
|
+
constructor(provider) {
|
|
5628
|
+
if (provider) this.provider = provider;
|
|
5629
|
+
}
|
|
5630
|
+
/**
|
|
5631
|
+
* 设置问题提供者(由 TUI 层注入)
|
|
5632
|
+
*/
|
|
5633
|
+
setProvider(provider) {
|
|
5634
|
+
this.provider = provider;
|
|
5635
|
+
}
|
|
5636
|
+
/**
|
|
5637
|
+
* 检查是否有问题提供者
|
|
5638
|
+
*/
|
|
5639
|
+
hasProvider() {
|
|
5640
|
+
return this.provider !== null;
|
|
5641
|
+
}
|
|
5642
|
+
/**
|
|
5643
|
+
* 向用户提问并等待回答
|
|
5644
|
+
*
|
|
5645
|
+
* 阻塞当前执行流,等待用户通过 TUI 做出回答。
|
|
5646
|
+
* 如果无 provider(headless 模式),抛出错误。
|
|
5647
|
+
*
|
|
5648
|
+
* @param params - 问题参数
|
|
5649
|
+
* @returns 用户回答结果
|
|
5650
|
+
*/
|
|
5651
|
+
async ask(params) {
|
|
5652
|
+
if (!this.provider) throw new Error("当前环境不支持交互式提问(headless 模式)");
|
|
5653
|
+
const requestId = `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5654
|
+
const deferred = createDeferred();
|
|
5655
|
+
deferred.promise.catch(() => {});
|
|
5656
|
+
const entry = {
|
|
5657
|
+
requestId,
|
|
5658
|
+
questions: params.questions,
|
|
5659
|
+
deferred,
|
|
5660
|
+
createdAt: Date.now()
|
|
5661
|
+
};
|
|
5662
|
+
this.pending.set(requestId, entry);
|
|
5663
|
+
eventBus.emit("question:asked", {
|
|
5664
|
+
requestId,
|
|
5665
|
+
questionCount: params.questions.length
|
|
5666
|
+
});
|
|
5667
|
+
log$11.debug("提问已发出", {
|
|
5668
|
+
requestId,
|
|
5669
|
+
questionCount: params.questions.length
|
|
5670
|
+
});
|
|
5671
|
+
const timeout = setTimeout(() => {
|
|
5672
|
+
if (!deferred.isSettled) {
|
|
5673
|
+
log$11.warn("提问超时,自动取消", { requestId });
|
|
5674
|
+
deferred.reject(/* @__PURE__ */ new Error("提问超时,用户未在 5 分钟内回答"));
|
|
5675
|
+
this.pending.delete(requestId);
|
|
5676
|
+
eventBus.emit("question:timeout", { requestId });
|
|
5677
|
+
}
|
|
5678
|
+
}, QUESTION_TIMEOUT_MS);
|
|
5679
|
+
try {
|
|
5680
|
+
const result = await this.provider.showQuestions(params);
|
|
5681
|
+
clearTimeout(timeout);
|
|
5682
|
+
deferred.resolve(result);
|
|
5683
|
+
eventBus.emit("question:answered", {
|
|
5684
|
+
requestId,
|
|
5685
|
+
answerCount: Object.keys(result.answers).length
|
|
5686
|
+
});
|
|
5687
|
+
log$11.debug("提问已回答", { requestId });
|
|
5688
|
+
return result;
|
|
5689
|
+
} catch (err) {
|
|
5690
|
+
clearTimeout(timeout);
|
|
5691
|
+
if (!deferred.isSettled) deferred.reject(err instanceof Error ? err : new Error(String(err)));
|
|
5692
|
+
eventBus.emit("question:cancelled", {
|
|
5693
|
+
requestId,
|
|
5694
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5695
|
+
});
|
|
5696
|
+
log$11.debug("提问已取消", {
|
|
5697
|
+
requestId,
|
|
5698
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5699
|
+
});
|
|
5700
|
+
throw err;
|
|
5701
|
+
} finally {
|
|
5702
|
+
this.pending.delete(requestId);
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
/**
|
|
5706
|
+
* 获取待处理的问题请求
|
|
5707
|
+
*/
|
|
5708
|
+
getPending(requestId) {
|
|
5709
|
+
return this.pending.get(requestId);
|
|
5710
|
+
}
|
|
5711
|
+
/**
|
|
5712
|
+
* 拒绝所有待处理的问题(用于 session 关闭时清理)
|
|
5713
|
+
*/
|
|
5714
|
+
rejectAll(error) {
|
|
5715
|
+
const count = this.pending.size;
|
|
5716
|
+
for (const [, entry] of this.pending) if (!entry.deferred.isSettled) entry.deferred.reject(error);
|
|
5717
|
+
this.pending.clear();
|
|
5718
|
+
if (count > 0) log$11.debug("已清理所有待处理问题", { count });
|
|
5719
|
+
}
|
|
5720
|
+
};
|
|
5721
|
+
let globalQuestionManager = null;
|
|
5722
|
+
/**
|
|
5723
|
+
* 获取全局 QuestionManager 单例
|
|
5724
|
+
*/
|
|
5725
|
+
function getQuestionManager() {
|
|
5726
|
+
if (!globalQuestionManager) globalQuestionManager = new QuestionManager();
|
|
5727
|
+
return globalQuestionManager;
|
|
5728
|
+
}
|
|
5729
|
+
|
|
5730
|
+
//#endregion
|
|
5731
|
+
//#region src/cli/repl/tools/ask-user-question.ts
|
|
5732
|
+
/**
|
|
5733
|
+
* 创建 AskUserQuestion 工具注册
|
|
5734
|
+
*/
|
|
5735
|
+
function createAskUserQuestionTool() {
|
|
5736
|
+
return {
|
|
5737
|
+
id: "AskUserQuestion",
|
|
5738
|
+
label: "向用户提问",
|
|
5739
|
+
defaultRisk: "medium",
|
|
5740
|
+
checkPermission: () => ({
|
|
5741
|
+
risk: "medium",
|
|
5742
|
+
requiresApproval: false,
|
|
5743
|
+
reason: "AskUserQuestion 使用独立的交互式 UI,不走 ToolGuard 审批路径"
|
|
5744
|
+
}),
|
|
5745
|
+
description: [
|
|
5746
|
+
"向用户提问以获取决策指导。当需要在多个可行方案之间选择、进行技术选型、",
|
|
5747
|
+
"或需要用户偏好时使用此工具。",
|
|
5748
|
+
"",
|
|
5749
|
+
"### 何时使用",
|
|
5750
|
+
"- 需要在多个可行方案之间做出选择时",
|
|
5751
|
+
"- 需要技术选型、架构决策等需要用户判断的问题",
|
|
5752
|
+
"- Plan Mode 中分析完方案后需要用户指定方向时",
|
|
5753
|
+
"- 需要明确用户偏好以实现个性化功能时",
|
|
5754
|
+
"",
|
|
5755
|
+
"### 何时不使用",
|
|
5756
|
+
"- 可以通过代码分析直接确定的结论",
|
|
5757
|
+
"- 简单确认(直接回复文本询问即可)",
|
|
5758
|
+
"- 已有明确最佳实践的问题",
|
|
5759
|
+
"",
|
|
5760
|
+
"### 提问原则",
|
|
5761
|
+
"- 每个问题提供 2-4 个具体、互斥的选项",
|
|
5762
|
+
"- header 字段控制在 12 个字符以内",
|
|
5763
|
+
"- 使用 multiSelect: true 表示多选",
|
|
5764
|
+
"- 推荐选项放在第一位并加 \"(Recommended)\" 后缀",
|
|
5765
|
+
"- 如果选项有代码示例/配置对比,可在 preview 字段中提供(markdown 格式)",
|
|
5766
|
+
"- 用户始终可以选择 \"Other\" 输入自定义答案"
|
|
5767
|
+
].join("\n"),
|
|
5768
|
+
parameters: {
|
|
5769
|
+
type: "object",
|
|
5770
|
+
properties: { questions: {
|
|
5771
|
+
type: "array",
|
|
5772
|
+
items: {
|
|
5773
|
+
type: "object",
|
|
5774
|
+
properties: {
|
|
5775
|
+
question: {
|
|
5776
|
+
type: "string",
|
|
5777
|
+
description: "完整问题文本,以问号结尾"
|
|
5778
|
+
},
|
|
5779
|
+
header: {
|
|
5780
|
+
type: "string",
|
|
5781
|
+
description: "短标签,用于 Tab 导航(最多 12 个字符)"
|
|
5782
|
+
},
|
|
5783
|
+
options: {
|
|
5784
|
+
type: "array",
|
|
5785
|
+
items: {
|
|
5786
|
+
type: "object",
|
|
5787
|
+
properties: {
|
|
5788
|
+
label: {
|
|
5789
|
+
type: "string",
|
|
5790
|
+
description: "选项显示文本(1-5 个词)"
|
|
5791
|
+
},
|
|
5792
|
+
description: {
|
|
5793
|
+
type: "string",
|
|
5794
|
+
description: "选项说明"
|
|
5795
|
+
},
|
|
5796
|
+
preview: {
|
|
5797
|
+
type: "string",
|
|
5798
|
+
description: "可选的预览内容(markdown 格式)。当任一选项有 preview 时,UI 切换为左右分栏布局"
|
|
5799
|
+
}
|
|
5800
|
+
},
|
|
5801
|
+
required: ["label", "description"]
|
|
5802
|
+
},
|
|
5803
|
+
minItems: 2,
|
|
5804
|
+
maxItems: 4,
|
|
5805
|
+
description: "2-4 个选项"
|
|
5806
|
+
},
|
|
5807
|
+
multiSelect: {
|
|
5808
|
+
type: "boolean",
|
|
5809
|
+
description: "是否允许多选",
|
|
5810
|
+
default: false
|
|
5811
|
+
}
|
|
5812
|
+
},
|
|
5813
|
+
required: [
|
|
5814
|
+
"question",
|
|
5815
|
+
"header",
|
|
5816
|
+
"options",
|
|
5817
|
+
"multiSelect"
|
|
5818
|
+
]
|
|
5819
|
+
},
|
|
5820
|
+
minItems: 1,
|
|
5821
|
+
maxItems: 4,
|
|
5822
|
+
description: "1-4 个问题"
|
|
5823
|
+
} },
|
|
5824
|
+
required: ["questions"]
|
|
5825
|
+
},
|
|
5826
|
+
execute: async (_toolCallId, params) => {
|
|
5827
|
+
const p = params;
|
|
5828
|
+
if (getToolGuardContext()?.isBackgroundAgent) throw new SecurityBlockedError("后台 Agent 不支持交互式提问(AskUserQuestion)", "AskUserQuestion", "medium", "后台 Agent 无用户交互界面");
|
|
5829
|
+
if (!p.questions || p.questions.length === 0) return {
|
|
5830
|
+
content: [{
|
|
5831
|
+
type: "text",
|
|
5832
|
+
text: "错误: 至少需要 1 个问题"
|
|
5833
|
+
}],
|
|
5834
|
+
details: {
|
|
5835
|
+
error: true,
|
|
5836
|
+
message: "questions 数组不能为空"
|
|
5837
|
+
}
|
|
5838
|
+
};
|
|
5839
|
+
for (let i = 0; i < p.questions.length; i++) {
|
|
5840
|
+
const q = p.questions[i];
|
|
5841
|
+
if (q.options.length < 2) return {
|
|
5842
|
+
content: [{
|
|
5843
|
+
type: "text",
|
|
5844
|
+
text: `错误: 问题 "${q.header}" 至少需要 2 个选项`
|
|
5845
|
+
}],
|
|
5846
|
+
details: {
|
|
5847
|
+
error: true,
|
|
5848
|
+
message: "选项数量不足",
|
|
5849
|
+
questionIndex: i
|
|
5850
|
+
}
|
|
5851
|
+
};
|
|
5852
|
+
if (q.header.length > 12) return {
|
|
5853
|
+
content: [{
|
|
5854
|
+
type: "text",
|
|
5855
|
+
text: `错误: 问题 "${q.header}" 的 header 超过 12 个字符限制`
|
|
4414
5856
|
}],
|
|
4415
|
-
details:
|
|
5857
|
+
details: {
|
|
5858
|
+
error: true,
|
|
5859
|
+
message: "header 过长",
|
|
5860
|
+
questionIndex: i
|
|
5861
|
+
}
|
|
5862
|
+
};
|
|
5863
|
+
}
|
|
5864
|
+
try {
|
|
5865
|
+
const questionManager = getQuestionManager();
|
|
5866
|
+
if (!questionManager.hasProvider()) return {
|
|
5867
|
+
content: [{
|
|
5868
|
+
type: "text",
|
|
5869
|
+
text: "错误: 当前环境不支持交互式提问(headless 模式或未初始化 UI)"
|
|
5870
|
+
}],
|
|
5871
|
+
details: {
|
|
5872
|
+
error: true,
|
|
5873
|
+
message: "无 QuestionProvider"
|
|
5874
|
+
}
|
|
5875
|
+
};
|
|
5876
|
+
const result = await questionManager.ask(p);
|
|
5877
|
+
return {
|
|
5878
|
+
content: [{
|
|
5879
|
+
type: "text",
|
|
5880
|
+
text: `用户已回答你的问题:\n${result.questions.map((q) => {
|
|
5881
|
+
const answer = result.answers[q.question];
|
|
5882
|
+
const answerStr = Array.isArray(answer) ? answer.join(", ") : answer ?? "(未回答)";
|
|
5883
|
+
return `"${q.question}" → "${answerStr}"`;
|
|
5884
|
+
}).join("\n")}\n\n你可以根据这些答案继续执行。`
|
|
5885
|
+
}],
|
|
5886
|
+
details: {
|
|
5887
|
+
questions: result.questions,
|
|
5888
|
+
answers: result.answers,
|
|
5889
|
+
annotations: result.annotations
|
|
5890
|
+
}
|
|
5891
|
+
};
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5894
|
+
return {
|
|
5895
|
+
content: [{
|
|
5896
|
+
type: "text",
|
|
5897
|
+
text: `提问被取消: ${message}`
|
|
5898
|
+
}],
|
|
5899
|
+
details: {
|
|
5900
|
+
error: true,
|
|
5901
|
+
message,
|
|
5902
|
+
cancelled: true
|
|
5903
|
+
}
|
|
4416
5904
|
};
|
|
4417
5905
|
}
|
|
4418
|
-
return { content: [{
|
|
4419
|
-
type: "text",
|
|
4420
|
-
text: "请指定 subagent_type(推荐的 Agent 类型)或 agents(已废弃的批量参数)。"
|
|
4421
|
-
}] };
|
|
4422
5906
|
}
|
|
4423
5907
|
};
|
|
4424
5908
|
}
|
|
@@ -4920,6 +6404,257 @@ async function buildStatusResult(scheduler) {
|
|
|
4920
6404
|
};
|
|
4921
6405
|
}
|
|
4922
6406
|
|
|
6407
|
+
//#endregion
|
|
6408
|
+
//#region src/cli/repl/tools/enter-worktree.ts
|
|
6409
|
+
/**
|
|
6410
|
+
* 创建 EnterWorktree 工具注册
|
|
6411
|
+
*/
|
|
6412
|
+
function createEnterWorktreeTool(worktreeManager) {
|
|
6413
|
+
return {
|
|
6414
|
+
id: "EnterWorktree",
|
|
6415
|
+
label: "进入工作树",
|
|
6416
|
+
description: [
|
|
6417
|
+
"创建 git worktree 并切换当前会话的工作目录到隔离环境。",
|
|
6418
|
+
"",
|
|
6419
|
+
"### 何时使用此工具",
|
|
6420
|
+
"- 需要在独立的工作区中进行代码修改时",
|
|
6421
|
+
"- 不希望当前修改影响主工作区时",
|
|
6422
|
+
"- Agent 需要隔离的文件系统环境时",
|
|
6423
|
+
"",
|
|
6424
|
+
"### 参数",
|
|
6425
|
+
"- **name**(可选): worktree 名称,用于标识和后续管理",
|
|
6426
|
+
"",
|
|
6427
|
+
"### 注意事项",
|
|
6428
|
+
"- 进入 worktree 后所有文件操作和 Shell 命令都在 worktree 中执行",
|
|
6429
|
+
"- 使用 ExitWorktree 工具退出 worktree"
|
|
6430
|
+
].join("\n"),
|
|
6431
|
+
defaultRisk: "high",
|
|
6432
|
+
parameters: {
|
|
6433
|
+
type: "object",
|
|
6434
|
+
properties: { name: {
|
|
6435
|
+
type: "string",
|
|
6436
|
+
description: "worktree 名称(可选,用于标识)"
|
|
6437
|
+
} },
|
|
6438
|
+
required: []
|
|
6439
|
+
},
|
|
6440
|
+
execute: async (_toolCallId, params) => {
|
|
6441
|
+
const name = params.name;
|
|
6442
|
+
try {
|
|
6443
|
+
const slug = name ?? `manual-${Date.now()}`;
|
|
6444
|
+
const info = await worktreeManager.create({
|
|
6445
|
+
slug,
|
|
6446
|
+
createdBy: "user"
|
|
6447
|
+
});
|
|
6448
|
+
process.chdir(info.worktreePath);
|
|
6449
|
+
return {
|
|
6450
|
+
content: [{
|
|
6451
|
+
type: "text",
|
|
6452
|
+
text: [
|
|
6453
|
+
"**已进入 worktree 隔离环境**",
|
|
6454
|
+
"",
|
|
6455
|
+
`- ID: \`${info.id}\``,
|
|
6456
|
+
`- 路径: \`${info.worktreePath}\``,
|
|
6457
|
+
`- 分支: \`${info.branchName}\``,
|
|
6458
|
+
"",
|
|
6459
|
+
"后续文件操作和 Shell 命令都将在 worktree 中执行。",
|
|
6460
|
+
"使用 ExitWorktree 工具退出。"
|
|
6461
|
+
].join("\n")
|
|
6462
|
+
}],
|
|
6463
|
+
details: {
|
|
6464
|
+
worktreeId: info.id,
|
|
6465
|
+
worktreePath: info.worktreePath,
|
|
6466
|
+
branchName: info.branchName
|
|
6467
|
+
}
|
|
6468
|
+
};
|
|
6469
|
+
} catch (err) {
|
|
6470
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6471
|
+
return {
|
|
6472
|
+
content: [{
|
|
6473
|
+
type: "text",
|
|
6474
|
+
text: `创建 worktree 失败: ${message}`
|
|
6475
|
+
}],
|
|
6476
|
+
details: { error: message }
|
|
6477
|
+
};
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
};
|
|
6481
|
+
}
|
|
6482
|
+
|
|
6483
|
+
//#endregion
|
|
6484
|
+
//#region src/core/worktree/worktree-context.ts
|
|
6485
|
+
/**
|
|
6486
|
+
* Worktree 运行时上下文
|
|
6487
|
+
*
|
|
6488
|
+
* 基于 AsyncLocalStorage 在整个工具调用链中传递 worktree 上下文。
|
|
6489
|
+
* 文件操作和 Shell 工具通过 resolveWorktreePath() 自动映射路径到 worktree。
|
|
6490
|
+
*
|
|
6491
|
+
* @module core/worktree
|
|
6492
|
+
*/
|
|
6493
|
+
const worktreeStorage = new AsyncLocalStorage();
|
|
6494
|
+
/** 获取当前 worktree 上下文 */
|
|
6495
|
+
function getWorktreeContext() {
|
|
6496
|
+
return worktreeStorage.getStore();
|
|
6497
|
+
}
|
|
6498
|
+
/** 在 worktree 上下文中执行回调 */
|
|
6499
|
+
async function runInWorktree(context, fn) {
|
|
6500
|
+
return worktreeStorage.run(context, fn);
|
|
6501
|
+
}
|
|
6502
|
+
/**
|
|
6503
|
+
* 将路径解析到当前 worktree
|
|
6504
|
+
*
|
|
6505
|
+
* 解析规则:
|
|
6506
|
+
* - 无 worktree 上下文 → 直接 resolve 返回
|
|
6507
|
+
* - 相对路径 → 在 worktree 根目录下解析
|
|
6508
|
+
* - 绝对路径(属于原项目目录) → 映射到 worktree 对应路径
|
|
6509
|
+
* - 绝对路径(不在项目目录内) → 保持原样
|
|
6510
|
+
*/
|
|
6511
|
+
function resolveWorktreePath(originalPath) {
|
|
6512
|
+
const ctx = worktreeStorage.getStore();
|
|
6513
|
+
if (!ctx) return resolve(originalPath);
|
|
6514
|
+
if (!isAbsolute(originalPath)) return resolve(ctx.worktreePath, originalPath);
|
|
6515
|
+
const normalizedOriginal = resolve(originalPath);
|
|
6516
|
+
const normalizedProjectRoot = resolve(ctx.originalPath);
|
|
6517
|
+
if (normalizedOriginal.startsWith(normalizedProjectRoot)) {
|
|
6518
|
+
const relPath = relative(normalizedProjectRoot, normalizedOriginal);
|
|
6519
|
+
return resolve(ctx.worktreePath, relPath);
|
|
6520
|
+
}
|
|
6521
|
+
return normalizedOriginal;
|
|
6522
|
+
}
|
|
6523
|
+
/**
|
|
6524
|
+
* 获取当前有效的工作目录
|
|
6525
|
+
*
|
|
6526
|
+
* 在 worktree 上下文中返回 worktree 路径,否则返回 process.cwd()
|
|
6527
|
+
*/
|
|
6528
|
+
function resolveWorkdir() {
|
|
6529
|
+
return worktreeStorage.getStore()?.worktreePath ?? process.cwd();
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
//#endregion
|
|
6533
|
+
//#region src/cli/repl/tools/exit-worktree.ts
|
|
6534
|
+
/**
|
|
6535
|
+
* ExitWorktree 工具
|
|
6536
|
+
*
|
|
6537
|
+
* 退出当前 worktree 隔离环境。
|
|
6538
|
+
* 支持 keep(保留 worktree)和 remove(删除 worktree)两种模式。
|
|
6539
|
+
*
|
|
6540
|
+
* @module cli/repl/tools
|
|
6541
|
+
*/
|
|
6542
|
+
/**
|
|
6543
|
+
* 创建 ExitWorktree 工具注册
|
|
6544
|
+
*/
|
|
6545
|
+
function createExitWorktreeTool(worktreeManager) {
|
|
6546
|
+
return {
|
|
6547
|
+
id: "ExitWorktree",
|
|
6548
|
+
label: "退出工作树",
|
|
6549
|
+
description: [
|
|
6550
|
+
"退出当前 worktree 隔离环境。",
|
|
6551
|
+
"",
|
|
6552
|
+
"### 参数",
|
|
6553
|
+
"- **action**: \"keep\" 保留 worktree,\"remove\" 删除 worktree",
|
|
6554
|
+
"- **discard_changes**: remove 时是否强制丢弃未提交变更(默认 false)",
|
|
6555
|
+
"",
|
|
6556
|
+
"### 安全门控",
|
|
6557
|
+
"- remove 模式:如果 worktree 有未提交变更且 discard_changes=false,操作将被拒绝",
|
|
6558
|
+
"- keep 模式:直接退出 worktree,worktree 保留在磁盘上"
|
|
6559
|
+
].join("\n"),
|
|
6560
|
+
defaultRisk: "high",
|
|
6561
|
+
parameters: {
|
|
6562
|
+
type: "object",
|
|
6563
|
+
properties: {
|
|
6564
|
+
action: {
|
|
6565
|
+
type: "string",
|
|
6566
|
+
enum: ["keep", "remove"],
|
|
6567
|
+
description: "keep=保留 worktree 并退出,remove=删除 worktree 并退出"
|
|
6568
|
+
},
|
|
6569
|
+
discard_changes: {
|
|
6570
|
+
type: "boolean",
|
|
6571
|
+
description: "remove 时是否强制丢弃未提交变更(默认 false)。设为 true 会强制删除 worktree"
|
|
6572
|
+
}
|
|
6573
|
+
},
|
|
6574
|
+
required: ["action"]
|
|
6575
|
+
},
|
|
6576
|
+
execute: async (_toolCallId, params) => {
|
|
6577
|
+
const p = params;
|
|
6578
|
+
const ctx = getWorktreeContext();
|
|
6579
|
+
if (!ctx) return { content: [{
|
|
6580
|
+
type: "text",
|
|
6581
|
+
text: "当前不处于 worktree 隔离环境。无需退出。"
|
|
6582
|
+
}] };
|
|
6583
|
+
try {
|
|
6584
|
+
if (p.action === "keep") {
|
|
6585
|
+
if (existsSync(ctx.originalPath)) process.chdir(ctx.originalPath);
|
|
6586
|
+
return {
|
|
6587
|
+
content: [{
|
|
6588
|
+
type: "text",
|
|
6589
|
+
text: [
|
|
6590
|
+
"**已退出 worktree 隔离环境(保留 worktree)**",
|
|
6591
|
+
"",
|
|
6592
|
+
`- Worktree ID: \`${ctx.worktreeId}\``,
|
|
6593
|
+
`- Worktree 路径: \`${ctx.worktreePath}\``,
|
|
6594
|
+
`- 已切换回: \`${ctx.originalPath}\``
|
|
6595
|
+
].join("\n")
|
|
6596
|
+
}],
|
|
6597
|
+
details: {
|
|
6598
|
+
action: "keep",
|
|
6599
|
+
worktreeId: ctx.worktreeId
|
|
6600
|
+
}
|
|
6601
|
+
};
|
|
6602
|
+
}
|
|
6603
|
+
try {
|
|
6604
|
+
await worktreeManager.remove(ctx.worktreeId, p.discard_changes);
|
|
6605
|
+
} catch (err) {
|
|
6606
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6607
|
+
if (!p.discard_changes && message.includes("changes")) return {
|
|
6608
|
+
content: [{
|
|
6609
|
+
type: "text",
|
|
6610
|
+
text: [
|
|
6611
|
+
"**无法删除 worktree:存在未提交的变更**",
|
|
6612
|
+
"",
|
|
6613
|
+
`错误: ${message}`,
|
|
6614
|
+
"",
|
|
6615
|
+
"请选择:",
|
|
6616
|
+
"- 设置 `discard_changes: true` 强制删除",
|
|
6617
|
+
"- 或使用 `action: \"keep\"` 保留 worktree"
|
|
6618
|
+
].join("\n")
|
|
6619
|
+
}],
|
|
6620
|
+
details: {
|
|
6621
|
+
action: "remove",
|
|
6622
|
+
error: "has_changes",
|
|
6623
|
+
worktreeId: ctx.worktreeId
|
|
6624
|
+
}
|
|
6625
|
+
};
|
|
6626
|
+
throw err;
|
|
6627
|
+
}
|
|
6628
|
+
if (existsSync(ctx.originalPath)) process.chdir(ctx.originalPath);
|
|
6629
|
+
return {
|
|
6630
|
+
content: [{
|
|
6631
|
+
type: "text",
|
|
6632
|
+
text: [
|
|
6633
|
+
"**已退出 worktree 隔离环境(已删除 worktree)**",
|
|
6634
|
+
"",
|
|
6635
|
+
`- Worktree ID: \`${ctx.worktreeId}\``,
|
|
6636
|
+
`- 已切换回: \`${ctx.originalPath}\``
|
|
6637
|
+
].join("\n")
|
|
6638
|
+
}],
|
|
6639
|
+
details: {
|
|
6640
|
+
action: "remove",
|
|
6641
|
+
worktreeId: ctx.worktreeId
|
|
6642
|
+
}
|
|
6643
|
+
};
|
|
6644
|
+
} catch (err) {
|
|
6645
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6646
|
+
return {
|
|
6647
|
+
content: [{
|
|
6648
|
+
type: "text",
|
|
6649
|
+
text: `退出 worktree 失败: ${message}`
|
|
6650
|
+
}],
|
|
6651
|
+
details: { error: message }
|
|
6652
|
+
};
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
};
|
|
6656
|
+
}
|
|
6657
|
+
|
|
4923
6658
|
//#endregion
|
|
4924
6659
|
//#region src/cli/repl/tools/file-security.ts
|
|
4925
6660
|
/**
|
|
@@ -5308,7 +7043,7 @@ function createEditFileTool() {
|
|
|
5308
7043
|
error: "old_string 与 new_string 相同"
|
|
5309
7044
|
}
|
|
5310
7045
|
};
|
|
5311
|
-
const pathResult = validateFilePath(params.file_path);
|
|
7046
|
+
const pathResult = validateFilePath(resolveWorktreePath(params.file_path));
|
|
5312
7047
|
if (!pathResult.valid) {
|
|
5313
7048
|
const details = {
|
|
5314
7049
|
filePath: params.file_path,
|
|
@@ -5532,7 +7267,7 @@ function createGlobTool() {
|
|
|
5532
7267
|
},
|
|
5533
7268
|
async execute(_toolCallId, params) {
|
|
5534
7269
|
const startTime = Date.now();
|
|
5535
|
-
const searchPath =
|
|
7270
|
+
const searchPath = resolveWorktreePath(params.path ?? process.cwd());
|
|
5536
7271
|
try {
|
|
5537
7272
|
const matches = globSync(params.pattern, searchPath);
|
|
5538
7273
|
const elapsedMs = Date.now() - startTime;
|
|
@@ -5748,7 +7483,7 @@ function createGrepTool() {
|
|
|
5748
7483
|
},
|
|
5749
7484
|
async execute(_toolCallId, params) {
|
|
5750
7485
|
const startTime = Date.now();
|
|
5751
|
-
const searchPath =
|
|
7486
|
+
const searchPath = resolveWorktreePath(params.path ?? process.cwd());
|
|
5752
7487
|
const outputMode = params.output_mode ?? "content";
|
|
5753
7488
|
const ignoreCase = params["-i"] === true;
|
|
5754
7489
|
const contextBefore = params["-B"] ?? params.context ?? 0;
|
|
@@ -5980,7 +7715,7 @@ function createWriteFileTool() {
|
|
|
5980
7715
|
checkPermission: checkFilePermission,
|
|
5981
7716
|
async execute(_toolCallId, params) {
|
|
5982
7717
|
const startTime = Date.now();
|
|
5983
|
-
const pathResult = validateFilePath(params.file_path);
|
|
7718
|
+
const pathResult = validateFilePath(resolveWorktreePath(params.file_path));
|
|
5984
7719
|
if (!pathResult.valid) {
|
|
5985
7720
|
const details = {
|
|
5986
7721
|
type: "create",
|
|
@@ -7085,7 +8820,7 @@ function createExecTool() {
|
|
|
7085
8820
|
durationMs: Date.now() - startTime
|
|
7086
8821
|
}
|
|
7087
8822
|
};
|
|
7088
|
-
const workdirResult = validateWorkdir(params.workdir);
|
|
8823
|
+
const workdirResult = validateWorkdir(params.workdir ?? resolveWorkdir());
|
|
7089
8824
|
if (!workdirResult.valid) return {
|
|
7090
8825
|
content: [{
|
|
7091
8826
|
type: "text",
|
|
@@ -9073,36 +10808,399 @@ function createWebSearchTool(webConfig) {
|
|
|
9073
10808
|
}
|
|
9074
10809
|
let results;
|
|
9075
10810
|
try {
|
|
9076
|
-
results = await provider.search(query, {
|
|
9077
|
-
maxResults: numResults,
|
|
9078
|
-
language
|
|
9079
|
-
}, providerConfig, signal);
|
|
10811
|
+
results = await provider.search(query, {
|
|
10812
|
+
maxResults: numResults,
|
|
10813
|
+
language
|
|
10814
|
+
}, providerConfig, signal);
|
|
10815
|
+
} catch (err) {
|
|
10816
|
+
if (err instanceof Error && err.message.includes("quota")) throw new WebError("WEB_SEARCH_QUOTA_EXCEEDED", err.message, { provider: providerName });
|
|
10817
|
+
throw new WebError("WEB_SEARCH_FAILED", `搜索失败: ${err instanceof Error ? err.message : String(err)}`, {
|
|
10818
|
+
provider: providerName,
|
|
10819
|
+
query
|
|
10820
|
+
});
|
|
10821
|
+
}
|
|
10822
|
+
const text = formatResults(results, query, provider.label);
|
|
10823
|
+
cache.set(cacheKey, text);
|
|
10824
|
+
const details = {
|
|
10825
|
+
query,
|
|
10826
|
+
provider: providerName,
|
|
10827
|
+
resultCount: results.length,
|
|
10828
|
+
cached: false,
|
|
10829
|
+
elapsedMs: Date.now() - startTime
|
|
10830
|
+
};
|
|
10831
|
+
return {
|
|
10832
|
+
content: [{
|
|
10833
|
+
type: "text",
|
|
10834
|
+
text
|
|
10835
|
+
}],
|
|
10836
|
+
details
|
|
10837
|
+
};
|
|
10838
|
+
}
|
|
10839
|
+
};
|
|
10840
|
+
}
|
|
10841
|
+
|
|
10842
|
+
//#endregion
|
|
10843
|
+
//#region src/core/worktree/types.ts
|
|
10844
|
+
/** Worktree 操作错误 */
|
|
10845
|
+
var WorktreeError = class extends Error {
|
|
10846
|
+
code;
|
|
10847
|
+
constructor(message, code) {
|
|
10848
|
+
super(message);
|
|
10849
|
+
this.name = "WorktreeError";
|
|
10850
|
+
this.code = code;
|
|
10851
|
+
}
|
|
10852
|
+
};
|
|
10853
|
+
|
|
10854
|
+
//#endregion
|
|
10855
|
+
//#region src/core/worktree/worktree-store.ts
|
|
10856
|
+
/**
|
|
10857
|
+
* Worktree 持久化存储
|
|
10858
|
+
*
|
|
10859
|
+
* 将 worktree 记录持久化到磁盘(~/.zapmyco/worktrees/ 目录),
|
|
10860
|
+
* 支持跨会话恢复和过期清理。
|
|
10861
|
+
*
|
|
10862
|
+
* @module core/worktree
|
|
10863
|
+
*/
|
|
10864
|
+
/** 获取默认存储目录 */
|
|
10865
|
+
function getDefaultBaseDir() {
|
|
10866
|
+
return join(__require("node:os").homedir(), ".zapmyco", "worktrees");
|
|
10867
|
+
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Worktree 持久化存储
|
|
10870
|
+
*/
|
|
10871
|
+
var WorktreeStore = class {
|
|
10872
|
+
baseDir;
|
|
10873
|
+
cache = /* @__PURE__ */ new Map();
|
|
10874
|
+
loaded = false;
|
|
10875
|
+
constructor(baseDir) {
|
|
10876
|
+
this.baseDir = baseDir || getDefaultBaseDir();
|
|
10877
|
+
}
|
|
10878
|
+
/** 获取存储目录 */
|
|
10879
|
+
getBaseDir() {
|
|
10880
|
+
return this.baseDir;
|
|
10881
|
+
}
|
|
10882
|
+
/** 保存记录到磁盘 */
|
|
10883
|
+
save(record) {
|
|
10884
|
+
this.ensureDir();
|
|
10885
|
+
this.cache.set(record.id, record);
|
|
10886
|
+
writeFileSync(this.getFilePath(record.id), JSON.stringify(record, null, 2), "utf-8");
|
|
10887
|
+
}
|
|
10888
|
+
/** 更新记录状态 */
|
|
10889
|
+
updateStatus(id, status) {
|
|
10890
|
+
const record = this.cache.get(id);
|
|
10891
|
+
if (record) {
|
|
10892
|
+
record.status = status;
|
|
10893
|
+
this.cache.set(id, record);
|
|
10894
|
+
this.save(record);
|
|
10895
|
+
}
|
|
10896
|
+
}
|
|
10897
|
+
/** 删除记录(从缓存和磁盘) */
|
|
10898
|
+
delete(id) {
|
|
10899
|
+
this.cache.delete(id);
|
|
10900
|
+
const filePath = this.getFilePath(id);
|
|
10901
|
+
try {
|
|
10902
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
10903
|
+
} catch {}
|
|
10904
|
+
}
|
|
10905
|
+
/** 获取单个记录 */
|
|
10906
|
+
get(id) {
|
|
10907
|
+
this.ensureLoaded();
|
|
10908
|
+
return this.cache.get(id);
|
|
10909
|
+
}
|
|
10910
|
+
/** 列出所有活跃记录 */
|
|
10911
|
+
listActive() {
|
|
10912
|
+
this.ensureLoaded();
|
|
10913
|
+
return Array.from(this.cache.values()).filter((r) => r.status === "active");
|
|
10914
|
+
}
|
|
10915
|
+
/** 列出所有记录 */
|
|
10916
|
+
listAll() {
|
|
10917
|
+
this.ensureLoaded();
|
|
10918
|
+
return Array.from(this.cache.values());
|
|
10919
|
+
}
|
|
10920
|
+
/** 从磁盘加载所有记录 */
|
|
10921
|
+
load() {
|
|
10922
|
+
this.ensureDir();
|
|
10923
|
+
this.cache.clear();
|
|
10924
|
+
try {
|
|
10925
|
+
const files = readdirSync(this.baseDir).filter((f) => f.endsWith(".json"));
|
|
10926
|
+
for (const file of files) try {
|
|
10927
|
+
const content = readFileSync(join(this.baseDir, file), "utf-8");
|
|
10928
|
+
const record = JSON.parse(content);
|
|
10929
|
+
if (record.id && record.worktreePath) this.cache.set(record.id, record);
|
|
10930
|
+
} catch {}
|
|
10931
|
+
} catch {}
|
|
10932
|
+
this.loaded = true;
|
|
10933
|
+
return Array.from(this.cache.values());
|
|
10934
|
+
}
|
|
10935
|
+
/** 清理过期记录文件 */
|
|
10936
|
+
cleanExpired(expireAfterMs) {
|
|
10937
|
+
this.ensureLoaded();
|
|
10938
|
+
let cleaned = 0;
|
|
10939
|
+
const now = Date.now();
|
|
10940
|
+
for (const [id, record] of this.cache) if (record.status === "expired") {
|
|
10941
|
+
this.delete(id);
|
|
10942
|
+
cleaned++;
|
|
10943
|
+
} else if (record.status === "active" && now - record.createdAt > expireAfterMs) {
|
|
10944
|
+
record.status = "expired";
|
|
10945
|
+
this.save(record);
|
|
10946
|
+
cleaned++;
|
|
10947
|
+
}
|
|
10948
|
+
return cleaned;
|
|
10949
|
+
}
|
|
10950
|
+
/** 确保目录存在 */
|
|
10951
|
+
ensureDir() {
|
|
10952
|
+
if (!existsSync(this.baseDir)) mkdirSync(this.baseDir, { recursive: true });
|
|
10953
|
+
}
|
|
10954
|
+
/** 确保已加载 */
|
|
10955
|
+
ensureLoaded() {
|
|
10956
|
+
if (!this.loaded) this.load();
|
|
10957
|
+
}
|
|
10958
|
+
/** 获取记录文件路径 */
|
|
10959
|
+
getFilePath(id) {
|
|
10960
|
+
const safeId = id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
10961
|
+
return join(this.baseDir, `${safeId}.json`);
|
|
10962
|
+
}
|
|
10963
|
+
};
|
|
10964
|
+
|
|
10965
|
+
//#endregion
|
|
10966
|
+
//#region src/core/worktree/worktree-manager.ts
|
|
10967
|
+
/**
|
|
10968
|
+
* Worktree 管理器
|
|
10969
|
+
*
|
|
10970
|
+
* 负责 git worktree 的完整生命周期管理:
|
|
10971
|
+
* - 创建隔离 worktree
|
|
10972
|
+
* - 执行后自动清理(无变更)或保留(有变更)
|
|
10973
|
+
* - 过期 worktree 清理
|
|
10974
|
+
*
|
|
10975
|
+
* 通过 child_process.execFile 调用 git 命令,不引入额外依赖。
|
|
10976
|
+
*
|
|
10977
|
+
* @module core/worktree
|
|
10978
|
+
*/
|
|
10979
|
+
const execFileAsync = promisify(execFile);
|
|
10980
|
+
const log$10 = logger.child("worktree-manager");
|
|
10981
|
+
let globalWorktreeManager = null;
|
|
10982
|
+
/** 获取全局 WorktreeManager 实例 */
|
|
10983
|
+
function getWorktreeManager() {
|
|
10984
|
+
return globalWorktreeManager ?? void 0;
|
|
10985
|
+
}
|
|
10986
|
+
/** 设置全局 WorktreeManager 实例 */
|
|
10987
|
+
function setWorktreeManager(manager) {
|
|
10988
|
+
globalWorktreeManager = manager;
|
|
10989
|
+
}
|
|
10990
|
+
var WorktreeManager = class {
|
|
10991
|
+
config;
|
|
10992
|
+
store;
|
|
10993
|
+
activeWorktrees = /* @__PURE__ */ new Map();
|
|
10994
|
+
constructor(config) {
|
|
10995
|
+
this.config = config;
|
|
10996
|
+
this.store = new WorktreeStore(config.baseDir);
|
|
10997
|
+
this.store.load();
|
|
10998
|
+
}
|
|
10999
|
+
/**
|
|
11000
|
+
* 创建新的 git worktree
|
|
11001
|
+
*/
|
|
11002
|
+
async create(options) {
|
|
11003
|
+
if (!this.config.enabled) throw new WorktreeError("Worktree 功能未启用", "DISABLED");
|
|
11004
|
+
const timestamp = Date.now();
|
|
11005
|
+
const branchName = `zapmyco-${options.slug}-${timestamp}`;
|
|
11006
|
+
const dirName = `${options.slug}-${timestamp}`;
|
|
11007
|
+
const worktreePath = join(this.store.getBaseDir(), dirName);
|
|
11008
|
+
let gitRoot;
|
|
11009
|
+
try {
|
|
11010
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { timeout: 5e3 });
|
|
11011
|
+
gitRoot = stdout.trim();
|
|
11012
|
+
} catch {
|
|
11013
|
+
throw new WorktreeError("无法确定 git 仓库根目录,请确保在 git 仓库中运行", "NOT_GIT_REPO");
|
|
11014
|
+
}
|
|
11015
|
+
try {
|
|
11016
|
+
log$10.info("创建 worktree", {
|
|
11017
|
+
branchName,
|
|
11018
|
+
worktreePath,
|
|
11019
|
+
gitRoot
|
|
11020
|
+
});
|
|
11021
|
+
await execFileAsync("git", [
|
|
11022
|
+
"worktree",
|
|
11023
|
+
"add",
|
|
11024
|
+
"--detach",
|
|
11025
|
+
worktreePath
|
|
11026
|
+
], {
|
|
11027
|
+
cwd: gitRoot,
|
|
11028
|
+
timeout: 3e4
|
|
11029
|
+
});
|
|
11030
|
+
} catch (err) {
|
|
11031
|
+
throw new WorktreeError(`创建 worktree 失败: ${err instanceof Error ? err.message : String(err)}`, "CREATE_FAILED");
|
|
11032
|
+
}
|
|
11033
|
+
try {
|
|
11034
|
+
await execFileAsync("git", [
|
|
11035
|
+
"checkout",
|
|
11036
|
+
"-b",
|
|
11037
|
+
branchName
|
|
11038
|
+
], {
|
|
11039
|
+
cwd: worktreePath,
|
|
11040
|
+
timeout: 1e4
|
|
11041
|
+
});
|
|
11042
|
+
} catch (err) {
|
|
11043
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11044
|
+
log$10.warn("创建分支失败,清理 worktree", {
|
|
11045
|
+
branchName,
|
|
11046
|
+
error: msg
|
|
11047
|
+
});
|
|
11048
|
+
try {
|
|
11049
|
+
await this.removeByPath(worktreePath, branchName, true);
|
|
11050
|
+
} catch {}
|
|
11051
|
+
throw new WorktreeError(`在 worktree 中创建分支失败: ${msg}`, "BRANCH_FAILED");
|
|
11052
|
+
}
|
|
11053
|
+
const info = {
|
|
11054
|
+
id: `${options.slug}-${timestamp}`,
|
|
11055
|
+
worktreePath,
|
|
11056
|
+
branchName,
|
|
11057
|
+
originalPath: gitRoot,
|
|
11058
|
+
createdAt: timestamp,
|
|
11059
|
+
createdBy: options.createdBy
|
|
11060
|
+
};
|
|
11061
|
+
this.activeWorktrees.set(info.id, info);
|
|
11062
|
+
this.store.save({
|
|
11063
|
+
id: info.id,
|
|
11064
|
+
worktreePath: info.worktreePath,
|
|
11065
|
+
branchName: info.branchName,
|
|
11066
|
+
originalPath: info.originalPath,
|
|
11067
|
+
createdAt: info.createdAt,
|
|
11068
|
+
createdBy: info.createdBy,
|
|
11069
|
+
status: "active"
|
|
11070
|
+
});
|
|
11071
|
+
log$10.info("Worktree 创建成功", {
|
|
11072
|
+
id: info.id,
|
|
11073
|
+
path: worktreePath
|
|
11074
|
+
});
|
|
11075
|
+
return info;
|
|
11076
|
+
}
|
|
11077
|
+
/**
|
|
11078
|
+
* 删除指定 worktree
|
|
11079
|
+
*/
|
|
11080
|
+
async remove(id, discardChanges) {
|
|
11081
|
+
const info = this.activeWorktrees.get(id);
|
|
11082
|
+
if (!info) {
|
|
11083
|
+
log$10.warn("尝试删除不存在的 worktree", { id });
|
|
11084
|
+
return;
|
|
11085
|
+
}
|
|
11086
|
+
await this.removeByPath(info.worktreePath, info.branchName, discardChanges);
|
|
11087
|
+
this.activeWorktrees.delete(id);
|
|
11088
|
+
this.store.delete(id);
|
|
11089
|
+
log$10.info("Worktree 已删除", { id });
|
|
11090
|
+
}
|
|
11091
|
+
/**
|
|
11092
|
+
* 通过路径删除 worktree(内部方法)
|
|
11093
|
+
*/
|
|
11094
|
+
async removeByPath(worktreePath, branchName, discardChanges) {
|
|
11095
|
+
if (!existsSync(worktreePath)) {
|
|
11096
|
+
try {
|
|
11097
|
+
await execFileAsync("git", ["worktree", "prune"], { timeout: 1e4 });
|
|
11098
|
+
} catch {}
|
|
11099
|
+
return;
|
|
11100
|
+
}
|
|
11101
|
+
const args = ["worktree", "remove"];
|
|
11102
|
+
if (discardChanges) args.push("--force");
|
|
11103
|
+
args.push(worktreePath);
|
|
11104
|
+
try {
|
|
11105
|
+
await execFileAsync("git", args, { timeout: 15e3 });
|
|
11106
|
+
} catch (err) {
|
|
11107
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11108
|
+
log$10.warn("git worktree remove 失败", {
|
|
11109
|
+
worktreePath,
|
|
11110
|
+
error: msg
|
|
11111
|
+
});
|
|
11112
|
+
}
|
|
11113
|
+
try {
|
|
11114
|
+
await execFileAsync("git", [
|
|
11115
|
+
"branch",
|
|
11116
|
+
"-D",
|
|
11117
|
+
branchName
|
|
11118
|
+
], { timeout: 1e4 });
|
|
11119
|
+
} catch {}
|
|
11120
|
+
try {
|
|
11121
|
+
await execFileAsync("git", ["worktree", "prune"], { timeout: 1e4 });
|
|
11122
|
+
} catch {}
|
|
11123
|
+
}
|
|
11124
|
+
/**
|
|
11125
|
+
* 检查 worktree 是否有变更,无变更则自动清理
|
|
11126
|
+
*/
|
|
11127
|
+
async autoCleanIfNoChanges(id) {
|
|
11128
|
+
const info = this.activeWorktrees.get(id);
|
|
11129
|
+
if (!info) return { cleaned: true };
|
|
11130
|
+
if (!this.config.autoCleanNoChanges) return {
|
|
11131
|
+
cleaned: false,
|
|
11132
|
+
worktreePath: info.worktreePath
|
|
11133
|
+
};
|
|
11134
|
+
if (!existsSync(info.worktreePath)) {
|
|
11135
|
+
this.activeWorktrees.delete(id);
|
|
11136
|
+
this.store.delete(id);
|
|
11137
|
+
return { cleaned: true };
|
|
11138
|
+
}
|
|
11139
|
+
if (!await this.checkHasChanges(info.worktreePath)) {
|
|
11140
|
+
await this.remove(id, true);
|
|
11141
|
+
return { cleaned: true };
|
|
11142
|
+
}
|
|
11143
|
+
log$10.info("Worktree 有变更,保留", {
|
|
11144
|
+
id,
|
|
11145
|
+
path: info.worktreePath
|
|
11146
|
+
});
|
|
11147
|
+
return {
|
|
11148
|
+
cleaned: false,
|
|
11149
|
+
worktreePath: info.worktreePath
|
|
11150
|
+
};
|
|
11151
|
+
}
|
|
11152
|
+
/**
|
|
11153
|
+
* 检查 worktree 中是否有未提交变更
|
|
11154
|
+
*/
|
|
11155
|
+
async checkHasChanges(worktreePath) {
|
|
11156
|
+
try {
|
|
11157
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain"], {
|
|
11158
|
+
cwd: worktreePath,
|
|
11159
|
+
timeout: 5e3
|
|
11160
|
+
});
|
|
11161
|
+
return stdout.trim().length > 0;
|
|
11162
|
+
} catch {
|
|
11163
|
+
return true;
|
|
11164
|
+
}
|
|
11165
|
+
}
|
|
11166
|
+
/**
|
|
11167
|
+
* 清理过期的 worktree
|
|
11168
|
+
*/
|
|
11169
|
+
async cleanExpired() {
|
|
11170
|
+
const now = Date.now();
|
|
11171
|
+
let cleaned = 0;
|
|
11172
|
+
this.store.cleanExpired(this.config.expireAfterMs);
|
|
11173
|
+
const allRecords = this.store.listAll();
|
|
11174
|
+
for (const record of allRecords) if (record.status === "expired" || now - record.createdAt > this.config.expireAfterMs) {
|
|
11175
|
+
if (existsSync(record.worktreePath)) try {
|
|
11176
|
+
await this.removeByPath(record.worktreePath, record.branchName, true);
|
|
11177
|
+
cleaned++;
|
|
9080
11178
|
} catch (err) {
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
11179
|
+
log$10.warn("过期 worktree 清理失败", {
|
|
11180
|
+
id: record.id,
|
|
11181
|
+
path: record.worktreePath,
|
|
11182
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9085
11183
|
});
|
|
9086
11184
|
}
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
const details = {
|
|
9090
|
-
query,
|
|
9091
|
-
provider: providerName,
|
|
9092
|
-
resultCount: results.length,
|
|
9093
|
-
cached: false,
|
|
9094
|
-
elapsedMs: Date.now() - startTime
|
|
9095
|
-
};
|
|
9096
|
-
return {
|
|
9097
|
-
content: [{
|
|
9098
|
-
type: "text",
|
|
9099
|
-
text
|
|
9100
|
-
}],
|
|
9101
|
-
details
|
|
9102
|
-
};
|
|
11185
|
+
this.activeWorktrees.delete(record.id);
|
|
11186
|
+
this.store.delete(record.id);
|
|
9103
11187
|
}
|
|
9104
|
-
|
|
9105
|
-
|
|
11188
|
+
if (cleaned > 0) log$10.info("过期 worktree 清理完成", { cleaned });
|
|
11189
|
+
return cleaned;
|
|
11190
|
+
}
|
|
11191
|
+
listActive() {
|
|
11192
|
+
return Array.from(this.activeWorktrees.values());
|
|
11193
|
+
}
|
|
11194
|
+
getWorktree(id) {
|
|
11195
|
+
return this.activeWorktrees.get(id);
|
|
11196
|
+
}
|
|
11197
|
+
getConfig() {
|
|
11198
|
+
return { ...this.config };
|
|
11199
|
+
}
|
|
11200
|
+
getStore() {
|
|
11201
|
+
return this.store;
|
|
11202
|
+
}
|
|
11203
|
+
};
|
|
9106
11204
|
|
|
9107
11205
|
//#endregion
|
|
9108
11206
|
//#region src/core/agent-team/types.ts
|
|
@@ -9146,7 +11244,7 @@ const AGENT_STANDARD_TOOLS = [
|
|
|
9146
11244
|
*
|
|
9147
11245
|
* @module core/agent-team
|
|
9148
11246
|
*/
|
|
9149
|
-
const log$
|
|
11247
|
+
const log$9 = logger.child("agent-factory");
|
|
9150
11248
|
/**
|
|
9151
11249
|
* 创建 Agent 实例
|
|
9152
11250
|
*
|
|
@@ -9178,7 +11276,7 @@ function createAgentFromType(definition, instance, parentAgent, availableTools,
|
|
|
9178
11276
|
const tools = resolveTools(definition, availableTools, instance.depth, config);
|
|
9179
11277
|
agent.registerTools(tools);
|
|
9180
11278
|
agent.systemPromptOverride = buildSystemPrompt(definition, instance.task.description, config);
|
|
9181
|
-
log$
|
|
11279
|
+
log$9.debug("创建 Agent 实例", {
|
|
9182
11280
|
typeId: definition.typeId,
|
|
9183
11281
|
instanceId: instance.instanceId,
|
|
9184
11282
|
depth: instance.depth,
|
|
@@ -9262,7 +11360,7 @@ function shareParentResources(subAgent, parentAgent) {
|
|
|
9262
11360
|
|
|
9263
11361
|
//#endregion
|
|
9264
11362
|
//#region src/core/agent-team/agent-result-aggregator.ts
|
|
9265
|
-
const log$
|
|
11363
|
+
const log$8 = logger.child("agent-result-aggregator");
|
|
9266
11364
|
/** 零值 TokenUsage */
|
|
9267
11365
|
const ZERO_TOKEN = {
|
|
9268
11366
|
inputTokens: 0,
|
|
@@ -9288,7 +11386,7 @@ function aggregateResults(teamId, workerResults) {
|
|
|
9288
11386
|
estimatedCostUsd: sum.estimatedCostUsd + (r.tokenUsage?.estimatedCostUsd ?? 0)
|
|
9289
11387
|
}), { ...ZERO_TOKEN });
|
|
9290
11388
|
const summary = buildTeamSummary(workerResults);
|
|
9291
|
-
log$
|
|
11389
|
+
log$8.debug("Team 结果聚合完成", {
|
|
9292
11390
|
teamId,
|
|
9293
11391
|
total: workerResults.length,
|
|
9294
11392
|
succeeded,
|
|
@@ -9366,7 +11464,7 @@ function escapeMarkdownTable(text) {
|
|
|
9366
11464
|
|
|
9367
11465
|
//#endregion
|
|
9368
11466
|
//#region src/core/agent-team/agent-orchestrator.ts
|
|
9369
|
-
const log$
|
|
11467
|
+
const log$7 = logger.child("agent-orchestrator");
|
|
9370
11468
|
/**
|
|
9371
11469
|
* Agent 编排器
|
|
9372
11470
|
*
|
|
@@ -9399,7 +11497,7 @@ var AgentOrchestrator = class {
|
|
|
9399
11497
|
const startTime = Date.now();
|
|
9400
11498
|
const defaultType = getAgentTypeRegistry().getDefault();
|
|
9401
11499
|
if (!defaultType) throw new Error("无法获取默认 Agent 类型(general-purpose)");
|
|
9402
|
-
log$
|
|
11500
|
+
log$7.info("开始扁平并行执行", {
|
|
9403
11501
|
count: specs.length,
|
|
9404
11502
|
maxConcurrent: this.flatConfig.maxConcurrent,
|
|
9405
11503
|
hasContext: context != null
|
|
@@ -9407,7 +11505,7 @@ var AgentOrchestrator = class {
|
|
|
9407
11505
|
const allResults = [];
|
|
9408
11506
|
for (let i = 0; i < specs.length; i += this.flatConfig.maxConcurrent) {
|
|
9409
11507
|
const batch = specs.slice(i, i + this.flatConfig.maxConcurrent);
|
|
9410
|
-
log$
|
|
11508
|
+
log$7.debug("执行扁平批次", {
|
|
9411
11509
|
batchStart: i,
|
|
9412
11510
|
batchSize: batch.length,
|
|
9413
11511
|
totalSpecs: specs.length
|
|
@@ -9417,7 +11515,7 @@ var AgentOrchestrator = class {
|
|
|
9417
11515
|
}
|
|
9418
11516
|
const succeeded = allResults.filter((r) => r.status === "success").length;
|
|
9419
11517
|
const totalDuration = Date.now() - startTime;
|
|
9420
|
-
log$
|
|
11518
|
+
log$7.info("扁平并行执行完成", {
|
|
9421
11519
|
total: allResults.length,
|
|
9422
11520
|
succeeded,
|
|
9423
11521
|
failed: allResults.length - succeeded,
|
|
@@ -9489,7 +11587,7 @@ var AgentOrchestrator = class {
|
|
|
9489
11587
|
} catch (error) {
|
|
9490
11588
|
const duration = Date.now() - startTime;
|
|
9491
11589
|
const message = error instanceof Error ? error.message : String(error);
|
|
9492
|
-
log$
|
|
11590
|
+
log$7.warn("扁平子任务执行失败", {
|
|
9493
11591
|
specId: spec.id,
|
|
9494
11592
|
error: message,
|
|
9495
11593
|
duration
|
|
@@ -9541,7 +11639,7 @@ var AgentOrchestrator = class {
|
|
|
9541
11639
|
const parentInstanceId = options?.parentInstanceId ?? "";
|
|
9542
11640
|
const depth = (parentInstanceId && instanceManager.get(parentInstanceId) ? instanceManager.get(parentInstanceId)?.depth ?? 0 : 0) + 1;
|
|
9543
11641
|
if (depth > this.teamConfig.maxGlobalDepth) {
|
|
9544
|
-
log$
|
|
11642
|
+
log$7.warn("Worker 创建被拒绝:超过全局最大深度", {
|
|
9545
11643
|
typeId,
|
|
9546
11644
|
depth,
|
|
9547
11645
|
maxGlobalDepth: this.teamConfig.maxGlobalDepth
|
|
@@ -9570,6 +11668,7 @@ var AgentOrchestrator = class {
|
|
|
9570
11668
|
const taskId = options?.taskId ?? `worker-${typeId}-${Date.now()}`;
|
|
9571
11669
|
const instanceId = `agent-${typeId}-${Date.now()}`;
|
|
9572
11670
|
const timeoutMs = options?.timeoutMs ?? this.flatConfig.taskTimeoutMs;
|
|
11671
|
+
let worktreeInfo;
|
|
9573
11672
|
try {
|
|
9574
11673
|
const agent = createAgentFromType(definition, {
|
|
9575
11674
|
instanceId,
|
|
@@ -9596,16 +11695,36 @@ var AgentOrchestrator = class {
|
|
|
9596
11695
|
if (options?.context) promptCtx.context = options.context;
|
|
9597
11696
|
const systemPrompt = definition.getSystemPrompt(promptCtx);
|
|
9598
11697
|
agent.systemPromptOverride = this.enrichSystemPrompt(systemPrompt, instanceId, parentInstanceId || null);
|
|
11698
|
+
let effectiveOptions = options;
|
|
11699
|
+
if (options?.isolation === "worktree" && getWorktreeManager()) {
|
|
11700
|
+
worktreeInfo = await getWorktreeManager().create({
|
|
11701
|
+
slug: `${typeId}-${instanceId}`,
|
|
11702
|
+
createdBy: instanceId
|
|
11703
|
+
});
|
|
11704
|
+
const innerWrapExecute = options.wrapExecute;
|
|
11705
|
+
effectiveOptions = { ...options };
|
|
11706
|
+
effectiveOptions.wrapExecute = (execute) => {
|
|
11707
|
+
const inner = innerWrapExecute ? () => innerWrapExecute(execute) : execute;
|
|
11708
|
+
return runInWorktree({
|
|
11709
|
+
worktreeId: worktreeInfo.id,
|
|
11710
|
+
worktreePath: worktreeInfo.worktreePath,
|
|
11711
|
+
originalPath: worktreeInfo.originalPath
|
|
11712
|
+
}, inner);
|
|
11713
|
+
};
|
|
11714
|
+
}
|
|
9599
11715
|
instanceManager.transition(instanceId, "running");
|
|
9600
|
-
const
|
|
11716
|
+
const workdir = worktreeInfo?.worktreePath ?? process.cwd();
|
|
11717
|
+
const executeFn = () => agent.execute({
|
|
9601
11718
|
taskId,
|
|
9602
11719
|
taskDescription,
|
|
9603
|
-
workdir
|
|
11720
|
+
workdir,
|
|
9604
11721
|
options: {
|
|
9605
11722
|
timeout: timeoutMs,
|
|
9606
11723
|
verbose: false
|
|
9607
11724
|
}
|
|
9608
|
-
})
|
|
11725
|
+
});
|
|
11726
|
+
const executePromise = effectiveOptions?.wrapExecute ? effectiveOptions.wrapExecute(executeFn) : executeFn();
|
|
11727
|
+
const result = await Promise.race([executePromise, this.createTimeoutPromise(taskId, timeoutMs)]);
|
|
9609
11728
|
const duration = Date.now() - startTime;
|
|
9610
11729
|
const taskResult = result;
|
|
9611
11730
|
const outputText = this.extractOutputText(result);
|
|
@@ -9636,7 +11755,7 @@ var AgentOrchestrator = class {
|
|
|
9636
11755
|
} catch (error) {
|
|
9637
11756
|
const duration = Date.now() - startTime;
|
|
9638
11757
|
const message = error instanceof Error ? error.message : String(error);
|
|
9639
|
-
log$
|
|
11758
|
+
log$7.warn("Worker 执行失败", {
|
|
9640
11759
|
typeId,
|
|
9641
11760
|
instanceId,
|
|
9642
11761
|
error: message,
|
|
@@ -9670,6 +11789,21 @@ var AgentOrchestrator = class {
|
|
|
9670
11789
|
const instance = instanceManager.get(instanceId);
|
|
9671
11790
|
if (instance) instance.agent.systemPromptOverride = null;
|
|
9672
11791
|
} catch {}
|
|
11792
|
+
if (worktreeInfo) {
|
|
11793
|
+
const wm = getWorktreeManager();
|
|
11794
|
+
if (wm) try {
|
|
11795
|
+
const cleanResult = await wm.autoCleanIfNoChanges(worktreeInfo.id);
|
|
11796
|
+
if (!cleanResult.cleaned) log$7.info("Worktree 有变更,保留", {
|
|
11797
|
+
id: worktreeInfo.id,
|
|
11798
|
+
path: cleanResult.worktreePath
|
|
11799
|
+
});
|
|
11800
|
+
} catch (err) {
|
|
11801
|
+
log$7.warn("Worktree 清理失败", {
|
|
11802
|
+
id: worktreeInfo.id,
|
|
11803
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11804
|
+
});
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
9673
11807
|
}
|
|
9674
11808
|
}
|
|
9675
11809
|
/**
|
|
@@ -9684,7 +11818,7 @@ var AgentOrchestrator = class {
|
|
|
9684
11818
|
*/
|
|
9685
11819
|
async spawnTeam(taskDescription, workerSpecs) {
|
|
9686
11820
|
const teamId = `team-${Date.now()}-${++this.teamCounter}`;
|
|
9687
|
-
log$
|
|
11821
|
+
log$7.info("创建 Team", {
|
|
9688
11822
|
teamId,
|
|
9689
11823
|
workerCount: workerSpecs.length,
|
|
9690
11824
|
taskDescription
|
|
@@ -9845,6 +11979,8 @@ const TOOL_RISK_MAP = {
|
|
|
9845
11979
|
Skill: "medium",
|
|
9846
11980
|
TaskManage: "medium",
|
|
9847
11981
|
ScheduledTask: "medium",
|
|
11982
|
+
LSP: "low",
|
|
11983
|
+
AskUserQuestion: "medium",
|
|
9848
11984
|
SpawnSubAgents: "high"
|
|
9849
11985
|
};
|
|
9850
11986
|
|
|
@@ -9856,7 +11992,7 @@ const TOOL_RISK_MAP = {
|
|
|
9856
11992
|
* @param webConfig - Web 工具配置(可选),传入时启用 WebFetch 和 WebSearch
|
|
9857
11993
|
* @param taskStore - TaskStore 实例(可选),传入时启用 TaskManage 工具
|
|
9858
11994
|
*/
|
|
9859
|
-
function createReplBuiltinTools(webConfig, taskStore, skillConfig, parentAgent, subAgentConfig, cronScheduler, agentTeamConfig) {
|
|
11995
|
+
function createReplBuiltinTools(webConfig, taskStore, skillConfig, parentAgent, subAgentConfig, cronScheduler, agentTeamConfig, worktreeManager) {
|
|
9860
11996
|
const tools = [
|
|
9861
11997
|
{
|
|
9862
11998
|
id: "GetCurrentTime",
|
|
@@ -9920,154 +12056,1353 @@ function createReplBuiltinTools(webConfig, taskStore, skillConfig, parentAgent,
|
|
|
9920
12056
|
type: "number",
|
|
9921
12057
|
description: "最大读取行数(可选,默认 2000)"
|
|
9922
12058
|
}
|
|
9923
|
-
},
|
|
9924
|
-
required: ["file_path"]
|
|
9925
|
-
},
|
|
9926
|
-
execute: async (_toolCallId, params) => {
|
|
9927
|
-
const fs = await import("node:fs/promises");
|
|
9928
|
-
const resolvedPath = (
|
|
9929
|
-
try {
|
|
9930
|
-
const lines = (await fs.readFile(resolvedPath, "utf-8")).split("\n");
|
|
9931
|
-
const offset = params.offset ? Math.max(1, params.offset) : 1;
|
|
9932
|
-
const limit = params.limit ?? 2e3;
|
|
9933
|
-
const startIdx = offset - 1;
|
|
9934
|
-
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
9935
|
-
const pageContent = lines.slice(startIdx, endIdx).map((l, i) => `${startIdx + i + 1}\t${l}`).join("\n");
|
|
9936
|
-
const truncated = endIdx < lines.length;
|
|
9937
|
-
readStateTracker.recordRead(resolvedPath);
|
|
9938
|
-
return {
|
|
12059
|
+
},
|
|
12060
|
+
required: ["file_path"]
|
|
12061
|
+
},
|
|
12062
|
+
execute: async (_toolCallId, params) => {
|
|
12063
|
+
const fs = await import("node:fs/promises");
|
|
12064
|
+
const resolvedPath = resolveWorktreePath(params.file_path);
|
|
12065
|
+
try {
|
|
12066
|
+
const lines = (await fs.readFile(resolvedPath, "utf-8")).split("\n");
|
|
12067
|
+
const offset = params.offset ? Math.max(1, params.offset) : 1;
|
|
12068
|
+
const limit = params.limit ?? 2e3;
|
|
12069
|
+
const startIdx = offset - 1;
|
|
12070
|
+
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
12071
|
+
const pageContent = lines.slice(startIdx, endIdx).map((l, i) => `${startIdx + i + 1}\t${l}`).join("\n");
|
|
12072
|
+
const truncated = endIdx < lines.length;
|
|
12073
|
+
readStateTracker.recordRead(resolvedPath);
|
|
12074
|
+
return {
|
|
12075
|
+
content: [{
|
|
12076
|
+
type: "text",
|
|
12077
|
+
text: pageContent + (truncated ? `\n\n[文件共 ${lines.length} 行,已显示 ${offset}-${endIdx} 行]` : "")
|
|
12078
|
+
}],
|
|
12079
|
+
details: {
|
|
12080
|
+
path: resolvedPath,
|
|
12081
|
+
totalLines: lines.length,
|
|
12082
|
+
displayedLines: endIdx - startIdx,
|
|
12083
|
+
offset,
|
|
12084
|
+
limit,
|
|
12085
|
+
truncated
|
|
12086
|
+
}
|
|
12087
|
+
};
|
|
12088
|
+
} catch (error) {
|
|
12089
|
+
return {
|
|
12090
|
+
content: [{
|
|
12091
|
+
type: "text",
|
|
12092
|
+
text: `读取失败: ${error instanceof Error ? error.message : String(error)}`
|
|
12093
|
+
}],
|
|
12094
|
+
details: {
|
|
12095
|
+
path: resolvedPath,
|
|
12096
|
+
error: true
|
|
12097
|
+
}
|
|
12098
|
+
};
|
|
12099
|
+
}
|
|
12100
|
+
}
|
|
12101
|
+
}
|
|
12102
|
+
];
|
|
12103
|
+
tools.push(createWriteFileTool());
|
|
12104
|
+
tools.push(createEditFileTool());
|
|
12105
|
+
tools.push(createGlobTool());
|
|
12106
|
+
tools.push(createGrepTool());
|
|
12107
|
+
tools.push(createExecTool());
|
|
12108
|
+
tools.push(createProcessTool());
|
|
12109
|
+
if (webConfig?.enabled !== false) {
|
|
12110
|
+
tools.push(createWebFetchTool(webConfig));
|
|
12111
|
+
tools.push(createWebSearchTool(webConfig));
|
|
12112
|
+
}
|
|
12113
|
+
if (taskStore) tools.push(createTaskManageTool(taskStore));
|
|
12114
|
+
tools.push(createMemoryTool());
|
|
12115
|
+
if (skillConfig?.enabled !== false) tools.push(createSkillTool(skillConfig));
|
|
12116
|
+
if (parentAgent && subAgentConfig?.enabled !== false && subAgentConfig) {
|
|
12117
|
+
let orchestrator;
|
|
12118
|
+
if (agentTeamConfig?.enabled) {
|
|
12119
|
+
orchestrator = new AgentOrchestrator(agentTeamConfig, subAgentConfig, parentAgent, tools);
|
|
12120
|
+
tools.push(createAgentTool(orchestrator));
|
|
12121
|
+
const bgManager = getBackgroundAgentManager();
|
|
12122
|
+
bgManager.setOrchestrator(orchestrator);
|
|
12123
|
+
bgManager.restore();
|
|
12124
|
+
}
|
|
12125
|
+
const manager = new SubAgentManager(subAgentConfig, parentAgent, tools, orchestrator);
|
|
12126
|
+
if (!agentTeamConfig?.enabled) tools.push(createSpawnSubAgentsTool(manager, subAgentConfig));
|
|
12127
|
+
}
|
|
12128
|
+
if (worktreeManager) {
|
|
12129
|
+
tools.push(createEnterWorktreeTool(worktreeManager));
|
|
12130
|
+
tools.push(createExitWorktreeTool(worktreeManager));
|
|
12131
|
+
}
|
|
12132
|
+
if (cronScheduler) tools.push(createCronTool(cronScheduler));
|
|
12133
|
+
tools.push(createAskUserQuestionTool());
|
|
12134
|
+
for (const tool of tools) if (!tool.defaultRisk) tool.defaultRisk = TOOL_RISK_MAP[tool.id] ?? "medium";
|
|
12135
|
+
return tools;
|
|
12136
|
+
}
|
|
12137
|
+
|
|
12138
|
+
//#endregion
|
|
12139
|
+
//#region src/cli/repl/theme.ts
|
|
12140
|
+
/** 根据颜色开关获取 chalk 实例 */
|
|
12141
|
+
function makeColor(colorEnabled) {
|
|
12142
|
+
return colorEnabled ? chalk : new Chalk({ level: 0 });
|
|
12143
|
+
}
|
|
12144
|
+
/**
|
|
12145
|
+
* 创建 zapmyco pi-tui 主题
|
|
12146
|
+
*/
|
|
12147
|
+
function createTheme(colorEnabled) {
|
|
12148
|
+
const c = makeColor(colorEnabled);
|
|
12149
|
+
/** 基础 selectList 主题 */
|
|
12150
|
+
const baseSelectListTheme = {
|
|
12151
|
+
selectedPrefix: (text) => c.cyan(text),
|
|
12152
|
+
selectedText: (text) => c.bold(c.cyan(text)),
|
|
12153
|
+
description: (text) => c.gray(text),
|
|
12154
|
+
scrollInfo: (text) => c.gray(text),
|
|
12155
|
+
noMatch: (text) => c.gray(text)
|
|
12156
|
+
};
|
|
12157
|
+
return {
|
|
12158
|
+
/** 主文本色 */
|
|
12159
|
+
text: (s) => s,
|
|
12160
|
+
/** 加粗 */
|
|
12161
|
+
bold: (s) => c.bold(s),
|
|
12162
|
+
/** 灰色/弱化文本 */
|
|
12163
|
+
dim: (s) => c.gray(s),
|
|
12164
|
+
/** 强调色 - 青色 */
|
|
12165
|
+
accent: (s) => c.cyan(s),
|
|
12166
|
+
/** 成功 - 绿色 */
|
|
12167
|
+
success: (s) => c.green(s),
|
|
12168
|
+
/** 错误 - 红色 */
|
|
12169
|
+
error: (s) => c.red(s),
|
|
12170
|
+
/** 警告 - 黄色 */
|
|
12171
|
+
warning: (s) => c.yellow(s),
|
|
12172
|
+
/** 边框色 - 灰色 */
|
|
12173
|
+
border: (s) => c.gray(s),
|
|
12174
|
+
/** Header 文本 */
|
|
12175
|
+
heading: (s) => c.bold(s),
|
|
12176
|
+
editorTheme: {
|
|
12177
|
+
borderColor: (text) => c.gray(text),
|
|
12178
|
+
selectList: baseSelectListTheme
|
|
12179
|
+
},
|
|
12180
|
+
selectListTheme: baseSelectListTheme
|
|
12181
|
+
};
|
|
12182
|
+
}
|
|
12183
|
+
|
|
12184
|
+
//#endregion
|
|
12185
|
+
//#region src/core/lsp/types.ts
|
|
12186
|
+
/** SymbolKind 名称映射 */
|
|
12187
|
+
const SYMBOL_KIND_NAMES = {
|
|
12188
|
+
[1]: "File",
|
|
12189
|
+
[2]: "Module",
|
|
12190
|
+
[3]: "Namespace",
|
|
12191
|
+
[4]: "Package",
|
|
12192
|
+
[5]: "Class",
|
|
12193
|
+
[6]: "Method",
|
|
12194
|
+
[7]: "Property",
|
|
12195
|
+
[8]: "Field",
|
|
12196
|
+
[9]: "Constructor",
|
|
12197
|
+
[10]: "Enum",
|
|
12198
|
+
[11]: "Interface",
|
|
12199
|
+
[12]: "Function",
|
|
12200
|
+
[13]: "Variable",
|
|
12201
|
+
[14]: "Constant",
|
|
12202
|
+
[15]: "String",
|
|
12203
|
+
[16]: "Number",
|
|
12204
|
+
[17]: "Boolean",
|
|
12205
|
+
[18]: "Array",
|
|
12206
|
+
[19]: "Object",
|
|
12207
|
+
[20]: "Key",
|
|
12208
|
+
[21]: "Null",
|
|
12209
|
+
[22]: "EnumMember",
|
|
12210
|
+
[23]: "Struct",
|
|
12211
|
+
[24]: "Event",
|
|
12212
|
+
[25]: "Operator",
|
|
12213
|
+
[26]: "TypeParameter"
|
|
12214
|
+
};
|
|
12215
|
+
/** LSP 操作错误 */
|
|
12216
|
+
var LspError = class extends Error {
|
|
12217
|
+
code;
|
|
12218
|
+
constructor(message, code) {
|
|
12219
|
+
super(message);
|
|
12220
|
+
this.name = "LspError";
|
|
12221
|
+
this.code = code;
|
|
12222
|
+
}
|
|
12223
|
+
};
|
|
12224
|
+
|
|
12225
|
+
//#endregion
|
|
12226
|
+
//#region src/cli/repl/tools/lsp-tool.ts
|
|
12227
|
+
/** 操作 → LSP 方法 */
|
|
12228
|
+
const METHOD_MAP = {
|
|
12229
|
+
goToDefinition: "textDocument/definition",
|
|
12230
|
+
findReferences: "textDocument/references",
|
|
12231
|
+
hover: "textDocument/hover",
|
|
12232
|
+
documentSymbol: "textDocument/documentSymbol",
|
|
12233
|
+
workspaceSymbol: "workspace/symbol",
|
|
12234
|
+
goToImplementation: "textDocument/implementation",
|
|
12235
|
+
prepareCallHierarchy: "textDocument/prepareCallHierarchy",
|
|
12236
|
+
incomingCalls: "callHierarchy/incomingCalls",
|
|
12237
|
+
outgoingCalls: "callHierarchy/outgoingCalls"
|
|
12238
|
+
};
|
|
12239
|
+
/**
|
|
12240
|
+
* 需要两步调用(先 prepareCallHierarchy)的操作
|
|
12241
|
+
*/
|
|
12242
|
+
const TWO_STEP_OPS = new Set(["incomingCalls", "outgoingCalls"]);
|
|
12243
|
+
/**
|
|
12244
|
+
* 需要位置参数的操作
|
|
12245
|
+
*/
|
|
12246
|
+
const POSITION_REQUIRED_OPS = new Set([
|
|
12247
|
+
"goToDefinition",
|
|
12248
|
+
"findReferences",
|
|
12249
|
+
"hover",
|
|
12250
|
+
"goToImplementation",
|
|
12251
|
+
"prepareCallHierarchy",
|
|
12252
|
+
"incomingCalls",
|
|
12253
|
+
"outgoingCalls"
|
|
12254
|
+
]);
|
|
12255
|
+
/**
|
|
12256
|
+
* 不需要 filePath 的操作
|
|
12257
|
+
*/
|
|
12258
|
+
const NO_FILEPATH_OPS = new Set(["workspaceSymbol"]);
|
|
12259
|
+
function formatLocation(loc) {
|
|
12260
|
+
if ("targetUri" in loc) return `${loc.targetUri.replace(/^file:\/\//, "")}:${loc.targetRange.start.line + 1}:${loc.targetRange.start.character + 1}`;
|
|
12261
|
+
return `${loc.uri.replace(/^file:\/\//, "")}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
|
|
12262
|
+
}
|
|
12263
|
+
function formatHoverContent(contents) {
|
|
12264
|
+
if (typeof contents === "string") return contents;
|
|
12265
|
+
if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : `\`\`\`${c.language}\n${c.value}\n\`\`\``).join("\n");
|
|
12266
|
+
if (typeof contents === "object" && "kind" in contents) return contents.value;
|
|
12267
|
+
return JSON.stringify(contents, null, 2);
|
|
12268
|
+
}
|
|
12269
|
+
function formatDocumentSymbol(symbols, indent = 0) {
|
|
12270
|
+
const prefix = " ".repeat(indent);
|
|
12271
|
+
const lines = [];
|
|
12272
|
+
for (const sym of symbols) {
|
|
12273
|
+
const kind = SYMBOL_KIND_NAMES[sym.kind] ?? `Kind(${sym.kind})`;
|
|
12274
|
+
const detail = sym.detail ? `: ${sym.detail}` : "";
|
|
12275
|
+
const line = sym.selectionRange.start.line + 1;
|
|
12276
|
+
const char = sym.selectionRange.start.character + 1;
|
|
12277
|
+
const deprecated = sym.deprecated ? " [deprecated]" : "";
|
|
12278
|
+
lines.push(`${prefix}${kind} ${sym.name}${detail} @ ${line}:${char}${deprecated}`);
|
|
12279
|
+
if (sym.children && sym.children.length > 0) lines.push(...formatDocumentSymbol(sym.children, indent + 1));
|
|
12280
|
+
}
|
|
12281
|
+
return lines.join("\n");
|
|
12282
|
+
}
|
|
12283
|
+
function formatWorkspaceSymbol(symbols) {
|
|
12284
|
+
const lines = [];
|
|
12285
|
+
for (const sym of symbols) {
|
|
12286
|
+
const kind = SYMBOL_KIND_NAMES[sym.kind] ?? `Kind(${sym.kind})`;
|
|
12287
|
+
const path = sym.location.uri.replace(/^file:\/\//, "");
|
|
12288
|
+
const line = sym.location.range.start.line + 1;
|
|
12289
|
+
const char = sym.location.range.start.character + 1;
|
|
12290
|
+
const container = sym.containerName ? ` (in ${sym.containerName})` : "";
|
|
12291
|
+
lines.push(`${kind} ${sym.name}${container} — ${path}:${line}:${char}`);
|
|
12292
|
+
}
|
|
12293
|
+
return lines.join("\n");
|
|
12294
|
+
}
|
|
12295
|
+
function formatCallHierarchyItem(item) {
|
|
12296
|
+
const kind = SYMBOL_KIND_NAMES[item.kind] ?? `Kind(${item.kind})`;
|
|
12297
|
+
const path = item.uri.replace(/^file:\/\//, "");
|
|
12298
|
+
const line = item.selectionRange.start.line + 1;
|
|
12299
|
+
const char = item.selectionRange.start.character + 1;
|
|
12300
|
+
const detail = item.detail ? `: ${item.detail}` : "";
|
|
12301
|
+
return `${kind} ${item.name}${detail} — ${path}:${line}:${char}`;
|
|
12302
|
+
}
|
|
12303
|
+
function createLspTool(lspManager) {
|
|
12304
|
+
return {
|
|
12305
|
+
id: "LSP",
|
|
12306
|
+
label: "LSP 代码智能",
|
|
12307
|
+
description: "使用 Language Server Protocol 进行代码智能操作。支持:goToDefinition(跳转到定义)、findReferences(查找引用)、hover(悬停信息)、documentSymbol(文档符号)、workspaceSymbol(工作区符号)、goToImplementation(跳转到实现)、prepareCallHierarchy(准备调用层次)、incomingCalls(传入调用)、outgoingCalls(传出调用)。所有操作都需要 filePath(绝对路径)、line(1-based)、character(1-based) 参数。LSP 服务器必须已配置才能使用此工具。",
|
|
12308
|
+
parameters: {
|
|
12309
|
+
type: "object",
|
|
12310
|
+
properties: {
|
|
12311
|
+
operation: {
|
|
12312
|
+
type: "string",
|
|
12313
|
+
enum: [
|
|
12314
|
+
"goToDefinition",
|
|
12315
|
+
"findReferences",
|
|
12316
|
+
"hover",
|
|
12317
|
+
"documentSymbol",
|
|
12318
|
+
"workspaceSymbol",
|
|
12319
|
+
"goToImplementation",
|
|
12320
|
+
"prepareCallHierarchy",
|
|
12321
|
+
"incomingCalls",
|
|
12322
|
+
"outgoingCalls"
|
|
12323
|
+
],
|
|
12324
|
+
description: "要执行的 LSP 操作"
|
|
12325
|
+
},
|
|
12326
|
+
filePath: {
|
|
12327
|
+
type: "string",
|
|
12328
|
+
description: "文件绝对路径或相对路径(workspaceSymbol 操作不需要)"
|
|
12329
|
+
},
|
|
12330
|
+
line: {
|
|
12331
|
+
type: "number",
|
|
12332
|
+
description: "行号(1-based,与编辑器一致。位置相关操作需要)"
|
|
12333
|
+
},
|
|
12334
|
+
character: {
|
|
12335
|
+
type: "number",
|
|
12336
|
+
description: "字符偏移(1-based,与编辑器一致。位置相关操作需要)"
|
|
12337
|
+
}
|
|
12338
|
+
},
|
|
12339
|
+
required: [
|
|
12340
|
+
"operation",
|
|
12341
|
+
"filePath",
|
|
12342
|
+
"line",
|
|
12343
|
+
"character"
|
|
12344
|
+
]
|
|
12345
|
+
},
|
|
12346
|
+
defaultRisk: "low",
|
|
12347
|
+
execute: async (_toolCallId, rawParams) => {
|
|
12348
|
+
const startTime = Date.now();
|
|
12349
|
+
const { operation, filePath, line, character } = rawParams;
|
|
12350
|
+
if (!NO_FILEPATH_OPS.has(operation) && !filePath) return {
|
|
12351
|
+
content: [{
|
|
12352
|
+
type: "text",
|
|
12353
|
+
text: "错误:需要 filePath 参数"
|
|
12354
|
+
}],
|
|
12355
|
+
details: {
|
|
12356
|
+
error: true,
|
|
12357
|
+
message: "filePath required"
|
|
12358
|
+
}
|
|
12359
|
+
};
|
|
12360
|
+
if (POSITION_REQUIRED_OPS.has(operation)) {
|
|
12361
|
+
if (line == null || character == null) return {
|
|
12362
|
+
content: [{
|
|
12363
|
+
type: "text",
|
|
12364
|
+
text: `错误:${operation} 需要 line 和 character 参数`
|
|
12365
|
+
}],
|
|
12366
|
+
details: {
|
|
12367
|
+
error: true,
|
|
12368
|
+
message: "position required"
|
|
12369
|
+
}
|
|
12370
|
+
};
|
|
12371
|
+
}
|
|
12372
|
+
try {
|
|
12373
|
+
const resolvedPath = NO_FILEPATH_OPS.has(operation) ? process.cwd() : resolveWorktreePath(filePath);
|
|
12374
|
+
const lspMethod = METHOD_MAP[operation];
|
|
12375
|
+
if (TWO_STEP_OPS.has(operation)) {
|
|
12376
|
+
const items = await lspManager.request(resolvedPath, "textDocument/prepareCallHierarchy", {
|
|
12377
|
+
textDocument: { uri: `file://${resolvedPath}` },
|
|
12378
|
+
position: {
|
|
12379
|
+
line: (line ?? 1) - 1,
|
|
12380
|
+
character: (character ?? 1) - 1
|
|
12381
|
+
}
|
|
12382
|
+
});
|
|
12383
|
+
const firstItem = items?.[0];
|
|
12384
|
+
if (!items || items.length === 0 || !firstItem) return {
|
|
9939
12385
|
content: [{
|
|
9940
12386
|
type: "text",
|
|
9941
|
-
text:
|
|
12387
|
+
text: "未找到调用层次项"
|
|
9942
12388
|
}],
|
|
9943
12389
|
details: {
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
limit,
|
|
9949
|
-
truncated
|
|
12390
|
+
operation,
|
|
12391
|
+
filePath: resolvedPath,
|
|
12392
|
+
resultCount: 0,
|
|
12393
|
+
elapsedMs: Date.now() - startTime
|
|
9950
12394
|
}
|
|
9951
12395
|
};
|
|
9952
|
-
|
|
12396
|
+
const results = await lspManager.request(resolvedPath, lspMethod, { item: firstItem });
|
|
12397
|
+
const elapsedMs = Date.now() - startTime;
|
|
12398
|
+
const callResults = results ?? [];
|
|
12399
|
+
if (operation === "incomingCalls") {
|
|
12400
|
+
const calls = callResults;
|
|
12401
|
+
const formatted = calls.map((c) => `← ${formatCallHierarchyItem(c.from)} (${c.fromRanges.length} 处调用)`).join("\n");
|
|
12402
|
+
return {
|
|
12403
|
+
content: [{
|
|
12404
|
+
type: "text",
|
|
12405
|
+
text: `传入调用 — ${formatCallHierarchyItem(firstItem)}\n\n${formatted || "无传入调用"}`
|
|
12406
|
+
}],
|
|
12407
|
+
details: {
|
|
12408
|
+
operation,
|
|
12409
|
+
filePath: resolvedPath,
|
|
12410
|
+
resultCount: calls.length,
|
|
12411
|
+
elapsedMs
|
|
12412
|
+
}
|
|
12413
|
+
};
|
|
12414
|
+
}
|
|
12415
|
+
const calls = callResults;
|
|
12416
|
+
const formatted = calls.map((c) => `→ ${formatCallHierarchyItem(c.to)} (${c.fromRanges.length} 处调用)`).join("\n");
|
|
9953
12417
|
return {
|
|
9954
12418
|
content: [{
|
|
9955
12419
|
type: "text",
|
|
9956
|
-
text:
|
|
12420
|
+
text: `传出调用 — ${formatCallHierarchyItem(firstItem)}\n\n${formatted || "无传出调用"}`
|
|
9957
12421
|
}],
|
|
9958
12422
|
details: {
|
|
9959
|
-
|
|
9960
|
-
|
|
12423
|
+
operation,
|
|
12424
|
+
filePath: resolvedPath,
|
|
12425
|
+
resultCount: calls.length,
|
|
12426
|
+
elapsedMs
|
|
12427
|
+
}
|
|
12428
|
+
};
|
|
12429
|
+
}
|
|
12430
|
+
const methodParams = {};
|
|
12431
|
+
if (operation === "workspaceSymbol") methodParams.query = rawParams.query ?? "";
|
|
12432
|
+
else if (operation === "documentSymbol") methodParams.textDocument = { uri: `file://${resolvedPath}` };
|
|
12433
|
+
else if (operation === "findReferences") {
|
|
12434
|
+
methodParams.textDocument = { uri: `file://${resolvedPath}` };
|
|
12435
|
+
methodParams.position = {
|
|
12436
|
+
line: (line ?? 1) - 1,
|
|
12437
|
+
character: (character ?? 1) - 1
|
|
12438
|
+
};
|
|
12439
|
+
methodParams.context = { includeDeclaration: true };
|
|
12440
|
+
} else {
|
|
12441
|
+
methodParams.textDocument = { uri: `file://${resolvedPath}` };
|
|
12442
|
+
methodParams.position = {
|
|
12443
|
+
line: (line ?? 1) - 1,
|
|
12444
|
+
character: (character ?? 1) - 1
|
|
12445
|
+
};
|
|
12446
|
+
}
|
|
12447
|
+
const result = await lspManager.request(resolvedPath, lspMethod, methodParams);
|
|
12448
|
+
const elapsedMs = Date.now() - startTime;
|
|
12449
|
+
switch (operation) {
|
|
12450
|
+
case "goToDefinition":
|
|
12451
|
+
case "goToImplementation": {
|
|
12452
|
+
const locations = result ?? [];
|
|
12453
|
+
const formatted = locations.map(formatLocation).join("\n");
|
|
12454
|
+
return {
|
|
12455
|
+
content: [{
|
|
12456
|
+
type: "text",
|
|
12457
|
+
text: `${operation === "goToDefinition" ? "定义" : "实现"} (${locations.length} 处)\n\n${formatted || "无结果"}`
|
|
12458
|
+
}],
|
|
12459
|
+
details: {
|
|
12460
|
+
operation,
|
|
12461
|
+
filePath: resolvedPath,
|
|
12462
|
+
resultCount: locations.length,
|
|
12463
|
+
elapsedMs
|
|
12464
|
+
}
|
|
12465
|
+
};
|
|
12466
|
+
}
|
|
12467
|
+
case "findReferences": {
|
|
12468
|
+
const refs = result ?? [];
|
|
12469
|
+
const formatted = refs.map(formatLocation).join("\n");
|
|
12470
|
+
return {
|
|
12471
|
+
content: [{
|
|
12472
|
+
type: "text",
|
|
12473
|
+
text: `引用 (${refs.length} 处)\n\n${formatted || "无引用"}`
|
|
12474
|
+
}],
|
|
12475
|
+
details: {
|
|
12476
|
+
operation,
|
|
12477
|
+
filePath: resolvedPath,
|
|
12478
|
+
resultCount: refs.length,
|
|
12479
|
+
elapsedMs
|
|
12480
|
+
}
|
|
12481
|
+
};
|
|
12482
|
+
}
|
|
12483
|
+
case "hover": {
|
|
12484
|
+
const hover = result;
|
|
12485
|
+
if (!hover?.contents) return {
|
|
12486
|
+
content: [{
|
|
12487
|
+
type: "text",
|
|
12488
|
+
text: "无悬停信息"
|
|
12489
|
+
}],
|
|
12490
|
+
details: {
|
|
12491
|
+
operation,
|
|
12492
|
+
filePath: resolvedPath,
|
|
12493
|
+
resultCount: 0,
|
|
12494
|
+
elapsedMs
|
|
12495
|
+
}
|
|
12496
|
+
};
|
|
12497
|
+
return {
|
|
12498
|
+
content: [{
|
|
12499
|
+
type: "text",
|
|
12500
|
+
text: formatHoverContent(hover.contents)
|
|
12501
|
+
}],
|
|
12502
|
+
details: {
|
|
12503
|
+
operation,
|
|
12504
|
+
filePath: resolvedPath,
|
|
12505
|
+
resultCount: 1,
|
|
12506
|
+
elapsedMs
|
|
12507
|
+
}
|
|
12508
|
+
};
|
|
12509
|
+
}
|
|
12510
|
+
case "documentSymbol": {
|
|
12511
|
+
const symbols = result ?? [];
|
|
12512
|
+
if (symbols.length === 0) return {
|
|
12513
|
+
content: [{
|
|
12514
|
+
type: "text",
|
|
12515
|
+
text: "无文档符号"
|
|
12516
|
+
}],
|
|
12517
|
+
details: {
|
|
12518
|
+
operation,
|
|
12519
|
+
filePath: resolvedPath,
|
|
12520
|
+
resultCount: 0,
|
|
12521
|
+
elapsedMs
|
|
12522
|
+
}
|
|
12523
|
+
};
|
|
12524
|
+
const formatted = formatDocumentSymbol(symbols);
|
|
12525
|
+
return {
|
|
12526
|
+
content: [{
|
|
12527
|
+
type: "text",
|
|
12528
|
+
text: `文档符号 (${symbols.length} 个顶层)\n\n${formatted}`
|
|
12529
|
+
}],
|
|
12530
|
+
details: {
|
|
12531
|
+
operation,
|
|
12532
|
+
filePath: resolvedPath,
|
|
12533
|
+
resultCount: symbols.length,
|
|
12534
|
+
elapsedMs
|
|
12535
|
+
}
|
|
12536
|
+
};
|
|
12537
|
+
}
|
|
12538
|
+
case "workspaceSymbol": {
|
|
12539
|
+
const symbols = result ?? [];
|
|
12540
|
+
if (symbols.length === 0) return {
|
|
12541
|
+
content: [{
|
|
12542
|
+
type: "text",
|
|
12543
|
+
text: "未找到工作区符号"
|
|
12544
|
+
}],
|
|
12545
|
+
details: {
|
|
12546
|
+
operation,
|
|
12547
|
+
resultCount: 0,
|
|
12548
|
+
elapsedMs
|
|
12549
|
+
}
|
|
12550
|
+
};
|
|
12551
|
+
const formatted = formatWorkspaceSymbol(symbols);
|
|
12552
|
+
return {
|
|
12553
|
+
content: [{
|
|
12554
|
+
type: "text",
|
|
12555
|
+
text: `工作区符号 (${symbols.length} 个)\n\n${formatted}`
|
|
12556
|
+
}],
|
|
12557
|
+
details: {
|
|
12558
|
+
operation,
|
|
12559
|
+
resultCount: symbols.length,
|
|
12560
|
+
elapsedMs
|
|
12561
|
+
}
|
|
12562
|
+
};
|
|
12563
|
+
}
|
|
12564
|
+
case "prepareCallHierarchy": {
|
|
12565
|
+
const items = result ?? [];
|
|
12566
|
+
if (items.length === 0) return {
|
|
12567
|
+
content: [{
|
|
12568
|
+
type: "text",
|
|
12569
|
+
text: "未找到调用层次项"
|
|
12570
|
+
}],
|
|
12571
|
+
details: {
|
|
12572
|
+
operation,
|
|
12573
|
+
filePath: resolvedPath,
|
|
12574
|
+
resultCount: 0,
|
|
12575
|
+
elapsedMs
|
|
12576
|
+
}
|
|
12577
|
+
};
|
|
12578
|
+
const formatted = items.map(formatCallHierarchyItem).join("\n");
|
|
12579
|
+
return {
|
|
12580
|
+
content: [{
|
|
12581
|
+
type: "text",
|
|
12582
|
+
text: `调用层次 (${items.length} 项)\n\n${formatted}`
|
|
12583
|
+
}],
|
|
12584
|
+
details: {
|
|
12585
|
+
operation,
|
|
12586
|
+
filePath: resolvedPath,
|
|
12587
|
+
resultCount: items.length,
|
|
12588
|
+
elapsedMs
|
|
12589
|
+
}
|
|
12590
|
+
};
|
|
12591
|
+
}
|
|
12592
|
+
default: return {
|
|
12593
|
+
content: [{
|
|
12594
|
+
type: "text",
|
|
12595
|
+
text: JSON.stringify(result, null, 2)
|
|
12596
|
+
}],
|
|
12597
|
+
details: {
|
|
12598
|
+
operation,
|
|
12599
|
+
filePath: resolvedPath,
|
|
12600
|
+
elapsedMs
|
|
9961
12601
|
}
|
|
9962
12602
|
};
|
|
9963
12603
|
}
|
|
12604
|
+
} catch (err) {
|
|
12605
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
12606
|
+
return {
|
|
12607
|
+
content: [{
|
|
12608
|
+
type: "text",
|
|
12609
|
+
text: `LSP 操作失败: ${errorMsg}`
|
|
12610
|
+
}],
|
|
12611
|
+
details: {
|
|
12612
|
+
error: true,
|
|
12613
|
+
operation,
|
|
12614
|
+
filePath,
|
|
12615
|
+
message: errorMsg
|
|
12616
|
+
}
|
|
12617
|
+
};
|
|
9964
12618
|
}
|
|
9965
12619
|
}
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
12620
|
+
};
|
|
12621
|
+
}
|
|
12622
|
+
|
|
12623
|
+
//#endregion
|
|
12624
|
+
//#region src/config/types.ts
|
|
12625
|
+
/**
|
|
12626
|
+
* 将用户配置的 MCP 格式标准化为 McpServerConfig 数组
|
|
12627
|
+
*
|
|
12628
|
+
* 支持两种输入格式自动检测:
|
|
12629
|
+
* - `{ servers: [...] }` → 直接返回数组
|
|
12630
|
+
* - `{ "server-a": {...}, "server-b": {...} }` → 以 key 作为 name 转换为数组
|
|
12631
|
+
*/
|
|
12632
|
+
function normalizeMcpConfig(raw) {
|
|
12633
|
+
if (raw.servers && Array.isArray(raw.servers) && raw.servers.length > 0) return raw.servers;
|
|
12634
|
+
const servers = [];
|
|
12635
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
12636
|
+
if (key === "servers") continue;
|
|
12637
|
+
if (value === null || value === void 0 || typeof value !== "object") continue;
|
|
12638
|
+
if (Array.isArray(value)) continue;
|
|
12639
|
+
const config = value;
|
|
12640
|
+
if (typeof config.command !== "string") continue;
|
|
12641
|
+
const server = {
|
|
12642
|
+
name: key,
|
|
12643
|
+
transport: "stdio",
|
|
12644
|
+
command: config.command
|
|
12645
|
+
};
|
|
12646
|
+
if (Array.isArray(config.args)) server.args = config.args;
|
|
12647
|
+
if (config.env && typeof config.env === "object") server.env = config.env;
|
|
12648
|
+
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
12649
|
+
if (typeof config.enabled === "boolean") server.enabled = config.enabled;
|
|
12650
|
+
if (typeof config.connectTimeoutMs === "number") server.connectTimeoutMs = config.connectTimeoutMs;
|
|
12651
|
+
servers.push(server);
|
|
9976
12652
|
}
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
12653
|
+
return servers;
|
|
12654
|
+
}
|
|
12655
|
+
|
|
12656
|
+
//#endregion
|
|
12657
|
+
//#region src/core/lsp/diagnostics.ts
|
|
12658
|
+
function createDiagnosticCollector() {
|
|
12659
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
12660
|
+
function init(_manager) {}
|
|
12661
|
+
function getForFile(filePath) {
|
|
12662
|
+
const diags = diagnosticsByFile.get(filePath);
|
|
12663
|
+
if (!diags) return [];
|
|
12664
|
+
return [...diags];
|
|
12665
|
+
}
|
|
12666
|
+
function clearForFile(filePath) {
|
|
12667
|
+
diagnosticsByFile.delete(filePath);
|
|
12668
|
+
}
|
|
12669
|
+
function clear() {
|
|
12670
|
+
diagnosticsByFile.clear();
|
|
12671
|
+
}
|
|
12672
|
+
return {
|
|
12673
|
+
init,
|
|
12674
|
+
getForFile,
|
|
12675
|
+
clearForFile,
|
|
12676
|
+
clear
|
|
12677
|
+
};
|
|
12678
|
+
}
|
|
12679
|
+
|
|
12680
|
+
//#endregion
|
|
12681
|
+
//#region src/core/lsp/lsp-config.ts
|
|
12682
|
+
/** 内置 TypeScript/JavaScript LSP 服务器配置 */
|
|
12683
|
+
const BUILTIN_LSP_SERVERS = [{
|
|
12684
|
+
name: "typescript",
|
|
12685
|
+
command: "typescript-language-server",
|
|
12686
|
+
args: ["--stdio"],
|
|
12687
|
+
languageIds: [
|
|
12688
|
+
"typescript",
|
|
12689
|
+
"javascript",
|
|
12690
|
+
"typescriptreact",
|
|
12691
|
+
"javascriptreact"
|
|
12692
|
+
],
|
|
12693
|
+
extensions: [
|
|
12694
|
+
".ts",
|
|
12695
|
+
".tsx",
|
|
12696
|
+
".js",
|
|
12697
|
+
".jsx",
|
|
12698
|
+
".mjs",
|
|
12699
|
+
".cjs",
|
|
12700
|
+
".mts",
|
|
12701
|
+
".cts"
|
|
12702
|
+
],
|
|
12703
|
+
connectTimeoutMs: 3e4,
|
|
12704
|
+
requestTimeoutMs: 15e3
|
|
12705
|
+
}];
|
|
12706
|
+
/**
|
|
12707
|
+
* 解析并合并 LSP 配置
|
|
12708
|
+
*
|
|
12709
|
+
* 规则:
|
|
12710
|
+
* 1. 加载内置 server 配置
|
|
12711
|
+
* 2. 用户 servers 中同名项覆盖内置,不同名追加
|
|
12712
|
+
* 3. enabled=false 的 server 移除
|
|
12713
|
+
* 4. userConfig.enabled=false 全局禁用,返回空数组
|
|
12714
|
+
*
|
|
12715
|
+
* @param userConfig - 用户配置(可选)
|
|
12716
|
+
* @returns 生效的 server 配置列表
|
|
12717
|
+
*/
|
|
12718
|
+
function resolveLspConfig(userConfig) {
|
|
12719
|
+
if (userConfig?.enabled === false) return [];
|
|
12720
|
+
const serverMap = /* @__PURE__ */ new Map();
|
|
12721
|
+
for (const builtin of BUILTIN_LSP_SERVERS) serverMap.set(builtin.name, { ...builtin });
|
|
12722
|
+
if (userConfig?.servers) for (const userServer of userConfig.servers) {
|
|
12723
|
+
if (userServer.enabled === false) {
|
|
12724
|
+
serverMap.delete(userServer.name);
|
|
12725
|
+
continue;
|
|
9985
12726
|
}
|
|
9986
|
-
const
|
|
9987
|
-
if (
|
|
12727
|
+
const existing = serverMap.get(userServer.name);
|
|
12728
|
+
if (existing) serverMap.set(userServer.name, {
|
|
12729
|
+
...existing,
|
|
12730
|
+
...userServer
|
|
12731
|
+
});
|
|
12732
|
+
else serverMap.set(userServer.name, { ...userServer });
|
|
9988
12733
|
}
|
|
9989
|
-
|
|
9990
|
-
for (const tool of tools) if (!tool.defaultRisk) tool.defaultRisk = TOOL_RISK_MAP[tool.id] ?? "medium";
|
|
9991
|
-
return tools;
|
|
12734
|
+
return Array.from(serverMap.values()).filter((s) => s.enabled !== false);
|
|
9992
12735
|
}
|
|
9993
12736
|
|
|
9994
12737
|
//#endregion
|
|
9995
|
-
//#region src/
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
12738
|
+
//#region src/core/lsp/json-rpc.ts
|
|
12739
|
+
const HEADER_CONTENT_LENGTH = "Content-Length";
|
|
12740
|
+
const HEADER_CONTENT_TYPE = "Content-Type";
|
|
12741
|
+
const DEFAULT_CONTENT_TYPE = "application/vscode-jsonrpc; charset=utf-8";
|
|
12742
|
+
const CRLF = "\r\n";
|
|
12743
|
+
const DOUBLE_CRLF = "\r\n\r\n";
|
|
12744
|
+
/**
|
|
12745
|
+
* 从 Readable 流(子进程 stdout)读取 JSON-RPC 消息。
|
|
12746
|
+
*
|
|
12747
|
+
* 处理 TCP 流特性:
|
|
12748
|
+
* - 消息可能分多个 chunk 到达
|
|
12749
|
+
* - 一个 chunk 可能包含多个消息
|
|
12750
|
+
* - Content-Length 头可能跨 chunk 边界
|
|
12751
|
+
*/
|
|
12752
|
+
function createMessageReader(onMessage, onError, _onClose, trace) {
|
|
12753
|
+
let buffer = "";
|
|
12754
|
+
let contentLength = -1;
|
|
12755
|
+
let headerEnd = -1;
|
|
12756
|
+
function parseHeaders(rawHeaders) {
|
|
12757
|
+
const headers = {};
|
|
12758
|
+
for (const line of rawHeaders.split(CRLF)) {
|
|
12759
|
+
const colon = line.indexOf(":");
|
|
12760
|
+
if (colon > 0) {
|
|
12761
|
+
const key = line.slice(0, colon).trim();
|
|
12762
|
+
headers[key] = line.slice(colon + 1).trim();
|
|
12763
|
+
}
|
|
12764
|
+
}
|
|
12765
|
+
return headers;
|
|
12766
|
+
}
|
|
12767
|
+
function processBuffer() {
|
|
12768
|
+
while (buffer.length > 0) {
|
|
12769
|
+
if (contentLength < 0) {
|
|
12770
|
+
headerEnd = buffer.indexOf(DOUBLE_CRLF);
|
|
12771
|
+
if (headerEnd < 0) return;
|
|
12772
|
+
const lengthStr = parseHeaders(buffer.slice(0, headerEnd))[HEADER_CONTENT_LENGTH];
|
|
12773
|
+
if (!lengthStr) {
|
|
12774
|
+
onError(/* @__PURE__ */ new Error(`Missing ${HEADER_CONTENT_LENGTH} header in LSP message`));
|
|
12775
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
12776
|
+
continue;
|
|
12777
|
+
}
|
|
12778
|
+
contentLength = parseInt(lengthStr, 10);
|
|
12779
|
+
if (Number.isNaN(contentLength) || contentLength < 0) {
|
|
12780
|
+
onError(/* @__PURE__ */ new Error(`Invalid ${HEADER_CONTENT_LENGTH}: ${lengthStr}`));
|
|
12781
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
12782
|
+
contentLength = -1;
|
|
12783
|
+
continue;
|
|
12784
|
+
}
|
|
12785
|
+
}
|
|
12786
|
+
const bodyStart = headerEnd + 4;
|
|
12787
|
+
const bodyEnd = bodyStart + contentLength;
|
|
12788
|
+
if (buffer.length < bodyEnd) return;
|
|
12789
|
+
const body = buffer.slice(bodyStart, bodyEnd);
|
|
12790
|
+
buffer = buffer.slice(bodyEnd);
|
|
12791
|
+
contentLength = -1;
|
|
12792
|
+
headerEnd = -1;
|
|
12793
|
+
if (body.length === 0) continue;
|
|
12794
|
+
try {
|
|
12795
|
+
trace?.(`LSP ← ${body.slice(0, 500)}${body.length > 500 ? "..." : ""}`);
|
|
12796
|
+
onMessage(JSON.parse(body));
|
|
12797
|
+
} catch (err) {
|
|
12798
|
+
onError(/* @__PURE__ */ new Error(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`));
|
|
12799
|
+
}
|
|
12800
|
+
}
|
|
12801
|
+
}
|
|
12802
|
+
return {
|
|
12803
|
+
feed(chunk) {
|
|
12804
|
+
buffer += chunk.toString("utf-8");
|
|
12805
|
+
processBuffer();
|
|
12806
|
+
},
|
|
12807
|
+
reset() {
|
|
12808
|
+
buffer = "";
|
|
12809
|
+
contentLength = -1;
|
|
12810
|
+
headerEnd = -1;
|
|
12811
|
+
}
|
|
12812
|
+
};
|
|
9999
12813
|
}
|
|
10000
12814
|
/**
|
|
10001
|
-
*
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
12815
|
+
* 向 Writable 流(子进程 stdin)写入 JSON-RPC 消息。
|
|
12816
|
+
* 自动添加 Content-Length 和 Content-Type 头。
|
|
12817
|
+
*/
|
|
12818
|
+
function createMessageWriter(writable, trace) {
|
|
12819
|
+
const encoder = new TextEncoder();
|
|
12820
|
+
async function write(message) {
|
|
12821
|
+
const body = JSON.stringify(message);
|
|
12822
|
+
const bodyBytes = encoder.encode(body);
|
|
12823
|
+
const header = `${HEADER_CONTENT_LENGTH}: ${bodyBytes.length}${CRLF}${HEADER_CONTENT_TYPE}: ${DEFAULT_CONTENT_TYPE}${CRLF}${CRLF}`;
|
|
12824
|
+
const headerBytes = encoder.encode(header);
|
|
12825
|
+
const fullMessage = Buffer.concat([headerBytes, bodyBytes]);
|
|
12826
|
+
trace?.(`LSP → ${body.slice(0, 500)}${body.length > 500 ? "..." : ""}`);
|
|
12827
|
+
return new Promise((resolve, reject) => {
|
|
12828
|
+
if (!writable.writable) {
|
|
12829
|
+
reject(/* @__PURE__ */ new Error("Stream is not writable"));
|
|
12830
|
+
return;
|
|
12831
|
+
}
|
|
12832
|
+
writable.write(fullMessage, (err) => {
|
|
12833
|
+
if (err) reject(err);
|
|
12834
|
+
else resolve();
|
|
12835
|
+
});
|
|
12836
|
+
});
|
|
12837
|
+
}
|
|
12838
|
+
return { write };
|
|
12839
|
+
}
|
|
12840
|
+
|
|
12841
|
+
//#endregion
|
|
12842
|
+
//#region src/core/lsp/lsp-client.ts
|
|
12843
|
+
/**
|
|
12844
|
+
* LSP 客户端 — Layer 1
|
|
12845
|
+
*
|
|
12846
|
+
* 启动 LSP 语言服务器子进程,通过 stdio 进行 JSON-RPC 2.0 通信。
|
|
12847
|
+
*
|
|
12848
|
+
* @module core/lsp
|
|
12849
|
+
*/
|
|
12850
|
+
function createLspClient(config) {
|
|
12851
|
+
const requestTimeoutMs = config.requestTimeoutMs ?? 3e4;
|
|
12852
|
+
let childProcess = null;
|
|
12853
|
+
let nextId = 1;
|
|
12854
|
+
let isStopping = false;
|
|
12855
|
+
let capabilities = null;
|
|
12856
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
12857
|
+
const notificationHandlers = /* @__PURE__ */ new Map();
|
|
12858
|
+
let messageWriter = null;
|
|
12859
|
+
function trace(dir, msg) {
|
|
12860
|
+
if (process.env.ZAPMYCO_LSP_TRACE) process.stderr.write(`[lsp-client] ${dir} ${msg}\n`);
|
|
12861
|
+
}
|
|
12862
|
+
function rejectAllPending(error) {
|
|
12863
|
+
for (const [, pending] of pendingRequests) {
|
|
12864
|
+
clearTimeout(pending.timer);
|
|
12865
|
+
pending.reject(error);
|
|
12866
|
+
}
|
|
12867
|
+
pendingRequests.clear();
|
|
12868
|
+
}
|
|
12869
|
+
function handleMessage(message) {
|
|
12870
|
+
if ("id" in message && !("method" in message)) {
|
|
12871
|
+
const response = message;
|
|
12872
|
+
const pending = pendingRequests.get(response.id);
|
|
12873
|
+
if (pending) {
|
|
12874
|
+
clearTimeout(pending.timer);
|
|
12875
|
+
pendingRequests.delete(response.id);
|
|
12876
|
+
if (response.error) pending.reject(new LspError(`LSP error ${response.error.code}: ${response.error.message}`, `LSP_ERROR_${response.error.code}`));
|
|
12877
|
+
else pending.resolve(response.result);
|
|
12878
|
+
}
|
|
12879
|
+
return;
|
|
12880
|
+
}
|
|
12881
|
+
if ("method" in message && !("id" in message)) {
|
|
12882
|
+
const notification = message;
|
|
12883
|
+
const handlers = notificationHandlers.get(notification.method);
|
|
12884
|
+
if (handlers) for (const handler of handlers) try {
|
|
12885
|
+
handler(notification.params);
|
|
12886
|
+
} catch {}
|
|
12887
|
+
}
|
|
12888
|
+
}
|
|
12889
|
+
function spawnProcess() {
|
|
12890
|
+
const child = spawn(config.command, config.args ?? [], {
|
|
12891
|
+
env: {
|
|
12892
|
+
...process.env,
|
|
12893
|
+
...config.env
|
|
12894
|
+
},
|
|
12895
|
+
stdio: [
|
|
12896
|
+
"pipe",
|
|
12897
|
+
"pipe",
|
|
12898
|
+
"pipe"
|
|
12899
|
+
]
|
|
12900
|
+
});
|
|
12901
|
+
child.on("error", (err) => {
|
|
12902
|
+
rejectAllPending(new LspError(`LSP process error: ${err.message}`, "PROCESS_ERROR"));
|
|
12903
|
+
});
|
|
12904
|
+
child.on("exit", (code, signal) => {
|
|
12905
|
+
if (!isStopping) rejectAllPending(new LspError(`LSP process exited unexpectedly (${signal ? `signal ${signal}` : `exit code ${code}`})`, "PROCESS_EXIT"));
|
|
12906
|
+
});
|
|
12907
|
+
if (child.stderr) child.stderr.on("data", (chunk) => {
|
|
12908
|
+
if (process.env.ZAPMYCO_LSP_TRACE) process.stderr.write(`[lsp-stderr] ${chunk.toString("utf-8")}`);
|
|
12909
|
+
});
|
|
12910
|
+
return child;
|
|
12911
|
+
}
|
|
12912
|
+
function ensureStarted() {
|
|
12913
|
+
if (childProcess) return;
|
|
12914
|
+
childProcess = spawnProcess();
|
|
12915
|
+
const reader = createMessageReader(handleMessage, (err) => trace("←", `Reader error: ${err.message}`), () => {
|
|
12916
|
+
if (!isStopping) rejectAllPending(new LspError("LSP stdout closed", "STDOUT_CLOSED"));
|
|
12917
|
+
}, (msg) => trace("←", msg));
|
|
12918
|
+
childProcess.stdout.on("data", (chunk) => {
|
|
12919
|
+
reader.feed(chunk);
|
|
12920
|
+
});
|
|
12921
|
+
childProcess.stdout.on("end", () => {
|
|
12922
|
+
if (!isStopping) rejectAllPending(new LspError("LSP stdout closed unexpectedly", "STDOUT_CLOSED"));
|
|
12923
|
+
});
|
|
12924
|
+
messageWriter = createMessageWriter(childProcess.stdin, (msg) => trace("→", msg));
|
|
12925
|
+
}
|
|
12926
|
+
async function initialize(rootUri, initializationOptions) {
|
|
12927
|
+
ensureStarted();
|
|
12928
|
+
const result = await sendRequest("initialize", {
|
|
12929
|
+
processId: process.pid,
|
|
12930
|
+
rootUri,
|
|
12931
|
+
rootPath: rootUri.replace(/^file:\/\//, ""),
|
|
12932
|
+
workspaceFolders: [{
|
|
12933
|
+
uri: rootUri,
|
|
12934
|
+
name: "workspace"
|
|
12935
|
+
}],
|
|
12936
|
+
capabilities: {
|
|
12937
|
+
textDocument: {
|
|
12938
|
+
synchronization: { didSave: true },
|
|
12939
|
+
definition: { linkSupport: true },
|
|
12940
|
+
references: {},
|
|
12941
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
12942
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
12943
|
+
implementation: { linkSupport: true },
|
|
12944
|
+
callHierarchy: {}
|
|
12945
|
+
},
|
|
12946
|
+
workspace: { symbol: {} }
|
|
12947
|
+
},
|
|
12948
|
+
initializationOptions
|
|
12949
|
+
});
|
|
12950
|
+
capabilities = result.capabilities;
|
|
12951
|
+
await sendNotification("initialized", {});
|
|
12952
|
+
return result;
|
|
12953
|
+
}
|
|
12954
|
+
async function sendRequest(method, params) {
|
|
12955
|
+
if (!childProcess || !messageWriter) throw new LspError("LSP client not started", "NOT_STARTED");
|
|
12956
|
+
if (isStopping) throw new LspError("LSP client is shutting down", "SHUTTING_DOWN");
|
|
12957
|
+
const id = nextId++;
|
|
12958
|
+
const request = {
|
|
12959
|
+
jsonrpc: "2.0",
|
|
12960
|
+
id,
|
|
12961
|
+
method,
|
|
12962
|
+
params
|
|
12963
|
+
};
|
|
12964
|
+
return new Promise((resolve, reject) => {
|
|
12965
|
+
const timer = setTimeout(() => {
|
|
12966
|
+
pendingRequests.delete(id);
|
|
12967
|
+
reject(new LspError(`LSP request timeout: ${method}`, "REQUEST_TIMEOUT"));
|
|
12968
|
+
}, requestTimeoutMs);
|
|
12969
|
+
pendingRequests.set(id, {
|
|
12970
|
+
resolve,
|
|
12971
|
+
reject,
|
|
12972
|
+
timer,
|
|
12973
|
+
method
|
|
12974
|
+
});
|
|
12975
|
+
messageWriter.write(request).catch((err) => {
|
|
12976
|
+
clearTimeout(timer);
|
|
12977
|
+
pendingRequests.delete(id);
|
|
12978
|
+
reject(err);
|
|
12979
|
+
});
|
|
12980
|
+
});
|
|
12981
|
+
}
|
|
12982
|
+
async function sendNotification(method, params) {
|
|
12983
|
+
if (!childProcess || !messageWriter) throw new LspError("LSP client not started", "NOT_STARTED");
|
|
12984
|
+
if (isStopping) return;
|
|
12985
|
+
const notification = {
|
|
12986
|
+
jsonrpc: "2.0",
|
|
12987
|
+
method,
|
|
12988
|
+
params
|
|
12989
|
+
};
|
|
12990
|
+
await messageWriter.write(notification);
|
|
12991
|
+
}
|
|
12992
|
+
function onNotification(method, handler) {
|
|
12993
|
+
let handlers = notificationHandlers.get(method);
|
|
12994
|
+
if (!handlers) {
|
|
12995
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12996
|
+
notificationHandlers.set(method, handlers);
|
|
12997
|
+
}
|
|
12998
|
+
handlers.add(handler);
|
|
12999
|
+
}
|
|
13000
|
+
function offNotification(method, handler) {
|
|
13001
|
+
const handlers = notificationHandlers.get(method);
|
|
13002
|
+
if (handlers) {
|
|
13003
|
+
handlers.delete(handler);
|
|
13004
|
+
if (handlers.size === 0) notificationHandlers.delete(method);
|
|
13005
|
+
}
|
|
13006
|
+
}
|
|
13007
|
+
function getCapabilities() {
|
|
13008
|
+
return capabilities;
|
|
13009
|
+
}
|
|
13010
|
+
function isAlive() {
|
|
13011
|
+
return childProcess !== null && !childProcess.killed && childProcess.exitCode === null;
|
|
13012
|
+
}
|
|
13013
|
+
async function shutdown() {
|
|
13014
|
+
if (!childProcess || isStopping) return;
|
|
13015
|
+
isStopping = true;
|
|
13016
|
+
try {
|
|
13017
|
+
await sendRequest("shutdown");
|
|
13018
|
+
} catch {}
|
|
13019
|
+
try {
|
|
13020
|
+
await sendNotification("exit");
|
|
13021
|
+
} catch {}
|
|
13022
|
+
await new Promise((resolve) => {
|
|
13023
|
+
const timer = setTimeout(() => {
|
|
13024
|
+
if (childProcess && !childProcess.killed) childProcess.kill("SIGKILL");
|
|
13025
|
+
resolve();
|
|
13026
|
+
}, 2e3);
|
|
13027
|
+
childProcess.once("exit", () => {
|
|
13028
|
+
clearTimeout(timer);
|
|
13029
|
+
resolve();
|
|
13030
|
+
});
|
|
13031
|
+
});
|
|
13032
|
+
childProcess = null;
|
|
13033
|
+
capabilities = null;
|
|
13034
|
+
messageWriter = null;
|
|
13035
|
+
pendingRequests.clear();
|
|
13036
|
+
notificationHandlers.clear();
|
|
13037
|
+
}
|
|
13038
|
+
return {
|
|
13039
|
+
sendRequest,
|
|
13040
|
+
sendNotification,
|
|
13041
|
+
onNotification,
|
|
13042
|
+
offNotification,
|
|
13043
|
+
initialize,
|
|
13044
|
+
getCapabilities,
|
|
13045
|
+
isAlive,
|
|
13046
|
+
shutdown
|
|
10012
13047
|
};
|
|
13048
|
+
}
|
|
13049
|
+
|
|
13050
|
+
//#endregion
|
|
13051
|
+
//#region src/core/lsp/lsp-server-instance.ts
|
|
13052
|
+
/**
|
|
13053
|
+
* LSP 服务器实例 — Layer 2
|
|
13054
|
+
*
|
|
13055
|
+
* 封装单个 LSP 服务器的完整生命周期:
|
|
13056
|
+
* - 状态机(stopped → starting → running → stopping → stopped + error)
|
|
13057
|
+
* - initialize 握手(capabilities 提取)
|
|
13058
|
+
* - 文档同步跟踪(didOpen/didChange/didClose)
|
|
13059
|
+
* - 重试逻辑(指数退避,最大 maxRetries 次)
|
|
13060
|
+
* - 能力查询
|
|
13061
|
+
*
|
|
13062
|
+
* @module core/lsp
|
|
13063
|
+
*/
|
|
13064
|
+
function createLspServerInstance(config) {
|
|
13065
|
+
const { serverId, languageIds, extensions, initializationOptions } = config;
|
|
13066
|
+
const maxRetries = config.maxRetries ?? 3;
|
|
13067
|
+
const retryBaseDelayMs = config.retryBaseDelayMs ?? 1e3;
|
|
13068
|
+
let client = null;
|
|
13069
|
+
let capabilities = null;
|
|
13070
|
+
let state = "stopped";
|
|
13071
|
+
let retryCount = 0;
|
|
13072
|
+
let requestCount = 0;
|
|
13073
|
+
let errorCount = 0;
|
|
13074
|
+
const openedDocuments = /* @__PURE__ */ new Map();
|
|
13075
|
+
function transition(newState) {
|
|
13076
|
+
state = newState;
|
|
13077
|
+
}
|
|
13078
|
+
function calculateDelay() {
|
|
13079
|
+
return Math.min(retryBaseDelayMs * 2 ** retryCount, 3e4);
|
|
13080
|
+
}
|
|
13081
|
+
async function ensureDocumentOpened(uri, languageId, text) {
|
|
13082
|
+
if (openedDocuments.get(uri)) return;
|
|
13083
|
+
await ensureRunning();
|
|
13084
|
+
try {
|
|
13085
|
+
await client.sendNotification("textDocument/didOpen", { textDocument: {
|
|
13086
|
+
uri,
|
|
13087
|
+
languageId,
|
|
13088
|
+
version: 1,
|
|
13089
|
+
text
|
|
13090
|
+
} });
|
|
13091
|
+
openedDocuments.set(uri, { version: 1 });
|
|
13092
|
+
} catch (err) {
|
|
13093
|
+
if (process.env.ZAPMYCO_LSP_TRACE) process.stderr.write(`[lsp-instance] didOpen failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
13094
|
+
}
|
|
13095
|
+
}
|
|
13096
|
+
async function notifyDocumentChanged(uri, text) {
|
|
13097
|
+
const existing = openedDocuments.get(uri);
|
|
13098
|
+
if (!existing) return;
|
|
13099
|
+
existing.version++;
|
|
13100
|
+
const version = existing.version;
|
|
13101
|
+
if (state !== "running" || !client?.isAlive()) return;
|
|
13102
|
+
try {
|
|
13103
|
+
await client.sendNotification("textDocument/didChange", {
|
|
13104
|
+
textDocument: {
|
|
13105
|
+
uri,
|
|
13106
|
+
version
|
|
13107
|
+
},
|
|
13108
|
+
contentChanges: [{ text }]
|
|
13109
|
+
});
|
|
13110
|
+
} catch {}
|
|
13111
|
+
}
|
|
13112
|
+
async function notifyDocumentClosed(uri) {
|
|
13113
|
+
openedDocuments.delete(uri);
|
|
13114
|
+
if (state !== "running" || !client?.isAlive()) return;
|
|
13115
|
+
try {
|
|
13116
|
+
await client.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
13117
|
+
} catch {}
|
|
13118
|
+
}
|
|
13119
|
+
async function ensureRunning() {
|
|
13120
|
+
if (state === "running" && client?.isAlive()) return;
|
|
13121
|
+
if (state === "stopped" || state === "error") await doInitialize();
|
|
13122
|
+
}
|
|
13123
|
+
async function doInitialize(rootUri) {
|
|
13124
|
+
if (state === "starting") return;
|
|
13125
|
+
transition("starting");
|
|
13126
|
+
try {
|
|
13127
|
+
if (client) try {
|
|
13128
|
+
await client.shutdown();
|
|
13129
|
+
} catch {}
|
|
13130
|
+
client = createLspClient(config.clientConfig);
|
|
13131
|
+
const uri = rootUri ?? `file://${process.cwd()}`;
|
|
13132
|
+
capabilities = (await client.initialize(uri, initializationOptions)).capabilities;
|
|
13133
|
+
retryCount = 0;
|
|
13134
|
+
transition("running");
|
|
13135
|
+
} catch (err) {
|
|
13136
|
+
errorCount++;
|
|
13137
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
13138
|
+
if (retryCount < maxRetries) {
|
|
13139
|
+
retryCount++;
|
|
13140
|
+
const delay = calculateDelay();
|
|
13141
|
+
if (process.env.ZAPMYCO_LSP_TRACE) process.stderr.write(`[lsp-instance] ${serverId} init failed, retry ${retryCount}/${maxRetries} in ${delay}ms\n`);
|
|
13142
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
13143
|
+
retryCount--;
|
|
13144
|
+
transition("error");
|
|
13145
|
+
return doInitialize(rootUri);
|
|
13146
|
+
}
|
|
13147
|
+
transition("error");
|
|
13148
|
+
throw new LspError(`Failed to initialize LSP server ${serverId}: ${error.message}`, "INIT_FAILED");
|
|
13149
|
+
}
|
|
13150
|
+
}
|
|
13151
|
+
async function initialize(rootUri) {
|
|
13152
|
+
await doInitialize(rootUri);
|
|
13153
|
+
}
|
|
13154
|
+
async function request(method, params) {
|
|
13155
|
+
await ensureRunning();
|
|
13156
|
+
requestCount++;
|
|
13157
|
+
try {
|
|
13158
|
+
return await client.sendRequest(method, params);
|
|
13159
|
+
} catch (err) {
|
|
13160
|
+
errorCount++;
|
|
13161
|
+
if (err instanceof LspError && err.code === "PROCESS_EXIT") transition("error");
|
|
13162
|
+
throw err;
|
|
13163
|
+
}
|
|
13164
|
+
}
|
|
13165
|
+
async function sendNotification(method, params) {
|
|
13166
|
+
if (state !== "running" || !client?.isAlive()) return;
|
|
13167
|
+
try {
|
|
13168
|
+
await client.sendNotification(method, params);
|
|
13169
|
+
} catch {}
|
|
13170
|
+
}
|
|
13171
|
+
function onNotification(method, handler) {
|
|
13172
|
+
if (client) client.onNotification(method, handler);
|
|
13173
|
+
}
|
|
13174
|
+
function supportsCapability(capability) {
|
|
13175
|
+
if (!capabilities) return false;
|
|
13176
|
+
return capabilities[capability] === true;
|
|
13177
|
+
}
|
|
13178
|
+
function getHealth() {
|
|
13179
|
+
return {
|
|
13180
|
+
state,
|
|
13181
|
+
requestCount,
|
|
13182
|
+
errorCount
|
|
13183
|
+
};
|
|
13184
|
+
}
|
|
13185
|
+
function getState() {
|
|
13186
|
+
return state;
|
|
13187
|
+
}
|
|
13188
|
+
function getServerId() {
|
|
13189
|
+
return serverId;
|
|
13190
|
+
}
|
|
13191
|
+
function getExtensions() {
|
|
13192
|
+
return extensions;
|
|
13193
|
+
}
|
|
13194
|
+
function getLanguageIds() {
|
|
13195
|
+
return languageIds;
|
|
13196
|
+
}
|
|
13197
|
+
async function shutdown() {
|
|
13198
|
+
if (state === "stopped" || state === "stopping") return;
|
|
13199
|
+
transition("stopping");
|
|
13200
|
+
for (const [uri] of openedDocuments) try {
|
|
13201
|
+
await client?.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
13202
|
+
} catch {}
|
|
13203
|
+
openedDocuments.clear();
|
|
13204
|
+
if (client) try {
|
|
13205
|
+
await client.shutdown();
|
|
13206
|
+
} catch {}
|
|
13207
|
+
client = null;
|
|
13208
|
+
capabilities = null;
|
|
13209
|
+
transition("stopped");
|
|
13210
|
+
}
|
|
10013
13211
|
return {
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
/** 边框色 - 灰色 */
|
|
10029
|
-
border: (s) => c.gray(s),
|
|
10030
|
-
/** Header 文本 */
|
|
10031
|
-
heading: (s) => c.bold(s),
|
|
10032
|
-
editorTheme: {
|
|
10033
|
-
borderColor: (text) => c.gray(text),
|
|
10034
|
-
selectList: baseSelectListTheme
|
|
10035
|
-
},
|
|
10036
|
-
selectListTheme: baseSelectListTheme
|
|
13212
|
+
initialize,
|
|
13213
|
+
shutdown,
|
|
13214
|
+
ensureDocumentOpened,
|
|
13215
|
+
notifyDocumentChanged,
|
|
13216
|
+
notifyDocumentClosed,
|
|
13217
|
+
request,
|
|
13218
|
+
sendNotification,
|
|
13219
|
+
onNotification,
|
|
13220
|
+
getHealth,
|
|
13221
|
+
supportsCapability,
|
|
13222
|
+
getState,
|
|
13223
|
+
getServerId,
|
|
13224
|
+
getExtensions,
|
|
13225
|
+
getLanguageIds
|
|
10037
13226
|
};
|
|
10038
13227
|
}
|
|
10039
13228
|
|
|
10040
13229
|
//#endregion
|
|
10041
|
-
//#region src/
|
|
13230
|
+
//#region src/core/lsp/lsp-server-manager.ts
|
|
10042
13231
|
/**
|
|
10043
|
-
*
|
|
13232
|
+
* LSP 服务器管理器 — Layer 3
|
|
10044
13233
|
*
|
|
10045
|
-
*
|
|
10046
|
-
* -
|
|
10047
|
-
* -
|
|
13234
|
+
* 管理多个 LSP 服务器实例:
|
|
13235
|
+
* - 根据文件扩展名路由请求
|
|
13236
|
+
* - 协调文档同步(didOpen/didChange/didClose)
|
|
13237
|
+
* - 追踪 broken server
|
|
13238
|
+
* - 懒启动(首次请求时才启动 server)
|
|
13239
|
+
*
|
|
13240
|
+
* @module core/lsp
|
|
10048
13241
|
*/
|
|
10049
|
-
function
|
|
10050
|
-
|
|
13242
|
+
function createLspServerManager() {
|
|
13243
|
+
/** 扩展名 → server 实例 */
|
|
13244
|
+
const extensionMap = /* @__PURE__ */ new Map();
|
|
13245
|
+
/** 所有 server 实例 */
|
|
10051
13246
|
const servers = [];
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
13247
|
+
/** 已打开文档追踪:uri → { serverId, languageId, version } */
|
|
13248
|
+
const openedFiles = /* @__PURE__ */ new Map();
|
|
13249
|
+
/** 永久失败的 server ID 集合 */
|
|
13250
|
+
const brokenServers = /* @__PURE__ */ new Set();
|
|
13251
|
+
/** 初始化状态 */
|
|
13252
|
+
let initialized = false;
|
|
13253
|
+
/** 工作区根路径 */
|
|
13254
|
+
let workspaceRoot = "";
|
|
13255
|
+
function findLanguageId(extension, server) {
|
|
13256
|
+
const languageIds = server.getLanguageIds();
|
|
13257
|
+
if (languageIds.length === 0) return void 0;
|
|
13258
|
+
return {
|
|
13259
|
+
".ts": "typescript",
|
|
13260
|
+
".tsx": "typescriptreact",
|
|
13261
|
+
".js": "javascript",
|
|
13262
|
+
".jsx": "javascriptreact",
|
|
13263
|
+
".mjs": "javascript",
|
|
13264
|
+
".cjs": "javascript",
|
|
13265
|
+
".mts": "typescript",
|
|
13266
|
+
".cts": "typescript"
|
|
13267
|
+
}[extension] ?? languageIds[0];
|
|
13268
|
+
}
|
|
13269
|
+
function getServerForFile(filePath) {
|
|
13270
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
13271
|
+
const server = extensionMap.get(ext);
|
|
13272
|
+
if (server && !brokenServers.has(server.getServerId())) return server;
|
|
13273
|
+
}
|
|
13274
|
+
function getLanguageForFile(filePath, server) {
|
|
13275
|
+
return findLanguageId(filePath.slice(filePath.lastIndexOf(".")).toLowerCase(), server) ?? "plaintext";
|
|
13276
|
+
}
|
|
13277
|
+
async function ensureFileOpened(filePath, server, content) {
|
|
13278
|
+
const uri = `file://${filePath}`;
|
|
13279
|
+
if (openedFiles.has(uri)) return;
|
|
13280
|
+
const languageId = getLanguageForFile(filePath, server);
|
|
13281
|
+
try {
|
|
13282
|
+
await server.ensureDocumentOpened(uri, languageId, content ?? "");
|
|
13283
|
+
openedFiles.set(uri, {
|
|
13284
|
+
serverId: server.getServerId(),
|
|
13285
|
+
languageId,
|
|
13286
|
+
version: 1
|
|
13287
|
+
});
|
|
13288
|
+
} catch (err) {
|
|
13289
|
+
if (process.env.ZAPMYCO_LSP_TRACE) process.stderr.write(`[lsp-manager] ensureFileOpened failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
13290
|
+
}
|
|
13291
|
+
}
|
|
13292
|
+
async function init(serverConfigs, root) {
|
|
13293
|
+
workspaceRoot = root;
|
|
13294
|
+
extensionMap.clear();
|
|
13295
|
+
servers.length = 0;
|
|
13296
|
+
brokenServers.clear();
|
|
13297
|
+
for (const config of serverConfigs) {
|
|
13298
|
+
const extensions = config.extensions ?? [];
|
|
13299
|
+
const languageIds = config.languageIds ?? [];
|
|
13300
|
+
const instance = createLspServerInstance({
|
|
13301
|
+
serverId: config.name,
|
|
13302
|
+
clientConfig: {
|
|
13303
|
+
command: config.command,
|
|
13304
|
+
args: config.args ?? void 0,
|
|
13305
|
+
env: config.env ?? void 0,
|
|
13306
|
+
connectTimeoutMs: config.connectTimeoutMs ?? void 0,
|
|
13307
|
+
requestTimeoutMs: config.requestTimeoutMs ?? void 0
|
|
13308
|
+
},
|
|
13309
|
+
languageIds,
|
|
13310
|
+
extensions,
|
|
13311
|
+
initializationOptions: config.initializationOptions
|
|
13312
|
+
});
|
|
13313
|
+
servers.push(instance);
|
|
13314
|
+
for (const ext of extensions) {
|
|
13315
|
+
const normalized = ext.toLowerCase();
|
|
13316
|
+
if (!extensionMap.has(normalized)) extensionMap.set(normalized, instance);
|
|
13317
|
+
}
|
|
13318
|
+
}
|
|
13319
|
+
initialized = true;
|
|
13320
|
+
}
|
|
13321
|
+
async function onFileOpened(filePath, content) {
|
|
13322
|
+
if (!initialized) return;
|
|
13323
|
+
const server = getServerForFile(filePath);
|
|
13324
|
+
if (!server) return;
|
|
13325
|
+
if (server.getState() === "stopped" || server.getState() === "error") try {
|
|
13326
|
+
await server.initialize(`file://${workspaceRoot}`);
|
|
13327
|
+
} catch {
|
|
13328
|
+
brokenServers.add(server.getServerId());
|
|
13329
|
+
return;
|
|
13330
|
+
}
|
|
13331
|
+
await ensureFileOpened(filePath, server, content);
|
|
13332
|
+
}
|
|
13333
|
+
async function onFileChanged(filePath, content) {
|
|
13334
|
+
const uri = `file://${filePath}`;
|
|
13335
|
+
const opened = openedFiles.get(uri);
|
|
13336
|
+
if (!opened) {
|
|
13337
|
+
await onFileOpened(filePath, content);
|
|
13338
|
+
return;
|
|
13339
|
+
}
|
|
13340
|
+
const server = servers.find((s) => s.getServerId() === opened.serverId);
|
|
13341
|
+
if (server && server.getState() === "running") await server.notifyDocumentChanged(uri, content);
|
|
13342
|
+
}
|
|
13343
|
+
async function onFileClosed(filePath) {
|
|
13344
|
+
const uri = `file://${filePath}`;
|
|
13345
|
+
const opened = openedFiles.get(uri);
|
|
13346
|
+
if (opened) {
|
|
13347
|
+
const server = servers.find((s) => s.getServerId() === opened.serverId);
|
|
13348
|
+
if (server) await server.notifyDocumentClosed(uri);
|
|
13349
|
+
openedFiles.delete(uri);
|
|
13350
|
+
}
|
|
13351
|
+
}
|
|
13352
|
+
async function request(filePath, method, params) {
|
|
13353
|
+
if (!initialized) throw new LspError("LSP manager not initialized", "NOT_INITIALIZED");
|
|
13354
|
+
const server = getServerForFile(filePath);
|
|
13355
|
+
if (!server) throw new LspError(`No LSP server available for file: ${filePath}`, "NO_SERVER");
|
|
13356
|
+
if (server.getState() === "stopped" || server.getState() === "error") try {
|
|
13357
|
+
await server.initialize(`file://${workspaceRoot}`);
|
|
13358
|
+
} catch (err) {
|
|
13359
|
+
brokenServers.add(server.getServerId());
|
|
13360
|
+
throw new LspError(`Failed to start LSP server for ${filePath}: ${err instanceof Error ? err.message : String(err)}`, "SERVER_START_FAILED");
|
|
13361
|
+
}
|
|
13362
|
+
await ensureFileOpened(filePath, server);
|
|
13363
|
+
return server.request(method, params);
|
|
13364
|
+
}
|
|
13365
|
+
function getStatus() {
|
|
13366
|
+
return {
|
|
13367
|
+
serverCount: servers.length,
|
|
13368
|
+
runningCount: servers.filter((s) => s.getState() === "running").length,
|
|
13369
|
+
errorCount: brokenServers.size,
|
|
13370
|
+
servers: servers.map((s) => ({
|
|
13371
|
+
serverId: s.getServerId(),
|
|
13372
|
+
state: s.getState(),
|
|
13373
|
+
extensions: s.getExtensions()
|
|
13374
|
+
}))
|
|
10062
13375
|
};
|
|
10063
|
-
if (Array.isArray(config.args)) server.args = config.args;
|
|
10064
|
-
if (config.env && typeof config.env === "object") server.env = config.env;
|
|
10065
|
-
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
10066
|
-
if (typeof config.enabled === "boolean") server.enabled = config.enabled;
|
|
10067
|
-
if (typeof config.connectTimeoutMs === "number") server.connectTimeoutMs = config.connectTimeoutMs;
|
|
10068
|
-
servers.push(server);
|
|
10069
13376
|
}
|
|
10070
|
-
|
|
13377
|
+
async function shutdown() {
|
|
13378
|
+
for (const [uri] of openedFiles) {
|
|
13379
|
+
const opened = openedFiles.get(uri);
|
|
13380
|
+
if (opened) {
|
|
13381
|
+
const server = servers.find((s) => s.getServerId() === opened.serverId);
|
|
13382
|
+
if (server) try {
|
|
13383
|
+
await server.notifyDocumentClosed(uri);
|
|
13384
|
+
} catch {}
|
|
13385
|
+
}
|
|
13386
|
+
}
|
|
13387
|
+
openedFiles.clear();
|
|
13388
|
+
for (const server of servers) try {
|
|
13389
|
+
await server.shutdown();
|
|
13390
|
+
} catch {}
|
|
13391
|
+
servers.length = 0;
|
|
13392
|
+
extensionMap.clear();
|
|
13393
|
+
brokenServers.clear();
|
|
13394
|
+
initialized = false;
|
|
13395
|
+
}
|
|
13396
|
+
return {
|
|
13397
|
+
init,
|
|
13398
|
+
onFileOpened,
|
|
13399
|
+
onFileChanged,
|
|
13400
|
+
onFileClosed,
|
|
13401
|
+
request,
|
|
13402
|
+
getServerForFile,
|
|
13403
|
+
getStatus,
|
|
13404
|
+
shutdown
|
|
13405
|
+
};
|
|
10071
13406
|
}
|
|
10072
13407
|
|
|
10073
13408
|
//#endregion
|
|
@@ -10080,7 +13415,7 @@ function normalizeMcpConfig(raw) {
|
|
|
10080
13415
|
*
|
|
10081
13416
|
* @module core/mcp/client
|
|
10082
13417
|
*/
|
|
10083
|
-
const log$
|
|
13418
|
+
const log$6 = logger.child("mcp:client");
|
|
10084
13419
|
/**
|
|
10085
13420
|
* 连接到单个 MCP Server 并发现其工具
|
|
10086
13421
|
*
|
|
@@ -10116,7 +13451,7 @@ async function connectMcpServer(config, signal) {
|
|
|
10116
13451
|
await withTimeout(client.connect(transport), timeoutMs);
|
|
10117
13452
|
const { tools: rawTools } = await withTimeout(client.listTools(), timeoutMs);
|
|
10118
13453
|
const tools = rawTools ?? [];
|
|
10119
|
-
log$
|
|
13454
|
+
log$6.debug(`MCP server "${serverName}" 已连接,发现 ${tools.length} 个工具`);
|
|
10120
13455
|
return {
|
|
10121
13456
|
client,
|
|
10122
13457
|
transport,
|
|
@@ -10124,7 +13459,7 @@ async function connectMcpServer(config, signal) {
|
|
|
10124
13459
|
serverName
|
|
10125
13460
|
};
|
|
10126
13461
|
} catch (error) {
|
|
10127
|
-
log$
|
|
13462
|
+
log$6.warn(`MCP server "${serverName}" 连接失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
10128
13463
|
return null;
|
|
10129
13464
|
}
|
|
10130
13465
|
}
|
|
@@ -10138,7 +13473,7 @@ async function closeMcpServer(conn) {
|
|
|
10138
13473
|
try {
|
|
10139
13474
|
await conn.client.close();
|
|
10140
13475
|
} catch {}
|
|
10141
|
-
log$
|
|
13476
|
+
log$6.debug(`MCP server "${conn.serverName}" 已断开`);
|
|
10142
13477
|
}
|
|
10143
13478
|
/**
|
|
10144
13479
|
* 简单的超时工具:promise 在 ms 毫秒内未完成则 reject
|
|
@@ -10191,7 +13526,7 @@ function mcpToolToRegistration(mcpTool, serverName, client) {
|
|
|
10191
13526
|
|
|
10192
13527
|
//#endregion
|
|
10193
13528
|
//#region src/core/mcp/index.ts
|
|
10194
|
-
const log$
|
|
13529
|
+
const log$5 = logger.child("mcp");
|
|
10195
13530
|
/**
|
|
10196
13531
|
* MCP 连接生命周期管理器
|
|
10197
13532
|
*
|
|
@@ -10212,9 +13547,9 @@ var McpManager = class {
|
|
|
10212
13547
|
async initialize(servers) {
|
|
10213
13548
|
const enabledServers = servers.filter((s) => s.enabled !== false);
|
|
10214
13549
|
if (enabledServers.length === 0) return [];
|
|
10215
|
-
log$
|
|
13550
|
+
log$5.info(`正在连接 ${enabledServers.length} 个 MCP Server...`);
|
|
10216
13551
|
const connectedCount = (await Promise.allSettled(enabledServers.map((config) => this.connectAndCollect(config)))).filter((r) => r.status === "fulfilled" && r.value !== null).length;
|
|
10217
|
-
log$
|
|
13552
|
+
log$5.info(`MCP: ${connectedCount}/${enabledServers.length} 个 Server 已连接,共 ${this.toolRegistrations.length} 个工具`);
|
|
10218
13553
|
return this.toolRegistrations;
|
|
10219
13554
|
}
|
|
10220
13555
|
/**
|
|
@@ -10222,7 +13557,7 @@ var McpManager = class {
|
|
|
10222
13557
|
*/
|
|
10223
13558
|
async shutdown() {
|
|
10224
13559
|
if (this.connections.length === 0) return;
|
|
10225
|
-
log$
|
|
13560
|
+
log$5.info(`正在关闭 ${this.connections.length} 个 MCP 连接...`);
|
|
10226
13561
|
await Promise.allSettled(this.connections.map((conn) => closeMcpServer(conn)));
|
|
10227
13562
|
this.connections = [];
|
|
10228
13563
|
this.toolRegistrations = [];
|
|
@@ -10455,7 +13790,7 @@ var TaskStore = class {
|
|
|
10455
13790
|
*
|
|
10456
13791
|
* @module security/approval-manager
|
|
10457
13792
|
*/
|
|
10458
|
-
const log$
|
|
13793
|
+
const log$4 = logger.child("approval-manager");
|
|
10459
13794
|
var ApprovalManager = class {
|
|
10460
13795
|
provider = null;
|
|
10461
13796
|
constructor(provider) {
|
|
@@ -10484,7 +13819,7 @@ var ApprovalManager = class {
|
|
|
10484
13819
|
*/
|
|
10485
13820
|
async requestApproval(request) {
|
|
10486
13821
|
if (!this.provider) {
|
|
10487
|
-
log$
|
|
13822
|
+
log$4.warn("无审批提供者,自动拒绝审批请求", {
|
|
10488
13823
|
toolId: request.toolId,
|
|
10489
13824
|
risk: request.risk
|
|
10490
13825
|
});
|
|
@@ -10507,7 +13842,7 @@ var ApprovalManager = class {
|
|
|
10507
13842
|
toolId: request.toolId,
|
|
10508
13843
|
scope: response.scope ?? "once"
|
|
10509
13844
|
});
|
|
10510
|
-
log$
|
|
13845
|
+
log$4.debug("审批通过", {
|
|
10511
13846
|
toolId: request.toolId,
|
|
10512
13847
|
scope: response.scope
|
|
10513
13848
|
});
|
|
@@ -10516,11 +13851,11 @@ var ApprovalManager = class {
|
|
|
10516
13851
|
toolId: request.toolId,
|
|
10517
13852
|
reason: "用户拒绝"
|
|
10518
13853
|
});
|
|
10519
|
-
log$
|
|
13854
|
+
log$4.debug("审批被拒绝", { toolId: request.toolId });
|
|
10520
13855
|
}
|
|
10521
13856
|
return response;
|
|
10522
13857
|
} catch (err) {
|
|
10523
|
-
log$
|
|
13858
|
+
log$4.error("审批提供者异常,自动拒绝", {
|
|
10524
13859
|
toolId: request.toolId,
|
|
10525
13860
|
error: err instanceof Error ? err.message : String(err)
|
|
10526
13861
|
});
|
|
@@ -10543,7 +13878,7 @@ var ApprovalManager = class {
|
|
|
10543
13878
|
*
|
|
10544
13879
|
* @module security/audit-logger
|
|
10545
13880
|
*/
|
|
10546
|
-
const log$
|
|
13881
|
+
const log$3 = logger.child("audit-logger");
|
|
10547
13882
|
/** 默认审计日志目录 */
|
|
10548
13883
|
const AUDIT_DIR = join(homedir(), ".zapmyco", "logs");
|
|
10549
13884
|
/** 审计日志文件名 */
|
|
@@ -10583,7 +13918,7 @@ var AuditLogger = class {
|
|
|
10583
13918
|
});
|
|
10584
13919
|
};
|
|
10585
13920
|
eventBus.on("security:violation", this.violationListener);
|
|
10586
|
-
log$
|
|
13921
|
+
log$3.debug("审计日志初始化", {
|
|
10587
13922
|
path: this.filePath,
|
|
10588
13923
|
level: this.level
|
|
10589
13924
|
});
|
|
@@ -10638,7 +13973,7 @@ var AuditLogger = class {
|
|
|
10638
13973
|
if (!existsSync(this.filePath)) return [];
|
|
10639
13974
|
return readFileSync(this.filePath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
10640
13975
|
} catch {
|
|
10641
|
-
log$
|
|
13976
|
+
log$3.warn("读取审计日志失败");
|
|
10642
13977
|
return [];
|
|
10643
13978
|
}
|
|
10644
13979
|
}
|
|
@@ -10657,7 +13992,7 @@ var AuditLogger = class {
|
|
|
10657
13992
|
this.violationListener = null;
|
|
10658
13993
|
}
|
|
10659
13994
|
this.flush();
|
|
10660
|
-
log$
|
|
13995
|
+
log$3.debug("审计日志已关闭");
|
|
10661
13996
|
}
|
|
10662
13997
|
/**
|
|
10663
13998
|
* 将缓冲写入文件
|
|
@@ -10670,7 +14005,7 @@ var AuditLogger = class {
|
|
|
10670
14005
|
const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
10671
14006
|
appendFileSync(this.filePath, lines, "utf-8");
|
|
10672
14007
|
} catch (err) {
|
|
10673
|
-
log$
|
|
14008
|
+
log$3.error("审计日志写入失败", { error: err instanceof Error ? err.message : String(err) });
|
|
10674
14009
|
if (this.buffer.length < 1e3) this.buffer = [...entries, ...this.buffer];
|
|
10675
14010
|
}
|
|
10676
14011
|
}
|
|
@@ -10789,7 +14124,7 @@ function matchParamPatterns(paramPatterns, actualParams) {
|
|
|
10789
14124
|
|
|
10790
14125
|
//#endregion
|
|
10791
14126
|
//#region src/security/permission-engine.ts
|
|
10792
|
-
const log$
|
|
14127
|
+
const log$2 = logger.child("permission-engine");
|
|
10793
14128
|
var PermissionEngine = class {
|
|
10794
14129
|
config;
|
|
10795
14130
|
store;
|
|
@@ -10814,7 +14149,7 @@ var PermissionEngine = class {
|
|
|
10814
14149
|
};
|
|
10815
14150
|
const toolInfo = this.resolveToolInfo(toolId);
|
|
10816
14151
|
for (const rule of BUILTIN_DENY_RULES) if (matchToolPattern(rule.toolPattern, toolId)) {
|
|
10817
|
-
log$
|
|
14152
|
+
log$2.debug("命中内置拒绝规则", {
|
|
10818
14153
|
toolId,
|
|
10819
14154
|
rule: rule.toolPattern
|
|
10820
14155
|
});
|
|
@@ -10827,7 +14162,7 @@ var PermissionEngine = class {
|
|
|
10827
14162
|
};
|
|
10828
14163
|
}
|
|
10829
14164
|
for (const rule of this.config.denyRules) if (matchToolPattern(rule.toolPattern, toolId) && matchParamPatterns(rule.paramPatterns, params)) {
|
|
10830
|
-
log$
|
|
14165
|
+
log$2.debug("命中用户拒绝规则", {
|
|
10831
14166
|
toolId,
|
|
10832
14167
|
ruleId: rule.id ?? rule.toolPattern
|
|
10833
14168
|
});
|
|
@@ -10856,7 +14191,7 @@ var PermissionEngine = class {
|
|
|
10856
14191
|
};
|
|
10857
14192
|
}
|
|
10858
14193
|
for (const rule of this.config.allowRules) if (matchToolPattern(rule.toolPattern, toolId) && matchParamPatterns(rule.paramPatterns, params)) {
|
|
10859
|
-
log$
|
|
14194
|
+
log$2.debug("命中用户允许规则", {
|
|
10860
14195
|
toolId,
|
|
10861
14196
|
ruleId: rule.id ?? rule.toolPattern
|
|
10862
14197
|
});
|
|
@@ -10869,7 +14204,7 @@ var PermissionEngine = class {
|
|
|
10869
14204
|
};
|
|
10870
14205
|
}
|
|
10871
14206
|
if (this.store.hasSessionApproval(toolId)) {
|
|
10872
|
-
log$
|
|
14207
|
+
log$2.debug("命中会话级审批", { toolId });
|
|
10873
14208
|
return {
|
|
10874
14209
|
action: "allow",
|
|
10875
14210
|
risk: "low",
|
|
@@ -10877,7 +14212,7 @@ var PermissionEngine = class {
|
|
|
10877
14212
|
};
|
|
10878
14213
|
}
|
|
10879
14214
|
if (this.store.hasPersistentApproval(toolId)) {
|
|
10880
|
-
log$
|
|
14215
|
+
log$2.debug("命中持久化审批", { toolId });
|
|
10881
14216
|
return {
|
|
10882
14217
|
action: "allow",
|
|
10883
14218
|
risk: "low",
|
|
@@ -10923,7 +14258,7 @@ var PermissionEngine = class {
|
|
|
10923
14258
|
try {
|
|
10924
14259
|
return toolInfo.checkPermission(params);
|
|
10925
14260
|
} catch (err) {
|
|
10926
|
-
log$
|
|
14261
|
+
log$2.warn("工具 checkPermission 执行异常", {
|
|
10927
14262
|
toolId,
|
|
10928
14263
|
error: err instanceof Error ? err.message : String(err)
|
|
10929
14264
|
});
|
|
@@ -11011,7 +14346,7 @@ var PermissionEngine = class {
|
|
|
11011
14346
|
*
|
|
11012
14347
|
* @module security/permission-store
|
|
11013
14348
|
*/
|
|
11014
|
-
const log$
|
|
14349
|
+
const log$1 = logger.child("permission-store");
|
|
11015
14350
|
function getStoragePath() {
|
|
11016
14351
|
return join(homedir(), ".zapmyco", "permissions.json");
|
|
11017
14352
|
}
|
|
@@ -11082,7 +14417,7 @@ var PermissionStore = class {
|
|
|
11082
14417
|
const before = this.persistentApprovals.length;
|
|
11083
14418
|
this.persistentApprovals = this.persistentApprovals.filter((a) => a.expiresAt === 0 || a.expiresAt > now);
|
|
11084
14419
|
if (this.persistentApprovals.length < before) {
|
|
11085
|
-
log$
|
|
14420
|
+
log$1.debug("清理过期审批条目", { removed: before - this.persistentApprovals.length });
|
|
11086
14421
|
this.saveToFile();
|
|
11087
14422
|
}
|
|
11088
14423
|
}
|
|
@@ -11113,7 +14448,7 @@ var PermissionStore = class {
|
|
|
11113
14448
|
const raw = readFileSync(getStoragePath(), "utf-8");
|
|
11114
14449
|
const data = JSON.parse(raw);
|
|
11115
14450
|
this.persistentApprovals = Array.isArray(data.approvals) ? data.approvals : [];
|
|
11116
|
-
log$
|
|
14451
|
+
log$1.debug("已加载持久化审批记录", { count: this.persistentApprovals.length });
|
|
11117
14452
|
} catch {
|
|
11118
14453
|
this.persistentApprovals = [];
|
|
11119
14454
|
}
|
|
@@ -11129,7 +14464,7 @@ var PermissionStore = class {
|
|
|
11129
14464
|
const data = { approvals: this.persistentApprovals };
|
|
11130
14465
|
writeFileSync(getStoragePath(), JSON.stringify(data, null, 2), "utf-8");
|
|
11131
14466
|
} catch (err) {
|
|
11132
|
-
log$
|
|
14467
|
+
log$1.warn("保存权限持久化文件失败", { error: err instanceof Error ? err.message : String(err) });
|
|
11133
14468
|
}
|
|
11134
14469
|
}
|
|
11135
14470
|
};
|
|
@@ -11427,155 +14762,6 @@ function threatSeverity(level) {
|
|
|
11427
14762
|
}
|
|
11428
14763
|
}
|
|
11429
14764
|
|
|
11430
|
-
//#endregion
|
|
11431
|
-
//#region src/security/tool-guard.ts
|
|
11432
|
-
const log$1 = logger.child("tool-guard");
|
|
11433
|
-
/**
|
|
11434
|
-
* 安全阻止错误
|
|
11435
|
-
*
|
|
11436
|
-
* 当工具调用被权限引擎拒绝时抛出。
|
|
11437
|
-
* 调用方(agent-adapter / session)应捕获此错误
|
|
11438
|
-
* 并转换为 LLM 友好的错误反馈。
|
|
11439
|
-
*/
|
|
11440
|
-
var SecurityBlockedError = class extends Error {
|
|
11441
|
-
toolId;
|
|
11442
|
-
risk;
|
|
11443
|
-
reason;
|
|
11444
|
-
constructor(message, toolId, risk, reason) {
|
|
11445
|
-
super(message);
|
|
11446
|
-
this.name = "SecurityBlockedError";
|
|
11447
|
-
this.toolId = toolId;
|
|
11448
|
-
this.risk = risk;
|
|
11449
|
-
this.reason = reason;
|
|
11450
|
-
}
|
|
11451
|
-
};
|
|
11452
|
-
var ToolGuard = class {
|
|
11453
|
-
engine;
|
|
11454
|
-
approvalManager;
|
|
11455
|
-
store;
|
|
11456
|
-
sessionId;
|
|
11457
|
-
auditLogger;
|
|
11458
|
-
constructor(engine, approvalManager, store, sessionId, auditLogger) {
|
|
11459
|
-
this.engine = engine;
|
|
11460
|
-
this.approvalManager = approvalManager;
|
|
11461
|
-
this.store = store;
|
|
11462
|
-
this.sessionId = sessionId ?? `session-${Date.now()}`;
|
|
11463
|
-
this.auditLogger = auditLogger;
|
|
11464
|
-
}
|
|
11465
|
-
/**
|
|
11466
|
-
* 包装单个 ToolRegistration
|
|
11467
|
-
*
|
|
11468
|
-
* 返回新 ToolRegistration,原对象不变。
|
|
11469
|
-
* execute 被替换为带安全检查的版本。
|
|
11470
|
-
*/
|
|
11471
|
-
wrap(registration) {
|
|
11472
|
-
const originalExecute = registration.execute;
|
|
11473
|
-
const toolId = registration.id;
|
|
11474
|
-
const toolLabel = registration.label;
|
|
11475
|
-
const guardedExecute = async (toolCallId, params, signal, onUpdate) => {
|
|
11476
|
-
const decision = this.engine.evaluate(toolId, params);
|
|
11477
|
-
if (decision.action === "deny") {
|
|
11478
|
-
const reason = decision.reason ?? `工具 ${toolId} 已被安全策略阻止`;
|
|
11479
|
-
log$1.warn("工具调用被阻止", {
|
|
11480
|
-
toolId,
|
|
11481
|
-
risk: decision.risk,
|
|
11482
|
-
reason
|
|
11483
|
-
});
|
|
11484
|
-
eventBus.emit("security:blocked", {
|
|
11485
|
-
toolId,
|
|
11486
|
-
risk: decision.risk,
|
|
11487
|
-
reason,
|
|
11488
|
-
params
|
|
11489
|
-
});
|
|
11490
|
-
this.auditLogger?.log({
|
|
11491
|
-
action: "BLOCK",
|
|
11492
|
-
toolId,
|
|
11493
|
-
risk: decision.risk,
|
|
11494
|
-
reason,
|
|
11495
|
-
params,
|
|
11496
|
-
...decision.matchedRule ? { matchedRule: decision.matchedRule } : {}
|
|
11497
|
-
});
|
|
11498
|
-
throw new SecurityBlockedError(reason, toolId, decision.risk, reason);
|
|
11499
|
-
}
|
|
11500
|
-
if (decision.action === "ask") {
|
|
11501
|
-
this.auditLogger?.log({
|
|
11502
|
-
action: "APPROVAL_REQUESTED",
|
|
11503
|
-
toolId,
|
|
11504
|
-
risk: decision.risk,
|
|
11505
|
-
params,
|
|
11506
|
-
...decision.reason ? { reason: decision.reason } : {}
|
|
11507
|
-
});
|
|
11508
|
-
const approvalResponse = await this.approvalManager.requestApproval({
|
|
11509
|
-
toolId,
|
|
11510
|
-
toolLabel,
|
|
11511
|
-
params,
|
|
11512
|
-
risk: decision.risk,
|
|
11513
|
-
reason: decision.reason ?? `工具 ${toolId} 需要审批`,
|
|
11514
|
-
sessionId: this.sessionId
|
|
11515
|
-
});
|
|
11516
|
-
if (!approvalResponse.approved) {
|
|
11517
|
-
const reason = `用户拒绝了工具 ${toolId} 的执行请求`;
|
|
11518
|
-
log$1.info("用户拒绝工具执行", { toolId });
|
|
11519
|
-
this.auditLogger?.log({
|
|
11520
|
-
action: "APPROVAL_DENIED",
|
|
11521
|
-
toolId,
|
|
11522
|
-
risk: decision.risk,
|
|
11523
|
-
reason
|
|
11524
|
-
});
|
|
11525
|
-
throw new SecurityBlockedError(reason, toolId, decision.risk, reason);
|
|
11526
|
-
}
|
|
11527
|
-
if (approvalResponse.scope === "session") this.store.addSessionApproval(toolId);
|
|
11528
|
-
else if (approvalResponse.scope === "always") this.store.addPersistentApproval(toolId);
|
|
11529
|
-
this.auditLogger?.log({
|
|
11530
|
-
action: "APPROVAL_GRANTED",
|
|
11531
|
-
toolId,
|
|
11532
|
-
risk: decision.risk,
|
|
11533
|
-
...approvalResponse.scope ? { scope: approvalResponse.scope } : {},
|
|
11534
|
-
...decision.reason ? { reason: decision.reason } : {}
|
|
11535
|
-
});
|
|
11536
|
-
log$1.debug("审批通过,执行工具", {
|
|
11537
|
-
toolId,
|
|
11538
|
-
scope: approvalResponse.scope
|
|
11539
|
-
});
|
|
11540
|
-
} else this.auditLogger?.log({
|
|
11541
|
-
action: "ALLOW",
|
|
11542
|
-
toolId,
|
|
11543
|
-
risk: decision.risk,
|
|
11544
|
-
params,
|
|
11545
|
-
...decision.matchedRule ? { matchedRule: decision.matchedRule } : {}
|
|
11546
|
-
});
|
|
11547
|
-
return originalExecute(toolCallId, params, signal, onUpdate);
|
|
11548
|
-
};
|
|
11549
|
-
return {
|
|
11550
|
-
...registration,
|
|
11551
|
-
execute: guardedExecute
|
|
11552
|
-
};
|
|
11553
|
-
}
|
|
11554
|
-
/**
|
|
11555
|
-
* 批量包装所有工具
|
|
11556
|
-
*/
|
|
11557
|
-
wrapAll(registrations) {
|
|
11558
|
-
return registrations.map((reg) => this.wrap(reg));
|
|
11559
|
-
}
|
|
11560
|
-
};
|
|
11561
|
-
/**
|
|
11562
|
-
* 从 ToolRegistration 数组构建 ToolInfoResolver
|
|
11563
|
-
*
|
|
11564
|
-
* 供 PermissionEngine 使用,将 toolId 映射到其安全信息。
|
|
11565
|
-
*/
|
|
11566
|
-
function createToolInfoResolver(registrations) {
|
|
11567
|
-
const map = /* @__PURE__ */ new Map();
|
|
11568
|
-
for (const reg of registrations) map.set(reg.id, reg);
|
|
11569
|
-
return (toolId) => {
|
|
11570
|
-
const reg = map.get(toolId);
|
|
11571
|
-
if (!reg) return void 0;
|
|
11572
|
-
return {
|
|
11573
|
-
checkPermission: reg.checkPermission,
|
|
11574
|
-
defaultRisk: reg.defaultRisk
|
|
11575
|
-
};
|
|
11576
|
-
};
|
|
11577
|
-
}
|
|
11578
|
-
|
|
11579
14765
|
//#endregion
|
|
11580
14766
|
//#region src/cli/repl/session.ts
|
|
11581
14767
|
/**
|
|
@@ -11645,6 +14831,7 @@ const DEFAULT_CONTINUATION_PROMPT = "... ";
|
|
|
11645
14831
|
* REPL 会话实现
|
|
11646
14832
|
*/
|
|
11647
14833
|
var ReplSession = class {
|
|
14834
|
+
config;
|
|
11648
14835
|
tui;
|
|
11649
14836
|
editor;
|
|
11650
14837
|
outputArea;
|
|
@@ -11659,6 +14846,10 @@ var ReplSession = class {
|
|
|
11659
14846
|
agent;
|
|
11660
14847
|
/** MCP 连接管理器(在 registerBuiltinTools 中异步初始化) */
|
|
11661
14848
|
mcpManager = null;
|
|
14849
|
+
/** LSP 服务器管理器(在 registerBuiltinTools 中异步初始化) */
|
|
14850
|
+
lspManager = null;
|
|
14851
|
+
/** LSP 诊断收集器 */
|
|
14852
|
+
diagnosticCollector = null;
|
|
11662
14853
|
/** 当前正在执行的 taskId(用于取消操作) */
|
|
11663
14854
|
currentTaskId = null;
|
|
11664
14855
|
/** 多轮对话上下文(兼容保留,Agent 内部也维护历史) */
|
|
@@ -11667,6 +14858,8 @@ var ReplSession = class {
|
|
|
11667
14858
|
cronScheduler = null;
|
|
11668
14859
|
/** 任务管理器(会话级持久化) */
|
|
11669
14860
|
taskStore;
|
|
14861
|
+
/** Worktree 隔离管理器 */
|
|
14862
|
+
worktreeManager;
|
|
11670
14863
|
/** 安全框架组件 */
|
|
11671
14864
|
permissionStore;
|
|
11672
14865
|
permissionEngine;
|
|
@@ -11675,6 +14868,8 @@ var ReplSession = class {
|
|
|
11675
14868
|
auditLogger;
|
|
11676
14869
|
secretRedactor;
|
|
11677
14870
|
skillGuard;
|
|
14871
|
+
/** 交互式提问管理器 */
|
|
14872
|
+
questionManager;
|
|
11678
14873
|
stats = {
|
|
11679
14874
|
totalRequests: 0,
|
|
11680
14875
|
successCount: 0,
|
|
@@ -11718,6 +14913,7 @@ var ReplSession = class {
|
|
|
11718
14913
|
this.taskStore.load();
|
|
11719
14914
|
this.cronScheduler = new CronScheduler(getCronStore(), { isIdle: () => this._state === "idle" });
|
|
11720
14915
|
this.cronScheduler.start();
|
|
14916
|
+
this.initWorktreeManager();
|
|
11721
14917
|
const memoryStore = getMemoryStore();
|
|
11722
14918
|
memoryStore.freezeSnapshot().then(() => {
|
|
11723
14919
|
this.agent.memorySnapshot = memoryStore.getSnapshot();
|
|
@@ -11727,6 +14923,8 @@ var ReplSession = class {
|
|
|
11727
14923
|
});
|
|
11728
14924
|
if (this.config.skill?.enabled !== false) this.initSkills();
|
|
11729
14925
|
this.initSecurity();
|
|
14926
|
+
this.questionManager = getQuestionManager();
|
|
14927
|
+
this.questionManager.setProvider(createTuiQuestionProvider(this.tui));
|
|
11730
14928
|
this.registerBuiltinCommands();
|
|
11731
14929
|
this.registerBuiltinTools();
|
|
11732
14930
|
this.setupEditorHandlers();
|
|
@@ -11754,6 +14952,7 @@ var ReplSession = class {
|
|
|
11754
14952
|
this.updateStatsState();
|
|
11755
14953
|
log.info("REPL 关闭", { reason: reason ?? "未知" });
|
|
11756
14954
|
this.cancelCurrentTask();
|
|
14955
|
+
if (this.questionManager) this.questionManager.rejectAll(/* @__PURE__ */ new Error("会话已关闭"));
|
|
11757
14956
|
this.editor.setExecuting(false);
|
|
11758
14957
|
eventBus.emit("system:shutdown", { reason });
|
|
11759
14958
|
if (this.cronScheduler) {
|
|
@@ -11765,6 +14964,14 @@ var ReplSession = class {
|
|
|
11765
14964
|
await this.mcpManager.shutdown();
|
|
11766
14965
|
this.mcpManager = null;
|
|
11767
14966
|
}
|
|
14967
|
+
if (this.lspManager) {
|
|
14968
|
+
await this.lspManager.shutdown();
|
|
14969
|
+
this.lspManager = null;
|
|
14970
|
+
}
|
|
14971
|
+
if (this.diagnosticCollector) {
|
|
14972
|
+
this.diagnosticCollector.clear();
|
|
14973
|
+
this.diagnosticCollector = null;
|
|
14974
|
+
}
|
|
11768
14975
|
this.tui.stop();
|
|
11769
14976
|
process.exit(0);
|
|
11770
14977
|
}
|
|
@@ -12201,6 +15408,29 @@ var ReplSession = class {
|
|
|
12201
15408
|
/**
|
|
12202
15409
|
* 初始化安全框架
|
|
12203
15410
|
*
|
|
15411
|
+
/**
|
|
15412
|
+
* 初始化 WorktreeManager
|
|
15413
|
+
*
|
|
15414
|
+
* 创建 git worktree 隔离管理器,注册为全局实例,
|
|
15415
|
+
* 并启动过期 worktree 清理。
|
|
15416
|
+
*/
|
|
15417
|
+
initWorktreeManager() {
|
|
15418
|
+
const rawConfig = this.config.worktree ?? {};
|
|
15419
|
+
const worktreeConfig = {
|
|
15420
|
+
enabled: true,
|
|
15421
|
+
autoCleanNoChanges: true,
|
|
15422
|
+
expireAfterMs: 1440 * 60 * 1e3,
|
|
15423
|
+
baseDir: join(homedir(), ".zapmyco", "worktrees"),
|
|
15424
|
+
...rawConfig
|
|
15425
|
+
};
|
|
15426
|
+
if (!worktreeConfig.baseDir) worktreeConfig.baseDir = join(homedir(), ".zapmyco", "worktrees");
|
|
15427
|
+
this.worktreeManager = new WorktreeManager(worktreeConfig);
|
|
15428
|
+
setWorktreeManager(this.worktreeManager);
|
|
15429
|
+
this.worktreeManager.cleanExpired().catch((err) => {
|
|
15430
|
+
log.warn("过期 worktree 清理失败", { error: err instanceof Error ? err.message : String(err) });
|
|
15431
|
+
});
|
|
15432
|
+
}
|
|
15433
|
+
/**
|
|
12204
15434
|
* 创建 PermissionEngine → ApprovalManager → ToolGuard 管道。
|
|
12205
15435
|
* 必须在 registerBuiltinTools() 之前调用。
|
|
12206
15436
|
*/
|
|
@@ -12318,7 +15548,7 @@ var ReplSession = class {
|
|
|
12318
15548
|
* MCP 工具在连接完成后自动追加。
|
|
12319
15549
|
*/
|
|
12320
15550
|
registerBuiltinTools() {
|
|
12321
|
-
const rawTools = createReplBuiltinTools(this.config.web, this.taskStore, this.config.skill, this.agent, this.config.subAgent, this.cronScheduler ?? void 0, this.config.agentTeam);
|
|
15551
|
+
const rawTools = createReplBuiltinTools(this.config.web, this.taskStore, this.config.skill, this.agent, this.config.subAgent, this.cronScheduler ?? void 0, this.config.agentTeam, this.worktreeManager);
|
|
12322
15552
|
this.permissionEngine.setToolInfoResolver(createToolInfoResolver(rawTools));
|
|
12323
15553
|
const guardedTools = this.toolGuard.wrapAll(rawTools);
|
|
12324
15554
|
this.agent.registerTools(guardedTools);
|
|
@@ -12328,6 +15558,29 @@ var ReplSession = class {
|
|
|
12328
15558
|
}).catch((err) => {
|
|
12329
15559
|
log.error("MCP 初始化失败", { error: err instanceof Error ? err.message : String(err) });
|
|
12330
15560
|
});
|
|
15561
|
+
if (this.config.lsp?.enabled !== false) {
|
|
15562
|
+
const lspServers = resolveLspConfig(this.config.lsp);
|
|
15563
|
+
if (lspServers.length > 0) {
|
|
15564
|
+
this.lspManager = createLspServerManager();
|
|
15565
|
+
this.lspManager.init(lspServers, process.cwd()).catch((err) => {
|
|
15566
|
+
log.error("LSP 初始化失败", { error: err instanceof Error ? err.message : String(err) });
|
|
15567
|
+
});
|
|
15568
|
+
rawTools.push(createLspTool(this.lspManager));
|
|
15569
|
+
this.diagnosticCollector = createDiagnosticCollector();
|
|
15570
|
+
this.diagnosticCollector.init(this.lspManager);
|
|
15571
|
+
const readFileTool = rawTools.find((t) => t.id === "ReadFile");
|
|
15572
|
+
if (readFileTool && this.lspManager) {
|
|
15573
|
+
const lspManagerRef = this.lspManager;
|
|
15574
|
+
const originalExecute = readFileTool.execute;
|
|
15575
|
+
readFileTool.execute = async (toolCallId, params, signal, onUpdate) => {
|
|
15576
|
+
const result = await originalExecute(toolCallId, params, signal, onUpdate);
|
|
15577
|
+
const filePath = params?.file_path;
|
|
15578
|
+
if (filePath && typeof filePath === "string" && !result.details?.error) lspManagerRef.onFileOpened(filePath, "").catch(() => {});
|
|
15579
|
+
return result;
|
|
15580
|
+
};
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
}
|
|
12331
15584
|
}
|
|
12332
15585
|
/**
|
|
12333
15586
|
* 初始化 Skill 系统
|