@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/plugin.ts CHANGED
@@ -8,14 +8,17 @@ import { EventEmitter } from "events";
8
8
  import { createRequire } from "module";
9
9
  import type { Database, Definition } from "@zhin.js/database";
10
10
  import { Schema } from "@zhin.js/schema";
11
- import type { Models, RegisteredAdapters } from "./types.js";
11
+ import type { Models, RegisteredAdapters, Tool, ToolContext } from "./types.js";
12
12
  import * as fs from "fs";
13
13
  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;
@@ -126,7 +138,7 @@ export interface Plugin extends Plugin.Extensions { }
126
138
  */
127
139
  export class Plugin extends EventEmitter<Plugin.Lifecycle> {
128
140
  static [contextsKey] = [] as string[];
129
-
141
+
130
142
  #cachedName?: string;
131
143
  adapters: (keyof Plugin.Contexts)[] = [];
132
144
  started = false;
@@ -157,10 +169,16 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
157
169
  };
158
170
  // 插件功能
159
171
  #middlewares: MessageMiddleware<RegisteredAdapter>[] = [this.#messageMiddleware];
160
-
172
+
173
+ // 本地工具存储(当 ToolService 不可用时使用)
174
+ #tools: Map<string, Tool> = new Map();
175
+
161
176
  // 统一的清理函数集合
162
177
  #disposables: Set<() => void | Promise<void>> = new Set();
163
-
178
+
179
+ // 记录当前插件向哪些 Feature 贡献了哪些 item
180
+ #featureContributions = new Map<string, Set<string>>();
181
+
164
182
  get middleware(): MessageMiddleware<RegisteredAdapter> {
165
183
  return compose<RegisteredAdapter>(this.#middlewares);
166
184
  }
@@ -169,7 +187,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
169
187
  */
170
188
  constructor(filePath: string = "", public parent?: Plugin) {
171
189
  super();
172
-
190
+
173
191
  // 增加 EventEmitter 监听器限制,避免警告
174
192
  // 因为插件可能注册多个命令/组件/中间件,每个都会添加 dispose 监听器
175
193
  this.setMaxListeners(50);
@@ -185,15 +203,24 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
185
203
  // 绑定方法以支持解构使用
186
204
  this.$bindMethods();
187
205
  }
188
-
206
+
189
207
  // 标记是否已绑定方法
190
208
  #methodsBound = false;
191
209
 
192
210
  /**
193
211
  * 添加中间件
194
212
  * 中间件用于处理消息流转
213
+ * @param middleware 中间件函数
195
214
  */
196
- addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>, name?: string) {
215
+ addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>): () => void {
216
+ if(this.parent){
217
+ const dispose= this.parent.addMiddleware(middleware);
218
+ this.#disposables.add(dispose);
219
+ return () => {
220
+ dispose();
221
+ this.#disposables.delete(dispose);
222
+ };
223
+ }
197
224
  this.#middlewares.push(middleware as MessageMiddleware<RegisteredAdapter>);
198
225
  const dispose = () => {
199
226
  remove(this.#middlewares, middleware);
@@ -203,22 +230,135 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
203
230
  return dispose;
204
231
  }
205
232
 
233
+ /**
234
+ * 添加工具
235
+ * 工具可以被 AI 服务调用,也会自动生成对应的命令
236
+ * @param tool 工具定义
237
+ * @param generateCommand 是否生成对应命令(默认 true)
238
+ * @returns 返回一个移除工具的函数
239
+ */
240
+ addTool(
241
+ tool: Tool,
242
+ generateCommand: boolean = true
243
+ ): () => void {
244
+ // 尝试使用 ToolFeature
245
+ const toolService = this.root.inject('tool' as any) as any;
246
+ if (toolService && typeof toolService.addTool === 'function') {
247
+ const dispose = toolService.addTool(tool, this.name, generateCommand);
248
+ this.#disposables.add(dispose);
249
+ return () => {
250
+ dispose();
251
+ this.#disposables.delete(dispose);
252
+ };
253
+ }
254
+
255
+ // 回退到本地存储
256
+ const toolWithSource: Tool = {
257
+ ...tool,
258
+ source: tool.source || `plugin:${this.name}`,
259
+ tags: [...(tool.tags || []), 'plugin', this.name],
260
+ };
261
+ this.#tools.set(tool.name, toolWithSource);
262
+ const dispose = () => {
263
+ this.#tools.delete(tool.name);
264
+ this.#disposables.delete(dispose);
265
+ };
266
+ this.#disposables.add(dispose);
267
+ return dispose;
268
+ }
269
+
270
+ /**
271
+ * 获取当前插件注册的所有工具
272
+ */
273
+ getTools(): Tool[] {
274
+ return Array.from(this.#tools.values());
275
+ }
276
+
277
+ /**
278
+ * 获取当前插件及所有子插件注册的工具
279
+ */
280
+ getAllTools(): Tool[] {
281
+ const tools: Tool[] = [...this.getTools()];
282
+ for (const child of this.children) {
283
+ tools.push(...child.getAllTools());
284
+ }
285
+ return tools;
286
+ }
287
+
288
+ /**
289
+ * 根据名称获取工具
290
+ */
291
+ getTool(name: string): Tool | undefined {
292
+ // 先在当前插件查找
293
+ const tool = this.#tools.get(name);
294
+ if (tool) return tool;
295
+ // 再在子插件中查找
296
+ for (const child of this.children) {
297
+ const childTool = child.getTool(name);
298
+ if (childTool) return childTool;
299
+ }
300
+ return undefined;
301
+ }
302
+
303
+ /**
304
+ * 收集所有可用的工具
305
+ * 优先使用 ToolService,否则回退到本地收集
306
+ */
307
+ collectAllTools(): Tool[] {
308
+ // 尝试使用 ToolService
309
+ const toolService = this.root.inject('tool' as any) as any;
310
+ if (toolService && typeof toolService.collectAll === 'function') {
311
+ return toolService.collectAll(this.root);
312
+ }
313
+
314
+ // 回退到本地收集
315
+ const tools: Tool[] = [];
316
+
317
+ // 收集插件树中的所有工具
318
+ const rootPlugin = this.root;
319
+ tools.push(...rootPlugin.getAllTools());
320
+
321
+ // 收集所有适配器的工具
322
+ for (const [name, context] of rootPlugin.contexts) {
323
+ const value = context.value;
324
+ if (value && typeof value === 'object' && 'getTools' in value) {
325
+ const adapter = value as Adapter;
326
+ tools.push(...adapter.getTools());
327
+ }
328
+ }
329
+
330
+ return tools;
331
+ }
332
+
206
333
  /**
207
334
  * 插件名称
208
335
  */
209
336
  get name(): string {
210
337
  if (this.#cachedName) return this.#cachedName;
211
338
 
212
- this.#cachedName = path
339
+ let name = path
213
340
  .relative(process.cwd(), this.filePath)
214
341
  .replace(/\?t=\d+$/, "")
215
342
  .replace(/\\/g, "/")
216
343
  .replace(/\/index\.(js|ts)x?$/, "")
217
- .replace(/\/(lib|src|dist)$/, "")
218
- .replace(/.*\/node_modules\//, "")
219
- .replace(/.*\//, "")
220
- .replace(/\.(js|ts)x?$/, "");
221
-
344
+ .replace(/\/(lib|src|dist)$/, "");
345
+
346
+ // 安全地提取 node_modules 后的包名或最后的文件名
347
+ const nodeModulesIndex = name.indexOf('node_modules/');
348
+ if (nodeModulesIndex !== -1) {
349
+ name = name.substring(nodeModulesIndex + 'node_modules/'.length);
350
+ }
351
+
352
+ // 提取最后一个路径段
353
+ const lastSlash = name.lastIndexOf('/');
354
+ if (lastSlash !== -1) {
355
+ name = name.substring(lastSlash + 1);
356
+ }
357
+
358
+ // 移除文件扩展名
359
+ name = name.replace(/\.(js|ts)x?$/, "");
360
+
361
+ this.#cachedName = name;
222
362
  return this.#cachedName;
223
363
  }
224
364
 
@@ -277,7 +417,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
277
417
  if (!this.#contextsIsReady(contexts)) return
278
418
  contextReadyCallback()
279
419
  }
280
- inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T]|undefined {
420
+ inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T] | undefined {
281
421
  const context = this.root.contexts.get(name as string);
282
422
  return context?.value as Plugin.Contexts[T];
283
423
  }
@@ -299,22 +439,16 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
299
439
  /**
300
440
  * 启动插件
301
441
  */
302
- async start(t?:number): Promise<void> {
442
+ async start(t?: number): Promise<void> {
303
443
  if (this.started) return;
304
444
  this.started = true; // 提前设置,防止重复启动
305
-
445
+
306
446
  // 启动所有服务
307
447
  for (const context of this.$contexts.values()) {
308
- if (typeof context.mounted === "function" && !context.value) {
309
- context.value = await context.mounted(this);
310
- }
311
- // 注册扩展方法到 Plugin.prototype
312
- if (context.extensions) {
313
- for (const [name, fn] of Object.entries(context.extensions)) {
314
- if (typeof fn === 'function') {
315
- Reflect.set(Plugin.prototype, name, fn);
316
- }
317
- }
448
+ if (typeof context.mounted === "function") {
449
+ const result = await context.mounted(this);
450
+ // 仅当 value 未预设时才赋值(mounted 始终执行,以支持副作用如设置内部引用)
451
+ if (!context.value) context.value = result;
318
452
  }
319
453
  this.dispatch('context.mounted', context.name)
320
454
  }
@@ -326,32 +460,163 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
326
460
  // 输出启动日志(使用 debug 级别,避免重复输出)
327
461
  // 只在根插件或重要插件时使用 info 级别
328
462
  if (!this.parent || this.name === 'setup') {
329
- this.logger.info(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
463
+ this.logger.info(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
330
464
  } else {
331
465
  this.logger.debug(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
332
466
  }
333
467
  }
334
468
  /**
335
- * 获取插件提供的功能
336
- * 从各个服务中获取数据
469
+ * 记录 Feature 贡献(由 Feature extensions 内部调用)
470
+ * @param featureName Feature 名称(如 'command')
471
+ * @param itemName item 标识(如命令 pattern)
337
472
  */
338
- get features(): Plugin.Features {
339
- const commandService = this.inject('command');
340
- const componentService = this.inject('component')
341
- const cronService = this.inject('cron');
342
-
343
- return {
344
- commands: commandService ? commandService.items.map(c => c.pattern) : [],
345
- components: componentService ? componentService.getAllNames() : [],
346
- crons: cronService ? cronService.items.map(c => c.cronExpression) : [],
347
- middlewares: this.#middlewares.map((m, i) => m.name || `middleware_${i}`),
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
+ }
479
+
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
+ }
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
+ }
348
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;
349
614
  }
350
615
 
351
616
  info(): Record<string, any> {
352
617
  return {
353
618
  [this.name]: {
354
- features: this.features,
619
+ features: this.getFeatures(),
355
620
  children: this.children.map(child => child.info())
356
621
  }
357
622
  }
@@ -387,12 +652,12 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
387
652
  // 清理 contexts Map
388
653
  this.$contexts.clear();
389
654
 
390
- // 清空缓存的名称
655
+ // 清空缓存的名称
391
656
  this.#cachedName = undefined;
392
657
 
393
658
  // 触发 dispose 事件
394
659
  this.emit("dispose");
395
-
660
+
396
661
  // 执行所有清理函数
397
662
  for (const dispose of this.#disposables) {
398
663
  try {
@@ -402,14 +667,17 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
402
667
  }
403
668
  }
404
669
  this.#disposables.clear();
405
-
670
+
406
671
  // 清理 middlewares 数组(保留默认的消息中间件)
407
672
  this.#middlewares.length = 1;
408
-
673
+
674
+ // 清理 feature 贡献记录
675
+ this.#featureContributions.clear();
676
+
409
677
  if (this.parent) {
410
678
  remove(this.parent?.children, this);
411
679
  }
412
-
680
+
413
681
  // 从全局 loadedModules Map 中移除,防止内存泄漏
414
682
  if (this.filePath) {
415
683
  try {
@@ -419,7 +687,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
419
687
  // 文件可能已不存在,忽略错误
420
688
  }
421
689
  }
422
-
690
+
423
691
  this.removeAllListeners();
424
692
  this.logger.debug(`Plugin "${this.name}" stopped`);
425
693
  }
@@ -478,13 +746,44 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
478
746
  // ============================================================================
479
747
 
480
748
  /**
481
- * 注册上下文
749
+ * 注册上下文(支持 Feature 实例或传统 Context 对象)
482
750
  */
483
- 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;
484
775
  if (!Plugin[contextsKey].includes(context.name as string)) {
485
776
  Plugin[contextsKey].push(context.name as string);
486
777
  }
487
778
  this.logger.debug(`Context "${context.name as string}" provided`);
779
+ // 注册扩展方法到 Plugin.prototype
780
+ if (context.extensions) {
781
+ for (const [name, fn] of Object.entries(context.extensions)) {
782
+ if (typeof fn === 'function') {
783
+ Reflect.set(Plugin.prototype, name, fn);
784
+ }
785
+ }
786
+ }
488
787
  this.$contexts.set(context.name as string, context);
489
788
  return this;
490
789
  }
@@ -496,7 +795,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
496
795
  /**
497
796
  * 导入插件
498
797
  */
499
- async import(entry: string,t?:number): Promise<Plugin> {
798
+ async import(entry: string, t?: number): Promise<Plugin> {
500
799
  if (!entry) throw new Error(`Plugin entry not found: ${entry}`);
501
800
  const resolved = resolveEntry(path.isAbsolute(entry) ?
502
801
  entry :
@@ -510,7 +809,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
510
809
 
511
810
  // 避免重复加载同一路径的插件
512
811
  const normalized = realPath.replace(/\?t=\d+$/, '').replace(/\\/g, '/');
513
- const existing = this.children.find(child =>
812
+ const existing = this.children.find(child =>
514
813
  child.filePath.replace(/\?t=\d+$/, '').replace(/\\/g, '/') === normalized
515
814
  );
516
815
  if (existing) {
@@ -533,7 +832,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
533
832
  */
534
833
  async reload(plugin: Plugin = this): Promise<void> {
535
834
  this.logger.info(`Plugin "${plugin.name}" reloading...`);
536
- const now=Date.now();
835
+ const now = Date.now();
537
836
  if (!plugin.parent) {
538
837
  // 根插件重载 = 退出进程(由 CLI 重启)
539
838
  return process.exit(51);
@@ -580,7 +879,8 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
580
879
  static #coreMethods = new Set([
581
880
  'addMiddleware', 'useContext', 'inject', 'contextIsReady',
582
881
  'start', 'stop', 'onMounted', 'onDispose',
583
- 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info'
882
+ 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info',
883
+ 'recordFeatureContribution', 'getFeatures'
584
884
  ]);
585
885
 
586
886
  /**
@@ -589,7 +889,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
589
889
  $bindMethods(): void {
590
890
  if (this.#methodsBound) return;
591
891
  this.#methodsBound = true;
592
-
892
+
593
893
  const proto = Object.getPrototypeOf(this);
594
894
  for (const key of Plugin.#coreMethods) {
595
895
  const value = proto[key];
@@ -622,7 +922,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
622
922
 
623
923
  const plugin = new Plugin(realPath, parent);
624
924
  plugin.fileHash = getFileHash(entryFile);
625
-
925
+
626
926
  // 先记录,防止循环依赖时重复加载
627
927
  loadedModules.set(realPath, plugin);
628
928
 
@@ -649,16 +949,6 @@ export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts
649
949
  // 类型定义
650
950
  // ============================================================================
651
951
  export namespace Plugin {
652
- /**
653
- * 插件提供的功能
654
- */
655
- export interface Features {
656
- commands: string[];
657
- components: string[];
658
- crons: string[];
659
- middlewares: string[];
660
- }
661
-
662
952
  /**
663
953
  * 生命周期事件
664
954
  */
@@ -680,14 +970,14 @@ export namespace Plugin {
680
970
  * 各个 Context 通过 declare module 扩展此接口
681
971
  */
682
972
  export interface Contexts extends Adapters {
683
- config: ConfigService;
684
- permission: PermissionService;
973
+ config: ConfigFeature;
974
+ permission: PermissionFeature;
685
975
  }
686
-
976
+
687
977
  /**
688
978
  * Service 扩展方法类型
689
979
  * 这些方法由各个 Context 的 extensions 提供
690
980
  * 在 Plugin.start() 时自动注册到 Plugin.prototype
691
981
  */
692
- export interface Extensions {}
982
+ export interface Extensions { }
693
983
  }