@zhin.js/core 1.5.2 → 1.5.4

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 (97) hide show
  1. package/README.md +2 -2
  2. package/lib/adapter.d.ts +7 -1
  3. package/lib/adapter.js +28 -24
  4. package/lib/agent-prompt.d.ts +2 -3
  5. package/lib/agent-prompt.js +5 -0
  6. package/lib/built/ai-outbound/parse.d.ts +1 -1
  7. package/lib/built/ai-outbound/parse.js +2 -6
  8. package/lib/built/ai-outbound/plain-mention-rewrite.js +4 -4
  9. package/lib/built/ai-outbound/resolve.js +8 -8
  10. package/lib/built/ai-outbound/types.d.ts +3 -3
  11. package/lib/built/ai-trigger.d.ts +5 -0
  12. package/lib/built/ai-trigger.js +40 -13
  13. package/lib/built/authorization.js +4 -4
  14. package/lib/built/command.d.ts +2 -2
  15. package/lib/built/command.js +2 -2
  16. package/lib/built/common-adapter-tools.d.ts +11 -11
  17. package/lib/built/common-adapter-tools.js +13 -13
  18. package/lib/built/config.d.ts +0 -4
  19. package/lib/built/config.js +0 -4
  20. package/lib/built/dispatcher.d.ts +2 -0
  21. package/lib/built/dispatcher.js +1 -1
  22. package/lib/built/endpoint-commands.js +2 -2
  23. package/lib/built/endpoint-lifecycle-service.js +13 -13
  24. package/lib/built/endpoint-lifecycle.d.ts +3 -3
  25. package/lib/built/endpoint-lifecycle.js +2 -2
  26. package/lib/built/endpoint-manager.d.ts +1 -1
  27. package/lib/built/interactive-segments/index.d.ts +1 -1
  28. package/lib/built/interactive-segments/index.js +1 -1
  29. package/lib/built/interactive-segments/keyboard-segment.d.ts +0 -1
  30. package/lib/built/interactive-segments/keyboard-segment.js +0 -1
  31. package/lib/built/interactive-segments/onebot-keyboard.d.ts +0 -3
  32. package/lib/built/interactive-segments/onebot-keyboard.js +0 -2
  33. package/lib/built/interactive-segments/resolve.d.ts +0 -3
  34. package/lib/built/interactive-segments/resolve.js +0 -3
  35. package/lib/built/interactive-segments/types.d.ts +8 -20
  36. package/lib/built/interactive-segments/types.js +0 -4
  37. package/lib/built/login-assist.d.ts +2 -2
  38. package/lib/built/login-assist.js +2 -2
  39. package/lib/built/message-enrich.d.ts +1 -1
  40. package/lib/built/message-enrich.js +2 -7
  41. package/lib/built/permission.d.ts +0 -4
  42. package/lib/built/permission.js +0 -4
  43. package/lib/built/prepend-quote-context.js +5 -5
  44. package/lib/built/rich-segments/capabilities.js +0 -1
  45. package/lib/built/rich-segments/html-segment.js +1 -4
  46. package/lib/built/rich-segments/markdown-segment.js +1 -4
  47. package/lib/built/rich-segments/types.d.ts +0 -5
  48. package/lib/built/schema-endpoint-manager.js +6 -6
  49. package/lib/built/tool.d.ts +5 -3
  50. package/lib/built/tool.js +14 -23
  51. package/lib/command.d.ts +4 -4
  52. package/lib/command.js +8 -11
  53. package/lib/endpoint-capabilities.d.ts +2 -3
  54. package/lib/endpoint-capabilities.js +4 -4
  55. package/lib/endpoint.d.ts +2 -0
  56. package/lib/endpoint.js +1 -0
  57. package/lib/errors.d.ts +2 -2
  58. package/lib/errors.js +4 -4
  59. package/lib/host-plugin-registry.d.ts +6 -5
  60. package/lib/host-plugin-registry.js +9 -4
  61. package/lib/im-scene.d.ts +1 -1
  62. package/lib/im-scene.js +5 -5
  63. package/lib/im-session-id.d.ts +2 -2
  64. package/lib/im-session-id.js +4 -4
  65. package/lib/index.d.ts +1 -2
  66. package/lib/index.js +0 -2
  67. package/lib/jsx-runtime.d.ts +1 -0
  68. package/lib/jsx.d.ts +19 -21
  69. package/lib/message.d.ts +1 -1
  70. package/lib/message.js +1 -1
  71. package/lib/notice.d.ts +2 -2
  72. package/lib/plugin-context.d.ts +11 -13
  73. package/lib/plugin-context.js +21 -18
  74. package/lib/plugin-runtime/im/contracts.d.ts +91 -12
  75. package/lib/plugin-runtime/im/contracts.js +79 -1
  76. package/lib/plugin-runtime/im/im-runtime.d.ts +33 -23
  77. package/lib/plugin-runtime/im/im-runtime.js +104 -63
  78. package/lib/plugin-runtime/im/index.d.ts +2 -0
  79. package/lib/plugin-runtime/im/index.js +2 -0
  80. package/lib/plugin-runtime/im/message-bus.d.ts +38 -0
  81. package/lib/plugin-runtime/im/message-bus.js +40 -0
  82. package/lib/plugin-runtime/im/message-dispatcher.js +13 -73
  83. package/lib/plugin-runtime/im/outbound-renderer.d.ts +3 -2
  84. package/lib/plugin-runtime/im/outbound-renderer.js +44 -8
  85. package/lib/plugin-runtime/im/service-tokens.d.ts +18 -0
  86. package/lib/plugin-runtime/im/service-tokens.js +13 -0
  87. package/lib/plugin.d.ts +4 -7
  88. package/lib/plugin.js +7 -30
  89. package/lib/request.d.ts +2 -2
  90. package/lib/side-event/types.d.ts +0 -4
  91. package/lib/types.d.ts +6 -11
  92. package/lib/utils.js +3 -1
  93. package/package.json +13 -12
  94. package/lib/built/qrcode-segment.d.ts +0 -18
  95. package/lib/built/qrcode-segment.js +0 -78
  96. package/lib/types-generator.d.ts +0 -5
  97. package/lib/types-generator.js +0 -71
@@ -40,6 +40,9 @@ export class Message {
40
40
  metadata;
41
41
  segments;
42
42
  message;
43
+ endpointId;
44
+ mentioned;
45
+ replyTo;
43
46
  constructor(conversation, content, generation, reply, sender, metadata = Object.freeze({}),
44
47
  /**
45
48
  * 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
@@ -47,7 +50,7 @@ export class Message {
47
50
  */
48
51
  segments,
49
52
  /** 结构化入站消息身份(平台原生 message id 经 MessageRef 传递)。 */
50
- message) {
53
+ message, endpointId, mentioned, replyTo) {
51
54
  this.conversation = conversation;
52
55
  this.content = content;
53
56
  this.generation = generation;
@@ -55,8 +58,42 @@ export class Message {
55
58
  this.metadata = metadata;
56
59
  this.segments = segments;
57
60
  this.message = message;
61
+ this.endpointId = endpointId;
62
+ this.mentioned = mentioned;
63
+ this.replyTo = replyTo;
58
64
  this.$reply = (content) => reply(content);
59
65
  this.$replyFrom = (requester, content) => reply(content, requester);
66
+ this.$sendTo = (target, content) => reply(content, undefined, target);
67
+ this.$replyToPrivate = (content, from) => {
68
+ if (!sender)
69
+ throw new Error('Cannot $replyToPrivate: message has no sender');
70
+ let parent;
71
+ if (from === true) {
72
+ if (conversation.kind !== 'group' && conversation.kind !== 'channel') {
73
+ throw new Error('Cannot $replyToPrivate with from=true: current conversation must be a group or channel');
74
+ }
75
+ parent = { kind: conversation.kind, id: conversation.id };
76
+ }
77
+ else if (from != null && typeof from === 'object') {
78
+ parent = from;
79
+ }
80
+ return reply(content, undefined, {
81
+ kind: 'private',
82
+ id: sender.id,
83
+ ...(parent ? { parent } : {}),
84
+ });
85
+ };
86
+ this.$replyToGroup = (groupId, content) => {
87
+ return reply(content, undefined, { kind: 'group', id: groupId });
88
+ };
89
+ this.$replyToChannel = (channelId, guildId, content, threadId) => {
90
+ return reply(content, undefined, {
91
+ kind: 'channel',
92
+ id: channelId,
93
+ parent: { kind: 'channel', id: guildId },
94
+ ...(threadId ? { threadId } : {}),
95
+ });
96
+ };
60
97
  Object.freeze(this);
61
98
  }
62
99
  /** 平台原生消息 id(`message` 未提供时为 undefined)。 */
@@ -65,6 +102,47 @@ export class Message {
65
102
  }
66
103
  $reply;
67
104
  $replyFrom;
105
+ /**
106
+ * 向同 Endpoint 的另一个通道发送消息(通用)。
107
+ *
108
+ * ```ts
109
+ * await message.$sendTo({ kind: 'channel', id: 'ch-1', parent: { kind: 'channel', id: 'guild-1' } }, '频道通知');
110
+ * ```
111
+ */
112
+ $sendTo;
113
+ /**
114
+ * 私信当前消息的发送者(同 Endpoint)。
115
+ *
116
+ * @param content 消息内容
117
+ * @param from 会话上下文:
118
+ * - `true` — 用当前群/频道作为 parent(要求当前不在私聊中)
119
+ * - `{ kind, id }` — 显式指定 parent(群或子频道)
120
+ * - 省略 — 直接私信
121
+ *
122
+ * ```ts
123
+ * await message.$replyToPrivate('直接私信');
124
+ * await message.$replyToPrivate('群临时私信', true);
125
+ * await message.$replyToPrivate('频道私信', { kind: 'channel', id: '子频道ID' });
126
+ * ```
127
+ */
128
+ $replyToPrivate;
129
+ /**
130
+ * 向指定群发送消息(同 Endpoint)。
131
+ *
132
+ * ```ts
133
+ * await message.$replyToGroup('67890', '群通知');
134
+ * ```
135
+ */
136
+ $replyToGroup;
137
+ /**
138
+ * 向指定频道/子频道发送消息(同 Endpoint)。
139
+ *
140
+ * ```ts
141
+ * await message.$replyToChannel('channel-1', 'guild-1', '频道通知');
142
+ * await message.$replyToChannel('channel-1', 'guild-1', '话题回复', 'thread-1');
143
+ * ```
144
+ */
145
+ $replyToChannel;
68
146
  }
69
147
  export function createOutboundEnvelope(request, initialPayload) {
70
148
  let payload = initialPayload;
@@ -1,16 +1,22 @@
1
- import { Scope, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
1
+ import { Scope, type PluginId, type SnapshotLease, type SnapshotStore } from '@zhin.js/plugin-runtime';
2
+ import { MessageBus } from './message-bus.js';
2
3
  import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
3
4
  import { type ConversationRef, type DeliveryReceipt, type MessageRef } from '@zhin.js/im-contract';
4
- import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type SendRequest } from './contracts.js';
5
+ import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type MessageSenderRef, type SendRequest } from './contracts.js';
5
6
  import { OutboundRenderer } from './outbound-renderer.js';
6
7
  import { type RuntimeInteractiveHandler } from './interactive.js';
7
8
  export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<MessageGateway>;
9
+ /** Generation-owned terminal route after interactive/command routing misses. */
10
+ export interface IngressRoute {
11
+ route(message: Message, lease: SnapshotLease, requester: PluginId): Promise<boolean>;
12
+ }
13
+ export declare const ingressRouteToken: import("@zhin.js/plugin-runtime").Token<IngressRoute>;
8
14
  /** Console 实时消息事件(SSE 推送源;content 仅为截断预览,不含完整原始段)。 */
9
15
  export interface RuntimeMessageEvent {
10
16
  readonly direction: 'inbound' | 'outbound';
11
17
  readonly conversation: ConversationRef;
12
- /** inbound:发送者 id。 */
13
- readonly sender?: string;
18
+ /** inbound:发送者。 */
19
+ readonly sender?: MessageSenderRef;
14
20
  /** outbound:发起方插件。 */
15
21
  readonly requester?: PluginId;
16
22
  /** 预览文本,截断至 200 字。 */
@@ -26,17 +32,15 @@ export interface ImRuntimeOptions {
26
32
  */
27
33
  readonly commandPrefix?: string;
28
34
  readonly renderer?: OutboundRenderer;
35
+ /** Process-root ingress claim (pending interaction, authentication challenge, etc.). */
36
+ readonly inboundClaim?: (message: Message) => boolean | Promise<boolean>;
29
37
  }
30
38
  export declare class ImRuntime implements MessageGateway {
31
39
  #private;
32
40
  constructor(options?: ImRuntimeOptions);
33
41
  attach(snapshots: SnapshotStore): void;
34
- /**
35
- * Optional Host AI / fallback path after Command miss (or non-prefixed text).
36
- * Return true when the message was handled (reply already sent).
37
- * `requester` is the Adapter Endpoint owner (for CapabilityIngress inheritance).
38
- */
39
- setUnmatchedHandler(handler: (message: Message, snapshot: RuntimeSnapshot, requester: PluginId) => Promise<boolean>): void;
42
+ readonly permissionHost: import("@zhin.js/permission").PermissionHost;
43
+ readonly messageBus: MessageBus;
40
44
  install(resources: Scope): void;
41
45
  /**
42
46
  * 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
@@ -60,7 +64,18 @@ export declare class ImRuntime implements MessageGateway {
60
64
  readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
61
65
  readonly managementCapabilities: readonly EndpointManagementCapability[];
62
66
  }[];
63
- getEndpoint(adapter: string, endpointId: string): {
67
+ /**
68
+ * Console `GET /api/stats` 同源计数:非 root 插件节点 + AdapterIndex endpoints。
69
+ * 供命令 / 状态卡等在 Plugin Runtime 下读取(legacy `root.adapters` / `root.children` 已不存在)。
70
+ */
71
+ inventory(): Readonly<{
72
+ plugins: number;
73
+ endpoints: {
74
+ readonly total: number;
75
+ readonly online: number;
76
+ };
77
+ }>;
78
+ getEndpoint(adapter: string, endpointKey: string): {
64
79
  readonly name: string;
65
80
  readonly adapter: string;
66
81
  readonly connected: boolean;
@@ -70,7 +85,7 @@ export declare class ImRuntime implements MessageGateway {
70
85
  } | null;
71
86
  sendEndpointMessage(input: {
72
87
  readonly adapter: string;
73
- readonly endpointId: string;
88
+ readonly endpointKey: string;
74
89
  readonly conversation: ConversationAddress;
75
90
  readonly content: unknown;
76
91
  }): Promise<{
@@ -79,7 +94,7 @@ export declare class ImRuntime implements MessageGateway {
79
94
  /** Activity-feedback: add a message reaction when the live Endpoint supports it. */
80
95
  addEndpointReaction(input: {
81
96
  readonly adapter: string;
82
- readonly endpointId: string;
97
+ readonly endpointKey: string;
83
98
  readonly message?: MessageRef;
84
99
  readonly messageId?: string;
85
100
  readonly emoji: string;
@@ -88,7 +103,7 @@ export declare class ImRuntime implements MessageGateway {
88
103
  }): Promise<string | null>;
89
104
  removeEndpointReaction(input: {
90
105
  readonly adapter: string;
91
- readonly endpointId: string;
106
+ readonly endpointKey: string;
92
107
  readonly message?: MessageRef;
93
108
  readonly messageId?: string;
94
109
  readonly reactionId: string;
@@ -96,32 +111,27 @@ export declare class ImRuntime implements MessageGateway {
96
111
  /** Activity-feedback autoRemove: recall a previously sent status message. */
97
112
  recallEndpointMessage(input: {
98
113
  readonly adapter: string;
99
- readonly endpointId: string;
114
+ readonly endpointKey: string;
100
115
  readonly message?: MessageRef;
101
116
  readonly messageId?: string;
102
117
  }): Promise<void>;
103
118
  editEndpointMessage(input: {
104
119
  readonly adapter: string;
105
- readonly endpointId: string;
120
+ readonly endpointKey: string;
106
121
  readonly message?: MessageRef;
107
122
  readonly messageId?: string;
108
123
  readonly content: unknown;
109
124
  }): Promise<string | null>;
110
125
  setEndpointTyping(input: {
111
126
  readonly adapter: string;
112
- readonly endpointId: string;
127
+ readonly endpointKey: string;
113
128
  readonly conversation: ConversationRef;
114
129
  readonly active?: boolean;
115
130
  }): Promise<void>;
116
- /**
117
- * Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
118
- * @deprecated Host callers should use `getEndpointManagement()`.
119
- */
120
- getLiveEndpoint(adapter: string, endpointId: string): unknown | null;
121
131
  /**
122
132
  * Narrow Host seam for Console social/group management. An empty object means
123
133
  * the Endpoint exists but implements no management operations; null means it
124
134
  * cannot be resolved.
125
135
  */
126
- getEndpointManagement(adapter: string, endpointId: string): EndpointManagement | null;
136
+ getEndpointManagement(adapter: string, endpointKey: string): EndpointManagement | null;
127
137
  }
@@ -1,4 +1,6 @@
1
1
  import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
2
+ import { createPermissionHost, permissionHostToken } from '@zhin.js/permission';
3
+ import { MessageBus, messageBusToken } from './message-bus.js';
2
4
  import { adapterFeatureId, isAdapterIndex, resolveEndpointControl, resolveEndpointManagement, } from '@zhin.js/adapter';
3
5
  import { formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
4
6
  import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
@@ -12,6 +14,7 @@ import { keyboardFallbackStore } from '../../built/interactive-segments/fallback
12
14
  import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveConversationKey, } from './interactive.js';
13
15
  const logger = getLogger('im');
14
16
  export const messageGatewayToken = createToken('zhin.im.message-gateway');
17
+ export const ingressRouteToken = createToken('zhin.im.ingress-route');
15
18
  export const messagePreviewLimit = 200;
16
19
  export class ImRuntime {
17
20
  #dispatcher;
@@ -19,12 +22,13 @@ export class ImRuntime {
19
22
  #messageListeners = new Set();
20
23
  #interactiveHandlers = [];
21
24
  #snapshots;
22
- #unmatchedHandler;
25
+ #inboundClaim;
23
26
  constructor(options = {}) {
24
27
  this.#dispatcher = new MessageDispatcher(options.commandPrefix === undefined
25
28
  ? defaultCommandPrefixResolver
26
29
  : () => options.commandPrefix ?? '');
27
30
  this.#renderer = options.renderer ?? new OutboundRenderer();
31
+ this.#inboundClaim = options.inboundClaim;
28
32
  }
29
33
  attach(snapshots) {
30
34
  if (this.#snapshots && this.#snapshots !== snapshots) {
@@ -32,16 +36,12 @@ export class ImRuntime {
32
36
  }
33
37
  this.#snapshots = snapshots;
34
38
  }
35
- /**
36
- * Optional Host AI / fallback path after Command miss (or non-prefixed text).
37
- * Return true when the message was handled (reply already sent).
38
- * `requester` is the Adapter Endpoint owner (for CapabilityIngress inheritance).
39
- */
40
- setUnmatchedHandler(handler) {
41
- this.#unmatchedHandler = handler;
42
- }
39
+ permissionHost = createPermissionHost();
40
+ messageBus = new MessageBus();
43
41
  install(resources) {
44
42
  resources.provide(messageGatewayToken, this);
43
+ resources.provide(permissionHostToken, this.permissionHost);
44
+ resources.provide(messageBusToken, this.messageBus);
45
45
  }
46
46
  /**
47
47
  * 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
@@ -82,48 +82,58 @@ export class ImRuntime {
82
82
  const adapter = conversation.endpoint.id;
83
83
  const requester = requireAdapters(lease.value).owner(adapter);
84
84
  logger.debug(formatCompact({
85
- op: 'im_inbound_receive',
86
- adapter: String(adapter).split('\0').pop() ?? String(adapter),
87
- conversation: formatConversationLog(conversation),
88
- sender: input.sender,
89
- id: input.message?.id,
85
+ op: 'receive',
86
+ conv: formatConversationLog(conversation),
87
+ sender: `${input.sender?.name || 'undefined'}(${input.sender?.id || 'undefined'})`,
90
88
  preview: truncatePreview(input.content),
91
- segments: input.segments?.length,
92
- generation: lease.value.generation,
93
89
  }));
94
- const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester) => {
90
+ const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester, targetConversation) => {
95
91
  if (!active)
96
92
  throw new Error('Message reply scope has ended');
93
+ const effectiveConversation = targetConversation
94
+ ? { endpoint: conversation.endpoint, ...targetConversation }
95
+ : conversation;
97
96
  return this.#sendWithSnapshot({
98
- conversation,
97
+ conversation: effectiveConversation,
99
98
  requester: replyRequester,
100
99
  content,
100
+ incoming: {
101
+ sender: input.sender,
102
+ content: input.content,
103
+ segments: input.segments,
104
+ messageId: input.message?.id,
105
+ timestamp: Date.now(),
106
+ endpointId: input.endpointId,
107
+ mentioned: input.mentioned,
108
+ },
101
109
  }, lease.value);
102
- }, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message);
110
+ }, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message, input.endpointId, input.mentioned, input.replyTo);
103
111
  let result = Object.freeze({ matched: false });
104
- await runMiddleware(lease.value, message, async () => {
105
- result = await this.#dispatchInteractive(message, requester)
106
- ?? await this.#dispatcher.dispatch(message, lease.value);
107
- if (!result.matched && this.#unmatchedHandler) {
108
- logger.debug(formatCompact({
109
- op: 'im_inbound_unmatched_handler',
110
- conversation: formatConversationLog(conversation),
111
- id: input.message?.id,
112
- }));
113
- const handled = await this.#unmatchedHandler(message, lease.value, requester);
114
- if (handled) {
115
- result = Object.freeze({ matched: true, command: 'ai', owner: requester });
112
+ const claimed = await this.#inboundClaim?.(message) === true;
113
+ if (claimed) {
114
+ result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
115
+ }
116
+ else {
117
+ await runMiddleware(lease.value, message, async () => {
118
+ result = await this.#dispatchInteractive(message, requester)
119
+ ?? await this.#dispatcher.dispatch(message, lease.value);
120
+ const ingressRoute = resolveIngressRoute(lease.value);
121
+ if (!result.matched && ingressRoute) {
122
+ logger.debug(formatCompact({ op: 'unmatched', conv: formatConversationLog(conversation) }));
123
+ const handled = await ingressRoute.route(message, lease, requester);
124
+ if (handled) {
125
+ result = Object.freeze({ matched: true, command: 'ai', owner: requester });
126
+ }
116
127
  }
117
- }
118
- }, 'inbound');
119
- logger.debug(formatCompact({
120
- op: 'im_inbound_done',
121
- conversation: formatConversationLog(conversation),
122
- id: input.message?.id,
123
- matched: result.matched,
124
- command: result.command,
125
- owner: result.owner,
126
- }));
128
+ }, 'inbound');
129
+ }
130
+ if (result.matched) {
131
+ logger.debug(formatCompact({
132
+ op: 'dispatched',
133
+ conv: formatConversationLog(conversation),
134
+ command: result.command,
135
+ }));
136
+ }
127
137
  this.#emitMessage({
128
138
  direction: 'inbound',
129
139
  conversation,
@@ -172,12 +182,42 @@ export class ImRuntime {
172
182
  return Object.freeze([]);
173
183
  }
174
184
  }
175
- getEndpoint(adapter, endpointId) {
185
+ /**
186
+ * Console `GET /api/stats` 同源计数:非 root 插件节点 + AdapterIndex endpoints。
187
+ * 供命令 / 状态卡等在 Plugin Runtime 下读取(legacy `root.adapters` / `root.children` 已不存在)。
188
+ */
189
+ inventory() {
190
+ try {
191
+ const lease = this.#acquire();
192
+ try {
193
+ const plugins = [...lease.value.tree.values()]
194
+ .filter((node) => node.parent !== undefined).length;
195
+ const endpoints = requireAdapters(lease.value).describe();
196
+ return Object.freeze({
197
+ plugins,
198
+ endpoints: Object.freeze({
199
+ total: endpoints.length,
200
+ online: endpoints.filter((endpoint) => endpoint.status === 'online').length,
201
+ }),
202
+ });
203
+ }
204
+ finally {
205
+ lease.release();
206
+ }
207
+ }
208
+ catch {
209
+ return Object.freeze({
210
+ plugins: 0,
211
+ endpoints: Object.freeze({ total: 0, online: 0 }),
212
+ });
213
+ }
214
+ }
215
+ getEndpoint(adapter, endpointKey) {
176
216
  try {
177
217
  const lease = this.#acquire();
178
218
  try {
179
219
  const index = requireAdapters(lease.value);
180
- const id = index.resolve(adapter, endpointId);
220
+ const id = index.resolve(adapter, endpointKey);
181
221
  if (!id)
182
222
  return null;
183
223
  const row = index.describe().find((item) => item.id === id);
@@ -206,7 +246,7 @@ export class ImRuntime {
206
246
  const lease = this.#acquire();
207
247
  try {
208
248
  const index = requireAdapters(lease.value);
209
- const resolved = index.resolve(input.adapter, input.endpointId);
249
+ const resolved = index.resolve(input.adapter, input.endpointKey);
210
250
  if (!resolved)
211
251
  throw new Error('endpoint not found');
212
252
  const requester = index.owner(resolved);
@@ -228,34 +268,34 @@ export class ImRuntime {
228
268
  }
229
269
  /** Activity-feedback: add a message reaction when the live Endpoint supports it. */
230
270
  async addEndpointReaction(input) {
231
- const control = this.#liveEndpointControl(input.adapter, input.endpointId);
271
+ const control = this.#liveEndpointControl(input.adapter, input.endpointKey);
232
272
  return control?.addReaction?.(legacyMessageTarget(input.message, input.messageId), input.emoji, {
233
273
  sceneType: input.sceneType,
234
274
  channelId: input.channelId,
235
275
  }) ?? null;
236
276
  }
237
277
  async removeEndpointReaction(input) {
238
- await this.#liveEndpointControl(input.adapter, input.endpointId)
278
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
239
279
  ?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
240
280
  }
241
281
  /** Activity-feedback autoRemove: recall a previously sent status message. */
242
282
  async recallEndpointMessage(input) {
243
- await this.#liveEndpointControl(input.adapter, input.endpointId)
283
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
244
284
  ?.recall?.(legacyMessageTarget(input.message, input.messageId));
245
285
  }
246
286
  async editEndpointMessage(input) {
247
- return this.#liveEndpointControl(input.adapter, input.endpointId)
287
+ return this.#liveEndpointControl(input.adapter, input.endpointKey)
248
288
  ?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
249
289
  }
250
290
  async setEndpointTyping(input) {
251
- await this.#liveEndpointControl(input.adapter, input.endpointId)
291
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
252
292
  ?.typing?.(input.conversation, input.active);
253
293
  }
254
- #liveEndpoint(adapter, endpointId) {
294
+ #liveEndpoint(adapter, endpointKey) {
255
295
  try {
256
296
  const lease = this.#acquire();
257
297
  try {
258
- const endpoint = requireAdapters(lease.value).instance(adapter, endpointId);
298
+ const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
259
299
  return endpoint ?? null;
260
300
  }
261
301
  finally {
@@ -266,24 +306,17 @@ export class ImRuntime {
266
306
  return null;
267
307
  }
268
308
  }
269
- #liveEndpointControl(adapter, endpointId) {
270
- const endpoint = this.#liveEndpoint(adapter, endpointId);
309
+ #liveEndpointControl(adapter, endpointKey) {
310
+ const endpoint = this.#liveEndpoint(adapter, endpointKey);
271
311
  return resolveEndpointControl(endpoint) ?? null;
272
312
  }
273
- /**
274
- * Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
275
- * @deprecated Host callers should use `getEndpointManagement()`.
276
- */
277
- getLiveEndpoint(adapter, endpointId) {
278
- return this.#liveEndpoint(adapter, endpointId);
279
- }
280
313
  /**
281
314
  * Narrow Host seam for Console social/group management. An empty object means
282
315
  * the Endpoint exists but implements no management operations; null means it
283
316
  * cannot be resolved.
284
317
  */
285
- getEndpointManagement(adapter, endpointId) {
286
- const endpoint = this.#liveEndpoint(adapter, endpointId);
318
+ getEndpointManagement(adapter, endpointKey) {
319
+ const endpoint = this.#liveEndpoint(adapter, endpointKey);
287
320
  if (!endpoint)
288
321
  return null;
289
322
  return resolveEndpointManagement(endpoint) ?? Object.freeze({});
@@ -292,7 +325,7 @@ export class ImRuntime {
292
325
  const adapter = request.conversation.endpoint.id;
293
326
  let initialPayload;
294
327
  try {
295
- const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
328
+ const rendered = await this.#renderer.render(request.content, request.requester, snapshot, request.conversation, request.incoming);
296
329
  initialPayload = await prepareOutboundPayload(rendered, request.conversation, snapshot);
297
330
  }
298
331
  catch {
@@ -371,6 +404,14 @@ export class ImRuntime {
371
404
  : undefined;
372
405
  }
373
406
  }
407
+ function resolveIngressRoute(snapshot) {
408
+ const candidate = snapshot.resources.get(snapshot.root)?.get(ingressRouteToken.id);
409
+ return candidate
410
+ && typeof candidate === 'object'
411
+ && typeof candidate.route === 'function'
412
+ ? candidate
413
+ : undefined;
414
+ }
374
415
  function requireAdapters(snapshot) {
375
416
  const projection = snapshot.projections.get(adapterFeatureId);
376
417
  if (!isAdapterIndex(projection)) {
@@ -1,6 +1,8 @@
1
1
  export * from './contracts.js';
2
2
  export * from './im-runtime.js';
3
3
  export * from './interactive.js';
4
+ export * from './message-bus.js';
4
5
  export * from './message-dispatcher.js';
5
6
  export * from './outbound-renderer.js';
6
7
  export * from './outbound-segments.js';
8
+ export * from './service-tokens.js';
@@ -1,6 +1,8 @@
1
1
  export * from './contracts.js';
2
2
  export * from './im-runtime.js';
3
3
  export * from './interactive.js';
4
+ export * from './message-bus.js';
4
5
  export * from './message-dispatcher.js';
5
6
  export * from './outbound-renderer.js';
6
7
  export * from './outbound-segments.js';
8
+ export * from './service-tokens.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * MessageBus — 独立于 Plugin 的 IM 生命周期事件总线。
3
+ *
4
+ * 覆盖 Plugin.Lifecycle 中关键 IM 事件,供 Scope+Token 路径消费。
5
+ * 遗留 Plugin.emit 通过短暂双写桥接(迁移窗口)。
6
+ */
7
+ import type { DeliveryReceipt } from '@zhin.js/im-contract';
8
+ import type { Message, OutboundEnvelope, SendContent } from './contracts.js';
9
+ export interface EndpointEvent {
10
+ readonly adapter: string;
11
+ readonly endpointKey: string;
12
+ readonly error?: Error;
13
+ }
14
+ export interface MessageBusEventMap {
15
+ 'message.receive': [message: Message];
16
+ 'before.sendMessage': [envelope: OutboundEnvelope];
17
+ 'message.send': [payload: {
18
+ adapter: string;
19
+ endpointKey: string;
20
+ content: SendContent;
21
+ receipt: DeliveryReceipt;
22
+ }];
23
+ 'endpoint.connect': [event: EndpointEvent];
24
+ 'endpoint.disconnect': [event: EndpointEvent];
25
+ 'endpoint.error': [event: EndpointEvent];
26
+ }
27
+ export type MessageBusEventName = keyof MessageBusEventMap;
28
+ export declare class MessageBus {
29
+ private readonly emitter;
30
+ constructor();
31
+ on<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): () => void;
32
+ once<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): () => void;
33
+ emit<E extends MessageBusEventName>(event: E, ...args: MessageBusEventMap[E]): void;
34
+ off<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): void;
35
+ listenerCount(event: MessageBusEventName): number;
36
+ removeAllListeners(event?: MessageBusEventName): void;
37
+ }
38
+ export declare const messageBusToken: import("@zhin.js/plugin-runtime").Token<MessageBus>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MessageBus — 独立于 Plugin 的 IM 生命周期事件总线。
3
+ *
4
+ * 覆盖 Plugin.Lifecycle 中关键 IM 事件,供 Scope+Token 路径消费。
5
+ * 遗留 Plugin.emit 通过短暂双写桥接(迁移窗口)。
6
+ */
7
+ import { EventEmitter } from 'node:events';
8
+ import { createToken } from '@zhin.js/plugin-runtime';
9
+ export class MessageBus {
10
+ emitter = new EventEmitter();
11
+ constructor() {
12
+ this.emitter.setMaxListeners(50);
13
+ }
14
+ on(event, listener) {
15
+ this.emitter.on(event, listener);
16
+ return () => this.emitter.off(event, listener);
17
+ }
18
+ once(event, listener) {
19
+ this.emitter.once(event, listener);
20
+ return () => this.emitter.off(event, listener);
21
+ }
22
+ emit(event, ...args) {
23
+ this.emitter.emit(event, ...args);
24
+ }
25
+ off(event, listener) {
26
+ this.emitter.off(event, listener);
27
+ }
28
+ listenerCount(event) {
29
+ return this.emitter.listenerCount(event);
30
+ }
31
+ removeAllListeners(event) {
32
+ if (event) {
33
+ this.emitter.removeAllListeners(event);
34
+ }
35
+ else {
36
+ this.emitter.removeAllListeners();
37
+ }
38
+ }
39
+ }
40
+ export const messageBusToken = createToken('zhin.im.message-bus');