@sider-ai/chrome-openclaw-sider 1.0.31
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 +75 -0
- package/README.zh_CN.md +108 -0
- package/dist/account-cW9SLuNu.d.ts +75 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +87 -0
- package/dist/setup-entry.d.ts +6 -0
- package/dist/setup-entry.js +11 -0
- package/dist/setup-plugin-api.d.ts +7 -0
- package/dist/setup-plugin-api.js +4 -0
- package/dist/src/account.d.ts +6 -0
- package/dist/src/account.js +260 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +225 -0
- package/dist/src/channel-auto-title.d.ts +6 -0
- package/dist/src/channel-auto-title.js +102 -0
- package/dist/src/channel-builders.d.ts +105 -0
- package/dist/src/channel-builders.js +238 -0
- package/dist/src/channel-hooks.d.ts +30 -0
- package/dist/src/channel-hooks.js +380 -0
- package/dist/src/channel-monitor.d.ts +34 -0
- package/dist/src/channel-monitor.js +335 -0
- package/dist/src/channel-parts.d.ts +26 -0
- package/dist/src/channel-parts.js +32 -0
- package/dist/src/channel-relay.d.ts +117 -0
- package/dist/src/channel-relay.js +574 -0
- package/dist/src/channel-runtime.d.ts +33 -0
- package/dist/src/channel-runtime.js +138 -0
- package/dist/src/channel-send-result.d.ts +19 -0
- package/dist/src/channel-send-result.js +126 -0
- package/dist/src/channel-send.d.ts +73 -0
- package/dist/src/channel-send.js +291 -0
- package/dist/src/channel-session-model.d.ts +19 -0
- package/dist/src/channel-session-model.js +244 -0
- package/dist/src/channel-shared.d.ts +153 -0
- package/dist/src/channel-shared.js +96 -0
- package/dist/src/channel-state.d.ts +93 -0
- package/dist/src/channel-state.js +475 -0
- package/dist/src/channel-streaming.d.ts +117 -0
- package/dist/src/channel-streaming.js +681 -0
- package/dist/src/channel-types.d.ts +209 -0
- package/dist/src/channel-types.js +40 -0
- package/dist/src/channel-typing.d.ts +17 -0
- package/dist/src/channel-typing.js +79 -0
- package/dist/src/channel-util.d.ts +78 -0
- package/dist/src/channel-util.js +604 -0
- package/dist/src/channel.d.ts +14 -0
- package/dist/src/channel.js +834 -0
- package/dist/src/channel.setup.d.ts +10 -0
- package/dist/src/channel.setup.js +9 -0
- package/dist/src/config.d.ts +18 -0
- package/dist/src/config.js +38 -0
- package/dist/src/inbound-media.d.ts +37 -0
- package/dist/src/inbound-media.js +148 -0
- package/dist/src/media-upload.d.ts +87 -0
- package/dist/src/media-upload.js +1308 -0
- package/dist/src/setup-core.d.ts +72 -0
- package/dist/src/setup-core.js +343 -0
- package/dist/src/user-agent.d.ts +4 -0
- package/dist/src/user-agent.js +6 -0
- package/openclaw.plugin.json +96 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
|
6
|
+
import { formatAuthorizationHeader } from "./auth.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MEDIA_SAVE_MAX_BYTES,
|
|
9
|
+
DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES
|
|
10
|
+
} from "./channel-types.js";
|
|
11
|
+
import { buildSourceFields } from "./channel-builders.js";
|
|
12
|
+
import { siderOutboundLogger } from "./channel-runtime.js";
|
|
13
|
+
import { resolveSiderDownloadUrl } from "./inbound-media.js";
|
|
14
|
+
import { SIDER_USER_AGENT } from "./user-agent.js";
|
|
15
|
+
const DEFAULT_MIME = "application/octet-stream";
|
|
16
|
+
const DEFAULT_DEFERRED_UPLOAD_MAX_ATTEMPTS = 3;
|
|
17
|
+
const DEFAULT_DEFERRED_UPLOAD_RETRY_BASE_MS = 1e3;
|
|
18
|
+
async function sleepMs(delayMs) {
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
20
|
+
}
|
|
21
|
+
const MIME_BY_EXTENSION = {
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".jpeg": "image/jpeg",
|
|
24
|
+
".png": "image/png",
|
|
25
|
+
".gif": "image/gif",
|
|
26
|
+
".webp": "image/webp",
|
|
27
|
+
".bmp": "image/bmp",
|
|
28
|
+
".svg": "image/svg+xml",
|
|
29
|
+
".pdf": "application/pdf",
|
|
30
|
+
".txt": "text/plain",
|
|
31
|
+
".json": "application/json",
|
|
32
|
+
".zip": "application/zip"
|
|
33
|
+
};
|
|
34
|
+
const EXTENSION_BY_MIME = {
|
|
35
|
+
"image/jpeg": ".jpg",
|
|
36
|
+
"image/png": ".png",
|
|
37
|
+
"image/gif": ".gif",
|
|
38
|
+
"image/webp": ".webp",
|
|
39
|
+
"image/bmp": ".bmp",
|
|
40
|
+
"image/svg+xml": ".svg",
|
|
41
|
+
"application/pdf": ".pdf",
|
|
42
|
+
"text/plain": ".txt",
|
|
43
|
+
"application/json": ".json",
|
|
44
|
+
"application/zip": ".zip"
|
|
45
|
+
};
|
|
46
|
+
const ATTACHMENT_REF_PREFIX = "attachment://";
|
|
47
|
+
const MARKDOWN_IMAGE_RE = /!\[[^\]]*]\(([^)\r\n]+)\)/g;
|
|
48
|
+
const MARKDOWN_LINK_RE = /\[[^\]]*]\(([^)\r\n]+)\)/g;
|
|
49
|
+
const LOCAL_PATH_PREFIX_RE = /^(\/|\.\/|\.\.\/|~\/|[a-zA-Z]:[\\/]|\\\\)/;
|
|
50
|
+
const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
51
|
+
const FILE_EXT_RE = /\.[a-z0-9]{1,10}$/i;
|
|
52
|
+
const GENERATED_ATTACHMENT_SUFFIX_RE = /---[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?:\.[a-z0-9]{1,10})?$/i;
|
|
53
|
+
function toRecord(value) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function normalizeMime(raw) {
|
|
60
|
+
const value = raw?.trim().toLowerCase();
|
|
61
|
+
if (!value) {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
const normalized = value.split(";")[0]?.trim();
|
|
65
|
+
return normalized || void 0;
|
|
66
|
+
}
|
|
67
|
+
function extFromPath(raw) {
|
|
68
|
+
const qPos = raw.indexOf("?");
|
|
69
|
+
const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
|
|
70
|
+
const slashPos = Math.max(sanitized.lastIndexOf("/"), sanitized.lastIndexOf("\\"));
|
|
71
|
+
const last = slashPos >= 0 ? sanitized.slice(slashPos + 1) : sanitized;
|
|
72
|
+
const dotPos = last.lastIndexOf(".");
|
|
73
|
+
return dotPos >= 0 ? last.slice(dotPos).toLowerCase() : "";
|
|
74
|
+
}
|
|
75
|
+
function fileNameFromMediaUrl(raw) {
|
|
76
|
+
try {
|
|
77
|
+
if (/^https?:\/\//i.test(raw) || /^file:\/\//i.test(raw)) {
|
|
78
|
+
const pathname = new URL(raw).pathname;
|
|
79
|
+
const seg2 = pathname.split("/").filter(Boolean).pop();
|
|
80
|
+
return seg2 || void 0;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
const qPos = raw.indexOf("?");
|
|
85
|
+
const sanitized = qPos >= 0 ? raw.slice(0, qPos) : raw;
|
|
86
|
+
const seg = sanitized.split(/[\\/]/).filter(Boolean).pop();
|
|
87
|
+
return seg || void 0;
|
|
88
|
+
}
|
|
89
|
+
function normalizeFileName(raw, fallback) {
|
|
90
|
+
const base = (raw ?? "").trim().split(/[\\/]/).filter(Boolean).pop() ?? "";
|
|
91
|
+
const cleaned = base.replace(/[\u0000-\u001f]/g, "");
|
|
92
|
+
if (!cleaned || cleaned === "." || cleaned === "..") {
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
return cleaned.slice(0, 255);
|
|
96
|
+
}
|
|
97
|
+
function inferMimeFromFileName(fileName) {
|
|
98
|
+
const ext = extFromPath(fileName);
|
|
99
|
+
return MIME_BY_EXTENSION[ext];
|
|
100
|
+
}
|
|
101
|
+
function inferFallbackFileName(params) {
|
|
102
|
+
const ext = EXTENSION_BY_MIME[params.mimeType];
|
|
103
|
+
if (ext) {
|
|
104
|
+
return `attachment${ext}`;
|
|
105
|
+
}
|
|
106
|
+
return params.mediaKind === "image" ? "attachment.png" : "attachment.bin";
|
|
107
|
+
}
|
|
108
|
+
function parsePositiveInteger(raw) {
|
|
109
|
+
if (!raw) {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
const value = Number(raw);
|
|
113
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
return Math.floor(value);
|
|
117
|
+
}
|
|
118
|
+
function resolveSource(...candidates) {
|
|
119
|
+
for (const candidate of candidates) {
|
|
120
|
+
const trimmed = candidate?.trim();
|
|
121
|
+
if (trimmed) {
|
|
122
|
+
return trimmed;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
function inferSourceMeta(sourceRef) {
|
|
128
|
+
const value = sourceRef?.trim();
|
|
129
|
+
if (!value) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value) || /^file:\/\//i.test(value)) {
|
|
133
|
+
return { path: value };
|
|
134
|
+
}
|
|
135
|
+
return { mediaUrl: value };
|
|
136
|
+
}
|
|
137
|
+
function resolveMediaKind(params) {
|
|
138
|
+
const contentType = normalizeMime(params.contentType);
|
|
139
|
+
return params.kind === "image" || contentType?.startsWith("image/") ? "image" : "file";
|
|
140
|
+
}
|
|
141
|
+
function resolveDirectMediaUrl(loadUrl, source) {
|
|
142
|
+
const primary = loadUrl.trim();
|
|
143
|
+
if (primary) {
|
|
144
|
+
return primary;
|
|
145
|
+
}
|
|
146
|
+
const fallback = source?.trim();
|
|
147
|
+
return fallback || void 0;
|
|
148
|
+
}
|
|
149
|
+
function buildCoreFilePayloadFields(params) {
|
|
150
|
+
const source = params.source?.trim() || resolveDirectMediaUrl(params.loadUrl ?? "", params.source);
|
|
151
|
+
return {
|
|
152
|
+
...buildSourceFields(source),
|
|
153
|
+
...params.mediaKind ? { media_kind: params.mediaKind } : {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildCoreFilePayloadBase(params) {
|
|
157
|
+
return {
|
|
158
|
+
...buildCoreFilePayloadFields({
|
|
159
|
+
source: params.source,
|
|
160
|
+
loadUrl: params.loadUrl,
|
|
161
|
+
mediaKind: params.mediaKind
|
|
162
|
+
}),
|
|
163
|
+
name: params.fileName,
|
|
164
|
+
...typeof params.size === "number" && Number.isFinite(params.size) ? { size: params.size } : {},
|
|
165
|
+
mime: params.mimeType
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function resolveLocalMediaPath(loadUrl) {
|
|
169
|
+
const trimmed = loadUrl.trim();
|
|
170
|
+
if (!trimmed || /^https?:\/\//i.test(trimmed) || /^data:/i.test(trimmed)) {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
if (/^file:\/\//i.test(trimmed)) {
|
|
174
|
+
try {
|
|
175
|
+
return path.resolve(new URL(trimmed).pathname);
|
|
176
|
+
} catch {
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (SCHEME_RE.test(trimmed)) {
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
return path.resolve(trimmed);
|
|
184
|
+
}
|
|
185
|
+
function estimateDataUrlSizeBytes(loadUrl) {
|
|
186
|
+
if (!/^data:/i.test(loadUrl)) {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
const commaIndex = loadUrl.indexOf(",");
|
|
190
|
+
if (commaIndex < 0) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
const metadata = loadUrl.slice(0, commaIndex);
|
|
194
|
+
const body = loadUrl.slice(commaIndex + 1);
|
|
195
|
+
if (/;base64/i.test(metadata)) {
|
|
196
|
+
const normalized = body.replace(/\s+/g, "");
|
|
197
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
198
|
+
return Math.max(0, Math.floor(normalized.length * 3 / 4) - padding);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return Buffer.byteLength(decodeURIComponent(body), "utf8");
|
|
202
|
+
} catch {
|
|
203
|
+
return Buffer.byteLength(body, "utf8");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function probeRemoteContentLengthBytes(params) {
|
|
207
|
+
if (!/^https?:\/\//i.test(params.loadUrl)) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetch(params.loadUrl, {
|
|
212
|
+
method: "HEAD",
|
|
213
|
+
headers: {
|
|
214
|
+
"User-Agent": SIDER_USER_AGENT
|
|
215
|
+
},
|
|
216
|
+
signal: AbortSignal.timeout(params.account.sendTimeoutMs)
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
221
|
+
return parsePositiveInteger(response.headers.get("content-length"));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
params.logger?.warn?.("sider media size probe failed; falling back to buffered load", {
|
|
224
|
+
accountId: params.account.accountId,
|
|
225
|
+
sessionId: params.sessionId,
|
|
226
|
+
mediaUrl: params.loadUrl,
|
|
227
|
+
error: String(error)
|
|
228
|
+
});
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function probeMediaSizeBytes(params) {
|
|
233
|
+
const dataUrlSize = estimateDataUrlSizeBytes(params.loadUrl);
|
|
234
|
+
if (typeof dataUrlSize === "number") {
|
|
235
|
+
return dataUrlSize;
|
|
236
|
+
}
|
|
237
|
+
const localPath = resolveLocalMediaPath(params.loadUrl);
|
|
238
|
+
if (localPath) {
|
|
239
|
+
try {
|
|
240
|
+
const stats = await stat(localPath);
|
|
241
|
+
if (stats.isFile()) {
|
|
242
|
+
return stats.size;
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
return await probeRemoteContentLengthBytes(params);
|
|
249
|
+
}
|
|
250
|
+
function buildDirectFilePart(params) {
|
|
251
|
+
const directUrl = resolveDirectMediaUrl(params.loadUrl ?? "", params.source);
|
|
252
|
+
const inferredKind = resolveMediaKind({
|
|
253
|
+
kind: params.mediaKind,
|
|
254
|
+
contentType: params.contentType
|
|
255
|
+
});
|
|
256
|
+
const fallbackName = inferFallbackFileName({
|
|
257
|
+
mimeType: normalizeMime(params.contentType) ?? DEFAULT_MIME,
|
|
258
|
+
mediaKind: inferredKind
|
|
259
|
+
});
|
|
260
|
+
const fileName = normalizeFileName(
|
|
261
|
+
params.fileName || fileNameFromMediaUrl(params.source ?? "") || fileNameFromMediaUrl(directUrl ?? ""),
|
|
262
|
+
fallbackName
|
|
263
|
+
);
|
|
264
|
+
const mimeType = normalizeMime(params.contentType) || inferMimeFromFileName(fileName) || DEFAULT_MIME;
|
|
265
|
+
return {
|
|
266
|
+
type: "core.file",
|
|
267
|
+
spec_version: 1,
|
|
268
|
+
payload: {
|
|
269
|
+
...directUrl ? { url: directUrl } : {},
|
|
270
|
+
...buildCoreFilePayloadBase({
|
|
271
|
+
source: params.source,
|
|
272
|
+
loadUrl: params.loadUrl,
|
|
273
|
+
mediaKind: inferredKind,
|
|
274
|
+
fileName,
|
|
275
|
+
size: params.size,
|
|
276
|
+
mimeType
|
|
277
|
+
}),
|
|
278
|
+
upload_status: "skipped"
|
|
279
|
+
},
|
|
280
|
+
meta: {
|
|
281
|
+
...params.meta ?? {},
|
|
282
|
+
upload_skipped: true,
|
|
283
|
+
upload_skip_max_bytes: DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function sha256Hex(buffer) {
|
|
288
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
289
|
+
}
|
|
290
|
+
function resolveGatewayApiUrl(gatewayUrl, path2) {
|
|
291
|
+
const url = new URL(gatewayUrl);
|
|
292
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
293
|
+
const suffix = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
294
|
+
url.pathname = `${basePath}${suffix}`;
|
|
295
|
+
url.search = "";
|
|
296
|
+
url.hash = "";
|
|
297
|
+
return url.toString();
|
|
298
|
+
}
|
|
299
|
+
async function readResponseErrorBody(response) {
|
|
300
|
+
try {
|
|
301
|
+
const text = (await response.text()).trim();
|
|
302
|
+
return text;
|
|
303
|
+
} catch {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function describeError(error) {
|
|
308
|
+
if (error instanceof Error) {
|
|
309
|
+
return error.message || String(error);
|
|
310
|
+
}
|
|
311
|
+
return String(error);
|
|
312
|
+
}
|
|
313
|
+
async function resolveDownloadUrlBestEffort(params) {
|
|
314
|
+
try {
|
|
315
|
+
return await resolveSiderDownloadUrl({
|
|
316
|
+
gatewayUrl: params.account.gatewayUrl,
|
|
317
|
+
objectKey: params.objectKey,
|
|
318
|
+
token: params.account.token,
|
|
319
|
+
timeoutMs: params.account.sendTimeoutMs
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
params.logger?.warn?.("sider failed to resolve outbound download url after files/init", {
|
|
323
|
+
accountId: params.account.accountId,
|
|
324
|
+
sessionId: params.sessionId,
|
|
325
|
+
objectKey: params.objectKey,
|
|
326
|
+
error: describeError(error)
|
|
327
|
+
});
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function stripQueryAndHash(raw) {
|
|
332
|
+
return raw.split(/[?#]/, 1)[0] ?? raw;
|
|
333
|
+
}
|
|
334
|
+
function parseMarkdownLinkTarget(rawTarget) {
|
|
335
|
+
const trimmed = rawTarget.trim();
|
|
336
|
+
if (!trimmed) {
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
let target = trimmed;
|
|
340
|
+
const wrappedInAngles = target.startsWith("<") && target.endsWith(">");
|
|
341
|
+
if (wrappedInAngles) {
|
|
342
|
+
target = target.slice(1, -1).trim();
|
|
343
|
+
}
|
|
344
|
+
if (!wrappedInAngles) {
|
|
345
|
+
const whitespaceIndex = target.search(/\s/);
|
|
346
|
+
if (whitespaceIndex > 0) {
|
|
347
|
+
target = target.slice(0, whitespaceIndex);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
target = target.trim();
|
|
351
|
+
return target || void 0;
|
|
352
|
+
}
|
|
353
|
+
function shouldTreatMarkdownLinkAsMediaTarget(target) {
|
|
354
|
+
const normalized = target.trim();
|
|
355
|
+
if (!normalized) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
if (normalized.toLowerCase().startsWith(ATTACHMENT_REF_PREFIX)) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (/^file:\/\//i.test(normalized)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (LOCAL_PATH_PREFIX_RE.test(normalized)) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (!SCHEME_RE.test(normalized)) {
|
|
368
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (FILE_EXT_RE.test(stripQueryAndHash(normalized))) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
function extractMarkdownMediaUrls(text) {
|
|
378
|
+
if (!text) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
const urls = [];
|
|
382
|
+
for (const match of text.matchAll(MARKDOWN_IMAGE_RE)) {
|
|
383
|
+
const target = parseMarkdownLinkTarget(match[1] ?? "");
|
|
384
|
+
if (target) {
|
|
385
|
+
urls.push(target);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const match of text.matchAll(MARKDOWN_LINK_RE)) {
|
|
389
|
+
const start = match.index ?? 0;
|
|
390
|
+
if (start > 0 && text[start - 1] === "!") {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const target = parseMarkdownLinkTarget(match[1] ?? "");
|
|
394
|
+
if (target && shouldTreatMarkdownLinkAsMediaTarget(target)) {
|
|
395
|
+
urls.push(target);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return urls;
|
|
399
|
+
}
|
|
400
|
+
function dedupeMediaUrls(values) {
|
|
401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
402
|
+
const deduped = [];
|
|
403
|
+
for (const raw of values) {
|
|
404
|
+
const trimmed = raw.trim();
|
|
405
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
seen.add(trimmed);
|
|
409
|
+
deduped.push(trimmed);
|
|
410
|
+
}
|
|
411
|
+
return deduped;
|
|
412
|
+
}
|
|
413
|
+
function isPathWithinRoot(rootDir, candidatePath) {
|
|
414
|
+
const relative = path.relative(rootDir, candidatePath);
|
|
415
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
416
|
+
}
|
|
417
|
+
function isPathWithinAnyRoot(filePath, roots) {
|
|
418
|
+
if (!roots?.length) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
for (const rootEntry of roots) {
|
|
422
|
+
const trimmedRoot = rootEntry?.trim();
|
|
423
|
+
if (!trimmedRoot) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (isPathWithinRoot(path.resolve(trimmedRoot), filePath)) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
async function isExistingFile(filePath) {
|
|
433
|
+
try {
|
|
434
|
+
const stats = await stat(filePath);
|
|
435
|
+
return stats.isFile();
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function looksLikeGeneratedAttachmentName(params) {
|
|
441
|
+
const requested = params.requestedFileName.trim();
|
|
442
|
+
const candidate = params.candidateName.trim();
|
|
443
|
+
if (!requested || !candidate) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (requested === candidate) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (!GENERATED_ATTACHMENT_SUFFIX_RE.test(candidate)) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
const requestedExt = path.extname(requested).toLowerCase();
|
|
453
|
+
const requestedBase = requestedExt ? requested.slice(0, -requestedExt.length) : requested;
|
|
454
|
+
if (!requestedBase) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
if (!candidate.startsWith(`${requestedBase}---`)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (requestedExt && !candidate.toLowerCase().endsWith(requestedExt)) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
function buildAttachmentLookupDirs(rootDir) {
|
|
466
|
+
return Array.from(
|
|
467
|
+
/* @__PURE__ */ new Set([
|
|
468
|
+
rootDir,
|
|
469
|
+
path.join(rootDir, "tmp"),
|
|
470
|
+
path.join(rootDir, "out"),
|
|
471
|
+
path.join(rootDir, "outbound"),
|
|
472
|
+
path.join(rootDir, "inbound"),
|
|
473
|
+
path.join(rootDir, "media"),
|
|
474
|
+
path.join(rootDir, "media", "outbound"),
|
|
475
|
+
path.join(rootDir, "media", "inbound"),
|
|
476
|
+
path.join(rootDir, "workspace"),
|
|
477
|
+
path.join(rootDir, "sandboxes")
|
|
478
|
+
])
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
async function resolveStoredAttachmentByFileName(params) {
|
|
482
|
+
let bestPath;
|
|
483
|
+
let bestMtimeMs = -1;
|
|
484
|
+
for (const dir of buildAttachmentLookupDirs(params.rootDir)) {
|
|
485
|
+
if (!isPathWithinRoot(params.rootDir, dir)) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
let entries;
|
|
489
|
+
try {
|
|
490
|
+
entries = await readdir(dir, { encoding: "utf8" });
|
|
491
|
+
} catch {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (!entries) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
for (const entryName of entries) {
|
|
498
|
+
if (!looksLikeGeneratedAttachmentName({
|
|
499
|
+
requestedFileName: params.fileName,
|
|
500
|
+
candidateName: entryName
|
|
501
|
+
})) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const candidate = path.join(dir, entryName);
|
|
505
|
+
if (!isPathWithinRoot(params.rootDir, candidate)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const fileStat = await stat(candidate);
|
|
510
|
+
if (!fileStat.isFile()) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (fileStat.mtimeMs >= bestMtimeMs) {
|
|
514
|
+
bestMtimeMs = fileStat.mtimeMs;
|
|
515
|
+
bestPath = candidate;
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return bestPath;
|
|
523
|
+
}
|
|
524
|
+
async function resolveAttachmentRefToLocalPath(params) {
|
|
525
|
+
const raw = params.attachmentRef.slice(ATTACHMENT_REF_PREFIX.length).trim();
|
|
526
|
+
if (!raw || !params.mediaLocalRoots?.length) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
let decoded;
|
|
530
|
+
try {
|
|
531
|
+
decoded = decodeURIComponent(raw);
|
|
532
|
+
} catch {
|
|
533
|
+
return void 0;
|
|
534
|
+
}
|
|
535
|
+
const decodedPath = stripQueryAndHash(decoded).trim();
|
|
536
|
+
if (!decodedPath) {
|
|
537
|
+
return void 0;
|
|
538
|
+
}
|
|
539
|
+
if (path.isAbsolute(decodedPath)) {
|
|
540
|
+
const absoluteCandidate = path.resolve(decodedPath);
|
|
541
|
+
if (isPathWithinAnyRoot(absoluteCandidate, params.mediaLocalRoots) && await isExistingFile(absoluteCandidate)) {
|
|
542
|
+
return absoluteCandidate;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const normalizedSlash = decodedPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
546
|
+
if (!normalizedSlash) {
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
const segments = normalizedSlash.split("/").filter(Boolean);
|
|
550
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
const relativePath = segments.join(path.sep);
|
|
554
|
+
const fileNameOnly = segments[segments.length - 1];
|
|
555
|
+
for (const rootEntry of params.mediaLocalRoots) {
|
|
556
|
+
const trimmedRoot = rootEntry?.trim();
|
|
557
|
+
if (!trimmedRoot) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const rootDir = path.resolve(trimmedRoot);
|
|
561
|
+
const directCandidate = path.resolve(rootDir, relativePath);
|
|
562
|
+
if (isPathWithinRoot(rootDir, directCandidate) && await isExistingFile(directCandidate)) {
|
|
563
|
+
return directCandidate;
|
|
564
|
+
}
|
|
565
|
+
if (!fileNameOnly) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const fallbackCandidates = [
|
|
569
|
+
path.resolve(rootDir, fileNameOnly),
|
|
570
|
+
path.resolve(rootDir, "tmp", fileNameOnly),
|
|
571
|
+
path.resolve(rootDir, "out", fileNameOnly),
|
|
572
|
+
path.resolve(rootDir, "outbound", fileNameOnly),
|
|
573
|
+
path.resolve(rootDir, "inbound", fileNameOnly),
|
|
574
|
+
path.resolve(rootDir, "media", fileNameOnly),
|
|
575
|
+
path.resolve(rootDir, "media", "outbound", fileNameOnly),
|
|
576
|
+
path.resolve(rootDir, "media", "inbound", fileNameOnly),
|
|
577
|
+
path.resolve(rootDir, "workspace", fileNameOnly),
|
|
578
|
+
path.resolve(rootDir, "sandboxes", fileNameOnly)
|
|
579
|
+
];
|
|
580
|
+
for (const candidate of fallbackCandidates) {
|
|
581
|
+
if (!isPathWithinRoot(rootDir, candidate)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (await isExistingFile(candidate)) {
|
|
585
|
+
return candidate;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const generatedCandidate = await resolveStoredAttachmentByFileName({
|
|
589
|
+
rootDir,
|
|
590
|
+
fileName: fileNameOnly
|
|
591
|
+
});
|
|
592
|
+
if (generatedCandidate) {
|
|
593
|
+
return generatedCandidate;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
function stripSentMarkdownMediaLinks(text, sentSourceUrls) {
|
|
599
|
+
const input = text ?? "";
|
|
600
|
+
if (!input.trim()) {
|
|
601
|
+
return "";
|
|
602
|
+
}
|
|
603
|
+
if (sentSourceUrls.size === 0) {
|
|
604
|
+
return input.trim();
|
|
605
|
+
}
|
|
606
|
+
const strippedImages = input.replace(MARKDOWN_IMAGE_RE, (match, rawTarget) => {
|
|
607
|
+
const target = parseMarkdownLinkTarget(rawTarget);
|
|
608
|
+
if (!target || !sentSourceUrls.has(target)) {
|
|
609
|
+
return match;
|
|
610
|
+
}
|
|
611
|
+
return "";
|
|
612
|
+
});
|
|
613
|
+
const strippedLinks = strippedImages.replace(
|
|
614
|
+
MARKDOWN_LINK_RE,
|
|
615
|
+
(match, rawTarget, offset, source) => {
|
|
616
|
+
if (offset > 0 && source[offset - 1] === "!") {
|
|
617
|
+
return match;
|
|
618
|
+
}
|
|
619
|
+
const target = parseMarkdownLinkTarget(rawTarget);
|
|
620
|
+
if (!target || !sentSourceUrls.has(target)) {
|
|
621
|
+
return match;
|
|
622
|
+
}
|
|
623
|
+
return "";
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
return strippedLinks.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
627
|
+
}
|
|
628
|
+
async function postJson(params) {
|
|
629
|
+
const headers = {
|
|
630
|
+
"Content-Type": "application/json",
|
|
631
|
+
"User-Agent": SIDER_USER_AGENT
|
|
632
|
+
};
|
|
633
|
+
if (params.authorization?.trim()) {
|
|
634
|
+
headers.Authorization = formatAuthorizationHeader(params.authorization);
|
|
635
|
+
}
|
|
636
|
+
const response = await fetch(params.url, {
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers,
|
|
639
|
+
body: JSON.stringify(params.body),
|
|
640
|
+
signal: AbortSignal.timeout(params.timeoutMs)
|
|
641
|
+
});
|
|
642
|
+
const text = await response.text();
|
|
643
|
+
if (!response.ok) {
|
|
644
|
+
const hint = text ? `: ${text.slice(0, 300)}` : "";
|
|
645
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}${hint}`);
|
|
646
|
+
}
|
|
647
|
+
let parsed = {};
|
|
648
|
+
if (text.trim()) {
|
|
649
|
+
try {
|
|
650
|
+
parsed = JSON.parse(text);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
throw new Error(`invalid JSON response from ${params.url}: ${String(error)}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return parsed;
|
|
656
|
+
}
|
|
657
|
+
function parseFileInitResult(value) {
|
|
658
|
+
const record = toRecord(value);
|
|
659
|
+
if (!record) {
|
|
660
|
+
throw new Error("files/init returned invalid response");
|
|
661
|
+
}
|
|
662
|
+
const fileId = typeof record.file_id === "string" ? record.file_id.trim() : "";
|
|
663
|
+
const objectKey = typeof record.object_key === "string" ? record.object_key.trim() : "";
|
|
664
|
+
const uploadUrl = typeof record.upload_url === "string" ? record.upload_url.trim() : "";
|
|
665
|
+
if (!fileId || !objectKey || !uploadUrl) {
|
|
666
|
+
throw new Error("files/init response missing file_id/object_key/upload_url");
|
|
667
|
+
}
|
|
668
|
+
return { fileId, objectKey, uploadUrl };
|
|
669
|
+
}
|
|
670
|
+
async function initFileUpload(params) {
|
|
671
|
+
const url = resolveGatewayApiUrl(params.account.gatewayUrl, "/v1/files/init");
|
|
672
|
+
try {
|
|
673
|
+
const response = await postJson({
|
|
674
|
+
url,
|
|
675
|
+
timeoutMs: params.account.sendTimeoutMs,
|
|
676
|
+
authorization: params.account.token,
|
|
677
|
+
body: {
|
|
678
|
+
session_id: params.sessionId,
|
|
679
|
+
name: params.fileName,
|
|
680
|
+
size: params.size,
|
|
681
|
+
mime: params.mimeType,
|
|
682
|
+
sha256: params.sha256
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return parseFileInitResult(response);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
params.logger?.warn?.("sider files/init failed", {
|
|
688
|
+
accountId: params.account.accountId,
|
|
689
|
+
sessionId: params.sessionId,
|
|
690
|
+
fileName: params.fileName,
|
|
691
|
+
size: params.size,
|
|
692
|
+
mimeType: params.mimeType,
|
|
693
|
+
sha256: params.sha256,
|
|
694
|
+
error: String(error)
|
|
695
|
+
});
|
|
696
|
+
throw new Error(`sider files/init failed: ${describeError(error)}`, {
|
|
697
|
+
cause: error instanceof Error ? error : void 0
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function uploadToObjectStore(params) {
|
|
702
|
+
const headers = {
|
|
703
|
+
"User-Agent": SIDER_USER_AGENT
|
|
704
|
+
};
|
|
705
|
+
if (params.mimeType) {
|
|
706
|
+
headers["Content-Type"] = params.mimeType;
|
|
707
|
+
}
|
|
708
|
+
if (params.sha256) {
|
|
709
|
+
headers["x-amz-meta-sha256"] = params.sha256;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
const response = await fetch(params.uploadUrl, {
|
|
713
|
+
method: "PUT",
|
|
714
|
+
headers,
|
|
715
|
+
body: new Uint8Array(params.buffer),
|
|
716
|
+
signal: AbortSignal.timeout(params.timeoutMs)
|
|
717
|
+
});
|
|
718
|
+
if (!response.ok) {
|
|
719
|
+
const detail = await readResponseErrorBody(response);
|
|
720
|
+
const hint = detail ? `: ${detail.slice(0, 300)}` : "";
|
|
721
|
+
throw new Error(`upload failed (${response.status} ${response.statusText})${hint}`);
|
|
722
|
+
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
params.logger?.warn?.("sider object upload failed", {
|
|
725
|
+
accountId: params.accountId,
|
|
726
|
+
sessionId: params.sessionId,
|
|
727
|
+
fileId: params.fileId,
|
|
728
|
+
objectKey: params.objectKey,
|
|
729
|
+
fileName: params.fileName,
|
|
730
|
+
mimeType: params.mimeType,
|
|
731
|
+
size: params.buffer.length,
|
|
732
|
+
hasSha256Header: Boolean(params.sha256),
|
|
733
|
+
error: String(error)
|
|
734
|
+
});
|
|
735
|
+
const label = params.fileName ?? params.objectKey ?? "attachment";
|
|
736
|
+
throw new Error(`sider object upload failed (${label}): ${describeError(error)}`, {
|
|
737
|
+
cause: error instanceof Error ? error : void 0
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function completeFileUploadBestEffort(params) {
|
|
742
|
+
const url = resolveGatewayApiUrl(params.account.gatewayUrl, "/v1/files/complete");
|
|
743
|
+
try {
|
|
744
|
+
await postJson({
|
|
745
|
+
url,
|
|
746
|
+
timeoutMs: params.account.sendTimeoutMs,
|
|
747
|
+
authorization: params.account.token,
|
|
748
|
+
body: {
|
|
749
|
+
session_id: params.sessionId,
|
|
750
|
+
file_id: params.fileId,
|
|
751
|
+
object_key: params.objectKey,
|
|
752
|
+
size: params.size,
|
|
753
|
+
sha256: params.sha256
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
} catch (error) {
|
|
757
|
+
params.logger?.warn?.("sider files/complete failed (ignored)", {
|
|
758
|
+
accountId: params.account.accountId,
|
|
759
|
+
sessionId: params.sessionId,
|
|
760
|
+
fileId: params.fileId,
|
|
761
|
+
objectKey: params.objectKey,
|
|
762
|
+
error: String(error)
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function buildSiderFilePart(params) {
|
|
767
|
+
return {
|
|
768
|
+
type: "core.file",
|
|
769
|
+
spec_version: 1,
|
|
770
|
+
payload: {
|
|
771
|
+
file_id: params.uploaded.fileId,
|
|
772
|
+
object_key: params.uploaded.objectKey,
|
|
773
|
+
download_url: params.uploaded.downloadUrl,
|
|
774
|
+
...buildCoreFilePayloadBase({
|
|
775
|
+
source: params.source,
|
|
776
|
+
loadUrl: params.loadUrl,
|
|
777
|
+
mediaKind: params.uploaded.mediaKind,
|
|
778
|
+
fileName: params.uploaded.fileName,
|
|
779
|
+
size: params.uploaded.size,
|
|
780
|
+
mimeType: params.uploaded.mimeType
|
|
781
|
+
}),
|
|
782
|
+
sha256: params.uploaded.sha256,
|
|
783
|
+
upload_status: params.uploadStatus ?? "uploaded"
|
|
784
|
+
},
|
|
785
|
+
meta: {
|
|
786
|
+
...params.meta ?? {}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function buildDeferredSiderUpload(params) {
|
|
791
|
+
return {
|
|
792
|
+
fileName: params.uploaded.fileName,
|
|
793
|
+
size: params.uploaded.size,
|
|
794
|
+
source: params.source,
|
|
795
|
+
upload: async () => {
|
|
796
|
+
await uploadInitializedMediaToSider({
|
|
797
|
+
account: params.account,
|
|
798
|
+
sessionId: params.sessionId,
|
|
799
|
+
uploaded: params.uploaded,
|
|
800
|
+
logger: params.logger
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function startDeferredSiderUploads(params) {
|
|
806
|
+
for (const item of params.uploads) {
|
|
807
|
+
void (async () => {
|
|
808
|
+
let lastError;
|
|
809
|
+
for (let attempt = 1; attempt <= DEFAULT_DEFERRED_UPLOAD_MAX_ATTEMPTS; attempt += 1) {
|
|
810
|
+
if (attempt > 1) {
|
|
811
|
+
const delayMs = DEFAULT_DEFERRED_UPLOAD_RETRY_BASE_MS * 2 ** (attempt - 2);
|
|
812
|
+
siderOutboundLogger.warn?.("sider deferred upload retry scheduled", {
|
|
813
|
+
accountId: params.account.accountId,
|
|
814
|
+
sessionId: params.sessionId,
|
|
815
|
+
fileName: item.fileName,
|
|
816
|
+
source: item.source,
|
|
817
|
+
size: item.size,
|
|
818
|
+
context: params.context,
|
|
819
|
+
attempt,
|
|
820
|
+
maxAttempts: DEFAULT_DEFERRED_UPLOAD_MAX_ATTEMPTS,
|
|
821
|
+
delayMs,
|
|
822
|
+
error: describeError(lastError)
|
|
823
|
+
});
|
|
824
|
+
await sleepMs(delayMs);
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
await item.upload();
|
|
828
|
+
siderOutboundLogger.debug?.(
|
|
829
|
+
attempt === 1 ? "sider deferred upload completed" : "sider deferred upload completed after retry",
|
|
830
|
+
{
|
|
831
|
+
accountId: params.account.accountId,
|
|
832
|
+
sessionId: params.sessionId,
|
|
833
|
+
fileName: item.fileName,
|
|
834
|
+
source: item.source,
|
|
835
|
+
size: item.size,
|
|
836
|
+
context: params.context,
|
|
837
|
+
attempt
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
return;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
lastError = error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
siderOutboundLogger.warn?.("sider deferred upload failed", {
|
|
846
|
+
accountId: params.account.accountId,
|
|
847
|
+
sessionId: params.sessionId,
|
|
848
|
+
fileName: item.fileName,
|
|
849
|
+
source: item.source,
|
|
850
|
+
size: item.size,
|
|
851
|
+
context: params.context,
|
|
852
|
+
attempts: DEFAULT_DEFERRED_UPLOAD_MAX_ATTEMPTS,
|
|
853
|
+
error: describeError(lastError)
|
|
854
|
+
});
|
|
855
|
+
})();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function initializePreparedMediaUpload(params) {
|
|
859
|
+
const contentType = normalizeMime(params.media.contentType);
|
|
860
|
+
const inferredKind = params.media.kind === "image" || contentType?.startsWith("image/") ? "image" : "file";
|
|
861
|
+
const sourceName = params.media.fileName || fileNameFromMediaUrl(params.media.source ?? "");
|
|
862
|
+
const fallbackName = inferFallbackFileName({
|
|
863
|
+
mimeType: contentType ?? DEFAULT_MIME,
|
|
864
|
+
mediaKind: inferredKind
|
|
865
|
+
});
|
|
866
|
+
const fileName = normalizeFileName(sourceName, fallbackName);
|
|
867
|
+
const mimeType = contentType || inferMimeFromFileName(fileName) || DEFAULT_MIME;
|
|
868
|
+
const size = params.media.buffer.length;
|
|
869
|
+
if (size <= 0) {
|
|
870
|
+
const suffix = params.media.source ? ` from ${params.media.source}` : "";
|
|
871
|
+
throw new Error(`empty media buffer${suffix}`);
|
|
872
|
+
}
|
|
873
|
+
const sha256 = sha256Hex(params.media.buffer);
|
|
874
|
+
const initResult = await initFileUpload({
|
|
875
|
+
account: params.account,
|
|
876
|
+
sessionId: params.sessionId,
|
|
877
|
+
fileName,
|
|
878
|
+
size,
|
|
879
|
+
mimeType,
|
|
880
|
+
sha256,
|
|
881
|
+
logger: params.logger
|
|
882
|
+
});
|
|
883
|
+
params.logger?.debug?.("sider files/init success", {
|
|
884
|
+
accountId: params.account.accountId,
|
|
885
|
+
sessionId: params.sessionId,
|
|
886
|
+
fileId: initResult.fileId,
|
|
887
|
+
objectKey: initResult.objectKey,
|
|
888
|
+
size,
|
|
889
|
+
mimeType
|
|
890
|
+
});
|
|
891
|
+
const downloadUrl = await resolveDownloadUrlBestEffort({
|
|
892
|
+
account: params.account,
|
|
893
|
+
sessionId: params.sessionId,
|
|
894
|
+
objectKey: initResult.objectKey,
|
|
895
|
+
logger: params.logger
|
|
896
|
+
});
|
|
897
|
+
return {
|
|
898
|
+
fileId: initResult.fileId,
|
|
899
|
+
objectKey: initResult.objectKey,
|
|
900
|
+
downloadUrl,
|
|
901
|
+
uploadUrl: initResult.uploadUrl,
|
|
902
|
+
fileName,
|
|
903
|
+
mimeType,
|
|
904
|
+
size,
|
|
905
|
+
sha256,
|
|
906
|
+
mediaKind: inferredKind,
|
|
907
|
+
buffer: params.media.buffer
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async function uploadInitializedMediaToSider(params) {
|
|
911
|
+
await uploadToObjectStore({
|
|
912
|
+
uploadUrl: params.uploaded.uploadUrl,
|
|
913
|
+
mimeType: params.uploaded.mimeType,
|
|
914
|
+
sha256: params.uploaded.sha256,
|
|
915
|
+
buffer: params.uploaded.buffer,
|
|
916
|
+
timeoutMs: params.account.sendTimeoutMs,
|
|
917
|
+
logger: params.logger,
|
|
918
|
+
accountId: params.account.accountId,
|
|
919
|
+
sessionId: params.sessionId,
|
|
920
|
+
fileId: params.uploaded.fileId,
|
|
921
|
+
objectKey: params.uploaded.objectKey,
|
|
922
|
+
fileName: params.uploaded.fileName
|
|
923
|
+
});
|
|
924
|
+
await completeFileUploadBestEffort({
|
|
925
|
+
account: params.account,
|
|
926
|
+
sessionId: params.sessionId,
|
|
927
|
+
fileId: params.uploaded.fileId,
|
|
928
|
+
objectKey: params.uploaded.objectKey,
|
|
929
|
+
size: params.uploaded.size,
|
|
930
|
+
sha256: params.uploaded.sha256,
|
|
931
|
+
logger: params.logger
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
async function uploadPreparedMediaToSider(params) {
|
|
935
|
+
const initialized = await initializePreparedMediaUpload(params);
|
|
936
|
+
await uploadInitializedMediaToSider({
|
|
937
|
+
account: params.account,
|
|
938
|
+
sessionId: params.sessionId,
|
|
939
|
+
uploaded: initialized,
|
|
940
|
+
logger: params.logger
|
|
941
|
+
});
|
|
942
|
+
return initialized;
|
|
943
|
+
}
|
|
944
|
+
async function uploadMediaToSider(params) {
|
|
945
|
+
const source = resolveSource(params.source, params.loadUrl);
|
|
946
|
+
const meta = params.meta ?? inferSourceMeta(source);
|
|
947
|
+
const sizeHint = await probeMediaSizeBytes({
|
|
948
|
+
account: params.account,
|
|
949
|
+
sessionId: params.sessionId,
|
|
950
|
+
loadUrl: params.loadUrl,
|
|
951
|
+
logger: params.logger
|
|
952
|
+
});
|
|
953
|
+
if (typeof sizeHint === "number" && Number.isFinite(sizeHint) && sizeHint > DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES) {
|
|
954
|
+
params.logger?.warn?.("sider skipping object-store upload for oversized outbound media", {
|
|
955
|
+
accountId: params.account.accountId,
|
|
956
|
+
sessionId: params.sessionId,
|
|
957
|
+
mediaUrl: params.loadUrl,
|
|
958
|
+
source,
|
|
959
|
+
size: sizeHint,
|
|
960
|
+
uploadSkipMaxBytes: DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES,
|
|
961
|
+
phase: "preflight"
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
part: buildDirectFilePart({
|
|
965
|
+
loadUrl: params.loadUrl,
|
|
966
|
+
source,
|
|
967
|
+
meta,
|
|
968
|
+
fileName: fileNameFromMediaUrl(source ?? params.loadUrl),
|
|
969
|
+
size: sizeHint
|
|
970
|
+
})
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
const loaded = await loadWebMedia(params.loadUrl, DEFAULT_MEDIA_SAVE_MAX_BYTES, {
|
|
974
|
+
localRoots: params.mediaLocalRoots
|
|
975
|
+
});
|
|
976
|
+
const fileName = loaded.fileName || fileNameFromMediaUrl(source ?? params.loadUrl);
|
|
977
|
+
if (loaded.buffer.length > DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES) {
|
|
978
|
+
params.logger?.warn?.("sider skipping object-store upload for oversized outbound media", {
|
|
979
|
+
accountId: params.account.accountId,
|
|
980
|
+
sessionId: params.sessionId,
|
|
981
|
+
mediaUrl: params.loadUrl,
|
|
982
|
+
source,
|
|
983
|
+
size: loaded.buffer.length,
|
|
984
|
+
uploadSkipMaxBytes: DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES,
|
|
985
|
+
phase: "post-load"
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
part: buildDirectFilePart({
|
|
989
|
+
loadUrl: params.loadUrl,
|
|
990
|
+
source,
|
|
991
|
+
meta,
|
|
992
|
+
fileName,
|
|
993
|
+
contentType: loaded.contentType,
|
|
994
|
+
mediaKind: loaded.kind,
|
|
995
|
+
size: loaded.buffer.length
|
|
996
|
+
})
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
const initialized = await initializePreparedMediaUpload({
|
|
1000
|
+
account: params.account,
|
|
1001
|
+
sessionId: params.sessionId,
|
|
1002
|
+
media: {
|
|
1003
|
+
buffer: loaded.buffer,
|
|
1004
|
+
contentType: loaded.contentType,
|
|
1005
|
+
fileName,
|
|
1006
|
+
kind: loaded.kind,
|
|
1007
|
+
source
|
|
1008
|
+
},
|
|
1009
|
+
logger: params.logger
|
|
1010
|
+
});
|
|
1011
|
+
if (params.deferUpload) {
|
|
1012
|
+
return {
|
|
1013
|
+
part: buildSiderFilePart({
|
|
1014
|
+
uploaded: initialized,
|
|
1015
|
+
source,
|
|
1016
|
+
loadUrl: params.loadUrl,
|
|
1017
|
+
meta,
|
|
1018
|
+
uploadStatus: "accepted"
|
|
1019
|
+
}),
|
|
1020
|
+
deferredUpload: buildDeferredSiderUpload({
|
|
1021
|
+
account: params.account,
|
|
1022
|
+
sessionId: params.sessionId,
|
|
1023
|
+
uploaded: initialized,
|
|
1024
|
+
source,
|
|
1025
|
+
logger: params.logger
|
|
1026
|
+
})
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
await uploadInitializedMediaToSider({
|
|
1030
|
+
account: params.account,
|
|
1031
|
+
sessionId: params.sessionId,
|
|
1032
|
+
uploaded: initialized,
|
|
1033
|
+
logger: params.logger
|
|
1034
|
+
});
|
|
1035
|
+
return {
|
|
1036
|
+
part: buildSiderFilePart({
|
|
1037
|
+
uploaded: initialized,
|
|
1038
|
+
source,
|
|
1039
|
+
loadUrl: params.loadUrl,
|
|
1040
|
+
meta
|
|
1041
|
+
})
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function buildReplyPayloadMediaMeta(params) {
|
|
1045
|
+
if (params.sourceKey === "mediaUrls") {
|
|
1046
|
+
return { mediaUrls: [params.source] };
|
|
1047
|
+
}
|
|
1048
|
+
return { mediaUrl: params.source };
|
|
1049
|
+
}
|
|
1050
|
+
function collectReplyPayloadMediaCandidates(payload) {
|
|
1051
|
+
const candidates = [];
|
|
1052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1053
|
+
const push = (source, sourceKey) => {
|
|
1054
|
+
if (typeof source !== "string") {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const trimmed = source.trim();
|
|
1058
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
seen.add(trimmed);
|
|
1062
|
+
candidates.push({ source: trimmed, sourceKey });
|
|
1063
|
+
};
|
|
1064
|
+
if (Array.isArray(payload.mediaUrls)) {
|
|
1065
|
+
for (const source of payload.mediaUrls) {
|
|
1066
|
+
push(source, "mediaUrls");
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
push(payload.mediaUrl, "mediaUrl");
|
|
1070
|
+
for (const source of extractMarkdownMediaUrls(payload.text)) {
|
|
1071
|
+
push(source, "text");
|
|
1072
|
+
}
|
|
1073
|
+
return candidates;
|
|
1074
|
+
}
|
|
1075
|
+
async function resolveOutboundMediaLoadUrl(params) {
|
|
1076
|
+
const source = params.source.trim();
|
|
1077
|
+
const trimmed = source.trim();
|
|
1078
|
+
if (!trimmed) {
|
|
1079
|
+
return trimmed;
|
|
1080
|
+
}
|
|
1081
|
+
if (source.toLowerCase().startsWith(ATTACHMENT_REF_PREFIX)) {
|
|
1082
|
+
const localPath = await resolveAttachmentRefToLocalPath({
|
|
1083
|
+
attachmentRef: source,
|
|
1084
|
+
mediaLocalRoots: params.mediaLocalRoots
|
|
1085
|
+
});
|
|
1086
|
+
if (!localPath) {
|
|
1087
|
+
siderOutboundLogger.warn?.("sider skipped unresolved attachment:// media reference", {
|
|
1088
|
+
mediaUrl: source,
|
|
1089
|
+
mediaLocalRootsCount: params.mediaLocalRoots?.length ?? 0
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
return localPath;
|
|
1093
|
+
}
|
|
1094
|
+
if (/^https?:\/\//i.test(trimmed) || /^data:/i.test(trimmed)) {
|
|
1095
|
+
return trimmed;
|
|
1096
|
+
}
|
|
1097
|
+
if (/^file:\/\//i.test(trimmed)) {
|
|
1098
|
+
try {
|
|
1099
|
+
return path.resolve(new URL(trimmed).pathname);
|
|
1100
|
+
} catch {
|
|
1101
|
+
return trimmed;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (trimmed === "~") {
|
|
1105
|
+
return os.homedir();
|
|
1106
|
+
}
|
|
1107
|
+
if (trimmed.startsWith("~/")) {
|
|
1108
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
1109
|
+
}
|
|
1110
|
+
if (path.isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
1111
|
+
return path.resolve(trimmed);
|
|
1112
|
+
}
|
|
1113
|
+
if (!SCHEME_RE.test(trimmed)) {
|
|
1114
|
+
return path.resolve(trimmed);
|
|
1115
|
+
}
|
|
1116
|
+
return trimmed;
|
|
1117
|
+
}
|
|
1118
|
+
async function resolveOutboundMediaItems(params) {
|
|
1119
|
+
const mediaItems = [];
|
|
1120
|
+
for (const candidate of collectReplyPayloadMediaCandidates(params.payload)) {
|
|
1121
|
+
const source = candidate.source;
|
|
1122
|
+
const loadUrl = await resolveOutboundMediaLoadUrl({
|
|
1123
|
+
source,
|
|
1124
|
+
mediaLocalRoots: params.mediaLocalRoots
|
|
1125
|
+
});
|
|
1126
|
+
if (loadUrl) {
|
|
1127
|
+
mediaItems.push({
|
|
1128
|
+
loadUrl,
|
|
1129
|
+
source,
|
|
1130
|
+
meta: buildReplyPayloadMediaMeta({
|
|
1131
|
+
source,
|
|
1132
|
+
sourceKey: candidate.sourceKey
|
|
1133
|
+
})
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return mediaItems;
|
|
1138
|
+
}
|
|
1139
|
+
async function buildSiderPartFromInlineAttachment(params) {
|
|
1140
|
+
const prepared = await prepareInlineAttachmentPart({
|
|
1141
|
+
account: params.account,
|
|
1142
|
+
sessionId: params.sessionId,
|
|
1143
|
+
buffer: params.buffer,
|
|
1144
|
+
contentType: params.contentType,
|
|
1145
|
+
fileName: params.fileName,
|
|
1146
|
+
mediaKind: params.mediaKind,
|
|
1147
|
+
source: params.source,
|
|
1148
|
+
logger: params.logger
|
|
1149
|
+
});
|
|
1150
|
+
return prepared.part;
|
|
1151
|
+
}
|
|
1152
|
+
async function prepareInlineAttachmentPart(params) {
|
|
1153
|
+
const meta = params.source ? inferSourceMeta(params.source) : void 0;
|
|
1154
|
+
if (params.buffer.length > DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES) {
|
|
1155
|
+
params.logger?.warn?.("sider skipping object-store upload for oversized inline attachment", {
|
|
1156
|
+
accountId: params.account.accountId,
|
|
1157
|
+
sessionId: params.sessionId,
|
|
1158
|
+
source: params.source,
|
|
1159
|
+
size: params.buffer.length,
|
|
1160
|
+
uploadSkipMaxBytes: DEFAULT_MEDIA_UPLOAD_SKIP_MAX_BYTES
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
part: buildDirectFilePart({
|
|
1164
|
+
loadUrl: params.source,
|
|
1165
|
+
source: params.source,
|
|
1166
|
+
meta,
|
|
1167
|
+
fileName: params.fileName,
|
|
1168
|
+
contentType: params.contentType,
|
|
1169
|
+
mediaKind: params.mediaKind,
|
|
1170
|
+
size: params.buffer.length
|
|
1171
|
+
})
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const initialized = await initializePreparedMediaUpload({
|
|
1175
|
+
account: params.account,
|
|
1176
|
+
sessionId: params.sessionId,
|
|
1177
|
+
media: {
|
|
1178
|
+
buffer: params.buffer,
|
|
1179
|
+
contentType: params.contentType,
|
|
1180
|
+
fileName: params.fileName,
|
|
1181
|
+
kind: params.mediaKind,
|
|
1182
|
+
source: params.source
|
|
1183
|
+
},
|
|
1184
|
+
logger: params.logger ?? siderOutboundLogger
|
|
1185
|
+
});
|
|
1186
|
+
if (params.deferUpload) {
|
|
1187
|
+
return {
|
|
1188
|
+
part: buildSiderFilePart({
|
|
1189
|
+
uploaded: initialized,
|
|
1190
|
+
source: params.source,
|
|
1191
|
+
loadUrl: params.source,
|
|
1192
|
+
meta,
|
|
1193
|
+
uploadStatus: "accepted"
|
|
1194
|
+
}),
|
|
1195
|
+
deferredUpload: buildDeferredSiderUpload({
|
|
1196
|
+
account: params.account,
|
|
1197
|
+
sessionId: params.sessionId,
|
|
1198
|
+
uploaded: initialized,
|
|
1199
|
+
source: params.source,
|
|
1200
|
+
logger: params.logger ?? siderOutboundLogger
|
|
1201
|
+
})
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
await uploadInitializedMediaToSider({
|
|
1205
|
+
account: params.account,
|
|
1206
|
+
sessionId: params.sessionId,
|
|
1207
|
+
uploaded: initialized,
|
|
1208
|
+
logger: params.logger
|
|
1209
|
+
});
|
|
1210
|
+
return {
|
|
1211
|
+
part: buildSiderFilePart({
|
|
1212
|
+
uploaded: initialized,
|
|
1213
|
+
source: params.source,
|
|
1214
|
+
loadUrl: params.source,
|
|
1215
|
+
meta
|
|
1216
|
+
})
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async function buildOptimisticSiderPartFromInlineAttachment(params) {
|
|
1220
|
+
return await prepareInlineAttachmentPart({
|
|
1221
|
+
account: params.account,
|
|
1222
|
+
sessionId: params.sessionId,
|
|
1223
|
+
buffer: params.buffer,
|
|
1224
|
+
contentType: params.contentType,
|
|
1225
|
+
fileName: params.fileName,
|
|
1226
|
+
mediaKind: params.mediaKind,
|
|
1227
|
+
source: params.source,
|
|
1228
|
+
logger: siderOutboundLogger,
|
|
1229
|
+
deferUpload: true
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async function buildSiderPartsFromReplyPayload(params) {
|
|
1233
|
+
const mediaItems = await resolveOutboundMediaItems({
|
|
1234
|
+
payload: params.payload,
|
|
1235
|
+
mediaLocalRoots: params.mediaLocalRoots
|
|
1236
|
+
});
|
|
1237
|
+
const mediaParts = [];
|
|
1238
|
+
const deferredUploads = [];
|
|
1239
|
+
const failures = [];
|
|
1240
|
+
const sentSourceUrls = /* @__PURE__ */ new Set();
|
|
1241
|
+
for (const media of mediaItems) {
|
|
1242
|
+
try {
|
|
1243
|
+
const prepared = await uploadMediaToSider({
|
|
1244
|
+
account: params.account,
|
|
1245
|
+
sessionId: params.sessionId,
|
|
1246
|
+
loadUrl: media.loadUrl,
|
|
1247
|
+
source: media.source,
|
|
1248
|
+
meta: media.meta,
|
|
1249
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
1250
|
+
logger: siderOutboundLogger,
|
|
1251
|
+
deferUpload: params.deferUploads === true
|
|
1252
|
+
});
|
|
1253
|
+
mediaParts.push(prepared.part);
|
|
1254
|
+
if (prepared.deferredUpload) {
|
|
1255
|
+
deferredUploads.push(prepared.deferredUpload);
|
|
1256
|
+
}
|
|
1257
|
+
sentSourceUrls.add(media.source);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
const describedError = describeError(error);
|
|
1260
|
+
failures.push({
|
|
1261
|
+
source: media.source,
|
|
1262
|
+
error: describedError
|
|
1263
|
+
});
|
|
1264
|
+
siderOutboundLogger.warn?.(
|
|
1265
|
+
params.deferUploads ? "sider skipped optimistic outbound media after init failure" : "sider skipped outbound media after upload failure",
|
|
1266
|
+
{
|
|
1267
|
+
accountId: params.account.accountId,
|
|
1268
|
+
sessionId: params.sessionId,
|
|
1269
|
+
mediaUrl: media.source,
|
|
1270
|
+
resolvedMediaUrl: media.loadUrl,
|
|
1271
|
+
error: describedError
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const parts = [];
|
|
1277
|
+
const text = stripSentMarkdownMediaLinks(params.payload.text, sentSourceUrls);
|
|
1278
|
+
if (text) {
|
|
1279
|
+
parts.push({
|
|
1280
|
+
type: "core.text",
|
|
1281
|
+
spec_version: 1,
|
|
1282
|
+
payload: {
|
|
1283
|
+
text,
|
|
1284
|
+
...params.payload.isError ? { is_error: true } : {}
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
parts.push(...mediaParts);
|
|
1289
|
+
if (parts.length === 0 && failures.length > 0) {
|
|
1290
|
+
const lastFailure = failures[failures.length - 1];
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`sider failed to ${params.deferUploads ? "prepare" : "upload"} outbound media ${lastFailure?.source ?? ""}: ${lastFailure?.error ?? "unknown error"}`.trim()
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
parts,
|
|
1297
|
+
deferredUploads
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
export {
|
|
1301
|
+
buildOptimisticSiderPartFromInlineAttachment,
|
|
1302
|
+
buildSiderPartFromInlineAttachment,
|
|
1303
|
+
buildSiderPartsFromReplyPayload,
|
|
1304
|
+
inferSourceMeta,
|
|
1305
|
+
resolveOutboundMediaLoadUrl,
|
|
1306
|
+
startDeferredSiderUploads,
|
|
1307
|
+
uploadMediaToSider
|
|
1308
|
+
};
|