@zhin.js/core 1.5.11 → 1.5.13
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 +2 -2
- package/lib/adapter.d.ts +3 -0
- package/lib/built/ai-trigger.d.ts +0 -2
- package/lib/built/ai-trigger.js +0 -1
- package/lib/built/inbound-pipeline.js +0 -1
- package/lib/built/inbound-runner.js +0 -2
- package/lib/built/login-assist.d.ts +22 -4
- package/lib/built/login-assist.js +71 -7
- package/lib/built/message-enrich.d.ts +0 -1
- package/lib/built/message-enrich.js +0 -1
- package/lib/built/rich-segments/markdown-to-text.js +2 -2
- package/lib/built/schema-endpoint-manager.js +6 -6
- package/lib/built/segment-contract/types.d.ts +4 -45
- package/lib/built/segment-contract/types.js +0 -4
- package/lib/built/user-interaction.d.ts +30 -0
- package/lib/built/user-interaction.js +248 -0
- package/lib/feature/handler.d.ts +1 -1
- package/lib/feature/handler.js +1 -1
- package/lib/im-scene.js +0 -2
- package/lib/index.d.ts +4 -3
- package/lib/index.js +3 -3
- package/lib/message.d.ts +0 -5
- package/lib/message.js +0 -13
- package/lib/plugin-runtime/im/im-runtime.d.ts +30 -8
- package/lib/plugin-runtime/im/im-runtime.js +471 -145
- package/lib/plugin-runtime/im/index.d.ts +2 -0
- package/lib/plugin-runtime/im/index.js +2 -0
- package/lib/plugin-runtime/im/login-assist-host.d.ts +6 -0
- package/lib/plugin-runtime/im/login-assist-host.js +6 -0
- package/lib/plugin-runtime/im/message-dispatcher.d.ts +2 -2
- package/lib/plugin-runtime/im/message-dispatcher.js +2 -2
- package/lib/plugin-runtime/im/outbound-segments.d.ts +4 -0
- package/lib/plugin-runtime/im/outbound-segments.js +26 -0
- package/lib/plugin-runtime/im/side-event-gateway.d.ts +13 -0
- package/lib/plugin-runtime/im/side-event-gateway.js +2 -0
- package/lib/plugin.d.ts +1 -0
- package/lib/{prompt.d.ts → schema-interaction.d.ts} +6 -6
- package/lib/{prompt.js → schema-interaction.js} +12 -12
- package/lib/side-event/base.d.ts +7 -0
- package/lib/side-event/dispatch.d.ts +15 -0
- package/lib/side-event/dispatch.js +140 -0
- package/lib/side-event/index.d.ts +1 -0
- package/lib/side-event/index.js +1 -0
- package/lib/side-event/normalize.d.ts +2 -0
- package/lib/side-event/normalize.js +4 -0
- package/lib/side-event/types.d.ts +4 -2
- package/lib/side-event/types.js +1 -1
- package/lib/system-event.d.ts +15 -0
- package/lib/system-event.js +7 -0
- package/lib/types.d.ts +0 -15
- package/package.json +11 -10
- package/lib/built/prepend-quote-context.d.ts +0 -34
- package/lib/built/prepend-quote-context.js +0 -130
- package/lib/message-quote.d.ts +0 -10
- package/lib/message-quote.js +0 -90
|
@@ -1,39 +1,97 @@
|
|
|
1
1
|
import { createToken, generationAdmissionBinder, htmlRendererToken, } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { createPermissionHost, permissionHostToken } from '@zhin.js/permission';
|
|
3
3
|
import { MessageBus, messageBusToken } from './message-bus.js';
|
|
4
|
-
import { adapterFeatureId, isAdapterIndex,
|
|
5
|
-
import {
|
|
4
|
+
import { adapterFeatureId, isAdapterIndex, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
5
|
+
import { MemoryConversationEventStore, conversationRefKey, messageRefKey, } from '@zhin.js/im-contract';
|
|
6
|
+
import { segmentsToPlainText } from '../../built/segment-contract/text.js';
|
|
6
7
|
import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
|
|
7
8
|
import { isHandlerIndex, handlerFeatureId } from '../../feature/handler.js';
|
|
8
9
|
import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
|
|
9
10
|
import { Message, createOutboundEnvelope, } from './contracts.js';
|
|
11
|
+
import { sideEventGatewayToken, } from './side-event-gateway.js';
|
|
12
|
+
import { loginAssistToken } from './login-assist-host.js';
|
|
13
|
+
import { LoginAssist } from '../../built/login-assist.js';
|
|
14
|
+
import { sideEventSendChannel } from '../../side-event/base.js';
|
|
10
15
|
import { defaultCommandPrefixResolver, MessageDispatcher } from './message-dispatcher.js';
|
|
11
16
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
12
|
-
import { applyOutboundInteractivePolicy, normalizeOutboundPayload, resolveOutboundInteractivePolicy, resolveOutboundMediaPolicy, } from './outbound-segments.js';
|
|
17
|
+
import { applyOutboundInteractivePolicy, applyOutboundMarkdownPolicy, normalizeOutboundPayload, resolveOutboundInteractivePolicy, resolveOutboundMarkdownPolicy, resolveOutboundMediaPolicy, } from './outbound-segments.js';
|
|
13
18
|
import { assertCanonicalSegments } from '../../built/segment-contract/assert.js';
|
|
14
19
|
import { keyboardFallbackStore } from '../../built/interactive-segments/fallback-store.js';
|
|
20
|
+
import { assertUserInteractionRequest, parseUserInteractionAnswer, projectUserInteraction, renderUserInteraction, } from '../../built/user-interaction.js';
|
|
15
21
|
import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveConversationKey, } from './interactive.js';
|
|
16
22
|
const logger = getLogger('im');
|
|
17
23
|
export const messageGatewayToken = createToken('zhin.im.message-gateway');
|
|
18
24
|
export const ingressRouteToken = createToken('zhin.im.ingress-route');
|
|
19
25
|
export const messagePreviewLimit = 200;
|
|
26
|
+
class UserInteractionTimeoutError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'UserInteractionTimeoutError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class UserInteractionSupersededError extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super('User interaction superseded');
|
|
35
|
+
this.name = 'UserInteractionSupersededError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class UserInteractionDeliveryError extends Error {
|
|
39
|
+
constructor(receipt) {
|
|
40
|
+
super(`User interaction delivery failed: ${receipt.failure?.code ?? receipt.status}`);
|
|
41
|
+
this.name = 'UserInteractionDeliveryError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
20
44
|
export class ImRuntime {
|
|
21
45
|
#dispatcher;
|
|
22
46
|
#renderer;
|
|
23
47
|
#messageListeners = new Set();
|
|
24
48
|
#interactiveHandlers = [];
|
|
25
|
-
#
|
|
49
|
+
#interactionClaims = new Map();
|
|
26
50
|
#snapshots;
|
|
27
51
|
#inboundClaim;
|
|
28
52
|
#enrichSender;
|
|
53
|
+
conversationEvents;
|
|
29
54
|
constructor(options = {}) {
|
|
30
55
|
this.#dispatcher = new MessageDispatcher(options.commandPrefix === undefined
|
|
31
56
|
? defaultCommandPrefixResolver
|
|
32
57
|
: () => options.commandPrefix ?? '');
|
|
33
58
|
this.#renderer = options.renderer ?? new OutboundRenderer();
|
|
59
|
+
this.conversationEvents = options.conversationEvents ?? new MemoryConversationEventStore();
|
|
34
60
|
this.#inboundClaim = options.inboundClaim;
|
|
35
61
|
this.#enrichSender = options.enrichSender;
|
|
36
62
|
}
|
|
63
|
+
/** Process composition replaces the bootstrap memory store after required DB activation. */
|
|
64
|
+
replaceConversationEventStore(store) {
|
|
65
|
+
this.conversationEvents = store;
|
|
66
|
+
}
|
|
67
|
+
async resolveConversationReference(lease, reference, context) {
|
|
68
|
+
if (!this.#snapshots?.owns(lease) || !lease.active) {
|
|
69
|
+
return Object.freeze({ status: 'expired', code: 'generation_lease_expired' });
|
|
70
|
+
}
|
|
71
|
+
if (reference.kind === 'message') {
|
|
72
|
+
const local = await this.conversationEvents.getMessage(reference.message);
|
|
73
|
+
if (local)
|
|
74
|
+
return Object.freeze({ status: 'resolved', reference, value: local });
|
|
75
|
+
}
|
|
76
|
+
context.signal.throwIfAborted();
|
|
77
|
+
const conversation = reference.kind === 'message' ? reference.message.conversation : reference.conversation;
|
|
78
|
+
return requireAdapters(lease.value).resolveContent(conversation.endpoint.id, reference, context);
|
|
79
|
+
}
|
|
80
|
+
async readConversationContext(conversation, consumer, throughSequence, limit = 50, excludeMessageId) {
|
|
81
|
+
if (!Number.isSafeInteger(throughSequence) || throughSequence < 0) {
|
|
82
|
+
throw new TypeError('Conversation context throughSequence must be a non-negative integer');
|
|
83
|
+
}
|
|
84
|
+
const cursor = await this.conversationEvents.getCursor(consumer, conversation);
|
|
85
|
+
const events = await this.conversationEvents.listBetween(conversation, cursor, throughSequence, limit);
|
|
86
|
+
const blocks = aggregateConversationContext(events, excludeMessageId);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
blocks: Object.freeze(blocks),
|
|
89
|
+
cursor: Math.max(cursor, throughSequence),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async commitConversationContext(conversation, consumer, cursor) {
|
|
93
|
+
await this.conversationEvents.commitCursor(consumer, conversation, cursor);
|
|
94
|
+
}
|
|
37
95
|
attach(snapshots) {
|
|
38
96
|
if (this.#snapshots && this.#snapshots !== snapshots) {
|
|
39
97
|
throw new Error('ImRuntime is already attached to another Root');
|
|
@@ -42,11 +100,36 @@ export class ImRuntime {
|
|
|
42
100
|
}
|
|
43
101
|
permissionHost = createPermissionHost();
|
|
44
102
|
messageBus = new MessageBus();
|
|
103
|
+
loginAssist = new LoginAssist();
|
|
45
104
|
install(resources) {
|
|
46
105
|
resources.provide(messageGatewayToken, this);
|
|
106
|
+
resources.provide(sideEventGatewayToken, this.#sideEventGateway);
|
|
107
|
+
resources.provide(loginAssistToken, this.loginAssist);
|
|
47
108
|
resources.provide(permissionHostToken, this.permissionHost);
|
|
48
109
|
resources.provide(messageBusToken, this.messageBus);
|
|
49
110
|
}
|
|
111
|
+
#sideEventGateway = (() => {
|
|
112
|
+
const self = this;
|
|
113
|
+
const gateway = {
|
|
114
|
+
receiveNotice: (notice) => self.receiveNotice(notice),
|
|
115
|
+
receiveRequest: (request) => self.receiveRequest(request),
|
|
116
|
+
receiveSystem: (event) => self.receiveSystem(event),
|
|
117
|
+
[generationAdmissionBinder](gate) {
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
receiveNotice: async (notice) => {
|
|
120
|
+
await gate.enter(() => self.receiveNotice(notice));
|
|
121
|
+
},
|
|
122
|
+
receiveRequest: async (request) => {
|
|
123
|
+
await gate.enter(() => self.receiveRequest(request));
|
|
124
|
+
},
|
|
125
|
+
receiveSystem: async (event) => {
|
|
126
|
+
await gate.enter(() => self.receiveSystem(event));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return gateway;
|
|
132
|
+
})();
|
|
50
133
|
[generationAdmissionBinder](gate) {
|
|
51
134
|
const gateway = {
|
|
52
135
|
receive: async (input) => gate.enter(() => this.#receive(input, gate))
|
|
@@ -74,18 +157,17 @@ export class ImRuntime {
|
|
|
74
157
|
};
|
|
75
158
|
}
|
|
76
159
|
// ==========================================================================
|
|
77
|
-
//
|
|
160
|
+
// User interaction claim — 命令/Agent 对话式交互
|
|
78
161
|
// ==========================================================================
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
return `${conv.endpoint.adapter}:${conv.endpoint.id}:${conv.kind}:${conv.id}:${subjectId}`;
|
|
162
|
+
#interactionConversationKey(message, subjectId = message.sender?.id ?? '') {
|
|
163
|
+
return `${conversationRefKey(message.conversation)}:${subjectId}`;
|
|
82
164
|
}
|
|
83
|
-
#
|
|
84
|
-
const key = this.#
|
|
85
|
-
const claim = this.#
|
|
165
|
+
#resolveInteractionClaim(message) {
|
|
166
|
+
const key = this.#interactionConversationKey(message);
|
|
167
|
+
const claim = this.#interactionClaims.get(key);
|
|
86
168
|
if (!claim)
|
|
87
169
|
return false;
|
|
88
|
-
claim.resolve(message.content);
|
|
170
|
+
claim.resolve(resolveRuntimeInteractivePayload(message) ?? message.content);
|
|
89
171
|
return true;
|
|
90
172
|
}
|
|
91
173
|
#claimNextMessage(message, timeout, timeoutText, signal, subjectId) {
|
|
@@ -94,146 +176,126 @@ export class ImRuntime {
|
|
|
94
176
|
reject(abortError(signal, timeoutText));
|
|
95
177
|
return;
|
|
96
178
|
}
|
|
97
|
-
const key = this.#
|
|
98
|
-
const existing = this.#
|
|
179
|
+
const key = this.#interactionConversationKey(message, subjectId);
|
|
180
|
+
const existing = this.#interactionClaims.get(key);
|
|
99
181
|
if (existing) {
|
|
100
182
|
clearTimeout(existing.timer);
|
|
101
|
-
existing.reject(new
|
|
183
|
+
existing.reject(new UserInteractionSupersededError());
|
|
102
184
|
}
|
|
103
185
|
const timer = setTimeout(() => {
|
|
104
|
-
settle(undefined, new
|
|
186
|
+
settle(undefined, new UserInteractionTimeoutError(timeoutText));
|
|
105
187
|
}, timeout);
|
|
106
188
|
const onAbort = () => settle(undefined, abortError(signal, timeoutText));
|
|
107
189
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
108
190
|
const settle = (value, error) => {
|
|
109
|
-
if (this.#
|
|
191
|
+
if (this.#interactionClaims.get(key) !== claim)
|
|
110
192
|
return;
|
|
111
|
-
this.#
|
|
193
|
+
this.#interactionClaims.delete(key);
|
|
112
194
|
clearTimeout(timer);
|
|
113
195
|
signal?.removeEventListener('abort', onAbort);
|
|
114
196
|
if (value !== undefined)
|
|
115
197
|
resolve(value);
|
|
116
198
|
else
|
|
117
|
-
reject(error ?? new
|
|
199
|
+
reject(error ?? new UserInteractionTimeoutError(timeoutText));
|
|
118
200
|
};
|
|
119
201
|
const claim = {
|
|
120
202
|
resolve: (raw) => settle(raw),
|
|
121
203
|
reject: (error) => settle(undefined, error),
|
|
122
204
|
timer,
|
|
123
205
|
};
|
|
124
|
-
this.#
|
|
206
|
+
this.#interactionClaims.set(key, claim);
|
|
125
207
|
});
|
|
126
208
|
}
|
|
127
|
-
#
|
|
209
|
+
#buildUserInteraction(message, subjectId) {
|
|
128
210
|
const DEFAULT_TIMEOUT = 3 * 60 * 1000;
|
|
129
211
|
const DEFAULT_TIMEOUT_TEXT = '输入超时';
|
|
130
212
|
const claim = (timeout, timeoutText, signal) => this.#claimNextMessage(message, timeout, timeoutText, signal, subjectId);
|
|
131
|
-
const reply = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
async number(tips, options) {
|
|
146
|
-
await reply(tips);
|
|
147
|
-
try {
|
|
148
|
-
const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
|
|
149
|
-
return +raw;
|
|
150
|
-
}
|
|
151
|
-
catch (e) {
|
|
152
|
-
if (options?.default !== undefined)
|
|
153
|
-
return options.default;
|
|
154
|
-
await reply(e.message);
|
|
155
|
-
throw e;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
async confirm(tips, options) {
|
|
159
|
-
const condition = options?.condition ?? 'yes';
|
|
160
|
-
await reply(`${tips}\n输入"${condition}"以确认`);
|
|
161
|
-
try {
|
|
162
|
-
const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
|
|
163
|
-
return raw === condition;
|
|
164
|
-
}
|
|
165
|
-
catch (e) {
|
|
166
|
-
if (options?.default !== undefined)
|
|
167
|
-
return options.default;
|
|
168
|
-
await reply(e.message);
|
|
169
|
-
throw e;
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
async list(tips, options) {
|
|
173
|
-
const separator = options?.separator ?? ',';
|
|
174
|
-
await reply(`${tips}\n值之间使用"${separator}"分隔`);
|
|
213
|
+
const reply = async (view) => {
|
|
214
|
+
const receipt = await message.$reply(renderUserInteraction(view));
|
|
215
|
+
if (receipt.status !== 'sent')
|
|
216
|
+
throw new UserInteractionDeliveryError(receipt);
|
|
217
|
+
};
|
|
218
|
+
const ask = async (request, progress) => {
|
|
219
|
+
assertUserInteractionRequest(request);
|
|
220
|
+
const timeout = request.timeout ?? DEFAULT_TIMEOUT;
|
|
221
|
+
const timeoutText = request.timeoutText ?? DEFAULT_TIMEOUT_TEXT;
|
|
222
|
+
const deadline = Date.now() + timeout;
|
|
223
|
+
const view = projectUserInteraction(request, progress);
|
|
224
|
+
await reply(view);
|
|
225
|
+
while (true) {
|
|
175
226
|
try {
|
|
176
|
-
const raw = await claim(
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return v;
|
|
227
|
+
const raw = await claim(Math.max(1, deadline - Date.now()), timeoutText, request.signal);
|
|
228
|
+
const result = parseUserInteractionAnswer(request, raw);
|
|
229
|
+
if (result.ok)
|
|
230
|
+
return result.value;
|
|
231
|
+
await reply({
|
|
232
|
+
...view,
|
|
233
|
+
tip: [request.invalidText ?? result.message, view.tip].filter(Boolean).join('\n'),
|
|
184
234
|
});
|
|
185
235
|
}
|
|
186
|
-
catch (
|
|
187
|
-
if (
|
|
188
|
-
return
|
|
189
|
-
await reply(e.message);
|
|
190
|
-
throw e;
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
async pick(tips, options) {
|
|
194
|
-
const items = options.options.map((o, i) => `${i + 1}.${o.label}`);
|
|
195
|
-
const separator = options.separator ?? ',';
|
|
196
|
-
if (options.multiple)
|
|
197
|
-
items.push(`多选请用"${separator}"分隔`);
|
|
198
|
-
await reply(`${tips}\n${items.join('\n')}`);
|
|
199
|
-
try {
|
|
200
|
-
const raw = await claim(options.timeout ?? DEFAULT_TIMEOUT, options.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options.signal);
|
|
201
|
-
if (!options.multiple) {
|
|
202
|
-
return options.options.find((_, i) => i + 1 === +raw)?.value;
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error instanceof UserInteractionTimeoutError && 'default' in request && request.default !== undefined) {
|
|
238
|
+
return request.default;
|
|
203
239
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
catch (e) {
|
|
210
|
-
if (options.default !== undefined)
|
|
211
|
-
return options.default;
|
|
212
|
-
await reply(e.message);
|
|
213
|
-
throw e;
|
|
240
|
+
if (error instanceof UserInteractionTimeoutError) {
|
|
241
|
+
await reply({ title: '交互已结束', description: error.message });
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
214
244
|
}
|
|
215
|
-
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const sequence = async (definition) => {
|
|
248
|
+
if (!definition.title.trim())
|
|
249
|
+
throw new TypeError('User interaction sequence title must not be empty');
|
|
250
|
+
const ids = new Set();
|
|
251
|
+
for (const step of definition.steps) {
|
|
252
|
+
if (!step.id.trim())
|
|
253
|
+
throw new TypeError('User interaction sequence step id must not be empty');
|
|
254
|
+
if (ids.has(step.id))
|
|
255
|
+
throw new TypeError(`Duplicate user interaction sequence step id: ${step.id}`);
|
|
256
|
+
ids.add(step.id);
|
|
257
|
+
}
|
|
258
|
+
const result = {};
|
|
259
|
+
for (let index = 0; index < definition.steps.length; index += 1) {
|
|
260
|
+
const step = definition.steps[index];
|
|
261
|
+
const request = {
|
|
262
|
+
...step,
|
|
263
|
+
timeout: step.timeout ?? definition.timeout,
|
|
264
|
+
timeoutText: step.timeoutText ?? definition.timeoutText,
|
|
265
|
+
invalidText: step.invalidText ?? definition.invalidText,
|
|
266
|
+
signal: step.signal ?? definition.signal,
|
|
267
|
+
};
|
|
268
|
+
result[step.id] = await ask(request, {
|
|
269
|
+
title: definition.title,
|
|
270
|
+
description: definition.description,
|
|
271
|
+
tip: definition.tip,
|
|
272
|
+
index: index + 1,
|
|
273
|
+
total: definition.steps.length,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return Object.freeze(result);
|
|
216
277
|
};
|
|
278
|
+
return Object.freeze({ ask, sequence });
|
|
217
279
|
}
|
|
218
280
|
/**
|
|
219
|
-
*
|
|
281
|
+
* User interaction bound to this conversation.
|
|
220
282
|
* `bind.subjectId` waits for that user (e.g. master) instead of the message sender.
|
|
221
283
|
*/
|
|
222
|
-
|
|
284
|
+
createInteraction(message, bind) {
|
|
223
285
|
const subjectId = bind?.subjectId?.trim();
|
|
224
286
|
if (bind && !subjectId)
|
|
225
287
|
return undefined;
|
|
226
288
|
if (typeof message.$reply !== 'function' || !message.conversation)
|
|
227
289
|
return undefined;
|
|
228
|
-
return this.#
|
|
290
|
+
return this.#buildUserInteraction(message, subjectId);
|
|
229
291
|
}
|
|
230
|
-
#
|
|
292
|
+
#createInteractionForSource(source) {
|
|
231
293
|
if (!source || typeof source !== 'object')
|
|
232
294
|
return undefined;
|
|
233
295
|
const msg = source;
|
|
234
296
|
if (typeof msg.$reply !== 'function' || !msg.conversation)
|
|
235
297
|
return undefined;
|
|
236
|
-
return this.#
|
|
298
|
+
return this.#buildUserInteraction(msg);
|
|
237
299
|
}
|
|
238
300
|
/**
|
|
239
301
|
* 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
|
|
@@ -293,24 +355,51 @@ export class ImRuntime {
|
|
|
293
355
|
},
|
|
294
356
|
}, lease.value);
|
|
295
357
|
}, enrichedSender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message, input.endpointId, input.mentioned, input.replyTo);
|
|
296
|
-
|
|
358
|
+
let conversationSequence;
|
|
359
|
+
if (input.message?.id) {
|
|
360
|
+
const appended = await this.conversationEvents.append(Object.freeze({
|
|
361
|
+
eventId: `message:${messageRefKey(input.message)}`,
|
|
362
|
+
conversation,
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
type: 'message.created',
|
|
365
|
+
message: Object.freeze({
|
|
366
|
+
ref: input.message,
|
|
367
|
+
...(enrichedSender ? { actor: Object.freeze({
|
|
368
|
+
id: enrichedSender.id,
|
|
369
|
+
...(enrichedSender.name ? { displayName: enrichedSender.name } : {}),
|
|
370
|
+
}) } : {}),
|
|
371
|
+
segments: Object.freeze(input.segments?.length
|
|
372
|
+
? [...input.segments]
|
|
373
|
+
: [{ type: 'text', data: { text: input.content } }]),
|
|
374
|
+
timestamp: Date.now(),
|
|
375
|
+
...(input.replyTo ? { replyTo: Object.freeze({ conversation, id: input.replyTo.id }) } : {}),
|
|
376
|
+
}),
|
|
377
|
+
}));
|
|
378
|
+
conversationSequence = appended.sequence;
|
|
379
|
+
}
|
|
297
380
|
let result = Object.freeze({ matched: false });
|
|
298
381
|
const claimed = await this.#inboundClaim?.(message) === true;
|
|
299
382
|
if (claimed) {
|
|
300
383
|
result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
|
|
301
384
|
}
|
|
302
|
-
else if (this.#
|
|
303
|
-
result = Object.freeze({ matched: true, command: '
|
|
385
|
+
else if (this.#resolveInteractionClaim(message)) {
|
|
386
|
+
result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
|
|
304
387
|
}
|
|
305
388
|
else {
|
|
306
|
-
const
|
|
389
|
+
const interactionFactory = (source) => this.#createInteractionForSource(source);
|
|
307
390
|
await runMiddleware(lease.value, message, async () => {
|
|
308
|
-
result = await this.#dispatchInteractive(message, requester, admission)
|
|
309
|
-
?? await this.#dispatcher.dispatch(message, lease.value, promptFactory);
|
|
310
391
|
const ingressRoute = resolveIngressRoute(lease.value);
|
|
392
|
+
const preRouted = await ingressRoute?.preRoute?.(message, lease, requester, conversationSequence) === true;
|
|
393
|
+
if (preRouted) {
|
|
394
|
+
result = Object.freeze({ matched: true, command: 'pre-route', owner: requester });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
await this.#runHandlers(lease.value, 'message.receive', [message]);
|
|
398
|
+
result = await this.#dispatchInteractive(message, requester, admission)
|
|
399
|
+
?? await this.#dispatcher.dispatch(message, lease.value, interactionFactory);
|
|
311
400
|
if (!result.matched && ingressRoute) {
|
|
312
401
|
logger.debug(formatCompact({ op: 'unmatched', conv: formatConversationLog(conversation) }));
|
|
313
|
-
const handled = await ingressRoute.route(message, lease, requester);
|
|
402
|
+
const handled = await ingressRoute.route(message, lease, requester, conversationSequence);
|
|
314
403
|
if (handled) {
|
|
315
404
|
result = Object.freeze({ matched: true, command: 'ai', owner: requester });
|
|
316
405
|
}
|
|
@@ -348,12 +437,108 @@ export class ImRuntime {
|
|
|
348
437
|
lease.release();
|
|
349
438
|
}
|
|
350
439
|
}
|
|
440
|
+
async receiveNotice(notice) {
|
|
441
|
+
const event = conversationEventFromNotice(notice);
|
|
442
|
+
if (event)
|
|
443
|
+
await this.conversationEvents.append(event);
|
|
444
|
+
await this.#receiveSideEvent('notice.receive', notice);
|
|
445
|
+
}
|
|
446
|
+
async receiveRequest(request) {
|
|
447
|
+
await this.#withRequestActionScope(request, async (scoped) => {
|
|
448
|
+
await this.#receiveSideEvent('request.receive', scoped);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async #withRequestActionScope(request, dispatch) {
|
|
452
|
+
let active = true;
|
|
453
|
+
const actions = new Set();
|
|
454
|
+
const run = (action) => {
|
|
455
|
+
if (!active)
|
|
456
|
+
throw new Error('Request action port expired with its generation operation');
|
|
457
|
+
const operation = Promise.resolve().then(action);
|
|
458
|
+
actions.add(operation);
|
|
459
|
+
void operation.then(() => actions.delete(operation), () => actions.delete(operation));
|
|
460
|
+
return operation;
|
|
461
|
+
};
|
|
462
|
+
const scoped = Object.assign(Object.create(Object.getPrototypeOf(request)), request, {
|
|
463
|
+
$approve: async (remark) => run(() => request.$approve(remark)),
|
|
464
|
+
$reject: async (reason) => run(() => request.$reject(reason)),
|
|
465
|
+
});
|
|
466
|
+
try {
|
|
467
|
+
await dispatch(scoped);
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
active = false;
|
|
471
|
+
await Promise.allSettled([...actions]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async receiveSystem(event) {
|
|
475
|
+
await this.#receiveSideEvent('system.receive', event);
|
|
476
|
+
}
|
|
477
|
+
async #receiveSideEvent(event, payload) {
|
|
478
|
+
const lease = this.#acquire();
|
|
479
|
+
try {
|
|
480
|
+
await this.#runHandlers(lease.value, event, [payload]);
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
lease.release();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* User interaction bound to a side-event scene (private/group channel derived from
|
|
488
|
+
* `$scene_type` / `$scene_id`). Returns undefined when outbound is unavailable.
|
|
489
|
+
*/
|
|
490
|
+
#createInteractionForSideEvent(payload, snapshot) {
|
|
491
|
+
const adapter = String(payload.$adapter);
|
|
492
|
+
const endpointKey = String(payload.$endpoint);
|
|
493
|
+
if (!adapter || !endpointKey)
|
|
494
|
+
return undefined;
|
|
495
|
+
const channel = sideEventSendChannel(payload);
|
|
496
|
+
const conversation = Object.freeze({
|
|
497
|
+
endpoint: Object.freeze({ adapter, id: endpointKey }),
|
|
498
|
+
kind: channel.type,
|
|
499
|
+
id: channel.id || endpointKey,
|
|
500
|
+
});
|
|
501
|
+
const requester = snapshot.root;
|
|
502
|
+
const source = Object.freeze({
|
|
503
|
+
conversation,
|
|
504
|
+
...(payload.$actor?.id
|
|
505
|
+
? { sender: Object.freeze({ id: payload.$actor.id }) }
|
|
506
|
+
: {}),
|
|
507
|
+
$reply: (content) => this.#sendWithSnapshot({
|
|
508
|
+
conversation,
|
|
509
|
+
requester,
|
|
510
|
+
content,
|
|
511
|
+
}, snapshot),
|
|
512
|
+
});
|
|
513
|
+
return this.#buildUserInteraction(source);
|
|
514
|
+
}
|
|
515
|
+
async #runHandlers(snapshot, event, args) {
|
|
516
|
+
const index = handlers(snapshot);
|
|
517
|
+
if (!index)
|
|
518
|
+
return;
|
|
519
|
+
const options = {
|
|
520
|
+
resolveInteraction: (name, interactionArgs) => {
|
|
521
|
+
const payload = interactionArgs[0];
|
|
522
|
+
if (name === 'message.receive' && payload instanceof Message) {
|
|
523
|
+
return this.createInteraction(payload);
|
|
524
|
+
}
|
|
525
|
+
if (name === 'notice.receive'
|
|
526
|
+
|| name === 'request.receive'
|
|
527
|
+
|| name === 'system.receive') {
|
|
528
|
+
return this.#createInteractionForSideEvent(payload, snapshot);
|
|
529
|
+
}
|
|
530
|
+
return undefined;
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
await index.dispatch(event, args, options);
|
|
534
|
+
}
|
|
351
535
|
/** Console `endpoint.list` — empty until Adapter Feature projection is ready. */
|
|
352
536
|
listEndpoints() {
|
|
353
537
|
try {
|
|
354
538
|
const lease = this.#acquire();
|
|
355
539
|
try {
|
|
356
540
|
return requireAdapters(lease.value).describe().map((row) => Object.freeze({
|
|
541
|
+
id: String(row.id),
|
|
357
542
|
name: row.name,
|
|
358
543
|
// adapter 列显示平台类型(owner 包名去 scope/adapter- 前缀),不是 slot localName
|
|
359
544
|
adapter: adapterTypeName(lease.value.tree.get(row.owner)?.packageName) ?? row.name,
|
|
@@ -361,6 +546,7 @@ export class ImRuntime {
|
|
|
361
546
|
connected: row.connected,
|
|
362
547
|
status: row.status,
|
|
363
548
|
phase: row.phase,
|
|
549
|
+
operations: row.operations,
|
|
364
550
|
managementCapabilities: row.managementCapabilities,
|
|
365
551
|
}));
|
|
366
552
|
}
|
|
@@ -421,6 +607,7 @@ export class ImRuntime {
|
|
|
421
607
|
connected: row.connected,
|
|
422
608
|
status: row.status,
|
|
423
609
|
phase: row.phase,
|
|
610
|
+
operations: row.operations,
|
|
424
611
|
managementCapabilities: row.managementCapabilities,
|
|
425
612
|
});
|
|
426
613
|
}
|
|
@@ -450,7 +637,7 @@ export class ImRuntime {
|
|
|
450
637
|
requester,
|
|
451
638
|
content,
|
|
452
639
|
}, lease.value);
|
|
453
|
-
return { messageId: result.message?.id ??
|
|
640
|
+
return { messageId: result.message?.id ?? '' };
|
|
454
641
|
}
|
|
455
642
|
finally {
|
|
456
643
|
lease.release();
|
|
@@ -458,25 +645,25 @@ export class ImRuntime {
|
|
|
458
645
|
}
|
|
459
646
|
/** Activity-feedback: add a message reaction when the live Endpoint supports it. */
|
|
460
647
|
async addEndpointReaction(input) {
|
|
461
|
-
return this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.addReaction?.(input.message, input.emoji, {
|
|
648
|
+
return this.#withEndpointControl(input.adapter, input.endpointKey, 'reaction', (control) => control.addReaction?.(input.message, input.emoji, {
|
|
462
649
|
sceneType: input.sceneType,
|
|
463
650
|
channelId: input.channelId,
|
|
464
651
|
}) ?? null, null);
|
|
465
652
|
}
|
|
466
653
|
async removeEndpointReaction(input) {
|
|
467
|
-
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.removeReaction?.(input.message, input.reactionId), undefined);
|
|
654
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, 'reaction', (control) => control.removeReaction?.(input.message, input.reactionId), undefined);
|
|
468
655
|
}
|
|
469
656
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
470
657
|
async recallEndpointMessage(input) {
|
|
471
|
-
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.recall?.(input.message), undefined);
|
|
658
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, 'recall', (control) => control.recall?.(input.message), undefined);
|
|
472
659
|
}
|
|
473
660
|
async editEndpointMessage(input) {
|
|
474
|
-
return this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.edit?.(input.message, input.content) ?? null, null);
|
|
661
|
+
return this.#withEndpointControl(input.adapter, input.endpointKey, 'edit', (control) => control.edit?.(input.message, input.content) ?? null, null);
|
|
475
662
|
}
|
|
476
663
|
async setEndpointTyping(input) {
|
|
477
|
-
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.typing?.(input.conversation, input.active), undefined);
|
|
664
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, 'typing', (control) => control.typing?.(input.conversation, input.active), undefined);
|
|
478
665
|
}
|
|
479
|
-
async #withEndpointControl(adapter, endpointKey, run, fallback) {
|
|
666
|
+
async #withEndpointControl(adapter, endpointKey, operation, run, fallback) {
|
|
480
667
|
let lease;
|
|
481
668
|
try {
|
|
482
669
|
lease = this.#acquire();
|
|
@@ -485,8 +672,9 @@ export class ImRuntime {
|
|
|
485
672
|
return fallback;
|
|
486
673
|
}
|
|
487
674
|
try {
|
|
488
|
-
const
|
|
489
|
-
const
|
|
675
|
+
const index = requireAdapters(lease.value);
|
|
676
|
+
const id = index.resolve(adapter, endpointKey);
|
|
677
|
+
const control = id ? index.control(id, operation) : undefined;
|
|
490
678
|
return control ? await run(control) : fallback;
|
|
491
679
|
}
|
|
492
680
|
finally {
|
|
@@ -545,19 +733,33 @@ export class ImRuntime {
|
|
|
545
733
|
conversation: request.conversation,
|
|
546
734
|
payload,
|
|
547
735
|
});
|
|
548
|
-
receipt = receiptFromEndpointResult(result);
|
|
736
|
+
receipt = receiptFromEndpointResult(result, request.conversation);
|
|
549
737
|
}
|
|
550
738
|
catch (error) {
|
|
551
739
|
receipt = receiptFromEndpointError(error);
|
|
552
740
|
}
|
|
553
741
|
if (receipt?.status === 'sent') {
|
|
742
|
+
if (receipt.message?.id) {
|
|
743
|
+
const segments = conversationSegmentsFromContent(request.content);
|
|
744
|
+
await this.conversationEvents.append(Object.freeze({
|
|
745
|
+
eventId: `message:${messageRefKey(receipt.message)}`,
|
|
746
|
+
conversation: request.conversation,
|
|
747
|
+
timestamp: Date.now(),
|
|
748
|
+
type: 'message.created',
|
|
749
|
+
message: Object.freeze({
|
|
750
|
+
ref: receipt.message,
|
|
751
|
+
segments,
|
|
752
|
+
timestamp: Date.now(),
|
|
753
|
+
}),
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
554
756
|
this.#emitMessage({
|
|
555
757
|
direction: 'outbound',
|
|
556
758
|
conversation: request.conversation,
|
|
557
759
|
requester: request.requester,
|
|
558
760
|
contentPreview: previewText(payload),
|
|
559
|
-
...(receipt.message?.id
|
|
560
|
-
? { messageId: receipt.message
|
|
761
|
+
...(receipt.message?.id
|
|
762
|
+
? { messageId: receipt.message.id }
|
|
561
763
|
: {}),
|
|
562
764
|
timestamp: Date.now(),
|
|
563
765
|
});
|
|
@@ -608,6 +810,133 @@ export class ImRuntime {
|
|
|
608
810
|
: undefined;
|
|
609
811
|
}
|
|
610
812
|
}
|
|
813
|
+
function conversationEventFromNotice(notice) {
|
|
814
|
+
const sceneType = notice.$scene_type;
|
|
815
|
+
const kind = sceneType === 'group' ? 'group'
|
|
816
|
+
: sceneType === 'channel' ? 'channel'
|
|
817
|
+
: sceneType === 'friend' || sceneType === 'private' ? 'private'
|
|
818
|
+
: undefined;
|
|
819
|
+
if (!kind || !notice.$scene_id || !notice.$id)
|
|
820
|
+
return undefined;
|
|
821
|
+
const conversation = Object.freeze({
|
|
822
|
+
endpoint: Object.freeze({ adapter: String(notice.$adapter), id: String(notice.$endpoint) }),
|
|
823
|
+
kind,
|
|
824
|
+
id: String(notice.$scene_id),
|
|
825
|
+
});
|
|
826
|
+
const actor = notice.$actor?.id
|
|
827
|
+
? Object.freeze({ id: String(notice.$actor.id), ...(notice.$actor.name ? { displayName: notice.$actor.name } : {}) })
|
|
828
|
+
: undefined;
|
|
829
|
+
const target = notice.$target?.id
|
|
830
|
+
? Object.freeze({ id: String(notice.$target.id), ...(notice.$target.name ? { displayName: notice.$target.name } : {}) })
|
|
831
|
+
: undefined;
|
|
832
|
+
const base = Object.freeze({
|
|
833
|
+
eventId: `notice:${conversationRefKey(conversation)}:${String(notice.$id)}`,
|
|
834
|
+
conversation,
|
|
835
|
+
timestamp: notice.$timestamp,
|
|
836
|
+
});
|
|
837
|
+
switch (notice.$sub_type) {
|
|
838
|
+
case 'member_increase':
|
|
839
|
+
case 'increase':
|
|
840
|
+
return target ? Object.freeze({ ...base, type: 'member.joined', member: target, ...(actor ? { actor } : {}) }) : undefined;
|
|
841
|
+
case 'member_decrease':
|
|
842
|
+
return target ? Object.freeze({ ...base, type: 'member.left', member: target, ...(actor ? { actor } : {}), reason: actor ? 'removed' : 'left' }) : undefined;
|
|
843
|
+
case 'ban':
|
|
844
|
+
if (!target)
|
|
845
|
+
return undefined;
|
|
846
|
+
return notice.$duration_seconds === 0
|
|
847
|
+
? Object.freeze({ ...base, type: 'member.unmuted', member: target, ...(actor ? { actor } : {}) })
|
|
848
|
+
: Object.freeze({ ...base, type: 'member.muted', member: target, ...(actor ? { actor } : {}), durationSeconds: notice.$duration_seconds ?? 0 });
|
|
849
|
+
case 'admin_change':
|
|
850
|
+
return target && notice.$role && typeof notice.$enabled === 'boolean'
|
|
851
|
+
? Object.freeze({ ...base, type: 'member.role_changed', member: target, ...(actor ? { actor } : {}), role: notice.$role, enabled: notice.$enabled })
|
|
852
|
+
: undefined;
|
|
853
|
+
case 'recall':
|
|
854
|
+
return notice.$message_id
|
|
855
|
+
? Object.freeze({ ...base, type: 'message.recalled', message: Object.freeze({ conversation, id: notice.$message_id }), ...(actor ? { actor } : {}) })
|
|
856
|
+
: undefined;
|
|
857
|
+
case 'emoji_reaction':
|
|
858
|
+
return notice.$message_id && notice.$reaction && notice.$operation
|
|
859
|
+
? Object.freeze({ ...base, type: 'message.reaction_changed', message: Object.freeze({ conversation, id: notice.$message_id }), ...(actor ? { actor } : {}), reaction: notice.$reaction, operation: notice.$operation })
|
|
860
|
+
: undefined;
|
|
861
|
+
case 'poke':
|
|
862
|
+
return Object.freeze({ ...base, type: 'conversation.poked', ...(actor ? { actor } : {}), ...(target ? { target } : {}) });
|
|
863
|
+
default:
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function describeConversationEvent(event) {
|
|
868
|
+
const actor = 'actor' in event && event.actor ? `${event.actor.displayName ?? event.actor.id} (${event.actor.id})` : undefined;
|
|
869
|
+
switch (event.type) {
|
|
870
|
+
case 'message.recalled': return `${actor ?? 'Someone'} recalled message ${event.message.id}.`;
|
|
871
|
+
case 'message.reaction_changed': return `${actor ?? 'Someone'} ${event.operation} reaction ${event.reaction} on message ${event.message.id}.`;
|
|
872
|
+
case 'conversation.poked': return `${actor ?? 'Someone'} poked ${event.target ? `${event.target.displayName ?? event.target.id} (${event.target.id})` : 'someone'}.`;
|
|
873
|
+
case 'member.joined': return `${event.member.displayName ?? event.member.id} (${event.member.id}) joined the conversation.`;
|
|
874
|
+
case 'member.left': return `${event.member.displayName ?? event.member.id} (${event.member.id}) left the conversation (${event.reason ?? 'left'}).`;
|
|
875
|
+
case 'member.muted': return `${event.member.displayName ?? event.member.id} (${event.member.id}) was muted for ${event.durationSeconds} seconds${actor ? ` by ${actor}` : ''}.`;
|
|
876
|
+
case 'member.unmuted': return `${event.member.displayName ?? event.member.id} (${event.member.id}) was unmuted${actor ? ` by ${actor}` : ''}.`;
|
|
877
|
+
case 'member.role_changed': return `${event.member.displayName ?? event.member.id} (${event.member.id}) role ${event.role} was ${event.enabled ? 'enabled' : 'disabled'}${actor ? ` by ${actor}` : ''}.`;
|
|
878
|
+
case 'message.created': return '';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function describeConversationMessage(message) {
|
|
882
|
+
const actor = message.actor
|
|
883
|
+
? `${message.actor.displayName ?? message.actor.id} (${message.actor.id})`
|
|
884
|
+
: undefined;
|
|
885
|
+
if (!actor)
|
|
886
|
+
return '';
|
|
887
|
+
const text = segmentsToPlainText(message.segments).trim();
|
|
888
|
+
const attachmentTypes = [...new Set(message.segments
|
|
889
|
+
.filter((segment) => segment.type !== 'text' && segment.type !== 'mention' && segment.type !== 'at')
|
|
890
|
+
.map((segment) => segment.type))];
|
|
891
|
+
const attachmentNote = attachmentTypes.length > 0
|
|
892
|
+
? ` [message also contains ${attachmentTypes.join(', ')} data; inspect its reference before relying on that content]`
|
|
893
|
+
: '';
|
|
894
|
+
return `${actor}: ${text || '(no plain-text content)'}${attachmentNote}`;
|
|
895
|
+
}
|
|
896
|
+
function aggregateConversationContext(events, excludeMessageId) {
|
|
897
|
+
const ordinary = [];
|
|
898
|
+
const noisy = new Map();
|
|
899
|
+
for (const { sequence, event } of events) {
|
|
900
|
+
if (event.type === 'message.created') {
|
|
901
|
+
if (event.message.ref.id === excludeMessageId)
|
|
902
|
+
continue;
|
|
903
|
+
const text = describeConversationMessage(event.message);
|
|
904
|
+
if (!text)
|
|
905
|
+
continue;
|
|
906
|
+
ordinary.push(Object.freeze({
|
|
907
|
+
kind: 'conversation_event',
|
|
908
|
+
sequence,
|
|
909
|
+
eventType: event.type,
|
|
910
|
+
text,
|
|
911
|
+
}));
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (event.type === 'message.reaction_changed' || event.type === 'conversation.poked') {
|
|
915
|
+
const key = event.type === 'message.reaction_changed'
|
|
916
|
+
? `${event.type}:${event.message.id}:${event.actor?.id ?? ''}:${event.reaction}:${event.operation}`
|
|
917
|
+
: `${event.type}:${event.actor?.id ?? ''}:${event.target?.id ?? ''}`;
|
|
918
|
+
const previous = noisy.get(key);
|
|
919
|
+
noisy.set(key, { sequence, event, count: (previous?.count ?? 0) + 1 });
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
ordinary.push(Object.freeze({
|
|
923
|
+
kind: 'conversation_event',
|
|
924
|
+
sequence,
|
|
925
|
+
eventType: event.type,
|
|
926
|
+
text: describeConversationEvent(event),
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
for (const { sequence, event, count } of noisy.values()) {
|
|
930
|
+
ordinary.push(Object.freeze({
|
|
931
|
+
kind: 'conversation_event',
|
|
932
|
+
sequence,
|
|
933
|
+
eventType: event.type,
|
|
934
|
+
text: `${describeConversationEvent(event)}${count > 1 ? ` (${count} similar events.)` : ''}`,
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
ordinary.sort((left, right) => left.sequence - right.sequence);
|
|
938
|
+
return Object.freeze(ordinary);
|
|
939
|
+
}
|
|
611
940
|
function resolveIngressRoute(snapshot) {
|
|
612
941
|
const candidate = snapshot.resources.get(snapshot.root)?.get(ingressRouteToken.id);
|
|
613
942
|
return candidate
|
|
@@ -641,9 +970,10 @@ function isDirectHtmlConsumer(snapshot, adapter) {
|
|
|
641
970
|
async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
|
|
642
971
|
const adapter = conversation.endpoint.id;
|
|
643
972
|
const directHtml = isDirectHtmlConsumer(snapshot, adapter);
|
|
973
|
+
const markdownResolved = applyOutboundMarkdownPolicy(rendered, resolveOutboundMarkdownPolicy(adapter, snapshot));
|
|
644
974
|
let payload = directHtml
|
|
645
|
-
?
|
|
646
|
-
: await normalizeOutboundPayload(
|
|
975
|
+
? markdownResolved
|
|
976
|
+
: await normalizeOutboundPayload(markdownResolved, resolveHtmlRenderer(snapshot), {
|
|
647
977
|
mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
|
|
648
978
|
});
|
|
649
979
|
if (finalizeInteractive) {
|
|
@@ -653,23 +983,12 @@ async function prepareOutboundPayload(rendered, conversation, snapshot, finalize
|
|
|
653
983
|
assertCanonicalSegments(payload);
|
|
654
984
|
return payload;
|
|
655
985
|
}
|
|
656
|
-
function receiptFromEndpointResult(
|
|
657
|
-
if (isDeliveryReceipt(result))
|
|
658
|
-
return result;
|
|
659
|
-
const legacyMessageId = legacyMessageIdOf(result);
|
|
986
|
+
function receiptFromEndpointResult(messageId, conversation) {
|
|
660
987
|
return Object.freeze({
|
|
661
988
|
status: 'sent',
|
|
662
|
-
|
|
989
|
+
message: Object.freeze({ conversation, id: messageId }),
|
|
663
990
|
});
|
|
664
991
|
}
|
|
665
|
-
function legacyMessageIdOf(result) {
|
|
666
|
-
if (typeof result === 'string' || typeof result === 'number')
|
|
667
|
-
return String(result);
|
|
668
|
-
if (!result || typeof result !== 'object')
|
|
669
|
-
return undefined;
|
|
670
|
-
const id = result.id;
|
|
671
|
-
return typeof id === 'string' || typeof id === 'number' ? String(id) : undefined;
|
|
672
|
-
}
|
|
673
992
|
function receiptFromEndpointError(error) {
|
|
674
993
|
const message = error instanceof Error ? error.message : String(error);
|
|
675
994
|
if (/Unknown Adapter Endpoint|does not support outbound/u.test(message)) {
|
|
@@ -725,11 +1044,6 @@ function handlers(snapshot) {
|
|
|
725
1044
|
const projection = snapshot.projections.get(handlerFeatureId);
|
|
726
1045
|
return isHandlerIndex(projection) ? projection : undefined;
|
|
727
1046
|
}
|
|
728
|
-
async function runHandlers(snapshot, event, ...args) {
|
|
729
|
-
const index = handlers(snapshot);
|
|
730
|
-
if (index)
|
|
731
|
-
await index.dispatch(event, ...args);
|
|
732
|
-
}
|
|
733
1047
|
function normalizeConsoleContent(content) {
|
|
734
1048
|
if (typeof content === 'string')
|
|
735
1049
|
return content;
|
|
@@ -782,6 +1096,18 @@ function flattenContent(content) {
|
|
|
782
1096
|
}
|
|
783
1097
|
return String(content);
|
|
784
1098
|
}
|
|
1099
|
+
function conversationSegmentsFromContent(content) {
|
|
1100
|
+
if (Array.isArray(content)) {
|
|
1101
|
+
try {
|
|
1102
|
+
assertCanonicalSegments(content);
|
|
1103
|
+
return Object.freeze([...content]);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
// Fall through to a truthful text projection of non-canonical output.
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return Object.freeze([{ type: 'text', data: Object.freeze({ text: flattenContent(content) }) }]);
|
|
1110
|
+
}
|
|
785
1111
|
function abortError(signal, fallback) {
|
|
786
1112
|
if (signal?.reason instanceof Error)
|
|
787
1113
|
return signal.reason;
|