@zhin.js/core 1.4.2 → 1.5.0
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 +3 -3
- package/lib/built/authorization.js +27 -3
- package/lib/built/generic-segment-mapper.d.ts +2 -4
- package/lib/built/generic-segment-mapper.js +64 -106
- package/lib/built/segment-contract/assert.d.ts +4 -1
- package/lib/built/segment-contract/assert.js +15 -6
- package/lib/built/segment-contract/index.d.ts +3 -3
- package/lib/built/segment-contract/index.js +2 -2
- package/lib/built/segment-contract/media.d.ts +1 -7
- package/lib/built/segment-contract/media.js +3 -50
- package/lib/built/segment-contract/preview.js +2 -2
- package/lib/built/segment-contract/types.d.ts +28 -3
- package/lib/built/segment-contract/validate.d.ts +91 -0
- package/lib/built/segment-contract/validate.js +49 -0
- package/lib/plugin-runtime/im/contracts.d.ts +34 -4
- package/lib/plugin-runtime/im/contracts.js +24 -2
- package/lib/plugin-runtime/im/im-runtime.d.ts +28 -9
- package/lib/plugin-runtime/im/im-runtime.js +175 -70
- package/lib/plugin-runtime/im/outbound-renderer.js +4 -1
- package/lib/plugin-runtime/im/outbound-segments.d.ts +13 -14
- package/lib/plugin-runtime/im/outbound-segments.js +37 -65
- package/package.json +8 -7
|
@@ -10,6 +10,8 @@ export const mediaRefSchema = Schema.object({
|
|
|
10
10
|
kind: mediaKindSchema.required(),
|
|
11
11
|
value: Schema.string().required(),
|
|
12
12
|
mime_type: Schema.string(),
|
|
13
|
+
file_name: Schema.string(),
|
|
14
|
+
size: Schema.number(),
|
|
13
15
|
});
|
|
14
16
|
export const textSegmentSchema = Schema.object({
|
|
15
17
|
type: Schema.const('text'),
|
|
@@ -32,6 +34,31 @@ export const imageSegmentSchema = Schema.object({
|
|
|
32
34
|
}).required(),
|
|
33
35
|
platform: platformSchema,
|
|
34
36
|
});
|
|
37
|
+
export const audioSegmentSchema = Schema.object({
|
|
38
|
+
type: Schema.const('audio'),
|
|
39
|
+
data: Schema.object({
|
|
40
|
+
media: mediaRefSchema.required(),
|
|
41
|
+
duration: Schema.number(),
|
|
42
|
+
}).required(),
|
|
43
|
+
platform: platformSchema,
|
|
44
|
+
});
|
|
45
|
+
export const videoSegmentSchema = Schema.object({
|
|
46
|
+
type: Schema.const('video'),
|
|
47
|
+
data: Schema.object({
|
|
48
|
+
media: mediaRefSchema.required(),
|
|
49
|
+
duration: Schema.number(),
|
|
50
|
+
alt: Schema.string(),
|
|
51
|
+
}).required(),
|
|
52
|
+
platform: platformSchema,
|
|
53
|
+
});
|
|
54
|
+
export const fileSegmentSchema = Schema.object({
|
|
55
|
+
type: Schema.const('file'),
|
|
56
|
+
data: Schema.object({
|
|
57
|
+
media: mediaRefSchema.required(),
|
|
58
|
+
name: Schema.string(),
|
|
59
|
+
}).required(),
|
|
60
|
+
platform: platformSchema,
|
|
61
|
+
});
|
|
35
62
|
export const replySegmentSchema = Schema.object({
|
|
36
63
|
type: Schema.const('reply'),
|
|
37
64
|
data: Schema.object({ message_id: Schema.string().required() }).required(),
|
|
@@ -87,6 +114,28 @@ export const canonicalSegmentSchema = Schema.discriminatedUnion('type', {
|
|
|
87
114
|
}).required(),
|
|
88
115
|
platform: platformSchema,
|
|
89
116
|
},
|
|
117
|
+
audio: {
|
|
118
|
+
data: Schema.object({
|
|
119
|
+
media: mediaRefSchema.required(),
|
|
120
|
+
duration: Schema.number(),
|
|
121
|
+
}).required(),
|
|
122
|
+
platform: platformSchema,
|
|
123
|
+
},
|
|
124
|
+
video: {
|
|
125
|
+
data: Schema.object({
|
|
126
|
+
media: mediaRefSchema.required(),
|
|
127
|
+
duration: Schema.number(),
|
|
128
|
+
alt: Schema.string(),
|
|
129
|
+
}).required(),
|
|
130
|
+
platform: platformSchema,
|
|
131
|
+
},
|
|
132
|
+
file: {
|
|
133
|
+
data: Schema.object({
|
|
134
|
+
media: mediaRefSchema.required(),
|
|
135
|
+
name: Schema.string(),
|
|
136
|
+
}).required(),
|
|
137
|
+
platform: platformSchema,
|
|
138
|
+
},
|
|
90
139
|
reply: {
|
|
91
140
|
data: Schema.object({ message_id: Schema.string().required() }).required(),
|
|
92
141
|
platform: platformSchema,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CapabilityId, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { ConversationRef, DeliveryReceipt, MessageRef } from '@zhin.js/im-contract';
|
|
2
3
|
import type { MediaRef, Segment } from '../../built/segment-contract/types.js';
|
|
3
4
|
export type { MediaRef, Segment };
|
|
4
5
|
declare const componentCallBrand: "zhin.component-call/1";
|
|
@@ -12,13 +13,23 @@ export interface RawContent<TPayload = unknown> {
|
|
|
12
13
|
readonly $content: typeof rawContentBrand;
|
|
13
14
|
readonly payload: TPayload;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 出站内容:纯文本 / canonical Segment(一等公民,媒体与富文本的统一表达)/
|
|
18
|
+
* ComponentCall / RawContent,可任意嵌套数组。
|
|
19
|
+
*/
|
|
20
|
+
export type SendContent = string | Segment | ComponentCall | RawContent | readonly SendContent[];
|
|
16
21
|
export declare function component<TProps>(name: string, props: TProps): ComponentCall<TProps>;
|
|
17
22
|
export declare function raw<TPayload>(payload: TPayload): RawContent<TPayload>;
|
|
18
23
|
export declare function isComponentCall(value: SendContent): value is ComponentCall;
|
|
19
24
|
export declare function isRawContent(value: SendContent): value is RawContent;
|
|
25
|
+
/** canonical Segment 一等公民判定(与 ComponentCall/RawContent 的 $content brand 互斥)。 */
|
|
26
|
+
export declare function isSegmentContent(value: unknown): value is Segment;
|
|
20
27
|
export interface IncomingMessage {
|
|
21
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. */
|
|
32
|
+
readonly message?: MessageRef;
|
|
22
33
|
readonly target: string;
|
|
23
34
|
/**
|
|
24
35
|
* 纯文本视图:与 `segments` 同源(adapter 从同一份入站载荷派生二者)。
|
|
@@ -38,6 +49,8 @@ export interface IncomingMessage {
|
|
|
38
49
|
}
|
|
39
50
|
export interface SendRequest {
|
|
40
51
|
readonly adapter: CapabilityId;
|
|
52
|
+
/** Structured destination supplied to Adapter endpoints in parallel with target. */
|
|
53
|
+
readonly conversation?: ConversationRef;
|
|
41
54
|
readonly target: string;
|
|
42
55
|
readonly requester: PluginId;
|
|
43
56
|
readonly content: SendContent;
|
|
@@ -51,6 +64,7 @@ export interface ChannelParent {
|
|
|
51
64
|
}
|
|
52
65
|
export interface OutboundEnvelope {
|
|
53
66
|
readonly adapter: CapabilityId;
|
|
67
|
+
readonly conversation?: ConversationRef;
|
|
54
68
|
readonly target: string;
|
|
55
69
|
readonly requester: PluginId;
|
|
56
70
|
readonly generation: number;
|
|
@@ -60,6 +74,10 @@ export interface OutboundEnvelope {
|
|
|
60
74
|
}
|
|
61
75
|
export interface MessageGateway {
|
|
62
76
|
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
|
+
*/
|
|
63
81
|
send(request: SendRequest): Promise<unknown>;
|
|
64
82
|
/**
|
|
65
83
|
* 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
|
|
@@ -74,6 +92,10 @@ export interface MessageGateway {
|
|
|
74
92
|
*/
|
|
75
93
|
setUnmatchedHandler(handler: (message: Message, snapshot: RuntimeSnapshot, requester: PluginId) => Promise<boolean>): void;
|
|
76
94
|
}
|
|
95
|
+
/** Structured outbound gateway exposed by the Plugin Runtime. */
|
|
96
|
+
export interface DeliveryMessageGateway extends MessageGateway {
|
|
97
|
+
send(request: SendRequest): Promise<DeliveryReceipt>;
|
|
98
|
+
}
|
|
77
99
|
export interface MessageDispatchResult {
|
|
78
100
|
readonly matched: boolean;
|
|
79
101
|
readonly command?: string;
|
|
@@ -93,13 +115,21 @@ export declare class Message {
|
|
|
93
115
|
* Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
|
|
94
116
|
*/
|
|
95
117
|
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. */
|
|
121
|
+
readonly message?: MessageRef | undefined;
|
|
96
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>>,
|
|
97
123
|
/**
|
|
98
124
|
* 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
|
|
99
125
|
* Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
|
|
100
126
|
*/
|
|
101
|
-
segments?: readonly Segment[] | undefined
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
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. */
|
|
131
|
+
message?: MessageRef | undefined);
|
|
132
|
+
readonly $reply: (content: SendContent) => Promise<DeliveryReceipt>;
|
|
133
|
+
readonly $replyFrom: (requester: PluginId, content: SendContent) => Promise<DeliveryReceipt>;
|
|
104
134
|
}
|
|
105
135
|
export declare function createOutboundEnvelope(request: Omit<OutboundEnvelope, 'payload' | 'replace'>, initialPayload: unknown): OutboundEnvelope;
|
|
@@ -22,6 +22,16 @@ export function isRawContent(value) {
|
|
|
22
22
|
&& '$content' in value
|
|
23
23
|
&& value.$content === rawContentBrand;
|
|
24
24
|
}
|
|
25
|
+
/** canonical Segment 一等公民判定(与 ComponentCall/RawContent 的 $content brand 互斥)。 */
|
|
26
|
+
export function isSegmentContent(value) {
|
|
27
|
+
return !Array.isArray(value)
|
|
28
|
+
&& typeof value === 'object'
|
|
29
|
+
&& value !== null
|
|
30
|
+
&& !('$content' in value)
|
|
31
|
+
&& typeof value.type === 'string'
|
|
32
|
+
&& typeof value.data === 'object'
|
|
33
|
+
&& value.data !== null;
|
|
34
|
+
}
|
|
25
35
|
export class Message {
|
|
26
36
|
adapter;
|
|
27
37
|
target;
|
|
@@ -31,12 +41,22 @@ export class Message {
|
|
|
31
41
|
sender;
|
|
32
42
|
metadata;
|
|
33
43
|
segments;
|
|
34
|
-
|
|
44
|
+
conversation;
|
|
45
|
+
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({}),
|
|
35
51
|
/**
|
|
36
52
|
* 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
|
|
37
53
|
* Command dispatcher 优先使用此字段,以支持 mention、image 等结构化参数。
|
|
38
54
|
*/
|
|
39
|
-
segments
|
|
55
|
+
segments,
|
|
56
|
+
/** Structured inbound conversation when supplied by a migrated adapter. */
|
|
57
|
+
conversation,
|
|
58
|
+
/** Structured inbound message identity when supplied by a migrated adapter. */
|
|
59
|
+
message) {
|
|
40
60
|
this.adapter = adapter;
|
|
41
61
|
this.target = target;
|
|
42
62
|
this.content = content;
|
|
@@ -45,6 +65,8 @@ export class Message {
|
|
|
45
65
|
this.sender = sender;
|
|
46
66
|
this.metadata = metadata;
|
|
47
67
|
this.segments = segments;
|
|
68
|
+
this.conversation = conversation;
|
|
69
|
+
this.message = message;
|
|
48
70
|
this.$reply = (content) => reply(content);
|
|
49
71
|
this.$replyFrom = (requester, content) => reply(content, requester);
|
|
50
72
|
Object.freeze(this);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Scope, type CapabilityId, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
|
|
3
|
-
import {
|
|
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
5
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
5
6
|
import { type RuntimeInteractiveHandler } from './interactive.js';
|
|
6
|
-
export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<
|
|
7
|
+
export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<DeliveryMessageGateway>;
|
|
7
8
|
/** Console 实时消息事件(SSE 推送源;content 仅为截断预览,不含完整原始段)。 */
|
|
8
9
|
export interface RuntimeMessageEvent {
|
|
9
10
|
readonly direction: 'inbound' | 'outbound';
|
|
@@ -29,7 +30,7 @@ export interface ImRuntimeOptions {
|
|
|
29
30
|
readonly commandPrefix?: string;
|
|
30
31
|
readonly renderer?: OutboundRenderer;
|
|
31
32
|
}
|
|
32
|
-
export declare class ImRuntime implements
|
|
33
|
+
export declare class ImRuntime implements DeliveryMessageGateway {
|
|
33
34
|
#private;
|
|
34
35
|
constructor(options?: ImRuntimeOptions);
|
|
35
36
|
attach(snapshots: SnapshotStore): void;
|
|
@@ -51,7 +52,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
51
52
|
*/
|
|
52
53
|
onMessage(listener: (event: RuntimeMessageEvent) => void): () => void;
|
|
53
54
|
receive(input: IncomingMessage): Promise<MessageDispatchResult>;
|
|
54
|
-
send(request: SendRequest): Promise<
|
|
55
|
+
send(request: SendRequest): Promise<DeliveryReceipt>;
|
|
55
56
|
/** Console `endpoint.list` — empty until Adapter Feature projection is ready. */
|
|
56
57
|
listEndpoints(): readonly {
|
|
57
58
|
readonly name: string;
|
|
@@ -73,8 +74,9 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
73
74
|
sendEndpointMessage(input: {
|
|
74
75
|
readonly adapter: string;
|
|
75
76
|
readonly endpointId: string;
|
|
76
|
-
readonly
|
|
77
|
-
readonly
|
|
77
|
+
readonly conversation?: ConversationRef;
|
|
78
|
+
readonly channelId?: string;
|
|
79
|
+
readonly channelType?: string;
|
|
78
80
|
readonly content: unknown;
|
|
79
81
|
readonly parent?: ChannelParent;
|
|
80
82
|
}): Promise<{
|
|
@@ -84,7 +86,8 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
84
86
|
addEndpointReaction(input: {
|
|
85
87
|
readonly adapter: string;
|
|
86
88
|
readonly endpointId: string;
|
|
87
|
-
readonly
|
|
89
|
+
readonly message?: MessageRef;
|
|
90
|
+
readonly messageId?: string;
|
|
88
91
|
readonly emoji: string;
|
|
89
92
|
readonly sceneType?: string;
|
|
90
93
|
readonly channelId?: string;
|
|
@@ -92,14 +95,30 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
92
95
|
removeEndpointReaction(input: {
|
|
93
96
|
readonly adapter: string;
|
|
94
97
|
readonly endpointId: string;
|
|
95
|
-
readonly
|
|
98
|
+
readonly message?: MessageRef;
|
|
99
|
+
readonly messageId?: string;
|
|
96
100
|
readonly reactionId: string;
|
|
97
101
|
}): Promise<void>;
|
|
98
102
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
99
103
|
recallEndpointMessage(input: {
|
|
100
104
|
readonly adapter: string;
|
|
101
105
|
readonly endpointId: string;
|
|
102
|
-
readonly
|
|
106
|
+
readonly message?: MessageRef;
|
|
107
|
+
readonly messageId?: string;
|
|
108
|
+
}): Promise<void>;
|
|
109
|
+
editEndpointMessage(input: {
|
|
110
|
+
readonly adapter: string;
|
|
111
|
+
readonly endpointId: string;
|
|
112
|
+
readonly message?: MessageRef;
|
|
113
|
+
readonly messageId?: string;
|
|
114
|
+
readonly content: unknown;
|
|
115
|
+
}): Promise<string | null>;
|
|
116
|
+
setEndpointTyping(input: {
|
|
117
|
+
readonly adapter: string;
|
|
118
|
+
readonly endpointId: string;
|
|
119
|
+
readonly conversation?: ConversationRef;
|
|
120
|
+
readonly target?: string;
|
|
121
|
+
readonly active?: boolean;
|
|
103
122
|
}): Promise<void>;
|
|
104
123
|
/**
|
|
105
124
|
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { adapterFeatureId, isAdapterIndex, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
2
|
+
import { adapterFeatureId, isAdapterIndex, resolveEndpointControl, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
3
|
+
import { formatLegacyConversationRef, formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
|
|
3
4
|
import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
|
|
4
5
|
import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
|
|
5
6
|
import { Message, createOutboundEnvelope, } from './contracts.js';
|
|
6
7
|
import { defaultCommandPrefixResolver, MessageDispatcher } from './message-dispatcher.js';
|
|
7
8
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
8
9
|
import { applyOutboundInteractivePolicy, normalizeOutboundPayload, resolveOutboundInteractivePolicy, resolveOutboundMediaPolicy, } from './outbound-segments.js';
|
|
10
|
+
import { assertCanonicalSegments } from '../../built/segment-contract/assert.js';
|
|
9
11
|
import { keyboardFallbackStore } from '../../built/interactive-segments/fallback-store.js';
|
|
10
12
|
import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveChannelKey, } from './interactive.js';
|
|
11
13
|
const logger = getLogger('im');
|
|
@@ -92,11 +94,12 @@ export class ImRuntime {
|
|
|
92
94
|
throw new Error('Message reply scope has ended');
|
|
93
95
|
return this.#sendWithSnapshot({
|
|
94
96
|
adapter: input.adapter,
|
|
97
|
+
...(input.conversation ? { conversation: input.conversation } : {}),
|
|
95
98
|
target: input.target,
|
|
96
99
|
requester: replyRequester,
|
|
97
100
|
content,
|
|
98
101
|
}, lease.value);
|
|
99
|
-
}, input.id, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined);
|
|
102
|
+
}, input.id ?? input.message?.id, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.conversation, input.message);
|
|
100
103
|
let result = Object.freeze({ matched: false });
|
|
101
104
|
await runMiddleware(lease.value, message, async () => {
|
|
102
105
|
result = await this.#dispatchInteractive(message, requester)
|
|
@@ -130,7 +133,9 @@ export class ImRuntime {
|
|
|
130
133
|
? { channelType: channelTypeOf(input.target) }
|
|
131
134
|
: {}),
|
|
132
135
|
contentPreview: previewText(input.content),
|
|
133
|
-
...(input.id
|
|
136
|
+
...(input.id ?? input.message?.id
|
|
137
|
+
? { messageId: input.id ?? input.message?.id }
|
|
138
|
+
: {}),
|
|
134
139
|
timestamp: Date.now(),
|
|
135
140
|
});
|
|
136
141
|
return result;
|
|
@@ -210,16 +215,21 @@ export class ImRuntime {
|
|
|
210
215
|
const capabilityId = index.resolve(input.adapter, input.endpointId);
|
|
211
216
|
if (!capabilityId)
|
|
212
217
|
throw new Error('endpoint not found');
|
|
213
|
-
const target =
|
|
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');
|
|
214
223
|
const content = normalizeConsoleContent(input.content);
|
|
215
224
|
const result = await this.#sendWithSnapshot({
|
|
216
225
|
adapter: capabilityId,
|
|
226
|
+
...(input.conversation ? { conversation: input.conversation } : {}),
|
|
217
227
|
target,
|
|
218
228
|
requester: index.owner(capabilityId),
|
|
219
229
|
content,
|
|
220
230
|
...(input.parent ? { parent: input.parent } : {}),
|
|
221
231
|
}, lease.value);
|
|
222
|
-
return { messageId: result
|
|
232
|
+
return { messageId: result.message?.id ?? result.legacyMessageId ?? '' };
|
|
223
233
|
}
|
|
224
234
|
finally {
|
|
225
235
|
lease.release();
|
|
@@ -227,47 +237,33 @@ export class ImRuntime {
|
|
|
227
237
|
}
|
|
228
238
|
/** Activity-feedback: add a message reaction when the live Endpoint supports it. */
|
|
229
239
|
async addEndpointReaction(input) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
sceneType: input.sceneType,
|
|
236
|
-
channelId: input.channelId,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
if (typeof endpoint.$addReaction === 'function') {
|
|
240
|
-
return endpoint.$addReaction(input.messageId, input.emoji, {
|
|
241
|
-
sceneType: input.sceneType,
|
|
242
|
-
channelId: input.channelId,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
return null;
|
|
240
|
+
const control = this.#liveEndpointControl(input.adapter, input.endpointId);
|
|
241
|
+
return control?.addReaction?.(legacyMessageTarget(input.message, input.messageId), input.emoji, {
|
|
242
|
+
sceneType: input.sceneType,
|
|
243
|
+
channelId: input.channelId,
|
|
244
|
+
}) ?? null;
|
|
246
245
|
}
|
|
247
246
|
async removeEndpointReaction(input) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return;
|
|
251
|
-
if (typeof endpoint.removeReaction === 'function') {
|
|
252
|
-
await endpoint.removeReaction(input.messageId, input.reactionId);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (typeof endpoint.$removeReaction === 'function') {
|
|
256
|
-
await endpoint.$removeReaction(input.messageId, input.reactionId);
|
|
257
|
-
}
|
|
247
|
+
await this.#liveEndpointControl(input.adapter, input.endpointId)
|
|
248
|
+
?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
|
|
258
249
|
}
|
|
259
250
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
260
251
|
async recallEndpointMessage(input) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
252
|
+
await this.#liveEndpointControl(input.adapter, input.endpointId)
|
|
253
|
+
?.recall?.(legacyMessageTarget(input.message, input.messageId));
|
|
254
|
+
}
|
|
255
|
+
async editEndpointMessage(input) {
|
|
256
|
+
return this.#liveEndpointControl(input.adapter, input.endpointId)
|
|
257
|
+
?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
|
|
258
|
+
}
|
|
259
|
+
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);
|
|
271
267
|
}
|
|
272
268
|
#liveEndpoint(adapter, endpointId) {
|
|
273
269
|
try {
|
|
@@ -284,6 +280,10 @@ export class ImRuntime {
|
|
|
284
280
|
return null;
|
|
285
281
|
}
|
|
286
282
|
}
|
|
283
|
+
#liveEndpointControl(adapter, endpointId) {
|
|
284
|
+
const endpoint = this.#liveEndpoint(adapter, endpointId);
|
|
285
|
+
return resolveEndpointControl(endpoint) ?? null;
|
|
286
|
+
}
|
|
287
287
|
/**
|
|
288
288
|
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
289
289
|
* @deprecated Host callers should use `getEndpointManagement()`.
|
|
@@ -303,41 +303,72 @@ export class ImRuntime {
|
|
|
303
303
|
return resolveEndpointManagement(endpoint) ?? Object.freeze({});
|
|
304
304
|
}
|
|
305
305
|
async #sendWithSnapshot(request, snapshot) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// interactive 中央执行:'text' 端点 keyboard → 编号文本,fallback 映射写
|
|
315
|
-
// 中央存储(入站数字回跳解析用);'native' 端点透传 keyboard。
|
|
316
|
-
payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(request.adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveChannelKey(String(request.adapter), request.target), map));
|
|
306
|
+
let initialPayload;
|
|
307
|
+
try {
|
|
308
|
+
const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
|
|
309
|
+
initialPayload = await prepareOutboundPayload(rendered, request, snapshot);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return rejectedReceipt('outbound_payload_rejected');
|
|
313
|
+
}
|
|
317
314
|
const envelope = createOutboundEnvelope({
|
|
318
315
|
adapter: request.adapter,
|
|
316
|
+
...(request.conversation ? { conversation: request.conversation } : {}),
|
|
319
317
|
target: request.target,
|
|
320
318
|
requester: request.requester,
|
|
321
319
|
generation: snapshot.generation,
|
|
322
320
|
...(request.parent ? { parent: request.parent } : {}),
|
|
323
|
-
},
|
|
324
|
-
let
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
321
|
+
}, initialPayload);
|
|
322
|
+
let terminalEntered = false;
|
|
323
|
+
let receipt;
|
|
324
|
+
try {
|
|
325
|
+
await runMiddleware(snapshot, envelope, async () => {
|
|
326
|
+
terminalEntered = true;
|
|
327
|
+
let payload;
|
|
328
|
+
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);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
receipt = rejectedReceipt('outbound_payload_rejected');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const result = await requireAdapters(snapshot).send(request.adapter, {
|
|
339
|
+
target: request.target,
|
|
340
|
+
...(request.conversation ? { conversation: request.conversation } : {}),
|
|
341
|
+
payload,
|
|
342
|
+
...(request.parent ? { parent: request.parent } : {}),
|
|
343
|
+
});
|
|
344
|
+
receipt = receiptFromEndpointResult(result);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
receipt = receiptFromEndpointError(error);
|
|
348
|
+
}
|
|
349
|
+
if (receipt?.status === 'sent') {
|
|
350
|
+
this.#emitMessage({
|
|
351
|
+
direction: 'outbound',
|
|
352
|
+
adapter: request.adapter,
|
|
353
|
+
target: request.target,
|
|
354
|
+
requester: request.requester,
|
|
355
|
+
contentPreview: previewText(payload),
|
|
356
|
+
...(receipt.message?.id || receipt.legacyMessageId
|
|
357
|
+
? { messageId: receipt.message?.id ?? receipt.legacyMessageId }
|
|
358
|
+
: {}),
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}, 'outbound');
|
|
363
|
+
}
|
|
364
|
+
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
|
+
return receipt ?? failedReceipt('outbound_middleware_failed');
|
|
368
|
+
}
|
|
369
|
+
if (!terminalEntered)
|
|
370
|
+
return suppressedReceipt();
|
|
371
|
+
return receipt ?? failedReceipt('outbound_delivery_incomplete');
|
|
341
372
|
}
|
|
342
373
|
#acquire() {
|
|
343
374
|
if (!this.#snapshots)
|
|
@@ -385,6 +416,80 @@ function isDirectHtmlConsumer(snapshot, adapter) {
|
|
|
385
416
|
const owner = snapshot.capabilities.get(adapter)?.owner;
|
|
386
417
|
return adapterTypeName(snapshot.tree.get(owner)?.packageName) === 'sandbox';
|
|
387
418
|
}
|
|
419
|
+
function legacyMessageTarget(message, messageId) {
|
|
420
|
+
if (message)
|
|
421
|
+
return formatLegacyMessageRef(message);
|
|
422
|
+
if (messageId)
|
|
423
|
+
return messageId;
|
|
424
|
+
throw new TypeError('message or messageId is required');
|
|
425
|
+
}
|
|
426
|
+
async function prepareOutboundPayload(rendered, request, snapshot, finalizeInteractive = false) {
|
|
427
|
+
const directHtml = isDirectHtmlConsumer(snapshot, request.adapter);
|
|
428
|
+
let payload = directHtml
|
|
429
|
+
? rendered
|
|
430
|
+
: await normalizeOutboundPayload(rendered, resolveHtmlRenderer(snapshot), {
|
|
431
|
+
mediaPolicy: resolveOutboundMediaPolicy(request.adapter, snapshot),
|
|
432
|
+
});
|
|
433
|
+
if (finalizeInteractive) {
|
|
434
|
+
payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(request.adapter, snapshot), (map) => keyboardFallbackStore.remember(runtimeInteractiveChannelKey(String(request.adapter), request.target), map));
|
|
435
|
+
}
|
|
436
|
+
// Segment arrays crossing the adapter boundary are canonical after every
|
|
437
|
+
// middleware transform. Direct html consumers retain their explicit wire API.
|
|
438
|
+
if (!directHtml && Array.isArray(payload))
|
|
439
|
+
assertCanonicalSegments(payload);
|
|
440
|
+
return payload;
|
|
441
|
+
}
|
|
442
|
+
function receiptFromEndpointResult(result) {
|
|
443
|
+
if (isDeliveryReceipt(result))
|
|
444
|
+
return result;
|
|
445
|
+
const legacyMessageId = legacyMessageIdOf(result);
|
|
446
|
+
return Object.freeze({
|
|
447
|
+
status: 'sent',
|
|
448
|
+
...(legacyMessageId ? { legacyMessageId } : {}),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function legacyMessageIdOf(result) {
|
|
452
|
+
if (typeof result === 'string' || typeof result === 'number')
|
|
453
|
+
return String(result);
|
|
454
|
+
if (!result || typeof result !== 'object')
|
|
455
|
+
return undefined;
|
|
456
|
+
const id = result.id;
|
|
457
|
+
return typeof id === 'string' || typeof id === 'number' ? String(id) : undefined;
|
|
458
|
+
}
|
|
459
|
+
function receiptFromEndpointError(error) {
|
|
460
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
461
|
+
if (/Unknown Adapter Endpoint|does not support outbound/u.test(message)) {
|
|
462
|
+
return unsupportedReceipt('outbound_unsupported');
|
|
463
|
+
}
|
|
464
|
+
if (/not active/u.test(message))
|
|
465
|
+
return failedReceipt('endpoint_inactive', true);
|
|
466
|
+
return failedReceipt('endpoint_send_failed', true);
|
|
467
|
+
}
|
|
468
|
+
function suppressedReceipt() {
|
|
469
|
+
return Object.freeze({ status: 'suppressed' });
|
|
470
|
+
}
|
|
471
|
+
function unsupportedReceipt(code) {
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
status: 'unsupported',
|
|
474
|
+
failure: Object.freeze({ code, message: 'Outbound delivery is not supported.' }),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function rejectedReceipt(code) {
|
|
478
|
+
return Object.freeze({
|
|
479
|
+
status: 'rejected',
|
|
480
|
+
failure: Object.freeze({ code, message: 'Outbound payload was rejected.' }),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function failedReceipt(code, retryable = false) {
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
status: 'failed',
|
|
486
|
+
failure: Object.freeze({
|
|
487
|
+
code,
|
|
488
|
+
message: 'Outbound delivery failed.',
|
|
489
|
+
...(retryable ? { retryable: true } : {}),
|
|
490
|
+
}),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
388
493
|
/**
|
|
389
494
|
* Build Adapter send target. If channelId already carries a scene prefix
|
|
390
495
|
* (`private:uid` / `group:gid`), do not double-prefix.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { componentFeatureId, isComponentIndex, } from '@zhin.js/component';
|
|
2
|
-
import { isComponentCall, isRawContent, } from './contracts.js';
|
|
2
|
+
import { isComponentCall, isRawContent, isSegmentContent, } from './contracts.js';
|
|
3
3
|
const maxComponentDepth = 32;
|
|
4
4
|
export class OutboundRenderer {
|
|
5
5
|
async render(content, requester, snapshot) {
|
|
@@ -15,6 +15,9 @@ export class OutboundRenderer {
|
|
|
15
15
|
}
|
|
16
16
|
if (isRawContent(content))
|
|
17
17
|
return content.payload;
|
|
18
|
+
// canonical Segment 一等公民:原样透传,由下游 normalizeOutboundPayload 归一与协商
|
|
19
|
+
if (isSegmentContent(content))
|
|
20
|
+
return content;
|
|
18
21
|
if (isComponentCall(content)) {
|
|
19
22
|
const rendered = await requireComponents(snapshot).render(requester, content.name, content.props);
|
|
20
23
|
return this.#render(rendered, requester, snapshot, depth + 1);
|