@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/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
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoginAssist — 登录辅助生产者-消费者
|
|
3
|
+
*
|
|
4
|
+
* 适配器在需要人为辅助登录时(扫码、短信、滑块等)作为生产者投递待办;
|
|
5
|
+
* Web 控制台、命令行等作为消费者拉取待办并提交结果。
|
|
6
|
+
* 待办未消费前会一直保留,刷新页面后仍可继续消费。
|
|
7
|
+
*
|
|
8
|
+
* 事件:
|
|
9
|
+
* bot.login.pending — 有新待办时触发,payload: PendingLoginTask
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Plugin } from '../plugin.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// 类型
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export type LoginAssistType = 'qrcode' | 'sms' | 'slider' | 'device' | 'other';
|
|
19
|
+
|
|
20
|
+
export interface PendingLoginTaskPayload {
|
|
21
|
+
/** 说明文案(如「请扫码登录」) */
|
|
22
|
+
message?: string;
|
|
23
|
+
/** 二维码图片 URL 或 base64(type=qrcode 时) */
|
|
24
|
+
image?: string;
|
|
25
|
+
/** 滑块验证 URL(type=slider 时) */
|
|
26
|
+
url?: string;
|
|
27
|
+
/** 其它扩展字段 */
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PendingLoginTask {
|
|
32
|
+
id: string;
|
|
33
|
+
adapter: string;
|
|
34
|
+
botId: string;
|
|
35
|
+
type: LoginAssistType;
|
|
36
|
+
payload: PendingLoginTaskPayload;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PendingEntry {
|
|
41
|
+
task: PendingLoginTask;
|
|
42
|
+
resolve: (value: string | Record<string, unknown>) => void;
|
|
43
|
+
reject: (err: Error) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// LoginAssist 服务
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export class LoginAssist {
|
|
51
|
+
private readonly plugin: Plugin;
|
|
52
|
+
private readonly pending = new Map<string, PendingEntry>();
|
|
53
|
+
private idSeq = 0;
|
|
54
|
+
|
|
55
|
+
constructor(plugin: Plugin) {
|
|
56
|
+
this.plugin = plugin;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 生产者:等待用户输入后 resolve。会发出 bot.login.pending 事件,未消费前可被 listPending 拉取(刷新后可继续消费)。
|
|
61
|
+
*/
|
|
62
|
+
waitForInput(
|
|
63
|
+
adapter: string,
|
|
64
|
+
botId: string,
|
|
65
|
+
type: LoginAssistType,
|
|
66
|
+
payload: PendingLoginTaskPayload = {},
|
|
67
|
+
): Promise<string | Record<string, unknown>> {
|
|
68
|
+
const id = `login-${Date.now()}-${++this.idSeq}`;
|
|
69
|
+
const task: PendingLoginTask = {
|
|
70
|
+
id,
|
|
71
|
+
adapter,
|
|
72
|
+
botId,
|
|
73
|
+
type,
|
|
74
|
+
payload: { message: payload.message ?? '', ...payload },
|
|
75
|
+
createdAt: Date.now(),
|
|
76
|
+
};
|
|
77
|
+
const promise = new Promise<string | Record<string, unknown>>((resolve, reject) => {
|
|
78
|
+
this.pending.set(id, { task, resolve, reject });
|
|
79
|
+
this.plugin.emit('bot.login.pending', task);
|
|
80
|
+
});
|
|
81
|
+
return promise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 消费者:提交结果,对应 waitForInput 的 Promise 会 resolve。
|
|
86
|
+
*/
|
|
87
|
+
submit(id: string, value: string | Record<string, unknown>): boolean {
|
|
88
|
+
const entry = this.pending.get(id);
|
|
89
|
+
if (!entry) return false;
|
|
90
|
+
this.pending.delete(id);
|
|
91
|
+
entry.resolve(value);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 消费者:取消某条待办,对应 Promise 会 reject。
|
|
97
|
+
*/
|
|
98
|
+
cancel(id: string, reason = 'cancelled'): boolean {
|
|
99
|
+
const entry = this.pending.get(id);
|
|
100
|
+
if (!entry) return false;
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
entry.reject(new Error(reason));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 列出未消费的待办(供 Web/CLI 展示,刷新后仍可拉取同一列表)。
|
|
108
|
+
*/
|
|
109
|
+
listPending(): PendingLoginTask[] {
|
|
110
|
+
return [...this.pending.values()].map((e) => e.task);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dispose(): void {
|
|
114
|
+
for (const [, entry] of this.pending) {
|
|
115
|
+
entry.reject(new Error('LoginAssist disposed'));
|
|
116
|
+
}
|
|
117
|
+
this.pending.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// 扩展 Plugin.Contexts
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
declare module '../plugin.js' {
|
|
126
|
+
namespace Plugin {
|
|
127
|
+
interface Contexts {
|
|
128
|
+
loginAssist: LoginAssist;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|