@zhin.js/core 1.0.50 → 1.0.52
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 +22 -0
- package/README.md +1 -1
- package/lib/adapter.d.ts +10 -19
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +47 -80
- package/lib/adapter.js.map +1 -1
- package/lib/ai/types.d.ts +11 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/built/common-adapter-tools.d.ts +2 -2
- package/lib/built/common-adapter-tools.js +3 -3
- package/lib/built/common-adapter-tools.js.map +1 -1
- package/lib/built/dispatcher.d.ts +52 -42
- package/lib/built/dispatcher.d.ts.map +1 -1
- package/lib/built/dispatcher.js +194 -60
- package/lib/built/dispatcher.js.map +1 -1
- package/lib/built/login-assist.d.ts +61 -0
- package/lib/built/login-assist.d.ts.map +1 -0
- package/lib/built/login-assist.js +75 -0
- package/lib/built/login-assist.js.map +1 -0
- package/lib/built/skill.d.ts +3 -20
- package/lib/built/skill.d.ts.map +1 -1
- package/lib/built/skill.js +1 -53
- package/lib/built/skill.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 +18 -0
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +1 -1
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +13 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.js +1 -1
- package/package.json +6 -6
- package/src/adapter.ts +51 -95
- package/src/ai/types.ts +3 -1
- package/src/built/common-adapter-tools.ts +3 -3
- package/src/built/dispatcher.ts +264 -99
- package/src/built/login-assist.ts +131 -0
- package/src/built/skill.ts +3 -75
- package/src/index.ts +2 -0
- package/src/plugin.ts +24 -5
- package/src/types.ts +16 -0
- package/src/utils.ts +1 -1
- package/tests/adapter.test.ts +40 -128
- package/tests/dispatcher.test.ts +155 -8
- package/tests/redos-protection.test.ts +5 -4
package/src/built/skill.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Plugin = 运行时容器(生命周期、服务注册、中间件)
|
|
6
6
|
* Skill = AI 可见的能力接口(名称、描述、工具列表)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* "我叫什么、我能做什么、我有哪些工具"
|
|
8
|
+
* Skill 记录由运行时注入(如 Agent 从磁盘 SKILL.md 同步),供 Agent 粗筛与工具关联。
|
|
10
9
|
*
|
|
11
10
|
* SkillFeature 全局收集所有 Skill,供 Agent 进行两级过滤:
|
|
12
11
|
* 1. 粗筛:根据用户消息选择相关 Skill
|
|
@@ -14,7 +13,6 @@
|
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { Feature, FeatureJSON } from '../feature.js';
|
|
17
|
-
import { Plugin, getPlugin } from '../plugin.js';
|
|
18
16
|
import type { Tool } from '../types.js';
|
|
19
17
|
|
|
20
18
|
// ============================================================================
|
|
@@ -51,35 +49,21 @@ export interface Skill {
|
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
*
|
|
55
|
-
* 由 plugin.declareSkill() 注册
|
|
52
|
+
* SKILL.md frontmatter 常见字段(类型提示;运行时由 Agent 等同步到 SkillFeature)
|
|
56
53
|
*/
|
|
57
54
|
export interface SkillMetadata {
|
|
58
55
|
/** 技能描述(必填) */
|
|
59
56
|
description: string;
|
|
60
57
|
|
|
61
|
-
/**
|
|
58
|
+
/** 触发关键词(可选) */
|
|
62
59
|
keywords?: string[];
|
|
63
60
|
|
|
64
61
|
/** 分类标签(可选) */
|
|
65
62
|
tags?: string[];
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// 扩展 Plugin 接口
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
export interface SkillContextExtensions {
|
|
73
|
-
/**
|
|
74
|
-
* 声明本插件的 Skill 元数据
|
|
75
|
-
* 调用后,插件的工具会自动聚合为一个 Skill 注册到 SkillFeature
|
|
76
|
-
*/
|
|
77
|
-
declareSkill(metadata: SkillMetadata): void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
65
|
declare module '../plugin.js' {
|
|
81
66
|
namespace Plugin {
|
|
82
|
-
interface Extensions extends SkillContextExtensions {}
|
|
83
67
|
interface Contexts {
|
|
84
68
|
skill: SkillFeature;
|
|
85
69
|
}
|
|
@@ -220,60 +204,4 @@ export class SkillFeature extends Feature<Skill> {
|
|
|
220
204
|
})),
|
|
221
205
|
};
|
|
222
206
|
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 提供给 Plugin.prototype 的扩展方法
|
|
226
|
-
*/
|
|
227
|
-
get extensions() {
|
|
228
|
-
const feature = this;
|
|
229
|
-
return {
|
|
230
|
-
declareSkill(metadata: SkillMetadata) {
|
|
231
|
-
const plugin = getPlugin();
|
|
232
|
-
const pluginName = plugin.name;
|
|
233
|
-
|
|
234
|
-
// 收集该插件注册的工具
|
|
235
|
-
const toolService = plugin.root.inject('tool') as { getToolsByPlugin?: (name: string) => Tool[] } | undefined;
|
|
236
|
-
let tools: Tool[] = [];
|
|
237
|
-
|
|
238
|
-
if (toolService && typeof toolService.getToolsByPlugin === 'function') {
|
|
239
|
-
tools = toolService.getToolsByPlugin(pluginName);
|
|
240
|
-
} else {
|
|
241
|
-
tools = plugin.getAllTools?.() || [];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 聚合关键词:开发者声明 + 工具自带
|
|
245
|
-
const allKeywords = new Set<string>(metadata.keywords || []);
|
|
246
|
-
for (const tool of tools) {
|
|
247
|
-
if (tool.keywords) {
|
|
248
|
-
for (const kw of tool.keywords) {
|
|
249
|
-
allKeywords.add(kw);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 聚合标签
|
|
255
|
-
const allTags = new Set<string>(metadata.tags || []);
|
|
256
|
-
for (const tool of tools) {
|
|
257
|
-
if (tool.tags) {
|
|
258
|
-
for (const tag of tool.tags) {
|
|
259
|
-
allTags.add(tag);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const skill: Skill = {
|
|
265
|
-
name: pluginName,
|
|
266
|
-
description: metadata.description,
|
|
267
|
-
tools,
|
|
268
|
-
keywords: Array.from(allKeywords),
|
|
269
|
-
tags: Array.from(allTags),
|
|
270
|
-
pluginName,
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const dispose = feature.add(skill, pluginName);
|
|
274
|
-
plugin.recordFeatureContribution(feature.name, pluginName);
|
|
275
|
-
plugin.onDispose(dispose);
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
207
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,8 @@ export * from './built/skill.js'
|
|
|
33
33
|
export * from './built/schema-feature.js'
|
|
34
34
|
// Common adapter tool factories (shared across adapters)
|
|
35
35
|
export * from './built/common-adapter-tools.js'
|
|
36
|
+
// Login assist (producer-consumer for QR / SMS / slider etc.)
|
|
37
|
+
export * from './built/login-assist.js'
|
|
36
38
|
// AI 模块 (原 @zhin.js/ai,已合并至 core)
|
|
37
39
|
export * from './ai/index.js'
|
|
38
40
|
|
package/src/plugin.ts
CHANGED
|
@@ -6,19 +6,24 @@
|
|
|
6
6
|
import { AsyncLocalStorage } from "async_hooks";
|
|
7
7
|
import { EventEmitter } from "events";
|
|
8
8
|
import { createRequire } from "module";
|
|
9
|
-
import type {
|
|
10
|
-
import { Schema } from "@zhin.js/schema";
|
|
11
|
-
import type { Models, RegisteredAdapters, Tool, ToolContext } from "./types.js";
|
|
9
|
+
import type { Tool } from "./types.js";
|
|
12
10
|
import * as fs from "fs";
|
|
13
11
|
import * as path from "path";
|
|
14
12
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
15
13
|
import logger, { Logger } from "@zhin.js/logger";
|
|
16
14
|
import { compose, remove, resolveEntry } from "./utils.js";
|
|
17
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MessageMiddleware,
|
|
17
|
+
RegisteredAdapter,
|
|
18
|
+
MaybePromise,
|
|
19
|
+
ArrayItem,
|
|
20
|
+
SendOptions,
|
|
21
|
+
} from "./types.js";
|
|
18
22
|
import type { ConfigFeature } from "./built/config.js";
|
|
19
23
|
import type { PermissionFeature } from "./built/permission.js";
|
|
20
24
|
import { Adapter, Adapters } from "./adapter.js";
|
|
21
25
|
import { Notice } from "./notice.js";
|
|
26
|
+
import { Message } from "./message.js";
|
|
22
27
|
import { Request } from "./request.js";
|
|
23
28
|
import { Feature, FeatureJSON } from "./feature.js";
|
|
24
29
|
import { createHash } from "crypto";
|
|
@@ -910,7 +915,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
|
|
|
910
915
|
'addMiddleware', 'useContext', 'inject', 'contextIsReady',
|
|
911
916
|
'start', 'stop', 'onMounted', 'onDispose',
|
|
912
917
|
'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info',
|
|
913
|
-
'recordFeatureContribution', 'getFeatures'
|
|
918
|
+
'recordFeatureContribution', 'getFeatures',
|
|
914
919
|
]);
|
|
915
920
|
|
|
916
921
|
/**
|
|
@@ -1011,6 +1016,17 @@ export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts
|
|
|
1011
1016
|
// ============================================================================
|
|
1012
1017
|
// 类型定义
|
|
1013
1018
|
// ============================================================================
|
|
1019
|
+
|
|
1020
|
+
/** 登录辅助:bot.login.pending 事件 payload */
|
|
1021
|
+
export interface BotLoginPendingTask {
|
|
1022
|
+
id: string;
|
|
1023
|
+
adapter: string;
|
|
1024
|
+
botId: string;
|
|
1025
|
+
type: string;
|
|
1026
|
+
payload?: { message?: string; image?: string; url?: string; [key: string]: unknown };
|
|
1027
|
+
createdAt: number;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1014
1030
|
export namespace Plugin {
|
|
1015
1031
|
/**
|
|
1016
1032
|
* 生命周期事件
|
|
@@ -1026,8 +1042,11 @@ export namespace Plugin {
|
|
|
1026
1042
|
'before.sendMessage': [SendOptions];
|
|
1027
1043
|
"context.mounted": [keyof Plugin.Contexts];
|
|
1028
1044
|
"context.dispose": [keyof Plugin.Contexts];
|
|
1045
|
+
/** 登录辅助:有待办时触发 */
|
|
1046
|
+
'bot.login.pending': [BotLoginPendingTask];
|
|
1029
1047
|
// Notice 事件
|
|
1030
1048
|
'notice.receive': [Notice];
|
|
1049
|
+
'message.receive': [Message];
|
|
1031
1050
|
[key: `notice.${string}`]: [Notice];
|
|
1032
1051
|
// Request 事件
|
|
1033
1052
|
'request.receive': [Request];
|
package/src/types.ts
CHANGED
|
@@ -64,6 +64,22 @@ export type MaybeArray<T>=T|T[]
|
|
|
64
64
|
* 消息发送内容类型
|
|
65
65
|
*/
|
|
66
66
|
export type SendContent=MaybeArray<string|MessageElement>
|
|
67
|
+
|
|
68
|
+
/** 出站回复来源(指令 / AI),仅当经 MessageDispatcher.replyWithPolish 发出时由框架填入异步上下文 */
|
|
69
|
+
export type OutboundReplySource = 'command' | 'ai'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 出站润色上下文(`dispatcher.addOutboundPolish` 的 handler 签名的同构类型)。
|
|
73
|
+
* 与 {@link Adapter.sendMessage} → `before.sendMessage` 同一管道;需 `message`/`source` 时见 `getOutboundReplyStore`(dispatcher 导出)。
|
|
74
|
+
*/
|
|
75
|
+
export interface OutboundPolishContext {
|
|
76
|
+
message: Message
|
|
77
|
+
content: SendContent
|
|
78
|
+
source: OutboundReplySource
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 返回 `SendContent` 则替换后续 `before.sendMessage` 与发送中的 content */
|
|
82
|
+
export type OutboundPolishMiddleware = (ctx: OutboundPolishContext) => MaybePromise<SendContent | void>
|
|
67
83
|
/**
|
|
68
84
|
* 消息发送者信息
|
|
69
85
|
*/
|
package/src/utils.ts
CHANGED
|
@@ -107,7 +107,7 @@ export namespace segment {
|
|
|
107
107
|
const toString = (template: string | MessageElement) => {
|
|
108
108
|
if (typeof template !== "string") return [template];
|
|
109
109
|
|
|
110
|
-
const MAX_TEMPLATE_LENGTH =
|
|
110
|
+
const MAX_TEMPLATE_LENGTH = 400000;
|
|
111
111
|
if (template.length > MAX_TEMPLATE_LENGTH) {
|
|
112
112
|
throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
|
|
113
113
|
}
|
package/tests/adapter.test.ts
CHANGED
|
@@ -107,9 +107,8 @@ describe('Adapter Core Functionality', () => {
|
|
|
107
107
|
expect(adapter).toBeInstanceOf(EventEmitter)
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it('should
|
|
111
|
-
|
|
112
|
-
expect(listeners.length).toBeGreaterThan(0)
|
|
110
|
+
it('should route message.receive via emit without default listener', () => {
|
|
111
|
+
expect(adapter.listenerCount('message.receive')).toBe(0)
|
|
113
112
|
})
|
|
114
113
|
|
|
115
114
|
it('should register call.recallMessage listener', () => {
|
|
@@ -199,13 +198,15 @@ describe('Adapter Core Functionality', () => {
|
|
|
199
198
|
|
|
200
199
|
it('should remove all event listeners', async () => {
|
|
201
200
|
await adapter.start()
|
|
201
|
+
const noop = () => {}
|
|
202
|
+
adapter.on('message.receive', noop)
|
|
202
203
|
const beforeCount = adapter.listenerCount('message.receive')
|
|
203
|
-
|
|
204
|
+
expect(beforeCount).toBe(1)
|
|
205
|
+
|
|
204
206
|
await adapter.stop()
|
|
205
207
|
const afterCount = adapter.listenerCount('message.receive')
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
expect(afterCount).toBe(0)
|
|
208
|
-
expect(beforeCount).toBeGreaterThan(0)
|
|
209
210
|
})
|
|
210
211
|
|
|
211
212
|
it('should handle bot disconnect errors gracefully', async () => {
|
|
@@ -377,97 +378,92 @@ describe('Adapter Core Functionality', () => {
|
|
|
377
378
|
})
|
|
378
379
|
|
|
379
380
|
describe('message.receive', () => {
|
|
380
|
-
it('should
|
|
381
|
+
it('should still dispatch plugin message.receive when dispatcher is missing (no middleware fallback)', async () => {
|
|
381
382
|
const config = [{ id: 'bot1' }]
|
|
382
383
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
383
384
|
await adapter.start()
|
|
384
|
-
|
|
385
|
+
|
|
385
386
|
let middlewareCalled = false
|
|
386
|
-
plugin.addMiddleware(async (
|
|
387
|
+
plugin.addMiddleware(async (_message, next) => {
|
|
387
388
|
middlewareCalled = true
|
|
388
389
|
await next()
|
|
389
390
|
})
|
|
390
|
-
|
|
391
|
+
|
|
392
|
+
let lifecycleCalled = false
|
|
393
|
+
plugin.on('message.receive', () => {
|
|
394
|
+
lifecycleCalled = true
|
|
395
|
+
})
|
|
396
|
+
|
|
391
397
|
const message = {
|
|
392
398
|
$bot: 'bot1',
|
|
393
399
|
$adapter: 'test',
|
|
394
400
|
$channel: { id: 'channel-id', type: 'text' },
|
|
395
|
-
$content: 'Hello'
|
|
401
|
+
$content: 'Hello',
|
|
396
402
|
} as any
|
|
397
|
-
|
|
403
|
+
|
|
398
404
|
adapter.emit('message.receive', message)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
expect(middlewareCalled).toBe(true)
|
|
405
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
406
|
+
expect(middlewareCalled).toBe(false)
|
|
407
|
+
expect(lifecycleCalled).toBe(true)
|
|
403
408
|
})
|
|
404
409
|
|
|
405
|
-
it('should
|
|
410
|
+
it('should await MessageDispatcher then plugin lifecycle', async () => {
|
|
406
411
|
const config = [{ id: 'bot1' }]
|
|
407
412
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
408
413
|
await adapter.start()
|
|
409
414
|
|
|
410
|
-
|
|
411
|
-
let middlewareCalled = false
|
|
412
|
-
|
|
413
|
-
// 注册 dispatcher context
|
|
415
|
+
const order: string[] = []
|
|
414
416
|
plugin.$contexts.set('dispatcher', {
|
|
415
417
|
name: 'dispatcher',
|
|
416
418
|
description: 'mock dispatcher',
|
|
417
419
|
value: {
|
|
418
|
-
dispatch: (
|
|
420
|
+
dispatch: async (_msg: any) => {
|
|
421
|
+
order.push('dispatcher')
|
|
422
|
+
},
|
|
419
423
|
},
|
|
420
424
|
} as any)
|
|
421
425
|
|
|
422
|
-
plugin.
|
|
423
|
-
|
|
424
|
-
await next()
|
|
426
|
+
plugin.on('message.receive', () => {
|
|
427
|
+
order.push('lifecycle')
|
|
425
428
|
})
|
|
426
429
|
|
|
427
430
|
const message = {
|
|
428
431
|
$bot: 'bot1',
|
|
429
432
|
$adapter: 'test',
|
|
430
433
|
$channel: { id: 'channel-id', type: 'text' },
|
|
431
|
-
$content: 'Hello'
|
|
434
|
+
$content: 'Hello',
|
|
432
435
|
} as any
|
|
433
436
|
|
|
434
437
|
adapter.emit('message.receive', message)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
expect(dispatchCalled).toBe(true)
|
|
438
|
-
expect(middlewareCalled).toBe(false)
|
|
438
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
439
|
+
expect(order).toEqual(['dispatcher', 'lifecycle'])
|
|
439
440
|
})
|
|
440
441
|
|
|
441
|
-
it('should
|
|
442
|
+
it('should call adapter.on observers after plugin lifecycle', async () => {
|
|
442
443
|
const config = [{ id: 'bot1' }]
|
|
443
444
|
const adapter = new MockAdapter(plugin, 'test', config)
|
|
444
445
|
await adapter.start()
|
|
445
446
|
|
|
446
|
-
let middlewareCalled = false
|
|
447
|
-
|
|
448
|
-
// 注册一个没有 dispatch 方法的 dispatcher
|
|
449
447
|
plugin.$contexts.set('dispatcher', {
|
|
450
448
|
name: 'dispatcher',
|
|
451
|
-
description: '
|
|
452
|
-
value: {
|
|
449
|
+
description: 'mock dispatcher',
|
|
450
|
+
value: { dispatch: async () => {} },
|
|
453
451
|
} as any)
|
|
454
452
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
})
|
|
453
|
+
const order: string[] = []
|
|
454
|
+
plugin.on('message.receive', () => order.push('lifecycle'))
|
|
455
|
+
adapter.on('message.receive', () => order.push('adapterObserver'))
|
|
459
456
|
|
|
460
457
|
const message = {
|
|
461
458
|
$bot: 'bot1',
|
|
462
459
|
$adapter: 'test',
|
|
463
460
|
$channel: { id: 'channel-id', type: 'text' },
|
|
464
|
-
$content: 'Hello'
|
|
461
|
+
$content: 'Hello',
|
|
465
462
|
} as any
|
|
466
463
|
|
|
467
464
|
adapter.emit('message.receive', message)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
expect(middlewareCalled).toBe(true)
|
|
465
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
466
|
+
expect(order).toEqual(['lifecycle', 'adapterObserver'])
|
|
471
467
|
})
|
|
472
468
|
})
|
|
473
469
|
})
|
|
@@ -617,90 +613,6 @@ describe('Adapter Core Functionality', () => {
|
|
|
617
613
|
})
|
|
618
614
|
})
|
|
619
615
|
|
|
620
|
-
describe('Adapter declareSkill', () => {
|
|
621
|
-
it('should register a skill when SkillFeature is available', () => {
|
|
622
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
623
|
-
|
|
624
|
-
// 模拟 SkillFeature
|
|
625
|
-
const mockSkillFeature = {
|
|
626
|
-
add: vi.fn(() => vi.fn()),
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// 设置 root plugin 和 inject
|
|
630
|
-
;(plugin as any)._root = plugin
|
|
631
|
-
const originalInject = plugin.inject.bind(plugin)
|
|
632
|
-
plugin.inject = ((name: string) => {
|
|
633
|
-
if (name === 'skill') return mockSkillFeature
|
|
634
|
-
return originalInject(name)
|
|
635
|
-
}) as any
|
|
636
|
-
;(plugin as any).recordFeatureContribution = vi.fn()
|
|
637
|
-
|
|
638
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
639
|
-
|
|
640
|
-
// 添加一个工具
|
|
641
|
-
adapter.addTool({
|
|
642
|
-
name: 'test_tool',
|
|
643
|
-
description: '测试工具',
|
|
644
|
-
parameters: { type: 'object', properties: {} },
|
|
645
|
-
execute: async () => '',
|
|
646
|
-
keywords: ['test'],
|
|
647
|
-
tags: ['testing'],
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
adapter.declareSkill({
|
|
651
|
-
description: '测试适配器的技能',
|
|
652
|
-
keywords: ['adapter'],
|
|
653
|
-
tags: ['adapter-tag'],
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
expect(mockSkillFeature.add).toHaveBeenCalledTimes(1)
|
|
657
|
-
const [skill] = mockSkillFeature.add.mock.calls[0]
|
|
658
|
-
expect(skill.name).toContain('test-adapter')
|
|
659
|
-
expect(skill.description).toBe('测试适配器的技能')
|
|
660
|
-
expect(skill.tools).toHaveLength(1)
|
|
661
|
-
// keywords 应合并适配器和工具的关键词
|
|
662
|
-
expect(skill.keywords).toContain('adapter')
|
|
663
|
-
expect(skill.keywords).toContain('test')
|
|
664
|
-
// tags 应合并
|
|
665
|
-
expect(skill.tags).toContain('adapter-tag')
|
|
666
|
-
expect(skill.tags).toContain('testing')
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
it('should clean up skill on stop', async () => {
|
|
670
|
-
const disposeSkill = vi.fn()
|
|
671
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
672
|
-
|
|
673
|
-
const mockSkillFeature = {
|
|
674
|
-
add: vi.fn(() => disposeSkill),
|
|
675
|
-
}
|
|
676
|
-
;(plugin as any)._root = plugin
|
|
677
|
-
plugin.inject = ((name: string) => {
|
|
678
|
-
if (name === 'skill') return mockSkillFeature
|
|
679
|
-
return undefined
|
|
680
|
-
}) as any
|
|
681
|
-
;(plugin as any).recordFeatureContribution = vi.fn()
|
|
682
|
-
|
|
683
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
684
|
-
adapter.declareSkill({ description: '测试' })
|
|
685
|
-
|
|
686
|
-
await adapter.stop()
|
|
687
|
-
|
|
688
|
-
expect(disposeSkill).toHaveBeenCalledTimes(1)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('should skip when SkillFeature is not available', () => {
|
|
692
|
-
const plugin = new Plugin('/test/adapter-plugin.ts')
|
|
693
|
-
;(plugin as any)._root = plugin
|
|
694
|
-
|
|
695
|
-
const adapter = new MockAdapter(plugin, 'test-adapter')
|
|
696
|
-
|
|
697
|
-
// 不应抛错
|
|
698
|
-
expect(() => {
|
|
699
|
-
adapter.declareSkill({ description: '测试' })
|
|
700
|
-
}).not.toThrow()
|
|
701
|
-
})
|
|
702
|
-
})
|
|
703
|
-
|
|
704
616
|
describe('Adapter Registry', () => {
|
|
705
617
|
it('should have a Registry Map', () => {
|
|
706
618
|
expect(Adapter.Registry).toBeInstanceOf(Map)
|