@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.
Files changed (211) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +45 -1
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +182 -1
  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/adapter-process.d.ts +4 -0
  88. package/lib/built/adapter-process.d.ts.map +1 -1
  89. package/lib/built/adapter-process.js +94 -0
  90. package/lib/built/adapter-process.js.map +1 -1
  91. package/lib/built/ai-trigger.d.ts +89 -0
  92. package/lib/built/ai-trigger.d.ts.map +1 -0
  93. package/lib/built/ai-trigger.js +166 -0
  94. package/lib/built/ai-trigger.js.map +1 -0
  95. package/lib/built/command.d.ts +33 -17
  96. package/lib/built/command.d.ts.map +1 -1
  97. package/lib/built/command.js +71 -44
  98. package/lib/built/command.js.map +1 -1
  99. package/lib/built/component.d.ts +42 -15
  100. package/lib/built/component.d.ts.map +1 -1
  101. package/lib/built/component.js +84 -52
  102. package/lib/built/component.js.map +1 -1
  103. package/lib/built/config.d.ts +54 -5
  104. package/lib/built/config.d.ts.map +1 -1
  105. package/lib/built/config.js +76 -10
  106. package/lib/built/config.js.map +1 -1
  107. package/lib/built/cron.d.ts +41 -18
  108. package/lib/built/cron.d.ts.map +1 -1
  109. package/lib/built/cron.js +106 -63
  110. package/lib/built/cron.js.map +1 -1
  111. package/lib/built/database.d.ts +55 -6
  112. package/lib/built/database.d.ts.map +1 -1
  113. package/lib/built/database.js +93 -22
  114. package/lib/built/database.js.map +1 -1
  115. package/lib/built/dispatcher.d.ts +118 -0
  116. package/lib/built/dispatcher.d.ts.map +1 -0
  117. package/lib/built/dispatcher.js +196 -0
  118. package/lib/built/dispatcher.js.map +1 -0
  119. package/lib/built/permission.d.ts +45 -5
  120. package/lib/built/permission.d.ts.map +1 -1
  121. package/lib/built/permission.js +56 -11
  122. package/lib/built/permission.js.map +1 -1
  123. package/lib/built/skill.d.ts +117 -0
  124. package/lib/built/skill.d.ts.map +1 -0
  125. package/lib/built/skill.js +191 -0
  126. package/lib/built/skill.js.map +1 -0
  127. package/lib/built/tool.d.ts +188 -0
  128. package/lib/built/tool.d.ts.map +1 -0
  129. package/lib/built/tool.js +749 -0
  130. package/lib/built/tool.js.map +1 -0
  131. package/lib/feature.d.ts +75 -0
  132. package/lib/feature.d.ts.map +1 -0
  133. package/lib/feature.js +69 -0
  134. package/lib/feature.js.map +1 -0
  135. package/lib/index.d.ts +6 -0
  136. package/lib/index.d.ts.map +1 -1
  137. package/lib/index.js +11 -0
  138. package/lib/index.js.map +1 -1
  139. package/lib/plugin.d.ts +53 -18
  140. package/lib/plugin.d.ts.map +1 -1
  141. package/lib/plugin.js +301 -31
  142. package/lib/plugin.js.map +1 -1
  143. package/lib/types.d.ts +248 -9
  144. package/lib/types.d.ts.map +1 -1
  145. package/lib/utils.d.ts.map +1 -1
  146. package/lib/utils.js +38 -12
  147. package/lib/utils.js.map +1 -1
  148. package/package.json +4 -4
  149. package/src/adapter.ts +206 -2
  150. package/src/ai/agent.ts +772 -0
  151. package/src/ai/context-manager.ts +440 -0
  152. package/src/ai/conversation-memory.ts +774 -0
  153. package/src/ai/follow-up.ts +357 -0
  154. package/src/ai/index.ts +128 -0
  155. package/src/ai/init.ts +502 -0
  156. package/src/ai/output.ts +261 -0
  157. package/src/ai/providers/anthropic.ts +375 -0
  158. package/src/ai/providers/base.ts +173 -0
  159. package/src/ai/providers/index.ts +13 -0
  160. package/src/ai/providers/ollama.ts +292 -0
  161. package/src/ai/providers/openai.ts +167 -0
  162. package/src/ai/rate-limiter.ts +129 -0
  163. package/src/ai/service.ts +319 -0
  164. package/src/ai/session.ts +544 -0
  165. package/src/ai/tone-detector.ts +89 -0
  166. package/src/ai/tools.ts +218 -0
  167. package/src/ai/types.ts +296 -0
  168. package/src/ai/user-profile.ts +181 -0
  169. package/src/ai/zhin-agent.ts +845 -0
  170. package/src/built/adapter-process.ts +99 -0
  171. package/src/built/ai-trigger.ts +259 -0
  172. package/src/built/command.ts +75 -69
  173. package/src/built/component.ts +94 -76
  174. package/src/built/config.ts +238 -128
  175. package/src/built/cron.ts +117 -101
  176. package/src/built/database.ts +128 -33
  177. package/src/built/dispatcher.ts +332 -0
  178. package/src/built/permission.ts +146 -54
  179. package/src/built/skill.ts +280 -0
  180. package/src/built/tool.ts +928 -0
  181. package/src/feature.ts +113 -0
  182. package/src/index.ts +11 -0
  183. package/src/plugin.ts +359 -69
  184. package/src/types.ts +306 -11
  185. package/src/utils.ts +37 -13
  186. package/tests/adapter.test.ts +153 -1
  187. package/tests/ai/agent.test.ts +614 -0
  188. package/tests/ai/ai-trigger.test.ts +368 -0
  189. package/tests/ai/context-manager.test.ts +413 -0
  190. package/tests/ai/conversation-memory.test.ts +128 -0
  191. package/tests/ai/follow-up.test.ts +175 -0
  192. package/tests/ai/integration.test.ts +584 -0
  193. package/tests/ai/output.test.ts +128 -0
  194. package/tests/ai/providers.integration.test.ts +227 -0
  195. package/tests/ai/rate-limiter.test.ts +108 -0
  196. package/tests/ai/session.test.ts +375 -0
  197. package/tests/ai/setup.ts +308 -0
  198. package/tests/ai/tone-detector.test.ts +80 -0
  199. package/tests/ai/tool.test.ts +800 -0
  200. package/tests/ai/tools-builtin.test.ts +346 -0
  201. package/tests/ai/user-profile.test.ts +73 -0
  202. package/tests/ai/zhin-agent.test.ts +177 -0
  203. package/tests/component-new.test.ts +17 -6
  204. package/tests/config.test.ts +46 -0
  205. package/tests/cron.test.ts +94 -5
  206. package/tests/dispatcher.test.ts +146 -0
  207. package/tests/feature.test.ts +145 -0
  208. package/tests/features-builtin.test.ts +191 -0
  209. package/tests/plugin.test.ts +88 -14
  210. package/tests/skill-feature.test.ts +179 -0
  211. package/tests/tool-feature.test.ts +254 -0
@@ -0,0 +1,928 @@
1
+ /**
2
+ * ToolFeature — 统一的工具管理服务
3
+ * 支持 Tool ↔ Command 互转
4
+ */
5
+ import { Feature, FeatureJSON } from "../feature.js";
6
+ import { MessageCommand } from "../command.js";
7
+ import { Message } from "../message.js";
8
+ import { Plugin, getPlugin } from "../plugin.js";
9
+ import type { Tool, ToolDefinition, RegisteredAdapter, AdapterMessage, ToolContext, ToolJsonSchema, ToolParametersSchema, PropertySchema, MaybePromise, ToolPermissionLevel, ToolScope } from "../types.js";
10
+ import { MatchResult } from "segment-matcher";
11
+
12
+ // ============================================================================
13
+ // 权限级别比较
14
+ // ============================================================================
15
+
16
+ /**
17
+ * 权限级别优先级(数字越大权限越高)
18
+ */
19
+ const PERMISSION_LEVEL_PRIORITY: Record<ToolPermissionLevel, number> = {
20
+ 'user': 0,
21
+ 'group_admin': 1,
22
+ 'group_owner': 2,
23
+ 'bot_admin': 3,
24
+ 'owner': 4,
25
+ };
26
+
27
+ /**
28
+ * 比较两个权限级别
29
+ * @returns 如果 a >= b 返回 true
30
+ */
31
+ function hasPermissionLevel(userLevel: ToolPermissionLevel, requiredLevel: ToolPermissionLevel): boolean {
32
+ return PERMISSION_LEVEL_PRIORITY[userLevel] >= PERMISSION_LEVEL_PRIORITY[requiredLevel];
33
+ }
34
+
35
+ /**
36
+ * 从 ToolContext 推断用户的权限级别
37
+ */
38
+ function inferPermissionLevel(context: ToolContext): ToolPermissionLevel {
39
+ if (context.senderPermissionLevel) {
40
+ return context.senderPermissionLevel;
41
+ }
42
+
43
+ // 按优先级检查
44
+ if (context.isOwner) return 'owner';
45
+ if (context.isBotAdmin) return 'bot_admin';
46
+ if (context.isGroupOwner) return 'group_owner';
47
+ if (context.isGroupAdmin) return 'group_admin';
48
+
49
+ return 'user';
50
+ }
51
+
52
+ /**
53
+ * 检查工具是否可被当前上下文访问
54
+ */
55
+ function canAccessTool(tool: Tool, context: ToolContext): boolean {
56
+ // 1. 检查平台限制
57
+ if (tool.platforms && tool.platforms.length > 0) {
58
+ if (!context.platform || !tool.platforms.includes(context.platform)) {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ // 2. 检查场景限制
64
+ if (tool.scopes && tool.scopes.length > 0) {
65
+ if (!context.scope || !tool.scopes.includes(context.scope)) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // 3. 检查权限级别
71
+ const requiredLevel = tool.permissionLevel || 'user';
72
+ const userLevel = inferPermissionLevel(context);
73
+
74
+ if (!hasPermissionLevel(userLevel, requiredLevel)) {
75
+ return false;
76
+ }
77
+
78
+ return true;
79
+ }
80
+
81
+ // ============================================================================
82
+ // Tool 工具函数
83
+ // ============================================================================
84
+
85
+ /**
86
+ * 从 Tool 参数生成命令模式
87
+ * @example
88
+ * parameters: { properties: { city: { type: 'string' } }, required: ['city'] }
89
+ * => 'toolName <city>'
90
+ */
91
+ export function generatePattern(tool: Tool): string {
92
+ const { name, parameters } = tool;
93
+
94
+ if (tool.command && tool.command.pattern) {
95
+ return tool.command.pattern;
96
+ }
97
+
98
+ if (!parameters.properties) {
99
+ return name;
100
+ }
101
+
102
+ const parts: string[] = [name];
103
+ const props = parameters.properties;
104
+ const required = parameters.required || [];
105
+
106
+ // 按照 required 优先、字母顺序排序
107
+ const sortedKeys = Object.keys(props).sort((a, b) => {
108
+ const aReq = required.includes(a) ? 0 : 1;
109
+ const bReq = required.includes(b) ? 0 : 1;
110
+ if (aReq !== bReq) return aReq - bReq;
111
+ return a.localeCompare(b);
112
+ });
113
+
114
+ for (const key of sortedKeys) {
115
+ const prop = props[key];
116
+ const isRequired = required.includes(key);
117
+ const paramType = prop.paramType || (prop.type === 'number' ? 'number' : 'text');
118
+
119
+ if (isRequired) {
120
+ parts.push(`<${key}:${paramType}>`);
121
+ } else {
122
+ parts.push(`[${key}:${paramType}]`);
123
+ }
124
+ }
125
+
126
+ return parts.join(' ');
127
+ }
128
+
129
+ /**
130
+ * 从参数定义中提取参数信息
131
+ */
132
+ export function extractParamInfo(parameters: ToolJsonSchema): Tool.ParamInfo[] {
133
+ if (!parameters.properties) return [];
134
+
135
+ const required = parameters.required || [];
136
+ return Object.entries(parameters.properties).map(([name, schema]) => ({
137
+ name,
138
+ type: schema.type,
139
+ required: required.includes(name),
140
+ description: schema.description,
141
+ default: schema.default,
142
+ enum: schema.enum,
143
+ }));
144
+ }
145
+
146
+ /**
147
+ * 定义工具的辅助函数(提供类型推断)
148
+ */
149
+ export function defineTool<TArgs extends Record<string, any> = Record<string, any>>(
150
+ tool: ToolDefinition<TArgs>
151
+ ): Tool {
152
+ return tool as Tool;
153
+ }
154
+
155
+ // ============================================================================
156
+ // ZhinTool 类(链式调用风格)
157
+ // ============================================================================
158
+
159
+ /**
160
+ * 参数定义(带顺序)
161
+ */
162
+ interface ParamDef {
163
+ name: string;
164
+ schema: PropertySchema;
165
+ required: boolean;
166
+ }
167
+
168
+ /**
169
+ * ZhinTool 类
170
+ * 提供类似 MessageCommand 的链式调用风格来定义工具
171
+ */
172
+ export class ZhinTool {
173
+ #name: string;
174
+ #description: string = '';
175
+ /** 有序的参数列表 */
176
+ #params: ParamDef[] = [];
177
+ #execute?: (args: Record<string, any>, context?: ToolContext) => MaybePromise<any>;
178
+ #platforms: string[] = [];
179
+ #scopes: ToolScope[] = [];
180
+ #permissionLevel: ToolPermissionLevel = 'user';
181
+ #permissions: string[] = [];
182
+ #tags: string[] = [];
183
+ #keywords: string[] = [];
184
+ /** 命令回调(入参是 message, matchResult) */
185
+ #commandCallback?: MessageCommand.Callback<RegisteredAdapter>;
186
+ /** 命令配置 */
187
+ #commandConfig: Omit<Tool.CommandConfig, 'enabled'> = {};
188
+ #hidden: boolean = false;
189
+ #source?: string;
190
+
191
+ constructor(name: string) {
192
+ this.#name = name;
193
+ }
194
+
195
+ get name(): string {
196
+ return this.#name;
197
+ }
198
+
199
+ get description(): string {
200
+ return this.#description;
201
+ }
202
+
203
+ get params(): ParamDef[] {
204
+ return [...this.#params];
205
+ }
206
+
207
+ desc(description: string): this {
208
+ this.#description = description;
209
+ return this;
210
+ }
211
+
212
+ param(name: string, schema: PropertySchema, required: boolean = false): this {
213
+ const existingIndex = this.#params.findIndex(p => p.name === name);
214
+ if (existingIndex >= 0) {
215
+ this.#params[existingIndex] = { name, schema, required };
216
+ } else {
217
+ this.#params.push({ name, schema, required });
218
+ }
219
+ return this;
220
+ }
221
+
222
+ platform(...platforms: string[]): this {
223
+ this.#platforms.push(...platforms);
224
+ return this;
225
+ }
226
+
227
+ scope(...scopes: ToolScope[]): this {
228
+ this.#scopes.push(...scopes);
229
+ return this;
230
+ }
231
+
232
+ permission(level: ToolPermissionLevel): this {
233
+ this.#permissionLevel = level;
234
+ return this;
235
+ }
236
+
237
+ permit(...permissions: string[]): this {
238
+ this.#permissions.push(...permissions);
239
+ return this;
240
+ }
241
+
242
+ tag(...tags: string[]): this {
243
+ this.#tags.push(...tags);
244
+ return this;
245
+ }
246
+
247
+ keyword(...keywords: string[]): this {
248
+ this.#keywords.push(...keywords);
249
+ return this;
250
+ }
251
+
252
+ hidden(value: boolean = true): this {
253
+ this.#hidden = value;
254
+ return this;
255
+ }
256
+
257
+ usage(...usage: string[]): this {
258
+ this.#commandConfig.usage = [...(this.#commandConfig.usage || []), ...usage];
259
+ return this;
260
+ }
261
+
262
+ examples(...examples: string[]): this {
263
+ this.#commandConfig.examples = [...(this.#commandConfig.examples || []), ...examples];
264
+ return this;
265
+ }
266
+
267
+ alias(...alias: string[]): this {
268
+ this.#commandConfig.alias = [...(this.#commandConfig.alias || []), ...alias];
269
+ return this;
270
+ }
271
+
272
+ pattern(pattern: string): this {
273
+ this.#commandConfig.pattern = pattern;
274
+ return this;
275
+ }
276
+
277
+ execute(callback: (args: Record<string, any>, context?: ToolContext) => MaybePromise<any>): this {
278
+ this.#execute = callback;
279
+ return this;
280
+ }
281
+
282
+ action(callback: MessageCommand.Callback<RegisteredAdapter>): this {
283
+ this.#commandCallback = callback;
284
+ return this;
285
+ }
286
+
287
+ #buildParameters(): ToolParametersSchema {
288
+ const properties: Record<string, PropertySchema> = {};
289
+ const required: string[] = [];
290
+
291
+ const sortedParams = [...this.#params].sort((a, b) => {
292
+ if (a.required && !b.required) return -1;
293
+ if (!a.required && b.required) return 1;
294
+ return 0;
295
+ });
296
+
297
+ for (const param of sortedParams) {
298
+ properties[param.name] = param.schema;
299
+ if (param.required) {
300
+ required.push(param.name);
301
+ }
302
+ }
303
+
304
+ return {
305
+ type: 'object',
306
+ properties,
307
+ required: required.length > 0 ? required : undefined,
308
+ };
309
+ }
310
+
311
+ #generatePattern(): string {
312
+ if (this.#commandConfig.pattern) {
313
+ return this.#commandConfig.pattern;
314
+ }
315
+
316
+ const parts: string[] = [this.#name];
317
+
318
+ const sortedParams = [...this.#params].sort((a, b) => {
319
+ if (a.required && !b.required) return -1;
320
+ if (!a.required && b.required) return 1;
321
+ return 0;
322
+ });
323
+
324
+ for (const param of sortedParams) {
325
+ const paramType = param.schema.paramType || (param.schema.type === 'number' ? 'number' : 'text');
326
+ if (param.required) {
327
+ parts.push(`<${param.name}:${paramType}>`);
328
+ } else {
329
+ parts.push(`[${param.name}:${paramType}]`);
330
+ }
331
+ }
332
+
333
+ return parts.join(' ');
334
+ }
335
+
336
+ toTool(): Tool {
337
+ if (!this.#execute) {
338
+ throw new Error(`Tool "${this.#name}" has no execute() defined`);
339
+ }
340
+
341
+ const tool: Tool = {
342
+ name: this.#name,
343
+ description: this.#description,
344
+ parameters: this.#buildParameters(),
345
+ execute: this.#execute,
346
+ };
347
+
348
+ if (this.#platforms.length > 0) tool.platforms = this.#platforms;
349
+ if (this.#scopes.length > 0) tool.scopes = this.#scopes;
350
+ if (this.#permissionLevel !== 'user') tool.permissionLevel = this.#permissionLevel;
351
+ if (this.#permissions.length > 0) tool.permissions = this.#permissions;
352
+ if (this.#tags.length > 0) tool.tags = this.#tags;
353
+ if (this.#hidden) tool.hidden = this.#hidden;
354
+ if (this.#source) tool.source = this.#source;
355
+ if (this.#keywords.length > 0) tool.keywords = this.#keywords;
356
+
357
+ if (!this.#commandCallback) {
358
+ tool.command = false;
359
+ } else {
360
+ tool.command = {
361
+ ...this.#commandConfig,
362
+ pattern: this.#generatePattern(),
363
+ enabled: true,
364
+ };
365
+ }
366
+
367
+ return tool;
368
+ }
369
+
370
+ getActionCallback(): MessageCommand.Callback<RegisteredAdapter> | undefined {
371
+ return this.#commandCallback;
372
+ }
373
+
374
+ toJSON(): {
375
+ name: string;
376
+ description: string;
377
+ parameters: ToolParametersSchema;
378
+ platforms?: string[];
379
+ scopes?: ToolScope[];
380
+ permissionLevel?: ToolPermissionLevel;
381
+ tags?: string[];
382
+ } {
383
+ const json: ReturnType<ZhinTool['toJSON']> = {
384
+ name: this.#name,
385
+ description: this.#description,
386
+ parameters: this.#buildParameters(),
387
+ };
388
+
389
+ if (this.#platforms.length > 0) json.platforms = this.#platforms;
390
+ if (this.#scopes.length > 0) json.scopes = this.#scopes;
391
+ if (this.#permissionLevel !== 'user') json.permissionLevel = this.#permissionLevel;
392
+ if (this.#tags.length > 0) json.tags = this.#tags;
393
+
394
+ return json;
395
+ }
396
+
397
+ get help(): string {
398
+ const lines: string[] = [this.#generatePattern()];
399
+ if (this.#description) lines.push(` ${this.#description}`);
400
+
401
+ if (this.#params.length > 0) {
402
+ lines.push(' 参数:');
403
+ for (const param of this.#params) {
404
+ const required = param.required ? '(必填)' : '(可选)';
405
+ const desc = param.schema.description || '';
406
+ lines.push(` ${param.name}: ${param.schema.type} ${required} ${desc}`);
407
+ }
408
+ }
409
+
410
+ if (this.#permissionLevel !== 'user') {
411
+ lines.push(` 权限: ${this.#permissionLevel}`);
412
+ }
413
+
414
+ if (this.#platforms.length > 0) {
415
+ lines.push(` 平台: ${this.#platforms.join(', ')}`);
416
+ }
417
+
418
+ if (this.#scopes.length > 0) {
419
+ lines.push(` 场景: ${this.#scopes.join(', ')}`);
420
+ }
421
+
422
+ if (this.#commandConfig.usage?.length) {
423
+ lines.push(' 用法:');
424
+ for (const u of this.#commandConfig.usage) {
425
+ lines.push(` ${u}`);
426
+ }
427
+ }
428
+
429
+ if (this.#commandConfig.examples?.length) {
430
+ lines.push(' 示例:');
431
+ for (const e of this.#commandConfig.examples) {
432
+ lines.push(` ${e}`);
433
+ }
434
+ }
435
+
436
+ return lines.join('\n');
437
+ }
438
+
439
+ toString(): string {
440
+ return `[ZhinTool: ${this.#name}] ${this.#description}`;
441
+ }
442
+ }
443
+
444
+ export function isZhinTool(obj: any): obj is ZhinTool {
445
+ return obj instanceof ZhinTool;
446
+ }
447
+
448
+ // ============================================================================
449
+ // ToolFeature 类型定义
450
+ // ============================================================================
451
+
452
+ /**
453
+ * 工具输入类型(支持 Tool 对象或 ZhinTool 实例)
454
+ */
455
+ export type ToolInput = Tool | ZhinTool;
456
+
457
+ /**
458
+ * ToolContext 扩展方法类型
459
+ */
460
+ export interface ToolContextExtensions {
461
+ /** 添加工具(自动生成命令) */
462
+ addTool(tool: ToolInput): () => void;
463
+ /** 仅添加工具,不生成命令 */
464
+ addToolOnly(tool: ToolInput): () => void;
465
+ }
466
+
467
+ // 扩展 Plugin 接口
468
+ declare module "../plugin.js" {
469
+ namespace Plugin {
470
+ interface Extensions extends ToolContextExtensions {}
471
+ interface Contexts {
472
+ tool: ToolFeature;
473
+ }
474
+ }
475
+ }
476
+
477
+ // ============================================================================
478
+ // 内部工具函数
479
+ // ============================================================================
480
+
481
+ /**
482
+ * 将 Tool 转换为 MessageCommand
483
+ */
484
+ function toolToCommand(tool: Tool): MessageCommand<RegisteredAdapter> {
485
+ const pattern = generatePattern(tool);
486
+ const command = new MessageCommand<RegisteredAdapter>(pattern);
487
+
488
+ command.desc(tool.description);
489
+
490
+ if (tool.command && tool.command.usage) {
491
+ command.usage(...tool.command.usage);
492
+ }
493
+
494
+ if (tool.command && tool.command.examples) {
495
+ command.examples(...tool.command.examples);
496
+ }
497
+
498
+ if (tool.permissions?.length) {
499
+ command.permit(...tool.permissions);
500
+ }
501
+
502
+ command.action(async (message: Message<AdapterMessage<RegisteredAdapter>>, result: MatchResult) => {
503
+ const context: ToolContext = {
504
+ platform: message.$adapter,
505
+ botId: message.$bot,
506
+ sceneId: message.$channel?.id || message.$sender.id,
507
+ senderId: message.$sender.id,
508
+ message,
509
+ };
510
+
511
+ const args = extractArgsFromMatchResult(result, tool.parameters);
512
+
513
+ try {
514
+ const response = await tool.execute(args, context);
515
+
516
+ if (response === undefined || response === null) {
517
+ return undefined;
518
+ }
519
+
520
+ if (typeof response === 'string') {
521
+ return response;
522
+ }
523
+
524
+ return formatToolResult(response);
525
+ } catch (error) {
526
+ const errorMsg = error instanceof Error ? error.message : String(error);
527
+ return `❌ 执行失败: ${errorMsg}`;
528
+ }
529
+ });
530
+
531
+ return command;
532
+ }
533
+
534
+ /**
535
+ * 将 MessageCommand 转换为 Tool
536
+ */
537
+ function commandToToolFn(
538
+ command: MessageCommand<RegisteredAdapter>,
539
+ pluginName: string
540
+ ): Tool {
541
+ const { pattern, helpInfo } = command;
542
+
543
+ const parameters = parseCommandPattern(pattern);
544
+
545
+ return {
546
+ name: `cmd_${pattern.split(' ')[0].replace(/[^a-zA-Z0-9_]/g, '_')}`,
547
+ description: helpInfo.desc.join(' ') || `执行命令: ${pattern}`,
548
+ parameters,
549
+ source: `command:${pluginName}`,
550
+ tags: ['command', pluginName],
551
+ execute: async (args, context) => {
552
+ const cmdParts = [pattern.split(' ')[0]];
553
+
554
+ if (parameters.properties) {
555
+ for (const [key, schema] of Object.entries(parameters.properties)) {
556
+ if (args[key] !== undefined) {
557
+ cmdParts.push(String(args[key]));
558
+ }
559
+ }
560
+ }
561
+
562
+ const cmdString = cmdParts.join(' ');
563
+
564
+ if (context?.message) {
565
+ const tempMessage = Object.create(context.message);
566
+ tempMessage.$content = cmdString;
567
+
568
+ const plugin = getPlugin();
569
+ const result = await command.handle(tempMessage, plugin);
570
+ return result;
571
+ }
572
+
573
+ return {
574
+ error: '此工具需要消息上下文才能执行',
575
+ command: cmdString
576
+ };
577
+ },
578
+ command: false,
579
+ };
580
+ }
581
+
582
+ /**
583
+ * 从 MatchResult 提取参数
584
+ */
585
+ function extractArgsFromMatchResult(
586
+ result: MatchResult,
587
+ schema: ToolJsonSchema
588
+ ): Record<string, any> {
589
+ const args: Record<string, any> = {};
590
+
591
+ if (result.params) {
592
+ Object.assign(args, result.params);
593
+ }
594
+
595
+ if (schema.properties) {
596
+ for (const [key, prop] of Object.entries(schema.properties)) {
597
+ if (args[key] !== undefined) {
598
+ if (prop.type === 'number') {
599
+ args[key] = Number(args[key]);
600
+ } else if (prop.type === 'boolean') {
601
+ args[key] = args[key] === 'true' || args[key] === true;
602
+ } else if (prop.type === 'array' && typeof args[key] === 'string') {
603
+ args[key] = args[key].split(',').map((s: string) => s.trim());
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ return args;
610
+ }
611
+
612
+ /**
613
+ * 解析命令模式,生成参数 Schema
614
+ */
615
+ function parseCommandPattern(pattern: string): ToolParametersSchema {
616
+ const properties: Record<string, PropertySchema> = {};
617
+ const required: string[] = [];
618
+
619
+ const paramRegex = /([<\[])(\w+)(?::(\w+))?([>\]])/g;
620
+ let match;
621
+
622
+ while ((match = paramRegex.exec(pattern)) !== null) {
623
+ const [, bracket, name, type] = match;
624
+ const isRequired = bracket === '<';
625
+
626
+ const schemaType = type === 'number' ? 'number' : type === 'boolean' ? 'boolean' : 'string';
627
+ properties[name] = {
628
+ type: schemaType,
629
+ description: `参数: ${name}`,
630
+ } as PropertySchema;
631
+
632
+ if (isRequired) {
633
+ required.push(name);
634
+ }
635
+ }
636
+
637
+ return {
638
+ type: 'object',
639
+ properties,
640
+ required: required.length > 0 ? required : undefined,
641
+ } as ToolParametersSchema;
642
+ }
643
+
644
+ /**
645
+ * 格式化工具执行结果
646
+ */
647
+ function formatToolResult(result: any): string {
648
+ if (result === null || result === undefined) {
649
+ return '';
650
+ }
651
+
652
+ if (typeof result === 'string') {
653
+ return result;
654
+ }
655
+
656
+ if (result.error) {
657
+ return `❌ ${result.error}`;
658
+ }
659
+
660
+ try {
661
+ return JSON.stringify(result, null, 2);
662
+ } catch {
663
+ return String(result);
664
+ }
665
+ }
666
+
667
+ // ============================================================================
668
+ // ToolFeature 实现
669
+ // ============================================================================
670
+
671
+ export class ToolFeature extends Feature<Tool> {
672
+ readonly name = 'tool' as const;
673
+ readonly icon = 'Wrench';
674
+ readonly desc = '工具';
675
+
676
+ /** 按名称索引 */
677
+ readonly byName = new Map<string, Tool>();
678
+
679
+ /** 工具对应的命令 */
680
+ readonly toolCommands = new Map<string, MessageCommand<RegisteredAdapter>>();
681
+
682
+ /** 工具到插件名的映射 */
683
+ readonly #toolPluginMap = new Map<string, string>();
684
+
685
+ /**
686
+ * 添加工具
687
+ * @param toolInput 工具或 ZhinTool 实例
688
+ * @param pluginName 注册插件名
689
+ * @param generateCommand 是否生成命令(默认 true)
690
+ */
691
+ addTool(toolInput: ToolInput, pluginName: string, generateCommand: boolean = true): () => void {
692
+ const zhinTool = isZhinTool(toolInput) ? toolInput : null;
693
+ const tool: Tool = zhinTool ? zhinTool.toTool() : toolInput as Tool;
694
+
695
+ const toolWithSource: Tool = {
696
+ ...tool,
697
+ source: tool.source || `plugin:${pluginName}`,
698
+ tags: [...(tool.tags || []), 'plugin', pluginName],
699
+ };
700
+
701
+ this.byName.set(tool.name, toolWithSource);
702
+ this.#toolPluginMap.set(tool.name, pluginName);
703
+
704
+ // 生成对应的命令
705
+ if (generateCommand && tool.command !== false) {
706
+ let command: MessageCommand<RegisteredAdapter>;
707
+
708
+ const customCallback = zhinTool?.getActionCallback();
709
+ if (customCallback) {
710
+ command = new MessageCommand<RegisteredAdapter>(
711
+ tool.command && typeof tool.command === 'object' && tool.command.pattern
712
+ ? tool.command.pattern
713
+ : generatePattern(toolWithSource)
714
+ );
715
+
716
+ command.desc(tool.description);
717
+
718
+ if (tool.command && typeof tool.command === 'object') {
719
+ if (tool.command.usage) command.usage(...tool.command.usage);
720
+ if (tool.command.examples) command.examples(...tool.command.examples);
721
+ }
722
+
723
+ if (tool.permissions?.length) {
724
+ command.permit(...tool.permissions);
725
+ }
726
+
727
+ command.action(customCallback);
728
+ } else {
729
+ command = toolToCommand(toolWithSource);
730
+ }
731
+
732
+ this.toolCommands.set(tool.name, command);
733
+
734
+ const plugin = getPlugin();
735
+ const commandService = plugin.root.inject('command');
736
+ if (commandService) {
737
+ commandService.add(command, pluginName);
738
+ }
739
+ }
740
+
741
+ // Use Feature.add for item tracking
742
+ const baseDispose = super.add(toolWithSource, pluginName);
743
+
744
+ return () => {
745
+ this.removeTool(tool.name);
746
+ baseDispose();
747
+ };
748
+ }
749
+
750
+ /**
751
+ * 移除工具
752
+ */
753
+ removeTool(name: string): boolean {
754
+ const tool = this.byName.get(name);
755
+ if (!tool) return false;
756
+
757
+ this.byName.delete(name);
758
+ this.#toolPluginMap.delete(name);
759
+
760
+ // 移除对应的命令
761
+ const command = this.toolCommands.get(name);
762
+ if (command) {
763
+ const plugin = getPlugin();
764
+ const commandService = plugin.root.inject('command');
765
+ if (commandService) {
766
+ commandService.remove(command);
767
+ }
768
+ this.toolCommands.delete(name);
769
+ }
770
+
771
+ // 移除 item
772
+ super.remove(tool);
773
+ return true;
774
+ }
775
+
776
+ /**
777
+ * 获取工具
778
+ */
779
+ get(name: string): Tool | undefined {
780
+ return this.byName.get(name);
781
+ }
782
+
783
+ /**
784
+ * 获取所有工具
785
+ */
786
+ getAll(): Tool[] {
787
+ return [...this.items];
788
+ }
789
+
790
+ /**
791
+ * 根据标签过滤工具
792
+ */
793
+ getByTags(tags: string[]): Tool[] {
794
+ return this.items.filter(tool =>
795
+ tags.some(tag => tool.tags?.includes(tag))
796
+ );
797
+ }
798
+
799
+ /**
800
+ * 执行工具
801
+ */
802
+ async execute(name: string, args: Record<string, any>, context?: ToolContext): Promise<any> {
803
+ const tool = this.byName.get(name);
804
+ if (!tool) {
805
+ throw new Error(`Tool "${name}" not found`);
806
+ }
807
+ return tool.execute(args, context);
808
+ }
809
+
810
+ /**
811
+ * 将 Command 转换为 Tool
812
+ */
813
+ commandToTool(command: MessageCommand<RegisteredAdapter>, pluginName: string): Tool {
814
+ return commandToToolFn(command, pluginName);
815
+ }
816
+
817
+ /**
818
+ * 收集所有可用工具(包括从 Command 转换的)
819
+ */
820
+ collectAll(plugin: Plugin): Tool[] {
821
+ const allTools: Tool[] = [];
822
+
823
+ allTools.push(...this.getAll());
824
+
825
+ const commandService = plugin.root.inject('command');
826
+ if (commandService) {
827
+ for (const command of commandService.items) {
828
+ const isFromTool = Array.from(this.toolCommands.values()).includes(command);
829
+ if (!isFromTool) {
830
+ const toolFromCmd = commandToToolFn(command, 'command');
831
+ allTools.push(toolFromCmd);
832
+ }
833
+ }
834
+ }
835
+
836
+ for (const [name, context] of plugin.root.contexts) {
837
+ const adapterValue = context.value;
838
+ if (adapterValue && typeof adapterValue === 'object' && 'getTools' in adapterValue) {
839
+ const adapter = adapterValue as { getTools(): Tool[] };
840
+ allTools.push(...adapter.getTools());
841
+ }
842
+ }
843
+
844
+ return allTools;
845
+ }
846
+
847
+ /**
848
+ * 根据上下文过滤工具
849
+ */
850
+ filterByContext(tools: Tool[], context: ToolContext): Tool[] {
851
+ return tools.filter(tool => canAccessTool(tool, context));
852
+ }
853
+
854
+ /**
855
+ * 按插件名获取工具
856
+ */
857
+ getToolsByPlugin(pluginName: string): Tool[] {
858
+ const result: Tool[] = [];
859
+ for (const [toolName, pName] of this.#toolPluginMap) {
860
+ if (pName === pluginName) {
861
+ const tool = this.byName.get(toolName);
862
+ if (tool) result.push(tool);
863
+ }
864
+ }
865
+ return result;
866
+ }
867
+
868
+ /**
869
+ * 兼容旧接口:tools Map
870
+ */
871
+ get tools(): Map<string, Tool> {
872
+ return this.byName;
873
+ }
874
+
875
+ /**
876
+ * 序列化为 JSON
877
+ */
878
+ toJSON(pluginName?: string): FeatureJSON {
879
+ const list = pluginName ? this.getByPlugin(pluginName) : this.items;
880
+ return {
881
+ name: this.name,
882
+ icon: this.icon,
883
+ desc: this.desc,
884
+ count: list.length,
885
+ items: list.map(t => ({
886
+ name: t.name,
887
+ desc: t.description,
888
+ platforms: t.platforms,
889
+ tags: t.tags,
890
+ })),
891
+ };
892
+ }
893
+
894
+ /**
895
+ * 提供给 Plugin.prototype 的扩展方法
896
+ */
897
+ get extensions() {
898
+ const feature = this;
899
+ return {
900
+ addTool(tool: ToolInput) {
901
+ const plugin = getPlugin();
902
+ const toolObj = isZhinTool(tool) ? tool.toTool() : tool as Tool;
903
+ const dispose = feature.addTool(tool, plugin.name, true);
904
+ plugin.recordFeatureContribution(feature.name, toolObj.name);
905
+ plugin.onDispose(dispose);
906
+ return dispose;
907
+ },
908
+ addToolOnly(tool: ToolInput) {
909
+ const plugin = getPlugin();
910
+ const toolObj = isZhinTool(tool) ? tool.toTool() : tool as Tool;
911
+ const dispose = feature.addTool(tool, plugin.name, false);
912
+ plugin.recordFeatureContribution(feature.name, toolObj.name);
913
+ plugin.onDispose(dispose);
914
+ return dispose;
915
+ },
916
+ };
917
+ }
918
+ }
919
+
920
+ // 导出类型和工具函数
921
+ export {
922
+ toolToCommand,
923
+ commandToToolFn as commandToTool,
924
+ canAccessTool,
925
+ inferPermissionLevel,
926
+ hasPermissionLevel,
927
+ PERMISSION_LEVEL_PRIORITY,
928
+ };