@zhin.js/ai 0.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/README.md +564 -0
- package/TOOLS.md +294 -0
- package/package.json +66 -0
- package/src/agent.ts +471 -0
- package/src/context-manager.ts +439 -0
- package/src/index.ts +1432 -0
- package/src/providers/anthropic.ts +375 -0
- package/src/providers/base.ts +173 -0
- package/src/providers/index.ts +13 -0
- package/src/providers/ollama.ts +283 -0
- package/src/providers/openai.ts +167 -0
- package/src/session.ts +537 -0
- package/src/tools.ts +205 -0
- package/src/types.ts +274 -0
- package/tests/agent.test.ts +484 -0
- package/tests/ai-trigger.test.ts +369 -0
- package/tests/context-manager.test.ts +387 -0
- package/tests/integration.test.ts +596 -0
- package/tests/providers.integration.test.ts +227 -0
- package/tests/session.test.ts +243 -0
- package/tests/setup.ts +304 -0
- package/tests/tool.test.ts +800 -0
- package/tests/tools-builtin.test.ts +346 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manager 测试
|
|
3
|
+
*
|
|
4
|
+
* 测试上下文管理功能:
|
|
5
|
+
* - 消息记录
|
|
6
|
+
* - 上下文构建
|
|
7
|
+
* - Token 估算
|
|
8
|
+
* - 自动总结
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// Mock Logger
|
|
13
|
+
vi.mock('@zhin.js/core', async (importOriginal) => {
|
|
14
|
+
const original = await importOriginal() as any;
|
|
15
|
+
return {
|
|
16
|
+
...original,
|
|
17
|
+
Logger: class {
|
|
18
|
+
debug = vi.fn();
|
|
19
|
+
info = vi.fn();
|
|
20
|
+
warn = vi.fn();
|
|
21
|
+
error = vi.fn();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import { ContextManager, createContextManager, CHAT_MESSAGE_MODEL, CONTEXT_SUMMARY_MODEL } from '../src/context-manager.js';
|
|
27
|
+
import type { MessageRecord } from '../src/context-manager.js';
|
|
28
|
+
|
|
29
|
+
describe('ContextManager', () => {
|
|
30
|
+
let manager: ContextManager;
|
|
31
|
+
let mockMessageModel: any;
|
|
32
|
+
let mockSummaryModel: any;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
// Mock 数据库模型
|
|
36
|
+
const messageStore: MessageRecord[] = [];
|
|
37
|
+
const summaryStore: any[] = [];
|
|
38
|
+
|
|
39
|
+
mockMessageModel = {
|
|
40
|
+
create: vi.fn((record: MessageRecord) => {
|
|
41
|
+
messageStore.push({ ...record, id: messageStore.length + 1 });
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}),
|
|
44
|
+
select: vi.fn((where?: any, options?: any) => {
|
|
45
|
+
let results = [...messageStore];
|
|
46
|
+
if (where?.scene_id) {
|
|
47
|
+
results = results.filter(m => m.scene_id === where.scene_id);
|
|
48
|
+
}
|
|
49
|
+
if (options?.orderBy?.time === 'desc') {
|
|
50
|
+
results.sort((a, b) => b.time - a.time);
|
|
51
|
+
}
|
|
52
|
+
if (options?.limit) {
|
|
53
|
+
results = results.slice(0, options.limit);
|
|
54
|
+
}
|
|
55
|
+
return Promise.resolve(results);
|
|
56
|
+
}),
|
|
57
|
+
delete: vi.fn(() => Promise.resolve(0)),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockSummaryModel = {
|
|
61
|
+
create: vi.fn((record: any) => {
|
|
62
|
+
summaryStore.push({ ...record, id: summaryStore.length + 1 });
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}),
|
|
65
|
+
select: vi.fn((where?: any) => {
|
|
66
|
+
let results = [...summaryStore];
|
|
67
|
+
if (where?.scene_id) {
|
|
68
|
+
results = results.filter(s => s.scene_id === where.scene_id);
|
|
69
|
+
}
|
|
70
|
+
return Promise.resolve(results);
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
manager = new ContextManager(mockMessageModel, mockSummaryModel, {
|
|
75
|
+
enabled: true,
|
|
76
|
+
maxRecentMessages: 100,
|
|
77
|
+
summaryThreshold: 50,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('消息记录', () => {
|
|
82
|
+
it('应该记录消息', async () => {
|
|
83
|
+
await manager.recordMessage({
|
|
84
|
+
platform: 'qq',
|
|
85
|
+
scene_id: 'group-123',
|
|
86
|
+
scene_type: 'group',
|
|
87
|
+
scene_name: '测试群',
|
|
88
|
+
sender_id: 'user-1',
|
|
89
|
+
sender_name: '张三',
|
|
90
|
+
message: '你好',
|
|
91
|
+
time: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(mockMessageModel.create).toHaveBeenCalledWith(
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
platform: 'qq',
|
|
97
|
+
scene_id: 'group-123',
|
|
98
|
+
message: '你好',
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('记录失败时不应抛出错误', async () => {
|
|
104
|
+
mockMessageModel.create.mockRejectedValue(new Error('数据库错误'));
|
|
105
|
+
|
|
106
|
+
await expect(manager.recordMessage({
|
|
107
|
+
platform: 'qq',
|
|
108
|
+
scene_id: 'group-123',
|
|
109
|
+
scene_type: 'group',
|
|
110
|
+
scene_name: '',
|
|
111
|
+
sender_id: 'user-1',
|
|
112
|
+
sender_name: '',
|
|
113
|
+
message: '测试',
|
|
114
|
+
time: Date.now(),
|
|
115
|
+
})).resolves.toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('获取最近消息', () => {
|
|
120
|
+
it('应该获取场景的最近消息', async () => {
|
|
121
|
+
// 添加一些消息
|
|
122
|
+
await manager.recordMessage({
|
|
123
|
+
platform: 'qq',
|
|
124
|
+
scene_id: 'group-123',
|
|
125
|
+
scene_type: 'group',
|
|
126
|
+
scene_name: '测试群',
|
|
127
|
+
sender_id: 'user-1',
|
|
128
|
+
sender_name: '张三',
|
|
129
|
+
message: '消息1',
|
|
130
|
+
time: 1000,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await manager.recordMessage({
|
|
134
|
+
platform: 'qq',
|
|
135
|
+
scene_id: 'group-123',
|
|
136
|
+
scene_type: 'group',
|
|
137
|
+
scene_name: '测试群',
|
|
138
|
+
sender_id: 'user-2',
|
|
139
|
+
sender_name: '李四',
|
|
140
|
+
message: '消息2',
|
|
141
|
+
time: 2000,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const messages = await manager.getRecentMessages('group-123');
|
|
145
|
+
|
|
146
|
+
expect(messages.length).toBe(2);
|
|
147
|
+
expect(mockMessageModel.select).toHaveBeenCalledWith(
|
|
148
|
+
{ scene_id: 'group-123' },
|
|
149
|
+
expect.objectContaining({ orderBy: { time: 'desc' } })
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('空场景应返回空数组', async () => {
|
|
154
|
+
const messages = await manager.getRecentMessages('nonexistent');
|
|
155
|
+
expect(messages).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('应该限制返回数量', async () => {
|
|
159
|
+
const messages = await manager.getRecentMessages('group-123', 10);
|
|
160
|
+
|
|
161
|
+
expect(mockMessageModel.select).toHaveBeenCalledWith(
|
|
162
|
+
{ scene_id: 'group-123' },
|
|
163
|
+
expect.objectContaining({ limit: 10 })
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('构建上下文', () => {
|
|
169
|
+
it('应该构建场景上下文', async () => {
|
|
170
|
+
await manager.recordMessage({
|
|
171
|
+
platform: 'qq',
|
|
172
|
+
scene_id: 'group-123',
|
|
173
|
+
scene_type: 'group',
|
|
174
|
+
scene_name: '测试群',
|
|
175
|
+
sender_id: 'user-1',
|
|
176
|
+
sender_name: '张三',
|
|
177
|
+
message: '你好',
|
|
178
|
+
time: Date.now(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const context = await manager.buildContext('group-123', 'qq');
|
|
182
|
+
|
|
183
|
+
expect(context.sceneId).toBe('group-123');
|
|
184
|
+
expect(context.platform).toBe('qq');
|
|
185
|
+
expect(context.chatMessages).toBeDefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('空场景应返回有效的空上下文', async () => {
|
|
189
|
+
const context = await manager.buildContext('empty-scene', 'qq');
|
|
190
|
+
|
|
191
|
+
expect(context.sceneId).toBe('empty-scene');
|
|
192
|
+
expect(context.recentMessages).toEqual([]);
|
|
193
|
+
expect(context.summaries).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('格式化消息', () => {
|
|
198
|
+
it('应该将消息格式化为 ChatMessage', () => {
|
|
199
|
+
const messages: MessageRecord[] = [
|
|
200
|
+
{
|
|
201
|
+
id: 1,
|
|
202
|
+
platform: 'qq',
|
|
203
|
+
scene_id: 'group-123',
|
|
204
|
+
scene_type: 'group',
|
|
205
|
+
scene_name: '测试群',
|
|
206
|
+
sender_id: 'user-1',
|
|
207
|
+
sender_name: '张三',
|
|
208
|
+
message: '你好',
|
|
209
|
+
time: Date.now(),
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const chatMessages = manager.formatToChatMessages([], messages);
|
|
214
|
+
|
|
215
|
+
expect(chatMessages.length).toBe(1);
|
|
216
|
+
expect(chatMessages[0].role).toBe('user');
|
|
217
|
+
expect(chatMessages[0].content).toContain('张三');
|
|
218
|
+
expect(chatMessages[0].content).toContain('你好');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('应该将总结作为系统消息添加', () => {
|
|
222
|
+
const summaries = ['这是之前的总结'];
|
|
223
|
+
const messages: MessageRecord[] = [];
|
|
224
|
+
|
|
225
|
+
const chatMessages = manager.formatToChatMessages(summaries, messages);
|
|
226
|
+
|
|
227
|
+
expect(chatMessages.length).toBe(1);
|
|
228
|
+
expect(chatMessages[0].role).toBe('system');
|
|
229
|
+
expect(chatMessages[0].content).toContain('这是之前的总结');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('应该识别机器人消息', () => {
|
|
233
|
+
const messages: MessageRecord[] = [
|
|
234
|
+
{
|
|
235
|
+
id: 1,
|
|
236
|
+
platform: 'qq',
|
|
237
|
+
scene_id: 'group-123',
|
|
238
|
+
scene_type: 'group',
|
|
239
|
+
scene_name: '',
|
|
240
|
+
sender_id: 'bot-123',
|
|
241
|
+
sender_name: 'MyBot',
|
|
242
|
+
message: '我是机器人',
|
|
243
|
+
time: Date.now(),
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const chatMessages = manager.formatToChatMessages([], messages);
|
|
248
|
+
|
|
249
|
+
expect(chatMessages[0].role).toBe('assistant');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Token 估算', () => {
|
|
254
|
+
it('应该估算中文文本的 token 数量', () => {
|
|
255
|
+
const text = '你好世界'; // 4 个中文字符
|
|
256
|
+
const tokens = manager.estimateTokens(text);
|
|
257
|
+
|
|
258
|
+
// 中文约 2 tokens/字
|
|
259
|
+
expect(tokens).toBeGreaterThanOrEqual(8);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('应该估算英文文本的 token 数量', () => {
|
|
263
|
+
const text = 'hello world'; // 11 个字符
|
|
264
|
+
const tokens = manager.estimateTokens(text);
|
|
265
|
+
|
|
266
|
+
// 英文约 0.25 tokens/字符
|
|
267
|
+
expect(tokens).toBeGreaterThanOrEqual(2);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('应该估算混合文本的 token 数量', () => {
|
|
271
|
+
const text = 'Hello 世界';
|
|
272
|
+
const tokens = manager.estimateTokens(text);
|
|
273
|
+
|
|
274
|
+
expect(tokens).toBeGreaterThan(0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('判断是否需要总结', () => {
|
|
279
|
+
it('消息少于阈值时不需要总结', async () => {
|
|
280
|
+
// 只添加少量消息
|
|
281
|
+
for (let i = 0; i < 5; i++) {
|
|
282
|
+
await manager.recordMessage({
|
|
283
|
+
platform: 'qq',
|
|
284
|
+
scene_id: 'group-123',
|
|
285
|
+
scene_type: 'group',
|
|
286
|
+
scene_name: '',
|
|
287
|
+
sender_id: 'user-1',
|
|
288
|
+
sender_name: 'User',
|
|
289
|
+
message: `消息 ${i}`,
|
|
290
|
+
time: Date.now() + i,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const shouldSummarize = await manager.shouldSummarize('group-123');
|
|
295
|
+
expect(shouldSummarize).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('总结功能', () => {
|
|
300
|
+
it('没有 AI 提供商时应返回 null', async () => {
|
|
301
|
+
const result = await manager.summarize('group-123');
|
|
302
|
+
expect(result).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('设置 AI 提供商后应该能总结', async () => {
|
|
306
|
+
// 添加足够多的消息
|
|
307
|
+
for (let i = 0; i < 60; i++) {
|
|
308
|
+
await manager.recordMessage({
|
|
309
|
+
platform: 'qq',
|
|
310
|
+
scene_id: 'group-123',
|
|
311
|
+
scene_type: 'group',
|
|
312
|
+
scene_name: '',
|
|
313
|
+
sender_id: 'user-1',
|
|
314
|
+
sender_name: 'User',
|
|
315
|
+
message: `消息 ${i}`,
|
|
316
|
+
time: Date.now() + i,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const mockProvider = {
|
|
321
|
+
models: ['test-model'],
|
|
322
|
+
chat: vi.fn().mockResolvedValue({
|
|
323
|
+
choices: [{ message: { content: '这是总结' } }],
|
|
324
|
+
}),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
manager.setAIProvider(mockProvider as any);
|
|
328
|
+
|
|
329
|
+
const result = await manager.summarize('group-123');
|
|
330
|
+
|
|
331
|
+
expect(mockProvider.chat).toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('场景统计', () => {
|
|
336
|
+
it('应该返回场景统计信息', async () => {
|
|
337
|
+
await manager.recordMessage({
|
|
338
|
+
platform: 'qq',
|
|
339
|
+
scene_id: 'group-123',
|
|
340
|
+
scene_type: 'group',
|
|
341
|
+
scene_name: '',
|
|
342
|
+
sender_id: 'user-1',
|
|
343
|
+
sender_name: 'User',
|
|
344
|
+
message: '测试',
|
|
345
|
+
time: Date.now(),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const stats = await manager.getSceneStats('group-123');
|
|
349
|
+
|
|
350
|
+
expect(stats.messageCount).toBeGreaterThan(0);
|
|
351
|
+
expect(stats.summaryCount).toBe(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('空场景应返回零统计', async () => {
|
|
355
|
+
const stats = await manager.getSceneStats('empty');
|
|
356
|
+
|
|
357
|
+
expect(stats.messageCount).toBe(0);
|
|
358
|
+
expect(stats.summaryCount).toBe(0);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('模型定义', () => {
|
|
364
|
+
it('CHAT_MESSAGE_MODEL 应该有正确的字段', () => {
|
|
365
|
+
expect(CHAT_MESSAGE_MODEL.platform).toBeDefined();
|
|
366
|
+
expect(CHAT_MESSAGE_MODEL.scene_id).toBeDefined();
|
|
367
|
+
expect(CHAT_MESSAGE_MODEL.message).toBeDefined();
|
|
368
|
+
expect(CHAT_MESSAGE_MODEL.time).toBeDefined();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('CONTEXT_SUMMARY_MODEL 应该有正确的字段', () => {
|
|
372
|
+
expect(CONTEXT_SUMMARY_MODEL.scene_id).toBeDefined();
|
|
373
|
+
expect(CONTEXT_SUMMARY_MODEL.summary).toBeDefined();
|
|
374
|
+
expect(CONTEXT_SUMMARY_MODEL.created_at).toBeDefined();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('createContextManager', () => {
|
|
379
|
+
it('应该创建 ContextManager 实例', () => {
|
|
380
|
+
const mockModel = { create: vi.fn(), select: vi.fn() };
|
|
381
|
+
const manager = createContextManager(mockModel, mockModel, {
|
|
382
|
+
maxRecentMessages: 50,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(manager).toBeInstanceOf(ContextManager);
|
|
386
|
+
});
|
|
387
|
+
});
|