@zhin.js/ai 1.0.0 → 1.0.1
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 +7 -0
- package/lib/agent.d.ts +54 -6
- package/lib/agent.d.ts.map +1 -1
- package/lib/agent.js +468 -116
- package/lib/agent.js.map +1 -1
- package/lib/compaction.d.ts +132 -0
- package/lib/compaction.d.ts.map +1 -0
- package/lib/compaction.js +370 -0
- package/lib/compaction.js.map +1 -0
- package/lib/context-manager.d.ts.map +1 -1
- package/lib/context-manager.js +10 -3
- package/lib/context-manager.js.map +1 -1
- package/lib/conversation-memory.d.ts +192 -0
- package/lib/conversation-memory.d.ts.map +1 -0
- package/lib/conversation-memory.js +619 -0
- package/lib/conversation-memory.js.map +1 -0
- package/lib/index.d.ts +25 -163
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +24 -1122
- package/lib/index.js.map +1 -1
- package/lib/output.d.ts +93 -0
- package/lib/output.d.ts.map +1 -0
- package/lib/output.js +176 -0
- package/lib/output.js.map +1 -0
- package/lib/providers/anthropic.d.ts +7 -0
- package/lib/providers/anthropic.d.ts.map +1 -1
- package/lib/providers/anthropic.js +5 -0
- package/lib/providers/anthropic.js.map +1 -1
- package/lib/providers/ollama.d.ts +10 -0
- package/lib/providers/ollama.d.ts.map +1 -1
- package/lib/providers/ollama.js +19 -4
- package/lib/providers/ollama.js.map +1 -1
- package/lib/providers/openai.d.ts +7 -0
- package/lib/providers/openai.d.ts.map +1 -1
- package/lib/providers/openai.js +11 -0
- package/lib/providers/openai.js.map +1 -1
- package/lib/rate-limiter.d.ts +38 -0
- package/lib/rate-limiter.d.ts.map +1 -0
- package/lib/rate-limiter.js +86 -0
- package/lib/rate-limiter.js.map +1 -0
- package/lib/session.d.ts +7 -0
- package/lib/session.d.ts.map +1 -1
- package/lib/session.js +47 -18
- package/lib/session.js.map +1 -1
- package/lib/storage.d.ts +68 -0
- package/lib/storage.d.ts.map +1 -0
- package/lib/storage.js +105 -0
- package/lib/storage.js.map +1 -0
- package/lib/tone-detector.d.ts +19 -0
- package/lib/tone-detector.d.ts.map +1 -0
- package/lib/tone-detector.js +72 -0
- package/lib/tone-detector.js.map +1 -0
- package/lib/types.d.ts +84 -8
- package/lib/types.d.ts.map +1 -1
- package/package.json +13 -42
- package/src/agent.ts +518 -135
- package/src/compaction.ts +529 -0
- package/src/context-manager.ts +10 -9
- package/src/conversation-memory.ts +816 -0
- package/src/index.ts +121 -1406
- package/src/output.ts +261 -0
- package/src/providers/anthropic.ts +4 -0
- package/src/providers/ollama.ts +23 -4
- package/src/providers/openai.ts +8 -1
- package/src/rate-limiter.ts +129 -0
- package/src/session.ts +47 -18
- package/src/storage.ts +135 -0
- package/src/tone-detector.ts +89 -0
- package/src/types.ts +95 -6
- package/tests/agent.test.ts +123 -70
- package/tests/compaction.test.ts +310 -0
- package/tests/context-manager.test.ts +73 -47
- package/tests/conversation-memory.test.ts +128 -0
- package/tests/output.test.ts +128 -0
- package/tests/providers.test.ts +574 -0
- package/tests/rate-limiter.test.ts +108 -0
- package/tests/session.test.ts +139 -48
- package/tests/setup.ts +82 -240
- package/tests/storage.test.ts +224 -0
- package/tests/tone-detector.test.ts +80 -0
- package/tsconfig.json +4 -5
- package/vitest.setup.ts +1 -0
- package/README.md +0 -564
- package/TOOLS.md +0 -294
- package/lib/tools.d.ts +0 -45
- package/lib/tools.d.ts.map +0 -1
- package/lib/tools.js +0 -194
- package/lib/tools.js.map +0 -1
- package/src/tools.ts +0 -205
- package/tests/ai-trigger.test.ts +0 -369
- package/tests/integration.test.ts +0 -596
- package/tests/providers.integration.test.ts +0 -227
- package/tests/tool.test.ts +0 -800
- package/tests/tools-builtin.test.ts +0 -346
package/tests/session.test.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session Manager 测试
|
|
3
|
-
*
|
|
4
|
-
* 测试会话管理功能:
|
|
5
|
-
* - 会话创建和获取
|
|
6
|
-
* - 消息历史管理
|
|
7
|
-
* - 会话超时清理
|
|
8
3
|
*/
|
|
9
4
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
vi.mock('@zhin.js/core', async (importOriginal) => {
|
|
6
|
+
vi.mock('@zhin.js/logger', async (importOriginal) => {
|
|
13
7
|
const original = await importOriginal() as any;
|
|
14
8
|
return {
|
|
15
9
|
...original,
|
|
@@ -22,11 +16,12 @@ vi.mock('@zhin.js/core', async (importOriginal) => {
|
|
|
22
16
|
};
|
|
23
17
|
});
|
|
24
18
|
|
|
25
|
-
import {
|
|
26
|
-
MemorySessionManager,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
import {
|
|
20
|
+
MemorySessionManager,
|
|
21
|
+
DatabaseSessionManager,
|
|
22
|
+
SessionManager,
|
|
23
|
+
createMemorySessionManager,
|
|
24
|
+
} from '@zhin.js/ai';
|
|
30
25
|
|
|
31
26
|
describe('MemorySessionManager', () => {
|
|
32
27
|
let manager: MemorySessionManager;
|
|
@@ -35,7 +30,7 @@ describe('MemorySessionManager', () => {
|
|
|
35
30
|
vi.useFakeTimers();
|
|
36
31
|
manager = new MemorySessionManager({
|
|
37
32
|
maxHistory: 10,
|
|
38
|
-
expireMs: 60000,
|
|
33
|
+
expireMs: 60000,
|
|
39
34
|
});
|
|
40
35
|
});
|
|
41
36
|
|
|
@@ -47,7 +42,6 @@ describe('MemorySessionManager', () => {
|
|
|
47
42
|
describe('会话创建', () => {
|
|
48
43
|
it('应该创建新会话', () => {
|
|
49
44
|
const session = manager.get('user-1');
|
|
50
|
-
|
|
51
45
|
expect(session).toBeDefined();
|
|
52
46
|
expect(session.id).toBe('user-1');
|
|
53
47
|
expect(session.messages).toEqual([]);
|
|
@@ -56,16 +50,13 @@ describe('MemorySessionManager', () => {
|
|
|
56
50
|
it('应该返回已存在的会话', () => {
|
|
57
51
|
const session1 = manager.get('user-1');
|
|
58
52
|
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
59
|
-
|
|
60
53
|
const session2 = manager.get('user-1');
|
|
61
|
-
|
|
62
54
|
expect(session2.messages).toHaveLength(1);
|
|
63
55
|
});
|
|
64
56
|
|
|
65
57
|
it('不同用户应该有不同的会话', () => {
|
|
66
58
|
const session1 = manager.get('user-1');
|
|
67
59
|
const session2 = manager.get('user-2');
|
|
68
|
-
|
|
69
60
|
expect(session1.id).not.toBe(session2.id);
|
|
70
61
|
});
|
|
71
62
|
});
|
|
@@ -74,7 +65,6 @@ describe('MemorySessionManager', () => {
|
|
|
74
65
|
it('应该添加消息到历史', () => {
|
|
75
66
|
manager.addMessage('user-1', { role: 'user', content: '你好' });
|
|
76
67
|
manager.addMessage('user-1', { role: 'assistant', content: '你好!有什么可以帮你的?' });
|
|
77
|
-
|
|
78
68
|
const messages = manager.getMessages('user-1');
|
|
79
69
|
expect(messages).toHaveLength(2);
|
|
80
70
|
expect(messages[0].role).toBe('user');
|
|
@@ -82,24 +72,20 @@ describe('MemorySessionManager', () => {
|
|
|
82
72
|
});
|
|
83
73
|
|
|
84
74
|
it('应该限制历史记录数量', () => {
|
|
85
|
-
// 添加超过限制的消息
|
|
86
75
|
for (let i = 0; i < 15; i++) {
|
|
87
76
|
manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
|
|
88
77
|
}
|
|
89
|
-
|
|
90
78
|
const messages = manager.getMessages('user-1');
|
|
91
79
|
expect(messages.length).toBeLessThanOrEqual(10);
|
|
92
80
|
});
|
|
93
81
|
|
|
94
82
|
it('系统消息应该保留', () => {
|
|
95
83
|
manager.setSystemPrompt('user-1', '你是一个助手');
|
|
96
|
-
|
|
97
84
|
for (let i = 0; i < 15; i++) {
|
|
98
85
|
manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
|
|
99
86
|
}
|
|
100
|
-
|
|
101
87
|
const messages = manager.getMessages('user-1');
|
|
102
|
-
const systemMessages = messages.filter(m => m.role === 'system');
|
|
88
|
+
const systemMessages = messages.filter((m: { role: string }) => m.role === 'system');
|
|
103
89
|
expect(systemMessages.length).toBe(1);
|
|
104
90
|
});
|
|
105
91
|
});
|
|
@@ -107,7 +93,6 @@ describe('MemorySessionManager', () => {
|
|
|
107
93
|
describe('系统提示', () => {
|
|
108
94
|
it('应该设置系统提示', () => {
|
|
109
95
|
manager.setSystemPrompt('user-1', '你是一个助手');
|
|
110
|
-
|
|
111
96
|
const messages = manager.getMessages('user-1');
|
|
112
97
|
expect(messages[0].role).toBe('system');
|
|
113
98
|
expect(messages[0].content).toBe('你是一个助手');
|
|
@@ -116,9 +101,8 @@ describe('MemorySessionManager', () => {
|
|
|
116
101
|
it('应该替换旧的系统提示', () => {
|
|
117
102
|
manager.setSystemPrompt('user-1', '旧提示');
|
|
118
103
|
manager.setSystemPrompt('user-1', '新提示');
|
|
119
|
-
|
|
120
104
|
const messages = manager.getMessages('user-1');
|
|
121
|
-
const systemMessages = messages.filter(m => m.role === 'system');
|
|
105
|
+
const systemMessages = messages.filter((m: { role: string }) => m.role === 'system');
|
|
122
106
|
expect(systemMessages.length).toBe(1);
|
|
123
107
|
expect(systemMessages[0].content).toBe('新提示');
|
|
124
108
|
});
|
|
@@ -128,7 +112,6 @@ describe('MemorySessionManager', () => {
|
|
|
128
112
|
it('应该清除指定会话', () => {
|
|
129
113
|
manager.get('user-1');
|
|
130
114
|
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
131
|
-
|
|
132
115
|
expect(manager.clear('user-1')).toBe(true);
|
|
133
116
|
expect(manager.has('user-1')).toBe(false);
|
|
134
117
|
});
|
|
@@ -140,9 +123,7 @@ describe('MemorySessionManager', () => {
|
|
|
140
123
|
it('reset 应该保留系统消息', () => {
|
|
141
124
|
manager.setSystemPrompt('user-1', '系统提示');
|
|
142
125
|
manager.addMessage('user-1', { role: 'user', content: 'test' });
|
|
143
|
-
|
|
144
126
|
manager.reset('user-1');
|
|
145
|
-
|
|
146
127
|
const messages = manager.getMessages('user-1');
|
|
147
128
|
expect(messages.length).toBe(1);
|
|
148
129
|
expect(messages[0].role).toBe('system');
|
|
@@ -152,33 +133,18 @@ describe('MemorySessionManager', () => {
|
|
|
152
133
|
describe('会话超时', () => {
|
|
153
134
|
it('应该在超时后清理会话', () => {
|
|
154
135
|
manager.get('user-1');
|
|
155
|
-
|
|
156
136
|
expect(manager.has('user-1')).toBe(true);
|
|
157
|
-
|
|
158
|
-
// 前进时间超过超时时间
|
|
159
137
|
vi.advanceTimersByTime(61000);
|
|
160
|
-
|
|
161
|
-
// 触发清理
|
|
162
138
|
manager.cleanup();
|
|
163
|
-
|
|
164
139
|
expect(manager.has('user-1')).toBe(false);
|
|
165
140
|
});
|
|
166
141
|
|
|
167
142
|
it('活动会话不应该被清理', () => {
|
|
168
143
|
manager.get('user-1');
|
|
169
|
-
|
|
170
|
-
// 前进一半时间
|
|
171
144
|
vi.advanceTimersByTime(30000);
|
|
172
|
-
|
|
173
|
-
// 添加新消息(刷新活动时间)
|
|
174
145
|
manager.addMessage('user-1', { role: 'user', content: '新消息' });
|
|
175
|
-
|
|
176
|
-
// 再前进一半时间
|
|
177
146
|
vi.advanceTimersByTime(30000);
|
|
178
|
-
|
|
179
147
|
manager.cleanup();
|
|
180
|
-
|
|
181
|
-
// 会话应该还在
|
|
182
148
|
expect(manager.has('user-1')).toBe(true);
|
|
183
149
|
});
|
|
184
150
|
});
|
|
@@ -187,7 +153,6 @@ describe('MemorySessionManager', () => {
|
|
|
187
153
|
it('应该返回正确的统计信息', () => {
|
|
188
154
|
manager.get('user-1');
|
|
189
155
|
manager.get('user-2');
|
|
190
|
-
|
|
191
156
|
const stats = manager.getStats();
|
|
192
157
|
expect(stats.total).toBe(2);
|
|
193
158
|
expect(stats.active).toBe(2);
|
|
@@ -198,7 +163,6 @@ describe('MemorySessionManager', () => {
|
|
|
198
163
|
manager.get('user-1');
|
|
199
164
|
manager.get('user-2');
|
|
200
165
|
manager.get('user-3');
|
|
201
|
-
|
|
202
166
|
const sessions = manager.listSessions();
|
|
203
167
|
expect(sessions).toContain('user-1');
|
|
204
168
|
expect(sessions).toContain('user-2');
|
|
@@ -210,9 +174,7 @@ describe('MemorySessionManager', () => {
|
|
|
210
174
|
it('应该清理所有会话', () => {
|
|
211
175
|
manager.get('user-1');
|
|
212
176
|
manager.get('user-2');
|
|
213
|
-
|
|
214
177
|
manager.dispose();
|
|
215
|
-
|
|
216
178
|
expect(manager.has('user-1')).toBe(false);
|
|
217
179
|
expect(manager.has('user-2')).toBe(false);
|
|
218
180
|
});
|
|
@@ -241,3 +203,132 @@ describe('createMemorySessionManager', () => {
|
|
|
241
203
|
manager.dispose();
|
|
242
204
|
});
|
|
243
205
|
});
|
|
206
|
+
|
|
207
|
+
describe('DatabaseSessionManager', () => {
|
|
208
|
+
function createMockModel(records: any[] = []) {
|
|
209
|
+
return {
|
|
210
|
+
select: vi.fn().mockReturnValue({
|
|
211
|
+
where: vi.fn().mockResolvedValue(records),
|
|
212
|
+
}),
|
|
213
|
+
create: vi.fn().mockResolvedValue(undefined),
|
|
214
|
+
update: vi.fn().mockReturnValue({
|
|
215
|
+
where: vi.fn().mockResolvedValue(undefined),
|
|
216
|
+
}),
|
|
217
|
+
delete: vi.fn().mockReturnValue({
|
|
218
|
+
where: vi.fn().mockResolvedValue(undefined),
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
vi.useRealTimers();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('loadSession JSON 解析', () => {
|
|
228
|
+
it('应该解析字符串形式的 messages (JSON)', async () => {
|
|
229
|
+
const messagesJson = JSON.stringify([
|
|
230
|
+
{ role: 'user', content: 'hello' },
|
|
231
|
+
{ role: 'assistant', content: 'hi there' },
|
|
232
|
+
]);
|
|
233
|
+
const model = createMockModel([{
|
|
234
|
+
session_id: 'test-1',
|
|
235
|
+
messages: messagesJson,
|
|
236
|
+
config: JSON.stringify({ provider: 'ollama' }),
|
|
237
|
+
created_at: 1000,
|
|
238
|
+
updated_at: 2000,
|
|
239
|
+
}]);
|
|
240
|
+
|
|
241
|
+
const manager = new DatabaseSessionManager(model);
|
|
242
|
+
const session = await manager.get('test-1');
|
|
243
|
+
|
|
244
|
+
expect(Array.isArray(session.messages)).toBe(true);
|
|
245
|
+
expect(session.messages).toHaveLength(2);
|
|
246
|
+
expect(session.messages[0]).toEqual({ role: 'user', content: 'hello' });
|
|
247
|
+
manager.dispose();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('应该解析字符串形式的 config (JSON)', async () => {
|
|
251
|
+
const model = createMockModel([{
|
|
252
|
+
session_id: 'test-2',
|
|
253
|
+
messages: '[]',
|
|
254
|
+
config: '{"provider":"ollama","maxHistory":100}',
|
|
255
|
+
created_at: 1000,
|
|
256
|
+
updated_at: 2000,
|
|
257
|
+
}]);
|
|
258
|
+
|
|
259
|
+
const manager = new DatabaseSessionManager(model);
|
|
260
|
+
const session = await manager.get('test-2');
|
|
261
|
+
|
|
262
|
+
expect(typeof session.config).toBe('object');
|
|
263
|
+
expect(session.config).toEqual({ provider: 'ollama', maxHistory: 100 });
|
|
264
|
+
manager.dispose();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('messages 已是数组时不应二次解析', async () => {
|
|
268
|
+
const messagesArray = [{ role: 'system', content: 'You are a bot' }];
|
|
269
|
+
const model = createMockModel([{
|
|
270
|
+
session_id: 'test-3',
|
|
271
|
+
messages: messagesArray,
|
|
272
|
+
config: { provider: 'openai' },
|
|
273
|
+
created_at: 1000,
|
|
274
|
+
updated_at: 2000,
|
|
275
|
+
}]);
|
|
276
|
+
|
|
277
|
+
const manager = new DatabaseSessionManager(model);
|
|
278
|
+
const session = await manager.get('test-3');
|
|
279
|
+
|
|
280
|
+
expect(session.messages).toBe(messagesArray);
|
|
281
|
+
expect(session.config).toEqual({ provider: 'openai' });
|
|
282
|
+
manager.dispose();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('加载后的 messages 应支持 push 操作', async () => {
|
|
286
|
+
const model = createMockModel([{
|
|
287
|
+
session_id: 'test-4',
|
|
288
|
+
messages: '[{"role":"user","content":"hi"}]',
|
|
289
|
+
config: '{"provider":"openai"}',
|
|
290
|
+
created_at: 1000,
|
|
291
|
+
updated_at: 2000,
|
|
292
|
+
}]);
|
|
293
|
+
|
|
294
|
+
const manager = new DatabaseSessionManager(model);
|
|
295
|
+
await manager.addMessage('test-4', { role: 'assistant', content: 'hello!' });
|
|
296
|
+
|
|
297
|
+
const session = await manager.get('test-4');
|
|
298
|
+
expect(session.messages).toHaveLength(2);
|
|
299
|
+
expect(session.messages[1]).toEqual({ role: 'assistant', content: 'hello!' });
|
|
300
|
+
manager.dispose();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('messages 或 config 为 null/undefined 时应使用默认值', async () => {
|
|
304
|
+
const model = createMockModel([{
|
|
305
|
+
session_id: 'test-5',
|
|
306
|
+
messages: null,
|
|
307
|
+
config: null,
|
|
308
|
+
created_at: 1000,
|
|
309
|
+
updated_at: 2000,
|
|
310
|
+
}]);
|
|
311
|
+
|
|
312
|
+
const manager = new DatabaseSessionManager(model);
|
|
313
|
+
const session = await manager.get('test-5');
|
|
314
|
+
|
|
315
|
+
expect(Array.isArray(session.messages)).toBe(true);
|
|
316
|
+
expect(session.messages).toHaveLength(0);
|
|
317
|
+
expect(session.config).toEqual({ provider: 'openai' });
|
|
318
|
+
manager.dispose();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('新会话创建', () => {
|
|
323
|
+
it('数据库无记录时应创建新会话', async () => {
|
|
324
|
+
const model = createMockModel([]);
|
|
325
|
+
const manager = new DatabaseSessionManager(model);
|
|
326
|
+
const session = await manager.get('new-session');
|
|
327
|
+
|
|
328
|
+
expect(session.id).toBe('new-session');
|
|
329
|
+
expect(Array.isArray(session.messages)).toBe(true);
|
|
330
|
+
expect(session.messages).toHaveLength(0);
|
|
331
|
+
manager.dispose();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
package/tests/setup.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 提供:
|
|
5
|
-
* - Mock 对象工厂
|
|
6
|
-
* - 测试辅助函数
|
|
7
|
-
* - 公共测试配置
|
|
2
|
+
* @zhin.js/ai 测试环境设置
|
|
3
|
+
* 提供通用 AI Mock 与测试辅助
|
|
8
4
|
*/
|
|
9
5
|
import { vi } from 'vitest';
|
|
10
|
-
import type {
|
|
11
|
-
import type { AIConfig, AIProviderConfig, ChatMessage } from '../src/types.js';
|
|
12
|
-
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// Logger Mock
|
|
15
|
-
// ============================================================================
|
|
6
|
+
import type { ChatMessage, AgentTool } from '@zhin.js/ai';
|
|
16
7
|
|
|
17
8
|
export const createMockLogger = () => ({
|
|
18
9
|
debug: vi.fn(),
|
|
@@ -21,139 +12,6 @@ export const createMockLogger = () => ({
|
|
|
21
12
|
error: vi.fn(),
|
|
22
13
|
});
|
|
23
14
|
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Plugin Mock
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
export const createMockPlugin = (name = 'test-plugin') => ({
|
|
29
|
-
name,
|
|
30
|
-
root: {
|
|
31
|
-
inject: vi.fn(),
|
|
32
|
-
contexts: new Map(),
|
|
33
|
-
middleware: vi.fn(),
|
|
34
|
-
},
|
|
35
|
-
logger: createMockLogger(),
|
|
36
|
-
onDispose: vi.fn(),
|
|
37
|
-
collectAllTools: vi.fn(() => []),
|
|
38
|
-
provide: vi.fn(),
|
|
39
|
-
useContext: vi.fn(),
|
|
40
|
-
addMiddleware: vi.fn(),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ============================================================================
|
|
44
|
-
// Message Mock
|
|
45
|
-
// ============================================================================
|
|
46
|
-
|
|
47
|
-
export interface MockMessageOptions {
|
|
48
|
-
content?: string;
|
|
49
|
-
elements?: MessageElement[];
|
|
50
|
-
platform?: string;
|
|
51
|
-
channelType?: 'group' | 'private' | 'guild';
|
|
52
|
-
channelId?: string;
|
|
53
|
-
senderId?: string;
|
|
54
|
-
senderPermissions?: string[];
|
|
55
|
-
senderRole?: string;
|
|
56
|
-
botId?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const createMockMessage = (options: MockMessageOptions = {}): Partial<Message> => {
|
|
60
|
-
const {
|
|
61
|
-
content = '测试消息',
|
|
62
|
-
elements,
|
|
63
|
-
platform = 'test',
|
|
64
|
-
channelType = 'group',
|
|
65
|
-
channelId = 'channel-1',
|
|
66
|
-
senderId = 'user-1',
|
|
67
|
-
senderPermissions = [],
|
|
68
|
-
senderRole,
|
|
69
|
-
botId = 'bot-1',
|
|
70
|
-
} = options;
|
|
71
|
-
|
|
72
|
-
const $content: MessageElement[] = elements || [
|
|
73
|
-
{ type: 'text', data: { text: content } },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
$content,
|
|
78
|
-
$bot: botId,
|
|
79
|
-
$adapter: platform,
|
|
80
|
-
$channel: {
|
|
81
|
-
type: channelType,
|
|
82
|
-
id: channelId,
|
|
83
|
-
name: 'Test Channel',
|
|
84
|
-
},
|
|
85
|
-
$sender: {
|
|
86
|
-
id: senderId,
|
|
87
|
-
name: 'Test User',
|
|
88
|
-
permissions: senderPermissions,
|
|
89
|
-
role: senderRole,
|
|
90
|
-
},
|
|
91
|
-
$reply: vi.fn().mockResolvedValue(undefined),
|
|
92
|
-
$quote: vi.fn().mockResolvedValue(undefined),
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// ============================================================================
|
|
97
|
-
// Tool Mock
|
|
98
|
-
// ============================================================================
|
|
99
|
-
|
|
100
|
-
export interface MockToolOptions {
|
|
101
|
-
name: string;
|
|
102
|
-
description?: string;
|
|
103
|
-
parameters?: Record<string, { type: string; description?: string }>;
|
|
104
|
-
required?: string[];
|
|
105
|
-
platforms?: string[];
|
|
106
|
-
scopes?: ('group' | 'private' | 'guild')[];
|
|
107
|
-
permissionLevel?: 'user' | 'group_admin' | 'group_owner' | 'bot_admin' | 'owner';
|
|
108
|
-
tags?: string[];
|
|
109
|
-
executeResult?: any;
|
|
110
|
-
executeError?: Error;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const createMockTool = (options: MockToolOptions): Tool => {
|
|
114
|
-
const {
|
|
115
|
-
name,
|
|
116
|
-
description = `${name} 工具`,
|
|
117
|
-
parameters = {},
|
|
118
|
-
required = [],
|
|
119
|
-
platforms,
|
|
120
|
-
scopes,
|
|
121
|
-
permissionLevel,
|
|
122
|
-
tags = [],
|
|
123
|
-
executeResult = 'success',
|
|
124
|
-
executeError,
|
|
125
|
-
} = options;
|
|
126
|
-
|
|
127
|
-
const properties: Record<string, any> = {};
|
|
128
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
129
|
-
properties[key] = {
|
|
130
|
-
type: value.type,
|
|
131
|
-
description: value.description,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
name,
|
|
137
|
-
description,
|
|
138
|
-
parameters: {
|
|
139
|
-
type: 'object',
|
|
140
|
-
properties,
|
|
141
|
-
required,
|
|
142
|
-
},
|
|
143
|
-
platforms,
|
|
144
|
-
scopes,
|
|
145
|
-
permissionLevel,
|
|
146
|
-
tags,
|
|
147
|
-
execute: executeError
|
|
148
|
-
? vi.fn().mockRejectedValue(executeError)
|
|
149
|
-
: vi.fn().mockResolvedValue(executeResult),
|
|
150
|
-
};
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// ============================================================================
|
|
154
|
-
// AI Provider Mock
|
|
155
|
-
// ============================================================================
|
|
156
|
-
|
|
157
15
|
export interface MockProviderOptions {
|
|
158
16
|
name?: string;
|
|
159
17
|
response?: string | AsyncGenerator<string>;
|
|
@@ -172,83 +30,54 @@ export const createMockProvider = (options: MockProviderOptions = {}) => {
|
|
|
172
30
|
if (error) {
|
|
173
31
|
return {
|
|
174
32
|
name,
|
|
33
|
+
models: ['mock-model'],
|
|
175
34
|
chat: vi.fn().mockRejectedValue(error),
|
|
176
35
|
healthCheck: vi.fn().mockResolvedValue(false),
|
|
177
36
|
};
|
|
178
37
|
}
|
|
179
38
|
|
|
180
|
-
const generateResponse = async function* (): AsyncGenerator<{
|
|
181
|
-
content?: string;
|
|
182
|
-
toolCalls?: Array<{ name: string; arguments: Record<string, any> }>;
|
|
183
|
-
done: boolean;
|
|
184
|
-
}> {
|
|
185
|
-
if (toolCalls.length > 0) {
|
|
186
|
-
yield { toolCalls, done: false };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (typeof response === 'string') {
|
|
190
|
-
yield { content: response, done: true };
|
|
191
|
-
} else {
|
|
192
|
-
for await (const chunk of response) {
|
|
193
|
-
yield { content: chunk, done: false };
|
|
194
|
-
}
|
|
195
|
-
yield { done: true };
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
39
|
return {
|
|
200
40
|
name,
|
|
201
|
-
|
|
41
|
+
models: ['mock-model'],
|
|
42
|
+
chat: vi.fn(),
|
|
202
43
|
healthCheck: vi.fn().mockResolvedValue(true),
|
|
203
44
|
};
|
|
204
45
|
};
|
|
205
46
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
47
|
+
/** 构建 ChatCompletionResponse 用于测试 */
|
|
48
|
+
export const createChatResponse = (content: string, toolCalls?: any[]) => ({
|
|
49
|
+
id: 'test-id',
|
|
50
|
+
object: 'chat.completion',
|
|
51
|
+
created: Date.now(),
|
|
52
|
+
model: 'mock-model',
|
|
53
|
+
choices: [{
|
|
54
|
+
index: 0,
|
|
55
|
+
message: {
|
|
56
|
+
role: 'assistant',
|
|
57
|
+
content,
|
|
58
|
+
tool_calls: toolCalls,
|
|
59
|
+
},
|
|
60
|
+
finish_reason: toolCalls ? 'tool_calls' : 'stop',
|
|
61
|
+
}],
|
|
62
|
+
usage: {
|
|
63
|
+
prompt_tokens: 10,
|
|
64
|
+
completion_tokens: 10,
|
|
65
|
+
total_tokens: 20,
|
|
66
|
+
},
|
|
215
67
|
});
|
|
216
68
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
sessions: {
|
|
224
|
-
maxHistory: 10,
|
|
225
|
-
timeout: 300000,
|
|
226
|
-
},
|
|
227
|
-
context: {
|
|
228
|
-
enabled: false,
|
|
229
|
-
maxSize: 100,
|
|
230
|
-
},
|
|
231
|
-
trigger: {
|
|
232
|
-
enabled: true,
|
|
233
|
-
prefixes: ['#'],
|
|
234
|
-
ignorePrefixes: ['/'],
|
|
235
|
-
allowAtBot: true,
|
|
236
|
-
allowPrivateChat: true,
|
|
237
|
-
},
|
|
238
|
-
...overrides,
|
|
69
|
+
export const createChatMessage = (
|
|
70
|
+
role: 'user' | 'assistant' | 'system',
|
|
71
|
+
content: string
|
|
72
|
+
): ChatMessage => ({
|
|
73
|
+
role,
|
|
74
|
+
content,
|
|
239
75
|
});
|
|
240
76
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// ============================================================================
|
|
77
|
+
export const delay = (ms: number): Promise<void> =>
|
|
78
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
244
79
|
|
|
245
|
-
|
|
246
|
-
* 等待 Promise 解决或超时
|
|
247
|
-
*/
|
|
248
|
-
export const waitFor = async <T>(
|
|
249
|
-
promise: Promise<T>,
|
|
250
|
-
timeout = 5000
|
|
251
|
-
): Promise<T> => {
|
|
80
|
+
export const waitFor = async <T>(promise: Promise<T>, timeout = 5000): Promise<T> => {
|
|
252
81
|
return Promise.race([
|
|
253
82
|
promise,
|
|
254
83
|
new Promise<T>((_, reject) =>
|
|
@@ -257,12 +86,7 @@ export const waitFor = async <T>(
|
|
|
257
86
|
]);
|
|
258
87
|
};
|
|
259
88
|
|
|
260
|
-
|
|
261
|
-
* 收集 AsyncGenerator 的所有值
|
|
262
|
-
*/
|
|
263
|
-
export const collectAsyncGenerator = async <T>(
|
|
264
|
-
generator: AsyncGenerator<T>
|
|
265
|
-
): Promise<T[]> => {
|
|
89
|
+
export const collectAsyncGenerator = async <T>(generator: AsyncGenerator<T>): Promise<T[]> => {
|
|
266
90
|
const results: T[] = [];
|
|
267
91
|
for await (const item of generator) {
|
|
268
92
|
results.push(item);
|
|
@@ -270,35 +94,53 @@ export const collectAsyncGenerator = async <T>(
|
|
|
270
94
|
return results;
|
|
271
95
|
};
|
|
272
96
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
97
|
+
export interface MockToolOptions {
|
|
98
|
+
name: string;
|
|
99
|
+
description?: string;
|
|
100
|
+
parameters?: Record<string, { type: string; description?: string }>;
|
|
101
|
+
required?: string[];
|
|
102
|
+
tags?: string[];
|
|
103
|
+
keywords?: string[];
|
|
104
|
+
permissionLevel?: number;
|
|
105
|
+
executeResult?: any;
|
|
106
|
+
executeError?: Error;
|
|
107
|
+
}
|
|
278
108
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
109
|
+
export const createMockTool = (options: MockToolOptions): AgentTool => {
|
|
110
|
+
const {
|
|
111
|
+
name,
|
|
112
|
+
description = `${name} 工具`,
|
|
113
|
+
parameters = {},
|
|
114
|
+
required = [],
|
|
115
|
+
tags = [],
|
|
116
|
+
keywords = [],
|
|
117
|
+
permissionLevel,
|
|
118
|
+
executeResult = 'success',
|
|
119
|
+
executeError,
|
|
120
|
+
} = options;
|
|
289
121
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expectedRequired: string[] = []
|
|
297
|
-
) => {
|
|
298
|
-
const props = Object.keys(tool.parameters.properties || {});
|
|
299
|
-
expect(props).toEqual(expect.arrayContaining(expectedProperties));
|
|
300
|
-
|
|
301
|
-
if (expectedRequired.length > 0) {
|
|
302
|
-
expect(tool.parameters.required).toEqual(expect.arrayContaining(expectedRequired));
|
|
122
|
+
const properties: Record<string, any> = {};
|
|
123
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
124
|
+
properties[key] = {
|
|
125
|
+
type: value.type,
|
|
126
|
+
description: value.description,
|
|
127
|
+
};
|
|
303
128
|
}
|
|
129
|
+
|
|
130
|
+
const tool: any = {
|
|
131
|
+
name,
|
|
132
|
+
description,
|
|
133
|
+
parameters: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties,
|
|
136
|
+
required,
|
|
137
|
+
},
|
|
138
|
+
tags,
|
|
139
|
+
keywords,
|
|
140
|
+
permissionLevel,
|
|
141
|
+
execute: executeError
|
|
142
|
+
? vi.fn().mockRejectedValue(executeError)
|
|
143
|
+
: vi.fn().mockResolvedValue(executeResult),
|
|
144
|
+
};
|
|
145
|
+
return tool;
|
|
304
146
|
};
|