@saber2pr/ai-agent 0.0.24 → 0.0.25
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/lib/core/agent-chain.js +1 -2
- package/lib/core/agent-graph.d.ts +21 -0
- package/lib/core/agent-graph.js +166 -54
- package/lib/core/agent.js +2 -1
- package/lib/model/AgentGraphModel.d.ts +5 -7
- package/lib/model/AgentGraphModel.js +55 -40
- package/lib/tools/filesystem/index.js +17 -29
- package/lib/tools/ts-lsp/index.js +5 -7
- package/lib/types/type.d.ts +6 -1
- package/lib/utils/convertToLangChainTool.js +1 -2
- package/lib/utils/createTool.d.ts +5 -2
- package/lib/utils/createTool.js +0 -10
- package/lib/utils/jsonSchemaToZod.d.ts +0 -10
- package/lib/utils/jsonSchemaToZod.js +2 -9
- package/package.json +2 -3
package/lib/core/agent-chain.js
CHANGED
|
@@ -46,7 +46,6 @@ const tools_1 = require("@langchain/core/tools");
|
|
|
46
46
|
const openai_1 = require("@langchain/openai");
|
|
47
47
|
const config_1 = require("../config/config");
|
|
48
48
|
const builtin_1 = require("../tools/builtin");
|
|
49
|
-
const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
|
|
50
49
|
class McpChainAgent {
|
|
51
50
|
constructor(options) {
|
|
52
51
|
this.allTools = [];
|
|
@@ -125,7 +124,7 @@ class McpChainAgent {
|
|
|
125
124
|
const langchainTools = this.allTools.map(t => new tools_1.DynamicStructuredTool({
|
|
126
125
|
name: t.function.name,
|
|
127
126
|
description: t.function.description || "",
|
|
128
|
-
schema: (
|
|
127
|
+
schema: (t.function.parameters),
|
|
129
128
|
func: async (args) => await t._handler(args),
|
|
130
129
|
}));
|
|
131
130
|
const prompt = prompts_1.PromptTemplate.fromTemplate(`{system_prompt}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { BaseMessage } from "@langchain/core/messages";
|
|
2
2
|
import { AgentOptions } from "../types/type";
|
|
3
3
|
export declare const CONFIG_FILE: string;
|
|
4
|
+
interface TokenUsage {
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
4
7
|
declare const AgentState: import("@langchain/langgraph").AnnotationRoot<{
|
|
5
8
|
messages: import("@langchain/langgraph").BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;
|
|
6
9
|
auditedFiles: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
7
10
|
targetCount: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
8
11
|
mode: import("@langchain/langgraph").BinaryOperatorAggregate<"chat" | "auto", "chat" | "auto">;
|
|
12
|
+
tokenUsage: import("@langchain/langgraph").BinaryOperatorAggregate<TokenUsage, TokenUsage>;
|
|
13
|
+
totalDuration: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
9
14
|
}>;
|
|
10
15
|
export default class McpGraphAgent {
|
|
11
16
|
private model;
|
|
@@ -15,6 +20,8 @@ export default class McpGraphAgent {
|
|
|
15
20
|
private checkpointer;
|
|
16
21
|
private langchainTools;
|
|
17
22
|
private stopLoadingFunc;
|
|
23
|
+
private verbose;
|
|
24
|
+
private alwaysSystem;
|
|
18
25
|
constructor(options?: AgentOptions);
|
|
19
26
|
private showLoading;
|
|
20
27
|
private startLoading;
|
|
@@ -26,30 +33,44 @@ export default class McpGraphAgent {
|
|
|
26
33
|
private renderOutput;
|
|
27
34
|
callModel(state: typeof AgentState.State): Promise<{
|
|
28
35
|
messages: unknown[];
|
|
36
|
+
tokenUsage: {
|
|
37
|
+
total: number;
|
|
38
|
+
};
|
|
39
|
+
totalDuration: number;
|
|
29
40
|
}>;
|
|
41
|
+
private getRecentToolCalls;
|
|
30
42
|
trackProgress(state: typeof AgentState.State): Promise<{
|
|
31
43
|
auditedFiles: string[];
|
|
32
44
|
}>;
|
|
45
|
+
private printFinalSummary;
|
|
33
46
|
createGraph(): Promise<import("@langchain/langgraph").CompiledStateGraph<import("@langchain/langgraph").StateType<{
|
|
34
47
|
messages: import("@langchain/langgraph").BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;
|
|
35
48
|
auditedFiles: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
36
49
|
targetCount: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
37
50
|
mode: import("@langchain/langgraph").BinaryOperatorAggregate<"chat" | "auto", "chat" | "auto">;
|
|
51
|
+
tokenUsage: import("@langchain/langgraph").BinaryOperatorAggregate<TokenUsage, TokenUsage>;
|
|
52
|
+
totalDuration: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
38
53
|
}>, import("@langchain/langgraph").UpdateType<{
|
|
39
54
|
messages: import("@langchain/langgraph").BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;
|
|
40
55
|
auditedFiles: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
41
56
|
targetCount: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
42
57
|
mode: import("@langchain/langgraph").BinaryOperatorAggregate<"chat" | "auto", "chat" | "auto">;
|
|
58
|
+
tokenUsage: import("@langchain/langgraph").BinaryOperatorAggregate<TokenUsage, TokenUsage>;
|
|
59
|
+
totalDuration: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
43
60
|
}>, "tools" | "agent" | "__start__" | "progress", {
|
|
44
61
|
messages: import("@langchain/langgraph").BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;
|
|
45
62
|
auditedFiles: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
46
63
|
targetCount: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
47
64
|
mode: import("@langchain/langgraph").BinaryOperatorAggregate<"chat" | "auto", "chat" | "auto">;
|
|
65
|
+
tokenUsage: import("@langchain/langgraph").BinaryOperatorAggregate<TokenUsage, TokenUsage>;
|
|
66
|
+
totalDuration: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
48
67
|
}, {
|
|
49
68
|
messages: import("@langchain/langgraph").BinaryOperatorAggregate<BaseMessage[], BaseMessage[]>;
|
|
50
69
|
auditedFiles: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
51
70
|
targetCount: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
52
71
|
mode: import("@langchain/langgraph").BinaryOperatorAggregate<"chat" | "auto", "chat" | "auto">;
|
|
72
|
+
tokenUsage: import("@langchain/langgraph").BinaryOperatorAggregate<TokenUsage, TokenUsage>;
|
|
73
|
+
totalDuration: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
|
|
53
74
|
}, import("@langchain/langgraph").StateDefinition>>;
|
|
54
75
|
}
|
|
55
76
|
export {};
|
package/lib/core/agent-graph.js
CHANGED
|
@@ -13,17 +13,20 @@ const readline_1 = __importDefault(require("readline"));
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const events_1 = require("events");
|
|
16
17
|
const builtin_1 = require("../tools/builtin");
|
|
17
18
|
const convertToLangChainTool_1 = require("../utils/convertToLangChainTool");
|
|
18
19
|
exports.CONFIG_FILE = path_1.default.join(os_1.default.homedir(), ".saber2pr-agent.json");
|
|
19
|
-
//
|
|
20
|
+
// ✅ 全局设置:修复 AbortSignal 监听器数量警告
|
|
21
|
+
// LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
|
|
22
|
+
events_1.EventEmitter.defaultMaxListeners = 100;
|
|
23
|
+
// --- 2. 定义状态 (State) ---
|
|
20
24
|
const AgentState = langgraph_1.Annotation.Root({
|
|
21
25
|
messages: (0, langgraph_1.Annotation)({
|
|
22
26
|
reducer: (x, y) => x.concat(y),
|
|
23
27
|
default: () => [],
|
|
24
28
|
}),
|
|
25
29
|
auditedFiles: (0, langgraph_1.Annotation)({
|
|
26
|
-
// 确保列表是累加且去重的
|
|
27
30
|
reducer: (x, y) => Array.from(new Set([...x, ...y])),
|
|
28
31
|
default: () => [],
|
|
29
32
|
}),
|
|
@@ -35,17 +38,33 @@ const AgentState = langgraph_1.Annotation.Root({
|
|
|
35
38
|
reducer: (x, y) => y !== null && y !== void 0 ? y : x,
|
|
36
39
|
default: () => "chat",
|
|
37
40
|
}),
|
|
41
|
+
// ✅ Token 累加器
|
|
42
|
+
tokenUsage: (0, langgraph_1.Annotation)({
|
|
43
|
+
reducer: (x, y) => ({
|
|
44
|
+
total: ((x === null || x === void 0 ? void 0 : x.total) || 0) + ((y === null || y === void 0 ? void 0 : y.total) || 0),
|
|
45
|
+
}),
|
|
46
|
+
default: () => ({ total: 0 }),
|
|
47
|
+
}),
|
|
48
|
+
// ✅ 耗时累加器
|
|
49
|
+
totalDuration: (0, langgraph_1.Annotation)({
|
|
50
|
+
reducer: (x, y) => (x || 0) + (y || 0),
|
|
51
|
+
default: () => 0,
|
|
52
|
+
}),
|
|
38
53
|
});
|
|
39
54
|
class McpGraphAgent {
|
|
40
55
|
constructor(options = {}) {
|
|
41
56
|
this.checkpointer = new langgraph_1.MemorySaver();
|
|
42
57
|
this.langchainTools = [];
|
|
43
|
-
// ✅ 存储清理 loading 的函数
|
|
44
58
|
this.stopLoadingFunc = null;
|
|
45
59
|
this.options = options;
|
|
60
|
+
this.verbose = options.verbose || false;
|
|
61
|
+
this.alwaysSystem = options.alwaysSystem || true;
|
|
46
62
|
this.targetDir = options.targetDir || process.cwd();
|
|
47
63
|
process.setMaxListeners(100);
|
|
48
|
-
// ✅
|
|
64
|
+
// ✅ 修复 AbortSignal 监听器数量警告
|
|
65
|
+
// LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
|
|
66
|
+
// 设置 EventEmitter 的默认 maxListeners,这会影响所有事件发射器(包括 AbortSignal)
|
|
67
|
+
events_1.EventEmitter.defaultMaxListeners = 100;
|
|
49
68
|
const cleanup = () => {
|
|
50
69
|
this.stopLoading();
|
|
51
70
|
process.stdout.write('\u001B[?25h');
|
|
@@ -53,15 +72,14 @@ class McpGraphAgent {
|
|
|
53
72
|
};
|
|
54
73
|
process.on("SIGINT", cleanup);
|
|
55
74
|
process.on("SIGTERM", cleanup);
|
|
75
|
+
// ✅ 初始化内置工具
|
|
56
76
|
const builtinToolInfos = (0, builtin_1.createDefaultBuiltinTools)({ options });
|
|
57
77
|
this.langchainTools = [...builtinToolInfos, ...(options.tools || [])].map((t) => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
|
|
58
78
|
this.toolNode = new prebuilt_1.ToolNode(this.langchainTools);
|
|
59
79
|
}
|
|
60
|
-
// ✅ 1. 核心 Loading 动画效果
|
|
61
80
|
showLoading(text) {
|
|
62
81
|
const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
63
82
|
let i = 0;
|
|
64
|
-
// 隐藏光标
|
|
65
83
|
process.stdout.write('\u001B[?25l');
|
|
66
84
|
const timer = setInterval(() => {
|
|
67
85
|
process.stdout.write(`\r\x1b[36m${chars[i]}\x1b[0m ${text}`);
|
|
@@ -69,8 +87,8 @@ class McpGraphAgent {
|
|
|
69
87
|
}, 80);
|
|
70
88
|
return () => {
|
|
71
89
|
clearInterval(timer);
|
|
72
|
-
process.stdout.write('\r\x1b[K');
|
|
73
|
-
process.stdout.write('\u001B[?25h');
|
|
90
|
+
process.stdout.write('\r\x1b[K');
|
|
91
|
+
process.stdout.write('\u001B[?25h');
|
|
74
92
|
};
|
|
75
93
|
}
|
|
76
94
|
startLoading(text) {
|
|
@@ -111,7 +129,6 @@ class McpGraphAgent {
|
|
|
111
129
|
if (!config.baseURL || !config.apiKey) {
|
|
112
130
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
113
131
|
const question = (q) => new Promise((res) => rl.question(q, res));
|
|
114
|
-
console.log(`💡 首次运行请配置信息:`);
|
|
115
132
|
config.baseURL = config.baseURL || await question(`? API Base URL: `);
|
|
116
133
|
config.apiKey = config.apiKey || await question(`? API Key: `);
|
|
117
134
|
config.model = config.model || await question(`? Model Name: `) || "gpt-4o";
|
|
@@ -126,11 +143,10 @@ class McpGraphAgent {
|
|
|
126
143
|
const stream = await app.stream({
|
|
127
144
|
messages: [new messages_1.HumanMessage(query)],
|
|
128
145
|
mode: "auto",
|
|
129
|
-
targetCount: 4
|
|
130
|
-
}, { configurable: { thread_id: "auto_worker" }, recursionLimit: 100 });
|
|
146
|
+
targetCount: 4,
|
|
147
|
+
}, { configurable: { thread_id: "auto_worker" }, recursionLimit: 100, debug: this.verbose });
|
|
131
148
|
for await (const output of stream)
|
|
132
149
|
this.renderOutput(output);
|
|
133
|
-
console.log("\n✅ 审计任务已完成。");
|
|
134
150
|
}
|
|
135
151
|
async start() {
|
|
136
152
|
await this.getModel();
|
|
@@ -142,7 +158,6 @@ class McpGraphAgent {
|
|
|
142
158
|
process.stdout.write('\u001B[?25h');
|
|
143
159
|
process.exit(0);
|
|
144
160
|
});
|
|
145
|
-
console.log(`\n💬 已进入交互审计模式 (Thread: session)`);
|
|
146
161
|
const ask = () => {
|
|
147
162
|
rl.question("> ", async (input) => {
|
|
148
163
|
if (input.toLowerCase() === "exit") {
|
|
@@ -158,80 +173,173 @@ class McpGraphAgent {
|
|
|
158
173
|
ask();
|
|
159
174
|
}
|
|
160
175
|
renderOutput(output) {
|
|
161
|
-
var _a, _b;
|
|
162
|
-
this.stopLoading(); //
|
|
163
|
-
// 1. 处理 Agent 节点的输出
|
|
176
|
+
var _a, _b, _c;
|
|
177
|
+
this.stopLoading(); // 停止加载动画
|
|
164
178
|
const agentNode = output.agent;
|
|
179
|
+
// ✅ 打印工具执行结果(tools 节点的输出)
|
|
180
|
+
const toolsNode = output.tools;
|
|
181
|
+
if (toolsNode && toolsNode.messages) {
|
|
182
|
+
const toolMessages = Array.isArray(toolsNode.messages) ? toolsNode.messages : [];
|
|
183
|
+
// 获取最近的 AI 消息以匹配 tool_call_id
|
|
184
|
+
const lastAiMsg = (_a = agentNode === null || agentNode === void 0 ? void 0 : agentNode.messages) === null || _a === void 0 ? void 0 : _a[agentNode.messages.length - 1];
|
|
185
|
+
const toolCallMap = new Map();
|
|
186
|
+
if (lastAiMsg === null || lastAiMsg === void 0 ? void 0 : lastAiMsg.tool_calls) {
|
|
187
|
+
lastAiMsg.tool_calls.forEach((tc) => {
|
|
188
|
+
if (tc.id)
|
|
189
|
+
toolCallMap.set(tc.id, tc.name);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
toolMessages.forEach((msg) => {
|
|
193
|
+
// ToolMessage 有 tool_call_id 字段
|
|
194
|
+
const toolCallId = msg.tool_call_id || msg.id;
|
|
195
|
+
if (toolCallId) {
|
|
196
|
+
const toolName = toolCallMap.get(toolCallId) || msg.name || 'unknown';
|
|
197
|
+
const content = typeof msg.content === 'string'
|
|
198
|
+
? msg.content
|
|
199
|
+
: JSON.stringify(msg.content);
|
|
200
|
+
// 如果内容太长,截断显示
|
|
201
|
+
const displayContent = content.length > 500
|
|
202
|
+
? content.substring(0, 500) + '...'
|
|
203
|
+
: content;
|
|
204
|
+
console.log(`✅ [工具结果] ${toolName}: ${displayContent}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
165
208
|
if (agentNode) {
|
|
166
|
-
const msg = agentNode.messages[
|
|
167
|
-
|
|
209
|
+
const msg = agentNode.messages[agentNode.messages.length - 1];
|
|
210
|
+
// 1. 打印思考过程(如果有)
|
|
211
|
+
const reasoning = (_b = msg.additional_kwargs) === null || _b === void 0 ? void 0 : _b.reasoning;
|
|
168
212
|
if (reasoning) {
|
|
169
|
-
console.log(
|
|
213
|
+
console.log(`\n🧠 [思考]: ${reasoning}`);
|
|
214
|
+
}
|
|
215
|
+
// 2. 打印 AI 回复内容
|
|
216
|
+
if (msg.content) {
|
|
217
|
+
console.log(`🤖 [AI]: ${msg.content}`);
|
|
218
|
+
}
|
|
219
|
+
// ✅ 3. 实时打印当次统计信息
|
|
220
|
+
// 这里的 meta 数据是从 AgentGraphModel 的 _generate 中塞进去的
|
|
221
|
+
const meta = msg.response_metadata || {};
|
|
222
|
+
const token = meta.token || 0;
|
|
223
|
+
const duration = meta.duration || 0;
|
|
224
|
+
if (token > 0 || duration > 0) {
|
|
225
|
+
process.stdout.write(`📊 \x1b[2m[实时统计] 消耗: ${token} tokens | 耗时: ${duration}ms\x1b[0m\n`);
|
|
170
226
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if ((_b = msg.tool_calls) === null || _b === void 0 ? void 0 : _b.length) {
|
|
227
|
+
// 4. 打印工具调用情况
|
|
228
|
+
if ((_c = msg.tool_calls) === null || _c === void 0 ? void 0 : _c.length) {
|
|
174
229
|
msg.tool_calls.forEach((call) => {
|
|
175
230
|
console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
|
|
176
231
|
});
|
|
177
232
|
}
|
|
178
233
|
}
|
|
179
|
-
// 2. 处理工具节点的简要反馈(防止大数据量锁死终端)
|
|
180
|
-
if (output.tools) {
|
|
181
|
-
console.log(`✅ [工具执行完毕]`);
|
|
182
|
-
}
|
|
183
234
|
}
|
|
184
235
|
async callModel(state) {
|
|
185
236
|
const auditedListStr = state.auditedFiles.length > 0
|
|
186
237
|
? state.auditedFiles.map(f => `\n - ${f}`).join("")
|
|
187
238
|
: "暂无";
|
|
188
|
-
const
|
|
189
|
-
|
|
239
|
+
const recentToolCalls = this.getRecentToolCalls(state.messages);
|
|
240
|
+
const recentToolCallsStr = recentToolCalls.length > 0
|
|
241
|
+
? `\n\n⚠️ 最近调用的工具(避免重复调用相同工具和参数):\n${recentToolCalls.map(tc => ` - ${tc.name}(${JSON.stringify(tc.args)})`).join("\n")}`
|
|
242
|
+
: "";
|
|
243
|
+
// 1. 构建当前的系统提示词模板
|
|
244
|
+
const systemPromptTemplate = `你是一个代码专家。工作目录:${this.targetDir}。
|
|
190
245
|
当前模式:{mode}
|
|
191
246
|
进度:{doneCount}/{targetCount}
|
|
192
247
|
已审计文件:{auditedList}
|
|
193
248
|
|
|
249
|
+
# 格式要求
|
|
250
|
+
1. Arguments 必须是纯粹的 JSON 对象,严禁将其作为字符串放入引号中。
|
|
251
|
+
2. 严禁对 JSON 内容进行二次转义。
|
|
252
|
+
|
|
194
253
|
# 指令
|
|
195
|
-
1.
|
|
196
|
-
2.
|
|
197
|
-
3.
|
|
198
|
-
{extraPrompt}
|
|
254
|
+
1. 一旦完成对一个文件的修改(apply_fix),请立即调用 generate_review 总结该文件的变动。
|
|
255
|
+
2. 避免陷入在同一个文件上的无限循环尝试。
|
|
256
|
+
3. 不要重复调用相同的工具和参数,如果工具已经返回结果,请基于结果继续工作而不是再次调用。{recentToolCalls}
|
|
257
|
+
{extraPrompt}`;
|
|
258
|
+
// 2. 核心逻辑:处理消息上下文
|
|
259
|
+
let inputMessages;
|
|
260
|
+
// ✅ 检查 options 中的 alwaysSystem 参数 (默认为 true 或根据你的需求设置)
|
|
261
|
+
// 如果不希望每次都携带(即只在首轮携带),则过滤掉历史消息里的 SystemMessage
|
|
262
|
+
if (this.options.alwaysSystem === false) {
|
|
263
|
+
inputMessages = state.messages.filter(msg => msg._getType() !== "system");
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// 默认模式:保持干净,由 PromptTemplate 重新生成最新的 System 状态
|
|
267
|
+
inputMessages = state.messages.filter(msg => msg._getType() !== "system");
|
|
268
|
+
}
|
|
269
|
+
const prompt = prompts_1.ChatPromptTemplate.fromMessages([
|
|
270
|
+
["system", systemPromptTemplate],
|
|
199
271
|
new prompts_1.MessagesPlaceholder("messages"),
|
|
200
272
|
]);
|
|
201
273
|
this.startLoading("AI 正在分析并思考中");
|
|
202
274
|
try {
|
|
203
275
|
const chain = prompt.pipe(this.model);
|
|
204
276
|
const response = await chain.invoke({
|
|
205
|
-
messages:
|
|
277
|
+
messages: inputMessages, // ✅ 使用处理后的消息列表
|
|
206
278
|
mode: state.mode,
|
|
207
279
|
targetCount: state.targetCount,
|
|
208
280
|
doneCount: state.auditedFiles.length,
|
|
209
281
|
auditedList: auditedListStr,
|
|
282
|
+
recentToolCalls: recentToolCallsStr,
|
|
210
283
|
extraPrompt: this.options.extraSystemPrompt || "",
|
|
211
284
|
});
|
|
212
285
|
this.stopLoading();
|
|
213
|
-
|
|
286
|
+
const meta = response.response_metadata || {};
|
|
287
|
+
const currentToken = Number(meta.token) || 0;
|
|
288
|
+
const currentDuration = Number(meta.duration) || 0;
|
|
289
|
+
return {
|
|
290
|
+
messages: [response],
|
|
291
|
+
tokenUsage: { total: currentToken },
|
|
292
|
+
totalDuration: currentDuration
|
|
293
|
+
};
|
|
214
294
|
}
|
|
215
295
|
catch (error) {
|
|
216
296
|
this.stopLoading();
|
|
217
297
|
throw error;
|
|
218
298
|
}
|
|
219
299
|
}
|
|
300
|
+
getRecentToolCalls(messages, limit = 5) {
|
|
301
|
+
var _a;
|
|
302
|
+
const toolCalls = [];
|
|
303
|
+
// 从后往前遍历消息,收集最近的工具调用
|
|
304
|
+
for (let i = messages.length - 1; i >= 0 && toolCalls.length < limit; i--) {
|
|
305
|
+
const msg = messages[i];
|
|
306
|
+
if ((_a = msg.tool_calls) === null || _a === void 0 ? void 0 : _a.length) {
|
|
307
|
+
for (const tc of msg.tool_calls) {
|
|
308
|
+
toolCalls.push({ name: tc.name, args: tc.args });
|
|
309
|
+
if (toolCalls.length >= limit)
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return toolCalls;
|
|
315
|
+
}
|
|
220
316
|
async trackProgress(state) {
|
|
221
317
|
var _a;
|
|
222
318
|
const lastAiMsg = state.messages[state.messages.length - 1];
|
|
223
|
-
const currentAudited =
|
|
319
|
+
const currentAudited = new Set(state.auditedFiles);
|
|
224
320
|
if ((_a = lastAiMsg === null || lastAiMsg === void 0 ? void 0 : lastAiMsg.tool_calls) === null || _a === void 0 ? void 0 : _a.length) {
|
|
225
321
|
for (const tc of lastAiMsg.tool_calls) {
|
|
226
|
-
//
|
|
227
|
-
const file = tc.args.path || tc.args.filePath || tc.args.file;
|
|
322
|
+
// 兼容所有可能的路径参数字段
|
|
323
|
+
const file = tc.args.path || tc.args.filePath || tc.args.file || tc.args.file_path;
|
|
228
324
|
if (file && typeof file === 'string') {
|
|
229
|
-
currentAudited.
|
|
325
|
+
currentAudited.add(file);
|
|
230
326
|
}
|
|
231
327
|
}
|
|
232
328
|
}
|
|
233
|
-
|
|
234
|
-
|
|
329
|
+
return { auditedFiles: Array.from(currentAudited) };
|
|
330
|
+
}
|
|
331
|
+
printFinalSummary(state) {
|
|
332
|
+
var _a;
|
|
333
|
+
const totalTokens = ((_a = state.tokenUsage) === null || _a === void 0 ? void 0 : _a.total) || 0;
|
|
334
|
+
const totalMs = state.totalDuration || 0;
|
|
335
|
+
if (totalTokens > 0 || totalMs > 0) {
|
|
336
|
+
console.log("\n" + "═".repeat(50));
|
|
337
|
+
console.log(`🏁 \x1b[32;1m[审计任务全量结算]\x1b[0m`);
|
|
338
|
+
console.log(` - 累计消耗总额: \x1b[33m${totalTokens}\x1b[0m Tokens`);
|
|
339
|
+
console.log(` - 累计执行耗时: \x1b[36m${(totalMs / 1000).toFixed(2)}\x1b[0m s`);
|
|
340
|
+
console.log(` - 审计文件总数: ${state.auditedFiles.length} 个`);
|
|
341
|
+
console.log("═".repeat(50) + "\n");
|
|
342
|
+
}
|
|
235
343
|
}
|
|
236
344
|
async createGraph() {
|
|
237
345
|
const workflow = new langgraph_1.StateGraph(AgentState)
|
|
@@ -240,25 +348,29 @@ class McpGraphAgent {
|
|
|
240
348
|
.addNode("progress", (state) => this.trackProgress(state))
|
|
241
349
|
.addEdge(langgraph_1.START, "agent")
|
|
242
350
|
.addConditionalEdges("agent", (state) => {
|
|
243
|
-
|
|
244
|
-
const lastMsg =
|
|
245
|
-
|
|
246
|
-
|
|
351
|
+
const messages = state.messages;
|
|
352
|
+
const lastMsg = messages[messages.length - 1];
|
|
353
|
+
const content = lastMsg.content || "";
|
|
354
|
+
// 1. 如果 AI 想要调用工具,去 tools 节点
|
|
355
|
+
if (lastMsg.tool_calls && lastMsg.tool_calls.length > 0) {
|
|
247
356
|
return "tools";
|
|
248
|
-
// 2. 自动模式下,如果审计文件数未达标,继续循环
|
|
249
|
-
if (state.mode === "auto") {
|
|
250
|
-
if (state.auditedFiles.length < state.targetCount)
|
|
251
|
-
return "agent";
|
|
252
357
|
}
|
|
253
|
-
//
|
|
358
|
+
// 2. 判定结束的条件:
|
|
359
|
+
// - 模式是 auto 且审计完成
|
|
360
|
+
// - 或者 AI 明确输出了结束语
|
|
361
|
+
// - 或者 AI 输出了普通内容且没有工具调用(针对问答模式)
|
|
362
|
+
const isAutoFinished = state.mode === "auto" && state.auditedFiles.length >= state.targetCount;
|
|
363
|
+
const isFinalAnswer = content.includes("Final Answer");
|
|
364
|
+
// ✅ 修复核心:如果 AI 只是在聊天(没有工具调用),直接结束,不要跳回 agent
|
|
365
|
+
if (isAutoFinished || isFinalAnswer || state.mode === "chat") {
|
|
366
|
+
this.printFinalSummary(state);
|
|
367
|
+
return langgraph_1.END;
|
|
368
|
+
}
|
|
369
|
+
// 兜底:如果是在 auto 模式且还没干完活,才跳回 agent(通常不会走到这里)
|
|
254
370
|
return langgraph_1.END;
|
|
255
|
-
}, {
|
|
256
|
-
tools: "tools",
|
|
257
|
-
agent: "agent",
|
|
258
|
-
[langgraph_1.END]: langgraph_1.END,
|
|
259
371
|
})
|
|
260
372
|
.addEdge("tools", "progress")
|
|
261
|
-
.addEdge("progress", "agent");
|
|
373
|
+
.addEdge("progress", "agent");
|
|
262
374
|
return workflow.compile({ checkpointer: this.checkpointer });
|
|
263
375
|
}
|
|
264
376
|
}
|
package/lib/core/agent.js
CHANGED
|
@@ -46,6 +46,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
|
46
46
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
47
47
|
const config_1 = require("../config/config");
|
|
48
48
|
const builtin_1 = require("../tools/builtin");
|
|
49
|
+
const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
|
|
49
50
|
class McpAgent {
|
|
50
51
|
constructor(options) {
|
|
51
52
|
this.modelName = "";
|
|
@@ -202,7 +203,7 @@ class McpAgent {
|
|
|
202
203
|
function: {
|
|
203
204
|
name: `${name}__${t.name}`,
|
|
204
205
|
description: t.description,
|
|
205
|
-
parameters: t.inputSchema,
|
|
206
|
+
parameters: (0, jsonSchemaToZod_1.jsonSchemaToZod)(t.inputSchema),
|
|
206
207
|
},
|
|
207
208
|
_originalName: t.name,
|
|
208
209
|
_client: client,
|
|
@@ -4,18 +4,16 @@ import { ChatResult } from '@langchain/core/outputs';
|
|
|
4
4
|
export interface AgentGraphLLMResponse {
|
|
5
5
|
text: string;
|
|
6
6
|
reasoning?: string;
|
|
7
|
-
|
|
7
|
+
token?: number;
|
|
8
|
+
duration?: number;
|
|
8
9
|
}
|
|
9
10
|
export declare abstract class AgentGraphModel extends BaseChatModel {
|
|
10
11
|
protected boundTools?: any[];
|
|
11
|
-
|
|
12
|
-
constructor(fields?: BaseChatModelParams & {
|
|
13
|
-
chatId?: string;
|
|
14
|
-
});
|
|
12
|
+
constructor(fields?: BaseChatModelParams);
|
|
15
13
|
bindTools(tools: any[]): any;
|
|
16
|
-
abstract callApi(prompt: string
|
|
14
|
+
abstract callApi(prompt: string): Promise<AgentGraphLLMResponse>;
|
|
17
15
|
_generate(messages: BaseMessage[]): Promise<ChatResult>;
|
|
18
|
-
private serializeMessages;
|
|
19
16
|
private parseToolCalls;
|
|
17
|
+
private serializeMessages;
|
|
20
18
|
_llmType(): string;
|
|
21
19
|
}
|
|
@@ -7,7 +7,6 @@ const function_calling_1 = require("@langchain/core/utils/function_calling");
|
|
|
7
7
|
class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
8
8
|
constructor(fields) {
|
|
9
9
|
super(fields || {});
|
|
10
|
-
this.chatId = fields === null || fields === void 0 ? void 0 : fields.chatId;
|
|
11
10
|
}
|
|
12
11
|
bindTools(tools) {
|
|
13
12
|
this.boundTools = tools.map(t => (0, function_calling_1.convertToOpenAITool)(t));
|
|
@@ -15,11 +14,9 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
15
14
|
}
|
|
16
15
|
async _generate(messages) {
|
|
17
16
|
const fullPrompt = this.serializeMessages(messages);
|
|
18
|
-
const response = await this.callApi(fullPrompt
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let { text, reasoning } = response;
|
|
22
|
-
// ✅ 通用逻辑:解析 <think> 标签
|
|
17
|
+
const response = await this.callApi(fullPrompt);
|
|
18
|
+
let { text, reasoning, token, duration } = response;
|
|
19
|
+
// 1. 处理思考内容
|
|
23
20
|
if (!reasoning && text.includes("<think>")) {
|
|
24
21
|
const match = text.match(/<think>([\s\S]*?)<\/think>/);
|
|
25
22
|
if (match) {
|
|
@@ -27,18 +24,69 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
27
24
|
text = text.replace(/<think>[\s\S]*?<\/think>/, "").trim();
|
|
28
25
|
}
|
|
29
26
|
}
|
|
27
|
+
// 2. 解析工具调用
|
|
30
28
|
const toolCalls = this.parseToolCalls(text);
|
|
29
|
+
// AgentGraphModel.ts 的 _generate 方法内
|
|
31
30
|
return {
|
|
32
31
|
generations: [{
|
|
33
32
|
text,
|
|
34
33
|
message: new messages_1.AIMessage({
|
|
35
34
|
content: text,
|
|
36
35
|
tool_calls: toolCalls,
|
|
37
|
-
additional_kwargs: {
|
|
36
|
+
additional_kwargs: {
|
|
37
|
+
reasoning: reasoning || "",
|
|
38
|
+
token: response.token, // 👈 必须
|
|
39
|
+
duration: response.duration, // 👈 必须
|
|
40
|
+
},
|
|
41
|
+
response_metadata: {
|
|
42
|
+
reasoning: reasoning || "",
|
|
43
|
+
token: response.token, // 👈 McpGraphAgent 读取路径
|
|
44
|
+
duration: response.duration,
|
|
45
|
+
}
|
|
38
46
|
})
|
|
39
47
|
}]
|
|
40
48
|
};
|
|
41
49
|
}
|
|
50
|
+
parseToolCalls(text) {
|
|
51
|
+
const actionMatch = text.match(/Action:\s*(\w+)/);
|
|
52
|
+
const argsMatch = text.match(/Arguments:\s*({[\s\S]*?})(?=\n|$)/);
|
|
53
|
+
if (!actionMatch)
|
|
54
|
+
return [];
|
|
55
|
+
let args = {};
|
|
56
|
+
if (argsMatch) {
|
|
57
|
+
try {
|
|
58
|
+
let raw = argsMatch[1].trim();
|
|
59
|
+
// ✅ 关键修复:递归解析,直到它变成真正的对象
|
|
60
|
+
// 这能处理 "\"{\\\"path\\\":...}\"" 这种套娃字符串
|
|
61
|
+
let safetyDepth = 0;
|
|
62
|
+
while (typeof raw === 'string' && safetyDepth < 5) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
66
|
+
raw = parsed;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
raw = parsed; // 如果解析后还是 string,继续剥
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
break; // 解析不动了,跳出
|
|
73
|
+
}
|
|
74
|
+
safetyDepth++;
|
|
75
|
+
}
|
|
76
|
+
args = raw;
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
args = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ✅ 此时返回的 args 必须是 object 类型
|
|
83
|
+
return [{
|
|
84
|
+
name: actionMatch[1],
|
|
85
|
+
args: typeof args === 'object' ? args : {},
|
|
86
|
+
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
87
|
+
type: "tool_call",
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
42
90
|
serializeMessages(messages) {
|
|
43
91
|
const systemMsg = messages.find(m => m._getType() === 'system');
|
|
44
92
|
const lastMsg = messages[messages.length - 1];
|
|
@@ -58,39 +106,6 @@ ${format(lastMsg)}
|
|
|
58
106
|
3. Arguments: {JSON}
|
|
59
107
|
`.trim();
|
|
60
108
|
}
|
|
61
|
-
parseToolCalls(text) {
|
|
62
|
-
const actionMatch = text.match(/Action:\s*(\w+)/);
|
|
63
|
-
const argsMatch = text.match(/Arguments:\s*({[\s\S]*})/);
|
|
64
|
-
if (!actionMatch)
|
|
65
|
-
return [];
|
|
66
|
-
let args = {};
|
|
67
|
-
if (argsMatch) {
|
|
68
|
-
try {
|
|
69
|
-
// 强力解析逻辑,处理物理换行
|
|
70
|
-
const rawArgs = argsMatch[1].trim().replace(/\n/g, "\\n");
|
|
71
|
-
args = JSON.parse(rawArgs);
|
|
72
|
-
// 参数映射
|
|
73
|
-
const anyArgs = args;
|
|
74
|
-
if (anyArgs.file_path && !anyArgs.path)
|
|
75
|
-
anyArgs.path = anyArgs.file_path;
|
|
76
|
-
if (anyArgs.filePath && !anyArgs.path)
|
|
77
|
-
anyArgs.path = anyArgs.filePath;
|
|
78
|
-
if (anyArgs.path && !anyArgs.filePath)
|
|
79
|
-
anyArgs.filePath = anyArgs.path;
|
|
80
|
-
if (anyArgs.file && !anyArgs.filePath)
|
|
81
|
-
anyArgs.filePath = anyArgs.file;
|
|
82
|
-
}
|
|
83
|
-
catch (e) {
|
|
84
|
-
console.warn("JSON Parse Error", e);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return [{
|
|
88
|
-
name: actionMatch[1],
|
|
89
|
-
args,
|
|
90
|
-
id: `call_${Date.now()}`,
|
|
91
|
-
type: "tool_call",
|
|
92
|
-
}];
|
|
93
|
-
}
|
|
94
109
|
_llmType() { return "agent_graph_model"; }
|
|
95
110
|
}
|
|
96
111
|
exports.AgentGraphModel = AgentGraphModel;
|
|
@@ -9,13 +9,12 @@ const minimatch_1 = require("minimatch");
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const zod_1 = require("zod");
|
|
11
11
|
const createTool_1 = require("../../utils/createTool");
|
|
12
|
-
const jsonSchemaToZod_1 = require("../../utils/jsonSchemaToZod");
|
|
13
12
|
const lib_1 = require("./lib");
|
|
14
13
|
// Schema definitions
|
|
15
14
|
const ReadTextFileArgsSchema = zod_1.z.object({
|
|
16
15
|
path: zod_1.z.string(),
|
|
17
|
-
tail: zod_1.z.number().
|
|
18
|
-
head: zod_1.z.number().
|
|
16
|
+
tail: zod_1.z.number().nullable().describe('If provided, returns only the last N lines of the file'),
|
|
17
|
+
head: zod_1.z.number().nullable().describe('If provided, returns only the first N lines of the file')
|
|
19
18
|
});
|
|
20
19
|
const ReadMultipleFilesArgsSchema = zod_1.z.object({
|
|
21
20
|
paths: zod_1.z
|
|
@@ -34,7 +33,7 @@ const EditOperation = zod_1.z.object({
|
|
|
34
33
|
const EditFileArgsSchema = zod_1.z.object({
|
|
35
34
|
path: zod_1.z.string(),
|
|
36
35
|
edits: zod_1.z.array(EditOperation),
|
|
37
|
-
dryRun: zod_1.z.boolean().default(false).describe('Preview changes using git-style diff format')
|
|
36
|
+
dryRun: zod_1.z.boolean().nullable().default(false).describe('Preview changes using git-style diff format')
|
|
38
37
|
});
|
|
39
38
|
const CreateDirectoryArgsSchema = zod_1.z.object({
|
|
40
39
|
path: zod_1.z.string(),
|
|
@@ -44,11 +43,11 @@ const ListDirectoryArgsSchema = zod_1.z.object({
|
|
|
44
43
|
});
|
|
45
44
|
const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
|
|
46
45
|
path: zod_1.z.string(),
|
|
47
|
-
sortBy: zod_1.z.enum(['name', 'size']).
|
|
46
|
+
sortBy: zod_1.z.enum(['name', 'size']).nullable().default('name').describe('Sort entries by name or size'),
|
|
48
47
|
});
|
|
49
48
|
const DirectoryTreeArgsSchema = zod_1.z.object({
|
|
50
49
|
path: zod_1.z.string(),
|
|
51
|
-
excludePatterns: zod_1.z.array(zod_1.z.string()).
|
|
50
|
+
excludePatterns: zod_1.z.array(zod_1.z.string()).nullable().default([])
|
|
52
51
|
});
|
|
53
52
|
const MoveFileArgsSchema = zod_1.z.object({
|
|
54
53
|
source: zod_1.z.string(),
|
|
@@ -57,7 +56,7 @@ const MoveFileArgsSchema = zod_1.z.object({
|
|
|
57
56
|
const SearchFilesArgsSchema = zod_1.z.object({
|
|
58
57
|
path: zod_1.z.string(),
|
|
59
58
|
pattern: zod_1.z.string(),
|
|
60
|
-
excludePatterns: zod_1.z.array(zod_1.z.string()).
|
|
59
|
+
excludePatterns: zod_1.z.array(zod_1.z.string()).nullable().default([])
|
|
61
60
|
});
|
|
62
61
|
const GetFileInfoArgsSchema = zod_1.z.object({
|
|
63
62
|
path: zod_1.z.string(),
|
|
@@ -90,8 +89,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
90
89
|
"the contents of a single file. Use the 'head' parameter to read only " +
|
|
91
90
|
"the first N lines of a file, or the 'tail' parameter to read only " +
|
|
92
91
|
"the last N lines of a file. Operates on the file as text regardless of extension.",
|
|
93
|
-
parameters:
|
|
94
|
-
validateParams: ["path"],
|
|
92
|
+
parameters: ReadTextFileArgsSchema,
|
|
95
93
|
handler: readTextFileHandler
|
|
96
94
|
});
|
|
97
95
|
const readMultipleFilesTool = (0, createTool_1.createTool)({
|
|
@@ -101,8 +99,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
101
99
|
"or compare multiple files. Each file's content is returned with its " +
|
|
102
100
|
"path as a reference. Failed reads for individual files won't stop " +
|
|
103
101
|
"the entire operation. Only works within allowed directories.",
|
|
104
|
-
parameters:
|
|
105
|
-
validateParams: ["paths"],
|
|
102
|
+
parameters: ReadMultipleFilesArgsSchema,
|
|
106
103
|
handler: async (args) => {
|
|
107
104
|
const results = await Promise.all(args.paths.map(async (filePath) => {
|
|
108
105
|
try {
|
|
@@ -124,8 +121,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
124
121
|
description: "Create a new file or completely overwrite an existing file with new content. " +
|
|
125
122
|
"Use with caution as it will overwrite existing files without warning. " +
|
|
126
123
|
"Handles text content with proper encoding. Only works within allowed directories.",
|
|
127
|
-
parameters:
|
|
128
|
-
validateParams: ["path", "content"],
|
|
124
|
+
parameters: WriteFileArgsSchema,
|
|
129
125
|
handler: async (args) => {
|
|
130
126
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
131
127
|
await (0, lib_1.writeFileContent)(validPath, args.content);
|
|
@@ -138,8 +134,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
138
134
|
description: "Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
|
139
135
|
"with new content. Returns a git-style diff showing the changes made. " +
|
|
140
136
|
"Only works within allowed directories.",
|
|
141
|
-
parameters:
|
|
142
|
-
validateParams: ["path", "edits"],
|
|
137
|
+
parameters: EditFileArgsSchema,
|
|
143
138
|
handler: async (args) => {
|
|
144
139
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
145
140
|
const result = await (0, lib_1.applyFileEdits)(validPath, args.edits, args.dryRun);
|
|
@@ -152,8 +147,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
152
147
|
"nested directories in one operation. If the directory already exists, " +
|
|
153
148
|
"this operation will succeed silently. Perfect for setting up directory " +
|
|
154
149
|
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
|
|
155
|
-
parameters:
|
|
156
|
-
validateParams: ["path"],
|
|
150
|
+
parameters: CreateDirectoryArgsSchema,
|
|
157
151
|
handler: async (args) => {
|
|
158
152
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
159
153
|
await promises_1.default.mkdir(validPath, { recursive: true });
|
|
@@ -167,8 +161,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
167
161
|
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
|
168
162
|
"prefixes. This tool is essential for understanding directory structure and " +
|
|
169
163
|
"finding specific files within a directory. Only works within allowed directories.",
|
|
170
|
-
parameters:
|
|
171
|
-
validateParams: ["path"],
|
|
164
|
+
parameters: ListDirectoryArgsSchema,
|
|
172
165
|
handler: async (args) => {
|
|
173
166
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
174
167
|
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
@@ -184,8 +177,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
184
177
|
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
|
185
178
|
"prefixes. This tool is useful for understanding directory structure and " +
|
|
186
179
|
"finding specific files within a directory. Only works within allowed directories.",
|
|
187
|
-
parameters:
|
|
188
|
-
validateParams: ["path"],
|
|
180
|
+
parameters: ListDirectoryWithSizesArgsSchema,
|
|
189
181
|
handler: async (args) => {
|
|
190
182
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
191
183
|
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
@@ -239,8 +231,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
239
231
|
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
|
240
232
|
"Files have no children array, while directories always have a children array (which may be empty). " +
|
|
241
233
|
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
|
242
|
-
parameters:
|
|
243
|
-
validateParams: ["path"],
|
|
234
|
+
parameters: DirectoryTreeArgsSchema,
|
|
244
235
|
handler: async (args) => {
|
|
245
236
|
const rootPath = args.path;
|
|
246
237
|
async function buildTree(currentPath, excludePatterns = []) {
|
|
@@ -284,8 +275,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
284
275
|
"and rename them in a single operation. If the destination exists, the " +
|
|
285
276
|
"operation will fail. Works across different directories and can be used " +
|
|
286
277
|
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
|
|
287
|
-
parameters:
|
|
288
|
-
validateParams: ["source", "destination"],
|
|
278
|
+
parameters: MoveFileArgsSchema,
|
|
289
279
|
handler: async (args) => {
|
|
290
280
|
const validSourcePath = await (0, lib_1.validatePath)(targetDir, args.source);
|
|
291
281
|
const validDestPath = await (0, lib_1.validatePath)(targetDir, args.destination);
|
|
@@ -298,8 +288,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
298
288
|
name: "search_files",
|
|
299
289
|
description: "Search for files matching a specific pattern in a specified path. " +
|
|
300
290
|
"Returns a list of files that match the pattern. Only works within allowed directories.",
|
|
301
|
-
parameters:
|
|
302
|
-
validateParams: ["path", "pattern"],
|
|
291
|
+
parameters: SearchFilesArgsSchema,
|
|
303
292
|
handler: async (args) => {
|
|
304
293
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
305
294
|
const results = await (0, lib_1.searchFilesWithValidation)(targetDir, validPath, args.pattern, [], { excludePatterns: args.excludePatterns });
|
|
@@ -311,8 +300,7 @@ const getFilesystemTools = (targetDir) => {
|
|
|
311
300
|
name: "get_file_info",
|
|
312
301
|
description: "Get detailed information about a file, including its size, last modified time, and type. " +
|
|
313
302
|
"Only works within allowed directories.",
|
|
314
|
-
parameters:
|
|
315
|
-
validateParams: ["path"],
|
|
303
|
+
parameters: GetFileInfoArgsSchema,
|
|
316
304
|
handler: async (args) => {
|
|
317
305
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
318
306
|
const info = await (0, lib_1.getFileStats)(validPath);
|
|
@@ -3,13 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getTsLspTools = void 0;
|
|
4
4
|
const ts_context_mcp_1 = require("@saber2pr/ts-context-mcp");
|
|
5
5
|
const createTool_1 = require("../../utils/createTool");
|
|
6
|
+
const zod_1 = require("zod");
|
|
6
7
|
const getTsLspTools = (targetDir) => {
|
|
7
8
|
const engine = new ts_context_mcp_1.PromptEngine(targetDir);
|
|
8
9
|
return [
|
|
9
10
|
(0, createTool_1.createTool)({
|
|
10
11
|
name: "get_repo_map",
|
|
11
12
|
description: "获取项目全局文件结构及导出清单,用于快速定位代码",
|
|
12
|
-
parameters:
|
|
13
|
+
parameters: zod_1.z.object({}),
|
|
13
14
|
handler: async () => {
|
|
14
15
|
engine.refresh();
|
|
15
16
|
return engine.getRepoMap();
|
|
@@ -18,15 +19,13 @@ const getTsLspTools = (targetDir) => {
|
|
|
18
19
|
(0, createTool_1.createTool)({
|
|
19
20
|
name: "analyze_deps",
|
|
20
21
|
description: "分析指定文件的依赖关系,支持 tsconfig 路径别名解析",
|
|
21
|
-
parameters:
|
|
22
|
-
validateParams: ["filePath"],
|
|
22
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe("文件相对路径") }),
|
|
23
23
|
handler: async ({ filePath }) => engine.getDeps(filePath),
|
|
24
24
|
}),
|
|
25
25
|
(0, createTool_1.createTool)({
|
|
26
26
|
name: "read_skeleton",
|
|
27
27
|
description: "提取文件的结构定义(接口、类、方法签名),忽略具体实现以节省 Token",
|
|
28
|
-
parameters:
|
|
29
|
-
validateParams: ["filePath"],
|
|
28
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe("文件相对路径") }),
|
|
30
29
|
handler: async (args) => {
|
|
31
30
|
const pathArg = args === null || args === void 0 ? void 0 : args.filePath;
|
|
32
31
|
if (typeof pathArg !== "string" || pathArg.trim() === "") {
|
|
@@ -45,8 +44,7 @@ const getTsLspTools = (targetDir) => {
|
|
|
45
44
|
(0, createTool_1.createTool)({
|
|
46
45
|
name: "get_method_body",
|
|
47
46
|
description: "获取指定文件内某个方法或函数的完整实现代码",
|
|
48
|
-
parameters:
|
|
49
|
-
validateParams: ["filePath", 'methodName'],
|
|
47
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe("文件相对路径"), methodName: zod_1.z.string().describe("方法名或函数名") }),
|
|
50
48
|
handler: async ({ filePath, methodName }) => {
|
|
51
49
|
return engine.getMethodImplementation(filePath, methodName);
|
|
52
50
|
},
|
package/lib/types/type.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
export interface ApiConfig {
|
|
3
4
|
baseURL: string;
|
|
4
5
|
apiKey: string;
|
|
@@ -9,7 +10,7 @@ export interface ToolInfo {
|
|
|
9
10
|
function: {
|
|
10
11
|
name: string;
|
|
11
12
|
description?: string;
|
|
12
|
-
parameters: any
|
|
13
|
+
parameters: z.ZodObject<any>;
|
|
13
14
|
};
|
|
14
15
|
_handler?: (args: any) => Promise<any>;
|
|
15
16
|
_client?: Client;
|
|
@@ -58,4 +59,8 @@ export interface AgentOptions {
|
|
|
58
59
|
* only for graph agent
|
|
59
60
|
*/
|
|
60
61
|
modelName?: string;
|
|
62
|
+
/**
|
|
63
|
+
* only for graph agent
|
|
64
|
+
*/
|
|
65
|
+
alwaysSystem?: boolean;
|
|
61
66
|
}
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.convertToLangChainTool = convertToLangChainTool;
|
|
4
4
|
const tools_1 = require("@langchain/core/tools");
|
|
5
|
-
const jsonSchemaToZod_1 = require("./jsonSchemaToZod");
|
|
6
5
|
function convertToLangChainTool(info) {
|
|
7
6
|
return new tools_1.DynamicStructuredTool({
|
|
8
7
|
name: info.function.name,
|
|
9
8
|
description: info.function.description || "",
|
|
10
|
-
schema:
|
|
9
|
+
schema: info.function.parameters,
|
|
11
10
|
func: async (args) => {
|
|
12
11
|
if (info._handler)
|
|
13
12
|
return await info._handler(args);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ToolInfo } from '../types/type';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
export interface CreateToolOptions {
|
|
3
4
|
name: string;
|
|
4
5
|
description: string;
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* zod@3.23.8
|
|
8
|
+
*/
|
|
9
|
+
parameters: z.ZodObject<any>;
|
|
6
10
|
handler: (args: any) => Promise<string>;
|
|
7
|
-
validateParams?: string[];
|
|
8
11
|
}
|
|
9
12
|
export declare function createTool(options: CreateToolOptions): ToolInfo;
|
package/lib/utils/createTool.js
CHANGED
|
@@ -10,7 +10,6 @@ function createTool(options) {
|
|
|
10
10
|
parameters: options.parameters,
|
|
11
11
|
},
|
|
12
12
|
_handler: async (input) => {
|
|
13
|
-
var _a;
|
|
14
13
|
// 兼容处理:如果 input 是字符串,尝试解析为 JSON 对象
|
|
15
14
|
let args = input;
|
|
16
15
|
if (typeof input === 'string') {
|
|
@@ -21,15 +20,6 @@ function createTool(options) {
|
|
|
21
20
|
args = input;
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
if (((_a = options.validateParams) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
25
|
-
for (const arg in args) {
|
|
26
|
-
if (options.validateParams.includes(arg)) {
|
|
27
|
-
if (typeof args[arg] === 'undefined') {
|
|
28
|
-
return `Error: 参数 '${arg}' 缺失`;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
23
|
return options.handler(args);
|
|
34
24
|
},
|
|
35
25
|
};
|
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export declare const zodObjectToJsonSchema: (zodObject: z.ZodObject<any>, name: string) => z.ZodObject<any, z.UnknownKeysParam, z.ZodTypeAny, {
|
|
3
|
-
[x: string]: any;
|
|
4
|
-
}, {
|
|
5
|
-
[x: string]: any;
|
|
6
|
-
}> | {
|
|
7
|
-
title?: string;
|
|
8
|
-
default?: any;
|
|
9
|
-
description?: string;
|
|
10
|
-
markdownDescription?: string;
|
|
11
|
-
};
|
|
12
2
|
export declare function jsonSchemaToZod(parameters: any): z.ZodObject<any>;
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.zodObjectToJsonSchema = void 0;
|
|
4
3
|
exports.jsonSchemaToZod = jsonSchemaToZod;
|
|
5
4
|
const zod_1 = require("zod");
|
|
6
|
-
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
7
|
-
const zodObjectToJsonSchema = (zodObject, name) => {
|
|
8
|
-
var _a, _b;
|
|
9
|
-
return ((_b = (_a = (0, zod_to_json_schema_1.zodToJsonSchema)(zodObject, name)) === null || _a === void 0 ? void 0 : _a.definitions) === null || _b === void 0 ? void 0 : _b[name]) || zodObject;
|
|
10
|
-
};
|
|
11
|
-
exports.zodObjectToJsonSchema = zodObjectToJsonSchema;
|
|
12
5
|
function jsonSchemaToZod(parameters) {
|
|
13
6
|
var _a;
|
|
14
7
|
if (!parameters || typeof parameters !== 'object') {
|
|
@@ -57,8 +50,8 @@ function jsonSchemaToZod(parameters) {
|
|
|
57
50
|
let schema = convertProp(prop);
|
|
58
51
|
// 处理必填项
|
|
59
52
|
const isRequired = (_a = parameters.required) === null || _a === void 0 ? void 0 : _a.includes(key);
|
|
60
|
-
obj[key] = isRequired ? schema : schema.
|
|
53
|
+
obj[key] = isRequired ? schema : schema.nullable();
|
|
61
54
|
}
|
|
62
55
|
// 使用 passthrough 或 loose 以增加容错性
|
|
63
|
-
return zod_1.z.object(obj)
|
|
56
|
+
return zod_1.z.object(obj);
|
|
64
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "AI Assistant CLI.",
|
|
5
5
|
"author": "saber2pr",
|
|
6
6
|
"license": "ISC",
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
"langchain": "0.3.15",
|
|
35
35
|
"minimatch": "^10.0.1",
|
|
36
36
|
"openai": "^6.16.0",
|
|
37
|
-
"zod": "3.23.8"
|
|
38
|
-
"zod-to-json-schema": "3.23.2"
|
|
37
|
+
"zod": "3.23.8"
|
|
39
38
|
},
|
|
40
39
|
"resolutions": {
|
|
41
40
|
"@langchain/core": "0.3.39"
|