@zhin.js/core 1.0.24 → 1.0.26

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 (211) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +45 -1
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +182 -1
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/adapter-process.d.ts +4 -0
  88. package/lib/built/adapter-process.d.ts.map +1 -1
  89. package/lib/built/adapter-process.js +94 -0
  90. package/lib/built/adapter-process.js.map +1 -1
  91. package/lib/built/ai-trigger.d.ts +89 -0
  92. package/lib/built/ai-trigger.d.ts.map +1 -0
  93. package/lib/built/ai-trigger.js +166 -0
  94. package/lib/built/ai-trigger.js.map +1 -0
  95. package/lib/built/command.d.ts +33 -17
  96. package/lib/built/command.d.ts.map +1 -1
  97. package/lib/built/command.js +71 -44
  98. package/lib/built/command.js.map +1 -1
  99. package/lib/built/component.d.ts +42 -15
  100. package/lib/built/component.d.ts.map +1 -1
  101. package/lib/built/component.js +84 -52
  102. package/lib/built/component.js.map +1 -1
  103. package/lib/built/config.d.ts +54 -5
  104. package/lib/built/config.d.ts.map +1 -1
  105. package/lib/built/config.js +76 -10
  106. package/lib/built/config.js.map +1 -1
  107. package/lib/built/cron.d.ts +41 -18
  108. package/lib/built/cron.d.ts.map +1 -1
  109. package/lib/built/cron.js +106 -63
  110. package/lib/built/cron.js.map +1 -1
  111. package/lib/built/database.d.ts +55 -6
  112. package/lib/built/database.d.ts.map +1 -1
  113. package/lib/built/database.js +93 -22
  114. package/lib/built/database.js.map +1 -1
  115. package/lib/built/dispatcher.d.ts +118 -0
  116. package/lib/built/dispatcher.d.ts.map +1 -0
  117. package/lib/built/dispatcher.js +196 -0
  118. package/lib/built/dispatcher.js.map +1 -0
  119. package/lib/built/permission.d.ts +45 -5
  120. package/lib/built/permission.d.ts.map +1 -1
  121. package/lib/built/permission.js +56 -11
  122. package/lib/built/permission.js.map +1 -1
  123. package/lib/built/skill.d.ts +117 -0
  124. package/lib/built/skill.d.ts.map +1 -0
  125. package/lib/built/skill.js +191 -0
  126. package/lib/built/skill.js.map +1 -0
  127. package/lib/built/tool.d.ts +188 -0
  128. package/lib/built/tool.d.ts.map +1 -0
  129. package/lib/built/tool.js +749 -0
  130. package/lib/built/tool.js.map +1 -0
  131. package/lib/feature.d.ts +75 -0
  132. package/lib/feature.d.ts.map +1 -0
  133. package/lib/feature.js +69 -0
  134. package/lib/feature.js.map +1 -0
  135. package/lib/index.d.ts +6 -0
  136. package/lib/index.d.ts.map +1 -1
  137. package/lib/index.js +11 -0
  138. package/lib/index.js.map +1 -1
  139. package/lib/plugin.d.ts +53 -18
  140. package/lib/plugin.d.ts.map +1 -1
  141. package/lib/plugin.js +301 -31
  142. package/lib/plugin.js.map +1 -1
  143. package/lib/types.d.ts +248 -9
  144. package/lib/types.d.ts.map +1 -1
  145. package/lib/utils.d.ts.map +1 -1
  146. package/lib/utils.js +38 -12
  147. package/lib/utils.js.map +1 -1
  148. package/package.json +4 -4
  149. package/src/adapter.ts +206 -2
  150. package/src/ai/agent.ts +772 -0
  151. package/src/ai/context-manager.ts +440 -0
  152. package/src/ai/conversation-memory.ts +774 -0
  153. package/src/ai/follow-up.ts +357 -0
  154. package/src/ai/index.ts +128 -0
  155. package/src/ai/init.ts +502 -0
  156. package/src/ai/output.ts +261 -0
  157. package/src/ai/providers/anthropic.ts +375 -0
  158. package/src/ai/providers/base.ts +173 -0
  159. package/src/ai/providers/index.ts +13 -0
  160. package/src/ai/providers/ollama.ts +292 -0
  161. package/src/ai/providers/openai.ts +167 -0
  162. package/src/ai/rate-limiter.ts +129 -0
  163. package/src/ai/service.ts +319 -0
  164. package/src/ai/session.ts +544 -0
  165. package/src/ai/tone-detector.ts +89 -0
  166. package/src/ai/tools.ts +218 -0
  167. package/src/ai/types.ts +296 -0
  168. package/src/ai/user-profile.ts +181 -0
  169. package/src/ai/zhin-agent.ts +845 -0
  170. package/src/built/adapter-process.ts +99 -0
  171. package/src/built/ai-trigger.ts +259 -0
  172. package/src/built/command.ts +75 -69
  173. package/src/built/component.ts +94 -76
  174. package/src/built/config.ts +238 -128
  175. package/src/built/cron.ts +117 -101
  176. package/src/built/database.ts +128 -33
  177. package/src/built/dispatcher.ts +332 -0
  178. package/src/built/permission.ts +146 -54
  179. package/src/built/skill.ts +280 -0
  180. package/src/built/tool.ts +928 -0
  181. package/src/feature.ts +113 -0
  182. package/src/index.ts +11 -0
  183. package/src/plugin.ts +359 -69
  184. package/src/types.ts +306 -11
  185. package/src/utils.ts +37 -13
  186. package/tests/adapter.test.ts +153 -1
  187. package/tests/ai/agent.test.ts +614 -0
  188. package/tests/ai/ai-trigger.test.ts +368 -0
  189. package/tests/ai/context-manager.test.ts +413 -0
  190. package/tests/ai/conversation-memory.test.ts +128 -0
  191. package/tests/ai/follow-up.test.ts +175 -0
  192. package/tests/ai/integration.test.ts +584 -0
  193. package/tests/ai/output.test.ts +128 -0
  194. package/tests/ai/providers.integration.test.ts +227 -0
  195. package/tests/ai/rate-limiter.test.ts +108 -0
  196. package/tests/ai/session.test.ts +375 -0
  197. package/tests/ai/setup.ts +308 -0
  198. package/tests/ai/tone-detector.test.ts +80 -0
  199. package/tests/ai/tool.test.ts +800 -0
  200. package/tests/ai/tools-builtin.test.ts +346 -0
  201. package/tests/ai/user-profile.test.ts +73 -0
  202. package/tests/ai/zhin-agent.test.ts +177 -0
  203. package/tests/component-new.test.ts +17 -6
  204. package/tests/config.test.ts +46 -0
  205. package/tests/cron.test.ts +94 -5
  206. package/tests/dispatcher.test.ts +146 -0
  207. package/tests/feature.test.ts +145 -0
  208. package/tests/features-builtin.test.ts +191 -0
  209. package/tests/plugin.test.ts +88 -14
  210. package/tests/skill-feature.test.ts +179 -0
  211. package/tests/tool-feature.test.ts +254 -0
package/src/types.ts CHANGED
@@ -86,16 +86,8 @@ export interface UserInfo {
86
86
  role?: string;
87
87
  }
88
88
 
89
- /**
90
- * 权限服务接口
91
- */
92
- import type { PermissionService } from './built/permission.js';
93
- /**
94
- * 配置服务接口
95
- */
96
- import type { ConfigService } from './built/config.js';
97
-
98
- export { PermissionService, ConfigService };
89
+ // PermissionService and ConfigService are now exported from their respective
90
+ // built files as backward-compatible aliases for PermissionFeature / ConfigFeature.
99
91
  /**
100
92
  * 群组信息结构
101
93
  */
@@ -134,4 +126,307 @@ export type QueueItem = {
134
126
  action: string;
135
127
  payload: any;
136
128
  };
137
- export type BeforeSendHandler=(options:SendOptions)=>MaybePromise<SendOptions|void>
129
+ export type BeforeSendHandler=(options:SendOptions)=>MaybePromise<SendOptions|void>
130
+
131
+ // ============================================================================
132
+ // 统一 Tool 类型定义
133
+ // 支持 AI Agent 调用和自动转换为 Command
134
+ // ============================================================================
135
+
136
+ /**
137
+ * JSON Schema 定义,用于描述工具参数
138
+ */
139
+ export interface ToolJsonSchema {
140
+ type: string;
141
+ properties?: Record<string, ToolJsonSchema & {
142
+ /** 参数类型提示,用于命令解析 */
143
+ paramType?: 'text' | 'number' | 'boolean' | 'rest';
144
+ }>;
145
+ required?: string[];
146
+ items?: ToolJsonSchema;
147
+ enum?: any[];
148
+ description?: string;
149
+ default?: any;
150
+ [key: string]: any;
151
+ }
152
+
153
+ // ============================================================================
154
+ // 类型反射工具类型
155
+ // ============================================================================
156
+
157
+ /**
158
+ * 从 TypeScript 类型推断 JSON Schema 的 type 字段
159
+ */
160
+ type InferSchemaType<T> =
161
+ T extends string ? 'string' :
162
+ T extends number ? 'number' :
163
+ T extends boolean ? 'boolean' :
164
+ T extends any[] ? 'array' :
165
+ T extends object ? 'object' :
166
+ 'string';
167
+
168
+ /**
169
+ * 单个属性的 Schema 定义
170
+ */
171
+ export interface PropertySchema<T = any> extends ToolJsonSchema {
172
+ type: InferSchemaType<T>;
173
+ description?: string;
174
+ default?: T;
175
+ enum?: T extends string | number ? T[] : never;
176
+ paramType?: 'text' | 'number' | 'boolean' | 'rest';
177
+ }
178
+
179
+ /**
180
+ * 从 TArgs 构建 properties 类型
181
+ * 每个属性的 key 必须与 TArgs 的 key 一致
182
+ */
183
+ type ToolPropertiesSchema<TArgs extends Record<string, any>> = {
184
+ [K in keyof TArgs]: PropertySchema<TArgs[K]>;
185
+ };
186
+
187
+ /**
188
+ * 提取必需的属性名
189
+ * 通过检查属性是否可以为 undefined 来判断
190
+ */
191
+ type RequiredKeys<T> = {
192
+ [K in keyof T]-?: undefined extends T[K] ? never : K;
193
+ }[keyof T];
194
+
195
+ /**
196
+ * 带类型反射的参数 Schema
197
+ * @template TArgs 参数类型
198
+ */
199
+ export interface ToolParametersSchema<TArgs extends Record<string, any> = Record<string, any>> {
200
+ type: 'object';
201
+ /** 属性定义,key 与 TArgs 的 key 一致 */
202
+ properties: ToolPropertiesSchema<TArgs>;
203
+ /** 必需的属性列表 */
204
+ required?: (keyof TArgs & string)[];
205
+ /** 描述 */
206
+ description?: string;
207
+ }
208
+
209
+ /**
210
+ * 工具执行上下文
211
+ * 包含消息来源、发送者等信息
212
+ */
213
+ export interface ToolContext {
214
+ /** 来源平台 */
215
+ platform?: string;
216
+ /** 来源 Bot */
217
+ botId?: string;
218
+ /** 场景 ID(群号/频道ID/私聊用户ID) */
219
+ sceneId?: string;
220
+ /** 发送者 ID */
221
+ senderId?: string;
222
+ /** 原始消息对象(如果从消息触发) */
223
+ message?: Message<any>;
224
+ /**
225
+ * 消息场景类型
226
+ * private: 私聊, group: 群聊, channel: 频道
227
+ */
228
+ scope?: ToolScope;
229
+ /**
230
+ * 发送者权限级别
231
+ * 用于工具权限过滤
232
+ */
233
+ senderPermissionLevel?: ToolPermissionLevel;
234
+ /**
235
+ * 发送者是否为群管理员
236
+ */
237
+ isGroupAdmin?: boolean;
238
+ /**
239
+ * 发送者是否为群主
240
+ */
241
+ isGroupOwner?: boolean;
242
+ /**
243
+ * 发送者是否为机器人管理员
244
+ */
245
+ isBotAdmin?: boolean;
246
+ /**
247
+ * 发送者是否为 Zhin 拥有者
248
+ */
249
+ isOwner?: boolean;
250
+ /** 额外数据 */
251
+ extra?: Record<string, any>;
252
+ }
253
+
254
+ /**
255
+ * 统一的 Tool 定义
256
+ * 可同时用于:
257
+ * - AI Agent 工具调用
258
+ * - 自动生成 Command
259
+ * - MCP 工具暴露
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * // 使用 defineTool 获得类型安全
264
+ * const weatherTool = defineTool<{ city: string }>({
265
+ * name: 'weather',
266
+ * description: '查询天气',
267
+ * parameters: {
268
+ * type: 'object',
269
+ * properties: {
270
+ * city: { type: 'string', description: '城市名称' }
271
+ * },
272
+ * required: ['city']
273
+ * },
274
+ * execute: async (args) => {
275
+ * return `${args.city} 的天气是晴天`; // args.city 有类型提示
276
+ * },
277
+ * });
278
+ *
279
+ * plugin.addTool(weatherTool); // 无需类型断言
280
+ * ```
281
+ */
282
+ /**
283
+ * 消息场景类型
284
+ */
285
+ export type ToolScope = 'private' | 'group' | 'channel';
286
+
287
+ /**
288
+ * 工具权限级别
289
+ * - user: 普通用户(默认)
290
+ * - group_admin: 群管理员
291
+ * - group_owner: 群主
292
+ * - bot_admin: 机器人管理员
293
+ * - owner: Zhin 拥有者(最高权限)
294
+ */
295
+ export type ToolPermissionLevel = 'user' | 'group_admin' | 'group_owner' | 'bot_admin' | 'owner';
296
+
297
+ export interface Tool {
298
+ /** 工具名称(唯一标识,建议使用 snake_case) */
299
+ name: string;
300
+
301
+ /** 工具描述(供 AI 和帮助系统使用) */
302
+ description: string;
303
+
304
+ /**
305
+ * 参数定义(JSON Schema 格式)
306
+ */
307
+ parameters: ToolParametersSchema;
308
+
309
+ /**
310
+ * 工具执行函数
311
+ * @param args 解析后的参数
312
+ * @param context 执行上下文(包含消息、发送者等信息)
313
+ * @returns 执行结果(字符串会直接作为回复,对象会被 JSON 序列化)
314
+ */
315
+ execute: (args: Record<string, any>, context?: ToolContext) => MaybePromise<any>;
316
+
317
+ /** 工具来源标识(自动填充:adapter:xxx / plugin:xxx) */
318
+ source?: string;
319
+
320
+ /** 工具标签(用于分类和过滤) */
321
+ tags?: string[];
322
+
323
+ /** 触发关键词(用户消息包含这些词时优先选择此工具) */
324
+ keywords?: string[];
325
+
326
+ /**
327
+ * 命令配置(可选)
328
+ * 如果不提供,会根据 parameters 自动生成命令模式
329
+ * 如果设置为 false,则不生成命令
330
+ */
331
+ command?: Tool.CommandConfig | false;
332
+
333
+ /**
334
+ * 权限要求(旧版,保留兼容)
335
+ * 执行此工具需要的权限列表
336
+ */
337
+ permissions?: string[];
338
+
339
+ /**
340
+ * 支持的平台列表
341
+ * 例如:['qq', 'telegram', 'discord']
342
+ * 不填则支持所有平台
343
+ */
344
+ platforms?: string[];
345
+
346
+ /**
347
+ * 支持的场景列表
348
+ * 例如:['private', 'group', 'channel']
349
+ * 不填则支持所有场景
350
+ */
351
+ scopes?: ToolScope[];
352
+
353
+ /**
354
+ * 调用所需的最低权限级别
355
+ * 默认为 'user'(普通用户可调用)
356
+ */
357
+ permissionLevel?: ToolPermissionLevel;
358
+
359
+ /**
360
+ * 是否隐藏
361
+ * 隐藏的工具不会出现在帮助列表中,但仍可被调用
362
+ */
363
+ hidden?: boolean;
364
+ }
365
+
366
+ /**
367
+ * 类型安全的 Tool 定义(用于 defineTool)
368
+ * 提供泛型参数以获得 execute 函数的类型推断
369
+ */
370
+ export interface ToolDefinition<TArgs extends Record<string, any> = Record<string, any>> {
371
+ name: string;
372
+ description: string;
373
+ parameters: ToolParametersSchema<TArgs>;
374
+ execute: (args: TArgs, context?: ToolContext) => MaybePromise<any>;
375
+ source?: string;
376
+ tags?: string[];
377
+ keywords?: string[];
378
+ command?: Tool.CommandConfig | false;
379
+ permissions?: string[];
380
+ /** 支持的平台列表(不填则支持所有平台) */
381
+ platforms?: string[];
382
+ /** 支持的场景列表(不填则支持所有场景) */
383
+ scopes?: ToolScope[];
384
+ /** 调用所需的最低权限级别(默认 'user') */
385
+ permissionLevel?: ToolPermissionLevel;
386
+ hidden?: boolean;
387
+ }
388
+
389
+ export namespace Tool {
390
+ /**
391
+ * 命令配置
392
+ */
393
+ export interface CommandConfig {
394
+ /**
395
+ * 自定义命令模式
396
+ * 如果不提供,会根据 parameters 自动生成
397
+ * @example 'weather <city>' | 'calc <expression:text>'
398
+ */
399
+ pattern?: string;
400
+
401
+ /** 命令别名 */
402
+ alias?: string[];
403
+
404
+ /** 命令使用说明 */
405
+ usage?: string[];
406
+
407
+ /** 命令示例 */
408
+ examples?: string[];
409
+
410
+ /** 是否启用(默认 true) */
411
+ enabled?: boolean;
412
+ }
413
+
414
+ /**
415
+ * 参数信息
416
+ */
417
+ export interface ParamInfo {
418
+ name: string;
419
+ type: string;
420
+ required: boolean;
421
+ description?: string;
422
+ default?: any;
423
+ enum?: any[];
424
+ }
425
+ }
426
+
427
+ // ============================================================================
428
+ // 兼容性别名(逐步废弃)
429
+ // ============================================================================
430
+
431
+ /** @deprecated 使用 Tool 替代 */
432
+ export type AITool = Tool;
package/src/utils.ts CHANGED
@@ -197,23 +197,46 @@ export namespace segment {
197
197
  template = unescape(template);
198
198
  const result: MessageElement[] = [];
199
199
  // 修复 ReDoS 漏洞:使用更安全的正则表达式
200
- // 原: /<(\S+)(\s[^>]+)?\/>/ 可能导致回溯
201
- const closingReg = /<(\w+)(?:\s+[^>]*?)?\/>/;
202
- // 原: /<(\S+)(\s[^>]+)?>([\s\S]*?)<\/\1>/ 可能导致回溯
203
- const twinningReg = /<(\w+)(?:\s+[^>]*?)?>([^]*?)<\/\1>/;
200
+ // 注意:需要使用捕获组来获取属性字符串,否则无法正确重建原始标签
201
+ // closingReg: 自闭合标签 <type attr="val"/>
202
+ const closingReg = /<(\w+)(\s+[^>]*?)?\/>/;
203
+ // twinningReg: 成对标签 <type attr="val">child</type>
204
+ const twinningReg = /<(\w+)(\s+[^>]*?)?>([^]*?)<\/\1>/;
204
205
 
205
206
  let iterations = 0;
206
207
  const MAX_ITERATIONS = 1000; // 防止无限循环
207
208
 
208
209
  while (template.length && iterations++ < MAX_ITERATIONS) {
209
- const [_, type, attrStr = "", child = ""] =
210
- template.match(twinningReg) || template.match(closingReg) || [];
211
- if (!type) break;
212
- const isClosing = closingReg.test(template);
213
- const matched = isClosing
214
- ? `<${type}${attrStr}/>`
215
- : `<${type}${attrStr}>${child}</${type}>`;
216
- const index = template.indexOf(matched);
210
+ const twinMatch = template.match(twinningReg);
211
+ const closeMatch = template.match(closingReg);
212
+
213
+ // 选择位置更靠前的匹配
214
+ let match: RegExpMatchArray | null = null;
215
+ let isClosing = false;
216
+
217
+ if (twinMatch && closeMatch) {
218
+ const twinIndex = template.indexOf(twinMatch[0]);
219
+ const closeIndex = template.indexOf(closeMatch[0]);
220
+ if (closeIndex <= twinIndex) {
221
+ match = closeMatch;
222
+ isClosing = true;
223
+ } else {
224
+ match = twinMatch;
225
+ }
226
+ } else if (closeMatch) {
227
+ match = closeMatch;
228
+ isClosing = true;
229
+ } else if (twinMatch) {
230
+ match = twinMatch;
231
+ }
232
+
233
+ if (!match) break;
234
+
235
+ const [fullMatch, type, attrStr = "", child = ""] = isClosing
236
+ ? [match[0], match[1], match[2] || ""]
237
+ : [match[0], match[1], match[2] || "", match[3] || ""];
238
+ const index = template.indexOf(fullMatch);
239
+ if (index === -1) break; // 安全检查
217
240
  const prevText = template.slice(0, index);
218
241
  if (prevText)
219
242
  result.push({
@@ -222,7 +245,7 @@ export namespace segment {
222
245
  text: unescape(prevText),
223
246
  },
224
247
  });
225
- template = template.slice(index + matched.length);
248
+ template = template.slice(index + fullMatch.length);
226
249
  // 修复 ReDoS 漏洞:使用更简单的正则表达式
227
250
  // 原: /\s([^=]+)(?=(?=="([^"]+)")|(?=='([^']+)'))/g 嵌套前瞻断言
228
251
  const attrArr = [
@@ -271,6 +294,7 @@ export namespace segment {
271
294
  if (typeof item === "string") return item;
272
295
  const { type, data } = item;
273
296
  if (type === "text") return data.text;
297
+ if (type === "at") return `@${data.user_id||data.qq}`;
274
298
  return data.text ? `{${type}}(${data.text})` : `{${type}}`;
275
299
  })
276
300
  .join("");
@@ -377,7 +377,7 @@ describe('Adapter Core Functionality', () => {
377
377
  })
378
378
 
379
379
  describe('message.receive', () => {
380
- it('should process received message through middleware', async () => {
380
+ it('should process received message through middleware when no dispatcher', async () => {
381
381
  const config = [{ id: 'bot1' }]
382
382
  const adapter = new MockAdapter(plugin, 'test', config)
383
383
  await adapter.start()
@@ -401,6 +401,74 @@ describe('Adapter Core Functionality', () => {
401
401
  await new Promise(resolve => setTimeout(resolve, 10))
402
402
  expect(middlewareCalled).toBe(true)
403
403
  })
404
+
405
+ it('should use MessageDispatcher when available', async () => {
406
+ const config = [{ id: 'bot1' }]
407
+ const adapter = new MockAdapter(plugin, 'test', config)
408
+ await adapter.start()
409
+
410
+ let dispatchCalled = false
411
+ let middlewareCalled = false
412
+
413
+ // 注册 dispatcher context
414
+ plugin.$contexts.set('dispatcher', {
415
+ name: 'dispatcher',
416
+ description: 'mock dispatcher',
417
+ value: {
418
+ dispatch: (msg: any) => { dispatchCalled = true },
419
+ },
420
+ } as any)
421
+
422
+ plugin.addMiddleware(async (message, next) => {
423
+ middlewareCalled = true
424
+ await next()
425
+ })
426
+
427
+ const message = {
428
+ $bot: 'bot1',
429
+ $adapter: 'test',
430
+ $channel: { id: 'channel-id', type: 'text' },
431
+ $content: 'Hello'
432
+ } as any
433
+
434
+ adapter.emit('message.receive', message)
435
+
436
+ await new Promise(resolve => setTimeout(resolve, 10))
437
+ expect(dispatchCalled).toBe(true)
438
+ expect(middlewareCalled).toBe(false)
439
+ })
440
+
441
+ it('should fallback to middleware when dispatcher has no dispatch method', async () => {
442
+ const config = [{ id: 'bot1' }]
443
+ const adapter = new MockAdapter(plugin, 'test', config)
444
+ await adapter.start()
445
+
446
+ let middlewareCalled = false
447
+
448
+ // 注册一个没有 dispatch 方法的 dispatcher
449
+ plugin.$contexts.set('dispatcher', {
450
+ name: 'dispatcher',
451
+ description: 'broken dispatcher',
452
+ value: { noDispatch: true },
453
+ } as any)
454
+
455
+ plugin.addMiddleware(async (message, next) => {
456
+ middlewareCalled = true
457
+ await next()
458
+ })
459
+
460
+ const message = {
461
+ $bot: 'bot1',
462
+ $adapter: 'test',
463
+ $channel: { id: 'channel-id', type: 'text' },
464
+ $content: 'Hello'
465
+ } as any
466
+
467
+ adapter.emit('message.receive', message)
468
+
469
+ await new Promise(resolve => setTimeout(resolve, 10))
470
+ expect(middlewareCalled).toBe(true)
471
+ })
404
472
  })
405
473
  })
406
474
 
@@ -549,6 +617,90 @@ describe('Adapter Core Functionality', () => {
549
617
  })
550
618
  })
551
619
 
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
+
552
704
  describe('Adapter Registry', () => {
553
705
  it('should have a Registry Map', () => {
554
706
  expect(Adapter.Registry).toBeInstanceOf(Map)