// ==UserScript==
// @name 云盘秒传工具(百度/夸克/天翼/123/光鸭)
// @version 2026.04.16
// @description 云盘秒传工具支持百度/夸克/天翼/123/光鸭
// @run-at document-idle
// @match https://pan.quark.cn/*
// @match https://drive.quark.cn/*
// @match https://cloud.189.cn/web/*
// @match *://*.123pan.com/*
// @match *://*.123pan.cn/*
// @match https://www.123pan.com/*
// @match https://www.123pan.com/
// @match http://www.123pan.com/*
// @match https://123pan.com/*
// @match https://123pan.com/
// @match http://123pan.com/*
// @match https://pan.baidu.com/*
// @match https://guangyapan.com/*
// @match https://*.guangyapan.com/*
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @connect drive.quark.cn
// @connect drive-pc.quark.cn
// @connect pc-api.uc.cn
// @connect cloud.189.cn
// @connect pan.baidu.com
// @connect api.guangyapan.com
// @namespace https://greasyfork.org/users/1224613
// ==/UserScript==
// @ts-nocheck
(function () {
"use strict";
const SCRIPT_VERSION = "2026.04.16";
const GUANGYA_API_BASE = "https://api.guangyapan.com";
const GUANGYA_CODE_RES_TOKEN_INSTANT = 156;
const GUANGYA_CODE_DIR_EXISTS = 159;
const GUANGYA_URL_GET_RES_CENTER_TOKEN = `${GUANGYA_API_BASE}/nd.bizuserres.s/v1/get_res_center_token`;
const GUANGYA_URL_CREATE_DIR = `${GUANGYA_API_BASE}/nd.bizuserres.s/v1/file/create_dir`;
const GUANGYA_URL_DELETE_UPLOAD_TASK = `${GUANGYA_API_BASE}/nd.bizuserres.s/v1/file/delete_upload_task`;
const KEY_GUANGYA_ACCESS_TOKEN = "guangya_guangyapan_access_token";
const BTN_GUANGYA_IMPORT_ID = "guangya-guangya-import-json-btn";
const INVALID_ETAG_POLICY = localStorage.getItem("guangya_etag_policy") || "skip";
const BTN_ID = "guangya-json-generator-btn";
const GUANGYA_BTN_TYPO_STYLE_ID = "guangya-rapid-json-typography";
const GUANGYA_TIANYI_SHARE_STYLE_ID = "guangya-tianyi-share-flex";
const BODY_SELECTOR = "body";
const PREFER_123_TOOLBAR = localStorage.getItem("guangya_123_use_toolbar") !== "0";
function guangyaJsonDetail(obj) {
try {
return JSON.stringify(obj, null, 2);
} catch {
return String(obj);
}
}
/** 秒传导入 JSON 文件/文本结构校验(不含 token、不校验每条 md5 是否合法) */
function validateGuangyaImportJsonShape(text) {
const trim = String(text || "").trim();
if (!trim) {
return { ok: false, message: "文件内容为空" };
}
let obj;
try {
obj = JSON.parse(trim);
} catch {
return {
ok: false,
message: "不是合法 JSON(请检查编码、逗号、引号与括号是否匹配)",
};
}
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
return {
ok: false,
message: "JSON 顶层须为对象 { … },不能是数组或纯数字/字符串",
};
}
if (!Array.isArray(obj.files)) {
return {
ok: false,
message:
'须包含数组字段 files,例如:{"files":[{"path":"…","etag":"…","size":0},…]}',
};
}
if (obj.files.length === 0) {
return { ok: false, message: "files 数组长度为 0,没有可导入条目" };
}
const badIdx = [];
for (let i = 0; i < obj.files.length; i++) {
const it = obj.files[i];
if (it == null || typeof it !== "object" || Array.isArray(it)) {
badIdx.push(i);
if (badIdx.length >= 8) break;
}
}
if (badIdx.length) {
const sample = badIdx.slice(0, 5).join("、");
const more =
badIdx.length > 5
? ` 等共 ${badIdx.length} 处`
: "";
return {
ok: false,
message: `files 中第 ${sample} 项${more}不是对象,每项应为 { path/name, etag/md5, size }`,
};
}
return { ok: true, fileCount: obj.files.length };
}
function guangyaParseImportResultCounts(resp, submittedCount, skipCount) {
const d = resp && resp.data;
if (d != null && typeof d === "object" && !Array.isArray(d)) {
const failedMd5s = d.failedMd5s ?? d.failed_md5s;
if (Array.isArray(failedMd5s)) {
const failedSet = new Set(
failedMd5s
.map((m) =>
String(m == null ? "" : m)
.trim()
.toLowerCase(),
)
.filter((x) => x.length > 0),
);
const transferFail = failedSet.size;
const transferOk = Math.max(0, submittedCount - transferFail);
return {
successCount: transferOk,
failCount: transferFail + skipCount,
};
}
const s =
d.successCount ??
d.success_num ??
d.okCount ??
d.successTotal;
const f =
d.failCount ??
d.failedCount ??
d.fail_num ??
d.errorCount ??
d.failTotal;
if (typeof s === "number" && typeof f === "number") {
return {
successCount: s,
failCount: f + skipCount,
};
}
if (typeof s === "number") {
return {
successCount: s,
failCount:
(typeof f === "number" ? f : 0) + skipCount,
};
}
if (typeof f === "number") {
return {
successCount: submittedCount,
failCount: f + skipCount,
};
}
const arr = d.details || d.results || d.list;
if (Array.isArray(arr) && arr.length) {
const failed = arr.filter((x) => {
if (!x || typeof x !== "object") return false;
if (x.success === false || x.ok === false) return true;
if (x.status === "fail" || x.status === "failed")
return true;
if (x.code != null && x.code !== 0) return true;
return false;
}).length;
return {
successCount: arr.length - failed,
failCount: failed + skipCount,
};
}
}
return {
successCount: submittedCount,
failCount: skipCount,
};
}
function guangyaBasenameFromPath(filePath) {
const s = String(filePath || "").replace(/\\/g, "/");
const parts = s.split("/").filter(Boolean);
return parts.length ? parts[parts.length - 1] : "file";
}
function guangyaDirSegmentsFromPath(filePath) {
const s = String(filePath || "").replace(/\\/g, "/");
const parts = s
.split("/")
.map((p) => String(p || "").trim())
.filter((p) => p.length > 0);
if (parts.length <= 1) return [];
return parts.slice(0, -1);
}
function guangyaPickFileIdFromObj(obj) {
if (!obj || typeof obj !== "object") return "";
const id =
obj.fileId ??
obj.fileid ??
obj.file_id ??
obj.id ??
obj.dirId ??
obj.dir_id ??
obj.folderId ??
obj.folder_id;
return id == null ? "" : String(id);
}
function guangyaNormMd5Token(m) {
return String(m == null ? "" : m)
.trim()
.toLowerCase();
}
/** 单批:用 data.failedMd5s 与当批 chunk 对应出路径(无路径时仅 md5) */
function guangyaTransferFailRowsFromResp(resp, chunk) {
const d = resp && resp.data;
if (!d || typeof d !== "object" || Array.isArray(d)) return [];
const raw = d.failedMd5s ?? d.failed_md5s;
if (!Array.isArray(raw) || raw.length === 0) return [];
const failedSet = new Set(
raw.map(guangyaNormMd5Token).filter((x) => x.length > 0),
);
const rows = [];
const covered = new Set();
for (const row of chunk) {
const m = guangyaNormMd5Token(row.md5);
if (!m || !failedSet.has(m)) continue;
const key = `${m}\t${String(row.filePath || "")}`;
if (covered.has(key)) continue;
covered.add(key);
rows.push({
md5: row.md5 || m,
filePath: String(row.filePath || ""),
});
}
for (const m of failedSet) {
if (chunk.some((r) => guangyaNormMd5Token(r.md5) === m)) continue;
rows.push({ md5: m, filePath: "" });
}
return rows;
}
/**
* @param {{ interfaceLines?: string[]; transferRows?: { md5: string; filePath: string }[]; mkdirSkipLines?: string[]; validateSkipLines?: string[]; transferExtraLines?: string[] }} parts
*/
function formatGuangyaImportCopyReport(parts) {
const iface = parts.interfaceLines;
const xfer = parts.transferRows || [];
const mkdirSkip = parts.mkdirSkipLines || [];
const validateSkip = parts.validateSkipLines || [];
const extra = parts.transferExtraLines || [];
const lines = [];
lines.push("========== 接口调用失败 ==========");
if (iface && iface.length) lines.push(...iface.map((x) => String(x)));
else lines.push("(无)");
lines.push("");
lines.push("========== 秒传失败 ==========");
if (xfer.length) {
for (const r of xfer) {
const p = String(r.filePath || "").trim();
lines.push(p || "—");
}
} else lines.push("(无)");
if (extra.length) {
lines.push(...extra.map((x) => String(x)));
}
if (mkdirSkip.length) {
lines.push("");
lines.push("========== 创建目录失败(未进入秒传) ==========");
lines.push(...mkdirSkip.map((x) => String(x)));
}
if (validateSkip.length) {
lines.push("");
lines.push("========== 校验未通过(未提交接口) ==========");
lines.push(...validateSkip.map((x) => String(x)));
}
return lines.join("\n");
}
/** 导出秒传 JSON 文件名:秒传_YYYYMMDD_HHmmss.json */
function makeRapidTransferExportFilename() {
const d = new Date();
const p = (n) => String(n).padStart(2, "0");
const date =
d.getFullYear() +
p(d.getMonth() + 1) +
p(d.getDate());
const time =
p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
return `秒传_${date}_${time}.json`;
}
function guangyaCreateTraceparent() {
const hex = (len) => {
const u = new Uint8Array(len);
crypto.getRandomValues(u);
return [...u].map((b) => b.toString(16).padStart(2, "0")).join("");
};
return `00-${hex(16)}-${hex(8)}-01`;
}
const helper = {
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const raw = parts.pop().split(";").shift();
if (raw == null || raw === "") return null;
try {
return decodeURIComponent(raw);
} catch {
return raw;
}
}
return null;
},
get(url, headers = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url,
headers,
onload: (resp) => {
if (resp.status >= 200 && resp.status < 300) {
resolve(resp.responseText);
return;
}
reject(new Error(`请求失败: ${resp.status}`));
},
onerror: () => reject(new Error("网络请求失败")),
});
});
},
postJson(url, data, headers = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url,
headers: {
"Content-Type": "application/json;charset=utf-8",
...headers,
},
data: JSON.stringify(data),
onload: (resp) => {
try {
resolve(JSON.parse(resp.responseText));
} catch {
reject(new Error("响应解析失败"));
}
},
onerror: () => reject(new Error("网络请求失败")),
});
});
},
/**
* @param {string} url
* @param {object} data
* @param {string} bearerToken
* @param {{ allowedBusinessCodes?: number[] }} [options] 若业务 code 非 0,需在此列出仍视为成功的 code(如 156)
*/
postJsonGuangya(url, data, bearerToken, options) {
const tok = String(bearerToken || "").replace(/^Bearer\s+/i, "").trim();
const traceparent = guangyaCreateTraceparent();
const origin =
typeof location !== "undefined" ? location.origin : "";
const referer =
typeof location !== "undefined" ? location.href : "";
const requestHeaders = {
"Content-Type": "application/json;charset=utf-8",
Authorization: `Bearer ${tok}`,
dt: "4",
traceparent,
...(origin ? { Origin: origin } : {}),
...(referer ? { Referer: referer } : {}),
};
const headersForLog = { ...requestHeaders };
const bodyStr = JSON.stringify(data);
const attachDetail = (err, extra) => {
err.guangyaDetail = guangyaJsonDetail({
summary: err.message,
url,
method: "POST",
requestHeaders: headersForLog,
requestBody: data,
...extra,
});
return err;
};
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url,
headers: requestHeaders,
data: bodyStr,
onload: (resp) => {
const raw = resp.responseText || "";
let parsedBody;
try {
parsedBody = JSON.parse(raw);
} catch {
parsedBody = null;
}
if (resp.status < 200 || resp.status >= 300) {
let short = raw;
try {
const ej = JSON.parse(raw);
short =
ej.msg ||
ej.message ||
(typeof ej.error === "string"
? ej.error
: "") ||
ej.error_description ||
raw;
} catch {
/* empty */
}
reject(
attachDetail(
new Error(
`HTTP ${resp.status}${
short
? `: ${String(short).slice(0, 600)}`
: ""
}`,
),
{
httpStatus: resp.status,
responseBody: parsedBody ?? raw,
},
),
);
return;
}
let j;
try {
j = JSON.parse(resp.responseText);
} catch {
reject(
attachDetail(
new Error("响应解析失败"),
{
httpStatus: resp.status,
responseTextPreview: raw.slice(
0,
12000,
),
},
),
);
return;
}
const allowed = options && options.allowedBusinessCodes;
const businessOk =
j.code == null ||
j.code === 0 ||
(Array.isArray(allowed) && allowed.includes(j.code));
if (j && j.code != null && !businessOk) {
reject(
attachDetail(
new Error(
j.msg || `接口错误 code=${j.code}`,
),
{
httpStatus: resp.status,
responseBody: j,
},
),
);
return;
}
resolve({
data: j,
requestLog: {
url,
method: "POST",
requestHeaders: headersForLog,
requestBody: data,
},
});
},
onerror: () => {
reject(
attachDetail(new Error("网络请求失败"), {
networkError: true,
}),
);
},
});
});
},
postQuarkPcJson(url, data, headers = {}) {
const QUARK_UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch";
const origin =
typeof location !== "undefined" ? location.origin : "https://pan.quark.cn";
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url,
headers: {
"Content-Type": "application/json;charset=utf-8",
"User-Agent": QUARK_UA,
Origin: origin,
Referer: `${origin}/`,
Dnt: "",
"Cache-Control": "no-cache",
Pragma: "no-cache",
Expires: "0",
...headers,
},
data: JSON.stringify(data),
onload: (resp) => {
try {
resolve(JSON.parse(resp.responseText));
} catch {
reject(new Error("响应解析失败"));
}
},
onerror: () => reject(new Error("网络请求失败")),
});
});
},
getCachedQuarkCookie() {
return GM_getValue("guangya_quark_cookie", "");
},
saveCachedQuarkCookie(cookie) {
GM_setValue("guangya_quark_cookie", cookie);
},
decodeMd5(md5) {
const s = (md5 == null ? "" : String(md5)).trim();
if (!s) return "";
if (/^[a-fA-F0-9]{32}$/.test(s)) return s;
try {
const normalized = s.replace(/-/g, "+").replace(/_/g, "/");
const padLen = (4 - (normalized.length % 4)) % 4;
const padded = normalized + "=".repeat(padLen);
const binary = atob(padded);
if (binary.length !== 16) return "";
return Array.from(binary, (ch) =>
ch.charCodeAt(0).toString(16).padStart(2, "0"),
).join("");
} catch {
return "";
}
},
formatSize(bytes) {
const n = Number(bytes) || 0;
if (n < 1024) return `${n} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = n / 1024;
let i = 0;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i++;
}
return `${value.toFixed(2)} ${units[i]}`;
},
normalizeFilePath(path) {
const clean = (path || "").replace(/^\/+/, "");
return `/${clean}`;
},
normalizeEtag(etag) {
return (etag || "").trim();
},
showQuarkCookieInputDialog(onSave, currentCookie = "") {
const existing = document.getElementById("guangya-quark-cookie-dialog");
if (existing) existing.remove();
const dialog = document.createElement("div");
dialog.id = "guangya-quark-cookie-dialog";
dialog.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.55);z-index:2147483647;display:flex;align-items:center;justify-content:center;">
<div style="background:#fff;padding:28px;border-radius:10px;width:82%;max-width:780px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.2);">
<div style="font-size:17px;font-weight:700;margin-bottom:12px;">设置夸克网盘 Cookie</div>
<div style="font-size:13px;color:#666;margin-bottom:12px;">
打开浏览器开发者工具 (F12) → Network → 找任意夸克请求 → 复制完整 Cookie 值<br>
<strong>需包含:__puus、__pus、ctoken 等关键字段</strong>
</div>
<textarea id="guangya-quark-cookie-input"
placeholder="粘贴完整 Cookie 字符串,例如:ctoken=xxx; __puus=xxx; __pus=xxx; ..."
style="flex:1;min-height:180px;padding:10px;border:1px solid #d9d9d9;border-radius:6px;font-family:monospace;font-size:12px;resize:vertical;">${currentCookie}</textarea>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:14px;">
<button id="guangya-quark-cookie-save" style="padding:8px 22px;background:#ff9800;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">保存</button>
<button id="guangya-quark-cookie-cancel" style="padding:8px 22px;background:#e0e0e0;color:#333;border:none;border-radius:6px;cursor:pointer;">取消</button>
</div>
</div>
</div>`;
document.body.appendChild(dialog);
const cookieSaveBtn = document.getElementById("guangya-quark-cookie-save");
const cookieCancelBtn = document.getElementById("guangya-quark-cookie-cancel");
const cookieInput = document.getElementById("guangya-quark-cookie-input");
if (cookieSaveBtn) {
cookieSaveBtn.onclick = () => {
const cookie =
cookieInput && "value" in cookieInput
? String(cookieInput.value).trim()
: "";
if (!cookie) {
alert("Cookie 不能为空");
return;
}
this.saveCachedQuarkCookie(cookie);
dialog.remove();
if (onSave) onSave(cookie);
};
}
if (cookieCancelBtn) {
cookieCancelBtn.onclick = () => {
dialog.remove();
if (onSave) onSave(null);
};
}
},
showLoadingDialog(title, msg = "") {
const existing = document.getElementById("guangya-loading-dialog");
if (existing) existing.remove();
const dialog = document.createElement("div");
dialog.id = "guangya-loading-dialog";
dialog.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:2147483646;display:flex;align-items:center;justify-content:center;">
<div style="background:#fff;padding:32px 36px;border-radius:10px;min-width:300px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.18);">
<div style="font-size:17px;font-weight:700;margin-bottom:10px;">${title}</div>
<div id="guangya-loading-msg" style="font-size:13px;color:#888;">${msg}</div>
</div>
</div>`;
document.body.appendChild(dialog);
},
updateLoadingMsg(msg) {
const el = document.getElementById("guangya-loading-msg");
if (el) el.textContent = msg;
},
closeLoadingDialog() {
const el = document.getElementById("guangya-loading-dialog");
if (el) el.remove();
},
showResultDialog(jsonData, shareTitle = "") {
const existing = document.getElementById("guangya-result-dialog");
if (existing) existing.remove();
let currentJson = jsonData;
const jsonStr = JSON.stringify(jsonData, null, 2);
const checkboxHtml = shareTitle ? `
<div style="margin-bottom:14px;padding:10px 14px;background:#fff8f0;border-radius:6px;border:1px solid #ffe0b2;">
<label style="display:flex;align-items:center;cursor:pointer;gap:8px;">
<input type="checkbox" id="guangya-commonpath-checkbox" style="width:15px;height:15px;cursor:pointer;">
<span style="font-size:13px;color:#333;">设置 commonPath 为分享标题:<strong>${shareTitle}</strong></span>
</label>
</div>` : "";
const dialog = document.createElement("div");
dialog.id = "guangya-result-dialog";
dialog.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:2147483646;display:flex;align-items:center;justify-content:center;">
<div style="background:#fff;padding:28px;border-radius:10px;width:82%;max-width:820px;max-height:84vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.2);">
<div style="font-size:17px;font-weight:700;margin-bottom:14px;">秒传 JSON 生成成功</div>
${checkboxHtml}
<div style="flex:1;overflow:auto;background:#f7f7f7;padding:14px;border-radius:6px;font-family:monospace;font-size:12px;margin-bottom:14px;">
<pre id="guangya-json-preview" style="margin:0;white-space:pre-wrap;word-break:break-all;">${jsonStr}</pre>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="guangya-result-copy" style="padding:8px 20px;background:#ff9800;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">复制 JSON</button>
<button id="guangya-result-download" style="padding:8px 20px;background:#4caf50;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">下载文件</button>
<button id="guangya-result-close" style="padding:8px 20px;background:#e0e0e0;color:#333;border:none;border-radius:6px;cursor:pointer;">关闭</button>
</div>
</div>
</div>`;
document.body.appendChild(dialog);
const getJsonStr = () => JSON.stringify(currentJson, null, 2);
if (shareTitle) {
const commonPathCb = document.getElementById(
"guangya-commonpath-checkbox",
);
if (commonPathCb && "checked" in commonPathCb) {
commonPathCb.onchange = () => {
currentJson = Object.assign({}, jsonData, {
commonPath: commonPathCb.checked
? shareTitle + "/"
: "",
});
const pre = document.getElementById("guangya-json-preview");
if (pre) pre.textContent = getJsonStr();
};
}
}
const resultCopyBtn = document.getElementById("guangya-result-copy");
if (resultCopyBtn) {
resultCopyBtn.onclick = () => {
GM_setClipboard(getJsonStr());
resultCopyBtn.textContent = "已复制!";
setTimeout(() => {
resultCopyBtn.textContent = "复制 JSON";
}, 1500);
};
}
const resultDlBtn = document.getElementById("guangya-result-download");
if (resultDlBtn) {
resultDlBtn.onclick = () => {
const text = getJsonStr();
const blob = new Blob([text], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = makeRapidTransferExportFilename();
a.click();
URL.revokeObjectURL(url);
};
}
const resultCloseBtn = document.getElementById("guangya-result-close");
if (resultCloseBtn) {
resultCloseBtn.onclick = () => dialog.remove();
}
},
makeJson(files) {
const invalidPaths = [""];
invalidPaths.length = 0;
const normalizedRaw = files
.filter((f) => f && f.path)
.map((f) => {
const path = this.normalizeFilePath(f.path);
const etag = this.normalizeEtag(f.etag || "");
const hasEtag = etag.length > 0;
if (!hasEtag) {
invalidPaths.push(path);
}
return {
etag:
hasEtag
? etag
: INVALID_ETAG_POLICY === "empty"
? ""
: etag,
size: String(f.size ?? 0),
path,
__valid: hasEtag,
};
});
if (invalidPaths.length > 0 && INVALID_ETAG_POLICY === "error") {
const preview = invalidPaths.slice(0, 8).join("\n");
const more =
invalidPaths.length > 8
? `\n... 另有 ${invalidPaths.length - 8} 个文件`
: "";
throw new Error(
`发现 ${invalidPaths.length} 个文件的 etag 为空。\n请检查数据源或改为 empty 策略。\n${preview}${more}`,
);
}
const normalized =
INVALID_ETAG_POLICY === "skip"
? normalizedRaw
.filter((f) => f.__valid)
.map(({ __valid, ...rest }) => rest)
: normalizedRaw.map(({ __valid, ...rest }) => rest);
const totalSize = normalized.reduce(
(sum, f) => sum + (Number(f.size) || 0),
0,
);
return {
scriptVersion: SCRIPT_VERSION,
totalFilesCount: normalized.length,
totalSize,
formattedTotalSize: this.formatSize(totalSize),
files: normalized,
};
},
output(jsonData) {
const text = JSON.stringify(jsonData, null, 2);
GM_setClipboard(text);
const okDownload = confirm(
"JSON 已复制到剪贴板。\n点击“确定”下载 .json 文件,点击“取消”仅保留复制结果。",
);
if (okDownload) {
const blob = new Blob([text], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = makeRapidTransferExportFilename();
a.click();
URL.revokeObjectURL(url);
}
},
};
function getQuarkFileListPropsFromDom(fileListDom) {
if (!fileListDom) return null;
const key = Object.keys(fileListDom).find(
(k) =>
k.startsWith("__reactFiber$") ||
k.startsWith("__reactInternalInstance$") ||
k.startsWith("__reactContainer$"),
);
if (!key) return null;
const rootFiber = fileListDom[key];
function scanTree(fiber, maxSteps) {
const q = [fiber];
let steps = 0;
while (q.length && steps < maxSteps) {
const cur = q.shift();
steps++;
if (!cur) continue;
const mp = cur.memoizedProps || cur.pendingProps;
if (
mp &&
Array.isArray(mp.list) &&
Object.prototype.hasOwnProperty.call(mp, "selectedRowKeys")
) {
return mp;
}
let ch = cur.child;
while (ch) {
q.push(ch);
ch = ch.sibling;
}
}
return null;
}
const fromTree = scanTree(rootFiber, 600);
if (fromTree) return fromTree;
let f = rootFiber;
for (let i = 0; i < 80 && f; i++) {
const mp = f.memoizedProps || f.pendingProps;
if (
mp &&
Array.isArray(mp.list) &&
Object.prototype.hasOwnProperty.call(mp, "selectedRowKeys")
) {
return mp;
}
f = f.return;
}
try {
const getCompFiber = (fib) => {
let p = fib;
while (p && typeof p.type === "string") p = p.return;
return p;
};
const fiber = rootFiber;
const reactObj = fiber._currentElement
? fiber._currentElement._owner?._instance
: getCompFiber(fiber)?.stateNode;
const props = reactObj?.props;
if (props && Array.isArray(props.list)) return props;
} catch {
// ignore
}
return null;
}
/** 从 DOM 取 React 内部实例,读取 props */
function findQuarkReactInstance(dom, traverseUp) {
const reactKey = Object.keys(dom).find(
(k) =>
k.startsWith("__reactFiber$") ||
k.startsWith("__reactInternalInstance$"),
);
if (!reactKey) return null;
const domFiber = dom[reactKey];
if (domFiber == null) return null;
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber =
compFiber &&
compFiber._currentElement &&
compFiber._currentElement._owner;
}
return compFiber && compFiber._instance;
}
const GetCompFiber = (fiber) => {
let parentFiber = fiber.return;
while (parentFiber && typeof parentFiber.type === "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber && GetCompFiber(compFiber);
}
return (compFiber && (compFiber.stateNode || compFiber)) || null;
}
function getReactFiberFromDom(el) {
if (!el || typeof el !== "object") return null;
const key = Object.keys(el).find(
(k) =>
k.startsWith("__reactFiber$") ||
k.startsWith("__reactInternalInstance$"),
);
return key ? el[key] : null;
}
/**
* 深度遍历 React Fiber,读取 Ant Design Table 的选中项(含 rowSelection)。
* 虚拟列表下 DOM 只能看到视口内勾选行,React 侧往往仍有完整 selectedRowKeys。
*/
function scanFiberForAntdTableSelection(rootFiber, maxNodes) {
const limit = maxNodes == null ? 12000 : maxNodes;
if (!rootFiber) return null;
const q = [rootFiber];
let nodes = 0;
/** @type {{ keys: string[]; unselectedKeys: string[] } | null} */
let best = null;
while (q.length && nodes < limit) {
const cur = q.shift();
nodes++;
if (!cur) continue;
const mp = cur.memoizedProps || cur.pendingProps;
if (mp && typeof mp === "object") {
let keys = null;
let unselected = null;
if (Array.isArray(mp.selectedRowKeys)) {
keys = mp.selectedRowKeys;
unselected = mp.unselectedRowKeys;
}
const rs = mp.rowSelection;
if ((!keys || !keys.length) && rs && typeof rs === "object") {
if (Array.isArray(rs.selectedRowKeys)) {
keys = rs.selectedRowKeys;
unselected = rs.unselectedRowKeys;
}
}
if (Array.isArray(keys) && keys.length) {
const ks = keys
.map((k) => String(k == null ? "" : k).trim())
.filter(Boolean);
if (ks.length) {
const us = Array.isArray(unselected)
? unselected
.map((k) => String(k == null ? "" : k).trim())
.filter(Boolean)
: [];
if (!best || ks.length > best.keys.length) {
best = { keys: ks, unselectedKeys: us };
}
}
}
}
let ch = cur.child;
while (ch) {
q.push(ch);
ch = ch.sibling;
}
}
return best;
}
function pan123ParseUiSelectionCount() {
const t = String(document.body?.innerText || "");
const m = t.match(/已选择\s*(\d+)\s*项/);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}
/** 勾选状态追踪 */
class TableRowSelector123 {
selectedRowKeys = [""];
unselectedRowKeys = [""];
isSelectAll = false;
_inited = false;
observer;
originalCreateElement;
init() {
if (this._inited) return;
this._inited = true;
const originalCreateElement = document.createElement.bind(document);
this.originalCreateElement = originalCreateElement;
const self = this;
document.createElement = function (tagName, options) {
const element = originalCreateElement(tagName, options);
if (tagName.toLowerCase() !== "input") return element;
const mo = new MutationObserver(() => {
if (element.classList.contains("ant-checkbox-input")) {
if (element.getAttribute("aria-label") === "Select all") {
self.unselectedRowKeys = [];
self.selectedRowKeys = [];
self.isSelectAll = false;
self._bindSelectAllEvent(element);
} else {
const input = element;
input.addEventListener("click", function () {
const row = input.closest(".ant-table-row");
const rowKey = row?.getAttribute("data-row-key");
if (!rowKey) return;
if (self.isSelectAll) {
if (!this.checked) {
if (!self.unselectedRowKeys.includes(rowKey)) {
self.unselectedRowKeys.push(rowKey);
}
} else {
const idx = self.unselectedRowKeys.indexOf(rowKey);
if (idx > -1) self.unselectedRowKeys.splice(idx, 1);
}
} else if (this.checked) {
if (!self.selectedRowKeys.includes(rowKey)) {
self.selectedRowKeys.push(rowKey);
}
} else {
const idx = self.selectedRowKeys.indexOf(rowKey);
if (idx > -1) self.selectedRowKeys.splice(idx, 1);
}
});
}
}
mo.disconnect();
});
mo.observe(element, {
attributes: true,
attributeFilter: ["class", "aria-label"],
});
return element;
};
}
_bindSelectAllEvent(checkbox) {
const targetElement = checkbox.parentElement;
if (!targetElement) return;
const self = this;
this.observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === "attributes" && m.attributeName === "class") {
onClassChanged(targetElement);
}
}
});
this.observer.observe(targetElement, {
attributes: true,
attributeOldValue: true,
attributeFilter: ["class"],
});
function onClassChanged(el) {
if (el.classList.contains("ant-checkbox-indeterminate")) return;
if (el.classList.contains("ant-checkbox-checked")) {
self.isSelectAll = true;
self.unselectedRowKeys = [];
self.selectedRowKeys = [];
} else {
self.isSelectAll = false;
self.selectedRowKeys = [];
self.unselectedRowKeys = [];
}
}
}
_syncFromDomFallback() {
const rowInputs = Array.from(
document.querySelectorAll(
".ant-table-body input[type='checkbox'], .ant-table-tbody input[type='checkbox'], [class*='list'] input[type='checkbox'], [class*='table'] input[type='checkbox']",
),
);
if (!rowInputs.length) {
const hasAnyChecked = !!document.querySelector(
".ant-table-body input[type='checkbox']:checked, .ant-table-tbody input[type='checkbox']:checked, [class*='list'] input[type='checkbox']:checked, [class*='table'] input[type='checkbox']:checked, [role='checkbox'][aria-checked='true']",
);
if (!hasAnyChecked) {
this.isSelectAll = false;
this.selectedRowKeys = [];
this.unselectedRowKeys = [];
}
return;
}
const checkedKeys = [""];
checkedKeys.length = 0;
const uncheckedKeys = [""];
uncheckedKeys.length = 0;
const readRowKey = (input) => {
const row =
input.closest("[data-row-key]") ||
input.closest("[data-file-id]") ||
input.closest("[data-fileid]") ||
input.closest("[data-id]") ||
input.closest("[data-key]") ||
input.closest("tr") ||
input.closest("li") ||
input.closest("[class*='row']") ||
input.closest("[class*='item']");
if (!row) return "";
const attrs = [
"data-row-key",
"data-file-id",
"data-fileid",
"data-id",
"data-key",
"row-key",
];
for (const a of attrs) {
const v = String(row.getAttribute(a) || "").trim();
if (v) return v;
}
return "";
};
for (const input of rowInputs) {
if (input.closest("thead")) continue;
const rowKey = readRowKey(input);
if (!rowKey) continue;
if (input.checked) checkedKeys.push(rowKey);
else uncheckedKeys.push(rowKey);
}
const headerChecked =
!!document.querySelector(
".ant-table-header .ant-checkbox-wrapper .ant-checkbox-checked, thead .ant-checkbox-wrapper .ant-checkbox-checked",
) ||
!!document.querySelector(
".ant-table-header .ant-checkbox-input:checked, thead .ant-checkbox-input:checked, .ant-table-header input[type='checkbox']:checked, thead input[type='checkbox']:checked",
);
if (headerChecked) {
this.isSelectAll = true;
this.selectedRowKeys = [];
this.unselectedRowKeys = uncheckedKeys;
return;
}
this.isSelectAll = false;
this.selectedRowKeys = checkedKeys;
this.unselectedRowKeys = [];
}
_syncFromReactFallback() {
const selectors = [
".ant-table-wrapper",
".ant-table",
"[class*='table']",
"[class*='list']",
"[class*='file']",
];
const checkedCount = document.querySelectorAll(
".ant-table-body input[type='checkbox']:checked, .ant-table-tbody input[type='checkbox']:checked, [class*='list'] input[type='checkbox']:checked, [class*='table'] input[type='checkbox']:checked, [role='checkbox'][aria-checked='true']",
).length;
for (const s of selectors) {
const nodes = document.querySelectorAll(s);
for (const dom of nodes) {
let props = getQuarkFileListPropsFromDom(dom);
if (!props) {
for (let up = 0; up < 10; up++) {
const reactObj = findQuarkReactInstance(dom, up);
const p = reactObj && reactObj.props;
if (!p) continue;
if (Array.isArray(p.selectedRowKeys)) {
props = p;
break;
}
}
}
const rawKeys =
props &&
(Array.isArray(props.selectedRowKeys)
? props.selectedRowKeys
: Array.isArray(props.selectedKeys)
? props.selectedKeys
: null);
if (!rawKeys || rawKeys.length === 0) continue;
const keys = rawKeys
.map((k) => String(k == null ? "" : k).trim())
.filter(Boolean);
if (!keys.length) continue;
this.isSelectAll = false;
this.selectedRowKeys = keys;
this.unselectedRowKeys = [];
return;
}
}
if (checkedCount > 0 && this.selectedRowKeys.length === 0 && !this.isSelectAll) {
// 有勾选但未拿到 row key:保留原状态,交由上层提示用户/继续兜底。
}
}
_pickTextFromRow(row) {
if (!row) return "";
const candidates = row.querySelectorAll(
"[title], [data-title], [data-name], .file-name, .name, a, span, div",
);
for (const el of candidates) {
const t1 = String(el.getAttribute?.("title") || "").trim();
if (t1 && t1.length <= 300) return t1;
const t2 = String(el.getAttribute?.("data-name") || "").trim();
if (t2 && t2.length <= 300) return t2;
const t3 = String(el.textContent || "").trim();
if (
t3 &&
t3.length <= 300 &&
!/^(\d+(\.\d+)?\s*(kb|mb|gb|tb)|\d{4}-\d{1,2}-\d{1,2})$/i.test(t3)
) {
return t3;
}
}
return "";
}
_getRowKeyFromRow(row) {
if (!row) return "";
const attrs = [
"data-row-key",
"data-file-id",
"data-fileid",
"data-id",
"data-key",
"row-key",
"file-id",
];
for (const a of attrs) {
const v = String(row.getAttribute?.(a) || "").trim();
if (v) return v;
}
return "";
}
_collectSelectedRows() {
const rows = new Set();
const collectRow = (node) => {
if (!node || !node.closest) return;
if (node.closest("thead")) return;
const row =
node.closest("[data-row-key]") ||
node.closest("[data-file-id]") ||
node.closest("[data-fileid]") ||
node.closest("[data-id]") ||
node.closest("[data-key]") ||
node.closest("tr") ||
node.closest("li") ||
node.closest("[class*='row']") ||
node.closest("[class*='item']");
if (!row || row.closest("thead")) return;
rows.add(row);
};
const checked = document.querySelectorAll(
".ant-table-body input[type='checkbox']:checked, .ant-table-tbody input[type='checkbox']:checked, [class*='list'] input[type='checkbox']:checked, [class*='table'] input[type='checkbox']:checked, [role='checkbox'][aria-checked='true']",
);
for (const el of checked) collectRow(el);
const selectedRows = document.querySelectorAll(
".ant-table-row-selected, [aria-selected='true'], [class*='row'][class*='selected'], [class*='item'][class*='selected']",
);
for (const el of selectedRows) collectRow(el);
return [...rows];
}
getSelectedNameHints() {
const names = new Set();
const rows = this._collectSelectedRows();
for (const row of rows) {
const name = this._pickTextFromRow(row);
if (name) names.add(name);
}
return [...names];
}
getSelectedRowKeyHints() {
const keys = new Set();
const rows = this._collectSelectedRows();
for (const row of rows) {
const k = this._getRowKeyFromRow(row);
if (k) keys.add(k);
}
return [...keys];
}
_scanBestReactTableSelection() {
const roots = document.querySelectorAll(
".ant-table-wrapper, .ant-table, [class*='ant-table']",
);
/** @type {{ keys: string[]; unselectedKeys: string[] } | null} */
let best = null;
for (let i = 0; i < roots.length; i++) {
const fiber = getReactFiberFromDom(roots[i]);
if (!fiber) continue;
const hit = scanFiberForAntdTableSelection(fiber, 12000);
if (hit && (!best || hit.keys.length > best.keys.length)) {
best = hit;
}
}
const appRoot =
document.getElementById("root") ||
document.getElementById("app") ||
document.body;
const rf = getReactFiberFromDom(appRoot);
if (rf) {
const hit = scanFiberForAntdTableSelection(rf, 16000);
if (hit && (!best || hit.keys.length > best.keys.length)) {
best = hit;
}
}
return best;
}
getSelection() {
this._syncFromDomFallback();
const domIsAll = this.isSelectAll;
const domKeys = [...this.selectedRowKeys];
const domUnselected = [...this.unselectedRowKeys];
const reactSel = this._scanBestReactTableSelection();
if (domIsAll) {
if (
reactSel &&
reactSel.unselectedKeys &&
reactSel.unselectedKeys.length > domUnselected.length
) {
this.unselectedRowKeys = [...reactSel.unselectedKeys];
}
return {
isSelectAll: true,
selectedRowKeys: [],
unselectedRowKeys: [...this.unselectedRowKeys],
};
}
if (reactSel && reactSel.keys.length) {
const merged = Array.from(
new Set(
[...reactSel.keys, ...domKeys].map((k) => String(k).trim()),
),
).filter(Boolean);
this.selectedRowKeys = merged;
this.unselectedRowKeys = [];
this.isSelectAll = false;
} else if (!this.isSelectAll && this.selectedRowKeys.length === 0) {
this._syncFromReactFallback();
}
const uiN = pan123ParseUiSelectionCount();
if (
uiN != null &&
!this.isSelectAll &&
this.selectedRowKeys.length > 0 &&
this.selectedRowKeys.length < uiN
) {
try {
console.warn(
`[秒传工具][123] 页面显示已选择 ${uiN} 项,但只解析到 ${this.selectedRowKeys.length} 个文件 ID。可尝试滚动列表让选中行进入视口后重试,或刷新页面。`,
);
} catch {
/* ignore */
}
}
return {
isSelectAll: this.isSelectAll,
selectedRowKeys: [...this.selectedRowKeys],
unselectedRowKeys: [...this.unselectedRowKeys],
};
}
}
function pan123PickInfoList(data) {
if (!data || typeof data !== "object") return [];
return data.InfoList || data.infoList || data.file_infos || data.fileInfos || [];
}
class Pan123Api {
host = "";
referer = "";
refresh() {
this.host = `${location.protocol}//${location.host}`;
this.referer = document.location.href;
}
get authToken() {
return localStorage.getItem("authorToken") || "";
}
get loginUuid() {
return localStorage.getItem("LoginUuid") || "";
}
async sendRequest(method, path, queryParams, body) {
this.refresh();
const qs = new URLSearchParams(queryParams).toString();
const url = `${this.host}${path}?${qs}`;
const headers = {
"Content-Type": "application/json;charset=UTF-8",
Authorization: `Bearer ${this.authToken}`,
platform: "web",
"App-Version": "3",
LoginUuid: this.loginUuid,
Origin: this.host,
Referer: this.referer,
};
/** @type {RequestInit & { body?: any }} */
const init = {
method,
headers,
credentials: "include",
};
if (method !== "GET" && method !== "HEAD" && body != null) {
init.body = body;
}
const res = await fetch(url, init);
const data = await res.json();
if (data.code !== 0) {
throw new Error(data.message || `123云盘 API 错误: ${data.code}`);
}
return data;
}
async getParentFileId() {
const raw = sessionStorage.getItem("filePath");
if (!raw) {
throw new Error(
"无法获取当前目录,请在 123 云盘「我的文件」列表页使用",
);
}
const homeFilePath = JSON.parse(raw).homeFilePath;
const parent = homeFilePath[homeFilePath.length - 1] ?? 0;
return String(parent);
}
async getOnePageFileList(parentFileId, page) {
const urlParams = {
driveId: "0",
limit: "100",
next: "0",
orderBy: "file_name",
orderDirection: "asc",
parentFileId: String(parentFileId),
trashed: "false",
SearchData: "",
Page: String(page),
OnlyLookAbnormalFile: "0",
event: "homeListFile",
operateType: "1",
inDirectSpace: "false",
};
return this.sendRequest("GET", "/b/api/file/list/new", urlParams, "");
}
async getFileList(parentFileId) {
let infoList = [];
const first = await this.getOnePageFileList(parentFileId, 1);
infoList = infoList.concat(pan123PickInfoList(first.data));
const totalRaw =
first.data?.Total ??
first.data?.total ??
first.data?.count ??
infoList.length;
const total = Number.isFinite(Number(totalRaw))
? Number(totalRaw)
: infoList.length;
if (total > 100) {
const times = Math.ceil(total / 100);
for (let i = 2; i <= times; i++) {
await helper.sleep(500);
const page = await this.getOnePageFileList(parentFileId, i);
infoList = infoList.concat(pan123PickInfoList(page.data));
}
}
return { data: { InfoList: infoList, total } };
}
async getFileInfoBatch(idList) {
const batchSize = 100;
const rows = [];
for (let i = 0; i < idList.length; i += batchSize) {
const batch = idList.slice(i, i + batchSize);
const fileIdList = batch.map((fileId) => ({ fileId }));
const data = await this.sendRequest(
"POST",
"/b/api/file/info",
{},
JSON.stringify({ fileIdList }),
);
const list = pan123PickInfoList(data.data);
for (const file of list) {
rows.push(pan123NormalizeFileInfo(file));
}
await helper.sleep(200);
}
return rows;
}
}
const pan123Selector = new TableRowSelector123();
const pan123Api = new Pan123Api();
const pan123GetEtagLike = (file) => {
const raw =
file?.Etag ??
file?.etag ??
file?.md5 ??
file?.MD5 ??
file?.Md5 ??
file?.fileMd5 ??
file?.FileMd5 ??
file?.hash ??
"";
return String(raw || "").trim();
};
const pan123NormalizeFileInfo = (file) => {
const fileName = String(file?.FileName ?? file?.file_name ?? file?.name ?? "").trim();
const sizeRaw = file?.Size ?? file?.size ?? 0;
const typeRaw = file?.Type ?? file?.type ?? file?.file_type ?? 0;
const fileId = file?.FileId ?? file?.fileId ?? file?.file_id ?? "";
const size = Number.isFinite(Number(sizeRaw)) ? Number(sizeRaw) : 0;
const type = Number.isFinite(Number(typeRaw)) ? Number(typeRaw) : 0;
return {
fileName,
etag: pan123GetEtagLike(file),
size,
type,
fileId,
};
};
const pan123 = {
initSelector() {
pan123Selector.init();
},
is123Host() {
return /(^|\.)123pan\.(com|cn)$/i.test(location.hostname);
},
async collectFiles() {
const sel = pan123Selector.getSelection();
const selectedRowKeyHints = sel.isSelectAll
? []
: pan123Selector.getSelectedRowKeyHints();
const selectedNameHints =
!sel.isSelectAll && sel.selectedRowKeys.length === 0
? pan123Selector.getSelectedNameHints()
: [];
if (
!sel.isSelectAll &&
sel.selectedRowKeys.length === 0 &&
selectedRowKeyHints.length === 0 &&
selectedNameHints.length === 0
) {
throw new Error("请先在 123 云盘勾选要导出的文件或文件夹");
}
const fileInfoList = [];
let folderRows = [];
if (sel.isSelectAll) {
const parentId = await pan123Api.getParentFileId();
const { data } = await pan123Api.getFileList(parentId);
const mapped = (data.InfoList || []).map((file) =>
pan123NormalizeFileInfo(file),
);
const files = mapped.filter((f) => f.type !== 1);
files
.filter(
(f) =>
!sel.unselectedRowKeys.includes(f.fileId.toString()),
)
.forEach((f) => {
fileInfoList.push({ ...f, path: f.fileName });
});
folderRows = mapped
.filter((f) => f.type === 1)
.filter(
(f) =>
!sel.unselectedRowKeys.includes(f.fileId.toString()),
);
} else {
const selectedKeys = Array.from(
new Set(
[...sel.selectedRowKeys, ...selectedRowKeyHints].map((k) =>
String(k == null ? "" : k).trim(),
),
),
).filter(Boolean);
if (selectedKeys.length > 0) {
const allFileInfo = await pan123Api.getFileInfoBatch(
selectedKeys,
);
allFileInfo
.filter((info) => info.type !== 1)
.forEach((f) => {
fileInfoList.push({ ...f, path: f.fileName });
});
folderRows = allFileInfo.filter((info) => info.type === 1);
} else {
// 兜底:部分新页面拿不到 row key,但可从已勾选行文本匹配当前目录项。
const parentId = await pan123Api.getParentFileId();
const { data } = await pan123Api.getFileList(parentId);
const mapped = (data.InfoList || []).map((file) =>
pan123NormalizeFileInfo(file),
);
const nameSet = new Set(selectedNameHints);
const picked = mapped.filter((f) => nameSet.has(f.fileName));
picked
.filter((info) => info.type !== 1)
.forEach((f) => {
fileInfoList.push({ ...f, path: f.fileName });
});
folderRows = picked.filter((info) => info.type === 1);
}
}
const walkFolder = async (parentFileId, prefix) => {
const { data } = await pan123Api.getFileList(parentFileId);
const mapped = (data.InfoList || []).map((file) =>
pan123NormalizeFileInfo(file),
);
mapped
.filter((f) => f.type !== 1)
.forEach((f) => {
fileInfoList.push({
...f,
path: prefix + f.fileName,
});
});
const dirs = mapped.filter((f) => f.type === 1);
for (const folder of dirs) {
await helper.sleep(300);
await walkFolder(
folder.fileId,
`${prefix}${folder.fileName}/`,
);
}
};
for (const folder of folderRows) {
await helper.sleep(300);
await walkFolder(folder.fileId, `${folder.fileName}/`);
}
if (!fileInfoList.length) {
throw new Error("没有可导出的文件(文件夹内为空或未选中文)");
}
return fileInfoList.map((f) => ({
path: f.path || f.fileName,
etag: String(f.etag || ""),
size: Number(f.size || 0),
}));
},
};
const quark = {
/** 当前目录面包屑 */
getCurrentPath() {
try {
const urlParams = new URLSearchParams(window.location.search);
const dirFid = urlParams.get("dir_fid");
if (!dirFid || dirFid === "0") {
return "";
}
const breadcrumb = document.querySelector(".breadcrumb-list");
if (breadcrumb) {
const items = breadcrumb.querySelectorAll(".breadcrumb-item");
const pathParts = [""];
pathParts.length = 0;
for (let i = 1; i < items.length; i++) {
const text = items[i].textContent.trim();
if (text) pathParts.push(text);
}
return pathParts.join("/");
}
return "";
} catch (e) {
return "";
}
},
getSelectedList() {
const isSharePath = /^\/(s|share)\//.test(location.pathname);
try {
if (typeof unsafeWindow !== "undefined") {
const apiList = isSharePath
? unsafeWindow.shareUser?.getSelectedFileList?.()
: unsafeWindow.file?.getSelectedFileList?.();
if (Array.isArray(apiList) && apiList.length) {
return apiList;
}
}
} catch {
/* ignore */
}
const a = document.getElementsByClassName("file-list")[0];
const b = document.querySelector(".file-list");
const c = document.querySelector("[class*='file-list']");
const candidates = [a, b, c].filter((el, i, arr) => {
if (!el) return false;
if (i === 1) return el !== arr[0];
if (i === 2) return el !== arr[0] && el !== arr[1];
return true;
});
for (const fileListDom of candidates) {
let props = getQuarkFileListPropsFromDom(fileListDom);
if (!props) {
for (let up = 0; up < 10; up++) {
const reactObj = findQuarkReactInstance(fileListDom, up);
const p = reactObj && reactObj.props;
if (
p &&
Array.isArray(p.list) &&
p.selectedRowKeys !== undefined
) {
props = p;
break;
}
}
}
if (props && Array.isArray(props.list)) {
const list = props.list || [];
const selectedKeys = props.selectedRowKeys || [];
return list.filter((it) => selectedKeys.includes(it.fid));
}
}
return [];
},
async getFolderFiles(folderId, folderPath = "") {
const files = [];
let page = 1;
const size = 50;
while (true) {
const url = `https://drive-pc.quark.cn/1/clouddrive/file/sort?pr=ucpro&fr=pc&pdir_fid=${folderId}&_page=${page}&_size=${size}&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc`;
const text = String(await helper.get(url));
const result = JSON.parse(text);
if (result?.code !== 0 || !Array.isArray(result?.data?.list)) break;
const list = result.data.list || [];
for (const item of list) {
const path = folderPath
? `${folderPath}/${item.file_name}`
: item.file_name;
if (item.dir) {
files.push(...(await this.getFolderFiles(item.fid, path)));
} else if (item.file) {
files.push({ ...item, path });
}
}
if (list.length < size) break;
page++;
}
return files;
},
async getHomeFiles() {
const selected = this.getSelectedList();
if (!selected.length) throw new Error("请先勾选夸克文件/文件夹");
const currentPath = this.getCurrentPath();
const all = [];
for (const item of selected) {
if (item.file) {
const filePath = currentPath
? `${currentPath}/${item.file_name}`
: item.file_name;
all.push({ ...item, path: filePath });
} else if (item.dir) {
const folderPath = currentPath
? `${currentPath}/${item.file_name}`
: item.file_name;
all.push(...(await this.getFolderFiles(item.fid, folderPath)));
}
}
if (!all.length) throw new Error("未找到可导出的夸克文件");
const fileOnly = all.filter((f) => f.file === true);
/** @type {Record<string, string>} */
const pathMap = {};
fileOnly.forEach((f) => (pathMap[f.fid] = f.path || f.file_name));
/** @type {{ path: string; etag: string; size: number }[]} */
const output = [];
const needFetch = [];
for (const f of fileOnly) {
const rawEtag = f.etag || f.md5 || f.hash || "";
const s = (rawEtag || "").toString().trim();
if (s) {
const dec = helper.decodeMd5(s);
output.push({
path: pathMap[f.fid] || f.file_name,
etag: (dec || s).toLowerCase(),
size: Number(f.size || 0),
});
} else {
needFetch.push(f);
}
}
const batchSize = 15;
for (let i = 0; i < needFetch.length; i += batchSize) {
const batch = needFetch.slice(i, i + batchSize);
const fids = batch.map((b) => b.fid);
let resp;
try {
resp = await helper.postQuarkPcJson(
"https://drive.quark.cn/1/clouddrive/file/download?pr=ucpro&fr=pc",
{ fids },
);
} catch {
continue;
}
if (resp && resp.code === 31001) throw new Error("夸克账号未登录");
if (!resp || resp.code !== 0) {
continue;
}
for (const f of resp.data || []) {
const raw = String(f.md5 || f.hash || f.etag || "").trim();
const decoded = helper.decodeMd5(raw);
const etagOut = (decoded || raw).toLowerCase();
if (!etagOut) {
continue;
}
output.push({
path: pathMap[f.fid] || f.file_name,
etag: etagOut,
size: Number(f.size || 0),
});
}
await helper.sleep(1000);
}
if (!output.length) {
throw new Error(
"未能得到任何文件的 etag:列表与 download 接口均无 md5/etag。请确认已登录 pan.quark.cn / drive.quark.cn,并勾选需导出的文件后重试。",
);
}
return output;
},
async getShareToken(shareId, cookie) {
const resp = /** @type {any} */ (await helper.postJson(
"https://pc-api.uc.cn/1/clouddrive/share/sharepage/token",
{ pwd_id: shareId, passcode: "" },
{
Cookie: cookie,
Referer: "https://pan.quark.cn/",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
},
));
if (resp?.code === 31001)
throw new Error("夸克分享页 Cookie 无效,请重新输入");
if (resp?.code !== 0 || !resp?.data?.stoken) {
throw new Error(`夸克分享 token 获取失败:${resp?.message || resp?.code}`);
}
return { stoken: resp.data.stoken, title: resp.data.title || "" };
},
async scanShareFiles(
shareId,
stoken,
cookie,
parentFid,
path = "",
recursive = true,
) {
const result = [];
let page = 1;
while (true) {
const url = `https://pc-api.uc.cn/1/clouddrive/share/sharepage/detail?pwd_id=${shareId}&stoken=${encodeURIComponent(
stoken,
)}&pdir_fid=${parentFid}&_page=${page}&_size=100&pr=ucpro&fr=pc`;
const text = String(
await helper.get(url, {
Cookie: cookie,
Referer: "https://pan.quark.cn/",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
}),
);
const data = JSON.parse(text);
if (data?.code !== 0 || !Array.isArray(data?.data?.list)) break;
const list = data.data.list || [];
for (const item of list) {
const itemPath = path ? `${path}/${item.file_name}` : item.file_name;
if (item.dir) {
if (recursive) {
result.push(
...(await this.scanShareFiles(
shareId,
stoken,
cookie,
item.fid,
itemPath,
true,
)),
);
}
} else if (item.file) {
result.push({
fid: item.fid,
token: item.share_fid_token,
size: Number(item.size || 0),
path: itemPath,
});
}
}
if (list.length < 100) break;
page++;
}
return result;
},
async getShareMd5Map(shareId, stoken, cookie, fileItems) {
/** @type {Record<string, string>} */
const md5Map = {};
const batchSize = 10;
for (let i = 0; i < fileItems.length; i += batchSize) {
const batch = fileItems.slice(i, i + batchSize);
const fids = batch.map((b) => b.fid);
const fidsToken = batch.map((b) => b.token);
const resp = /** @type {any} */ (await helper.postJson(
`https://pc-api.uc.cn/1/clouddrive/file/download?pr=ucpro&fr=pc&uc_param_str=&__dt=${Math.floor(Math.random() * 4 + 1) * 60 * 1000}&__t=${Date.now()}`,
{ fids, pwd_id: shareId, stoken, fids_token: fidsToken },
{
Cookie: cookie,
Referer: "https://pan.quark.cn/",
Origin: "https://pan.quark.cn",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.14.2 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch",
Accept: "application/json, text/plain, */*",
},
));
if (resp?.code === 0 && resp?.data) {
const arr = Array.isArray(resp.data) ? resp.data : [resp.data];
arr.forEach((it, idx) => {
const fid = fids[idx];
const raw = String(it.md5 || it.hash || it.etag || "").trim();
const dec = helper.decodeMd5(raw);
md5Map[fid] = dec || raw;
});
} else {
fids.forEach((fid) => (md5Map[fid] = ""));
}
await helper.sleep(700);
}
return md5Map;
},
async getShareFiles() {
const selected = this.getSelectedList();
if (!selected.length) throw new Error("请先勾选夸克分享文件/文件夹");
const m = location.pathname.match(/\/(s|share)\/([a-zA-Z0-9]+)/);
if (!m) throw new Error("无法识别夸克分享ID");
const shareId = m[2];
let cookie = helper.getCachedQuarkCookie();
if (!cookie || cookie.length < 8) {
helper.closeLoadingDialog();
cookie = await new Promise((resolve) => {
helper.showQuarkCookieInputDialog(resolve, helper.getCachedQuarkCookie());
});
if (!cookie) throw new Error("已取消输入 Cookie,操作中断");
helper.showLoadingDialog("正在扫描分享文件", "请稍候...");
}
const { stoken, title } = await this.getShareToken(shareId, cookie);
helper.updateLoadingMsg("正在扫描文件列表...");
const fileItems = [];
for (const item of selected) {
if (item.file) {
const parentFid = item.pdir_fid;
const list = await this.scanShareFiles(
shareId,
stoken,
cookie,
parentFid,
"",
false,
);
const found = list.find((x) => x.fid === item.fid);
if (found) {
fileItems.push(found);
} else {
fileItems.push({
fid: item.fid,
token: item.share_fid_token,
size: Number(item.size || 0),
path: item.file_name,
});
}
} else if (item.dir) {
fileItems.push(
...(await this.scanShareFiles(
shareId,
stoken,
cookie,
item.fid,
item.file_name,
true,
)),
);
}
}
if (!fileItems.length) throw new Error("未找到可导出的夸克分享文件");
helper.updateLoadingMsg(`已扫描到 ${fileItems.length} 个文件,正在获取 MD5...`);
const md5Map = await this.getShareMd5Map(shareId, stoken, cookie, fileItems);
const rows = fileItems.map((f) => ({
path: f.path,
etag: (md5Map[f.fid] || "").trim(),
size: Number(f.size || 0),
}));
const anyEtag = rows.some((r) => r.etag && r.etag.length > 0);
if (!anyEtag) {
const isCookieErr = true;
throw new Error(
"分享页未能得到任何 etag:请检查 Cookie 是否有效,或稍后重试(接口限频时也会失败)。" +
(isCookieErr ? "\n\n可能是 Cookie 已过期,请点击按钮更新 Cookie 后重试。" : ""),
);
}
return { files: rows, title };
},
};
const tianyi = {
/** 优先页面 API(unsafeWindow),再回退 DOM + __vue__。 */
getSelectedFiles() {
try {
if (typeof unsafeWindow !== "undefined") {
let list;
if (/\/web\/share/.test(location.href)) {
list = unsafeWindow.shareUser?.getSelectedFileList?.();
} else {
list = unsafeWindow.file?.getSelectedFileList?.();
}
if (list && list.length > 0) return list;
}
} catch {
// ignore
}
const selectedItems = [];
let selectedElements = document.querySelectorAll("li.c-file-item-select");
if (selectedElements.length === 0) {
const checkedBoxes = document.querySelectorAll(".ant-checkbox-checked");
if (checkedBoxes.length > 0) {
selectedElements = Array.from(checkedBoxes)
.map((box) => box.closest("li.c-file-item"))
.filter((el) => el);
}
}
if (selectedElements.length === 0) return [];
selectedElements.forEach((itemEl) => {
if (itemEl.__vue__) {
const vueInstance = itemEl.__vue__;
const fileData =
vueInstance.fileItem ||
vueInstance.fileInfo ||
vueInstance.item ||
vueInstance.file;
if (fileData) {
const fid = fileData.id || fileData.fileId;
if (!selectedItems.some((item) => (item.id || item.fileId) === fid)) {
selectedItems.push({
id: fid,
fileId: fid,
name: fileData.name || fileData.fileName,
fileName: fileData.name || fileData.fileName,
isFolder: !!(fileData.isFolder || fileData.fileCata === 2),
fileCata: fileData.fileCata,
md5: fileData.md5,
size: fileData.size,
});
}
}
}
});
return selectedItems;
},
sign(params) {
const sorted = Object.keys(params)
.sort()
.map((k) => `${k}=${params[k]}`)
.join("&");
return this.md5(sorted);
},
md5(str) {
function rotateLeft(v, s) {
return (v << s) | (v >>> (32 - s));
}
function addUnsigned(x, y) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
function F(x, y, z) {
return (x & y) | (~x & z);
}
function G(x, y, z) {
return (x & z) | (y & ~z);
}
function H(x, y, z) {
return x ^ y ^ z;
}
function I(x, y, z) {
return y ^ (x | ~z);
}
function FF(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function GG(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function HH(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function II(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
/** @param {string} s @returns {number[]} */
function convertToWordArray(s) {
const l = s.length;
const words = [0];
let i;
for (i = 0; i < l - 3; i += 4) {
words.push(
s.charCodeAt(i) |
(s.charCodeAt(i + 1) << 8) |
(s.charCodeAt(i + 2) << 16) |
(s.charCodeAt(i + 3) << 24),
);
}
let val = 0;
switch (l % 4) {
case 0:
val = 0x080000000;
break;
case 1:
val = s.charCodeAt(i) | 0x0800000;
break;
case 2:
val = s.charCodeAt(i) | (s.charCodeAt(i + 1) << 8) | 0x080000;
break;
default:
val =
s.charCodeAt(i) |
(s.charCodeAt(i + 1) << 8) |
(s.charCodeAt(i + 2) << 16) |
0x80;
}
words.push(val);
while ((words.length % 16) !== 14) words.push(0);
words.push(l << 3);
words.push(l >>> 29);
words.shift();
return words;
}
function toHex(v) {
let out = "";
for (let i = 0; i <= 3; i++) {
out += ("0" + ((v >>> (i * 8)) & 255).toString(16)).slice(-2);
}
return out;
}
let a = 0x67452301;
let b = 0xefcdab89;
let c = 0x98badcfe;
let d = 0x10325476;
const x = convertToWordArray(unescape(encodeURIComponent(str)));
const S11 = 7, S12 = 12, S13 = 17, S14 = 22;
const S21 = 5, S22 = 9, S23 = 14, S24 = 20;
const S31 = 4, S32 = 11, S33 = 16, S34 = 23;
const S41 = 6, S42 = 10, S43 = 15, S44 = 21;
for (let k = 0; k < x.length; k += 16) {
const AA = a, BB = b, CC = c, DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);
b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);
b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05);
a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);
c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);
b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);
a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);
d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);
b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);
a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);
d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);
c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
return (toHex(a) + toHex(b) + toHex(c) + toHex(d)).toLowerCase();
},
/** 天翼个人盘:开放接口 getFile.action,用于补全文件 MD5。 */
async getPersonalFileDetails(fileId) {
const appKey = "600100422";
const timestamp = String(Date.now());
const params = { fileId: String(fileId) };
const signature = this.sign({
...params,
Timestamp: timestamp,
AppKey: appKey,
});
const url =
"https://cloud.189.cn/api/open/file/getFile.action?" +
new URLSearchParams(params).toString();
const text = String(
await helper.get(url, {
Accept: "application/json;charset=UTF-8",
"Sign-Type": "1",
Signature: signature,
Timestamp: timestamp,
AppKey: appKey,
}),
);
const data = /** @type {any} */ (
JSON.parse(
text.replace(
/"(id|fileId|parentId|shareId)":"?(\d{15,})"?/g,
'"$1":"$2"',
),
)
);
if (data.res_code !== 0) {
throw new Error(data.res_message || String(data.res_code));
}
const md5 =
data.md5 ||
data.file?.md5 ||
data.fileData?.md5 ||
data.userFile?.md5 ||
"";
return { md5: String(md5 || "").toLowerCase() };
},
async listPersonal(folderId, path = "") {
const files = [];
let pageNum = 1;
const pageSize = 100;
while (true) {
const appKey = "600100422";
const timestamp = String(Date.now());
const params = {
folderId: String(folderId),
pageNum: String(pageNum),
pageSize: String(pageSize),
orderBy: "lastOpTime",
descending: "true",
};
const signature = this.sign({
...params,
Timestamp: timestamp,
AppKey: appKey,
});
const url = `https://cloud.189.cn/api/open/file/listFiles.action?${new URLSearchParams(
/** @type {Record<string, string>} */ (params),
).toString()}`;
const text = String(
await helper.get(url, {
Accept: "application/json;charset=UTF-8",
"Sign-Type": "1",
Signature: signature,
Timestamp: timestamp,
AppKey: appKey,
}),
);
const data = /** @type {any} */ (JSON.parse(text));
if (data.res_code !== 0) break;
const fileList = data.fileListAO?.fileList || [];
const folderList = data.fileListAO?.folderList || [];
if (!fileList.length && !folderList.length) break;
for (const f of fileList) {
files.push({
path: path ? `${path}/${f.name}` : f.name,
etag: (f.md5 || "").toLowerCase(),
size: Number(f.size || 0),
fileId: f.id,
});
}
for (const d of folderList) {
const subPath = path ? `${path}/${d.name}` : d.name;
files.push(...(await this.listPersonal(d.id, subPath)));
}
if (fileList.length + folderList.length < pageSize) break;
pageNum++;
}
return files;
},
async getFiles() {
const selected = this.getSelectedFiles();
if (!selected.length) throw new Error("请先勾选天翼文件/文件夹");
const all = [];
for (const item of selected) {
const id = item.id || item.fileId;
const name = item.name || item.fileName;
const isFolder = item.isFolder || item.fileCata === 2;
if (isFolder) {
all.push(...(await this.listPersonal(id, name)));
} else {
all.push({
path: name,
etag: (item.md5 || "").toLowerCase(),
size: Number(item.size || 0),
fileId: id,
});
}
}
if (!all.length) throw new Error("未找到可导出的天翼文件");
const missing = all.filter((f) => f.path && !f.etag && f.fileId);
for (let i = 0; i < missing.length; i++) {
const f = missing[i];
try {
helper.updateLoadingMsg(
`正在补全文件 MD5 (${i + 1}/${missing.length})...`,
);
const d = await this.getPersonalFileDetails(f.fileId);
f.etag = (d.md5 || "").toLowerCase();
} catch {
/* skip md5 for this file */
}
await helper.sleep(100);
}
return all;
},
/** 提取码、checkAccessCode、分享标题 */
async getBaseShareInfo(shareUrl, sharePwd = "") {
const match =
shareUrl.match(/\/t\/([a-zA-Z0-9]+)/) ||
shareUrl.match(/[?&]code=([a-zA-Z0-9]+)/);
if (!match) throw new Error("无效的189网盘分享链接");
const shareCode = match[1];
let accessCode = sharePwd || "";
if (!accessCode) {
const cookieName = `share_${shareCode}`;
const cookiePwd = helper.getCookie(cookieName);
if (cookiePwd) {
accessCode = cookiePwd;
} else {
try {
const decodedUrl = decodeURIComponent(shareUrl);
const pwdMatch = decodedUrl.match(
/[((]访问码[::]\s*([a-zA-Z0-9]+)/,
);
if (pwdMatch && pwdMatch[1]) accessCode = pwdMatch[1];
} catch {
/* ignore */
}
}
}
let shareId = shareCode;
if (accessCode) {
const checkUrl =
"https://cloud.189.cn/api/open/share/checkAccessCode.action?" +
`shareCode=${encodeURIComponent(shareCode)}&accessCode=${encodeURIComponent(accessCode)}`;
try {
const checkText = await helper.get(checkUrl, {
Accept: "application/json;charset=UTF-8",
Referer: "https://cloud.189.cn/web/main/",
});
const checkData = /** @type {any} */ (JSON.parse(checkText));
if (checkData.shareId) shareId = checkData.shareId;
} catch {
/* ignore */
}
}
const params = { shareCode, accessCode };
const timestamp = String(Date.now());
const appKey = "600100422";
const signData = { ...params, Timestamp: timestamp, AppKey: appKey };
const signature = this.sign(signData);
const apiUrl = `https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action?${new URLSearchParams(
params,
).toString()}`;
const text = String(
await helper.get(apiUrl, {
Accept: "application/json;charset=UTF-8",
"Sign-Type": "1",
Signature: signature,
Timestamp: timestamp,
AppKey: appKey,
Referer: "https://cloud.189.cn/web/main/",
}),
);
let data;
try {
data = /** @type {any} */ (
JSON.parse(
text.replace(
/"(id|fileId|parentId|shareId)":"?(\d{15,})"?/g,
'"$1":"$2"',
),
)
);
} catch {
throw new Error("解析分享信息失败");
}
if (data.res_code !== 0) {
if (data.res_code === 40401 && !accessCode) {
throw new Error("该分享需要提取码,请输入提取码");
}
throw new Error(
`获取分享信息失败: ${data.res_message || data.res_code || "未知错误"}`,
);
}
return {
shareId: data.shareId || shareId,
shareMode: data.shareMode || "0",
accessCode,
shareCode,
title: data.fileName || "",
};
},
async listShare(
shareId,
shareDirFileId,
fileId,
path = "",
shareMode = "0",
accessCode = "",
shareCode = "",
) {
const files = [];
let page = 1;
while (true) {
const params = {
pageNum: String(page),
pageSize: "100",
fileId: String(fileId),
shareDirFileId: String(shareDirFileId),
isFolder: "true",
shareId: String(shareId),
shareMode,
iconOption: "5",
orderBy: "lastOpTime",
descending: "true",
accessCode: accessCode || "",
};
/** @type {Record<string, string>} */
const headers = {
Accept: "application/json;charset=UTF-8",
Referer: "https://cloud.189.cn/web/main/",
};
if (shareCode && accessCode) {
headers["Cookie"] = `share_${shareCode}=${accessCode}`;
}
const url = `https://cloud.189.cn/api/open/share/listShareDir.action?${new URLSearchParams(
params,
).toString()}`;
const text = String(await helper.get(url, headers));
/** @type {any} */
let data;
try {
const fixedText = text.replace(
/"(id|fileId|parentId|shareId)":(\d{15,})/g,
'"$1":"$2"',
);
data = JSON.parse(fixedText);
} catch {
break;
}
if (data.res_code !== 0) {
break;
}
const fileList = data.fileListAO?.fileList || [];
const folderList = data.fileListAO?.folderList || [];
for (const f of fileList) {
files.push({
path: path ? `${path}/${f.name}` : f.name,
etag: (f.md5 || "").toLowerCase(),
size: Number(f.size || 0),
});
}
for (const d of folderList) {
const subPath = path ? `${path}/${d.name}` : d.name;
files.push(
...(await this.listShare(
shareId,
d.id,
d.id,
subPath,
shareMode,
accessCode,
shareCode,
)),
);
}
if (fileList.length + folderList.length < 100) break;
page++;
}
return files;
},
async getShareFiles() {
const selected = this.getSelectedFiles();
if (!selected.length) throw new Error("请先勾选天翼分享文件/文件夹");
const info = await this.getBaseShareInfo(location.href, "");
const all = [];
for (const item of selected) {
const id = item.id || item.fileId;
const name = item.name || item.fileName;
const isFolder = item.isFolder || item.fileCata === 2;
if (isFolder) {
all.push(
...(await this.listShare(
info.shareId,
id,
id,
name,
info.shareMode,
info.accessCode,
info.shareCode,
)),
);
} else {
all.push({
path: name,
etag: (item.md5 || "").toLowerCase(),
size: Number(item.size || 0),
});
}
}
if (!all.length) throw new Error("未找到可导出的天翼分享文件");
return { files: all, title: info.title || "" };
},
};
const baidu = {
isBaiduHost() {
return /^pan\.baidu\.com$/.test(location.hostname);
},
/**
* 解密百度网盘加密的 MD5。
* 百度加密流程:重组(swap前后8位块) → 逐位XOR(key=pos&15) → 第9位替换为 chr('g'+val)
* 解密为其逆过程(重组与XOR均自逆)。
* 若传入已是标准32位十六进制则原样返回。
*/
decodeBaiduMd5(encrypted) {
const s = String(encrypted || "").trim();
if (!s || s.length !== 32) return s.toLowerCase();
// 已是标准32位十六进制MD5,无需解密
if (/^[0-9a-f]{32}$/i.test(s)) return s.toLowerCase();
// 加密后第9位(索引9)是 'g'~'v' 范围字符,代表 0~15 的偏移量
const specialChar = s.charAt(9);
const offset = specialChar.charCodeAt(0) - "g".charCodeAt(0);
if (offset < 0 || offset > 15) return s.toLowerCase();
// 恢复 r[9]:将特殊字符替换回对应十六进制字符
const r = s.toLowerCase().split("");
r[9] = offset.toString(16);
// 逆向XOR(XOR自逆):i[o] = parseInt(r[o], 16) ^ (15 & o)
const dec = [];
for (let o = 0; o < 32; o++) {
const v = parseInt(r[o], 16);
if (isNaN(v)) return s.toLowerCase();
dec[o] = (v ^ (15 & o)).toString(16);
}
// 逆向重组(与加密时相同,因为 swap 自逆)
const original =
dec.slice(8, 16).join("") +
dec.slice(0, 8).join("") +
dec.slice(24, 32).join("") +
dec.slice(16, 24).join("");
if (/^[0-9a-f]{32}$/.test(original)) return original;
return s.toLowerCase();
},
/** 从页面全局变量或 script 标签提取 bdstoken */
getBdstoken() {
try {
if (typeof unsafeWindow !== "undefined") {
const yw = unsafeWindow.yunData;
if (yw && yw.MYBDSTOKEN) return String(yw.MYBDSTOKEN);
}
} catch { /* ignore */ }
const scripts = document.querySelectorAll("script");
for (const s of scripts) {
const m = (s.textContent || "").match(/"bdstoken"\s*[=:]\s*"([a-f0-9]{32})"/i);
if (m) return m[1];
}
return "";
},
/** 从 React fiber / DOM / 全局变量获取选中文件/文件夹的 fs_id 列表 */
getSelectedFsIds() {
const ids = new Set();
// 方法1:扫描 React fiber 树,查找含 selectedList/checkedList 的组件 state/props
const scanFiber = (root, maxNodes) => {
if (!root) return;
const q = [root];
let n = 0;
while (q.length && n < maxNodes) {
const cur = q.shift();
n++;
if (!cur) continue;
// 检查 hooks memoizedState 链
let hs = cur.memoizedState;
while (hs) {
const v = hs.memoizedState;
if (v && typeof v === "object" && !Array.isArray(v)) {
const list = v.selectedList || v.checkedList || v.selectedFiles || v.checkList;
if (Array.isArray(list) && list.length) {
list.forEach((f) => {
const id = String(f?.fs_id || f?.fsId || "");
if (/^\d+$/.test(id)) ids.add(id);
});
}
}
hs = hs.next;
}
// 检查 memoizedProps
const mp = cur.memoizedProps;
if (mp && typeof mp === "object") {
const list = mp.selectedList || mp.checkedList || mp.selectedFiles;
if (Array.isArray(list) && list.length) {
list.forEach((f) => {
const id = String(f?.fs_id || f?.fsId || "");
if (/^\d+$/.test(id)) ids.add(id);
});
}
}
let ch = cur.child;
while (ch) { q.push(ch); ch = ch.sibling; }
}
};
const roots = [
document.getElementById("root"),
document.getElementById("app"),
document.querySelector("[id^='app']"),
document.body,
].filter(Boolean);
for (const root of roots) {
const fk = Object.keys(root).find((k) =>
k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"),
);
if (fk) scanFiber(root[fk], 20000);
if (ids.size) break;
}
if (ids.size) return [...ids];
// 方法2:从选中 DOM 元素的 fiber props 提取 fs_id
const extractFsIdFromEl = (el) => {
const fk = Object.keys(el).find((k) =>
k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"),
);
if (!fk) return null;
let fiber = el[fk];
for (let i = 0; i < 25 && fiber; i++) {
const mp = fiber.memoizedProps || fiber.pendingProps;
if (mp && typeof mp === "object") {
const item = mp.item || mp.file || mp.fileInfo || mp.data || mp.fileItem;
if (item && typeof item === "object") {
const id = String(item.fs_id || item.fsId || "");
if (/^\d+$/.test(id)) return id;
}
const id = String(mp.fs_id || mp.fsId || "");
if (/^\d+$/.test(id)) return id;
}
fiber = fiber.return;
}
return null;
};
document.querySelectorAll(
"[class*='selected']:not(head):not(style):not(script), [aria-selected='true']",
).forEach((el) => {
if (el.tagName === "INPUT" || el.tagName === "BUTTON" || el.closest("thead")) return;
const id = extractFsIdFromEl(el);
if (id) ids.add(id);
});
if (ids.size) return [...ids];
// 方法3:Redux store / yunData 全局变量
try {
if (typeof unsafeWindow !== "undefined") {
const stores = [
unsafeWindow.__redux_store__,
unsafeWindow.store,
unsafeWindow.reduxStore,
];
for (const store of stores) {
if (!store || typeof store.getState !== "function") continue;
const state = store.getState() || {};
for (const key of Object.keys(state)) {
const slice = state[key];
if (!slice || typeof slice !== "object") continue;
const list = slice.selectedList || slice.checkedList ||
slice.selectedFiles || slice.selectedFsIds;
if (!Array.isArray(list) || !list.length) continue;
list.forEach((f) => {
const id = typeof f === "object"
? String(f?.fs_id || f?.fsId || f?.id || "")
: String(f);
if (/^\d+$/.test(id)) ids.add(id);
});
if (ids.size) break;
}
if (ids.size) break;
}
if (!ids.size) {
const yw = unsafeWindow.yunData;
const sel = yw?.selectedFsIds;
if (Array.isArray(sel)) sel.forEach((id) => ids.add(String(id)));
}
}
} catch { /* ignore */ }
return [...ids];
},
/** 从 URL hash/search 获取当前目录路径 */
getCurrentDir() {
const src = location.hash + location.search;
const m = src.match(/[?&]path=([^&]+)/);
if (m) {
try { return decodeURIComponent(m[1]); } catch { return m[1]; }
}
return "/";
},
/** 从选中行的 title 属性或文本内容提取文件名 */
getSelectedFileNames() {
const names = new Set();
const candidates = [
...document.querySelectorAll("[class*='selected']"),
...document.querySelectorAll("[aria-selected='true']"),
];
for (const el of candidates) {
if (el.tagName === "INPUT" || el.tagName === "BUTTON" || el.closest("thead")) continue;
// 优先子元素 title 属性(最稳定)
for (const te of el.querySelectorAll("[title]")) {
const t = te.getAttribute("title") || "";
if (t.length > 0 && t.length < 500 && !t.startsWith("http") && !t.includes("://")) {
names.add(t);
break;
}
}
// 自身 title
const st = el.getAttribute("title") || "";
if (st.length > 0 && st.length < 500 && !st.startsWith("http") && !st.includes("://")) {
names.add(st);
}
}
return [...names];
},
/** 递归列出目录下所有文件 */
async listDir(dir, pathPrefix) {
const files = [];
let page = 1;
const bdstoken = this.getBdstoken();
while (true) {
const url = "https://pan.baidu.com/api/list?" +
`dir=${encodeURIComponent(dir)}&order=name&desc=0&showempty=0` +
`&web=1&page=${page}&num=100&channel=chunlei&app_id=250528` +
`&bdstoken=${encodeURIComponent(bdstoken)}`;
const text = await helper.get(url, { Referer: "https://pan.baidu.com/disk/main" });
const data = JSON.parse(text);
if (data.errno !== 0 || !Array.isArray(data.list)) break;
for (const item of data.list) {
const itemPath = pathPrefix ? `${pathPrefix}/${item.server_filename}` : item.server_filename;
if (item.isdir === 1) {
files.push(...(await this.listDir(item.path, itemPath)));
} else {
files.push({
fs_id: String(item.fs_id),
path: itemPath,
size: Number(item.size || 0),
md5: this.decodeBaiduMd5(item.md5),
});
}
}
if (data.list.length < 100) break;
page++;
await helper.sleep(400);
}
return files;
},
/** 批量获取文件元数据(含 md5、path、isdir) */
async getFileMetas(fsIds) {
const result = {};
const bdstoken = this.getBdstoken();
const batchSize = 100;
for (let i = 0; i < fsIds.length; i += batchSize) {
const batch = fsIds.slice(i, i + batchSize);
const url = "https://pan.baidu.com/api/filemetas?" +
`fsids=${encodeURIComponent(JSON.stringify(batch.map(Number)))}` +
`&dlink=0&thumb=0&extra=0&needmedia=0&detail=1` +
`&channel=chunlei&web=1&app_id=250528` +
`&bdstoken=${encodeURIComponent(bdstoken)}`;
try {
const text = await helper.get(url, { Referer: "https://pan.baidu.com/disk/main" });
const data = JSON.parse(text);
if (data.errno === 0 && Array.isArray(data.info)) {
for (const item of data.info) {
result[String(item.fs_id)] = {
md5: this.decodeBaiduMd5(item.md5),
path: String(item.path || ""),
size: Number(item.size || 0),
filename: String(item.filename || item.server_filename || ""),
isdir: item.isdir === 1,
};
}
}
} catch { /* ignore */ }
await helper.sleep(300);
}
return result;
},
async collectFiles() {
const output = [];
const folderItems = [];
// 方式一:fs_id(React fiber / Redux)
const fsIds = this.getSelectedFsIds();
if (fsIds.length) {
helper.updateLoadingMsg("正在获取文件信息...");
const metas = await this.getFileMetas(fsIds);
for (const id of fsIds) {
const meta = metas[id];
if (!meta) continue;
if (meta.isdir) {
folderItems.push({ baiduPath: meta.path, name: meta.filename });
} else if (meta.md5) {
output.push({ path: meta.filename || meta.path.split("/").pop(), etag: meta.md5, size: meta.size });
}
}
}
// 方式二:DOM 文件名 + list API 匹配(兜底)
if (!output.length && !folderItems.length) {
const selectedNames = this.getSelectedFileNames();
if (!selectedNames.length) throw new Error("请先在百度网盘勾选要导出的文件或文件夹");
const currentDir = this.getCurrentDir();
helper.updateLoadingMsg("正在获取文件列表...");
const bdstoken = this.getBdstoken();
const url = "https://pan.baidu.com/api/list?" +
`dir=${encodeURIComponent(currentDir)}&order=name&desc=0&showempty=0` +
`&web=1&page=1&num=1000&channel=chunlei&app_id=250528` +
`&bdstoken=${encodeURIComponent(bdstoken)}`;
const text = await helper.get(url, { Referer: "https://pan.baidu.com/disk/main" });
const data = JSON.parse(text);
if (data.errno !== 0 || !Array.isArray(data.list)) {
throw new Error(`获取文件列表失败(errno=${data.errno}),请确认已登录百度网盘`);
}
const nameSet = new Set(selectedNames);
for (const item of data.list) {
if (!nameSet.has(item.server_filename)) continue;
if (item.isdir === 1) {
folderItems.push({ baiduPath: item.path, name: item.server_filename });
} else {
output.push({
path: item.server_filename,
etag: this.decodeBaiduMd5(item.md5),
size: Number(item.size || 0),
});
}
}
if (!output.length && !folderItems.length) {
throw new Error(`已勾选 ${selectedNames.length} 个文件名,但在当前目录(${currentDir})未匹配到对应文件,请确认当前目录与勾选文件一致`);
}
}
// 递归遍历文件夹
for (const folder of folderItems) {
helper.updateLoadingMsg(`正在扫描:${folder.name}...`);
const subFiles = await this.listDir(folder.baiduPath, folder.name);
for (const f of subFiles) {
output.push({ path: f.path, etag: f.md5 || "", size: f.size });
}
}
if (!output.length) throw new Error("没有可导出的百度网盘文件");
return output;
},
};
/** 判断是否为文件名违禁词错误(光鸭 code=166),可安全跳过并继续下一个文件 */
function isGuangyaForbiddenNameError(err) {
if (!err) return false;
try {
const detail = JSON.parse(String(err.guangyaDetail || "{}"));
const rb = detail && detail.responseBody;
if (rb && typeof rb === "object" && rb.code === 166) return true;
} catch {
/* ignore */
}
return false;
}
function guangyaExtractMd5FromEtag(raw) {
const s = String(raw ?? "").trim();
if (!s) return { ok: false, reason: "etag/md5 为空" };
const lower = s.toLowerCase();
// 标准 32 位十六进制 MD5
if (/^[0-9a-f]{32}$/.test(lower)) {
return { ok: true, md5: lower };
}
// 尝试 Base64 / Base64url 解码(夸克等网盘返回 Base64 编码的 MD5)
const decoded = helper.decodeMd5(s);
if (decoded && /^[0-9a-f]{32}$/.test(decoded)) {
return { ok: true, md5: decoded };
}
// 去掉分隔符后恰好 32 位十六进制(如带连字符的 UUID 格式)
const stripped = lower.replace(/[^0-9a-f]/g, "");
if (stripped.length === 32) {
return { ok: true, md5: stripped };
}
// 32 位字符串但含非十六进制字符:透传给接口,由服务端决定是否有效
// 部分网盘(如 123pan)返回的 etag 使用非标准字母表,客户端无法转换,
// 直接透传可让已有有效 etag 的文件正常导入,无效的由接口返回失败。
if (s.length === 32 && /^[0-9a-zA-Z+/=_-]{32}$/.test(s)) {
return { ok: true, md5: lower };
}
if (s.length === 32) {
// 含特殊符号,仍透传,不在客户端硬拦截
return { ok: true, md5: lower };
}
return {
ok: false,
reason: `etag 长度 ${s.length} 位,去除分隔符后十六进制位数为 ${stripped.length},无法识别为有效 MD5`,
};
}
const panGuangya = {
isHost() {
const h = location.hostname;
return h === "guangyapan.com" || h.endsWith(".guangyapan.com");
},
pickTokenFromPageStorage() {
const storages = [localStorage, sessionStorage];
for (const st of storages) {
try {
for (let i = 0; i < st.length; i++) {
const k = st.key(i);
if (!k) continue;
const v = st.getItem(k);
if (!v || v.length > 60000) continue;
const keyHit = /token|oauth|auth|session|login|xbase|user/i.test(k);
const vHit = /access_token|accessToken/i.test(v.slice(0, 120));
if (!keyHit && !vHit) continue;
try {
const j = JSON.parse(v);
const t =
j.access_token ||
j.accessToken ||
(j.token &&
(j.token.access_token || j.token.accessToken)) ||
(j.data &&
(j.data.access_token || j.data.accessToken));
if (typeof t === "string" && t.length > 20) {
return t.trim();
}
} catch {
/* 非 JSON */
}
if (
keyHit &&
/^[a-zA-Z0-9._-]{30,}$/.test(v.trim()) &&
!v.includes("{")
) {
return v.trim();
}
}
} catch {
/* ignore */
}
}
return "";
},
async getAccessToken() {
let t = String(GM_getValue(KEY_GUANGYA_ACCESS_TOKEN, "") || "").trim();
if (t) return t;
t = panGuangya.pickTokenFromPageStorage();
if (t) return t;
return await panGuangya.promptPasteToken();
},
promptPasteToken() {
return new Promise((resolve) => {
const backdrop = document.createElement("div");
Object.assign(backdrop.style, {
position: "fixed",
inset: "0",
background: "rgba(0,0,0,0.45)",
zIndex: "2147483646",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "system-ui,sans-serif",
});
const box = document.createElement("div");
box.style.cssText =
"background:#fff;padding:18px 20px;border-radius:10px;max-width:92vw;width:440px;box-shadow:0 8px 32px rgba(0,0,0,.2)";
const p = document.createElement("p");
p.style.cssText =
"margin:0 0 10px;font-size:13px;line-height:1.55;color:#333;";
p.innerHTML =
"未在页面存储中找到 access_token。<br/>请在<strong>已登录</strong>状态下打开开发者工具 → Network,点选任意发往 <code>api.guangyapan.com</code> 的请求,在请求头里复制 <code>Authorization</code> 的 Bearer 后面整段 token;或从 Application → Local Storage 里找 JSON 中的 <code>access_token</code>。";
const inp = document.createElement("input");
inp.type = "password";
inp.placeholder = "粘贴 access_token(可含 Bearer 前缀)";
inp.style.cssText =
"width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px;margin-bottom:12px;";
const row = document.createElement("div");
row.style.cssText =
"display:flex;gap:10px;justify-content:flex-end;";
const btnOk = document.createElement("button");
btnOk.textContent = "确定并保存";
btnOk.style.cssText =
"padding:8px 16px;border-radius:6px;border:none;background:#1677ff;color:#fff;cursor:pointer;";
const btnCancel = document.createElement("button");
btnCancel.textContent = "取消";
btnCancel.style.cssText =
"padding:8px 16px;border-radius:6px;border:1px solid #ccc;background:#fff;cursor:pointer;";
const cleanup = () => backdrop.remove();
btnCancel.onclick = () => {
cleanup();
resolve("");
};
btnOk.onclick = () => {
let v = inp.value.trim();
if (v.startsWith("Bearer ")) v = v.slice(7).trim();
if (v) GM_setValue(KEY_GUANGYA_ACCESS_TOKEN, v);
cleanup();
resolve(v);
};
row.appendChild(btnCancel);
row.appendChild(btnOk);
box.appendChild(p);
box.appendChild(inp);
box.appendChild(row);
backdrop.appendChild(box);
document.body.appendChild(backdrop);
inp.focus();
});
},
showImportDialog() {
const backdrop = document.createElement("div");
Object.assign(backdrop.style, {
position: "fixed",
inset: "0",
background: "rgba(0,0,0,0.45)",
zIndex: "2147483646",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "system-ui,sans-serif",
});
const box = document.createElement("div");
box.style.cssText =
"background:#fff;padding:18px 20px;border-radius:10px;max-width:94vw;width:520px;max-height:88vh;overflow:auto;box-shadow:0 8px 32px rgba(0,0,0,.2)";
const title = document.createElement("div");
title.textContent = "导入秒传 JSON 到当前账号";
title.style.cssText =
"font-weight:600;font-size:15px;margin-bottom:10px;color:#111;";
const hint = document.createElement("p");
hint.style.cssText =
"margin:0 0 8px;font-size:12px;line-height:1.5;color:#555;";
hint.innerHTML =
"可<strong>粘贴</strong> JSON 或<strong>选择单个 .json 文件</strong>(<code>files</code>:path、<code>etag</code>=32 位十六进制 MD5、size)。<br>选择文件后会<strong>自动校验</strong> JSON 结构;不符则不会填入下方。<br>可点「清除」去掉已选文件并清空下方内容后重新选择。";
const fileRow = document.createElement("div");
fileRow.style.cssText =
"margin:10px 0 8px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;";
const filePickLabel = document.createElement("span");
filePickLabel.textContent = "选择文件:";
filePickLabel.style.cssText =
"font-size:12px;color:#555;flex-shrink:0;";
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json,application/json,text/json";
fileInput.style.cssText =
"font-size:12px;max-width:min(100%,280px);";
const btnClearFile = document.createElement("button");
btnClearFile.type = "button";
btnClearFile.textContent = "清除";
btnClearFile.title = "清空已选文件与下方 JSON,可重新选择";
btnClearFile.style.cssText =
"padding:4px 12px;border-radius:6px;border:1px solid #ccc;background:#fff;color:#333;cursor:pointer;font-size:12px;flex-shrink:0;";
const fileLoadedHint = document.createElement("span");
fileLoadedHint.style.cssText =
"font-size:11px;color:#888;word-break:break-all;flex:1;min-width:120px;";
const readFileAsTextPromise = (file) =>
new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(String(r.result || ""));
r.onerror = () => reject(new Error("读取失败"));
try {
r.readAsText(file, "UTF-8");
} catch (e) {
reject(e);
}
});
fileInput.addEventListener("change", async () => {
const picked = fileInput.files;
fileLoadedHint.textContent = "";
if (!picked || picked.length === 0) return;
const f = picked[0];
const onReadFail = () => {
fileLoadedHint.textContent = "";
status.style.color = "#c00";
status.textContent = "读取文件失败,请重试或改用粘贴";
};
try {
const text = await readFileAsTextPromise(f);
const vr = validateGuangyaImportJsonShape(text);
if (!vr.ok) {
fileInput.value = "";
ta.value = "";
fileLoadedHint.textContent = "";
status.style.color = "#c00";
status.textContent = `格式不符:${vr.message}`;
return;
}
ta.value = text;
fileLoadedHint.textContent = `格式校验通过 · ${f.name}(${(f.size / 1024).toFixed(1)} KB),共 ${vr.fileCount} 条`;
status.textContent = "";
status.style.removeProperty("color");
setDetailText("");
} catch {
onReadFail();
}
});
fileRow.appendChild(filePickLabel);
fileRow.appendChild(fileInput);
fileRow.appendChild(btnClearFile);
fileRow.appendChild(fileLoadedHint);
const ta = document.createElement("textarea");
ta.placeholder = '{"files":[{"path":"a.mp4","etag":"…32位md5…","size":123}]}';
ta.style.cssText =
"width:100%;box-sizing:border-box;min-height:180px;padding:10px;border:1px solid #ccc;border-radius:6px;font-size:12px;font-family:ui-monospace,monospace;margin-top:4px;";
const status = document.createElement("div");
status.style.cssText =
"margin-top:10px;font-size:12px;color:#c00;min-height:18px;white-space:pre-wrap;";
const detailWrap = document.createElement("div");
detailWrap.style.cssText = "display:none;margin-top:10px;";
const detailLabel = document.createElement("div");
detailLabel.textContent =
"分类明细(接口失败 / 秒传失败 / 校验),可滚动复制";
detailLabel.style.cssText =
"font-size:12px;color:#666;margin-bottom:6px;";
const detailTa = document.createElement("textarea");
detailTa.readOnly = true;
detailTa.rows = 14;
detailTa.style.cssText =
"width:100%;box-sizing:border-box;font-size:11px;line-height:1.4;font-family:ui-monospace,monospace;padding:8px;border:1px solid #ddd;border-radius:6px;resize:vertical;min-height:160px;";
const copyRow = document.createElement("div");
copyRow.style.cssText =
"margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;";
const btnCopyDetail = document.createElement("button");
btnCopyDetail.type = "button";
btnCopyDetail.textContent = "复制详情";
btnCopyDetail.style.cssText =
"padding:6px 14px;border-radius:6px;border:1px solid #1677ff;background:#e6f4ff;color:#1677ff;cursor:pointer;font-size:12px;";
const copyHint = document.createElement("span");
copyHint.style.cssText = "font-size:11px;color:#999;";
copyHint.textContent = "";
copyRow.appendChild(btnCopyDetail);
copyRow.appendChild(copyHint);
btnCopyDetail.onclick = () => {
const t = detailTa.value;
if (!t) return;
try {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(t, "text");
copyHint.textContent = "已复制";
setTimeout(() => {
copyHint.textContent = "";
}, 2000);
return;
}
} catch {
/* fallthrough */
}
detailTa.focus();
detailTa.select();
try {
document.execCommand("copy");
copyHint.textContent = "已复制";
setTimeout(() => {
copyHint.textContent = "";
}, 2000);
} catch {
copyHint.textContent = "请手动 Ctrl+C";
}
};
detailWrap.appendChild(detailLabel);
detailWrap.appendChild(detailTa);
detailWrap.appendChild(copyRow);
const setDetailText = (text) => {
if (text) {
detailTa.value = text;
detailWrap.style.display = "block";
} else {
detailTa.value = "";
detailWrap.style.display = "none";
}
};
btnClearFile.onclick = () => {
fileInput.value = "";
fileLoadedHint.textContent = "";
ta.value = "";
status.textContent = "";
status.style.removeProperty("color");
setDetailText("");
};
const row = document.createElement("div");
row.style.cssText =
"display:flex;gap:10px;justify-content:flex-end;margin-top:14px;flex-wrap:wrap;";
const btnRun = document.createElement("button");
btnRun.textContent = "开始导入";
btnRun.style.cssText =
"padding:8px 16px;border-radius:6px;border:none;background:#1677ff;color:#fff;cursor:pointer;";
const btnClose = document.createElement("button");
btnClose.textContent = "关闭";
btnClose.style.cssText =
"padding:8px 16px;border-radius:6px;border:1px solid #ccc;background:#fff;cursor:pointer;";
const cleanup = () => backdrop.remove();
btnClose.onclick = cleanup;
btnRun.onclick = async () => {
status.textContent = "";
setDetailText("");
btnRun.textContent = "导入中";
btnRun.disabled = true;
btnRun.style.cursor = "not-allowed";
btnRun.style.opacity = "0.75";
status.style.color = "#1677ff";
status.textContent = "导入中...";
try {
const r = await panGuangya.importMd5Json(
ta.value,
(p) => {
status.style.color = "#1677ff";
if (p.phase === "mkdir") {
status.textContent = `导入中... 创建目录 ${p.index}/${p.total}`;
} else if (p.phase === "probe") {
status.textContent = `导入中... 秒传 ${p.index}/${p.total}`;
} else {
status.textContent = `导入中... 第 ${p.index}/${p.total} 批(本批 ${p.chunkSize} 条)`;
}
},
);
const sum =
r.importSummary ||
(() => {
const c = guangyaParseImportResultCounts(
r.resp,
r.okCount,
r.skipCount,
);
const transferFail = Math.max(
0,
c.failCount - (r.skipCount || 0),
);
const xfer = guangyaTransferFailRowsFromResp(
r.resp,
[],
);
return {
batchCount: 1,
transferSuccess: c.successCount,
transferFail,
skipCount: r.skipCount || 0,
transferFailedEntries: xfer,
transferFailedMissingDetail:
transferFail > 0 && xfer.length === 0,
};
})();
const mkdirFailedCount =
sum.mkdirFailedCount != null
? Number(sum.mkdirFailedCount) || 0
: Array.isArray(r.skipped)
? r.skipped.filter((x) =>
String(x).includes("创建目录失败"),
).length
: 0;
const transferTotal =
sum.transferSuccess + sum.transferFail + mkdirFailedCount;
const transferFailTotal = sum.transferFail + mkdirFailedCount;
const nonMkdirSkipCount = Math.max(
0,
(sum.skipCount || 0) - mkdirFailedCount,
);
const probeCount =
sum.probeTotal != null
? Number(sum.probeTotal) || 0
: sum.transferSuccess + sum.transferFail;
const ifaceLine = `阶段统计:创建目录失败(未进入秒传)${mkdirFailedCount} 条;进入秒传阶段 ${probeCount} 条。`;
const lines = [
ifaceLine,
`秒传结果:共 ${transferTotal} 条,成功 ${sum.transferSuccess} 条,失败 ${transferFailTotal} 条,其中 ${mkdirFailedCount} 条因创建目录失败未导入。`,
];
if (nonMkdirSkipCount > 0) {
lines.push(
`校验未通过(未提交接口):${nonMkdirSkipCount} 条。`,
);
}
const warn = transferFailTotal > 0 || nonMkdirSkipCount > 0;
status.style.color = warn ? "#a60" : "#080";
status.textContent = lines.join("\n");
const xferRows = sum.transferFailedEntries || [];
const needCopy =
sum.transferFail > 0 ||
sum.skipCount > 0 ||
sum.transferFailedMissingDetail;
if (needCopy) {
const transferExtra = [];
const rawSkipped = Array.isArray(r.skipped) ? r.skipped : [];
const mkdirSkipLines = rawSkipped.filter((x) =>
String(x).includes("创建目录失败"),
);
const validateSkipLines = rawSkipped.filter(
(x) => !String(x).includes("创建目录失败"),
);
if (sum.transferFailedMissingDetail && sum.transferFail > 0) {
transferExtra.push(
`(说明:共有 ${sum.transferFail} 条秒传失败,但接口未返回失败明细,无法逐条列出路径。)`,
);
}
setDetailText(
formatGuangyaImportCopyReport({
interfaceLines: [
"(无)各批 HTTP 状态与业务 code 均成功。",
],
transferRows: xferRows,
mkdirSkipLines:
mkdirSkipLines.length > 0
? mkdirSkipLines
: undefined,
validateSkipLines:
validateSkipLines.length > 0
? validateSkipLines
: undefined,
transferExtraLines: transferExtra,
}),
);
} else {
setDetailText("");
}
} catch (e) {
status.style.color = "#c00";
status.textContent = e?.message || String(e);
const iface = [
e?.message || String(e),
"",
e?.importFailedAtMkdirIndex != null
? `失败位置:第 ${e.importFailedAtMkdirIndex} 条(创建目录阶段)。`
: e?.importFailedAtProbeIndex != null
? `失败位置:第 ${e.importFailedAtProbeIndex} 条(秒传阶段)。`
: e?.importFailedAtBatchIndex != null
? `失败位置:第 ${e.importFailedAtBatchIndex} 批。`
: "",
].filter(Boolean);
if (e?.guangyaDetail) {
iface.push("", String(e.guangyaDetail));
}
const xferRows = Array.isArray(e?.partialTransferFailures)
? e.partialTransferFailures
: [];
setDetailText(
formatGuangyaImportCopyReport({
interfaceLines: iface,
transferRows: xferRows,
}),
);
} finally {
btnRun.textContent = "开始导入";
btnRun.disabled = false;
btnRun.style.cursor = "pointer";
btnRun.style.opacity = "1";
}
};
row.appendChild(btnClose);
row.appendChild(btnRun);
box.appendChild(title);
box.appendChild(hint);
box.appendChild(fileRow);
box.appendChild(ta);
box.appendChild(status);
box.appendChild(detailWrap);
box.appendChild(row);
backdrop.appendChild(box);
document.body.appendChild(backdrop);
ta.focus();
},
async importMd5Json(rawJson, onBatchProgress) {
let obj;
try {
obj = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson;
} catch {
const err = new Error("JSON 解析失败");
err.guangyaDetail = guangyaJsonDetail({
summary: err.message,
phase: "解析输入",
});
throw err;
}
const list = Array.isArray(obj.files) ? obj.files : [];
if (!list.length) {
const err = new Error("JSON 中无 files 数组");
err.guangyaDetail = guangyaJsonDetail({
summary: err.message,
phase: "校验",
hint: "顶层需有 files 数组",
});
throw err;
}
const token = await panGuangya.getAccessToken();
if (!token) {
const err = new Error("未设置 access_token,已取消");
err.guangyaDetail = guangyaJsonDetail({
summary: err.message,
phase: "登录态",
});
throw err;
}
const rootParentId = "";
const files = [];
const skip = [];
for (const f of list) {
const pathStr = String(f.path || "").trim();
const nameStr = String(f.name || "").trim();
const fullPath = pathStr || nameStr || "file";
const numSize = Number(f.size != null ? f.size : 0);
const ex = guangyaExtractMd5FromEtag(f.etag || f.md5);
if (!ex.ok) {
skip.push(`${fullPath}:${ex.reason}`);
continue;
}
files.push({
md5: ex.md5,
filePath: fullPath,
fileName: guangyaBasenameFromPath(fullPath),
dirSegments: guangyaDirSegmentsFromPath(fullPath),
fileSize: Number.isFinite(numSize) && numSize >= 0 ? numSize : 0,
});
}
if (!files.length) {
const preview = skip.slice(0, 5).join("\n");
const msg = `没有可用的有效 MD5,常见原因:etag 虽 32 位但含字母 p/n 等非十六进制字符(不是标准 MD5)。\n${preview}${skip.length > 5 ? "\n…" : ""}`;
const err = new Error(msg);
err.guangyaDetail = guangyaJsonDetail({
summary: "无有效 MD5 条目",
phase: "校验",
skipped: skip,
parentId: rootParentId,
});
throw err;
}
const dirIdCache = new Map();
dirIdCache.set("", rootParentId);
let mkdirFailedCount = 0;
const ensureDirPath = async (row) => {
const parts = Array.isArray(row.dirSegments) ? row.dirSegments : [];
if (!parts.length) return rootParentId;
let currentParentId = rootParentId;
let fullDirPath = "";
for (const dirNameRaw of parts) {
const dirName = String(dirNameRaw || "").trim();
if (!dirName) continue;
fullDirPath = fullDirPath ? `${fullDirPath}/${dirName}` : dirName;
if (dirIdCache.has(fullDirPath)) {
currentParentId = String(dirIdCache.get(fullDirPath) || "");
continue;
}
const parentIdForReq = String(currentParentId || "");
let mkdirBody;
try {
const ret = await helper.postJsonGuangya(
GUANGYA_URL_CREATE_DIR,
{
dirName,
parentId: parentIdForReq,
failIfNameExist: true,
},
token,
{
allowedBusinessCodes: [0, GUANGYA_CODE_DIR_EXISTS],
},
);
mkdirBody = ret.data;
} catch (mkdirErr) {
mkdirFailedCount += 1;
let codeText = "";
try {
const d = JSON.parse(String(mkdirErr?.guangyaDetail || "{}"));
if (d && d.responseBody && d.responseBody.code != null) {
codeText = String(d.responseBody.code);
}
} catch {
/* ignore */
}
skip.push(
`${row.filePath}:创建目录失败(目录=${fullDirPath}${codeText ? `,code=${codeText}` : ""}),已跳过`,
);
return null;
}
const code = mkdirBody && mkdirBody.code;
const nextId = guangyaPickFileIdFromObj(mkdirBody && mkdirBody.data);
if (!nextId) {
mkdirFailedCount += 1;
skip.push(
`${row.filePath}:创建目录失败(目录=${fullDirPath},code=${code},无法取得目录ID),已跳过`,
);
return null;
}
dirIdCache.set(fullDirPath, nextId);
currentParentId = nextId;
}
return String(currentParentId || "");
};
const payloadFiles = [];
for (let i = 0; i < files.length; i++) {
const row = files[i];
if (typeof onBatchProgress === "function") {
try {
onBatchProgress({
phase: "mkdir",
index: i + 1,
total: files.length,
chunkSize: 1,
});
} catch {
/* ignore */
}
}
const parentId = await ensureDirPath(row);
if (parentId == null) {
continue;
}
payloadFiles.push({
md5: row.md5,
filePath: row.filePath,
fileName: row.fileName,
fileSize: row.fileSize,
parentId,
});
}
let lastResp = null;
let aggTransferOk = 0;
let aggTransferFail = 0;
/** @type {{ md5: string; filePath: string }[]} */
const transferFailedEntries = [];
const probeTotal = payloadFiles.length;
let instantHitCount = 0;
const pushFailRow = (row) => {
aggTransferFail += 1;
transferFailedEntries.push({
md5: row.md5,
filePath: row.filePath,
});
};
for (let fi = 0; fi < payloadFiles.length; fi++) {
const row = payloadFiles[fi];
if (typeof onBatchProgress === "function") {
try {
onBatchProgress({
phase: "probe",
index: fi + 1,
total: probeTotal,
chunkSize: 1,
});
} catch {
/* ignore */
}
}
await helper.sleep(0);
try {
const { data: apiBody } = await helper.postJsonGuangya(
GUANGYA_URL_GET_RES_CENTER_TOKEN,
{
capacity: 1,
res: {
md5: row.md5,
fileSize: row.fileSize,
},
name: row.fileName || guangyaBasenameFromPath(row.filePath),
parentId: String(row.parentId || ""),
},
token,
{
allowedBusinessCodes: [
0,
GUANGYA_CODE_RES_TOKEN_INSTANT,
],
},
);
lastResp = apiBody;
const code = apiBody && apiBody.code;
if (code === GUANGYA_CODE_RES_TOKEN_INSTANT) {
instantHitCount += 1;
aggTransferOk += 1;
} else {
const d = apiBody && apiBody.data;
const tid =
d &&
(d.taskId != null
? d.taskId
: d.task_id != null
? d.task_id
: "");
if (tid !== "" && tid != null) {
try {
await helper.postJsonGuangya(
GUANGYA_URL_DELETE_UPLOAD_TASK,
{ taskIds: [String(tid)] },
token,
);
} catch {
/* ignore */
}
}
pushFailRow(row);
}
} catch (apiErr) {
if (isGuangyaForbiddenNameError(apiErr)) {
// 文件名含违禁词,记录并跳过,继续导入剩余文件
skip.push(
`${row.filePath}:文件名含违禁词,已跳过(${String(apiErr.message || "").slice(0, 200)})`,
);
continue;
}
pushFailRow(row);
apiErr.partialTransferFailures = transferFailedEntries.slice();
apiErr.importFailedAtProbeIndex = fi + 1;
throw apiErr;
}
}
const transferFailedMissingDetail =
aggTransferFail > 0 && transferFailedEntries.length === 0;
return {
resp: lastResp,
skipCount: skip.length,
okCount: payloadFiles.length,
skipped: skip,
importSummary: {
batchCount: 0,
probeTotal,
instantHitCount,
mkdirFailedCount,
transferSuccess: aggTransferOk,
transferFail: aggTransferFail,
skipCount: skip.length,
transferFailedEntries,
transferFailedMissingDetail,
},
};
},
};
async function generate() {
const host = location.hostname;
let files;
let shareTitle = "";
helper.showLoadingDialog("正在生成秒传 JSON", "请稍候...");
try {
if (pan123.is123Host()) {
files = await pan123.collectFiles();
} else if (host.includes("quark.cn")) {
const isSharePage = /^\/(s|share)\//.test(location.pathname);
if (isSharePage) {
const result = await quark.getShareFiles();
files = result.files;
shareTitle = result.title || "";
} else {
helper.updateLoadingMsg("正在扫描个人文件...");
files = await quark.getHomeFiles();
}
} else if (host.includes("cloud.189.cn")) {
const isMain = location.pathname.startsWith("/web/main");
if (isMain) {
files = await tianyi.getFiles();
} else {
const sh = await tianyi.getShareFiles();
files = sh.files;
shareTitle = sh.title || "";
}
} else if (baidu.isBaiduHost()) {
files = await baidu.collectFiles();
} else {
throw new Error("当前站点不支持");
}
} catch (e) {
helper.closeLoadingDialog();
throw e;
}
const jsonData = helper.makeJson(files);
helper.closeLoadingDialog();
if (!jsonData.files.length) {
const policy = INVALID_ETAG_POLICY;
if (files.length > 0) {
throw new Error(
`生成结果为空:共 ${files.length} 条记录,但 etag 均为空,已按策略处理(guangya_etag_policy=${policy})。若为 skip,可尝试:localStorage.setItem("guangya_etag_policy","empty") 后重试。`,
);
}
throw new Error(
"生成结果为空:没有可导出条目。请勾选文件/文件夹后再生成;若已勾选仍为空,请刷新页面后重试。",
);
}
helper.showResultDialog(jsonData, shareTitle);
}
function resolveQuarkContainer() {
const isShare = /^\/(s|share)\//.test(location.pathname);
const selectors = isShare
? [
".share-btns",
".ant-layout-content .operate-bar",
".share-detail-header .operate-bar",
".share-header-btns",
".share-operate-btns",
".ant-btn-group",
".ant-layout-content",
]
: [
".btn-operate .btn-main",
".btn-operate",
".operate-bar",
".ant-layout-content",
];
for (const s of selectors) {
const el = document.querySelector(s);
if (el) return el;
}
return null;
}
/** 天翼左侧导航/侧栏内会出现与主内容区相似的节点,误匹配会导致按钮飞到左上角 */
function isInTianyiSidebar(el) {
if (!el || !el.closest) return false;
return !!el.closest(
".ant-layout-sider, aside, [class*='layout-sider'], [class*='side-bar'], [class*='sidebar'], " +
"[class*='Sider'], .c-nav-left, .left-nav, [class*='NavLeft'], [class*='nav-left'], " +
"[class*='menu-side'], [class*='sideMenu']",
);
}
function findTianyiUploadAnchor() {
const nodes = document.querySelectorAll(
"button, a, .ant-btn, span.ant-btn, [role='button']",
);
for (let i = 0; i < nodes.length; i++) {
const el = nodes[i];
if (isInTianyiSidebar(el)) continue;
const text = (el.textContent || "").replace(/\s+/g, "");
const aria =
(el.getAttribute("aria-label") || "") + (el.getAttribute("title") || "");
if (!text.includes("上传") && !aria.includes("上传")) continue;
const r = el.getBoundingClientRect();
if (r.width < 2 || r.height < 2) continue;
const cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
return el;
}
return null;
}
/**
* 在多个 FileHead / file-head 节点中选「主文件区工具栏」(含上传/刷新等),排除侧栏误匹配。
*/
function resolveTianyiFileHeadToolbarScored() {
const all = document.querySelectorAll(
'[class*="FileHead"], .file-head-left, .file-head-right, .c-file-head__left, .c-file-head__right',
);
let best = null;
let bestScore = -1;
for (let i = 0; i < all.length; i++) {
const el = all[i];
if (isInTianyiSidebar(el)) continue;
const r = el.getBoundingClientRect();
if (r.width < 60 || r.height < 12) continue;
const cs = window.getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
let score = 0;
if (el.closest(".ant-layout-content, main, [class*='Content'], [class*='content-main']")) {
score += 18;
}
const snippet = (el.textContent || "").replace(/\s+/g, "").slice(0, 120);
if (snippet.includes("上传")) score += 25;
if (snippet.includes("刷新")) score += 10;
if (snippet.includes("新建文件夹") || snippet.includes("新建")) score += 8;
// 主区工具栏一般在顶栏下方、偏左,避免选到页脚或侧栏条
if (r.top > 40 && r.top < 280 && r.left > 80) score += 12;
if (score > bestScore) {
bestScore = score;
best = el;
}
}
return best;
}
function resolveTianyiContainer() {
const isMain = location.pathname.startsWith("/web/main");
if (!isMain) {
const shareSelectors = [
".file-operate",
".outlink-box-b .file-operate",
".c-file-operate",
];
for (const s of shareSelectors) {
const el = document.querySelector(s);
if (el && !isInTianyiSidebar(el)) return el;
}
return null;
}
const upload = findTianyiUploadAnchor();
if (upload && upload.parentElement && !isInTianyiSidebar(upload.parentElement)) {
return upload.parentElement;
}
const scored = resolveTianyiFileHeadToolbarScored();
if (scored) return scored;
const legacy = [
'[class*="FileHead_file-head-left"]',
".FileHead_file-head-left",
".file-head-left",
".c-file-head__left",
];
for (const s of legacy) {
const els = document.querySelectorAll(s);
for (let j = 0; j < els.length; j++) {
const el = els[j];
if (!isInTianyiSidebar(el)) return el;
}
}
return null;
}
/** 生成按钮是否已挂在天翼主文件工具栏附近(非侧栏、非左上角 fixed 兜底) */
function isGuangyaBtnOkOnTianyi(btn) {
if (!btn || !btn.isConnected) return false;
if (isInTianyiSidebar(btn)) return false;
const r = btn.getBoundingClientRect();
if (r.width < 2 || r.height < 2) return false;
if (r.top > window.innerHeight * 0.55) return false;
const cs = window.getComputedStyle(btn);
if (cs.position === "fixed" && r.top < 120 && r.left < 120) return false;
return true;
}
/** 工具栏 flex 横向排布 */
function ensureTianyiShareToolbarFlexStyle() {
if (document.getElementById(GUANGYA_TIANYI_SHARE_STYLE_ID)) return;
const style = document.createElement("style");
style.id = GUANGYA_TIANYI_SHARE_STYLE_ID;
style.textContent =
".outlink-box-b .file-operate{display:flex!important;flex-wrap:nowrap!important;" +
"justify-content:flex-end!important;align-items:center!important;float:none!important;" +
"text-align:unset!important;}" +
".outlink-box-b .file-operate .btn-save-as{margin-left:0!important;}";
document.head.appendChild(style);
}
/**
* 天翼:插到「上传」左侧,或紧挨 123 脚本的「生成JSON」;避免 querySelector 命中侧栏导致 fixed 兜底。
*/
function tryMountTianyiBesideToolbar() {
if (!location.hostname.includes("cloud.189.cn")) return false;
const isMain = location.pathname.startsWith("/web/main");
const existing = document.getElementById(BTN_ID);
if (existing && isGuangyaBtnOkOnTianyi(existing)) return true;
if (existing) existing.remove();
const jsonGen = document.getElementById("quark-json-generator-btn");
const upload = findTianyiUploadAnchor();
if (isMain) {
if (upload && upload.parentElement) {
upload.insertAdjacentElement("beforebegin", makeGuangyaButtonElement(false));
return isGuangyaBtnOkOnTianyi(document.getElementById(BTN_ID));
}
if (jsonGen && jsonGen.parentElement && !isInTianyiSidebar(jsonGen)) {
jsonGen.insertAdjacentElement("afterend", makeGuangyaButtonElement(false));
return isGuangyaBtnOkOnTianyi(document.getElementById(BTN_ID));
}
const row = resolveTianyiFileHeadToolbarScored() || resolveTianyiContainer();
if (row) {
row.appendChild(makeGuangyaButtonElement(false));
return isGuangyaBtnOkOnTianyi(document.getElementById(BTN_ID));
}
return false;
}
ensureTianyiShareToolbarFlexStyle();
const fo =
document.querySelector(".file-operate, .outlink-box-b .file-operate, .c-file-operate") ||
null;
if (!fo || isInTianyiSidebar(fo)) return false;
if (jsonGen && fo.contains(jsonGen)) {
jsonGen.insertAdjacentElement("afterend", makeGuangyaButtonElement(false));
} else if (upload && fo.contains(upload)) {
upload.insertAdjacentElement("beforebegin", makeGuangyaButtonElement(false));
} else {
fo.insertBefore(makeGuangyaButtonElement(false), fo.firstChild);
}
return isGuangyaBtnOkOnTianyi(document.getElementById(BTN_ID));
}
/** `upload-button` / `mfy-button`,优先用它定位,比纯文案稳 */
function find123UploadInContainer(container) {
if (!container) return null;
const byClass = container.querySelector(
"button.upload-button, .upload-button.ant-btn, button.mfy-button.upload-button, .mfy-button.upload-button",
);
if (byClass) return byClass;
const nodes = container.querySelectorAll("button, .ant-btn, [role='button']");
for (let i = 0; i < nodes.length; i++) {
const el = nodes[i];
const text = (el.textContent || "").replace(/\s+/g, "");
const aria =
(el.getAttribute("aria-label") || "") +
(el.getAttribute("title") || "");
if (text.includes("上传") || aria.includes("上传")) return el;
}
return null;
}
/**
* 在多个 .home-operator-button-group / .home-operator 中选「主文件区」工具栏(避免顶栏横幅先渲染导致误插)。
*/
function resolve123ToolbarAndUpload() {
/** @type {{ toolbar: Element; upload: Element; score: number }[]} */
const candidates = [];
const groups = document.querySelectorAll(
".home-operator-button-group, .home-operator",
);
for (let i = 0; i < groups.length; i++) {
const g = groups[i];
const upload = find123UploadInContainer(g);
if (!upload) continue;
const rect = g.getBoundingClientRect();
if (rect.width < 8 || rect.height < 8) continue;
const cs = window.getComputedStyle(g);
if (cs.display === "none" || cs.visibility === "hidden") continue;
let score = 0;
if (g.closest(".ant-layout-content, main, [class*='layout-content']")) {
score += 20;
}
if (g.querySelector(".upload-button, button.upload-button")) {
score += 10;
}
const cls = (g.className && String(g.className)) || "";
if (/banner|promo|advert|top-notice|activity/i.test(cls)) {
score -= 30;
}
candidates.push({ toolbar: g, upload, score });
}
candidates.sort((a, b) => b.score - a.score);
if (!candidates.length) return { toolbar: null, upload: null };
return {
toolbar: candidates[0].toolbar,
upload: candidates[0].upload,
};
}
/**
* 将生成按钮挂到上传左侧;若已有按钮但在错误位置(例如先挂了浮动),则移除后重挂。
* @returns {boolean} 是否已成功挂载或已正确挂载
*/
function tryMount123BesideUpload() {
const { upload } = resolve123ToolbarAndUpload();
if (!upload || !upload.parentElement) return false;
const existing = document.getElementById(BTN_ID);
if (existing) {
const ok =
existing.nextElementSibling === upload ||
upload.previousElementSibling === existing;
if (ok) return true;
existing.remove();
}
upload.insertAdjacentElement("beforebegin", makeGuangyaButtonElement(false));
return true;
}
function injectGuangyaButtonTypographyStyles() {
if (document.getElementById(GUANGYA_BTN_TYPO_STYLE_ID)) return;
const st = document.createElement("style");
st.id = GUANGYA_BTN_TYPO_STYLE_ID;
st.textContent =
"#" +
BTN_ID +
".guangya-rapid-json-btn.ant-btn," +
"#" +
BTN_ID +
".guangya-rapid-json-btn.ant-btn > span," +
"#" +
BTN_GUANGYA_IMPORT_ID +
".guangya-rapid-json-btn.ant-btn," +
"#" +
BTN_GUANGYA_IMPORT_ID +
".guangya-rapid-json-btn.ant-btn > span {" +
"font-size:18px !important;" +
"line-height:1.45 !important;" +
"font-weight:500 !important;" +
"}";
document.head.appendChild(st);
}
function guangyaAntBtnFromDropdownTrigger(trig) {
if (!trig || !trig.matches) return null;
if (trig.matches("button.ant-btn") || trig.matches(".ant-btn")) {
return trig;
}
return trig.querySelector("button.ant-btn, .ant-btn");
}
function guangyaIsUploadButtonLabel(normalizedText) {
if (normalizedText === "上传") return true;
return /^upload$/i.test(normalizedText);
}
function guangyaRowLooksLikeUploadToolbar(row, uploadBtn) {
if (!row || !uploadBtn) return false;
const n = (row.textContent || "").replace(/\s+/g, "");
if (n.includes("新建文件夹") || n.includes("云添加")) return true;
const primaries = Array.from(
row.querySelectorAll("button.ant-btn-primary"),
).filter((b) => b.id !== BTN_GUANGYA_IMPORT_ID);
return primaries.length === 1 && primaries[0] === uploadBtn;
}
function resolveGuangyaUploadToolbarRow() {
const triggers = document.querySelectorAll(".ant-dropdown-trigger");
for (const trig of triggers) {
const inner = guangyaAntBtnFromDropdownTrigger(trig);
if (!inner || inner.id === BTN_GUANGYA_IMPORT_ID) continue;
if (!inner.classList.contains("ant-btn-primary")) continue;
const txt = (inner.textContent || "").replace(/\s+/g, "");
if (!guangyaIsUploadButtonLabel(txt)) continue;
const row = trig.parentElement;
if (!row || !guangyaRowLooksLikeUploadToolbar(row, inner)) continue;
return { row, uploadAnchor: trig };
}
const primaries = document.querySelectorAll("button.ant-btn-primary");
for (const inner of primaries) {
if (inner.id === BTN_GUANGYA_IMPORT_ID) continue;
const txt = (inner.textContent || "").replace(/\s+/g, "");
if (!guangyaIsUploadButtonLabel(txt)) continue;
const row = inner.parentElement;
if (!row || !guangyaRowLooksLikeUploadToolbar(row, inner)) continue;
return { row, uploadAnchor: inner };
}
return null;
}
function makeGuangyaPanImportButtonElement(floating) {
injectGuangyaButtonTypographyStyles();
const btn = document.createElement("button");
btn.id = BTN_GUANGYA_IMPORT_ID;
btn.type = "button";
btn.className = "ant-btn ant-btn-primary guangya-rapid-json-btn";
const span = document.createElement("span");
span.textContent = "导入秒传JSON";
btn.appendChild(span);
span.style.setProperty("font-size", "18px", "important");
span.style.setProperty("line-height", "1.45", "important");
span.style.setProperty("font-weight", "500", "important");
let css =
"box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;" +
"height:44px;min-height:44px;padding:0 22px;" +
"border-radius:8px;white-space:nowrap;cursor:pointer;vertical-align:middle;" +
"background:#ff9800 !important;border:1px solid #ff9800 !important;color:#fff !important;" +
"font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial," +
"\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;";
if (floating) {
css +=
"position:fixed;left:24px;top:24px;z-index:2147483647;margin-right:0;box-shadow:0 6px 20px rgba(0,0,0,.2);";
} else {
css += "position:static;margin-right:0;margin-left:0;";
}
btn.style.cssText = css;
btn.style.setProperty("font-size", "18px", "important");
btn.style.setProperty("line-height", "1.45", "important");
btn.onclick = () => {
try {
panGuangya.showImportDialog();
} catch (e) {
alert(e?.message || String(e));
}
};
return btn;
}
function styleGuangyaImportButtonForToolbar(btn) {
btn.style.position = "static";
btn.style.left = "";
btn.style.top = "";
btn.style.boxShadow = "";
btn.style.zIndex = "";
btn.style.marginRight = "0";
btn.style.verticalAlign = "middle";
}
function tryMountGuangyaBesideUpload() {
const hit = resolveGuangyaUploadToolbarRow();
if (!hit) return false;
let btn = document.getElementById(BTN_GUANGYA_IMPORT_ID);
if (!btn) {
btn = makeGuangyaPanImportButtonElement(false);
} else {
styleGuangyaImportButtonForToolbar(btn);
}
const anchor = hit.uploadAnchor;
if (anchor.previousElementSibling === btn) {
return true;
}
anchor.insertAdjacentElement("beforebegin", btn);
return true;
}
function makeGuangyaButtonElement(floating) {
injectGuangyaButtonTypographyStyles();
const btn = document.createElement("button");
btn.id = BTN_ID;
btn.type = "button";
btn.className = "ant-btn ant-btn-primary guangya-rapid-json-btn";
const span = document.createElement("span");
span.textContent = "生成秒传JSON";
btn.appendChild(span);
span.style.setProperty("font-size", "18px", "important");
span.style.setProperty("line-height", "1.45", "important");
span.style.setProperty("font-weight", "500", "important");
const setLabel = (t) => {
if (span) span.textContent = t;
else btn.textContent = t;
};
let css =
"box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;" +
"height:44px;min-height:44px;padding:0 22px;margin-right:8px;" +
"border-radius:8px;white-space:nowrap;" +
"cursor:pointer;vertical-align:middle;" +
"background:#ff9800 !important;border-color:#ff9800 !important;color:#fff !important;" +
"font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial," +
"\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;";
if (floating) {
css +=
"position:fixed;right:24px;top:24px;z-index:2147483647;margin-right:0;" +
"box-shadow:0 4px 18px rgba(255,152,0,.5),0 2px 8px rgba(0,0,0,.18);" +
"border-radius:10px;transition:box-shadow .2s,transform .15s,opacity .15s;";
btn.addEventListener("mouseenter", () => {
btn.style.setProperty("box-shadow", "0 6px 24px rgba(255,152,0,.7),0 3px 12px rgba(0,0,0,.22)", "important");
btn.style.setProperty("transform", "translateY(-1px)", "important");
});
btn.addEventListener("mouseleave", () => {
btn.style.removeProperty("box-shadow");
btn.style.removeProperty("transform");
});
}
btn.style.cssText = css;
btn.style.setProperty("font-size", "18px", "important");
btn.style.setProperty("line-height", "1.45", "important");
btn.onclick = async () => {
try {
btn.disabled = true;
setLabel("生成中...");
await generate();
setLabel("生成秒传JSON");
} catch (e) {
alert(e?.message || "生成失败");
setLabel("生成秒传JSON");
} finally {
btn.disabled = false;
}
};
return btn;
}
function createButton() {
const host = location.hostname;
if (panGuangya.isHost()) {
if (tryMountGuangyaBesideUpload()) return;
if (!document.getElementById(BTN_GUANGYA_IMPORT_ID)) {
const body =
document.querySelector(BODY_SELECTOR) ||
document.documentElement;
body.appendChild(makeGuangyaPanImportButtonElement(true));
}
return;
}
if (pan123.is123Host() && PREFER_123_TOOLBAR) {
if (tryMount123BesideUpload()) return;
return;
}
if (host.includes("cloud.189.cn")) {
if (tryMountTianyiBesideToolbar()) return;
}
if (document.getElementById(BTN_ID)) return;
if (pan123.is123Host() && !PREFER_123_TOOLBAR) {
const body = document.querySelector(BODY_SELECTOR) || document.documentElement;
body.appendChild(makeGuangyaButtonElement(true));
return;
}
/** @type {Element} */
let container = document.querySelector("*");
let matchedHost = false;
let useFloating = false;
if (pan123.is123Host()) {
matchedHost = true;
container =
document.querySelector(BODY_SELECTOR) || document.documentElement;
useFloating = true;
} else if (host.includes("quark.cn")) {
matchedHost = true;
const found = resolveQuarkContainer();
if (!found) {
container = document.querySelector(BODY_SELECTOR);
useFloating = true;
} else {
container = found;
}
} else if (host.includes("cloud.189.cn")) {
matchedHost = true;
const found = resolveTianyiContainer();
if (!found) return;
container = found;
} else if (baidu.isBaiduHost()) {
matchedHost = true;
// 挂到 html 元素,避免百度 SPA 替换 body 内容时按钮丢失导致重复创建
container = document.documentElement;
useFloating = true;
}
if (!matchedHost || !container) return;
const btn = makeGuangyaButtonElement(useFloating);
if (
host.includes("quark.cn") &&
!/^\/(s|share)\//.test(location.pathname)
) {
if (useFloating) {
container.appendChild(btn);
} else {
container.insertBefore(btn, container.firstChild);
}
} else {
container.appendChild(btn);
}
}
function init() {
if (
!location.hostname.includes("quark.cn") &&
!location.hostname.includes("cloud.189.cn") &&
!pan123.is123Host() &&
!baidu.isBaiduHost() &&
!panGuangya.isHost()
) {
return;
}
const obsRoot = document.body || document.documentElement;
if (!obsRoot) return;
injectGuangyaButtonTypographyStyles();
if (
location.hostname.includes("cloud.189.cn") &&
!location.pathname.startsWith("/web/main")
) {
ensureTianyiShareToolbarFlexStyle();
}
const observer = new MutationObserver(() => createButton());
observer.observe(obsRoot, { childList: true, subtree: true });
createButton();
[400, 1500, 4000, 8000].forEach((ms) => setTimeout(createButton, ms));
if (pan123.is123Host()) {
try {
pan123.initSelector();
} catch {
/* ignore */
}
}
}
if (typeof GM_registerMenuCommand === "function") {
try {
GM_registerMenuCommand("[秒传工具] 清除当前网盘 access_token(GM 保存)", () => {
GM_setValue(KEY_GUANGYA_ACCESS_TOKEN, "");
alert("已清除当前网盘 access_token;下次导入会尝试读页面存储或再弹窗粘贴。");
});
} catch {
/* ignore */
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();