@zhin.js/core 1.0.25 → 1.0.26

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 (200) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +17 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +84 -2
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/ai-trigger.d.ts.map +1 -1
  88. package/lib/built/ai-trigger.js +7 -3
  89. package/lib/built/ai-trigger.js.map +1 -1
  90. package/lib/built/command.d.ts +33 -17
  91. package/lib/built/command.d.ts.map +1 -1
  92. package/lib/built/command.js +71 -44
  93. package/lib/built/command.js.map +1 -1
  94. package/lib/built/component.d.ts +42 -15
  95. package/lib/built/component.d.ts.map +1 -1
  96. package/lib/built/component.js +84 -52
  97. package/lib/built/component.js.map +1 -1
  98. package/lib/built/config.d.ts +54 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +76 -10
  101. package/lib/built/config.js.map +1 -1
  102. package/lib/built/cron.d.ts +41 -18
  103. package/lib/built/cron.d.ts.map +1 -1
  104. package/lib/built/cron.js +106 -63
  105. package/lib/built/cron.js.map +1 -1
  106. package/lib/built/database.d.ts +55 -6
  107. package/lib/built/database.d.ts.map +1 -1
  108. package/lib/built/database.js +93 -22
  109. package/lib/built/database.js.map +1 -1
  110. package/lib/built/dispatcher.d.ts +118 -0
  111. package/lib/built/dispatcher.d.ts.map +1 -0
  112. package/lib/built/dispatcher.js +196 -0
  113. package/lib/built/dispatcher.js.map +1 -0
  114. package/lib/built/permission.d.ts +45 -5
  115. package/lib/built/permission.d.ts.map +1 -1
  116. package/lib/built/permission.js +56 -11
  117. package/lib/built/permission.js.map +1 -1
  118. package/lib/built/skill.d.ts +117 -0
  119. package/lib/built/skill.d.ts.map +1 -0
  120. package/lib/built/skill.js +191 -0
  121. package/lib/built/skill.js.map +1 -0
  122. package/lib/built/tool.d.ts +71 -164
  123. package/lib/built/tool.d.ts.map +1 -1
  124. package/lib/built/tool.js +212 -297
  125. package/lib/built/tool.js.map +1 -1
  126. package/lib/feature.d.ts +75 -0
  127. package/lib/feature.d.ts.map +1 -0
  128. package/lib/feature.js +69 -0
  129. package/lib/feature.js.map +1 -0
  130. package/lib/index.d.ts +4 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +7 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/plugin.d.ts +25 -17
  135. package/lib/plugin.d.ts.map +1 -1
  136. package/lib/plugin.js +180 -20
  137. package/lib/plugin.js.map +1 -1
  138. package/lib/types.d.ts +4 -9
  139. package/lib/types.d.ts.map +1 -1
  140. package/package.json +4 -4
  141. package/src/adapter.ts +101 -2
  142. package/src/ai/agent.ts +772 -0
  143. package/src/ai/context-manager.ts +440 -0
  144. package/src/ai/conversation-memory.ts +774 -0
  145. package/src/ai/follow-up.ts +357 -0
  146. package/src/ai/index.ts +128 -0
  147. package/src/ai/init.ts +502 -0
  148. package/src/ai/output.ts +261 -0
  149. package/src/ai/providers/anthropic.ts +375 -0
  150. package/src/ai/providers/base.ts +173 -0
  151. package/src/ai/providers/index.ts +13 -0
  152. package/src/ai/providers/ollama.ts +292 -0
  153. package/src/ai/providers/openai.ts +167 -0
  154. package/src/ai/rate-limiter.ts +129 -0
  155. package/src/ai/service.ts +319 -0
  156. package/src/ai/session.ts +544 -0
  157. package/src/ai/tone-detector.ts +89 -0
  158. package/src/ai/tools.ts +218 -0
  159. package/src/ai/types.ts +296 -0
  160. package/src/ai/user-profile.ts +181 -0
  161. package/src/ai/zhin-agent.ts +845 -0
  162. package/src/built/ai-trigger.ts +6 -3
  163. package/src/built/command.ts +75 -69
  164. package/src/built/component.ts +94 -76
  165. package/src/built/config.ts +238 -128
  166. package/src/built/cron.ts +117 -101
  167. package/src/built/database.ts +128 -33
  168. package/src/built/dispatcher.ts +332 -0
  169. package/src/built/permission.ts +146 -54
  170. package/src/built/skill.ts +280 -0
  171. package/src/built/tool.ts +245 -366
  172. package/src/feature.ts +113 -0
  173. package/src/index.ts +7 -0
  174. package/src/plugin.ts +198 -33
  175. package/src/types.ts +6 -10
  176. package/tests/adapter.test.ts +153 -1
  177. package/tests/ai/agent.test.ts +614 -0
  178. package/tests/ai/ai-trigger.test.ts +368 -0
  179. package/tests/ai/context-manager.test.ts +413 -0
  180. package/tests/ai/conversation-memory.test.ts +128 -0
  181. package/tests/ai/follow-up.test.ts +175 -0
  182. package/tests/ai/integration.test.ts +584 -0
  183. package/tests/ai/output.test.ts +128 -0
  184. package/tests/ai/providers.integration.test.ts +227 -0
  185. package/tests/ai/rate-limiter.test.ts +108 -0
  186. package/tests/ai/session.test.ts +375 -0
  187. package/tests/ai/setup.ts +308 -0
  188. package/tests/ai/tone-detector.test.ts +80 -0
  189. package/tests/ai/tool.test.ts +800 -0
  190. package/tests/ai/tools-builtin.test.ts +346 -0
  191. package/tests/ai/user-profile.test.ts +73 -0
  192. package/tests/ai/zhin-agent.test.ts +177 -0
  193. package/tests/config.test.ts +46 -0
  194. package/tests/cron.test.ts +94 -5
  195. package/tests/dispatcher.test.ts +146 -0
  196. package/tests/feature.test.ts +145 -0
  197. package/tests/features-builtin.test.ts +191 -0
  198. package/tests/plugin.test.ts +88 -14
  199. package/tests/skill-feature.test.ts +179 -0
  200. package/tests/tool-feature.test.ts +254 -0
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Session Manager 测试
3
+ *
4
+ * 测试会话管理功能:
5
+ * - 会话创建和获取
6
+ * - 消息历史管理
7
+ * - 会话超时清理
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+
11
+ // Mock Logger
12
+ vi.mock('@zhin.js/core', async (importOriginal) => {
13
+ const original = await importOriginal() as any;
14
+ return {
15
+ ...original,
16
+ Logger: class {
17
+ debug = vi.fn();
18
+ info = vi.fn();
19
+ warn = vi.fn();
20
+ error = vi.fn();
21
+ },
22
+ };
23
+ });
24
+
25
+ import {
26
+ MemorySessionManager,
27
+ DatabaseSessionManager,
28
+ SessionManager,
29
+ createMemorySessionManager
30
+ } from '../../src/ai/session.js';
31
+
32
+ describe('MemorySessionManager', () => {
33
+ let manager: MemorySessionManager;
34
+
35
+ beforeEach(() => {
36
+ vi.useFakeTimers();
37
+ manager = new MemorySessionManager({
38
+ maxHistory: 10,
39
+ expireMs: 60000, // 1 分钟
40
+ });
41
+ });
42
+
43
+ afterEach(() => {
44
+ manager.dispose();
45
+ vi.useRealTimers();
46
+ });
47
+
48
+ describe('会话创建', () => {
49
+ it('应该创建新会话', () => {
50
+ const session = manager.get('user-1');
51
+
52
+ expect(session).toBeDefined();
53
+ expect(session.id).toBe('user-1');
54
+ expect(session.messages).toEqual([]);
55
+ });
56
+
57
+ it('应该返回已存在的会话', () => {
58
+ const session1 = manager.get('user-1');
59
+ manager.addMessage('user-1', { role: 'user', content: 'test' });
60
+
61
+ const session2 = manager.get('user-1');
62
+
63
+ expect(session2.messages).toHaveLength(1);
64
+ });
65
+
66
+ it('不同用户应该有不同的会话', () => {
67
+ const session1 = manager.get('user-1');
68
+ const session2 = manager.get('user-2');
69
+
70
+ expect(session1.id).not.toBe(session2.id);
71
+ });
72
+ });
73
+
74
+ describe('消息历史', () => {
75
+ it('应该添加消息到历史', () => {
76
+ manager.addMessage('user-1', { role: 'user', content: '你好' });
77
+ manager.addMessage('user-1', { role: 'assistant', content: '你好!有什么可以帮你的?' });
78
+
79
+ const messages = manager.getMessages('user-1');
80
+ expect(messages).toHaveLength(2);
81
+ expect(messages[0].role).toBe('user');
82
+ expect(messages[1].role).toBe('assistant');
83
+ });
84
+
85
+ it('应该限制历史记录数量', () => {
86
+ // 添加超过限制的消息
87
+ for (let i = 0; i < 15; i++) {
88
+ manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
89
+ }
90
+
91
+ const messages = manager.getMessages('user-1');
92
+ expect(messages.length).toBeLessThanOrEqual(10);
93
+ });
94
+
95
+ it('系统消息应该保留', () => {
96
+ manager.setSystemPrompt('user-1', '你是一个助手');
97
+
98
+ for (let i = 0; i < 15; i++) {
99
+ manager.addMessage('user-1', { role: 'user', content: `消息 ${i}` });
100
+ }
101
+
102
+ const messages = manager.getMessages('user-1');
103
+ const systemMessages = messages.filter(m => m.role === 'system');
104
+ expect(systemMessages.length).toBe(1);
105
+ });
106
+ });
107
+
108
+ describe('系统提示', () => {
109
+ it('应该设置系统提示', () => {
110
+ manager.setSystemPrompt('user-1', '你是一个助手');
111
+
112
+ const messages = manager.getMessages('user-1');
113
+ expect(messages[0].role).toBe('system');
114
+ expect(messages[0].content).toBe('你是一个助手');
115
+ });
116
+
117
+ it('应该替换旧的系统提示', () => {
118
+ manager.setSystemPrompt('user-1', '旧提示');
119
+ manager.setSystemPrompt('user-1', '新提示');
120
+
121
+ const messages = manager.getMessages('user-1');
122
+ const systemMessages = messages.filter(m => m.role === 'system');
123
+ expect(systemMessages.length).toBe(1);
124
+ expect(systemMessages[0].content).toBe('新提示');
125
+ });
126
+ });
127
+
128
+ describe('会话清理', () => {
129
+ it('应该清除指定会话', () => {
130
+ manager.get('user-1');
131
+ manager.addMessage('user-1', { role: 'user', content: 'test' });
132
+
133
+ expect(manager.clear('user-1')).toBe(true);
134
+ expect(manager.has('user-1')).toBe(false);
135
+ });
136
+
137
+ it('清除不存在的会话应返回 false', () => {
138
+ expect(manager.clear('nonexistent')).toBe(false);
139
+ });
140
+
141
+ it('reset 应该保留系统消息', () => {
142
+ manager.setSystemPrompt('user-1', '系统提示');
143
+ manager.addMessage('user-1', { role: 'user', content: 'test' });
144
+
145
+ manager.reset('user-1');
146
+
147
+ const messages = manager.getMessages('user-1');
148
+ expect(messages.length).toBe(1);
149
+ expect(messages[0].role).toBe('system');
150
+ });
151
+ });
152
+
153
+ describe('会话超时', () => {
154
+ it('应该在超时后清理会话', () => {
155
+ manager.get('user-1');
156
+
157
+ expect(manager.has('user-1')).toBe(true);
158
+
159
+ // 前进时间超过超时时间
160
+ vi.advanceTimersByTime(61000);
161
+
162
+ // 触发清理
163
+ manager.cleanup();
164
+
165
+ expect(manager.has('user-1')).toBe(false);
166
+ });
167
+
168
+ it('活动会话不应该被清理', () => {
169
+ manager.get('user-1');
170
+
171
+ // 前进一半时间
172
+ vi.advanceTimersByTime(30000);
173
+
174
+ // 添加新消息(刷新活动时间)
175
+ manager.addMessage('user-1', { role: 'user', content: '新消息' });
176
+
177
+ // 再前进一半时间
178
+ vi.advanceTimersByTime(30000);
179
+
180
+ manager.cleanup();
181
+
182
+ // 会话应该还在
183
+ expect(manager.has('user-1')).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe('统计信息', () => {
188
+ it('应该返回正确的统计信息', () => {
189
+ manager.get('user-1');
190
+ manager.get('user-2');
191
+
192
+ const stats = manager.getStats();
193
+ expect(stats.total).toBe(2);
194
+ expect(stats.active).toBe(2);
195
+ expect(stats.expired).toBe(0);
196
+ });
197
+
198
+ it('应该返回会话列表', () => {
199
+ manager.get('user-1');
200
+ manager.get('user-2');
201
+ manager.get('user-3');
202
+
203
+ const sessions = manager.listSessions();
204
+ expect(sessions).toContain('user-1');
205
+ expect(sessions).toContain('user-2');
206
+ expect(sessions).toContain('user-3');
207
+ });
208
+ });
209
+
210
+ describe('dispose', () => {
211
+ it('应该清理所有会话', () => {
212
+ manager.get('user-1');
213
+ manager.get('user-2');
214
+
215
+ manager.dispose();
216
+
217
+ expect(manager.has('user-1')).toBe(false);
218
+ expect(manager.has('user-2')).toBe(false);
219
+ });
220
+ });
221
+ });
222
+
223
+ describe('SessionManager', () => {
224
+ describe('generateId', () => {
225
+ it('应该生成正确的会话 ID(带频道)', () => {
226
+ const id = SessionManager.generateId('qq', 'user123', 'channel456');
227
+ expect(id).toBe('qq:channel456:user123');
228
+ });
229
+
230
+ it('应该生成正确的会话 ID(无频道)', () => {
231
+ const id = SessionManager.generateId('telegram', 'user123');
232
+ expect(id).toBe('telegram:user123');
233
+ });
234
+ });
235
+ });
236
+
237
+ describe('createMemorySessionManager', () => {
238
+ it('应该创建 SessionManager 实例', () => {
239
+ const manager = createMemorySessionManager({ maxHistory: 50 });
240
+ expect(manager).toBeDefined();
241
+ expect(manager.get).toBeDefined();
242
+ manager.dispose();
243
+ });
244
+ });
245
+
246
+ describe('DatabaseSessionManager', () => {
247
+ // Mock 数据库模型
248
+ function createMockModel(records: any[] = []) {
249
+ return {
250
+ select: vi.fn().mockReturnValue({
251
+ where: vi.fn().mockResolvedValue(records),
252
+ }),
253
+ create: vi.fn().mockResolvedValue(undefined),
254
+ update: vi.fn().mockReturnValue({
255
+ where: vi.fn().mockResolvedValue(undefined),
256
+ }),
257
+ delete: vi.fn().mockReturnValue({
258
+ where: vi.fn().mockResolvedValue(undefined),
259
+ }),
260
+ };
261
+ }
262
+
263
+ afterEach(() => {
264
+ vi.useRealTimers();
265
+ });
266
+
267
+ describe('loadSession JSON 解析', () => {
268
+ it('应该解析字符串形式的 messages (JSON)', async () => {
269
+ const messagesJson = JSON.stringify([
270
+ { role: 'user', content: 'hello' },
271
+ { role: 'assistant', content: 'hi there' },
272
+ ]);
273
+ const model = createMockModel([{
274
+ session_id: 'test-1',
275
+ messages: messagesJson,
276
+ config: JSON.stringify({ provider: 'ollama' }),
277
+ created_at: 1000,
278
+ updated_at: 2000,
279
+ }]);
280
+
281
+ const manager = new DatabaseSessionManager(model);
282
+ const session = await manager.get('test-1');
283
+
284
+ expect(Array.isArray(session.messages)).toBe(true);
285
+ expect(session.messages).toHaveLength(2);
286
+ expect(session.messages[0]).toEqual({ role: 'user', content: 'hello' });
287
+ manager.dispose();
288
+ });
289
+
290
+ it('应该解析字符串形式的 config (JSON)', async () => {
291
+ const model = createMockModel([{
292
+ session_id: 'test-2',
293
+ messages: '[]',
294
+ config: '{"provider":"ollama","maxHistory":100}',
295
+ created_at: 1000,
296
+ updated_at: 2000,
297
+ }]);
298
+
299
+ const manager = new DatabaseSessionManager(model);
300
+ const session = await manager.get('test-2');
301
+
302
+ expect(typeof session.config).toBe('object');
303
+ expect(session.config).toEqual({ provider: 'ollama', maxHistory: 100 });
304
+ manager.dispose();
305
+ });
306
+
307
+ it('messages 已是数组时不应二次解析', async () => {
308
+ const messagesArray = [{ role: 'system', content: 'You are a bot' }];
309
+ const model = createMockModel([{
310
+ session_id: 'test-3',
311
+ messages: messagesArray,
312
+ config: { provider: 'openai' },
313
+ created_at: 1000,
314
+ updated_at: 2000,
315
+ }]);
316
+
317
+ const manager = new DatabaseSessionManager(model);
318
+ const session = await manager.get('test-3');
319
+
320
+ expect(session.messages).toBe(messagesArray);
321
+ expect(session.config).toEqual({ provider: 'openai' });
322
+ manager.dispose();
323
+ });
324
+
325
+ it('加载后的 messages 应支持 push 操作', async () => {
326
+ const model = createMockModel([{
327
+ session_id: 'test-4',
328
+ messages: '[{"role":"user","content":"hi"}]',
329
+ config: '{"provider":"openai"}',
330
+ created_at: 1000,
331
+ updated_at: 2000,
332
+ }]);
333
+
334
+ const manager = new DatabaseSessionManager(model);
335
+ // addMessage 内部调用 session.messages.push
336
+ await manager.addMessage('test-4', { role: 'assistant', content: 'hello!' });
337
+
338
+ const session = await manager.get('test-4');
339
+ expect(session.messages).toHaveLength(2);
340
+ expect(session.messages[1]).toEqual({ role: 'assistant', content: 'hello!' });
341
+ manager.dispose();
342
+ });
343
+
344
+ it('messages 或 config 为 null/undefined 时应使用默认值', async () => {
345
+ const model = createMockModel([{
346
+ session_id: 'test-5',
347
+ messages: null,
348
+ config: null,
349
+ created_at: 1000,
350
+ updated_at: 2000,
351
+ }]);
352
+
353
+ const manager = new DatabaseSessionManager(model);
354
+ const session = await manager.get('test-5');
355
+
356
+ expect(Array.isArray(session.messages)).toBe(true);
357
+ expect(session.messages).toHaveLength(0);
358
+ expect(session.config).toEqual({ provider: 'openai' });
359
+ manager.dispose();
360
+ });
361
+ });
362
+
363
+ describe('新会话创建', () => {
364
+ it('数据库无记录时应创建新会话', async () => {
365
+ const model = createMockModel([]); // 空结果
366
+ const manager = new DatabaseSessionManager(model);
367
+ const session = await manager.get('new-session');
368
+
369
+ expect(session.id).toBe('new-session');
370
+ expect(Array.isArray(session.messages)).toBe(true);
371
+ expect(session.messages).toHaveLength(0);
372
+ manager.dispose();
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,308 @@
1
+ /**
2
+ * AI 模块测试环境设置
3
+ *
4
+ * 提供:
5
+ * - Mock 对象工厂
6
+ * - 测试辅助函数
7
+ * - 公共测试配置
8
+ */
9
+ import { vi } from 'vitest';
10
+ import type { Message, MessageElement, Tool, ToolContext } from '@zhin.js/core';
11
+ import type { AIConfig, AIProviderConfig, ChatMessage } from '../../src/ai/types.js';
12
+
13
+ // ============================================================================
14
+ // Logger Mock
15
+ // ============================================================================
16
+
17
+ export const createMockLogger = () => ({
18
+ debug: vi.fn(),
19
+ info: vi.fn(),
20
+ warn: vi.fn(),
21
+ error: vi.fn(),
22
+ });
23
+
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
+ keywords?: string[];
110
+ executeResult?: any;
111
+ executeError?: Error;
112
+ }
113
+
114
+ export const createMockTool = (options: MockToolOptions): Tool => {
115
+ const {
116
+ name,
117
+ description = `${name} 工具`,
118
+ parameters = {},
119
+ required = [],
120
+ platforms,
121
+ scopes,
122
+ permissionLevel,
123
+ tags = [],
124
+ keywords = [],
125
+ executeResult = 'success',
126
+ executeError,
127
+ } = options;
128
+
129
+ const properties: Record<string, any> = {};
130
+ for (const [key, value] of Object.entries(parameters)) {
131
+ properties[key] = {
132
+ type: value.type,
133
+ description: value.description,
134
+ };
135
+ }
136
+
137
+ const tool: any = {
138
+ name,
139
+ description,
140
+ parameters: {
141
+ type: 'object',
142
+ properties,
143
+ required,
144
+ },
145
+ platforms,
146
+ scopes,
147
+ permissionLevel,
148
+ tags,
149
+ execute: executeError
150
+ ? vi.fn().mockRejectedValue(executeError)
151
+ : vi.fn().mockResolvedValue(executeResult),
152
+ };
153
+ if (keywords.length > 0) tool.keywords = keywords;
154
+ return tool;
155
+ };
156
+
157
+ // ============================================================================
158
+ // AI Provider Mock
159
+ // ============================================================================
160
+
161
+ export interface MockProviderOptions {
162
+ name?: string;
163
+ response?: string | AsyncGenerator<string>;
164
+ toolCalls?: Array<{ name: string; arguments: Record<string, any> }>;
165
+ error?: Error;
166
+ }
167
+
168
+ export const createMockProvider = (options: MockProviderOptions = {}) => {
169
+ const {
170
+ name = 'mock',
171
+ response = '这是 AI 的回复',
172
+ toolCalls = [],
173
+ error,
174
+ } = options;
175
+
176
+ if (error) {
177
+ return {
178
+ name,
179
+ chat: vi.fn().mockRejectedValue(error),
180
+ healthCheck: vi.fn().mockResolvedValue(false),
181
+ };
182
+ }
183
+
184
+ const generateResponse = async function* (): AsyncGenerator<{
185
+ content?: string;
186
+ toolCalls?: Array<{ name: string; arguments: Record<string, any> }>;
187
+ done: boolean;
188
+ }> {
189
+ if (toolCalls.length > 0) {
190
+ yield { toolCalls, done: false };
191
+ }
192
+
193
+ if (typeof response === 'string') {
194
+ yield { content: response, done: true };
195
+ } else {
196
+ for await (const chunk of response) {
197
+ yield { content: chunk, done: false };
198
+ }
199
+ yield { done: true };
200
+ }
201
+ };
202
+
203
+ return {
204
+ name,
205
+ chat: vi.fn().mockImplementation(() => generateResponse()),
206
+ healthCheck: vi.fn().mockResolvedValue(true),
207
+ };
208
+ };
209
+
210
+ // ============================================================================
211
+ // Context Factory
212
+ // ============================================================================
213
+
214
+ export const createToolContext = (options: Partial<ToolContext> = {}): ToolContext => ({
215
+ platform: 'test',
216
+ scope: 'group',
217
+ permissionLevel: 'user',
218
+ ...options,
219
+ });
220
+
221
+ // ============================================================================
222
+ // AI Config Factory
223
+ // ============================================================================
224
+
225
+ export const createMockAIConfig = (overrides: Partial<AIConfig> = {}): AIConfig => ({
226
+ defaultProvider: 'mock',
227
+ sessions: {
228
+ maxHistory: 10,
229
+ timeout: 300000,
230
+ },
231
+ context: {
232
+ enabled: false,
233
+ maxSize: 100,
234
+ },
235
+ trigger: {
236
+ enabled: true,
237
+ prefixes: ['#'],
238
+ ignorePrefixes: ['/'],
239
+ allowAtBot: true,
240
+ allowPrivateChat: true,
241
+ },
242
+ ...overrides,
243
+ });
244
+
245
+ // ============================================================================
246
+ // Test Utilities
247
+ // ============================================================================
248
+
249
+ /**
250
+ * 等待 Promise 解决或超时
251
+ */
252
+ export const waitFor = async <T>(
253
+ promise: Promise<T>,
254
+ timeout = 5000
255
+ ): Promise<T> => {
256
+ return Promise.race([
257
+ promise,
258
+ new Promise<T>((_, reject) =>
259
+ setTimeout(() => reject(new Error('Timeout')), timeout)
260
+ ),
261
+ ]);
262
+ };
263
+
264
+ /**
265
+ * 收集 AsyncGenerator 的所有值
266
+ */
267
+ export const collectAsyncGenerator = async <T>(
268
+ generator: AsyncGenerator<T>
269
+ ): Promise<T[]> => {
270
+ const results: T[] = [];
271
+ for await (const item of generator) {
272
+ results.push(item);
273
+ }
274
+ return results;
275
+ };
276
+
277
+ /**
278
+ * 创建延迟 Promise
279
+ */
280
+ export const delay = (ms: number): Promise<void> =>
281
+ new Promise((resolve) => setTimeout(resolve, ms));
282
+
283
+ /**
284
+ * 创建带有所有字段的 ChatMessage
285
+ */
286
+ export const createChatMessage = (
287
+ role: 'user' | 'assistant' | 'system',
288
+ content: string
289
+ ): ChatMessage => ({
290
+ role,
291
+ content,
292
+ });
293
+
294
+ /**
295
+ * 验证工具参数结构
296
+ */
297
+ export const assertToolParameters = (
298
+ tool: Tool,
299
+ expectedProperties: string[],
300
+ expectedRequired: string[] = []
301
+ ) => {
302
+ const props = Object.keys(tool.parameters.properties || {});
303
+ expect(props).toEqual(expect.arrayContaining(expectedProperties));
304
+
305
+ if (expectedRequired.length > 0) {
306
+ expect(tool.parameters.required).toEqual(expect.arrayContaining(expectedRequired));
307
+ }
308
+ };