@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/built/dispatcher.ts
CHANGED
|
@@ -11,42 +11,88 @@
|
|
|
11
11
|
* ▼
|
|
12
12
|
* ┌────────────────────────────────────────┐
|
|
13
13
|
* │ Stage 2: Route(路径判定) │
|
|
14
|
-
* │
|
|
15
|
-
* │
|
|
14
|
+
* │ exclusive:命令与 AI 互斥(旧行为) │
|
|
15
|
+
* │ dual:命令与 AI 独立判定,可同时命中 │
|
|
16
16
|
* └──────────────┬─────────────────────────┘
|
|
17
17
|
* ▼
|
|
18
18
|
* ┌────────────────────────────────────────┐
|
|
19
19
|
* │ Stage 3: Handle(处理) │
|
|
20
20
|
* │ Command: commandService.handle() │
|
|
21
21
|
* │ AI: aiHandler (由 AI 模块注册) │
|
|
22
|
+
* │ 出站:replyWithPolish → $reply → Adapter.sendMessage → before.sendMessage │
|
|
22
23
|
* └────────────────────────────────────────┘
|
|
23
24
|
*
|
|
24
25
|
* 注意:Context key 为 'dispatcher',避免与 HTTP 模块的 'router' 冲突。
|
|
26
|
+
*
|
|
27
|
+
* 默认路由为 exclusive(命令与 AI 互斥);需双轨时请显式 dualRoute.mode: 'dual'。
|
|
25
28
|
*/
|
|
26
29
|
|
|
30
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
27
31
|
import { Message } from '../message.js';
|
|
28
32
|
import { Plugin, getPlugin } from '../plugin.js';
|
|
29
33
|
import type {
|
|
30
34
|
MessageMiddleware,
|
|
31
35
|
RegisteredAdapter,
|
|
32
|
-
AdapterMessage,
|
|
33
36
|
MaybePromise,
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
SendContent,
|
|
38
|
+
OutboundReplySource,
|
|
39
|
+
OutboundPolishContext,
|
|
40
|
+
OutboundPolishMiddleware,
|
|
41
|
+
BeforeSendHandler,
|
|
36
42
|
} from '../types.js';
|
|
37
43
|
import type { Context } from '../plugin.js';
|
|
38
44
|
|
|
45
|
+
/** Dispatcher 管理的「会话回复」异步上下文,供 `before.sendMessage` 内读取(与 Adapter.renderSendMessage 同链) */
|
|
46
|
+
const outboundReplyAls = new AsyncLocalStorage<{ message: Message<any>; source: OutboundReplySource }>();
|
|
47
|
+
|
|
48
|
+
export function getOutboundReplyStore(): { message: Message<any>; source: OutboundReplySource } | undefined {
|
|
49
|
+
return outboundReplyAls.getStore();
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
// ============================================================================
|
|
40
53
|
// 类型定义
|
|
41
54
|
// ============================================================================
|
|
42
55
|
|
|
43
56
|
/**
|
|
44
|
-
*
|
|
57
|
+
* 路由判定结果(互斥模式 legacy)
|
|
45
58
|
*/
|
|
46
59
|
export type RouteResult =
|
|
47
|
-
| { type: 'command' }
|
|
48
|
-
| { type: 'ai'; content: string }
|
|
49
|
-
| { type: 'skip' };
|
|
60
|
+
| { type: 'command' }
|
|
61
|
+
| { type: 'ai'; content: string }
|
|
62
|
+
| { type: 'skip' };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 双轨分流配置
|
|
66
|
+
*/
|
|
67
|
+
export interface DualRouteConfig {
|
|
68
|
+
/**
|
|
69
|
+
* exclusive:与旧版一致,命中命令则不再走 AI;
|
|
70
|
+
* dual:命令与 AI 独立判定,可同时执行(顺序由 order 决定)
|
|
71
|
+
*/
|
|
72
|
+
mode?: 'exclusive' | 'dual';
|
|
73
|
+
/** 同时命中时的执行顺序,默认先指令后 AI */
|
|
74
|
+
order?: 'command-first' | 'ai-first';
|
|
75
|
+
/**
|
|
76
|
+
* 是否允许在双命中时各回复一次;为 false 时仅执行 order 中的第一个分支
|
|
77
|
+
*/
|
|
78
|
+
allowDualReply?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ResolvedDualRouteConfig = Required<DualRouteConfig>;
|
|
82
|
+
|
|
83
|
+
const DUAL_ROUTE_DEFAULTS: ResolvedDualRouteConfig = {
|
|
84
|
+
mode: 'exclusive',
|
|
85
|
+
order: 'command-first',
|
|
86
|
+
allowDualReply: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function resolveDualRouteConfig(partial?: Partial<DualRouteConfig>): ResolvedDualRouteConfig {
|
|
90
|
+
return {
|
|
91
|
+
mode: partial?.mode ?? DUAL_ROUTE_DEFAULTS.mode,
|
|
92
|
+
order: partial?.order ?? DUAL_ROUTE_DEFAULTS.order,
|
|
93
|
+
allowDualReply: partial?.allowDualReply ?? DUAL_ROUTE_DEFAULTS.allowDualReply,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
50
96
|
|
|
51
97
|
/**
|
|
52
98
|
* AI 处理函数签名
|
|
@@ -59,68 +105,66 @@ export type AIHandler = (
|
|
|
59
105
|
|
|
60
106
|
/**
|
|
61
107
|
* 命令前缀判定函数
|
|
62
|
-
* 返回 true 表示该消息是精确命令调用
|
|
63
108
|
*/
|
|
64
109
|
export type CommandMatcher = (text: string, message: Message<any>) => boolean;
|
|
65
110
|
|
|
66
111
|
/**
|
|
67
112
|
* AI 触发判定函数
|
|
68
|
-
* 返回 { triggered, content } 表示是否应该触发 AI 以及提取的内容
|
|
69
113
|
*/
|
|
70
114
|
export type AITriggerMatcher = (message: Message<any>) => { triggered: boolean; content: string };
|
|
71
115
|
|
|
72
|
-
/**
|
|
73
|
-
* Guardrail 中间件
|
|
74
|
-
* 与 MessageMiddleware 签名一致,但语义上只用于鉴权/限流/安全/日志
|
|
75
|
-
* 返回 false 或抛异常表示拦截消息
|
|
76
|
-
*/
|
|
77
116
|
export type GuardrailMiddleware = MessageMiddleware<RegisteredAdapter>;
|
|
78
117
|
|
|
118
|
+
/** @alias OutboundReplySource:出站回复来源(指令 / AI) */
|
|
119
|
+
export type ReplySource = OutboundReplySource;
|
|
120
|
+
|
|
121
|
+
export interface CreateMessageDispatcherOptions {
|
|
122
|
+
dualRoute?: Partial<DualRouteConfig>;
|
|
123
|
+
}
|
|
124
|
+
|
|
79
125
|
// ============================================================================
|
|
80
126
|
// MessageDispatcher 服务
|
|
81
127
|
// ============================================================================
|
|
82
128
|
|
|
83
|
-
/**
|
|
84
|
-
* MessageDispatcher 服务接口
|
|
85
|
-
*/
|
|
86
129
|
export interface MessageDispatcherService {
|
|
87
|
-
/**
|
|
88
|
-
* 调度一条消息 — 这是唯一的入口
|
|
89
|
-
* Adapter 的 message.receive 事件应该调用此方法
|
|
90
|
-
*/
|
|
91
130
|
dispatch(message: Message<any>): Promise<void>;
|
|
92
131
|
|
|
93
|
-
/**
|
|
94
|
-
* 注册 Guardrail(护栏中间件)
|
|
95
|
-
* Guardrail 始终执行,用于鉴权、限流、安全过滤、日志等
|
|
96
|
-
* @returns 移除函数
|
|
97
|
-
*/
|
|
98
132
|
addGuardrail(guardrail: GuardrailMiddleware): () => void;
|
|
99
133
|
|
|
100
|
-
/**
|
|
101
|
-
* 设置命令匹配器
|
|
102
|
-
* 用于判定消息是否为精确命令调用
|
|
103
|
-
* 默认:检查消息是否以已注册命令的 pattern 开头
|
|
104
|
-
*/
|
|
105
134
|
setCommandMatcher(matcher: CommandMatcher): void;
|
|
106
135
|
|
|
136
|
+
setAITriggerMatcher(matcher: AITriggerMatcher): void;
|
|
137
|
+
|
|
138
|
+
setAIHandler(handler: AIHandler): void;
|
|
139
|
+
|
|
140
|
+
hasAIHandler(): boolean;
|
|
141
|
+
|
|
142
|
+
/** 合并更新双轨配置 */
|
|
143
|
+
setDualRouteConfig(config: Partial<DualRouteConfig>): void;
|
|
144
|
+
|
|
145
|
+
getDualRouteConfig(): Readonly<ResolvedDualRouteConfig>;
|
|
146
|
+
|
|
147
|
+
/** 注册出站润色:挂到根插件 `before.sendMessage`;仅在 `replyWithPolish` 触发的发送中生效(见 getOutboundReplyStore) */
|
|
148
|
+
addOutboundPolish(handler: OutboundPolishMiddleware): () => void;
|
|
149
|
+
|
|
107
150
|
/**
|
|
108
|
-
*
|
|
109
|
-
* 用于判定消息是否应该触发 AI 处理
|
|
110
|
-
* 由 AI 模块注册
|
|
151
|
+
* 在 `before.sendMessage` 管道内调用 `message.$reply`(与 Adapter#sendMessage 同一出站链)
|
|
111
152
|
*/
|
|
112
|
-
|
|
153
|
+
replyWithPolish(
|
|
154
|
+
message: Message<any>,
|
|
155
|
+
source: ReplySource,
|
|
156
|
+
content: SendContent,
|
|
157
|
+
): Promise<unknown>;
|
|
113
158
|
|
|
114
159
|
/**
|
|
115
|
-
*
|
|
116
|
-
* 由 AI 模块注册,当消息路由到 AI 路径时调用
|
|
160
|
+
* 是否匹配为指令路径(与 dispatch 内判定一致)
|
|
117
161
|
*/
|
|
118
|
-
|
|
162
|
+
matchCommand(message: Message<any>): boolean;
|
|
119
163
|
|
|
120
164
|
/**
|
|
121
|
-
*
|
|
165
|
+
* AI 触发判定结果
|
|
122
166
|
*/
|
|
123
|
-
|
|
167
|
+
matchAI(message: Message<any>): { triggered: boolean; content: string };
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
// ============================================================================
|
|
@@ -128,8 +172,8 @@ export interface MessageDispatcherService {
|
|
|
128
172
|
// ============================================================================
|
|
129
173
|
|
|
130
174
|
export interface DispatcherContextExtensions {
|
|
131
|
-
/** 注册 Guardrail(护栏中间件) */
|
|
132
175
|
addGuardrail(guardrail: GuardrailMiddleware): () => void;
|
|
176
|
+
addOutboundPolish(handler: OutboundPolishMiddleware): () => void;
|
|
133
177
|
}
|
|
134
178
|
|
|
135
179
|
declare module '../plugin.js' {
|
|
@@ -145,17 +189,18 @@ declare module '../plugin.js' {
|
|
|
145
189
|
// 实现
|
|
146
190
|
// ============================================================================
|
|
147
191
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
export function createMessageDispatcher(): Context<'dispatcher', DispatcherContextExtensions> {
|
|
192
|
+
export function createMessageDispatcher(
|
|
193
|
+
options?: CreateMessageDispatcherOptions,
|
|
194
|
+
): Context<'dispatcher', DispatcherContextExtensions> {
|
|
152
195
|
const guardrails: GuardrailMiddleware[] = [];
|
|
196
|
+
/** mounted 前注册的润色,在 mounted 时挂到 root.before.sendMessage */
|
|
197
|
+
const pendingOutboundPolish: OutboundPolishMiddleware[] = [];
|
|
153
198
|
let aiHandler: AIHandler | null = null;
|
|
154
199
|
let aiTriggerMatcher: AITriggerMatcher | null = null;
|
|
155
200
|
let commandMatcher: CommandMatcher | null = null;
|
|
156
201
|
let rootPlugin: Plugin | null = null;
|
|
202
|
+
let dualRoute = resolveDualRouteConfig(options?.dualRoute);
|
|
157
203
|
|
|
158
|
-
// Command prefix index — rebuilt lazily for O(1) lookup
|
|
159
204
|
let commandPrefixIndex: Map<string, boolean> | null = null;
|
|
160
205
|
let lastCommandCount = -1;
|
|
161
206
|
|
|
@@ -183,16 +228,15 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
183
228
|
return commandPrefixIndex;
|
|
184
229
|
}
|
|
185
230
|
|
|
186
|
-
/**
|
|
187
|
-
* Guardrail pipeline — a guardrail that does NOT call next() blocks the message.
|
|
188
|
-
*/
|
|
189
231
|
async function runGuardrails(message: Message<any>): Promise<boolean> {
|
|
190
232
|
if (guardrails.length === 0) return true;
|
|
191
233
|
|
|
192
234
|
for (const guardrail of guardrails) {
|
|
193
235
|
let nextCalled = false;
|
|
194
236
|
try {
|
|
195
|
-
await guardrail(message, async () => {
|
|
237
|
+
await guardrail(message, async () => {
|
|
238
|
+
nextCalled = true;
|
|
239
|
+
});
|
|
196
240
|
} catch {
|
|
197
241
|
return false;
|
|
198
242
|
}
|
|
@@ -201,14 +245,41 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
201
245
|
return true;
|
|
202
246
|
}
|
|
203
247
|
|
|
204
|
-
function
|
|
248
|
+
function extractText(message: Message<any>): string {
|
|
249
|
+
if (!message.$content) return '';
|
|
250
|
+
return message.$content
|
|
251
|
+
.map((seg: any) => {
|
|
252
|
+
if (typeof seg === 'string') return seg;
|
|
253
|
+
if (seg.type === 'text') return seg.data?.text || '';
|
|
254
|
+
return '';
|
|
255
|
+
})
|
|
256
|
+
.join('')
|
|
257
|
+
.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function matchCommandInternal(message: Message<any>): boolean {
|
|
261
|
+
const text = extractText(message);
|
|
262
|
+
if (commandMatcher && commandMatcher(text, message)) return true;
|
|
263
|
+
const index = getCommandIndex();
|
|
264
|
+
for (const [prefix] of index) {
|
|
265
|
+
if (text.startsWith(prefix)) return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function matchAIInternal(message: Message<any>): { triggered: boolean; content: string } {
|
|
271
|
+
if (!aiTriggerMatcher) return { triggered: false, content: '' };
|
|
272
|
+
return aiTriggerMatcher(message);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 互斥路由(与旧版 route 一致) */
|
|
276
|
+
function routeExclusive(message: Message<any>): RouteResult {
|
|
205
277
|
const text = extractText(message);
|
|
206
278
|
|
|
207
279
|
if (commandMatcher && commandMatcher(text, message)) {
|
|
208
280
|
return { type: 'command' };
|
|
209
281
|
}
|
|
210
282
|
|
|
211
|
-
// Use indexed lookup instead of O(N) scan
|
|
212
283
|
const index = getCommandIndex();
|
|
213
284
|
for (const [prefix] of index) {
|
|
214
285
|
if (text.startsWith(prefix)) {
|
|
@@ -226,64 +297,117 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
226
297
|
return { type: 'skip' };
|
|
227
298
|
}
|
|
228
299
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
300
|
+
function wrapPolishAsBeforeSend(handler: OutboundPolishMiddleware): BeforeSendHandler {
|
|
301
|
+
return async (options) => {
|
|
302
|
+
const store = outboundReplyAls.getStore();
|
|
303
|
+
if (!store) return;
|
|
304
|
+
const ctx: OutboundPolishContext = {
|
|
305
|
+
message: store.message,
|
|
306
|
+
content: options.content,
|
|
307
|
+
source: store.source,
|
|
308
|
+
};
|
|
309
|
+
const next = await handler(ctx);
|
|
310
|
+
if (next !== undefined) return { ...options, content: next };
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function flushPendingOutboundPolish(): void {
|
|
315
|
+
if (!rootPlugin) return;
|
|
316
|
+
const root = rootPlugin.root;
|
|
317
|
+
for (const mw of pendingOutboundPolish) {
|
|
318
|
+
const fn = wrapPolishAsBeforeSend(mw);
|
|
319
|
+
root.on('before.sendMessage', fn);
|
|
320
|
+
}
|
|
321
|
+
pendingOutboundPolish.length = 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function replyWithPolishInternal(
|
|
325
|
+
message: Message<any>,
|
|
326
|
+
source: ReplySource,
|
|
327
|
+
content: SendContent,
|
|
328
|
+
): Promise<unknown> {
|
|
329
|
+
if (!rootPlugin) {
|
|
330
|
+
return message.$reply(content);
|
|
331
|
+
}
|
|
332
|
+
return outboundReplyAls.run({ message, source }, () => message.$reply(content));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function runCommandBranch(message: Message<any>): Promise<void> {
|
|
336
|
+
if (!rootPlugin) return;
|
|
337
|
+
const commandService = rootPlugin.inject('command');
|
|
338
|
+
if (!commandService) return;
|
|
339
|
+
const response = await commandService.handle(message, rootPlugin);
|
|
340
|
+
if (response) {
|
|
341
|
+
await replyWithPolishInternal(message, 'command', response);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runCustomMiddlewares(message: Message<any>): Promise<void> {
|
|
346
|
+
if (!rootPlugin) return;
|
|
347
|
+
const customMiddlewares = (rootPlugin as any)._getCustomMiddlewares?.() as
|
|
348
|
+
| MessageMiddleware<RegisteredAdapter>[]
|
|
349
|
+
| undefined;
|
|
350
|
+
if (customMiddlewares && customMiddlewares.length > 0) {
|
|
351
|
+
const { compose } = await import('../utils.js');
|
|
352
|
+
const composed = compose(customMiddlewares);
|
|
353
|
+
await composed(message, async () => {});
|
|
354
|
+
}
|
|
239
355
|
}
|
|
240
356
|
|
|
241
357
|
const service: MessageDispatcherService = {
|
|
242
358
|
async dispatch(message: Message<any>) {
|
|
243
|
-
// Stage 1: Guardrail
|
|
244
359
|
const passed = await runGuardrails(message);
|
|
245
360
|
if (!passed) return;
|
|
246
361
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
362
|
+
const cfg = dualRoute;
|
|
363
|
+
|
|
364
|
+
if (cfg.mode === 'exclusive') {
|
|
365
|
+
const result = routeExclusive(message);
|
|
366
|
+
switch (result.type) {
|
|
367
|
+
case 'command':
|
|
368
|
+
await runCommandBranch(message);
|
|
369
|
+
break;
|
|
370
|
+
case 'ai':
|
|
371
|
+
if (aiHandler) await aiHandler(message, result.content);
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
break;
|
|
263
375
|
}
|
|
376
|
+
await runCustomMiddlewares(message);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
264
379
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
380
|
+
// dual 模式
|
|
381
|
+
let wantCmd = matchCommandInternal(message);
|
|
382
|
+
const aiRes = matchAIInternal(message);
|
|
383
|
+
let wantAi = aiRes.triggered;
|
|
271
384
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
385
|
+
if (!wantCmd && !wantAi) {
|
|
386
|
+
await runCustomMiddlewares(message);
|
|
387
|
+
return;
|
|
275
388
|
}
|
|
276
389
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const customMiddlewares = (rootPlugin as any)._getCustomMiddlewares?.() as MessageMiddleware<RegisteredAdapter>[] | undefined;
|
|
281
|
-
if (customMiddlewares && customMiddlewares.length > 0) {
|
|
282
|
-
const { compose } = await import('../utils.js');
|
|
283
|
-
const composed = compose(customMiddlewares);
|
|
284
|
-
await composed(message, async () => {});
|
|
285
|
-
}
|
|
390
|
+
if (!cfg.allowDualReply && wantCmd && wantAi) {
|
|
391
|
+
if (cfg.order === 'command-first') wantAi = false;
|
|
392
|
+
else wantCmd = false;
|
|
286
393
|
}
|
|
394
|
+
|
|
395
|
+
const runCmd = async () => {
|
|
396
|
+
if (wantCmd) await runCommandBranch(message);
|
|
397
|
+
};
|
|
398
|
+
const runAi = async () => {
|
|
399
|
+
if (wantAi && aiHandler) await aiHandler(message, aiRes.content);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (cfg.order === 'ai-first') {
|
|
403
|
+
await runAi();
|
|
404
|
+
await runCmd();
|
|
405
|
+
} else {
|
|
406
|
+
await runCmd();
|
|
407
|
+
await runAi();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await runCustomMiddlewares(message);
|
|
287
411
|
},
|
|
288
412
|
|
|
289
413
|
addGuardrail(guardrail: GuardrailMiddleware) {
|
|
@@ -309,6 +433,40 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
309
433
|
hasAIHandler() {
|
|
310
434
|
return aiHandler !== null;
|
|
311
435
|
},
|
|
436
|
+
|
|
437
|
+
setDualRouteConfig(config: Partial<DualRouteConfig>) {
|
|
438
|
+
dualRoute = resolveDualRouteConfig({ ...dualRoute, ...config });
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
getDualRouteConfig() {
|
|
442
|
+
return { ...dualRoute };
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
addOutboundPolish(handler: OutboundPolishMiddleware) {
|
|
446
|
+
const fn = wrapPolishAsBeforeSend(handler);
|
|
447
|
+
if (rootPlugin) {
|
|
448
|
+
const root = rootPlugin.root;
|
|
449
|
+
root.on('before.sendMessage', fn);
|
|
450
|
+
return () => root.off('before.sendMessage', fn);
|
|
451
|
+
}
|
|
452
|
+
pendingOutboundPolish.push(handler);
|
|
453
|
+
return () => {
|
|
454
|
+
const i = pendingOutboundPolish.indexOf(handler);
|
|
455
|
+
if (i !== -1) pendingOutboundPolish.splice(i, 1);
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
replyWithPolish(message, source, content) {
|
|
460
|
+
return replyWithPolishInternal(message, source, content);
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
matchCommand(message) {
|
|
464
|
+
return matchCommandInternal(message);
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
matchAI(message) {
|
|
468
|
+
return matchAIInternal(message);
|
|
469
|
+
},
|
|
312
470
|
};
|
|
313
471
|
|
|
314
472
|
return {
|
|
@@ -317,6 +475,7 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
317
475
|
value: service,
|
|
318
476
|
mounted(plugin: Plugin) {
|
|
319
477
|
rootPlugin = plugin.root;
|
|
478
|
+
flushPendingOutboundPolish();
|
|
320
479
|
return service;
|
|
321
480
|
},
|
|
322
481
|
extensions: {
|
|
@@ -326,6 +485,12 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
326
485
|
plugin.onDispose(dispose);
|
|
327
486
|
return dispose;
|
|
328
487
|
},
|
|
488
|
+
addOutboundPolish(handler: OutboundPolishMiddleware) {
|
|
489
|
+
const plugin = getPlugin();
|
|
490
|
+
const dispose = service.addOutboundPolish(handler);
|
|
491
|
+
plugin.onDispose(dispose);
|
|
492
|
+
return dispose;
|
|
493
|
+
},
|
|
329
494
|
},
|
|
330
495
|
};
|
|
331
496
|
}
|
package/src/built/skill.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Plugin = 运行时容器(生命周期、服务注册、中间件)
|
|
6
6
|
* Skill = AI 可见的能力接口(名称、描述、工具列表)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* "我叫什么、我能做什么、我有哪些工具"
|
|
8
|
+
* Skill 记录由运行时注入(如 Agent 从磁盘 SKILL.md 同步),供 Agent 粗筛与工具关联。
|
|
10
9
|
*
|
|
11
10
|
* SkillFeature 全局收集所有 Skill,供 Agent 进行两级过滤:
|
|
12
11
|
* 1. 粗筛:根据用户消息选择相关 Skill
|
|
@@ -14,7 +13,6 @@
|
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { Feature, FeatureJSON } from '../feature.js';
|
|
17
|
-
import { Plugin, getPlugin } from '../plugin.js';
|
|
18
16
|
import type { Tool } from '../types.js';
|
|
19
17
|
|
|
20
18
|
// ============================================================================
|
|
@@ -51,35 +49,21 @@ export interface Skill {
|
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
*
|
|
55
|
-
* 由 plugin.declareSkill() 注册
|
|
52
|
+
* SKILL.md frontmatter 常见字段(类型提示;运行时由 Agent 等同步到 SkillFeature)
|
|
56
53
|
*/
|
|
57
54
|
export interface SkillMetadata {
|
|
58
55
|
/** 技能描述(必填) */
|
|
59
56
|
description: string;
|
|
60
57
|
|
|
61
|
-
/**
|
|
58
|
+
/** 触发关键词(可选) */
|
|
62
59
|
keywords?: string[];
|
|
63
60
|
|
|
64
61
|
/** 分类标签(可选) */
|
|
65
62
|
tags?: string[];
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// 扩展 Plugin 接口
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
export interface SkillContextExtensions {
|
|
73
|
-
/**
|
|
74
|
-
* 声明本插件的 Skill 元数据
|
|
75
|
-
* 调用后,插件的工具会自动聚合为一个 Skill 注册到 SkillFeature
|
|
76
|
-
*/
|
|
77
|
-
declareSkill(metadata: SkillMetadata): void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
65
|
declare module '../plugin.js' {
|
|
81
66
|
namespace Plugin {
|
|
82
|
-
interface Extensions extends SkillContextExtensions {}
|
|
83
67
|
interface Contexts {
|
|
84
68
|
skill: SkillFeature;
|
|
85
69
|
}
|
|
@@ -220,60 +204,4 @@ export class SkillFeature extends Feature<Skill> {
|
|
|
220
204
|
})),
|
|
221
205
|
};
|
|
222
206
|
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 提供给 Plugin.prototype 的扩展方法
|
|
226
|
-
*/
|
|
227
|
-
get extensions() {
|
|
228
|
-
const feature = this;
|
|
229
|
-
return {
|
|
230
|
-
declareSkill(metadata: SkillMetadata) {
|
|
231
|
-
const plugin = getPlugin();
|
|
232
|
-
const pluginName = plugin.name;
|
|
233
|
-
|
|
234
|
-
// 收集该插件注册的工具
|
|
235
|
-
const toolService = plugin.root.inject('tool') as { getToolsByPlugin?: (name: string) => Tool[] } | undefined;
|
|
236
|
-
let tools: Tool[] = [];
|
|
237
|
-
|
|
238
|
-
if (toolService && typeof toolService.getToolsByPlugin === 'function') {
|
|
239
|
-
tools = toolService.getToolsByPlugin(pluginName);
|
|
240
|
-
} else {
|
|
241
|
-
tools = plugin.getAllTools?.() || [];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 聚合关键词:开发者声明 + 工具自带
|
|
245
|
-
const allKeywords = new Set<string>(metadata.keywords || []);
|
|
246
|
-
for (const tool of tools) {
|
|
247
|
-
if (tool.keywords) {
|
|
248
|
-
for (const kw of tool.keywords) {
|
|
249
|
-
allKeywords.add(kw);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 聚合标签
|
|
255
|
-
const allTags = new Set<string>(metadata.tags || []);
|
|
256
|
-
for (const tool of tools) {
|
|
257
|
-
if (tool.tags) {
|
|
258
|
-
for (const tag of tool.tags) {
|
|
259
|
-
allTags.add(tag);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const skill: Skill = {
|
|
265
|
-
name: pluginName,
|
|
266
|
-
description: metadata.description,
|
|
267
|
-
tools,
|
|
268
|
-
keywords: Array.from(allKeywords),
|
|
269
|
-
tags: Array.from(allTags),
|
|
270
|
-
pluginName,
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const dispose = feature.add(skill, pluginName);
|
|
274
|
-
plugin.recordFeatureContribution(feature.name, pluginName);
|
|
275
|
-
plugin.onDispose(dispose);
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
207
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -6,19 +6,24 @@
|
|
|
6
6
|
import { AsyncLocalStorage } from "async_hooks";
|
|
7
7
|
import { EventEmitter } from "events";
|
|
8
8
|
import { createRequire } from "module";
|
|
9
|
-
import type {
|
|
10
|
-
import { Schema } from "@zhin.js/schema";
|
|
11
|
-
import type { Models, RegisteredAdapters, Tool, ToolContext } from "./types.js";
|
|
9
|
+
import type { Tool } from "./types.js";
|
|
12
10
|
import * as fs from "fs";
|
|
13
11
|
import * as path from "path";
|
|
14
12
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
15
13
|
import logger, { Logger } from "@zhin.js/logger";
|
|
16
14
|
import { compose, remove, resolveEntry } from "./utils.js";
|
|
17
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MessageMiddleware,
|
|
17
|
+
RegisteredAdapter,
|
|
18
|
+
MaybePromise,
|
|
19
|
+
ArrayItem,
|
|
20
|
+
SendOptions,
|
|
21
|
+
} from "./types.js";
|
|
18
22
|
import type { ConfigFeature } from "./built/config.js";
|
|
19
23
|
import type { PermissionFeature } from "./built/permission.js";
|
|
20
24
|
import { Adapter, Adapters } from "./adapter.js";
|
|
21
25
|
import { Notice } from "./notice.js";
|
|
26
|
+
import { Message } from "./message.js";
|
|
22
27
|
import { Request } from "./request.js";
|
|
23
28
|
import { Feature, FeatureJSON } from "./feature.js";
|
|
24
29
|
import { createHash } from "crypto";
|
|
@@ -910,7 +915,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
|
|
|
910
915
|
'addMiddleware', 'useContext', 'inject', 'contextIsReady',
|
|
911
916
|
'start', 'stop', 'onMounted', 'onDispose',
|
|
912
917
|
'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info',
|
|
913
|
-
'recordFeatureContribution', 'getFeatures'
|
|
918
|
+
'recordFeatureContribution', 'getFeatures',
|
|
914
919
|
]);
|
|
915
920
|
|
|
916
921
|
/**
|
|
@@ -1041,6 +1046,7 @@ export namespace Plugin {
|
|
|
1041
1046
|
'bot.login.pending': [BotLoginPendingTask];
|
|
1042
1047
|
// Notice 事件
|
|
1043
1048
|
'notice.receive': [Notice];
|
|
1049
|
+
'message.receive': [Message];
|
|
1044
1050
|
[key: `notice.${string}`]: [Notice];
|
|
1045
1051
|
// Request 事件
|
|
1046
1052
|
'request.receive': [Request];
|