@zhin.js/core 1.5.11 → 1.5.12
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 +28 -8
- package/lib/plugin-runtime/im/im-runtime.js +459 -136
- 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
|
@@ -2,38 +2,96 @@ import { createToken, generationAdmissionBinder, htmlRendererToken, } from '@zhi
|
|
|
2
2
|
import { createPermissionHost, permissionHostToken } from '@zhin.js/permission';
|
|
3
3
|
import { MessageBus, messageBusToken } from './message-bus.js';
|
|
4
4
|
import { adapterFeatureId, isAdapterIndex, endpointControlOf, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
5
|
-
import {
|
|
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,
|
|
@@ -450,7 +635,7 @@ export class ImRuntime {
|
|
|
450
635
|
requester,
|
|
451
636
|
content,
|
|
452
637
|
}, lease.value);
|
|
453
|
-
return { messageId: result.message?.id ??
|
|
638
|
+
return { messageId: result.message?.id ?? '' };
|
|
454
639
|
}
|
|
455
640
|
finally {
|
|
456
641
|
lease.release();
|
|
@@ -545,19 +730,33 @@ export class ImRuntime {
|
|
|
545
730
|
conversation: request.conversation,
|
|
546
731
|
payload,
|
|
547
732
|
});
|
|
548
|
-
receipt = receiptFromEndpointResult(result);
|
|
733
|
+
receipt = receiptFromEndpointResult(result, request.conversation);
|
|
549
734
|
}
|
|
550
735
|
catch (error) {
|
|
551
736
|
receipt = receiptFromEndpointError(error);
|
|
552
737
|
}
|
|
553
738
|
if (receipt?.status === 'sent') {
|
|
739
|
+
if (receipt.message?.id) {
|
|
740
|
+
const segments = conversationSegmentsFromContent(request.content);
|
|
741
|
+
await this.conversationEvents.append(Object.freeze({
|
|
742
|
+
eventId: `message:${messageRefKey(receipt.message)}`,
|
|
743
|
+
conversation: request.conversation,
|
|
744
|
+
timestamp: Date.now(),
|
|
745
|
+
type: 'message.created',
|
|
746
|
+
message: Object.freeze({
|
|
747
|
+
ref: receipt.message,
|
|
748
|
+
segments,
|
|
749
|
+
timestamp: Date.now(),
|
|
750
|
+
}),
|
|
751
|
+
}));
|
|
752
|
+
}
|
|
554
753
|
this.#emitMessage({
|
|
555
754
|
direction: 'outbound',
|
|
556
755
|
conversation: request.conversation,
|
|
557
756
|
requester: request.requester,
|
|
558
757
|
contentPreview: previewText(payload),
|
|
559
|
-
...(receipt.message?.id
|
|
560
|
-
? { messageId: receipt.message
|
|
758
|
+
...(receipt.message?.id
|
|
759
|
+
? { messageId: receipt.message.id }
|
|
561
760
|
: {}),
|
|
562
761
|
timestamp: Date.now(),
|
|
563
762
|
});
|
|
@@ -608,6 +807,133 @@ export class ImRuntime {
|
|
|
608
807
|
: undefined;
|
|
609
808
|
}
|
|
610
809
|
}
|
|
810
|
+
function conversationEventFromNotice(notice) {
|
|
811
|
+
const sceneType = notice.$scene_type;
|
|
812
|
+
const kind = sceneType === 'group' ? 'group'
|
|
813
|
+
: sceneType === 'channel' ? 'channel'
|
|
814
|
+
: sceneType === 'friend' || sceneType === 'private' ? 'private'
|
|
815
|
+
: undefined;
|
|
816
|
+
if (!kind || !notice.$scene_id || !notice.$id)
|
|
817
|
+
return undefined;
|
|
818
|
+
const conversation = Object.freeze({
|
|
819
|
+
endpoint: Object.freeze({ adapter: String(notice.$adapter), id: String(notice.$endpoint) }),
|
|
820
|
+
kind,
|
|
821
|
+
id: String(notice.$scene_id),
|
|
822
|
+
});
|
|
823
|
+
const actor = notice.$actor?.id
|
|
824
|
+
? Object.freeze({ id: String(notice.$actor.id), ...(notice.$actor.name ? { displayName: notice.$actor.name } : {}) })
|
|
825
|
+
: undefined;
|
|
826
|
+
const target = notice.$target?.id
|
|
827
|
+
? Object.freeze({ id: String(notice.$target.id), ...(notice.$target.name ? { displayName: notice.$target.name } : {}) })
|
|
828
|
+
: undefined;
|
|
829
|
+
const base = Object.freeze({
|
|
830
|
+
eventId: `notice:${conversationRefKey(conversation)}:${String(notice.$id)}`,
|
|
831
|
+
conversation,
|
|
832
|
+
timestamp: notice.$timestamp,
|
|
833
|
+
});
|
|
834
|
+
switch (notice.$sub_type) {
|
|
835
|
+
case 'member_increase':
|
|
836
|
+
case 'increase':
|
|
837
|
+
return target ? Object.freeze({ ...base, type: 'member.joined', member: target, ...(actor ? { actor } : {}) }) : undefined;
|
|
838
|
+
case 'member_decrease':
|
|
839
|
+
return target ? Object.freeze({ ...base, type: 'member.left', member: target, ...(actor ? { actor } : {}), reason: actor ? 'removed' : 'left' }) : undefined;
|
|
840
|
+
case 'ban':
|
|
841
|
+
if (!target)
|
|
842
|
+
return undefined;
|
|
843
|
+
return notice.$duration_seconds === 0
|
|
844
|
+
? Object.freeze({ ...base, type: 'member.unmuted', member: target, ...(actor ? { actor } : {}) })
|
|
845
|
+
: Object.freeze({ ...base, type: 'member.muted', member: target, ...(actor ? { actor } : {}), durationSeconds: notice.$duration_seconds ?? 0 });
|
|
846
|
+
case 'admin_change':
|
|
847
|
+
return target && notice.$role && typeof notice.$enabled === 'boolean'
|
|
848
|
+
? Object.freeze({ ...base, type: 'member.role_changed', member: target, ...(actor ? { actor } : {}), role: notice.$role, enabled: notice.$enabled })
|
|
849
|
+
: undefined;
|
|
850
|
+
case 'recall':
|
|
851
|
+
return notice.$message_id
|
|
852
|
+
? Object.freeze({ ...base, type: 'message.recalled', message: Object.freeze({ conversation, id: notice.$message_id }), ...(actor ? { actor } : {}) })
|
|
853
|
+
: undefined;
|
|
854
|
+
case 'emoji_reaction':
|
|
855
|
+
return notice.$message_id && notice.$reaction && notice.$operation
|
|
856
|
+
? Object.freeze({ ...base, type: 'message.reaction_changed', message: Object.freeze({ conversation, id: notice.$message_id }), ...(actor ? { actor } : {}), reaction: notice.$reaction, operation: notice.$operation })
|
|
857
|
+
: undefined;
|
|
858
|
+
case 'poke':
|
|
859
|
+
return Object.freeze({ ...base, type: 'conversation.poked', ...(actor ? { actor } : {}), ...(target ? { target } : {}) });
|
|
860
|
+
default:
|
|
861
|
+
return undefined;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function describeConversationEvent(event) {
|
|
865
|
+
const actor = 'actor' in event && event.actor ? `${event.actor.displayName ?? event.actor.id} (${event.actor.id})` : undefined;
|
|
866
|
+
switch (event.type) {
|
|
867
|
+
case 'message.recalled': return `${actor ?? 'Someone'} recalled message ${event.message.id}.`;
|
|
868
|
+
case 'message.reaction_changed': return `${actor ?? 'Someone'} ${event.operation} reaction ${event.reaction} on message ${event.message.id}.`;
|
|
869
|
+
case 'conversation.poked': return `${actor ?? 'Someone'} poked ${event.target ? `${event.target.displayName ?? event.target.id} (${event.target.id})` : 'someone'}.`;
|
|
870
|
+
case 'member.joined': return `${event.member.displayName ?? event.member.id} (${event.member.id}) joined the conversation.`;
|
|
871
|
+
case 'member.left': return `${event.member.displayName ?? event.member.id} (${event.member.id}) left the conversation (${event.reason ?? 'left'}).`;
|
|
872
|
+
case 'member.muted': return `${event.member.displayName ?? event.member.id} (${event.member.id}) was muted for ${event.durationSeconds} seconds${actor ? ` by ${actor}` : ''}.`;
|
|
873
|
+
case 'member.unmuted': return `${event.member.displayName ?? event.member.id} (${event.member.id}) was unmuted${actor ? ` by ${actor}` : ''}.`;
|
|
874
|
+
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}` : ''}.`;
|
|
875
|
+
case 'message.created': return '';
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function describeConversationMessage(message) {
|
|
879
|
+
const actor = message.actor
|
|
880
|
+
? `${message.actor.displayName ?? message.actor.id} (${message.actor.id})`
|
|
881
|
+
: undefined;
|
|
882
|
+
if (!actor)
|
|
883
|
+
return '';
|
|
884
|
+
const text = segmentsToPlainText(message.segments).trim();
|
|
885
|
+
const attachmentTypes = [...new Set(message.segments
|
|
886
|
+
.filter((segment) => segment.type !== 'text' && segment.type !== 'mention' && segment.type !== 'at')
|
|
887
|
+
.map((segment) => segment.type))];
|
|
888
|
+
const attachmentNote = attachmentTypes.length > 0
|
|
889
|
+
? ` [message also contains ${attachmentTypes.join(', ')} data; inspect its reference before relying on that content]`
|
|
890
|
+
: '';
|
|
891
|
+
return `${actor}: ${text || '(no plain-text content)'}${attachmentNote}`;
|
|
892
|
+
}
|
|
893
|
+
function aggregateConversationContext(events, excludeMessageId) {
|
|
894
|
+
const ordinary = [];
|
|
895
|
+
const noisy = new Map();
|
|
896
|
+
for (const { sequence, event } of events) {
|
|
897
|
+
if (event.type === 'message.created') {
|
|
898
|
+
if (event.message.ref.id === excludeMessageId)
|
|
899
|
+
continue;
|
|
900
|
+
const text = describeConversationMessage(event.message);
|
|
901
|
+
if (!text)
|
|
902
|
+
continue;
|
|
903
|
+
ordinary.push(Object.freeze({
|
|
904
|
+
kind: 'conversation_event',
|
|
905
|
+
sequence,
|
|
906
|
+
eventType: event.type,
|
|
907
|
+
text,
|
|
908
|
+
}));
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (event.type === 'message.reaction_changed' || event.type === 'conversation.poked') {
|
|
912
|
+
const key = event.type === 'message.reaction_changed'
|
|
913
|
+
? `${event.type}:${event.message.id}:${event.actor?.id ?? ''}:${event.reaction}:${event.operation}`
|
|
914
|
+
: `${event.type}:${event.actor?.id ?? ''}:${event.target?.id ?? ''}`;
|
|
915
|
+
const previous = noisy.get(key);
|
|
916
|
+
noisy.set(key, { sequence, event, count: (previous?.count ?? 0) + 1 });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
ordinary.push(Object.freeze({
|
|
920
|
+
kind: 'conversation_event',
|
|
921
|
+
sequence,
|
|
922
|
+
eventType: event.type,
|
|
923
|
+
text: describeConversationEvent(event),
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
for (const { sequence, event, count } of noisy.values()) {
|
|
927
|
+
ordinary.push(Object.freeze({
|
|
928
|
+
kind: 'conversation_event',
|
|
929
|
+
sequence,
|
|
930
|
+
eventType: event.type,
|
|
931
|
+
text: `${describeConversationEvent(event)}${count > 1 ? ` (${count} similar events.)` : ''}`,
|
|
932
|
+
}));
|
|
933
|
+
}
|
|
934
|
+
ordinary.sort((left, right) => left.sequence - right.sequence);
|
|
935
|
+
return Object.freeze(ordinary);
|
|
936
|
+
}
|
|
611
937
|
function resolveIngressRoute(snapshot) {
|
|
612
938
|
const candidate = snapshot.resources.get(snapshot.root)?.get(ingressRouteToken.id);
|
|
613
939
|
return candidate
|
|
@@ -641,9 +967,10 @@ function isDirectHtmlConsumer(snapshot, adapter) {
|
|
|
641
967
|
async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
|
|
642
968
|
const adapter = conversation.endpoint.id;
|
|
643
969
|
const directHtml = isDirectHtmlConsumer(snapshot, adapter);
|
|
970
|
+
const markdownResolved = applyOutboundMarkdownPolicy(rendered, resolveOutboundMarkdownPolicy(adapter, snapshot));
|
|
644
971
|
let payload = directHtml
|
|
645
|
-
?
|
|
646
|
-
: await normalizeOutboundPayload(
|
|
972
|
+
? markdownResolved
|
|
973
|
+
: await normalizeOutboundPayload(markdownResolved, resolveHtmlRenderer(snapshot), {
|
|
647
974
|
mediaPolicy: resolveOutboundMediaPolicy(adapter, snapshot),
|
|
648
975
|
});
|
|
649
976
|
if (finalizeInteractive) {
|
|
@@ -653,23 +980,12 @@ async function prepareOutboundPayload(rendered, conversation, snapshot, finalize
|
|
|
653
980
|
assertCanonicalSegments(payload);
|
|
654
981
|
return payload;
|
|
655
982
|
}
|
|
656
|
-
function receiptFromEndpointResult(
|
|
657
|
-
if (isDeliveryReceipt(result))
|
|
658
|
-
return result;
|
|
659
|
-
const legacyMessageId = legacyMessageIdOf(result);
|
|
983
|
+
function receiptFromEndpointResult(messageId, conversation) {
|
|
660
984
|
return Object.freeze({
|
|
661
985
|
status: 'sent',
|
|
662
|
-
|
|
986
|
+
message: Object.freeze({ conversation, id: messageId }),
|
|
663
987
|
});
|
|
664
988
|
}
|
|
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
989
|
function receiptFromEndpointError(error) {
|
|
674
990
|
const message = error instanceof Error ? error.message : String(error);
|
|
675
991
|
if (/Unknown Adapter Endpoint|does not support outbound/u.test(message)) {
|
|
@@ -725,11 +1041,6 @@ function handlers(snapshot) {
|
|
|
725
1041
|
const projection = snapshot.projections.get(handlerFeatureId);
|
|
726
1042
|
return isHandlerIndex(projection) ? projection : undefined;
|
|
727
1043
|
}
|
|
728
|
-
async function runHandlers(snapshot, event, ...args) {
|
|
729
|
-
const index = handlers(snapshot);
|
|
730
|
-
if (index)
|
|
731
|
-
await index.dispatch(event, ...args);
|
|
732
|
-
}
|
|
733
1044
|
function normalizeConsoleContent(content) {
|
|
734
1045
|
if (typeof content === 'string')
|
|
735
1046
|
return content;
|
|
@@ -782,6 +1093,18 @@ function flattenContent(content) {
|
|
|
782
1093
|
}
|
|
783
1094
|
return String(content);
|
|
784
1095
|
}
|
|
1096
|
+
function conversationSegmentsFromContent(content) {
|
|
1097
|
+
if (Array.isArray(content)) {
|
|
1098
|
+
try {
|
|
1099
|
+
assertCanonicalSegments(content);
|
|
1100
|
+
return Object.freeze([...content]);
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
// Fall through to a truthful text projection of non-canonical output.
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return Object.freeze([{ type: 'text', data: Object.freeze({ text: flattenContent(content) }) }]);
|
|
1107
|
+
}
|
|
785
1108
|
function abortError(signal, fallback) {
|
|
786
1109
|
if (signal?.reason instanceof Error)
|
|
787
1110
|
return signal.reason;
|