@zhin.js/core 1.0.24 → 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 (211) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +45 -1
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +182 -1
  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/adapter-process.d.ts +4 -0
  88. package/lib/built/adapter-process.d.ts.map +1 -1
  89. package/lib/built/adapter-process.js +94 -0
  90. package/lib/built/adapter-process.js.map +1 -1
  91. package/lib/built/ai-trigger.d.ts +89 -0
  92. package/lib/built/ai-trigger.d.ts.map +1 -0
  93. package/lib/built/ai-trigger.js +166 -0
  94. package/lib/built/ai-trigger.js.map +1 -0
  95. package/lib/built/command.d.ts +33 -17
  96. package/lib/built/command.d.ts.map +1 -1
  97. package/lib/built/command.js +71 -44
  98. package/lib/built/command.js.map +1 -1
  99. package/lib/built/component.d.ts +42 -15
  100. package/lib/built/component.d.ts.map +1 -1
  101. package/lib/built/component.js +84 -52
  102. package/lib/built/component.js.map +1 -1
  103. package/lib/built/config.d.ts +54 -5
  104. package/lib/built/config.d.ts.map +1 -1
  105. package/lib/built/config.js +76 -10
  106. package/lib/built/config.js.map +1 -1
  107. package/lib/built/cron.d.ts +41 -18
  108. package/lib/built/cron.d.ts.map +1 -1
  109. package/lib/built/cron.js +106 -63
  110. package/lib/built/cron.js.map +1 -1
  111. package/lib/built/database.d.ts +55 -6
  112. package/lib/built/database.d.ts.map +1 -1
  113. package/lib/built/database.js +93 -22
  114. package/lib/built/database.js.map +1 -1
  115. package/lib/built/dispatcher.d.ts +118 -0
  116. package/lib/built/dispatcher.d.ts.map +1 -0
  117. package/lib/built/dispatcher.js +196 -0
  118. package/lib/built/dispatcher.js.map +1 -0
  119. package/lib/built/permission.d.ts +45 -5
  120. package/lib/built/permission.d.ts.map +1 -1
  121. package/lib/built/permission.js +56 -11
  122. package/lib/built/permission.js.map +1 -1
  123. package/lib/built/skill.d.ts +117 -0
  124. package/lib/built/skill.d.ts.map +1 -0
  125. package/lib/built/skill.js +191 -0
  126. package/lib/built/skill.js.map +1 -0
  127. package/lib/built/tool.d.ts +188 -0
  128. package/lib/built/tool.d.ts.map +1 -0
  129. package/lib/built/tool.js +749 -0
  130. package/lib/built/tool.js.map +1 -0
  131. package/lib/feature.d.ts +75 -0
  132. package/lib/feature.d.ts.map +1 -0
  133. package/lib/feature.js +69 -0
  134. package/lib/feature.js.map +1 -0
  135. package/lib/index.d.ts +6 -0
  136. package/lib/index.d.ts.map +1 -1
  137. package/lib/index.js +11 -0
  138. package/lib/index.js.map +1 -1
  139. package/lib/plugin.d.ts +53 -18
  140. package/lib/plugin.d.ts.map +1 -1
  141. package/lib/plugin.js +301 -31
  142. package/lib/plugin.js.map +1 -1
  143. package/lib/types.d.ts +248 -9
  144. package/lib/types.d.ts.map +1 -1
  145. package/lib/utils.d.ts.map +1 -1
  146. package/lib/utils.js +38 -12
  147. package/lib/utils.js.map +1 -1
  148. package/package.json +4 -4
  149. package/src/adapter.ts +206 -2
  150. package/src/ai/agent.ts +772 -0
  151. package/src/ai/context-manager.ts +440 -0
  152. package/src/ai/conversation-memory.ts +774 -0
  153. package/src/ai/follow-up.ts +357 -0
  154. package/src/ai/index.ts +128 -0
  155. package/src/ai/init.ts +502 -0
  156. package/src/ai/output.ts +261 -0
  157. package/src/ai/providers/anthropic.ts +375 -0
  158. package/src/ai/providers/base.ts +173 -0
  159. package/src/ai/providers/index.ts +13 -0
  160. package/src/ai/providers/ollama.ts +292 -0
  161. package/src/ai/providers/openai.ts +167 -0
  162. package/src/ai/rate-limiter.ts +129 -0
  163. package/src/ai/service.ts +319 -0
  164. package/src/ai/session.ts +544 -0
  165. package/src/ai/tone-detector.ts +89 -0
  166. package/src/ai/tools.ts +218 -0
  167. package/src/ai/types.ts +296 -0
  168. package/src/ai/user-profile.ts +181 -0
  169. package/src/ai/zhin-agent.ts +845 -0
  170. package/src/built/adapter-process.ts +99 -0
  171. package/src/built/ai-trigger.ts +259 -0
  172. package/src/built/command.ts +75 -69
  173. package/src/built/component.ts +94 -76
  174. package/src/built/config.ts +238 -128
  175. package/src/built/cron.ts +117 -101
  176. package/src/built/database.ts +128 -33
  177. package/src/built/dispatcher.ts +332 -0
  178. package/src/built/permission.ts +146 -54
  179. package/src/built/skill.ts +280 -0
  180. package/src/built/tool.ts +928 -0
  181. package/src/feature.ts +113 -0
  182. package/src/index.ts +11 -0
  183. package/src/plugin.ts +359 -69
  184. package/src/types.ts +306 -11
  185. package/src/utils.ts +37 -13
  186. package/tests/adapter.test.ts +153 -1
  187. package/tests/ai/agent.test.ts +614 -0
  188. package/tests/ai/ai-trigger.test.ts +368 -0
  189. package/tests/ai/context-manager.test.ts +413 -0
  190. package/tests/ai/conversation-memory.test.ts +128 -0
  191. package/tests/ai/follow-up.test.ts +175 -0
  192. package/tests/ai/integration.test.ts +584 -0
  193. package/tests/ai/output.test.ts +128 -0
  194. package/tests/ai/providers.integration.test.ts +227 -0
  195. package/tests/ai/rate-limiter.test.ts +108 -0
  196. package/tests/ai/session.test.ts +375 -0
  197. package/tests/ai/setup.ts +308 -0
  198. package/tests/ai/tone-detector.test.ts +80 -0
  199. package/tests/ai/tool.test.ts +800 -0
  200. package/tests/ai/tools-builtin.test.ts +346 -0
  201. package/tests/ai/user-profile.test.ts +73 -0
  202. package/tests/ai/zhin-agent.test.ts +177 -0
  203. package/tests/component-new.test.ts +17 -6
  204. package/tests/config.test.ts +46 -0
  205. package/tests/cron.test.ts +94 -5
  206. package/tests/dispatcher.test.ts +146 -0
  207. package/tests/feature.test.ts +145 -0
  208. package/tests/features-builtin.test.ts +191 -0
  209. package/tests/plugin.test.ts +88 -14
  210. package/tests/skill-feature.test.ts +179 -0
  211. package/tests/tool-feature.test.ts +254 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * AI 模块集成测试
3
+ *
4
+ * 完整测试环境,包括:
5
+ * 1. AI 服务初始化
6
+ * 2. 工具服务功能
7
+ * 3. AI 触发中间件
8
+ * 4. 内置工具
9
+ */
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+
12
+ // Mock Logger first
13
+ vi.mock('@zhin.js/core', async (importOriginal) => {
14
+ const original = await importOriginal() as any;
15
+ return {
16
+ ...original,
17
+ defineModel: vi.fn(), // Mock defineModel
18
+ getPlugin: vi.fn(() => ({
19
+ name: 'test-plugin',
20
+ root: { inject: vi.fn() },
21
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
22
+ onDispose: vi.fn(),
23
+ collectAllTools: vi.fn(() => []),
24
+ })),
25
+ usePlugin: vi.fn(() => ({
26
+ name: 'test-plugin',
27
+ root: {
28
+ inject: vi.fn(),
29
+ addMiddleware: vi.fn(),
30
+ },
31
+ provide: vi.fn(),
32
+ useContext: vi.fn(),
33
+ addMiddleware: vi.fn(),
34
+ defineModel: vi.fn(),
35
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
36
+ })),
37
+ Logger: class {
38
+ debug = vi.fn();
39
+ info = vi.fn();
40
+ warn = vi.fn();
41
+ error = vi.fn();
42
+ },
43
+ segment: {
44
+ toString: (elements: any[]) => {
45
+ if (!Array.isArray(elements)) return String(elements);
46
+ return elements.map(el => {
47
+ if (typeof el === 'string') return el;
48
+ if (el.type === 'text') return el.data?.text || '';
49
+ if (el.type === 'at') return `<at user_id="${el.data?.user_id || el.data?.qq}"/>`;
50
+ if (el.type === 'image') return `<image url="${el.data?.url}"/>`;
51
+ return '';
52
+ }).join('');
53
+ },
54
+ from: (str: string) => {
55
+ if (!str) return [];
56
+ return [{ type: 'text', data: { text: str } }];
57
+ },
58
+ },
59
+ };
60
+ });
61
+
62
+ // Import after mocking
63
+ import { AIService } from '../../src/ai/index.js';
64
+ import { ToolFeature, ZhinTool, shouldTriggerAI, inferSenderPermissions } from '@zhin.js/core';
65
+ import { calculatorTool, timeTool, getBuiltinTools } from '../../src/ai/tools.js';
66
+ import type { Tool, ToolContext } from '@zhin.js/core';
67
+ import type { AgentTool } from '../../src/ai/types.js';
68
+
69
+ // ============================================================================
70
+ // AI Service 测试
71
+ // ============================================================================
72
+
73
+ describe('AI Service 集成测试', () => {
74
+ let aiService: AIService;
75
+
76
+ beforeEach(() => {
77
+ vi.clearAllMocks();
78
+ aiService = new AIService({
79
+ defaultProvider: 'mock',
80
+ sessions: { maxHistory: 10 },
81
+ });
82
+ });
83
+
84
+ afterEach(() => {
85
+ aiService?.dispose();
86
+ });
87
+
88
+ describe('服务初始化', () => {
89
+ it('应该创建 AI 服务实例', () => {
90
+ expect(aiService).toBeDefined();
91
+ expect(aiService.sessions).toBeDefined();
92
+ });
93
+
94
+ it('没有配置提供商时 isReady 应返回 false', () => {
95
+ expect(aiService.isReady()).toBe(false);
96
+ });
97
+
98
+ it('应该返回空的提供商列表', () => {
99
+ expect(aiService.listProviders()).toEqual([]);
100
+ });
101
+
102
+ it('获取不存在的提供商应抛出错误', () => {
103
+ expect(() => aiService.getProvider('nonexistent')).toThrow();
104
+ });
105
+
106
+ it('应该根据配置初始化所有 Provider', () => {
107
+ const fullService = new AIService({
108
+ providers: {
109
+ openai: { apiKey: 'sk-test' },
110
+ anthropic: { apiKey: 'sk-ant-test' },
111
+ deepseek: { apiKey: 'sk-deepseek' },
112
+ moonshot: { apiKey: 'sk-moonshot' },
113
+ zhipu: { apiKey: 'sk-zhipu' },
114
+ ollama: { baseUrl: 'http://localhost:11434' },
115
+ },
116
+ });
117
+
118
+ const providers = fullService.listProviders();
119
+ expect(providers).toContain('openai');
120
+ expect(providers).toContain('anthropic');
121
+ expect(providers).toContain('deepseek');
122
+ expect(providers).toContain('moonshot');
123
+ expect(providers).toContain('zhipu');
124
+ expect(providers).toContain('ollama');
125
+ expect(providers).toHaveLength(6);
126
+
127
+ fullService.dispose();
128
+ });
129
+
130
+ it('应该只初始化有 apiKey 的 Provider', () => {
131
+ const partialService = new AIService({
132
+ providers: {
133
+ openai: { apiKey: 'sk-test' },
134
+ deepseek: {}, // 没有 apiKey
135
+ moonshot: { apiKey: 'sk-moonshot' },
136
+ },
137
+ });
138
+
139
+ const providers = partialService.listProviders();
140
+ expect(providers).toContain('openai');
141
+ expect(providers).toContain('moonshot');
142
+ expect(providers).not.toContain('deepseek');
143
+ expect(providers).toHaveLength(2);
144
+
145
+ partialService.dispose();
146
+ });
147
+ });
148
+
149
+ describe('配置管理', () => {
150
+ it('应该返回会话配置', () => {
151
+ const config = aiService.getSessionConfig();
152
+ expect(config.maxHistory).toBe(10);
153
+ });
154
+
155
+ it('应该返回上下文配置', () => {
156
+ const config = aiService.getContextConfig();
157
+ expect(config).toBeDefined();
158
+ });
159
+
160
+ it('应该返回触发器配置', () => {
161
+ const config = aiService.getTriggerConfig();
162
+ expect(config).toBeDefined();
163
+ });
164
+ });
165
+
166
+ describe('工具管理', () => {
167
+ it('应该收集内置工具', () => {
168
+ const tools = aiService.collectAllTools();
169
+ expect(Array.isArray(tools)).toBe(true);
170
+ const names = tools.map(t => t.name);
171
+ expect(names).toContain('calculator');
172
+ expect(names).toContain('get_time');
173
+ });
174
+
175
+ it('应该注册自定义工具', () => {
176
+ const customTool: AgentTool = {
177
+ name: 'custom_tool',
178
+ description: '自定义工具',
179
+ parameters: { type: 'object', properties: {} },
180
+ execute: async () => 'result',
181
+ };
182
+
183
+ const dispose = aiService.registerTool(customTool);
184
+
185
+ const tools = aiService.collectAllTools();
186
+ expect(tools.some(t => t.name === 'custom_tool')).toBe(true);
187
+
188
+ dispose();
189
+ const toolsAfter = aiService.collectAllTools();
190
+ expect(toolsAfter.some(t => t.name === 'custom_tool')).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe('dispose', () => {
195
+ it('应该正确清理资源', () => {
196
+ aiService.dispose();
197
+ expect(aiService.listProviders()).toEqual([]);
198
+ });
199
+ });
200
+ });
201
+
202
+ // ============================================================================
203
+ // Tool Service 测试
204
+ // ============================================================================
205
+
206
+ describe('Tool Service 集成测试', () => {
207
+ let service: ToolFeature;
208
+
209
+ beforeEach(() => {
210
+ vi.clearAllMocks();
211
+ service = new ToolFeature();
212
+ });
213
+
214
+ describe('Context 创建', () => {
215
+ it('应该创建正确的 Context', () => {
216
+ expect(service.name).toBe('tool');
217
+ expect(service.desc).toBeDefined();
218
+ expect(service.icon).toBe('Wrench');
219
+ });
220
+ });
221
+
222
+ describe('工具注册', () => {
223
+ it('应该注册 Tool 对象', () => {
224
+ const tool: Tool = {
225
+ name: 'test_tool',
226
+ description: '测试工具',
227
+ parameters: { type: 'object', properties: {} },
228
+ execute: async () => 'result',
229
+ };
230
+
231
+ const dispose = service.addTool(tool, 'test-plugin', false);
232
+
233
+ expect(service.get('test_tool')).toBeDefined();
234
+ expect(service.getAll()).toHaveLength(1);
235
+
236
+ dispose();
237
+ expect(service.get('test_tool')).toBeUndefined();
238
+ });
239
+
240
+ it('应该注册 ZhinTool 实例', () => {
241
+ const zhinTool = new ZhinTool('zhin_tool')
242
+ .desc('ZhinTool 测试')
243
+ .execute(async () => 'result');
244
+
245
+ service.addTool(zhinTool, 'test-plugin', false);
246
+
247
+ const registered = service.get('zhin_tool');
248
+ expect(registered).toBeDefined();
249
+ expect(registered?.description).toBe('ZhinTool 测试');
250
+ });
251
+
252
+ it('应该正确移除工具', () => {
253
+ const tool: Tool = {
254
+ name: 'removable',
255
+ description: '',
256
+ parameters: { type: 'object', properties: {} },
257
+ execute: async () => '',
258
+ };
259
+
260
+ service.addTool(tool, 'test', false);
261
+ expect(service.removeTool('removable')).toBe(true);
262
+ expect(service.removeTool('removable')).toBe(false);
263
+ });
264
+
265
+ it('应该自动添加来源标识', () => {
266
+ const tool: Tool = {
267
+ name: 'sourced_tool',
268
+ description: '',
269
+ parameters: { type: 'object', properties: {} },
270
+ execute: async () => '',
271
+ };
272
+
273
+ service.addTool(tool, 'my-plugin', false);
274
+
275
+ const registered = service.get('sourced_tool');
276
+ expect(registered?.source).toContain('my-plugin');
277
+ expect(registered?.tags).toContain('my-plugin');
278
+ });
279
+ });
280
+
281
+ describe('工具执行', () => {
282
+ it('应该执行已注册的工具', async () => {
283
+ const tool: Tool = {
284
+ name: 'executable',
285
+ description: '',
286
+ parameters: { type: 'object', properties: {} },
287
+ execute: async (args) => `received: ${JSON.stringify(args)}`,
288
+ };
289
+
290
+ service.addTool(tool, 'test', false);
291
+
292
+ const result = await service.execute('executable', { key: 'value' });
293
+ expect(result).toBe('received: {"key":"value"}');
294
+ });
295
+
296
+ it('执行不存在的工具应抛出错误', async () => {
297
+ await expect(service.execute('nonexistent', {})).rejects.toThrow('not found');
298
+ });
299
+ });
300
+
301
+ describe('标签过滤', () => {
302
+ it('应该按标签过滤工具', () => {
303
+ service.addTool({
304
+ name: 'tagged1',
305
+ description: '',
306
+ parameters: { type: 'object', properties: {} },
307
+ tags: ['utility'],
308
+ execute: async () => '',
309
+ }, 'test', false);
310
+
311
+ service.addTool({
312
+ name: 'tagged2',
313
+ description: '',
314
+ parameters: { type: 'object', properties: {} },
315
+ tags: ['helper'],
316
+ execute: async () => '',
317
+ }, 'test', false);
318
+
319
+ const utilityTools = service.getByTags(['utility']);
320
+ expect(utilityTools).toHaveLength(1);
321
+ expect(utilityTools[0].name).toBe('tagged1');
322
+ });
323
+ });
324
+
325
+ describe('上下文过滤', () => {
326
+ it('应该按平台过滤工具', () => {
327
+ service.addTool({
328
+ name: 'qq_only',
329
+ description: '',
330
+ parameters: { type: 'object', properties: {} },
331
+ platforms: ['qq'],
332
+ execute: async () => '',
333
+ }, 'test', false);
334
+
335
+ service.addTool({
336
+ name: 'all_platforms',
337
+ description: '',
338
+ parameters: { type: 'object', properties: {} },
339
+ execute: async () => '',
340
+ }, 'test', false);
341
+
342
+ const qqContext: ToolContext = { platform: 'qq' };
343
+ const telegramContext: ToolContext = { platform: 'telegram' };
344
+
345
+ const allTools = service.getAll();
346
+
347
+ const qqFiltered = service.filterByContext(allTools, qqContext);
348
+ expect(qqFiltered.some(t => t.name === 'qq_only')).toBe(true);
349
+ expect(qqFiltered.some(t => t.name === 'all_platforms')).toBe(true);
350
+
351
+ const telegramFiltered = service.filterByContext(allTools, telegramContext);
352
+ expect(telegramFiltered.some(t => t.name === 'qq_only')).toBe(false);
353
+ expect(telegramFiltered.some(t => t.name === 'all_platforms')).toBe(true);
354
+ });
355
+
356
+ it('应该按权限过滤工具', () => {
357
+ service.addTool({
358
+ name: 'admin_tool',
359
+ description: '',
360
+ parameters: { type: 'object', properties: {} },
361
+ permissionLevel: 'bot_admin',
362
+ execute: async () => '',
363
+ }, 'test', false);
364
+
365
+ const allTools = service.getAll();
366
+
367
+ const userContext: ToolContext = {};
368
+ const adminContext: ToolContext = { isBotAdmin: true };
369
+
370
+ const userFiltered = service.filterByContext(allTools, userContext);
371
+ expect(userFiltered.some(t => t.name === 'admin_tool')).toBe(false);
372
+
373
+ const adminFiltered = service.filterByContext(allTools, adminContext);
374
+ expect(adminFiltered.some(t => t.name === 'admin_tool')).toBe(true);
375
+ });
376
+ });
377
+ });
378
+
379
+ // ============================================================================
380
+ // AI Trigger 工具函数测试
381
+ // ============================================================================
382
+
383
+ describe('AI Trigger 工具函数测试', () => {
384
+ function createMockMessage(options: {
385
+ content: string | any[];
386
+ bot?: string;
387
+ channelType?: 'private' | 'group' | 'channel';
388
+ senderId?: string;
389
+ senderPermissions?: string[];
390
+ }) {
391
+ const content = typeof options.content === 'string'
392
+ ? [{ type: 'text', data: { text: options.content } }]
393
+ : options.content;
394
+
395
+ return {
396
+ $content: content,
397
+ $bot: options.bot || 'bot123',
398
+ $channel: options.channelType ? { type: options.channelType, id: 'channel1' } : null,
399
+ $sender: {
400
+ id: options.senderId || 'user1',
401
+ permissions: options.senderPermissions || [],
402
+ },
403
+ $adapter: 'test',
404
+ };
405
+ }
406
+
407
+ describe('shouldTriggerAI', () => {
408
+ it('应该检测前缀触发', () => {
409
+ const message = createMockMessage({ content: '# 你好' });
410
+ const result = shouldTriggerAI(message as any, { prefixes: ['#'] });
411
+
412
+ expect(result.triggered).toBe(true);
413
+ expect(result.content).toBe('你好');
414
+ });
415
+
416
+ it('没有匹配前缀时不应触发', () => {
417
+ const message = createMockMessage({ content: '普通消息' });
418
+ const result = shouldTriggerAI(message as any, { prefixes: ['#'] });
419
+
420
+ expect(result.triggered).toBe(false);
421
+ });
422
+
423
+ it('私聊应该直接触发', () => {
424
+ const message = createMockMessage({ content: '你好', channelType: 'private' });
425
+ const result = shouldTriggerAI(message as any, { respondToPrivate: true });
426
+
427
+ expect(result.triggered).toBe(true);
428
+ expect(result.content).toBe('你好');
429
+ });
430
+
431
+ it('禁用时不应触发', () => {
432
+ const message = createMockMessage({ content: '# 你好' });
433
+ const result = shouldTriggerAI(message as any, { enabled: false, prefixes: ['#'] });
434
+
435
+ expect(result.triggered).toBe(false);
436
+ });
437
+ });
438
+
439
+ describe('inferSenderPermissions', () => {
440
+ it('应该正确推断 owner 权限', () => {
441
+ const message = createMockMessage({ content: 'test', senderId: 'owner1' });
442
+ const result = inferSenderPermissions(message as any, { owners: ['owner1'] });
443
+
444
+ expect(result.isOwner).toBe(true);
445
+ expect(result.permissionLevel).toBe('owner');
446
+ });
447
+
448
+ it('默认应该是 user 权限', () => {
449
+ const message = createMockMessage({ content: 'test' });
450
+ const result = inferSenderPermissions(message as any, {});
451
+
452
+ expect(result.permissionLevel).toBe('user');
453
+ });
454
+ });
455
+ });
456
+
457
+ // ============================================================================
458
+ // 内置工具测试
459
+ // ============================================================================
460
+
461
+ describe('内置工具完整测试', () => {
462
+ describe('计算器工具', () => {
463
+ const calculator = calculatorTool.toTool();
464
+
465
+ it('应该正确处理嵌套括号', async () => {
466
+ const result = await calculator.execute({ expression: '((2 + 3) * (4 - 1))' });
467
+ expect(result.result).toBe(15);
468
+ });
469
+
470
+ it('应该处理负数', async () => {
471
+ const result = await calculator.execute({ expression: '-5 + 3' });
472
+ expect(result.result).toBe(-2);
473
+ });
474
+
475
+ it('应该处理小数', async () => {
476
+ const result = await calculator.execute({ expression: '1.5 + 2.5' });
477
+ expect(result.result).toBe(4);
478
+ });
479
+
480
+ it('应该处理科学计数法', async () => {
481
+ const result = await calculator.execute({ expression: '1e2 + 50' });
482
+ expect(result.result).toBe(150);
483
+ });
484
+ });
485
+
486
+ describe('时间工具', () => {
487
+ const timeToolObj = timeTool.toTool();
488
+
489
+ it('应该返回 ISO 格式时间', async () => {
490
+ const result = await timeToolObj.execute({ format: 'timestamp' });
491
+ expect(result.iso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
492
+ });
493
+
494
+ it('时间戳应该是合理的', async () => {
495
+ const result = await timeToolObj.execute({});
496
+ const now = Date.now();
497
+ expect(Math.abs(result.timestamp - now)).toBeLessThan(1000);
498
+ });
499
+ });
500
+
501
+ describe('getBuiltinTools', () => {
502
+ it('应该返回内置工具列表', () => {
503
+ const tools = getBuiltinTools();
504
+ expect(Array.isArray(tools)).toBe(true);
505
+ expect(tools.length).toBeGreaterThan(0);
506
+
507
+ const names = tools.map(t => t.name);
508
+ expect(names).toContain('calculator');
509
+ expect(names).toContain('get_time');
510
+ });
511
+ });
512
+ });
513
+
514
+ // ============================================================================
515
+ // ZhinTool 完整流程测试
516
+ // ============================================================================
517
+
518
+ describe('ZhinTool 完整流程', () => {
519
+ it('应该支持完整的工具定义流程', async () => {
520
+ const tool = new ZhinTool('complete_tool')
521
+ .desc('完整测试工具')
522
+ .param('required_param', { type: 'string', description: '必填参数' }, true)
523
+ .param('optional_param', { type: 'number', description: '可选参数' }, false)
524
+ .platform('qq', 'telegram')
525
+ .scope('group', 'private')
526
+ .permission('user')
527
+ .tag('test', 'example')
528
+ .usage('这是使用说明')
529
+ .examples('/complete_tool arg1', '/complete_tool arg1 123')
530
+ .alias('ct')
531
+ .execute(async (args, ctx) => {
532
+ return {
533
+ received: args,
534
+ platform: ctx?.platform,
535
+ };
536
+ })
537
+ .action(async (message, result) => {
538
+ return `Command executed: ${result.params.required_param}`;
539
+ });
540
+
541
+ // 转换为 Tool
542
+ const toolObj = tool.toTool();
543
+
544
+ // 验证基本属性
545
+ expect(toolObj.name).toBe('complete_tool');
546
+ expect(toolObj.description).toBe('完整测试工具');
547
+ expect(toolObj.platforms).toEqual(['qq', 'telegram']);
548
+ expect(toolObj.scopes).toEqual(['group', 'private']);
549
+ expect(toolObj.tags).toContain('test');
550
+ expect(toolObj.tags).toContain('example');
551
+
552
+ // 验证参数
553
+ expect(toolObj.parameters.properties).toHaveProperty('required_param');
554
+ expect(toolObj.parameters.properties).toHaveProperty('optional_param');
555
+ expect(toolObj.parameters.required).toContain('required_param');
556
+
557
+ // 验证命令配置
558
+ expect(toolObj.command).not.toBe(false);
559
+ expect((toolObj.command as any).usage).toContain('这是使用说明');
560
+ expect((toolObj.command as any).examples).toContain('/complete_tool arg1');
561
+ expect((toolObj.command as any).alias).toContain('ct');
562
+
563
+ // 验证执行
564
+ const result = await toolObj.execute(
565
+ { required_param: 'test', optional_param: 42 },
566
+ { platform: 'qq' }
567
+ );
568
+
569
+ expect(result.received.required_param).toBe('test');
570
+ expect(result.received.optional_param).toBe(42);
571
+ expect(result.platform).toBe('qq');
572
+
573
+ // 验证 JSON 输出
574
+ const json = tool.toJSON();
575
+ expect(json.name).toBe('complete_tool');
576
+ expect(json).not.toHaveProperty('execute');
577
+
578
+ // 验证帮助信息
579
+ const help = tool.help;
580
+ expect(help).toContain('complete_tool');
581
+ expect(help).toContain('必填参数');
582
+ expect(help).toContain('可选参数');
583
+ });
584
+ });