@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.
- package/README.md +6 -3
- package/lib/adapter.d.ts +7 -1
- package/lib/adapter.js +28 -24
- package/lib/agent-prompt.d.ts +2 -3
- package/lib/agent-prompt.js +5 -0
- package/lib/built/ai-outbound/parse.d.ts +1 -1
- package/lib/built/ai-outbound/parse.js +2 -6
- package/lib/built/ai-outbound/plain-mention-rewrite.js +4 -4
- package/lib/built/ai-outbound/resolve.js +8 -8
- package/lib/built/ai-outbound/types.d.ts +3 -3
- package/lib/built/ai-trigger.d.ts +5 -0
- package/lib/built/ai-trigger.js +40 -13
- package/lib/built/authorization.js +4 -4
- package/lib/built/command.d.ts +2 -2
- package/lib/built/command.js +2 -2
- package/lib/built/common-adapter-tools.d.ts +11 -11
- package/lib/built/common-adapter-tools.js +13 -13
- package/lib/built/config.d.ts +0 -4
- package/lib/built/config.js +0 -4
- package/lib/built/dispatcher.d.ts +2 -0
- package/lib/built/dispatcher.js +1 -1
- package/lib/built/endpoint-commands.js +2 -2
- package/lib/built/endpoint-lifecycle-service.js +13 -13
- package/lib/built/endpoint-lifecycle.d.ts +3 -3
- package/lib/built/endpoint-lifecycle.js +2 -2
- package/lib/built/endpoint-manager.d.ts +1 -1
- package/lib/built/interactive-segments/index.d.ts +1 -1
- package/lib/built/interactive-segments/index.js +1 -1
- package/lib/built/interactive-segments/keyboard-segment.d.ts +0 -1
- package/lib/built/interactive-segments/keyboard-segment.js +0 -1
- package/lib/built/interactive-segments/onebot-keyboard.d.ts +0 -3
- package/lib/built/interactive-segments/onebot-keyboard.js +0 -2
- package/lib/built/interactive-segments/resolve.d.ts +0 -3
- package/lib/built/interactive-segments/resolve.js +0 -3
- package/lib/built/interactive-segments/types.d.ts +8 -20
- package/lib/built/interactive-segments/types.js +0 -4
- package/lib/built/login-assist.d.ts +2 -2
- package/lib/built/login-assist.js +2 -2
- package/lib/built/message-enrich.d.ts +1 -1
- package/lib/built/message-enrich.js +2 -7
- package/lib/built/permission.d.ts +0 -4
- package/lib/built/permission.js +0 -4
- package/lib/built/prepend-quote-context.js +5 -5
- package/lib/built/rich-segments/capabilities.js +0 -1
- package/lib/built/rich-segments/html-segment.js +1 -4
- package/lib/built/rich-segments/markdown-segment.js +1 -4
- package/lib/built/rich-segments/types.d.ts +0 -5
- package/lib/built/schema-endpoint-manager.js +6 -6
- package/lib/built/tool.d.ts +5 -3
- package/lib/built/tool.js +14 -23
- package/lib/command.d.ts +4 -4
- package/lib/command.js +8 -11
- package/lib/endpoint-capabilities.d.ts +2 -3
- package/lib/endpoint-capabilities.js +4 -4
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +1 -0
- package/lib/errors.d.ts +2 -2
- package/lib/errors.js +4 -4
- package/lib/host-plugin-registry.d.ts +6 -5
- package/lib/host-plugin-registry.js +9 -4
- package/lib/im-scene.d.ts +1 -1
- package/lib/im-scene.js +5 -5
- package/lib/im-session-id.d.ts +2 -2
- package/lib/im-session-id.js +4 -4
- package/lib/index.d.ts +1 -2
- package/lib/index.js +0 -2
- package/lib/jsx-runtime.d.ts +1 -0
- package/lib/jsx.d.ts +19 -21
- package/lib/message.d.ts +1 -1
- package/lib/message.js +1 -1
- package/lib/notice.d.ts +2 -2
- package/lib/plugin-context.d.ts +11 -13
- package/lib/plugin-context.js +21 -18
- package/lib/plugin-runtime/im/contracts.d.ts +104 -60
- package/lib/plugin-runtime/im/contracts.js +87 -17
- package/lib/plugin-runtime/im/im-runtime.d.ts +38 -35
- package/lib/plugin-runtime/im/im-runtime.js +140 -131
- package/lib/plugin-runtime/im/index.d.ts +2 -0
- package/lib/plugin-runtime/im/index.js +2 -0
- 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-bus.d.ts +38 -0
- package/lib/plugin-runtime/im/message-bus.js +40 -0
- package/lib/plugin-runtime/im/message-dispatcher.js +14 -74
- package/lib/plugin-runtime/im/outbound-renderer.d.ts +3 -2
- package/lib/plugin-runtime/im/outbound-renderer.js +44 -8
- package/lib/plugin-runtime/im/service-tokens.d.ts +18 -0
- package/lib/plugin-runtime/im/service-tokens.js +13 -0
- package/lib/plugin.d.ts +4 -7
- package/lib/plugin.js +7 -30
- package/lib/request.d.ts +2 -2
- package/lib/side-event/types.d.ts +0 -4
- package/lib/types.d.ts +6 -11
- package/lib/utils.js +3 -1
- package/package.json +13 -12
- package/lib/built/qrcode-segment.d.ts +0 -18
- package/lib/built/qrcode-segment.js +0 -78
- package/lib/types-generator.d.ts +0 -5
- 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 {
|
|
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,
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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: '
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
216
|
-
if (!
|
|
249
|
+
const resolved = index.resolve(input.adapter, input.endpointKey);
|
|
250
|
+
if (!resolved)
|
|
217
251
|
throw new Error('endpoint not found');
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
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,
|
|
294
|
+
#liveEndpoint(adapter, endpointKey) {
|
|
269
295
|
try {
|
|
270
296
|
const lease = this.#acquire();
|
|
271
297
|
try {
|
|
272
|
-
const endpoint = requireAdapters(lease.value).instance(adapter,
|
|
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,
|
|
284
|
-
const endpoint = this.#liveEndpoint(adapter,
|
|
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,
|
|
300
|
-
const endpoint = this.#liveEndpoint(adapter,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
339
|
-
|
|
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
|
-
|
|
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,
|
|
427
|
-
const
|
|
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(
|
|
450
|
+
mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
|
|
432
451
|
});
|
|
433
452
|
if (finalizeInteractive) {
|
|
434
|
-
payload = applyOutboundInteractivePolicy(payload, resolveOutboundInteractivePolicy(
|
|
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
|
-
/**
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
|
|
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
|
-
/**
|
|
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 ?? []) {
|
|
@@ -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');
|