@zhin.js/core 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/lib/adapter.d.ts +1 -26
  2. package/lib/adapter.d.ts.map +1 -1
  3. package/lib/adapter.js +20 -117
  4. package/lib/adapter.js.map +1 -1
  5. package/lib/built/adapter-process.d.ts +0 -4
  6. package/lib/built/adapter-process.d.ts.map +1 -1
  7. package/lib/built/adapter-process.js +0 -95
  8. package/lib/built/adapter-process.js.map +1 -1
  9. package/lib/built/agent-preset.d.ts +2 -0
  10. package/lib/built/agent-preset.d.ts.map +1 -1
  11. package/lib/built/agent-preset.js +4 -0
  12. package/lib/built/agent-preset.js.map +1 -1
  13. package/lib/built/command.d.ts +4 -0
  14. package/lib/built/command.d.ts.map +1 -1
  15. package/lib/built/command.js +6 -0
  16. package/lib/built/command.js.map +1 -1
  17. package/lib/built/component.d.ts.map +1 -1
  18. package/lib/built/component.js +1 -0
  19. package/lib/built/component.js.map +1 -1
  20. package/lib/built/dispatcher.d.ts.map +1 -1
  21. package/lib/built/dispatcher.js +0 -13
  22. package/lib/built/dispatcher.js.map +1 -1
  23. package/lib/built/message-filter.d.ts +2 -0
  24. package/lib/built/message-filter.d.ts.map +1 -1
  25. package/lib/built/message-filter.js +5 -0
  26. package/lib/built/message-filter.js.map +1 -1
  27. package/lib/built/skill.d.ts +11 -0
  28. package/lib/built/skill.d.ts.map +1 -1
  29. package/lib/built/skill.js +14 -0
  30. package/lib/built/skill.js.map +1 -1
  31. package/lib/built/tool.d.ts +11 -44
  32. package/lib/built/tool.d.ts.map +1 -1
  33. package/lib/built/tool.js +14 -353
  34. package/lib/built/tool.js.map +1 -1
  35. package/lib/plugin.d.ts +1 -25
  36. package/lib/plugin.d.ts.map +1 -1
  37. package/lib/plugin.js +1 -77
  38. package/lib/plugin.js.map +1 -1
  39. package/lib/types.d.ts +0 -25
  40. package/lib/types.d.ts.map +1 -1
  41. package/package.json +10 -7
  42. package/CHANGELOG.md +0 -561
  43. package/REFACTORING_COMPLETE.md +0 -178
  44. package/REFACTORING_STATUS.md +0 -263
  45. package/src/adapter.ts +0 -275
  46. package/src/ai/index.ts +0 -55
  47. package/src/ai/providers/anthropic.ts +0 -379
  48. package/src/ai/providers/base.ts +0 -175
  49. package/src/ai/providers/index.ts +0 -13
  50. package/src/ai/providers/ollama.ts +0 -302
  51. package/src/ai/providers/openai.ts +0 -174
  52. package/src/ai/types.ts +0 -348
  53. package/src/bot.ts +0 -37
  54. package/src/built/adapter-process.ts +0 -177
  55. package/src/built/agent-preset.ts +0 -136
  56. package/src/built/ai-trigger.ts +0 -259
  57. package/src/built/command.ts +0 -108
  58. package/src/built/common-adapter-tools.ts +0 -242
  59. package/src/built/component.ts +0 -130
  60. package/src/built/config.ts +0 -335
  61. package/src/built/cron.ts +0 -156
  62. package/src/built/database.ts +0 -134
  63. package/src/built/dispatcher.ts +0 -496
  64. package/src/built/login-assist.ts +0 -131
  65. package/src/built/message-filter.ts +0 -390
  66. package/src/built/permission.ts +0 -151
  67. package/src/built/schema-feature.ts +0 -190
  68. package/src/built/skill.ts +0 -221
  69. package/src/built/tool.ts +0 -948
  70. package/src/command.ts +0 -87
  71. package/src/component.ts +0 -565
  72. package/src/cron.ts +0 -4
  73. package/src/errors.ts +0 -46
  74. package/src/feature.ts +0 -7
  75. package/src/index.ts +0 -53
  76. package/src/jsx-dev-runtime.ts +0 -2
  77. package/src/jsx-runtime.ts +0 -12
  78. package/src/jsx.ts +0 -135
  79. package/src/message.ts +0 -48
  80. package/src/models/system-log.ts +0 -20
  81. package/src/models/user.ts +0 -15
  82. package/src/notice.ts +0 -98
  83. package/src/plugin.ts +0 -896
  84. package/src/prompt.ts +0 -293
  85. package/src/request.ts +0 -95
  86. package/src/scheduler/index.ts +0 -19
  87. package/src/scheduler/scheduler.ts +0 -372
  88. package/src/scheduler/types.ts +0 -74
  89. package/src/tool-zod.ts +0 -115
  90. package/src/types-generator.ts +0 -78
  91. package/src/types.ts +0 -505
  92. package/src/utils.ts +0 -227
  93. package/tests/adapter.test.ts +0 -638
  94. package/tests/ai/ai-trigger.test.ts +0 -368
  95. package/tests/ai/providers.integration.test.ts +0 -227
  96. package/tests/ai/setup.ts +0 -308
  97. package/tests/ai/tool.test.ts +0 -800
  98. package/tests/bot.test.ts +0 -151
  99. package/tests/command.test.ts +0 -737
  100. package/tests/component-new.test.ts +0 -361
  101. package/tests/config.test.ts +0 -372
  102. package/tests/cron.test.ts +0 -82
  103. package/tests/dispatcher.test.ts +0 -293
  104. package/tests/errors.test.ts +0 -21
  105. package/tests/expression-evaluation.test.ts +0 -258
  106. package/tests/features-builtin.test.ts +0 -191
  107. package/tests/jsx-runtime.test.ts +0 -45
  108. package/tests/jsx.test.ts +0 -319
  109. package/tests/message-filter.test.ts +0 -566
  110. package/tests/message.test.ts +0 -402
  111. package/tests/notice.test.ts +0 -198
  112. package/tests/plugin.test.ts +0 -779
  113. package/tests/prompt.test.ts +0 -78
  114. package/tests/redos-protection.test.ts +0 -198
  115. package/tests/request.test.ts +0 -221
  116. package/tests/schema.test.ts +0 -248
  117. package/tests/skill-feature.test.ts +0 -179
  118. package/tests/test-utils.ts +0 -59
  119. package/tests/tool-feature.test.ts +0 -254
  120. package/tests/types.test.ts +0 -162
  121. package/tests/utils.test.ts +0 -135
  122. package/tsconfig.json +0 -24
package/src/plugin.ts DELETED
@@ -1,896 +0,0 @@
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, AITool, ToolContext, PluginManifest } 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, SendOptions } from "./types.js";
18
- import { Adapter, Adapters } from "./adapter.js";
19
- import { createHash } from "crypto";
20
- import { Feature } from "./feature.js";
21
- const contextsKey = Symbol("contexts");
22
- const loadedModules = new Map<string, Plugin>(); // 记录已加载的模块
23
- const require = createRequire(import.meta.url);
24
-
25
-
26
- export type SideEffect<A extends (keyof Plugin.Contexts)[]> = {
27
- (...args: ContextList<A>): MaybePromise<void | DisposeFn<ContextList<A>>>;
28
- finished?: boolean
29
- }
30
- export type DisposeFn<A> = (context: ArrayItem<A>) => MaybePromise<void>
31
- 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[]
32
- type ContextItem<L> = L extends keyof Plugin.Contexts ? Plugin.Contexts[L] : never
33
- // ============================================================================
34
- // AsyncLocalStorage 上下文
35
- // ============================================================================
36
-
37
- export const storage = new AsyncLocalStorage<Plugin>();
38
-
39
- /**
40
- * 获取当前文件路径(调用者)
41
- */
42
- function getCurrentFile(metaUrl = import.meta.url): string {
43
- const previousPrepareStackTrace = Error.prepareStackTrace;
44
- Error.prepareStackTrace = function (_, stack) {
45
- return stack;
46
- };
47
- const stack = new Error().stack as unknown as NodeJS.CallSite[];
48
- Error.prepareStackTrace = previousPrepareStackTrace;
49
- const stackFiles = Array.from(
50
- new Set(stack.map((site) => site.getFileName()))
51
- );
52
- const idx = stackFiles.findIndex(
53
- (f) => f === fileURLToPath(metaUrl) || f === metaUrl
54
- );
55
- const result = stackFiles[idx + 1];
56
- if (!result) throw new Error("Cannot resolve current file path");
57
- try {
58
- return fileURLToPath(result);
59
- } catch {
60
- return result;
61
- }
62
- }
63
-
64
- /**
65
- * usePlugin - 获取或创建当前插件实例
66
- * 类似 React Hooks 的设计,根据调用文件自动创建插件树
67
- * 同一上下文中同一文件多次调用返回同一实例
68
- */
69
- export function usePlugin(): Plugin {
70
- const callerFile = getCurrentFile();
71
- // 如果当前 store 已是同一文件创建的插件,直接返回
72
- const current = storage.getStore();
73
- if (current && current.filePath.replace(/\?t=\d+$/, '') === callerFile.replace(/\?t=\d+$/, '')) {
74
- return current;
75
- }
76
- const parentPlugin = current;
77
- const newPlugin = new Plugin(callerFile, parentPlugin);
78
- storage.enterWith(newPlugin);
79
- return newPlugin;
80
- }
81
-
82
- /**
83
- * getPlugin - 获取当前 AsyncLocalStorage 中的插件实例
84
- * 用于 extensions 等场景,不创建新插件
85
- */
86
- export function getPlugin(): Plugin {
87
- const plugin = storage.getStore();
88
- if (!plugin) {
89
- throw new Error('getPlugin() must be called within a plugin context');
90
- }
91
- return plugin;
92
- }
93
-
94
- // ============================================================================
95
- // 文件工具函数
96
- // ============================================================================
97
-
98
- /**
99
- * 获取文件 Hash(用于检测变更)
100
- */
101
- function getFileHash(filePath: string): string {
102
- try {
103
- const content = fs.readFileSync(filePath, "utf-8");
104
- return createHash("md5")
105
- .update(content)
106
- .digest("hex");
107
- } catch {
108
- return "";
109
- }
110
- }
111
-
112
- /**
113
- * 监听文件变化
114
- */
115
- function watchFile(filePath: string, callback: () => void): () => void {
116
- try {
117
- const watcher = fs.watch(filePath, callback);
118
- watcher.on("error", (_error) => { });
119
- return () => watcher.close();
120
- } catch (error) {
121
- return () => { };
122
- }
123
- }
124
-
125
- // ============================================================================
126
- // Plugin 类
127
- // ============================================================================
128
-
129
- export interface Plugin extends Plugin.Extensions { }
130
- /**
131
- * Plugin 类 - 核心插件系统
132
- * 直接继承 EventEmitter
133
- */
134
- export class Plugin extends EventEmitter<Plugin.Lifecycle> {
135
- static [contextsKey] = [] as string[];
136
-
137
- #cachedName?: string;
138
- #manifest?: PluginManifest | null;
139
- adapters: (keyof Plugin.Contexts)[] = [];
140
- started = false;
141
-
142
- // 上下文存储
143
- $contexts: Map<string, Context<any>> = new Map();
144
-
145
-
146
- // 子插件
147
- children: Plugin[] = [];
148
-
149
- // 文件信息
150
- filePath: string;
151
- fileHash: string = "";
152
-
153
- // Logger
154
- logger: Logger;
155
-
156
- #messageMiddleware: MessageMiddleware<RegisteredAdapter> = async (message, next) => {
157
- const commandService = this.inject('command');
158
- if (!commandService) return await next();
159
- const result = await commandService.handle(message, this);
160
- if (!result) return await next();
161
- const adapter = this.inject(message.$adapter) as Adapter;
162
- if (!adapter || !(adapter instanceof Adapter)) return await next();
163
- await message.$reply(result);
164
- await next();
165
- };
166
- // 插件功能
167
- #middlewares: MessageMiddleware<RegisteredAdapter>[] = [this.#messageMiddleware];
168
-
169
- // AI 工具
170
- #tools: Map<string, AITool> = new Map();
171
-
172
- // Feature 贡献追踪
173
- #featureContributions = new Map<string, Set<string>>();
174
-
175
- // 统一的清理函数集合
176
- #disposables: Set<() => void | Promise<void>> = new Set();
177
-
178
- get middleware(): MessageMiddleware<RegisteredAdapter> {
179
- return compose<RegisteredAdapter>(this.#middlewares);
180
- }
181
-
182
- recordFeatureContribution(featureName: string, itemName: string): void {
183
- if (!this.#featureContributions.has(featureName)) {
184
- this.#featureContributions.set(featureName, new Set());
185
- }
186
- this.#featureContributions.get(featureName)!.add(itemName);
187
- }
188
- /**
189
- * 构造函数
190
- */
191
- constructor(filePath: string = "", public parent?: Plugin) {
192
- super();
193
-
194
- // 增加 EventEmitter 监听器限制,避免警告
195
- // 因为插件可能注册多个命令/组件/中间件,每个都会添加 dispose 监听器
196
- this.setMaxListeners(50);
197
-
198
- this.filePath = filePath.replace(/\?t=\d+$/, "");
199
- this.logger = this.name ? logger.getLogger(this.name) : logger;
200
-
201
- // 自动添加到父节点
202
- if (parent && !parent.children.includes(this)) {
203
- parent.children.push(this);
204
- }
205
-
206
- // 绑定方法以支持解构使用
207
- this.$bindMethods();
208
- }
209
-
210
- // 标记是否已绑定方法
211
- #methodsBound = false;
212
-
213
- /**
214
- * 添加中间件
215
- * 中间件用于处理消息流转
216
- */
217
- addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>, name?: string) {
218
- this.#middlewares.push(middleware as MessageMiddleware<RegisteredAdapter>);
219
- const dispose = () => {
220
- remove(this.#middlewares, middleware);
221
- this.#disposables.delete(dispose);
222
- };
223
- this.#disposables.add(dispose);
224
- return dispose;
225
- }
226
-
227
- /**
228
- * 添加 AI 工具
229
- * 工具可以被 AI 服务调用来执行操作
230
- * @param tool 工具定义
231
- * @returns 返回一个移除工具的函数
232
- */
233
- addTool(tool: AITool): () => void {
234
- // 自动添加插件源标识
235
- const toolWithSource: AITool = {
236
- ...tool,
237
- source: tool.source || `plugin:${this.name}`,
238
- tags: [...(tool.tags || []), 'plugin', this.name],
239
- };
240
- this.#tools.set(tool.name, toolWithSource);
241
- const dispose = () => {
242
- this.#tools.delete(tool.name);
243
- this.#disposables.delete(dispose);
244
- };
245
- this.#disposables.add(dispose);
246
- return dispose;
247
- }
248
-
249
- /**
250
- * 获取当前插件注册的所有工具
251
- */
252
- getTools(): AITool[] {
253
- return Array.from(this.#tools.values());
254
- }
255
-
256
- /**
257
- * 获取当前插件及所有子插件注册的工具
258
- */
259
- getAllTools(): AITool[] {
260
- const tools: AITool[] = [...this.getTools()];
261
- for (const child of this.children) {
262
- tools.push(...child.getAllTools());
263
- }
264
- return tools;
265
- }
266
-
267
- /**
268
- * 根据名称获取工具
269
- */
270
- getTool(name: string): AITool | undefined {
271
- // 先在当前插件查找
272
- const tool = this.#tools.get(name);
273
- if (tool) return tool;
274
- // 再在子插件中查找
275
- for (const child of this.children) {
276
- const childTool = child.getTool(name);
277
- if (childTool) return childTool;
278
- }
279
- return undefined;
280
- }
281
-
282
- /**
283
- * 收集所有可用的工具(包括适配器提供的)
284
- * 这是 AI 服务获取工具的主入口
285
- */
286
- collectAllTools(): AITool[] {
287
- const tools: AITool[] = [];
288
-
289
- // 收集插件树中的所有工具
290
- const rootPlugin = this.root;
291
- tools.push(...rootPlugin.getAllTools());
292
-
293
- // 收集所有适配器的工具
294
- for (const [name, context] of rootPlugin.contexts) {
295
- const value = context.value;
296
- if (value && typeof value === 'object' && 'getTools' in value) {
297
- const adapter = value as Adapter;
298
- tools.push(...adapter.getTools());
299
- }
300
- }
301
-
302
- return tools;
303
- }
304
-
305
- /**
306
- * 插件名称
307
- */
308
- get name(): string {
309
- if (this.#cachedName) return this.#cachedName;
310
-
311
- this.#cachedName = path
312
- .relative(process.cwd(), this.filePath)
313
- .replace(/\?t=\d+$/, "")
314
- .replace(/\\/g, "/")
315
- .replace(/\/index\.(js|ts)x?$/, "")
316
- .replace(/\/(lib|src|dist)$/, "")
317
- .replace(/.*\/node_modules\//, "")
318
- .replace(/.*\//, "")
319
- .replace(/\.(js|ts)x?$/, "");
320
-
321
- return this.#cachedName;
322
- }
323
-
324
- /**
325
- * 插件清单(从 plugin.yml 或 package.json 延迟读取)
326
- */
327
- get manifest(): PluginManifest | undefined {
328
- if (this.#manifest !== undefined) return this.#manifest ?? undefined;
329
- if (!this.filePath) {
330
- this.#manifest = null;
331
- return undefined;
332
- }
333
- const dir = path.dirname(this.filePath);
334
- // 优先读取 plugin.yml
335
- const ymlPath = path.join(dir, 'plugin.yml');
336
- if (fs.existsSync(ymlPath)) {
337
- try {
338
- const content = fs.readFileSync(ymlPath, 'utf-8');
339
- const match = content.match(/^name:\s*(.+)$/m);
340
- const descMatch = content.match(/^description:\s*(.+)$/m);
341
- const verMatch = content.match(/^version:\s*(.+)$/m);
342
- if (match) {
343
- this.#manifest = {
344
- name: match[1].trim(),
345
- description: descMatch?.[1]?.trim(),
346
- version: verMatch?.[1]?.trim(),
347
- };
348
- return this.#manifest;
349
- }
350
- } catch { /* ignore */ }
351
- }
352
- // Fallback: package.json
353
- const pkgPath = path.join(dir, 'package.json');
354
- if (fs.existsSync(pkgPath)) {
355
- try {
356
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
357
- if (pkg.name) {
358
- this.#manifest = {
359
- name: pkg.name,
360
- description: pkg.description,
361
- version: pkg.version,
362
- };
363
- return this.#manifest;
364
- }
365
- } catch { /* ignore */ }
366
- }
367
- this.#manifest = null;
368
- return undefined;
369
- }
370
-
371
- /**
372
- * 根插件
373
- */
374
- get root(): Plugin {
375
- if (!this.parent) return this;
376
- return this.parent.root;
377
- }
378
- get contexts(): Map<string, Context> {
379
- const result = new Map<string, Context>();
380
- for (const [key, value] of this.$contexts) {
381
- result.set(key, value);
382
- }
383
- for (const child of this.children) {
384
- for (const [key, value] of child.contexts) {
385
- result.set(key, value);
386
- }
387
- }
388
- return result;
389
- }
390
-
391
-
392
- useContext<T extends (keyof Plugin.Contexts)[]>(...args: [...T, SideEffect<T>]) {
393
- const contexts = args.slice(0, -1) as T
394
- const sideEffect = args[args.length - 1] as SideEffect<T>
395
- const contextReadyCallback = async () => {
396
- if (sideEffect.finished) return;
397
- sideEffect.finished = true;
398
- const args = contexts.map(item => this.inject(item))
399
- const dispose = await sideEffect(...args as ContextList<T>)
400
- if (!dispose) return;
401
- const disposeFn = async (name: keyof Plugin.Contexts) => {
402
- if (contexts.includes(name)) {
403
- await dispose(this.inject(name) as any)
404
- }
405
- this.off('context.dispose', disposeFn)
406
- sideEffect.finished = false;
407
- }
408
- this.on('context.dispose', disposeFn)
409
- // 确保 dispose 时清理监听器(只注册一次)
410
- const cleanupOnDispose = () => {
411
- this.off('context.dispose', disposeFn)
412
- dispose(this.inject(args[0] as any) as any)
413
- }
414
- this.once('dispose', cleanupOnDispose)
415
- }
416
- const onContextMounted = async (name: keyof Plugin.Contexts) => {
417
- if (!this.#contextsIsReady(contexts) || !(contexts).includes(name)) return
418
- await contextReadyCallback()
419
- }
420
- this.on('context.mounted', onContextMounted)
421
- // 插件销毁时移除 context.mounted 监听器
422
- this.once('dispose', () => this.off('context.mounted', onContextMounted))
423
- if (!this.#contextsIsReady(contexts)) return
424
- contextReadyCallback()
425
- }
426
- inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T]|undefined;
427
- inject(name: string): unknown;
428
- inject(name: string): unknown {
429
- const context = this.root.contexts.get(name);
430
- return context?.value;
431
- }
432
- #contextsIsReady<CS extends (keyof Plugin.Contexts)[]>(contexts: CS) {
433
- if (!contexts.length) return true
434
- return contexts.every(name => this.contextIsReady(name))
435
- }
436
- contextIsReady<T extends keyof Plugin.Contexts>(name: T) {
437
- try {
438
- return !!this.inject(name)
439
- } catch {
440
- return false
441
- }
442
- }
443
- // ============================================================================
444
- // 生命周期方法
445
- // ============================================================================
446
-
447
- /**
448
- * 启动插件
449
- */
450
- async start(t?:number): Promise<void> {
451
- if (this.started) return;
452
- this.started = true; // 提前设置,防止重复启动
453
-
454
- // 启动所有服务
455
- for (const context of this.$contexts.values()) {
456
- if (typeof context.mounted === "function") {
457
- const result = await context.mounted(this);
458
- // 仅在没有预设 value 时才用 mounted 返回值赋值
459
- if (!context.value) {
460
- context.value = result;
461
- }
462
- }
463
- // 注册扩展方法到 Plugin.prototype
464
- if (context.extensions) {
465
- for (const [name, fn] of Object.entries(context.extensions)) {
466
- if (typeof fn === 'function') {
467
- Reflect.set(Plugin.prototype, name, fn);
468
- }
469
- }
470
- }
471
- this.dispatch('context.mounted', context.name)
472
- }
473
- await this.broadcast("mounted");
474
- // 先启动子插件,再打印当前插件启动日志
475
- for (const child of this.children) {
476
- await child.start(t);
477
- }
478
- // 输出启动日志(使用 debug 级别,避免重复输出)
479
- // 只在根插件或重要插件时使用 info 级别
480
- if (!this.parent || this.name === 'setup') {
481
- this.logger.info(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
482
- } else {
483
- this.logger.debug(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
484
- }
485
- }
486
- /**
487
- * 获取插件提供的功能
488
- * 从各个服务中获取数据
489
- */
490
- get features(): Plugin.Features {
491
- const commandService = this.inject('command');
492
- const componentService = this.inject('component')
493
- const cronService = this.inject('cron');
494
-
495
- return {
496
- commands: commandService ? commandService.items.map(c => c.pattern) : [],
497
- components: componentService ? componentService.getAllNames() : [],
498
- crons: cronService ? cronService.items.map(c => c.cronExpression) : [],
499
- middlewares: this.#middlewares.map((m, i) => m.name || `middleware_${i}`),
500
- };
501
- }
502
-
503
- /**
504
- * 获取插件功能摘要(数组形式)
505
- * 返回各功能类型的名称和数量
506
- */
507
- getFeatures(): Array<{ name: string; count: number }> {
508
- const result: Array<{ name: string; count: number }> = [];
509
- const f = this.features;
510
- if (f.commands.length > 0) result.push({ name: 'command', count: f.commands.length });
511
- if (f.components.length > 0) result.push({ name: 'component', count: f.components.length });
512
- if (f.crons.length > 0) result.push({ name: 'cron', count: f.crons.length });
513
- // #middlewares includes the default command middleware, only count user-added ones
514
- const userMiddlewareCount = this.#middlewares.length - 1; // subtract default #messageMiddleware
515
- if (userMiddlewareCount > 0) result.push({ name: 'middleware', count: userMiddlewareCount });
516
- if (this.#tools.size > 0) result.push({ name: 'tool', count: this.#tools.size });
517
- return result;
518
- }
519
-
520
- info(): Record<string, any> {
521
- return {
522
- [this.name]: {
523
- features: this.features,
524
- children: this.children.map(child => child.info())
525
- }
526
- }
527
- }
528
-
529
- /**
530
- * 停止插件
531
- */
532
- async stop(): Promise<void> {
533
- if (!this.started) return;
534
- this.logger.debug(`Stopping plugin "${this.name}"`);
535
- this.started = false;
536
-
537
- // 停止子插件
538
- for (const child of this.children) {
539
- await child.stop();
540
- }
541
- this.children = [];
542
-
543
- // 停止服务
544
- for (const [name, context] of this.$contexts) {
545
- remove(Plugin[contextsKey], name);
546
- // 移除扩展方法
547
- if (context.extensions) {
548
- for (const key of Object.keys(context.extensions)) {
549
- delete (Plugin.prototype as any)[key];
550
- }
551
- }
552
- if (typeof context.dispose === "function") {
553
- await context.dispose(context.value);
554
- }
555
- }
556
- // 清理 contexts Map
557
- this.$contexts.clear();
558
-
559
- // 清空缓存的名称
560
- this.#cachedName = undefined;
561
-
562
- // 触发 dispose 事件
563
- this.emit("dispose");
564
-
565
- // 执行所有清理函数
566
- for (const dispose of this.#disposables) {
567
- try {
568
- await dispose();
569
- } catch (e) {
570
- this.logger.warn(`Dispose callback failed: ${e}`);
571
- }
572
- }
573
- this.#disposables.clear();
574
-
575
- // 清理 middlewares 数组(保留默认的消息中间件)
576
- this.#middlewares.length = 1;
577
-
578
- // 清理 feature 贡献记录
579
- this.#featureContributions.clear();
580
-
581
- if (this.parent) {
582
- remove(this.parent?.children, this);
583
- }
584
-
585
- // 从全局 loadedModules Map 中移除,防止内存泄漏
586
- if (this.filePath) {
587
- try {
588
- const realPath = fs.realpathSync(this.filePath);
589
- loadedModules.delete(realPath);
590
- } catch {
591
- // 文件可能已不存在,忽略错误
592
- }
593
- }
594
-
595
- this.removeAllListeners();
596
- this.logger.debug(`Plugin "${this.name}" stopped`);
597
- }
598
-
599
- // ============================================================================
600
- // 生命周期钩子
601
- // ============================================================================
602
-
603
- onMounted(callback: () => void | Promise<void>): void {
604
- this.on("mounted", callback);
605
- }
606
-
607
- onDispose(callback: () => void | Promise<void>): () => void {
608
- this.#disposables.add(callback);
609
- return () => {
610
- this.#disposables.delete(callback);
611
- };
612
- }
613
-
614
- // ============================================================================
615
- // 事件广播
616
- // ============================================================================
617
-
618
- /**
619
- * dispatch - 向上冒泡到父插件,或在根节点广播
620
- */
621
- async dispatch<K extends keyof Plugin.Lifecycle>(
622
- name: K,
623
- ...args: Plugin.Lifecycle[K]
624
- ): Promise<void> {
625
- if (this.parent) {
626
- return this.parent.dispatch(name, ...args);
627
- }
628
- return this.broadcast(name, ...args);
629
- }
630
-
631
- /**
632
- * broadcast - 向下广播到所有子插件
633
- */
634
- async broadcast<K extends keyof Plugin.Lifecycle>(
635
- name: K,
636
- ...args: Plugin.Lifecycle[K]
637
- ): Promise<void> {
638
- const listeners = this.listeners(name) as ((...args: any[]) => any)[];
639
- for (const listener of listeners) {
640
- await listener(...args);
641
- }
642
-
643
- for (const child of this.children) {
644
- await child.broadcast(name, ...args);
645
- }
646
- }
647
-
648
- // ============================================================================
649
- // 依赖注入
650
- // ============================================================================
651
-
652
- /**
653
- * 注册上下文
654
- */
655
- provide<T extends keyof Plugin.Contexts>(context: Context<T>): this;
656
- provide(feature: Feature<unknown>): this;
657
- provide(target: Context<keyof Plugin.Contexts> | Feature<unknown>): this {
658
- if (target instanceof Feature) {
659
- const feature = target;
660
- const ctx: Context<keyof Plugin.Contexts> = {
661
- name: feature.name as keyof Plugin.Contexts,
662
- description: feature.desc,
663
- value: feature as Plugin.Contexts[keyof Plugin.Contexts],
664
- mounted: feature.mounted
665
- ? async (plugin: Plugin) => {
666
- await feature.mounted!(plugin);
667
- return feature as Plugin.Contexts[keyof Plugin.Contexts];
668
- }
669
- : undefined,
670
- dispose: feature.dispose
671
- ? async () => { await feature.dispose!(); }
672
- : undefined,
673
- extensions: feature.extensions,
674
- };
675
- return this.provide(ctx);
676
- }
677
- const context = target;
678
- if (!Plugin[contextsKey].includes(context.name as string)) {
679
- Plugin[contextsKey].push(context.name as string);
680
- }
681
- this.logger.debug(`Context "${context.name as string}" provided`);
682
- this.$contexts.set(context.name as string, context);
683
- // 立即注册扩展方法到 Plugin.prototype,确保后续 import 的插件可用
684
- if (context.extensions) {
685
- for (const [name, fn] of Object.entries(context.extensions)) {
686
- if (typeof fn === 'function') {
687
- Reflect.set(Plugin.prototype, name, fn);
688
- }
689
- }
690
- }
691
- return this;
692
- }
693
-
694
- // ============================================================================
695
- // 插件加载
696
- // ============================================================================
697
-
698
- /**
699
- * 导入插件
700
- */
701
- async import(entry: string,t?:number): Promise<Plugin> {
702
- if (!entry) throw new Error(`Plugin entry not found: ${entry}`);
703
- const resolved = resolveEntry(path.isAbsolute(entry) ?
704
- entry :
705
- path.resolve(path.dirname(this.filePath), entry)) || entry;
706
- let realPath: string;
707
- try {
708
- realPath = fs.realpathSync(resolved);
709
- } catch {
710
- realPath = resolved;
711
- }
712
-
713
- // 避免重复加载同一路径的插件
714
- const normalized = realPath.replace(/\?t=\d+$/, '').replace(/\\/g, '/');
715
- const existing = this.children.find(child =>
716
- child.filePath.replace(/\?t=\d+$/, '').replace(/\\/g, '/') === normalized
717
- );
718
- if (existing) {
719
- this.logger.debug(`Plugin "${entry}" already loaded, skipping...`);
720
- if (this.started && !existing.started) await existing.start(t);
721
- return existing;
722
- }
723
-
724
- const plugin = await Plugin.create(realPath, this);
725
- if (this.started) await plugin.start(t);
726
-
727
- if (process.env.NODE_ENV === 'development') {
728
- plugin.watch((p) => p.reload())
729
- }
730
- return plugin;
731
- }
732
-
733
- /**
734
- * 重载插件
735
- */
736
- async reload(plugin: Plugin = this): Promise<void> {
737
- this.logger.info(`Plugin "${plugin.name}" reloading...`);
738
- const now=Date.now();
739
- if (!plugin.parent) {
740
- // 根插件重载 = 退出进程(由 CLI 重启)
741
- return process.exit(51);
742
- }
743
-
744
- await plugin.stop();
745
- await plugin.parent.import(plugin.filePath, now);
746
- await plugin.broadcast("mounted");
747
- this.logger.debug(`Plugin "${plugin.name}" reloaded`);
748
- }
749
-
750
- /**
751
- * 监听文件变化
752
- */
753
- watch(
754
- callback: (p: Plugin) => void | Promise<void>,
755
- recursive = false
756
- ): void {
757
- if (!this.filePath || this.filePath.includes("node_modules")) return;
758
-
759
- const unwatch = watchFile(this.filePath, () => {
760
- const newHash = getFileHash(this.filePath);
761
- if (newHash === this.fileHash) return;
762
-
763
- this.logger.debug(`Plugin "${this.name}" file changed, reloading...`);
764
- callback(this);
765
- this.fileHash = newHash;
766
- });
767
-
768
- this.on("dispose", unwatch);
769
-
770
- if (recursive) {
771
- for (const child of this.children) {
772
- child.watch(callback, recursive);
773
- }
774
- }
775
- }
776
-
777
- // ============================================================================
778
- // 辅助方法
779
- // ============================================================================
780
-
781
- // 核心方法列表(需要绑定的方法)
782
- static #coreMethods = new Set([
783
- 'addMiddleware', 'useContext', 'inject', 'contextIsReady',
784
- 'start', 'stop', 'onMounted', 'onDispose',
785
- 'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info'
786
- ]);
787
-
788
- /**
789
- * 自动绑定核心方法(只在构造函数中调用一次)
790
- */
791
- $bindMethods(): void {
792
- if (this.#methodsBound) return;
793
- this.#methodsBound = true;
794
-
795
- const proto = Object.getPrototypeOf(this);
796
- for (const key of Plugin.#coreMethods) {
797
- const value = proto[key];
798
- if (typeof value === "function") {
799
- (this as any)[key] = value.bind(this);
800
- }
801
- }
802
- }
803
-
804
- // ============================================================================
805
- // 静态方法
806
- // ============================================================================
807
-
808
- /**
809
- * 创建插件实例(异步加载)
810
- */
811
- static async create(entry: string, parent?: Plugin): Promise<Plugin> {
812
- entry = path.resolve(
813
- path.dirname(parent?.filePath || fileURLToPath(import.meta.url)),
814
- entry
815
- );
816
- const entryFile = fs.existsSync(entry) ? entry : require.resolve(entry);
817
- const realPath = fs.realpathSync(entryFile);
818
-
819
- // 检查模块是否已加载
820
- const existing = loadedModules.get(realPath);
821
- if (existing) {
822
- return existing;
823
- }
824
-
825
- const plugin = new Plugin(realPath, parent);
826
- plugin.fileHash = getFileHash(entryFile);
827
-
828
- // 先记录,防止循环依赖时重复加载
829
- loadedModules.set(realPath, plugin);
830
-
831
- await storage.run(plugin, async () => {
832
- await import(`${import.meta.resolve(entryFile)}?t=${Date.now()}`);
833
- });
834
-
835
- return plugin;
836
- }
837
- }
838
- export function defineContext<T extends keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}>(options: Context<T, E>): Context<T, E> {
839
- return options;
840
- }
841
- export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}> {
842
- name: T;
843
- description: string
844
- value?: Plugin.Contexts[T];
845
- mounted?: (parent: Plugin) => Plugin.Contexts[T] | Promise<Plugin.Contexts[T]>;
846
- dispose?: (value: Plugin.Contexts[T]) => void;
847
- /** 扩展方法,会自动挂载到 Plugin.prototype 上 */
848
- extensions?: E;
849
- }
850
- // ============================================================================
851
- // 类型定义
852
- // ============================================================================
853
- export namespace Plugin {
854
- /**
855
- * 插件提供的功能
856
- */
857
- export interface Features {
858
- commands: string[];
859
- components: string[];
860
- crons: string[];
861
- middlewares: string[];
862
- }
863
-
864
- /**
865
- * 生命周期事件
866
- */
867
- export interface Lifecycle {
868
- mounted: [];
869
- dispose: [];
870
- "before-start": [Plugin];
871
- started: [Plugin];
872
- "before-mount": [Plugin];
873
- "before-dispose": [Plugin];
874
- "call.recallMessage": [string, string, string];
875
- 'before.sendMessage': [SendOptions];
876
- "context.mounted": [keyof Plugin.Contexts];
877
- "context.dispose": [keyof Plugin.Contexts];
878
- "message.receive": [any];
879
- "bot.login.pending": [import('./built/login-assist.js').PendingLoginTask];
880
- "request.receive": [any];
881
- "notice.receive": [any];
882
- }
883
-
884
- /**
885
- * 服务类型扩展点
886
- * 各个 Context 通过 declare module 扩展此接口
887
- */
888
- export interface Contexts extends Adapters {}
889
-
890
- /**
891
- * Service 扩展方法类型
892
- * 这些方法由各个 Context 的 extensions 提供
893
- * 在 Plugin.start() 时自动注册到 Plugin.prototype
894
- */
895
- export interface Extensions {}
896
- }