@hywkp/sider 0.0.4 → 0.0.6
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 +280 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/src/channel.d.ts +12 -0
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +494 -155
- package/dist/src/channel.js.map +1 -1
- package/dist/src/inbound-media.d.ts +24 -0
- package/dist/src/inbound-media.d.ts.map +1 -0
- package/dist/src/inbound-media.js +56 -0
- package/dist/src/inbound-media.js.map +1 -0
- package/dist/src/media-upload.d.ts +41 -0
- package/dist/src/media-upload.d.ts.map +1 -0
- package/dist/src/media-upload.js +389 -0
- package/dist/src/media-upload.js.map +1 -0
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
import { DEFAULT_ACCOUNT_ID, createReplyPrefixOptions, createTypingCallbacks, formatTextWithAttachmentLinks, normalizeAccountId, resolveOutboundMediaUrls, } from "openclaw/plugin-sdk";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, createReplyPrefixOptions, createTypingCallbacks, formatTextWithAttachmentLinks, jsonResult, normalizeAccountId, readStringParam, resolveChannelMediaMaxBytes, resolveOutboundMediaUrls, } from "openclaw/plugin-sdk";
|
|
2
|
+
import { resolveInboundSiderMedia } from "./inbound-media.js";
|
|
3
|
+
import { buildSiderPartFromInlineAttachment, buildSiderPartsFromReplyPayload } from "./media-upload.js";
|
|
2
4
|
const CHANNEL_ID = "sider";
|
|
3
5
|
const DEFAULT_GATEWAY_URL = "http://47.82.167.142:3001";
|
|
4
6
|
const DEFAULT_CONNECT_TIMEOUT_MS = 8_000;
|
|
5
7
|
const DEFAULT_SEND_TIMEOUT_MS = 12_000;
|
|
6
8
|
const DEFAULT_RECONNECT_DELAY_MS = 2_000;
|
|
7
|
-
const
|
|
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
|
-
};
|
|
9
|
+
const DEFAULT_MEDIA_SAVE_MAX_BYTES = 5 * 1024 * 1024;
|
|
32
10
|
const meta = {
|
|
33
11
|
id: CHANNEL_ID,
|
|
34
12
|
label: "Sider",
|
|
@@ -40,6 +18,9 @@ const meta = {
|
|
|
40
18
|
order: 97,
|
|
41
19
|
};
|
|
42
20
|
let runtimeRef = null;
|
|
21
|
+
const SIDER_SESSION_BINDING_TTL_MS = 6 * 60 * 60 * 1000;
|
|
22
|
+
const SIDER_SESSION_BINDING_MAX = 512;
|
|
23
|
+
const siderSessionBindings = new Map();
|
|
43
24
|
export function setSiderRuntime(runtime) {
|
|
44
25
|
runtimeRef = runtime;
|
|
45
26
|
}
|
|
@@ -49,22 +30,127 @@ function getSiderRuntime() {
|
|
|
49
30
|
}
|
|
50
31
|
return runtimeRef;
|
|
51
32
|
}
|
|
33
|
+
function formatLogMessage(message, data) {
|
|
34
|
+
if (!data || Object.keys(data).length === 0) {
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return `${message} ${JSON.stringify(data)}`;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return message;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
52
44
|
function logDebug(message, data) {
|
|
53
45
|
const core = getSiderRuntime();
|
|
54
46
|
if (!core.logging.shouldLogVerbose()) {
|
|
55
47
|
return;
|
|
56
48
|
}
|
|
57
|
-
core.logging.getChildLogger({ channel: CHANNEL_ID }).debug?.(message, data);
|
|
49
|
+
core.logging.getChildLogger({ channel: CHANNEL_ID }).debug?.(formatLogMessage(message, data));
|
|
58
50
|
}
|
|
59
51
|
function logInfo(message, data) {
|
|
60
|
-
getSiderRuntime()
|
|
52
|
+
getSiderRuntime()
|
|
53
|
+
.logging.getChildLogger({ channel: CHANNEL_ID })
|
|
54
|
+
.info(formatLogMessage(message, data));
|
|
61
55
|
}
|
|
62
56
|
function logWarn(message, data) {
|
|
63
|
-
getSiderRuntime()
|
|
57
|
+
getSiderRuntime()
|
|
58
|
+
.logging.getChildLogger({ channel: CHANNEL_ID })
|
|
59
|
+
.warn(formatLogMessage(message, data));
|
|
64
60
|
}
|
|
65
61
|
function sleep(ms) {
|
|
66
62
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
63
|
}
|
|
64
|
+
function normalizeSessionBindingKey(raw) {
|
|
65
|
+
const key = raw?.trim().toLowerCase();
|
|
66
|
+
return key || undefined;
|
|
67
|
+
}
|
|
68
|
+
function pruneSiderSessionBindings(now = Date.now()) {
|
|
69
|
+
for (const [key, binding] of siderSessionBindings) {
|
|
70
|
+
if (now - binding.lastSeenAt > SIDER_SESSION_BINDING_TTL_MS) {
|
|
71
|
+
siderSessionBindings.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (siderSessionBindings.size <= SIDER_SESSION_BINDING_MAX) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const sorted = [...siderSessionBindings.entries()].sort((a, b) => a[1].lastSeenAt - b[1].lastSeenAt);
|
|
78
|
+
const overflow = siderSessionBindings.size - SIDER_SESSION_BINDING_MAX;
|
|
79
|
+
for (let index = 0; index < overflow; index += 1) {
|
|
80
|
+
const key = sorted[index]?.[0];
|
|
81
|
+
if (key) {
|
|
82
|
+
siderSessionBindings.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function rememberSiderSessionBinding(params) {
|
|
87
|
+
const key = normalizeSessionBindingKey(params.sessionKey);
|
|
88
|
+
if (!key) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const existing = siderSessionBindings.get(key);
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.account = params.account;
|
|
95
|
+
existing.sessionId = params.sessionId;
|
|
96
|
+
existing.lastSeenAt = now;
|
|
97
|
+
pruneSiderSessionBindings(now);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
siderSessionBindings.set(key, {
|
|
101
|
+
account: params.account,
|
|
102
|
+
sessionId: params.sessionId,
|
|
103
|
+
lastSeenAt: now,
|
|
104
|
+
toolSeq: 0,
|
|
105
|
+
currentToolCallId: undefined,
|
|
106
|
+
callIdByToolCallId: new Map(),
|
|
107
|
+
});
|
|
108
|
+
pruneSiderSessionBindings(now);
|
|
109
|
+
}
|
|
110
|
+
function resolveSiderSessionBinding(sessionKey) {
|
|
111
|
+
const key = normalizeSessionBindingKey(sessionKey);
|
|
112
|
+
if (!key) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const binding = siderSessionBindings.get(key);
|
|
116
|
+
if (!binding) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
binding.lastSeenAt = Date.now();
|
|
120
|
+
return binding;
|
|
121
|
+
}
|
|
122
|
+
function toJsonSafeValue(value) {
|
|
123
|
+
if (value === undefined) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(JSON.stringify(value));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return String(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function extractToolResultText(result) {
|
|
134
|
+
const maxChars = 4000;
|
|
135
|
+
const clip = (text) => {
|
|
136
|
+
if (text.length <= maxChars) {
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
return `${text.slice(0, maxChars)}...`;
|
|
140
|
+
};
|
|
141
|
+
const record = toRecord(result);
|
|
142
|
+
if (!record) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
const candidateKeys = ["text", "content", "message", "output", "stdout"];
|
|
146
|
+
for (const key of candidateKeys) {
|
|
147
|
+
const value = record[key];
|
|
148
|
+
if (typeof value === "string" && value.trim()) {
|
|
149
|
+
return clip(value.trim());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
68
154
|
function toRecord(value) {
|
|
69
155
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
156
|
return null;
|
|
@@ -138,6 +224,26 @@ function parseSessionTarget(raw) {
|
|
|
138
224
|
}
|
|
139
225
|
return trimmed;
|
|
140
226
|
}
|
|
227
|
+
function normalizeSiderMessagingTarget(raw) {
|
|
228
|
+
const trimmed = raw.trim();
|
|
229
|
+
if (!trimmed) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const withoutProviderPrefix = trimmed.replace(/^sider:/i, "").trim();
|
|
233
|
+
return withoutProviderPrefix || undefined;
|
|
234
|
+
}
|
|
235
|
+
function looksLikeSiderTargetId(raw, normalized) {
|
|
236
|
+
const candidate = (normalized ?? raw ?? "").trim();
|
|
237
|
+
if (!candidate) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const match = candidate.match(/^session:(.+)$/i);
|
|
241
|
+
if (match) {
|
|
242
|
+
return match[1].trim().length > 0;
|
|
243
|
+
}
|
|
244
|
+
// Sider has no directory lookup yet; treat non-whitespace tokens as explicit ids.
|
|
245
|
+
return !/\s/.test(candidate);
|
|
246
|
+
}
|
|
141
247
|
function resolveOutboundSessionId(params) {
|
|
142
248
|
const target = params.to?.trim() || params.account.defaultTo?.trim() || "";
|
|
143
249
|
if (!target) {
|
|
@@ -145,6 +251,39 @@ function resolveOutboundSessionId(params) {
|
|
|
145
251
|
}
|
|
146
252
|
return parseSessionTarget(target);
|
|
147
253
|
}
|
|
254
|
+
function decodeBase64AttachmentBuffer(raw) {
|
|
255
|
+
const normalized = raw.trim();
|
|
256
|
+
if (!normalized) {
|
|
257
|
+
throw new Error("sider sendAttachment requires non-empty buffer");
|
|
258
|
+
}
|
|
259
|
+
const buffer = Buffer.from(normalized, "base64");
|
|
260
|
+
if (buffer.length === 0) {
|
|
261
|
+
throw new Error("sider sendAttachment requires valid non-empty base64 buffer");
|
|
262
|
+
}
|
|
263
|
+
return buffer;
|
|
264
|
+
}
|
|
265
|
+
function extractSiderToolSend(args) {
|
|
266
|
+
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
267
|
+
if (action !== "sendAttachment") {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const to = typeof args.to === "string" ? args.to.trim() : "";
|
|
271
|
+
if (!to) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const accountId = typeof args.accountId === "string" && args.accountId.trim() ? args.accountId.trim() : undefined;
|
|
275
|
+
const threadId = typeof args.threadId === "string" && args.threadId.trim() ? args.threadId.trim() : undefined;
|
|
276
|
+
return { to, accountId, threadId };
|
|
277
|
+
}
|
|
278
|
+
async function persistInlineAttachmentBuffer(params) {
|
|
279
|
+
const configuredMaxBytes = resolveChannelMediaMaxBytes({
|
|
280
|
+
cfg: params.cfg,
|
|
281
|
+
accountId: params.accountId,
|
|
282
|
+
resolveChannelLimitMb: () => undefined,
|
|
283
|
+
});
|
|
284
|
+
const maxBytes = Math.max(params.buffer.length, configuredMaxBytes ?? 0, DEFAULT_MEDIA_SAVE_MAX_BYTES);
|
|
285
|
+
return await getSiderRuntime().channel.media.saveMediaBuffer(params.buffer, params.contentType, "outbound", maxBytes, params.fileName);
|
|
286
|
+
}
|
|
148
287
|
function resolveRelayWsUrl(gatewayUrl) {
|
|
149
288
|
const url = new URL(gatewayUrl);
|
|
150
289
|
if (url.pathname === "" || url.pathname === "/") {
|
|
@@ -415,94 +554,6 @@ async function sendSiderEventBestEffort(params) {
|
|
|
415
554
|
});
|
|
416
555
|
}
|
|
417
556
|
}
|
|
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
557
|
function parseTextFromPart(part) {
|
|
507
558
|
if (part.type !== "core.text") {
|
|
508
559
|
return null;
|
|
@@ -519,21 +570,27 @@ function parseTextFromPart(part) {
|
|
|
519
570
|
return null;
|
|
520
571
|
}
|
|
521
572
|
function parseMediaFromPart(part) {
|
|
522
|
-
if (part.type !== "core.media") {
|
|
573
|
+
if (part.type !== "core.media" && part.type !== "core.file") {
|
|
523
574
|
return null;
|
|
524
575
|
}
|
|
525
576
|
const payload = toRecord(part.payload);
|
|
526
577
|
if (!payload) {
|
|
527
578
|
return null;
|
|
528
579
|
}
|
|
529
|
-
const urlCandidates = [payload.url, payload.resource_url, payload.media_url];
|
|
580
|
+
const urlCandidates = [payload.download_url, payload.url, payload.resource_url, payload.media_url];
|
|
530
581
|
const url = urlCandidates.find((entry) => typeof entry === "string" && entry.trim());
|
|
531
582
|
if (!url) {
|
|
532
583
|
return null;
|
|
533
584
|
}
|
|
534
|
-
const mimeCandidates = [payload.mime_type, payload.content_type];
|
|
585
|
+
const mimeCandidates = [payload.mime, payload.mime_type, payload.content_type];
|
|
535
586
|
const mimeType = mimeCandidates.find((entry) => typeof entry === "string" && entry.trim());
|
|
536
|
-
|
|
587
|
+
const fileNameCandidates = [payload.name, payload.file_name];
|
|
588
|
+
const fileName = fileNameCandidates.find((entry) => typeof entry === "string" && entry.trim());
|
|
589
|
+
return {
|
|
590
|
+
url: url.trim(),
|
|
591
|
+
mimeType: mimeType?.trim() || undefined,
|
|
592
|
+
fileName: fileName?.trim() || undefined,
|
|
593
|
+
};
|
|
537
594
|
}
|
|
538
595
|
function buildEventMeta(params) {
|
|
539
596
|
return {
|
|
@@ -594,6 +651,147 @@ function buildStreamingDoneEvent(params) {
|
|
|
594
651
|
meta: buildEventMeta({ accountId: params.accountId }),
|
|
595
652
|
};
|
|
596
653
|
}
|
|
654
|
+
function buildToolCallEvent(params) {
|
|
655
|
+
return {
|
|
656
|
+
eventType: "tool.call",
|
|
657
|
+
payload: {
|
|
658
|
+
session_id: params.sessionId,
|
|
659
|
+
seq: params.seq,
|
|
660
|
+
call_id: params.callId,
|
|
661
|
+
phase: params.phase,
|
|
662
|
+
tool_name: params.toolName,
|
|
663
|
+
tool_call_id: params.toolCallId,
|
|
664
|
+
run_id: params.runId,
|
|
665
|
+
session_key: params.sessionKey,
|
|
666
|
+
tool_args: params.toolArgs,
|
|
667
|
+
error: params.error,
|
|
668
|
+
duration_ms: params.durationMs,
|
|
669
|
+
ts: Date.now(),
|
|
670
|
+
},
|
|
671
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function buildToolResultEvent(params) {
|
|
675
|
+
const text = extractToolResultText(params.result);
|
|
676
|
+
const safeResult = toJsonSafeValue(params.result);
|
|
677
|
+
const safeToolArgs = toJsonSafeValue(params.toolArgs);
|
|
678
|
+
return {
|
|
679
|
+
eventType: "tool.result",
|
|
680
|
+
payload: {
|
|
681
|
+
session_id: params.sessionId,
|
|
682
|
+
seq: params.seq,
|
|
683
|
+
call_id: params.callId,
|
|
684
|
+
tool_name: params.toolName,
|
|
685
|
+
tool_call_id: params.toolCallId,
|
|
686
|
+
run_id: params.runId,
|
|
687
|
+
session_key: params.sessionKey,
|
|
688
|
+
tool_args: safeToolArgs,
|
|
689
|
+
result: safeResult,
|
|
690
|
+
error: params.error,
|
|
691
|
+
duration_ms: params.durationMs,
|
|
692
|
+
text,
|
|
693
|
+
has_text: text.trim().length > 0,
|
|
694
|
+
media_urls: [],
|
|
695
|
+
media_count: 0,
|
|
696
|
+
is_error: Boolean(params.error),
|
|
697
|
+
ts: Date.now(),
|
|
698
|
+
},
|
|
699
|
+
meta: buildEventMeta({ accountId: params.accountId }),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function resolveRelayCallIdForToolEvent(params) {
|
|
703
|
+
if (params.toolCallId) {
|
|
704
|
+
const existing = params.binding.callIdByToolCallId.get(params.toolCallId);
|
|
705
|
+
if (existing) {
|
|
706
|
+
params.binding.currentToolCallId = existing;
|
|
707
|
+
return existing;
|
|
708
|
+
}
|
|
709
|
+
const created = crypto.randomUUID();
|
|
710
|
+
params.binding.callIdByToolCallId.set(params.toolCallId, created);
|
|
711
|
+
params.binding.currentToolCallId = created;
|
|
712
|
+
return created;
|
|
713
|
+
}
|
|
714
|
+
if (params.phase === "start" || !params.binding.currentToolCallId) {
|
|
715
|
+
params.binding.currentToolCallId = crypto.randomUUID();
|
|
716
|
+
}
|
|
717
|
+
return params.binding.currentToolCallId;
|
|
718
|
+
}
|
|
719
|
+
function clearRelayCallIdForToolEvent(params) {
|
|
720
|
+
if (params.toolCallId) {
|
|
721
|
+
params.binding.callIdByToolCallId.delete(params.toolCallId);
|
|
722
|
+
}
|
|
723
|
+
if (params.binding.currentToolCallId === params.callId) {
|
|
724
|
+
params.binding.currentToolCallId = undefined;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
export async function emitSiderToolHookEvent(params) {
|
|
728
|
+
const binding = resolveSiderSessionBinding(params.sessionKey);
|
|
729
|
+
if (!binding) {
|
|
730
|
+
logDebug("skip sider tool hook event: session binding not found", {
|
|
731
|
+
sessionKey: params.sessionKey,
|
|
732
|
+
toolName: params.toolName,
|
|
733
|
+
toolCallId: params.toolCallId,
|
|
734
|
+
phase: params.phase,
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const callId = resolveRelayCallIdForToolEvent({
|
|
739
|
+
binding,
|
|
740
|
+
phase: params.phase,
|
|
741
|
+
toolCallId: params.toolCallId,
|
|
742
|
+
});
|
|
743
|
+
if (params.phase === "start") {
|
|
744
|
+
binding.toolSeq += 1;
|
|
745
|
+
const toolCallEvent = buildToolCallEvent({
|
|
746
|
+
sessionId: binding.sessionId,
|
|
747
|
+
accountId: binding.account.accountId,
|
|
748
|
+
seq: binding.toolSeq,
|
|
749
|
+
callId,
|
|
750
|
+
phase: "start",
|
|
751
|
+
toolName: params.toolName,
|
|
752
|
+
toolCallId: params.toolCallId,
|
|
753
|
+
runId: params.runId,
|
|
754
|
+
sessionKey: params.sessionKey,
|
|
755
|
+
toolArgs: params.params,
|
|
756
|
+
error: params.error,
|
|
757
|
+
durationMs: params.durationMs,
|
|
758
|
+
});
|
|
759
|
+
await sendSiderEventBestEffort({
|
|
760
|
+
account: binding.account,
|
|
761
|
+
sessionId: binding.sessionId,
|
|
762
|
+
event: toolCallEvent,
|
|
763
|
+
context: "tool.call.hook.start",
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (params.phase !== "start") {
|
|
767
|
+
binding.toolSeq += 1;
|
|
768
|
+
const toolResultEvent = buildToolResultEvent({
|
|
769
|
+
sessionId: binding.sessionId,
|
|
770
|
+
accountId: binding.account.accountId,
|
|
771
|
+
seq: binding.toolSeq,
|
|
772
|
+
callId,
|
|
773
|
+
toolName: params.toolName,
|
|
774
|
+
toolCallId: params.toolCallId,
|
|
775
|
+
runId: params.runId,
|
|
776
|
+
sessionKey: params.sessionKey,
|
|
777
|
+
toolArgs: params.params,
|
|
778
|
+
result: params.result,
|
|
779
|
+
error: params.error,
|
|
780
|
+
durationMs: params.durationMs,
|
|
781
|
+
});
|
|
782
|
+
await sendSiderEventBestEffort({
|
|
783
|
+
account: binding.account,
|
|
784
|
+
sessionId: binding.sessionId,
|
|
785
|
+
event: toolResultEvent,
|
|
786
|
+
context: `tool.result.hook.${params.phase}`,
|
|
787
|
+
});
|
|
788
|
+
clearRelayCallIdForToolEvent({
|
|
789
|
+
binding,
|
|
790
|
+
callId,
|
|
791
|
+
toolCallId: params.toolCallId,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
597
795
|
async function openStreamingSessionIfNeeded(params) {
|
|
598
796
|
if (params.streamState.active && params.streamState.streamId) {
|
|
599
797
|
return;
|
|
@@ -752,34 +950,42 @@ async function handleStreamingPartialSnapshot(params) {
|
|
|
752
950
|
params.streamState.accumulatedBlockText = params.snapshot;
|
|
753
951
|
}
|
|
754
952
|
async function deliverReplyPayloadToSider(params) {
|
|
953
|
+
const payloadMediaUrls = resolveOutboundMediaUrls(params.payload);
|
|
755
954
|
if (params.kind === "block") {
|
|
756
955
|
const delta = typeof params.payload.text === "string" ? params.payload.text : "";
|
|
757
|
-
if (delta.length
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
params.streamState.blockDeltaCount += 1;
|
|
761
|
-
mergeBlockTextIntoStreamState({
|
|
762
|
-
streamState: params.streamState,
|
|
763
|
-
text: delta,
|
|
764
|
-
});
|
|
765
|
-
if (params.streamState.partialDeltaCount === 0) {
|
|
766
|
-
await sendStreamingDeltaEvent({
|
|
767
|
-
account: params.account,
|
|
768
|
-
sessionId: params.sessionId,
|
|
956
|
+
if (delta.length > 0) {
|
|
957
|
+
params.streamState.blockDeltaCount += 1;
|
|
958
|
+
mergeBlockTextIntoStreamState({
|
|
769
959
|
streamState: params.streamState,
|
|
770
|
-
delta,
|
|
771
|
-
context: "stream.delta.block",
|
|
960
|
+
text: delta,
|
|
772
961
|
});
|
|
962
|
+
if (params.streamState.partialDeltaCount === 0) {
|
|
963
|
+
await sendStreamingDeltaEvent({
|
|
964
|
+
account: params.account,
|
|
965
|
+
sessionId: params.sessionId,
|
|
966
|
+
streamState: params.streamState,
|
|
967
|
+
delta,
|
|
968
|
+
context: "stream.delta.block",
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
logDebug("skip block stream delta because partial streaming is active", {
|
|
973
|
+
accountId: params.account.accountId,
|
|
974
|
+
sessionId: params.sessionId,
|
|
975
|
+
deltaLength: delta.length,
|
|
976
|
+
partialDeltaCount: params.streamState.partialDeltaCount,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
773
979
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
accountId: params.account.accountId,
|
|
777
|
-
sessionId: params.sessionId,
|
|
778
|
-
deltaLength: delta.length,
|
|
779
|
-
partialDeltaCount: params.streamState.partialDeltaCount,
|
|
780
|
-
});
|
|
980
|
+
if (payloadMediaUrls.length === 0) {
|
|
981
|
+
return;
|
|
781
982
|
}
|
|
782
|
-
|
|
983
|
+
logDebug("block payload contains media; sending persisted sider message", {
|
|
984
|
+
accountId: params.account.accountId,
|
|
985
|
+
sessionId: params.sessionId,
|
|
986
|
+
mediaCount: payloadMediaUrls.length,
|
|
987
|
+
hasText: delta.length > 0,
|
|
988
|
+
});
|
|
783
989
|
}
|
|
784
990
|
if (params.kind === "final") {
|
|
785
991
|
await flushStreamEventQueue(params.streamState);
|
|
@@ -810,20 +1016,39 @@ async function deliverReplyPayloadToSider(params) {
|
|
|
810
1016
|
}
|
|
811
1017
|
const parts = await buildSiderPartsFromReplyPayload({
|
|
812
1018
|
account: params.account,
|
|
1019
|
+
sessionId: params.sessionId,
|
|
813
1020
|
payload: params.payload,
|
|
1021
|
+
logger: {
|
|
1022
|
+
debug: logDebug,
|
|
1023
|
+
warn: logWarn,
|
|
1024
|
+
},
|
|
814
1025
|
});
|
|
815
1026
|
if (parts.length === 0) {
|
|
1027
|
+
logDebug("skip sider outbound message: empty parts", {
|
|
1028
|
+
accountId: params.account.accountId,
|
|
1029
|
+
sessionId: params.sessionId,
|
|
1030
|
+
kind: params.kind,
|
|
1031
|
+
hasText: typeof params.payload.text === "string" && params.payload.text.length > 0,
|
|
1032
|
+
mediaCount: payloadMediaUrls.length,
|
|
1033
|
+
});
|
|
816
1034
|
return;
|
|
817
1035
|
}
|
|
1036
|
+
logDebug("sending sider outbound message from payload", {
|
|
1037
|
+
accountId: params.account.accountId,
|
|
1038
|
+
sessionId: params.sessionId,
|
|
1039
|
+
kind: params.kind,
|
|
1040
|
+
partTypes: parts.map((part) => part.type),
|
|
1041
|
+
mediaCount: payloadMediaUrls.length,
|
|
1042
|
+
});
|
|
818
1043
|
await sendMessageToSider({
|
|
819
1044
|
account: params.account,
|
|
820
1045
|
sessionId: params.sessionId,
|
|
821
1046
|
parts,
|
|
822
1047
|
});
|
|
823
|
-
if (params.kind === "final"
|
|
824
|
-
typeof params.payload.text === "string" &&
|
|
825
|
-
params.payload.text.trim()) {
|
|
1048
|
+
if (params.kind === "final" || payloadMediaUrls.length > 0) {
|
|
826
1049
|
params.streamState.persistedFinalText = true;
|
|
1050
|
+
}
|
|
1051
|
+
if (params.kind === "final") {
|
|
827
1052
|
params.streamState.accumulatedBlockText = "";
|
|
828
1053
|
params.streamState.blockDeltaCount = 0;
|
|
829
1054
|
params.streamState.partialDeltaCount = 0;
|
|
@@ -856,7 +1081,7 @@ async function handleInboundRealtimeMessage(params) {
|
|
|
856
1081
|
}
|
|
857
1082
|
const rawText = textChunks.join("\n").trim();
|
|
858
1083
|
const mediaUrls = mediaItems.map((item) => item.url);
|
|
859
|
-
const
|
|
1084
|
+
const parsedMediaTypes = mediaItems
|
|
860
1085
|
.map((item) => item.mimeType)
|
|
861
1086
|
.filter((item) => Boolean(item));
|
|
862
1087
|
const hasControlCommand = rawText ? core.channel.text.hasControlCommand(rawText, cfg) : false;
|
|
@@ -895,7 +1120,27 @@ async function handleInboundRealtimeMessage(params) {
|
|
|
895
1120
|
commandAuthorized,
|
|
896
1121
|
});
|
|
897
1122
|
}
|
|
898
|
-
const
|
|
1123
|
+
const resolvedInboundMedia = await resolveInboundSiderMedia({
|
|
1124
|
+
runtime: core,
|
|
1125
|
+
cfg,
|
|
1126
|
+
accountId: account.accountId,
|
|
1127
|
+
mediaItems,
|
|
1128
|
+
logger: {
|
|
1129
|
+
debug: logDebug,
|
|
1130
|
+
warn: logWarn,
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
const unresolvedMediaUrls = resolvedInboundMedia.unresolvedMedia.map((item) => item.url);
|
|
1134
|
+
if (unresolvedMediaUrls.length > 0) {
|
|
1135
|
+
logWarn("sider inbound media fallback to attachment url", {
|
|
1136
|
+
accountId: account.accountId,
|
|
1137
|
+
sessionId,
|
|
1138
|
+
unresolvedCount: unresolvedMediaUrls.length,
|
|
1139
|
+
unresolvedMediaUrls,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
const bodyForAgent = formatTextWithAttachmentLinks(rawText, unresolvedMediaUrls);
|
|
1143
|
+
const mediaPayload = resolvedInboundMedia.mediaPayload;
|
|
899
1144
|
const route = core.channel.routing.resolveAgentRoute({
|
|
900
1145
|
cfg,
|
|
901
1146
|
channel: CHANNEL_ID,
|
|
@@ -937,12 +1182,21 @@ async function handleInboundRealtimeMessage(params) {
|
|
|
937
1182
|
WasMentioned: true,
|
|
938
1183
|
OriginatingChannel: CHANNEL_ID,
|
|
939
1184
|
OriginatingTo: to,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1185
|
+
...mediaPayload,
|
|
1186
|
+
MediaUrl: mediaPayload.MediaUrl ||
|
|
1187
|
+
(unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls[0] : undefined),
|
|
1188
|
+
MediaUrls: mediaPayload.MediaUrls ||
|
|
1189
|
+
(unresolvedMediaUrls.length > 0 ? unresolvedMediaUrls : undefined),
|
|
1190
|
+
MediaType: mediaPayload.MediaType || parsedMediaTypes[0],
|
|
1191
|
+
MediaTypes: mediaPayload.MediaTypes ||
|
|
1192
|
+
(parsedMediaTypes.length > 0 ? parsedMediaTypes : undefined),
|
|
944
1193
|
CommandAuthorized: commandAuthorized,
|
|
945
1194
|
});
|
|
1195
|
+
rememberSiderSessionBinding({
|
|
1196
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
1197
|
+
account,
|
|
1198
|
+
sessionId,
|
|
1199
|
+
});
|
|
946
1200
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
947
1201
|
agentId: route.agentId,
|
|
948
1202
|
});
|
|
@@ -971,6 +1225,8 @@ async function handleInboundRealtimeMessage(params) {
|
|
|
971
1225
|
commandAuthorized,
|
|
972
1226
|
textLength: rawText.length,
|
|
973
1227
|
mediaCount: mediaUrls.length,
|
|
1228
|
+
mediaDownloadedCount: mediaPayload.MediaPaths?.length ?? 0,
|
|
1229
|
+
mediaDownloadFailedCount: unresolvedMediaUrls.length,
|
|
974
1230
|
});
|
|
975
1231
|
const streamState = {
|
|
976
1232
|
active: false,
|
|
@@ -1128,8 +1384,6 @@ async function handleInboundRealtimeMessage(params) {
|
|
|
1128
1384
|
});
|
|
1129
1385
|
streamState.persistedFinalText = true;
|
|
1130
1386
|
streamState.accumulatedBlockText = "";
|
|
1131
|
-
streamState.partialDeltaCount = 0;
|
|
1132
|
-
streamState.partialSnapshot = "";
|
|
1133
1387
|
}
|
|
1134
1388
|
}
|
|
1135
1389
|
async function handleInboundRealtimeEvent(params) {
|
|
@@ -1331,6 +1585,78 @@ export const siderPlugin = {
|
|
|
1331
1585
|
}),
|
|
1332
1586
|
resolveDefaultTo: ({ cfg, accountId }) => resolveSiderAccount(cfg, accountId).defaultTo,
|
|
1333
1587
|
},
|
|
1588
|
+
actions: {
|
|
1589
|
+
listActions: ({ cfg }) => listSiderAccountIds(cfg).some((accountId) => resolveSiderAccount(cfg, accountId).configured)
|
|
1590
|
+
? ["sendAttachment"]
|
|
1591
|
+
: [],
|
|
1592
|
+
supportsAction: ({ action }) => action === "sendAttachment",
|
|
1593
|
+
extractToolSend: ({ args }) => extractSiderToolSend(args),
|
|
1594
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
1595
|
+
if (action !== "sendAttachment") {
|
|
1596
|
+
throw new Error(`sider action ${action} is not supported`);
|
|
1597
|
+
}
|
|
1598
|
+
const account = resolveSiderAccount(cfg, accountId);
|
|
1599
|
+
if (!account.configured) {
|
|
1600
|
+
throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
|
|
1601
|
+
}
|
|
1602
|
+
const to = readStringParam(params, "to", { required: true });
|
|
1603
|
+
const rawBuffer = readStringParam(params, "buffer", { trim: false, required: true });
|
|
1604
|
+
const contentType = readStringParam(params, "contentType", { trim: false }) ??
|
|
1605
|
+
readStringParam(params, "mimeType", { trim: false });
|
|
1606
|
+
const fileName = readStringParam(params, "filename", { trim: false }) ?? undefined;
|
|
1607
|
+
const text = readStringParam(params, "caption", { allowEmpty: true }) ??
|
|
1608
|
+
readStringParam(params, "message", { allowEmpty: true }) ??
|
|
1609
|
+
undefined;
|
|
1610
|
+
const sessionId = resolveOutboundSessionId({ account, to });
|
|
1611
|
+
const buffer = decodeBase64AttachmentBuffer(rawBuffer);
|
|
1612
|
+
const savedAttachment = await persistInlineAttachmentBuffer({
|
|
1613
|
+
cfg,
|
|
1614
|
+
accountId,
|
|
1615
|
+
buffer,
|
|
1616
|
+
contentType: contentType ?? undefined,
|
|
1617
|
+
fileName,
|
|
1618
|
+
});
|
|
1619
|
+
const resolvedContentType = contentType ?? savedAttachment.contentType ?? undefined;
|
|
1620
|
+
const resolvedFileName = fileName ?? savedAttachment.path;
|
|
1621
|
+
const parts = [];
|
|
1622
|
+
const trimmedText = text?.trim();
|
|
1623
|
+
if (trimmedText) {
|
|
1624
|
+
parts.push({
|
|
1625
|
+
type: "core.text",
|
|
1626
|
+
spec_version: 1,
|
|
1627
|
+
payload: { text: trimmedText },
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
parts.push(await buildSiderPartFromInlineAttachment({
|
|
1631
|
+
account,
|
|
1632
|
+
sessionId,
|
|
1633
|
+
buffer,
|
|
1634
|
+
contentType: resolvedContentType,
|
|
1635
|
+
fileName: resolvedFileName,
|
|
1636
|
+
sourceLabel: resolvedFileName,
|
|
1637
|
+
logger: {
|
|
1638
|
+
debug: logDebug,
|
|
1639
|
+
warn: logWarn,
|
|
1640
|
+
},
|
|
1641
|
+
}));
|
|
1642
|
+
const result = await sendMessageToSider({
|
|
1643
|
+
account,
|
|
1644
|
+
sessionId,
|
|
1645
|
+
parts,
|
|
1646
|
+
});
|
|
1647
|
+
return jsonResult({
|
|
1648
|
+
ok: true,
|
|
1649
|
+
channel: CHANNEL_ID,
|
|
1650
|
+
messageId: result.messageId,
|
|
1651
|
+
conversationId: result.conversationId,
|
|
1652
|
+
path: savedAttachment.path,
|
|
1653
|
+
mediaUrl: savedAttachment.path,
|
|
1654
|
+
mediaUrls: [savedAttachment.path],
|
|
1655
|
+
contentType: resolvedContentType,
|
|
1656
|
+
filename: resolvedFileName,
|
|
1657
|
+
});
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1334
1660
|
outbound: {
|
|
1335
1661
|
deliveryMode: "direct",
|
|
1336
1662
|
sendText: async ({ cfg, accountId, to, text }) => {
|
|
@@ -1360,7 +1686,7 @@ export const siderPlugin = {
|
|
|
1360
1686
|
conversationId: result.conversationId,
|
|
1361
1687
|
};
|
|
1362
1688
|
},
|
|
1363
|
-
sendMedia: async ({ cfg, accountId, to, text, mediaUrl }) => {
|
|
1689
|
+
sendMedia: async ({ cfg, accountId, to, text, mediaUrl, mediaLocalRoots }) => {
|
|
1364
1690
|
const account = resolveSiderAccount(cfg, accountId);
|
|
1365
1691
|
if (!account.configured) {
|
|
1366
1692
|
throw new Error(`sider account "${account.accountId}" is not configured: missing gatewayUrl/sessionId(or sessionKey)/relayId`);
|
|
@@ -1368,10 +1694,16 @@ export const siderPlugin = {
|
|
|
1368
1694
|
const sessionId = resolveOutboundSessionId({ account, to });
|
|
1369
1695
|
const parts = await buildSiderPartsFromReplyPayload({
|
|
1370
1696
|
account,
|
|
1697
|
+
sessionId,
|
|
1371
1698
|
payload: {
|
|
1372
1699
|
text,
|
|
1373
1700
|
mediaUrl,
|
|
1374
1701
|
},
|
|
1702
|
+
mediaLocalRoots,
|
|
1703
|
+
logger: {
|
|
1704
|
+
debug: logDebug,
|
|
1705
|
+
warn: logWarn,
|
|
1706
|
+
},
|
|
1375
1707
|
});
|
|
1376
1708
|
if (parts.length === 0) {
|
|
1377
1709
|
throw new Error("sider sendMedia requires text and/or mediaUrl");
|
|
@@ -1404,5 +1736,12 @@ export const siderPlugin = {
|
|
|
1404
1736
|
ctx.log?.info(`[${account.accountId}] sider relay monitor stopped`);
|
|
1405
1737
|
},
|
|
1406
1738
|
},
|
|
1739
|
+
messaging: {
|
|
1740
|
+
normalizeTarget: normalizeSiderMessagingTarget,
|
|
1741
|
+
targetResolver: {
|
|
1742
|
+
looksLikeId: looksLikeSiderTargetId,
|
|
1743
|
+
hint: "session:<sessionId> (or raw <sessionId>)",
|
|
1744
|
+
},
|
|
1745
|
+
},
|
|
1407
1746
|
};
|
|
1408
1747
|
//# sourceMappingURL=channel.js.map
|