@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.
@@ -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 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
- };
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().logging.getChildLogger({ channel: CHANNEL_ID }).info(message, data);
52
+ getSiderRuntime()
53
+ .logging.getChildLogger({ channel: CHANNEL_ID })
54
+ .info(formatLogMessage(message, data));
61
55
  }
62
56
  function logWarn(message, data) {
63
- getSiderRuntime().logging.getChildLogger({ channel: CHANNEL_ID }).warn(message, data);
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
- return { url: url.trim(), mimeType: mimeType?.trim() || undefined };
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 === 0) {
758
- return;
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
- else {
775
- logDebug("skip block stream delta because partial streaming is active", {
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
- return;
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 mediaTypes = mediaItems
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 bodyForAgent = formatTextWithAttachmentLinks(rawText, mediaUrls);
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
- MediaUrl: mediaUrls[0],
941
- MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
942
- MediaType: mediaTypes[0],
943
- MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
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