@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 spinner;
17
+ private stopLoadingFunc;
18
18
  constructor(options?: AgentOptions);
19
- private askForConfig;
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;
@@ -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
- this.spinner = (0, ora_1.default)({ color: "cyan" });
42
+ // 用于存储清理 loading 的函数
43
+ this.stopLoadingFunc = null;
44
44
  this.options = options;
45
45
  this.targetDir = options.targetDir || process.cwd();
46
- process.setMaxListeners(50); // 防止 AbortSignal 监听器过多的警告
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
- const externalToolInfos = options.tools || [];
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
- const threadId = `session_${Date.now()}`;
105
- console.log(`\n💬 已进入交互审计模式 (Thread: ${threadId})`);
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: threadId }, recursionLimit: 50 });
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
- // 每次渲染输出前,确保停止 Spinner
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) + "\n");
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
- // ✅ 显示 Loading
176
- this.spinner.start("AI 正在思考并分析代码...");
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: auditedListStr,
185
- extraPrompt: extraPromptStr, // 变量方式传入更安全
193
+ auditedList: state.auditedFiles.join(", "),
194
+ extraPrompt: this.options.extraSystemPrompt || "",
186
195
  });
187
- this.spinner.stop(); // 得到响应即停止
196
+ this.stopLoading();
188
197
  return { messages: [response] };
189
198
  }
190
199
  catch (error) {
191
- this.spinner.fail("AI 响应异常");
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
- // 2. 如果是自动模式且未达标
227
- if (state.mode === "auto") {
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 { z } from "zod";
3
- export declare function convertToLangChainTool(info: any): DynamicStructuredTool<z.ZodRecord<z.ZodString, z.ZodAny>>;
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 zod_1 = require("zod");
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: zod_1.z.record(zod_1.z.any()),
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.21",
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
  },