@saber2pr/ai-agent 0.0.39 → 0.0.41
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.d.ts +3 -3
- package/lib/core/agent.js +52 -48
- package/package.json +1 -2
package/lib/core/agent.d.ts
CHANGED
|
@@ -24,9 +24,9 @@ export default class McpAgent {
|
|
|
24
24
|
private getToolsForOpenAIAPI;
|
|
25
25
|
private processChat;
|
|
26
26
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
* 裁剪上下文消息列表
|
|
28
|
+
* 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
|
|
29
|
+
*/
|
|
30
30
|
private pruneMessages;
|
|
31
31
|
/**
|
|
32
32
|
* 简易 Loading 动画辅助函数
|
package/lib/core/agent.js
CHANGED
|
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
const fs_1 = __importDefault(require("fs"));
|
|
40
|
-
const js_tiktoken_1 = require("js-tiktoken");
|
|
41
40
|
const openai_1 = __importDefault(require("openai"));
|
|
42
41
|
const os_1 = __importDefault(require("os"));
|
|
43
42
|
const path_1 = __importDefault(require("path"));
|
|
@@ -45,15 +44,15 @@ const readline = __importStar(require("readline"));
|
|
|
45
44
|
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
46
45
|
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
47
46
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
47
|
+
const tiktoken_1 = require("@langchain/core/utils/tiktoken");
|
|
48
48
|
const config_1 = require("../config/config");
|
|
49
49
|
const builtin_1 = require("../tools/builtin");
|
|
50
50
|
const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
|
|
51
51
|
class McpAgent {
|
|
52
52
|
constructor(options) {
|
|
53
|
-
this.modelName =
|
|
53
|
+
this.modelName = '';
|
|
54
54
|
this.allTools = [];
|
|
55
55
|
this.messages = [];
|
|
56
|
-
this.encoder = (0, js_tiktoken_1.getEncoding)("cl100k_base");
|
|
57
56
|
this.extraTools = [];
|
|
58
57
|
this.mcpClients = [];
|
|
59
58
|
this.targetDir = (options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd();
|
|
@@ -82,7 +81,7 @@ class McpAgent {
|
|
|
82
81
|
baseSystemPrompt += `\n\n[额外执行指令]:\n${extra}`;
|
|
83
82
|
}
|
|
84
83
|
this.messages.push({
|
|
85
|
-
role:
|
|
84
|
+
role: 'system',
|
|
86
85
|
content: baseSystemPrompt,
|
|
87
86
|
});
|
|
88
87
|
this.initTools(options); // 注入外部工具
|
|
@@ -101,20 +100,23 @@ class McpAgent {
|
|
|
101
100
|
* 计算当前消息列表的总 Token 消耗
|
|
102
101
|
* 兼容多模态内容 (Content Parts) 和 工具调用 (Tool Calls)
|
|
103
102
|
*/
|
|
104
|
-
calculateTokens() {
|
|
103
|
+
async calculateTokens() {
|
|
104
|
+
if (!this.encoder) {
|
|
105
|
+
this.encoder = await (0, tiktoken_1.getEncoding)('cl100k_base');
|
|
106
|
+
}
|
|
105
107
|
let total = 0;
|
|
106
108
|
for (const msg of this.messages) {
|
|
107
109
|
// 1. 处理消息内容 (Content)
|
|
108
110
|
if (msg.content) {
|
|
109
|
-
if (typeof msg.content ===
|
|
111
|
+
if (typeof msg.content === 'string') {
|
|
110
112
|
// 普通文本消息
|
|
111
113
|
total += this.encoder.encode(msg.content).length;
|
|
112
114
|
}
|
|
113
115
|
else if (Array.isArray(msg.content)) {
|
|
114
116
|
// 多模态/复合内容消息 (ChatCompletionContentPart[])
|
|
115
117
|
for (const part of msg.content) {
|
|
116
|
-
if (part.type ===
|
|
117
|
-
total += this.encoder.encode(part.text ||
|
|
118
|
+
if (part.type === 'text' && 'text' in part) {
|
|
119
|
+
total += this.encoder.encode(part.text || '').length;
|
|
118
120
|
}
|
|
119
121
|
// 注意:图片 (image_url) 的 Token 计算通常基于分辨率,tiktoken 无法计算
|
|
120
122
|
}
|
|
@@ -122,9 +124,9 @@ class McpAgent {
|
|
|
122
124
|
}
|
|
123
125
|
// 2. 处理助手角色发出的工具调用请求 (Assistant Tool Calls)
|
|
124
126
|
// 这是为了统计 AI 发出的指令所占用的 Token
|
|
125
|
-
if (msg.role ===
|
|
127
|
+
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
126
128
|
for (const call of msg.tool_calls) {
|
|
127
|
-
if (call.type ===
|
|
129
|
+
if (call.type === 'function') {
|
|
128
130
|
// 统计函数名和参数字符串
|
|
129
131
|
total += this.encoder.encode(call.function.name).length;
|
|
130
132
|
total += this.encoder.encode(call.function.arguments).length;
|
|
@@ -133,7 +135,7 @@ class McpAgent {
|
|
|
133
135
|
}
|
|
134
136
|
// 3. 处理工具返回的结果 (Tool Role)
|
|
135
137
|
// 在 processChat 中,我们确保了工具返回的 result 最终被转为了 string
|
|
136
|
-
if (msg.role ===
|
|
138
|
+
if (msg.role === 'tool' && typeof msg.content === 'string') {
|
|
137
139
|
total += this.encoder.encode(msg.content).length;
|
|
138
140
|
}
|
|
139
141
|
}
|
|
@@ -145,10 +147,10 @@ class McpAgent {
|
|
|
145
147
|
...(0, builtin_1.createDefaultBuiltinTools)({
|
|
146
148
|
options: {
|
|
147
149
|
...options,
|
|
148
|
-
...this
|
|
149
|
-
}
|
|
150
|
+
...this,
|
|
151
|
+
},
|
|
150
152
|
}),
|
|
151
|
-
...this.extraTools
|
|
153
|
+
...this.extraTools,
|
|
152
154
|
];
|
|
153
155
|
if (allTools === null || allTools === void 0 ? void 0 : allTools.length) {
|
|
154
156
|
this.allTools.push(...allTools);
|
|
@@ -159,18 +161,18 @@ class McpAgent {
|
|
|
159
161
|
if (this.apiConfig)
|
|
160
162
|
return this.apiConfig;
|
|
161
163
|
if (fs_1.default.existsSync(config_1.CONFIG_FILE)) {
|
|
162
|
-
return JSON.parse(fs_1.default.readFileSync(config_1.CONFIG_FILE,
|
|
164
|
+
return JSON.parse(fs_1.default.readFileSync(config_1.CONFIG_FILE, 'utf-8'));
|
|
163
165
|
}
|
|
164
166
|
const rl = readline.createInterface({
|
|
165
167
|
input: process.stdin,
|
|
166
168
|
output: process.stdout,
|
|
167
169
|
});
|
|
168
|
-
const question = (q) => new Promise(
|
|
169
|
-
console.log(
|
|
170
|
+
const question = (q) => new Promise(res => rl.question(q, res));
|
|
171
|
+
console.log('\n🔑 配置 API 凭据:');
|
|
170
172
|
const config = {
|
|
171
|
-
baseURL: await question(
|
|
172
|
-
apiKey: await question(
|
|
173
|
-
model: await question(
|
|
173
|
+
baseURL: await question('? API Base URL (如 https://api.openai.com/v1): '),
|
|
174
|
+
apiKey: await question('? API Key: '),
|
|
175
|
+
model: await question('? Model Name (如 gpt-4o): '),
|
|
174
176
|
};
|
|
175
177
|
fs_1.default.writeFileSync(config_1.CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
176
178
|
rl.close();
|
|
@@ -179,12 +181,12 @@ class McpAgent {
|
|
|
179
181
|
loadMcpConfigs() {
|
|
180
182
|
const combined = { mcpServers: {} };
|
|
181
183
|
const paths = [
|
|
182
|
-
path_1.default.join(os_1.default.homedir(),
|
|
183
|
-
path_1.default.join(os_1.default.homedir(),
|
|
184
|
+
path_1.default.join(os_1.default.homedir(), '.cursor', 'mcp.json'),
|
|
185
|
+
path_1.default.join(os_1.default.homedir(), '.vscode', 'mcp.json'),
|
|
184
186
|
];
|
|
185
|
-
paths.forEach(
|
|
187
|
+
paths.forEach(p => {
|
|
186
188
|
if (fs_1.default.existsSync(p)) {
|
|
187
|
-
const content = JSON.parse(fs_1.default.readFileSync(p,
|
|
189
|
+
const content = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
|
|
188
190
|
Object.assign(combined.mcpServers, content.mcpServers);
|
|
189
191
|
}
|
|
190
192
|
});
|
|
@@ -206,12 +208,12 @@ class McpAgent {
|
|
|
206
208
|
args: server.args || [],
|
|
207
209
|
env: { ...process.env, ...server.env },
|
|
208
210
|
});
|
|
209
|
-
const client = new index_js_1.Client({ name, version:
|
|
211
|
+
const client = new index_js_1.Client({ name, version: '1.0.0' }, { capabilities: {} });
|
|
210
212
|
await client.connect(transport);
|
|
211
213
|
this.mcpClients.push(client);
|
|
212
214
|
const { tools } = await client.listTools();
|
|
213
215
|
this.allTools.push(...tools.map((t) => ({
|
|
214
|
-
type:
|
|
216
|
+
type: 'function',
|
|
215
217
|
function: {
|
|
216
218
|
name: `${name}__${t.name}`,
|
|
217
219
|
description: t.description,
|
|
@@ -228,12 +230,14 @@ class McpAgent {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
getToolsForOpenAIAPI() {
|
|
231
|
-
return this.allTools.map(
|
|
233
|
+
return this.allTools.map(tool => {
|
|
232
234
|
const { _handler, _client, _originalName, ...rest } = tool;
|
|
233
235
|
let parameters = rest.function.parameters;
|
|
234
236
|
// 💡 核心逻辑:判断是否为 Zod 实例并转换
|
|
235
237
|
// Zod 对象通常包含 _def 属性,或者你可以用 instanceof z.ZodType
|
|
236
|
-
if (parameters &&
|
|
238
|
+
if (parameters &&
|
|
239
|
+
typeof parameters === 'object' &&
|
|
240
|
+
('_def' in parameters || parameters.safeParse)) {
|
|
237
241
|
// 使用 zod-to-json-schema 转换为标准 JSON Schema
|
|
238
242
|
parameters = (0, zod_to_json_schema_1.zodToJsonSchema)(parameters);
|
|
239
243
|
}
|
|
@@ -251,31 +255,31 @@ class McpAgent {
|
|
|
251
255
|
this.messages.push({ role: 'user', content: userInput });
|
|
252
256
|
while (true) {
|
|
253
257
|
// --- 新增:发送请求前先检查并裁剪 ---
|
|
254
|
-
this.pruneMessages();
|
|
258
|
+
await this.pruneMessages();
|
|
255
259
|
// 打印当前上下文的累计 Token
|
|
256
|
-
const currentInputTokens = this.calculateTokens();
|
|
260
|
+
const currentInputTokens = await this.calculateTokens();
|
|
257
261
|
console.log(`\n📊 当前上下文累计: ${currentInputTokens} tokens`);
|
|
258
262
|
// 如果接近上限(如 80%),在消息队列中插入一条隐含的系统指令
|
|
259
263
|
if (currentInputTokens > this.maxTokens * 0.8 && currentInputTokens <= this.maxTokens) {
|
|
260
264
|
this.messages.push({
|
|
261
|
-
role:
|
|
262
|
-
content:
|
|
265
|
+
role: 'system',
|
|
266
|
+
content: '注意:上下文即将耗尽。请停止读取新文件,优先处理现有信息并尽快输出结果。',
|
|
263
267
|
});
|
|
264
268
|
}
|
|
265
|
-
const stopLoading = this.showLoading(
|
|
269
|
+
const stopLoading = this.showLoading('🤖 Agent 正在思考...');
|
|
266
270
|
let response;
|
|
267
271
|
try {
|
|
268
272
|
response = await this.openai.chat.completions.create({
|
|
269
273
|
model: this.modelName,
|
|
270
274
|
messages: this.messages,
|
|
271
275
|
tools: this.getToolsForOpenAIAPI(),
|
|
272
|
-
tool_choice: 'auto'
|
|
276
|
+
tool_choice: 'auto',
|
|
273
277
|
});
|
|
274
278
|
}
|
|
275
279
|
finally {
|
|
276
280
|
stopLoading();
|
|
277
281
|
}
|
|
278
|
-
const message = response.choices[0]
|
|
282
|
+
const { message } = response.choices[0];
|
|
279
283
|
this.messages.push(message);
|
|
280
284
|
// 计算本次 AI 回复生成的 Token
|
|
281
285
|
const completionTokens = ((_a = response.usage) === null || _a === void 0 ? void 0 : _a.completion_tokens) ||
|
|
@@ -303,35 +307,35 @@ class McpAgent {
|
|
|
303
307
|
else if ((tool === null || tool === void 0 ? void 0 : tool._client) && tool._originalName) {
|
|
304
308
|
const mcpRes = await tool._client.callTool({
|
|
305
309
|
name: tool._originalName,
|
|
306
|
-
arguments: args
|
|
310
|
+
arguments: args,
|
|
307
311
|
});
|
|
308
312
|
result = mcpRes.content;
|
|
309
313
|
}
|
|
310
|
-
const resultContent = typeof result ===
|
|
314
|
+
const resultContent = typeof result === 'string' ? result : JSON.stringify(result);
|
|
311
315
|
// 打印工具返回结果的 Token 消耗
|
|
312
316
|
const toolResultTokens = this.encoder.encode(resultContent).length;
|
|
313
317
|
console.log(` 📝 工具输出: ${toolResultTokens} tokens`);
|
|
314
318
|
this.messages.push({
|
|
315
319
|
role: 'tool',
|
|
316
320
|
tool_call_id: call.id,
|
|
317
|
-
content: resultContent
|
|
321
|
+
content: resultContent,
|
|
318
322
|
});
|
|
319
323
|
console.log(` ✅ 完成: ${call.function.name}`);
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
pruneMessages() {
|
|
328
|
-
const currentTokens = this.calculateTokens();
|
|
328
|
+
* 裁剪上下文消息列表
|
|
329
|
+
* 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
|
|
330
|
+
*/
|
|
331
|
+
async pruneMessages() {
|
|
332
|
+
const currentTokens = await this.calculateTokens();
|
|
329
333
|
if (currentTokens <= this.maxTokens)
|
|
330
334
|
return;
|
|
331
335
|
console.log(`\n⚠️ 上下文达到限制 (${currentTokens} tokens),正在自动裁剪...`);
|
|
332
336
|
// 策略:保留索引 0 (System),从索引 1 开始删除
|
|
333
337
|
// 每次删除一对 (通常是助理请求 + 工具回复,或者用户提问 + 助理回答)
|
|
334
|
-
while (this.calculateTokens() > this.maxTokens && this.messages.length > 2) {
|
|
338
|
+
while (await this.calculateTokens() > this.maxTokens && this.messages.length > 2) {
|
|
335
339
|
// 始终保留系统提示词 (index 0) 和最后一条消息 (保持对话连贯)
|
|
336
340
|
// 删除索引为 1 的消息
|
|
337
341
|
this.messages.splice(1, 1);
|
|
@@ -371,7 +375,7 @@ class McpAgent {
|
|
|
371
375
|
return lastMsg.role === 'assistant' ? lastMsg.content : '';
|
|
372
376
|
}
|
|
373
377
|
catch (error) {
|
|
374
|
-
console.error(
|
|
378
|
+
console.error('\n❌ 系统错误:', error.message);
|
|
375
379
|
return '';
|
|
376
380
|
}
|
|
377
381
|
finally {
|
|
@@ -389,15 +393,15 @@ class McpAgent {
|
|
|
389
393
|
});
|
|
390
394
|
console.log(`\n🚀 代码助手已启动 (目标目录: ${this.targetDir})`);
|
|
391
395
|
const chatLoop = () => {
|
|
392
|
-
rl.question(
|
|
393
|
-
if (input.toLowerCase() ===
|
|
396
|
+
rl.question('\n👤 你: ', async (input) => {
|
|
397
|
+
if (input.toLowerCase() === 'exit')
|
|
394
398
|
process.exit(0);
|
|
395
399
|
try {
|
|
396
400
|
// 这里统一调用 chat 或核心逻辑
|
|
397
401
|
await this.chat(input);
|
|
398
402
|
}
|
|
399
403
|
catch (err) {
|
|
400
|
-
console.error(
|
|
404
|
+
console.error('\n❌ 系统错误:', err.message);
|
|
401
405
|
}
|
|
402
406
|
chatLoop();
|
|
403
407
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saber2pr/ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.41",
|
|
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",
|