@zhin.js/core 1.0.25 → 1.0.27
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/CHANGELOG.md +19 -0
- package/README.md +84 -342
- package/lib/adapter.d.ts +17 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +84 -2
- package/lib/adapter.js.map +1 -1
- package/lib/ai/agent.d.ts +126 -0
- package/lib/ai/agent.d.ts.map +1 -0
- package/lib/ai/agent.js +645 -0
- package/lib/ai/agent.js.map +1 -0
- package/lib/ai/context-manager.d.ts +213 -0
- package/lib/ai/context-manager.d.ts.map +1 -0
- package/lib/ai/context-manager.js +313 -0
- package/lib/ai/context-manager.js.map +1 -0
- package/lib/ai/conversation-memory.d.ts +181 -0
- package/lib/ai/conversation-memory.d.ts.map +1 -0
- package/lib/ai/conversation-memory.js +581 -0
- package/lib/ai/conversation-memory.js.map +1 -0
- package/lib/ai/follow-up.d.ts +131 -0
- package/lib/ai/follow-up.d.ts.map +1 -0
- package/lib/ai/follow-up.js +265 -0
- package/lib/ai/follow-up.js.map +1 -0
- package/lib/ai/index.d.ts +29 -0
- package/lib/ai/index.d.ts.map +1 -0
- package/lib/ai/index.js +34 -0
- package/lib/ai/index.js.map +1 -0
- package/lib/ai/init.d.ts +30 -0
- package/lib/ai/init.d.ts.map +1 -0
- package/lib/ai/init.js +424 -0
- package/lib/ai/init.js.map +1 -0
- package/lib/ai/output.d.ts +93 -0
- package/lib/ai/output.d.ts.map +1 -0
- package/lib/ai/output.js +176 -0
- package/lib/ai/output.js.map +1 -0
- package/lib/ai/providers/anthropic.d.ts +23 -0
- package/lib/ai/providers/anthropic.d.ts.map +1 -0
- package/lib/ai/providers/anthropic.js +322 -0
- package/lib/ai/providers/anthropic.js.map +1 -0
- package/lib/ai/providers/base.d.ts +43 -0
- package/lib/ai/providers/base.d.ts.map +1 -0
- package/lib/ai/providers/base.js +135 -0
- package/lib/ai/providers/base.js.map +1 -0
- package/lib/ai/providers/index.d.ts +12 -0
- package/lib/ai/providers/index.d.ts.map +1 -0
- package/lib/ai/providers/index.js +9 -0
- package/lib/ai/providers/index.js.map +1 -0
- package/lib/ai/providers/ollama.d.ts +25 -0
- package/lib/ai/providers/ollama.d.ts.map +1 -0
- package/lib/ai/providers/ollama.js +243 -0
- package/lib/ai/providers/ollama.js.map +1 -0
- package/lib/ai/providers/openai.d.ts +46 -0
- package/lib/ai/providers/openai.d.ts.map +1 -0
- package/lib/ai/providers/openai.js +132 -0
- package/lib/ai/providers/openai.js.map +1 -0
- package/lib/ai/rate-limiter.d.ts +38 -0
- package/lib/ai/rate-limiter.d.ts.map +1 -0
- package/lib/ai/rate-limiter.js +86 -0
- package/lib/ai/rate-limiter.js.map +1 -0
- package/lib/ai/service.d.ts +81 -0
- package/lib/ai/service.d.ts.map +1 -0
- package/lib/ai/service.js +274 -0
- package/lib/ai/service.js.map +1 -0
- package/lib/ai/session.d.ts +186 -0
- package/lib/ai/session.d.ts.map +1 -0
- package/lib/ai/session.js +443 -0
- package/lib/ai/session.js.map +1 -0
- package/lib/ai/tone-detector.d.ts +19 -0
- package/lib/ai/tone-detector.d.ts.map +1 -0
- package/lib/ai/tone-detector.js +72 -0
- package/lib/ai/tone-detector.js.map +1 -0
- package/lib/ai/tools.d.ts +45 -0
- package/lib/ai/tools.d.ts.map +1 -0
- package/lib/ai/tools.js +206 -0
- package/lib/ai/tools.js.map +1 -0
- package/lib/ai/types.d.ts +264 -0
- package/lib/ai/types.d.ts.map +1 -0
- package/lib/ai/types.js +6 -0
- package/lib/ai/types.js.map +1 -0
- package/lib/ai/user-profile.d.ts +56 -0
- package/lib/ai/user-profile.d.ts.map +1 -0
- package/lib/ai/user-profile.js +130 -0
- package/lib/ai/user-profile.js.map +1 -0
- package/lib/ai/zhin-agent.d.ts +165 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -0
- package/lib/ai/zhin-agent.js +707 -0
- package/lib/ai/zhin-agent.js.map +1 -0
- package/lib/built/ai-trigger.d.ts.map +1 -1
- package/lib/built/ai-trigger.js +7 -3
- package/lib/built/ai-trigger.js.map +1 -1
- package/lib/built/command.d.ts +33 -17
- package/lib/built/command.d.ts.map +1 -1
- package/lib/built/command.js +71 -44
- package/lib/built/command.js.map +1 -1
- package/lib/built/component.d.ts +42 -15
- package/lib/built/component.d.ts.map +1 -1
- package/lib/built/component.js +84 -52
- package/lib/built/component.js.map +1 -1
- package/lib/built/config.d.ts +64 -5
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +129 -12
- package/lib/built/config.js.map +1 -1
- package/lib/built/cron.d.ts +41 -18
- package/lib/built/cron.d.ts.map +1 -1
- package/lib/built/cron.js +106 -63
- package/lib/built/cron.js.map +1 -1
- package/lib/built/database.d.ts +55 -6
- package/lib/built/database.d.ts.map +1 -1
- package/lib/built/database.js +93 -22
- package/lib/built/database.js.map +1 -1
- package/lib/built/dispatcher.d.ts +118 -0
- package/lib/built/dispatcher.d.ts.map +1 -0
- package/lib/built/dispatcher.js +196 -0
- package/lib/built/dispatcher.js.map +1 -0
- package/lib/built/permission.d.ts +45 -5
- package/lib/built/permission.d.ts.map +1 -1
- package/lib/built/permission.js +56 -11
- package/lib/built/permission.js.map +1 -1
- package/lib/built/skill.d.ts +117 -0
- package/lib/built/skill.d.ts.map +1 -0
- package/lib/built/skill.js +191 -0
- package/lib/built/skill.js.map +1 -0
- package/lib/built/tool.d.ts +71 -164
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +212 -297
- package/lib/built/tool.js.map +1 -1
- package/lib/feature.d.ts +75 -0
- package/lib/feature.d.ts.map +1 -0
- package/lib/feature.js +69 -0
- package/lib/feature.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +25 -17
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +180 -20
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +4 -9
- package/lib/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/adapter.ts +101 -2
- package/src/ai/agent.ts +772 -0
- package/src/ai/context-manager.ts +440 -0
- package/src/ai/conversation-memory.ts +774 -0
- package/src/ai/follow-up.ts +357 -0
- package/src/ai/index.ts +128 -0
- package/src/ai/init.ts +502 -0
- package/src/ai/output.ts +261 -0
- package/src/ai/providers/anthropic.ts +375 -0
- package/src/ai/providers/base.ts +173 -0
- package/src/ai/providers/index.ts +13 -0
- package/src/ai/providers/ollama.ts +292 -0
- package/src/ai/providers/openai.ts +167 -0
- package/src/ai/rate-limiter.ts +129 -0
- package/src/ai/service.ts +319 -0
- package/src/ai/session.ts +544 -0
- package/src/ai/tone-detector.ts +89 -0
- package/src/ai/tools.ts +218 -0
- package/src/ai/types.ts +296 -0
- package/src/ai/user-profile.ts +181 -0
- package/src/ai/zhin-agent.ts +845 -0
- package/src/built/ai-trigger.ts +6 -3
- package/src/built/command.ts +75 -69
- package/src/built/component.ts +94 -76
- package/src/built/config.ts +288 -128
- package/src/built/cron.ts +117 -101
- package/src/built/database.ts +128 -33
- package/src/built/dispatcher.ts +332 -0
- package/src/built/permission.ts +146 -54
- package/src/built/skill.ts +280 -0
- package/src/built/tool.ts +245 -366
- package/src/feature.ts +113 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +198 -33
- package/src/types.ts +6 -10
- package/tests/adapter.test.ts +153 -1
- package/tests/ai/agent.test.ts +614 -0
- package/tests/ai/ai-trigger.test.ts +368 -0
- package/tests/ai/context-manager.test.ts +413 -0
- package/tests/ai/conversation-memory.test.ts +128 -0
- package/tests/ai/follow-up.test.ts +175 -0
- package/tests/ai/integration.test.ts +584 -0
- package/tests/ai/output.test.ts +128 -0
- package/tests/ai/providers.integration.test.ts +227 -0
- package/tests/ai/rate-limiter.test.ts +108 -0
- package/tests/ai/session.test.ts +375 -0
- package/tests/ai/setup.ts +308 -0
- package/tests/ai/tone-detector.test.ts +80 -0
- package/tests/ai/tool.test.ts +800 -0
- package/tests/ai/tools-builtin.test.ts +346 -0
- package/tests/ai/user-profile.test.ts +73 -0
- package/tests/ai/zhin-agent.test.ts +177 -0
- package/tests/config.test.ts +46 -0
- package/tests/cron.test.ts +94 -5
- package/tests/dispatcher.test.ts +146 -0
- package/tests/feature.test.ts +145 -0
- package/tests/features-builtin.test.ts +191 -0
- package/tests/plugin.test.ts +88 -14
- package/tests/skill-feature.test.ts +179 -0
- package/tests/tool-feature.test.ts +254 -0
- package/test/minimal-bot.ts +0 -31
- package/test/stress-test.ts +0 -123
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 集成测试
|
|
3
|
+
*
|
|
4
|
+
* 测试 AI Agent 的完整流程,包括:
|
|
5
|
+
* - 工具调用流程
|
|
6
|
+
* - 对话历史管理
|
|
7
|
+
* - 错误处理
|
|
8
|
+
* - 重复调用检测
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { createMockTool } from './setup.js';
|
|
12
|
+
|
|
13
|
+
// Mock Logger
|
|
14
|
+
vi.mock('@zhin.js/core', async (importOriginal) => {
|
|
15
|
+
const original = await importOriginal() as any;
|
|
16
|
+
return {
|
|
17
|
+
...original,
|
|
18
|
+
Logger: class {
|
|
19
|
+
debug = vi.fn();
|
|
20
|
+
info = vi.fn();
|
|
21
|
+
warn = vi.fn();
|
|
22
|
+
error = vi.fn();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
import { createAgent, Agent } from '../../src/ai/agent.js';
|
|
28
|
+
|
|
29
|
+
// 创建完整的 mock provider
|
|
30
|
+
const createMockProvider = () => ({
|
|
31
|
+
name: 'mock',
|
|
32
|
+
models: ['mock-model'],
|
|
33
|
+
chat: vi.fn(),
|
|
34
|
+
healthCheck: vi.fn().mockResolvedValue(true),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 创建标准 chat 响应
|
|
38
|
+
const createChatResponse = (content: string, toolCalls?: any[]) => ({
|
|
39
|
+
id: 'test-id',
|
|
40
|
+
object: 'chat.completion',
|
|
41
|
+
created: Date.now(),
|
|
42
|
+
model: 'mock-model',
|
|
43
|
+
choices: [{
|
|
44
|
+
index: 0,
|
|
45
|
+
message: {
|
|
46
|
+
role: 'assistant',
|
|
47
|
+
content,
|
|
48
|
+
tool_calls: toolCalls,
|
|
49
|
+
},
|
|
50
|
+
finish_reason: toolCalls ? 'tool_calls' : 'stop',
|
|
51
|
+
}],
|
|
52
|
+
usage: {
|
|
53
|
+
prompt_tokens: 10,
|
|
54
|
+
completion_tokens: 10,
|
|
55
|
+
total_tokens: 20,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Agent 完整流程测试', () => {
|
|
60
|
+
let mockProvider: ReturnType<typeof createMockProvider>;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
mockProvider = createMockProvider();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('基本对话', () => {
|
|
68
|
+
it('应该处理简单的文本响应', async () => {
|
|
69
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('你好!'));
|
|
70
|
+
|
|
71
|
+
const agent = createAgent(mockProvider as any, {
|
|
72
|
+
systemPrompt: '你是一个助手',
|
|
73
|
+
tools: [],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await agent.run('你好');
|
|
77
|
+
|
|
78
|
+
expect(result.content).toBe('你好!');
|
|
79
|
+
expect(result.iterations).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('应该正确统计 token 用量', async () => {
|
|
83
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('回复'));
|
|
84
|
+
|
|
85
|
+
const agent = createAgent(mockProvider as any, {
|
|
86
|
+
tools: [],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await agent.run('测试');
|
|
90
|
+
|
|
91
|
+
expect(result.usage.total_tokens).toBe(20);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('工具调用', () => {
|
|
96
|
+
it('应该正确执行单个工具调用', async () => {
|
|
97
|
+
const calculatorTool = createMockTool({
|
|
98
|
+
name: 'calculator',
|
|
99
|
+
description: '计算器',
|
|
100
|
+
parameters: {
|
|
101
|
+
expression: { type: 'string', description: '表达式' },
|
|
102
|
+
},
|
|
103
|
+
required: ['expression'],
|
|
104
|
+
executeResult: JSON.stringify({ result: 42 }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 第一次调用返回工具调用
|
|
108
|
+
mockProvider.chat
|
|
109
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
110
|
+
id: 'call-1',
|
|
111
|
+
type: 'function',
|
|
112
|
+
function: {
|
|
113
|
+
name: 'calculator',
|
|
114
|
+
arguments: JSON.stringify({ expression: '6 * 7' }),
|
|
115
|
+
},
|
|
116
|
+
}]))
|
|
117
|
+
.mockResolvedValueOnce(createChatResponse('计算结果是 42'));
|
|
118
|
+
|
|
119
|
+
const agent = createAgent(mockProvider as any, {
|
|
120
|
+
tools: [calculatorTool],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await agent.run('计算 6 * 7');
|
|
124
|
+
|
|
125
|
+
// 工具应该被调用
|
|
126
|
+
expect(calculatorTool.execute).toHaveBeenCalledWith(
|
|
127
|
+
{ expression: '6 * 7' }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// 应该返回最终结果
|
|
131
|
+
expect(result.content).toBe('计算结果是 42');
|
|
132
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
133
|
+
expect(result.toolCalls[0].tool).toBe('calculator');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('应该处理多个工具调用', async () => {
|
|
137
|
+
const tool1 = createMockTool({
|
|
138
|
+
name: 'tool1',
|
|
139
|
+
executeResult: 'result1',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const tool2 = createMockTool({
|
|
143
|
+
name: 'tool2',
|
|
144
|
+
executeResult: 'result2',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
mockProvider.chat
|
|
148
|
+
.mockResolvedValueOnce(createChatResponse('', [
|
|
149
|
+
{
|
|
150
|
+
id: 'call-1',
|
|
151
|
+
type: 'function',
|
|
152
|
+
function: { name: 'tool1', arguments: '{}' },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'call-2',
|
|
156
|
+
type: 'function',
|
|
157
|
+
function: { name: 'tool2', arguments: '{}' },
|
|
158
|
+
},
|
|
159
|
+
]))
|
|
160
|
+
.mockResolvedValueOnce(createChatResponse('完成'));
|
|
161
|
+
|
|
162
|
+
const agent = createAgent(mockProvider as any, {
|
|
163
|
+
tools: [tool1, tool2],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await agent.run('执行两个工具');
|
|
167
|
+
|
|
168
|
+
expect(tool1.execute).toHaveBeenCalled();
|
|
169
|
+
expect(tool2.execute).toHaveBeenCalled();
|
|
170
|
+
expect(result.toolCalls).toHaveLength(2);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('应该检测并阻止重复工具调用', async () => {
|
|
174
|
+
const tool = createMockTool({
|
|
175
|
+
name: 'repeatable',
|
|
176
|
+
parameters: { query: { type: 'string' } },
|
|
177
|
+
executeResult: 'result',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 模拟重复调用同一工具
|
|
181
|
+
mockProvider.chat
|
|
182
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
183
|
+
id: 'call-1',
|
|
184
|
+
type: 'function',
|
|
185
|
+
function: { name: 'repeatable', arguments: JSON.stringify({ query: 'same' }) },
|
|
186
|
+
}]))
|
|
187
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
188
|
+
id: 'call-2',
|
|
189
|
+
type: 'function',
|
|
190
|
+
function: { name: 'repeatable', arguments: JSON.stringify({ query: 'same' }) },
|
|
191
|
+
}]))
|
|
192
|
+
.mockResolvedValueOnce(createChatResponse('完成'));
|
|
193
|
+
|
|
194
|
+
const agent = createAgent(mockProvider as any, {
|
|
195
|
+
tools: [tool],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await agent.run('测试重复');
|
|
199
|
+
|
|
200
|
+
// 工具应该只被实际执行一次(第一次)
|
|
201
|
+
expect(tool.execute).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('工具执行错误处理', () => {
|
|
206
|
+
it('应该处理工具执行错误', async () => {
|
|
207
|
+
const errorTool = createMockTool({
|
|
208
|
+
name: 'error_tool',
|
|
209
|
+
executeError: new Error('工具执行失败'),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
mockProvider.chat
|
|
213
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
214
|
+
id: 'call-1',
|
|
215
|
+
type: 'function',
|
|
216
|
+
function: { name: 'error_tool', arguments: '{}' },
|
|
217
|
+
}]))
|
|
218
|
+
.mockResolvedValueOnce(createChatResponse('工具出错了'));
|
|
219
|
+
|
|
220
|
+
const agent = createAgent(mockProvider as any, {
|
|
221
|
+
tools: [errorTool],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await agent.run('执行会出错的工具');
|
|
225
|
+
|
|
226
|
+
// 应该继续执行,不会崩溃
|
|
227
|
+
expect(result.content).toBeDefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('应该处理不存在的工具调用', async () => {
|
|
231
|
+
mockProvider.chat
|
|
232
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
233
|
+
id: 'call-1',
|
|
234
|
+
type: 'function',
|
|
235
|
+
function: { name: 'nonexistent', arguments: '{}' },
|
|
236
|
+
}]))
|
|
237
|
+
.mockResolvedValueOnce(createChatResponse('工具不存在'));
|
|
238
|
+
|
|
239
|
+
const agent = createAgent(mockProvider as any, {
|
|
240
|
+
tools: [],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await agent.run('调用不存在的工具');
|
|
244
|
+
|
|
245
|
+
// 应该继续执行,包含错误信息
|
|
246
|
+
expect(result).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('事件监听', () => {
|
|
251
|
+
it('应该触发 tool_call 事件', async () => {
|
|
252
|
+
const tool = createMockTool({
|
|
253
|
+
name: 'event_tool',
|
|
254
|
+
executeResult: 'done',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
mockProvider.chat
|
|
258
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
259
|
+
id: 'call-1',
|
|
260
|
+
type: 'function',
|
|
261
|
+
function: { name: 'event_tool', arguments: JSON.stringify({ test: true }) },
|
|
262
|
+
}]))
|
|
263
|
+
.mockResolvedValueOnce(createChatResponse('完成'));
|
|
264
|
+
|
|
265
|
+
const agent = createAgent(mockProvider as any, {
|
|
266
|
+
tools: [tool],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const toolCallHandler = vi.fn();
|
|
270
|
+
agent.on('tool_call', toolCallHandler);
|
|
271
|
+
|
|
272
|
+
await agent.run('测试事件');
|
|
273
|
+
|
|
274
|
+
expect(toolCallHandler).toHaveBeenCalledWith('event_tool', { test: true });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('应该触发 tool_result 事件', async () => {
|
|
278
|
+
const tool = createMockTool({
|
|
279
|
+
name: 'result_tool',
|
|
280
|
+
executeResult: JSON.stringify({ data: 'test' }),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
mockProvider.chat
|
|
284
|
+
.mockResolvedValueOnce(createChatResponse('', [{
|
|
285
|
+
id: 'call-1',
|
|
286
|
+
type: 'function',
|
|
287
|
+
function: { name: 'result_tool', arguments: '{}' },
|
|
288
|
+
}]))
|
|
289
|
+
.mockResolvedValueOnce(createChatResponse('完成'));
|
|
290
|
+
|
|
291
|
+
const agent = createAgent(mockProvider as any, {
|
|
292
|
+
tools: [tool],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const toolResultHandler = vi.fn();
|
|
296
|
+
agent.on('tool_result', toolResultHandler);
|
|
297
|
+
|
|
298
|
+
await agent.run('测试结果事件');
|
|
299
|
+
|
|
300
|
+
// Result is the raw string from tool execution
|
|
301
|
+
expect(toolResultHandler).toHaveBeenCalledWith(
|
|
302
|
+
'result_tool',
|
|
303
|
+
expect.stringContaining('data')
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('应该触发 complete 事件', async () => {
|
|
308
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('完成'));
|
|
309
|
+
|
|
310
|
+
const agent = createAgent(mockProvider as any, {
|
|
311
|
+
tools: [],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const completeHandler = vi.fn();
|
|
315
|
+
agent.on('complete', completeHandler);
|
|
316
|
+
|
|
317
|
+
await agent.run('测试');
|
|
318
|
+
|
|
319
|
+
expect(completeHandler).toHaveBeenCalledWith(
|
|
320
|
+
expect.objectContaining({
|
|
321
|
+
content: '完成',
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('应该返回取消订阅函数', async () => {
|
|
327
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
328
|
+
|
|
329
|
+
const agent = createAgent(mockProvider as any, {
|
|
330
|
+
tools: [],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const handler = vi.fn();
|
|
334
|
+
const unsubscribe = agent.on('complete', handler);
|
|
335
|
+
|
|
336
|
+
// 取消订阅
|
|
337
|
+
unsubscribe();
|
|
338
|
+
|
|
339
|
+
await agent.run('测试');
|
|
340
|
+
|
|
341
|
+
// handler 不应该被调用
|
|
342
|
+
expect(handler).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('Agent 配置测试', () => {
|
|
348
|
+
it('应该使用自定义系统提示', async () => {
|
|
349
|
+
const mockProvider = createMockProvider();
|
|
350
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
351
|
+
|
|
352
|
+
const agent = createAgent(mockProvider as any, {
|
|
353
|
+
systemPrompt: '你是一个代码助手',
|
|
354
|
+
tools: [],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await agent.run('你好');
|
|
358
|
+
|
|
359
|
+
// 检查传递给 provider 的消息包含系统提示
|
|
360
|
+
expect(mockProvider.chat).toHaveBeenCalledWith(
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
messages: expect.arrayContaining([
|
|
363
|
+
expect.objectContaining({
|
|
364
|
+
role: 'system',
|
|
365
|
+
content: expect.stringContaining('代码助手'),
|
|
366
|
+
}),
|
|
367
|
+
]),
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('应该传递工具定义给 provider', async () => {
|
|
373
|
+
const mockProvider = createMockProvider();
|
|
374
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
375
|
+
|
|
376
|
+
const tool = createMockTool({
|
|
377
|
+
name: 'test_tool',
|
|
378
|
+
description: '测试工具',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const agent = createAgent(mockProvider as any, {
|
|
382
|
+
tools: [tool],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await agent.run('测试');
|
|
386
|
+
|
|
387
|
+
expect(mockProvider.chat).toHaveBeenCalledWith(
|
|
388
|
+
expect.objectContaining({
|
|
389
|
+
tools: expect.arrayContaining([
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
type: 'function',
|
|
392
|
+
function: expect.objectContaining({
|
|
393
|
+
name: 'test_tool',
|
|
394
|
+
}),
|
|
395
|
+
}),
|
|
396
|
+
]),
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('应该限制最大迭代次数', async () => {
|
|
402
|
+
const mockProvider = createMockProvider();
|
|
403
|
+
|
|
404
|
+
// 模拟一直返回工具调用
|
|
405
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('', [{
|
|
406
|
+
id: 'call-1',
|
|
407
|
+
type: 'function',
|
|
408
|
+
function: { name: 'infinite', arguments: '{}' },
|
|
409
|
+
}]));
|
|
410
|
+
|
|
411
|
+
const tool = createMockTool({
|
|
412
|
+
name: 'infinite',
|
|
413
|
+
executeResult: 'loop',
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const agent = createAgent(mockProvider as any, {
|
|
417
|
+
tools: [tool],
|
|
418
|
+
maxIterations: 3,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const result = await agent.run('无限循环');
|
|
422
|
+
|
|
423
|
+
// 应该在达到最大迭代次数后停止
|
|
424
|
+
expect(result.iterations).toBeLessThanOrEqual(3);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('Agent.filterTools 程序化过滤', () => {
|
|
429
|
+
it('应该通过 keywords 匹配工具', () => {
|
|
430
|
+
const tools = [
|
|
431
|
+
createMockTool({ name: 'weather', description: '查询天气', keywords: ['天气', 'weather'] }),
|
|
432
|
+
createMockTool({ name: 'calculator', description: '计算器', keywords: ['计算', 'calc'] }),
|
|
433
|
+
createMockTool({ name: 'news', description: '新闻', keywords: ['新闻', 'news'] }),
|
|
434
|
+
] as any[];
|
|
435
|
+
|
|
436
|
+
const result = Agent.filterTools('今天天气怎么样', tools);
|
|
437
|
+
|
|
438
|
+
expect(result).toHaveLength(1);
|
|
439
|
+
expect(result[0].name).toBe('weather');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('应该通过 tags 匹配工具', () => {
|
|
443
|
+
const tools = [
|
|
444
|
+
createMockTool({ name: 'ai.models', description: '列出模型', tags: ['ai', 'management'] }),
|
|
445
|
+
createMockTool({ name: 'weather', description: '天气', tags: ['weather', 'utility'] }),
|
|
446
|
+
] as any[];
|
|
447
|
+
|
|
448
|
+
const result = Agent.filterTools('ai 相关功能', tools);
|
|
449
|
+
|
|
450
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
451
|
+
expect(result[0].name).toBe('ai.models');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('应该通过工具名 token 匹配', () => {
|
|
455
|
+
const tools = [
|
|
456
|
+
createMockTool({ name: 'gold_price', description: '查询金价' }),
|
|
457
|
+
createMockTool({ name: 'fuel_price', description: '查询油价' }),
|
|
458
|
+
] as any[];
|
|
459
|
+
|
|
460
|
+
const result = Agent.filterTools('gold 价格', tools);
|
|
461
|
+
|
|
462
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
463
|
+
expect(result[0].name).toBe('gold_price');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('应该按权限过滤工具', () => {
|
|
467
|
+
const tools = [
|
|
468
|
+
createMockTool({ name: 'public_tool', description: '公共工具' }),
|
|
469
|
+
createMockTool({ name: 'admin_tool', description: '管理员工具' }),
|
|
470
|
+
] as any[];
|
|
471
|
+
// 设置权限级别
|
|
472
|
+
tools[0].permissionLevel = 0;
|
|
473
|
+
tools[1].permissionLevel = 3;
|
|
474
|
+
|
|
475
|
+
const result = Agent.filterTools('管理', tools, { callerPermissionLevel: 1 });
|
|
476
|
+
|
|
477
|
+
// admin_tool 需要 3,用户只有 1,应该被过滤掉
|
|
478
|
+
const names = result.map((t: any) => t.name);
|
|
479
|
+
expect(names).not.toContain('admin_tool');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('应该按分数排序并限制数量', () => {
|
|
483
|
+
const tools = [
|
|
484
|
+
createMockTool({ name: 'tool1', description: '天气查询', keywords: ['天气'] }),
|
|
485
|
+
createMockTool({ name: 'tool2', description: '天气预报', keywords: ['天气', '预报'] }),
|
|
486
|
+
createMockTool({ name: 'tool3', description: '无关工具' }),
|
|
487
|
+
] as any[];
|
|
488
|
+
|
|
489
|
+
const result = Agent.filterTools('天气预报', tools, { maxTools: 2 });
|
|
490
|
+
|
|
491
|
+
expect(result.length).toBeLessThanOrEqual(2);
|
|
492
|
+
// tool2 应该排更前面(匹配了两个 keywords)
|
|
493
|
+
if (result.length >= 2) {
|
|
494
|
+
expect(result[0].name).toBe('tool2');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('空消息应该返回空结果', () => {
|
|
499
|
+
const tools = [
|
|
500
|
+
createMockTool({ name: 'tool1', description: '工具', keywords: ['关键词'] }),
|
|
501
|
+
] as any[];
|
|
502
|
+
|
|
503
|
+
const result = Agent.filterTools('', tools);
|
|
504
|
+
|
|
505
|
+
expect(result).toHaveLength(0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('无工具时应该返回空结果', () => {
|
|
509
|
+
const result = Agent.filterTools('测试消息', []);
|
|
510
|
+
|
|
511
|
+
expect(result).toHaveLength(0);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('应该支持中文描述双向匹配', () => {
|
|
515
|
+
const tools = [
|
|
516
|
+
createMockTool({ name: 'get_time', description: '获取当前时间和日期信息' }),
|
|
517
|
+
createMockTool({ name: 'weather', description: '查询天气信息' }),
|
|
518
|
+
] as any[];
|
|
519
|
+
|
|
520
|
+
// "时间" 出现在 get_time 的描述中(双向匹配)
|
|
521
|
+
const result = Agent.filterTools('时间', tools);
|
|
522
|
+
|
|
523
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
524
|
+
expect(result[0].name).toBe('get_time');
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('Agent run 带过滤选项', () => {
|
|
529
|
+
let mockProvider: ReturnType<typeof createMockProvider>;
|
|
530
|
+
|
|
531
|
+
beforeEach(() => {
|
|
532
|
+
vi.clearAllMocks();
|
|
533
|
+
mockProvider = createMockProvider();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('应该用过滤后的工具调用 AI', async () => {
|
|
537
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
538
|
+
|
|
539
|
+
const relevantTool = createMockTool({ name: 'weather', description: '天气', keywords: ['天气'] });
|
|
540
|
+
const irrelevantTool = createMockTool({ name: 'calculator', description: '计算', keywords: ['计算'] });
|
|
541
|
+
|
|
542
|
+
const agent = createAgent(mockProvider as any, {
|
|
543
|
+
tools: [relevantTool, irrelevantTool] as any[],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await agent.run('天气怎么样', undefined, { maxTools: 5, minScore: 0.1 });
|
|
547
|
+
|
|
548
|
+
// 应该只传递匹配的工具给 AI
|
|
549
|
+
const chatCall = mockProvider.chat.mock.calls[0][0];
|
|
550
|
+
if (chatCall.tools) {
|
|
551
|
+
const toolNames = chatCall.tools.map((t: any) => t.function.name);
|
|
552
|
+
expect(toolNames).toContain('weather');
|
|
553
|
+
expect(toolNames).not.toContain('calculator');
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('Agent 类', () => {
|
|
559
|
+
it('应该正确创建实例', () => {
|
|
560
|
+
const mockProvider = createMockProvider();
|
|
561
|
+
const agent = new Agent(mockProvider as any, {
|
|
562
|
+
tools: [],
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(agent).toBeInstanceOf(Agent);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('应该支持动态添加工具', async () => {
|
|
569
|
+
const mockProvider = createMockProvider();
|
|
570
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
571
|
+
|
|
572
|
+
const agent = new Agent(mockProvider as any, {
|
|
573
|
+
tools: [],
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const tool = createMockTool({ name: 'dynamic_tool' });
|
|
577
|
+
agent.addTool(tool);
|
|
578
|
+
|
|
579
|
+
await agent.run('测试');
|
|
580
|
+
|
|
581
|
+
expect(mockProvider.chat).toHaveBeenCalledWith(
|
|
582
|
+
expect.objectContaining({
|
|
583
|
+
tools: expect.arrayContaining([
|
|
584
|
+
expect.objectContaining({
|
|
585
|
+
function: expect.objectContaining({
|
|
586
|
+
name: 'dynamic_tool',
|
|
587
|
+
}),
|
|
588
|
+
}),
|
|
589
|
+
]),
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('应该支持动态移除工具', async () => {
|
|
595
|
+
const mockProvider = createMockProvider();
|
|
596
|
+
mockProvider.chat.mockResolvedValue(createChatResponse('ok'));
|
|
597
|
+
|
|
598
|
+
const tool = createMockTool({ name: 'removable' });
|
|
599
|
+
const agent = new Agent(mockProvider as any, {
|
|
600
|
+
tools: [tool],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
agent.removeTool('removable');
|
|
604
|
+
|
|
605
|
+
await agent.run('测试');
|
|
606
|
+
|
|
607
|
+
// 应该没有工具
|
|
608
|
+
expect(mockProvider.chat).toHaveBeenCalledWith(
|
|
609
|
+
expect.objectContaining({
|
|
610
|
+
tools: undefined,
|
|
611
|
+
})
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
});
|