@zhin.js/core 1.0.50 → 1.0.52
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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/lib/adapter.d.ts +10 -19
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +47 -80
- package/lib/adapter.js.map +1 -1
- package/lib/ai/types.d.ts +11 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/built/common-adapter-tools.d.ts +2 -2
- package/lib/built/common-adapter-tools.js +3 -3
- package/lib/built/common-adapter-tools.js.map +1 -1
- package/lib/built/dispatcher.d.ts +52 -42
- package/lib/built/dispatcher.d.ts.map +1 -1
- package/lib/built/dispatcher.js +194 -60
- package/lib/built/dispatcher.js.map +1 -1
- package/lib/built/login-assist.d.ts +61 -0
- package/lib/built/login-assist.d.ts.map +1 -0
- package/lib/built/login-assist.js +75 -0
- package/lib/built/login-assist.js.map +1 -0
- package/lib/built/skill.d.ts +3 -20
- package/lib/built/skill.d.ts.map +1 -1
- package/lib/built/skill.js +1 -53
- package/lib/built/skill.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +18 -0
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +1 -1
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +13 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.js +1 -1
- package/package.json +6 -6
- package/src/adapter.ts +51 -95
- package/src/ai/types.ts +3 -1
- package/src/built/common-adapter-tools.ts +3 -3
- package/src/built/dispatcher.ts +264 -99
- package/src/built/login-assist.ts +131 -0
- package/src/built/skill.ts +3 -75
- package/src/index.ts +2 -0
- package/src/plugin.ts +24 -5
- package/src/types.ts +16 -0
- package/src/utils.ts +1 -1
- package/tests/adapter.test.ts +40 -128
- package/tests/dispatcher.test.ts +155 -8
- package/tests/redos-protection.test.ts +5 -4
package/tests/dispatcher.test.ts
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* MessageDispatcher 测试
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
5
|
-
import {
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import {
|
|
7
|
+
createMessageDispatcher,
|
|
8
|
+
type MessageDispatcherService,
|
|
9
|
+
} from '../src/built/dispatcher.js';
|
|
6
10
|
import type { Message } from '../src/message.js';
|
|
11
|
+
import type { Plugin } from '../src/plugin.js';
|
|
12
|
+
import type { BeforeSendHandler } from '../src/types.js';
|
|
7
13
|
|
|
8
14
|
function makeMessage(text: string, overrides: Partial<Message<any>> = {}): Message<any> {
|
|
9
15
|
return {
|
|
@@ -21,6 +27,42 @@ function makeMessage(text: string, overrides: Partial<Message<any>> = {}): Messa
|
|
|
21
27
|
} as any;
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
function makeRootWithCommand(
|
|
31
|
+
handleImpl?: (msg: Message<any>, root: Plugin) => Promise<string | void>,
|
|
32
|
+
) {
|
|
33
|
+
const handle = handleImpl ?? vi.fn(async () => 'cmd-ok');
|
|
34
|
+
const cmdService = {
|
|
35
|
+
items: [{ pattern: '/help ping' }],
|
|
36
|
+
handle,
|
|
37
|
+
};
|
|
38
|
+
const root = new EventEmitter() as unknown as Plugin;
|
|
39
|
+
(root as any).inject = (name: string) => {
|
|
40
|
+
if (name === 'command') return cmdService;
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
(root as any).root = root;
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 模拟 $reply → adapter.sendMessage → renderSendMessage(before.sendMessage) */
|
|
48
|
+
function wireMessageReplyThroughBeforeSend(msg: Message<any>, root: EventEmitter) {
|
|
49
|
+
msg.$reply = vi.fn(async (content: any) => {
|
|
50
|
+
let options = {
|
|
51
|
+
content,
|
|
52
|
+
bot: msg.$bot,
|
|
53
|
+
id: msg.$channel.id,
|
|
54
|
+
type: msg.$channel.type,
|
|
55
|
+
context: 'test',
|
|
56
|
+
};
|
|
57
|
+
const fns = root.listeners('before.sendMessage') as BeforeSendHandler[];
|
|
58
|
+
for (const fn of fns) {
|
|
59
|
+
const r = await fn(options);
|
|
60
|
+
if (r) options = r;
|
|
61
|
+
}
|
|
62
|
+
return 'mock-msg-id';
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
describe('createMessageDispatcher', () => {
|
|
25
67
|
let context: ReturnType<typeof createMessageDispatcher>;
|
|
26
68
|
let service: MessageDispatcherService;
|
|
@@ -40,6 +82,10 @@ describe('createMessageDispatcher', () => {
|
|
|
40
82
|
expect(typeof context.value.setAITriggerMatcher).toBe('function');
|
|
41
83
|
expect(typeof context.value.setAIHandler).toBe('function');
|
|
42
84
|
expect(typeof context.value.hasAIHandler).toBe('function');
|
|
85
|
+
expect(typeof context.value.addOutboundPolish).toBe('function');
|
|
86
|
+
expect(typeof context.value.replyWithPolish).toBe('function');
|
|
87
|
+
expect(typeof service.matchCommand).toBe('function');
|
|
88
|
+
expect(typeof service.matchAI).toBe('function');
|
|
43
89
|
});
|
|
44
90
|
|
|
45
91
|
describe('Guardrail', () => {
|
|
@@ -98,7 +144,7 @@ describe('createMessageDispatcher', () => {
|
|
|
98
144
|
it('AI 触发时应调用 handler', async () => {
|
|
99
145
|
const handler = vi.fn();
|
|
100
146
|
service.setAIHandler(handler);
|
|
101
|
-
service.setAITriggerMatcher((
|
|
147
|
+
service.setAITriggerMatcher(() => ({
|
|
102
148
|
triggered: true,
|
|
103
149
|
content: 'processed content',
|
|
104
150
|
}));
|
|
@@ -121,20 +167,118 @@ describe('createMessageDispatcher', () => {
|
|
|
121
167
|
});
|
|
122
168
|
});
|
|
123
169
|
|
|
124
|
-
describe('Command Matcher', () => {
|
|
125
|
-
it('
|
|
170
|
+
describe('Command Matcher (exclusive)', () => {
|
|
171
|
+
it('自定义命令匹配器应优先并阻断 AI(互斥模式)', async () => {
|
|
172
|
+
const exclusiveCtx = createMessageDispatcher({ dualRoute: { mode: 'exclusive' } });
|
|
173
|
+
const d = exclusiveCtx.value;
|
|
174
|
+
const aiHandler = vi.fn();
|
|
175
|
+
d.setAIHandler(aiHandler);
|
|
176
|
+
d.setAITriggerMatcher(() => ({ triggered: true, content: '' }));
|
|
177
|
+
d.setCommandMatcher((text) => text.startsWith('/'));
|
|
178
|
+
|
|
179
|
+
const msg = makeMessage('/help');
|
|
180
|
+
await d.dispatch(msg);
|
|
181
|
+
|
|
182
|
+
expect(aiHandler).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('双轨 dual 模式', () => {
|
|
187
|
+
it('同时命中指令与 AI 时应各执行一次(默认 command-first)', async () => {
|
|
188
|
+
const root = makeRootWithCommand();
|
|
189
|
+
const fakePlugin = { root } as Plugin;
|
|
190
|
+
context.mounted(fakePlugin);
|
|
191
|
+
|
|
126
192
|
const aiHandler = vi.fn();
|
|
127
193
|
service.setAIHandler(aiHandler);
|
|
128
|
-
service.setAITriggerMatcher(() => ({ triggered: true, content: '' }));
|
|
194
|
+
service.setAITriggerMatcher(() => ({ triggered: true, content: 'hi' }));
|
|
195
|
+
service.setDualRouteConfig({ mode: 'dual', order: 'command-first', allowDualReply: true });
|
|
196
|
+
|
|
197
|
+
const msg = makeMessage('/help');
|
|
198
|
+
wireMessageReplyThroughBeforeSend(msg, root as unknown as EventEmitter);
|
|
199
|
+
await service.dispatch(msg);
|
|
129
200
|
|
|
130
|
-
|
|
131
|
-
|
|
201
|
+
const cmd = root.inject('command') as any;
|
|
202
|
+
expect(cmd.handle).toHaveBeenCalled();
|
|
203
|
+
expect(aiHandler).toHaveBeenCalledWith(msg, 'hi');
|
|
204
|
+
expect(msg.$reply).toHaveBeenCalledWith('cmd-ok');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('ai-first 时应先 AI 后指令', async () => {
|
|
208
|
+
const order: string[] = [];
|
|
209
|
+
const root = makeRootWithCommand(async () => {
|
|
210
|
+
order.push('cmd');
|
|
211
|
+
return 'c';
|
|
212
|
+
});
|
|
213
|
+
const fakePlugin = { root } as Plugin;
|
|
214
|
+
context.mounted(fakePlugin);
|
|
215
|
+
|
|
216
|
+
service.setAIHandler(async () => {
|
|
217
|
+
order.push('ai');
|
|
218
|
+
});
|
|
219
|
+
service.setAITriggerMatcher(() => ({ triggered: true, content: 'x' }));
|
|
220
|
+
service.setDualRouteConfig({ mode: 'dual', order: 'ai-first', allowDualReply: true });
|
|
132
221
|
|
|
133
222
|
const msg = makeMessage('/help');
|
|
223
|
+
wireMessageReplyThroughBeforeSend(msg, root as unknown as EventEmitter);
|
|
224
|
+
await service.dispatch(msg);
|
|
225
|
+
|
|
226
|
+
expect(order).toEqual(['ai', 'cmd']);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('allowDualReply false 且 command-first 时仅执行指令', async () => {
|
|
230
|
+
const root = makeRootWithCommand();
|
|
231
|
+
const fakePlugin = { root } as Plugin;
|
|
232
|
+
context.mounted(fakePlugin);
|
|
233
|
+
|
|
234
|
+
const aiHandler = vi.fn();
|
|
235
|
+
service.setAIHandler(aiHandler);
|
|
236
|
+
service.setAITriggerMatcher(() => ({ triggered: true, content: 'x' }));
|
|
237
|
+
service.setDualRouteConfig({ mode: 'dual', order: 'command-first', allowDualReply: false });
|
|
238
|
+
|
|
239
|
+
const msg = makeMessage('/help');
|
|
240
|
+
wireMessageReplyThroughBeforeSend(msg, root as unknown as EventEmitter);
|
|
134
241
|
await service.dispatch(msg);
|
|
135
242
|
|
|
136
|
-
// 命令路径不应触发 AI
|
|
137
243
|
expect(aiHandler).not.toHaveBeenCalled();
|
|
244
|
+
expect(msg.$reply).toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('出站润色', () => {
|
|
249
|
+
it('replyWithPolish 应经 before.sendMessage 链式润色再发出($reply 入参仍为原文)', async () => {
|
|
250
|
+
const root = new EventEmitter() as unknown as Plugin;
|
|
251
|
+
(root as any).inject = () => undefined;
|
|
252
|
+
(root as any).root = root;
|
|
253
|
+
context.mounted({ root } as Plugin);
|
|
254
|
+
|
|
255
|
+
const p1 = vi.fn(async (ctx: any) => `[1]${ctx.content}`);
|
|
256
|
+
const p2 = vi.fn(async (ctx: any) => `${ctx.content}[2]`);
|
|
257
|
+
service.addOutboundPolish(p1);
|
|
258
|
+
service.addOutboundPolish(p2);
|
|
259
|
+
|
|
260
|
+
const msg = makeMessage('x');
|
|
261
|
+
wireMessageReplyThroughBeforeSend(msg, root as unknown as EventEmitter);
|
|
262
|
+
await service.replyWithPolish(msg, 'ai', 'hello');
|
|
263
|
+
|
|
264
|
+
expect(p1).toHaveBeenCalled();
|
|
265
|
+
expect(p2).toHaveBeenCalled();
|
|
266
|
+
expect(msg.$reply).toHaveBeenCalledWith('hello');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('指令路径应经过润色', async () => {
|
|
270
|
+
const root = makeRootWithCommand(async () => 'out');
|
|
271
|
+
const fakePlugin = { root } as Plugin;
|
|
272
|
+
context.mounted(fakePlugin);
|
|
273
|
+
|
|
274
|
+
service.addOutboundPolish(async (ctx) => `<<${ctx.content}>>`);
|
|
275
|
+
service.setDualRouteConfig({ mode: 'exclusive' });
|
|
276
|
+
|
|
277
|
+
const msg = makeMessage('/help');
|
|
278
|
+
wireMessageReplyThroughBeforeSend(msg, root as unknown as EventEmitter);
|
|
279
|
+
await service.dispatch(msg);
|
|
280
|
+
|
|
281
|
+
expect(msg.$reply).toHaveBeenCalledWith('out');
|
|
138
282
|
});
|
|
139
283
|
});
|
|
140
284
|
|
|
@@ -142,5 +286,8 @@ describe('createMessageDispatcher', () => {
|
|
|
142
286
|
it('应提供 addGuardrail 扩展', () => {
|
|
143
287
|
expect(typeof context.extensions.addGuardrail).toBe('function');
|
|
144
288
|
});
|
|
289
|
+
it('应提供 addOutboundPolish 扩展', () => {
|
|
290
|
+
expect(typeof context.extensions.addOutboundPolish).toBe('function');
|
|
291
|
+
});
|
|
145
292
|
});
|
|
146
293
|
});
|
|
@@ -72,12 +72,13 @@ describe('ReDoS Protection Tests', () => {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
it('should reject extremely large templates', () => {
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
75
|
+
// segment.from 在 utils 中限制单段模板长度 > 400_000(见 MAX_TEMPLATE_LENGTH)
|
|
76
|
+
const chunk = '<tag>content</tag>';
|
|
77
|
+
const hugeTemplate = chunk.repeat(Math.ceil(400_001 / chunk.length));
|
|
78
|
+
|
|
78
79
|
expect(() => {
|
|
79
80
|
segment.from(hugeTemplate);
|
|
80
|
-
}).toThrow(
|
|
81
|
+
}).toThrow(/Template too large/);
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
it('should handle nested tags without exponential backtracking', () => {
|