@zhin.js/core 1.0.51 → 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 +12 -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/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/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/plugin.d.ts +2 -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/built/common-adapter-tools.ts +3 -3
- package/src/built/dispatcher.ts +264 -99
- package/src/built/skill.ts +3 -75
- package/src/plugin.ts +11 -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/src/types.ts
CHANGED
|
@@ -64,6 +64,22 @@ export type MaybeArray<T>=T|T[]
|
|
|
64
64
|
* 消息发送内容类型
|
|
65
65
|
*/
|
|
66
66
|
export type SendContent=MaybeArray<string|MessageElement>
|
|
67
|
+
|
|
68
|
+
/** 出站回复来源(指令 / AI),仅当经 MessageDispatcher.replyWithPolish 发出时由框架填入异步上下文 */
|
|
69
|
+
export type OutboundReplySource = 'command' | 'ai'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 出站润色上下文(`dispatcher.addOutboundPolish` 的 handler 签名的同构类型)。
|
|
73
|
+
* 与 {@link Adapter.sendMessage} → `before.sendMessage` 同一管道;需 `message`/`source` 时见 `getOutboundReplyStore`(dispatcher 导出)。
|
|
74
|
+
*/
|
|
75
|
+
export interface OutboundPolishContext {
|
|
76
|
+
message: Message
|
|
77
|
+
content: SendContent
|
|
78
|
+
source: OutboundReplySource
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 返回 `SendContent` 则替换后续 `before.sendMessage` 与发送中的 content */
|
|
82
|
+
export type OutboundPolishMiddleware = (ctx: OutboundPolishContext) => MaybePromise<SendContent | void>
|
|
67
83
|
/**
|
|
68
84
|
* 消息发送者信息
|
|
69
85
|
*/
|
package/src/utils.ts
CHANGED
|
@@ -107,7 +107,7 @@ export namespace segment {
|
|
|
107
107
|
const toString = (template: string | MessageElement) => {
|
|
108
108
|
if (typeof template !== "string") return [template];
|
|
109
109
|
|
|
110
|
-
const MAX_TEMPLATE_LENGTH =
|
|
110
|
+
const MAX_TEMPLATE_LENGTH = 400000;
|
|
111
111
|
if (template.length > MAX_TEMPLATE_LENGTH) {
|
|
112
112
|
throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
|
|
113
113
|
}
|
package/tests/adapter.test.ts
CHANGED
|
@@ -107,9 +107,8 @@ describe('Adapter Core Functionality', () => {
|
|
|
107
107
|
expect(adapter).toBeInstanceOf(EventEmitter)
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it('should
|
|
111
|
-
|
|
112
|
-
expect(listeners.length).toBeGreaterThan(0)
|
|
110
|
+
it('should route message.receive via emit without default listener', () => {
|
|
111
|
+
expect(adapter.listenerCount('message.receive')).toBe(0)
|
|
113
112
|
})
|
|
114
113
|
|
|
115
114
|
it('should register call.recallMessage listener', () => {
|
|
@@ -199,13 +198,15 @@ describe('Adapter Core Functionality', () => {
|
|
|
199
198
|
|
|
200
199
|
it('should remove all event listeners', async () => {
|
|
201
200
|
await adapter.start()
|
|
201
|
+
const noop = () => {}
|
|
202
|
+
adapter.on('message.receive', noop)
|
|
202
203
|
const beforeCount = adapter.listenerCount('message.receive')
|
|
203
|
-
|
|
204
|
+
expect(beforeCount).toBe(1)
|
|
205
|
+
|
|
204
206
|
await adapter.stop()
|
|
205
207
|
const afterCount = adapter.listenerCount('message.receive')
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
expect(afterCount).toBe(0)
|
|
208
|
-
expect(beforeCount).toBeGreaterThan(0)
|
|
209
210
|
})
|
|
210
211
|
|
|
211
212
|
it('should handle bot disconnect errors gracefully', async () => {
|
|
@@ -377,97 +378,92 @@ describe('Adapter Core Functionality', () => {
|
|
|
377
378
|
})
|
|
378
379
|
|
|
379
380
|
describe('message.receive', () => {
|
|
380
|
-
it('should
|
|
381
|
+
it('should still dispatch plugin message.receive when dispatcher is missing (no middleware fallback)', async () => {
|
|
381
382
|
const config = [{ id: 'bot1' }]
|
|
382
383
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
383
384
|
await adapter.start()
|
|
384
|
-
|
|
385
|
+
|
|
385
386
|
let middlewareCalled = false
|
|
386
|
-
plugin.addMiddleware(async (
|
|
387
|
+
plugin.addMiddleware(async (_message, next) => {
|
|
387
388
|
middlewareCalled = true
|
|
388
389
|
await next()
|
|
389
390
|
})
|
|
390
|
-
|
|
391
|
+
|
|
392
|
+
let lifecycleCalled = false
|
|
393
|
+
plugin.on('message.receive', () => {
|
|
394
|
+
lifecycleCalled = true
|
|
395
|
+
})
|
|
396
|
+
|
|
391
397
|
const message = {
|
|
392
398
|
$bot: 'bot1',
|
|
393
399
|
$adapter: 'test',
|
|
394
400
|
$channel: { id: 'channel-id', type: 'text' },
|
|
395
|
-
$content: 'Hello'
|
|
401
|
+
$content: 'Hello',
|
|
396
402
|
} as any
|
|
397
|
-
|
|
403
|
+
|
|
398
404
|
adapter.emit('message.receive', message)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
expect(middlewareCalled).toBe(true)
|
|
405
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
406
|
+
expect(middlewareCalled).toBe(false)
|
|
407
|
+
expect(lifecycleCalled).toBe(true)
|
|
403
408
|
})
|
|
404
409
|
|
|
405
|
-
it('should
|
|
410
|
+
it('should await MessageDispatcher then plugin lifecycle', async () => {
|
|
406
411
|
const config = [{ id: 'bot1' }]
|
|
407
412
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
408
413
|
await adapter.start()
|
|
409
414
|
|
|
410
|
-
|
|
411
|
-
let middlewareCalled = false
|
|
412
|
-
|
|
413
|
-
// 注册 dispatcher context
|
|
415
|
+
const order: string[] = []
|
|
414
416
|
plugin.$contexts.set('dispatcher', {
|
|
415
417
|
name: 'dispatcher',
|
|
416
418
|
description: 'mock dispatcher',
|
|
417
419
|
value: {
|
|
418
|
-
dispatch: (
|
|
420
|
+
dispatch: async (_msg: any) => {
|
|
421
|
+
order.push('dispatcher')
|
|
422
|
+
},
|
|
419
423
|
},
|
|
420
424
|
} as any)
|
|
421
425
|
|
|
422
|
-
plugin.
|
|
423
|
-
|
|
424
|
-
await next()
|
|
426
|
+
plugin.on('message.receive', () => {
|
|
427
|
+
order.push('lifecycle')
|
|
425
428
|
})
|
|
426
429
|
|
|
427
430
|
const message = {
|
|
428
431
|
$bot: 'bot1',
|
|
429
432
|
$adapter: 'test',
|
|
430
433
|
$channel: { id: 'channel-id', type: 'text' },
|
|
431
|
-
$content: 'Hello'
|
|
434
|
+
$content: 'Hello',
|
|
432
435
|
} as any
|
|
433
436
|
|
|
434
437
|
adapter.emit('message.receive', message)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
expect(dispatchCalled).toBe(true)
|
|
438
|
-
expect(middlewareCalled).toBe(false)
|
|
438
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
439
|
+
expect(order).toEqual(['dispatcher', 'lifecycle'])
|
|
439
440
|
})
|
|
440
441
|
|
|
441
|
-
it('should
|
|
442
|
+
it('should call adapter.on observers after plugin lifecycle', async () => {
|
|
442
443
|
const config = [{ id: 'bot1' }]
|
|
443
444
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
444
445
|
await adapter.start()
|
|
445
446
|
|
|
446
|
-
let middlewareCalled = false
|
|
447
|
-
|
|
448
|
-
// 注册一个没有 dispatch 方法的 dispatcher
|
|
449
447
|
plugin.$contexts.set('dispatcher', {
|
|
450
448
|
name: 'dispatcher',
|
|
451
|
-
description: '
|
|
452
|
-
value: {
|
|
449
|
+
description: 'mock dispatcher',
|
|
450
|
+
value: { dispatch: async () => {} },
|
|
453
451
|
} as any)
|
|
454
452
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
})
|
|
453
|
+
const order: string[] = []
|
|
454
|
+
plugin.on('message.receive', () => order.push('lifecycle'))
|
|
455
|
+
adapter.on('message.receive', () => order.push('adapterObserver'))
|
|
459
456
|
|
|
460
457
|
const message = {
|
|
461
458
|
$bot: 'bot1',
|
|
462
459
|
$adapter: 'test',
|
|
463
460
|
$channel: { id: 'channel-id', type: 'text' },
|
|
464
|
-
$content: 'Hello'
|
|
461
|
+
$content: 'Hello',
|
|
465
462
|
} as any
|
|
466
463
|
|
|
467
464
|
adapter.emit('message.receive', message)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
expect(middlewareCalled).toBe(true)
|
|
465
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
466
|
+
expect(order).toEqual(['lifecycle', 'adapterObserver'])
|
|
471
467
|
})
|
|
472
468
|
})
|
|
473
469
|
})
|
|
@@ -617,90 +613,6 @@ describe('Adapter Core Functionality', () => {
|
|
|
617
613
|
})
|
|
618
614
|
})
|
|
619
615
|
|
|
620
|
-
describe('Adapter declareSkill', () => {
|
|
621
|
-
it('should register a skill when SkillFeature is available', () => {
|
|
622
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
623
|
-
|
|
624
|
-
// 模拟 SkillFeature
|
|
625
|
-
const mockSkillFeature = {
|
|
626
|
-
add: vi.fn(() => vi.fn()),
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// 设置 root plugin 和 inject
|
|
630
|
-
;(plugin as any)._root = plugin
|
|
631
|
-
const originalInject = plugin.inject.bind(plugin)
|
|
632
|
-
plugin.inject = ((name: string) => {
|
|
633
|
-
if (name === 'skill') return mockSkillFeature
|
|
634
|
-
return originalInject(name)
|
|
635
|
-
}) as any
|
|
636
|
-
;(plugin as any).recordFeatureContribution = vi.fn()
|
|
637
|
-
|
|
638
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
639
|
-
|
|
640
|
-
// 添加一个工具
|
|
641
|
-
adapter.addTool({
|
|
642
|
-
name: 'test_tool',
|
|
643
|
-
description: '测试工具',
|
|
644
|
-
parameters: { type: 'object', properties: {} },
|
|
645
|
-
execute: async () => '',
|
|
646
|
-
keywords: ['test'],
|
|
647
|
-
tags: ['testing'],
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
adapter.declareSkill({
|
|
651
|
-
description: '测试适配器的技能',
|
|
652
|
-
keywords: ['adapter'],
|
|
653
|
-
tags: ['adapter-tag'],
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
expect(mockSkillFeature.add).toHaveBeenCalledTimes(1)
|
|
657
|
-
const [skill] = mockSkillFeature.add.mock.calls[0]
|
|
658
|
-
expect(skill.name).toContain('test-adapter')
|
|
659
|
-
expect(skill.description).toBe('测试适配器的技能')
|
|
660
|
-
expect(skill.tools).toHaveLength(1)
|
|
661
|
-
// keywords 应合并适配器和工具的关键词
|
|
662
|
-
expect(skill.keywords).toContain('adapter')
|
|
663
|
-
expect(skill.keywords).toContain('test')
|
|
664
|
-
// tags 应合并
|
|
665
|
-
expect(skill.tags).toContain('adapter-tag')
|
|
666
|
-
expect(skill.tags).toContain('testing')
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
it('should clean up skill on stop', async () => {
|
|
670
|
-
const disposeSkill = vi.fn()
|
|
671
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
672
|
-
|
|
673
|
-
const mockSkillFeature = {
|
|
674
|
-
add: vi.fn(() => disposeSkill),
|
|
675
|
-
}
|
|
676
|
-
;(plugin as any)._root = plugin
|
|
677
|
-
plugin.inject = ((name: string) => {
|
|
678
|
-
if (name === 'skill') return mockSkillFeature
|
|
679
|
-
return undefined
|
|
680
|
-
}) as any
|
|
681
|
-
;(plugin as any).recordFeatureContribution = vi.fn()
|
|
682
|
-
|
|
683
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
684
|
-
adapter.declareSkill({ description: '测试' })
|
|
685
|
-
|
|
686
|
-
await adapter.stop()
|
|
687
|
-
|
|
688
|
-
expect(disposeSkill).toHaveBeenCalledTimes(1)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('should skip when SkillFeature is not available', () => {
|
|
692
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
693
|
-
;(plugin as any)._root = plugin
|
|
694
|
-
|
|
695
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
696
|
-
|
|
697
|
-
// 不应抛错
|
|
698
|
-
expect(() => {
|
|
699
|
-
adapter.declareSkill({ description: '测试' })
|
|
700
|
-
}).not.toThrow()
|
|
701
|
-
})
|
|
702
|
-
})
|
|
703
|
-
|
|
704
616
|
describe('Adapter Registry', () => {
|
|
705
617
|
it('should have a Registry Map', () => {
|
|
706
618
|
expect(Adapter.Registry).toBeInstanceOf(Map)
|
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', () => {
|