@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.
- package/CHANGELOG.md +18 -0
- package/README.md +57 -3
- package/lib/adapter.d.ts +11 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +61 -0
- package/lib/adapter.js.map +1 -1
- package/lib/ai/index.d.ts +3 -39
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +2 -44
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/providers/anthropic.d.ts.map +1 -1
- package/lib/ai/providers/anthropic.js +2 -0
- package/lib/ai/providers/anthropic.js.map +1 -1
- package/lib/ai/providers/openai.d.ts.map +1 -1
- package/lib/ai/providers/openai.js +8 -0
- package/lib/ai/providers/openai.js.map +1 -1
- package/lib/ai/types.d.ts +5 -3
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/built/ai-trigger.js.map +1 -1
- package/lib/built/common-adapter-tools.d.ts +55 -0
- package/lib/built/common-adapter-tools.d.ts.map +1 -0
- package/lib/built/common-adapter-tools.js +158 -0
- package/lib/built/common-adapter-tools.js.map +1 -0
- package/lib/built/dispatcher.d.ts.map +1 -1
- package/lib/built/dispatcher.js +50 -46
- package/lib/built/dispatcher.js.map +1 -1
- package/lib/built/skill.d.ts.map +1 -1
- package/lib/built/skill.js +0 -1
- package/lib/built/skill.js.map +1 -1
- package/lib/built/tool.d.ts +3 -3
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js.map +1 -1
- package/lib/feature.d.ts +16 -1
- package/lib/feature.d.ts.map +1 -1
- package/lib/feature.js +41 -2
- package/lib/feature.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +38 -1
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +73 -22
- package/lib/plugin.js.map +1 -1
- package/lib/scheduler/scheduler.js +1 -1
- package/lib/scheduler/scheduler.js.map +1 -1
- package/lib/types.d.ts +43 -28
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts +12 -3
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +64 -54
- package/lib/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/adapter.ts +85 -5
- package/src/ai/index.ts +8 -186
- package/src/ai/providers/anthropic.ts +1 -0
- package/src/ai/providers/openai.ts +5 -1
- package/src/ai/types.ts +6 -4
- package/src/built/ai-trigger.ts +2 -2
- package/src/built/common-adapter-tools.ts +207 -0
- package/src/built/dispatcher.ts +51 -52
- package/src/built/skill.ts +3 -4
- package/src/built/tool.ts +3 -3
- package/src/feature.ts +45 -2
- package/src/index.ts +2 -0
- package/src/plugin.ts +92 -31
- package/src/scheduler/scheduler.ts +1 -1
- package/src/types.ts +39 -28
- package/src/utils.ts +63 -52
- package/tests/ai/setup.ts +2 -2
- package/tests/utils.test.ts +1 -3
- package/lib/ai/agent.d.ts +0 -130
- package/lib/ai/agent.d.ts.map +0 -1
- package/lib/ai/agent.js +0 -702
- package/lib/ai/agent.js.map +0 -1
- package/lib/ai/bootstrap.d.ts +0 -91
- package/lib/ai/bootstrap.d.ts.map +0 -1
- package/lib/ai/bootstrap.js +0 -243
- package/lib/ai/bootstrap.js.map +0 -1
- package/lib/ai/builtin-tools.d.ts +0 -59
- package/lib/ai/builtin-tools.d.ts.map +0 -1
- package/lib/ai/builtin-tools.js +0 -777
- package/lib/ai/builtin-tools.js.map +0 -1
- package/lib/ai/compaction.d.ts +0 -132
- package/lib/ai/compaction.d.ts.map +0 -1
- package/lib/ai/compaction.js +0 -370
- package/lib/ai/compaction.js.map +0 -1
- package/lib/ai/context-manager.d.ts +0 -213
- package/lib/ai/context-manager.d.ts.map +0 -1
- package/lib/ai/context-manager.js +0 -313
- package/lib/ai/context-manager.js.map +0 -1
- package/lib/ai/conversation-memory.d.ts +0 -181
- package/lib/ai/conversation-memory.d.ts.map +0 -1
- package/lib/ai/conversation-memory.js +0 -581
- package/lib/ai/conversation-memory.js.map +0 -1
- package/lib/ai/cron-engine.d.ts +0 -92
- package/lib/ai/cron-engine.d.ts.map +0 -1
- package/lib/ai/cron-engine.js +0 -278
- package/lib/ai/cron-engine.js.map +0 -1
- package/lib/ai/follow-up.d.ts +0 -131
- package/lib/ai/follow-up.d.ts.map +0 -1
- package/lib/ai/follow-up.js +0 -265
- package/lib/ai/follow-up.js.map +0 -1
- package/lib/ai/hooks.d.ts +0 -143
- package/lib/ai/hooks.d.ts.map +0 -1
- package/lib/ai/hooks.js +0 -108
- package/lib/ai/hooks.js.map +0 -1
- package/lib/ai/init.d.ts +0 -30
- package/lib/ai/init.d.ts.map +0 -1
- package/lib/ai/init.js +0 -686
- package/lib/ai/init.js.map +0 -1
- package/lib/ai/output.d.ts +0 -93
- package/lib/ai/output.d.ts.map +0 -1
- package/lib/ai/output.js +0 -176
- package/lib/ai/output.js.map +0 -1
- package/lib/ai/rate-limiter.d.ts +0 -38
- package/lib/ai/rate-limiter.d.ts.map +0 -1
- package/lib/ai/rate-limiter.js +0 -86
- package/lib/ai/rate-limiter.js.map +0 -1
- package/lib/ai/service.d.ts +0 -88
- package/lib/ai/service.d.ts.map +0 -1
- package/lib/ai/service.js +0 -285
- package/lib/ai/service.js.map +0 -1
- package/lib/ai/session.d.ts +0 -186
- package/lib/ai/session.d.ts.map +0 -1
- package/lib/ai/session.js +0 -443
- package/lib/ai/session.js.map +0 -1
- package/lib/ai/subagent.d.ts +0 -50
- package/lib/ai/subagent.d.ts.map +0 -1
- package/lib/ai/subagent.js +0 -144
- package/lib/ai/subagent.js.map +0 -1
- package/lib/ai/tone-detector.d.ts +0 -19
- package/lib/ai/tone-detector.d.ts.map +0 -1
- package/lib/ai/tone-detector.js +0 -72
- package/lib/ai/tone-detector.js.map +0 -1
- package/lib/ai/tools.d.ts +0 -45
- package/lib/ai/tools.d.ts.map +0 -1
- package/lib/ai/tools.js +0 -206
- package/lib/ai/tools.js.map +0 -1
- package/lib/ai/user-profile.d.ts +0 -56
- package/lib/ai/user-profile.d.ts.map +0 -1
- package/lib/ai/user-profile.js +0 -130
- package/lib/ai/user-profile.js.map +0 -1
- package/lib/ai/zhin-agent/builtin-tools.d.ts +0 -17
- package/lib/ai/zhin-agent/builtin-tools.d.ts.map +0 -1
- package/lib/ai/zhin-agent/builtin-tools.js +0 -220
- package/lib/ai/zhin-agent/builtin-tools.js.map +0 -1
- package/lib/ai/zhin-agent/config.d.ts +0 -54
- package/lib/ai/zhin-agent/config.d.ts.map +0 -1
- package/lib/ai/zhin-agent/config.js +0 -76
- package/lib/ai/zhin-agent/config.js.map +0 -1
- package/lib/ai/zhin-agent/exec-policy.d.ts +0 -20
- package/lib/ai/zhin-agent/exec-policy.d.ts.map +0 -1
- package/lib/ai/zhin-agent/exec-policy.js +0 -71
- package/lib/ai/zhin-agent/exec-policy.js.map +0 -1
- package/lib/ai/zhin-agent/index.d.ts +0 -70
- package/lib/ai/zhin-agent/index.d.ts.map +0 -1
- package/lib/ai/zhin-agent/index.js +0 -404
- package/lib/ai/zhin-agent/index.js.map +0 -1
- package/lib/ai/zhin-agent/prompt.d.ts +0 -21
- package/lib/ai/zhin-agent/prompt.d.ts.map +0 -1
- package/lib/ai/zhin-agent/prompt.js +0 -111
- package/lib/ai/zhin-agent/prompt.js.map +0 -1
- package/lib/ai/zhin-agent/tool-collector.d.ts +0 -22
- package/lib/ai/zhin-agent/tool-collector.d.ts.map +0 -1
- package/lib/ai/zhin-agent/tool-collector.js +0 -218
- package/lib/ai/zhin-agent/tool-collector.js.map +0 -1
- package/src/ai/agent.ts +0 -831
- package/src/ai/bootstrap.ts +0 -309
- package/src/ai/builtin-tools.ts +0 -849
- package/src/ai/compaction.ts +0 -529
- package/src/ai/context-manager.ts +0 -440
- package/src/ai/conversation-memory.ts +0 -774
- package/src/ai/cron-engine.ts +0 -337
- package/src/ai/follow-up.ts +0 -357
- package/src/ai/hooks.ts +0 -223
- package/src/ai/init.ts +0 -762
- package/src/ai/output.ts +0 -261
- package/src/ai/rate-limiter.ts +0 -129
- package/src/ai/service.ts +0 -331
- package/src/ai/session.ts +0 -544
- package/src/ai/subagent.ts +0 -209
- package/src/ai/tone-detector.ts +0 -89
- package/src/ai/tools.ts +0 -218
- package/src/ai/user-profile.ts +0 -181
- package/src/ai/zhin-agent/builtin-tools.ts +0 -247
- package/src/ai/zhin-agent/config.ts +0 -113
- package/src/ai/zhin-agent/exec-policy.ts +0 -78
- package/src/ai/zhin-agent/index.ts +0 -512
- package/src/ai/zhin-agent/prompt.ts +0 -131
- package/src/ai/zhin-agent/tool-collector.ts +0 -243
- package/tests/ai/agent.test.ts +0 -614
- package/tests/ai/context-manager.test.ts +0 -413
- package/tests/ai/conversation-memory.test.ts +0 -128
- package/tests/ai/follow-up.test.ts +0 -175
- package/tests/ai/integration.test.ts +0 -584
- package/tests/ai/output.test.ts +0 -128
- package/tests/ai/rate-limiter.test.ts +0 -108
- package/tests/ai/session.test.ts +0 -375
- package/tests/ai/subagent.test.ts +0 -270
- package/tests/ai/tone-detector.test.ts +0 -80
- package/tests/ai/tools-builtin.test.ts +0 -346
- package/tests/ai/user-profile.test.ts +0 -73
- 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
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
353
|
+
* @returns 执行结果
|
|
325
354
|
*/
|
|
326
|
-
execute: (args:
|
|
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
|
-
*
|
|
390
|
-
* 提供泛型参数以获得 execute 函数的类型推断
|
|
418
|
+
* @deprecated 使用 `Tool<TArgs>` 替代。Tool 已原生支持泛型。
|
|
391
419
|
*/
|
|
392
|
-
export
|
|
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
|
-
|
|
15
|
-
if (result === `return(${template})`) return undefined;
|
|
16
|
-
return result;
|
|
15
|
+
return evaluate(template, ctx);
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
84
|
+
// LRU cache for compiled vm.Script instances
|
|
78
85
|
const MAX_EVAL_CACHE_SIZE = 1000;
|
|
79
|
-
const
|
|
80
|
-
const evalCacheKeys: string[] = [];
|
|
86
|
+
const scriptCache = new Map<string, vm.Script>();
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
return fn.apply(context, [context]);
|
|
116
|
-
} catch {
|
|
117
|
-
return exp as T;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
130
|
+
}
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
409
|
+
dateInput = Date.now() + parsed;
|
|
399
410
|
} else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
|
|
400
|
-
|
|
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
|
-
|
|
413
|
+
dateInput = `${new Date().getFullYear()}-${date}`;
|
|
403
414
|
}
|
|
404
|
-
return
|
|
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,
|
|
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
|
-
|
|
229
|
+
expireMs: 300000,
|
|
230
230
|
},
|
|
231
231
|
context: {
|
|
232
232
|
enabled: false,
|
package/tests/utils.test.ts
CHANGED
|
@@ -140,9 +140,7 @@ describe('evaluate and execute', () => {
|
|
|
140
140
|
})
|
|
141
141
|
|
|
142
142
|
it('should handle invalid expressions gracefully', () => {
|
|
143
|
-
|
|
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', () => {
|