@zhin.js/core 1.5.1 → 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 (99) hide show
  1. package/README.md +6 -3
  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 +104 -60
  75. package/lib/plugin-runtime/im/contracts.js +87 -17
  76. package/lib/plugin-runtime/im/im-runtime.d.ts +38 -35
  77. package/lib/plugin-runtime/im/im-runtime.js +140 -131
  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/interactive.d.ts +3 -2
  81. package/lib/plugin-runtime/im/interactive.js +9 -4
  82. package/lib/plugin-runtime/im/message-bus.d.ts +38 -0
  83. package/lib/plugin-runtime/im/message-bus.js +40 -0
  84. package/lib/plugin-runtime/im/message-dispatcher.js +14 -74
  85. package/lib/plugin-runtime/im/outbound-renderer.d.ts +3 -2
  86. package/lib/plugin-runtime/im/outbound-renderer.js +44 -8
  87. package/lib/plugin-runtime/im/service-tokens.d.ts +18 -0
  88. package/lib/plugin-runtime/im/service-tokens.js +13 -0
  89. package/lib/plugin.d.ts +4 -7
  90. package/lib/plugin.js +7 -30
  91. package/lib/request.d.ts +2 -2
  92. package/lib/side-event/types.d.ts +0 -4
  93. package/lib/types.d.ts +6 -11
  94. package/lib/utils.js +3 -1
  95. package/package.json +13 -12
  96. package/lib/built/qrcode-segment.d.ts +0 -18
  97. package/lib/built/qrcode-segment.js +0 -78
  98. package/lib/types-generator.d.ts +0 -5
  99. package/lib/types-generator.js +0 -71
@@ -1,6 +1,8 @@
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
- import { formatLegacyConversationRef, formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
5
+ import { formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
4
6
  import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
5
7
  import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
6
8
  import { Message, createOutboundEnvelope, } from './contracts.js';
@@ -9,9 +11,10 @@ import { OutboundRenderer } from './outbound-renderer.js';
9
11
  import { applyOutboundInteractivePolicy, normalizeOutboundPayload, resolveOutboundInteractivePolicy, resolveOutboundMediaPolicy, } from './outbound-segments.js';
10
12
  import { assertCanonicalSegments } from '../../built/segment-contract/assert.js';
11
13
  import { keyboardFallbackStore } from '../../built/interactive-segments/fallback-store.js';
12
- import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveChannelKey, } from './interactive.js';
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 最长匹配;返回注销函数)。
@@ -78,64 +78,68 @@ export class ImRuntime {
78
78
  const lease = this.#acquire();
79
79
  let active = true;
80
80
  try {
81
- const requester = requireAdapters(lease.value).owner(input.adapter);
81
+ const conversation = input.conversation;
82
+ const adapter = conversation.endpoint.id;
83
+ const requester = requireAdapters(lease.value).owner(adapter);
82
84
  logger.debug(formatCompact({
83
- op: 'im_inbound_receive',
84
- adapter: String(input.adapter).split('\0').pop() ?? String(input.adapter),
85
- target: input.target,
86
- sender: input.sender,
87
- id: input.id,
85
+ op: 'receive',
86
+ conv: formatConversationLog(conversation),
87
+ sender: `${input.sender?.name || 'undefined'}(${input.sender?.id || 'undefined'})`,
88
88
  preview: truncatePreview(input.content),
89
- segments: input.segments?.length,
90
- generation: lease.value.generation,
91
89
  }));
92
- const message = new Message(input.adapter, input.target, input.content, lease.value.generation, (content, replyRequester = requester) => {
90
+ const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester, targetConversation) => {
93
91
  if (!active)
94
92
  throw new Error('Message reply scope has ended');
93
+ const effectiveConversation = targetConversation
94
+ ? { endpoint: conversation.endpoint, ...targetConversation }
95
+ : conversation;
95
96
  return this.#sendWithSnapshot({
96
- adapter: input.adapter,
97
- ...(input.conversation ? { conversation: input.conversation } : {}),
98
- target: input.target,
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.id ?? input.message?.id, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.conversation, 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
- target: input.target,
111
- id: input.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
- target: input.target,
122
- id: input.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
- adapter: input.adapter,
130
- target: input.target,
139
+ conversation,
131
140
  ...(input.sender !== undefined ? { sender: input.sender } : {}),
132
- ...(channelTypeOf(input.target)
133
- ? { channelType: channelTypeOf(input.target) }
134
- : {}),
135
141
  contentPreview: previewText(input.content),
136
- ...(input.id ?? input.message?.id
137
- ? { messageId: input.id ?? input.message?.id }
138
- : {}),
142
+ ...(input.message?.id ? { messageId: input.message.id } : {}),
139
143
  timestamp: Date.now(),
140
144
  });
141
145
  return result;
@@ -178,12 +182,42 @@ export class ImRuntime {
178
182
  return Object.freeze([]);
179
183
  }
180
184
  }
181
- 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) {
182
216
  try {
183
217
  const lease = this.#acquire();
184
218
  try {
185
219
  const index = requireAdapters(lease.value);
186
- const id = index.resolve(adapter, endpointId);
220
+ const id = index.resolve(adapter, endpointKey);
187
221
  if (!id)
188
222
  return null;
189
223
  const row = index.describe().find((item) => item.id === id);
@@ -212,22 +246,19 @@ export class ImRuntime {
212
246
  const lease = this.#acquire();
213
247
  try {
214
248
  const index = requireAdapters(lease.value);
215
- const capabilityId = index.resolve(input.adapter, input.endpointId);
216
- if (!capabilityId)
249
+ const resolved = index.resolve(input.adapter, input.endpointKey);
250
+ if (!resolved)
217
251
  throw new Error('endpoint not found');
218
- const target = input.conversation
219
- ? formatLegacyConversationRef(input.conversation)
220
- : composeSendTarget(input.channelType ?? '', input.channelId ?? '');
221
- if (!target)
222
- throw new TypeError('conversation or channelId is required');
252
+ const requester = index.owner(resolved);
253
+ const conversation = {
254
+ endpoint: { id: String(resolved), adapter: String(requester) },
255
+ ...input.conversation,
256
+ };
223
257
  const content = normalizeConsoleContent(input.content);
224
258
  const result = await this.#sendWithSnapshot({
225
- adapter: capabilityId,
226
- ...(input.conversation ? { conversation: input.conversation } : {}),
227
- target,
228
- requester: index.owner(capabilityId),
259
+ conversation,
260
+ requester,
229
261
  content,
230
- ...(input.parent ? { parent: input.parent } : {}),
231
262
  }, lease.value);
232
263
  return { messageId: result.message?.id ?? result.legacyMessageId ?? '' };
233
264
  }
@@ -237,39 +268,34 @@ export class ImRuntime {
237
268
  }
238
269
  /** Activity-feedback: add a message reaction when the live Endpoint supports it. */
239
270
  async addEndpointReaction(input) {
240
- const control = this.#liveEndpointControl(input.adapter, input.endpointId);
271
+ const control = this.#liveEndpointControl(input.adapter, input.endpointKey);
241
272
  return control?.addReaction?.(legacyMessageTarget(input.message, input.messageId), input.emoji, {
242
273
  sceneType: input.sceneType,
243
274
  channelId: input.channelId,
244
275
  }) ?? null;
245
276
  }
246
277
  async removeEndpointReaction(input) {
247
- await this.#liveEndpointControl(input.adapter, input.endpointId)
278
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
248
279
  ?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
249
280
  }
250
281
  /** Activity-feedback autoRemove: recall a previously sent status message. */
251
282
  async recallEndpointMessage(input) {
252
- await this.#liveEndpointControl(input.adapter, input.endpointId)
283
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
253
284
  ?.recall?.(legacyMessageTarget(input.message, input.messageId));
254
285
  }
255
286
  async editEndpointMessage(input) {
256
- return this.#liveEndpointControl(input.adapter, input.endpointId)
287
+ return this.#liveEndpointControl(input.adapter, input.endpointKey)
257
288
  ?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
258
289
  }
259
290
  async setEndpointTyping(input) {
260
- const target = input.conversation
261
- ? formatLegacyConversationRef(input.conversation)
262
- : input.target;
263
- if (!target)
264
- throw new TypeError('conversation or target is required');
265
- await this.#liveEndpointControl(input.adapter, input.endpointId)
266
- ?.typing?.(target, input.active);
291
+ await this.#liveEndpointControl(input.adapter, input.endpointKey)
292
+ ?.typing?.(input.conversation, input.active);
267
293
  }
268
- #liveEndpoint(adapter, endpointId) {
294
+ #liveEndpoint(adapter, endpointKey) {
269
295
  try {
270
296
  const lease = this.#acquire();
271
297
  try {
272
- const endpoint = requireAdapters(lease.value).instance(adapter, endpointId);
298
+ const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
273
299
  return endpoint ?? null;
274
300
  }
275
301
  finally {
@@ -280,44 +306,35 @@ export class ImRuntime {
280
306
  return null;
281
307
  }
282
308
  }
283
- #liveEndpointControl(adapter, endpointId) {
284
- const endpoint = this.#liveEndpoint(adapter, endpointId);
309
+ #liveEndpointControl(adapter, endpointKey) {
310
+ const endpoint = this.#liveEndpoint(adapter, endpointKey);
285
311
  return resolveEndpointControl(endpoint) ?? null;
286
312
  }
287
- /**
288
- * Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
289
- * @deprecated Host callers should use `getEndpointManagement()`.
290
- */
291
- getLiveEndpoint(adapter, endpointId) {
292
- return this.#liveEndpoint(adapter, endpointId);
293
- }
294
313
  /**
295
314
  * Narrow Host seam for Console social/group management. An empty object means
296
315
  * the Endpoint exists but implements no management operations; null means it
297
316
  * cannot be resolved.
298
317
  */
299
- getEndpointManagement(adapter, endpointId) {
300
- const endpoint = this.#liveEndpoint(adapter, endpointId);
318
+ getEndpointManagement(adapter, endpointKey) {
319
+ const endpoint = this.#liveEndpoint(adapter, endpointKey);
301
320
  if (!endpoint)
302
321
  return null;
303
322
  return resolveEndpointManagement(endpoint) ?? Object.freeze({});
304
323
  }
305
324
  async #sendWithSnapshot(request, snapshot) {
325
+ const adapter = request.conversation.endpoint.id;
306
326
  let initialPayload;
307
327
  try {
308
- const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
309
- initialPayload = await prepareOutboundPayload(rendered, request, snapshot);
328
+ const rendered = await this.#renderer.render(request.content, request.requester, snapshot, request.conversation, request.incoming);
329
+ initialPayload = await prepareOutboundPayload(rendered, request.conversation, snapshot);
310
330
  }
311
331
  catch {
312
332
  return rejectedReceipt('outbound_payload_rejected');
313
333
  }
314
334
  const envelope = createOutboundEnvelope({
315
- adapter: request.adapter,
316
- ...(request.conversation ? { conversation: request.conversation } : {}),
317
- target: request.target,
335
+ conversation: request.conversation,
318
336
  requester: request.requester,
319
337
  generation: snapshot.generation,
320
- ...(request.parent ? { parent: request.parent } : {}),
321
338
  }, initialPayload);
322
339
  let terminalEntered = false;
323
340
  let receipt;
@@ -326,20 +343,16 @@ export class ImRuntime {
326
343
  terminalEntered = true;
327
344
  let payload;
328
345
  try {
329
- // Middleware may replace a payload with legacy/wire segments. Normalize
330
- // again at the transport boundary so it cannot bypass core policy.
331
- payload = await prepareOutboundPayload(envelope.payload, request, snapshot, true);
346
+ payload = await prepareOutboundPayload(envelope.payload, request.conversation, snapshot, true);
332
347
  }
333
348
  catch {
334
349
  receipt = rejectedReceipt('outbound_payload_rejected');
335
350
  return;
336
351
  }
337
352
  try {
338
- const result = await requireAdapters(snapshot).send(request.adapter, {
339
- target: request.target,
340
- ...(request.conversation ? { conversation: request.conversation } : {}),
353
+ const result = await requireAdapters(snapshot).send(adapter, {
354
+ conversation: request.conversation,
341
355
  payload,
342
- ...(request.parent ? { parent: request.parent } : {}),
343
356
  });
344
357
  receipt = receiptFromEndpointResult(result);
345
358
  }
@@ -349,8 +362,7 @@ export class ImRuntime {
349
362
  if (receipt?.status === 'sent') {
350
363
  this.#emitMessage({
351
364
  direction: 'outbound',
352
- adapter: request.adapter,
353
- target: request.target,
365
+ conversation: request.conversation,
354
366
  requester: request.requester,
355
367
  contentPreview: previewText(payload),
356
368
  ...(receipt.message?.id || receipt.legacyMessageId
@@ -362,8 +374,6 @@ export class ImRuntime {
362
374
  }, 'outbound');
363
375
  }
364
376
  catch {
365
- // If a middleware fails after the terminal, the endpoint has already
366
- // produced its receipt (and, for a real send, its event). Keep that fact.
367
377
  return receipt ?? failedReceipt('outbound_middleware_failed');
368
378
  }
369
379
  if (!terminalEntered)
@@ -394,6 +404,14 @@ export class ImRuntime {
394
404
  : undefined;
395
405
  }
396
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
+ }
397
415
  function requireAdapters(snapshot) {
398
416
  const projection = snapshot.projections.get(adapterFeatureId);
399
417
  if (!isAdapterIndex(projection)) {
@@ -423,18 +441,17 @@ function legacyMessageTarget(message, messageId) {
423
441
  return messageId;
424
442
  throw new TypeError('message or messageId is required');
425
443
  }
426
- async function prepareOutboundPayload(rendered, request, snapshot, finalizeInteractive = false) {
427
- const directHtml = isDirectHtmlConsumer(snapshot, request.adapter);
444
+ async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
445
+ const adapter = conversation.endpoint.id;
446
+ const directHtml = isDirectHtmlConsumer(snapshot, adapter);
428
447
  let payload = directHtml
429
448
  ? rendered
430
449
  : await normalizeOutboundPayload(rendered, resolveHtmlRenderer(snapshot), {
431
- mediaPolicy: resolveOutboundMediaPolicy(request.adapter, snapshot),
450
+ mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
432
451
  });
433
452
  if (finalizeInteractive) {
434
- payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(request.adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveChannelKey(String(request.adapter), request.target), map));
453
+ payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveConversationKey(conversation), map));
435
454
  }
436
- // Segment arrays crossing the adapter boundary are canonical after every
437
- // middleware transform. Direct html consumers retain their explicit wire API.
438
455
  if (!directHtml && Array.isArray(payload))
439
456
  assertCanonicalSegments(payload);
440
457
  return payload;
@@ -490,18 +507,6 @@ function failedReceipt(code, retryable = false) {
490
507
  }),
491
508
  });
492
509
  }
493
- /**
494
- * Build Adapter send target. If channelId already carries a scene prefix
495
- * (`private:uid` / `group:gid`), do not double-prefix.
496
- */
497
- function composeSendTarget(channelType, channelId) {
498
- const id = channelId.trim();
499
- if (!id)
500
- return channelType || '';
501
- if (/^(private|group|channel|direct|c2c|temp):/iu.test(id))
502
- return id;
503
- return channelType ? `${channelType}:${id}` : id;
504
- }
505
510
  /** `@zhin.js/adapter-icqq` → `icqq`;非 adapter 包名原样返回。 */
506
511
  function adapterTypeName(packageName) {
507
512
  if (!packageName)
@@ -528,10 +533,14 @@ function normalizeConsoleContent(content) {
528
533
  return content;
529
534
  return String(content);
530
535
  }
531
- /** target 前缀场景:`group:123` `group`;无前缀返回 undefined。 */
532
- function channelTypeOf(target) {
533
- const match = /^([a-z0-9-]+):/iu.exec(target);
534
- return match?.[1];
536
+ /** 日志用会话摘要(`kind:id@parentKind:parentId#threadId`),不拼 legacy target。 */
537
+ function formatConversationLog(conversation) {
538
+ const base = `${conversation.kind}:${conversation.id}`;
539
+ const parent = conversation.parent
540
+ ? `@${conversation.parent.kind}:${conversation.parent.id}`
541
+ : '';
542
+ const thread = conversation.threadId ? `#${conversation.threadId}` : '';
543
+ return `${base}${parent}${thread}`;
535
544
  }
536
545
  /** 消息内容 → 预览文本(截断 200 字);wire 段取 `data.text`,其余段记 `[type]`。 */
537
546
  function previewText(content) {
@@ -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';
@@ -1,3 +1,4 @@
1
+ import type { ConversationRef } from '@zhin.js/im-contract';
1
2
  import type { Message } from './contracts.js';
2
3
  /**
3
4
  * Plugin Runtime IM 管线的 interactive action 回跳(旧轨
@@ -14,8 +15,8 @@ export interface RegisteredRuntimeInteractiveHandler {
14
15
  readonly prefix: string;
15
16
  readonly handler: RuntimeInteractiveHandler;
16
17
  }
17
- /** 频道键:出站降级写入与入站回跳读取共用(`adapter~target`)。 */
18
- export declare function runtimeInteractiveChannelKey(adapter: string, target: string): string;
18
+ /** 频道键:出站降级写入与入站回跳读取共用(由 ConversationRef 派生,稳定即可)。 */
19
+ export declare function runtimeInteractiveConversationKey(conversation: ConversationRef): string;
19
20
  /** prefix 最长匹配(与旧轨 findHandler 一致)。 */
20
21
  export declare function findRuntimeInteractiveHandler(handlers: readonly RegisteredRuntimeInteractiveHandler[], payload: string): RuntimeInteractiveHandler | undefined;
21
22
  /**
@@ -1,8 +1,13 @@
1
1
  import { resolvePayloadFromText } from '../../built/interactive-segments/action.js';
2
2
  import { keyboardFallbackStore } from '../../built/interactive-segments/fallback-store.js';
3
- /** 频道键:出站降级写入与入站回跳读取共用(`adapter~target`)。 */
4
- export function runtimeInteractiveChannelKey(adapter, target) {
5
- return `${adapter}~${target}`;
3
+ /** 频道键:出站降级写入与入站回跳读取共用(由 ConversationRef 派生,稳定即可)。 */
4
+ export function runtimeInteractiveConversationKey(conversation) {
5
+ const base = `${String(conversation.endpoint.id)}~${conversation.kind}:${conversation.id}`;
6
+ const parent = conversation.parent
7
+ ? `@${conversation.parent.kind}:${conversation.parent.id}`
8
+ : '';
9
+ const thread = conversation.threadId ? `#${conversation.threadId}` : '';
10
+ return `${base}${parent}${thread}`;
6
11
  }
7
12
  /** prefix 最长匹配(与旧轨 findHandler 一致)。 */
8
13
  export function findRuntimeInteractiveHandler(handlers, payload) {
@@ -27,7 +32,7 @@ export function resolveRuntimeInteractivePayload(message) {
27
32
  const raw = message.content.trim();
28
33
  if (!raw)
29
34
  return undefined;
30
- return resolvePayloadFromText(raw, keyboardFallbackStore.mapFor(runtimeInteractiveChannelKey(String(message.adapter), message.target)));
35
+ return resolvePayloadFromText(raw, keyboardFallbackStore.mapFor(runtimeInteractiveConversationKey(message.conversation)));
31
36
  }
32
37
  function actionPayloadFromSegments(segments) {
33
38
  for (const seg of segments ?? []) {
@@ -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');