@zhin.js/core 1.0.36 → 1.0.38
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/types.d.ts +4 -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/types.ts +5 -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 -684
- 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 -812
- 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/built/dispatcher.ts
CHANGED
|
@@ -149,72 +149,73 @@ declare module '../plugin.js' {
|
|
|
149
149
|
* 创建 MessageDispatcher Context
|
|
150
150
|
*/
|
|
151
151
|
export function createMessageDispatcher(): Context<'dispatcher', DispatcherContextExtensions> {
|
|
152
|
-
// 护栏中间件列表
|
|
153
152
|
const guardrails: GuardrailMiddleware[] = [];
|
|
154
|
-
|
|
155
|
-
// AI 处理函数(由 AI 模块注册)
|
|
156
153
|
let aiHandler: AIHandler | null = null;
|
|
157
|
-
|
|
158
|
-
// AI 触发判定器(由 AI 模块注册)
|
|
159
154
|
let aiTriggerMatcher: AITriggerMatcher | null = null;
|
|
160
|
-
|
|
161
|
-
// 命令匹配器(默认由 command service 驱动)
|
|
162
155
|
let commandMatcher: CommandMatcher | null = null;
|
|
163
|
-
|
|
164
|
-
// 根插件引用(mounted 时设置)
|
|
165
156
|
let rootPlugin: Plugin | null = null;
|
|
166
157
|
|
|
158
|
+
// Command prefix index — rebuilt lazily for O(1) lookup
|
|
159
|
+
let commandPrefixIndex: Map<string, boolean> | null = null;
|
|
160
|
+
let lastCommandCount = -1;
|
|
161
|
+
|
|
162
|
+
function rebuildCommandIndex(): Map<string, boolean> {
|
|
163
|
+
const index = new Map<string, boolean>();
|
|
164
|
+
if (rootPlugin) {
|
|
165
|
+
const commandService = rootPlugin.inject('command');
|
|
166
|
+
if (commandService?.items) {
|
|
167
|
+
for (const cmd of commandService.items) {
|
|
168
|
+
const prefix = cmd.pattern?.split(/\s/)[0];
|
|
169
|
+
if (prefix) index.set(prefix, true);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return index;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getCommandIndex(): Map<string, boolean> {
|
|
177
|
+
const commandService = rootPlugin?.inject('command');
|
|
178
|
+
const currentCount = commandService?.items?.length ?? 0;
|
|
179
|
+
if (!commandPrefixIndex || currentCount !== lastCommandCount) {
|
|
180
|
+
commandPrefixIndex = rebuildCommandIndex();
|
|
181
|
+
lastCommandCount = currentCount;
|
|
182
|
+
}
|
|
183
|
+
return commandPrefixIndex;
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
/**
|
|
168
|
-
*
|
|
169
|
-
* 返回 true 表示消息通过,false 表示被拦截
|
|
187
|
+
* Guardrail pipeline — a guardrail that does NOT call next() blocks the message.
|
|
170
188
|
*/
|
|
171
189
|
async function runGuardrails(message: Message<any>): Promise<boolean> {
|
|
172
190
|
if (guardrails.length === 0) return true;
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const runNext = async (): Promise<void> => {
|
|
178
|
-
if (index >= guardrails.length) return;
|
|
179
|
-
const guardrail = guardrails[index++];
|
|
192
|
+
for (const guardrail of guardrails) {
|
|
193
|
+
let nextCalled = false;
|
|
180
194
|
try {
|
|
181
|
-
await guardrail(message,
|
|
195
|
+
await guardrail(message, async () => { nextCalled = true; });
|
|
182
196
|
} catch {
|
|
183
|
-
|
|
184
|
-
passed = false;
|
|
197
|
+
return false;
|
|
185
198
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return passed;
|
|
199
|
+
if (!nextCalled) return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
190
202
|
}
|
|
191
203
|
|
|
192
|
-
/**
|
|
193
|
-
* 路由判定:这条消息该走哪条路径?
|
|
194
|
-
*/
|
|
195
204
|
function route(message: Message<any>): RouteResult {
|
|
196
|
-
// 提取文本内容
|
|
197
205
|
const text = extractText(message);
|
|
198
206
|
|
|
199
|
-
// 1. 如果有自定义命令匹配器,优先使用
|
|
200
207
|
if (commandMatcher && commandMatcher(text, message)) {
|
|
201
208
|
return { type: 'command' };
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
const pattern = cmd.pattern?.split(/\s/)[0];
|
|
210
|
-
if (pattern && text.startsWith(pattern)) {
|
|
211
|
-
return { type: 'command' };
|
|
212
|
-
}
|
|
213
|
-
}
|
|
211
|
+
// Use indexed lookup instead of O(N) scan
|
|
212
|
+
const index = getCommandIndex();
|
|
213
|
+
for (const [prefix] of index) {
|
|
214
|
+
if (text.startsWith(prefix)) {
|
|
215
|
+
return { type: 'command' };
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
// 3. AI 触发判定
|
|
218
219
|
if (aiTriggerMatcher) {
|
|
219
220
|
const { triggered, content } = aiTriggerMatcher(message);
|
|
220
221
|
if (triggered) {
|
|
@@ -222,13 +223,9 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
// 4. 都不匹配 → 跳过
|
|
226
226
|
return { type: 'skip' };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
/**
|
|
230
|
-
* 提取消息文本(简单实现,与 ai-trigger 中的 extractTextContent 对齐)
|
|
231
|
-
*/
|
|
232
229
|
function extractText(message: Message<any>): string {
|
|
233
230
|
if (!message.$content) return '';
|
|
234
231
|
return message.$content
|
|
@@ -241,8 +238,6 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
241
238
|
.trim();
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
// ─── 服务实例 ───────────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
241
|
const service: MessageDispatcherService = {
|
|
247
242
|
async dispatch(message: Message<any>) {
|
|
248
243
|
// Stage 1: Guardrail
|
|
@@ -255,7 +250,6 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
255
250
|
// Stage 3: Handle
|
|
256
251
|
switch (result.type) {
|
|
257
252
|
case 'command': {
|
|
258
|
-
// 命令快速路径 — 直接委托给 commandService
|
|
259
253
|
if (rootPlugin) {
|
|
260
254
|
const commandService = rootPlugin.inject('command');
|
|
261
255
|
if (commandService) {
|
|
@@ -269,7 +263,6 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
269
263
|
}
|
|
270
264
|
|
|
271
265
|
case 'ai': {
|
|
272
|
-
// AI Agent 路径
|
|
273
266
|
if (aiHandler) {
|
|
274
267
|
await aiHandler(message, result.content);
|
|
275
268
|
}
|
|
@@ -278,13 +271,19 @@ export function createMessageDispatcher(): Context<'dispatcher', DispatcherConte
|
|
|
278
271
|
|
|
279
272
|
case 'skip':
|
|
280
273
|
default:
|
|
281
|
-
// 不处理
|
|
282
274
|
break;
|
|
283
275
|
}
|
|
284
276
|
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
277
|
+
// Run legacy custom middlewares (skip the built-in command middleware at index 0).
|
|
278
|
+
// This ensures plugins that registered middlewares via addMiddleware() still work.
|
|
279
|
+
if (rootPlugin) {
|
|
280
|
+
const customMiddlewares = (rootPlugin as any)._getCustomMiddlewares?.() as MessageMiddleware<RegisteredAdapter>[] | undefined;
|
|
281
|
+
if (customMiddlewares && customMiddlewares.length > 0) {
|
|
282
|
+
const { compose } = await import('../utils.js');
|
|
283
|
+
const composed = compose(customMiddlewares);
|
|
284
|
+
await composed(message, async () => {});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
288
287
|
},
|
|
289
288
|
|
|
290
289
|
addGuardrail(guardrail: GuardrailMiddleware) {
|
package/src/built/skill.ts
CHANGED
|
@@ -232,21 +232,20 @@ export class SkillFeature extends Feature<Skill> {
|
|
|
232
232
|
const pluginName = plugin.name;
|
|
233
233
|
|
|
234
234
|
// 收集该插件注册的工具
|
|
235
|
-
const toolService = plugin.root.inject('tool' as
|
|
235
|
+
const toolService = plugin.root.inject('tool') as { getToolsByPlugin?: (name: string) => Tool[] } | undefined;
|
|
236
236
|
let tools: Tool[] = [];
|
|
237
237
|
|
|
238
238
|
if (toolService && typeof toolService.getToolsByPlugin === 'function') {
|
|
239
239
|
tools = toolService.getToolsByPlugin(pluginName);
|
|
240
240
|
} else {
|
|
241
|
-
// 回退:从插件本地工具获取
|
|
242
241
|
tools = plugin.getAllTools?.() || [];
|
|
243
242
|
}
|
|
244
243
|
|
|
245
244
|
// 聚合关键词:开发者声明 + 工具自带
|
|
246
245
|
const allKeywords = new Set<string>(metadata.keywords || []);
|
|
247
246
|
for (const tool of tools) {
|
|
248
|
-
if (
|
|
249
|
-
for (const kw of
|
|
247
|
+
if (tool.keywords) {
|
|
248
|
+
for (const kw of tool.keywords) {
|
|
250
249
|
allKeywords.add(kw);
|
|
251
250
|
}
|
|
252
251
|
}
|
package/src/built/tool.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Feature, FeatureJSON } from "../feature.js";
|
|
|
6
6
|
import { MessageCommand } from "../command.js";
|
|
7
7
|
import { Message } from "../message.js";
|
|
8
8
|
import { Plugin, getPlugin } from "../plugin.js";
|
|
9
|
-
import type { Tool,
|
|
9
|
+
import type { Tool, RegisteredAdapter, AdapterMessage, ToolContext, ToolJsonSchema, ToolParametersSchema, PropertySchema, MaybePromise, ToolPermissionLevel, ToolScope } from "../types.js";
|
|
10
10
|
import { MatchResult } from "segment-matcher";
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
@@ -147,7 +147,7 @@ export function extractParamInfo(parameters: ToolJsonSchema): Tool.ParamInfo[] {
|
|
|
147
147
|
* 定义工具的辅助函数(提供类型推断)
|
|
148
148
|
*/
|
|
149
149
|
export function defineTool<TArgs extends Record<string, any> = Record<string, any>>(
|
|
150
|
-
tool:
|
|
150
|
+
tool: Tool<TArgs>
|
|
151
151
|
): Tool {
|
|
152
152
|
return tool as Tool;
|
|
153
153
|
}
|
|
@@ -587,7 +587,7 @@ function commandToToolFn(
|
|
|
587
587
|
|
|
588
588
|
const plugin = getPlugin();
|
|
589
589
|
const result = await command.handle(tempMessage, plugin);
|
|
590
|
-
return result;
|
|
590
|
+
return result as import("../types.js").ToolResult;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
return {
|
package/src/feature.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface FeatureJSON {
|
|
|
16
16
|
items: any[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Feature 变更事件监听器类型
|
|
21
|
+
*/
|
|
22
|
+
export type FeatureListener<T> = (item: T, pluginName: string) => void;
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Feature<T> 抽象类
|
|
21
26
|
* - name / icon / desc: 自描述元数据
|
|
@@ -24,6 +29,7 @@ export interface FeatureJSON {
|
|
|
24
29
|
* - toJSON: 控制 HTTP API 返回的数据格式
|
|
25
30
|
* - extensions: 提供给 Plugin.prototype 的扩展方法(如 addCommand)
|
|
26
31
|
* - mounted / dispose: 可选生命周期钩子
|
|
32
|
+
* - on('add' | 'remove'): 变更事件通知
|
|
27
33
|
*/
|
|
28
34
|
export abstract class Feature<T = any> {
|
|
29
35
|
abstract readonly name: string;
|
|
@@ -36,6 +42,32 @@ export abstract class Feature<T = any> {
|
|
|
36
42
|
/** 按插件名分组的 item 列表 */
|
|
37
43
|
protected pluginItems = new Map<string, T[]>();
|
|
38
44
|
|
|
45
|
+
/** 事件监听器 */
|
|
46
|
+
#listeners = new Map<string, Set<FeatureListener<T>>>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 监听变更事件
|
|
50
|
+
* @param event 'add' | 'remove'
|
|
51
|
+
* @param listener 回调函数
|
|
52
|
+
* @returns 取消监听的函数
|
|
53
|
+
*/
|
|
54
|
+
on(event: 'add' | 'remove', listener: FeatureListener<T>): () => void {
|
|
55
|
+
if (!this.#listeners.has(event)) {
|
|
56
|
+
this.#listeners.set(event, new Set());
|
|
57
|
+
}
|
|
58
|
+
this.#listeners.get(event)!.add(listener);
|
|
59
|
+
return () => { this.#listeners.get(event)?.delete(listener); };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 触发事件 */
|
|
63
|
+
protected emit(event: 'add' | 'remove', item: T, pluginName: string): void {
|
|
64
|
+
const listeners = this.#listeners.get(event);
|
|
65
|
+
if (!listeners) return;
|
|
66
|
+
for (const listener of listeners) {
|
|
67
|
+
try { listener(item, pluginName); } catch { /* 防止监听器异常影响主流程 */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
/**
|
|
40
72
|
* 添加 item,同时记录所属插件
|
|
41
73
|
* @returns dispose 函数,用于移除该 item
|
|
@@ -46,25 +78,36 @@ export abstract class Feature<T = any> {
|
|
|
46
78
|
this.pluginItems.set(pluginName, []);
|
|
47
79
|
}
|
|
48
80
|
this.pluginItems.get(pluginName)!.push(item);
|
|
49
|
-
|
|
81
|
+
this.emit('add', item, pluginName);
|
|
82
|
+
return () => this.remove(item, pluginName);
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
/**
|
|
53
86
|
* 移除 item
|
|
54
87
|
*/
|
|
55
|
-
remove(item: T): boolean {
|
|
88
|
+
remove(item: T, pluginName?: string): boolean {
|
|
56
89
|
const idx = this.items.indexOf(item);
|
|
57
90
|
if (idx !== -1) {
|
|
58
91
|
this.items.splice(idx, 1);
|
|
92
|
+
const resolvedPluginName = pluginName ?? this.#findPluginName(item);
|
|
59
93
|
for (const [, items] of this.pluginItems) {
|
|
60
94
|
const i = items.indexOf(item);
|
|
61
95
|
if (i !== -1) items.splice(i, 1);
|
|
62
96
|
}
|
|
97
|
+
this.emit('remove', item, resolvedPluginName ?? '');
|
|
63
98
|
return true;
|
|
64
99
|
}
|
|
65
100
|
return false;
|
|
66
101
|
}
|
|
67
102
|
|
|
103
|
+
/** 反查 item 所属的 pluginName */
|
|
104
|
+
#findPluginName(item: T): string | undefined {
|
|
105
|
+
for (const [name, items] of this.pluginItems) {
|
|
106
|
+
if (items.includes(item)) return name;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
68
111
|
/**
|
|
69
112
|
* 获取指定插件注册的 item 列表
|
|
70
113
|
*/
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ export * from './built/ai-trigger.js'
|
|
|
26
26
|
export * from './built/dispatcher.js'
|
|
27
27
|
// Skill 系统 (AI 能力描述)
|
|
28
28
|
export * from './built/skill.js'
|
|
29
|
+
// Common adapter tool factories (shared across adapters)
|
|
30
|
+
export * from './built/common-adapter-tools.js'
|
|
29
31
|
// AI 模块 (原 @zhin.js/ai,已合并至 core)
|
|
30
32
|
export * from './ai/index.js'
|
|
31
33
|
|
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 {
|