@zhin.js/core 1.0.16 → 1.0.18

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 (118) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/REFACTORING_COMPLETE.md +178 -0
  3. package/REFACTORING_STATUS.md +263 -0
  4. package/lib/adapter.d.ts +44 -19
  5. package/lib/adapter.d.ts.map +1 -1
  6. package/lib/adapter.js +81 -50
  7. package/lib/adapter.js.map +1 -1
  8. package/lib/bot.d.ts +7 -12
  9. package/lib/bot.d.ts.map +1 -1
  10. package/lib/built/adapter-process.d.ts +36 -0
  11. package/lib/built/adapter-process.d.ts.map +1 -0
  12. package/lib/built/adapter-process.js +77 -0
  13. package/lib/built/adapter-process.js.map +1 -0
  14. package/lib/built/command.d.ts +46 -0
  15. package/lib/built/command.d.ts.map +1 -0
  16. package/lib/built/command.js +54 -0
  17. package/lib/built/command.js.map +1 -0
  18. package/lib/built/component.d.ts +42 -0
  19. package/lib/built/component.d.ts.map +1 -0
  20. package/lib/built/component.js +66 -0
  21. package/lib/built/component.js.map +1 -0
  22. package/lib/built/config.d.ts +31 -0
  23. package/lib/built/config.d.ts.map +1 -0
  24. package/lib/built/config.js +141 -0
  25. package/lib/built/config.js.map +1 -0
  26. package/lib/built/cron.d.ts +53 -0
  27. package/lib/built/cron.d.ts.map +1 -0
  28. package/lib/built/cron.js +79 -0
  29. package/lib/built/cron.js.map +1 -0
  30. package/lib/built/database.d.ts +17 -0
  31. package/lib/built/database.d.ts.map +1 -0
  32. package/lib/built/database.js +28 -0
  33. package/lib/built/database.js.map +1 -0
  34. package/lib/{permissions.d.ts → built/permission.d.ts} +5 -10
  35. package/lib/built/permission.d.ts.map +1 -0
  36. package/lib/{permissions.js → built/permission.js} +11 -10
  37. package/lib/built/permission.js.map +1 -0
  38. package/lib/command.d.ts +18 -7
  39. package/lib/command.d.ts.map +1 -1
  40. package/lib/command.js +36 -15
  41. package/lib/command.js.map +1 -1
  42. package/lib/component.d.ts +1 -1
  43. package/lib/component.d.ts.map +1 -1
  44. package/lib/component.js.map +1 -1
  45. package/lib/cron.d.ts +4 -12
  46. package/lib/cron.d.ts.map +1 -1
  47. package/lib/cron.js +33 -64
  48. package/lib/cron.js.map +1 -1
  49. package/lib/index.d.ts +11 -3
  50. package/lib/index.d.ts.map +1 -1
  51. package/lib/index.js +14 -4
  52. package/lib/index.js.map +1 -1
  53. package/lib/jsx-runtime.d.ts +2 -2
  54. package/lib/jsx.d.ts +2 -3
  55. package/lib/jsx.d.ts.map +1 -1
  56. package/lib/jsx.js.map +1 -1
  57. package/lib/message.d.ts +4 -7
  58. package/lib/message.d.ts.map +1 -1
  59. package/lib/message.js.map +1 -1
  60. package/lib/plugin.d.ts +164 -51
  61. package/lib/plugin.d.ts.map +1 -1
  62. package/lib/plugin.js +520 -137
  63. package/lib/plugin.js.map +1 -1
  64. package/lib/prompt.d.ts +1 -1
  65. package/lib/prompt.d.ts.map +1 -1
  66. package/lib/prompt.js +2 -1
  67. package/lib/prompt.js.map +1 -1
  68. package/lib/types.d.ts +33 -33
  69. package/lib/types.d.ts.map +1 -1
  70. package/lib/utils.d.ts +16 -1
  71. package/lib/utils.d.ts.map +1 -1
  72. package/lib/utils.js +166 -66
  73. package/lib/utils.js.map +1 -1
  74. package/package.json +17 -11
  75. package/src/adapter.ts +131 -80
  76. package/src/bot.ts +8 -13
  77. package/src/built/adapter-process.ts +77 -0
  78. package/src/built/command.ts +102 -0
  79. package/src/built/component.ts +111 -0
  80. package/src/built/config.ts +126 -0
  81. package/src/built/cron.ts +140 -0
  82. package/src/built/database.ts +38 -0
  83. package/src/{permissions.ts → built/permission.ts} +9 -12
  84. package/src/command.ts +48 -20
  85. package/src/component.ts +2 -3
  86. package/src/cron.ts +35 -70
  87. package/src/index.ts +15 -5
  88. package/src/jsx.ts +2 -3
  89. package/src/message.ts +3 -4
  90. package/src/plugin.ts +671 -184
  91. package/src/prompt.ts +4 -3
  92. package/src/types.ts +41 -35
  93. package/src/utils.ts +418 -296
  94. package/test/minimal-bot.ts +31 -0
  95. package/test/stress-test.ts +123 -0
  96. package/tests/command.test.ts +124 -44
  97. package/ASYNC-JSX-SUPPORT.md +0 -173
  98. package/lib/app.d.ts +0 -191
  99. package/lib/app.d.ts.map +0 -1
  100. package/lib/app.js +0 -604
  101. package/lib/app.js.map +0 -1
  102. package/lib/config.d.ts +0 -54
  103. package/lib/config.d.ts.map +0 -1
  104. package/lib/config.js +0 -308
  105. package/lib/config.js.map +0 -1
  106. package/lib/log-transport.d.ts +0 -37
  107. package/lib/log-transport.d.ts.map +0 -1
  108. package/lib/log-transport.js +0 -136
  109. package/lib/log-transport.js.map +0 -1
  110. package/lib/permissions.d.ts.map +0 -1
  111. package/lib/permissions.js.map +0 -1
  112. package/src/app.ts +0 -772
  113. package/src/config.ts +0 -397
  114. package/src/log-transport.ts +0 -163
  115. package/tests/app.test.ts +0 -265
  116. package/tests/permissions.test.ts +0 -358
  117. package/tests/plugin.test.ts +0 -234
  118. package/tests/prompt.test.ts +0 -223
package/src/plugin.ts CHANGED
@@ -1,212 +1,699 @@
1
+ /**
2
+ * Plugin 类 - 基于 zhinjs/next 的 Hooks 实现
3
+ * 移除 Dependency 继承,使用 AsyncLocalStorage 管理上下文
4
+ */
5
+
6
+ import { AsyncLocalStorage } from "async_hooks";
7
+ import { EventEmitter } from "events";
8
+ import { createRequire } from "module";
9
+ import type { Database, Definition } from "@zhin.js/database";
10
+ import { Schema } from "@zhin.js/schema";
11
+ import type { Models, RegisteredAdapters } from "./types.js";
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { fileURLToPath } from "url";
15
+ import logger, { Logger } from "@zhin.js/logger";
16
+ import { compose, remove, resolveEntry } from "./utils.js";
17
+ import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, ConfigService, PermissionService, SendOptions } from "./types.js";
18
+ import { Adapter } from "./adapter.js";
19
+ import { createHash } from "crypto";
20
+ const contextsKey = Symbol("contexts");
21
+ const loadedModules = new Map<string, Plugin>(); // 记录已加载的模块
22
+ const require = createRequire(import.meta.url);
23
+
24
+
25
+ export type SideEffect<A extends (keyof Plugin.Contexts)[]> = {
26
+ (...args: ContextList<A>): MaybePromise<void | DisposeFn<ContextList<A>>>;
27
+ finished?: boolean
28
+ }
29
+ export type DisposeFn<A> = (context: ArrayItem<A>) => MaybePromise<void>
30
+ export type ContextList<CS extends (keyof Plugin.Contexts)[]> = CS extends [infer L, ...infer R] ? R extends (keyof Plugin.Contexts)[] ? [ContextItem<L>, ...ContextList<R>] : never[] : never[]
31
+ type ContextItem<L> = L extends keyof Plugin.Contexts ? Plugin.Contexts[L] : never
1
32
  // ============================================================================
2
- // 插件类型定义(定义插件中间件、生命周期、命令、组件等核心类型)
33
+ // AsyncLocalStorage 上下文
3
34
  // ============================================================================
4
35
 
36
+ export const storage = new AsyncLocalStorage<Plugin>();
37
+
38
+ /**
39
+ * 获取当前文件路径(调用者)
40
+ */
41
+ function getCurrentFile(metaUrl = import.meta.url): string {
42
+ const previousPrepareStackTrace = Error.prepareStackTrace;
43
+ Error.prepareStackTrace = function (_, stack) {
44
+ return stack;
45
+ };
46
+ const stack = new Error().stack as unknown as NodeJS.CallSite[];
47
+ Error.prepareStackTrace = previousPrepareStackTrace;
48
+ const stackFiles = Array.from(
49
+ new Set(stack.map((site) => site.getFileName()))
50
+ );
51
+ const idx = stackFiles.findIndex(
52
+ (f) => f === fileURLToPath(metaUrl) || f === metaUrl
53
+ );
54
+ const result = stackFiles[idx + 1];
55
+ if (!result) throw new Error("Cannot resolve current file path");
56
+ try {
57
+ return fileURLToPath(result);
58
+ } catch {
59
+ return result;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * usePlugin - 获取或创建当前插件实例
65
+ * 类似 React Hooks 的设计,根据调用文件自动创建插件树
66
+ */
67
+ export function usePlugin(): Plugin {
68
+ const callerFile = getCurrentFile();
69
+ const parentPlugin = storage.getStore();
70
+ const newPlugin = new Plugin(callerFile, parentPlugin);
71
+ storage.enterWith(newPlugin);
72
+ return newPlugin;
73
+ }
74
+
75
+ /**
76
+ * getPlugin - 获取当前 AsyncLocalStorage 中的插件实例
77
+ * 用于 extensions 等场景,不创建新插件
78
+ */
79
+ export function getPlugin(): Plugin {
80
+ const plugin = storage.getStore();
81
+ if (!plugin) {
82
+ throw new Error('getPlugin() must be called within a plugin context');
83
+ }
84
+ return plugin;
85
+ }
86
+
87
+ // ============================================================================
88
+ // 文件工具函数
89
+ // ============================================================================
5
90
 
6
- import * as fs from 'fs';
7
- import {AdapterMessage, BeforeSendHandler,MessageMiddleware, RegisteredAdapter, SendOptions} from "./types.js";
8
- import { PermissionItem,PermissionChecker } from './permissions.js';
9
- import {Message} from './message.js'
10
- import { Schema } from '@zhin.js/hmr';
11
- import {Dependency, Logger,} from "@zhin.js/hmr";
12
- import {App} from "./app";
13
- import {MessageCommand} from "./command.js";
14
- import {Component, renderComponents} from "./component.js";
15
- import { PluginError, MessageError, errorManager } from './errors.js';
16
- import {remove} from "./utils.js";
17
- import {Prompt} from "./prompt.js";
18
- import { Definition } from '@zhin.js/database';
19
- import { Cron} from './cron.js';
91
+ /**
92
+ * 获取文件 Hash(用于检测变更)
93
+ */
94
+ function getFileHash(filePath: string): string {
95
+ try {
96
+ const content = fs.readFileSync(filePath, "utf-8");
97
+ return createHash("md5")
98
+ .update(content)
99
+ .digest("hex");
100
+ } catch {
101
+ return "";
102
+ }
103
+ }
20
104
 
105
+ /**
106
+ * 监听文件变化
107
+ */
108
+ function watchFile(filePath: string, callback: () => void): () => void {
109
+ try {
110
+ const watcher = fs.watch(filePath, callback);
111
+ watcher.on("error", (_error) => { });
112
+ return () => watcher.close();
113
+ } catch (error) {
114
+ return () => { };
115
+ }
116
+ }
21
117
 
22
118
  // ============================================================================
23
- // Plugin 类(插件的生命周期、命令/中间件/组件/定时任务等管理)
119
+ // Plugin
24
120
  // ============================================================================
25
121
 
122
+ export interface Plugin extends Plugin.Extensions { }
26
123
  /**
27
- * 插件类:继承自 Dependency,提供机器人特定功能与生命周期管理。
28
- * 支持命令注册、中间件、组件、定时任务、模型等。
124
+ * Plugin 类 - 核心插件系统
125
+ * 直接继承 EventEmitter
29
126
  */
30
- export class Plugin extends Dependency<Plugin> {
31
- middlewares: MessageMiddleware<RegisteredAdapter>[] = [];
32
- components: Map<string, Component<any>> = new Map();
33
- permissions: PermissionItem<RegisteredAdapter>[]=[];
34
- definitions: Map<string,Definition<any>>=new Map();
35
- commands:MessageCommand[]=[];
36
- crons:Cron[]=[];
37
- #logger?:Logger
38
- /**
39
- * 构造函数:初始化插件,注册消息事件、命令中间件、资源清理等
40
- * @param parent 所属 App 实例
41
- * @param name 插件名
42
- * @param filePath 插件文件路径
43
- */
44
- constructor(parent: Dependency<Plugin>, name: string, filePath: string) {
45
- // 在测试环境中跳过文件检查
46
- if (process.env.NODE_ENV !== 'test' && !filePath.startsWith('/mock/')) {
47
- filePath = fs.realpathSync(filePath);
127
+ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
128
+ static [contextsKey] = [] as string[];
129
+
130
+ #cachedName?: string;
131
+ adapters: (keyof Plugin.Contexts)[] = [];
132
+ started = false;
133
+
134
+ // 上下文存储
135
+ $contexts: Map<string, Context<any>> = new Map();
136
+
137
+
138
+ // 子插件
139
+ children: Plugin[] = [];
140
+
141
+ // 文件信息
142
+ filePath: string;
143
+ fileHash: string = "";
144
+
145
+ // Logger
146
+ logger: Logger;
147
+
148
+ #messageMiddleware: MessageMiddleware<RegisteredAdapter> = async (message, next) => {
149
+ const commandService = this.inject('command');
150
+ if (!commandService) return await next();
151
+ const result = await commandService.handle(message, this);
152
+ if (!result) return await next();
153
+ const adapter = this.inject(message.$adapter) as Adapter;
154
+ if (!adapter || !(adapter instanceof Adapter)) return await next();
155
+ await adapter.emit('call.sendMessage', message.$bot, {
156
+ context: message.$adapter,
157
+ bot: message.$bot,
158
+ content: result,
159
+ id: message.$channel.id,
160
+ type: message.$channel.type,
161
+ });
162
+ await next();
163
+ };
164
+ // 插件功能
165
+ #middlewares: MessageMiddleware<RegisteredAdapter>[] = [this.#messageMiddleware];
166
+
167
+ // 统一的清理函数集合
168
+ #disposables: Set<() => void | Promise<void>> = new Set();
169
+
170
+ get middleware(): MessageMiddleware<RegisteredAdapter> {
171
+ return compose<RegisteredAdapter>(this.#middlewares);
172
+ }
173
+ /**
174
+ * 构造函数
175
+ */
176
+ constructor(filePath: string = "", public parent?: Plugin) {
177
+ super();
178
+
179
+ // 增加 EventEmitter 监听器限制,避免警告
180
+ // 因为插件可能注册多个命令/组件/中间件,每个都会添加 dispose 监听器
181
+ this.setMaxListeners(50);
182
+
183
+ this.filePath = filePath.replace(/\?t=\d+$/, "");
184
+ this.logger = this.name ? logger.getLogger(this.name) : logger;
185
+
186
+ // 自动添加到父节点
187
+ if (parent && !parent.children.includes(this)) {
188
+ parent.children.push(this);
189
+ }
190
+
191
+ // 绑定方法以支持解构使用
192
+ this.$bindMethods();
193
+ }
194
+
195
+ // 标记是否已绑定方法
196
+ #methodsBound = false;
197
+
198
+ /**
199
+ * 添加中间件
200
+ * 中间件用于处理消息流转
201
+ */
202
+ addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>, name?: string) {
203
+ this.#middlewares.push(middleware as MessageMiddleware<RegisteredAdapter>);
204
+ const dispose = () => {
205
+ remove(this.#middlewares, middleware);
206
+ this.#disposables.delete(dispose);
207
+ };
208
+ this.#disposables.add(dispose);
209
+ return dispose;
210
+ }
211
+
212
+ /**
213
+ * 插件名称
214
+ */
215
+ get name(): string {
216
+ if (this.#cachedName) return this.#cachedName;
217
+
218
+ this.#cachedName = path
219
+ .relative(process.cwd(), this.filePath)
220
+ .replace(/\?t=\d+$/, "")
221
+ .replace(/\\/g, "/")
222
+ .replace(/\/index\.(js|ts)x?$/, "")
223
+ .replace(/\/(lib|src|dist)$/, "")
224
+ .replace(/.*\/node_modules\//, "")
225
+ .replace(/.*\//, "")
226
+ .replace(/\.(js|ts)x?$/, "");
227
+
228
+ return this.#cachedName;
229
+ }
230
+
231
+ /**
232
+ * 根插件
233
+ */
234
+ get root(): Plugin {
235
+ if (!this.parent) return this;
236
+ return this.parent.root;
237
+ }
238
+ get contexts(): Map<string, Context> {
239
+ const result = new Map<string, Context>();
240
+ for (const [key, value] of this.$contexts) {
241
+ result.set(key, value);
242
+ }
243
+ for (const child of this.children) {
244
+ for (const [key, value] of child.contexts) {
245
+ result.set(key, value);
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+
251
+
252
+ useContext<T extends (keyof Plugin.Contexts)[]>(...args: [...T, SideEffect<T>]) {
253
+ const contexts = args.slice(0, -1) as T
254
+ const sideEffect = args[args.length - 1] as SideEffect<T>
255
+ const contextReadyCallback = async () => {
256
+ if (sideEffect.finished) return;
257
+ sideEffect.finished = true;
258
+ const args = contexts.map(item => this.inject(item))
259
+ const dispose = await sideEffect(...args as ContextList<T>)
260
+ if (!dispose) return;
261
+ const disposeFn = async (name: keyof Plugin.Contexts) => {
262
+ if (contexts.includes(name)) {
263
+ await dispose(this.inject(name) as any)
48
264
  }
49
- super(parent, name, filePath);
50
- this.logger.debug(`plugin ${name} created at ${filePath}`);
51
- // 绑定消息事件,自动分发到命令和中间件
52
- // 发送前渲染组件
53
- this.beforeSend((options)=>renderComponents(this.components,options))
54
- // 资源清理:卸载时清空模型、定时任务等
55
- this.on('dispose',()=>{
56
- for(const name of this.definitions.keys()){
57
- this.app.database?.models.delete(name);
58
- }
59
- this.definitions.clear();
60
- for(const cron of this.crons){
61
- cron.dispose();
62
- }
63
- this.crons.length = 0;
64
- });
65
- // 挂载时启动定时任务
66
- this.on('mounted',()=>{
67
- for(const cron of this.crons){
68
- cron.run();
69
- }
70
- });
71
- }
72
- get config(){
73
- return this.app.getConfig(this.name as string) as Record<string,any>
74
- }
75
- defineSchema<S extends Schema>(rules: S): S {
76
- const result= super.defineSchema(rules);
77
- this.app.changeSchema(this.name as string, this.schema);
78
-
79
- return result;
80
- }
81
- set config(newConfig:Record<string,any>){
82
- this.app.setConfig(this.name,newConfig);
83
- }
84
- addPermit<T extends RegisteredAdapter>(name:string|RegExp,check:PermissionChecker<T>){
85
- this.permissions.push({name,check});
86
- return this;
87
- }
88
- getPermit<T extends RegisteredAdapter>(name:string):PermissionItem<T>|undefined{
89
- return this.app.permissions.get(name);
90
- }
91
- cron(cronExpression:string,callback:()=>void){
92
- const cronJob = new Cron(cronExpression,callback);
93
- this.crons.push(cronJob);
94
- return this;
95
- }
96
- async #runMiddlewares(message: Message, index: number): Promise<void> {
97
- const middlewareList=[...this.app.middlewares,...this.middlewares]
98
- if (index >= middlewareList.length) return
99
-
100
- const middleware = middlewareList[index]
101
-
102
- try {
103
- await middleware(message, () => this.#runMiddlewares(message, index + 1))
104
- } catch (error) {
105
- throw new PluginError(
106
- `中间件执行失败: ${(error as Error).message}`,
107
- this.name,
108
- { middlewareIndex: index, originalError: error }
109
- )
265
+ this.off('context.dispose', disposeFn)
266
+ sideEffect.finished = false;
267
+ }
268
+ this.on('context.dispose', disposeFn)
269
+ // 确保 dispose 时清理监听器(只注册一次)
270
+ const cleanupOnDispose = () => {
271
+ this.off('context.dispose', disposeFn)
272
+ dispose(this.inject(args[0] as any) as any)
273
+ }
274
+ this.once('dispose', cleanupOnDispose)
275
+ }
276
+ const onContextMounted = async (name: keyof Plugin.Contexts) => {
277
+ if (!this.#contextsIsReady(contexts) || !(contexts).includes(name)) return
278
+ await contextReadyCallback()
279
+ }
280
+ this.on('context.mounted', onContextMounted)
281
+ // 插件销毁时移除 context.mounted 监听器
282
+ this.once('dispose', () => this.off('context.mounted', onContextMounted))
283
+ if (!this.#contextsIsReady(contexts)) return
284
+ contextReadyCallback()
285
+ }
286
+ inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T]|undefined {
287
+ const context = this.root.contexts.get(name as string);
288
+ return context?.value as Plugin.Contexts[T];
289
+ }
290
+ #contextsIsReady<CS extends (keyof Plugin.Contexts)[]>(contexts: CS) {
291
+ if (!contexts.length) return true
292
+ return contexts.every(name => this.contextIsReady(name))
293
+ }
294
+ contextIsReady<T extends keyof Plugin.Contexts>(name: T) {
295
+ try {
296
+ return !!this.inject(name)
297
+ } catch {
298
+ return false
299
+ }
300
+ }
301
+ // ============================================================================
302
+ // 生命周期方法
303
+ // ============================================================================
304
+
305
+ /**
306
+ * 启动插件
307
+ */
308
+ async start(t?:number): Promise<void> {
309
+ if (this.started) return;
310
+ this.started = true; // 提前设置,防止重复启动
311
+
312
+ // 启动所有服务
313
+ for (const context of this.$contexts.values()) {
314
+ if (typeof context.mounted === "function" && !context.value) {
315
+ context.value = await context.mounted(this);
316
+ }
317
+ // 注册扩展方法到 Plugin.prototype
318
+ if (context.extensions) {
319
+ for (const [name, fn] of Object.entries(context.extensions)) {
320
+ if (typeof fn === 'function') {
321
+ Reflect.set(Plugin.prototype, name, fn);
322
+ }
110
323
  }
324
+ }
325
+ this.dispatch('context.mounted', context.name)
111
326
  }
112
- defineModel<S extends Record<string,any>>(name:string,definition:Definition<S>){
113
- this.definitions.set(name,definition);
114
- return this;
327
+ await this.broadcast("mounted");
328
+ // 先启动子插件,再打印当前插件启动日志
329
+ for (const child of this.children) {
330
+ await child.start(t);
115
331
  }
116
- beforeSend(handler:BeforeSendHandler){
117
- this.before('message.send',handler)
332
+ // 输出启动日志(使用 debug 级别,避免重复输出)
333
+ // 只在根插件或重要插件时使用 info 级别
334
+ if (!this.parent || this.name === 'setup') {
335
+ this.logger.info(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
336
+ } else {
337
+ this.logger.debug(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
118
338
  }
119
- before(event:string,listener:(...args:any[])=>any){
120
- this.on(`before-${event}`,listener)
339
+ }
340
+ /**
341
+ * 获取插件提供的功能
342
+ * 从各个服务中获取数据
343
+ */
344
+ get features(): Plugin.Features {
345
+ const commandService = this.inject('command');
346
+ const componentService = this.inject('component')
347
+ const cronService = this.inject('cron');
348
+
349
+ return {
350
+ commands: commandService ? commandService.items.map(c => c.pattern) : [],
351
+ components: componentService ? componentService.getAllNames() : [],
352
+ crons: cronService ? cronService.items.map(c => c.cronExpression) : [],
353
+ middlewares: this.#middlewares.map((m, i) => m.name || `middleware_${i}`),
354
+ };
355
+ }
356
+
357
+ info(): Record<string, any> {
358
+ return {
359
+ [this.name]: {
360
+ features: this.features,
361
+ children: this.children.map(child => child.info())
362
+ }
121
363
  }
122
- /** 获取所属的App实例 */
123
- get app(): App {
124
- return this.parent as App;
364
+ }
365
+
366
+ /**
367
+ * 停止插件
368
+ */
369
+ async stop(): Promise<void> {
370
+ if (!this.started) return;
371
+ this.logger.debug(`Stopping plugin "${this.name}"`);
372
+ this.started = false;
373
+
374
+ // 停止子插件
375
+ for (const child of this.children) {
376
+ await child.stop();
125
377
  }
126
- get logger(): Logger {
127
- if(this.#logger) return this.#logger
128
- const names = [];
129
- let temp=this as Dependency<Plugin>
130
- while(temp.parent){
131
- names.unshift(temp.name)
132
- temp=temp.parent
133
- }
134
- return temp.getLogger(names.join('/'))
135
- }
136
- /** 添加组件 */
137
- addComponent<T=any>(component:Component<T>){
138
- this.components.set(component.name,component);
139
- }
140
- /** 添加中间件 */
141
- addCommand(command:MessageCommand){
142
- this.commands.push(command);
143
- this.dispatch('command.add',command);
144
- }
145
- /** 添加中间件 */
146
- addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>) {
147
- this.middlewares.push(middleware);
148
- this.dispatch('middleware.add',middleware)
149
- return ()=>{
150
- remove(this.middlewares,middleware)
378
+ this.children = [];
379
+
380
+ // 停止服务
381
+ for (const [name, context] of this.$contexts) {
382
+ remove(Plugin[contextsKey], name);
383
+ // 移除扩展方法
384
+ if (context.extensions) {
385
+ for (const key of Object.keys(context.extensions)) {
386
+ delete (Plugin.prototype as any)[key];
151
387
  }
388
+ }
389
+ if (typeof context.dispose === "function") {
390
+ await context.dispose(context.value);
391
+ }
392
+ }
393
+ // 清理 contexts Map
394
+ this.$contexts.clear();
395
+
396
+ // 清空缓存的名称
397
+ this.#cachedName = undefined;
398
+
399
+ // 触发 dispose 事件
400
+ this.emit("dispose");
401
+
402
+ // 执行所有清理函数
403
+ for (const dispose of this.#disposables) {
404
+ try {
405
+ await dispose();
406
+ } catch (e) {
407
+ this.logger.warn(`Dispose callback failed: ${e}`);
408
+ }
409
+ }
410
+ this.#disposables.clear();
411
+
412
+ // 清理 middlewares 数组(保留默认的消息中间件)
413
+ this.#middlewares.length = 1;
414
+
415
+ if (this.parent) {
416
+ remove(this.parent?.children, this);
152
417
  }
153
- prompt<P extends RegisteredAdapter>(message:Message<AdapterMessage<P>>){
154
- return new Prompt<P>(this,message)
418
+
419
+ // 从全局 loadedModules Map 中移除,防止内存泄漏
420
+ if (this.filePath) {
421
+ try {
422
+ const realPath = fs.realpathSync(this.filePath);
423
+ loadedModules.delete(realPath);
424
+ } catch {
425
+ // 文件可能已不存在,忽略错误
426
+ }
155
427
  }
428
+
429
+ this.removeAllListeners();
430
+ this.logger.debug(`Plugin "${this.name}" stopped`);
431
+ }
156
432
 
433
+ // ============================================================================
434
+ // 生命周期钩子
435
+ // ============================================================================
157
436
 
437
+ onMounted(callback: () => void | Promise<void>): void {
438
+ this.on("mounted", callback);
439
+ }
158
440
 
159
- /** 发送消息 */
160
- async sendMessage(options: SendOptions): Promise<void> {
161
- try {
162
- await this.app.sendMessage(options);
163
- } catch (error) {
164
- const messageError = new MessageError(
165
- `发送消息失败: ${(error as Error).message}`,
166
- undefined,
167
- (options as any).channel_id,
168
- { pluginName: this.name, sendOptions: options, originalError: error }
169
- )
170
-
171
- await errorManager.handle(messageError)
172
- throw messageError
173
- }
441
+ onDispose(callback: () => void | Promise<void>): () => void {
442
+ this.#disposables.add(callback);
443
+ return () => {
444
+ this.#disposables.delete(callback);
445
+ };
446
+ }
447
+
448
+ // ============================================================================
449
+ // 事件广播
450
+ // ============================================================================
451
+
452
+ /**
453
+ * dispatch - 向上冒泡到父插件,或在根节点广播
454
+ */
455
+ async dispatch<K extends keyof Plugin.Lifecycle>(
456
+ name: K,
457
+ ...args: Plugin.Lifecycle[K]
458
+ ): Promise<void> {
459
+ if (this.parent) {
460
+ return this.parent.dispatch(name, ...args);
174
461
  }
175
- recallMessage(adapter:string,bot:string,id:string){
176
- try{
177
- this.app.recallMessage(adapter,bot,id)
178
- }catch(error){
179
- const messageError = new MessageError(
180
- `撤回消息失败: ${(error as Error).message}`,
181
- id,
182
- undefined,
183
- { originalError: error }
184
- )
185
- errorManager.handle(messageError)
186
- throw messageError
187
- }
462
+ return this.broadcast(name, ...args);
463
+ }
464
+
465
+ /**
466
+ * broadcast - 向下广播到所有子插件
467
+ */
468
+ async broadcast<K extends keyof Plugin.Lifecycle>(
469
+ name: K,
470
+ ...args: Plugin.Lifecycle[K]
471
+ ): Promise<void> {
472
+ const listeners = this.listeners(name) as ((...args: any[]) => any)[];
473
+ for (const listener of listeners) {
474
+ await listener(...args);
188
475
  }
189
476
 
190
- /** 销毁插件 */
191
- dispose(): void {
192
- try {
193
- // 移除所有中间件
194
- for (const middleware of this.middlewares) {
195
- this.dispatch('middleware.remove', middleware)
196
- }
197
- this.middlewares = []
198
-
199
- // 调用父类的dispose方法
200
- super.dispose()
201
- } catch (error) {
202
- const pluginError = new PluginError(
203
- `插件销毁失败: ${(error as Error).message}`,
204
- this.name,
205
- { originalError: error }
206
- )
207
-
208
- errorManager.handle(pluginError).catch(console.error)
209
- throw pluginError
210
- }
477
+ for (const child of this.children) {
478
+ await child.broadcast(name, ...args);
211
479
  }
212
- }
480
+ }
481
+
482
+ // ============================================================================
483
+ // 依赖注入
484
+ // ============================================================================
485
+
486
+ /**
487
+ * 注册上下文
488
+ */
489
+ provide<T extends keyof Plugin.Contexts>(context: Context<T>): this {
490
+ if (!Plugin[contextsKey].includes(context.name as string)) {
491
+ Plugin[contextsKey].push(context.name as string);
492
+ }
493
+ this.logger.debug(`Context "${context.name as string}" provided`);
494
+ this.$contexts.set(context.name as string, context);
495
+ return this;
496
+ }
497
+
498
+ // ============================================================================
499
+ // 插件加载
500
+ // ============================================================================
501
+
502
+ /**
503
+ * 导入插件
504
+ */
505
+ async import(entry: string,t?:number): Promise<Plugin> {
506
+ if (!entry) throw new Error(`Plugin entry not found: ${entry}`);
507
+ const resolved = resolveEntry(path.isAbsolute(entry) ?
508
+ entry :
509
+ path.resolve(path.dirname(this.filePath), entry)) || entry;
510
+ let realPath: string;
511
+ try {
512
+ realPath = fs.realpathSync(resolved);
513
+ } catch {
514
+ realPath = resolved;
515
+ }
516
+
517
+ // 避免重复加载同一路径的插件
518
+ const normalized = realPath.replace(/\?t=\d+$/, '').replace(/\\/g, '/');
519
+ const existing = this.children.find(child =>
520
+ child.filePath.replace(/\?t=\d+$/, '').replace(/\\/g, '/') === normalized
521
+ );
522
+ if (existing) {
523
+ this.logger.debug(`Plugin "${entry}" already loaded, skipping...`);
524
+ if (this.started && !existing.started) await existing.start(t);
525
+ return existing;
526
+ }
527
+
528
+ const plugin = await Plugin.create(realPath, this);
529
+ if (this.started) await plugin.start(t);
530
+
531
+ if (process.env.NODE_ENV === 'development') {
532
+ plugin.watch((p) => p.reload())
533
+ }
534
+ return plugin;
535
+ }
536
+
537
+ /**
538
+ * 重载插件
539
+ */
540
+ async reload(plugin: Plugin = this): Promise<void> {
541
+ this.logger.info(`Plugin "${plugin.name}" reloading...`);
542
+ const now=Date.now();
543
+ if (!plugin.parent) {
544
+ // 根插件重载 = 退出进程(由 CLI 重启)
545
+ return process.exit(51);
546
+ }
547
+
548
+ await plugin.stop();
549
+ await plugin.parent.import(plugin.filePath, now);
550
+ await plugin.broadcast("mounted");
551
+ this.logger.debug(`Plugin "${plugin.name}" reloaded`);
552
+ }
553
+
554
+ /**
555
+ * 监听文件变化
556
+ */
557
+ watch(
558
+ callback: (p: Plugin) => void | Promise<void>,
559
+ recursive = false
560
+ ): void {
561
+ if (!this.filePath || this.filePath.includes("node_modules")) return;
562
+
563
+ const unwatch = watchFile(this.filePath, () => {
564
+ const newHash = getFileHash(this.filePath);
565
+ if (newHash === this.fileHash) return;
566
+
567
+ this.logger.debug(`Plugin "${this.name}" file changed, reloading...`);
568
+ callback(this);
569
+ this.fileHash = newHash;
570
+ });
571
+
572
+ this.on("dispose", unwatch);
573
+
574
+ if (recursive) {
575
+ for (const child of this.children) {
576
+ child.watch(callback, recursive);
577
+ }
578
+ }
579
+ }
580
+
581
+ // ============================================================================
582
+ // 辅助方法
583
+ // ============================================================================
584
+
585
+ // 核心方法列表(需要绑定的方法)
586
+ static #coreMethods = new Set([
587
+ 'addMiddleware', 'useContext', 'inject', 'contextIsReady',
588
+ 'start', 'stop', 'onMounted', 'onDispose',
589
+ 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info'
590
+ ]);
591
+
592
+ /**
593
+ * 自动绑定核心方法(只在构造函数中调用一次)
594
+ */
595
+ $bindMethods(): void {
596
+ if (this.#methodsBound) return;
597
+ this.#methodsBound = true;
598
+
599
+ const proto = Object.getPrototypeOf(this);
600
+ for (const key of Plugin.#coreMethods) {
601
+ const value = proto[key];
602
+ if (typeof value === "function") {
603
+ (this as any)[key] = value.bind(this);
604
+ }
605
+ }
606
+ }
607
+
608
+ // ============================================================================
609
+ // 静态方法
610
+ // ============================================================================
611
+
612
+ /**
613
+ * 创建插件实例(异步加载)
614
+ */
615
+ static async create(entry: string, parent?: Plugin): Promise<Plugin> {
616
+ entry = path.resolve(
617
+ path.dirname(parent?.filePath || fileURLToPath(import.meta.url)),
618
+ entry
619
+ );
620
+ const entryFile = fs.existsSync(entry) ? entry : require.resolve(entry);
621
+ const realPath = fs.realpathSync(entryFile);
622
+
623
+ // 检查模块是否已加载
624
+ const existing = loadedModules.get(realPath);
625
+ if (existing) {
626
+ return existing;
627
+ }
628
+
629
+ const plugin = new Plugin(realPath, parent);
630
+ plugin.fileHash = getFileHash(entryFile);
631
+
632
+ // 先记录,防止循环依赖时重复加载
633
+ loadedModules.set(realPath, plugin);
634
+
635
+ await storage.run(plugin, async () => {
636
+ await import(`${import.meta.resolve(entryFile)}?t=${Date.now()}`);
637
+ });
638
+
639
+ return plugin;
640
+ }
641
+ }
642
+ export function defineContext<T extends keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}>(options: Context<T, E>): Context<T, E> {
643
+ return options;
644
+ }
645
+ export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}> {
646
+ name: T;
647
+ description: string
648
+ value?: Plugin.Contexts[T];
649
+ mounted?: (parent: Plugin) => Plugin.Contexts[T] | Promise<Plugin.Contexts[T]>;
650
+ dispose?: (value: Plugin.Contexts[T]) => void;
651
+ /** 扩展方法,会自动挂载到 Plugin.prototype 上 */
652
+ extensions?: E;
653
+ }
654
+ // ============================================================================
655
+ // 类型定义
656
+ // ============================================================================
657
+ export namespace Plugin {
658
+ /**
659
+ * 插件提供的功能
660
+ */
661
+ export interface Features {
662
+ commands: string[];
663
+ components: string[];
664
+ crons: string[];
665
+ middlewares: string[];
666
+ }
667
+
668
+ /**
669
+ * 生命周期事件
670
+ */
671
+ export interface Lifecycle {
672
+ mounted: [];
673
+ dispose: [];
674
+ "before-start": [Plugin];
675
+ started: [Plugin];
676
+ "before-mount": [Plugin];
677
+ "before-dispose": [Plugin];
678
+ "call.recallMessage": [string, string, string];
679
+ 'before.sendMessage': [SendOptions];
680
+ "context.mounted": [keyof Plugin.Contexts];
681
+ "context.dispose": [keyof Plugin.Contexts];
682
+ }
683
+
684
+ /**
685
+ * 服务类型扩展点
686
+ * 各个 Context 通过 declare module 扩展此接口
687
+ */
688
+ export interface Contexts extends RegisteredAdapters {
689
+ config: ConfigService;
690
+ permission: PermissionService;
691
+ }
692
+
693
+ /**
694
+ * Service 扩展方法类型
695
+ * 这些方法由各个 Context 的 extensions 提供
696
+ * 在 Plugin.start() 时自动注册到 Plugin.prototype
697
+ */
698
+ export interface Extensions {}
699
+ }