@zhin.js/core 1.0.25 → 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 (200) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +17 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +84 -2
  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/ai-trigger.d.ts.map +1 -1
  88. package/lib/built/ai-trigger.js +7 -3
  89. package/lib/built/ai-trigger.js.map +1 -1
  90. package/lib/built/command.d.ts +33 -17
  91. package/lib/built/command.d.ts.map +1 -1
  92. package/lib/built/command.js +71 -44
  93. package/lib/built/command.js.map +1 -1
  94. package/lib/built/component.d.ts +42 -15
  95. package/lib/built/component.d.ts.map +1 -1
  96. package/lib/built/component.js +84 -52
  97. package/lib/built/component.js.map +1 -1
  98. package/lib/built/config.d.ts +54 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +76 -10
  101. package/lib/built/config.js.map +1 -1
  102. package/lib/built/cron.d.ts +41 -18
  103. package/lib/built/cron.d.ts.map +1 -1
  104. package/lib/built/cron.js +106 -63
  105. package/lib/built/cron.js.map +1 -1
  106. package/lib/built/database.d.ts +55 -6
  107. package/lib/built/database.d.ts.map +1 -1
  108. package/lib/built/database.js +93 -22
  109. package/lib/built/database.js.map +1 -1
  110. package/lib/built/dispatcher.d.ts +118 -0
  111. package/lib/built/dispatcher.d.ts.map +1 -0
  112. package/lib/built/dispatcher.js +196 -0
  113. package/lib/built/dispatcher.js.map +1 -0
  114. package/lib/built/permission.d.ts +45 -5
  115. package/lib/built/permission.d.ts.map +1 -1
  116. package/lib/built/permission.js +56 -11
  117. package/lib/built/permission.js.map +1 -1
  118. package/lib/built/skill.d.ts +117 -0
  119. package/lib/built/skill.d.ts.map +1 -0
  120. package/lib/built/skill.js +191 -0
  121. package/lib/built/skill.js.map +1 -0
  122. package/lib/built/tool.d.ts +71 -164
  123. package/lib/built/tool.d.ts.map +1 -1
  124. package/lib/built/tool.js +212 -297
  125. package/lib/built/tool.js.map +1 -1
  126. package/lib/feature.d.ts +75 -0
  127. package/lib/feature.d.ts.map +1 -0
  128. package/lib/feature.js +69 -0
  129. package/lib/feature.js.map +1 -0
  130. package/lib/index.d.ts +4 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +7 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/plugin.d.ts +25 -17
  135. package/lib/plugin.d.ts.map +1 -1
  136. package/lib/plugin.js +180 -20
  137. package/lib/plugin.js.map +1 -1
  138. package/lib/types.d.ts +4 -9
  139. package/lib/types.d.ts.map +1 -1
  140. package/package.json +4 -4
  141. package/src/adapter.ts +101 -2
  142. package/src/ai/agent.ts +772 -0
  143. package/src/ai/context-manager.ts +440 -0
  144. package/src/ai/conversation-memory.ts +774 -0
  145. package/src/ai/follow-up.ts +357 -0
  146. package/src/ai/index.ts +128 -0
  147. package/src/ai/init.ts +502 -0
  148. package/src/ai/output.ts +261 -0
  149. package/src/ai/providers/anthropic.ts +375 -0
  150. package/src/ai/providers/base.ts +173 -0
  151. package/src/ai/providers/index.ts +13 -0
  152. package/src/ai/providers/ollama.ts +292 -0
  153. package/src/ai/providers/openai.ts +167 -0
  154. package/src/ai/rate-limiter.ts +129 -0
  155. package/src/ai/service.ts +319 -0
  156. package/src/ai/session.ts +544 -0
  157. package/src/ai/tone-detector.ts +89 -0
  158. package/src/ai/tools.ts +218 -0
  159. package/src/ai/types.ts +296 -0
  160. package/src/ai/user-profile.ts +181 -0
  161. package/src/ai/zhin-agent.ts +845 -0
  162. package/src/built/ai-trigger.ts +6 -3
  163. package/src/built/command.ts +75 -69
  164. package/src/built/component.ts +94 -76
  165. package/src/built/config.ts +238 -128
  166. package/src/built/cron.ts +117 -101
  167. package/src/built/database.ts +128 -33
  168. package/src/built/dispatcher.ts +332 -0
  169. package/src/built/permission.ts +146 -54
  170. package/src/built/skill.ts +280 -0
  171. package/src/built/tool.ts +245 -366
  172. package/src/feature.ts +113 -0
  173. package/src/index.ts +7 -0
  174. package/src/plugin.ts +198 -33
  175. package/src/types.ts +6 -10
  176. package/tests/adapter.test.ts +153 -1
  177. package/tests/ai/agent.test.ts +614 -0
  178. package/tests/ai/ai-trigger.test.ts +368 -0
  179. package/tests/ai/context-manager.test.ts +413 -0
  180. package/tests/ai/conversation-memory.test.ts +128 -0
  181. package/tests/ai/follow-up.test.ts +175 -0
  182. package/tests/ai/integration.test.ts +584 -0
  183. package/tests/ai/output.test.ts +128 -0
  184. package/tests/ai/providers.integration.test.ts +227 -0
  185. package/tests/ai/rate-limiter.test.ts +108 -0
  186. package/tests/ai/session.test.ts +375 -0
  187. package/tests/ai/setup.ts +308 -0
  188. package/tests/ai/tone-detector.test.ts +80 -0
  189. package/tests/ai/tool.test.ts +800 -0
  190. package/tests/ai/tools-builtin.test.ts +346 -0
  191. package/tests/ai/user-profile.test.ts +73 -0
  192. package/tests/ai/zhin-agent.test.ts +177 -0
  193. package/tests/config.test.ts +46 -0
  194. package/tests/cron.test.ts +94 -5
  195. package/tests/dispatcher.test.ts +146 -0
  196. package/tests/feature.test.ts +145 -0
  197. package/tests/features-builtin.test.ts +191 -0
  198. package/tests/plugin.test.ts +88 -14
  199. package/tests/skill-feature.test.ts +179 -0
  200. package/tests/tool-feature.test.ts +254 -0
package/src/feature.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type { Plugin } from "./plugin.js";
2
+ /**
3
+ * Feature 抽象基类
4
+ * 所有可追踪、可序列化的插件功能(命令、组件、定时任务、中间件等)的基类。
5
+ * Feature 替换原来的 Context 接口,用于 provide/inject 机制。
6
+ */
7
+
8
+ /**
9
+ * Feature 序列化后的 JSON 格式,用于 HTTP API 返回给前端
10
+ */
11
+ export interface FeatureJSON {
12
+ name: string;
13
+ icon: string;
14
+ desc: string;
15
+ count: number;
16
+ items: any[];
17
+ }
18
+
19
+ /**
20
+ * Feature<T> 抽象类
21
+ * - name / icon / desc: 自描述元数据
22
+ * - items: 全局 item 列表
23
+ * - pluginItems: 按插件名分组的 item 列表
24
+ * - toJSON: 控制 HTTP API 返回的数据格式
25
+ * - extensions: 提供给 Plugin.prototype 的扩展方法(如 addCommand)
26
+ * - mounted / dispose: 可选生命周期钩子
27
+ */
28
+ export abstract class Feature<T = any> {
29
+ abstract readonly name: string;
30
+ abstract readonly icon: string;
31
+ abstract readonly desc: string;
32
+
33
+ /** 全局 item 列表 */
34
+ readonly items: T[] = [];
35
+
36
+ /** 按插件名分组的 item 列表 */
37
+ protected pluginItems = new Map<string, T[]>();
38
+
39
+ /**
40
+ * 添加 item,同时记录所属插件
41
+ * @returns dispose 函数,用于移除该 item
42
+ */
43
+ add(item: T, pluginName: string): () => void {
44
+ this.items.push(item);
45
+ if (!this.pluginItems.has(pluginName)) {
46
+ this.pluginItems.set(pluginName, []);
47
+ }
48
+ this.pluginItems.get(pluginName)!.push(item);
49
+ return () => this.remove(item);
50
+ }
51
+
52
+ /**
53
+ * 移除 item
54
+ */
55
+ remove(item: T): boolean {
56
+ const idx = this.items.indexOf(item);
57
+ if (idx !== -1) {
58
+ this.items.splice(idx, 1);
59
+ for (const [, items] of this.pluginItems) {
60
+ const i = items.indexOf(item);
61
+ if (i !== -1) items.splice(i, 1);
62
+ }
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * 获取指定插件注册的 item 列表
70
+ */
71
+ getByPlugin(pluginName: string): T[] {
72
+ return this.pluginItems.get(pluginName) || [];
73
+ }
74
+
75
+ /**
76
+ * 全局 item 数量
77
+ */
78
+ get count(): number {
79
+ return this.items.length;
80
+ }
81
+
82
+ /**
83
+ * 指定插件的 item 数量
84
+ */
85
+ countByPlugin(pluginName: string): number {
86
+ return this.getByPlugin(pluginName).length;
87
+ }
88
+
89
+ /**
90
+ * 序列化为 JSON(用于 HTTP API)
91
+ * @param pluginName 如果提供,则只返回该插件的 item;否则返回全部
92
+ */
93
+ abstract toJSON(pluginName?: string): FeatureJSON;
94
+
95
+ /**
96
+ * 提供给 Plugin.prototype 的扩展方法
97
+ * 子类重写此 getter 以注册扩展(如 addCommand)
98
+ */
99
+ get extensions(): Record<string, Function> {
100
+ return {};
101
+ }
102
+
103
+ /**
104
+ * 生命周期: 服务挂载时调用
105
+ * @param plugin 宿主插件(通常是 root plugin)
106
+ */
107
+ mounted?(plugin: Plugin): void | Promise<void>;
108
+
109
+ /**
110
+ * 生命周期: 服务销毁时调用
111
+ */
112
+ dispose?(): void;
113
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Core exports
2
+ export * from './feature.js'
2
3
  export * from './bot.js'
3
4
  export * from './plugin.js'
4
5
  export * from './command.js'
@@ -21,6 +22,12 @@ export * from './built/database.js'
21
22
  export * from './built/tool.js'
22
23
  // AI Trigger Service (纯工具,无副作用)
23
24
  export * from './built/ai-trigger.js'
25
+ // MessageDispatcher (消息调度器)
26
+ export * from './built/dispatcher.js'
27
+ // Skill 系统 (AI 能力描述)
28
+ export * from './built/skill.js'
29
+ // AI 模块 (原 @zhin.js/ai,已合并至 core)
30
+ export * from './ai/index.js'
24
31
 
25
32
  export * from './types.js'
26
33
  export * from './utils.js'
package/src/plugin.ts CHANGED
@@ -14,8 +14,11 @@ import * as path from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import logger, { Logger } from "@zhin.js/logger";
16
16
  import { compose, remove, resolveEntry } from "./utils.js";
17
- import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, ConfigService, PermissionService, SendOptions } from "./types.js";
17
+ import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, SendOptions } from "./types.js";
18
+ import type { ConfigFeature } from "./built/config.js";
19
+ import type { PermissionFeature } from "./built/permission.js";
18
20
  import { Adapter, Adapters } from "./adapter.js";
21
+ import { Feature, FeatureJSON } from "./feature.js";
19
22
  import { createHash } from "crypto";
20
23
  const contextsKey = Symbol("contexts");
21
24
  const loadedModules = new Map<string, Plugin>(); // 记录已加载的模块
@@ -63,10 +66,19 @@ function getCurrentFile(metaUrl = import.meta.url): string {
63
66
  /**
64
67
  * usePlugin - 获取或创建当前插件实例
65
68
  * 类似 React Hooks 的设计,根据调用文件自动创建插件树
69
+ *
70
+ * 同一个文件多次调用 usePlugin() 返回同一个实例,
71
+ * 避免 Plugin.create() + usePlugin() 产生不必要的双层包装。
66
72
  */
67
73
  export function usePlugin(): Plugin {
68
74
  const callerFile = getCurrentFile();
69
75
  const parentPlugin = storage.getStore();
76
+
77
+ // 同一文件再次调用 usePlugin(),直接复用已有实例
78
+ if (parentPlugin && parentPlugin.filePath === callerFile) {
79
+ return parentPlugin;
80
+ }
81
+
70
82
  const newPlugin = new Plugin(callerFile, parentPlugin);
71
83
  storage.enterWith(newPlugin);
72
84
  return newPlugin;
@@ -164,6 +176,9 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
164
176
  // 统一的清理函数集合
165
177
  #disposables: Set<() => void | Promise<void>> = new Set();
166
178
 
179
+ // 记录当前插件向哪些 Feature 贡献了哪些 item
180
+ #featureContributions = new Map<string, Set<string>>();
181
+
167
182
  get middleware(): MessageMiddleware<RegisteredAdapter> {
168
183
  return compose<RegisteredAdapter>(this.#middlewares);
169
184
  }
@@ -226,10 +241,10 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
226
241
  tool: Tool,
227
242
  generateCommand: boolean = true
228
243
  ): () => void {
229
- // 尝试使用 ToolService
244
+ // 尝试使用 ToolFeature
230
245
  const toolService = this.root.inject('tool' as any) as any;
231
- if (toolService && typeof toolService.add === 'function') {
232
- const dispose = toolService.add(tool, this.name, generateCommand);
246
+ if (toolService && typeof toolService.addTool === 'function') {
247
+ const dispose = toolService.addTool(tool, this.name, generateCommand);
233
248
  this.#disposables.add(dispose);
234
249
  return () => {
235
250
  dispose();
@@ -430,8 +445,10 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
430
445
 
431
446
  // 启动所有服务
432
447
  for (const context of this.$contexts.values()) {
433
- if (typeof context.mounted === "function" && !context.value) {
434
- context.value = await context.mounted(this);
448
+ if (typeof context.mounted === "function") {
449
+ const result = await context.mounted(this);
450
+ // 仅当 value 未预设时才赋值(mounted 始终执行,以支持副作用如设置内部引用)
451
+ if (!context.value) context.value = result;
435
452
  }
436
453
  this.dispatch('context.mounted', context.name)
437
454
  }
@@ -449,26 +466,157 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
449
466
  }
450
467
  }
451
468
  /**
452
- * 获取插件提供的功能
453
- * 从各个服务中获取数据
469
+ * 记录 Feature 贡献(由 Feature extensions 内部调用)
470
+ * @param featureName Feature 名称(如 'command')
471
+ * @param itemName item 标识(如命令 pattern)
454
472
  */
455
- get features(): Plugin.Features {
456
- const commandService = this.inject('command');
457
- const componentService = this.inject('component')
458
- const cronService = this.inject('cron');
473
+ recordFeatureContribution(featureName: string, itemName: string): void {
474
+ if (!this.#featureContributions.has(featureName)) {
475
+ this.#featureContributions.set(featureName, new Set());
476
+ }
477
+ this.#featureContributions.get(featureName)!.add(itemName);
478
+ }
459
479
 
460
- return {
461
- commands: commandService ? commandService.items.map(c => c.pattern) : [],
462
- components: componentService ? componentService.getAllNames() : [],
463
- crons: cronService ? cronService.items.map(c => c.cronExpression) : [],
464
- middlewares: this.#middlewares.map((m, i) => m.name || `middleware_${i}`),
480
+ /**
481
+ * 收集本插件及所有后代插件的 Feature 贡献名称
482
+ * 解决 Plugin.create() 包装问题:外层插件的 #featureContributions 为空,
483
+ * 实际贡献记录在 usePlugin() 创建的内层子插件上
484
+ */
485
+ #collectAllFeatureNames(names: Set<string>): void {
486
+ for (const name of this.#featureContributions.keys()) {
487
+ names.add(name);
488
+ }
489
+ for (const child of this.children) {
490
+ child.#collectAllFeatureNames(names);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * 收集本插件及所有后代插件的 plugin name 集合
496
+ * 用于 Feature.toJSON(pluginName) 匹配
497
+ */
498
+ #collectAllPluginNames(names: Set<string>): void {
499
+ names.add(this.name);
500
+ for (const child of this.children) {
501
+ child.#collectAllPluginNames(names);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * 获取当前插件的所有 Feature 数据(用于 HTTP API)
507
+ * 遍历插件贡献的 Feature,调用各 Feature 的 toJSON(pluginName) 获取序列化数据
508
+ * 同时包含 middleware(方案 B: 本地构造)
509
+ *
510
+ * 注意:Plugin.create() 创建 "外层" 插件,usePlugin() 创建 "内层" 子插件。
511
+ * extension 方法 (addCommand 等) 通过 getPlugin() 记录在内层插件上。
512
+ * 因此需要遍历整个子树来收集 feature 贡献名称。
513
+ */
514
+ getFeatures(): FeatureJSON[] {
515
+ const result: FeatureJSON[] = [];
516
+
517
+ // 收集本插件及所有后代插件的 feature 名称和 plugin 名称
518
+ const featureNames = new Set<string>();
519
+ this.#collectAllFeatureNames(featureNames);
520
+
521
+ const pluginNames = new Set<string>();
522
+ this.#collectAllPluginNames(pluginNames);
523
+
524
+ // 从 Feature 贡献中收集
525
+ for (const featureName of featureNames) {
526
+ const feature = this.inject(featureName as keyof Plugin.Contexts);
527
+ if (feature instanceof Feature) {
528
+ // 先用当前插件名尝试
529
+ let json = feature.toJSON(this.name);
530
+ if (json.count === 0) {
531
+ // 当前插件名匹配不到(可能名称不同),尝试后代插件名
532
+ for (const pName of pluginNames) {
533
+ if (pName === this.name) continue;
534
+ json = feature.toJSON(pName);
535
+ if (json.count > 0) break;
536
+ }
537
+ }
538
+ if (json.count > 0) {
539
+ result.push(json);
540
+ }
541
+ }
542
+ }
543
+
544
+ // middleware(方案 B: 本地构造,因为 middleware 是 Plugin 私有属性)
545
+ // 同样需要收集子插件树中的 middleware
546
+ const allMiddlewareNames: string[] = [];
547
+ const collectMiddlewares = (plugin: Plugin) => {
548
+ const mws = plugin.#middlewares
549
+ .filter(m => m !== plugin.#messageMiddleware)
550
+ .map((m, i) => m.name || `middleware_${i}`);
551
+ allMiddlewareNames.push(...mws);
552
+ for (const child of plugin.children) {
553
+ collectMiddlewares(child);
554
+ }
465
555
  };
556
+ collectMiddlewares(this);
557
+
558
+ if (allMiddlewareNames.length > 0) {
559
+ result.push({
560
+ name: 'middleware',
561
+ icon: 'Layers',
562
+ desc: '中间件',
563
+ count: allMiddlewareNames.length,
564
+ items: allMiddlewareNames.map(name => ({ name })),
565
+ });
566
+ }
567
+
568
+ // 自动检测适配器和服务上下文(从 $contexts 中发现非 Feature 的贡献)
569
+ const adapterItems: { name: string; bots: number; online: number; tools: number }[] = [];
570
+ const serviceItems: { name: string; desc: string }[] = [];
571
+
572
+ const scanContexts = (plugin: Plugin) => {
573
+ for (const [name, context] of plugin.$contexts) {
574
+ const value = context.value;
575
+ if (value instanceof Adapter) {
576
+ adapterItems.push({
577
+ name,
578
+ bots: value.bots.size,
579
+ online: Array.from(value.bots.values()).filter(b => b.$connected).length,
580
+ tools: value.tools.size,
581
+ });
582
+ } else if (value !== undefined && !(value instanceof Feature)) {
583
+ // 非 Feature、非 Adapter 的上下文 = 服务
584
+ serviceItems.push({ name, desc: context.description || name });
585
+ }
586
+ }
587
+ for (const child of plugin.children) {
588
+ scanContexts(child);
589
+ }
590
+ };
591
+ scanContexts(this);
592
+
593
+ if (adapterItems.length > 0) {
594
+ result.push({
595
+ name: 'adapter',
596
+ icon: 'Plug',
597
+ desc: '适配器',
598
+ count: adapterItems.length,
599
+ items: adapterItems,
600
+ });
601
+ }
602
+
603
+ if (serviceItems.length > 0) {
604
+ result.push({
605
+ name: 'service',
606
+ icon: 'Server',
607
+ desc: '服务',
608
+ count: serviceItems.length,
609
+ items: serviceItems,
610
+ });
611
+ }
612
+
613
+ return result;
466
614
  }
467
615
 
468
616
  info(): Record<string, any> {
469
617
  return {
470
618
  [this.name]: {
471
- features: this.features,
619
+ features: this.getFeatures(),
472
620
  children: this.children.map(child => child.info())
473
621
  }
474
622
  }
@@ -523,6 +671,9 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
523
671
  // 清理 middlewares 数组(保留默认的消息中间件)
524
672
  this.#middlewares.length = 1;
525
673
 
674
+ // 清理 feature 贡献记录
675
+ this.#featureContributions.clear();
676
+
526
677
  if (this.parent) {
527
678
  remove(this.parent?.children, this);
528
679
  }
@@ -595,9 +746,32 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
595
746
  // ============================================================================
596
747
 
597
748
  /**
598
- * 注册上下文
749
+ * 注册上下文(支持 Feature 实例或传统 Context 对象)
599
750
  */
600
- provide<T extends keyof Plugin.Contexts>(context: Context<T>): this {
751
+ provide<T extends keyof Plugin.Contexts>(target: Feature | Context<T>): this {
752
+ if (target instanceof Feature) {
753
+ // Feature → 自动转为内部 Context 格式存储
754
+ const feature = target;
755
+ const context: Context<T> = {
756
+ name: feature.name as T,
757
+ description: feature.desc,
758
+ value: feature as any,
759
+ mounted: feature.mounted
760
+ ? async (plugin: Plugin) => {
761
+ await feature.mounted!(plugin);
762
+ return feature as any;
763
+ }
764
+ : undefined,
765
+ dispose: feature.dispose
766
+ ? async () => { await feature.dispose!(); }
767
+ : undefined,
768
+ extensions: feature.extensions,
769
+ };
770
+ return this.provide(context);
771
+ }
772
+
773
+ // 传统 Context 路径
774
+ const context = target;
601
775
  if (!Plugin[contextsKey].includes(context.name as string)) {
602
776
  Plugin[contextsKey].push(context.name as string);
603
777
  }
@@ -705,7 +879,8 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
705
879
  static #coreMethods = new Set([
706
880
  'addMiddleware', 'useContext', 'inject', 'contextIsReady',
707
881
  'start', 'stop', 'onMounted', 'onDispose',
708
- 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info'
882
+ 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info',
883
+ 'recordFeatureContribution', 'getFeatures'
709
884
  ]);
710
885
 
711
886
  /**
@@ -774,16 +949,6 @@ export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts
774
949
  // 类型定义
775
950
  // ============================================================================
776
951
  export namespace Plugin {
777
- /**
778
- * 插件提供的功能
779
- */
780
- export interface Features {
781
- commands: string[];
782
- components: string[];
783
- crons: string[];
784
- middlewares: string[];
785
- }
786
-
787
952
  /**
788
953
  * 生命周期事件
789
954
  */
@@ -805,8 +970,8 @@ export namespace Plugin {
805
970
  * 各个 Context 通过 declare module 扩展此接口
806
971
  */
807
972
  export interface Contexts extends Adapters {
808
- config: ConfigService;
809
- permission: PermissionService;
973
+ config: ConfigFeature;
974
+ permission: PermissionFeature;
810
975
  }
811
976
 
812
977
  /**
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
  */
@@ -327,6 +319,9 @@ export interface Tool {
327
319
 
328
320
  /** 工具标签(用于分类和过滤) */
329
321
  tags?: string[];
322
+
323
+ /** 触发关键词(用户消息包含这些词时优先选择此工具) */
324
+ keywords?: string[];
330
325
 
331
326
  /**
332
327
  * 命令配置(可选)
@@ -379,6 +374,7 @@ export interface ToolDefinition<TArgs extends Record<string, any> = Record<strin
379
374
  execute: (args: TArgs, context?: ToolContext) => MaybePromise<any>;
380
375
  source?: string;
381
376
  tags?: string[];
377
+ keywords?: string[];
382
378
  command?: Tool.CommandConfig | false;
383
379
  permissions?: string[];
384
380
  /** 支持的平台列表(不填则支持所有平台) */
@@ -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)