@saber2pr/ai-agent 0.0.21 → 0.0.23
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.
|
@@ -14,10 +14,13 @@ export default class McpGraphAgent {
|
|
|
14
14
|
private options;
|
|
15
15
|
private checkpointer;
|
|
16
16
|
private langchainTools;
|
|
17
|
-
private
|
|
17
|
+
private stopLoadingFunc;
|
|
18
18
|
constructor(options?: AgentOptions);
|
|
19
|
-
private
|
|
19
|
+
private showLoading;
|
|
20
|
+
private startLoading;
|
|
21
|
+
private stopLoading;
|
|
20
22
|
private getModel;
|
|
23
|
+
private askForConfig;
|
|
21
24
|
chat(query?: string): Promise<void>;
|
|
22
25
|
start(): Promise<void>;
|
|
23
26
|
private renderOutput;
|
package/lib/core/agent-graph.js
CHANGED
|
@@ -13,7 +13,6 @@ 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 ora_1 = __importDefault(require("ora")); // 用于显示 Loading 动画
|
|
17
16
|
const builtin_1 = require("../tools/builtin");
|
|
18
17
|
const convertToLangChainTool_1 = require("../utils/convertToLangChainTool");
|
|
19
18
|
exports.CONFIG_FILE = path_1.default.join(os_1.default.homedir(), ".saber2pr-agent.json");
|
|
@@ -40,15 +39,66 @@ class McpGraphAgent {
|
|
|
40
39
|
constructor(options = {}) {
|
|
41
40
|
this.checkpointer = new langgraph_1.MemorySaver();
|
|
42
41
|
this.langchainTools = [];
|
|
43
|
-
|
|
42
|
+
// ✅ 用于存储清理 loading 的函数
|
|
43
|
+
this.stopLoadingFunc = null;
|
|
44
44
|
this.options = options;
|
|
45
45
|
this.targetDir = options.targetDir || process.cwd();
|
|
46
|
-
process.setMaxListeners(
|
|
46
|
+
process.setMaxListeners(100);
|
|
47
|
+
// ✅ 退出清理
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
this.stopLoading();
|
|
50
|
+
process.stdout.write('\u001B[?25h'); // 显示光标
|
|
51
|
+
process.exit(0);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", cleanup);
|
|
54
|
+
process.on("SIGTERM", cleanup);
|
|
47
55
|
const builtinToolInfos = (0, builtin_1.createDefaultBuiltinTools)({ options });
|
|
48
|
-
|
|
49
|
-
this.langchainTools = [...builtinToolInfos, ...externalToolInfos].map((t) => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
|
|
56
|
+
this.langchainTools = [...builtinToolInfos, ...(options.tools || [])].map((t) => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
|
|
50
57
|
this.toolNode = new prebuilt_1.ToolNode(this.langchainTools);
|
|
51
58
|
}
|
|
59
|
+
// ✅ 1. 复现你提供的 showLoading 效果
|
|
60
|
+
showLoading(text) {
|
|
61
|
+
const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
62
|
+
let i = 0;
|
|
63
|
+
// 隐藏光标,防止闪烁
|
|
64
|
+
process.stdout.write('\u001B[?25l');
|
|
65
|
+
const timer = setInterval(() => {
|
|
66
|
+
process.stdout.write(`\r\x1b[36m${chars[i]}\x1b[0m ${text}`);
|
|
67
|
+
i = (i + 1) % chars.length;
|
|
68
|
+
}, 80);
|
|
69
|
+
return () => {
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
process.stdout.write('\r\x1b[K'); // 清除这一行
|
|
72
|
+
process.stdout.write('\u001B[?25h'); // 恢复光标
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// ✅ 2. 封装内部调用方法
|
|
76
|
+
startLoading(text) {
|
|
77
|
+
this.stopLoading();
|
|
78
|
+
this.stopLoadingFunc = this.showLoading(text);
|
|
79
|
+
}
|
|
80
|
+
stopLoading() {
|
|
81
|
+
if (this.stopLoadingFunc) {
|
|
82
|
+
this.stopLoadingFunc();
|
|
83
|
+
this.stopLoadingFunc = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async getModel() {
|
|
87
|
+
if (this.model)
|
|
88
|
+
return this.model;
|
|
89
|
+
let modelInstance = this.options.apiModel;
|
|
90
|
+
if (!modelInstance) {
|
|
91
|
+
const config = await this.askForConfig();
|
|
92
|
+
modelInstance = new openai_1.ChatOpenAI({
|
|
93
|
+
openAIApiKey: config.apiKey,
|
|
94
|
+
configuration: { baseURL: config.baseURL },
|
|
95
|
+
modelName: config.model,
|
|
96
|
+
temperature: 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.model = modelInstance.bindTools(this.langchainTools);
|
|
100
|
+
return this.model;
|
|
101
|
+
}
|
|
52
102
|
async askForConfig() {
|
|
53
103
|
let config = {};
|
|
54
104
|
if (fs_1.default.existsSync(exports.CONFIG_FILE)) {
|
|
@@ -60,7 +110,6 @@ class McpGraphAgent {
|
|
|
60
110
|
if (!config.baseURL || !config.apiKey) {
|
|
61
111
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
62
112
|
const question = (q) => new Promise((res) => rl.question(q, res));
|
|
63
|
-
console.log(`💡 首次运行请配置信息:`);
|
|
64
113
|
config.baseURL = config.baseURL || await question(`? API Base URL: `);
|
|
65
114
|
config.apiKey = config.apiKey || await question(`? API Key: `);
|
|
66
115
|
config.model = config.model || await question(`? Model Name: `) || "gpt-4o";
|
|
@@ -69,22 +118,6 @@ class McpGraphAgent {
|
|
|
69
118
|
}
|
|
70
119
|
return config;
|
|
71
120
|
}
|
|
72
|
-
async getModel() {
|
|
73
|
-
if (this.model)
|
|
74
|
-
return this.model;
|
|
75
|
-
let modelInstance = this.options.apiModel;
|
|
76
|
-
if (!modelInstance) {
|
|
77
|
-
const config = await this.askForConfig();
|
|
78
|
-
modelInstance = new openai_1.ChatOpenAI({
|
|
79
|
-
openAIApiKey: config.apiKey,
|
|
80
|
-
configuration: { baseURL: config.baseURL },
|
|
81
|
-
modelName: config.model,
|
|
82
|
-
temperature: 0,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
this.model = modelInstance.bindTools(this.langchainTools);
|
|
86
|
-
return this.model;
|
|
87
|
-
}
|
|
88
121
|
async chat(query = "开始代码审计") {
|
|
89
122
|
await this.getModel();
|
|
90
123
|
const app = await this.createGraph();
|
|
@@ -95,21 +128,24 @@ class McpGraphAgent {
|
|
|
95
128
|
}, { configurable: { thread_id: "auto_worker" }, recursionLimit: 100 });
|
|
96
129
|
for await (const output of stream)
|
|
97
130
|
this.renderOutput(output);
|
|
98
|
-
console.log("✅ 任务执行完毕。");
|
|
99
131
|
}
|
|
100
132
|
async start() {
|
|
101
133
|
await this.getModel();
|
|
102
134
|
const app = await this.createGraph();
|
|
103
135
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
rl.on("SIGINT", () => {
|
|
137
|
+
this.stopLoading();
|
|
138
|
+
rl.close();
|
|
139
|
+
process.stdout.write('\u001B[?25h');
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
106
142
|
const ask = () => {
|
|
107
143
|
rl.question("> ", async (input) => {
|
|
108
144
|
if (input.toLowerCase() === "exit") {
|
|
109
145
|
rl.close();
|
|
110
146
|
return;
|
|
111
147
|
}
|
|
112
|
-
const stream = await app.stream({ messages: [new messages_1.HumanMessage(input)], mode: "chat" }, { configurable: { thread_id:
|
|
148
|
+
const stream = await app.stream({ messages: [new messages_1.HumanMessage(input)], mode: "chat" }, { configurable: { thread_id: "session" }, recursionLimit: 50 });
|
|
113
149
|
for await (const output of stream)
|
|
114
150
|
this.renderOutput(output);
|
|
115
151
|
ask();
|
|
@@ -119,21 +155,16 @@ class McpGraphAgent {
|
|
|
119
155
|
}
|
|
120
156
|
renderOutput(output) {
|
|
121
157
|
var _a, _b;
|
|
122
|
-
//
|
|
123
|
-
if (this.spinner.isSpinning)
|
|
124
|
-
this.spinner.stop();
|
|
158
|
+
this.stopLoading(); // 收到任何输出前,先关掉转圈圈
|
|
125
159
|
const agentNode = output.agent;
|
|
126
160
|
if (agentNode) {
|
|
127
161
|
const msg = agentNode.messages[0];
|
|
128
|
-
// 1. 打印思考过程
|
|
129
162
|
const reasoning = (_a = msg.additional_kwargs) === null || _a === void 0 ? void 0 : _a.reasoning;
|
|
130
163
|
if (reasoning) {
|
|
131
|
-
console.log("\n🧠 [思考过程]:\n" + "─".repeat(50) + "\n" + reasoning + "\n" + "─".repeat(50)
|
|
164
|
+
console.log("\n🧠 [思考过程]:\n" + "─".repeat(50) + "\n" + reasoning + "\n" + "─".repeat(50));
|
|
132
165
|
}
|
|
133
|
-
// 2. 打印正式回答
|
|
134
166
|
if (msg.content)
|
|
135
167
|
console.log("🤖 [AI]:", msg.content);
|
|
136
|
-
// 3. 打印工具调用
|
|
137
168
|
if ((_b = msg.tool_calls) === null || _b === void 0 ? void 0 : _b.length) {
|
|
138
169
|
msg.tool_calls.forEach((call) => {
|
|
139
170
|
console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
|
|
@@ -141,39 +172,17 @@ class McpGraphAgent {
|
|
|
141
172
|
}
|
|
142
173
|
}
|
|
143
174
|
}
|
|
144
|
-
// --- 节点逻辑 ---
|
|
145
175
|
async callModel(state) {
|
|
146
|
-
// 处理变量序列化,防止 [object Object]
|
|
147
|
-
const auditedListStr = state.auditedFiles.length > 0
|
|
148
|
-
? state.auditedFiles.map(f => `\n - ${f}`).join("")
|
|
149
|
-
: "暂无";
|
|
150
|
-
const extraPromptStr = typeof this.options.extraSystemPrompt === 'object'
|
|
151
|
-
? JSON.stringify(this.options.extraSystemPrompt, null, 2)
|
|
152
|
-
: (this.options.extraSystemPrompt || "");
|
|
153
|
-
// 使用变量占位符 {extraPrompt} 避免内容中的 {} 引发模板解析错误
|
|
154
176
|
const prompt = prompts_1.ChatPromptTemplate.fromMessages([
|
|
155
177
|
["system", `你是一个代码专家。工作目录:${this.targetDir}。
|
|
178
|
+
当前模式:{mode}。进度:{doneCount}/{targetCount}。
|
|
179
|
+
已审计文件:{auditedList}
|
|
156
180
|
|
|
157
|
-
# 当前进度状态
|
|
158
|
-
- 审计模式: {mode}
|
|
159
|
-
- 目标任务数: {targetCount}
|
|
160
|
-
- 已完成数量: {doneCount}
|
|
161
|
-
- 已审计文件列表: {auditedList}
|
|
162
|
-
|
|
163
|
-
# 核心任务准则
|
|
164
|
-
1. 目标导向:如果 {doneCount} >= {targetCount},说明任务已达标。请直接输出总结,不要再调用任何工具。
|
|
165
|
-
2. 避免死循环:不要反复尝试审计同一个文件或调用同样的工具。如果你发现某个文件修复失败,请尝试审计其他文件。
|
|
166
|
-
3. 严格格式:
|
|
167
|
-
- 必须先在 <think> 标签内进行推理。
|
|
168
|
-
- 工具调用必须严格按照 Action: 名称 和 Arguments: {{JSON}} 的格式。
|
|
169
|
-
- 【重要】Arguments 中的 JSON 字符串,所有的换行符必须转义为 \\n,严禁出现物理换行符。
|
|
170
|
-
|
|
171
|
-
# 附加指令
|
|
172
181
|
{extraPrompt}`],
|
|
173
182
|
new prompts_1.MessagesPlaceholder("messages"),
|
|
174
183
|
]);
|
|
175
|
-
// ✅
|
|
176
|
-
this.
|
|
184
|
+
// ✅ 调用 AI 前开启转圈圈
|
|
185
|
+
this.startLoading("AI 正在分析并思考中...");
|
|
177
186
|
try {
|
|
178
187
|
const chain = prompt.pipe(this.model);
|
|
179
188
|
const response = await chain.invoke({
|
|
@@ -181,34 +190,28 @@ class McpGraphAgent {
|
|
|
181
190
|
mode: state.mode,
|
|
182
191
|
targetCount: state.targetCount,
|
|
183
192
|
doneCount: state.auditedFiles.length,
|
|
184
|
-
auditedList:
|
|
185
|
-
extraPrompt:
|
|
193
|
+
auditedList: state.auditedFiles.join(", "),
|
|
194
|
+
extraPrompt: this.options.extraSystemPrompt || "",
|
|
186
195
|
});
|
|
187
|
-
this.
|
|
196
|
+
this.stopLoading();
|
|
188
197
|
return { messages: [response] };
|
|
189
198
|
}
|
|
190
199
|
catch (error) {
|
|
191
|
-
this.
|
|
200
|
+
this.stopLoading();
|
|
192
201
|
throw error;
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
|
-
// agent-graph.ts 中的 trackProgress 节点
|
|
196
204
|
async trackProgress(state) {
|
|
197
205
|
var _a;
|
|
198
|
-
// 获取最后一条 AI 消息(即发起工具调用的那条)
|
|
199
206
|
const lastAiMsg = state.messages[state.messages.length - 1];
|
|
200
207
|
const newFiles = [];
|
|
201
208
|
if ((_a = lastAiMsg === null || lastAiMsg === void 0 ? void 0 : lastAiMsg.tool_calls) === null || _a === void 0 ? void 0 : _a.length) {
|
|
202
209
|
for (const tc of lastAiMsg.tool_calls) {
|
|
203
|
-
// 这里的逻辑要宽容:只要 AI 尝试处理了这些文件,就计入进度
|
|
204
210
|
const file = tc.args.path || tc.args.filePath || tc.args.file;
|
|
205
|
-
if (file && typeof file === 'string')
|
|
211
|
+
if (file && typeof file === 'string')
|
|
206
212
|
newFiles.push(file);
|
|
207
|
-
}
|
|
208
213
|
}
|
|
209
214
|
}
|
|
210
|
-
// 如果这一轮没有任何新文件被处理,且 AI 也没给最终回复,
|
|
211
|
-
// 我们需要防止它在下一轮条件判断中陷入死循环
|
|
212
215
|
return { auditedFiles: newFiles };
|
|
213
216
|
}
|
|
214
217
|
async createGraph() {
|
|
@@ -220,22 +223,11 @@ class McpGraphAgent {
|
|
|
220
223
|
.addConditionalEdges("agent", (state) => {
|
|
221
224
|
var _a;
|
|
222
225
|
const lastMsg = state.messages[state.messages.length - 1];
|
|
223
|
-
// 1. 如果 AI 想要调用工具,去 tools 节点
|
|
224
226
|
if ((_a = lastMsg.tool_calls) === null || _a === void 0 ? void 0 : _a.length)
|
|
225
227
|
return "tools";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const isDone = state.auditedFiles.length >= state.targetCount;
|
|
229
|
-
// 如果还没达标,继续让 agent 思考下一步
|
|
230
|
-
if (!isDone)
|
|
231
|
-
return "agent";
|
|
232
|
-
}
|
|
233
|
-
// 3. 其他情况(达标了,或者对话模式 AI 给出了回复)一律结束
|
|
228
|
+
if (state.mode === "auto" && state.auditedFiles.length < state.targetCount)
|
|
229
|
+
return "agent";
|
|
234
230
|
return langgraph_1.END;
|
|
235
|
-
}, {
|
|
236
|
-
tools: "tools",
|
|
237
|
-
agent: "agent",
|
|
238
|
-
[langgraph_1.END]: langgraph_1.END,
|
|
239
231
|
})
|
|
240
232
|
.addEdge("tools", "progress")
|
|
241
233
|
.addEdge("progress", "agent");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
2
|
-
import {
|
|
3
|
-
export declare function convertToLangChainTool(info:
|
|
2
|
+
import { ToolInfo } from "../types/type";
|
|
3
|
+
export declare function convertToLangChainTool(info: ToolInfo): DynamicStructuredTool<import("zod").ZodObject<any, import("zod").UnknownKeysParam, import("zod").ZodTypeAny, {
|
|
4
|
+
[x: string]: any;
|
|
5
|
+
}, {
|
|
6
|
+
[x: string]: any;
|
|
7
|
+
}>>;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.convertToLangChainTool = convertToLangChainTool;
|
|
4
4
|
const tools_1 = require("@langchain/core/tools");
|
|
5
|
-
const
|
|
5
|
+
const jsonSchemaToZod_1 = require("./jsonSchemaToZod");
|
|
6
6
|
function convertToLangChainTool(info) {
|
|
7
7
|
return new tools_1.DynamicStructuredTool({
|
|
8
8
|
name: info.function.name,
|
|
9
9
|
description: info.function.description || "",
|
|
10
|
-
schema:
|
|
10
|
+
schema: (0, jsonSchemaToZod_1.jsonSchemaToZod)(info.function.parameters),
|
|
11
11
|
func: async (args) => {
|
|
12
12
|
if (info._handler)
|
|
13
13
|
return await info._handler(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "AI Assistant CLI.",
|
|
5
5
|
"author": "saber2pr",
|
|
6
6
|
"license": "ISC",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"langchain": "0.3.15",
|
|
35
35
|
"minimatch": "^10.0.1",
|
|
36
36
|
"openai": "^6.16.0",
|
|
37
|
-
"ora": "^9.3.0",
|
|
38
37
|
"zod": "3.23.8",
|
|
39
38
|
"zod-to-json-schema": "3.23.2"
|
|
40
39
|
},
|