@zhin.js/core 1.0.25 → 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.
Files changed (200) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +17 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +84 -2
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/ai-trigger.d.ts.map +1 -1
  88. package/lib/built/ai-trigger.js +7 -3
  89. package/lib/built/ai-trigger.js.map +1 -1
  90. package/lib/built/command.d.ts +33 -17
  91. package/lib/built/command.d.ts.map +1 -1
  92. package/lib/built/command.js +71 -44
  93. package/lib/built/command.js.map +1 -1
  94. package/lib/built/component.d.ts +42 -15
  95. package/lib/built/component.d.ts.map +1 -1
  96. package/lib/built/component.js +84 -52
  97. package/lib/built/component.js.map +1 -1
  98. package/lib/built/config.d.ts +54 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +76 -10
  101. package/lib/built/config.js.map +1 -1
  102. package/lib/built/cron.d.ts +41 -18
  103. package/lib/built/cron.d.ts.map +1 -1
  104. package/lib/built/cron.js +106 -63
  105. package/lib/built/cron.js.map +1 -1
  106. package/lib/built/database.d.ts +55 -6
  107. package/lib/built/database.d.ts.map +1 -1
  108. package/lib/built/database.js +93 -22
  109. package/lib/built/database.js.map +1 -1
  110. package/lib/built/dispatcher.d.ts +118 -0
  111. package/lib/built/dispatcher.d.ts.map +1 -0
  112. package/lib/built/dispatcher.js +196 -0
  113. package/lib/built/dispatcher.js.map +1 -0
  114. package/lib/built/permission.d.ts +45 -5
  115. package/lib/built/permission.d.ts.map +1 -1
  116. package/lib/built/permission.js +56 -11
  117. package/lib/built/permission.js.map +1 -1
  118. package/lib/built/skill.d.ts +117 -0
  119. package/lib/built/skill.d.ts.map +1 -0
  120. package/lib/built/skill.js +191 -0
  121. package/lib/built/skill.js.map +1 -0
  122. package/lib/built/tool.d.ts +71 -164
  123. package/lib/built/tool.d.ts.map +1 -1
  124. package/lib/built/tool.js +212 -297
  125. package/lib/built/tool.js.map +1 -1
  126. package/lib/feature.d.ts +75 -0
  127. package/lib/feature.d.ts.map +1 -0
  128. package/lib/feature.js +69 -0
  129. package/lib/feature.js.map +1 -0
  130. package/lib/index.d.ts +4 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +7 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/plugin.d.ts +25 -17
  135. package/lib/plugin.d.ts.map +1 -1
  136. package/lib/plugin.js +180 -20
  137. package/lib/plugin.js.map +1 -1
  138. package/lib/types.d.ts +4 -9
  139. package/lib/types.d.ts.map +1 -1
  140. package/package.json +4 -4
  141. package/src/adapter.ts +101 -2
  142. package/src/ai/agent.ts +772 -0
  143. package/src/ai/context-manager.ts +440 -0
  144. package/src/ai/conversation-memory.ts +774 -0
  145. package/src/ai/follow-up.ts +357 -0
  146. package/src/ai/index.ts +128 -0
  147. package/src/ai/init.ts +502 -0
  148. package/src/ai/output.ts +261 -0
  149. package/src/ai/providers/anthropic.ts +375 -0
  150. package/src/ai/providers/base.ts +173 -0
  151. package/src/ai/providers/index.ts +13 -0
  152. package/src/ai/providers/ollama.ts +292 -0
  153. package/src/ai/providers/openai.ts +167 -0
  154. package/src/ai/rate-limiter.ts +129 -0
  155. package/src/ai/service.ts +319 -0
  156. package/src/ai/session.ts +544 -0
  157. package/src/ai/tone-detector.ts +89 -0
  158. package/src/ai/tools.ts +218 -0
  159. package/src/ai/types.ts +296 -0
  160. package/src/ai/user-profile.ts +181 -0
  161. package/src/ai/zhin-agent.ts +845 -0
  162. package/src/built/ai-trigger.ts +6 -3
  163. package/src/built/command.ts +75 -69
  164. package/src/built/component.ts +94 -76
  165. package/src/built/config.ts +238 -128
  166. package/src/built/cron.ts +117 -101
  167. package/src/built/database.ts +128 -33
  168. package/src/built/dispatcher.ts +332 -0
  169. package/src/built/permission.ts +146 -54
  170. package/src/built/skill.ts +280 -0
  171. package/src/built/tool.ts +245 -366
  172. package/src/feature.ts +113 -0
  173. package/src/index.ts +7 -0
  174. package/src/plugin.ts +198 -33
  175. package/src/types.ts +6 -10
  176. package/tests/adapter.test.ts +153 -1
  177. package/tests/ai/agent.test.ts +614 -0
  178. package/tests/ai/ai-trigger.test.ts +368 -0
  179. package/tests/ai/context-manager.test.ts +413 -0
  180. package/tests/ai/conversation-memory.test.ts +128 -0
  181. package/tests/ai/follow-up.test.ts +175 -0
  182. package/tests/ai/integration.test.ts +584 -0
  183. package/tests/ai/output.test.ts +128 -0
  184. package/tests/ai/providers.integration.test.ts +227 -0
  185. package/tests/ai/rate-limiter.test.ts +108 -0
  186. package/tests/ai/session.test.ts +375 -0
  187. package/tests/ai/setup.ts +308 -0
  188. package/tests/ai/tone-detector.test.ts +80 -0
  189. package/tests/ai/tool.test.ts +800 -0
  190. package/tests/ai/tools-builtin.test.ts +346 -0
  191. package/tests/ai/user-profile.test.ts +73 -0
  192. package/tests/ai/zhin-agent.test.ts +177 -0
  193. package/tests/config.test.ts +46 -0
  194. package/tests/cron.test.ts +94 -5
  195. package/tests/dispatcher.test.ts +146 -0
  196. package/tests/feature.test.ts +145 -0
  197. package/tests/features-builtin.test.ts +191 -0
  198. package/tests/plugin.test.ts +88 -14
  199. package/tests/skill-feature.test.ts +179 -0
  200. package/tests/tool-feature.test.ts +254 -0
@@ -232,13 +232,12 @@ describe('Plugin AsyncLocalStorage', () => {
232
232
  })
233
233
  })
234
234
 
235
- it('should create child plugin when called within parent context', () => {
235
+ it('should return same instance when called twice from same file', () => {
236
236
  storage.run(undefined, () => {
237
- const parent = usePlugin()
238
- const child = usePlugin()
237
+ const first = usePlugin()
238
+ const second = usePlugin()
239
239
 
240
- expect(child.parent).toBe(parent)
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.features
709
+ const features = plugin.getFeatures()
635
710
 
636
- expect(features.commands).toEqual([])
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 names', () => {
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(), 'test-middleware')
716
+ plugin.addMiddleware(async (msg: any, next: any) => await next())
645
717
 
646
- const features = plugin.features
647
- expect(features.middlewares.length).toBeGreaterThan(0)
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
+ });