@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.
@@ -24,9 +24,9 @@ export default class McpAgent {
24
24
  private getToolsForOpenAIAPI;
25
25
  private processChat;
26
26
  /**
27
- * 裁剪上下文消息列表
28
- * 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
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: "system",
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 === "string") {
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 === "text" && "text" in part) {
117
- total += this.encoder.encode(part.text || "").length;
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 === "assistant" && msg.tool_calls) {
127
+ if (msg.role === 'assistant' && msg.tool_calls) {
126
128
  for (const call of msg.tool_calls) {
127
- if (call.type === "function") {
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 === "tool" && typeof msg.content === "string") {
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, "utf-8"));
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((res) => rl.question(q, res));
169
- console.log("\n🔑 配置 API 凭据:");
170
+ const question = (q) => new Promise(res => rl.question(q, res));
171
+ console.log('\n🔑 配置 API 凭据:');
170
172
  const config = {
171
- baseURL: await question("? API Base URL (如 https://api.openai.com/v1): "),
172
- apiKey: await question("? API Key: "),
173
- model: await question("? Model Name (如 gpt-4o): "),
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(), ".cursor", "mcp.json"),
183
- path_1.default.join(os_1.default.homedir(), ".vscode", "mcp.json"),
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((p) => {
187
+ paths.forEach(p => {
186
188
  if (fs_1.default.existsSync(p)) {
187
- const content = JSON.parse(fs_1.default.readFileSync(p, "utf-8"));
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: "1.0.0" }, { capabilities: {} });
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: "function",
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((tool) => {
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 && typeof parameters === 'object' && ('_def' in parameters || parameters.safeParse)) {
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: "system",
262
- content: "注意:上下文即将耗尽。请停止读取新文件,优先处理现有信息并尽快输出结果。"
265
+ role: 'system',
266
+ content: '注意:上下文即将耗尽。请停止读取新文件,优先处理现有信息并尽快输出结果。',
263
267
  });
264
268
  }
265
- const stopLoading = this.showLoading("🤖 Agent 正在思考...");
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].message;
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 === "string" ? result : JSON.stringify(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
- * 保留第一条 System 消息,并移除中间的旧消息直到低于阈值
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("\n❌ 系统错误:", error.message);
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("\n👤 你: ", async (input) => {
393
- if (input.toLowerCase() === "exit")
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("\n❌ 系统错误:", err.message);
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.39",
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",