@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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Trigger 工具函数测试
|
|
3
|
+
*
|
|
4
|
+
* 测试内容:
|
|
5
|
+
* 1. 前缀触发检测
|
|
6
|
+
* 2. @机器人触发检测
|
|
7
|
+
* 3. 私聊直接对话检测
|
|
8
|
+
* 4. 关键词触发检测
|
|
9
|
+
* 5. 忽略前缀检测
|
|
10
|
+
* 6. 权限推断
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
shouldTriggerAI,
|
|
15
|
+
mergeAITriggerConfig,
|
|
16
|
+
inferSenderPermissions,
|
|
17
|
+
parseRichMediaContent,
|
|
18
|
+
extractTextContent,
|
|
19
|
+
DEFAULT_AI_TRIGGER_CONFIG,
|
|
20
|
+
type AITriggerConfig,
|
|
21
|
+
} from '@zhin.js/core';
|
|
22
|
+
|
|
23
|
+
// 创建模拟消息
|
|
24
|
+
function createMockMessage(options: {
|
|
25
|
+
content: string | any[];
|
|
26
|
+
bot?: string;
|
|
27
|
+
channelType?: 'private' | 'group' | 'channel';
|
|
28
|
+
senderId?: string;
|
|
29
|
+
senderPermissions?: string[];
|
|
30
|
+
senderRole?: string;
|
|
31
|
+
}) {
|
|
32
|
+
const content = typeof options.content === 'string'
|
|
33
|
+
? [{ type: 'text', data: { text: options.content } }]
|
|
34
|
+
: options.content;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
$content: content,
|
|
38
|
+
$bot: options.bot || 'bot123',
|
|
39
|
+
$channel: options.channelType ? { type: options.channelType, id: 'channel1' } : null,
|
|
40
|
+
$sender: {
|
|
41
|
+
id: options.senderId || 'user1',
|
|
42
|
+
permissions: options.senderPermissions || [],
|
|
43
|
+
role: options.senderRole,
|
|
44
|
+
},
|
|
45
|
+
$adapter: 'test',
|
|
46
|
+
$reply: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('AI Trigger 工具函数', () => {
|
|
51
|
+
describe('shouldTriggerAI - 前缀触发', () => {
|
|
52
|
+
it('应该检测 # 前缀', () => {
|
|
53
|
+
const message = createMockMessage({ content: '# 你好' });
|
|
54
|
+
const result = shouldTriggerAI(message as any, { prefixes: ['#'] });
|
|
55
|
+
|
|
56
|
+
expect(result.triggered).toBe(true);
|
|
57
|
+
expect(result.content).toBe('你好');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('应该检测 AI: 前缀', () => {
|
|
61
|
+
const message = createMockMessage({ content: 'AI:帮我计算' });
|
|
62
|
+
const result = shouldTriggerAI(message as any, { prefixes: ['AI:', 'ai:'] });
|
|
63
|
+
|
|
64
|
+
expect(result.triggered).toBe(true);
|
|
65
|
+
expect(result.content).toBe('帮我计算');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('应该检测小写 ai: 前缀', () => {
|
|
69
|
+
const message = createMockMessage({ content: 'ai:今天天气' });
|
|
70
|
+
const result = shouldTriggerAI(message as any, { prefixes: ['AI:', 'ai:'] });
|
|
71
|
+
|
|
72
|
+
expect(result.triggered).toBe(true);
|
|
73
|
+
expect(result.content).toBe('今天天气');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('没有匹配前缀时不应触发', () => {
|
|
77
|
+
const message = createMockMessage({ content: '普通消息' });
|
|
78
|
+
const result = shouldTriggerAI(message as any, { prefixes: ['#'] });
|
|
79
|
+
|
|
80
|
+
expect(result.triggered).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('shouldTriggerAI - 忽略前缀', () => {
|
|
85
|
+
it('应该忽略命令前缀 /', () => {
|
|
86
|
+
const message = createMockMessage({
|
|
87
|
+
content: '/help',
|
|
88
|
+
channelType: 'private',
|
|
89
|
+
});
|
|
90
|
+
const result = shouldTriggerAI(message as any, {
|
|
91
|
+
prefixes: ['#'],
|
|
92
|
+
ignorePrefixes: ['/'],
|
|
93
|
+
respondToPrivate: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.triggered).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('应该忽略命令前缀 !', () => {
|
|
100
|
+
const message = createMockMessage({
|
|
101
|
+
content: '!command',
|
|
102
|
+
channelType: 'private',
|
|
103
|
+
});
|
|
104
|
+
const result = shouldTriggerAI(message as any, {
|
|
105
|
+
prefixes: ['#'],
|
|
106
|
+
ignorePrefixes: ['!', '!'],
|
|
107
|
+
respondToPrivate: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result.triggered).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('shouldTriggerAI - @机器人触发', () => {
|
|
115
|
+
it('应该检测 @机器人', () => {
|
|
116
|
+
const message = createMockMessage({
|
|
117
|
+
content: [
|
|
118
|
+
{ type: 'at', data: { user_id: 'bot123' } },
|
|
119
|
+
{ type: 'text', data: { text: ' 你好呀' } },
|
|
120
|
+
],
|
|
121
|
+
bot: 'bot123',
|
|
122
|
+
});
|
|
123
|
+
const result = shouldTriggerAI(message as any, { respondToAt: true });
|
|
124
|
+
|
|
125
|
+
expect(result.triggered).toBe(true);
|
|
126
|
+
expect(result.content).toBe(' 你好呀');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('关闭 respondToAt 时不应触发', () => {
|
|
130
|
+
const message = createMockMessage({
|
|
131
|
+
content: [
|
|
132
|
+
{ type: 'at', data: { user_id: 'bot123' } },
|
|
133
|
+
{ type: 'text', data: { text: ' 你好' } },
|
|
134
|
+
],
|
|
135
|
+
bot: 'bot123',
|
|
136
|
+
});
|
|
137
|
+
const result = shouldTriggerAI(message as any, { respondToAt: false });
|
|
138
|
+
|
|
139
|
+
expect(result.triggered).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('@其他人时不应触发', () => {
|
|
143
|
+
const message = createMockMessage({
|
|
144
|
+
content: [
|
|
145
|
+
{ type: 'at', data: { user_id: 'other_user' } },
|
|
146
|
+
{ type: 'text', data: { text: ' 你好' } },
|
|
147
|
+
],
|
|
148
|
+
bot: 'bot123',
|
|
149
|
+
});
|
|
150
|
+
const result = shouldTriggerAI(message as any, { respondToAt: true });
|
|
151
|
+
|
|
152
|
+
expect(result.triggered).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('shouldTriggerAI - 私聊直接对话', () => {
|
|
157
|
+
it('私聊应该直接触发', () => {
|
|
158
|
+
const message = createMockMessage({
|
|
159
|
+
content: '你好',
|
|
160
|
+
channelType: 'private',
|
|
161
|
+
});
|
|
162
|
+
const result = shouldTriggerAI(message as any, { respondToPrivate: true });
|
|
163
|
+
|
|
164
|
+
expect(result.triggered).toBe(true);
|
|
165
|
+
expect(result.content).toBe('你好');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('关闭 respondToPrivate 时私聊不应触发', () => {
|
|
169
|
+
const message = createMockMessage({
|
|
170
|
+
content: '你好',
|
|
171
|
+
channelType: 'private',
|
|
172
|
+
});
|
|
173
|
+
const result = shouldTriggerAI(message as any, { respondToPrivate: false });
|
|
174
|
+
|
|
175
|
+
expect(result.triggered).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('群聊时不应直接触发(需要前缀或@)', () => {
|
|
179
|
+
const message = createMockMessage({
|
|
180
|
+
content: '你好',
|
|
181
|
+
channelType: 'group',
|
|
182
|
+
});
|
|
183
|
+
const result = shouldTriggerAI(message as any, { respondToPrivate: true });
|
|
184
|
+
|
|
185
|
+
expect(result.triggered).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('shouldTriggerAI - 关键词触发', () => {
|
|
190
|
+
it('应该检测关键词', () => {
|
|
191
|
+
const message = createMockMessage({ content: '今天天气怎么样' });
|
|
192
|
+
const result = shouldTriggerAI(message as any, { keywords: ['天气', '新闻'] });
|
|
193
|
+
|
|
194
|
+
expect(result.triggered).toBe(true);
|
|
195
|
+
expect(result.content).toBe('今天天气怎么样');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('关键词不区分大小写', () => {
|
|
199
|
+
const message = createMockMessage({ content: '说 hello 世界' });
|
|
200
|
+
const result = shouldTriggerAI(message as any, { keywords: ['HELLO'] });
|
|
201
|
+
|
|
202
|
+
expect(result.triggered).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('没有关键词配置时不应触发', () => {
|
|
206
|
+
const message = createMockMessage({ content: '天气真好' });
|
|
207
|
+
const result = shouldTriggerAI(message as any, { keywords: [] });
|
|
208
|
+
|
|
209
|
+
expect(result.triggered).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('shouldTriggerAI - 禁用状态', () => {
|
|
214
|
+
it('enabled 为 false 时不应触发', () => {
|
|
215
|
+
const message = createMockMessage({ content: '# 你好' });
|
|
216
|
+
const result = shouldTriggerAI(message as any, {
|
|
217
|
+
enabled: false,
|
|
218
|
+
prefixes: ['#'],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result.triggered).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('触发优先级', () => {
|
|
226
|
+
it('前缀触发应该优先于私聊触发', () => {
|
|
227
|
+
const message = createMockMessage({
|
|
228
|
+
content: '# 命令内容',
|
|
229
|
+
channelType: 'private',
|
|
230
|
+
});
|
|
231
|
+
const result = shouldTriggerAI(message as any, {
|
|
232
|
+
prefixes: ['#'],
|
|
233
|
+
respondToPrivate: true,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.triggered).toBe(true);
|
|
237
|
+
expect(result.content).toBe('命令内容'); // 应该去掉前缀
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('边界情况', () => {
|
|
242
|
+
it('空消息不应触发', () => {
|
|
243
|
+
const message = createMockMessage({
|
|
244
|
+
content: '',
|
|
245
|
+
channelType: 'private',
|
|
246
|
+
});
|
|
247
|
+
const result = shouldTriggerAI(message as any, { respondToPrivate: true });
|
|
248
|
+
|
|
249
|
+
expect(result.triggered).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('只有空格的消息不应触发', () => {
|
|
253
|
+
const message = createMockMessage({
|
|
254
|
+
content: ' ',
|
|
255
|
+
channelType: 'private',
|
|
256
|
+
});
|
|
257
|
+
const result = shouldTriggerAI(message as any, { respondToPrivate: true });
|
|
258
|
+
|
|
259
|
+
expect(result.triggered).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('前缀后没有内容时应该触发但内容为空', () => {
|
|
263
|
+
const message = createMockMessage({ content: '#' });
|
|
264
|
+
const result = shouldTriggerAI(message as any, { prefixes: ['#'] });
|
|
265
|
+
|
|
266
|
+
expect(result.triggered).toBe(true);
|
|
267
|
+
expect(result.content).toBe('');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('mergeAITriggerConfig', () => {
|
|
272
|
+
it('应该合并配置', () => {
|
|
273
|
+
const config = mergeAITriggerConfig({ prefixes: ['##'] });
|
|
274
|
+
|
|
275
|
+
expect(config.prefixes).toEqual(['##']);
|
|
276
|
+
expect(config.enabled).toBe(true);
|
|
277
|
+
expect(config.respondToAt).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('应该使用默认值', () => {
|
|
281
|
+
const config = mergeAITriggerConfig({});
|
|
282
|
+
|
|
283
|
+
expect(config).toEqual(DEFAULT_AI_TRIGGER_CONFIG);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('inferSenderPermissions', () => {
|
|
288
|
+
it('应该推断 owner 权限', () => {
|
|
289
|
+
const message = createMockMessage({
|
|
290
|
+
content: 'test',
|
|
291
|
+
senderId: 'owner1',
|
|
292
|
+
});
|
|
293
|
+
const result = inferSenderPermissions(message as any, { owners: ['owner1'] });
|
|
294
|
+
|
|
295
|
+
expect(result.isOwner).toBe(true);
|
|
296
|
+
expect(result.permissionLevel).toBe('owner');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('应该推断 bot_admin 权限', () => {
|
|
300
|
+
const message = createMockMessage({
|
|
301
|
+
content: 'test',
|
|
302
|
+
senderId: 'admin1',
|
|
303
|
+
});
|
|
304
|
+
const result = inferSenderPermissions(message as any, { botAdmins: ['admin1'] });
|
|
305
|
+
|
|
306
|
+
expect(result.isBotAdmin).toBe(true);
|
|
307
|
+
expect(result.permissionLevel).toBe('bot_admin');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('应该推断 group_owner 权限', () => {
|
|
311
|
+
const message = createMockMessage({
|
|
312
|
+
content: 'test',
|
|
313
|
+
senderPermissions: ['owner'],
|
|
314
|
+
});
|
|
315
|
+
const result = inferSenderPermissions(message as any, {});
|
|
316
|
+
|
|
317
|
+
expect(result.isGroupOwner).toBe(true);
|
|
318
|
+
expect(result.permissionLevel).toBe('group_owner');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('应该推断 group_admin 权限', () => {
|
|
322
|
+
const message = createMockMessage({
|
|
323
|
+
content: 'test',
|
|
324
|
+
senderPermissions: ['admin'],
|
|
325
|
+
});
|
|
326
|
+
const result = inferSenderPermissions(message as any, {});
|
|
327
|
+
|
|
328
|
+
expect(result.isGroupAdmin).toBe(true);
|
|
329
|
+
expect(result.permissionLevel).toBe('group_admin');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('默认应该是 user 权限', () => {
|
|
333
|
+
const message = createMockMessage({ content: 'test' });
|
|
334
|
+
const result = inferSenderPermissions(message as any, {});
|
|
335
|
+
|
|
336
|
+
expect(result.permissionLevel).toBe('user');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('应该推断 scope', () => {
|
|
340
|
+
const privateMsg = createMockMessage({ content: 'test', channelType: 'private' });
|
|
341
|
+
const groupMsg = createMockMessage({ content: 'test', channelType: 'group' });
|
|
342
|
+
|
|
343
|
+
expect(inferSenderPermissions(privateMsg as any, {}).scope).toBe('private');
|
|
344
|
+
expect(inferSenderPermissions(groupMsg as any, {}).scope).toBe('group');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('parseRichMediaContent', () => {
|
|
349
|
+
it('应该解析纯文本', () => {
|
|
350
|
+
const result = parseRichMediaContent('Hello World');
|
|
351
|
+
|
|
352
|
+
expect(result.length).toBeGreaterThan(0);
|
|
353
|
+
const textElement = result.find(el => el.type === 'text');
|
|
354
|
+
expect(textElement?.data?.text).toContain('Hello');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('应该处理 XML 标签', () => {
|
|
358
|
+
const result = parseRichMediaContent('文本<image url="test.jpg"/>');
|
|
359
|
+
|
|
360
|
+
expect(result.length).toBeGreaterThan(0);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('应该处理空字符串', () => {
|
|
364
|
+
const result = parseRichMediaContent('');
|
|
365
|
+
|
|
366
|
+
expect(Array.isArray(result)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|