@saber2pr/ai-agent 0.0.40 → 0.0.42
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/cli-graph.js +3 -1
- package/lib/core/agent-graph.d.ts +18 -3
- package/lib/core/agent-graph.js +244 -83
- package/lib/model/AgentGraphModel.d.ts +17 -1
- package/lib/model/AgentGraphModel.js +53 -0
- package/lib/types/type.d.ts +2 -0
- package/package.json +1 -2
package/lib/cli-graph.js
CHANGED
|
@@ -5,5 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const agent_graph_1 = __importDefault(require("./core/agent-graph"));
|
|
8
|
-
const agent = new agent_graph_1.default(
|
|
8
|
+
const agent = new agent_graph_1.default({
|
|
9
|
+
stream: true,
|
|
10
|
+
});
|
|
9
11
|
agent.start();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { BaseMessage } from '@langchain/core/messages';
|
|
1
|
+
import { AIMessage, BaseMessage } from '@langchain/core/messages';
|
|
2
2
|
import { GraphAgentOptions } from '../types/type';
|
|
3
3
|
import { AgentGraphModel } from '../model/AgentGraphModel';
|
|
4
|
-
export declare const CONFIG_FILE: string;
|
|
5
4
|
interface TokenUsage {
|
|
6
5
|
total: number;
|
|
7
6
|
}
|
|
@@ -28,7 +27,18 @@ export default class McpGraphAgent<T extends AgentGraphModel = any> {
|
|
|
28
27
|
private maxTargetCount;
|
|
29
28
|
private maxTokens;
|
|
30
29
|
private mcpClients;
|
|
30
|
+
private streamEnabled;
|
|
31
|
+
private streamOutputCallback;
|
|
31
32
|
constructor(options?: GraphAgentOptions<T>);
|
|
33
|
+
/**
|
|
34
|
+
* 设置外部流式输出回调(如 VS Code Webview)。
|
|
35
|
+
* 设置后,callModel 的流式输出将通过回调发送而非写入 stdout。
|
|
36
|
+
*/
|
|
37
|
+
setStreamOutput(callback: (chunk: string, type: 'think' | 'text') => void): void;
|
|
38
|
+
/**
|
|
39
|
+
* 清除外部流式输出回调,恢复默认的 stdout 输出。
|
|
40
|
+
*/
|
|
41
|
+
clearStreamOutput(): void;
|
|
32
42
|
private printLoadedTools;
|
|
33
43
|
private loadMcpConfigs;
|
|
34
44
|
private initMcpTools;
|
|
@@ -41,10 +51,15 @@ export default class McpGraphAgent<T extends AgentGraphModel = any> {
|
|
|
41
51
|
getModel(): Promise<T>;
|
|
42
52
|
private askForConfig;
|
|
43
53
|
chat(query?: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* 流式执行单次查询(编程式 API)。
|
|
56
|
+
* 无论 options.stream 是否开启,此方法始终以流式方式输出。
|
|
57
|
+
*/
|
|
58
|
+
stream(query?: string): Promise<void>;
|
|
44
59
|
start(): Promise<void>;
|
|
45
60
|
private renderOutput;
|
|
46
61
|
callModel(state: typeof AgentState.State): Promise<{
|
|
47
|
-
messages:
|
|
62
|
+
messages: AIMessage[];
|
|
48
63
|
tokenUsage: {
|
|
49
64
|
total: number;
|
|
50
65
|
};
|
package/lib/core/agent-graph.js
CHANGED
|
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CONFIG_FILE = void 0;
|
|
7
6
|
const events_1 = require("events");
|
|
8
7
|
const fs_1 = __importDefault(require("fs"));
|
|
9
8
|
const os_1 = __importDefault(require("os"));
|
|
@@ -20,7 +19,7 @@ const builtin_1 = require("../tools/builtin");
|
|
|
20
19
|
const convertToLangChainTool_1 = require("../utils/convertToLangChainTool");
|
|
21
20
|
const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
|
|
22
21
|
const formatSchema_1 = require("../utils/formatSchema");
|
|
23
|
-
|
|
22
|
+
const config_1 = require("../config/config");
|
|
24
23
|
// ✅ 全局设置:修复 AbortSignal 监听器数量警告
|
|
25
24
|
// LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
|
|
26
25
|
events_1.EventEmitter.defaultMaxListeners = 100;
|
|
@@ -40,7 +39,7 @@ const AgentState = langgraph_1.Annotation.Root({
|
|
|
40
39
|
}),
|
|
41
40
|
mode: (0, langgraph_1.Annotation)({
|
|
42
41
|
reducer: (x, y) => y !== null && y !== void 0 ? y : x,
|
|
43
|
-
default: () =>
|
|
42
|
+
default: () => 'chat',
|
|
44
43
|
}),
|
|
45
44
|
// ✅ Token 累加器
|
|
46
45
|
tokenUsage: (0, langgraph_1.Annotation)({
|
|
@@ -61,6 +60,7 @@ class McpGraphAgent {
|
|
|
61
60
|
this.langchainTools = [];
|
|
62
61
|
this.stopLoadingFunc = null;
|
|
63
62
|
this.mcpClients = [];
|
|
63
|
+
this.streamOutputCallback = null;
|
|
64
64
|
this.options = options;
|
|
65
65
|
this.verbose = options.verbose || false;
|
|
66
66
|
this.alwaysSystem = options.alwaysSystem || true;
|
|
@@ -69,6 +69,7 @@ class McpGraphAgent {
|
|
|
69
69
|
this.apiConfig = options.apiConfig;
|
|
70
70
|
this.maxTargetCount = options.maxTargetCount || 4;
|
|
71
71
|
this.maxTokens = options.maxTokens || 8000;
|
|
72
|
+
this.streamEnabled = options.stream || false;
|
|
72
73
|
process.setMaxListeners(100);
|
|
73
74
|
// ✅ 修复 AbortSignal 监听器数量警告
|
|
74
75
|
// LangChain 的 HTTP 客户端会创建多个 AbortSignal,需要增加默认限制
|
|
@@ -80,16 +81,29 @@ class McpGraphAgent {
|
|
|
80
81
|
process.stdout.write('\u001B[?25h');
|
|
81
82
|
process.exit(0);
|
|
82
83
|
};
|
|
83
|
-
process.on(
|
|
84
|
-
process.on(
|
|
84
|
+
process.on('SIGINT', cleanup);
|
|
85
|
+
process.on('SIGTERM', cleanup);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 设置外部流式输出回调(如 VS Code Webview)。
|
|
89
|
+
* 设置后,callModel 的流式输出将通过回调发送而非写入 stdout。
|
|
90
|
+
*/
|
|
91
|
+
setStreamOutput(callback) {
|
|
92
|
+
this.streamOutputCallback = callback;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 清除外部流式输出回调,恢复默认的 stdout 输出。
|
|
96
|
+
*/
|
|
97
|
+
clearStreamOutput() {
|
|
98
|
+
this.streamOutputCallback = null;
|
|
85
99
|
}
|
|
86
100
|
printLoadedTools() {
|
|
87
|
-
console.log(
|
|
101
|
+
console.log('\n🛠️ [Graph] 正在加载工具节点...');
|
|
88
102
|
this.langchainTools.forEach((tool) => {
|
|
89
103
|
// 工具名称
|
|
90
104
|
console.log(`\n🧰 工具名: ${tool.name}`);
|
|
91
105
|
// 提取参数结构 (LangChain Tool 的 schema 是 Zod 对象)
|
|
92
|
-
const schema = tool
|
|
106
|
+
const { schema } = tool;
|
|
93
107
|
if (schema && schema.shape) {
|
|
94
108
|
// 如果是 ZodObject,打印其内部 key
|
|
95
109
|
console.log(` 参数结构:\n${(0, formatSchema_1.formatSchema)(schema)}`);
|
|
@@ -108,12 +122,12 @@ class McpGraphAgent {
|
|
|
108
122
|
loadMcpConfigs() {
|
|
109
123
|
const combined = { mcpServers: {} };
|
|
110
124
|
const paths = [
|
|
111
|
-
path_1.default.join(os_1.default.homedir(),
|
|
112
|
-
path_1.default.join(os_1.default.homedir(),
|
|
125
|
+
path_1.default.join(os_1.default.homedir(), '.cursor', 'mcp.json'),
|
|
126
|
+
path_1.default.join(os_1.default.homedir(), '.vscode', 'mcp.json'),
|
|
113
127
|
];
|
|
114
|
-
paths.forEach(
|
|
128
|
+
paths.forEach(p => {
|
|
115
129
|
if (fs_1.default.existsSync(p)) {
|
|
116
|
-
const content = JSON.parse(fs_1.default.readFileSync(p,
|
|
130
|
+
const content = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
|
|
117
131
|
Object.assign(combined.mcpServers, content.mcpServers);
|
|
118
132
|
}
|
|
119
133
|
});
|
|
@@ -130,13 +144,13 @@ class McpGraphAgent {
|
|
|
130
144
|
args: config.args,
|
|
131
145
|
env: { ...process.env, ...(config.env || {}) },
|
|
132
146
|
});
|
|
133
|
-
const client = new index_js_1.Client({ name:
|
|
147
|
+
const client = new index_js_1.Client({ name: 'mcp-graph-client', version: '1.0.0' }, { capabilities: {} });
|
|
134
148
|
await client.connect(transport);
|
|
135
149
|
this.mcpClients.push(client);
|
|
136
150
|
const { tools } = await client.listTools();
|
|
137
|
-
tools.forEach(
|
|
151
|
+
tools.forEach(tool => {
|
|
138
152
|
mcpToolInfos.push({
|
|
139
|
-
type:
|
|
153
|
+
type: 'function',
|
|
140
154
|
function: {
|
|
141
155
|
name: tool.name,
|
|
142
156
|
description: tool.description,
|
|
@@ -163,12 +177,8 @@ class McpGraphAgent {
|
|
|
163
177
|
const builtinToolInfos = (0, builtin_1.createDefaultBuiltinTools)({ options: this.options });
|
|
164
178
|
const mcpToolInfos = await this.initMcpTools();
|
|
165
179
|
// 合并内置、手动传入和 MCP 工具
|
|
166
|
-
const allToolInfos = [
|
|
167
|
-
|
|
168
|
-
...(this.options.tools || []),
|
|
169
|
-
...mcpToolInfos
|
|
170
|
-
];
|
|
171
|
-
this.langchainTools = allToolInfos.map((t) => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
|
|
180
|
+
const allToolInfos = [...builtinToolInfos, ...(this.options.tools || []), ...mcpToolInfos];
|
|
181
|
+
this.langchainTools = allToolInfos.map(t => (0, convertToLangChainTool_1.convertToLangChainTool)(t));
|
|
172
182
|
this.toolNode = new prebuilt_1.ToolNode(this.langchainTools);
|
|
173
183
|
}
|
|
174
184
|
// ✅ 修改:初始化逻辑
|
|
@@ -228,6 +238,7 @@ class McpGraphAgent {
|
|
|
228
238
|
modelName: config.model,
|
|
229
239
|
temperature: 0,
|
|
230
240
|
maxTokens: this.maxTokens,
|
|
241
|
+
streaming: this.streamEnabled,
|
|
231
242
|
});
|
|
232
243
|
}
|
|
233
244
|
this.model = modelInstance.bindTools(this.langchainTools);
|
|
@@ -237,40 +248,75 @@ class McpGraphAgent {
|
|
|
237
248
|
if (this.apiConfig)
|
|
238
249
|
return this.apiConfig;
|
|
239
250
|
let config = {};
|
|
240
|
-
if (fs_1.default.existsSync(
|
|
251
|
+
if (fs_1.default.existsSync(config_1.CONFIG_FILE)) {
|
|
241
252
|
try {
|
|
242
|
-
config = JSON.parse(fs_1.default.readFileSync(
|
|
253
|
+
config = JSON.parse(fs_1.default.readFileSync(config_1.CONFIG_FILE, 'utf-8'));
|
|
243
254
|
}
|
|
244
255
|
catch (e) { }
|
|
245
256
|
}
|
|
246
257
|
if (!config.baseURL || !config.apiKey) {
|
|
247
258
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
-
const question = (q) => new Promise(
|
|
249
|
-
config.baseURL = config.baseURL || await question(`? API Base URL: `);
|
|
250
|
-
config.apiKey = config.apiKey || await question(`? API Key: `);
|
|
251
|
-
config.model = config.model || await question(`? Model Name: `) ||
|
|
252
|
-
fs_1.default.writeFileSync(
|
|
259
|
+
const question = (q) => new Promise(res => rl.question(q, res));
|
|
260
|
+
config.baseURL = config.baseURL || (await question(`? API Base URL: `));
|
|
261
|
+
config.apiKey = config.apiKey || (await question(`? API Key: `));
|
|
262
|
+
config.model = config.model || (await question(`? Model Name: `)) || 'gpt-4o';
|
|
263
|
+
fs_1.default.writeFileSync(config_1.CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
253
264
|
rl.close();
|
|
254
265
|
}
|
|
255
266
|
return config;
|
|
256
267
|
}
|
|
257
|
-
async chat(query =
|
|
268
|
+
async chat(query = '开始代码审计') {
|
|
269
|
+
try {
|
|
270
|
+
await this.ensureInitialized();
|
|
271
|
+
await this.getModel();
|
|
272
|
+
const app = await this.createGraph();
|
|
273
|
+
const graphStream = await app.stream({
|
|
274
|
+
messages: [new messages_1.HumanMessage(query)],
|
|
275
|
+
mode: 'auto',
|
|
276
|
+
targetCount: this.maxTargetCount,
|
|
277
|
+
}, {
|
|
278
|
+
configurable: { thread_id: 'auto_worker' },
|
|
279
|
+
recursionLimit: this.recursionLimit,
|
|
280
|
+
debug: this.verbose,
|
|
281
|
+
});
|
|
282
|
+
for await (const output of graphStream)
|
|
283
|
+
this.renderOutput(output, this.streamEnabled);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error('\n❌ Chat 过程中发生错误:', error);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await this.closeMcpClients();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 流式执行单次查询(编程式 API)。
|
|
294
|
+
* 无论 options.stream 是否开启,此方法始终以流式方式输出。
|
|
295
|
+
*/
|
|
296
|
+
async stream(query = '开始代码审计') {
|
|
297
|
+
const prevStream = this.streamEnabled;
|
|
298
|
+
this.streamEnabled = true;
|
|
258
299
|
try {
|
|
259
300
|
await this.ensureInitialized();
|
|
260
301
|
await this.getModel();
|
|
261
302
|
const app = await this.createGraph();
|
|
262
|
-
const
|
|
303
|
+
const graphStream = await app.stream({
|
|
263
304
|
messages: [new messages_1.HumanMessage(query)],
|
|
264
|
-
mode:
|
|
305
|
+
mode: 'auto',
|
|
265
306
|
targetCount: this.maxTargetCount,
|
|
266
|
-
}, {
|
|
267
|
-
|
|
268
|
-
this.
|
|
307
|
+
}, {
|
|
308
|
+
configurable: { thread_id: 'stream_worker' },
|
|
309
|
+
recursionLimit: this.recursionLimit,
|
|
310
|
+
debug: this.verbose,
|
|
311
|
+
});
|
|
312
|
+
for await (const output of graphStream)
|
|
313
|
+
this.renderOutput(output, true);
|
|
269
314
|
}
|
|
270
315
|
catch (error) {
|
|
271
|
-
console.error(
|
|
316
|
+
console.error('\n❌ Stream 过程中发生错误:', error);
|
|
272
317
|
}
|
|
273
318
|
finally {
|
|
319
|
+
this.streamEnabled = prevStream;
|
|
274
320
|
await this.closeMcpClients();
|
|
275
321
|
}
|
|
276
322
|
}
|
|
@@ -279,27 +325,31 @@ class McpGraphAgent {
|
|
|
279
325
|
await this.getModel();
|
|
280
326
|
const app = await this.createGraph();
|
|
281
327
|
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
282
|
-
rl.on(
|
|
328
|
+
rl.on('SIGINT', () => {
|
|
283
329
|
this.stopLoading();
|
|
284
330
|
rl.close();
|
|
285
331
|
process.stdout.write('\u001B[?25h');
|
|
286
332
|
process.exit(0);
|
|
287
333
|
});
|
|
288
334
|
const ask = () => {
|
|
289
|
-
rl.question(
|
|
290
|
-
if (input.toLowerCase() ===
|
|
335
|
+
rl.question('> ', async (input) => {
|
|
336
|
+
if (input.toLowerCase() === 'exit') {
|
|
291
337
|
rl.close();
|
|
292
338
|
return;
|
|
293
339
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
this.
|
|
340
|
+
const graphStream = await app.stream({ messages: [new messages_1.HumanMessage(input)], mode: 'chat' }, {
|
|
341
|
+
configurable: { thread_id: 'session' },
|
|
342
|
+
recursionLimit: this.recursionLimit,
|
|
343
|
+
debug: this.verbose,
|
|
344
|
+
});
|
|
345
|
+
for await (const output of graphStream)
|
|
346
|
+
this.renderOutput(output, this.streamEnabled);
|
|
297
347
|
ask();
|
|
298
348
|
});
|
|
299
349
|
};
|
|
300
350
|
ask();
|
|
301
351
|
}
|
|
302
|
-
renderOutput(output) {
|
|
352
|
+
renderOutput(output, isStreaming = false) {
|
|
303
353
|
var _a, _b, _c;
|
|
304
354
|
this.stopLoading(); // 停止加载动画
|
|
305
355
|
const agentNode = output.agent;
|
|
@@ -321,13 +371,9 @@ class McpGraphAgent {
|
|
|
321
371
|
const toolCallId = msg.tool_call_id || msg.id;
|
|
322
372
|
if (toolCallId) {
|
|
323
373
|
const toolName = toolCallMap.get(toolCallId) || msg.name || 'unknown';
|
|
324
|
-
const content = typeof msg.content === 'string'
|
|
325
|
-
? msg.content
|
|
326
|
-
: JSON.stringify(msg.content);
|
|
374
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
327
375
|
// 如果内容太长,截断显示
|
|
328
|
-
const displayContent = content.length > 500
|
|
329
|
-
? content.substring(0, 500) + '...'
|
|
330
|
-
: content;
|
|
376
|
+
const displayContent = content.length > 500 ? content.substring(0, 500) + '...' : content;
|
|
331
377
|
console.log(`✅ [工具结果] ${toolName}: ${displayContent}`);
|
|
332
378
|
}
|
|
333
379
|
});
|
|
@@ -335,12 +381,13 @@ class McpGraphAgent {
|
|
|
335
381
|
if (agentNode) {
|
|
336
382
|
const msg = agentNode.messages[agentNode.messages.length - 1];
|
|
337
383
|
// 1. 打印思考过程(如果有)
|
|
384
|
+
// 流式模式下思考内容可能已经随流输出,此处仅在非流式模式或有独立 reasoning 字段时打印
|
|
338
385
|
const reasoning = (_b = msg.additional_kwargs) === null || _b === void 0 ? void 0 : _b.reasoning;
|
|
339
|
-
if (reasoning) {
|
|
386
|
+
if (reasoning && !isStreaming) {
|
|
340
387
|
console.log(`\n🧠 [思考]: ${reasoning}`);
|
|
341
388
|
}
|
|
342
|
-
// 2. 打印 AI
|
|
343
|
-
if (msg.content) {
|
|
389
|
+
// 2. 打印 AI 回复内容(流式模式下已在 callModel 中逐字输出,跳过)
|
|
390
|
+
if (msg.content && !isStreaming) {
|
|
344
391
|
console.log(`🤖 [AI]: ${msg.content}`);
|
|
345
392
|
}
|
|
346
393
|
// ✅ 3. 实时打印当次统计信息
|
|
@@ -353,20 +400,20 @@ class McpGraphAgent {
|
|
|
353
400
|
}
|
|
354
401
|
// 4. 打印工具调用情况
|
|
355
402
|
if ((_c = msg.tool_calls) === null || _c === void 0 ? void 0 : _c.length) {
|
|
356
|
-
msg.tool_calls.forEach(
|
|
403
|
+
msg.tool_calls.forEach(call => {
|
|
357
404
|
console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
|
|
358
405
|
});
|
|
359
406
|
}
|
|
360
407
|
}
|
|
361
408
|
}
|
|
362
409
|
async callModel(state) {
|
|
363
|
-
const auditedListStr = state.auditedFiles.length > 0
|
|
364
|
-
? state.auditedFiles.map(f => `\n - ${f}`).join("")
|
|
365
|
-
: "暂无";
|
|
410
|
+
const auditedListStr = state.auditedFiles.length > 0 ? state.auditedFiles.map(f => `\n - ${f}`).join('') : '暂无';
|
|
366
411
|
const recentToolCalls = this.getRecentToolCalls(state.messages);
|
|
367
412
|
const recentToolCallsStr = recentToolCalls.length > 0
|
|
368
|
-
? `\n\n⚠️ 最近调用的工具(避免重复调用相同工具和参数):\n${recentToolCalls
|
|
369
|
-
|
|
413
|
+
? `\n\n⚠️ 最近调用的工具(避免重复调用相同工具和参数):\n${recentToolCalls
|
|
414
|
+
.map(tc => ` - ${tc.name}(${JSON.stringify(tc.args)})`)
|
|
415
|
+
.join('\n')}`
|
|
416
|
+
: '';
|
|
370
417
|
// 1. 构建当前的系统提示词模板
|
|
371
418
|
const systemPromptTemplate = `你是一个代码专家。工作目录:${this.targetDir}。
|
|
372
419
|
当前模式:{mode}
|
|
@@ -391,28 +438,142 @@ class McpGraphAgent {
|
|
|
391
438
|
// ✅ 检查 options 中的 alwaysSystem 参数 (默认为 true 或根据你的需求设置)
|
|
392
439
|
// 如果不希望每次都携带(即只在首轮携带),则过滤掉历史消息里的 SystemMessage
|
|
393
440
|
if (this.options.alwaysSystem === false) {
|
|
394
|
-
inputMessages = state.messages.filter(msg => msg._getType() !==
|
|
441
|
+
inputMessages = state.messages.filter(msg => msg._getType() !== 'system');
|
|
395
442
|
}
|
|
396
443
|
else {
|
|
397
444
|
// 默认模式:保持干净,由 PromptTemplate 重新生成最新的 System 状态
|
|
398
|
-
inputMessages = state.messages.filter(msg => msg._getType() !==
|
|
445
|
+
inputMessages = state.messages.filter(msg => msg._getType() !== 'system');
|
|
399
446
|
}
|
|
400
447
|
const prompt = prompts_1.ChatPromptTemplate.fromMessages([
|
|
401
|
-
[
|
|
402
|
-
new prompts_1.MessagesPlaceholder(
|
|
448
|
+
['system', systemPromptTemplate],
|
|
449
|
+
new prompts_1.MessagesPlaceholder('messages'),
|
|
403
450
|
]);
|
|
404
|
-
this.startLoading(
|
|
451
|
+
this.startLoading('AI 正在分析并思考中');
|
|
405
452
|
try {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
messages: inputMessages, // ✅ 使用处理后的消息列表
|
|
453
|
+
const promptParams = {
|
|
454
|
+
messages: inputMessages,
|
|
409
455
|
mode: state.mode,
|
|
410
456
|
targetCount: state.targetCount,
|
|
411
457
|
doneCount: state.auditedFiles.length,
|
|
412
458
|
auditedList: auditedListStr,
|
|
413
459
|
recentToolCalls: recentToolCallsStr,
|
|
414
|
-
extraPrompt: this.options.extraSystemPrompt ||
|
|
415
|
-
}
|
|
460
|
+
extraPrompt: this.options.extraSystemPrompt || '',
|
|
461
|
+
};
|
|
462
|
+
if (this.streamEnabled) {
|
|
463
|
+
// ✅ 流式模式:通过 AgentGraphModel.streamGenerate 进行流式输出
|
|
464
|
+
const formattedMessages = await prompt.formatMessages(promptParams);
|
|
465
|
+
this.stopLoading();
|
|
466
|
+
// --- 流式 <think> 标签实时过滤 + 流式打印思考内容 ---
|
|
467
|
+
const THINK_OPEN = '<think>';
|
|
468
|
+
const THINK_CLOSE = '</think>';
|
|
469
|
+
let inThink = false;
|
|
470
|
+
let textBuffer = '';
|
|
471
|
+
let aiHeaderPrinted = false;
|
|
472
|
+
let thinkHeaderPrinted = false;
|
|
473
|
+
const hasExternalHandler = !!this.streamOutputCallback;
|
|
474
|
+
const flushText = (text) => {
|
|
475
|
+
if (!text)
|
|
476
|
+
return;
|
|
477
|
+
if (hasExternalHandler) {
|
|
478
|
+
this.streamOutputCallback(text, 'text');
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
if (!aiHeaderPrinted) {
|
|
482
|
+
process.stdout.write('🤖 [AI]: ');
|
|
483
|
+
aiHeaderPrinted = true;
|
|
484
|
+
}
|
|
485
|
+
process.stdout.write(text);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const flushThink = (text) => {
|
|
489
|
+
if (!text)
|
|
490
|
+
return;
|
|
491
|
+
if (hasExternalHandler) {
|
|
492
|
+
this.streamOutputCallback(text, 'think');
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
if (!thinkHeaderPrinted) {
|
|
496
|
+
process.stdout.write('\x1b[2m🧠 [思考]: ');
|
|
497
|
+
thinkHeaderPrinted = true;
|
|
498
|
+
}
|
|
499
|
+
process.stdout.write(text);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const onChunk = (chunk) => {
|
|
503
|
+
textBuffer += chunk;
|
|
504
|
+
let processing = true;
|
|
505
|
+
while (processing) {
|
|
506
|
+
if (inThink) {
|
|
507
|
+
// 在 <think> 块内,寻找 </think>
|
|
508
|
+
const closeIdx = textBuffer.indexOf(THINK_CLOSE);
|
|
509
|
+
if (closeIdx !== -1) {
|
|
510
|
+
// 流式打印 </think> 之前的思考内容
|
|
511
|
+
flushThink(textBuffer.slice(0, closeIdx));
|
|
512
|
+
textBuffer = textBuffer.slice(closeIdx + THINK_CLOSE.length);
|
|
513
|
+
inThink = false;
|
|
514
|
+
// 思考块结束:stdout 模式下换行 + 重置样式
|
|
515
|
+
if (!hasExternalHandler && thinkHeaderPrinted) {
|
|
516
|
+
process.stdout.write('\x1b[0m\n');
|
|
517
|
+
thinkHeaderPrinted = false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// 未找到闭合标签,安全输出可确认部分(防止 </think> 跨 chunk 截断)
|
|
522
|
+
const safeLen = Math.max(0, textBuffer.length - (THINK_CLOSE.length - 1));
|
|
523
|
+
flushThink(textBuffer.slice(0, safeLen));
|
|
524
|
+
textBuffer = textBuffer.slice(safeLen);
|
|
525
|
+
processing = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// 不在 <think> 块内,寻找 <think>
|
|
530
|
+
const openIdx = textBuffer.indexOf(THINK_OPEN);
|
|
531
|
+
if (openIdx !== -1) {
|
|
532
|
+
flushText(textBuffer.slice(0, openIdx));
|
|
533
|
+
textBuffer = textBuffer.slice(openIdx + THINK_OPEN.length);
|
|
534
|
+
inThink = true;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// 未找到开启标签,安全输出可确认部分(防止 <think> 跨 chunk 截断)
|
|
538
|
+
const safeLen = Math.max(0, textBuffer.length - (THINK_OPEN.length - 1));
|
|
539
|
+
flushText(textBuffer.slice(0, safeLen));
|
|
540
|
+
textBuffer = textBuffer.slice(safeLen);
|
|
541
|
+
processing = false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const result = await this.model.streamGenerate(formattedMessages, onChunk);
|
|
547
|
+
// 刷新残留缓冲
|
|
548
|
+
if (textBuffer) {
|
|
549
|
+
if (inThink) {
|
|
550
|
+
flushThink(textBuffer);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
flushText(textBuffer);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// 收尾:stdout 模式下关闭样式和换行
|
|
557
|
+
if (!hasExternalHandler) {
|
|
558
|
+
if (thinkHeaderPrinted) {
|
|
559
|
+
process.stdout.write('\x1b[0m\n');
|
|
560
|
+
}
|
|
561
|
+
if (aiHeaderPrinted)
|
|
562
|
+
process.stdout.write('\n');
|
|
563
|
+
}
|
|
564
|
+
const aiMsg = result.generations[0].message;
|
|
565
|
+
const meta = aiMsg.response_metadata || {};
|
|
566
|
+
const currentToken = Number(meta.token) || 0;
|
|
567
|
+
const currentDuration = Number(meta.duration) || 0;
|
|
568
|
+
return {
|
|
569
|
+
messages: [aiMsg],
|
|
570
|
+
tokenUsage: { total: currentToken },
|
|
571
|
+
totalDuration: currentDuration,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// 非流式模式(原有逻辑)
|
|
575
|
+
const chain = prompt.pipe(this.model);
|
|
576
|
+
const response = await chain.invoke(promptParams);
|
|
416
577
|
this.stopLoading();
|
|
417
578
|
const meta = response.response_metadata || {};
|
|
418
579
|
const currentToken = Number(meta.token) || 0;
|
|
@@ -420,7 +581,7 @@ class McpGraphAgent {
|
|
|
420
581
|
return {
|
|
421
582
|
messages: [response],
|
|
422
583
|
tokenUsage: { total: currentToken },
|
|
423
|
-
totalDuration: currentDuration
|
|
584
|
+
totalDuration: currentDuration,
|
|
424
585
|
};
|
|
425
586
|
}
|
|
426
587
|
catch (error) {
|
|
@@ -464,51 +625,51 @@ class McpGraphAgent {
|
|
|
464
625
|
const totalTokens = ((_a = state.tokenUsage) === null || _a === void 0 ? void 0 : _a.total) || 0;
|
|
465
626
|
const totalMs = state.totalDuration || 0;
|
|
466
627
|
if (totalTokens > 0 || totalMs > 0) {
|
|
467
|
-
console.log(
|
|
628
|
+
console.log('\n' + '═'.repeat(50));
|
|
468
629
|
console.log(`🏁 \x1b[32;1m[审计任务全量结算]\x1b[0m`);
|
|
469
630
|
console.log(` - 累计消耗总额: \x1b[33m${totalTokens}\x1b[0m Tokens`);
|
|
470
631
|
console.log(` - 累计执行耗时: \x1b[36m${(totalMs / 1000).toFixed(2)}\x1b[0m s`);
|
|
471
632
|
console.log(` - 审计文件总数: ${state.auditedFiles.length} 个`);
|
|
472
|
-
console.log(
|
|
633
|
+
console.log('═'.repeat(50) + '\n');
|
|
473
634
|
}
|
|
474
635
|
}
|
|
475
636
|
async createGraph() {
|
|
476
637
|
const workflow = new langgraph_1.StateGraph(AgentState)
|
|
477
|
-
.addNode(
|
|
478
|
-
.addNode(
|
|
479
|
-
.addNode(
|
|
480
|
-
.addEdge(langgraph_1.START,
|
|
481
|
-
.addConditionalEdges(
|
|
482
|
-
const messages = state
|
|
638
|
+
.addNode('agent', state => this.callModel(state))
|
|
639
|
+
.addNode('tools', this.toolNode)
|
|
640
|
+
.addNode('progress', state => this.trackProgress(state))
|
|
641
|
+
.addEdge(langgraph_1.START, 'agent')
|
|
642
|
+
.addConditionalEdges('agent', state => {
|
|
643
|
+
const { messages } = state;
|
|
483
644
|
const lastMsg = messages[messages.length - 1];
|
|
484
|
-
const content = lastMsg.content ||
|
|
645
|
+
const content = lastMsg.content || '';
|
|
485
646
|
// 🛑 新增:全局 Token 熔断保护
|
|
486
647
|
// 如果已消耗 Token 超过了 options 中设置的 maxTokens (假设是总限额)
|
|
487
648
|
if (this.options.maxTokens && state.tokenUsage.total >= this.options.maxTokens) {
|
|
488
|
-
console.warn(
|
|
649
|
+
console.warn('⚠️ [警告] 已达到最大 Token 限制,强制结束任务。');
|
|
489
650
|
this.printFinalSummary(state);
|
|
490
651
|
return langgraph_1.END;
|
|
491
652
|
}
|
|
492
653
|
// 1. 如果 AI 想要调用工具,去 tools 节点
|
|
493
654
|
if (lastMsg.tool_calls && lastMsg.tool_calls.length > 0) {
|
|
494
|
-
return
|
|
655
|
+
return 'tools';
|
|
495
656
|
}
|
|
496
657
|
// 2. 判定结束的条件:
|
|
497
658
|
// - 模式是 auto 且审计完成
|
|
498
659
|
// - 或者 AI 明确输出了结束语
|
|
499
660
|
// - 或者 AI 输出了普通内容且没有工具调用(针对问答模式)
|
|
500
|
-
const isAutoFinished = state.mode ===
|
|
501
|
-
const isFinalAnswer = content.includes(
|
|
661
|
+
const isAutoFinished = state.mode === 'auto' && state.auditedFiles.length > state.targetCount;
|
|
662
|
+
const isFinalAnswer = content.includes('Final Answer');
|
|
502
663
|
// ✅ 修复核心:如果 AI 只是在聊天(没有工具调用),直接结束,不要跳回 agent
|
|
503
|
-
if (isAutoFinished || isFinalAnswer || state.mode ===
|
|
664
|
+
if (isAutoFinished || isFinalAnswer || state.mode === 'chat') {
|
|
504
665
|
this.printFinalSummary(state);
|
|
505
666
|
return langgraph_1.END;
|
|
506
667
|
}
|
|
507
668
|
// 兜底:如果是在 auto 模式且还没干完活,才跳回 agent(通常不会走到这里)
|
|
508
669
|
return langgraph_1.END;
|
|
509
670
|
})
|
|
510
|
-
.addEdge(
|
|
511
|
-
.addEdge(
|
|
671
|
+
.addEdge('tools', 'progress')
|
|
672
|
+
.addEdge('progress', 'agent');
|
|
512
673
|
return workflow.compile({ checkpointer: this.checkpointer });
|
|
513
674
|
}
|
|
514
675
|
}
|
|
@@ -7,13 +7,29 @@ export interface AgentGraphLLMResponse {
|
|
|
7
7
|
token?: number;
|
|
8
8
|
duration?: number;
|
|
9
9
|
}
|
|
10
|
+
/** 流式输出的回调类型 */
|
|
11
|
+
export type StreamChunkCallback = (chunk: string) => void;
|
|
10
12
|
export declare abstract class AgentGraphModel extends BaseChatModel {
|
|
11
13
|
protected boundTools?: any[];
|
|
12
14
|
constructor(fields?: BaseChatModelParams);
|
|
13
15
|
bindTools(tools: any[]): any;
|
|
14
16
|
abstract callApi(prompt: string): Promise<AgentGraphLLMResponse>;
|
|
17
|
+
/**
|
|
18
|
+
* 流式调用 API,子类可覆盖以实现真正的 SSE 流式传输。
|
|
19
|
+
* 默认回退到 callApi 非流式调用。
|
|
20
|
+
* @param prompt 序列化后的提示词
|
|
21
|
+
* @param onChunk 每收到一段文本时的回调
|
|
22
|
+
* @returns 完整的响应结果
|
|
23
|
+
*/
|
|
24
|
+
callApiStream(prompt: string, onChunk: StreamChunkCallback): Promise<AgentGraphLLMResponse>;
|
|
15
25
|
_generate(messages: BaseMessage[]): Promise<ChatResult>;
|
|
16
26
|
private parseToolCalls;
|
|
17
|
-
|
|
27
|
+
/**
|
|
28
|
+
* 流式生成:调用 callApiStream 进行流式输出,完成后构建完整的 ChatResult。
|
|
29
|
+
* @param messages LangChain 消息列表
|
|
30
|
+
* @param onChunk 每收到一段文本时的回调
|
|
31
|
+
*/
|
|
32
|
+
streamGenerate(messages: BaseMessage[], onChunk: StreamChunkCallback): Promise<ChatResult>;
|
|
33
|
+
serializeMessages(messages: BaseMessage[]): string;
|
|
18
34
|
_llmType(): string;
|
|
19
35
|
}
|
|
@@ -13,6 +13,18 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
13
13
|
this.boundTools = tools.map(t => (0, function_calling_1.convertToOpenAITool)(t));
|
|
14
14
|
return this;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* 流式调用 API,子类可覆盖以实现真正的 SSE 流式传输。
|
|
18
|
+
* 默认回退到 callApi 非流式调用。
|
|
19
|
+
* @param prompt 序列化后的提示词
|
|
20
|
+
* @param onChunk 每收到一段文本时的回调
|
|
21
|
+
* @returns 完整的响应结果
|
|
22
|
+
*/
|
|
23
|
+
async callApiStream(prompt, onChunk) {
|
|
24
|
+
const result = await this.callApi(prompt);
|
|
25
|
+
onChunk(result.text);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
16
28
|
async _generate(messages) {
|
|
17
29
|
const fullPrompt = this.serializeMessages(messages);
|
|
18
30
|
const response = await this.callApi(fullPrompt);
|
|
@@ -92,6 +104,47 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
|
|
|
92
104
|
},
|
|
93
105
|
];
|
|
94
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* 流式生成:调用 callApiStream 进行流式输出,完成后构建完整的 ChatResult。
|
|
109
|
+
* @param messages LangChain 消息列表
|
|
110
|
+
* @param onChunk 每收到一段文本时的回调
|
|
111
|
+
*/
|
|
112
|
+
async streamGenerate(messages, onChunk) {
|
|
113
|
+
const fullPrompt = this.serializeMessages(messages);
|
|
114
|
+
const response = await this.callApiStream(fullPrompt, onChunk);
|
|
115
|
+
let { text, reasoning } = response;
|
|
116
|
+
// 1. 处理思考内容
|
|
117
|
+
if (!reasoning && text.includes('<think>')) {
|
|
118
|
+
const match = text.match(/<think>([\s\S]*?)<\/think>/);
|
|
119
|
+
if (match) {
|
|
120
|
+
reasoning = match[1].trim();
|
|
121
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 2. 解析工具调用
|
|
125
|
+
const toolCalls = this.parseToolCalls(text);
|
|
126
|
+
return {
|
|
127
|
+
generations: [
|
|
128
|
+
{
|
|
129
|
+
text,
|
|
130
|
+
message: new messages_1.AIMessage({
|
|
131
|
+
content: text,
|
|
132
|
+
tool_calls: toolCalls,
|
|
133
|
+
additional_kwargs: {
|
|
134
|
+
reasoning: reasoning || '',
|
|
135
|
+
token: response.token,
|
|
136
|
+
duration: response.duration,
|
|
137
|
+
},
|
|
138
|
+
response_metadata: {
|
|
139
|
+
reasoning: reasoning || '',
|
|
140
|
+
token: response.token,
|
|
141
|
+
duration: response.duration,
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
95
148
|
serializeMessages(messages) {
|
|
96
149
|
const systemMsg = messages.find(m => m._getType() === 'system');
|
|
97
150
|
const lastMsg = messages[messages.length - 1];
|
package/lib/types/type.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"description": "AI Assistant CLI.",
|
|
5
5
|
"author": "saber2pr",
|
|
6
6
|
"license": "ISC",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"@saber2pr/ts-context-mcp": "^0.0.9",
|
|
31
31
|
"diff": "^8.0.3",
|
|
32
32
|
"glob": "^10.5.0",
|
|
33
|
-
"js-tiktoken": "^1.0.21",
|
|
34
33
|
"minimatch": "^10.0.1",
|
|
35
34
|
"openai": "^6.16.0",
|
|
36
35
|
"typescript": "^5.9.3",
|