@zhin.js/core 1.1.0 → 1.1.3

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.
Files changed (122) hide show
  1. package/lib/adapter.d.ts +1 -26
  2. package/lib/adapter.d.ts.map +1 -1
  3. package/lib/adapter.js +20 -117
  4. package/lib/adapter.js.map +1 -1
  5. package/lib/built/adapter-process.d.ts +0 -4
  6. package/lib/built/adapter-process.d.ts.map +1 -1
  7. package/lib/built/adapter-process.js +0 -95
  8. package/lib/built/adapter-process.js.map +1 -1
  9. package/lib/built/agent-preset.d.ts +2 -0
  10. package/lib/built/agent-preset.d.ts.map +1 -1
  11. package/lib/built/agent-preset.js +4 -0
  12. package/lib/built/agent-preset.js.map +1 -1
  13. package/lib/built/command.d.ts +4 -0
  14. package/lib/built/command.d.ts.map +1 -1
  15. package/lib/built/command.js +6 -0
  16. package/lib/built/command.js.map +1 -1
  17. package/lib/built/component.d.ts.map +1 -1
  18. package/lib/built/component.js +1 -0
  19. package/lib/built/component.js.map +1 -1
  20. package/lib/built/dispatcher.d.ts.map +1 -1
  21. package/lib/built/dispatcher.js +0 -13
  22. package/lib/built/dispatcher.js.map +1 -1
  23. package/lib/built/message-filter.d.ts +2 -0
  24. package/lib/built/message-filter.d.ts.map +1 -1
  25. package/lib/built/message-filter.js +5 -0
  26. package/lib/built/message-filter.js.map +1 -1
  27. package/lib/built/skill.d.ts +11 -0
  28. package/lib/built/skill.d.ts.map +1 -1
  29. package/lib/built/skill.js +14 -0
  30. package/lib/built/skill.js.map +1 -1
  31. package/lib/built/tool.d.ts +11 -44
  32. package/lib/built/tool.d.ts.map +1 -1
  33. package/lib/built/tool.js +14 -353
  34. package/lib/built/tool.js.map +1 -1
  35. package/lib/plugin.d.ts +1 -25
  36. package/lib/plugin.d.ts.map +1 -1
  37. package/lib/plugin.js +1 -77
  38. package/lib/plugin.js.map +1 -1
  39. package/lib/types.d.ts +0 -25
  40. package/lib/types.d.ts.map +1 -1
  41. package/package.json +10 -7
  42. package/CHANGELOG.md +0 -561
  43. package/REFACTORING_COMPLETE.md +0 -178
  44. package/REFACTORING_STATUS.md +0 -263
  45. package/src/adapter.ts +0 -275
  46. package/src/ai/index.ts +0 -55
  47. package/src/ai/providers/anthropic.ts +0 -379
  48. package/src/ai/providers/base.ts +0 -175
  49. package/src/ai/providers/index.ts +0 -13
  50. package/src/ai/providers/ollama.ts +0 -302
  51. package/src/ai/providers/openai.ts +0 -174
  52. package/src/ai/types.ts +0 -348
  53. package/src/bot.ts +0 -37
  54. package/src/built/adapter-process.ts +0 -177
  55. package/src/built/agent-preset.ts +0 -136
  56. package/src/built/ai-trigger.ts +0 -259
  57. package/src/built/command.ts +0 -108
  58. package/src/built/common-adapter-tools.ts +0 -242
  59. package/src/built/component.ts +0 -130
  60. package/src/built/config.ts +0 -335
  61. package/src/built/cron.ts +0 -156
  62. package/src/built/database.ts +0 -134
  63. package/src/built/dispatcher.ts +0 -496
  64. package/src/built/login-assist.ts +0 -131
  65. package/src/built/message-filter.ts +0 -390
  66. package/src/built/permission.ts +0 -151
  67. package/src/built/schema-feature.ts +0 -190
  68. package/src/built/skill.ts +0 -221
  69. package/src/built/tool.ts +0 -948
  70. package/src/command.ts +0 -87
  71. package/src/component.ts +0 -565
  72. package/src/cron.ts +0 -4
  73. package/src/errors.ts +0 -46
  74. package/src/feature.ts +0 -7
  75. package/src/index.ts +0 -53
  76. package/src/jsx-dev-runtime.ts +0 -2
  77. package/src/jsx-runtime.ts +0 -12
  78. package/src/jsx.ts +0 -135
  79. package/src/message.ts +0 -48
  80. package/src/models/system-log.ts +0 -20
  81. package/src/models/user.ts +0 -15
  82. package/src/notice.ts +0 -98
  83. package/src/plugin.ts +0 -896
  84. package/src/prompt.ts +0 -293
  85. package/src/request.ts +0 -95
  86. package/src/scheduler/index.ts +0 -19
  87. package/src/scheduler/scheduler.ts +0 -372
  88. package/src/scheduler/types.ts +0 -74
  89. package/src/tool-zod.ts +0 -115
  90. package/src/types-generator.ts +0 -78
  91. package/src/types.ts +0 -505
  92. package/src/utils.ts +0 -227
  93. package/tests/adapter.test.ts +0 -638
  94. package/tests/ai/ai-trigger.test.ts +0 -368
  95. package/tests/ai/providers.integration.test.ts +0 -227
  96. package/tests/ai/setup.ts +0 -308
  97. package/tests/ai/tool.test.ts +0 -800
  98. package/tests/bot.test.ts +0 -151
  99. package/tests/command.test.ts +0 -737
  100. package/tests/component-new.test.ts +0 -361
  101. package/tests/config.test.ts +0 -372
  102. package/tests/cron.test.ts +0 -82
  103. package/tests/dispatcher.test.ts +0 -293
  104. package/tests/errors.test.ts +0 -21
  105. package/tests/expression-evaluation.test.ts +0 -258
  106. package/tests/features-builtin.test.ts +0 -191
  107. package/tests/jsx-runtime.test.ts +0 -45
  108. package/tests/jsx.test.ts +0 -319
  109. package/tests/message-filter.test.ts +0 -566
  110. package/tests/message.test.ts +0 -402
  111. package/tests/notice.test.ts +0 -198
  112. package/tests/plugin.test.ts +0 -779
  113. package/tests/prompt.test.ts +0 -78
  114. package/tests/redos-protection.test.ts +0 -198
  115. package/tests/request.test.ts +0 -221
  116. package/tests/schema.test.ts +0 -248
  117. package/tests/skill-feature.test.ts +0 -179
  118. package/tests/test-utils.ts +0 -59
  119. package/tests/tool-feature.test.ts +0 -254
  120. package/tests/types.test.ts +0 -162
  121. package/tests/utils.test.ts +0 -135
  122. package/tsconfig.json +0 -24
@@ -1,134 +0,0 @@
1
- /**
2
- * DatabaseFeature
3
- * 数据库服务,管理数据模型定义,继承自 Feature 抽象类
4
- */
5
- import { Registry, Definition, Databases, Database } from "@zhin.js/database";
6
- import { DatabaseConfig, Models } from "../types.js";
7
- import { Feature, FeatureJSON } from "../feature.js";
8
- import { Plugin, getPlugin } from "../plugin.js";
9
- import type { PluginLike } from '@zhin.js/kernel';
10
- import { SystemLogDefinition } from "../models/system-log.js";
11
- import { UserDefinition } from "../models/user.js";
12
-
13
- /**
14
- * 模型定义记录
15
- */
16
- export interface ModelRecord {
17
- name: string;
18
- definition: Definition<any>;
19
- }
20
-
21
- declare module "../plugin.js" {
22
- namespace Plugin {
23
- interface Extensions {
24
- defineModel<K extends keyof Models>(name: K, definition: Definition<Models[K]>): void;
25
- }
26
- interface Contexts {
27
- database: DatabaseFeature;
28
- }
29
- }
30
- }
31
-
32
- export class DatabaseFeature extends Feature<ModelRecord> {
33
- readonly name = 'database' as const;
34
- readonly icon = 'Database';
35
- readonly desc = '数据模型';
36
-
37
- /** 内部数据库实例 */
38
- readonly db: Database<any, Models, any>;
39
-
40
- /** 按模型名索引 */
41
- readonly byName = new Map<string, ModelRecord>();
42
-
43
- constructor(config: DatabaseConfig) {
44
- super();
45
- this.db = Registry.create<Models, keyof Databases>(config.dialect, config, {
46
- SystemLog: SystemLogDefinition,
47
- User: UserDefinition,
48
- });
49
- }
50
-
51
- // ====================================================================
52
- // 向后兼容代理:旧代码 inject('database').xxx 可直接使用
53
- // ====================================================================
54
-
55
- get models() {
56
- return this.db.models;
57
- }
58
-
59
- define<K extends keyof Models>(name: K, definition: Definition<Models[K]>) {
60
- return this.db.define(name, definition);
61
- }
62
-
63
- start() {
64
- return this.db.start();
65
- }
66
-
67
- stop() {
68
- return this.db.stop();
69
- }
70
-
71
- /**
72
- * 添加模型定义
73
- */
74
- add(record: ModelRecord, pluginName: string): () => void {
75
- this.db.define(record.name as keyof Models, record.definition);
76
- this.byName.set(record.name, record);
77
- return super.add(record, pluginName);
78
- }
79
-
80
- /**
81
- * 移除模型定义
82
- */
83
- remove(record: ModelRecord): boolean {
84
- this.byName.delete(record.name);
85
- return super.remove(record);
86
- }
87
-
88
- /**
89
- * 生命周期: 启动数据库
90
- */
91
- async mounted(plugin: PluginLike): Promise<void> {
92
- plugin.logger.info('Database service started');
93
- await this.db.start();
94
- }
95
-
96
- /**
97
- * 生命周期: 停止数据库
98
- */
99
- async dispose(): Promise<void> {
100
- await this.db.stop();
101
- }
102
-
103
- /**
104
- * 序列化为 JSON
105
- */
106
- toJSON(pluginName?: string): FeatureJSON {
107
- const list = pluginName ? this.getByPlugin(pluginName) : this.items;
108
- return {
109
- name: this.name,
110
- icon: this.icon,
111
- desc: this.desc,
112
- count: list.length,
113
- items: list.map(r => ({
114
- name: r.name,
115
- })),
116
- };
117
- }
118
-
119
- /**
120
- * 提供给 Plugin.prototype 的扩展方法
121
- */
122
- get extensions() {
123
- const feature = this;
124
- return {
125
- defineModel<K extends keyof Models>(name: K, definition: Definition<Models[K]>) {
126
- const plugin = getPlugin();
127
- const record: ModelRecord = { name: name as string, definition };
128
- const dispose = feature.add(record, plugin.name);
129
- plugin.recordFeatureContribution(feature.name, name as string);
130
- plugin.onDispose(dispose);
131
- },
132
- };
133
- }
134
- }
@@ -1,496 +0,0 @@
1
- /**
2
- * MessageDispatcher — 消息调度器
3
- *
4
- * 取代原先的"大杂烩中间件链",将消息处理分为三个清晰阶段:
5
- *
6
- * ┌────────────────────────────────────────┐
7
- * │ Stage 1: Guardrail(护栏) │
8
- * │ 鉴权、限流、安全过滤、日志记录 │
9
- * │ 始终执行,不可被 AI 跳过 │
10
- * └──────────────┬─────────────────────────┘
11
- * ▼
12
- * ┌────────────────────────────────────────┐
13
- * │ Stage 2: Route(路径判定) │
14
- * │ exclusive:命令与 AI 互斥(旧行为) │
15
- * │ dual:命令与 AI 独立判定,可同时命中 │
16
- * └──────────────┬─────────────────────────┘
17
- * ▼
18
- * ┌────────────────────────────────────────┐
19
- * │ Stage 3: Handle(处理) │
20
- * │ Command: commandService.handle() │
21
- * │ AI: aiHandler (由 AI 模块注册) │
22
- * │ 出站:replyWithPolish → $reply → Adapter.sendMessage → before.sendMessage │
23
- * └────────────────────────────────────────┘
24
- *
25
- * 注意:Context key 为 'dispatcher',避免与 HTTP 模块的 'router' 冲突。
26
- *
27
- * 默认路由为 exclusive(命令与 AI 互斥);需双轨时请显式 dualRoute.mode: 'dual'。
28
- */
29
-
30
- import { AsyncLocalStorage } from 'node:async_hooks';
31
- import { Message } from '../message.js';
32
- import { Plugin, getPlugin } from '../plugin.js';
33
- import type {
34
- MessageMiddleware,
35
- RegisteredAdapter,
36
- MaybePromise,
37
- SendContent,
38
- OutboundReplySource,
39
- OutboundPolishContext,
40
- OutboundPolishMiddleware,
41
- BeforeSendHandler,
42
- } from '../types.js';
43
- import type { Context } from '../plugin.js';
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
-
52
- // ============================================================================
53
- // 类型定义
54
- // ============================================================================
55
-
56
- /**
57
- * 路由判定结果(互斥模式 legacy)
58
- */
59
- export type RouteResult =
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
- }
96
-
97
- /**
98
- * AI 处理函数签名
99
- * 由 AI 模块通过 dispatcher.setAIHandler() 注册
100
- */
101
- export type AIHandler = (
102
- message: Message<any>,
103
- content: string,
104
- ) => MaybePromise<void>;
105
-
106
- /**
107
- * 命令前缀判定函数
108
- */
109
- export type CommandMatcher = (text: string, message: Message<any>) => boolean;
110
-
111
- /**
112
- * AI 触发判定函数
113
- */
114
- export type AITriggerMatcher = (message: Message<any>) => { triggered: boolean; content: string };
115
-
116
- export type GuardrailMiddleware = MessageMiddleware<RegisteredAdapter>;
117
-
118
- /** @alias OutboundReplySource:出站回复来源(指令 / AI) */
119
- export type ReplySource = OutboundReplySource;
120
-
121
- export interface CreateMessageDispatcherOptions {
122
- dualRoute?: Partial<DualRouteConfig>;
123
- }
124
-
125
- // ============================================================================
126
- // MessageDispatcher 服务
127
- // ============================================================================
128
-
129
- export interface MessageDispatcherService {
130
- dispatch(message: Message<any>): Promise<void>;
131
-
132
- addGuardrail(guardrail: GuardrailMiddleware): () => void;
133
-
134
- setCommandMatcher(matcher: CommandMatcher): void;
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
-
150
- /**
151
- * 在 `before.sendMessage` 管道内调用 `message.$reply`(与 Adapter#sendMessage 同一出站链)
152
- */
153
- replyWithPolish(
154
- message: Message<any>,
155
- source: ReplySource,
156
- content: SendContent,
157
- ): Promise<unknown>;
158
-
159
- /**
160
- * 是否匹配为指令路径(与 dispatch 内判定一致)
161
- */
162
- matchCommand(message: Message<any>): boolean;
163
-
164
- /**
165
- * AI 触发判定结果
166
- */
167
- matchAI(message: Message<any>): { triggered: boolean; content: string };
168
- }
169
-
170
- // ============================================================================
171
- // 扩展 Plugin 接口
172
- // ============================================================================
173
-
174
- export interface DispatcherContextExtensions {
175
- addGuardrail(guardrail: GuardrailMiddleware): () => void;
176
- addOutboundPolish(handler: OutboundPolishMiddleware): () => void;
177
- }
178
-
179
- declare module '../plugin.js' {
180
- namespace Plugin {
181
- interface Extensions extends DispatcherContextExtensions {}
182
- interface Contexts {
183
- dispatcher: MessageDispatcherService;
184
- }
185
- }
186
- }
187
-
188
- // ============================================================================
189
- // 实现
190
- // ============================================================================
191
-
192
- export function createMessageDispatcher(
193
- options?: CreateMessageDispatcherOptions,
194
- ): Context<'dispatcher', DispatcherContextExtensions> {
195
- const guardrails: GuardrailMiddleware[] = [];
196
- /** mounted 前注册的润色,在 mounted 时挂到 root.before.sendMessage */
197
- const pendingOutboundPolish: OutboundPolishMiddleware[] = [];
198
- let aiHandler: AIHandler | null = null;
199
- let aiTriggerMatcher: AITriggerMatcher | null = null;
200
- let commandMatcher: CommandMatcher | null = null;
201
- let rootPlugin: Plugin | null = null;
202
- let dualRoute = resolveDualRouteConfig(options?.dualRoute);
203
-
204
- let commandPrefixIndex: Map<string, boolean> | null = null;
205
- let lastCommandCount = -1;
206
-
207
- function rebuildCommandIndex(): Map<string, boolean> {
208
- const index = new Map<string, boolean>();
209
- if (rootPlugin) {
210
- const commandService = rootPlugin.inject('command');
211
- if (commandService?.items) {
212
- for (const cmd of commandService.items) {
213
- const prefix = cmd.pattern?.split(/\s/)[0];
214
- if (prefix) index.set(prefix, true);
215
- }
216
- }
217
- }
218
- return index;
219
- }
220
-
221
- function getCommandIndex(): Map<string, boolean> {
222
- const commandService = rootPlugin?.inject('command');
223
- const currentCount = commandService?.items?.length ?? 0;
224
- if (!commandPrefixIndex || currentCount !== lastCommandCount) {
225
- commandPrefixIndex = rebuildCommandIndex();
226
- lastCommandCount = currentCount;
227
- }
228
- return commandPrefixIndex;
229
- }
230
-
231
- async function runGuardrails(message: Message<any>): Promise<boolean> {
232
- if (guardrails.length === 0) return true;
233
-
234
- for (const guardrail of guardrails) {
235
- let nextCalled = false;
236
- try {
237
- await guardrail(message, async () => {
238
- nextCalled = true;
239
- });
240
- } catch {
241
- return false;
242
- }
243
- if (!nextCalled) return false;
244
- }
245
- return true;
246
- }
247
-
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 {
277
- const text = extractText(message);
278
-
279
- if (commandMatcher && commandMatcher(text, message)) {
280
- return { type: 'command' };
281
- }
282
-
283
- const index = getCommandIndex();
284
- for (const [prefix] of index) {
285
- if (text.startsWith(prefix)) {
286
- return { type: 'command' };
287
- }
288
- }
289
-
290
- if (aiTriggerMatcher) {
291
- const { triggered, content } = aiTriggerMatcher(message);
292
- if (triggered) {
293
- return { type: 'ai', content };
294
- }
295
- }
296
-
297
- return { type: 'skip' };
298
- }
299
-
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
- }
355
- }
356
-
357
- const service: MessageDispatcherService = {
358
- async dispatch(message: Message<any>) {
359
- const passed = await runGuardrails(message);
360
- if (!passed) return;
361
-
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;
375
- }
376
- await runCustomMiddlewares(message);
377
- return;
378
- }
379
-
380
- // dual 模式
381
- let wantCmd = matchCommandInternal(message);
382
- const aiRes = matchAIInternal(message);
383
- let wantAi = aiRes.triggered;
384
-
385
- if (!wantCmd && !wantAi) {
386
- await runCustomMiddlewares(message);
387
- return;
388
- }
389
-
390
- if (!cfg.allowDualReply && wantCmd && wantAi) {
391
- if (cfg.order === 'command-first') wantAi = false;
392
- else wantCmd = false;
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);
411
- },
412
-
413
- addGuardrail(guardrail: GuardrailMiddleware) {
414
- guardrails.push(guardrail);
415
- return () => {
416
- const index = guardrails.indexOf(guardrail);
417
- if (index !== -1) guardrails.splice(index, 1);
418
- };
419
- },
420
-
421
- setCommandMatcher(matcher: CommandMatcher) {
422
- commandMatcher = matcher;
423
- },
424
-
425
- setAITriggerMatcher(matcher: AITriggerMatcher) {
426
- aiTriggerMatcher = matcher;
427
- },
428
-
429
- setAIHandler(handler: AIHandler) {
430
- aiHandler = handler;
431
- },
432
-
433
- hasAIHandler() {
434
- return aiHandler !== null;
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
- },
470
- };
471
-
472
- return {
473
- name: 'dispatcher',
474
- description: '消息调度器 — 统一消息路由分流 (Command / AI)',
475
- value: service,
476
- mounted(plugin: Plugin) {
477
- rootPlugin = plugin.root;
478
- flushPendingOutboundPolish();
479
- return service;
480
- },
481
- extensions: {
482
- addGuardrail(guardrail: GuardrailMiddleware) {
483
- const plugin = getPlugin();
484
- const dispose = service.addGuardrail(guardrail);
485
- plugin.onDispose(dispose);
486
- return dispose;
487
- },
488
- addOutboundPolish(handler: OutboundPolishMiddleware) {
489
- const plugin = getPlugin();
490
- const dispose = service.addOutboundPolish(handler);
491
- plugin.onDispose(dispose);
492
- return dispose;
493
- },
494
- },
495
- };
496
- }