@hywkp/sider 0.0.3
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/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/src/channel.d.ts +33 -0
- package/dist/src/channel.d.ts.map +1 -0
- package/dist/src/channel.js +1201 -0
- package/dist/src/channel.js.map +1 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, createReplyPrefixOptions, createTypingCallbacks, formatTextWithAttachmentLinks, normalizeAccountId, resolveOutboundMediaUrls, } from "openclaw/plugin-sdk";
|
|
2
|
+
const CHANNEL_ID = "sider";
|
|
3
|
+
const DEFAULT_GATEWAY_URL = "http://47.82.167.142:3001";
|
|
4
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 8_000;
|
|
5
|
+
const DEFAULT_SEND_TIMEOUT_MS = 12_000;
|
|
6
|
+
const DEFAULT_RECONNECT_DELAY_MS = 2_000;
|
|
7
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
8
|
+
".jpg",
|
|
9
|
+
".jpeg",
|
|
10
|
+
".png",
|
|
11
|
+
".gif",
|
|
12
|
+
".webp",
|
|
13
|
+
".bmp",
|
|
14
|
+
".svg",
|
|
15
|
+
".heic",
|
|
16
|
+
".heif",
|
|
17
|
+
".avif",
|
|
18
|
+
]);
|
|
19
|
+
const MIME_BY_EXTENSION = {
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".png": "image/png",
|
|
23
|
+
".gif": "image/gif",
|
|
24
|
+
".webp": "image/webp",
|
|
25
|
+
".bmp": "image/bmp",
|
|
26
|
+
".svg": "image/svg+xml",
|
|
27
|
+
".pdf": "application/pdf",
|
|
28
|
+
".txt": "text/plain",
|
|
29
|
+
".json": "application/json",
|
|
30
|
+
".zip": "application/zip",
|
|
31
|
+
};
|
|
32
|
+
const meta = {
|
|
33
|
+
id: CHANNEL_ID,
|
|
34
|
+
label: "Sider",
|
|
35
|
+
selectionLabel: "Sider (siderclaw-gateway)",
|
|
36
|
+
docsPath: "/channels/sider",
|
|
37
|
+
docsLabel: "sider",
|
|
38
|
+
blurb: "Bridge OpenClaw with siderclaw-gateway via relay websocket.",
|
|
39
|
+
aliases: ["siderclaw"],
|
|
40
|
+
order: 97,
|
|
41
|
+
};
|
|
42
|
+
let runtimeRef = null;
|
|
43
|
+
export function setSiderRuntime(runtime) {
|
|
44
|
+
runtimeRef = runtime;
|
|
45
|
+
}
|
|
46
|
+
function getSiderRuntime() {
|
|
47
|
+
if (!runtimeRef) {
|
|
48
|
+
throw new Error("sider runtime not initialized");
|
|
49
|
+
}
|
|
50
|
+
return runtimeRef;
|
|
51
|
+
}
|
|
52
|
+
function logDebug(message, data) {
|
|
53
|
+
const core = getSiderRuntime();
|
|
54
|
+
if (!core.logging.shouldLogVerbose()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
core.logging.getChildLogger({ channel: CHANNEL_ID }).debug?.(message, data);
|
|
58
|
+
}
|
|
59
|
+
function logInfo(message, data) {
|
|
60
|
+
getSiderRuntime().logging.getChildLogger({ channel: CHANNEL_ID }).info(message, data);
|
|
61
|
+
}
|
|
62
|
+
function logWarn(message, data) {
|
|
63
|
+
getSiderRuntime().logging.getChildLogger({ channel: CHANNEL_ID }).warn(message, data);
|
|
64
|
+
}
|
|
65
|
+
function sleep(ms) {
|
|
66
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
+
}
|
|
68
|
+
function toRecord(value) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function normalizeGatewayUrl(raw) {
|
|
75
|
+
const fallback = DEFAULT_GATEWAY_URL;
|
|
76
|
+
const trimmed = raw?.trim() || fallback;
|
|
77
|
+
return trimmed.replace(/\/+$/, "");
|
|
78
|
+
}
|
|
79
|
+
function normalizeTimeout(raw, fallback) {
|
|
80
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
return Math.floor(raw);
|
|
84
|
+
}
|
|
85
|
+
function getSiderConfig(cfg) {
|
|
86
|
+
return (cfg.channels?.[CHANNEL_ID] ?? {});
|
|
87
|
+
}
|
|
88
|
+
function resolveSiderAccount(cfg, accountId) {
|
|
89
|
+
const id = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
|
|
90
|
+
const channelCfg = getSiderConfig(cfg);
|
|
91
|
+
const accountCfg = id === DEFAULT_ACCOUNT_ID ? channelCfg : (channelCfg.accounts?.[id] ?? {});
|
|
92
|
+
const gatewayUrl = normalizeGatewayUrl(accountCfg.gatewayUrl);
|
|
93
|
+
const sessionId = accountCfg.sessionId?.trim() || accountCfg.sessionKey?.trim() || undefined;
|
|
94
|
+
const relayId = accountCfg.relayId?.trim() || `openclaw-${id}`;
|
|
95
|
+
const defaultTo = accountCfg.defaultTo?.trim() || (sessionId ? `session:${sessionId}` : undefined);
|
|
96
|
+
return {
|
|
97
|
+
accountId: id,
|
|
98
|
+
name: accountCfg.name?.trim() || id,
|
|
99
|
+
enabled: accountCfg.enabled !== false,
|
|
100
|
+
gatewayUrl,
|
|
101
|
+
sessionId,
|
|
102
|
+
relayId,
|
|
103
|
+
relayToken: accountCfg.relayToken?.trim() || undefined,
|
|
104
|
+
defaultTo,
|
|
105
|
+
connectTimeoutMs: normalizeTimeout(accountCfg.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS),
|
|
106
|
+
sendTimeoutMs: normalizeTimeout(accountCfg.sendTimeoutMs, DEFAULT_SEND_TIMEOUT_MS),
|
|
107
|
+
reconnectDelayMs: normalizeTimeout(accountCfg.reconnectDelayMs, DEFAULT_RECONNECT_DELAY_MS),
|
|
108
|
+
configured: Boolean(gatewayUrl && sessionId && relayId),
|
|
109
|
+
config: accountCfg,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function listSiderAccountIds(cfg) {
|
|
113
|
+
const channelCfg = getSiderConfig(cfg);
|
|
114
|
+
const ids = new Set();
|
|
115
|
+
if (channelCfg.enabled !== undefined ||
|
|
116
|
+
channelCfg.gatewayUrl ||
|
|
117
|
+
channelCfg.sessionId ||
|
|
118
|
+
channelCfg.sessionKey ||
|
|
119
|
+
channelCfg.relayId) {
|
|
120
|
+
ids.add(DEFAULT_ACCOUNT_ID);
|
|
121
|
+
}
|
|
122
|
+
for (const id of Object.keys(channelCfg.accounts ?? {})) {
|
|
123
|
+
ids.add(id);
|
|
124
|
+
}
|
|
125
|
+
return ids.size > 0 ? Array.from(ids) : [DEFAULT_ACCOUNT_ID];
|
|
126
|
+
}
|
|
127
|
+
function parseSessionTarget(raw) {
|
|
128
|
+
const trimmed = raw.trim();
|
|
129
|
+
if (!trimmed) {
|
|
130
|
+
throw new Error("Missing sider session target");
|
|
131
|
+
}
|
|
132
|
+
if (trimmed.startsWith("session:")) {
|
|
133
|
+
const sessionId = trimmed.slice("session:".length).trim();
|
|
134
|
+
if (!sessionId) {
|
|
135
|
+
throw new Error("Invalid sider target, empty session id");
|
|
136
|
+
}
|
|
137
|
+
return sessionId;
|
|
138
|
+
}
|
|
139
|
+
return trimmed;
|
|
140
|
+
}
|
|
141
|
+
function resolveOutboundSessionId(params) {
|
|
142
|
+
const target = params.to?.trim() || params.account.defaultTo?.trim() || "";
|
|
143
|
+
if (!target) {
|
|
144
|
+
throw new Error(`sider account "${params.account.accountId}" missing target; set 'to' or channels.${CHANNEL_ID}.defaultTo`);
|
|
145
|
+
}
|
|
146
|
+
return parseSessionTarget(target);
|
|
147
|
+
}
|
|
148
|
+
function resolveRelayWsUrl(gatewayUrl) {
|
|
149
|
+
const url = new URL(gatewayUrl);
|
|
150
|
+
if (url.pathname === "" || url.pathname === "/") {
|
|
151
|
+
url.pathname = "/ws/relay";
|
|
152
|
+
}
|
|
153
|
+
else if (!url.pathname.endsWith("/ws/relay")) {
|
|
154
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/ws/relay`;
|
|
155
|
+
}
|
|
156
|
+
if (url.protocol === "http:") {
|
|
157
|
+
url.protocol = "ws:";
|
|
158
|
+
}
|
|
159
|
+
else if (url.protocol === "https:") {
|
|
160
|
+
url.protocol = "wss:";
|
|
161
|
+
}
|
|
162
|
+
else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
163
|
+
throw new Error(`Unsupported gateway URL protocol: ${url.protocol}`);
|
|
164
|
+
}
|
|
165
|
+
return url.toString();
|
|
166
|
+
}
|
|
167
|
+
async function wsDataToText(data) {
|
|
168
|
+
if (typeof data === "string") {
|
|
169
|
+
return data;
|
|
170
|
+
}
|
|
171
|
+
if (data instanceof ArrayBuffer) {
|
|
172
|
+
return new TextDecoder().decode(data);
|
|
173
|
+
}
|
|
174
|
+
if (ArrayBuffer.isView(data)) {
|
|
175
|
+
return new TextDecoder().decode(data);
|
|
176
|
+
}
|
|
177
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
178
|
+
return await data.text();
|
|
179
|
+
}
|
|
180
|
+
return String(data ?? "");
|
|
181
|
+
}
|
|
182
|
+
function parseInboundFrame(rawText) {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(rawText);
|
|
185
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (typeof parsed.type !== "string" || !parsed.type.trim()) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function isRealtimeMessageFrame(frame) {
|
|
198
|
+
return frame.type === "message";
|
|
199
|
+
}
|
|
200
|
+
function isRealtimeEventFrame(frame) {
|
|
201
|
+
return frame.type === "event";
|
|
202
|
+
}
|
|
203
|
+
async function waitForSocketOpen(params) {
|
|
204
|
+
const { ws, timeoutMs, accountId } = params;
|
|
205
|
+
await new Promise((resolve, reject) => {
|
|
206
|
+
const timeout = setTimeout(() => {
|
|
207
|
+
cleanup();
|
|
208
|
+
reject(new Error(`[${accountId}] websocket open timeout (${timeoutMs}ms)`));
|
|
209
|
+
}, timeoutMs);
|
|
210
|
+
const cleanup = () => {
|
|
211
|
+
clearTimeout(timeout);
|
|
212
|
+
ws.removeEventListener("open", onOpen);
|
|
213
|
+
ws.removeEventListener("close", onClose);
|
|
214
|
+
ws.removeEventListener("error", onError);
|
|
215
|
+
};
|
|
216
|
+
const onOpen = () => {
|
|
217
|
+
cleanup();
|
|
218
|
+
resolve();
|
|
219
|
+
};
|
|
220
|
+
const onClose = () => {
|
|
221
|
+
cleanup();
|
|
222
|
+
reject(new Error(`[${accountId}] websocket closed before open`));
|
|
223
|
+
};
|
|
224
|
+
const onError = () => {
|
|
225
|
+
cleanup();
|
|
226
|
+
reject(new Error(`[${accountId}] websocket failed before open`));
|
|
227
|
+
};
|
|
228
|
+
ws.addEventListener("open", onOpen);
|
|
229
|
+
ws.addEventListener("close", onClose);
|
|
230
|
+
ws.addEventListener("error", onError);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function closeSocketQuietly(ws) {
|
|
234
|
+
try {
|
|
235
|
+
ws.close();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// no-op
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function connectRelaySocket(params) {
|
|
242
|
+
const wsUrl = resolveRelayWsUrl(params.account.gatewayUrl);
|
|
243
|
+
logDebug("opening sider relay websocket", {
|
|
244
|
+
accountId: params.account.accountId,
|
|
245
|
+
wsUrl,
|
|
246
|
+
sessionId: params.sessionId,
|
|
247
|
+
relayId: params.relayId,
|
|
248
|
+
});
|
|
249
|
+
const ws = new WebSocket(wsUrl);
|
|
250
|
+
await waitForSocketOpen({
|
|
251
|
+
ws,
|
|
252
|
+
timeoutMs: params.account.connectTimeoutMs,
|
|
253
|
+
accountId: params.account.accountId,
|
|
254
|
+
});
|
|
255
|
+
const registerPayload = {
|
|
256
|
+
type: "register",
|
|
257
|
+
session_id: params.sessionId,
|
|
258
|
+
relay_id: params.relayId,
|
|
259
|
+
};
|
|
260
|
+
if (params.account.relayToken) {
|
|
261
|
+
registerPayload.token = params.account.relayToken;
|
|
262
|
+
}
|
|
263
|
+
ws.send(JSON.stringify(registerPayload));
|
|
264
|
+
return ws;
|
|
265
|
+
}
|
|
266
|
+
async function waitForAck(params) {
|
|
267
|
+
return await new Promise((resolve, reject) => {
|
|
268
|
+
let settled = false;
|
|
269
|
+
const timeout = setTimeout(() => {
|
|
270
|
+
cleanup();
|
|
271
|
+
reject(new Error(`[${params.accountId}] timeout waiting sider ack`));
|
|
272
|
+
}, params.timeoutMs);
|
|
273
|
+
const cleanup = () => {
|
|
274
|
+
if (settled) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
settled = true;
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
params.ws.removeEventListener("message", onMessage);
|
|
280
|
+
params.ws.removeEventListener("close", onClose);
|
|
281
|
+
params.ws.removeEventListener("error", onError);
|
|
282
|
+
};
|
|
283
|
+
const rejectWith = (err) => {
|
|
284
|
+
cleanup();
|
|
285
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
286
|
+
};
|
|
287
|
+
const onMessage = (event) => {
|
|
288
|
+
void (async () => {
|
|
289
|
+
const text = await wsDataToText(event.data);
|
|
290
|
+
const frame = parseInboundFrame(text);
|
|
291
|
+
if (!frame) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (frame.type === "ping") {
|
|
295
|
+
params.ws.send(JSON.stringify({ type: "pong" }));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (frame.type === "replaced") {
|
|
299
|
+
rejectWith(new Error(`[${params.accountId}] relay replaced while waiting ack`));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (frame.type !== "ack") {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const ackSession = typeof frame.session_id === "string" ? frame.session_id : "";
|
|
306
|
+
const ackId = typeof frame.id === "string" ? frame.id.trim() : "";
|
|
307
|
+
if (ackSession === params.sessionId && ackId) {
|
|
308
|
+
cleanup();
|
|
309
|
+
resolve(ackId);
|
|
310
|
+
}
|
|
311
|
+
})().catch((err) => {
|
|
312
|
+
rejectWith(err);
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
const onClose = () => {
|
|
316
|
+
rejectWith(new Error(`[${params.accountId}] websocket closed before ack`));
|
|
317
|
+
};
|
|
318
|
+
const onError = () => {
|
|
319
|
+
rejectWith(new Error(`[${params.accountId}] websocket error before ack`));
|
|
320
|
+
};
|
|
321
|
+
params.ws.addEventListener("message", onMessage);
|
|
322
|
+
params.ws.addEventListener("close", onClose);
|
|
323
|
+
params.ws.addEventListener("error", onError);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
async function sendMessageToSider(params) {
|
|
327
|
+
if (params.parts.length === 0) {
|
|
328
|
+
throw new Error("Cannot send sider message with empty parts");
|
|
329
|
+
}
|
|
330
|
+
const relayId = `${params.account.relayId}-tx-${crypto.randomUUID().slice(0, 8)}`;
|
|
331
|
+
const clientReqId = crypto.randomUUID();
|
|
332
|
+
const ws = await connectRelaySocket({
|
|
333
|
+
account: params.account,
|
|
334
|
+
sessionId: params.sessionId,
|
|
335
|
+
relayId,
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
ws.send(JSON.stringify({
|
|
339
|
+
type: "message",
|
|
340
|
+
session_id: params.sessionId,
|
|
341
|
+
client_req_id: clientReqId,
|
|
342
|
+
parts: params.parts,
|
|
343
|
+
}));
|
|
344
|
+
const messageId = await waitForAck({
|
|
345
|
+
ws,
|
|
346
|
+
sessionId: params.sessionId,
|
|
347
|
+
timeoutMs: params.account.sendTimeoutMs,
|
|
348
|
+
accountId: params.account.accountId,
|
|
349
|
+
});
|
|
350
|
+
logDebug("sider outbound ack", {
|
|
351
|
+
accountId: params.account.accountId,
|
|
352
|
+
sessionId: params.sessionId,
|
|
353
|
+
clientReqId,
|
|
354
|
+
messageId,
|
|
355
|
+
});
|
|
356
|
+
return { messageId, conversationId: params.sessionId };
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
closeSocketQuietly(ws);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function sendEventToSider(params) {
|
|
363
|
+
const relayId = `${params.account.relayId}-tx-${crypto.randomUUID().slice(0, 8)}`;
|
|
364
|
+
const clientReqId = crypto.randomUUID();
|
|
365
|
+
const ws = await connectRelaySocket({
|
|
366
|
+
account: params.account,
|
|
367
|
+
sessionId: params.sessionId,
|
|
368
|
+
relayId,
|
|
369
|
+
});
|
|
370
|
+
try {
|
|
371
|
+
ws.send(JSON.stringify({
|
|
372
|
+
type: "event",
|
|
373
|
+
session_id: params.sessionId,
|
|
374
|
+
client_req_id: clientReqId,
|
|
375
|
+
event_type: params.eventType,
|
|
376
|
+
payload: params.payload,
|
|
377
|
+
meta: params.meta ?? {},
|
|
378
|
+
}));
|
|
379
|
+
const eventId = await waitForAck({
|
|
380
|
+
ws,
|
|
381
|
+
sessionId: params.sessionId,
|
|
382
|
+
timeoutMs: params.account.sendTimeoutMs,
|
|
383
|
+
accountId: params.account.accountId,
|
|
384
|
+
});
|
|
385
|
+
logDebug("sider outbound event ack", {
|
|
386
|
+
accountId: params.account.accountId,
|
|
387
|
+
sessionId: params.sessionId,
|
|
388
|
+
eventType: params.eventType,
|
|
389
|
+
clientReqId,
|
|
390
|
+
eventId,
|
|
391
|
+
});
|
|
392
|
+
return { eventId, conversationId: params.sessionId };
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
closeSocketQuietly(ws);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function sendSiderEventBestEffort(params) {
|
|
399
|
+
try {
|
|
400
|
+
await sendEventToSider({
|
|
401
|
+
account: params.account,
|
|
402
|
+
sessionId: params.sessionId,
|
|
403
|
+
eventType: params.event.eventType,
|
|
404
|
+
payload: params.event.payload,
|
|
405
|
+
meta: params.event.meta,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
logWarn("sider outbound event failed", {
|
|
410
|
+
accountId: params.account.accountId,
|
|
411
|
+
sessionId: params.sessionId,
|
|
412
|
+
eventType: params.event.eventType,
|
|
413
|
+
context: params.context,
|
|
414
|
+
error: String(err),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function extFromMediaUrl(raw) {
|
|
419
|
+
try {
|
|
420
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
421
|
+
const pathname = new URL(raw).pathname;
|
|
422
|
+
return pathname.includes(".") ? pathname.slice(pathname.lastIndexOf(".")).toLowerCase() : "";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// no-op
|
|
427
|
+
}
|
|
428
|
+
const qPos = raw.indexOf("?");
|
|
429
|
+
const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
|
|
430
|
+
const slashPos = sanitized.lastIndexOf("/");
|
|
431
|
+
const last = slashPos >= 0 ? sanitized.slice(slashPos + 1) : sanitized;
|
|
432
|
+
const dotPos = last.lastIndexOf(".");
|
|
433
|
+
return dotPos >= 0 ? last.slice(dotPos).toLowerCase() : "";
|
|
434
|
+
}
|
|
435
|
+
function fileNameFromMediaUrl(raw) {
|
|
436
|
+
try {
|
|
437
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
438
|
+
const pathname = new URL(raw).pathname;
|
|
439
|
+
const seg = pathname.split("/").filter(Boolean).pop();
|
|
440
|
+
return seg || undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// no-op
|
|
445
|
+
}
|
|
446
|
+
const qPos = raw.indexOf("?");
|
|
447
|
+
const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
|
|
448
|
+
const seg = sanitized.split("/").filter(Boolean).pop();
|
|
449
|
+
return seg || undefined;
|
|
450
|
+
}
|
|
451
|
+
function inferMediaKind(mediaUrl) {
|
|
452
|
+
const ext = extFromMediaUrl(mediaUrl);
|
|
453
|
+
return IMAGE_EXTENSIONS.has(ext) ? "image" : "file";
|
|
454
|
+
}
|
|
455
|
+
function inferMediaMimeType(mediaUrl) {
|
|
456
|
+
const ext = extFromMediaUrl(mediaUrl);
|
|
457
|
+
return MIME_BY_EXTENSION[ext];
|
|
458
|
+
}
|
|
459
|
+
async function uploadMediaToSider(params) {
|
|
460
|
+
const mediaKind = inferMediaKind(params.mediaUrl);
|
|
461
|
+
const fileName = fileNameFromMediaUrl(params.mediaUrl);
|
|
462
|
+
const mimeType = inferMediaMimeType(params.mediaUrl);
|
|
463
|
+
// TODO(siderclaw-upload): Upload local/remote media to sider server and return hosted resource URL.
|
|
464
|
+
// Current placeholder returns the original mediaUrl directly.
|
|
465
|
+
const resourceUrl = params.mediaUrl;
|
|
466
|
+
void params.account;
|
|
467
|
+
return {
|
|
468
|
+
resourceUrl,
|
|
469
|
+
mediaKind,
|
|
470
|
+
mimeType,
|
|
471
|
+
fileName,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function buildSiderPartsFromReplyPayload(params) {
|
|
475
|
+
const parts = [];
|
|
476
|
+
const text = params.payload.text?.trim();
|
|
477
|
+
if (text) {
|
|
478
|
+
parts.push({
|
|
479
|
+
type: "core.text",
|
|
480
|
+
spec_version: 1,
|
|
481
|
+
payload: { text },
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const mediaUrls = resolveOutboundMediaUrls(params.payload);
|
|
485
|
+
for (const mediaUrl of mediaUrls) {
|
|
486
|
+
const uploaded = await uploadMediaToSider({
|
|
487
|
+
account: params.account,
|
|
488
|
+
mediaUrl,
|
|
489
|
+
});
|
|
490
|
+
parts.push({
|
|
491
|
+
type: "core.media",
|
|
492
|
+
spec_version: 1,
|
|
493
|
+
payload: {
|
|
494
|
+
media_type: uploaded.mediaKind,
|
|
495
|
+
url: uploaded.resourceUrl,
|
|
496
|
+
mime_type: uploaded.mimeType,
|
|
497
|
+
file_name: uploaded.fileName,
|
|
498
|
+
},
|
|
499
|
+
meta: {
|
|
500
|
+
source_media_url: mediaUrl,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return parts;
|
|
505
|
+
}
|
|
506
|
+
function parseTextFromPart(part) {
|
|
507
|
+
if (part.type !== "core.text") {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const payload = toRecord(part.payload);
|
|
511
|
+
if (payload?.text && typeof payload.text === "string") {
|
|
512
|
+
const text = payload.text.trim();
|
|
513
|
+
return text || null;
|
|
514
|
+
}
|
|
515
|
+
if (typeof part.payload === "string") {
|
|
516
|
+
const text = part.payload.trim();
|
|
517
|
+
return text || null;
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
function parseMediaFromPart(part) {
|
|
522
|
+
if (part.type !== "core.media") {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const payload = toRecord(part.payload);
|
|
526
|
+
if (!payload) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const urlCandidates = [payload.url, payload.resource_url, payload.media_url];
|
|
530
|
+
const url = urlCandidates.find((entry) => typeof entry === "string" && entry.trim());
|
|
531
|
+
if (!url) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const mimeCandidates = [payload.mime_type, payload.content_type];
|
|
535
|
+
const mimeType = mimeCandidates.find((entry) => typeof entry === "string" && entry.trim());
|
|
536
|
+
return { url: url.trim(), mimeType: mimeType?.trim() || undefined };
|
|
537
|
+
}
|
|
538
|
+
function buildEventMeta(params) {
|
|
539
|
+
return {
|
|
540
|
+
channel: CHANNEL_ID,
|
|
541
|
+
account_id: params.accountId,
|
|
542
|
+
schema_version: 1,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function buildTypingEvent(params) {
|
|
546
|
+
return {
|
|
547
|
+
eventType: "typing",
|
|
548
|
+
payload: {
|
|
549
|
+
on: params.state === "typing",
|
|
550
|
+
state: params.state,
|
|
551
|
+
session_id: params.sessionId,
|
|
552
|
+
ts: Date.now(),
|
|
553
|
+
},
|
|
554
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function buildStreamingStartEvent(params) {
|
|
558
|
+
return {
|
|
559
|
+
eventType: "stream.start",
|
|
560
|
+
payload: {
|
|
561
|
+
session_id: params.sessionId,
|
|
562
|
+
stream_id: params.streamId,
|
|
563
|
+
ts: Date.now(),
|
|
564
|
+
},
|
|
565
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function buildStreamingDeltaEvent(params) {
|
|
569
|
+
return {
|
|
570
|
+
eventType: "stream.delta",
|
|
571
|
+
payload: {
|
|
572
|
+
session_id: params.sessionId,
|
|
573
|
+
stream_id: params.streamId,
|
|
574
|
+
seq: params.seq,
|
|
575
|
+
delta: params.delta,
|
|
576
|
+
done: false,
|
|
577
|
+
chunk_chars: params.delta.length,
|
|
578
|
+
ts: Date.now(),
|
|
579
|
+
},
|
|
580
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function buildStreamingDoneEvent(params) {
|
|
584
|
+
return {
|
|
585
|
+
eventType: "stream.done",
|
|
586
|
+
payload: {
|
|
587
|
+
session_id: params.sessionId,
|
|
588
|
+
stream_id: params.streamId,
|
|
589
|
+
seq: params.seq,
|
|
590
|
+
done: true,
|
|
591
|
+
reason: params.reason,
|
|
592
|
+
ts: Date.now(),
|
|
593
|
+
},
|
|
594
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
async function deliverReplyPayloadToSider(params) {
|
|
598
|
+
if (params.kind === "block") {
|
|
599
|
+
const delta = typeof params.payload.text === "string" ? params.payload.text : "";
|
|
600
|
+
if (delta.length === 0) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (!params.streamState.active || !params.streamState.streamId) {
|
|
604
|
+
params.streamState.active = true;
|
|
605
|
+
params.streamState.streamId = crypto.randomUUID();
|
|
606
|
+
params.streamState.seq = 0;
|
|
607
|
+
const streamStartEvent = buildStreamingStartEvent({
|
|
608
|
+
sessionId: params.sessionId,
|
|
609
|
+
streamId: params.streamState.streamId,
|
|
610
|
+
accountId: params.account.accountId,
|
|
611
|
+
});
|
|
612
|
+
await sendSiderEventBestEffort({
|
|
613
|
+
account: params.account,
|
|
614
|
+
sessionId: params.sessionId,
|
|
615
|
+
event: streamStartEvent,
|
|
616
|
+
context: "stream.start",
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
params.streamState.seq += 1;
|
|
620
|
+
const streamDeltaEvent = buildStreamingDeltaEvent({
|
|
621
|
+
sessionId: params.sessionId,
|
|
622
|
+
streamId: params.streamState.streamId,
|
|
623
|
+
seq: params.streamState.seq,
|
|
624
|
+
delta,
|
|
625
|
+
accountId: params.account.accountId,
|
|
626
|
+
});
|
|
627
|
+
logDebug("sending stream delta event", {
|
|
628
|
+
accountId: params.account.accountId,
|
|
629
|
+
sessionId: params.sessionId,
|
|
630
|
+
eventType: streamDeltaEvent.eventType,
|
|
631
|
+
streamId: params.streamState.streamId,
|
|
632
|
+
seq: params.streamState.seq,
|
|
633
|
+
deltaLength: delta.length,
|
|
634
|
+
});
|
|
635
|
+
await sendSiderEventBestEffort({
|
|
636
|
+
account: params.account,
|
|
637
|
+
sessionId: params.sessionId,
|
|
638
|
+
event: streamDeltaEvent,
|
|
639
|
+
context: "stream.delta",
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (params.streamState.active && params.streamState.streamId) {
|
|
644
|
+
params.streamState.seq += 1;
|
|
645
|
+
const streamDoneEvent = buildStreamingDoneEvent({
|
|
646
|
+
sessionId: params.sessionId,
|
|
647
|
+
streamId: params.streamState.streamId,
|
|
648
|
+
seq: params.streamState.seq,
|
|
649
|
+
accountId: params.account.accountId,
|
|
650
|
+
reason: "final",
|
|
651
|
+
});
|
|
652
|
+
await sendSiderEventBestEffort({
|
|
653
|
+
account: params.account,
|
|
654
|
+
sessionId: params.sessionId,
|
|
655
|
+
event: streamDoneEvent,
|
|
656
|
+
context: "stream.done",
|
|
657
|
+
});
|
|
658
|
+
params.streamState.active = false;
|
|
659
|
+
params.streamState.streamId = undefined;
|
|
660
|
+
params.streamState.seq = 0;
|
|
661
|
+
}
|
|
662
|
+
const parts = await buildSiderPartsFromReplyPayload({
|
|
663
|
+
account: params.account,
|
|
664
|
+
payload: params.payload,
|
|
665
|
+
});
|
|
666
|
+
if (parts.length === 0) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await sendMessageToSider({
|
|
670
|
+
account: params.account,
|
|
671
|
+
sessionId: params.sessionId,
|
|
672
|
+
parts,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async function handleInboundRealtimeMessage(params) {
|
|
676
|
+
const { cfg, account, event } = params;
|
|
677
|
+
const core = getSiderRuntime();
|
|
678
|
+
const sessionId = typeof event.session_id === "string" ? event.session_id.trim() : "";
|
|
679
|
+
if (!sessionId) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if ((event.source_role ?? "").toLowerCase() === "relay") {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const parts = Array.isArray(event.parts) ? event.parts : [];
|
|
686
|
+
const textChunks = [];
|
|
687
|
+
const mediaItems = [];
|
|
688
|
+
for (const part of parts) {
|
|
689
|
+
const text = parseTextFromPart(part);
|
|
690
|
+
if (text) {
|
|
691
|
+
textChunks.push(text);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const media = parseMediaFromPart(part);
|
|
695
|
+
if (media) {
|
|
696
|
+
mediaItems.push(media);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const rawText = textChunks.join("\n").trim();
|
|
700
|
+
const mediaUrls = mediaItems.map((item) => item.url);
|
|
701
|
+
const mediaTypes = mediaItems
|
|
702
|
+
.map((item) => item.mimeType)
|
|
703
|
+
.filter((item) => Boolean(item));
|
|
704
|
+
const hasControlCommand = rawText ? core.channel.text.hasControlCommand(rawText, cfg) : false;
|
|
705
|
+
const shouldComputeCommandAuthorized = rawText
|
|
706
|
+
? core.channel.commands.shouldComputeCommandAuthorized(rawText, cfg)
|
|
707
|
+
: false;
|
|
708
|
+
const commandAuthorized = shouldComputeCommandAuthorized
|
|
709
|
+
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
710
|
+
useAccessGroups: cfg.commands?.useAccessGroups !== false,
|
|
711
|
+
// Sider currently has no per-sender allowlist. Treat the authenticated relay session as trusted.
|
|
712
|
+
authorizers: [{ configured: true, allowed: true }],
|
|
713
|
+
})
|
|
714
|
+
: undefined;
|
|
715
|
+
if (!rawText && mediaUrls.length === 0) {
|
|
716
|
+
logDebug("drop inbound sider message: no text/media", {
|
|
717
|
+
accountId: account.accountId,
|
|
718
|
+
sessionId,
|
|
719
|
+
messageId: event.message_id,
|
|
720
|
+
partTypes: parts.map((part) => part.type),
|
|
721
|
+
});
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (hasControlCommand) {
|
|
725
|
+
logInfo("sider control command inbound", {
|
|
726
|
+
accountId: account.accountId,
|
|
727
|
+
sessionId,
|
|
728
|
+
messageId: event.message_id,
|
|
729
|
+
commandAuthorized,
|
|
730
|
+
});
|
|
731
|
+
logDebug("detected sider control command", {
|
|
732
|
+
accountId: account.accountId,
|
|
733
|
+
sessionId,
|
|
734
|
+
messageId: event.message_id,
|
|
735
|
+
commandBody: rawText,
|
|
736
|
+
shouldComputeCommandAuthorized,
|
|
737
|
+
commandAuthorized,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const bodyForAgent = formatTextWithAttachmentLinks(rawText, mediaUrls);
|
|
741
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
742
|
+
cfg,
|
|
743
|
+
channel: CHANNEL_ID,
|
|
744
|
+
accountId: account.accountId,
|
|
745
|
+
peer: {
|
|
746
|
+
kind: "direct",
|
|
747
|
+
id: sessionId,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
const to = `session:${sessionId}`;
|
|
751
|
+
const timestamp = typeof event.created_at === "number" && Number.isFinite(event.created_at)
|
|
752
|
+
? event.created_at
|
|
753
|
+
: Date.now();
|
|
754
|
+
const envelopeBody = core.channel.reply.formatAgentEnvelope({
|
|
755
|
+
channel: "Sider",
|
|
756
|
+
from: sessionId,
|
|
757
|
+
timestamp,
|
|
758
|
+
envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
759
|
+
body: bodyForAgent,
|
|
760
|
+
});
|
|
761
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
762
|
+
Body: envelopeBody,
|
|
763
|
+
BodyForAgent: bodyForAgent,
|
|
764
|
+
RawBody: bodyForAgent,
|
|
765
|
+
CommandBody: rawText,
|
|
766
|
+
BodyForCommands: rawText,
|
|
767
|
+
From: `${CHANNEL_ID}:client:${sessionId}`,
|
|
768
|
+
To: to,
|
|
769
|
+
SessionKey: route.sessionKey,
|
|
770
|
+
AccountId: route.accountId,
|
|
771
|
+
ChatType: "direct",
|
|
772
|
+
ConversationLabel: sessionId,
|
|
773
|
+
SenderId: "client",
|
|
774
|
+
SenderName: "client",
|
|
775
|
+
Provider: CHANNEL_ID,
|
|
776
|
+
Surface: CHANNEL_ID,
|
|
777
|
+
MessageSid: typeof event.message_id === "string" ? event.message_id : undefined,
|
|
778
|
+
Timestamp: timestamp,
|
|
779
|
+
WasMentioned: true,
|
|
780
|
+
OriginatingChannel: CHANNEL_ID,
|
|
781
|
+
OriginatingTo: to,
|
|
782
|
+
MediaUrl: mediaUrls[0],
|
|
783
|
+
MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
|
|
784
|
+
MediaType: mediaTypes[0],
|
|
785
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
786
|
+
CommandAuthorized: commandAuthorized,
|
|
787
|
+
});
|
|
788
|
+
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
789
|
+
agentId: route.agentId,
|
|
790
|
+
});
|
|
791
|
+
await core.channel.session.recordInboundSession({
|
|
792
|
+
storePath,
|
|
793
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
794
|
+
ctx: ctxPayload,
|
|
795
|
+
onRecordError: (err) => {
|
|
796
|
+
core.logging.getChildLogger({ channel: CHANNEL_ID }).warn("sider failed to record session", {
|
|
797
|
+
error: String(err),
|
|
798
|
+
});
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
802
|
+
cfg,
|
|
803
|
+
agentId: route.agentId,
|
|
804
|
+
channel: CHANNEL_ID,
|
|
805
|
+
accountId: account.accountId,
|
|
806
|
+
});
|
|
807
|
+
logDebug("dispatching sider inbound message", {
|
|
808
|
+
accountId: account.accountId,
|
|
809
|
+
sessionId,
|
|
810
|
+
messageId: event.message_id,
|
|
811
|
+
routeSessionKey: route.sessionKey,
|
|
812
|
+
hasControlCommand,
|
|
813
|
+
commandAuthorized,
|
|
814
|
+
textLength: rawText.length,
|
|
815
|
+
mediaCount: mediaUrls.length,
|
|
816
|
+
});
|
|
817
|
+
const streamState = {
|
|
818
|
+
active: false,
|
|
819
|
+
streamId: undefined,
|
|
820
|
+
seq: 0,
|
|
821
|
+
};
|
|
822
|
+
const typingCallbacks = createTypingCallbacks({
|
|
823
|
+
start: async () => {
|
|
824
|
+
const typingEvent = buildTypingEvent({
|
|
825
|
+
state: "typing",
|
|
826
|
+
sessionId,
|
|
827
|
+
accountId: account.accountId,
|
|
828
|
+
});
|
|
829
|
+
logDebug("sending typing start event", {
|
|
830
|
+
accountId: account.accountId,
|
|
831
|
+
sessionId,
|
|
832
|
+
eventType: typingEvent.eventType,
|
|
833
|
+
});
|
|
834
|
+
await sendSiderEventBestEffort({
|
|
835
|
+
account,
|
|
836
|
+
sessionId,
|
|
837
|
+
event: typingEvent,
|
|
838
|
+
context: "typing.start",
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
stop: async () => {
|
|
842
|
+
const typingEvent = buildTypingEvent({
|
|
843
|
+
state: "idle",
|
|
844
|
+
sessionId,
|
|
845
|
+
accountId: account.accountId,
|
|
846
|
+
});
|
|
847
|
+
logDebug("sending typing stop event", {
|
|
848
|
+
accountId: account.accountId,
|
|
849
|
+
sessionId,
|
|
850
|
+
eventType: typingEvent.eventType,
|
|
851
|
+
});
|
|
852
|
+
await sendSiderEventBestEffort({
|
|
853
|
+
account,
|
|
854
|
+
sessionId,
|
|
855
|
+
event: typingEvent,
|
|
856
|
+
context: "typing.stop",
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
onStartError: (err) => {
|
|
860
|
+
logWarn("sider typing start failed", {
|
|
861
|
+
accountId: account.accountId,
|
|
862
|
+
sessionId,
|
|
863
|
+
error: String(err),
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
onStopError: (err) => {
|
|
867
|
+
logWarn("sider typing stop failed", {
|
|
868
|
+
accountId: account.accountId,
|
|
869
|
+
sessionId,
|
|
870
|
+
error: String(err),
|
|
871
|
+
});
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
try {
|
|
875
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
876
|
+
ctx: ctxPayload,
|
|
877
|
+
cfg,
|
|
878
|
+
dispatcherOptions: {
|
|
879
|
+
...prefixOptions,
|
|
880
|
+
typingCallbacks,
|
|
881
|
+
deliver: async (payload, info) => {
|
|
882
|
+
await deliverReplyPayloadToSider({
|
|
883
|
+
account,
|
|
884
|
+
sessionId,
|
|
885
|
+
streamState,
|
|
886
|
+
payload,
|
|
887
|
+
kind: info.kind,
|
|
888
|
+
});
|
|
889
|
+
},
|
|
890
|
+
onError: (err, info) => {
|
|
891
|
+
core.logging
|
|
892
|
+
.getChildLogger({ channel: CHANNEL_ID })
|
|
893
|
+
.error(`sider ${info.kind} reply failed`, { error: String(err) });
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
replyOptions: {
|
|
897
|
+
onModelSelected,
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
logDebug("completed sider inbound dispatch", {
|
|
901
|
+
accountId: account.accountId,
|
|
902
|
+
sessionId,
|
|
903
|
+
messageId: event.message_id,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
finally {
|
|
907
|
+
if (streamState.active && streamState.streamId) {
|
|
908
|
+
streamState.seq += 1;
|
|
909
|
+
const streamDoneEvent = buildStreamingDoneEvent({
|
|
910
|
+
sessionId,
|
|
911
|
+
streamId: streamState.streamId,
|
|
912
|
+
seq: streamState.seq,
|
|
913
|
+
accountId: account.accountId,
|
|
914
|
+
reason: "interrupted",
|
|
915
|
+
});
|
|
916
|
+
await sendSiderEventBestEffort({
|
|
917
|
+
account,
|
|
918
|
+
sessionId,
|
|
919
|
+
event: streamDoneEvent,
|
|
920
|
+
context: "stream.done.interrupted",
|
|
921
|
+
});
|
|
922
|
+
streamState.active = false;
|
|
923
|
+
streamState.streamId = undefined;
|
|
924
|
+
streamState.seq = 0;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async function handleInboundRealtimeEvent(params) {
|
|
929
|
+
const { account, event } = params;
|
|
930
|
+
const sessionId = typeof event.session_id === "string" ? event.session_id.trim() : "";
|
|
931
|
+
if (!sessionId) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if ((event.source_role ?? "").toLowerCase() === "relay") {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
logDebug("received sider realtime event", {
|
|
938
|
+
accountId: account.accountId,
|
|
939
|
+
sessionId,
|
|
940
|
+
eventId: event.event_id,
|
|
941
|
+
eventType: event.event_type,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
async function monitorSiderSession(params) {
|
|
945
|
+
const { cfg, account, abortSignal, log } = params;
|
|
946
|
+
const sessionId = account.sessionId?.trim();
|
|
947
|
+
if (!sessionId) {
|
|
948
|
+
throw new Error(`sider account "${account.accountId}" missing sessionId`);
|
|
949
|
+
}
|
|
950
|
+
while (!abortSignal.aborted) {
|
|
951
|
+
let ws = null;
|
|
952
|
+
try {
|
|
953
|
+
ws = await connectRelaySocket({
|
|
954
|
+
account,
|
|
955
|
+
sessionId,
|
|
956
|
+
relayId: account.relayId,
|
|
957
|
+
});
|
|
958
|
+
log?.info(`[${account.accountId}] sider relay connected (${sessionId})`);
|
|
959
|
+
logInfo("sider relay connected", {
|
|
960
|
+
accountId: account.accountId,
|
|
961
|
+
sessionId,
|
|
962
|
+
relayId: account.relayId,
|
|
963
|
+
});
|
|
964
|
+
await new Promise((resolve) => {
|
|
965
|
+
const socket = ws;
|
|
966
|
+
if (!socket) {
|
|
967
|
+
resolve();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const cleanup = () => {
|
|
971
|
+
socket.removeEventListener("message", onMessage);
|
|
972
|
+
socket.removeEventListener("close", onClose);
|
|
973
|
+
socket.removeEventListener("error", onError);
|
|
974
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
975
|
+
};
|
|
976
|
+
const onMessage = (event) => {
|
|
977
|
+
void (async () => {
|
|
978
|
+
const text = await wsDataToText(event.data);
|
|
979
|
+
const frame = parseInboundFrame(text);
|
|
980
|
+
if (!frame) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (frame.type === "ping") {
|
|
984
|
+
socket.send(JSON.stringify({ type: "pong" }));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (frame.type === "replaced") {
|
|
988
|
+
log?.warn(`[${account.accountId}] sider relay replaced by a newer connection`);
|
|
989
|
+
closeSocketQuietly(socket);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (frame.type === "ack") {
|
|
993
|
+
logDebug("received sider ack on monitor socket", {
|
|
994
|
+
accountId: account.accountId,
|
|
995
|
+
sessionId: frame.session_id,
|
|
996
|
+
id: frame.id,
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (isRealtimeEventFrame(frame)) {
|
|
1001
|
+
await handleInboundRealtimeEvent({
|
|
1002
|
+
account,
|
|
1003
|
+
event: frame,
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!isRealtimeMessageFrame(frame)) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await handleInboundRealtimeMessage({
|
|
1011
|
+
cfg,
|
|
1012
|
+
account,
|
|
1013
|
+
event: frame,
|
|
1014
|
+
});
|
|
1015
|
+
})().catch((err) => {
|
|
1016
|
+
log?.error(`[${account.accountId}] sider inbound handling failed: ${String(err)}`);
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
const onClose = () => {
|
|
1020
|
+
cleanup();
|
|
1021
|
+
resolve();
|
|
1022
|
+
};
|
|
1023
|
+
const onError = () => {
|
|
1024
|
+
cleanup();
|
|
1025
|
+
resolve();
|
|
1026
|
+
};
|
|
1027
|
+
const onAbort = () => {
|
|
1028
|
+
closeSocketQuietly(socket);
|
|
1029
|
+
cleanup();
|
|
1030
|
+
resolve();
|
|
1031
|
+
};
|
|
1032
|
+
socket.addEventListener("message", onMessage);
|
|
1033
|
+
socket.addEventListener("close", onClose);
|
|
1034
|
+
socket.addEventListener("error", onError);
|
|
1035
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1036
|
+
if (abortSignal.aborted) {
|
|
1037
|
+
onAbort();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
catch (err) {
|
|
1042
|
+
log?.warn(`[${account.accountId}] sider relay loop error: ${String(err)}`);
|
|
1043
|
+
logWarn("sider relay loop error", {
|
|
1044
|
+
accountId: account.accountId,
|
|
1045
|
+
error: String(err),
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
finally {
|
|
1049
|
+
if (ws) {
|
|
1050
|
+
closeSocketQuietly(ws);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (!abortSignal.aborted) {
|
|
1054
|
+
await sleep(account.reconnectDelayMs);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
export const siderPlugin = {
|
|
1059
|
+
id: CHANNEL_ID,
|
|
1060
|
+
meta,
|
|
1061
|
+
capabilities: {
|
|
1062
|
+
chatTypes: ["direct"],
|
|
1063
|
+
threads: false,
|
|
1064
|
+
media: true,
|
|
1065
|
+
blockStreaming: true,
|
|
1066
|
+
},
|
|
1067
|
+
streaming: {
|
|
1068
|
+
blockStreamingCoalesceDefaults: { minChars: 1200, idleMs: 900 },
|
|
1069
|
+
},
|
|
1070
|
+
configSchema: {
|
|
1071
|
+
schema: {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
additionalProperties: false,
|
|
1074
|
+
properties: {
|
|
1075
|
+
enabled: { type: "boolean" },
|
|
1076
|
+
name: { type: "string" },
|
|
1077
|
+
gatewayUrl: { type: "string" },
|
|
1078
|
+
sessionId: { type: "string" },
|
|
1079
|
+
sessionKey: { type: "string" },
|
|
1080
|
+
relayId: { type: "string" },
|
|
1081
|
+
relayToken: { type: "string" },
|
|
1082
|
+
defaultTo: { type: "string" },
|
|
1083
|
+
connectTimeoutMs: { type: "number", minimum: 1 },
|
|
1084
|
+
sendTimeoutMs: { type: "number", minimum: 1 },
|
|
1085
|
+
reconnectDelayMs: { type: "number", minimum: 1 },
|
|
1086
|
+
accounts: {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
additionalProperties: {
|
|
1089
|
+
type: "object",
|
|
1090
|
+
additionalProperties: false,
|
|
1091
|
+
properties: {
|
|
1092
|
+
enabled: { type: "boolean" },
|
|
1093
|
+
name: { type: "string" },
|
|
1094
|
+
gatewayUrl: { type: "string" },
|
|
1095
|
+
sessionId: { type: "string" },
|
|
1096
|
+
sessionKey: { type: "string" },
|
|
1097
|
+
relayId: { type: "string" },
|
|
1098
|
+
relayToken: { type: "string" },
|
|
1099
|
+
defaultTo: { type: "string" },
|
|
1100
|
+
connectTimeoutMs: { type: "number", minimum: 1 },
|
|
1101
|
+
sendTimeoutMs: { type: "number", minimum: 1 },
|
|
1102
|
+
reconnectDelayMs: { type: "number", minimum: 1 },
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
|
|
1110
|
+
config: {
|
|
1111
|
+
listAccountIds: (cfg) => listSiderAccountIds(cfg),
|
|
1112
|
+
resolveAccount: (cfg, accountId) => resolveSiderAccount(cfg, accountId),
|
|
1113
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
1114
|
+
isConfigured: (account) => account.configured,
|
|
1115
|
+
describeAccount: (account) => ({
|
|
1116
|
+
accountId: account.accountId,
|
|
1117
|
+
enabled: account.enabled,
|
|
1118
|
+
configured: account.configured,
|
|
1119
|
+
name: account.name,
|
|
1120
|
+
baseUrl: account.gatewayUrl,
|
|
1121
|
+
relayId: account.relayId,
|
|
1122
|
+
sessionId: account.sessionId,
|
|
1123
|
+
sessionKey: account.sessionId,
|
|
1124
|
+
}),
|
|
1125
|
+
resolveDefaultTo: ({ cfg, accountId }) => resolveSiderAccount(cfg, accountId).defaultTo,
|
|
1126
|
+
},
|
|
1127
|
+
outbound: {
|
|
1128
|
+
deliveryMode: "direct",
|
|
1129
|
+
sendText: async ({ cfg, accountId, to, text }) => {
|
|
1130
|
+
const account = resolveSiderAccount(cfg, accountId);
|
|
1131
|
+
if (!account.configured) {
|
|
1132
|
+
throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
|
|
1133
|
+
}
|
|
1134
|
+
const sessionId = resolveOutboundSessionId({ account, to });
|
|
1135
|
+
const payload = text?.trim();
|
|
1136
|
+
if (!payload) {
|
|
1137
|
+
throw new Error("sider sendText requires non-empty text");
|
|
1138
|
+
}
|
|
1139
|
+
const result = await sendMessageToSider({
|
|
1140
|
+
account,
|
|
1141
|
+
sessionId,
|
|
1142
|
+
parts: [
|
|
1143
|
+
{
|
|
1144
|
+
type: "core.text",
|
|
1145
|
+
spec_version: 1,
|
|
1146
|
+
payload: { text: payload },
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
});
|
|
1150
|
+
return {
|
|
1151
|
+
channel: CHANNEL_ID,
|
|
1152
|
+
messageId: result.messageId,
|
|
1153
|
+
conversationId: result.conversationId,
|
|
1154
|
+
};
|
|
1155
|
+
},
|
|
1156
|
+
sendMedia: async ({ cfg, accountId, to, text, mediaUrl }) => {
|
|
1157
|
+
const account = resolveSiderAccount(cfg, accountId);
|
|
1158
|
+
if (!account.configured) {
|
|
1159
|
+
throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
|
|
1160
|
+
}
|
|
1161
|
+
const sessionId = resolveOutboundSessionId({ account, to });
|
|
1162
|
+
const parts = await buildSiderPartsFromReplyPayload({
|
|
1163
|
+
account,
|
|
1164
|
+
payload: {
|
|
1165
|
+
text,
|
|
1166
|
+
mediaUrl,
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
if (parts.length === 0) {
|
|
1170
|
+
throw new Error("sider sendMedia requires text and/or mediaUrl");
|
|
1171
|
+
}
|
|
1172
|
+
const result = await sendMessageToSider({
|
|
1173
|
+
account,
|
|
1174
|
+
sessionId,
|
|
1175
|
+
parts,
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
channel: CHANNEL_ID,
|
|
1179
|
+
messageId: result.messageId,
|
|
1180
|
+
conversationId: result.conversationId,
|
|
1181
|
+
};
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
gateway: {
|
|
1185
|
+
startAccount: async (ctx) => {
|
|
1186
|
+
const account = resolveSiderAccount(ctx.cfg, ctx.accountId);
|
|
1187
|
+
if (!account.configured) {
|
|
1188
|
+
throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
|
|
1189
|
+
}
|
|
1190
|
+
ctx.log?.info(`[${account.accountId}] starting sider relay monitor (${account.gatewayUrl}, relayId=${account.relayId})`);
|
|
1191
|
+
await monitorSiderSession({
|
|
1192
|
+
cfg: ctx.cfg,
|
|
1193
|
+
account,
|
|
1194
|
+
abortSignal: ctx.abortSignal,
|
|
1195
|
+
log: ctx.log,
|
|
1196
|
+
});
|
|
1197
|
+
ctx.log?.info(`[${account.accountId}] sider relay monitor stopped`);
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
//# sourceMappingURL=channel.js.map
|