@zhin.js/core 1.0.50 → 1.0.52

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 (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/lib/adapter.d.ts +10 -19
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +47 -80
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/types.d.ts +11 -0
  8. package/lib/ai/types.d.ts.map +1 -1
  9. package/lib/built/common-adapter-tools.d.ts +2 -2
  10. package/lib/built/common-adapter-tools.js +3 -3
  11. package/lib/built/common-adapter-tools.js.map +1 -1
  12. package/lib/built/dispatcher.d.ts +52 -42
  13. package/lib/built/dispatcher.d.ts.map +1 -1
  14. package/lib/built/dispatcher.js +194 -60
  15. package/lib/built/dispatcher.js.map +1 -1
  16. package/lib/built/login-assist.d.ts +61 -0
  17. package/lib/built/login-assist.d.ts.map +1 -0
  18. package/lib/built/login-assist.js +75 -0
  19. package/lib/built/login-assist.js.map +1 -0
  20. package/lib/built/skill.d.ts +3 -20
  21. package/lib/built/skill.d.ts.map +1 -1
  22. package/lib/built/skill.js +1 -53
  23. package/lib/built/skill.js.map +1 -1
  24. package/lib/index.d.ts +1 -0
  25. package/lib/index.d.ts.map +1 -1
  26. package/lib/index.js +2 -0
  27. package/lib/index.js.map +1 -1
  28. package/lib/plugin.d.ts +18 -0
  29. package/lib/plugin.d.ts.map +1 -1
  30. package/lib/plugin.js +1 -1
  31. package/lib/plugin.js.map +1 -1
  32. package/lib/types.d.ts +13 -0
  33. package/lib/types.d.ts.map +1 -1
  34. package/lib/utils.js +1 -1
  35. package/package.json +6 -6
  36. package/src/adapter.ts +51 -95
  37. package/src/ai/types.ts +3 -1
  38. package/src/built/common-adapter-tools.ts +3 -3
  39. package/src/built/dispatcher.ts +264 -99
  40. package/src/built/login-assist.ts +131 -0
  41. package/src/built/skill.ts +3 -75
  42. package/src/index.ts +2 -0
  43. package/src/plugin.ts +24 -5
  44. package/src/types.ts +16 -0
  45. package/src/utils.ts +1 -1
  46. package/tests/adapter.test.ts +40 -128
  47. package/tests/dispatcher.test.ts +155 -8
  48. package/tests/redos-protection.test.ts +5 -4
@@ -5,8 +5,7 @@
5
5
  * Plugin = 运行时容器(生命周期、服务注册、中间件)
6
6
  * Skill = AI 可见的能力接口(名称、描述、工具列表)
7
7
  *
8
- * 每个 Plugin 可以声明一个 Skill 描述,告诉 AI Agent
9
- * "我叫什么、我能做什么、我有哪些工具"
8
+ * Skill 记录由运行时注入(如 Agent 从磁盘 SKILL.md 同步),供 Agent 粗筛与工具关联。
10
9
  *
11
10
  * SkillFeature 全局收集所有 Skill,供 Agent 进行两级过滤:
12
11
  * 1. 粗筛:根据用户消息选择相关 Skill
@@ -14,7 +13,6 @@
14
13
  */
15
14
 
16
15
  import { Feature, FeatureJSON } from '../feature.js';
17
- import { Plugin, getPlugin } from '../plugin.js';
18
16
  import type { Tool } from '../types.js';
19
17
 
20
18
  // ============================================================================
@@ -51,35 +49,21 @@ export interface Skill {
51
49
  }
52
50
 
53
51
  /**
54
- * Skill 元数据 开发者在插件中声明
55
- * 由 plugin.declareSkill() 注册
52
+ * SKILL.md frontmatter 常见字段(类型提示;运行时由 Agent 等同步到 SkillFeature)
56
53
  */
57
54
  export interface SkillMetadata {
58
55
  /** 技能描述(必填) */
59
56
  description: string;
60
57
 
61
- /** 触发关键词(可选,自动从工具中聚合) */
58
+ /** 触发关键词(可选) */
62
59
  keywords?: string[];
63
60
 
64
61
  /** 分类标签(可选) */
65
62
  tags?: string[];
66
63
  }
67
64
 
68
- // ============================================================================
69
- // 扩展 Plugin 接口
70
- // ============================================================================
71
-
72
- export interface SkillContextExtensions {
73
- /**
74
- * 声明本插件的 Skill 元数据
75
- * 调用后,插件的工具会自动聚合为一个 Skill 注册到 SkillFeature
76
- */
77
- declareSkill(metadata: SkillMetadata): void;
78
- }
79
-
80
65
  declare module '../plugin.js' {
81
66
  namespace Plugin {
82
- interface Extensions extends SkillContextExtensions {}
83
67
  interface Contexts {
84
68
  skill: SkillFeature;
85
69
  }
@@ -220,60 +204,4 @@ export class SkillFeature extends Feature<Skill> {
220
204
  })),
221
205
  };
222
206
  }
223
-
224
- /**
225
- * 提供给 Plugin.prototype 的扩展方法
226
- */
227
- get extensions() {
228
- const feature = this;
229
- return {
230
- declareSkill(metadata: SkillMetadata) {
231
- const plugin = getPlugin();
232
- const pluginName = plugin.name;
233
-
234
- // 收集该插件注册的工具
235
- const toolService = plugin.root.inject('tool') as { getToolsByPlugin?: (name: string) => Tool[] } | undefined;
236
- let tools: Tool[] = [];
237
-
238
- if (toolService && typeof toolService.getToolsByPlugin === 'function') {
239
- tools = toolService.getToolsByPlugin(pluginName);
240
- } else {
241
- tools = plugin.getAllTools?.() || [];
242
- }
243
-
244
- // 聚合关键词:开发者声明 + 工具自带
245
- const allKeywords = new Set<string>(metadata.keywords || []);
246
- for (const tool of tools) {
247
- if (tool.keywords) {
248
- for (const kw of tool.keywords) {
249
- allKeywords.add(kw);
250
- }
251
- }
252
- }
253
-
254
- // 聚合标签
255
- const allTags = new Set<string>(metadata.tags || []);
256
- for (const tool of tools) {
257
- if (tool.tags) {
258
- for (const tag of tool.tags) {
259
- allTags.add(tag);
260
- }
261
- }
262
- }
263
-
264
- const skill: Skill = {
265
- name: pluginName,
266
- description: metadata.description,
267
- tools,
268
- keywords: Array.from(allKeywords),
269
- tags: Array.from(allTags),
270
- pluginName,
271
- };
272
-
273
- const dispose = feature.add(skill, pluginName);
274
- plugin.recordFeatureContribution(feature.name, pluginName);
275
- plugin.onDispose(dispose);
276
- },
277
- };
278
- }
279
207
  }
package/src/index.ts CHANGED
@@ -33,6 +33,8 @@ export * from './built/skill.js'
33
33
  export * from './built/schema-feature.js'
34
34
  // Common adapter tool factories (shared across adapters)
35
35
  export * from './built/common-adapter-tools.js'
36
+ // Login assist (producer-consumer for QR / SMS / slider etc.)
37
+ export * from './built/login-assist.js'
36
38
  // AI 模块 (原 @zhin.js/ai,已合并至 core)
37
39
  export * from './ai/index.js'
38
40
 
package/src/plugin.ts CHANGED
@@ -6,19 +6,24 @@
6
6
  import { AsyncLocalStorage } from "async_hooks";
7
7
  import { EventEmitter } from "events";
8
8
  import { createRequire } from "module";
9
- import type { Database, Definition } from "@zhin.js/database";
10
- import { Schema } from "@zhin.js/schema";
11
- import type { Models, RegisteredAdapters, Tool, ToolContext } from "./types.js";
9
+ import type { Tool } from "./types.js";
12
10
  import * as fs from "fs";
13
11
  import * as path from "path";
14
12
  import { fileURLToPath, pathToFileURL } from "url";
15
13
  import logger, { Logger } from "@zhin.js/logger";
16
14
  import { compose, remove, resolveEntry } from "./utils.js";
17
- import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, SendOptions } from "./types.js";
15
+ import {
16
+ MessageMiddleware,
17
+ RegisteredAdapter,
18
+ MaybePromise,
19
+ ArrayItem,
20
+ SendOptions,
21
+ } from "./types.js";
18
22
  import type { ConfigFeature } from "./built/config.js";
19
23
  import type { PermissionFeature } from "./built/permission.js";
20
24
  import { Adapter, Adapters } from "./adapter.js";
21
25
  import { Notice } from "./notice.js";
26
+ import { Message } from "./message.js";
22
27
  import { Request } from "./request.js";
23
28
  import { Feature, FeatureJSON } from "./feature.js";
24
29
  import { createHash } from "crypto";
@@ -910,7 +915,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
910
915
  'addMiddleware', 'useContext', 'inject', 'contextIsReady',
911
916
  'start', 'stop', 'onMounted', 'onDispose',
912
917
  'dispatch', 'broadcast', 'provide', 'import', 'reload', 'watch', 'info',
913
- 'recordFeatureContribution', 'getFeatures'
918
+ 'recordFeatureContribution', 'getFeatures',
914
919
  ]);
915
920
 
916
921
  /**
@@ -1011,6 +1016,17 @@ export interface Context<T extends keyof Plugin.Contexts = keyof Plugin.Contexts
1011
1016
  // ============================================================================
1012
1017
  // 类型定义
1013
1018
  // ============================================================================
1019
+
1020
+ /** 登录辅助:bot.login.pending 事件 payload */
1021
+ export interface BotLoginPendingTask {
1022
+ id: string;
1023
+ adapter: string;
1024
+ botId: string;
1025
+ type: string;
1026
+ payload?: { message?: string; image?: string; url?: string; [key: string]: unknown };
1027
+ createdAt: number;
1028
+ }
1029
+
1014
1030
  export namespace Plugin {
1015
1031
  /**
1016
1032
  * 生命周期事件
@@ -1026,8 +1042,11 @@ export namespace Plugin {
1026
1042
  'before.sendMessage': [SendOptions];
1027
1043
  "context.mounted": [keyof Plugin.Contexts];
1028
1044
  "context.dispose": [keyof Plugin.Contexts];
1045
+ /** 登录辅助:有待办时触发 */
1046
+ 'bot.login.pending': [BotLoginPendingTask];
1029
1047
  // Notice 事件
1030
1048
  'notice.receive': [Notice];
1049
+ 'message.receive': [Message];
1031
1050
  [key: `notice.${string}`]: [Notice];
1032
1051
  // Request 事件
1033
1052
  'request.receive': [Request];
package/src/types.ts CHANGED
@@ -64,6 +64,22 @@ export type MaybeArray<T>=T|T[]
64
64
  * 消息发送内容类型
65
65
  */
66
66
  export type SendContent=MaybeArray<string|MessageElement>
67
+
68
+ /** 出站回复来源(指令 / AI),仅当经 MessageDispatcher.replyWithPolish 发出时由框架填入异步上下文 */
69
+ export type OutboundReplySource = 'command' | 'ai'
70
+
71
+ /**
72
+ * 出站润色上下文(`dispatcher.addOutboundPolish` 的 handler 签名的同构类型)。
73
+ * 与 {@link Adapter.sendMessage} → `before.sendMessage` 同一管道;需 `message`/`source` 时见 `getOutboundReplyStore`(dispatcher 导出)。
74
+ */
75
+ export interface OutboundPolishContext {
76
+ message: Message
77
+ content: SendContent
78
+ source: OutboundReplySource
79
+ }
80
+
81
+ /** 返回 `SendContent` 则替换后续 `before.sendMessage` 与发送中的 content */
82
+ export type OutboundPolishMiddleware = (ctx: OutboundPolishContext) => MaybePromise<SendContent | void>
67
83
  /**
68
84
  * 消息发送者信息
69
85
  */
package/src/utils.ts CHANGED
@@ -107,7 +107,7 @@ export namespace segment {
107
107
  const toString = (template: string | MessageElement) => {
108
108
  if (typeof template !== "string") return [template];
109
109
 
110
- const MAX_TEMPLATE_LENGTH = 100000;
110
+ const MAX_TEMPLATE_LENGTH = 400000;
111
111
  if (template.length > MAX_TEMPLATE_LENGTH) {
112
112
  throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
113
113
  }
@@ -107,9 +107,8 @@ describe('Adapter Core Functionality', () => {
107
107
  expect(adapter).toBeInstanceOf(EventEmitter)
108
108
  })
109
109
 
110
- it('should register message.receive listener', () => {
111
- const listeners = adapter.listeners('message.receive')
112
- expect(listeners.length).toBeGreaterThan(0)
110
+ it('should route message.receive via emit without default listener', () => {
111
+ expect(adapter.listenerCount('message.receive')).toBe(0)
113
112
  })
114
113
 
115
114
  it('should register call.recallMessage listener', () => {
@@ -199,13 +198,15 @@ describe('Adapter Core Functionality', () => {
199
198
 
200
199
  it('should remove all event listeners', async () => {
201
200
  await adapter.start()
201
+ const noop = () => {}
202
+ adapter.on('message.receive', noop)
202
203
  const beforeCount = adapter.listenerCount('message.receive')
203
-
204
+ expect(beforeCount).toBe(1)
205
+
204
206
  await adapter.stop()
205
207
  const afterCount = adapter.listenerCount('message.receive')
206
-
208
+
207
209
  expect(afterCount).toBe(0)
208
- expect(beforeCount).toBeGreaterThan(0)
209
210
  })
210
211
 
211
212
  it('should handle bot disconnect errors gracefully', async () => {
@@ -377,97 +378,92 @@ describe('Adapter Core Functionality', () => {
377
378
  })
378
379
 
379
380
  describe('message.receive', () => {
380
- it('should process received message through middleware when no dispatcher', async () => {
381
+ it('should still dispatch plugin message.receive when dispatcher is missing (no middleware fallback)', async () => {
381
382
  const config = [{ id: 'bot1' }]
382
383
  const adapter = new MockAdapter(plugin, 'test', config)
383
384
  await adapter.start()
384
-
385
+
385
386
  let middlewareCalled = false
386
- plugin.addMiddleware(async (message, next) => {
387
+ plugin.addMiddleware(async (_message, next) => {
387
388
  middlewareCalled = true
388
389
  await next()
389
390
  })
390
-
391
+
392
+ let lifecycleCalled = false
393
+ plugin.on('message.receive', () => {
394
+ lifecycleCalled = true
395
+ })
396
+
391
397
  const message = {
392
398
  $bot: 'bot1',
393
399
  $adapter: 'test',
394
400
  $channel: { id: 'channel-id', type: 'text' },
395
- $content: 'Hello'
401
+ $content: 'Hello',
396
402
  } as any
397
-
403
+
398
404
  adapter.emit('message.receive', message)
399
-
400
- // 等待异步处理
401
- await new Promise(resolve => setTimeout(resolve, 10))
402
- expect(middlewareCalled).toBe(true)
405
+ await new Promise((r) => setTimeout(r, 20))
406
+ expect(middlewareCalled).toBe(false)
407
+ expect(lifecycleCalled).toBe(true)
403
408
  })
404
409
 
405
- it('should use MessageDispatcher when available', async () => {
410
+ it('should await MessageDispatcher then plugin lifecycle', async () => {
406
411
  const config = [{ id: 'bot1' }]
407
412
  const adapter = new MockAdapter(plugin, 'test', config)
408
413
  await adapter.start()
409
414
 
410
- let dispatchCalled = false
411
- let middlewareCalled = false
412
-
413
- // 注册 dispatcher context
415
+ const order: string[] = []
414
416
  plugin.$contexts.set('dispatcher', {
415
417
  name: 'dispatcher',
416
418
  description: 'mock dispatcher',
417
419
  value: {
418
- dispatch: (msg: any) => { dispatchCalled = true; return Promise.resolve() },
420
+ dispatch: async (_msg: any) => {
421
+ order.push('dispatcher')
422
+ },
419
423
  },
420
424
  } as any)
421
425
 
422
- plugin.addMiddleware(async (message, next) => {
423
- middlewareCalled = true
424
- await next()
426
+ plugin.on('message.receive', () => {
427
+ order.push('lifecycle')
425
428
  })
426
429
 
427
430
  const message = {
428
431
  $bot: 'bot1',
429
432
  $adapter: 'test',
430
433
  $channel: { id: 'channel-id', type: 'text' },
431
- $content: 'Hello'
434
+ $content: 'Hello',
432
435
  } as any
433
436
 
434
437
  adapter.emit('message.receive', message)
435
-
436
- await new Promise(resolve => setTimeout(resolve, 10))
437
- expect(dispatchCalled).toBe(true)
438
- expect(middlewareCalled).toBe(false)
438
+ await new Promise((r) => setTimeout(r, 20))
439
+ expect(order).toEqual(['dispatcher', 'lifecycle'])
439
440
  })
440
441
 
441
- it('should fallback to middleware when dispatcher has no dispatch method', async () => {
442
+ it('should call adapter.on observers after plugin lifecycle', async () => {
442
443
  const config = [{ id: 'bot1' }]
443
444
  const adapter = new MockAdapter(plugin, 'test', config)
444
445
  await adapter.start()
445
446
 
446
- let middlewareCalled = false
447
-
448
- // 注册一个没有 dispatch 方法的 dispatcher
449
447
  plugin.$contexts.set('dispatcher', {
450
448
  name: 'dispatcher',
451
- description: 'broken dispatcher',
452
- value: { noDispatch: true },
449
+ description: 'mock dispatcher',
450
+ value: { dispatch: async () => {} },
453
451
  } as any)
454
452
 
455
- plugin.addMiddleware(async (message, next) => {
456
- middlewareCalled = true
457
- await next()
458
- })
453
+ const order: string[] = []
454
+ plugin.on('message.receive', () => order.push('lifecycle'))
455
+ adapter.on('message.receive', () => order.push('adapterObserver'))
459
456
 
460
457
  const message = {
461
458
  $bot: 'bot1',
462
459
  $adapter: 'test',
463
460
  $channel: { id: 'channel-id', type: 'text' },
464
- $content: 'Hello'
461
+ $content: 'Hello',
465
462
  } as any
466
463
 
467
464
  adapter.emit('message.receive', message)
468
-
469
- await new Promise(resolve => setTimeout(resolve, 10))
470
- expect(middlewareCalled).toBe(true)
465
+ await new Promise((r) => setTimeout(r, 20))
466
+ expect(order).toEqual(['lifecycle', 'adapterObserver'])
471
467
  })
472
468
  })
473
469
  })
@@ -617,90 +613,6 @@ describe('Adapter Core Functionality', () => {
617
613
  })
618
614
  })
619
615
 
620
- describe('Adapter declareSkill', () => {
621
- it('should register a skill when SkillFeature is available', () => {
622
- const plugin = new Plugin('/test/adapter-plugin.ts')
623
-
624
- // 模拟 SkillFeature
625
- const mockSkillFeature = {
626
- add: vi.fn(() => vi.fn()),
627
- }
628
-
629
- // 设置 root plugin 和 inject
630
- ;(plugin as any)._root = plugin
631
- const originalInject = plugin.inject.bind(plugin)
632
- plugin.inject = ((name: string) => {
633
- if (name === 'skill') return mockSkillFeature
634
- return originalInject(name)
635
- }) as any
636
- ;(plugin as any).recordFeatureContribution = vi.fn()
637
-
638
- const adapter = new MockAdapter(plugin, 'test-adapter')
639
-
640
- // 添加一个工具
641
- adapter.addTool({
642
- name: 'test_tool',
643
- description: '测试工具',
644
- parameters: { type: 'object', properties: {} },
645
- execute: async () => '',
646
- keywords: ['test'],
647
- tags: ['testing'],
648
- })
649
-
650
- adapter.declareSkill({
651
- description: '测试适配器的技能',
652
- keywords: ['adapter'],
653
- tags: ['adapter-tag'],
654
- })
655
-
656
- expect(mockSkillFeature.add).toHaveBeenCalledTimes(1)
657
- const [skill] = mockSkillFeature.add.mock.calls[0]
658
- expect(skill.name).toContain('test-adapter')
659
- expect(skill.description).toBe('测试适配器的技能')
660
- expect(skill.tools).toHaveLength(1)
661
- // keywords 应合并适配器和工具的关键词
662
- expect(skill.keywords).toContain('adapter')
663
- expect(skill.keywords).toContain('test')
664
- // tags 应合并
665
- expect(skill.tags).toContain('adapter-tag')
666
- expect(skill.tags).toContain('testing')
667
- })
668
-
669
- it('should clean up skill on stop', async () => {
670
- const disposeSkill = vi.fn()
671
- const plugin = new Plugin('/test/adapter-plugin.ts')
672
-
673
- const mockSkillFeature = {
674
- add: vi.fn(() => disposeSkill),
675
- }
676
- ;(plugin as any)._root = plugin
677
- plugin.inject = ((name: string) => {
678
- if (name === 'skill') return mockSkillFeature
679
- return undefined
680
- }) as any
681
- ;(plugin as any).recordFeatureContribution = vi.fn()
682
-
683
- const adapter = new MockAdapter(plugin, 'test-adapter')
684
- adapter.declareSkill({ description: '测试' })
685
-
686
- await adapter.stop()
687
-
688
- expect(disposeSkill).toHaveBeenCalledTimes(1)
689
- })
690
-
691
- it('should skip when SkillFeature is not available', () => {
692
- const plugin = new Plugin('/test/adapter-plugin.ts')
693
- ;(plugin as any)._root = plugin
694
-
695
- const adapter = new MockAdapter(plugin, 'test-adapter')
696
-
697
- // 不应抛错
698
- expect(() => {
699
- adapter.declareSkill({ description: '测试' })
700
- }).not.toThrow()
701
- })
702
- })
703
-
704
616
  describe('Adapter Registry', () => {
705
617
  it('should have a Registry Map', () => {
706
618
  expect(Adapter.Registry).toBeInstanceOf(Map)