@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 +4 -1
- package/lib/plugin-runtime/im/contracts.d.ts +16 -51
- package/lib/plugin-runtime/im/contracts.js +8 -16
- package/lib/plugin-runtime/im/im-runtime.d.ts +7 -14
- package/lib/plugin-runtime/im/im-runtime.js +47 -79
- package/lib/plugin-runtime/im/interactive.d.ts +3 -2
- package/lib/plugin-runtime/im/interactive.js +9 -4
- package/lib/plugin-runtime/im/message-dispatcher.js +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -36,11 +36,14 @@ export default definePlugin({
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
// commands/hello/[name
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
57
|
-
conversation,
|
|
58
|
-
/** Structured inbound message identity when supplied by a migrated adapter. */
|
|
49
|
+
/** 结构化入站消息身份(平台原生 message id 经 MessageRef 传递)。 */
|
|
59
50
|
message) {
|
|
60
|
-
this.
|
|
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
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
|
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(
|
|
85
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
216
|
-
if (!
|
|
209
|
+
const resolved = index.resolve(input.adapter, input.endpointId);
|
|
210
|
+
if (!resolved)
|
|
217
211
|
throw new Error('endpoint not found');
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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?.(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
339
|
-
|
|
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
|
-
|
|
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,
|
|
427
|
-
const
|
|
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(
|
|
409
|
+
mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
|
|
432
410
|
});
|
|
433
411
|
if (finalizeInteractive) {
|
|
434
|
-
payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(
|
|
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
|
-
/**
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
|
|
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
|
-
/**
|
|
18
|
-
export declare function
|
|
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
|
-
/**
|
|
4
|
-
export function
|
|
5
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
74
|
-
"@zhin.js/command": "1.0.
|
|
75
|
-
"@zhin.js/component": "1.0.
|
|
76
|
-
"@zhin.js/
|
|
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.
|
|
80
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
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"
|