@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/agent.d.ts +54 -6
  3. package/lib/agent.d.ts.map +1 -1
  4. package/lib/agent.js +468 -116
  5. package/lib/agent.js.map +1 -1
  6. package/lib/compaction.d.ts +132 -0
  7. package/lib/compaction.d.ts.map +1 -0
  8. package/lib/compaction.js +370 -0
  9. package/lib/compaction.js.map +1 -0
  10. package/lib/context-manager.d.ts.map +1 -1
  11. package/lib/context-manager.js +10 -3
  12. package/lib/context-manager.js.map +1 -1
  13. package/lib/conversation-memory.d.ts +192 -0
  14. package/lib/conversation-memory.d.ts.map +1 -0
  15. package/lib/conversation-memory.js +619 -0
  16. package/lib/conversation-memory.js.map +1 -0
  17. package/lib/index.d.ts +25 -163
  18. package/lib/index.d.ts.map +1 -1
  19. package/lib/index.js +24 -1122
  20. package/lib/index.js.map +1 -1
  21. package/lib/output.d.ts +93 -0
  22. package/lib/output.d.ts.map +1 -0
  23. package/lib/output.js +176 -0
  24. package/lib/output.js.map +1 -0
  25. package/lib/providers/anthropic.d.ts +7 -0
  26. package/lib/providers/anthropic.d.ts.map +1 -1
  27. package/lib/providers/anthropic.js +5 -0
  28. package/lib/providers/anthropic.js.map +1 -1
  29. package/lib/providers/ollama.d.ts +10 -0
  30. package/lib/providers/ollama.d.ts.map +1 -1
  31. package/lib/providers/ollama.js +19 -4
  32. package/lib/providers/ollama.js.map +1 -1
  33. package/lib/providers/openai.d.ts +7 -0
  34. package/lib/providers/openai.d.ts.map +1 -1
  35. package/lib/providers/openai.js +11 -0
  36. package/lib/providers/openai.js.map +1 -1
  37. package/lib/rate-limiter.d.ts +38 -0
  38. package/lib/rate-limiter.d.ts.map +1 -0
  39. package/lib/rate-limiter.js +86 -0
  40. package/lib/rate-limiter.js.map +1 -0
  41. package/lib/session.d.ts +7 -0
  42. package/lib/session.d.ts.map +1 -1
  43. package/lib/session.js +47 -18
  44. package/lib/session.js.map +1 -1
  45. package/lib/storage.d.ts +68 -0
  46. package/lib/storage.d.ts.map +1 -0
  47. package/lib/storage.js +105 -0
  48. package/lib/storage.js.map +1 -0
  49. package/lib/tone-detector.d.ts +19 -0
  50. package/lib/tone-detector.d.ts.map +1 -0
  51. package/lib/tone-detector.js +72 -0
  52. package/lib/tone-detector.js.map +1 -0
  53. package/lib/types.d.ts +84 -8
  54. package/lib/types.d.ts.map +1 -1
  55. package/package.json +13 -42
  56. package/src/agent.ts +518 -135
  57. package/src/compaction.ts +529 -0
  58. package/src/context-manager.ts +10 -9
  59. package/src/conversation-memory.ts +816 -0
  60. package/src/index.ts +121 -1406
  61. package/src/output.ts +261 -0
  62. package/src/providers/anthropic.ts +4 -0
  63. package/src/providers/ollama.ts +23 -4
  64. package/src/providers/openai.ts +8 -1
  65. package/src/rate-limiter.ts +129 -0
  66. package/src/session.ts +47 -18
  67. package/src/storage.ts +135 -0
  68. package/src/tone-detector.ts +89 -0
  69. package/src/types.ts +95 -6
  70. package/tests/agent.test.ts +123 -70
  71. package/tests/compaction.test.ts +310 -0
  72. package/tests/context-manager.test.ts +73 -47
  73. package/tests/conversation-memory.test.ts +128 -0
  74. package/tests/output.test.ts +128 -0
  75. package/tests/providers.test.ts +574 -0
  76. package/tests/rate-limiter.test.ts +108 -0
  77. package/tests/session.test.ts +139 -48
  78. package/tests/setup.ts +82 -240
  79. package/tests/storage.test.ts +224 -0
  80. package/tests/tone-detector.test.ts +80 -0
  81. package/tsconfig.json +4 -5
  82. package/vitest.setup.ts +1 -0
  83. package/README.md +0 -564
  84. package/TOOLS.md +0 -294
  85. package/lib/tools.d.ts +0 -45
  86. package/lib/tools.d.ts.map +0 -1
  87. package/lib/tools.js +0 -194
  88. package/lib/tools.js.map +0 -1
  89. package/src/tools.ts +0 -205
  90. package/tests/ai-trigger.test.ts +0 -369
  91. package/tests/integration.test.ts +0 -596
  92. package/tests/providers.integration.test.ts +0 -227
  93. package/tests/tool.test.ts +0 -800
  94. package/tests/tools-builtin.test.ts +0 -346
@@ -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
- // Mock Logger
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
- SessionManager,
28
- createMemorySessionManager
29
- } from '../src/session.js';
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, // 1 分钟
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
- * AI 模块测试环境设置
3
- *
4
- * 提供:
5
- * - Mock 对象工厂
6
- * - 测试辅助函数
7
- * - 公共测试配置
2
+ * @zhin.js/ai 测试环境设置
3
+ * 提供通用 AI Mock 与测试辅助
8
4
  */
9
5
  import { vi } from 'vitest';
10
- import type { Message, MessageElement, Tool, ToolContext } from '@zhin.js/core';
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
- chat: vi.fn().mockImplementation(() => generateResponse()),
41
+ models: ['mock-model'],
42
+ chat: vi.fn(),
202
43
  healthCheck: vi.fn().mockResolvedValue(true),
203
44
  };
204
45
  };
205
46
 
206
- // ============================================================================
207
- // Context Factory
208
- // ============================================================================
209
-
210
- export const createToolContext = (options: Partial<ToolContext> = {}): ToolContext => ({
211
- platform: 'test',
212
- scope: 'group',
213
- permissionLevel: 'user',
214
- ...options,
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
- // AI Config Factory
219
- // ============================================================================
220
-
221
- export const createMockAIConfig = (overrides: Partial<AIConfig> = {}): AIConfig => ({
222
- defaultProvider: 'mock',
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
- // Test Utilities
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
- * 创建延迟 Promise
275
- */
276
- export const delay = (ms: number): Promise<void> =>
277
- new Promise((resolve) => setTimeout(resolve, ms));
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
- * 创建带有所有字段的 ChatMessage
281
- */
282
- export const createChatMessage = (
283
- role: 'user' | 'assistant' | 'system',
284
- content: string
285
- ): ChatMessage => ({
286
- role,
287
- content,
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
- export const assertToolParameters = (
294
- tool: Tool,
295
- expectedProperties: string[],
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
  };