@zhin.js/core 1.5.1 → 1.5.2

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.
package/README.md CHANGED
@@ -36,11 +36,14 @@ export default definePlugin({
36
36
  ```
37
37
 
38
38
  ```typescript
39
- // commands/hello/[name:string].ts
39
+ // commands/hello/[name].ts
40
40
  import { defineCommand } from '@zhin.js/command'
41
41
 
42
42
  export default defineCommand({
43
43
  description: '打招呼',
44
+ params: {
45
+ name: { type: 'string' },
46
+ },
44
47
  execute({ params }) {
45
48
  return `Hello, ${params.name}!`
46
49
  },
@@ -1,4 +1,4 @@
1
- import type { CapabilityId, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
1
+ import type { PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { ConversationRef, DeliveryReceipt, MessageRef } from '@zhin.js/im-contract';
3
3
  import type { MediaRef, Segment } from '../../built/segment-contract/types.js';
4
4
  export type { MediaRef, Segment };
@@ -25,60 +25,33 @@ export declare function isRawContent(value: SendContent): value is RawContent;
25
25
  /** canonical Segment 一等公民判定(与 ComponentCall/RawContent 的 $content brand 互斥)。 */
26
26
  export declare function isSegmentContent(value: unknown): value is Segment;
27
27
  export interface IncomingMessage {
28
- readonly adapter: CapabilityId;
29
- /** Structured identity supplied by migrated adapters; target remains the bridge. */
30
- readonly conversation?: ConversationRef;
31
- /** Structured native message identity supplied by migrated adapters. */
28
+ readonly conversation: ConversationRef;
32
29
  readonly message?: MessageRef;
33
- readonly target: string;
34
- /**
35
- * 纯文本视图:与 `segments` 同源(adapter 从同一份入站载荷派生二者)。
36
- * 触发判定与 Console 预览读取此字段;命令匹配在有 segments 时优先使用结构化视图。
37
- */
38
30
  readonly content: string;
39
- /**
40
- * 结构化段视图(canonical Segment SSOT,见 built/segment-contract)。
41
- * 与 `content` 同源:segments 承载纯文本无法表达的媒体(image/audio/video/file
42
- * 的 MediaRef)、mention、reply 等信息。旧 adapter 未迁移时可缺省,
43
- * 读取方必须容忍 undefined。
44
- */
45
31
  readonly segments?: readonly Segment[];
46
- readonly id?: string;
47
32
  readonly sender?: string;
48
33
  readonly metadata?: Readonly<Record<string, unknown>>;
49
34
  }
50
35
  export interface SendRequest {
51
- readonly adapter: CapabilityId;
52
- /** Structured destination supplied to Adapter endpoints in parallel with target. */
53
- readonly conversation?: ConversationRef;
54
- readonly target: string;
36
+ readonly conversation: ConversationRef;
55
37
  readonly requester: PluginId;
56
38
  readonly content: SendContent;
57
- readonly parent?: ChannelParent;
58
- }
59
- /** Console 通道的来源场景(群临时会话 parent.group / QQ 子频道 parent.guild)。 */
60
- export interface ChannelParent {
61
- readonly type?: string;
62
- readonly id?: string;
63
- readonly name?: string;
64
39
  }
40
+ /**
41
+ * Host 侧未锚定 endpoint 的会话地址(Console RPC / OutboundHost 入参);
42
+ * ImRuntime 解析 adapter/endpointId 后锚定为完整 ConversationRef。
43
+ */
44
+ export type ConversationAddress = Omit<ConversationRef, 'endpoint'>;
65
45
  export interface OutboundEnvelope {
66
- readonly adapter: CapabilityId;
67
- readonly conversation?: ConversationRef;
68
- readonly target: string;
46
+ readonly conversation: ConversationRef;
69
47
  readonly requester: PluginId;
70
48
  readonly generation: number;
71
49
  readonly payload: unknown;
72
- readonly parent?: ChannelParent;
73
50
  replace(payload: unknown): void;
74
51
  }
75
52
  export interface MessageGateway {
76
53
  receive(input: IncomingMessage): Promise<MessageDispatchResult>;
77
- /**
78
- * Compatibility surface for existing Adapter-facing gateway consumers.
79
- * Runtime callers that need an outcome use DeliveryMessageGateway instead.
80
- */
81
- send(request: SendRequest): Promise<unknown>;
54
+ send(request: SendRequest): Promise<DeliveryReceipt>;
82
55
  /**
83
56
  * 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
84
57
  * 平台 callback 的 action 段、'text' 端点的数字回跳与指令预填直出
@@ -92,10 +65,6 @@ export interface MessageGateway {
92
65
  */
93
66
  setUnmatchedHandler(handler: (message: Message, snapshot: RuntimeSnapshot, requester: PluginId) => Promise<boolean>): void;
94
67
  }
95
- /** Structured outbound gateway exposed by the Plugin Runtime. */
96
- export interface DeliveryMessageGateway extends MessageGateway {
97
- send(request: SendRequest): Promise<DeliveryReceipt>;
98
- }
99
68
  export interface MessageDispatchResult {
100
69
  readonly matched: boolean;
101
70
  readonly command?: string;
@@ -103,11 +72,9 @@ export interface MessageDispatchResult {
103
72
  readonly value?: unknown;
104
73
  }
105
74
  export declare class Message {
106
- readonly adapter: CapabilityId;
107
- readonly target: string;
75
+ readonly conversation: ConversationRef;
108
76
  readonly content: string;
109
77
  readonly generation: number;
110
- readonly id?: string | undefined;
111
78
  readonly sender?: string | undefined;
112
79
  readonly metadata: Readonly<Record<string, unknown>>;
113
80
  /**
@@ -115,20 +82,18 @@ export declare class Message {
115
82
  * Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
116
83
  */
117
84
  readonly segments?: readonly Segment[] | undefined;
118
- /** Structured inbound conversation when supplied by a migrated adapter. */
119
- readonly conversation?: ConversationRef | undefined;
120
- /** Structured inbound message identity when supplied by a migrated adapter. */
85
+ /** 结构化入站消息身份(平台原生 message id MessageRef 传递)。 */
121
86
  readonly message?: MessageRef | undefined;
122
- constructor(adapter: CapabilityId, target: string, content: string, generation: number, reply: (content: SendContent, requester?: PluginId) => Promise<unknown>, id?: string | undefined, sender?: string | undefined, metadata?: Readonly<Record<string, unknown>>,
87
+ constructor(conversation: ConversationRef, content: string, generation: number, reply: (content: SendContent, requester?: PluginId) => Promise<DeliveryReceipt>, sender?: string | undefined, metadata?: Readonly<Record<string, unknown>>,
123
88
  /**
124
89
  * 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
125
90
  * Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
126
91
  */
127
92
  segments?: readonly Segment[] | undefined,
128
- /** Structured inbound conversation when supplied by a migrated adapter. */
129
- conversation?: ConversationRef | undefined,
130
- /** Structured inbound message identity when supplied by a migrated adapter. */
93
+ /** 结构化入站消息身份(平台原生 message id MessageRef 传递)。 */
131
94
  message?: MessageRef | undefined);
95
+ /** 平台原生消息 id(`message` 未提供时为 undefined)。 */
96
+ get id(): string | undefined;
132
97
  readonly $reply: (content: SendContent) => Promise<DeliveryReceipt>;
133
98
  readonly $replyFrom: (requester: PluginId, content: SendContent) => Promise<DeliveryReceipt>;
134
99
  }
@@ -33,44 +33,36 @@ export function isSegmentContent(value) {
33
33
  && value.data !== null;
34
34
  }
35
35
  export class Message {
36
- adapter;
37
- target;
36
+ conversation;
38
37
  content;
39
38
  generation;
40
- id;
41
39
  sender;
42
40
  metadata;
43
41
  segments;
44
- conversation;
45
42
  message;
46
- constructor(adapter, target, content, generation,
47
- // Compatibility at the construction boundary: legacy tests and embedders
48
- // may still supply an untyped reply callback. ImRuntime always supplies a
49
- // DeliveryReceipt-producing implementation.
50
- reply, id, sender, metadata = Object.freeze({}),
43
+ constructor(conversation, content, generation, reply, sender, metadata = Object.freeze({}),
51
44
  /**
52
45
  * 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
53
46
  * Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
54
47
  */
55
48
  segments,
56
- /** Structured inbound conversation when supplied by a migrated adapter. */
57
- conversation,
58
- /** Structured inbound message identity when supplied by a migrated adapter. */
49
+ /** 结构化入站消息身份(平台原生 message id MessageRef 传递)。 */
59
50
  message) {
60
- this.adapter = adapter;
61
- this.target = target;
51
+ this.conversation = conversation;
62
52
  this.content = content;
63
53
  this.generation = generation;
64
- this.id = id;
65
54
  this.sender = sender;
66
55
  this.metadata = metadata;
67
56
  this.segments = segments;
68
- this.conversation = conversation;
69
57
  this.message = message;
70
58
  this.$reply = (content) => reply(content);
71
59
  this.$replyFrom = (requester, content) => reply(content, requester);
72
60
  Object.freeze(this);
73
61
  }
62
+ /** 平台原生消息 id(`message` 未提供时为 undefined)。 */
63
+ get id() {
64
+ return this.message?.id;
65
+ }
74
66
  $reply;
75
67
  $replyFrom;
76
68
  }
@@ -1,21 +1,18 @@
1
- import { Scope, type CapabilityId, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
1
+ import { Scope, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
2
2
  import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
3
3
  import { type ConversationRef, type DeliveryReceipt, type MessageRef } from '@zhin.js/im-contract';
4
- import { Message, type ChannelParent, type DeliveryMessageGateway, type IncomingMessage, type MessageDispatchResult, type SendRequest } from './contracts.js';
4
+ import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type SendRequest } from './contracts.js';
5
5
  import { OutboundRenderer } from './outbound-renderer.js';
6
6
  import { type RuntimeInteractiveHandler } from './interactive.js';
7
- export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<DeliveryMessageGateway>;
7
+ export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<MessageGateway>;
8
8
  /** Console 实时消息事件(SSE 推送源;content 仅为截断预览,不含完整原始段)。 */
9
9
  export interface RuntimeMessageEvent {
10
10
  readonly direction: 'inbound' | 'outbound';
11
- readonly adapter: CapabilityId;
12
- readonly target: string;
11
+ readonly conversation: ConversationRef;
13
12
  /** inbound:发送者 id。 */
14
13
  readonly sender?: string;
15
14
  /** outbound:发起方插件。 */
16
15
  readonly requester?: PluginId;
17
- /** inbound:从 target 前缀解析的场景(`group:xx` → `group`)。 */
18
- readonly channelType?: string;
19
16
  /** 预览文本,截断至 200 字。 */
20
17
  readonly contentPreview: string;
21
18
  readonly messageId?: string;
@@ -30,7 +27,7 @@ export interface ImRuntimeOptions {
30
27
  readonly commandPrefix?: string;
31
28
  readonly renderer?: OutboundRenderer;
32
29
  }
33
- export declare class ImRuntime implements DeliveryMessageGateway {
30
+ export declare class ImRuntime implements MessageGateway {
34
31
  #private;
35
32
  constructor(options?: ImRuntimeOptions);
36
33
  attach(snapshots: SnapshotStore): void;
@@ -74,11 +71,8 @@ export declare class ImRuntime implements DeliveryMessageGateway {
74
71
  sendEndpointMessage(input: {
75
72
  readonly adapter: string;
76
73
  readonly endpointId: string;
77
- readonly conversation?: ConversationRef;
78
- readonly channelId?: string;
79
- readonly channelType?: string;
74
+ readonly conversation: ConversationAddress;
80
75
  readonly content: unknown;
81
- readonly parent?: ChannelParent;
82
76
  }): Promise<{
83
77
  messageId: string;
84
78
  }>;
@@ -116,8 +110,7 @@ export declare class ImRuntime implements DeliveryMessageGateway {
116
110
  setEndpointTyping(input: {
117
111
  readonly adapter: string;
118
112
  readonly endpointId: string;
119
- readonly conversation?: ConversationRef;
120
- readonly target?: string;
113
+ readonly conversation: ConversationRef;
121
114
  readonly active?: boolean;
122
115
  }): Promise<void>;
123
116
  /**
@@ -1,6 +1,6 @@
1
1
  import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
2
2
  import { adapterFeatureId, isAdapterIndex, resolveEndpointControl, resolveEndpointManagement, } from '@zhin.js/adapter';
3
- import { formatLegacyConversationRef, formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
3
+ import { formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
4
4
  import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
5
5
  import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
6
6
  import { Message, createOutboundEnvelope, } from './contracts.js';
@@ -9,7 +9,7 @@ import { OutboundRenderer } from './outbound-renderer.js';
9
9
  import { applyOutboundInteractivePolicy, normalizeOutboundPayload, resolveOutboundInteractivePolicy, resolveOutboundMediaPolicy, } from './outbound-segments.js';
10
10
  import { assertCanonicalSegments } from '../../built/segment-contract/assert.js';
11
11
  import { keyboardFallbackStore } from '../../built/interactive-segments/fallback-store.js';
12
- import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveChannelKey, } from './interactive.js';
12
+ import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveConversationKey, } from './interactive.js';
13
13
  const logger = getLogger('im');
14
14
  export const messageGatewayToken = createToken('zhin.im.message-gateway');
15
15
  export const messagePreviewLimit = 200;
@@ -78,28 +78,28 @@ 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
85
  op: 'im_inbound_receive',
84
- adapter: String(input.adapter).split('\0').pop() ?? String(input.adapter),
85
- target: input.target,
86
+ adapter: String(adapter).split('\0').pop() ?? String(adapter),
87
+ conversation: formatConversationLog(conversation),
86
88
  sender: input.sender,
87
- id: input.id,
89
+ id: input.message?.id,
88
90
  preview: truncatePreview(input.content),
89
91
  segments: input.segments?.length,
90
92
  generation: lease.value.generation,
91
93
  }));
92
- const message = new Message(input.adapter, input.target, input.content, lease.value.generation, (content, replyRequester = requester) => {
94
+ const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester) => {
93
95
  if (!active)
94
96
  throw new Error('Message reply scope has ended');
95
97
  return this.#sendWithSnapshot({
96
- adapter: input.adapter,
97
- ...(input.conversation ? { conversation: input.conversation } : {}),
98
- target: input.target,
98
+ conversation,
99
99
  requester: replyRequester,
100
100
  content,
101
101
  }, 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);
102
+ }, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message);
103
103
  let result = Object.freeze({ matched: false });
104
104
  await runMiddleware(lease.value, message, async () => {
105
105
  result = await this.#dispatchInteractive(message, requester)
@@ -107,8 +107,8 @@ export class ImRuntime {
107
107
  if (!result.matched && this.#unmatchedHandler) {
108
108
  logger.debug(formatCompact({
109
109
  op: 'im_inbound_unmatched_handler',
110
- target: input.target,
111
- id: input.id,
110
+ conversation: formatConversationLog(conversation),
111
+ id: input.message?.id,
112
112
  }));
113
113
  const handled = await this.#unmatchedHandler(message, lease.value, requester);
114
114
  if (handled) {
@@ -118,24 +118,18 @@ export class ImRuntime {
118
118
  }, 'inbound');
119
119
  logger.debug(formatCompact({
120
120
  op: 'im_inbound_done',
121
- target: input.target,
122
- id: input.id,
121
+ conversation: formatConversationLog(conversation),
122
+ id: input.message?.id,
123
123
  matched: result.matched,
124
124
  command: result.command,
125
125
  owner: result.owner,
126
126
  }));
127
127
  this.#emitMessage({
128
128
  direction: 'inbound',
129
- adapter: input.adapter,
130
- target: input.target,
129
+ conversation,
131
130
  ...(input.sender !== undefined ? { sender: input.sender } : {}),
132
- ...(channelTypeOf(input.target)
133
- ? { channelType: channelTypeOf(input.target) }
134
- : {}),
135
131
  contentPreview: previewText(input.content),
136
- ...(input.id ?? input.message?.id
137
- ? { messageId: input.id ?? input.message?.id }
138
- : {}),
132
+ ...(input.message?.id ? { messageId: input.message.id } : {}),
139
133
  timestamp: Date.now(),
140
134
  });
141
135
  return result;
@@ -212,22 +206,19 @@ export class ImRuntime {
212
206
  const lease = this.#acquire();
213
207
  try {
214
208
  const index = requireAdapters(lease.value);
215
- const capabilityId = index.resolve(input.adapter, input.endpointId);
216
- if (!capabilityId)
209
+ const resolved = index.resolve(input.adapter, input.endpointId);
210
+ if (!resolved)
217
211
  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');
212
+ const requester = index.owner(resolved);
213
+ const conversation = {
214
+ endpoint: { id: String(resolved), adapter: String(requester) },
215
+ ...input.conversation,
216
+ };
223
217
  const content = normalizeConsoleContent(input.content);
224
218
  const result = await this.#sendWithSnapshot({
225
- adapter: capabilityId,
226
- ...(input.conversation ? { conversation: input.conversation } : {}),
227
- target,
228
- requester: index.owner(capabilityId),
219
+ conversation,
220
+ requester,
229
221
  content,
230
- ...(input.parent ? { parent: input.parent } : {}),
231
222
  }, lease.value);
232
223
  return { messageId: result.message?.id ?? result.legacyMessageId ?? '' };
233
224
  }
@@ -257,13 +248,8 @@ export class ImRuntime {
257
248
  ?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
258
249
  }
259
250
  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
251
  await this.#liveEndpointControl(input.adapter, input.endpointId)
266
- ?.typing?.(target, input.active);
252
+ ?.typing?.(input.conversation, input.active);
267
253
  }
268
254
  #liveEndpoint(adapter, endpointId) {
269
255
  try {
@@ -303,21 +289,19 @@ export class ImRuntime {
303
289
  return resolveEndpointManagement(endpoint) ?? Object.freeze({});
304
290
  }
305
291
  async #sendWithSnapshot(request, snapshot) {
292
+ const adapter = request.conversation.endpoint.id;
306
293
  let initialPayload;
307
294
  try {
308
295
  const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
309
- initialPayload = await prepareOutboundPayload(rendered, request, snapshot);
296
+ initialPayload = await prepareOutboundPayload(rendered, request.conversation, snapshot);
310
297
  }
311
298
  catch {
312
299
  return rejectedReceipt('outbound_payload_rejected');
313
300
  }
314
301
  const envelope = createOutboundEnvelope({
315
- adapter: request.adapter,
316
- ...(request.conversation ? { conversation: request.conversation } : {}),
317
- target: request.target,
302
+ conversation: request.conversation,
318
303
  requester: request.requester,
319
304
  generation: snapshot.generation,
320
- ...(request.parent ? { parent: request.parent } : {}),
321
305
  }, initialPayload);
322
306
  let terminalEntered = false;
323
307
  let receipt;
@@ -326,20 +310,16 @@ export class ImRuntime {
326
310
  terminalEntered = true;
327
311
  let payload;
328
312
  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);
313
+ payload = await prepareOutboundPayload(envelope.payload, request.conversation, snapshot, true);
332
314
  }
333
315
  catch {
334
316
  receipt = rejectedReceipt('outbound_payload_rejected');
335
317
  return;
336
318
  }
337
319
  try {
338
- const result = await requireAdapters(snapshot).send(request.adapter, {
339
- target: request.target,
340
- ...(request.conversation ? { conversation: request.conversation } : {}),
320
+ const result = await requireAdapters(snapshot).send(adapter, {
321
+ conversation: request.conversation,
341
322
  payload,
342
- ...(request.parent ? { parent: request.parent } : {}),
343
323
  });
344
324
  receipt = receiptFromEndpointResult(result);
345
325
  }
@@ -349,8 +329,7 @@ export class ImRuntime {
349
329
  if (receipt?.status === 'sent') {
350
330
  this.#emitMessage({
351
331
  direction: 'outbound',
352
- adapter: request.adapter,
353
- target: request.target,
332
+ conversation: request.conversation,
354
333
  requester: request.requester,
355
334
  contentPreview: previewText(payload),
356
335
  ...(receipt.message?.id || receipt.legacyMessageId
@@ -362,8 +341,6 @@ export class ImRuntime {
362
341
  }, 'outbound');
363
342
  }
364
343
  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
344
  return receipt ?? failedReceipt('outbound_middleware_failed');
368
345
  }
369
346
  if (!terminalEntered)
@@ -423,18 +400,17 @@ function legacyMessageTarget(message, messageId) {
423
400
  return messageId;
424
401
  throw new TypeError('message or messageId is required');
425
402
  }
426
- async function prepareOutboundPayload(rendered, request, snapshot, finalizeInteractive = false) {
427
- const directHtml = isDirectHtmlConsumer(snapshot, request.adapter);
403
+ async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
404
+ const adapter = conversation.endpoint.id;
405
+ const directHtml = isDirectHtmlConsumer(snapshot, adapter);
428
406
  let payload = directHtml
429
407
  ? rendered
430
408
  : await normalizeOutboundPayload(rendered, resolveHtmlRenderer(snapshot), {
431
- mediaPolicy: resolveOutboundMediaPolicy(request.adapter, snapshot),
409
+ mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
432
410
  });
433
411
  if (finalizeInteractive) {
434
- payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(request.adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveChannelKey(String(request.adapter), request.target), map));
412
+ payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveConversationKey(conversation), map));
435
413
  }
436
- // Segment arrays crossing the adapter boundary are canonical after every
437
- // middleware transform. Direct html consumers retain their explicit wire API.
438
414
  if (!directHtml && Array.isArray(payload))
439
415
  assertCanonicalSegments(payload);
440
416
  return payload;
@@ -490,18 +466,6 @@ function failedReceipt(code, retryable = false) {
490
466
  }),
491
467
  });
492
468
  }
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
469
  /** `@zhin.js/adapter-icqq` → `icqq`;非 adapter 包名原样返回。 */
506
470
  function adapterTypeName(packageName) {
507
471
  if (!packageName)
@@ -528,10 +492,14 @@ function normalizeConsoleContent(content) {
528
492
  return content;
529
493
  return String(content);
530
494
  }
531
- /** target 前缀场景:`group:123` `group`;无前缀返回 undefined。 */
532
- function channelTypeOf(target) {
533
- const match = /^([a-z0-9-]+):/iu.exec(target);
534
- return match?.[1];
495
+ /** 日志用会话摘要(`kind:id@parentKind:parentId#threadId`),不拼 legacy target。 */
496
+ function formatConversationLog(conversation) {
497
+ const base = `${conversation.kind}:${conversation.id}`;
498
+ const parent = conversation.parent
499
+ ? `@${conversation.parent.kind}:${conversation.parent.id}`
500
+ : '';
501
+ const thread = conversation.threadId ? `#${conversation.threadId}` : '';
502
+ return `${base}${parent}${thread}`;
535
503
  }
536
504
  /** 消息内容 → 预览文本(截断 200 字);wire 段取 `data.text`,其余段记 `[type]`。 */
537
505
  function previewText(content) {
@@ -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 ?? []) {
@@ -2,7 +2,7 @@ import { commandFeatureId, isCommandIndex, } from '@zhin.js/command';
2
2
  import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
3
3
  const logger = getLogger('command');
4
4
  function ownerOfMessage(message) {
5
- return String(message.adapter).split('\0')[0];
5
+ return message.conversation.endpoint.adapter;
6
6
  }
7
7
  /**
8
8
  * 默认解析:读消息所属适配器实例 config 的 `commandPrefix`(默认 `''`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/core",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Zhin机器人核心框架",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -70,16 +70,16 @@
70
70
  "segment-matcher": "^1.0.5",
71
71
  "smol-toml": "^1.7.1",
72
72
  "yaml": "^2.9.0",
73
- "@zhin.js/adapter": "1.1.4",
74
- "@zhin.js/command": "1.0.6",
75
- "@zhin.js/component": "1.0.5",
76
- "@zhin.js/im-contract": "1.0.0",
73
+ "@zhin.js/adapter": "1.1.5",
74
+ "@zhin.js/command": "1.0.7",
75
+ "@zhin.js/component": "1.0.6",
76
+ "@zhin.js/database": "1.0.78",
77
+ "@zhin.js/im-contract": "1.0.1",
77
78
  "@zhin.js/kernel": "1.0.5",
78
79
  "@zhin.js/logger": "1.0.75",
79
- "@zhin.js/middleware": "1.0.5",
80
- "@zhin.js/plugin-runtime": "1.1.2",
81
- "@zhin.js/schema": "1.0.72",
82
- "@zhin.js/database": "1.0.78"
80
+ "@zhin.js/middleware": "1.0.6",
81
+ "@zhin.js/plugin-runtime": "1.1.3",
82
+ "@zhin.js/schema": "1.0.72"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "zod": "^4.0.0"