@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.
Files changed (202) hide show
  1. package/CHANGELOG.md +19 -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 +64 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +129 -12
  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 +6 -6
  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 +288 -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
  201. package/test/minimal-bot.ts +0 -31
  202. package/test/stress-test.ts +0 -123
package/src/built/tool.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Tool Service
3
- * 统一的工具管理服务,支持 Tool ↔ Command 互转
2
+ * ToolFeature — 统一的工具管理服务
3
+ * 支持 Tool ↔ Command 互转
4
4
  */
5
+ import { Feature, FeatureJSON } from "../feature.js";
5
6
  import { MessageCommand } from "../command.js";
6
7
  import { Message } from "../message.js";
7
- import { Context, Plugin, getPlugin } from "../plugin.js";
8
+ import { Plugin, getPlugin } from "../plugin.js";
8
9
  import type { Tool, ToolDefinition, RegisteredAdapter, AdapterMessage, ToolContext, ToolJsonSchema, ToolParametersSchema, PropertySchema, MaybePromise, ToolPermissionLevel, ToolScope } from "../types.js";
9
10
  import { MatchResult } from "segment-matcher";
10
11
 
@@ -144,34 +145,10 @@ export function extractParamInfo(parameters: ToolJsonSchema): Tool.ParamInfo[] {
144
145
 
145
146
  /**
146
147
  * 定义工具的辅助函数(提供类型推断)
147
- *
148
- * 使用泛型参数获得 execute 函数的类型检查,
149
- * 返回通用的 Tool 类型,可直接传给 addTool
150
- *
151
- * @example
152
- * ```typescript
153
- * const myTool = defineTool<{ name: string }>({
154
- * name: 'greet',
155
- * description: '打招呼',
156
- * parameters: {
157
- * type: 'object',
158
- * properties: {
159
- * name: { type: 'string', description: '名字' }
160
- * },
161
- * required: ['name']
162
- * },
163
- * execute: async (args) => {
164
- * return `你好,${args.name}!`; // args.name 有类型提示
165
- * }
166
- * });
167
- *
168
- * plugin.addTool(myTool); // 无需类型断言
169
- * ```
170
148
  */
171
149
  export function defineTool<TArgs extends Record<string, any> = Record<string, any>>(
172
150
  tool: ToolDefinition<TArgs>
173
151
  ): Tool {
174
- // ToolDefinition<TArgs> 兼容 Tool,因为 execute 参数是协变的
175
152
  return tool as Tool;
176
153
  }
177
154
 
@@ -191,28 +168,6 @@ interface ParamDef {
191
168
  /**
192
169
  * ZhinTool 类
193
170
  * 提供类似 MessageCommand 的链式调用风格来定义工具
194
- *
195
- * @example
196
- * ```typescript
197
- * const weatherTool = new ZhinTool('weather')
198
- * .desc('查询天气信息')
199
- * .param('city', { type: 'string', description: '城市名称' }, true)
200
- * .param('days', { type: 'number', description: '预报天数' })
201
- * .platform('qq', 'telegram')
202
- * .scope('group', 'private')
203
- * .permission('user')
204
- * // AI 调用时执行(必须)
205
- * .execute(async (args, ctx) => {
206
- * return `${args.city} 的天气是晴天`;
207
- * })
208
- * // 可选:命令回调(与 MessageCommand.action 一致)
209
- * // 如果定义了此回调,会生成 Command
210
- * .action(async (message, result) => {
211
- * return `命令执行: ${result.params.city}`;
212
- * });
213
- *
214
- * plugin.addTool(weatherTool);
215
- * ```
216
171
  */
217
172
  export class ZhinTool {
218
173
  #name: string;
@@ -225,6 +180,7 @@ export class ZhinTool {
225
180
  #permissionLevel: ToolPermissionLevel = 'user';
226
181
  #permissions: string[] = [];
227
182
  #tags: string[] = [];
183
+ #keywords: string[] = [];
228
184
  /** 命令回调(入参是 message, matchResult) */
229
185
  #commandCallback?: MessageCommand.Callback<RegisteredAdapter>;
230
186
  /** 命令配置 */
@@ -232,46 +188,28 @@ export class ZhinTool {
232
188
  #hidden: boolean = false;
233
189
  #source?: string;
234
190
 
235
- /**
236
- * 创建工具实例
237
- * @param name 工具名称(唯一标识,建议使用 snake_case)
238
- */
239
191
  constructor(name: string) {
240
192
  this.#name = name;
241
193
  }
242
194
 
243
- /** 获取工具名称 */
244
195
  get name(): string {
245
196
  return this.#name;
246
197
  }
247
198
 
248
- /** 获取工具描述 */
249
199
  get description(): string {
250
200
  return this.#description;
251
201
  }
252
202
 
253
- /** 获取有序的参数列表 */
254
203
  get params(): ParamDef[] {
255
204
  return [...this.#params];
256
205
  }
257
206
 
258
- /**
259
- * 设置工具描述
260
- * @param description 工具描述(供 AI 和帮助系统使用)
261
- */
262
207
  desc(description: string): this {
263
208
  this.#description = description;
264
209
  return this;
265
210
  }
266
211
 
267
- /**
268
- * 添加参数(按调用顺序保持有序)
269
- * @param name 参数名称
270
- * @param schema 参数 Schema(类型、描述等)
271
- * @param required 是否必填(默认 false)
272
- */
273
212
  param(name: string, schema: PropertySchema, required: boolean = false): this {
274
- // 如果已存在同名参数,更新它
275
213
  const existingIndex = this.#params.findIndex(p => p.name === name);
276
214
  if (existingIndex >= 0) {
277
215
  this.#params[existingIndex] = { name, schema, required };
@@ -281,125 +219,75 @@ export class ZhinTool {
281
219
  return this;
282
220
  }
283
221
 
284
- /**
285
- * 设置支持的平台
286
- * @param platforms 平台名称列表(如 'qq', 'telegram')
287
- */
288
222
  platform(...platforms: string[]): this {
289
223
  this.#platforms.push(...platforms);
290
224
  return this;
291
225
  }
292
226
 
293
- /**
294
- * 设置支持的场景
295
- * @param scopes 场景类型列表('private', 'group', 'channel')
296
- */
297
227
  scope(...scopes: ToolScope[]): this {
298
228
  this.#scopes.push(...scopes);
299
229
  return this;
300
230
  }
301
231
 
302
- /**
303
- * 设置权限级别
304
- * @param level 权限级别('user', 'group_admin', 'group_owner', 'bot_admin', 'owner')
305
- */
306
232
  permission(level: ToolPermissionLevel): this {
307
233
  this.#permissionLevel = level;
308
234
  return this;
309
235
  }
310
236
 
311
- /**
312
- * 添加旧版权限要求(兼容 MessageCommand)
313
- * @param permissions 权限字符串列表
314
- */
315
237
  permit(...permissions: string[]): this {
316
238
  this.#permissions.push(...permissions);
317
239
  return this;
318
240
  }
319
241
 
320
- /**
321
- * 添加标签
322
- * @param tags 标签列表
323
- */
324
242
  tag(...tags: string[]): this {
325
243
  this.#tags.push(...tags);
326
244
  return this;
327
245
  }
328
246
 
329
- /**
330
- * 设置是否隐藏
331
- * @param value 是否隐藏(默认 true)
332
- */
247
+ keyword(...keywords: string[]): this {
248
+ this.#keywords.push(...keywords);
249
+ return this;
250
+ }
251
+
333
252
  hidden(value: boolean = true): this {
334
253
  this.#hidden = value;
335
254
  return this;
336
255
  }
337
256
 
338
- /**
339
- * 设置使用说明(命令配置)
340
- * @param usage 使用说明列表
341
- */
342
257
  usage(...usage: string[]): this {
343
258
  this.#commandConfig.usage = [...(this.#commandConfig.usage || []), ...usage];
344
259
  return this;
345
260
  }
346
261
 
347
- /**
348
- * 设置示例(命令配置)
349
- * @param examples 示例列表
350
- */
351
262
  examples(...examples: string[]): this {
352
263
  this.#commandConfig.examples = [...(this.#commandConfig.examples || []), ...examples];
353
264
  return this;
354
265
  }
355
266
 
356
- /**
357
- * 设置别名(命令配置)
358
- * @param alias 别名列表
359
- */
360
267
  alias(...alias: string[]): this {
361
268
  this.#commandConfig.alias = [...(this.#commandConfig.alias || []), ...alias];
362
269
  return this;
363
270
  }
364
271
 
365
- /**
366
- * 设置自定义命令模式
367
- * @param pattern 命令模式(如 'weather <city> [days]')
368
- */
369
272
  pattern(pattern: string): this {
370
273
  this.#commandConfig.pattern = pattern;
371
274
  return this;
372
275
  }
373
276
 
374
- /**
375
- * 设置 AI 工具执行函数
376
- * @param callback 执行回调,入参是 (args, context)
377
- */
378
277
  execute(callback: (args: Record<string, any>, context?: ToolContext) => MaybePromise<any>): this {
379
278
  this.#execute = callback;
380
279
  return this;
381
280
  }
382
281
 
383
- /**
384
- * 设置命令回调(可选,与 MessageCommand.action 一致)
385
- * 如果定义了此回调,会自动生成 Command
386
- * 入参是 (message, matchResult)
387
- *
388
- * @param callback 命令回调
389
- */
390
282
  action(callback: MessageCommand.Callback<RegisteredAdapter>): this {
391
283
  this.#commandCallback = callback;
392
284
  return this;
393
285
  }
394
286
 
395
- /**
396
- * 构建有序的 ToolParametersSchema
397
- */
398
287
  #buildParameters(): ToolParametersSchema {
399
288
  const properties: Record<string, PropertySchema> = {};
400
289
  const required: string[] = [];
401
290
 
402
- // 按顺序构建(必填参数排前面)
403
291
  const sortedParams = [...this.#params].sort((a, b) => {
404
292
  if (a.required && !b.required) return -1;
405
293
  if (!a.required && b.required) return 1;
@@ -420,9 +308,6 @@ export class ZhinTool {
420
308
  };
421
309
  }
422
310
 
423
- /**
424
- * 生成命令模式字符串
425
- */
426
311
  #generatePattern(): string {
427
312
  if (this.#commandConfig.pattern) {
428
313
  return this.#commandConfig.pattern;
@@ -430,7 +315,6 @@ export class ZhinTool {
430
315
 
431
316
  const parts: string[] = [this.#name];
432
317
 
433
- // 按顺序生成参数(必填在前,可选在后)
434
318
  const sortedParams = [...this.#params].sort((a, b) => {
435
319
  if (a.required && !b.required) return -1;
436
320
  if (!a.required && b.required) return 1;
@@ -449,9 +333,6 @@ export class ZhinTool {
449
333
  return parts.join(' ');
450
334
  }
451
335
 
452
- /**
453
- * 转换为 Tool 对象
454
- */
455
336
  toTool(): Tool {
456
337
  if (!this.#execute) {
457
338
  throw new Error(`Tool "${this.#name}" has no execute() defined`);
@@ -464,7 +345,6 @@ export class ZhinTool {
464
345
  execute: this.#execute,
465
346
  };
466
347
 
467
- // 添加可选字段
468
348
  if (this.#platforms.length > 0) tool.platforms = this.#platforms;
469
349
  if (this.#scopes.length > 0) tool.scopes = this.#scopes;
470
350
  if (this.#permissionLevel !== 'user') tool.permissionLevel = this.#permissionLevel;
@@ -472,8 +352,8 @@ export class ZhinTool {
472
352
  if (this.#tags.length > 0) tool.tags = this.#tags;
473
353
  if (this.#hidden) tool.hidden = this.#hidden;
474
354
  if (this.#source) tool.source = this.#source;
355
+ if (this.#keywords.length > 0) tool.keywords = this.#keywords;
475
356
 
476
- // 命令配置:如果没有定义 command 回调,则不生成命令
477
357
  if (!this.#commandCallback) {
478
358
  tool.command = false;
479
359
  } else {
@@ -487,17 +367,10 @@ export class ZhinTool {
487
367
  return tool;
488
368
  }
489
369
 
490
- /**
491
- * 获取 action 回调(命令回调,如果有)
492
- */
493
370
  getActionCallback(): MessageCommand.Callback<RegisteredAdapter> | undefined {
494
371
  return this.#commandCallback;
495
372
  }
496
373
 
497
- /**
498
- * 转换为 JSON 格式(供 AI 使用,不包含 execute 函数)
499
- * 符合 OpenAI Function Calling 规范
500
- */
501
374
  toJSON(): {
502
375
  name: string;
503
376
  description: string;
@@ -513,7 +386,6 @@ export class ZhinTool {
513
386
  parameters: this.#buildParameters(),
514
387
  };
515
388
 
516
- // 添加可选字段
517
389
  if (this.#platforms.length > 0) json.platforms = this.#platforms;
518
390
  if (this.#scopes.length > 0) json.scopes = this.#scopes;
519
391
  if (this.#permissionLevel !== 'user') json.permissionLevel = this.#permissionLevel;
@@ -522,14 +394,10 @@ export class ZhinTool {
522
394
  return json;
523
395
  }
524
396
 
525
- /**
526
- * 输出帮助信息(类似 MessageCommand)
527
- */
528
397
  get help(): string {
529
398
  const lines: string[] = [this.#generatePattern()];
530
399
  if (this.#description) lines.push(` ${this.#description}`);
531
400
 
532
- // 参数信息(按顺序)
533
401
  if (this.#params.length > 0) {
534
402
  lines.push(' 参数:');
535
403
  for (const param of this.#params) {
@@ -539,22 +407,18 @@ export class ZhinTool {
539
407
  }
540
408
  }
541
409
 
542
- // 权限信息
543
410
  if (this.#permissionLevel !== 'user') {
544
411
  lines.push(` 权限: ${this.#permissionLevel}`);
545
412
  }
546
413
 
547
- // 平台限制
548
414
  if (this.#platforms.length > 0) {
549
415
  lines.push(` 平台: ${this.#platforms.join(', ')}`);
550
416
  }
551
417
 
552
- // 场景限制
553
418
  if (this.#scopes.length > 0) {
554
419
  lines.push(` 场景: ${this.#scopes.join(', ')}`);
555
420
  }
556
421
 
557
- // 命令说明
558
422
  if (this.#commandConfig.usage?.length) {
559
423
  lines.push(' 用法:');
560
424
  for (const u of this.#commandConfig.usage) {
@@ -562,7 +426,6 @@ export class ZhinTool {
562
426
  }
563
427
  }
564
428
 
565
- // 命令示例
566
429
  if (this.#commandConfig.examples?.length) {
567
430
  lines.push(' 示例:');
568
431
  for (const e of this.#commandConfig.examples) {
@@ -573,22 +436,17 @@ export class ZhinTool {
573
436
  return lines.join('\n');
574
437
  }
575
438
 
576
- /**
577
- * 输出简短信息
578
- */
579
439
  toString(): string {
580
440
  return `[ZhinTool: ${this.#name}] ${this.#description}`;
581
441
  }
582
442
  }
583
443
 
584
- // 为了兼容 addTool,让 ZhinTool 可以隐式转换为 Tool
585
- // 通过在 ToolService 中检查是否是 ZhinTool 实例
586
444
  export function isZhinTool(obj: any): obj is ZhinTool {
587
445
  return obj instanceof ZhinTool;
588
446
  }
589
447
 
590
448
  // ============================================================================
591
- // ToolService 类型定义
449
+ // ToolFeature 类型定义
592
450
  // ============================================================================
593
451
 
594
452
  /**
@@ -597,7 +455,7 @@ export function isZhinTool(obj: any): obj is ZhinTool {
597
455
  export type ToolInput = Tool | ZhinTool;
598
456
 
599
457
  /**
600
- * ToolService 扩展方法类型
458
+ * ToolContext 扩展方法类型
601
459
  */
602
460
  export interface ToolContextExtensions {
603
461
  /** 添加工具(自动生成命令) */
@@ -611,51 +469,14 @@ declare module "../plugin.js" {
611
469
  namespace Plugin {
612
470
  interface Extensions extends ToolContextExtensions {}
613
471
  interface Contexts {
614
- tool: ToolService;
472
+ tool: ToolFeature;
615
473
  }
616
474
  }
617
475
  }
618
476
 
619
- /**
620
- * 工具服务
621
- */
622
- export interface ToolService {
623
- /** 所有注册的工具 */
624
- readonly tools: Map<string, Tool>;
625
-
626
- /** 工具对应的命令(如果生成了) */
627
- readonly toolCommands: Map<string, MessageCommand<RegisteredAdapter>>;
628
-
629
- /** 添加工具(支持 Tool 对象或 ZhinTool 实例) */
630
- add(tool: ToolInput, pluginName: string, generateCommand?: boolean): () => void;
631
-
632
- /** 移除工具 */
633
- remove(name: string): boolean;
634
-
635
- /** 获取工具 */
636
- get(name: string): Tool | undefined;
637
-
638
- /** 获取所有工具 */
639
- getAll(): Tool[];
640
-
641
- /** 根据标签过滤工具 */
642
- getByTags(tags: string[]): Tool[];
643
-
644
- /** 执行工具 */
645
- execute(name: string, args: Record<string, any>, context?: ToolContext): Promise<any>;
646
-
647
- /** 将 Command 转换为 Tool */
648
- commandToTool(command: MessageCommand<RegisteredAdapter>, pluginName: string): Tool;
649
-
650
- /** 收集所有可用工具(包括从 Command 转换的) */
651
- collectAll(plugin: Plugin): Tool[];
652
-
653
- /**
654
- * 根据上下文过滤工具
655
- * 检查平台、场景、权限是否匹配
656
- */
657
- filterByContext(tools: Tool[], context: ToolContext): Tool[];
658
- }
477
+ // ============================================================================
478
+ // 内部工具函数
479
+ // ============================================================================
659
480
 
660
481
  /**
661
482
  * 将 Tool 转换为 MessageCommand
@@ -664,27 +485,21 @@ function toolToCommand(tool: Tool): MessageCommand<RegisteredAdapter> {
664
485
  const pattern = generatePattern(tool);
665
486
  const command = new MessageCommand<RegisteredAdapter>(pattern);
666
487
 
667
- // 设置描述
668
488
  command.desc(tool.description);
669
489
 
670
- // 设置使用说明
671
490
  if (tool.command && tool.command.usage) {
672
491
  command.usage(...tool.command.usage);
673
492
  }
674
493
 
675
- // 设置示例
676
494
  if (tool.command && tool.command.examples) {
677
495
  command.examples(...tool.command.examples);
678
496
  }
679
497
 
680
- // 设置权限
681
498
  if (tool.permissions?.length) {
682
499
  command.permit(...tool.permissions);
683
500
  }
684
501
 
685
- // 设置执行回调
686
502
  command.action(async (message: Message<AdapterMessage<RegisteredAdapter>>, result: MatchResult) => {
687
- // 构建工具上下文
688
503
  const context: ToolContext = {
689
504
  platform: message.$adapter,
690
505
  botId: message.$bot,
@@ -693,13 +508,11 @@ function toolToCommand(tool: Tool): MessageCommand<RegisteredAdapter> {
693
508
  message,
694
509
  };
695
510
 
696
- // 从 MatchResult 提取参数
697
511
  const args = extractArgsFromMatchResult(result, tool.parameters);
698
512
 
699
513
  try {
700
514
  const response = await tool.execute(args, context);
701
515
 
702
- // 处理返回值
703
516
  if (response === undefined || response === null) {
704
517
  return undefined;
705
518
  }
@@ -708,7 +521,6 @@ function toolToCommand(tool: Tool): MessageCommand<RegisteredAdapter> {
708
521
  return response;
709
522
  }
710
523
 
711
- // 对象类型,格式化输出
712
524
  return formatToolResult(response);
713
525
  } catch (error) {
714
526
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -722,13 +534,12 @@ function toolToCommand(tool: Tool): MessageCommand<RegisteredAdapter> {
722
534
  /**
723
535
  * 将 MessageCommand 转换为 Tool
724
536
  */
725
- function commandToTool(
537
+ function commandToToolFn(
726
538
  command: MessageCommand<RegisteredAdapter>,
727
539
  pluginName: string
728
540
  ): Tool {
729
541
  const { pattern, helpInfo } = command;
730
542
 
731
- // 解析命令模式,提取参数
732
543
  const parameters = parseCommandPattern(pattern);
733
544
 
734
545
  return {
@@ -738,7 +549,6 @@ function commandToTool(
738
549
  source: `command:${pluginName}`,
739
550
  tags: ['command', pluginName],
740
551
  execute: async (args, context) => {
741
- // 重建命令字符串
742
552
  const cmdParts = [pattern.split(' ')[0]];
743
553
 
744
554
  if (parameters.properties) {
@@ -751,9 +561,7 @@ function commandToTool(
751
561
 
752
562
  const cmdString = cmdParts.join(' ');
753
563
 
754
- // 如果有消息上下文,模拟命令执行
755
564
  if (context?.message) {
756
- // 创建一个临时消息副本,修改内容为命令字符串
757
565
  const tempMessage = Object.create(context.message);
758
566
  tempMessage.$content = cmdString;
759
567
 
@@ -767,7 +575,7 @@ function commandToTool(
767
575
  command: cmdString
768
576
  };
769
577
  },
770
- command: false, // 不再生成命令(已经是命令了)
578
+ command: false,
771
579
  };
772
580
  }
773
581
 
@@ -780,12 +588,10 @@ function extractArgsFromMatchResult(
780
588
  ): Record<string, any> {
781
589
  const args: Record<string, any> = {};
782
590
 
783
- // params 包含所有提取的参数
784
591
  if (result.params) {
785
592
  Object.assign(args, result.params);
786
593
  }
787
594
 
788
- // 类型转换
789
595
  if (schema.properties) {
790
596
  for (const [key, prop] of Object.entries(schema.properties)) {
791
597
  if (args[key] !== undefined) {
@@ -805,13 +611,11 @@ function extractArgsFromMatchResult(
805
611
 
806
612
  /**
807
613
  * 解析命令模式,生成参数 Schema
808
- * @example 'weather <city> [days:number]' => { properties: { city: {...}, days: {...} }, required: ['city'] }
809
614
  */
810
615
  function parseCommandPattern(pattern: string): ToolParametersSchema {
811
616
  const properties: Record<string, PropertySchema> = {};
812
617
  const required: string[] = [];
813
618
 
814
- // 匹配 <name:type> 或 [name:type] 格式
815
619
  const paramRegex = /([<\[])(\w+)(?::(\w+))?([>\]])/g;
816
620
  let match;
817
621
 
@@ -853,7 +657,6 @@ function formatToolResult(result: any): string {
853
657
  return `❌ ${result.error}`;
854
658
  }
855
659
 
856
- // 尝试友好格式化
857
660
  try {
858
661
  return JSON.stringify(result, null, 2);
859
662
  } catch {
@@ -861,189 +664,265 @@ function formatToolResult(result: any): string {
861
664
  }
862
665
  }
863
666
 
864
- /**
865
- * 创建工具服务 Context
866
- */
867
- export function createToolService(): Context<'tool', ToolContextExtensions> {
868
- const tools = new Map<string, Tool>();
869
- const toolCommands = new Map<string, MessageCommand<RegisteredAdapter>>();
870
- const toolPluginMap = new Map<string, string>();
871
-
872
- const value: ToolService = {
873
- tools,
874
- toolCommands,
875
-
876
- add(toolInput, pluginName, generateCommand = true) {
877
- // 保存原始 ZhinTool 引用(用于获取命令回调)
878
- const zhinTool = isZhinTool(toolInput) ? toolInput : null;
879
-
880
- // 转换为 Tool 对象
881
- const tool: Tool = zhinTool ? zhinTool.toTool() : toolInput as Tool;
882
-
883
- // 自动添加来源标识
884
- const toolWithSource: Tool = {
885
- ...tool,
886
- source: tool.source || `plugin:${pluginName}`,
887
- tags: [...(tool.tags || []), 'plugin', pluginName],
888
- };
889
-
890
- tools.set(tool.name, toolWithSource);
891
- toolPluginMap.set(tool.name, pluginName);
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>;
892
707
 
893
- // 生成对应的命令
894
- // 只有当 tool.command !== false 且 generateCommand 为 true 时才生成
895
- if (generateCommand && tool.command !== false) {
896
- let command: MessageCommand<RegisteredAdapter>;
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
+ );
897
715
 
898
- // 如果是 ZhinTool 且有自定义 action 回调,使用自定义回调
899
- const customCallback = zhinTool?.getActionCallback();
900
- if (customCallback) {
901
- // 使用自定义回调创建命令
902
- command = new MessageCommand<RegisteredAdapter>(
903
- tool.command && typeof tool.command === 'object' && tool.command.pattern
904
- ? tool.command.pattern
905
- : generatePattern(toolWithSource)
906
- );
907
-
908
- command.desc(tool.description);
909
-
910
- if (tool.command && typeof tool.command === 'object') {
911
- if (tool.command.usage) command.usage(...tool.command.usage);
912
- if (tool.command.examples) command.examples(...tool.command.examples);
913
- }
914
-
915
- if (tool.permissions?.length) {
916
- command.permit(...tool.permissions);
917
- }
918
-
919
- // 使用自定义回调
920
- command.action(customCallback);
921
- } else {
922
- // 使用默认的 toolToCommand(基于 tool.execute)
923
- command = toolToCommand(toolWithSource);
924
- }
716
+ command.desc(tool.description);
925
717
 
926
- toolCommands.set(tool.name, command);
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
+ }
927
722
 
928
- // 注册到命令服务
929
- const plugin = getPlugin();
930
- const commandService = plugin.root.inject('command');
931
- if (commandService) {
932
- commandService.add(command, pluginName);
723
+ if (tool.permissions?.length) {
724
+ command.permit(...tool.permissions);
933
725
  }
726
+
727
+ command.action(customCallback);
728
+ } else {
729
+ command = toolToCommand(toolWithSource);
934
730
  }
935
731
 
936
- return () => value.remove(tool.name);
937
- },
938
-
939
- remove(name) {
940
- const existed = tools.has(name);
941
- tools.delete(name);
942
- toolPluginMap.delete(name);
732
+ this.toolCommands.set(tool.name, command);
943
733
 
944
- // 移除对应的命令
945
- const command = toolCommands.get(name);
946
- if (command) {
947
- const plugin = getPlugin();
948
- const commandService = plugin.root.inject('command');
949
- if (commandService) {
950
- commandService.remove(command);
951
- }
952
- toolCommands.delete(name);
734
+ const plugin = getPlugin();
735
+ const commandService = plugin.root.inject('command');
736
+ if (commandService) {
737
+ commandService.add(command, pluginName);
953
738
  }
954
-
955
- return existed;
956
- },
957
-
958
- get(name) {
959
- return tools.get(name);
960
- },
961
-
962
- getAll() {
963
- return Array.from(tools.values());
964
- },
965
-
966
- getByTags(tags) {
967
- return Array.from(tools.values()).filter(tool =>
968
- tags.some(tag => tool.tags?.includes(tag))
969
- );
970
- },
971
-
972
- async execute(name, args, context) {
973
- const tool = tools.get(name);
974
- if (!tool) {
975
- throw new Error(`Tool "${name}" not found`);
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);
976
767
  }
977
- return tool.execute(args, context);
978
- },
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[] = [];
979
822
 
980
- commandToTool,
823
+ allTools.push(...this.getAll());
981
824
 
982
- collectAll(plugin) {
983
- const allTools: Tool[] = [];
984
-
985
- // 1. 收集 ToolService 中的所有工具
986
- allTools.push(...value.getAll());
987
-
988
- // 2. 收集 Command 并转换为 Tool
989
- const commandService = plugin.root.inject('command');
990
- if (commandService) {
991
- for (const command of commandService.items) {
992
- // 跳过已经由 Tool 生成的命令
993
- const isFromTool = Array.from(toolCommands.values()).includes(command);
994
- if (!isFromTool) {
995
- const toolFromCmd = commandToTool(command, 'command');
996
- allTools.push(toolFromCmd);
997
- }
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);
998
832
  }
999
833
  }
1000
-
1001
- // 3. 收集适配器提供的工具
1002
- for (const [name, context] of plugin.root.contexts) {
1003
- const adapterValue = context.value;
1004
- if (adapterValue && typeof adapterValue === 'object' && 'getTools' in adapterValue) {
1005
- const adapter = adapterValue as { getTools(): Tool[] };
1006
- allTools.push(...adapter.getTools());
1007
- }
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());
1008
841
  }
1009
-
1010
- return allTools;
1011
- },
842
+ }
1012
843
 
1013
- filterByContext(tools, context) {
1014
- return tools.filter(tool => canAccessTool(tool, context));
1015
- },
1016
- };
1017
-
1018
- return {
1019
- name: 'tool',
1020
- description: '统一工具服务',
1021
- value,
1022
- extensions: {
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 {
1023
900
  addTool(tool: ToolInput) {
1024
901
  const plugin = getPlugin();
1025
- const dispose = value.add(tool, plugin.name, true);
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);
1026
905
  plugin.onDispose(dispose);
1027
906
  return dispose;
1028
907
  },
1029
908
  addToolOnly(tool: ToolInput) {
1030
909
  const plugin = getPlugin();
1031
- const dispose = value.add(tool, plugin.name, false);
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);
1032
913
  plugin.onDispose(dispose);
1033
914
  return dispose;
1034
915
  },
1035
- },
1036
- };
916
+ };
917
+ }
1037
918
  }
1038
919
 
1039
920
  // 导出类型和工具函数
1040
- // 注意:ZhinTool, isZhinTool, ToolInput 已通过 export 关键字直接导出
1041
921
  export {
1042
922
  toolToCommand,
1043
- commandToTool,
923
+ commandToToolFn as commandToTool,
1044
924
  canAccessTool,
1045
925
  inferPermissionLevel,
1046
926
  hasPermissionLevel,
1047
927
  PERMISSION_LEVEL_PRIORITY,
1048
928
  };
1049
-