karin-plugin-qgroup-file2openlist 0.0.20 → 0.0.22

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.
@@ -0,0 +1,2282 @@
1
+ import {
2
+ config,
3
+ time
4
+ } from "./chunk-5WVKHIPK.js";
5
+ import {
6
+ dir
7
+ } from "./chunk-IZS467MR.js";
8
+
9
+ // src/apps/groupFiles.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { Readable, Transform } from "stream";
13
+ import { setTimeout as sleep } from "timers/promises";
14
+ import { pathToFileURL } from "url";
15
+ import { karin, logger } from "node-karin";
16
+ var MAX_FILE_TIMEOUT_SEC = 3e3;
17
+ var MIN_FILE_TIMEOUT_SEC = 10;
18
+ var DEFAULT_PROGRESS_REPORT_EVERY = 10;
19
+ var MAX_TRANSFER_CONCURRENCY = 5;
20
+ var pickFirstString = (...values) => {
21
+ for (const value of values) {
22
+ if (typeof value === "string" && value) return value;
23
+ }
24
+ };
25
+ var pickFirstNumber = (...values) => {
26
+ for (const value of values) {
27
+ if (typeof value === "number" && Number.isFinite(value)) return value;
28
+ if (typeof value === "string" && value && Number.isFinite(Number(value))) return Number(value);
29
+ }
30
+ };
31
+ var csvEscape = (value) => {
32
+ const str = String(value ?? "");
33
+ if (/[",\n]/.test(str)) return `"${str.replaceAll('"', '""')}"`;
34
+ return str;
35
+ };
36
+ var ensureDir = (dirPath) => fs.mkdirSync(dirPath, { recursive: true });
37
+ var readJsonSafe = (filePath) => {
38
+ try {
39
+ if (!fs.existsSync(filePath)) return {};
40
+ const raw = fs.readFileSync(filePath, "utf8");
41
+ return raw ? JSON.parse(raw) : {};
42
+ } catch {
43
+ return {};
44
+ }
45
+ };
46
+ var writeJsonSafe = (filePath, data) => {
47
+ ensureDir(path.dirname(filePath));
48
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
49
+ };
50
+ var normalizePosixPath = (inputPath, { ensureLeadingSlash = true, stripTrailingSlash = true } = {}) => {
51
+ let value = String(inputPath ?? "").trim().replaceAll("\\", "/");
52
+ value = value.replace(/\/+/g, "/");
53
+ if (!value) value = "/";
54
+ if (ensureLeadingSlash && !value.startsWith("/")) value = `/${value}`;
55
+ if (stripTrailingSlash && value.length > 1) value = value.replace(/\/+$/, "");
56
+ return value;
57
+ };
58
+ var safePathSegment = (input) => {
59
+ const value = String(input ?? "").replaceAll("\0", "").replaceAll("\\", "_").replaceAll("/", "_").trim();
60
+ if (!value || value === "." || value === "..") return "unnamed";
61
+ return value;
62
+ };
63
+ var encodePathForUrl = (posixPath) => {
64
+ const normalized = normalizePosixPath(posixPath);
65
+ const segments = normalized.split("/").filter(Boolean).map(encodeURIComponent);
66
+ return `/${segments.join("/")}`;
67
+ };
68
+ var createThrottleTransform = (bytesPerSec) => {
69
+ const limit = Math.floor(bytesPerSec || 0);
70
+ if (!Number.isFinite(limit) || limit <= 0) return null;
71
+ let nextTime = Date.now();
72
+ const msPerByte = 1e3 / limit;
73
+ return new Transform({
74
+ transform(chunk, _enc, cb) {
75
+ void (async () => {
76
+ const size = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
77
+ const now = Date.now();
78
+ const start = Math.max(now, nextTime);
79
+ nextTime = start + size * msPerByte;
80
+ const waitMs = start - now;
81
+ if (waitMs > 0) await sleep(waitMs);
82
+ cb(null, chunk);
83
+ })().catch((err) => cb(err));
84
+ }
85
+ });
86
+ };
87
+ var webdavPropfindListNames = async (params) => {
88
+ const { davBaseUrl, auth, dirPath, timeoutMs } = params;
89
+ const controller = new AbortController();
90
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
91
+ try {
92
+ const url = `${davBaseUrl}${encodePathForUrl(dirPath)}`;
93
+ const body = `<?xml version="1.0" encoding="utf-8" ?>
94
+ <d:propfind xmlns:d="DAV:">
95
+ <d:prop>
96
+ <d:displayname />
97
+ </d:prop>
98
+ </d:propfind>`;
99
+ const res = await fetch(url, {
100
+ method: "PROPFIND",
101
+ headers: {
102
+ Authorization: auth,
103
+ Depth: "1",
104
+ "Content-Type": "application/xml; charset=utf-8"
105
+ },
106
+ body,
107
+ redirect: "follow",
108
+ signal: controller.signal
109
+ });
110
+ if (!res.ok) return /* @__PURE__ */ new Set();
111
+ const text = await res.text();
112
+ if (!text) return /* @__PURE__ */ new Set();
113
+ const names = /* @__PURE__ */ new Set();
114
+ const hrefRegex = /<d:href>([^<]+)<\/d:href>/gi;
115
+ let match;
116
+ while (match = hrefRegex.exec(text)) {
117
+ const href = match[1] ?? "";
118
+ const decoded = decodeURIComponent(href);
119
+ const cleaned = decoded.replace(/\/+$/, "");
120
+ const base = cleaned.split("/").filter(Boolean).pop();
121
+ if (base) names.add(base);
122
+ }
123
+ return names;
124
+ } catch {
125
+ return /* @__PURE__ */ new Set();
126
+ } finally {
127
+ clearTimeout(timer);
128
+ }
129
+ };
130
+ var buildUploadFileCandidates = (filePath) => {
131
+ const normalized = filePath.replaceAll("\\", "/");
132
+ const candidates = [
133
+ filePath,
134
+ normalized
135
+ ];
136
+ try {
137
+ candidates.push(pathToFileURL(filePath).href);
138
+ } catch {
139
+ }
140
+ if (/^[a-zA-Z]:\//.test(normalized)) {
141
+ candidates.push(`file:///${normalized}`);
142
+ }
143
+ return [...new Set(candidates.filter(Boolean))];
144
+ };
145
+ var getGroupSyncStatePath = (groupId) => path.join(dir.DataDir, "group-file-sync-state", `${String(groupId)}.json`);
146
+ var readGroupSyncState = (groupId) => {
147
+ const raw = readJsonSafe(getGroupSyncStatePath(groupId));
148
+ if (raw && typeof raw === "object" && raw.version === 1 && raw.files && typeof raw.files === "object") {
149
+ return {
150
+ version: 1,
151
+ groupId: String(raw.groupId ?? groupId),
152
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : Date.now(),
153
+ lastSyncAt: typeof raw.lastSyncAt === "number" ? raw.lastSyncAt : void 0,
154
+ files: raw.files
155
+ };
156
+ }
157
+ return {
158
+ version: 1,
159
+ groupId: String(groupId),
160
+ updatedAt: Date.now(),
161
+ files: {}
162
+ };
163
+ };
164
+ var writeGroupSyncState = (groupId, state) => {
165
+ const next = {
166
+ version: 1,
167
+ groupId: String(groupId),
168
+ updatedAt: Date.now(),
169
+ lastSyncAt: typeof state.lastSyncAt === "number" ? state.lastSyncAt : void 0,
170
+ files: state.files ?? {}
171
+ };
172
+ writeJsonSafe(getGroupSyncStatePath(groupId), next);
173
+ };
174
+ var normalizeSyncMode = (value, fallback) => {
175
+ const v = String(value ?? "").trim().toLowerCase();
176
+ if (v === "full" || v === "\u5168\u91CF") return "full";
177
+ if (v === "incremental" || v === "\u589E\u91CF" || v === "inc") return "incremental";
178
+ return fallback;
179
+ };
180
+ var getGroupSyncTarget = (cfg, groupId) => {
181
+ const list = cfg?.groupSyncTargets;
182
+ if (!Array.isArray(list)) return void 0;
183
+ return list.find((it) => String(it?.groupId) === String(groupId));
184
+ };
185
+ var getGroupFileListCompat = async (bot, groupId, folderId) => {
186
+ const groupNum = Number(groupId);
187
+ if (Number.isFinite(groupNum)) {
188
+ const onebot = bot?._onebot;
189
+ if (!folderId && typeof onebot?.getGroupRootFiles === "function") {
190
+ try {
191
+ const res = await onebot.getGroupRootFiles(groupNum);
192
+ return {
193
+ files: Array.isArray(res?.files) ? res.files : [],
194
+ folders: Array.isArray(res?.folders) ? res.folders : []
195
+ };
196
+ } catch (error) {
197
+ logger.debug(`[\u7FA4\u6587\u4EF6\u5BFC\u51FA] onebot.getGroupRootFiles \u8C03\u7528\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5\u5176\u5B83\u63A5\u53E3: ${formatErrorMessage(error)}`);
198
+ }
199
+ }
200
+ if (folderId && typeof onebot?.getGroupFilesByFolder === "function") {
201
+ try {
202
+ const res = await onebot.getGroupFilesByFolder(groupNum, folderId);
203
+ return {
204
+ files: Array.isArray(res?.files) ? res.files : [],
205
+ folders: Array.isArray(res?.folders) ? res.folders : []
206
+ };
207
+ } catch (error) {
208
+ logger.debug(`[\u7FA4\u6587\u4EF6\u5BFC\u51FA] onebot.getGroupFilesByFolder \u8C03\u7528\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5\u5176\u5B83\u63A5\u53E3: ${formatErrorMessage(error)}`);
209
+ }
210
+ }
211
+ }
212
+ if (typeof bot?.getGroupFileList === "function") {
213
+ try {
214
+ const res = await bot.getGroupFileList(groupId, folderId);
215
+ return {
216
+ files: Array.isArray(res?.files) ? res.files : [],
217
+ folders: Array.isArray(res?.folders) ? res.folders : []
218
+ };
219
+ } catch (error) {
220
+ logger.debug(`[\u7FA4\u6587\u4EF6\u5BFC\u51FA] getGroupFileList \u8C03\u7528\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5 OneBot \u6269\u5C55: ${String(error)}`);
221
+ }
222
+ }
223
+ if (!Number.isFinite(groupNum)) {
224
+ throw new Error("\u7FA4\u53F7\u65E0\u6CD5\u8F6C\u6362\u4E3A number\uFF0C\u4E14\u5F53\u524D\u9002\u914D\u5668\u4E0D\u652F\u6301 getGroupFileList");
225
+ }
226
+ if (!folderId && typeof bot?.getGroupRootFiles === "function") {
227
+ const res = await bot.getGroupRootFiles(groupNum);
228
+ return {
229
+ files: Array.isArray(res?.files) ? res.files : [],
230
+ folders: Array.isArray(res?.folders) ? res.folders : []
231
+ };
232
+ }
233
+ if (folderId && typeof bot?.getGroupFilesByFolder === "function") {
234
+ const res = await bot.getGroupFilesByFolder(groupNum, folderId);
235
+ return {
236
+ files: Array.isArray(res?.files) ? res.files : [],
237
+ folders: Array.isArray(res?.folders) ? res.folders : []
238
+ };
239
+ }
240
+ throw new Error("\u5F53\u524D\u9002\u914D\u5668\u4E0D\u652F\u6301\u83B7\u53D6\u7FA4\u6587\u4EF6\u5217\u8868\uFF08getGroupFileList / getGroupRootFiles / getGroupFilesByFolder \u5747\u4E0D\u53EF\u7528\uFF09");
241
+ };
242
+ var resolveGroupFileUrl = async (bot, contact, groupId, file) => {
243
+ if (!file.fileId) throw new Error("\u7F3A\u5C11 fileId");
244
+ const reasons = [];
245
+ if (typeof bot?.getFileUrl === "function") {
246
+ try {
247
+ const url = await bot.getFileUrl(contact, file.fileId);
248
+ if (typeof url === "string" && url) return url;
249
+ reasons.push("getFileUrl \u8FD4\u56DE\u7A7A\u503C");
250
+ } catch (error) {
251
+ reasons.push(`getFileUrl: ${error?.message ?? String(error)}`);
252
+ }
253
+ }
254
+ const groupNum = Number(groupId);
255
+ if (!Number.isFinite(groupNum)) {
256
+ throw new Error(reasons[0] ?? "\u7FA4\u53F7\u65E0\u6CD5\u8F6C\u6362\u4E3A number");
257
+ }
258
+ const onebot = bot?._onebot;
259
+ if (typeof onebot?.nc_getFile === "function") {
260
+ try {
261
+ const res = await onebot.nc_getFile(file.fileId);
262
+ if (typeof res?.url === "string" && res.url) return res.url;
263
+ reasons.push("nc_getFile \u8FD4\u56DE\u7A7A\u503C");
264
+ } catch (error) {
265
+ reasons.push(`nc_getFile: ${error?.message ?? String(error)}`);
266
+ }
267
+ }
268
+ if (typeof onebot?.getGroupFileUrl === "function") {
269
+ try {
270
+ const res = await onebot.getGroupFileUrl(groupNum, file.fileId, file.busid);
271
+ if (typeof res?.url === "string" && res.url) return res.url;
272
+ reasons.push("onebot.getGroupFileUrl \u8FD4\u56DE\u7A7A\u503C");
273
+ } catch (error) {
274
+ reasons.push(`onebot.getGroupFileUrl: ${error?.message ?? String(error)}`);
275
+ }
276
+ try {
277
+ const res = await onebot.getGroupFileUrl(groupNum, file.fileId);
278
+ if (typeof res?.url === "string" && res.url) return res.url;
279
+ } catch {
280
+ }
281
+ }
282
+ if (typeof bot?.getGroupFileUrl === "function") {
283
+ try {
284
+ const res = await bot.getGroupFileUrl(groupNum, file.fileId, file.busid);
285
+ if (typeof res?.url === "string" && res.url) return res.url;
286
+ reasons.push("getGroupFileUrl \u8FD4\u56DE\u7A7A\u503C");
287
+ } catch (error) {
288
+ reasons.push(`getGroupFileUrl: ${error?.message ?? String(error)}`);
289
+ }
290
+ try {
291
+ const res = await bot.getGroupFileUrl(groupNum, file.fileId);
292
+ if (typeof res?.url === "string" && res.url) return res.url;
293
+ } catch (error) {
294
+ reasons.push(`getGroupFileUrl(no busid): ${error?.message ?? String(error)}`);
295
+ }
296
+ }
297
+ throw new Error(reasons[0] ?? "\u65E0\u6CD5\u83B7\u53D6\u4E0B\u8F7DURL\uFF08\u672A\u627E\u5230\u53EF\u7528\u63A5\u53E3\uFF09");
298
+ };
299
+ var buildOpenListDavBaseUrl = (baseUrl) => {
300
+ const normalized = String(baseUrl ?? "").trim().replace(/\/+$/, "");
301
+ if (!normalized) return "";
302
+ return `${normalized}/dav`;
303
+ };
304
+ var buildOpenListApiBaseUrl = (baseUrl) => {
305
+ const normalized = String(baseUrl ?? "").trim().replace(/\/+$/, "");
306
+ if (!normalized) return "";
307
+ return `${normalized}/api`;
308
+ };
309
+ var isSameOriginUrl = (a, b) => {
310
+ try {
311
+ return new URL(a).origin === new URL(b).origin;
312
+ } catch {
313
+ return false;
314
+ }
315
+ };
316
+ var buildOpenListRawUrlAuthHeaders = (params) => {
317
+ const { rawUrl, baseUrl, token } = params;
318
+ if (!token) return void 0;
319
+ if (!isSameOriginUrl(rawUrl, baseUrl)) return void 0;
320
+ return { Authorization: token };
321
+ };
322
+ var buildOpenListAuthHeader = (username, password) => {
323
+ const user = String(username ?? "");
324
+ const pass = String(password ?? "");
325
+ const token = Buffer.from(`${user}:${pass}`).toString("base64");
326
+ return `Basic ${token}`;
327
+ };
328
+ var formatErrorMessage = (error) => {
329
+ if (error instanceof Error) {
330
+ const base = error.message || String(error);
331
+ const cause = error.cause;
332
+ if (cause) {
333
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
334
+ const causeCode = typeof cause === "object" && cause && "code" in cause ? String(cause.code) : "";
335
+ const extra = [causeCode, causeMsg].filter(Boolean).join(" ");
336
+ if (extra && extra !== base) return `${base} (${extra})`;
337
+ }
338
+ return base;
339
+ }
340
+ return String(error);
341
+ };
342
+ var isAbortError = (error) => {
343
+ return Boolean(error && typeof error === "object" && "name" in error && error.name === "AbortError");
344
+ };
345
+ var isRetryableWebDavError = (error) => {
346
+ const msg = formatErrorMessage(error);
347
+ return /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|UND_ERR|socket hang up/i.test(msg);
348
+ };
349
+ var fetchTextSafely = async (res) => {
350
+ try {
351
+ const text = await res.text();
352
+ return text.slice(0, 500);
353
+ } catch {
354
+ return "";
355
+ }
356
+ };
357
+ var webdavMkcolOk = (status) => status === 201 || status === 405;
358
+ var openlistApiReadJson = async (res) => {
359
+ try {
360
+ return await res.json();
361
+ } catch {
362
+ return {};
363
+ }
364
+ };
365
+ var openlistApiLogin = async (params) => {
366
+ const { baseUrl, username, password, timeoutMs } = params;
367
+ const apiBaseUrl = buildOpenListApiBaseUrl(baseUrl);
368
+ if (!apiBaseUrl) throw new Error(`OpenList API \u5730\u5740\u4E0D\u6B63\u786E: ${baseUrl}`);
369
+ const controller = new AbortController();
370
+ const timer = setTimeout(() => controller.abort(), Math.max(1e3, Math.floor(timeoutMs) || 0));
371
+ try {
372
+ const url = `${apiBaseUrl}/auth/login`;
373
+ let res;
374
+ try {
375
+ res = await fetch(url, {
376
+ method: "POST",
377
+ headers: { "Content-Type": "application/json" },
378
+ body: JSON.stringify({
379
+ username: String(username ?? ""),
380
+ password: String(password ?? "")
381
+ }),
382
+ redirect: "follow",
383
+ signal: controller.signal
384
+ });
385
+ } catch (error) {
386
+ if (isAbortError(error)) throw new Error(`OpenList \u767B\u5F55\u8D85\u65F6: ${baseUrl}`);
387
+ throw new Error(`OpenList \u767B\u5F55\u8BF7\u6C42\u5931\u8D25: ${baseUrl} - ${formatErrorMessage(error)}`);
388
+ }
389
+ if (!res.ok) {
390
+ const body = await fetchTextSafely(res);
391
+ throw new Error(`OpenList \u767B\u5F55\u5931\u8D25: ${baseUrl} -> ${res.status} ${res.statusText}${body ? ` - ${body}` : ""}`);
392
+ }
393
+ const json = await openlistApiReadJson(res);
394
+ if (typeof json?.code === "number" && json.code !== 200) {
395
+ const msg = json?.message ? ` - ${json.message}` : "";
396
+ throw new Error(`OpenList \u767B\u5F55\u5931\u8D25: ${baseUrl} -> code=${json.code}${msg}`);
397
+ }
398
+ if (typeof json?.data?.token === "string" && json.data.token) return json.data.token;
399
+ const hint = json?.message ? ` - ${json.message}` : "";
400
+ throw new Error(`OpenList \u767B\u5F55\u5931\u8D25: ${baseUrl} \u672A\u83B7\u53D6\u5230 token${hint}`);
401
+ } finally {
402
+ clearTimeout(timer);
403
+ }
404
+ };
405
+ var openlistApiPost = async (params) => {
406
+ const { baseUrl, token, apiPath, body, timeoutMs } = params;
407
+ const apiBaseUrl = buildOpenListApiBaseUrl(baseUrl);
408
+ if (!apiBaseUrl) throw new Error("OpenList API \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 baseUrl");
409
+ const controller = new AbortController();
410
+ const timer = setTimeout(() => controller.abort(), Math.max(1e3, Math.floor(timeoutMs) || 0));
411
+ try {
412
+ const url = `${apiBaseUrl}${apiPath.startsWith("/") ? apiPath : `/${apiPath}`}`;
413
+ let res;
414
+ try {
415
+ const headers = {
416
+ "Content-Type": "application/json"
417
+ };
418
+ const authToken = String(token ?? "").trim();
419
+ if (authToken) headers.Authorization = authToken;
420
+ res = await fetch(url, {
421
+ method: "POST",
422
+ headers,
423
+ body: JSON.stringify(body ?? {}),
424
+ redirect: "follow",
425
+ signal: controller.signal
426
+ });
427
+ } catch (error) {
428
+ if (isAbortError(error)) throw new Error(`OpenList API \u8BF7\u6C42\u8D85\u65F6: ${apiPath}`);
429
+ throw new Error(`OpenList API \u8BF7\u6C42\u5931\u8D25: ${apiPath} - ${formatErrorMessage(error)}`);
430
+ }
431
+ if (!res.ok) {
432
+ const text = await fetchTextSafely(res);
433
+ throw new Error(`OpenList API \u5931\u8D25: ${apiPath} -> ${res.status} ${res.statusText}${text ? ` - ${text}` : ""}`);
434
+ }
435
+ const json = await openlistApiReadJson(res);
436
+ if (typeof json?.code === "number" && json.code !== 200) {
437
+ const msg = json?.message ? ` - ${json.message}` : "";
438
+ throw new Error(`OpenList API \u5931\u8D25: ${apiPath} -> code=${json.code}${msg}`);
439
+ }
440
+ return json;
441
+ } finally {
442
+ clearTimeout(timer);
443
+ }
444
+ };
445
+ var openlistApiListEntries = async (params) => {
446
+ const { baseUrl, token, dirPath, timeoutMs } = params;
447
+ const normalized = normalizePosixPath(dirPath);
448
+ const requestedPerPage = typeof params.perPage === "number" ? params.perPage : 1e3;
449
+ const perPage = Math.max(1, Math.min(5e3, Math.floor(requestedPerPage) || 1e3));
450
+ const out = [];
451
+ const maxPages = 2e4;
452
+ for (let page = 1; page <= maxPages; page++) {
453
+ const json = await openlistApiPost({
454
+ baseUrl,
455
+ token,
456
+ apiPath: "/fs/list",
457
+ timeoutMs,
458
+ body: {
459
+ path: normalized,
460
+ password: "",
461
+ page,
462
+ per_page: perPage,
463
+ refresh: false
464
+ }
465
+ });
466
+ const items = Array.isArray(json?.data?.content) ? json.data.content : [];
467
+ for (const it of items) {
468
+ const name = String(it?.name ?? "").trim();
469
+ if (!name) continue;
470
+ out.push({ name, isDir: Boolean(it?.is_dir) });
471
+ }
472
+ const total = typeof json?.data?.total === "number" ? json.data.total : void 0;
473
+ if (!items.length) break;
474
+ if (typeof total === "number" && Number.isFinite(total) && out.length >= total) break;
475
+ }
476
+ return out;
477
+ };
478
+ var openlistApiGetRawUrl = async (params) => {
479
+ const { baseUrl, token, filePath, timeoutMs } = params;
480
+ const normalized = normalizePosixPath(filePath);
481
+ const json = await openlistApiPost({
482
+ baseUrl,
483
+ token,
484
+ apiPath: "/fs/get",
485
+ timeoutMs,
486
+ body: { path: normalized, password: "", refresh: false }
487
+ });
488
+ const rawUrl = typeof json?.data?.raw_url === "string" ? json.data.raw_url : "";
489
+ if (!rawUrl) throw new Error(`OpenList \u83B7\u53D6 raw_url \u5931\u8D25: ${normalized}`);
490
+ return rawUrl;
491
+ };
492
+ var openlistApiPathExists = async (params) => {
493
+ const { baseUrl, token, path: filePath, timeoutMs } = params;
494
+ try {
495
+ await openlistApiPost({
496
+ baseUrl,
497
+ token,
498
+ apiPath: "/fs/get",
499
+ timeoutMs,
500
+ body: { path: normalizePosixPath(filePath), password: "", refresh: false }
501
+ });
502
+ return true;
503
+ } catch (error) {
504
+ const msg = formatErrorMessage(error);
505
+ if (/code=404\b/i.test(msg) || /not found/i.test(msg) || /object not found/i.test(msg)) return false;
506
+ return false;
507
+ }
508
+ };
509
+ var createOpenListApiDirEnsurer = (baseUrl, token, timeoutMs) => {
510
+ const ensured = /* @__PURE__ */ new Map();
511
+ const requestTimeoutMs = Math.max(1e3, Math.floor(timeoutMs) || 0);
512
+ const ensureDir2 = async (dirPath) => {
513
+ const normalized = normalizePosixPath(dirPath);
514
+ if (normalized === "/") return;
515
+ const segments = normalized.split("/").filter(Boolean);
516
+ let current = "";
517
+ for (const segment of segments) {
518
+ current += `/${segment}`;
519
+ const existing = ensured.get(current);
520
+ if (existing) {
521
+ await existing;
522
+ continue;
523
+ }
524
+ const promise = (async () => {
525
+ try {
526
+ await openlistApiPost({
527
+ baseUrl,
528
+ token,
529
+ apiPath: "/fs/mkdir",
530
+ timeoutMs: requestTimeoutMs,
531
+ body: { path: current }
532
+ });
533
+ } catch (error) {
534
+ const msg = formatErrorMessage(error);
535
+ if (/exist/i.test(msg) || /already/i.test(msg) || /重复/i.test(msg)) return;
536
+ throw error;
537
+ }
538
+ })();
539
+ ensured.set(current, promise);
540
+ try {
541
+ await promise;
542
+ } catch (error) {
543
+ ensured.delete(current);
544
+ throw error;
545
+ }
546
+ }
547
+ };
548
+ return { ensureDir: ensureDir2 };
549
+ };
550
+ var downloadAndUploadByOpenListApiPut = async (params) => {
551
+ const { sourceUrl, sourceHeaders, targetBaseUrl, targetToken, targetPath, timeoutMs } = params;
552
+ const apiBaseUrl = buildOpenListApiBaseUrl(targetBaseUrl);
553
+ if (!apiBaseUrl) throw new Error("\u76EE\u6807 OpenList API \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u76EE\u6807 OpenList \u5730\u5740");
554
+ const controller = new AbortController();
555
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
556
+ try {
557
+ let downloadRes;
558
+ try {
559
+ downloadRes = await fetch(sourceUrl, {
560
+ headers: sourceHeaders,
561
+ redirect: "follow",
562
+ signal: controller.signal
563
+ });
564
+ } catch (error) {
565
+ if (isAbortError(error)) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u8D85\u65F6");
566
+ throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${formatErrorMessage(error)}`);
567
+ }
568
+ if (!downloadRes.ok) {
569
+ const body = await fetchTextSafely(downloadRes);
570
+ throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
571
+ }
572
+ if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
573
+ const headers = {
574
+ "File-Path": encodeURIComponent(normalizePosixPath(targetPath))
575
+ };
576
+ const authToken = String(targetToken ?? "").trim();
577
+ if (authToken) headers.Authorization = authToken;
578
+ const contentType = downloadRes.headers.get("content-type");
579
+ const contentLength = downloadRes.headers.get("content-length");
580
+ if (contentType) headers["Content-Type"] = contentType;
581
+ if (contentLength) headers["Content-Length"] = contentLength;
582
+ const sourceStream = Readable.fromWeb(downloadRes.body);
583
+ let putRes;
584
+ try {
585
+ putRes = await fetch(`${apiBaseUrl}/fs/put`, {
586
+ method: "PUT",
587
+ headers,
588
+ body: sourceStream,
589
+ // @ts-expect-error Node fetch streaming body requires duplex
590
+ duplex: "half",
591
+ redirect: "follow",
592
+ signal: controller.signal
593
+ });
594
+ } catch (error) {
595
+ if (isAbortError(error)) throw new Error("\u76EE\u6807\u7AEF\u5199\u5165\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5\u5BF9\u7AEFOpenList\u8FDE\u63A5/\u6743\u9650\uFF09");
596
+ throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${formatErrorMessage(error)}`);
597
+ }
598
+ if (!putRes.ok) {
599
+ const body = await fetchTextSafely(putRes);
600
+ throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
601
+ }
602
+ const json = await openlistApiReadJson(putRes);
603
+ if (typeof json?.code === "number" && json.code !== 200) {
604
+ const msg = json?.message ? ` - ${json.message}` : "";
605
+ throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: code=${json.code}${msg}`);
606
+ }
607
+ } finally {
608
+ clearTimeout(timer);
609
+ }
610
+ };
611
+ var retryAsync = async (fn, options) => {
612
+ const retries = Math.max(0, Math.floor(options.retries) || 0);
613
+ const delaysMs = options.delaysMs?.length ? options.delaysMs : [300, 900, 2e3];
614
+ const isRetryable = options.isRetryable ?? (() => true);
615
+ let lastError;
616
+ for (let attempt = 0; attempt <= retries; attempt++) {
617
+ try {
618
+ return await fn();
619
+ } catch (error) {
620
+ lastError = error;
621
+ if (attempt >= retries || !isRetryable(error)) throw error;
622
+ const delay = delaysMs[Math.min(attempt, delaysMs.length - 1)];
623
+ await sleep(delay);
624
+ }
625
+ }
626
+ throw lastError;
627
+ };
628
+ var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
629
+ const ensured = /* @__PURE__ */ new Map();
630
+ const requestTimeoutMs = Math.max(1e3, Math.floor(timeoutMs) || 0);
631
+ const ensureDir2 = async (dirPath) => {
632
+ const normalized = normalizePosixPath(dirPath);
633
+ if (normalized === "/") return;
634
+ const segments = normalized.split("/").filter(Boolean);
635
+ let current = "";
636
+ for (const segment of segments) {
637
+ current += `/${segment}`;
638
+ const existing = ensured.get(current);
639
+ if (existing) {
640
+ await existing;
641
+ continue;
642
+ }
643
+ const promise = (async () => {
644
+ const url = `${davBaseUrl}${encodePathForUrl(current)}`;
645
+ const controller = new AbortController();
646
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
647
+ try {
648
+ let res;
649
+ try {
650
+ res = await fetch(url, {
651
+ method: "MKCOL",
652
+ headers: { Authorization: auth },
653
+ redirect: "follow",
654
+ signal: controller.signal
655
+ });
656
+ } catch (error) {
657
+ if (isAbortError(error)) throw new Error(`MKCOL \u8D85\u65F6: ${current}`);
658
+ throw new Error(`MKCOL \u8BF7\u6C42\u5931\u8D25: ${current} - ${formatErrorMessage(error)}`);
659
+ }
660
+ if (webdavMkcolOk(res.status) || res.ok) return;
661
+ const body = await fetchTextSafely(res);
662
+ const hint = res.status === 401 ? "\uFF08\u8D26\u53F7/\u5BC6\u7801\u9519\u8BEF\uFF0C\u6216\u672A\u5F00\u542F WebDAV\uFF09" : res.status === 403 ? "\uFF08\u6CA1\u6709 WebDAV \u7BA1\u7406/\u5199\u5165\u6743\u9650\uFF0C\u6216\u76EE\u6807\u76EE\u5F55\u4E0D\u53EF\u5199/\u4E0D\u5728\u7528\u6237\u53EF\u8BBF\u95EE\u8303\u56F4\uFF09" : "";
663
+ throw new Error(`MKCOL \u5931\u8D25: ${current} -> ${res.status} ${res.statusText}${hint}${body ? ` - ${body}` : ""}`);
664
+ } finally {
665
+ clearTimeout(timer);
666
+ }
667
+ })();
668
+ ensured.set(current, promise);
669
+ try {
670
+ await promise;
671
+ } catch (error) {
672
+ ensured.delete(current);
673
+ throw error;
674
+ }
675
+ }
676
+ };
677
+ return { ensureDir: ensureDir2 };
678
+ };
679
+ var downloadAndUploadByWebDav = async (params) => {
680
+ const { sourceUrl, sourceHeaders, targetUrl, auth, timeoutMs, rateLimitBytesPerSec } = params;
681
+ const controller = new AbortController();
682
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
683
+ try {
684
+ let downloadRes;
685
+ try {
686
+ downloadRes = await fetch(sourceUrl, { headers: sourceHeaders, redirect: "follow", signal: controller.signal });
687
+ } catch (error) {
688
+ if (isAbortError(error)) throw new Error("\u4E0B\u8F7D\u8D85\u65F6\uFF08URL\u53EF\u80FD\u5DF2\u5931\u6548\uFF09");
689
+ throw new Error(`\u4E0B\u8F7D\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
690
+ }
691
+ if (!downloadRes.ok) {
692
+ const body = await fetchTextSafely(downloadRes);
693
+ const hint = downloadRes.status === 403 ? "\uFF08\u53EF\u80FDURL\u5DF2\u8FC7\u671F\uFF0C\u9700\u8981\u91CD\u65B0\u83B7\u53D6\uFF09" : "";
694
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
695
+ }
696
+ if (!downloadRes.body) throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
697
+ const headers = { Authorization: auth };
698
+ const contentType = downloadRes.headers.get("content-type");
699
+ const contentLength = downloadRes.headers.get("content-length");
700
+ if (contentType) headers["Content-Type"] = contentType;
701
+ if (contentLength) headers["Content-Length"] = contentLength;
702
+ const sourceStream = Readable.fromWeb(downloadRes.body);
703
+ const throttle = createThrottleTransform(Math.floor(rateLimitBytesPerSec || 0));
704
+ const bodyStream = throttle ? sourceStream.pipe(throttle) : sourceStream;
705
+ let putRes;
706
+ try {
707
+ putRes = await fetch(targetUrl, {
708
+ method: "PUT",
709
+ headers,
710
+ body: bodyStream,
711
+ // @ts-expect-error Node fetch streaming body requires duplex
712
+ duplex: "half",
713
+ redirect: "follow",
714
+ signal: controller.signal
715
+ });
716
+ } catch (error) {
717
+ if (isAbortError(error)) throw new Error("\u4E0A\u4F20\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5 OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
718
+ throw new Error(`\u4E0A\u4F20\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
719
+ }
720
+ if (!putRes.ok) {
721
+ const body = await fetchTextSafely(putRes);
722
+ const hint = putRes.status === 401 ? "\uFF08\u8D26\u53F7/\u5BC6\u7801\u9519\u8BEF\uFF0C\u6216\u672A\u5F00\u542F WebDAV\uFF09" : putRes.status === 403 ? "\uFF08\u6CA1\u6709 WebDAV \u7BA1\u7406/\u5199\u5165\u6743\u9650\uFF0C\u6216\u76EE\u6807\u76EE\u5F55\u4E0D\u53EF\u5199/\u4E0D\u5728\u7528\u6237\u53EF\u8BBF\u95EE\u8303\u56F4\uFF09" : "";
723
+ throw new Error(`\u4E0A\u4F20\u5931\u8D25: ${putRes.status} ${putRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
724
+ }
725
+ } finally {
726
+ clearTimeout(timer);
727
+ }
728
+ };
729
+ var activeGroupSync = /* @__PURE__ */ new Set();
730
+ var withGroupSyncLock = async (groupId, fn) => {
731
+ const key = String(groupId);
732
+ if (activeGroupSync.has(key)) throw new Error("\u8BE5\u7FA4\u540C\u6B65\u4EFB\u52A1\u6B63\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5");
733
+ activeGroupSync.add(key);
734
+ try {
735
+ return await fn();
736
+ } finally {
737
+ activeGroupSync.delete(key);
738
+ }
739
+ };
740
+ var buildRemotePathForItem = (item, targetDir, flat) => {
741
+ const relativeParts = (flat ? [item.name] : item.path.split("/")).filter(Boolean).map(safePathSegment);
742
+ return normalizePosixPath(path.posix.join(targetDir, ...relativeParts));
743
+ };
744
+ var isSameSyncedFile = (prev, item) => {
745
+ if (!prev) return false;
746
+ if (prev.fileId && item.fileId && prev.fileId !== item.fileId) return false;
747
+ const md5Ok = prev.md5 && item.md5 && prev.md5 === item.md5;
748
+ const sha1Ok = prev.sha1 && item.sha1 && prev.sha1 === item.sha1;
749
+ if (md5Ok || sha1Ok) return true;
750
+ const sizeOk = typeof prev.size === "number" && typeof item.size === "number" && prev.size === item.size;
751
+ const timeOk = typeof prev.uploadTime === "number" && typeof item.uploadTime === "number" && prev.uploadTime === item.uploadTime;
752
+ if (sizeOk && timeOk) return true;
753
+ if (timeOk && prev.fileId && item.fileId && prev.fileId === item.fileId) return true;
754
+ if (sizeOk && prev.fileId && item.fileId && prev.fileId === item.fileId) return true;
755
+ return false;
756
+ };
757
+ var runWithConcurrency = async (items, concurrency, fn) => {
758
+ const limit = Math.max(1, Math.floor(concurrency) || 1);
759
+ const executing = /* @__PURE__ */ new Set();
760
+ for (let index = 0; index < items.length; index++) {
761
+ const item = items[index];
762
+ const task = (async () => fn(item, index))();
763
+ executing.add(task);
764
+ task.finally(() => executing.delete(task));
765
+ if (executing.size >= limit) {
766
+ await Promise.race(executing);
767
+ }
768
+ }
769
+ await Promise.all(executing);
770
+ };
771
+ var runWithAdaptiveConcurrency = async (items, options) => {
772
+ const max = Math.max(1, Math.floor(options.max) || 1);
773
+ let current = Math.min(max, Math.max(1, Math.floor(options.initial) || 1));
774
+ const onAdjust = options.onAdjust;
775
+ const results = [];
776
+ const pushResult = (ok, ms, reason) => {
777
+ results.push({ ok, ms, reason });
778
+ if (results.length > 20) results.shift();
779
+ if (results.length < 10) return;
780
+ if (results.length % 5 !== 0) return;
781
+ const failCount = results.filter((r) => !r.ok).length;
782
+ const failRate = failCount / results.length;
783
+ const avgMs = results.reduce((acc, r) => acc + r.ms, 0) / results.length;
784
+ const hasTimeout = results.some((r) => (r.reason || "").includes("\u8D85\u65F6"));
785
+ if (hasTimeout || failRate >= 0.2) {
786
+ if (current > 1) {
787
+ current -= 1;
788
+ onAdjust?.(current, hasTimeout ? "timeout" : `failRate=${failRate.toFixed(2)}`);
789
+ }
790
+ return;
791
+ }
792
+ if (failCount === 0 && current < max) {
793
+ if (avgMs < 6e4 || results.length === 20) {
794
+ current += 1;
795
+ onAdjust?.(current, "stable");
796
+ }
797
+ }
798
+ };
799
+ let nextIndex = 0;
800
+ const executing = /* @__PURE__ */ new Set();
801
+ const launch = (index) => {
802
+ const item = items[index];
803
+ const start = Date.now();
804
+ const task = (async () => {
805
+ try {
806
+ await options.fn(item, index);
807
+ pushResult(true, Date.now() - start);
808
+ } catch (error) {
809
+ const msg = formatErrorMessage(error);
810
+ pushResult(false, Date.now() - start, msg);
811
+ throw error;
812
+ }
813
+ })();
814
+ executing.add(task);
815
+ task.finally(() => executing.delete(task));
816
+ return task;
817
+ };
818
+ while (nextIndex < items.length || executing.size) {
819
+ while (nextIndex < items.length && executing.size < current) {
820
+ launch(nextIndex);
821
+ nextIndex++;
822
+ }
823
+ if (executing.size) await Promise.race(executing);
824
+ }
825
+ };
826
+ var collectAllGroupFiles = async (bot, groupId, startFolderId) => {
827
+ const files = [];
828
+ const visitedFolders = /* @__PURE__ */ new Set();
829
+ const walk = async (folderId, prefix) => {
830
+ if (folderId) {
831
+ if (visitedFolders.has(folderId)) return;
832
+ visitedFolders.add(folderId);
833
+ }
834
+ const { files: rawFiles, folders: rawFolders } = await getGroupFileListCompat(bot, groupId, folderId);
835
+ for (const raw of rawFiles) {
836
+ const fileId = pickFirstString(raw?.fid, raw?.file_id, raw?.fileId, raw?.id);
837
+ const name = pickFirstString(raw?.name, raw?.file_name, raw?.fileName) ?? (fileId ? `file-${fileId}` : "unknown-file");
838
+ const filePath = prefix ? `${prefix}/${name}` : name;
839
+ files.push({
840
+ path: filePath,
841
+ fileId: fileId ?? "",
842
+ name,
843
+ size: pickFirstNumber(raw?.size, raw?.file_size, raw?.fileSize),
844
+ uploadTime: pickFirstNumber(raw?.uploadTime, raw?.upload_time),
845
+ uploaderId: pickFirstString(raw?.uploadId, raw?.uploader, raw?.uploader_id),
846
+ uploaderName: pickFirstString(raw?.uploadName, raw?.uploader_name),
847
+ md5: pickFirstString(raw?.md5),
848
+ sha1: pickFirstString(raw?.sha1),
849
+ sha3: pickFirstString(raw?.sha3),
850
+ busid: pickFirstNumber(raw?.busid, raw?.busId)
851
+ });
852
+ }
853
+ for (const raw of rawFolders) {
854
+ const folderId2 = pickFirstString(raw?.id, raw?.folder_id, raw?.folderId);
855
+ if (!folderId2) continue;
856
+ const folderName = pickFirstString(raw?.name, raw?.folder_name, raw?.folderName) ?? folderId2;
857
+ const nextPrefix = prefix ? `${prefix}/${folderName}` : folderName;
858
+ await walk(folderId2, nextPrefix);
859
+ }
860
+ };
861
+ await walk(startFolderId, "");
862
+ return files;
863
+ };
864
+ var normalizeGroupFileRelativePath = (input) => {
865
+ return String(input ?? "").replaceAll("\0", "").replaceAll("\\", "/").trim().replace(/^\/+/, "").replace(/\/+$/, "");
866
+ };
867
+ var locateGroupFileById = async (bot, groupId, fileId, options) => {
868
+ const timeoutMs = Math.max(1e3, Math.floor(options?.timeoutMs ?? 12e3));
869
+ const maxFolders = Math.max(1, Math.floor(options?.maxFolders ?? 2e3));
870
+ const expectedName = String(options?.expectedName ?? "").trim();
871
+ const expectedSize = typeof options?.expectedSize === "number" && Number.isFinite(options.expectedSize) ? Math.max(0, Math.floor(options.expectedSize)) : void 0;
872
+ const start = Date.now();
873
+ const visited = /* @__PURE__ */ new Set();
874
+ const queued = /* @__PURE__ */ new Set();
875
+ const stack = [{ folderId: void 0, prefix: "" }];
876
+ let scanned = 0;
877
+ let bestCandidate;
878
+ while (stack.length) {
879
+ if (Date.now() - start > timeoutMs) return bestCandidate;
880
+ if (scanned >= maxFolders) return bestCandidate;
881
+ const current = stack.pop();
882
+ const folderId = current.folderId;
883
+ const prefix = current.prefix;
884
+ if (folderId) {
885
+ if (visited.has(folderId)) continue;
886
+ visited.add(folderId);
887
+ }
888
+ scanned++;
889
+ const { files: rawFiles, folders: rawFolders } = await getGroupFileListCompat(bot, groupId, folderId);
890
+ for (const raw of rawFiles) {
891
+ const id = pickFirstString(raw?.fid, raw?.file_id, raw?.fileId, raw?.id);
892
+ const uploadTime = pickFirstNumber(raw?.uploadTime, raw?.upload_time);
893
+ const size = pickFirstNumber(raw?.size, raw?.file_size, raw?.fileSize);
894
+ const busid = pickFirstNumber(raw?.busid, raw?.busId);
895
+ if (!id || String(id) !== String(fileId)) continue;
896
+ const name = pickFirstString(raw?.name, raw?.file_name, raw?.fileName) ?? (id ? `file-${id}` : "unknown-file");
897
+ const filePath = prefix ? `${prefix}/${name}` : name;
898
+ return {
899
+ path: filePath,
900
+ name,
901
+ busid
902
+ };
903
+ }
904
+ if (expectedName) {
905
+ for (const raw of rawFiles) {
906
+ const id = pickFirstString(raw?.fid, raw?.file_id, raw?.fileId, raw?.id);
907
+ if (!id) continue;
908
+ const name = pickFirstString(raw?.name, raw?.file_name, raw?.fileName) ?? (id ? `file-${id}` : "unknown-file");
909
+ if (String(name).trim() !== expectedName) continue;
910
+ const size = pickFirstNumber(raw?.size, raw?.file_size, raw?.fileSize);
911
+ if (typeof expectedSize === "number") {
912
+ if (typeof size !== "number" || !Number.isFinite(size) || Math.floor(size) !== expectedSize) continue;
913
+ }
914
+ const uploadTime = pickFirstNumber(raw?.uploadTime, raw?.upload_time);
915
+ const busid = pickFirstNumber(raw?.busid, raw?.busId);
916
+ const filePath = prefix ? `${prefix}/${name}` : name;
917
+ if (!bestCandidate) {
918
+ bestCandidate = { path: filePath, name, busid, uploadTime };
919
+ continue;
920
+ }
921
+ const bestTime = typeof bestCandidate.uploadTime === "number" ? bestCandidate.uploadTime : -1;
922
+ const currentTime = typeof uploadTime === "number" ? uploadTime : -1;
923
+ if (currentTime > bestTime) bestCandidate = { path: filePath, name, busid, uploadTime };
924
+ }
925
+ }
926
+ for (const raw of rawFolders) {
927
+ const id = pickFirstString(raw?.id, raw?.folder_id, raw?.folderId);
928
+ if (!id) continue;
929
+ if (visited.has(id) || queued.has(id)) continue;
930
+ queued.add(id);
931
+ const folderName = pickFirstString(raw?.name, raw?.folder_name, raw?.folderName) ?? id;
932
+ const nextPrefix = prefix ? `${prefix}/${folderName}` : folderName;
933
+ stack.push({ folderId: id, prefix: nextPrefix });
934
+ }
935
+ }
936
+ return bestCandidate;
937
+ };
938
+ var locateGroupFileByIdWithRetry = async (bot, groupId, fileId, options) => {
939
+ const retries = Math.max(0, Math.floor(options?.retries ?? 2));
940
+ const delayMs = Math.max(0, Math.floor(options?.delayMs ?? 800));
941
+ let lastError;
942
+ for (let attempt = 0; attempt <= retries; attempt++) {
943
+ try {
944
+ const found = await locateGroupFileById(bot, groupId, fileId, options);
945
+ if (found?.path) return found;
946
+ } catch (error) {
947
+ lastError = error;
948
+ }
949
+ if (attempt < retries && delayMs > 0) await sleep(delayMs);
950
+ }
951
+ if (lastError) throw lastError;
952
+ };
953
+ var writeExportFile = (format, outPath, payload, list) => {
954
+ ensureDir(path.dirname(outPath));
955
+ if (format === "json") {
956
+ fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), "utf8");
957
+ return;
958
+ }
959
+ const header = ["path", "name", "fileId", "size", "uploadTime", "uploaderId", "uploaderName", "md5", "sha1", "sha3", "url", "busid"];
960
+ const rows = [header.join(",")];
961
+ for (const item of list) {
962
+ rows.push([
963
+ csvEscape(item.path),
964
+ csvEscape(item.name),
965
+ csvEscape(item.fileId),
966
+ csvEscape(item.size ?? ""),
967
+ csvEscape(item.uploadTime ?? ""),
968
+ csvEscape(item.uploaderId ?? ""),
969
+ csvEscape(item.uploaderName ?? ""),
970
+ csvEscape(item.md5 ?? ""),
971
+ csvEscape(item.sha1 ?? ""),
972
+ csvEscape(item.sha3 ?? ""),
973
+ csvEscape(item.url ?? ""),
974
+ csvEscape(item.busid ?? "")
975
+ ].join(","));
976
+ }
977
+ fs.writeFileSync(outPath, rows.join("\n"), "utf8");
978
+ };
979
+ var parseArgs = (text) => {
980
+ const raw = text.trim();
981
+ const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
982
+ const format = /(^|\s)(--csv|csv)(\s|$)/i.test(raw) ? "csv" : "json";
983
+ const withUrl = !/(^|\s)(--no-url|--nourl|no-url|nourl)(\s|$)/i.test(raw);
984
+ const urlOnly = /(^|\s)(--url-only|--urlonly|url-only|urlonly)(\s|$)/i.test(raw);
985
+ const sendFile = /(^|\s)(--send-file|--sendfile|send-file|sendfile)(\s|$)/i.test(raw);
986
+ let groupId;
987
+ for (let i = 0; i < tokens.length; i++) {
988
+ const token = tokens[i];
989
+ const nextToken = tokens[i + 1];
990
+ if (/^--(group|gid|groupid)$/i.test(token) && nextToken && /^\d+$/.test(nextToken)) {
991
+ groupId = nextToken;
992
+ break;
993
+ }
994
+ const assignMatch = token.match(/^(group|gid|groupid)=(\d+)$/i);
995
+ if (assignMatch) {
996
+ groupId = assignMatch[2];
997
+ break;
998
+ }
999
+ }
1000
+ if (!groupId) {
1001
+ for (let i = 0; i < tokens.length; i++) {
1002
+ const token = tokens[i];
1003
+ const prevToken = tokens[i - 1];
1004
+ if (!/^\d+$/.test(token)) continue;
1005
+ if (prevToken && /^--(folder|max|concurrency|group|gid|groupid)$/i.test(prevToken)) continue;
1006
+ groupId = token;
1007
+ break;
1008
+ }
1009
+ }
1010
+ const folderMatch = raw.match(/--folder\s+(\S+)/i) ?? raw.match(/(^|\s)folder=(\S+)/i);
1011
+ const folderId = folderMatch ? folderMatch[folderMatch.length - 1] : void 0;
1012
+ const maxMatch = raw.match(/--max\s+(\d+)/i) ?? raw.match(/(^|\s)max=(\d+)/i);
1013
+ const maxFiles = maxMatch ? Number(maxMatch[maxMatch.length - 1]) : void 0;
1014
+ const concurrencyMatch = raw.match(/--concurrency\s+(\d+)/i) ?? raw.match(/(^|\s)concurrency=(\d+)/i);
1015
+ const concurrency = concurrencyMatch ? Number(concurrencyMatch[concurrencyMatch.length - 1]) : void 0;
1016
+ const concurrencySpecified = Boolean(concurrencyMatch);
1017
+ const help = /(^|\s)(--help|-h|help|\?)(\s|$)/i.test(raw);
1018
+ return { groupId, format, withUrl, urlOnly, sendFile, folderId, maxFiles, concurrency, concurrencySpecified, help };
1019
+ };
1020
+ var parseSyncArgs = (text) => {
1021
+ const raw = text.trim();
1022
+ const base = parseArgs(text);
1023
+ const toMatch = raw.match(/--to\s+(\S+)/i) ?? raw.match(/(^|\s)to=(\S+)/i);
1024
+ const to = toMatch ? toMatch[toMatch.length - 1] : void 0;
1025
+ const toSpecified = Boolean(toMatch);
1026
+ const flatFlag = /(^|\s)(--flat|flat)(\s|$)/i.test(raw);
1027
+ const keepFlag = /(^|\s)(--keep|--no-flat|keep|no-flat)(\s|$)/i.test(raw);
1028
+ const flatSpecified = flatFlag || keepFlag;
1029
+ const flat = flatFlag ? true : keepFlag ? false : void 0;
1030
+ const help = /(^|\s)(--help|-h|help|\?)(\s|$)/i.test(raw);
1031
+ const concurrency = base.concurrency;
1032
+ const concurrencySpecified = base.concurrencySpecified;
1033
+ const timeoutMatch = raw.match(/--timeout\s+(\d+)/i) ?? raw.match(/(^|\s)timeout=(\d+)/i);
1034
+ const timeoutSec = timeoutMatch ? Number(timeoutMatch[timeoutMatch.length - 1]) : void 0;
1035
+ const timeoutSpecified = Boolean(timeoutMatch);
1036
+ const modeFull = /(^|\s)(--full|full)(\s|$)/i.test(raw);
1037
+ const modeInc = /(^|\s)(--inc|--incremental|inc|incremental)(\s|$)/i.test(raw);
1038
+ const mode = modeFull ? "full" : modeInc ? "incremental" : void 0;
1039
+ return {
1040
+ groupId: base.groupId,
1041
+ folderId: base.folderId,
1042
+ maxFiles: base.maxFiles,
1043
+ concurrency,
1044
+ concurrencySpecified,
1045
+ flat,
1046
+ flatSpecified,
1047
+ to,
1048
+ toSpecified,
1049
+ timeoutSec,
1050
+ timeoutSpecified,
1051
+ mode,
1052
+ help
1053
+ };
1054
+ };
1055
+ var helpText = [
1056
+ "\u7FA4\u6587\u4EF6\u5BFC\u51FA\u7528\u6CD5\uFF1A",
1057
+ "- \u8BF7\u79C1\u804A\u53D1\u9001\uFF1A#\u5BFC\u51FA\u7FA4\u6587\u4EF6 <\u7FA4\u53F7> [\u53C2\u6570]",
1058
+ "- \u793A\u4F8B\uFF1A#\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456",
1059
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --no-url\uFF1A\u53EA\u5BFC\u51FA\u5217\u8868\uFF0C\u4E0D\u89E3\u6790URL",
1060
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --url-only\uFF1A\u4EC5\u8F93\u51FAURL\uFF08\u66F4\u65B9\u4FBF\u590D\u5236\uFF09",
1061
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --csv\uFF1A\u5BFC\u51FA\u4E3ACSV\uFF08\u9ED8\u8BA4JSON\uFF09",
1062
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --folder <id>\uFF1A\u4ECE\u6307\u5B9A\u6587\u4EF6\u5939\u5F00\u59CB\u5BFC\u51FA",
1063
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --max <n>\uFF1A\u6700\u591A\u5BFC\u51FAn\u6761\u6587\u4EF6\u8BB0\u5F55",
1064
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --concurrency <n>\uFF1A\u89E3\u6790URL\u5E76\u53D1\u6570\uFF08\u9ED8\u8BA43\uFF09",
1065
+ "- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --send-file\uFF1A\u5C1D\u8BD5\u53D1\u9001\u5BFC\u51FA\u6587\u4EF6\uFF08\u4F9D\u8D56\u534F\u8BAE\u7AEF\u652F\u6301\uFF09",
1066
+ "\u63D0\u793A\uFF1A\u4E0B\u8F7DURL\u901A\u5E38\u6709\u65F6\u6548\uFF0C\u8FC7\u671F\u540E\u9700\u91CD\u65B0\u5BFC\u51FA\u3002"
1067
+ ].join("\n");
1068
+ var exportGroupFiles = karin.command(/^#?(导出群文件|群文件导出)(.*)$/i, async (e) => {
1069
+ if (!e.isPrivate) return false;
1070
+ const argsText = e.msg.replace(/^#?(导出群文件|群文件导出)/i, "");
1071
+ const { groupId, format, withUrl, urlOnly, sendFile, folderId, maxFiles, concurrency, help } = parseArgs(argsText);
1072
+ if (help) {
1073
+ await e.reply(helpText);
1074
+ return true;
1075
+ }
1076
+ if (!groupId) {
1077
+ await e.reply(`\u7F3A\u5C11\u7FA4\u53F7\u53C2\u6570
1078
+
1079
+ ${helpText}`);
1080
+ return true;
1081
+ }
1082
+ const groupContact = karin.contactGroup(groupId);
1083
+ await e.reply(`\u5F00\u59CB\u5BFC\u51FA\u7FA4\u6587\u4EF6\u5217\u8868\uFF0C\u8BF7\u7A0D\u5019...
1084
+ - \u7FA4\u53F7\uFF1A${groupId}
1085
+ - \u683C\u5F0F\uFF1A${format}
1086
+ - \u5305\u542BURL\uFF1A${withUrl ? "\u662F" : "\u5426"}`);
1087
+ const errors = [];
1088
+ let list = [];
1089
+ try {
1090
+ list = await collectAllGroupFiles(e.bot, groupId, folderId);
1091
+ } catch (error) {
1092
+ logger.error(error);
1093
+ await e.reply(`\u5BFC\u51FA\u5931\u8D25\uFF1A${formatErrorMessage(error)}`);
1094
+ return true;
1095
+ }
1096
+ const limitedList = typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0 ? list.slice(0, Math.floor(maxFiles)) : list;
1097
+ if (withUrl) {
1098
+ const urlConcurrency = typeof concurrency === "number" && Number.isFinite(concurrency) && concurrency > 0 ? concurrency : 3;
1099
+ await runWithConcurrency(limitedList, urlConcurrency, async (item) => {
1100
+ try {
1101
+ item.url = await resolveGroupFileUrl(e.bot, groupContact, groupId, item);
1102
+ } catch (error) {
1103
+ errors.push({
1104
+ fileId: item.fileId,
1105
+ path: item.path,
1106
+ message: formatErrorMessage(error)
1107
+ });
1108
+ }
1109
+ });
1110
+ }
1111
+ const exportDir = path.join(dir.karinPath, "data", "group-files-export");
1112
+ const exportName = `group-files-${groupId}-${time("YYYYMMDD-HHmmss")}.${format}`;
1113
+ const exportPath = path.join(exportDir, exportName);
1114
+ const payload = {
1115
+ type: "group-files-export",
1116
+ plugin: { name: dir.name, version: dir.version },
1117
+ groupId,
1118
+ folderId: folderId ?? null,
1119
+ exportedAt: time(),
1120
+ withUrl,
1121
+ fileCount: limitedList.length,
1122
+ errors,
1123
+ files: limitedList
1124
+ };
1125
+ try {
1126
+ writeExportFile(format, exportPath, payload, limitedList);
1127
+ } catch (error) {
1128
+ logger.error(error);
1129
+ await e.reply(`\u5199\u5165\u5BFC\u51FA\u6587\u4EF6\u5931\u8D25\uFF1A${formatErrorMessage(error)}`);
1130
+ return true;
1131
+ }
1132
+ const urlOk = withUrl ? limitedList.filter((v) => typeof v.url === "string" && v.url).length : 0;
1133
+ const urlFail = withUrl ? limitedList.length - urlOk : 0;
1134
+ const summary = [
1135
+ `\u5BFC\u51FA\u5B8C\u6210\uFF1A${limitedList.length} \u4E2A\u6587\u4EF6`,
1136
+ withUrl ? `URL\uFF1A\u6210\u529F ${urlOk} / \u5931\u8D25 ${urlFail}` : null,
1137
+ `\u5BFC\u51FA\u6587\u4EF6\uFF1A${exportName}`,
1138
+ `\u5BFC\u51FA\u6587\u4EF6\u5DF2\u4FDD\u5B58\u81F3\uFF1A${exportPath}`
1139
+ ].filter(Boolean).join("\n");
1140
+ await e.reply(summary);
1141
+ const textMax = 200;
1142
+ const preview = limitedList.slice(0, textMax);
1143
+ const errorByFileId = /* @__PURE__ */ new Map();
1144
+ for (const err of errors) {
1145
+ if (err.fileId && err.message && !errorByFileId.has(err.fileId)) errorByFileId.set(err.fileId, err.message);
1146
+ }
1147
+ const compactError = (message) => message.replace(/\s+/g, " ").slice(0, 120);
1148
+ const lines = preview.map((item, index) => {
1149
+ if (!withUrl) return `${index + 1}. ${item.path} ${item.fileId}`;
1150
+ if (urlOnly) return item.url ? item.url : `(\u83B7\u53D6URL\u5931\u8D25) ${item.path} (${item.fileId})`;
1151
+ if (item.url) return `${index + 1}. ${item.path}
1152
+ ${item.url}`;
1153
+ const errMsg = errorByFileId.get(item.fileId);
1154
+ return `${index + 1}. ${item.path}
1155
+ (\u83B7\u53D6URL\u5931\u8D25) fileId=${item.fileId}${errMsg ? `
1156
+ \u539F\u56E0\uFF1A${compactError(errMsg)}` : ""}`;
1157
+ });
1158
+ const chunks = [];
1159
+ const maxChunkLen = 1500;
1160
+ let buf = "";
1161
+ for (const line of lines) {
1162
+ const next = buf ? `${buf}
1163
+
1164
+ ${line}` : line;
1165
+ if (next.length > maxChunkLen) {
1166
+ if (buf) chunks.push(buf);
1167
+ buf = line;
1168
+ } else {
1169
+ buf = next;
1170
+ }
1171
+ }
1172
+ if (buf) chunks.push(buf);
1173
+ const maxMessages = 10;
1174
+ for (const chunk of chunks.slice(0, maxMessages)) {
1175
+ await e.reply(chunk);
1176
+ }
1177
+ if (limitedList.length > preview.length) {
1178
+ await e.reply(`\uFF08\u5DF2\u7701\u7565 ${limitedList.length - preview.length} \u6761\uFF0C\u53EF\u4F7F\u7528 --max \u8C03\u6574\uFF09`);
1179
+ } else if (chunks.length > maxMessages) {
1180
+ await e.reply(`\uFF08\u6D88\u606F\u8FC7\u957F\uFF0C\u5DF2\u7701\u7565\u540E\u7EED\u5185\u5BB9\uFF1B\u53EF\u4F7F\u7528 --max \u51CF\u5C11\u6761\u6570\uFF09`);
1181
+ }
1182
+ if (sendFile && typeof e.bot?.uploadFile === "function") {
1183
+ const candidates = buildUploadFileCandidates(exportPath);
1184
+ for (const fileParam of candidates) {
1185
+ try {
1186
+ await e.bot.uploadFile(e.contact, fileParam, exportName);
1187
+ break;
1188
+ } catch {
1189
+ }
1190
+ }
1191
+ }
1192
+ return true;
1193
+ }, {
1194
+ priority: 9999,
1195
+ log: true,
1196
+ name: "\u5BFC\u51FA\u7FA4\u6587\u4EF6",
1197
+ permission: "all"
1198
+ });
1199
+ var syncHelpText = [
1200
+ "\u7FA4\u6587\u4EF6\u540C\u6B65\u5230 OpenList \u7528\u6CD5\uFF1A",
1201
+ "- \u79C1\u804A\uFF1A#\u540C\u6B65\u7FA4\u6587\u4EF6 <\u7FA4\u53F7> [\u53C2\u6570]",
1202
+ "- \u6CE8\u610F\uFF1A\u9ED8\u8BA4\u4EC5\u79C1\u804A\u54CD\u5E94\uFF08\u7FA4\u804A\u4E0D\u4F1A\u89E6\u53D1\u8BE5\u6307\u4EE4\uFF09",
1203
+ "- \u793A\u4F8B\uFF1A#\u540C\u6B65\u7FA4\u6587\u4EF6 123456",
1204
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --to /\u76EE\u6807\u76EE\u5F55\uFF1A\u4E0A\u4F20\u5230\u6307\u5B9A\u76EE\u5F55\uFF08\u9ED8\u8BA4\u4F7F\u7528\u914D\u7F6E openlistTargetDir\uFF09",
1205
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --flat\uFF1A\u4E0D\u4FDD\u7559\u7FA4\u6587\u4EF6\u5939\u7ED3\u6784\uFF0C\u5168\u90E8\u5E73\u94FA\u5230\u76EE\u6807\u76EE\u5F55",
1206
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --keep\uFF1A\u5F3A\u5236\u4FDD\u7559\u76EE\u5F55\u7ED3\u6784\uFF08\u8986\u76D6\u7FA4\u914D\u7F6E flat\uFF09",
1207
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --folder <id>\uFF1A\u4ECE\u6307\u5B9A\u6587\u4EF6\u5939\u5F00\u59CB",
1208
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --max <n>\uFF1A\u6700\u591A\u5904\u7406 n \u4E2A\u6587\u4EF6",
1209
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --concurrency <n>\uFF1A\u5E76\u53D1\u6570\uFF08\u4F1A\u8986\u76D6\u7FA4\u914D\u7F6E\u7684\u5E76\u53D1\uFF09",
1210
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --timeout <sec>\uFF1A\u5355\u6587\u4EF6\u8D85\u65F6\u79D2\u6570\uFF08\u4EC5\u5F71\u54CD\u5355\u4E2A\u6587\u4EF6\uFF09",
1211
+ "- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --full/--inc\uFF1A\u8986\u76D6\u7FA4\u914D\u7F6E\u7684\u540C\u6B65\u6A21\u5F0F\uFF08\u5168\u91CF/\u589E\u91CF\uFF09",
1212
+ "\u524D\u7F6E\uFF1A\u8BF7\u5148\u5728\u914D\u7F6E\u6587\u4EF6\u586B\u5199 openlistBaseUrl/openlistUsername/openlistPassword\u3002"
1213
+ ].join("\n");
1214
+ var syncGroupFilesToOpenListCore = async (params) => {
1215
+ const {
1216
+ bot,
1217
+ groupId,
1218
+ folderId,
1219
+ maxFiles,
1220
+ flat,
1221
+ targetDir,
1222
+ mode,
1223
+ urlConcurrency,
1224
+ transferConcurrency,
1225
+ fileTimeoutSec,
1226
+ retryTimes,
1227
+ retryDelayMs,
1228
+ progressReportEvery,
1229
+ downloadLimitKbps,
1230
+ uploadLimitKbps,
1231
+ report
1232
+ } = params;
1233
+ const cfg = config();
1234
+ const baseUrl = String(cfg.openlistBaseUrl ?? "").trim();
1235
+ const username = String(cfg.openlistUsername ?? "").trim();
1236
+ const password = String(cfg.openlistPassword ?? "").trim();
1237
+ const defaultTargetDir = String(cfg.openlistTargetDir ?? "/").trim();
1238
+ if (!baseUrl || !username || !password) {
1239
+ throw new Error([
1240
+ "\u8BF7\u5148\u914D\u7F6E OpenList \u4FE1\u606F\uFF08openlistBaseUrl/openlistUsername/openlistPassword\uFF09",
1241
+ `\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E\uFF1A${dir.ConfigDir}/config.json`
1242
+ ].join("\n"));
1243
+ }
1244
+ const davBaseUrl = buildOpenListDavBaseUrl(baseUrl);
1245
+ if (!davBaseUrl) throw new Error("OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl");
1246
+ const finalTargetDir = normalizePosixPath(targetDir || defaultTargetDir);
1247
+ const auth = buildOpenListAuthHeader(username, password);
1248
+ const safeFileTimeoutSec = Math.min(
1249
+ MAX_FILE_TIMEOUT_SEC,
1250
+ Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeoutSec || 0))
1251
+ );
1252
+ const safeProgressReportEvery = Math.max(
1253
+ 0,
1254
+ Math.floor(typeof progressReportEvery === "number" ? progressReportEvery : DEFAULT_PROGRESS_REPORT_EVERY)
1255
+ );
1256
+ const rateDown = Math.max(0, Math.floor(typeof downloadLimitKbps === "number" ? downloadLimitKbps : 0));
1257
+ const rateUp = Math.max(0, Math.floor(typeof uploadLimitKbps === "number" ? uploadLimitKbps : 0));
1258
+ const effectiveRateLimitBytesPerSec = (() => {
1259
+ const a = rateDown > 0 ? rateDown * 1024 : 0;
1260
+ const b = rateUp > 0 ? rateUp * 1024 : 0;
1261
+ if (a > 0 && b > 0) return Math.min(a, b);
1262
+ return a > 0 ? a : b > 0 ? b : 0;
1263
+ })();
1264
+ const transferTimeoutMs = safeFileTimeoutSec * 1e3;
1265
+ const webdavTimeoutMs = 15e3;
1266
+ const dirEnsurer = createWebDavDirEnsurer(davBaseUrl, auth, webdavTimeoutMs);
1267
+ const groupContact = karin.contactGroup(groupId);
1268
+ const state = readGroupSyncState(groupId);
1269
+ return await withGroupSyncLock(groupId, async () => {
1270
+ report && await report([
1271
+ "\u5F00\u59CB\u540C\u6B65\u7FA4\u6587\u4EF6\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019...",
1272
+ `- \u7FA4\u53F7\uFF1A${groupId}`,
1273
+ `- \u76EE\u6807\u76EE\u5F55\uFF1A${finalTargetDir}`,
1274
+ `- \u6A21\u5F0F\uFF1A${mode === "incremental" ? "\u589E\u91CF" : "\u5168\u91CF"}`,
1275
+ `- \u4FDD\u7559\u76EE\u5F55\u7ED3\u6784\uFF1A${flat ? "\u5426" : "\u662F"}`,
1276
+ `- \u5E76\u53D1\uFF1AURL ${urlConcurrency} / \u4F20\u8F93 ${transferConcurrency}`
1277
+ ].join("\n"));
1278
+ let list = [];
1279
+ list = await collectAllGroupFiles(bot, groupId, folderId);
1280
+ const limitedList = typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0 ? list.slice(0, Math.floor(maxFiles)) : list;
1281
+ const candidates = limitedList.map((item) => {
1282
+ const remotePath = buildRemotePathForItem(item, finalTargetDir, flat);
1283
+ return { item, remotePath };
1284
+ });
1285
+ let skipped = 0;
1286
+ let needSync = mode === "incremental" ? candidates.filter(({ item, remotePath }) => {
1287
+ const prev = state.files[remotePath];
1288
+ const ok = isSameSyncedFile(prev, item);
1289
+ if (ok) skipped++;
1290
+ return !ok;
1291
+ }) : candidates;
1292
+ if (mode === "incremental" && needSync.length) {
1293
+ const dirs = Array.from(new Set(
1294
+ needSync.map(({ remotePath }) => normalizePosixPath(path.posix.dirname(remotePath)))
1295
+ ));
1296
+ const namesByDir = /* @__PURE__ */ new Map();
1297
+ await runWithConcurrency(dirs, 3, async (dirPath) => {
1298
+ const names = await webdavPropfindListNames({
1299
+ davBaseUrl,
1300
+ auth,
1301
+ dirPath,
1302
+ timeoutMs: webdavTimeoutMs
1303
+ });
1304
+ namesByDir.set(dirPath, names);
1305
+ });
1306
+ needSync = needSync.filter(({ remotePath }) => {
1307
+ const dirPath = normalizePosixPath(path.posix.dirname(remotePath));
1308
+ const base = path.posix.basename(remotePath);
1309
+ const names = namesByDir.get(dirPath);
1310
+ if (names && names.has(base)) {
1311
+ skipped++;
1312
+ return false;
1313
+ }
1314
+ return true;
1315
+ });
1316
+ }
1317
+ if (!needSync.length) {
1318
+ report && await report(`\u6CA1\u6709\u9700\u8981\u540C\u6B65\u7684\u6587\u4EF6\uFF08\u589E\u91CF\u6A21\u5F0F\u5DF2\u8DF3\u8FC7 ${skipped} \u6761\uFF09\u3002`);
1319
+ return { total: limitedList.length, skipped, urlOk: 0, ok: 0, fail: 0 };
1320
+ }
1321
+ const urlErrors = [];
1322
+ await runWithConcurrency(needSync, Math.max(1, Math.floor(urlConcurrency) || 1), async ({ item }) => {
1323
+ try {
1324
+ item.url = await resolveGroupFileUrl(bot, groupContact, groupId, item);
1325
+ } catch (error) {
1326
+ urlErrors.push({ fileId: item.fileId, path: item.path, message: formatErrorMessage(error) });
1327
+ }
1328
+ });
1329
+ const withUrl = needSync.filter(({ item }) => typeof item.url === "string" && item.url).map((v) => v);
1330
+ report && await report(`URL\u83B7\u53D6\u5B8C\u6210\uFF1A\u6210\u529F ${withUrl.length} / \u5931\u8D25 ${needSync.length - withUrl.length}\uFF08\u589E\u91CF\u8DF3\u8FC7 ${skipped}\uFF09`);
1331
+ if (!withUrl.length) {
1332
+ report && await report("\u6CA1\u6709\u53EF\u7528\u7684\u4E0B\u8F7DURL\uFF0C\u65E0\u6CD5\u540C\u6B65\u5230 OpenList");
1333
+ return { total: limitedList.length, skipped, urlOk: 0, ok: 0, fail: 0 };
1334
+ }
1335
+ const syncErrors = [];
1336
+ let okCount = 0;
1337
+ const shouldRefreshUrl = (message) => {
1338
+ return /403|URL已过期|url已过期|url可能已失效|需要重新获取|下载超时/.test(message);
1339
+ };
1340
+ const transferOne = async (sourceUrl, targetUrl) => {
1341
+ await downloadAndUploadByWebDav({
1342
+ sourceUrl,
1343
+ targetUrl,
1344
+ auth,
1345
+ timeoutMs: transferTimeoutMs,
1346
+ rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
1347
+ });
1348
+ };
1349
+ report && await report("\u5F00\u59CB\u4E0B\u8F7D\u5E76\u4E0A\u4F20\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019...");
1350
+ const transferInitial = Math.min(MAX_TRANSFER_CONCURRENCY, Math.max(1, Math.floor(transferConcurrency) || 1));
1351
+ const adaptiveTransfer = effectiveRateLimitBytesPerSec <= 0;
1352
+ const transferFn = async ({ item, remotePath }, index) => {
1353
+ logger.info(`[\u7FA4\u6587\u4EF6\u540C\u6B65][${groupId}] \u540C\u6B65\u4E2D (${index + 1}/${withUrl.length}): ${item.path}`);
1354
+ const remoteDir = normalizePosixPath(path.posix.dirname(remotePath));
1355
+ const targetUrl = `${davBaseUrl}${encodePathForUrl(remotePath)}`;
1356
+ let lastError;
1357
+ let succeeded = false;
1358
+ const attempts = Math.max(0, Math.floor(retryTimes) || 0) + 1;
1359
+ for (let attempt = 1; attempt <= attempts; attempt++) {
1360
+ try {
1361
+ await dirEnsurer.ensureDir(remoteDir);
1362
+ const currentUrl = item.url;
1363
+ if (!currentUrl) throw new Error("\u7F3A\u5C11\u4E0B\u8F7DURL");
1364
+ await transferOne(currentUrl, targetUrl);
1365
+ okCount++;
1366
+ succeeded = true;
1367
+ lastError = void 0;
1368
+ state.files[remotePath] = {
1369
+ fileId: item.fileId,
1370
+ size: item.size,
1371
+ uploadTime: item.uploadTime,
1372
+ md5: item.md5,
1373
+ sha1: item.sha1,
1374
+ syncedAt: Date.now()
1375
+ };
1376
+ break;
1377
+ } catch (error) {
1378
+ lastError = error;
1379
+ const msg = formatErrorMessage(error);
1380
+ if (attempt < attempts) {
1381
+ if (shouldRefreshUrl(msg)) {
1382
+ try {
1383
+ item.url = await resolveGroupFileUrl(bot, groupContact, groupId, item);
1384
+ } catch {
1385
+ }
1386
+ }
1387
+ const delay = Math.max(0, Math.floor(retryDelayMs) || 0) * Math.pow(2, attempt - 1);
1388
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
1389
+ continue;
1390
+ }
1391
+ }
1392
+ }
1393
+ if (!succeeded && lastError) {
1394
+ syncErrors.push({
1395
+ path: item.path,
1396
+ fileId: item.fileId,
1397
+ message: formatErrorMessage(lastError)
1398
+ });
1399
+ }
1400
+ if (safeProgressReportEvery > 0 && (index + 1) % safeProgressReportEvery === 0) {
1401
+ report && await report(`\u540C\u6B65\u8FDB\u5EA6\uFF1A${index + 1}/${withUrl.length}\uFF08\u6210\u529F ${okCount}\uFF09`);
1402
+ }
1403
+ };
1404
+ if (adaptiveTransfer) {
1405
+ logger.info(`[\u7FA4\u6587\u4EF6\u540C\u6B65][${groupId}] \u672A\u914D\u7F6E\u9650\u901F\uFF0C\u5C06\u81EA\u9002\u5E94\u8C03\u6574\u4F20\u8F93\u5E76\u53D1\uFF08\u6700\u5927 ${MAX_TRANSFER_CONCURRENCY}\uFF09`);
1406
+ await runWithAdaptiveConcurrency(withUrl, {
1407
+ initial: transferInitial,
1408
+ max: MAX_TRANSFER_CONCURRENCY,
1409
+ fn: transferFn,
1410
+ onAdjust: (current, reason) => {
1411
+ logger.info(`[\u7FA4\u6587\u4EF6\u540C\u6B65][${groupId}] \u81EA\u9002\u5E94\u8C03\u6574\u4F20\u8F93\u5E76\u53D1=${current} (${reason})`);
1412
+ }
1413
+ });
1414
+ } else {
1415
+ await runWithConcurrency(withUrl, transferInitial, transferFn);
1416
+ }
1417
+ state.lastSyncAt = Date.now();
1418
+ writeGroupSyncState(groupId, state);
1419
+ const failCount = withUrl.length - okCount;
1420
+ report && await report(`\u540C\u6B65\u5B8C\u6210\uFF1A\u6210\u529F ${okCount} / \u5931\u8D25 ${failCount}\uFF08\u589E\u91CF\u8DF3\u8FC7 ${skipped}\uFF09`);
1421
+ if (failCount) {
1422
+ const preview = syncErrors.slice(0, 5).map((it) => `${it.path} (${it.fileId})
1423
+ ${it.message}`).join("\n\n");
1424
+ report && await report(`\u5931\u8D25\u793A\u4F8B\uFF08\u524D5\u6761\uFF09\uFF1A
1425
+ ${preview}`);
1426
+ }
1427
+ if (urlErrors.length) {
1428
+ const preview = urlErrors.slice(0, 5).map((it) => `${it.path ?? ""} (${it.fileId ?? ""})
1429
+ ${it.message}`).join("\n\n");
1430
+ report && await report(`URL\u83B7\u53D6\u5931\u8D25\u793A\u4F8B\uFF08\u524D5\u6761\uFF09\uFF1A
1431
+ ${preview}`);
1432
+ }
1433
+ return { total: limitedList.length, skipped, urlOk: withUrl.length, ok: okCount, fail: failCount };
1434
+ });
1435
+ };
1436
+ var syncGroupFilesToOpenList = karin.command(/^#?(同步群文件|群文件同步)(.*)$/i, async (e) => {
1437
+ if (!e.isPrivate) return false;
1438
+ const argsText = e.msg.replace(/^#?(同步群文件|群文件同步)/i, "");
1439
+ const {
1440
+ groupId: parsedGroupId,
1441
+ folderId: parsedFolderId,
1442
+ maxFiles: parsedMaxFiles,
1443
+ concurrency,
1444
+ concurrencySpecified,
1445
+ flat,
1446
+ flatSpecified,
1447
+ to,
1448
+ toSpecified,
1449
+ timeoutSec,
1450
+ timeoutSpecified,
1451
+ mode: forcedMode,
1452
+ help
1453
+ } = parseSyncArgs(argsText);
1454
+ if (help) {
1455
+ await e.reply(syncHelpText);
1456
+ return true;
1457
+ }
1458
+ const cfg = config();
1459
+ const groupId = parsedGroupId ?? (e.isGroup ? e.groupId : void 0);
1460
+ if (!groupId) {
1461
+ await e.reply(`\u7F3A\u5C11\u7FA4\u53F7\u53C2\u6570
1462
+
1463
+ ${syncHelpText}`);
1464
+ return true;
1465
+ }
1466
+ const defaults = cfg.groupSyncDefaults ?? {};
1467
+ const targetCfg = getGroupSyncTarget(cfg, groupId);
1468
+ const mode = forcedMode ?? (targetCfg ? normalizeSyncMode(targetCfg?.mode, normalizeSyncMode(defaults?.mode, "incremental")) : "full");
1469
+ const urlC = concurrencySpecified ? typeof concurrency === "number" ? concurrency : 3 : typeof targetCfg?.urlConcurrency === "number" ? targetCfg.urlConcurrency : typeof defaults?.urlConcurrency === "number" ? defaults.urlConcurrency : 3;
1470
+ const transferC = concurrencySpecified ? typeof concurrency === "number" ? concurrency : 3 : typeof targetCfg?.transferConcurrency === "number" ? targetCfg.transferConcurrency : typeof defaults?.transferConcurrency === "number" ? defaults.transferConcurrency : 3;
1471
+ const fileTimeout = timeoutSpecified ? typeof timeoutSec === "number" ? timeoutSec : 600 : typeof targetCfg?.fileTimeoutSec === "number" ? targetCfg.fileTimeoutSec : typeof defaults?.fileTimeoutSec === "number" ? defaults.fileTimeoutSec : 600;
1472
+ const retryTimes = typeof targetCfg?.retryTimes === "number" ? targetCfg.retryTimes : typeof defaults?.retryTimes === "number" ? defaults.retryTimes : 2;
1473
+ const retryDelayMs = typeof targetCfg?.retryDelayMs === "number" ? targetCfg.retryDelayMs : typeof defaults?.retryDelayMs === "number" ? defaults.retryDelayMs : 1500;
1474
+ const progressEvery = typeof targetCfg?.progressReportEvery === "number" ? targetCfg.progressReportEvery : typeof defaults?.progressReportEvery === "number" ? defaults.progressReportEvery : DEFAULT_PROGRESS_REPORT_EVERY;
1475
+ const downloadLimitKbps = typeof targetCfg?.downloadLimitKbps === "number" ? targetCfg.downloadLimitKbps : typeof defaults?.downloadLimitKbps === "number" ? defaults.downloadLimitKbps : 0;
1476
+ const uploadLimitKbps = typeof targetCfg?.uploadLimitKbps === "number" ? targetCfg.uploadLimitKbps : typeof defaults?.uploadLimitKbps === "number" ? defaults.uploadLimitKbps : 0;
1477
+ const targetDir = normalizePosixPath(
1478
+ toSpecified ? to ?? "" : String(targetCfg?.targetDir ?? "").trim() || path.posix.join(String(cfg.openlistTargetDir ?? "/"), String(groupId))
1479
+ );
1480
+ const finalFlat = flatSpecified ? Boolean(flat) : typeof targetCfg?.flat === "boolean" ? targetCfg.flat : Boolean(defaults?.flat ?? false);
1481
+ const folderId = parsedFolderId ?? targetCfg?.sourceFolderId;
1482
+ const maxFiles = typeof parsedMaxFiles === "number" ? parsedMaxFiles : targetCfg?.maxFiles;
1483
+ try {
1484
+ await syncGroupFilesToOpenListCore({
1485
+ bot: e.bot,
1486
+ groupId,
1487
+ folderId,
1488
+ maxFiles,
1489
+ flat: Boolean(finalFlat),
1490
+ targetDir,
1491
+ mode,
1492
+ urlConcurrency: Math.max(1, Math.floor(urlC) || 1),
1493
+ transferConcurrency: Math.max(1, Math.floor(transferC) || 1),
1494
+ fileTimeoutSec: Math.min(MAX_FILE_TIMEOUT_SEC, Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeout) || MIN_FILE_TIMEOUT_SEC)),
1495
+ retryTimes: Math.max(0, Math.floor(retryTimes) || 0),
1496
+ retryDelayMs: Math.max(0, Math.floor(retryDelayMs) || 0),
1497
+ progressReportEvery: Math.max(0, Math.floor(progressEvery) || 0),
1498
+ downloadLimitKbps: Math.max(0, Math.floor(downloadLimitKbps) || 0),
1499
+ uploadLimitKbps: Math.max(0, Math.floor(uploadLimitKbps) || 0),
1500
+ report: (msg) => e.reply(msg)
1501
+ });
1502
+ } catch (error) {
1503
+ logger.error(error);
1504
+ await e.reply(formatErrorMessage(error));
1505
+ return true;
1506
+ }
1507
+ return true;
1508
+ }, {
1509
+ priority: 9999,
1510
+ log: true,
1511
+ name: "\u540C\u6B65\u7FA4\u6587\u4EF6\u5230OpenList",
1512
+ permission: "all"
1513
+ });
1514
+ var webdavPropfindListEntries = async (params) => {
1515
+ const { davBaseUrl, auth, dirPath, timeoutMs } = params;
1516
+ const controller = new AbortController();
1517
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1518
+ try {
1519
+ const url = `${davBaseUrl}${encodePathForUrl(dirPath)}`;
1520
+ const requestPath = encodePathForUrl(dirPath).replace(/\/+$/, "");
1521
+ const body = `<?xml version="1.0" encoding="utf-8" ?>
1522
+ <d:propfind xmlns:d="DAV:">
1523
+ <d:prop>
1524
+ <d:displayname />
1525
+ <d:resourcetype />
1526
+ </d:prop>
1527
+ </d:propfind>`;
1528
+ const headers = {
1529
+ Depth: "1",
1530
+ "Content-Type": "application/xml; charset=utf-8"
1531
+ };
1532
+ const authHeader = String(auth ?? "").trim();
1533
+ if (authHeader) headers.Authorization = authHeader;
1534
+ const res = await fetch(url, {
1535
+ method: "PROPFIND",
1536
+ headers,
1537
+ body,
1538
+ redirect: "follow",
1539
+ signal: controller.signal
1540
+ });
1541
+ if (!res.ok) return [];
1542
+ const text = await res.text();
1543
+ if (!text) return [];
1544
+ const out = [];
1545
+ const responseRegex = /<d:response\b[\s\S]*?<\/d:response>/gi;
1546
+ let match;
1547
+ while (match = responseRegex.exec(text)) {
1548
+ const block = match[0] ?? "";
1549
+ const hrefMatch = block.match(/<d:href>([^<]+)<\/d:href>/i);
1550
+ if (!hrefMatch) continue;
1551
+ const href = hrefMatch[1] ?? "";
1552
+ const decoded = decodeURIComponent(href);
1553
+ const cleaned = decoded.replace(/\/+$/, "");
1554
+ if (requestPath && cleaned.endsWith(requestPath)) continue;
1555
+ const name = cleaned.split("/").filter(Boolean).pop();
1556
+ if (!name) continue;
1557
+ const isDir = /<d:collection\b/i.test(block) || /\/$/.test(decoded);
1558
+ out.push({ name, isDir });
1559
+ }
1560
+ const selfBase = normalizePosixPath(dirPath).split("/").filter(Boolean).pop();
1561
+ const unique = /* @__PURE__ */ new Map();
1562
+ for (const it of out) {
1563
+ if (selfBase && it.name === selfBase) continue;
1564
+ unique.set(`${it.name}::${it.isDir ? "d" : "f"}`, it);
1565
+ }
1566
+ return [...unique.values()];
1567
+ } catch {
1568
+ return [];
1569
+ } finally {
1570
+ clearTimeout(timer);
1571
+ }
1572
+ };
1573
+ var safeHostDirName = (baseUrl) => {
1574
+ try {
1575
+ const u = new URL(String(baseUrl));
1576
+ const host = u.hostname || String(baseUrl);
1577
+ const port = u.port ? `_${u.port}` : "";
1578
+ const raw = `${host}${port}`.replaceAll(".", "_").replaceAll(":", "_");
1579
+ return safePathSegment(raw);
1580
+ } catch {
1581
+ const raw = String(baseUrl ?? "").replace(/^https?:\/\//i, "").replaceAll(".", "_").replaceAll(":", "_");
1582
+ return safePathSegment(raw);
1583
+ }
1584
+ };
1585
+ var webdavHeadExists = async (params) => {
1586
+ const { davBaseUrl, auth, filePath, timeoutMs } = params;
1587
+ const controller = new AbortController();
1588
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1589
+ try {
1590
+ const url = `${davBaseUrl}${encodePathForUrl(filePath)}`;
1591
+ const res = await fetch(url, {
1592
+ method: "HEAD",
1593
+ headers: { Authorization: auth },
1594
+ redirect: "follow",
1595
+ signal: controller.signal
1596
+ });
1597
+ return res.ok;
1598
+ } catch {
1599
+ return false;
1600
+ } finally {
1601
+ clearTimeout(timer);
1602
+ }
1603
+ };
1604
+ var copyWebDavToWebDav = async (params) => {
1605
+ const { sourceDavBaseUrl, sourceAuth, sourcePath, targetDavBaseUrl, targetAuth, targetPath, timeoutMs } = params;
1606
+ const controller = new AbortController();
1607
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1608
+ try {
1609
+ const sourceUrl = `${sourceDavBaseUrl}${encodePathForUrl(sourcePath)}`;
1610
+ const targetUrl = `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`;
1611
+ let downloadRes;
1612
+ try {
1613
+ const downloadHeaders = {};
1614
+ const authHeader = String(sourceAuth ?? "").trim();
1615
+ if (authHeader) downloadHeaders.Authorization = authHeader;
1616
+ downloadRes = await fetch(sourceUrl, {
1617
+ method: "GET",
1618
+ headers: downloadHeaders,
1619
+ redirect: "follow",
1620
+ signal: controller.signal
1621
+ });
1622
+ } catch (error) {
1623
+ if (isAbortError(error)) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u8D85\u65F6");
1624
+ throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${formatErrorMessage(error)}`);
1625
+ }
1626
+ if (!downloadRes.ok) {
1627
+ const body = await fetchTextSafely(downloadRes);
1628
+ throw new Error(`\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${body ? ` - ${body}` : ""}`);
1629
+ }
1630
+ if (!downloadRes.body) throw new Error("\u6E90\u7AEF\u8BFB\u53D6\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
1631
+ const headers = { Authorization: targetAuth };
1632
+ const contentType = downloadRes.headers.get("content-type");
1633
+ const contentLength = downloadRes.headers.get("content-length");
1634
+ if (contentType) headers["Content-Type"] = contentType;
1635
+ if (contentLength) headers["Content-Length"] = contentLength;
1636
+ const sourceStream = Readable.fromWeb(downloadRes.body);
1637
+ let putRes;
1638
+ try {
1639
+ putRes = await fetch(targetUrl, {
1640
+ method: "PUT",
1641
+ headers,
1642
+ body: sourceStream,
1643
+ // @ts-expect-error Node fetch streaming body requires duplex
1644
+ duplex: "half",
1645
+ redirect: "follow",
1646
+ signal: controller.signal
1647
+ });
1648
+ } catch (error) {
1649
+ if (isAbortError(error)) throw new Error("\u76EE\u6807\u7AEF\u5199\u5165\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5\u5BF9\u7AEF OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
1650
+ throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${formatErrorMessage(error)}`);
1651
+ }
1652
+ if (!putRes.ok) {
1653
+ const body = await fetchTextSafely(putRes);
1654
+ throw new Error(`\u76EE\u6807\u7AEF\u5199\u5165\u5931\u8D25: ${putRes.status} ${putRes.statusText}${body ? ` - ${body}` : ""}`);
1655
+ }
1656
+ } finally {
1657
+ clearTimeout(timer);
1658
+ }
1659
+ };
1660
+ var parseBackupOpenListArgs = (text) => {
1661
+ const raw = text.trim();
1662
+ const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
1663
+ const help = /(^|\s)(--help|-h|help|\?)(\s|$)/i.test(raw);
1664
+ const first = tokens[0];
1665
+ const sourceBaseUrl = first && /^https?:\/\//i.test(first) ? first : void 0;
1666
+ const restRaw = sourceBaseUrl ? raw.slice(first.length).trim() : raw;
1667
+ const srcMatch = restRaw.match(/--src\s+(\S+)/i) ?? restRaw.match(/(^|\s)src=(\S+)/i);
1668
+ const srcDir = srcMatch ? srcMatch[srcMatch.length - 1] : void 0;
1669
+ const srcSpecified = Boolean(srcMatch);
1670
+ const toMatch = restRaw.match(/--to\s+(\S+)/i) ?? restRaw.match(/(^|\s)to=(\S+)/i);
1671
+ const toDir = toMatch ? toMatch[toMatch.length - 1] : void 0;
1672
+ const toSpecified = Boolean(toMatch);
1673
+ const maxMatch = restRaw.match(/--max\s+(\d+)/i) ?? restRaw.match(/(^|\s)max=(\d+)/i);
1674
+ const maxFiles = maxMatch ? Number(maxMatch[maxMatch.length - 1]) : void 0;
1675
+ const concurrencyMatch = restRaw.match(/--concurrency\s+(\d+)/i) ?? restRaw.match(/(^|\s)concurrency=(\d+)/i);
1676
+ const concurrency = concurrencyMatch ? Number(concurrencyMatch[concurrencyMatch.length - 1]) : void 0;
1677
+ const concurrencySpecified = Boolean(concurrencyMatch);
1678
+ const timeoutMatch = restRaw.match(/--timeout\s+(\d+)/i) ?? restRaw.match(/(^|\s)timeout=(\d+)/i);
1679
+ const timeoutSec = timeoutMatch ? Number(timeoutMatch[timeoutMatch.length - 1]) : void 0;
1680
+ const timeoutSpecified = Boolean(timeoutMatch);
1681
+ const scanMatch = restRaw.match(/--scan(?:-concurrency)?\s+(\d+)/i) ?? restRaw.match(/(^|\s)scan=(\d+)/i) ?? restRaw.match(/(^|\s)scanConcurrency=(\d+)/i) ?? restRaw.match(/(^|\s)scan_concurrency=(\d+)/i);
1682
+ const scanConcurrency = scanMatch ? Number(scanMatch[scanMatch.length - 1]) : void 0;
1683
+ const scanConcurrencySpecified = Boolean(scanMatch);
1684
+ const perPageMatch = restRaw.match(/--per-page\s+(\d+)/i) ?? restRaw.match(/--perpage\s+(\d+)/i) ?? restRaw.match(/--page-size\s+(\d+)/i) ?? restRaw.match(/(^|\s)per[_-]?page=(\d+)/i) ?? restRaw.match(/(^|\s)pageSize=(\d+)/i) ?? restRaw.match(/(^|\s)per_page=(\d+)/i);
1685
+ const perPage = perPageMatch ? Number(perPageMatch[perPageMatch.length - 1]) : void 0;
1686
+ const perPageSpecified = Boolean(perPageMatch);
1687
+ const modeFull = /(^|\s)(--full|full)(\s|$)/i.test(restRaw);
1688
+ const modeInc = /(^|\s)(--inc|--incremental|inc|incremental)(\s|$)/i.test(restRaw);
1689
+ const mode = modeFull ? "full" : modeInc ? "incremental" : void 0;
1690
+ const transportApi = /(^|\s)(--api)(\s|$)/i.test(restRaw);
1691
+ const transportWebDav = /(^|\s)(--webdav|--dav)(\s|$)/i.test(restRaw);
1692
+ const transportAuto = /(^|\s)(--auto)(\s|$)/i.test(restRaw);
1693
+ const transportSpecified = transportApi || transportWebDav || transportAuto;
1694
+ const transport = transportApi ? "api" : transportWebDav ? "webdav" : transportAuto ? "auto" : void 0;
1695
+ return {
1696
+ sourceBaseUrl,
1697
+ srcDir,
1698
+ srcSpecified,
1699
+ toDir,
1700
+ toSpecified,
1701
+ maxFiles,
1702
+ concurrency,
1703
+ concurrencySpecified,
1704
+ timeoutSec,
1705
+ timeoutSpecified,
1706
+ scanConcurrency,
1707
+ scanConcurrencySpecified,
1708
+ perPage,
1709
+ perPageSpecified,
1710
+ mode,
1711
+ transport,
1712
+ transportSpecified,
1713
+ help
1714
+ };
1715
+ };
1716
+ var openListToOpenListHelpText = [
1717
+ "OpenList -> OpenList \u5907\u4EFD\u7528\u6CD5\uFF1A",
1718
+ "- \u79C1\u804A\uFF1A#\u5907\u4EFDoplist [\u6E90OpenList\u5730\u5740] [\u53C2\u6570]",
1719
+ "- \u793A\u4F8B\uFF1A#\u5907\u4EFDoplist https://pan.example.com",
1720
+ "- #\u5907\u4EFDoplist https://pan.example.com --src / --to /backup --inc",
1721
+ "- #\u5907\u4EFDoplist https://pan.example.com --api",
1722
+ "- #\u5907\u4EFDoplist https://pan.example.com --webdav",
1723
+ "- #\u5907\u4EFDoplist https://pan.example.com --full --concurrency 3 --timeout 600",
1724
+ "- #\u5907\u4EFDoplist https://pan.example.com --scan 30 --per-page 2000",
1725
+ "\u63D0\u793A\uFF1A\u5907\u4EFD\u76EE\u7684\u7AEF\u4F7F\u7528 openlistBaseUrl/openlistUsername/openlistPassword\uFF08\u4E0E\u7FA4\u6587\u4EF6\u5907\u4EFD/\u540C\u6B65\u5171\u7528\uFF09\u3002",
1726
+ "\u63D0\u793A\uFF1A\u4F20\u8F93\u9ED8\u8BA4 auto\uFF08\u4E0B\u8F7D\u8D70 API\uFF0C\u4E0A\u4F20\u8D70 WebDAV\uFF1B\u5931\u8D25\u81EA\u52A8\u56DE\u9000\u5230\u53EF\u7528\u65B9\u5F0F\uFF09\u3002",
1727
+ "\u63D0\u793A\uFF1A--scan \u53EA\u5F71\u54CD\u201C\u83B7\u53D6\u6587\u4EF6\u5217\u8868\u201D\u7684\u5E76\u53D1\uFF1B--per-page \u53EA\u5F71\u54CD\u6E90\u7AEF\u5217\u8868\u8D70 API \u65F6\u7684\u6BCF\u9875\u6570\u91CF\uFF08\u503C\u8D8A\u5927\u8D8A\u5FEB\uFF0C\u4F46\u53EF\u80FD\u66F4\u5403\u5185\u5B58/\u66F4\u5BB9\u6613\u88AB\u9650\u6D41\uFF09\u3002",
1728
+ '\u8BF4\u660E\uFF1A\u4F1A\u5728\u76EE\u7684\u7AEF\u76EE\u6807\u76EE\u5F55\u4E0B\u521B\u5EFA\u5B50\u76EE\u5F55\uFF08\u6E90 OpenList \u57DF\u540D\uFF0C"." \u66FF\u6362\u4E3A "_"\uFF09\uFF0C\u5E76\u540C\u6B65\u5176\u4E0B\u6240\u6709\u6587\u4EF6\u3002'
1729
+ ].join("\n");
1730
+ var activeOpenListBackup = /* @__PURE__ */ new Set();
1731
+ var backupOpenListToOpenList = karin.command(/^#?备份oplist(.*)$/i, async (e) => {
1732
+ if (!e.isPrivate) return false;
1733
+ const argsText = e.msg.replace(/^#?备份oplist/i, "");
1734
+ const {
1735
+ sourceBaseUrl: sourceBaseUrlArg,
1736
+ srcDir,
1737
+ srcSpecified,
1738
+ toDir,
1739
+ toSpecified,
1740
+ maxFiles,
1741
+ concurrency,
1742
+ concurrencySpecified,
1743
+ timeoutSec,
1744
+ timeoutSpecified,
1745
+ scanConcurrency: scanConcurrencyArg,
1746
+ scanConcurrencySpecified,
1747
+ perPage: perPageArg,
1748
+ perPageSpecified,
1749
+ mode: forcedMode,
1750
+ transport: forcedTransport,
1751
+ transportSpecified,
1752
+ help
1753
+ } = parseBackupOpenListArgs(argsText);
1754
+ if (help) {
1755
+ await e.reply(openListToOpenListHelpText);
1756
+ return true;
1757
+ }
1758
+ const cfg = config();
1759
+ const sourceBaseUrl = String(sourceBaseUrlArg ?? "").trim();
1760
+ const targetBaseUrl = String(cfg.openlistBaseUrl ?? "").trim();
1761
+ const targetUsername = String(cfg.openlistUsername ?? "").trim();
1762
+ const targetPassword = String(cfg.openlistPassword ?? "").trim();
1763
+ if (!sourceBaseUrl) {
1764
+ await e.reply(openListToOpenListHelpText);
1765
+ return true;
1766
+ }
1767
+ if (!targetBaseUrl || !targetUsername || !targetPassword) {
1768
+ await e.reply("\u8BF7\u5148\u914D\u7F6E\u76EE\u7684\u7AEF OpenList \u4FE1\u606F\uFF08openlistBaseUrl/openlistUsername/openlistPassword\uFF09");
1769
+ return true;
1770
+ }
1771
+ const srcDavBaseUrl = buildOpenListDavBaseUrl(sourceBaseUrl);
1772
+ const targetDavBaseUrl = buildOpenListDavBaseUrl(targetBaseUrl);
1773
+ if (!srcDavBaseUrl) {
1774
+ await e.reply("\u6E90 OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5\u547D\u4EE4\u53C2\u6570");
1775
+ return true;
1776
+ }
1777
+ if (!targetDavBaseUrl) {
1778
+ await e.reply("\u76EE\u7684\u7AEF OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl");
1779
+ return true;
1780
+ }
1781
+ const srcAuth = "";
1782
+ const targetAuth = buildOpenListAuthHeader(targetUsername, targetPassword);
1783
+ const transport = forcedTransport ?? "auto";
1784
+ const mode = forcedMode ?? "incremental";
1785
+ const copyConcurrency = concurrencySpecified ? typeof concurrency === "number" ? concurrency : 3 : 3;
1786
+ const fileTimeout = timeoutSpecified ? typeof timeoutSec === "number" ? timeoutSec : 600 : 600;
1787
+ const normalizedSrcDir = normalizePosixPath(srcSpecified ? srcDir ?? "" : "/");
1788
+ const normalizedTargetBaseDir = normalizePosixPath(
1789
+ toSpecified ? toDir ?? "" : String(cfg.openlistTargetDir ?? "/").trim() || "/"
1790
+ );
1791
+ const targetRoot = normalizePosixPath(path.posix.join(normalizedTargetBaseDir, safeHostDirName(sourceBaseUrl)));
1792
+ const lockKey = `${sourceBaseUrl} -> ${targetBaseUrl}`;
1793
+ if (activeOpenListBackup.has(lockKey)) {
1794
+ await e.reply("\u5907\u4EFD\u4EFB\u52A1\u6B63\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
1795
+ return true;
1796
+ }
1797
+ let ticker;
1798
+ let tickChain = Promise.resolve();
1799
+ activeOpenListBackup.add(lockKey);
1800
+ try {
1801
+ await e.reply([
1802
+ "\u5F00\u59CB\u5907\u4EFD OpenList...",
1803
+ `\u6E90\uFF1A${sourceBaseUrl}`,
1804
+ `\u6E90\u76EE\u5F55\uFF1A${normalizedSrcDir}`,
1805
+ `\u76EE\u7684\u7AEF\uFF1A${targetBaseUrl}`,
1806
+ `\u76EE\u7684\u7AEF\u76EE\u5F55\uFF1A${targetRoot}`,
1807
+ `\u6A21\u5F0F\uFF1A${mode}`,
1808
+ `\u4F20\u8F93\uFF1A${transport}`
1809
+ ].join("\n"));
1810
+ const timeoutMs = Math.min(MAX_FILE_TIMEOUT_SEC, Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeout) || MIN_FILE_TIMEOUT_SEC)) * 1e3;
1811
+ const listTimeoutMs = Math.min(15e3, timeoutMs);
1812
+ const listPerPage = perPageSpecified ? Math.max(1, Math.min(5e3, Math.floor(perPageArg || 0) || 1e3)) : 1e3;
1813
+ const allowAutoFallback = transport === "auto";
1814
+ let sourceTransport = transport === "webdav" ? "webdav" : "api";
1815
+ let targetTransport = transport === "api" ? "api" : "webdav";
1816
+ let sourceToken;
1817
+ let targetToken;
1818
+ let targetTokenPromise;
1819
+ const getSourceToken = async () => {
1820
+ if (typeof sourceToken === "string") return sourceToken;
1821
+ sourceToken = "";
1822
+ return sourceToken;
1823
+ };
1824
+ const getTargetToken = async () => {
1825
+ if (typeof targetToken === "string") return targetToken;
1826
+ if (targetTokenPromise) return await targetTokenPromise;
1827
+ targetTokenPromise = openlistApiLogin({
1828
+ baseUrl: targetBaseUrl,
1829
+ username: targetUsername,
1830
+ password: targetPassword,
1831
+ timeoutMs: listTimeoutMs
1832
+ }).then((token) => {
1833
+ targetToken = token;
1834
+ return token;
1835
+ }).catch((error) => {
1836
+ targetTokenPromise = void 0;
1837
+ throw error;
1838
+ });
1839
+ return await targetTokenPromise;
1840
+ };
1841
+ const targetDirEnsurerWebDav = createWebDavDirEnsurer(targetDavBaseUrl, targetAuth, timeoutMs);
1842
+ let targetDirEnsurerApi;
1843
+ const getTargetDirEnsurerApi = async () => {
1844
+ if (targetDirEnsurerApi) return targetDirEnsurerApi;
1845
+ const token = await getTargetToken();
1846
+ targetDirEnsurerApi = createOpenListApiDirEnsurer(targetBaseUrl, token, timeoutMs);
1847
+ return targetDirEnsurerApi;
1848
+ };
1849
+ const ensureTargetDir = async (dirPath) => {
1850
+ let webdavError;
1851
+ if (targetTransport === "webdav") {
1852
+ try {
1853
+ await retryAsync(() => targetDirEnsurerWebDav.ensureDir(dirPath), { retries: 2, isRetryable: isRetryableWebDavError });
1854
+ return;
1855
+ } catch (error) {
1856
+ webdavError = error;
1857
+ if (!allowAutoFallback) throw error;
1858
+ targetTransport = "api";
1859
+ }
1860
+ }
1861
+ const ensurer = await getTargetDirEnsurerApi();
1862
+ try {
1863
+ await retryAsync(() => ensurer.ensureDir(dirPath), { retries: 2, isRetryable: isRetryableWebDavError });
1864
+ } catch (error) {
1865
+ if (webdavError) {
1866
+ throw new Error([
1867
+ `\u76EE\u6807\u7AEF WebDAV \u4E0D\u53EF\u7528\uFF0C\u5DF2\u56DE\u9000\u5230 OpenList API\uFF0C\u4F46\u4ECD\u5931\u8D25\u3002`,
1868
+ `\u76EE\u6807\uFF1A${targetBaseUrl}`,
1869
+ `\u76EE\u5F55\uFF1A${dirPath}`,
1870
+ `WebDAV: ${formatErrorMessage(webdavError)}`,
1871
+ `API: ${formatErrorMessage(error)}`,
1872
+ "\u8BF7\u68C0\u67E5 openlistUsername/openlistPassword \u662F\u5426\u6B63\u786E\uFF0C\u6216\u5728\u76EE\u7684\u7AEF\u542F\u7528 WebDAV \u7BA1\u7406/\u653E\u884C MKCOL/PUT\u3002"
1873
+ ].join("\n"));
1874
+ }
1875
+ throw error;
1876
+ }
1877
+ };
1878
+ const files = [];
1879
+ const visited = /* @__PURE__ */ new Set();
1880
+ const stack = [normalizedSrcDir];
1881
+ let scannedDirs = 0;
1882
+ const max = typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0 ? Math.floor(maxFiles) : 0;
1883
+ let ok = 0;
1884
+ let skipped = 0;
1885
+ let fail = 0;
1886
+ let phase = "scan";
1887
+ const startedAt = Date.now();
1888
+ ticker = setInterval(() => {
1889
+ const elapsed = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
1890
+ const msg = phase === "scan" ? `\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09
1891
+ \u9636\u6BB5\uFF1A\u626B\u63CF
1892
+ \u5DF2\u626B\u63CF\u76EE\u5F55\uFF1A${scannedDirs}
1893
+ \u5DF2\u53D1\u73B0\u6587\u4EF6\uFF1A${files.length}` : `\u5907\u4EFD\u8FDB\u884C\u4E2D\uFF08${elapsed}s\uFF09
1894
+ \u9636\u6BB5\uFF1A\u590D\u5236
1895
+ \u8FDB\u5EA6\uFF1A${ok + skipped + fail}/${files.length}
1896
+ \u6210\u529F ${ok} \u8DF3\u8FC7 ${skipped} \u5931\u8D25 ${fail}`;
1897
+ tickChain = tickChain.then(() => e.reply(msg)).catch(() => void 0);
1898
+ }, 6e4);
1899
+ const maxScanConcurrency = scanConcurrencySpecified ? Math.max(1, Math.min(200, Math.floor(scanConcurrencyArg || 0) || 1)) : 50;
1900
+ let scanConcurrency = scanConcurrencySpecified ? maxScanConcurrency : Math.max(1, Math.min(maxScanConcurrency, Math.max(8, (Math.floor(copyConcurrency) || 1) * 3)));
1901
+ const scanResults = [];
1902
+ const pushScanResult = (ok2, ms, reason) => {
1903
+ scanResults.push({ ok: ok2, ms, reason });
1904
+ if (scanResults.length > 20) scanResults.shift();
1905
+ if (scanResults.length < 10) return;
1906
+ if (scanResults.length % 5 !== 0) return;
1907
+ const failCount = scanResults.filter((r) => !r.ok).length;
1908
+ const failRate = failCount / scanResults.length;
1909
+ const avgMs = scanResults.reduce((acc, r) => acc + r.ms, 0) / scanResults.length;
1910
+ const hasTimeout = scanResults.some((r) => (r.reason || "").includes("\u8D85\u65F6"));
1911
+ if (hasTimeout || failRate >= 0.2) {
1912
+ if (scanConcurrency > 1) scanConcurrency -= 1;
1913
+ return;
1914
+ }
1915
+ if (failCount === 0 && scanConcurrency < maxScanConcurrency) {
1916
+ if (avgMs < 6e4 || scanResults.length === 20) scanConcurrency += 1;
1917
+ }
1918
+ };
1919
+ let stopScan = false;
1920
+ let scanError;
1921
+ const isRetryableListError = (error) => {
1922
+ const msg = formatErrorMessage(error);
1923
+ return /超时|timeout|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|UND_ERR|socket hang up|\b429\b|\b5\d\d\b/i.test(msg);
1924
+ };
1925
+ const listDirEntries = async (dirPath) => {
1926
+ return await retryAsync(async () => {
1927
+ return sourceTransport === "webdav" ? await webdavPropfindListEntries({
1928
+ davBaseUrl: srcDavBaseUrl,
1929
+ auth: srcAuth,
1930
+ dirPath,
1931
+ timeoutMs: listTimeoutMs
1932
+ }) : await openlistApiListEntries({
1933
+ baseUrl: sourceBaseUrl,
1934
+ token: await getSourceToken(),
1935
+ dirPath,
1936
+ timeoutMs: listTimeoutMs,
1937
+ perPage: listPerPage
1938
+ });
1939
+ }, { retries: 2, isRetryable: isRetryableListError });
1940
+ };
1941
+ let scanning = 0;
1942
+ let resolveScanDone;
1943
+ const scanDone = new Promise((resolve) => {
1944
+ resolveScanDone = resolve;
1945
+ });
1946
+ const scheduleScan = () => {
1947
+ while (!stopScan && scanning < scanConcurrency) {
1948
+ const rawPath = stack.pop();
1949
+ if (!rawPath) break;
1950
+ const current = normalizePosixPath(rawPath);
1951
+ if (visited.has(current)) continue;
1952
+ visited.add(current);
1953
+ scannedDirs++;
1954
+ scanning++;
1955
+ (async () => {
1956
+ const start = Date.now();
1957
+ try {
1958
+ const entries = await listDirEntries(current);
1959
+ pushScanResult(true, Date.now() - start);
1960
+ for (const entry of entries) {
1961
+ if (stopScan) break;
1962
+ const childPath = normalizePosixPath(path.posix.join(current, entry.name));
1963
+ if (entry.isDir) {
1964
+ if (!visited.has(childPath)) stack.push(childPath);
1965
+ continue;
1966
+ }
1967
+ files.push(childPath);
1968
+ if (max > 0 && files.length >= max) {
1969
+ stopScan = true;
1970
+ break;
1971
+ }
1972
+ }
1973
+ } catch (error) {
1974
+ pushScanResult(false, Date.now() - start, formatErrorMessage(error));
1975
+ throw error;
1976
+ }
1977
+ scheduleScan();
1978
+ })().catch((error) => {
1979
+ scanError = error;
1980
+ stopScan = true;
1981
+ }).finally(() => {
1982
+ scanning--;
1983
+ scheduleScan();
1984
+ if ((stopScan || stack.length === 0) && scanning === 0) resolveScanDone?.();
1985
+ });
1986
+ }
1987
+ if ((stopScan || stack.length === 0) && scanning === 0) resolveScanDone?.();
1988
+ };
1989
+ scheduleScan();
1990
+ await scanDone;
1991
+ if (scanError) throw scanError;
1992
+ if (!files.length) {
1993
+ await e.reply("\u672A\u53D1\u73B0\u53EF\u5907\u4EFD\u7684\u6587\u4EF6\uFF08\u6E90\u76EE\u5F55\u4E3A\u7A7A\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF09\u3002");
1994
+ return true;
1995
+ }
1996
+ phase = "copy";
1997
+ await ensureTargetDir(targetRoot);
1998
+ await e.reply(`\u5DF2\u53D1\u73B0 ${files.length} \u4E2A\u6587\u4EF6\uFF0C\u5F00\u59CB\u590D\u5236...`);
1999
+ const limit = Math.max(1, Math.min(MAX_TRANSFER_CONCURRENCY, Math.floor(copyConcurrency) || 1));
2000
+ await runWithConcurrency(files, limit, async (sourcePath, index) => {
2001
+ const rel = path.posix.relative(normalizedSrcDir, sourcePath);
2002
+ const relParts = rel.split("/").filter(Boolean).map(safePathSegment);
2003
+ const targetPath = normalizePosixPath(path.posix.join(targetRoot, ...relParts));
2004
+ const targetDirPath = normalizePosixPath(path.posix.dirname(targetPath));
2005
+ if (mode === "incremental") {
2006
+ const exists = targetTransport === "webdav" ? await webdavHeadExists({
2007
+ davBaseUrl: targetDavBaseUrl,
2008
+ auth: targetAuth,
2009
+ filePath: targetPath,
2010
+ timeoutMs: Math.max(5e3, Math.floor(timeoutMs / 5))
2011
+ }) : await openlistApiPathExists({
2012
+ baseUrl: targetBaseUrl,
2013
+ token: await getTargetToken(),
2014
+ path: targetPath,
2015
+ timeoutMs: Math.max(5e3, Math.floor(timeoutMs / 5))
2016
+ });
2017
+ if (exists) {
2018
+ skipped++;
2019
+ return;
2020
+ }
2021
+ }
2022
+ try {
2023
+ await ensureTargetDir(targetDirPath);
2024
+ if (sourceTransport === "webdav" && targetTransport === "webdav") {
2025
+ await copyWebDavToWebDav({
2026
+ sourceDavBaseUrl: srcDavBaseUrl,
2027
+ sourceAuth: srcAuth,
2028
+ sourcePath,
2029
+ targetDavBaseUrl,
2030
+ targetAuth,
2031
+ targetPath,
2032
+ timeoutMs
2033
+ });
2034
+ } else if (sourceTransport === "webdav" && targetTransport === "api") {
2035
+ await downloadAndUploadByOpenListApiPut({
2036
+ sourceUrl: `${srcDavBaseUrl}${encodePathForUrl(sourcePath)}`,
2037
+ sourceHeaders: srcAuth ? { Authorization: srcAuth } : void 0,
2038
+ targetBaseUrl,
2039
+ targetToken: await getTargetToken(),
2040
+ targetPath,
2041
+ timeoutMs
2042
+ });
2043
+ } else if (sourceTransport === "api" && targetTransport === "webdav") {
2044
+ const token = await getSourceToken();
2045
+ const rawUrl = await openlistApiGetRawUrl({
2046
+ baseUrl: sourceBaseUrl,
2047
+ token,
2048
+ filePath: sourcePath,
2049
+ timeoutMs: Math.max(5e3, Math.floor(timeoutMs / 5))
2050
+ });
2051
+ const sourceHeaders = buildOpenListRawUrlAuthHeaders({ rawUrl, baseUrl: sourceBaseUrl, token });
2052
+ await downloadAndUploadByWebDav({
2053
+ sourceUrl: rawUrl,
2054
+ sourceHeaders,
2055
+ targetUrl: `${targetDavBaseUrl}${encodePathForUrl(targetPath)}`,
2056
+ auth: targetAuth,
2057
+ timeoutMs
2058
+ });
2059
+ } else {
2060
+ const token = await getSourceToken();
2061
+ const rawUrl = await openlistApiGetRawUrl({
2062
+ baseUrl: sourceBaseUrl,
2063
+ token,
2064
+ filePath: sourcePath,
2065
+ timeoutMs: Math.max(5e3, Math.floor(timeoutMs / 5))
2066
+ });
2067
+ const sourceHeaders = buildOpenListRawUrlAuthHeaders({ rawUrl, baseUrl: sourceBaseUrl, token });
2068
+ await downloadAndUploadByOpenListApiPut({
2069
+ sourceUrl: rawUrl,
2070
+ sourceHeaders,
2071
+ targetBaseUrl,
2072
+ targetToken: await getTargetToken(),
2073
+ targetPath,
2074
+ timeoutMs
2075
+ });
2076
+ }
2077
+ ok++;
2078
+ } catch (error) {
2079
+ fail++;
2080
+ logger.error(error);
2081
+ }
2082
+ });
2083
+ await e.reply(`\u5907\u4EFD\u5B8C\u6210\uFF1A\u6210\u529F ${ok}\uFF0C\u8DF3\u8FC7 ${skipped}\uFF0C\u5931\u8D25 ${fail}`);
2084
+ return true;
2085
+ } catch (error) {
2086
+ logger.error(error);
2087
+ await e.reply(formatErrorMessage(error));
2088
+ return true;
2089
+ } finally {
2090
+ if (ticker) clearInterval(ticker);
2091
+ await tickChain.catch(() => void 0);
2092
+ activeOpenListBackup.delete(lockKey);
2093
+ }
2094
+ }, {
2095
+ priority: 9999,
2096
+ log: true,
2097
+ name: "OpenList\u5907\u4EFD\u5230\u5BF9\u7AEFOpenList",
2098
+ permission: "all"
2099
+ });
2100
+ var activeGroupFileUploadBackups = /* @__PURE__ */ new Map();
2101
+ var uploadBackupSkipLoggedGroups = /* @__PURE__ */ new Set();
2102
+ var uploadBackupUrlFallbackLoggedGroups = /* @__PURE__ */ new Set();
2103
+ var enqueueGroupFileUploadBackup = (groupId, task) => {
2104
+ const key = String(groupId);
2105
+ const previous = activeGroupFileUploadBackups.get(key) ?? Promise.resolve();
2106
+ const nextTask = previous.catch(() => void 0).then(task);
2107
+ activeGroupFileUploadBackups.set(key, nextTask);
2108
+ nextTask.finally(() => {
2109
+ if (activeGroupFileUploadBackups.get(key) === nextTask) activeGroupFileUploadBackups.delete(key);
2110
+ });
2111
+ };
2112
+ var groupFileUploadedAutoBackup = karin.accept("notice.groupFileUploaded", (e, next) => {
2113
+ try {
2114
+ const groupId = String(e?.groupId ?? "").trim();
2115
+ if (!groupId) return;
2116
+ const cfg = config();
2117
+ const targetCfg = getGroupSyncTarget(cfg, groupId);
2118
+ const uploadBackupEnabled = targetCfg?.uploadBackup === true || ["true", "1", "on"].includes(String(targetCfg?.uploadBackup ?? "").trim().toLowerCase());
2119
+ if (!targetCfg || targetCfg.enabled === false || !uploadBackupEnabled) {
2120
+ if (!uploadBackupSkipLoggedGroups.has(groupId)) {
2121
+ uploadBackupSkipLoggedGroups.add(groupId);
2122
+ logger.info(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] uploadBackup \u672A\u542F\u7528\u6216\u8BE5\u7FA4\u672A\u914D\u7F6E\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u53EF\u5728 WebUI \u5F00\u542F uploadBackup\uFF09`);
2123
+ }
2124
+ return;
2125
+ }
2126
+ const file = e.content;
2127
+ const fid = String(file?.fid ?? "").trim();
2128
+ const name = String(file?.name ?? "").trim();
2129
+ const size = typeof file?.size === "number" && Number.isFinite(file.size) ? Math.max(0, Math.floor(file.size)) : void 0;
2130
+ const getUrl = typeof file?.url === "function" ? file.url : null;
2131
+ if (!fid || !name || !getUrl) {
2132
+ logger.warn(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u7FA4\u6587\u4EF6\u4E0A\u4F20\u4E8B\u4EF6\u7F3A\u5C11\u5FC5\u8981\u5B57\u6BB5\uFF08fid/name/url\uFF09\uFF0C\u5DF2\u8DF3\u8FC7`);
2133
+ return;
2134
+ }
2135
+ enqueueGroupFileUploadBackup(groupId, async () => {
2136
+ const baseUrl = String(cfg.openlistBaseUrl ?? "").trim();
2137
+ const username = String(cfg.openlistUsername ?? "").trim();
2138
+ const password = String(cfg.openlistPassword ?? "").trim();
2139
+ const defaultTargetDir = String(cfg.openlistTargetDir ?? "/").trim();
2140
+ if (!baseUrl || !username || !password) {
2141
+ logger.error(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u7F3A\u5C11 OpenList \u914D\u7F6E\uFF08openlistBaseUrl/openlistUsername/openlistPassword\uFF09`);
2142
+ return;
2143
+ }
2144
+ const davBaseUrl = buildOpenListDavBaseUrl(baseUrl);
2145
+ if (!davBaseUrl) {
2146
+ logger.error(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl`);
2147
+ return;
2148
+ }
2149
+ const defaults = cfg.groupSyncDefaults ?? {};
2150
+ const targetDir = normalizePosixPath(
2151
+ String(targetCfg?.targetDir ?? "").trim() || path.posix.join(String(defaultTargetDir || "/"), String(groupId))
2152
+ );
2153
+ const flat = typeof targetCfg?.flat === "boolean" ? targetCfg.flat : Boolean(defaults?.flat ?? false);
2154
+ const item = {
2155
+ path: name,
2156
+ fileId: fid,
2157
+ name,
2158
+ size,
2159
+ busid: typeof file?.subId === "number" && Number.isFinite(file.subId) ? Math.floor(file.subId) : void 0
2160
+ };
2161
+ if (!flat) {
2162
+ const direct = pickFirstString(file?.path, file?.filePath, file?.file_path, file?.fullPath, file?.full_path);
2163
+ const normalized = direct ? normalizeGroupFileRelativePath(direct) : "";
2164
+ if (normalized) item.path = normalized;
2165
+ const bot = e?.bot;
2166
+ if (bot && !item.path.includes("/")) {
2167
+ try {
2168
+ const found = await locateGroupFileByIdWithRetry(bot, groupId, fid, {
2169
+ retries: 3,
2170
+ delayMs: 1200,
2171
+ timeoutMs: 15e3,
2172
+ maxFolders: 4e3,
2173
+ expectedName: name,
2174
+ expectedSize: size
2175
+ });
2176
+ if (found?.path) item.path = found.path;
2177
+ if (typeof found?.busid === "number" && Number.isFinite(found.busid)) item.busid = Math.floor(found.busid);
2178
+ if (!found?.path) {
2179
+ logger.warn(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u672A\u80FD\u89E3\u6790\u7FA4\u6587\u4EF6\u5939\u8DEF\u5F84\uFF0C\u53EF\u80FD\u534F\u8BAE\u7AEF\u672A\u63D0\u4F9B\u76EE\u5F55\u4FE1\u606F\u6216\u6587\u4EF6\u5C1A\u672A\u5165\u5E93\uFF0C\u5C06\u5907\u4EFD\u5230\u7FA4\u6839\u76EE\u5F55\uFF1A${name}`);
2180
+ }
2181
+ } catch (error) {
2182
+ logger.debug(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u83B7\u53D6\u7FA4\u5185\u6587\u4EF6\u8DEF\u5F84\u5931\u8D25\uFF0C\u5C06\u9000\u5316\u4E3A\u6839\u76EE\u5F55: ${formatErrorMessage(error)}`);
2183
+ }
2184
+ }
2185
+ }
2186
+ const resolveUrl = async () => {
2187
+ try {
2188
+ const url2 = await getUrl();
2189
+ if (typeof url2 === "string" && url2.trim()) return url2.trim();
2190
+ } catch {
2191
+ }
2192
+ const bot = e?.bot;
2193
+ const contact = e?.contact;
2194
+ if (!bot) throw new Error("\u4E8B\u4EF6\u5BF9\u8C61\u7F3A\u5C11 bot\uFF0C\u65E0\u6CD5\u901A\u8FC7\u63A5\u53E3\u83B7\u53D6\u4E0B\u8F7DURL");
2195
+ const url = await resolveGroupFileUrl(bot, contact, groupId, item);
2196
+ if (typeof url === "string" && url.trim()) {
2197
+ if (!uploadBackupUrlFallbackLoggedGroups.has(groupId)) {
2198
+ uploadBackupUrlFallbackLoggedGroups.add(groupId);
2199
+ logger.debug(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] file.url() \u4E0D\u53EF\u7528\uFF0C\u5DF2\u81EA\u52A8\u4F7F\u7528\u63A5\u53E3\u83B7\u53D6\u4E0B\u8F7DURL\uFF08\u8BE5\u63D0\u793A\u4EC5\u51FA\u73B0\u4E00\u6B21\uFF09`);
2200
+ }
2201
+ return url.trim();
2202
+ }
2203
+ throw new Error("\u901A\u8FC7\u63A5\u53E3\u83B7\u53D6\u4E0B\u8F7DURL\u5931\u8D25");
2204
+ };
2205
+ const remotePath = buildRemotePathForItem(item, targetDir, flat);
2206
+ const remoteDir = normalizePosixPath(path.posix.dirname(remotePath));
2207
+ const state = readGroupSyncState(groupId);
2208
+ if (state.files?.[remotePath]?.fileId && String(state.files[remotePath].fileId) === fid) {
2209
+ logger.info(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u5DF2\u5907\u4EFD\uFF0C\u8DF3\u8FC7: ${name} (${fid})`);
2210
+ return;
2211
+ }
2212
+ const auth = buildOpenListAuthHeader(username, password);
2213
+ const fileTimeoutSec = typeof targetCfg?.fileTimeoutSec === "number" ? targetCfg.fileTimeoutSec : typeof defaults?.fileTimeoutSec === "number" ? defaults.fileTimeoutSec : 600;
2214
+ const safeFileTimeoutSec = Math.min(
2215
+ MAX_FILE_TIMEOUT_SEC,
2216
+ Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeoutSec || 0))
2217
+ );
2218
+ const rateDown = Math.max(0, Math.floor(typeof targetCfg?.downloadLimitKbps === "number" ? targetCfg.downloadLimitKbps : typeof defaults?.downloadLimitKbps === "number" ? defaults.downloadLimitKbps : 0));
2219
+ const rateUp = Math.max(0, Math.floor(typeof targetCfg?.uploadLimitKbps === "number" ? targetCfg.uploadLimitKbps : typeof defaults?.uploadLimitKbps === "number" ? defaults.uploadLimitKbps : 0));
2220
+ const effectiveRateLimitBytesPerSec = (() => {
2221
+ const a = rateDown > 0 ? rateDown * 1024 : 0;
2222
+ const b = rateUp > 0 ? rateUp * 1024 : 0;
2223
+ if (a > 0 && b > 0) return Math.min(a, b);
2224
+ return a > 0 ? a : b > 0 ? b : 0;
2225
+ })();
2226
+ const retryTimes = typeof targetCfg?.retryTimes === "number" ? targetCfg.retryTimes : typeof defaults?.retryTimes === "number" ? defaults.retryTimes : 2;
2227
+ const retryDelayMs = typeof targetCfg?.retryDelayMs === "number" ? targetCfg.retryDelayMs : typeof defaults?.retryDelayMs === "number" ? defaults.retryDelayMs : 1500;
2228
+ const transferTimeoutMs = safeFileTimeoutSec * 1e3;
2229
+ const webdavTimeoutMs = 15e3;
2230
+ const dirEnsurer = createWebDavDirEnsurer(davBaseUrl, auth, webdavTimeoutMs);
2231
+ const targetUrl = `${davBaseUrl}${encodePathForUrl(remotePath)}`;
2232
+ logger.info(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u5F00\u59CB: ${name} (${fid}) -> ${remotePath}`);
2233
+ let lastError;
2234
+ const attempts = Math.max(0, Math.floor(retryTimes) || 0) + 1;
2235
+ for (let attempt = 1; attempt <= attempts; attempt++) {
2236
+ try {
2237
+ await dirEnsurer.ensureDir(remoteDir);
2238
+ const url = await resolveUrl();
2239
+ await downloadAndUploadByWebDav({
2240
+ sourceUrl: url,
2241
+ targetUrl,
2242
+ auth,
2243
+ timeoutMs: transferTimeoutMs,
2244
+ rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
2245
+ });
2246
+ state.files[remotePath] = {
2247
+ fileId: fid,
2248
+ size,
2249
+ syncedAt: Date.now()
2250
+ };
2251
+ state.lastSyncAt = Date.now();
2252
+ writeGroupSyncState(groupId, state);
2253
+ logger.info(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u5B8C\u6210: ${name} -> ${remotePath}`);
2254
+ lastError = void 0;
2255
+ break;
2256
+ } catch (error) {
2257
+ lastError = error;
2258
+ logger.error(error);
2259
+ if (attempt < attempts) {
2260
+ const delayMs = Math.max(0, Math.floor(retryDelayMs) || 0) * Math.pow(2, attempt - 1);
2261
+ if (delayMs > 0) await sleep(delayMs);
2262
+ }
2263
+ }
2264
+ }
2265
+ if (lastError) {
2266
+ logger.error(`[\u7FA4\u4E0A\u4F20\u5907\u4EFD][${groupId}] \u5931\u8D25: ${name} (${fid}) - ${formatErrorMessage(lastError)}`);
2267
+ }
2268
+ });
2269
+ } catch (error) {
2270
+ logger.error(error);
2271
+ } finally {
2272
+ next();
2273
+ }
2274
+ }, { log: false, name: "\u7FA4\u6587\u4EF6\u4E0A\u4F20\u81EA\u52A8\u5907\u4EFD" });
2275
+
2276
+ export {
2277
+ exportGroupFiles,
2278
+ syncGroupFilesToOpenListCore,
2279
+ syncGroupFilesToOpenList,
2280
+ backupOpenListToOpenList,
2281
+ groupFileUploadedAutoBackup
2282
+ };