@zhin.js/core 1.0.37 → 1.0.39

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 (204) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +57 -3
  3. package/lib/adapter.d.ts +11 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +61 -0
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/index.d.ts +3 -39
  8. package/lib/ai/index.d.ts.map +1 -1
  9. package/lib/ai/index.js +2 -44
  10. package/lib/ai/index.js.map +1 -1
  11. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  12. package/lib/ai/providers/anthropic.js +2 -0
  13. package/lib/ai/providers/anthropic.js.map +1 -1
  14. package/lib/ai/providers/openai.d.ts.map +1 -1
  15. package/lib/ai/providers/openai.js +8 -0
  16. package/lib/ai/providers/openai.js.map +1 -1
  17. package/lib/ai/types.d.ts +5 -3
  18. package/lib/ai/types.d.ts.map +1 -1
  19. package/lib/built/ai-trigger.js.map +1 -1
  20. package/lib/built/common-adapter-tools.d.ts +55 -0
  21. package/lib/built/common-adapter-tools.d.ts.map +1 -0
  22. package/lib/built/common-adapter-tools.js +158 -0
  23. package/lib/built/common-adapter-tools.js.map +1 -0
  24. package/lib/built/dispatcher.d.ts.map +1 -1
  25. package/lib/built/dispatcher.js +50 -46
  26. package/lib/built/dispatcher.js.map +1 -1
  27. package/lib/built/skill.d.ts.map +1 -1
  28. package/lib/built/skill.js +0 -1
  29. package/lib/built/skill.js.map +1 -1
  30. package/lib/built/tool.d.ts +3 -3
  31. package/lib/built/tool.d.ts.map +1 -1
  32. package/lib/built/tool.js.map +1 -1
  33. package/lib/feature.d.ts +16 -1
  34. package/lib/feature.d.ts.map +1 -1
  35. package/lib/feature.js +41 -2
  36. package/lib/feature.js.map +1 -1
  37. package/lib/index.d.ts +1 -0
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js +2 -0
  40. package/lib/index.js.map +1 -1
  41. package/lib/plugin.d.ts +38 -1
  42. package/lib/plugin.d.ts.map +1 -1
  43. package/lib/plugin.js +73 -22
  44. package/lib/plugin.js.map +1 -1
  45. package/lib/scheduler/scheduler.js +1 -1
  46. package/lib/scheduler/scheduler.js.map +1 -1
  47. package/lib/types.d.ts +43 -28
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/utils.d.ts +12 -3
  50. package/lib/utils.d.ts.map +1 -1
  51. package/lib/utils.js +64 -54
  52. package/lib/utils.js.map +1 -1
  53. package/package.json +5 -5
  54. package/src/adapter.ts +85 -5
  55. package/src/ai/index.ts +8 -186
  56. package/src/ai/providers/anthropic.ts +1 -0
  57. package/src/ai/providers/openai.ts +5 -1
  58. package/src/ai/types.ts +6 -4
  59. package/src/built/ai-trigger.ts +2 -2
  60. package/src/built/common-adapter-tools.ts +207 -0
  61. package/src/built/dispatcher.ts +51 -52
  62. package/src/built/skill.ts +3 -4
  63. package/src/built/tool.ts +3 -3
  64. package/src/feature.ts +45 -2
  65. package/src/index.ts +2 -0
  66. package/src/plugin.ts +92 -31
  67. package/src/scheduler/scheduler.ts +1 -1
  68. package/src/types.ts +39 -28
  69. package/src/utils.ts +63 -52
  70. package/tests/ai/setup.ts +2 -2
  71. package/tests/utils.test.ts +1 -3
  72. package/lib/ai/agent.d.ts +0 -130
  73. package/lib/ai/agent.d.ts.map +0 -1
  74. package/lib/ai/agent.js +0 -702
  75. package/lib/ai/agent.js.map +0 -1
  76. package/lib/ai/bootstrap.d.ts +0 -91
  77. package/lib/ai/bootstrap.d.ts.map +0 -1
  78. package/lib/ai/bootstrap.js +0 -243
  79. package/lib/ai/bootstrap.js.map +0 -1
  80. package/lib/ai/builtin-tools.d.ts +0 -59
  81. package/lib/ai/builtin-tools.d.ts.map +0 -1
  82. package/lib/ai/builtin-tools.js +0 -777
  83. package/lib/ai/builtin-tools.js.map +0 -1
  84. package/lib/ai/compaction.d.ts +0 -132
  85. package/lib/ai/compaction.d.ts.map +0 -1
  86. package/lib/ai/compaction.js +0 -370
  87. package/lib/ai/compaction.js.map +0 -1
  88. package/lib/ai/context-manager.d.ts +0 -213
  89. package/lib/ai/context-manager.d.ts.map +0 -1
  90. package/lib/ai/context-manager.js +0 -313
  91. package/lib/ai/context-manager.js.map +0 -1
  92. package/lib/ai/conversation-memory.d.ts +0 -181
  93. package/lib/ai/conversation-memory.d.ts.map +0 -1
  94. package/lib/ai/conversation-memory.js +0 -581
  95. package/lib/ai/conversation-memory.js.map +0 -1
  96. package/lib/ai/cron-engine.d.ts +0 -92
  97. package/lib/ai/cron-engine.d.ts.map +0 -1
  98. package/lib/ai/cron-engine.js +0 -278
  99. package/lib/ai/cron-engine.js.map +0 -1
  100. package/lib/ai/follow-up.d.ts +0 -131
  101. package/lib/ai/follow-up.d.ts.map +0 -1
  102. package/lib/ai/follow-up.js +0 -265
  103. package/lib/ai/follow-up.js.map +0 -1
  104. package/lib/ai/hooks.d.ts +0 -143
  105. package/lib/ai/hooks.d.ts.map +0 -1
  106. package/lib/ai/hooks.js +0 -108
  107. package/lib/ai/hooks.js.map +0 -1
  108. package/lib/ai/init.d.ts +0 -30
  109. package/lib/ai/init.d.ts.map +0 -1
  110. package/lib/ai/init.js +0 -686
  111. package/lib/ai/init.js.map +0 -1
  112. package/lib/ai/output.d.ts +0 -93
  113. package/lib/ai/output.d.ts.map +0 -1
  114. package/lib/ai/output.js +0 -176
  115. package/lib/ai/output.js.map +0 -1
  116. package/lib/ai/rate-limiter.d.ts +0 -38
  117. package/lib/ai/rate-limiter.d.ts.map +0 -1
  118. package/lib/ai/rate-limiter.js +0 -86
  119. package/lib/ai/rate-limiter.js.map +0 -1
  120. package/lib/ai/service.d.ts +0 -88
  121. package/lib/ai/service.d.ts.map +0 -1
  122. package/lib/ai/service.js +0 -285
  123. package/lib/ai/service.js.map +0 -1
  124. package/lib/ai/session.d.ts +0 -186
  125. package/lib/ai/session.d.ts.map +0 -1
  126. package/lib/ai/session.js +0 -443
  127. package/lib/ai/session.js.map +0 -1
  128. package/lib/ai/subagent.d.ts +0 -50
  129. package/lib/ai/subagent.d.ts.map +0 -1
  130. package/lib/ai/subagent.js +0 -144
  131. package/lib/ai/subagent.js.map +0 -1
  132. package/lib/ai/tone-detector.d.ts +0 -19
  133. package/lib/ai/tone-detector.d.ts.map +0 -1
  134. package/lib/ai/tone-detector.js +0 -72
  135. package/lib/ai/tone-detector.js.map +0 -1
  136. package/lib/ai/tools.d.ts +0 -45
  137. package/lib/ai/tools.d.ts.map +0 -1
  138. package/lib/ai/tools.js +0 -206
  139. package/lib/ai/tools.js.map +0 -1
  140. package/lib/ai/user-profile.d.ts +0 -56
  141. package/lib/ai/user-profile.d.ts.map +0 -1
  142. package/lib/ai/user-profile.js +0 -130
  143. package/lib/ai/user-profile.js.map +0 -1
  144. package/lib/ai/zhin-agent/builtin-tools.d.ts +0 -17
  145. package/lib/ai/zhin-agent/builtin-tools.d.ts.map +0 -1
  146. package/lib/ai/zhin-agent/builtin-tools.js +0 -220
  147. package/lib/ai/zhin-agent/builtin-tools.js.map +0 -1
  148. package/lib/ai/zhin-agent/config.d.ts +0 -54
  149. package/lib/ai/zhin-agent/config.d.ts.map +0 -1
  150. package/lib/ai/zhin-agent/config.js +0 -76
  151. package/lib/ai/zhin-agent/config.js.map +0 -1
  152. package/lib/ai/zhin-agent/exec-policy.d.ts +0 -20
  153. package/lib/ai/zhin-agent/exec-policy.d.ts.map +0 -1
  154. package/lib/ai/zhin-agent/exec-policy.js +0 -71
  155. package/lib/ai/zhin-agent/exec-policy.js.map +0 -1
  156. package/lib/ai/zhin-agent/index.d.ts +0 -70
  157. package/lib/ai/zhin-agent/index.d.ts.map +0 -1
  158. package/lib/ai/zhin-agent/index.js +0 -404
  159. package/lib/ai/zhin-agent/index.js.map +0 -1
  160. package/lib/ai/zhin-agent/prompt.d.ts +0 -21
  161. package/lib/ai/zhin-agent/prompt.d.ts.map +0 -1
  162. package/lib/ai/zhin-agent/prompt.js +0 -111
  163. package/lib/ai/zhin-agent/prompt.js.map +0 -1
  164. package/lib/ai/zhin-agent/tool-collector.d.ts +0 -22
  165. package/lib/ai/zhin-agent/tool-collector.d.ts.map +0 -1
  166. package/lib/ai/zhin-agent/tool-collector.js +0 -218
  167. package/lib/ai/zhin-agent/tool-collector.js.map +0 -1
  168. package/src/ai/agent.ts +0 -831
  169. package/src/ai/bootstrap.ts +0 -309
  170. package/src/ai/builtin-tools.ts +0 -849
  171. package/src/ai/compaction.ts +0 -529
  172. package/src/ai/context-manager.ts +0 -440
  173. package/src/ai/conversation-memory.ts +0 -774
  174. package/src/ai/cron-engine.ts +0 -337
  175. package/src/ai/follow-up.ts +0 -357
  176. package/src/ai/hooks.ts +0 -223
  177. package/src/ai/init.ts +0 -762
  178. package/src/ai/output.ts +0 -261
  179. package/src/ai/rate-limiter.ts +0 -129
  180. package/src/ai/service.ts +0 -331
  181. package/src/ai/session.ts +0 -544
  182. package/src/ai/subagent.ts +0 -209
  183. package/src/ai/tone-detector.ts +0 -89
  184. package/src/ai/tools.ts +0 -218
  185. package/src/ai/user-profile.ts +0 -181
  186. package/src/ai/zhin-agent/builtin-tools.ts +0 -247
  187. package/src/ai/zhin-agent/config.ts +0 -113
  188. package/src/ai/zhin-agent/exec-policy.ts +0 -78
  189. package/src/ai/zhin-agent/index.ts +0 -512
  190. package/src/ai/zhin-agent/prompt.ts +0 -131
  191. package/src/ai/zhin-agent/tool-collector.ts +0 -243
  192. package/tests/ai/agent.test.ts +0 -614
  193. package/tests/ai/context-manager.test.ts +0 -413
  194. package/tests/ai/conversation-memory.test.ts +0 -128
  195. package/tests/ai/follow-up.test.ts +0 -175
  196. package/tests/ai/integration.test.ts +0 -584
  197. package/tests/ai/output.test.ts +0 -128
  198. package/tests/ai/rate-limiter.test.ts +0 -108
  199. package/tests/ai/session.test.ts +0 -375
  200. package/tests/ai/subagent.test.ts +0 -270
  201. package/tests/ai/tone-detector.test.ts +0 -80
  202. package/tests/ai/tools-builtin.test.ts +0 -346
  203. package/tests/ai/user-profile.test.ts +0 -73
  204. package/tests/ai/zhin-agent.test.ts +0 -177
package/src/plugin.ts CHANGED
@@ -21,7 +21,8 @@ import { Adapter, Adapters } from "./adapter.js";
21
21
  import { Feature, FeatureJSON } from "./feature.js";
22
22
  import { createHash } from "crypto";
23
23
  const contextsKey = Symbol("contexts");
24
- const loadedModules = new Map<string, Plugin>(); // 记录已加载的模块
24
+ const extensionOwnersKey = Symbol("extensionOwners");
25
+ const loadedModules = new Map<string, Plugin>();
25
26
  const require = createRequire(import.meta.url);
26
27
 
27
28
 
@@ -138,8 +139,11 @@ export interface Plugin extends Plugin.Extensions { }
138
139
  */
139
140
  export class Plugin extends EventEmitter<Plugin.Lifecycle> {
140
141
  static [contextsKey] = [] as string[];
142
+ /** Maps extension method name → context name that owns it */
143
+ static [extensionOwnersKey] = new Map<string, string>();
141
144
 
142
145
  #cachedName?: string;
146
+ #explicitName?: string;
143
147
  adapters: (keyof Plugin.Contexts)[] = [];
144
148
  started = false;
145
149
 
@@ -182,6 +186,14 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
182
186
  get middleware(): MessageMiddleware<RegisteredAdapter> {
183
187
  return compose<RegisteredAdapter>(this.#middlewares);
184
188
  }
189
+
190
+ /**
191
+ * Returns custom middlewares (excluding the built-in command middleware).
192
+ * Used by MessageDispatcher to run legacy middlewares after dispatch.
193
+ */
194
+ _getCustomMiddlewares(): MessageMiddleware<RegisteredAdapter>[] {
195
+ return this.#middlewares.filter(m => m !== this.#messageMiddleware);
196
+ }
185
197
  /**
186
198
  * 构造函数
187
199
  */
@@ -213,8 +225,10 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
213
225
  * @param middleware 中间件函数
214
226
  */
215
227
  addMiddleware<T extends RegisteredAdapter>(middleware: MessageMiddleware<T>): () => void {
216
- if(this.parent){
217
- const dispose= this.parent.addMiddleware(middleware);
228
+ // Always register on root so middlewares reach the global chain
229
+ const target = this.root;
230
+ if (target !== this) {
231
+ const dispose = target.addMiddleware(middleware);
218
232
  this.#disposables.add(dispose);
219
233
  return () => {
220
234
  dispose();
@@ -241,8 +255,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
241
255
  tool: Tool,
242
256
  generateCommand: boolean = true
243
257
  ): () => void {
244
- // 尝试使用 ToolFeature
245
- const toolService = this.root.inject('tool' as any) as any;
258
+ const toolService = this.root.inject('tool') as { addTool?: (tool: Tool, name: string, gen: boolean) => () => void } | undefined;
246
259
  if (toolService && typeof toolService.addTool === 'function') {
247
260
  const dispose = toolService.addTool(tool, this.name, generateCommand);
248
261
  this.#disposables.add(dispose);
@@ -305,8 +318,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
305
318
  * 优先使用 ToolService,否则回退到本地收集
306
319
  */
307
320
  collectAllTools(): Tool[] {
308
- // 尝试使用 ToolService
309
- const toolService = this.root.inject('tool' as any) as any;
321
+ const toolService = this.root.inject('tool') as { collectAll?: (root: Plugin) => Tool[] } | undefined;
310
322
  if (toolService && typeof toolService.collectAll === 'function') {
311
323
  return toolService.collectAll(this.root);
312
324
  }
@@ -331,9 +343,22 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
331
343
  }
332
344
 
333
345
  /**
334
- * 插件名称
346
+ * 显式设置插件名称(优先级高于路径推导)。
347
+ * 可通过以下方式声明:
348
+ * 1. 模块导出 `export const pluginName = 'my-plugin'`
349
+ * 2. 调用 `plugin.setName('my-plugin')`
350
+ */
351
+ setName(name: string): void {
352
+ this.#explicitName = name;
353
+ this.#cachedName = name;
354
+ this.logger = logger.getLogger(name);
355
+ }
356
+
357
+ /**
358
+ * 插件名称(显式名称 > 路径推导)
335
359
  */
336
360
  get name(): string {
361
+ if (this.#explicitName) return this.#explicitName;
337
362
  if (this.#cachedName) return this.#cachedName;
338
363
 
339
364
  let name = path
@@ -394,16 +419,15 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
394
419
  if (!dispose) return;
395
420
  const disposeFn = async (name: keyof Plugin.Contexts) => {
396
421
  if (contexts.includes(name)) {
397
- await dispose(this.inject(name) as any)
422
+ await dispose(this.inject(name) as ArrayItem<ContextList<T>>)
398
423
  }
399
424
  this.off('context.dispose', disposeFn)
400
425
  sideEffect.finished = false;
401
426
  }
402
427
  this.on('context.dispose', disposeFn)
403
- // 确保 dispose 时清理监听器(只注册一次)
404
428
  const cleanupOnDispose = () => {
405
429
  this.off('context.dispose', disposeFn)
406
- dispose(this.inject(args[0] as any) as any)
430
+ dispose(this.inject(args[0] as unknown as keyof Plugin.Contexts) as unknown as ArrayItem<ContextList<T>>)
407
431
  }
408
432
  this.once('dispose', cleanupOnDispose)
409
433
  }
@@ -417,9 +441,11 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
417
441
  if (!this.#contextsIsReady(contexts)) return
418
442
  contextReadyCallback()
419
443
  }
420
- inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T] | undefined {
421
- const context = this.root.contexts.get(name as string);
422
- return context?.value as Plugin.Contexts[T];
444
+ inject<T extends keyof Plugin.Contexts>(name: T): Plugin.Contexts[T] | undefined;
445
+ inject(name: string): unknown;
446
+ inject(name: string): unknown {
447
+ const context = this.root.contexts.get(name);
448
+ return context?.value;
423
449
  }
424
450
  #contextsIsReady<CS extends (keyof Plugin.Contexts)[]>(contexts: CS) {
425
451
  if (!contexts.length) return true
@@ -457,13 +483,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
457
483
  for (const child of this.children) {
458
484
  await child.start(t);
459
485
  }
460
- // 输出启动日志(使用 debug 级别,避免重复输出)
461
- // 只在根插件或重要插件时使用 info 级别
462
- if (!this.parent || this.name === 'setup') {
463
- this.logger.info(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
464
- } else {
465
- this.logger.debug(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
466
- }
486
+ this.logger.debug(`Plugin "${this.name}" ${t ? `reloaded in ${Date.now() - t}ms` : "started"}`);
467
487
  }
468
488
  /**
469
489
  * 记录 Feature 贡献(由 Feature extensions 内部调用)
@@ -636,13 +656,15 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
636
656
  }
637
657
  this.children = [];
638
658
 
639
- // 停止服务
659
+ // 停止服务 — only remove extensions owned by this plugin's contexts
640
660
  for (const [name, context] of this.$contexts) {
641
661
  remove(Plugin[contextsKey], name);
642
- // 移除扩展方法
643
662
  if (context.extensions) {
644
663
  for (const key of Object.keys(context.extensions)) {
645
- delete (Plugin.prototype as any)[key];
664
+ if (Plugin[extensionOwnersKey].get(key) === name) {
665
+ delete (Plugin.prototype as unknown as Record<string, unknown>)[key];
666
+ Plugin[extensionOwnersKey].delete(key);
667
+ }
646
668
  }
647
669
  }
648
670
  if (typeof context.dispose === "function") {
@@ -750,16 +772,15 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
750
772
  */
751
773
  provide<T extends keyof Plugin.Contexts>(target: Feature | Context<T>): this {
752
774
  if (target instanceof Feature) {
753
- // Feature → 自动转为内部 Context 格式存储
754
775
  const feature = target;
755
776
  const context: Context<T> = {
756
777
  name: feature.name as T,
757
778
  description: feature.desc,
758
- value: feature as any,
779
+ value: feature as Plugin.Contexts[T],
759
780
  mounted: feature.mounted
760
781
  ? async (plugin: Plugin) => {
761
782
  await feature.mounted!(plugin);
762
- return feature as any;
783
+ return feature as Plugin.Contexts[T];
763
784
  }
764
785
  : undefined,
765
786
  dispose: feature.dispose
@@ -770,17 +791,24 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
770
791
  return this.provide(context);
771
792
  }
772
793
 
773
- // 传统 Context 路径
774
794
  const context = target;
775
795
  if (!Plugin[contextsKey].includes(context.name as string)) {
776
796
  Plugin[contextsKey].push(context.name as string);
777
797
  }
778
798
  this.logger.debug(`Context "${context.name as string}" provided`);
779
- // 注册扩展方法到 Plugin.prototype
799
+
800
+ // Track which extension methods this context registers.
801
+ // On collision, log a warning but allow override (last-write-wins).
780
802
  if (context.extensions) {
803
+ const extNames: string[] = [];
781
804
  for (const [name, fn] of Object.entries(context.extensions)) {
782
805
  if (typeof fn === 'function') {
806
+ if (Reflect.has(Plugin.prototype, name) && !Plugin[extensionOwnersKey].has(name)) {
807
+ this.logger.warn(`Extension method "${name}" shadows an existing Plugin method`);
808
+ }
783
809
  Reflect.set(Plugin.prototype, name, fn);
810
+ Plugin[extensionOwnersKey].set(name, context.name as string);
811
+ extNames.push(name);
784
812
  }
785
813
  }
786
814
  }
@@ -894,7 +922,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
894
922
  for (const key of Plugin.#coreMethods) {
895
923
  const value = proto[key];
896
924
  if (typeof value === "function") {
897
- (this as any)[key] = value.bind(this);
925
+ (this as Record<string, unknown>)[key] = value.bind(this);
898
926
  }
899
927
  }
900
928
  }
@@ -926,16 +954,49 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
926
954
  // 先记录,防止循环依赖时重复加载
927
955
  loadedModules.set(realPath, plugin);
928
956
 
957
+ let mod: any;
929
958
  await storage.run(plugin, async () => {
930
- await import(`${pathToFileURL(entryFile).href}?t=${Date.now()}`);
959
+ mod = await import(`${pathToFileURL(entryFile).href}?t=${Date.now()}`);
931
960
  });
932
961
 
962
+ // 支持模块显式声明插件名称:export const pluginName = 'my-plugin'
963
+ if (mod?.pluginName && typeof mod.pluginName === 'string') {
964
+ plugin.setName(mod.pluginName);
965
+ }
966
+
933
967
  return plugin;
934
968
  }
935
969
  }
936
970
  export function defineContext<T extends keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}>(options: Context<T, E>): Context<T, E> {
937
971
  return options;
938
972
  }
973
+
974
+ /**
975
+ * 声明式定义插件。
976
+ * 提供显式名称 + setup 函数,自动设置当前插件名称并执行 setup。
977
+ *
978
+ * @example
979
+ * ```typescript
980
+ * // plugins/weather/index.ts
981
+ * export default definePlugin({
982
+ * name: 'weather',
983
+ * setup(plugin) {
984
+ * plugin.addTool({ ... });
985
+ * },
986
+ * });
987
+ * ```
988
+ */
989
+ export function definePlugin(options: {
990
+ name: string;
991
+ setup: (plugin: Plugin) => MaybePromise<void>;
992
+ }): { pluginName: string } {
993
+ const plugin = usePlugin();
994
+ if (options.name) {
995
+ plugin.setName(options.name);
996
+ }
997
+ options.setup(plugin);
998
+ return { pluginName: options.name };
999
+ }
939
1000
  export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts, E extends Partial<Plugin.Extensions> = {}> {
940
1001
  name: T;
941
1002
  description: string
@@ -200,7 +200,7 @@ export class Scheduler implements IScheduler {
200
200
  };
201
201
  this.heartbeatJobId = job.id;
202
202
  this.store.jobs.push(job);
203
- logger.info({ intervalMs: this.heartbeatIntervalMs }, 'Heartbeat job added');
203
+ logger.debug({ intervalMs: this.heartbeatIntervalMs }, 'Heartbeat job added');
204
204
  }
205
205
 
206
206
  private recomputeNextRuns(): void {
package/src/types.ts CHANGED
@@ -14,7 +14,7 @@ export interface Models extends Record<string,object>{
14
14
  SystemLog: SystemLog
15
15
  User: User,
16
16
  }
17
- export type MaybePromise<T> = T extends Promise<infer U> ? T|U : T|Promise<T>;
17
+ export type MaybePromise<T> = [T] extends [Promise<infer U>] ? T|U : T|Promise<T>;
18
18
  export interface RegisteredAdapters {
19
19
  process: ProcessAdapter;
20
20
  }
@@ -71,6 +71,8 @@ export interface MessageSender{
71
71
  id: string;
72
72
  name?: string;
73
73
  permissions?:string[]
74
+ /** 平台侧角色标识(owner / admin / member 等) */
75
+ role?: string;
74
76
  }
75
77
  /**
76
78
  * 通用字典类型
@@ -305,25 +307,52 @@ export type ToolScope = 'private' | 'group' | 'channel';
305
307
  */
306
308
  export type ToolPermissionLevel = 'user' | 'group_admin' | 'group_owner' | 'bot_admin' | 'owner';
307
309
 
308
- export interface Tool {
310
+ /**
311
+ * 标准化工具返回类型。
312
+ * execute 可返回以下任一形式:
313
+ * - string: 直接作为文本回复
314
+ * - { text: string }: 结构化文本
315
+ * - { data: unknown; format?: string }: 结构化数据
316
+ * - void/null/undefined: 无回复
317
+ * - Record / Array: 自动 JSON.stringify
318
+ */
319
+ export type ToolResult = string | void | null | undefined | { text: string } | { data: unknown; format?: string } | Record<string, unknown> | unknown[];
320
+
321
+ /**
322
+ * 统一的 Tool 定义(支持泛型参数类型推断)。
323
+ *
324
+ * @template TArgs 参数类型,默认 Record<string, any>
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * // 无泛型 — 兼容旧代码
329
+ * const tool: Tool = { name: 'ping', ... };
330
+ *
331
+ * // 有泛型 — 通过 defineTool 获得类型安全
332
+ * const tool = defineTool<{ city: string }>({
333
+ * name: 'weather',
334
+ * parameters: { type: 'object', properties: { city: { type: 'string', description: '城市' } }, required: ['city'] },
335
+ * execute: async (args) => args.city, // args.city 有类型提示
336
+ * });
337
+ * ```
338
+ */
339
+ export interface Tool<TArgs extends Record<string, any> = Record<string, any>> {
309
340
  /** 工具名称(唯一标识,建议使用 snake_case) */
310
341
  name: string;
311
342
 
312
343
  /** 工具描述(供 AI 和帮助系统使用) */
313
344
  description: string;
314
345
 
315
- /**
316
- * 参数定义(JSON Schema 格式)
317
- */
318
- parameters: ToolParametersSchema;
346
+ /** 参数定义(JSON Schema 格式) */
347
+ parameters: ToolParametersSchema<TArgs>;
319
348
 
320
349
  /**
321
350
  * 工具执行函数
322
351
  * @param args 解析后的参数
323
352
  * @param context 执行上下文(包含消息、发送者等信息)
324
- * @returns 执行结果(字符串会直接作为回复,对象会被 JSON 序列化)
353
+ * @returns 执行结果
325
354
  */
326
- execute: (args: Record<string, any>, context?: ToolContext) => MaybePromise<any>;
355
+ execute: (args: TArgs, context?: ToolContext) => MaybePromise<ToolResult>;
327
356
 
328
357
  /** 工具来源标识(自动填充:adapter:xxx / plugin:xxx) */
329
358
  source?: string;
@@ -386,27 +415,9 @@ export interface Tool {
386
415
  }
387
416
 
388
417
  /**
389
- * 类型安全的 Tool 定义(用于 defineTool)
390
- * 提供泛型参数以获得 execute 函数的类型推断
418
+ * @deprecated 使用 `Tool<TArgs>` 替代。Tool 已原生支持泛型。
391
419
  */
392
- export interface ToolDefinition<TArgs extends Record<string, any> = Record<string, any>> {
393
- name: string;
394
- description: string;
395
- parameters: ToolParametersSchema<TArgs>;
396
- execute: (args: TArgs, context?: ToolContext) => MaybePromise<any>;
397
- source?: string;
398
- tags?: string[];
399
- keywords?: string[];
400
- command?: Tool.CommandConfig | false;
401
- permissions?: string[];
402
- /** 支持的平台列表(不填则支持所有平台) */
403
- platforms?: string[];
404
- /** 支持的场景列表(不填则支持所有场景) */
405
- scopes?: ToolScope[];
406
- /** 调用所需的最低权限级别(默认 'user') */
407
- permissionLevel?: ToolPermissionLevel;
408
- hidden?: boolean;
409
- }
420
+ export type ToolDefinition<TArgs extends Record<string, any> = Record<string, any>> = Tool<TArgs>;
410
421
 
411
422
  export namespace Tool {
412
423
  /**
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as path from "path";
2
2
  import * as fs from "fs";
3
+ import * as vm from "vm";
3
4
  import {
4
5
  AdapterMessage,
5
6
  Dict,
@@ -11,15 +12,21 @@ import {
11
12
  import { Message } from "./message.js";
12
13
 
13
14
  export function getValueWithRuntime(template: string, ctx: Dict) {
14
- const result = evaluate(template, ctx);
15
- if (result === `return(${template})`) return undefined;
16
- return result;
15
+ return evaluate(template, ctx);
17
16
  }
18
- export const evaluate = <S, T = any>(exp: string, context: S) => {
19
- const result = execute<S, T>(`return(${exp})`, context);
20
- // 如果结果是原始表达式,说明访问被阻止,返回 undefined
21
- if (result === `return(${exp})`) return undefined;
22
- return result;
17
+ /**
18
+ * Evaluate a single expression in a sandboxed vm context.
19
+ * Unlike `execute`, does NOT wrap in IIFE — the expression value is returned directly.
20
+ */
21
+ export const evaluate = <S extends Record<string, unknown>, T = unknown>(exp: string, context: S): T | undefined => {
22
+ const script = getOrCompileScript(exp);
23
+ if (!script) return undefined;
24
+
25
+ try {
26
+ return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
27
+ } catch {
28
+ return undefined;
29
+ }
23
30
  };
24
31
  /**
25
32
  * 组合中间件,洋葱模型
@@ -74,25 +81,28 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
74
81
  return dispatch(0);
75
82
  };
76
83
  }
77
- // 使用 LRU 缓存限制大小,防止内存泄漏
84
+ // LRU cache for compiled vm.Script instances
78
85
  const MAX_EVAL_CACHE_SIZE = 1000;
79
- const evalCache: Record<string, Function> = Object.create(null);
80
- const evalCacheKeys: string[] = [];
86
+ const scriptCache = new Map<string, vm.Script>();
81
87
 
82
- export const execute = <S, T = any>(exp: string, context: S): T => {
83
- let fn = evalCache[exp];
84
-
85
- if (!fn) {
86
- // 如果缓存已满,删除最旧的条目(LRU)
87
- if (evalCacheKeys.length >= MAX_EVAL_CACHE_SIZE) {
88
- const oldest = evalCacheKeys.shift()!;
89
- delete evalCache[oldest];
90
- }
91
-
92
- fn = evalCache[exp] = toFunction(exp);
93
- evalCacheKeys.push(exp);
88
+ function getOrCompileScript(code: string): vm.Script | null {
89
+ let script = scriptCache.get(code);
90
+ if (script) return script;
91
+ try {
92
+ script = new vm.Script(code);
93
+ } catch {
94
+ return null;
94
95
  }
95
- context = {
96
+ if (scriptCache.size >= MAX_EVAL_CACHE_SIZE) {
97
+ const oldest = scriptCache.keys().next().value;
98
+ if (oldest !== undefined) scriptCache.delete(oldest);
99
+ }
100
+ scriptCache.set(code, script);
101
+ return script;
102
+ }
103
+
104
+ function buildSandbox<S extends Record<string, unknown>>(context: S): Record<string, unknown> {
105
+ return {
96
106
  ...context,
97
107
  process: {
98
108
  version: process.version,
@@ -106,48 +116,48 @@ export const execute = <S, T = any>(exp: string, context: S): T => {
106
116
  pid: process.pid,
107
117
  ppid: process.ppid,
108
118
  },
109
- Bun: "你想干嘛",
110
119
  global: undefined,
120
+ globalThis: undefined,
111
121
  Buffer: undefined,
112
122
  crypto: undefined,
123
+ require: undefined,
124
+ import: undefined,
125
+ __dirname: undefined,
126
+ __filename: undefined,
127
+ Bun: undefined,
128
+ Deno: undefined,
113
129
  };
114
- try {
115
- return fn.apply(context, [context]);
116
- } catch {
117
- return exp as T;
118
- }
119
- };
130
+ }
120
131
 
121
- const toFunction = (exp: string): Function => {
122
- try {
123
- return new Function(`$data`, `with($data){${exp}}`);
124
- } catch {
125
- return () => { };
126
- }
132
+ /**
133
+ * Execute a code block in a sandboxed vm context.
134
+ * Supports `return` statements by wrapping in an IIFE.
135
+ * Throws on compilation or runtime errors.
136
+ */
137
+ export const execute = <S extends Record<string, unknown>, T = unknown>(code: string, context: S): T => {
138
+ const wrapped = `(function(){${code}})()`;
139
+ const script = getOrCompileScript(wrapped);
140
+ if (!script) throw new SyntaxError(`Failed to compile: ${code.slice(0, 80)}`);
141
+
142
+ return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
127
143
  };
128
144
 
129
- // 清理 evalCache(用于内存调试)
130
145
  export function clearEvalCache(): void {
131
- Object.keys(evalCache).forEach(key => {
132
- delete evalCache[key];
133
- });
134
- evalCacheKeys.length = 0;
146
+ scriptCache.clear();
135
147
  }
136
148
 
137
- // 获取 evalCache 统计信息(用于内存调试)
138
149
  export function getEvalCacheStats(): { size: number; maxSize: number } {
139
150
  return {
140
- size: evalCacheKeys.length,
141
- maxSize: MAX_EVAL_CACHE_SIZE
151
+ size: scriptCache.size,
152
+ maxSize: MAX_EVAL_CACHE_SIZE,
142
153
  };
143
154
  }
144
155
  export function compiler(template: string, ctx: Dict) {
145
156
  const matched = [...template.matchAll(/\${([^}]*?)}/g)];
146
157
  for (const item of matched) {
147
158
  const tpl = item[1];
148
- let value = getValueWithRuntime(tpl, ctx);
149
- if (value === tpl) continue;
150
- if (typeof value !== "string") value = JSON.stringify(value, null, 2);
159
+ const raw = getValueWithRuntime(tpl, ctx);
160
+ const value = typeof raw === 'string' ? raw : (raw == null ? 'undefined' : JSON.stringify(raw, null, 2));
151
161
  template = template.replace(`\${${item[1]}}`, value);
152
162
  }
153
163
  return template;
@@ -394,14 +404,15 @@ export namespace Time {
394
404
 
395
405
  export function parseDate(date: string) {
396
406
  const parsed = parseTime(date);
407
+ let dateInput: string | number = date;
397
408
  if (parsed) {
398
- date = (Date.now() + parsed) as any;
409
+ dateInput = Date.now() + parsed;
399
410
  } else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
400
- date = `${new Date().toLocaleDateString()}-${date}`;
411
+ dateInput = `${new Date().toLocaleDateString()}-${date}`;
401
412
  } else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
402
- date = `${new Date().getFullYear()}-${date}`;
413
+ dateInput = `${new Date().getFullYear()}-${date}`;
403
414
  }
404
- return date ? new Date(date) : new Date();
415
+ return dateInput ? new Date(dateInput) : new Date();
405
416
  }
406
417
 
407
418
  export function formatTimeShort(ms: number) {
package/tests/ai/setup.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { vi } from 'vitest';
10
10
  import type { Message, MessageElement, Tool, ToolContext } from '@zhin.js/core';
11
- import type { AIConfig, AIProviderConfig, ChatMessage } from '../../src/ai/types.js';
11
+ import type { AIConfig, ChatMessage } from '@zhin.js/core';
12
12
 
13
13
  // ============================================================================
14
14
  // Logger Mock
@@ -226,7 +226,7 @@ export const createMockAIConfig = (overrides: Partial<AIConfig> = {}): AIConfig
226
226
  defaultProvider: 'mock',
227
227
  sessions: {
228
228
  maxHistory: 10,
229
- timeout: 300000,
229
+ expireMs: 300000,
230
230
  },
231
231
  context: {
232
232
  enabled: false,
@@ -140,9 +140,7 @@ describe('evaluate and execute', () => {
140
140
  })
141
141
 
142
142
  it('should handle invalid expressions gracefully', () => {
143
- const result = execute('invalid syntax here !!!', {})
144
- // 无效表达式会被 try-catch 捕获,返回 undefined
145
- expect(result).toBeUndefined()
143
+ expect(() => execute('invalid syntax here !!!', {})).toThrow()
146
144
  })
147
145
 
148
146
  it('should provide safe process context', () => {