@zhin.js/core 1.0.25 → 1.0.27
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 +19 -0
- package/README.md +84 -342
- package/lib/adapter.d.ts +17 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +84 -2
- 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/ai-trigger.d.ts.map +1 -1
- package/lib/built/ai-trigger.js +7 -3
- package/lib/built/ai-trigger.js.map +1 -1
- 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 +64 -5
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +129 -12
- 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 +71 -164
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +212 -297
- package/lib/built/tool.js.map +1 -1
- 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 +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +25 -17
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +180 -20
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +4 -9
- package/lib/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/adapter.ts +101 -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/ai-trigger.ts +6 -3
- package/src/built/command.ts +75 -69
- package/src/built/component.ts +94 -76
- package/src/built/config.ts +288 -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 +245 -366
- package/src/feature.ts +113 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +198 -33
- package/src/types.ts +6 -10
- 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/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/test/minimal-bot.ts +0 -31
- package/test/stress-test.ts +0 -123
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
|
+
});
|
package/test/minimal-bot.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { App } from '../src/app';
|
|
2
|
-
import { LogLevel } from '@zhin.js/logger';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
async function runMinimalBot() {
|
|
10
|
-
const app = new App({
|
|
11
|
-
log_level: LogLevel.INFO,
|
|
12
|
-
plugin_dirs: [path.join(__dirname, 'plugins')],
|
|
13
|
-
plugins: [],
|
|
14
|
-
bots: [],
|
|
15
|
-
debug: true
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
console.log('Starting Minimal Bot...');
|
|
19
|
-
await app.start();
|
|
20
|
-
console.log('Minimal Bot Started');
|
|
21
|
-
|
|
22
|
-
// Simulate a message
|
|
23
|
-
// await app.receiveMessage(...)
|
|
24
|
-
|
|
25
|
-
await app.stop();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (process.argv[1] === __filename) {
|
|
29
|
-
runMinimalBot().catch(console.error);
|
|
30
|
-
}
|
|
31
|
-
|