karin-plugin-qgroup-file2openlist 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -0
- package/config/config.json +20 -0
- package/lib/apps/example.js +41 -0
- package/lib/apps/groupFiles.js +12 -0
- package/lib/apps/groupSyncConfig.js +398 -0
- package/lib/apps/groupSyncScheduler.js +188 -0
- package/lib/apps/handler.js +24 -0
- package/lib/apps/render.js +89 -0
- package/lib/apps/sendMsg.js +128 -0
- package/lib/apps/task.js +19 -0
- package/lib/chunk-5WVKHIPK.js +38 -0
- package/lib/chunk-IZS467MR.js +47 -0
- package/lib/chunk-QVWWPGHK.js +1138 -0
- package/lib/dir.js +6 -0
- package/lib/index.js +7 -0
- package/lib/web.config.js +756 -0
- package/package.json +67 -0
- package/resources/image//345/220/257/347/250/213/345/256/243/345/217/221.png +0 -0
- package/resources/template/test.html +21 -0
|
@@ -0,0 +1,1138 @@
|
|
|
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
|
+
return value || "unnamed";
|
|
61
|
+
};
|
|
62
|
+
var encodePathForUrl = (posixPath) => {
|
|
63
|
+
const normalized = normalizePosixPath(posixPath);
|
|
64
|
+
const segments = normalized.split("/").filter(Boolean).map(encodeURIComponent);
|
|
65
|
+
return `/${segments.join("/")}`;
|
|
66
|
+
};
|
|
67
|
+
var createThrottleTransform = (bytesPerSec) => {
|
|
68
|
+
const limit = Math.floor(bytesPerSec || 0);
|
|
69
|
+
if (!Number.isFinite(limit) || limit <= 0) return null;
|
|
70
|
+
let nextTime = Date.now();
|
|
71
|
+
const msPerByte = 1e3 / limit;
|
|
72
|
+
return new Transform({
|
|
73
|
+
transform(chunk, _enc, cb) {
|
|
74
|
+
void (async () => {
|
|
75
|
+
const size = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const start = Math.max(now, nextTime);
|
|
78
|
+
nextTime = start + size * msPerByte;
|
|
79
|
+
const waitMs = start - now;
|
|
80
|
+
if (waitMs > 0) await sleep(waitMs);
|
|
81
|
+
cb(null, chunk);
|
|
82
|
+
})().catch((err) => cb(err));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
var webdavPropfindListNames = async (params) => {
|
|
87
|
+
const { davBaseUrl, auth, dirPath, timeoutMs } = params;
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
const url = `${davBaseUrl}${encodePathForUrl(dirPath)}`;
|
|
92
|
+
const body = `<?xml version="1.0" encoding="utf-8" ?>
|
|
93
|
+
<d:propfind xmlns:d="DAV:">
|
|
94
|
+
<d:prop>
|
|
95
|
+
<d:displayname />
|
|
96
|
+
</d:prop>
|
|
97
|
+
</d:propfind>`;
|
|
98
|
+
const res = await fetch(url, {
|
|
99
|
+
method: "PROPFIND",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: auth,
|
|
102
|
+
Depth: "1",
|
|
103
|
+
"Content-Type": "application/xml; charset=utf-8"
|
|
104
|
+
},
|
|
105
|
+
body,
|
|
106
|
+
redirect: "follow",
|
|
107
|
+
signal: controller.signal
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) return /* @__PURE__ */ new Set();
|
|
110
|
+
const text = await res.text();
|
|
111
|
+
if (!text) return /* @__PURE__ */ new Set();
|
|
112
|
+
const names = /* @__PURE__ */ new Set();
|
|
113
|
+
const hrefRegex = /<d:href>([^<]+)<\/d:href>/gi;
|
|
114
|
+
let match;
|
|
115
|
+
while (match = hrefRegex.exec(text)) {
|
|
116
|
+
const href = match[1] ?? "";
|
|
117
|
+
const decoded = decodeURIComponent(href);
|
|
118
|
+
const cleaned = decoded.replace(/\/+$/, "");
|
|
119
|
+
const base = cleaned.split("/").filter(Boolean).pop();
|
|
120
|
+
if (base) names.add(base);
|
|
121
|
+
}
|
|
122
|
+
return names;
|
|
123
|
+
} catch {
|
|
124
|
+
return /* @__PURE__ */ new Set();
|
|
125
|
+
} finally {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var buildUploadFileCandidates = (filePath) => {
|
|
130
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
131
|
+
const candidates = [
|
|
132
|
+
filePath,
|
|
133
|
+
normalized
|
|
134
|
+
];
|
|
135
|
+
try {
|
|
136
|
+
candidates.push(pathToFileURL(filePath).href);
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
if (/^[a-zA-Z]:\//.test(normalized)) {
|
|
140
|
+
candidates.push(`file:///${normalized}`);
|
|
141
|
+
}
|
|
142
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
143
|
+
};
|
|
144
|
+
var getGroupSyncStatePath = (groupId) => path.join(dir.DataDir, "group-file-sync-state", `${String(groupId)}.json`);
|
|
145
|
+
var readGroupSyncState = (groupId) => {
|
|
146
|
+
const raw = readJsonSafe(getGroupSyncStatePath(groupId));
|
|
147
|
+
if (raw && typeof raw === "object" && raw.version === 1 && raw.files && typeof raw.files === "object") {
|
|
148
|
+
return {
|
|
149
|
+
version: 1,
|
|
150
|
+
groupId: String(raw.groupId ?? groupId),
|
|
151
|
+
updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : Date.now(),
|
|
152
|
+
lastSyncAt: typeof raw.lastSyncAt === "number" ? raw.lastSyncAt : void 0,
|
|
153
|
+
files: raw.files
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
version: 1,
|
|
158
|
+
groupId: String(groupId),
|
|
159
|
+
updatedAt: Date.now(),
|
|
160
|
+
files: {}
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
var writeGroupSyncState = (groupId, state) => {
|
|
164
|
+
const next = {
|
|
165
|
+
version: 1,
|
|
166
|
+
groupId: String(groupId),
|
|
167
|
+
updatedAt: Date.now(),
|
|
168
|
+
lastSyncAt: typeof state.lastSyncAt === "number" ? state.lastSyncAt : void 0,
|
|
169
|
+
files: state.files ?? {}
|
|
170
|
+
};
|
|
171
|
+
writeJsonSafe(getGroupSyncStatePath(groupId), next);
|
|
172
|
+
};
|
|
173
|
+
var normalizeSyncMode = (value, fallback) => {
|
|
174
|
+
const v = String(value ?? "").trim().toLowerCase();
|
|
175
|
+
if (v === "full" || v === "\u5168\u91CF") return "full";
|
|
176
|
+
if (v === "incremental" || v === "\u589E\u91CF" || v === "inc") return "incremental";
|
|
177
|
+
return fallback;
|
|
178
|
+
};
|
|
179
|
+
var getGroupSyncTarget = (cfg, groupId) => {
|
|
180
|
+
const list = cfg?.groupSyncTargets;
|
|
181
|
+
if (!Array.isArray(list)) return void 0;
|
|
182
|
+
return list.find((it) => String(it?.groupId) === String(groupId));
|
|
183
|
+
};
|
|
184
|
+
var getGroupFileListCompat = async (bot, groupId, folderId) => {
|
|
185
|
+
const groupNum = Number(groupId);
|
|
186
|
+
if (Number.isFinite(groupNum)) {
|
|
187
|
+
const onebot = bot?._onebot;
|
|
188
|
+
if (!folderId && typeof onebot?.getGroupRootFiles === "function") {
|
|
189
|
+
const res = await onebot.getGroupRootFiles(groupNum);
|
|
190
|
+
return {
|
|
191
|
+
files: Array.isArray(res?.files) ? res.files : [],
|
|
192
|
+
folders: Array.isArray(res?.folders) ? res.folders : []
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (folderId && typeof onebot?.getGroupFilesByFolder === "function") {
|
|
196
|
+
const res = await onebot.getGroupFilesByFolder(groupNum, folderId);
|
|
197
|
+
return {
|
|
198
|
+
files: Array.isArray(res?.files) ? res.files : [],
|
|
199
|
+
folders: Array.isArray(res?.folders) ? res.folders : []
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (typeof bot?.getGroupFileList === "function") {
|
|
204
|
+
try {
|
|
205
|
+
const res = await bot.getGroupFileList(groupId, folderId);
|
|
206
|
+
return {
|
|
207
|
+
files: Array.isArray(res?.files) ? res.files : [],
|
|
208
|
+
folders: Array.isArray(res?.folders) ? res.folders : []
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.debug(`[\u7FA4\u6587\u4EF6\u5BFC\u51FA] getGroupFileList \u8C03\u7528\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5 OneBot \u6269\u5C55: ${String(error)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!Number.isFinite(groupNum)) {
|
|
215
|
+
throw new Error("\u7FA4\u53F7\u65E0\u6CD5\u8F6C\u6362\u4E3A number\uFF0C\u4E14\u5F53\u524D\u9002\u914D\u5668\u4E0D\u652F\u6301 getGroupFileList");
|
|
216
|
+
}
|
|
217
|
+
if (!folderId && typeof bot?.getGroupRootFiles === "function") {
|
|
218
|
+
const res = await bot.getGroupRootFiles(groupNum);
|
|
219
|
+
return {
|
|
220
|
+
files: Array.isArray(res?.files) ? res.files : [],
|
|
221
|
+
folders: Array.isArray(res?.folders) ? res.folders : []
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (folderId && typeof bot?.getGroupFilesByFolder === "function") {
|
|
225
|
+
const res = await bot.getGroupFilesByFolder(groupNum, folderId);
|
|
226
|
+
return {
|
|
227
|
+
files: Array.isArray(res?.files) ? res.files : [],
|
|
228
|
+
folders: Array.isArray(res?.folders) ? res.folders : []
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
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");
|
|
232
|
+
};
|
|
233
|
+
var resolveGroupFileUrl = async (bot, contact, groupId, file) => {
|
|
234
|
+
if (!file.fileId) throw new Error("\u7F3A\u5C11 fileId");
|
|
235
|
+
const reasons = [];
|
|
236
|
+
if (typeof bot?.getFileUrl === "function") {
|
|
237
|
+
try {
|
|
238
|
+
const url = await bot.getFileUrl(contact, file.fileId);
|
|
239
|
+
if (typeof url === "string" && url) return url;
|
|
240
|
+
reasons.push("getFileUrl \u8FD4\u56DE\u7A7A\u503C");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
reasons.push(`getFileUrl: ${error?.message ?? String(error)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const groupNum = Number(groupId);
|
|
246
|
+
if (!Number.isFinite(groupNum)) {
|
|
247
|
+
throw new Error(reasons[0] ?? "\u7FA4\u53F7\u65E0\u6CD5\u8F6C\u6362\u4E3A number");
|
|
248
|
+
}
|
|
249
|
+
const onebot = bot?._onebot;
|
|
250
|
+
if (typeof onebot?.nc_getFile === "function") {
|
|
251
|
+
try {
|
|
252
|
+
const res = await onebot.nc_getFile(file.fileId);
|
|
253
|
+
if (typeof res?.url === "string" && res.url) return res.url;
|
|
254
|
+
reasons.push("nc_getFile \u8FD4\u56DE\u7A7A\u503C");
|
|
255
|
+
} catch (error) {
|
|
256
|
+
reasons.push(`nc_getFile: ${error?.message ?? String(error)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (typeof onebot?.getGroupFileUrl === "function") {
|
|
260
|
+
try {
|
|
261
|
+
const res = await onebot.getGroupFileUrl(groupNum, file.fileId, file.busid);
|
|
262
|
+
if (typeof res?.url === "string" && res.url) return res.url;
|
|
263
|
+
reasons.push("onebot.getGroupFileUrl \u8FD4\u56DE\u7A7A\u503C");
|
|
264
|
+
} catch (error) {
|
|
265
|
+
reasons.push(`onebot.getGroupFileUrl: ${error?.message ?? String(error)}`);
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const res = await onebot.getGroupFileUrl(groupNum, file.fileId);
|
|
269
|
+
if (typeof res?.url === "string" && res.url) return res.url;
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (typeof bot?.getGroupFileUrl === "function") {
|
|
274
|
+
try {
|
|
275
|
+
const res = await bot.getGroupFileUrl(groupNum, file.fileId, file.busid);
|
|
276
|
+
if (typeof res?.url === "string" && res.url) return res.url;
|
|
277
|
+
reasons.push("getGroupFileUrl \u8FD4\u56DE\u7A7A\u503C");
|
|
278
|
+
} catch (error) {
|
|
279
|
+
reasons.push(`getGroupFileUrl: ${error?.message ?? String(error)}`);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const res = await bot.getGroupFileUrl(groupNum, file.fileId);
|
|
283
|
+
if (typeof res?.url === "string" && res.url) return res.url;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
reasons.push(`getGroupFileUrl(no busid): ${error?.message ?? String(error)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
throw new Error(reasons[0] ?? "\u65E0\u6CD5\u83B7\u53D6\u4E0B\u8F7DURL\uFF08\u672A\u627E\u5230\u53EF\u7528\u63A5\u53E3\uFF09");
|
|
289
|
+
};
|
|
290
|
+
var buildOpenListDavBaseUrl = (baseUrl) => {
|
|
291
|
+
const normalized = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
292
|
+
if (!normalized) return "";
|
|
293
|
+
return `${normalized}/dav`;
|
|
294
|
+
};
|
|
295
|
+
var buildOpenListAuthHeader = (username, password) => {
|
|
296
|
+
const user = String(username ?? "");
|
|
297
|
+
const pass = String(password ?? "");
|
|
298
|
+
const token = Buffer.from(`${user}:${pass}`).toString("base64");
|
|
299
|
+
return `Basic ${token}`;
|
|
300
|
+
};
|
|
301
|
+
var formatErrorMessage = (error) => {
|
|
302
|
+
if (error instanceof Error) {
|
|
303
|
+
const base = error.message || String(error);
|
|
304
|
+
const cause = error.cause;
|
|
305
|
+
if (cause) {
|
|
306
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
307
|
+
const causeCode = typeof cause === "object" && cause && "code" in cause ? String(cause.code) : "";
|
|
308
|
+
const extra = [causeCode, causeMsg].filter(Boolean).join(" ");
|
|
309
|
+
if (extra && extra !== base) return `${base} (${extra})`;
|
|
310
|
+
}
|
|
311
|
+
return base;
|
|
312
|
+
}
|
|
313
|
+
return String(error);
|
|
314
|
+
};
|
|
315
|
+
var isAbortError = (error) => {
|
|
316
|
+
return Boolean(error && typeof error === "object" && "name" in error && error.name === "AbortError");
|
|
317
|
+
};
|
|
318
|
+
var fetchTextSafely = async (res) => {
|
|
319
|
+
try {
|
|
320
|
+
const text = await res.text();
|
|
321
|
+
return text.slice(0, 500);
|
|
322
|
+
} catch {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
var webdavMkcolOk = (status) => status === 201 || status === 405;
|
|
327
|
+
var createWebDavDirEnsurer = (davBaseUrl, auth, timeoutMs) => {
|
|
328
|
+
const ensured = /* @__PURE__ */ new Map();
|
|
329
|
+
const requestTimeoutMs = Math.max(1e3, Math.floor(timeoutMs) || 0);
|
|
330
|
+
const ensureDir2 = async (dirPath) => {
|
|
331
|
+
const normalized = normalizePosixPath(dirPath);
|
|
332
|
+
if (normalized === "/") return;
|
|
333
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
334
|
+
let current = "";
|
|
335
|
+
for (const segment of segments) {
|
|
336
|
+
current += `/${segment}`;
|
|
337
|
+
const existing = ensured.get(current);
|
|
338
|
+
if (existing) {
|
|
339
|
+
await existing;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const promise = (async () => {
|
|
343
|
+
const url = `${davBaseUrl}${encodePathForUrl(current)}`;
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
346
|
+
try {
|
|
347
|
+
let res;
|
|
348
|
+
try {
|
|
349
|
+
res = await fetch(url, {
|
|
350
|
+
method: "MKCOL",
|
|
351
|
+
headers: { Authorization: auth },
|
|
352
|
+
redirect: "follow",
|
|
353
|
+
signal: controller.signal
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (isAbortError(error)) throw new Error(`MKCOL \u8D85\u65F6: ${current}`);
|
|
357
|
+
throw new Error(`MKCOL \u8BF7\u6C42\u5931\u8D25: ${current} - ${formatErrorMessage(error)}`);
|
|
358
|
+
}
|
|
359
|
+
if (webdavMkcolOk(res.status) || res.ok) return;
|
|
360
|
+
const body = await fetchTextSafely(res);
|
|
361
|
+
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" : "";
|
|
362
|
+
throw new Error(`MKCOL \u5931\u8D25: ${current} -> ${res.status} ${res.statusText}${hint}${body ? ` - ${body}` : ""}`);
|
|
363
|
+
} finally {
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
}
|
|
366
|
+
})();
|
|
367
|
+
ensured.set(current, promise);
|
|
368
|
+
try {
|
|
369
|
+
await promise;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
ensured.delete(current);
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
return { ensureDir: ensureDir2 };
|
|
377
|
+
};
|
|
378
|
+
var downloadAndUploadByWebDav = async (params) => {
|
|
379
|
+
const { sourceUrl, targetUrl, auth, timeoutMs, rateLimitBytesPerSec } = params;
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
382
|
+
try {
|
|
383
|
+
let downloadRes;
|
|
384
|
+
try {
|
|
385
|
+
downloadRes = await fetch(sourceUrl, { redirect: "follow", signal: controller.signal });
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (isAbortError(error)) throw new Error("\u4E0B\u8F7D\u8D85\u65F6\uFF08URL\u53EF\u80FD\u5DF2\u5931\u6548\uFF09");
|
|
388
|
+
throw new Error(`\u4E0B\u8F7D\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
389
|
+
}
|
|
390
|
+
if (!downloadRes.ok) {
|
|
391
|
+
const body = await fetchTextSafely(downloadRes);
|
|
392
|
+
const hint = downloadRes.status === 403 ? "\uFF08\u53EF\u80FDURL\u5DF2\u8FC7\u671F\uFF0C\u9700\u8981\u91CD\u65B0\u83B7\u53D6\uFF09" : "";
|
|
393
|
+
throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${downloadRes.status} ${downloadRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
|
|
394
|
+
}
|
|
395
|
+
if (!downloadRes.body) throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
396
|
+
const headers = { Authorization: auth };
|
|
397
|
+
const contentType = downloadRes.headers.get("content-type");
|
|
398
|
+
const contentLength = downloadRes.headers.get("content-length");
|
|
399
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
400
|
+
if (contentLength) headers["Content-Length"] = contentLength;
|
|
401
|
+
const sourceStream = Readable.fromWeb(downloadRes.body);
|
|
402
|
+
const throttle = createThrottleTransform(Math.floor(rateLimitBytesPerSec || 0));
|
|
403
|
+
const bodyStream = throttle ? sourceStream.pipe(throttle) : sourceStream;
|
|
404
|
+
let putRes;
|
|
405
|
+
try {
|
|
406
|
+
putRes = await fetch(targetUrl, {
|
|
407
|
+
method: "PUT",
|
|
408
|
+
headers,
|
|
409
|
+
body: bodyStream,
|
|
410
|
+
// @ts-expect-error Node fetch streaming body requires duplex
|
|
411
|
+
duplex: "half",
|
|
412
|
+
redirect: "follow",
|
|
413
|
+
signal: controller.signal
|
|
414
|
+
});
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (isAbortError(error)) throw new Error("\u4E0A\u4F20\u8D85\u65F6\uFF08\u8BF7\u68C0\u67E5 OpenList \u8FDE\u63A5/\u6743\u9650\uFF09");
|
|
417
|
+
throw new Error(`\u4E0A\u4F20\u8BF7\u6C42\u5931\u8D25: ${formatErrorMessage(error)}`);
|
|
418
|
+
}
|
|
419
|
+
if (!putRes.ok) {
|
|
420
|
+
const body = await fetchTextSafely(putRes);
|
|
421
|
+
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" : "";
|
|
422
|
+
throw new Error(`\u4E0A\u4F20\u5931\u8D25: ${putRes.status} ${putRes.statusText}${hint}${body ? ` - ${body}` : ""}`);
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
clearTimeout(timer);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
var activeGroupSync = /* @__PURE__ */ new Set();
|
|
429
|
+
var withGroupSyncLock = async (groupId, fn) => {
|
|
430
|
+
const key = String(groupId);
|
|
431
|
+
if (activeGroupSync.has(key)) throw new Error("\u8BE5\u7FA4\u540C\u6B65\u4EFB\u52A1\u6B63\u5728\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5");
|
|
432
|
+
activeGroupSync.add(key);
|
|
433
|
+
try {
|
|
434
|
+
return await fn();
|
|
435
|
+
} finally {
|
|
436
|
+
activeGroupSync.delete(key);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var buildRemotePathForItem = (item, targetDir, flat) => {
|
|
440
|
+
const relativeParts = (flat ? [item.name] : item.path.split("/")).filter(Boolean).map(safePathSegment);
|
|
441
|
+
return normalizePosixPath(path.posix.join(targetDir, ...relativeParts));
|
|
442
|
+
};
|
|
443
|
+
var isSameSyncedFile = (prev, item) => {
|
|
444
|
+
if (!prev) return false;
|
|
445
|
+
if (prev.fileId && item.fileId && prev.fileId !== item.fileId) return false;
|
|
446
|
+
const md5Ok = prev.md5 && item.md5 && prev.md5 === item.md5;
|
|
447
|
+
const sha1Ok = prev.sha1 && item.sha1 && prev.sha1 === item.sha1;
|
|
448
|
+
if (md5Ok || sha1Ok) return true;
|
|
449
|
+
const sizeOk = typeof prev.size === "number" && typeof item.size === "number" && prev.size === item.size;
|
|
450
|
+
const timeOk = typeof prev.uploadTime === "number" && typeof item.uploadTime === "number" && prev.uploadTime === item.uploadTime;
|
|
451
|
+
if (sizeOk && timeOk) return true;
|
|
452
|
+
if (timeOk && prev.fileId && item.fileId && prev.fileId === item.fileId) return true;
|
|
453
|
+
if (sizeOk && prev.fileId && item.fileId && prev.fileId === item.fileId) return true;
|
|
454
|
+
return false;
|
|
455
|
+
};
|
|
456
|
+
var runWithConcurrency = async (items, concurrency, fn) => {
|
|
457
|
+
const limit = Math.max(1, Math.floor(concurrency) || 1);
|
|
458
|
+
const executing = /* @__PURE__ */ new Set();
|
|
459
|
+
for (let index = 0; index < items.length; index++) {
|
|
460
|
+
const item = items[index];
|
|
461
|
+
const task = (async () => fn(item, index))();
|
|
462
|
+
executing.add(task);
|
|
463
|
+
task.finally(() => executing.delete(task));
|
|
464
|
+
if (executing.size >= limit) {
|
|
465
|
+
await Promise.race(executing);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
await Promise.all(executing);
|
|
469
|
+
};
|
|
470
|
+
var runWithAdaptiveConcurrency = async (items, options) => {
|
|
471
|
+
const max = Math.max(1, Math.floor(options.max) || 1);
|
|
472
|
+
let current = Math.min(max, Math.max(1, Math.floor(options.initial) || 1));
|
|
473
|
+
const onAdjust = options.onAdjust;
|
|
474
|
+
const results = [];
|
|
475
|
+
const pushResult = (ok, ms, reason) => {
|
|
476
|
+
results.push({ ok, ms, reason });
|
|
477
|
+
if (results.length > 20) results.shift();
|
|
478
|
+
if (results.length < 10) return;
|
|
479
|
+
if (results.length % 5 !== 0) return;
|
|
480
|
+
const failCount = results.filter((r) => !r.ok).length;
|
|
481
|
+
const failRate = failCount / results.length;
|
|
482
|
+
const avgMs = results.reduce((acc, r) => acc + r.ms, 0) / results.length;
|
|
483
|
+
const hasTimeout = results.some((r) => (r.reason || "").includes("\u8D85\u65F6"));
|
|
484
|
+
if (hasTimeout || failRate >= 0.2) {
|
|
485
|
+
if (current > 1) {
|
|
486
|
+
current -= 1;
|
|
487
|
+
onAdjust?.(current, hasTimeout ? "timeout" : `failRate=${failRate.toFixed(2)}`);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (failCount === 0 && current < max) {
|
|
492
|
+
if (avgMs < 6e4 || results.length === 20) {
|
|
493
|
+
current += 1;
|
|
494
|
+
onAdjust?.(current, "stable");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
let nextIndex = 0;
|
|
499
|
+
const executing = /* @__PURE__ */ new Set();
|
|
500
|
+
const launch = (index) => {
|
|
501
|
+
const item = items[index];
|
|
502
|
+
const start = Date.now();
|
|
503
|
+
const task = (async () => {
|
|
504
|
+
try {
|
|
505
|
+
await options.fn(item, index);
|
|
506
|
+
pushResult(true, Date.now() - start);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const msg = formatErrorMessage(error);
|
|
509
|
+
pushResult(false, Date.now() - start, msg);
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
})();
|
|
513
|
+
executing.add(task);
|
|
514
|
+
task.finally(() => executing.delete(task));
|
|
515
|
+
return task;
|
|
516
|
+
};
|
|
517
|
+
while (nextIndex < items.length || executing.size) {
|
|
518
|
+
while (nextIndex < items.length && executing.size < current) {
|
|
519
|
+
launch(nextIndex);
|
|
520
|
+
nextIndex++;
|
|
521
|
+
}
|
|
522
|
+
if (executing.size) await Promise.race(executing);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var collectAllGroupFiles = async (bot, groupId, startFolderId) => {
|
|
526
|
+
const files = [];
|
|
527
|
+
const visitedFolders = /* @__PURE__ */ new Set();
|
|
528
|
+
const walk = async (folderId, prefix) => {
|
|
529
|
+
if (folderId) {
|
|
530
|
+
if (visitedFolders.has(folderId)) return;
|
|
531
|
+
visitedFolders.add(folderId);
|
|
532
|
+
}
|
|
533
|
+
const { files: rawFiles, folders: rawFolders } = await getGroupFileListCompat(bot, groupId, folderId);
|
|
534
|
+
for (const raw of rawFiles) {
|
|
535
|
+
const fileId = pickFirstString(raw?.fid, raw?.file_id, raw?.fileId, raw?.id);
|
|
536
|
+
const name = pickFirstString(raw?.name, raw?.file_name, raw?.fileName) ?? (fileId ? `file-${fileId}` : "unknown-file");
|
|
537
|
+
const filePath = prefix ? `${prefix}/${name}` : name;
|
|
538
|
+
files.push({
|
|
539
|
+
path: filePath,
|
|
540
|
+
fileId: fileId ?? "",
|
|
541
|
+
name,
|
|
542
|
+
size: pickFirstNumber(raw?.size, raw?.file_size, raw?.fileSize),
|
|
543
|
+
uploadTime: pickFirstNumber(raw?.uploadTime, raw?.upload_time),
|
|
544
|
+
uploaderId: pickFirstString(raw?.uploadId, raw?.uploader, raw?.uploader_id),
|
|
545
|
+
uploaderName: pickFirstString(raw?.uploadName, raw?.uploader_name),
|
|
546
|
+
md5: pickFirstString(raw?.md5),
|
|
547
|
+
sha1: pickFirstString(raw?.sha1),
|
|
548
|
+
sha3: pickFirstString(raw?.sha3),
|
|
549
|
+
busid: pickFirstNumber(raw?.busid, raw?.busId)
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
for (const raw of rawFolders) {
|
|
553
|
+
const folderId2 = pickFirstString(raw?.id, raw?.folder_id, raw?.folderId);
|
|
554
|
+
if (!folderId2) continue;
|
|
555
|
+
const folderName = pickFirstString(raw?.name, raw?.folder_name, raw?.folderName) ?? folderId2;
|
|
556
|
+
const nextPrefix = prefix ? `${prefix}/${folderName}` : folderName;
|
|
557
|
+
await walk(folderId2, nextPrefix);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
await walk(startFolderId, "");
|
|
561
|
+
return files;
|
|
562
|
+
};
|
|
563
|
+
var writeExportFile = (format, outPath, payload, list) => {
|
|
564
|
+
ensureDir(path.dirname(outPath));
|
|
565
|
+
if (format === "json") {
|
|
566
|
+
fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), "utf8");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const header = ["path", "name", "fileId", "size", "uploadTime", "uploaderId", "uploaderName", "md5", "sha1", "sha3", "url", "busid"];
|
|
570
|
+
const rows = [header.join(",")];
|
|
571
|
+
for (const item of list) {
|
|
572
|
+
rows.push([
|
|
573
|
+
csvEscape(item.path),
|
|
574
|
+
csvEscape(item.name),
|
|
575
|
+
csvEscape(item.fileId),
|
|
576
|
+
csvEscape(item.size ?? ""),
|
|
577
|
+
csvEscape(item.uploadTime ?? ""),
|
|
578
|
+
csvEscape(item.uploaderId ?? ""),
|
|
579
|
+
csvEscape(item.uploaderName ?? ""),
|
|
580
|
+
csvEscape(item.md5 ?? ""),
|
|
581
|
+
csvEscape(item.sha1 ?? ""),
|
|
582
|
+
csvEscape(item.sha3 ?? ""),
|
|
583
|
+
csvEscape(item.url ?? ""),
|
|
584
|
+
csvEscape(item.busid ?? "")
|
|
585
|
+
].join(","));
|
|
586
|
+
}
|
|
587
|
+
fs.writeFileSync(outPath, rows.join("\n"), "utf8");
|
|
588
|
+
};
|
|
589
|
+
var parseArgs = (text) => {
|
|
590
|
+
const raw = text.trim();
|
|
591
|
+
const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
592
|
+
const format = /(^|\s)(--csv|csv)(\s|$)/i.test(raw) ? "csv" : "json";
|
|
593
|
+
const withUrl = !/(^|\s)(--no-url|--nourl|no-url|nourl)(\s|$)/i.test(raw);
|
|
594
|
+
const urlOnly = /(^|\s)(--url-only|--urlonly|url-only|urlonly)(\s|$)/i.test(raw);
|
|
595
|
+
const sendFile = /(^|\s)(--send-file|--sendfile|send-file|sendfile)(\s|$)/i.test(raw);
|
|
596
|
+
let groupId;
|
|
597
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
598
|
+
const token = tokens[i];
|
|
599
|
+
const nextToken = tokens[i + 1];
|
|
600
|
+
if (/^--(group|gid|groupid)$/i.test(token) && nextToken && /^\d+$/.test(nextToken)) {
|
|
601
|
+
groupId = nextToken;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
const assignMatch = token.match(/^(group|gid|groupid)=(\d+)$/i);
|
|
605
|
+
if (assignMatch) {
|
|
606
|
+
groupId = assignMatch[2];
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (!groupId) {
|
|
611
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
612
|
+
const token = tokens[i];
|
|
613
|
+
const prevToken = tokens[i - 1];
|
|
614
|
+
if (!/^\d+$/.test(token)) continue;
|
|
615
|
+
if (prevToken && /^--(folder|max|concurrency|group|gid|groupid)$/i.test(prevToken)) continue;
|
|
616
|
+
groupId = token;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const folderMatch = raw.match(/--folder\s+(\S+)/i) ?? raw.match(/(^|\s)folder=(\S+)/i);
|
|
621
|
+
const folderId = folderMatch ? folderMatch[folderMatch.length - 1] : void 0;
|
|
622
|
+
const maxMatch = raw.match(/--max\s+(\d+)/i) ?? raw.match(/(^|\s)max=(\d+)/i);
|
|
623
|
+
const maxFiles = maxMatch ? Number(maxMatch[maxMatch.length - 1]) : void 0;
|
|
624
|
+
const concurrencyMatch = raw.match(/--concurrency\s+(\d+)/i) ?? raw.match(/(^|\s)concurrency=(\d+)/i);
|
|
625
|
+
const concurrency = concurrencyMatch ? Number(concurrencyMatch[concurrencyMatch.length - 1]) : void 0;
|
|
626
|
+
const concurrencySpecified = Boolean(concurrencyMatch);
|
|
627
|
+
const help = /(^|\s)(--help|-h|help|\?)(\s|$)/i.test(raw);
|
|
628
|
+
return { groupId, format, withUrl, urlOnly, sendFile, folderId, maxFiles, concurrency, concurrencySpecified, help };
|
|
629
|
+
};
|
|
630
|
+
var parseSyncArgs = (text) => {
|
|
631
|
+
const raw = text.trim();
|
|
632
|
+
const base = parseArgs(text);
|
|
633
|
+
const toMatch = raw.match(/--to\s+(\S+)/i) ?? raw.match(/(^|\s)to=(\S+)/i);
|
|
634
|
+
const to = toMatch ? toMatch[toMatch.length - 1] : void 0;
|
|
635
|
+
const toSpecified = Boolean(toMatch);
|
|
636
|
+
const flatFlag = /(^|\s)(--flat|flat)(\s|$)/i.test(raw);
|
|
637
|
+
const keepFlag = /(^|\s)(--keep|--no-flat|keep|no-flat)(\s|$)/i.test(raw);
|
|
638
|
+
const flatSpecified = flatFlag || keepFlag;
|
|
639
|
+
const flat = flatFlag ? true : keepFlag ? false : void 0;
|
|
640
|
+
const help = /(^|\s)(--help|-h|help|\?)(\s|$)/i.test(raw);
|
|
641
|
+
const concurrency = base.concurrency;
|
|
642
|
+
const concurrencySpecified = base.concurrencySpecified;
|
|
643
|
+
const timeoutMatch = raw.match(/--timeout\s+(\d+)/i) ?? raw.match(/(^|\s)timeout=(\d+)/i);
|
|
644
|
+
const timeoutSec = timeoutMatch ? Number(timeoutMatch[timeoutMatch.length - 1]) : void 0;
|
|
645
|
+
const timeoutSpecified = Boolean(timeoutMatch);
|
|
646
|
+
const modeFull = /(^|\s)(--full|full)(\s|$)/i.test(raw);
|
|
647
|
+
const modeInc = /(^|\s)(--inc|--incremental|inc|incremental)(\s|$)/i.test(raw);
|
|
648
|
+
const mode = modeFull ? "full" : modeInc ? "incremental" : void 0;
|
|
649
|
+
return {
|
|
650
|
+
groupId: base.groupId,
|
|
651
|
+
folderId: base.folderId,
|
|
652
|
+
maxFiles: base.maxFiles,
|
|
653
|
+
concurrency,
|
|
654
|
+
concurrencySpecified,
|
|
655
|
+
flat,
|
|
656
|
+
flatSpecified,
|
|
657
|
+
to,
|
|
658
|
+
toSpecified,
|
|
659
|
+
timeoutSec,
|
|
660
|
+
timeoutSpecified,
|
|
661
|
+
mode,
|
|
662
|
+
help
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
var helpText = [
|
|
666
|
+
"\u7FA4\u6587\u4EF6\u5BFC\u51FA\u7528\u6CD5\uFF1A",
|
|
667
|
+
"- \u8BF7\u79C1\u804A\u53D1\u9001\uFF1A#\u5BFC\u51FA\u7FA4\u6587\u4EF6 <\u7FA4\u53F7> [\u53C2\u6570]",
|
|
668
|
+
"- \u793A\u4F8B\uFF1A#\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456",
|
|
669
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --no-url\uFF1A\u53EA\u5BFC\u51FA\u5217\u8868\uFF0C\u4E0D\u89E3\u6790URL",
|
|
670
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --url-only\uFF1A\u4EC5\u8F93\u51FAURL\uFF08\u66F4\u65B9\u4FBF\u590D\u5236\uFF09",
|
|
671
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --csv\uFF1A\u5BFC\u51FA\u4E3ACSV\uFF08\u9ED8\u8BA4JSON\uFF09",
|
|
672
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --folder <id>\uFF1A\u4ECE\u6307\u5B9A\u6587\u4EF6\u5939\u5F00\u59CB\u5BFC\u51FA",
|
|
673
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --max <n>\uFF1A\u6700\u591A\u5BFC\u51FAn\u6761\u6587\u4EF6\u8BB0\u5F55",
|
|
674
|
+
"- #\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456 --concurrency <n>\uFF1A\u89E3\u6790URL\u5E76\u53D1\u6570\uFF08\u9ED8\u8BA43\uFF09",
|
|
675
|
+
"- #\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",
|
|
676
|
+
"\u63D0\u793A\uFF1A\u4E0B\u8F7DURL\u901A\u5E38\u6709\u65F6\u6548\uFF0C\u8FC7\u671F\u540E\u9700\u91CD\u65B0\u5BFC\u51FA\u3002"
|
|
677
|
+
].join("\n");
|
|
678
|
+
var exportGroupFiles = karin.command(/^#?(导出群文件|群文件导出)(.*)$/i, async (e) => {
|
|
679
|
+
if (!e.isPrivate) {
|
|
680
|
+
await e.reply("\u8BF7\u79C1\u804A\u4F7F\u7528\u8BE5\u547D\u4EE4\uFF0C\u5E76\u5728\u53C2\u6570\u4E2D\u6307\u5B9A\u7FA4\u53F7\n\u4F8B\u5982\uFF1A#\u5BFC\u51FA\u7FA4\u6587\u4EF6 123456");
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
const argsText = e.msg.replace(/^#?(导出群文件|群文件导出)/i, "");
|
|
684
|
+
const { groupId, format, withUrl, urlOnly, sendFile, folderId, maxFiles, concurrency, help } = parseArgs(argsText);
|
|
685
|
+
if (help) {
|
|
686
|
+
await e.reply(helpText);
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
if (!groupId) {
|
|
690
|
+
await e.reply(`\u7F3A\u5C11\u7FA4\u53F7\u53C2\u6570
|
|
691
|
+
|
|
692
|
+
${helpText}`);
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const groupContact = karin.contactGroup(groupId);
|
|
696
|
+
await e.reply(`\u5F00\u59CB\u5BFC\u51FA\u7FA4\u6587\u4EF6\u5217\u8868\uFF0C\u8BF7\u7A0D\u5019...
|
|
697
|
+
- \u7FA4\u53F7\uFF1A${groupId}
|
|
698
|
+
- \u683C\u5F0F\uFF1A${format}
|
|
699
|
+
- \u5305\u542BURL\uFF1A${withUrl ? "\u662F" : "\u5426"}`);
|
|
700
|
+
const errors = [];
|
|
701
|
+
let list = [];
|
|
702
|
+
try {
|
|
703
|
+
list = await collectAllGroupFiles(e.bot, groupId, folderId);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
logger.error(error);
|
|
706
|
+
await e.reply(`\u5BFC\u51FA\u5931\u8D25\uFF1A${formatErrorMessage(error)}`);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
const limitedList = typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0 ? list.slice(0, Math.floor(maxFiles)) : list;
|
|
710
|
+
if (withUrl) {
|
|
711
|
+
const urlConcurrency = typeof concurrency === "number" && Number.isFinite(concurrency) && concurrency > 0 ? concurrency : 3;
|
|
712
|
+
await runWithConcurrency(limitedList, urlConcurrency, async (item) => {
|
|
713
|
+
try {
|
|
714
|
+
item.url = await resolveGroupFileUrl(e.bot, groupContact, groupId, item);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
errors.push({
|
|
717
|
+
fileId: item.fileId,
|
|
718
|
+
path: item.path,
|
|
719
|
+
message: formatErrorMessage(error)
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
const exportDir = path.join(dir.karinPath, "data", "group-files-export");
|
|
725
|
+
const exportName = `group-files-${groupId}-${time("YYYYMMDD-HHmmss")}.${format}`;
|
|
726
|
+
const exportPath = path.join(exportDir, exportName);
|
|
727
|
+
const payload = {
|
|
728
|
+
type: "group-files-export",
|
|
729
|
+
plugin: { name: dir.name, version: dir.version },
|
|
730
|
+
groupId,
|
|
731
|
+
folderId: folderId ?? null,
|
|
732
|
+
exportedAt: time(),
|
|
733
|
+
withUrl,
|
|
734
|
+
fileCount: limitedList.length,
|
|
735
|
+
errors,
|
|
736
|
+
files: limitedList
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
writeExportFile(format, exportPath, payload, limitedList);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
logger.error(error);
|
|
742
|
+
await e.reply(`\u5199\u5165\u5BFC\u51FA\u6587\u4EF6\u5931\u8D25\uFF1A${formatErrorMessage(error)}`);
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
const urlOk = withUrl ? limitedList.filter((v) => typeof v.url === "string" && v.url).length : 0;
|
|
746
|
+
const urlFail = withUrl ? limitedList.length - urlOk : 0;
|
|
747
|
+
const summary = [
|
|
748
|
+
`\u5BFC\u51FA\u5B8C\u6210\uFF1A${limitedList.length} \u4E2A\u6587\u4EF6`,
|
|
749
|
+
withUrl ? `URL\uFF1A\u6210\u529F ${urlOk} / \u5931\u8D25 ${urlFail}` : null,
|
|
750
|
+
`\u5BFC\u51FA\u6587\u4EF6\uFF1A${exportName}`,
|
|
751
|
+
`\u5BFC\u51FA\u6587\u4EF6\u5DF2\u4FDD\u5B58\u81F3\uFF1A${exportPath}`
|
|
752
|
+
].filter(Boolean).join("\n");
|
|
753
|
+
await e.reply(summary);
|
|
754
|
+
const textMax = 200;
|
|
755
|
+
const preview = limitedList.slice(0, textMax);
|
|
756
|
+
const errorByFileId = /* @__PURE__ */ new Map();
|
|
757
|
+
for (const err of errors) {
|
|
758
|
+
if (err.fileId && err.message && !errorByFileId.has(err.fileId)) errorByFileId.set(err.fileId, err.message);
|
|
759
|
+
}
|
|
760
|
+
const compactError = (message) => message.replace(/\s+/g, " ").slice(0, 120);
|
|
761
|
+
const lines = preview.map((item, index) => {
|
|
762
|
+
if (!withUrl) return `${index + 1}. ${item.path} ${item.fileId}`;
|
|
763
|
+
if (urlOnly) return item.url ? item.url : `(\u83B7\u53D6URL\u5931\u8D25) ${item.path} (${item.fileId})`;
|
|
764
|
+
if (item.url) return `${index + 1}. ${item.path}
|
|
765
|
+
${item.url}`;
|
|
766
|
+
const errMsg = errorByFileId.get(item.fileId);
|
|
767
|
+
return `${index + 1}. ${item.path}
|
|
768
|
+
(\u83B7\u53D6URL\u5931\u8D25) fileId=${item.fileId}${errMsg ? `
|
|
769
|
+
\u539F\u56E0\uFF1A${compactError(errMsg)}` : ""}`;
|
|
770
|
+
});
|
|
771
|
+
const chunks = [];
|
|
772
|
+
const maxChunkLen = 1500;
|
|
773
|
+
let buf = "";
|
|
774
|
+
for (const line of lines) {
|
|
775
|
+
const next = buf ? `${buf}
|
|
776
|
+
|
|
777
|
+
${line}` : line;
|
|
778
|
+
if (next.length > maxChunkLen) {
|
|
779
|
+
if (buf) chunks.push(buf);
|
|
780
|
+
buf = line;
|
|
781
|
+
} else {
|
|
782
|
+
buf = next;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (buf) chunks.push(buf);
|
|
786
|
+
const maxMessages = 10;
|
|
787
|
+
for (const chunk of chunks.slice(0, maxMessages)) {
|
|
788
|
+
await e.reply(chunk);
|
|
789
|
+
}
|
|
790
|
+
if (limitedList.length > preview.length) {
|
|
791
|
+
await e.reply(`\uFF08\u5DF2\u7701\u7565 ${limitedList.length - preview.length} \u6761\uFF0C\u53EF\u4F7F\u7528 --max \u8C03\u6574\uFF09`);
|
|
792
|
+
} else if (chunks.length > maxMessages) {
|
|
793
|
+
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`);
|
|
794
|
+
}
|
|
795
|
+
if (sendFile && typeof e.bot?.uploadFile === "function") {
|
|
796
|
+
const candidates = buildUploadFileCandidates(exportPath);
|
|
797
|
+
for (const fileParam of candidates) {
|
|
798
|
+
try {
|
|
799
|
+
await e.bot.uploadFile(e.contact, fileParam, exportName);
|
|
800
|
+
break;
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return true;
|
|
806
|
+
}, {
|
|
807
|
+
priority: 9999,
|
|
808
|
+
log: true,
|
|
809
|
+
name: "\u5BFC\u51FA\u7FA4\u6587\u4EF6",
|
|
810
|
+
permission: "all"
|
|
811
|
+
});
|
|
812
|
+
var syncHelpText = [
|
|
813
|
+
"\u7FA4\u6587\u4EF6\u540C\u6B65\u5230 OpenList \u7528\u6CD5\uFF1A",
|
|
814
|
+
"- \u79C1\u804A\uFF1A#\u540C\u6B65\u7FA4\u6587\u4EF6 <\u7FA4\u53F7> [\u53C2\u6570]",
|
|
815
|
+
"- \u7FA4\u804A\uFF1A#\u540C\u6B65\u7FA4\u6587\u4EF6\uFF08\u9ED8\u8BA4\u540C\u6B65\u672C\u7FA4\uFF1B\u5EFA\u8BAE\u5148\u5728 WebUI \u914D\u7F6E\u540C\u6B65\u5BF9\u8C61\u7FA4\uFF09",
|
|
816
|
+
"- \u793A\u4F8B\uFF1A#\u540C\u6B65\u7FA4\u6587\u4EF6 123456",
|
|
817
|
+
"- #\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",
|
|
818
|
+
"- #\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",
|
|
819
|
+
"- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --keep\uFF1A\u5F3A\u5236\u4FDD\u7559\u76EE\u5F55\u7ED3\u6784\uFF08\u8986\u76D6\u7FA4\u914D\u7F6E flat\uFF09",
|
|
820
|
+
"- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --folder <id>\uFF1A\u4ECE\u6307\u5B9A\u6587\u4EF6\u5939\u5F00\u59CB",
|
|
821
|
+
"- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --max <n>\uFF1A\u6700\u591A\u5904\u7406 n \u4E2A\u6587\u4EF6",
|
|
822
|
+
"- #\u540C\u6B65\u7FA4\u6587\u4EF6 123456 --concurrency <n>\uFF1A\u5E76\u53D1\u6570\uFF08\u4F1A\u8986\u76D6\u7FA4\u914D\u7F6E\u7684\u5E76\u53D1\uFF09",
|
|
823
|
+
"- #\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",
|
|
824
|
+
"- #\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",
|
|
825
|
+
"\u524D\u7F6E\uFF1A\u8BF7\u5148\u5728\u914D\u7F6E\u6587\u4EF6\u586B\u5199 openlistBaseUrl/openlistUsername/openlistPassword\u3002"
|
|
826
|
+
].join("\n");
|
|
827
|
+
var syncGroupFilesToOpenListCore = async (params) => {
|
|
828
|
+
const {
|
|
829
|
+
bot,
|
|
830
|
+
groupId,
|
|
831
|
+
folderId,
|
|
832
|
+
maxFiles,
|
|
833
|
+
flat,
|
|
834
|
+
targetDir,
|
|
835
|
+
mode,
|
|
836
|
+
urlConcurrency,
|
|
837
|
+
transferConcurrency,
|
|
838
|
+
fileTimeoutSec,
|
|
839
|
+
retryTimes,
|
|
840
|
+
retryDelayMs,
|
|
841
|
+
progressReportEvery,
|
|
842
|
+
downloadLimitKbps,
|
|
843
|
+
uploadLimitKbps,
|
|
844
|
+
report
|
|
845
|
+
} = params;
|
|
846
|
+
const cfg = config();
|
|
847
|
+
const baseUrl = String(cfg.openlistBaseUrl ?? "").trim();
|
|
848
|
+
const username = String(cfg.openlistUsername ?? "").trim();
|
|
849
|
+
const password = String(cfg.openlistPassword ?? "").trim();
|
|
850
|
+
const defaultTargetDir = String(cfg.openlistTargetDir ?? "/").trim();
|
|
851
|
+
if (!baseUrl || !username || !password) {
|
|
852
|
+
throw new Error([
|
|
853
|
+
"\u8BF7\u5148\u914D\u7F6E OpenList \u4FE1\u606F\uFF08openlistBaseUrl/openlistUsername/openlistPassword\uFF09",
|
|
854
|
+
`\u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E\uFF1A${dir.ConfigDir}/config.json`
|
|
855
|
+
].join("\n"));
|
|
856
|
+
}
|
|
857
|
+
const davBaseUrl = buildOpenListDavBaseUrl(baseUrl);
|
|
858
|
+
if (!davBaseUrl) throw new Error("OpenList \u5730\u5740\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 openlistBaseUrl");
|
|
859
|
+
const finalTargetDir = normalizePosixPath(targetDir || defaultTargetDir);
|
|
860
|
+
const auth = buildOpenListAuthHeader(username, password);
|
|
861
|
+
const safeFileTimeoutSec = Math.min(
|
|
862
|
+
MAX_FILE_TIMEOUT_SEC,
|
|
863
|
+
Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeoutSec || 0))
|
|
864
|
+
);
|
|
865
|
+
const safeProgressReportEvery = Math.max(
|
|
866
|
+
0,
|
|
867
|
+
Math.floor(typeof progressReportEvery === "number" ? progressReportEvery : DEFAULT_PROGRESS_REPORT_EVERY)
|
|
868
|
+
);
|
|
869
|
+
const rateDown = Math.max(0, Math.floor(typeof downloadLimitKbps === "number" ? downloadLimitKbps : 0));
|
|
870
|
+
const rateUp = Math.max(0, Math.floor(typeof uploadLimitKbps === "number" ? uploadLimitKbps : 0));
|
|
871
|
+
const effectiveRateLimitBytesPerSec = (() => {
|
|
872
|
+
const a = rateDown > 0 ? rateDown * 1024 : 0;
|
|
873
|
+
const b = rateUp > 0 ? rateUp * 1024 : 0;
|
|
874
|
+
if (a > 0 && b > 0) return Math.min(a, b);
|
|
875
|
+
return a > 0 ? a : b > 0 ? b : 0;
|
|
876
|
+
})();
|
|
877
|
+
const transferTimeoutMs = safeFileTimeoutSec * 1e3;
|
|
878
|
+
const webdavTimeoutMs = 15e3;
|
|
879
|
+
const dirEnsurer = createWebDavDirEnsurer(davBaseUrl, auth, webdavTimeoutMs);
|
|
880
|
+
const groupContact = karin.contactGroup(groupId);
|
|
881
|
+
const state = readGroupSyncState(groupId);
|
|
882
|
+
return await withGroupSyncLock(groupId, async () => {
|
|
883
|
+
report && await report([
|
|
884
|
+
"\u5F00\u59CB\u540C\u6B65\u7FA4\u6587\u4EF6\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019...",
|
|
885
|
+
`- \u7FA4\u53F7\uFF1A${groupId}`,
|
|
886
|
+
`- \u76EE\u6807\u76EE\u5F55\uFF1A${finalTargetDir}`,
|
|
887
|
+
`- \u6A21\u5F0F\uFF1A${mode === "incremental" ? "\u589E\u91CF" : "\u5168\u91CF"}`,
|
|
888
|
+
`- \u4FDD\u7559\u76EE\u5F55\u7ED3\u6784\uFF1A${flat ? "\u5426" : "\u662F"}`,
|
|
889
|
+
`- \u5E76\u53D1\uFF1AURL ${urlConcurrency} / \u4F20\u8F93 ${transferConcurrency}`
|
|
890
|
+
].join("\n"));
|
|
891
|
+
let list = [];
|
|
892
|
+
list = await collectAllGroupFiles(bot, groupId, folderId);
|
|
893
|
+
const limitedList = typeof maxFiles === "number" && Number.isFinite(maxFiles) && maxFiles > 0 ? list.slice(0, Math.floor(maxFiles)) : list;
|
|
894
|
+
const candidates = limitedList.map((item) => {
|
|
895
|
+
const remotePath = buildRemotePathForItem(item, finalTargetDir, flat);
|
|
896
|
+
return { item, remotePath };
|
|
897
|
+
});
|
|
898
|
+
let skipped = 0;
|
|
899
|
+
let needSync = mode === "incremental" ? candidates.filter(({ item, remotePath }) => {
|
|
900
|
+
const prev = state.files[remotePath];
|
|
901
|
+
const ok = isSameSyncedFile(prev, item);
|
|
902
|
+
if (ok) skipped++;
|
|
903
|
+
return !ok;
|
|
904
|
+
}) : candidates;
|
|
905
|
+
if (mode === "incremental" && needSync.length) {
|
|
906
|
+
const dirs = Array.from(new Set(
|
|
907
|
+
needSync.map(({ remotePath }) => normalizePosixPath(path.posix.dirname(remotePath)))
|
|
908
|
+
));
|
|
909
|
+
const namesByDir = /* @__PURE__ */ new Map();
|
|
910
|
+
await runWithConcurrency(dirs, 3, async (dirPath) => {
|
|
911
|
+
const names = await webdavPropfindListNames({
|
|
912
|
+
davBaseUrl,
|
|
913
|
+
auth,
|
|
914
|
+
dirPath,
|
|
915
|
+
timeoutMs: webdavTimeoutMs
|
|
916
|
+
});
|
|
917
|
+
namesByDir.set(dirPath, names);
|
|
918
|
+
});
|
|
919
|
+
needSync = needSync.filter(({ remotePath }) => {
|
|
920
|
+
const dirPath = normalizePosixPath(path.posix.dirname(remotePath));
|
|
921
|
+
const base = path.posix.basename(remotePath);
|
|
922
|
+
const names = namesByDir.get(dirPath);
|
|
923
|
+
if (names && names.has(base)) {
|
|
924
|
+
skipped++;
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
if (!needSync.length) {
|
|
931
|
+
report && await report(`\u6CA1\u6709\u9700\u8981\u540C\u6B65\u7684\u6587\u4EF6\uFF08\u589E\u91CF\u6A21\u5F0F\u5DF2\u8DF3\u8FC7 ${skipped} \u6761\uFF09\u3002`);
|
|
932
|
+
return { total: limitedList.length, skipped, urlOk: 0, ok: 0, fail: 0 };
|
|
933
|
+
}
|
|
934
|
+
const urlErrors = [];
|
|
935
|
+
await runWithConcurrency(needSync, Math.max(1, Math.floor(urlConcurrency) || 1), async ({ item }) => {
|
|
936
|
+
try {
|
|
937
|
+
item.url = await resolveGroupFileUrl(bot, groupContact, groupId, item);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
urlErrors.push({ fileId: item.fileId, path: item.path, message: formatErrorMessage(error) });
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
const withUrl = needSync.filter(({ item }) => typeof item.url === "string" && item.url).map((v) => v);
|
|
943
|
+
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`);
|
|
944
|
+
if (!withUrl.length) {
|
|
945
|
+
report && await report("\u6CA1\u6709\u53EF\u7528\u7684\u4E0B\u8F7DURL\uFF0C\u65E0\u6CD5\u540C\u6B65\u5230 OpenList");
|
|
946
|
+
return { total: limitedList.length, skipped, urlOk: 0, ok: 0, fail: 0 };
|
|
947
|
+
}
|
|
948
|
+
const syncErrors = [];
|
|
949
|
+
let okCount = 0;
|
|
950
|
+
const shouldRefreshUrl = (message) => {
|
|
951
|
+
return /403|URL已过期|url已过期|url可能已失效|需要重新获取|下载超时/.test(message);
|
|
952
|
+
};
|
|
953
|
+
const transferOne = async (sourceUrl, targetUrl) => {
|
|
954
|
+
await downloadAndUploadByWebDav({
|
|
955
|
+
sourceUrl,
|
|
956
|
+
targetUrl,
|
|
957
|
+
auth,
|
|
958
|
+
timeoutMs: transferTimeoutMs,
|
|
959
|
+
rateLimitBytesPerSec: effectiveRateLimitBytesPerSec || void 0
|
|
960
|
+
});
|
|
961
|
+
};
|
|
962
|
+
report && await report("\u5F00\u59CB\u4E0B\u8F7D\u5E76\u4E0A\u4F20\u5230 OpenList\uFF0C\u8BF7\u7A0D\u5019...");
|
|
963
|
+
const transferInitial = Math.min(MAX_TRANSFER_CONCURRENCY, Math.max(1, Math.floor(transferConcurrency) || 1));
|
|
964
|
+
const adaptiveTransfer = effectiveRateLimitBytesPerSec <= 0;
|
|
965
|
+
const transferFn = async ({ item, remotePath }, index) => {
|
|
966
|
+
logger.info(`[\u7FA4\u6587\u4EF6\u540C\u6B65][${groupId}] \u540C\u6B65\u4E2D (${index + 1}/${withUrl.length}): ${item.path}`);
|
|
967
|
+
const remoteDir = normalizePosixPath(path.posix.dirname(remotePath));
|
|
968
|
+
const targetUrl = `${davBaseUrl}${encodePathForUrl(remotePath)}`;
|
|
969
|
+
let lastError;
|
|
970
|
+
let succeeded = false;
|
|
971
|
+
const attempts = Math.max(0, Math.floor(retryTimes) || 0) + 1;
|
|
972
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
973
|
+
try {
|
|
974
|
+
await dirEnsurer.ensureDir(remoteDir);
|
|
975
|
+
const currentUrl = item.url;
|
|
976
|
+
if (!currentUrl) throw new Error("\u7F3A\u5C11\u4E0B\u8F7DURL");
|
|
977
|
+
await transferOne(currentUrl, targetUrl);
|
|
978
|
+
okCount++;
|
|
979
|
+
succeeded = true;
|
|
980
|
+
lastError = void 0;
|
|
981
|
+
state.files[remotePath] = {
|
|
982
|
+
fileId: item.fileId,
|
|
983
|
+
size: item.size,
|
|
984
|
+
uploadTime: item.uploadTime,
|
|
985
|
+
md5: item.md5,
|
|
986
|
+
sha1: item.sha1,
|
|
987
|
+
syncedAt: Date.now()
|
|
988
|
+
};
|
|
989
|
+
break;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
lastError = error;
|
|
992
|
+
const msg = formatErrorMessage(error);
|
|
993
|
+
if (attempt < attempts) {
|
|
994
|
+
if (shouldRefreshUrl(msg)) {
|
|
995
|
+
try {
|
|
996
|
+
item.url = await resolveGroupFileUrl(bot, groupContact, groupId, item);
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const delay = Math.max(0, Math.floor(retryDelayMs) || 0) * Math.pow(2, attempt - 1);
|
|
1001
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!succeeded && lastError) {
|
|
1007
|
+
syncErrors.push({
|
|
1008
|
+
path: item.path,
|
|
1009
|
+
fileId: item.fileId,
|
|
1010
|
+
message: formatErrorMessage(lastError)
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
if (safeProgressReportEvery > 0 && (index + 1) % safeProgressReportEvery === 0) {
|
|
1014
|
+
report && await report(`\u540C\u6B65\u8FDB\u5EA6\uFF1A${index + 1}/${withUrl.length}\uFF08\u6210\u529F ${okCount}\uFF09`);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
if (adaptiveTransfer) {
|
|
1018
|
+
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`);
|
|
1019
|
+
await runWithAdaptiveConcurrency(withUrl, {
|
|
1020
|
+
initial: transferInitial,
|
|
1021
|
+
max: MAX_TRANSFER_CONCURRENCY,
|
|
1022
|
+
fn: transferFn,
|
|
1023
|
+
onAdjust: (current, reason) => {
|
|
1024
|
+
logger.info(`[\u7FA4\u6587\u4EF6\u540C\u6B65][${groupId}] \u81EA\u9002\u5E94\u8C03\u6574\u4F20\u8F93\u5E76\u53D1=${current} (${reason})`);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
} else {
|
|
1028
|
+
await runWithConcurrency(withUrl, transferInitial, transferFn);
|
|
1029
|
+
}
|
|
1030
|
+
state.lastSyncAt = Date.now();
|
|
1031
|
+
writeGroupSyncState(groupId, state);
|
|
1032
|
+
const failCount = withUrl.length - okCount;
|
|
1033
|
+
report && await report(`\u540C\u6B65\u5B8C\u6210\uFF1A\u6210\u529F ${okCount} / \u5931\u8D25 ${failCount}\uFF08\u589E\u91CF\u8DF3\u8FC7 ${skipped}\uFF09`);
|
|
1034
|
+
if (failCount) {
|
|
1035
|
+
const preview = syncErrors.slice(0, 5).map((it) => `${it.path} (${it.fileId})
|
|
1036
|
+
${it.message}`).join("\n\n");
|
|
1037
|
+
report && await report(`\u5931\u8D25\u793A\u4F8B\uFF08\u524D5\u6761\uFF09\uFF1A
|
|
1038
|
+
${preview}`);
|
|
1039
|
+
}
|
|
1040
|
+
if (urlErrors.length) {
|
|
1041
|
+
const preview = urlErrors.slice(0, 5).map((it) => `${it.path ?? ""} (${it.fileId ?? ""})
|
|
1042
|
+
${it.message}`).join("\n\n");
|
|
1043
|
+
report && await report(`URL\u83B7\u53D6\u5931\u8D25\u793A\u4F8B\uFF08\u524D5\u6761\uFF09\uFF1A
|
|
1044
|
+
${preview}`);
|
|
1045
|
+
}
|
|
1046
|
+
return { total: limitedList.length, skipped, urlOk: withUrl.length, ok: okCount, fail: failCount };
|
|
1047
|
+
});
|
|
1048
|
+
};
|
|
1049
|
+
var syncGroupFilesToOpenList = karin.command(/^#?(同步群文件|群文件同步)(.*)$/i, async (e) => {
|
|
1050
|
+
const argsText = e.msg.replace(/^#?(同步群文件|群文件同步)/i, "");
|
|
1051
|
+
const {
|
|
1052
|
+
groupId: parsedGroupId,
|
|
1053
|
+
folderId: parsedFolderId,
|
|
1054
|
+
maxFiles: parsedMaxFiles,
|
|
1055
|
+
concurrency,
|
|
1056
|
+
concurrencySpecified,
|
|
1057
|
+
flat,
|
|
1058
|
+
flatSpecified,
|
|
1059
|
+
to,
|
|
1060
|
+
toSpecified,
|
|
1061
|
+
timeoutSec,
|
|
1062
|
+
timeoutSpecified,
|
|
1063
|
+
mode: forcedMode,
|
|
1064
|
+
help
|
|
1065
|
+
} = parseSyncArgs(argsText);
|
|
1066
|
+
if (help) {
|
|
1067
|
+
await e.reply(syncHelpText);
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
const cfg = config();
|
|
1071
|
+
const groupId = parsedGroupId ?? (e.isGroup ? e.groupId : void 0);
|
|
1072
|
+
if (!groupId) {
|
|
1073
|
+
await e.reply(`\u7F3A\u5C11\u7FA4\u53F7\u53C2\u6570
|
|
1074
|
+
|
|
1075
|
+
${syncHelpText}`);
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
if (e.isGroup) {
|
|
1079
|
+
const role = e.sender?.role;
|
|
1080
|
+
if (role !== "owner" && role !== "admin") {
|
|
1081
|
+
await e.reply("\u8BF7\u7FA4\u7BA1\u7406\u5458\u4F7F\u7528\u8BE5\u547D\u4EE4\uFF08\u6216\u5728\u79C1\u804A\u4E2D\u64CD\u4F5C\uFF09\u3002");
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const defaults = cfg.groupSyncDefaults ?? {};
|
|
1086
|
+
const targetCfg = getGroupSyncTarget(cfg, groupId);
|
|
1087
|
+
const mode = forcedMode ?? (targetCfg ? normalizeSyncMode(targetCfg?.mode, normalizeSyncMode(defaults?.mode, "incremental")) : "full");
|
|
1088
|
+
const urlC = concurrencySpecified ? typeof concurrency === "number" ? concurrency : 3 : typeof targetCfg?.urlConcurrency === "number" ? targetCfg.urlConcurrency : typeof defaults?.urlConcurrency === "number" ? defaults.urlConcurrency : 3;
|
|
1089
|
+
const transferC = concurrencySpecified ? typeof concurrency === "number" ? concurrency : 3 : typeof targetCfg?.transferConcurrency === "number" ? targetCfg.transferConcurrency : typeof defaults?.transferConcurrency === "number" ? defaults.transferConcurrency : 3;
|
|
1090
|
+
const fileTimeout = timeoutSpecified ? typeof timeoutSec === "number" ? timeoutSec : 600 : typeof targetCfg?.fileTimeoutSec === "number" ? targetCfg.fileTimeoutSec : typeof defaults?.fileTimeoutSec === "number" ? defaults.fileTimeoutSec : 600;
|
|
1091
|
+
const retryTimes = typeof targetCfg?.retryTimes === "number" ? targetCfg.retryTimes : typeof defaults?.retryTimes === "number" ? defaults.retryTimes : 2;
|
|
1092
|
+
const retryDelayMs = typeof targetCfg?.retryDelayMs === "number" ? targetCfg.retryDelayMs : typeof defaults?.retryDelayMs === "number" ? defaults.retryDelayMs : 1500;
|
|
1093
|
+
const progressEvery = typeof targetCfg?.progressReportEvery === "number" ? targetCfg.progressReportEvery : typeof defaults?.progressReportEvery === "number" ? defaults.progressReportEvery : DEFAULT_PROGRESS_REPORT_EVERY;
|
|
1094
|
+
const downloadLimitKbps = typeof targetCfg?.downloadLimitKbps === "number" ? targetCfg.downloadLimitKbps : typeof defaults?.downloadLimitKbps === "number" ? defaults.downloadLimitKbps : 0;
|
|
1095
|
+
const uploadLimitKbps = typeof targetCfg?.uploadLimitKbps === "number" ? targetCfg.uploadLimitKbps : typeof defaults?.uploadLimitKbps === "number" ? defaults.uploadLimitKbps : 0;
|
|
1096
|
+
const targetDir = normalizePosixPath(
|
|
1097
|
+
toSpecified ? to ?? "" : String(targetCfg?.targetDir ?? "").trim() || path.posix.join(String(cfg.openlistTargetDir ?? "/"), String(groupId))
|
|
1098
|
+
);
|
|
1099
|
+
const finalFlat = flatSpecified ? Boolean(flat) : typeof targetCfg?.flat === "boolean" ? targetCfg.flat : Boolean(defaults?.flat ?? false);
|
|
1100
|
+
const folderId = parsedFolderId ?? targetCfg?.sourceFolderId;
|
|
1101
|
+
const maxFiles = typeof parsedMaxFiles === "number" ? parsedMaxFiles : targetCfg?.maxFiles;
|
|
1102
|
+
try {
|
|
1103
|
+
await syncGroupFilesToOpenListCore({
|
|
1104
|
+
bot: e.bot,
|
|
1105
|
+
groupId,
|
|
1106
|
+
folderId,
|
|
1107
|
+
maxFiles,
|
|
1108
|
+
flat: Boolean(finalFlat),
|
|
1109
|
+
targetDir,
|
|
1110
|
+
mode,
|
|
1111
|
+
urlConcurrency: Math.max(1, Math.floor(urlC) || 1),
|
|
1112
|
+
transferConcurrency: Math.max(1, Math.floor(transferC) || 1),
|
|
1113
|
+
fileTimeoutSec: Math.min(MAX_FILE_TIMEOUT_SEC, Math.max(MIN_FILE_TIMEOUT_SEC, Math.floor(fileTimeout) || MIN_FILE_TIMEOUT_SEC)),
|
|
1114
|
+
retryTimes: Math.max(0, Math.floor(retryTimes) || 0),
|
|
1115
|
+
retryDelayMs: Math.max(0, Math.floor(retryDelayMs) || 0),
|
|
1116
|
+
progressReportEvery: Math.max(0, Math.floor(progressEvery) || 0),
|
|
1117
|
+
downloadLimitKbps: Math.max(0, Math.floor(downloadLimitKbps) || 0),
|
|
1118
|
+
uploadLimitKbps: Math.max(0, Math.floor(uploadLimitKbps) || 0),
|
|
1119
|
+
report: (msg) => e.reply(msg)
|
|
1120
|
+
});
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
logger.error(error);
|
|
1123
|
+
await e.reply(formatErrorMessage(error));
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
return true;
|
|
1127
|
+
}, {
|
|
1128
|
+
priority: 9999,
|
|
1129
|
+
log: true,
|
|
1130
|
+
name: "\u540C\u6B65\u7FA4\u6587\u4EF6\u5230OpenList",
|
|
1131
|
+
permission: "all"
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
export {
|
|
1135
|
+
exportGroupFiles,
|
|
1136
|
+
syncGroupFilesToOpenListCore,
|
|
1137
|
+
syncGroupFilesToOpenList
|
|
1138
|
+
};
|