@zhin.js/core 1.0.24 → 1.0.26
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 +84 -342
- package/lib/adapter.d.ts +45 -1
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +182 -1
- package/lib/adapter.js.map +1 -1
- package/lib/ai/agent.d.ts +126 -0
- package/lib/ai/agent.d.ts.map +1 -0
- package/lib/ai/agent.js +645 -0
- package/lib/ai/agent.js.map +1 -0
- package/lib/ai/context-manager.d.ts +213 -0
- package/lib/ai/context-manager.d.ts.map +1 -0
- package/lib/ai/context-manager.js +313 -0
- package/lib/ai/context-manager.js.map +1 -0
- package/lib/ai/conversation-memory.d.ts +181 -0
- package/lib/ai/conversation-memory.d.ts.map +1 -0
- package/lib/ai/conversation-memory.js +581 -0
- package/lib/ai/conversation-memory.js.map +1 -0
- package/lib/ai/follow-up.d.ts +131 -0
- package/lib/ai/follow-up.d.ts.map +1 -0
- package/lib/ai/follow-up.js +265 -0
- package/lib/ai/follow-up.js.map +1 -0
- package/lib/ai/index.d.ts +29 -0
- package/lib/ai/index.d.ts.map +1 -0
- package/lib/ai/index.js +34 -0
- package/lib/ai/index.js.map +1 -0
- package/lib/ai/init.d.ts +30 -0
- package/lib/ai/init.d.ts.map +1 -0
- package/lib/ai/init.js +424 -0
- package/lib/ai/init.js.map +1 -0
- package/lib/ai/output.d.ts +93 -0
- package/lib/ai/output.d.ts.map +1 -0
- package/lib/ai/output.js +176 -0
- package/lib/ai/output.js.map +1 -0
- package/lib/ai/providers/anthropic.d.ts +23 -0
- package/lib/ai/providers/anthropic.d.ts.map +1 -0
- package/lib/ai/providers/anthropic.js +322 -0
- package/lib/ai/providers/anthropic.js.map +1 -0
- package/lib/ai/providers/base.d.ts +43 -0
- package/lib/ai/providers/base.d.ts.map +1 -0
- package/lib/ai/providers/base.js +135 -0
- package/lib/ai/providers/base.js.map +1 -0
- package/lib/ai/providers/index.d.ts +12 -0
- package/lib/ai/providers/index.d.ts.map +1 -0
- package/lib/ai/providers/index.js +9 -0
- package/lib/ai/providers/index.js.map +1 -0
- package/lib/ai/providers/ollama.d.ts +25 -0
- package/lib/ai/providers/ollama.d.ts.map +1 -0
- package/lib/ai/providers/ollama.js +243 -0
- package/lib/ai/providers/ollama.js.map +1 -0
- package/lib/ai/providers/openai.d.ts +46 -0
- package/lib/ai/providers/openai.d.ts.map +1 -0
- package/lib/ai/providers/openai.js +132 -0
- package/lib/ai/providers/openai.js.map +1 -0
- package/lib/ai/rate-limiter.d.ts +38 -0
- package/lib/ai/rate-limiter.d.ts.map +1 -0
- package/lib/ai/rate-limiter.js +86 -0
- package/lib/ai/rate-limiter.js.map +1 -0
- package/lib/ai/service.d.ts +81 -0
- package/lib/ai/service.d.ts.map +1 -0
- package/lib/ai/service.js +274 -0
- package/lib/ai/service.js.map +1 -0
- package/lib/ai/session.d.ts +186 -0
- package/lib/ai/session.d.ts.map +1 -0
- package/lib/ai/session.js +443 -0
- package/lib/ai/session.js.map +1 -0
- package/lib/ai/tone-detector.d.ts +19 -0
- package/lib/ai/tone-detector.d.ts.map +1 -0
- package/lib/ai/tone-detector.js +72 -0
- package/lib/ai/tone-detector.js.map +1 -0
- package/lib/ai/tools.d.ts +45 -0
- package/lib/ai/tools.d.ts.map +1 -0
- package/lib/ai/tools.js +206 -0
- package/lib/ai/tools.js.map +1 -0
- package/lib/ai/types.d.ts +264 -0
- package/lib/ai/types.d.ts.map +1 -0
- package/lib/ai/types.js +6 -0
- package/lib/ai/types.js.map +1 -0
- package/lib/ai/user-profile.d.ts +56 -0
- package/lib/ai/user-profile.d.ts.map +1 -0
- package/lib/ai/user-profile.js +130 -0
- package/lib/ai/user-profile.js.map +1 -0
- package/lib/ai/zhin-agent.d.ts +165 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -0
- package/lib/ai/zhin-agent.js +707 -0
- package/lib/ai/zhin-agent.js.map +1 -0
- package/lib/built/adapter-process.d.ts +4 -0
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +94 -0
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/ai-trigger.d.ts +89 -0
- package/lib/built/ai-trigger.d.ts.map +1 -0
- package/lib/built/ai-trigger.js +166 -0
- package/lib/built/ai-trigger.js.map +1 -0
- package/lib/built/command.d.ts +33 -17
- package/lib/built/command.d.ts.map +1 -1
- package/lib/built/command.js +71 -44
- package/lib/built/command.js.map +1 -1
- package/lib/built/component.d.ts +42 -15
- package/lib/built/component.d.ts.map +1 -1
- package/lib/built/component.js +84 -52
- package/lib/built/component.js.map +1 -1
- package/lib/built/config.d.ts +54 -5
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +76 -10
- package/lib/built/config.js.map +1 -1
- package/lib/built/cron.d.ts +41 -18
- package/lib/built/cron.d.ts.map +1 -1
- package/lib/built/cron.js +106 -63
- package/lib/built/cron.js.map +1 -1
- package/lib/built/database.d.ts +55 -6
- package/lib/built/database.d.ts.map +1 -1
- package/lib/built/database.js +93 -22
- package/lib/built/database.js.map +1 -1
- package/lib/built/dispatcher.d.ts +118 -0
- package/lib/built/dispatcher.d.ts.map +1 -0
- package/lib/built/dispatcher.js +196 -0
- package/lib/built/dispatcher.js.map +1 -0
- package/lib/built/permission.d.ts +45 -5
- package/lib/built/permission.d.ts.map +1 -1
- package/lib/built/permission.js +56 -11
- package/lib/built/permission.js.map +1 -1
- package/lib/built/skill.d.ts +117 -0
- package/lib/built/skill.d.ts.map +1 -0
- package/lib/built/skill.js +191 -0
- package/lib/built/skill.js.map +1 -0
- package/lib/built/tool.d.ts +188 -0
- package/lib/built/tool.d.ts.map +1 -0
- package/lib/built/tool.js +749 -0
- package/lib/built/tool.js.map +1 -0
- package/lib/feature.d.ts +75 -0
- package/lib/feature.d.ts.map +1 -0
- package/lib/feature.js +69 -0
- package/lib/feature.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +53 -18
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +301 -31
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +248 -9
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +38 -12
- package/lib/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +206 -2
- package/src/ai/agent.ts +772 -0
- package/src/ai/context-manager.ts +440 -0
- package/src/ai/conversation-memory.ts +774 -0
- package/src/ai/follow-up.ts +357 -0
- package/src/ai/index.ts +128 -0
- package/src/ai/init.ts +502 -0
- package/src/ai/output.ts +261 -0
- package/src/ai/providers/anthropic.ts +375 -0
- package/src/ai/providers/base.ts +173 -0
- package/src/ai/providers/index.ts +13 -0
- package/src/ai/providers/ollama.ts +292 -0
- package/src/ai/providers/openai.ts +167 -0
- package/src/ai/rate-limiter.ts +129 -0
- package/src/ai/service.ts +319 -0
- package/src/ai/session.ts +544 -0
- package/src/ai/tone-detector.ts +89 -0
- package/src/ai/tools.ts +218 -0
- package/src/ai/types.ts +296 -0
- package/src/ai/user-profile.ts +181 -0
- package/src/ai/zhin-agent.ts +845 -0
- package/src/built/adapter-process.ts +99 -0
- package/src/built/ai-trigger.ts +259 -0
- package/src/built/command.ts +75 -69
- package/src/built/component.ts +94 -76
- package/src/built/config.ts +238 -128
- package/src/built/cron.ts +117 -101
- package/src/built/database.ts +128 -33
- package/src/built/dispatcher.ts +332 -0
- package/src/built/permission.ts +146 -54
- package/src/built/skill.ts +280 -0
- package/src/built/tool.ts +928 -0
- package/src/feature.ts +113 -0
- package/src/index.ts +11 -0
- package/src/plugin.ts +359 -69
- package/src/types.ts +306 -11
- package/src/utils.ts +37 -13
- package/tests/adapter.test.ts +153 -1
- package/tests/ai/agent.test.ts +614 -0
- package/tests/ai/ai-trigger.test.ts +368 -0
- package/tests/ai/context-manager.test.ts +413 -0
- package/tests/ai/conversation-memory.test.ts +128 -0
- package/tests/ai/follow-up.test.ts +175 -0
- package/tests/ai/integration.test.ts +584 -0
- package/tests/ai/output.test.ts +128 -0
- package/tests/ai/providers.integration.test.ts +227 -0
- package/tests/ai/rate-limiter.test.ts +108 -0
- package/tests/ai/session.test.ts +375 -0
- package/tests/ai/setup.ts +308 -0
- package/tests/ai/tone-detector.test.ts +80 -0
- package/tests/ai/tool.test.ts +800 -0
- package/tests/ai/tools-builtin.test.ts +346 -0
- package/tests/ai/user-profile.test.ts +73 -0
- package/tests/ai/zhin-agent.test.ts +177 -0
- package/tests/component-new.test.ts +17 -6
- package/tests/config.test.ts +46 -0
- package/tests/cron.test.ts +94 -5
- package/tests/dispatcher.test.ts +146 -0
- package/tests/feature.test.ts +145 -0
- package/tests/features-builtin.test.ts +191 -0
- package/tests/plugin.test.ts +88 -14
- package/tests/skill-feature.test.ts +179 -0
- package/tests/tool-feature.test.ts +254 -0
package/tests/plugin.test.ts
CHANGED
|
@@ -232,13 +232,12 @@ describe('Plugin AsyncLocalStorage', () => {
|
|
|
232
232
|
})
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
-
it('should
|
|
235
|
+
it('should return same instance when called twice from same file', () => {
|
|
236
236
|
storage.run(undefined, () => {
|
|
237
|
-
const
|
|
238
|
-
const
|
|
237
|
+
const first = usePlugin()
|
|
238
|
+
const second = usePlugin()
|
|
239
239
|
|
|
240
|
-
expect(
|
|
241
|
-
expect(parent.children).toContain(child)
|
|
240
|
+
expect(second).toBe(first)
|
|
242
241
|
})
|
|
243
242
|
})
|
|
244
243
|
|
|
@@ -561,6 +560,82 @@ describe('Plugin Event Broadcasting', () => {
|
|
|
561
560
|
})
|
|
562
561
|
})
|
|
563
562
|
|
|
563
|
+
describe('Plugin Context mounted Behavior', () => {
|
|
564
|
+
it('should always call mounted callback even when value is preset', async () => {
|
|
565
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
566
|
+
let mountedCalled = false
|
|
567
|
+
|
|
568
|
+
plugin.provide({
|
|
569
|
+
name: 'test-ctx' as any,
|
|
570
|
+
description: 'Test context with both value and mounted',
|
|
571
|
+
value: { original: true },
|
|
572
|
+
mounted(_p: any) {
|
|
573
|
+
mountedCalled = true
|
|
574
|
+
return { fromMounted: true }
|
|
575
|
+
},
|
|
576
|
+
} as any)
|
|
577
|
+
|
|
578
|
+
await plugin.start()
|
|
579
|
+
expect(mountedCalled).toBe(true)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('should NOT overwrite preset value with mounted return', async () => {
|
|
583
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
584
|
+
const presetValue = { original: true }
|
|
585
|
+
|
|
586
|
+
plugin.provide({
|
|
587
|
+
name: 'test-ctx' as any,
|
|
588
|
+
description: 'Test context with both value and mounted',
|
|
589
|
+
value: presetValue,
|
|
590
|
+
mounted(_p: any) {
|
|
591
|
+
return { fromMounted: true }
|
|
592
|
+
},
|
|
593
|
+
} as any)
|
|
594
|
+
|
|
595
|
+
await plugin.start()
|
|
596
|
+
const injected = plugin.inject('test-ctx' as any)
|
|
597
|
+
expect(injected).toBe(presetValue)
|
|
598
|
+
expect(injected).toEqual({ original: true })
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('should assign mounted return value when context.value is not set', async () => {
|
|
602
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
603
|
+
const mountedValue = { fromMounted: true }
|
|
604
|
+
|
|
605
|
+
plugin.provide({
|
|
606
|
+
name: 'test-ctx' as any,
|
|
607
|
+
description: 'Test context with mounted only',
|
|
608
|
+
mounted(_p: any) {
|
|
609
|
+
return mountedValue
|
|
610
|
+
},
|
|
611
|
+
} as any)
|
|
612
|
+
|
|
613
|
+
await plugin.start()
|
|
614
|
+
const injected = plugin.inject('test-ctx' as any)
|
|
615
|
+
expect(injected).toBe(mountedValue)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('mounted side effects should run even when value is preset', async () => {
|
|
619
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
620
|
+
let sideEffectCounter = 0
|
|
621
|
+
|
|
622
|
+
plugin.provide({
|
|
623
|
+
name: 'test-ctx' as any,
|
|
624
|
+
description: 'Test mounted side effects',
|
|
625
|
+
value: { data: 'preset' },
|
|
626
|
+
mounted(_p: any) {
|
|
627
|
+
sideEffectCounter++
|
|
628
|
+
return { data: 'mounted' }
|
|
629
|
+
},
|
|
630
|
+
} as any)
|
|
631
|
+
|
|
632
|
+
await plugin.start()
|
|
633
|
+
expect(sideEffectCounter).toBe(1)
|
|
634
|
+
// value should remain preset
|
|
635
|
+
expect(plugin.inject('test-ctx' as any)).toEqual({ data: 'preset' })
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
|
|
564
639
|
describe('Plugin Context Management', () => {
|
|
565
640
|
describe('provide', () => {
|
|
566
641
|
it('should register context', () => {
|
|
@@ -631,20 +706,19 @@ describe('Plugin Context Management', () => {
|
|
|
631
706
|
describe('Plugin Features', () => {
|
|
632
707
|
it('should return empty features by default', () => {
|
|
633
708
|
const plugin = new Plugin('/test/plugin.ts')
|
|
634
|
-
const features = plugin.
|
|
709
|
+
const features = plugin.getFeatures()
|
|
635
710
|
|
|
636
|
-
expect(features
|
|
637
|
-
expect(features.components).toEqual([])
|
|
638
|
-
expect(features.crons).toEqual([])
|
|
639
|
-
expect(Array.isArray(features.middlewares)).toBe(true)
|
|
711
|
+
expect(features).toEqual([])
|
|
640
712
|
})
|
|
641
713
|
|
|
642
|
-
it('should include middleware
|
|
714
|
+
it('should include middleware in getFeatures', () => {
|
|
643
715
|
const plugin = new Plugin('/test/plugin.ts')
|
|
644
|
-
plugin.addMiddleware(async (msg: any, next: any) => await next()
|
|
716
|
+
plugin.addMiddleware(async (msg: any, next: any) => await next())
|
|
645
717
|
|
|
646
|
-
const features = plugin.
|
|
647
|
-
|
|
718
|
+
const features = plugin.getFeatures()
|
|
719
|
+
const middlewareFeature = features.find(f => f.name === 'middleware')
|
|
720
|
+
expect(middlewareFeature).toBeDefined()
|
|
721
|
+
expect(middlewareFeature!.count).toBeGreaterThan(0)
|
|
648
722
|
})
|
|
649
723
|
})
|
|
650
724
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillFeature 测试
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import { SkillFeature, Skill } from '../src/built/skill.js';
|
|
6
|
+
import type { Tool } from '../src/types.js';
|
|
7
|
+
|
|
8
|
+
function makeTool(name: string, desc: string = ''): Tool {
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
description: desc,
|
|
12
|
+
parameters: { type: 'object', properties: {} },
|
|
13
|
+
execute: async () => '',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeSkill(overrides: Partial<Skill> & { name: string }): Skill {
|
|
18
|
+
return {
|
|
19
|
+
description: '默认描述',
|
|
20
|
+
tools: [],
|
|
21
|
+
pluginName: overrides.name,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('SkillFeature', () => {
|
|
27
|
+
let feature: SkillFeature;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
feature = new SkillFeature();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('add / remove', () => {
|
|
34
|
+
it('应该能添加 Skill', () => {
|
|
35
|
+
const skill = makeSkill({ name: 'weather', description: '天气查询' });
|
|
36
|
+
const dispose = feature.add(skill, 'plugin-weather');
|
|
37
|
+
|
|
38
|
+
expect(feature.items).toHaveLength(1);
|
|
39
|
+
expect(feature.byName.get('weather')).toBe(skill);
|
|
40
|
+
expect(feature.size).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('dispose 应移除 Skill', () => {
|
|
44
|
+
const skill = makeSkill({ name: 'weather' });
|
|
45
|
+
const dispose = feature.add(skill, 'plugin-weather');
|
|
46
|
+
|
|
47
|
+
dispose();
|
|
48
|
+
expect(feature.items).toHaveLength(0);
|
|
49
|
+
expect(feature.byName.has('weather')).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('remove 应从 byName 和 items 中移除', () => {
|
|
53
|
+
const skill = makeSkill({ name: 'weather' });
|
|
54
|
+
feature.add(skill, 'plugin-weather');
|
|
55
|
+
|
|
56
|
+
feature.remove(skill);
|
|
57
|
+
expect(feature.items).toHaveLength(0);
|
|
58
|
+
expect(feature.byName.has('weather')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('get / getAll', () => {
|
|
63
|
+
it('按名称获取', () => {
|
|
64
|
+
const skill = makeSkill({ name: 'news' });
|
|
65
|
+
feature.add(skill, 'plugin-news');
|
|
66
|
+
|
|
67
|
+
expect(feature.get('news')).toBe(skill);
|
|
68
|
+
expect(feature.get('nonexistent')).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('获取所有', () => {
|
|
72
|
+
feature.add(makeSkill({ name: 'a' }), 'pa');
|
|
73
|
+
feature.add(makeSkill({ name: 'b' }), 'pb');
|
|
74
|
+
|
|
75
|
+
const all = feature.getAll();
|
|
76
|
+
expect(all).toHaveLength(2);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('search', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
feature.add(makeSkill({
|
|
83
|
+
name: 'weather',
|
|
84
|
+
description: '查询天气预报',
|
|
85
|
+
keywords: ['天气', '气温', '温度'],
|
|
86
|
+
tags: ['weather', '生活'],
|
|
87
|
+
tools: [makeTool('get_weather', '获取天气')],
|
|
88
|
+
}), 'plugin-weather');
|
|
89
|
+
|
|
90
|
+
feature.add(makeSkill({
|
|
91
|
+
name: 'news',
|
|
92
|
+
description: '获取新闻资讯',
|
|
93
|
+
keywords: ['新闻', '头条', '资讯'],
|
|
94
|
+
tags: ['news', '信息'],
|
|
95
|
+
tools: [makeTool('get_news', '获取新闻')],
|
|
96
|
+
}), 'plugin-news');
|
|
97
|
+
|
|
98
|
+
feature.add(makeSkill({
|
|
99
|
+
name: 'music',
|
|
100
|
+
description: '音乐搜索与播放',
|
|
101
|
+
keywords: ['音乐', '歌曲', '播放'],
|
|
102
|
+
tags: ['music', '娱乐'],
|
|
103
|
+
tools: [makeTool('search_music')],
|
|
104
|
+
}), 'plugin-music');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('关键词匹配应返回相关 Skill', () => {
|
|
108
|
+
const results = feature.search('今天天气怎么样');
|
|
109
|
+
expect(results.length).toBeGreaterThan(0);
|
|
110
|
+
expect(results[0].name).toBe('weather');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('标签匹配应返回相关 Skill', () => {
|
|
114
|
+
const results = feature.search('news');
|
|
115
|
+
expect(results.some(s => s.name === 'news')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('无匹配时应返回空数组', () => {
|
|
119
|
+
const results = feature.search('完全无关的内容 xyz');
|
|
120
|
+
expect(results).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('maxResults 应限制返回数量', () => {
|
|
124
|
+
// 搜索一个泛化词,可能匹配多个
|
|
125
|
+
const results = feature.search('获取', { maxResults: 1 });
|
|
126
|
+
expect(results.length).toBeLessThanOrEqual(1);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('collectAllTools', () => {
|
|
131
|
+
it('应收集所有 Skill 的工具', () => {
|
|
132
|
+
feature.add(makeSkill({
|
|
133
|
+
name: 'a',
|
|
134
|
+
tools: [makeTool('t1'), makeTool('t2')],
|
|
135
|
+
}), 'pa');
|
|
136
|
+
feature.add(makeSkill({
|
|
137
|
+
name: 'b',
|
|
138
|
+
tools: [makeTool('t3')],
|
|
139
|
+
}), 'pb');
|
|
140
|
+
|
|
141
|
+
const tools = feature.collectAllTools();
|
|
142
|
+
expect(tools).toHaveLength(3);
|
|
143
|
+
expect(tools.map(t => t.name)).toEqual(['t1', 't2', 't3']);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('toJSON', () => {
|
|
148
|
+
it('应序列化所有 Skill', () => {
|
|
149
|
+
feature.add(makeSkill({
|
|
150
|
+
name: 'test',
|
|
151
|
+
description: '测试技能',
|
|
152
|
+
keywords: ['test'],
|
|
153
|
+
tags: ['t'],
|
|
154
|
+
tools: [makeTool('t1'), makeTool('t2')],
|
|
155
|
+
}), 'plugin-test');
|
|
156
|
+
|
|
157
|
+
const json = feature.toJSON();
|
|
158
|
+
expect(json.name).toBe('skill');
|
|
159
|
+
expect(json.icon).toBe('Brain');
|
|
160
|
+
expect(json.count).toBe(1);
|
|
161
|
+
expect(json.items[0]).toMatchObject({
|
|
162
|
+
name: 'test',
|
|
163
|
+
desc: '测试技能',
|
|
164
|
+
toolCount: 2,
|
|
165
|
+
keywords: ['test'],
|
|
166
|
+
tags: ['t'],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('按插件名过滤', () => {
|
|
171
|
+
feature.add(makeSkill({ name: 'a' }), 'pa');
|
|
172
|
+
feature.add(makeSkill({ name: 'b' }), 'pb');
|
|
173
|
+
|
|
174
|
+
const json = feature.toJSON('pa');
|
|
175
|
+
expect(json.count).toBe(1);
|
|
176
|
+
expect(json.items[0].name).toBe('a');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolFeature 补全测试
|
|
3
|
+
* 测试 toolToCommand / commandToTool / ToolFeature.add / filterByContext
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
generatePattern,
|
|
8
|
+
extractParamInfo,
|
|
9
|
+
toolToCommand,
|
|
10
|
+
commandToTool,
|
|
11
|
+
canAccessTool,
|
|
12
|
+
inferPermissionLevel,
|
|
13
|
+
hasPermissionLevel,
|
|
14
|
+
ZhinTool,
|
|
15
|
+
isZhinTool,
|
|
16
|
+
ToolFeature,
|
|
17
|
+
} from '../src/built/tool.js';
|
|
18
|
+
import type { Tool, ToolContext } from '../src/types.js';
|
|
19
|
+
|
|
20
|
+
describe('generatePattern', () => {
|
|
21
|
+
it('应生成无参数的模式', () => {
|
|
22
|
+
const tool: Tool = {
|
|
23
|
+
name: 'ping',
|
|
24
|
+
description: '测试',
|
|
25
|
+
parameters: { type: 'object', properties: {} },
|
|
26
|
+
execute: async () => 'pong',
|
|
27
|
+
};
|
|
28
|
+
expect(generatePattern(tool)).toBe('ping');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('应生成有必填参数的模式', () => {
|
|
32
|
+
const tool: Tool = {
|
|
33
|
+
name: 'weather',
|
|
34
|
+
description: '天气',
|
|
35
|
+
parameters: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: { city: { type: 'string', description: '城市' } },
|
|
38
|
+
required: ['city'],
|
|
39
|
+
},
|
|
40
|
+
execute: async () => '',
|
|
41
|
+
};
|
|
42
|
+
expect(generatePattern(tool)).toBe('weather <city:text>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('应生成有可选参数的模式', () => {
|
|
46
|
+
const tool: Tool = {
|
|
47
|
+
name: 'search',
|
|
48
|
+
description: '搜索',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { query: { type: 'string' }, limit: { type: 'number' } },
|
|
52
|
+
required: ['query'],
|
|
53
|
+
},
|
|
54
|
+
execute: async () => '',
|
|
55
|
+
};
|
|
56
|
+
const pattern = generatePattern(tool);
|
|
57
|
+
expect(pattern).toContain('<query:text>');
|
|
58
|
+
expect(pattern).toContain('[limit:number]');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('应使用自定义 command.pattern', () => {
|
|
62
|
+
const tool: Tool = {
|
|
63
|
+
name: 'custom',
|
|
64
|
+
description: '自定义',
|
|
65
|
+
parameters: { type: 'object', properties: {} },
|
|
66
|
+
execute: async () => '',
|
|
67
|
+
command: { pattern: 'my-custom <arg:text>', enabled: true },
|
|
68
|
+
};
|
|
69
|
+
expect(generatePattern(tool)).toBe('my-custom <arg:text>');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('extractParamInfo', () => {
|
|
74
|
+
it('应从空 properties 返回空数组', () => {
|
|
75
|
+
expect(extractParamInfo({ type: 'object', properties: {} })).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('应提取参数信息', () => {
|
|
79
|
+
const result = extractParamInfo({
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
city: { type: 'string', description: '城市名' },
|
|
83
|
+
days: { type: 'number', description: '天数' },
|
|
84
|
+
},
|
|
85
|
+
required: ['city'],
|
|
86
|
+
});
|
|
87
|
+
expect(result).toHaveLength(2);
|
|
88
|
+
expect(result[0]).toMatchObject({ name: 'city', type: 'string', required: true, description: '城市名' });
|
|
89
|
+
expect(result[1]).toMatchObject({ name: 'days', type: 'number', required: false, description: '天数' });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('canAccessTool', () => {
|
|
94
|
+
const baseTool: Tool = {
|
|
95
|
+
name: 'test',
|
|
96
|
+
description: '测试',
|
|
97
|
+
parameters: { type: 'object', properties: {} },
|
|
98
|
+
execute: async () => '',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const baseContext: ToolContext = {
|
|
102
|
+
platform: 'qq',
|
|
103
|
+
botId: 'bot1',
|
|
104
|
+
sceneId: 'scene1',
|
|
105
|
+
senderId: 'user1',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
it('无限制的工具应始终可访问', () => {
|
|
109
|
+
expect(canAccessTool(baseTool, baseContext)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('应检查平台限制', () => {
|
|
113
|
+
const tool = { ...baseTool, platforms: ['discord'] };
|
|
114
|
+
expect(canAccessTool(tool, { ...baseContext, platform: 'qq' })).toBe(false);
|
|
115
|
+
expect(canAccessTool(tool, { ...baseContext, platform: 'discord' })).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('应检查场景限制', () => {
|
|
119
|
+
const tool = { ...baseTool, scopes: ['group' as const] };
|
|
120
|
+
expect(canAccessTool(tool, { ...baseContext, scope: 'private' })).toBe(false);
|
|
121
|
+
expect(canAccessTool(tool, { ...baseContext, scope: 'group' })).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('应检查权限级别', () => {
|
|
125
|
+
const tool = { ...baseTool, permissionLevel: 'group_admin' as const };
|
|
126
|
+
expect(canAccessTool(tool, { ...baseContext })).toBe(false); // user level
|
|
127
|
+
expect(canAccessTool(tool, { ...baseContext, isGroupAdmin: true })).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('inferPermissionLevel', () => {
|
|
132
|
+
it('应使用 senderPermissionLevel 优先', () => {
|
|
133
|
+
expect(inferPermissionLevel({ senderPermissionLevel: 'owner' } as any)).toBe('owner');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('应按优先级推断', () => {
|
|
137
|
+
expect(inferPermissionLevel({ isOwner: true } as any)).toBe('owner');
|
|
138
|
+
expect(inferPermissionLevel({ isBotAdmin: true } as any)).toBe('bot_admin');
|
|
139
|
+
expect(inferPermissionLevel({ isGroupOwner: true } as any)).toBe('group_owner');
|
|
140
|
+
expect(inferPermissionLevel({ isGroupAdmin: true } as any)).toBe('group_admin');
|
|
141
|
+
expect(inferPermissionLevel({} as any)).toBe('user');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('hasPermissionLevel', () => {
|
|
146
|
+
it('相同级别应返回 true', () => {
|
|
147
|
+
expect(hasPermissionLevel('user', 'user')).toBe(true);
|
|
148
|
+
expect(hasPermissionLevel('owner', 'owner')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('高级别应能访问低级别', () => {
|
|
152
|
+
expect(hasPermissionLevel('owner', 'user')).toBe(true);
|
|
153
|
+
expect(hasPermissionLevel('bot_admin', 'group_admin')).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('低级别不能访问高级别', () => {
|
|
157
|
+
expect(hasPermissionLevel('user', 'group_admin')).toBe(false);
|
|
158
|
+
expect(hasPermissionLevel('group_admin', 'owner')).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('ToolFeature', () => {
|
|
163
|
+
let feature: ToolFeature;
|
|
164
|
+
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
feature = new ToolFeature();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('基础操作', () => {
|
|
170
|
+
it('初始状态应无工具', () => {
|
|
171
|
+
expect(feature.items).toHaveLength(0);
|
|
172
|
+
expect(feature.byName.size).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('toJSON 应返回正确结构', () => {
|
|
176
|
+
const json = feature.toJSON();
|
|
177
|
+
expect(json).toMatchObject({
|
|
178
|
+
name: 'tool',
|
|
179
|
+
icon: 'Wrench',
|
|
180
|
+
desc: '工具',
|
|
181
|
+
count: 0,
|
|
182
|
+
items: [],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('filterByContext', () => {
|
|
188
|
+
it('应过滤不符合权限的工具', () => {
|
|
189
|
+
const tools: Tool[] = [
|
|
190
|
+
{ name: 'public', description: '公开', parameters: { type: 'object', properties: {} }, execute: async () => '' },
|
|
191
|
+
{ name: 'admin', description: '管理', parameters: { type: 'object', properties: {} }, execute: async () => '', permissionLevel: 'bot_admin' },
|
|
192
|
+
];
|
|
193
|
+
const context: ToolContext = { platform: 'qq', botId: 'b', sceneId: 's', senderId: 'u' };
|
|
194
|
+
const filtered = feature.filterByContext(tools, context);
|
|
195
|
+
expect(filtered).toHaveLength(1);
|
|
196
|
+
expect(filtered[0].name).toBe('public');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('应过滤不符合平台的工具', () => {
|
|
200
|
+
const tools: Tool[] = [
|
|
201
|
+
{ name: 'all', description: '', parameters: { type: 'object', properties: {} }, execute: async () => '' },
|
|
202
|
+
{ name: 'discord-only', description: '', parameters: { type: 'object', properties: {} }, execute: async () => '', platforms: ['discord'] },
|
|
203
|
+
];
|
|
204
|
+
const context: ToolContext = { platform: 'qq', botId: 'b', sceneId: 's', senderId: 'u' };
|
|
205
|
+
const filtered = feature.filterByContext(tools, context);
|
|
206
|
+
expect(filtered).toHaveLength(1);
|
|
207
|
+
expect(filtered[0].name).toBe('all');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('ZhinTool', () => {
|
|
213
|
+
it('应构建完整的 Tool 对象', () => {
|
|
214
|
+
const zt = new ZhinTool('test')
|
|
215
|
+
.desc('测试工具')
|
|
216
|
+
.param('city', { type: 'string', description: '城市' }, true)
|
|
217
|
+
.execute(async ({ city }) => `${city}: 晴`);
|
|
218
|
+
|
|
219
|
+
const tool = zt.toTool();
|
|
220
|
+
expect(tool.name).toBe('test');
|
|
221
|
+
expect(tool.description).toBe('测试工具');
|
|
222
|
+
expect(tool.parameters.properties).toHaveProperty('city');
|
|
223
|
+
expect(tool.parameters.required).toEqual(['city']);
|
|
224
|
+
expect(typeof tool.execute).toBe('function');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('无 execute 应抛错', () => {
|
|
228
|
+
const zt = new ZhinTool('no-exec').desc('无执行');
|
|
229
|
+
expect(() => zt.toTool()).toThrow('has no execute()');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('isZhinTool 应正确判断', () => {
|
|
233
|
+
expect(isZhinTool(new ZhinTool('x'))).toBe(true);
|
|
234
|
+
expect(isZhinTool({})).toBe(false);
|
|
235
|
+
expect(isZhinTool(null)).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('toJSON 应返回序列化数据', () => {
|
|
239
|
+
const zt = new ZhinTool('test')
|
|
240
|
+
.desc('描述')
|
|
241
|
+
.param('a', { type: 'string' }, true)
|
|
242
|
+
.tag('t1')
|
|
243
|
+
.platform('qq')
|
|
244
|
+
.permission('bot_admin')
|
|
245
|
+
.execute(async () => '');
|
|
246
|
+
|
|
247
|
+
const json = zt.toJSON();
|
|
248
|
+
expect(json.name).toBe('test');
|
|
249
|
+
expect(json.description).toBe('描述');
|
|
250
|
+
expect(json.tags).toEqual(['t1']);
|
|
251
|
+
expect(json.platforms).toEqual(['qq']);
|
|
252
|
+
expect(json.permissionLevel).toBe('bot_admin');
|
|
253
|
+
});
|
|
254
|
+
});
|