// ==UserScript==
// @name 123FastLinkPlus
// @version 1.0.1
// @description 一键将夸克网盘、天翼云盘的个人文件和分享链接转存到123云盘
// @author meguoe
// @license Apache-2.0
// @match https://pan.quark.cn/*
// @match https://drive.quark.cn/*
// @match https://pan.quark.cn/s/*
// @match https://drive.quark.cn/s/*
// @match https://cloud.189.cn/web/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=123pan.com
// @grant GM_setClipboard
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// @connect drive.quark.cn
// @connect drive-pc.quark.cn
// @connect pc-api.uc.cn
// @connect cloud.189.cn
// @connect www.123pan.com
// ==/UserScript==
(function () {
"use strict";
// 添加统一的CSS样式
const addStyles = () => {
if (document.getElementById("fastlink-styles")) return;
const style = document.createElement("style");
style.id = "fastlink-styles";
style.textContent = `
/* CSS变量定义 */
:root {
--fastlink-primary-color: #1890ff;
--fastlink-primary-light: #40a9ff;
--fastlink-primary-dark: #096dd9;
--fastlink-success-color: #52c41a;
--fastlink-success-light: #73d13d;
--fastlink-success-dark: #389e0d;
--fastlink-error-color: #ff4d4f;
--fastlink-error-light: #ff7875;
--fastlink-error-dark: #cf1322;
--fastlink-warning-color: #faad14;
--fastlink-warning-light: #ffc53d;
--fastlink-warning-dark: #d48806;
--fastlink-info-color: #13c2c2;
--fastlink-info-light: #36cfc9;
--fastlink-info-dark: #08979c;
--fastlink-text-color: #262626;
--fastlink-text-secondary: #595959;
--fastlink-text-light: #8c8c8c;
--fastlink-text-lighter: #bfbfbf;
--fastlink-bg-color: #ffffff;
--fastlink-bg-light: #fafafa;
--fastlink-bg-lighter: #f5f5f5;
--fastlink-border-color: #d9d9d9;
--fastlink-border-light: #e8e8e8;
--fastlink-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
--fastlink-shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1);
--fastlink-radius: 8px;
--fastlink-radius-small: 4px;
--fastlink-radius-large: 12px;
--fastlink-font-size: 14px;
--fastlink-font-size-small: 12px;
--fastlink-font-size-large: 16px;
--fastlink-font-size-xl: 18px;
--fastlink-spacing: 16px;
--fastlink-spacing-small: 8px;
--fastlink-spacing-large: 24px;
--fastlink-spacing-xl: 32px;
--fastlink-transition: all 0.3s ease;
--fastlink-transition-fast: all 0.15s ease;
}
/* 对话框基础样式 */
.fastlink-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
.fastlink-dialog {
background: var(--fastlink-bg-color);
padding: var(--fastlink-spacing-large);
border-radius: var(--fastlink-radius);
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: var(--fastlink-shadow);
animation: fastlink-dialog-fade-in 0.3s ease;
}
@keyframes fastlink-dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fastlink-breadcrumb-item {
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fastlink-dialog-title {
font-size: var(--fastlink-font-size-large);
font-weight: 600;
margin-bottom: var(--fastlink-spacing);
color: var(--fastlink-text-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.fastlink-dialog-content {
flex: 1;
overflow-y: hidden;
margin-bottom: var(--fastlink-spacing);
}
.fastlink-dialog-footer {
display: flex;
gap: var(--fastlink-spacing-small);
justify-content: flex-end;
margin-top: var(--fastlink-spacing);
}
/* 按钮样式 */
.fastlink-btn {
padding: 10px 24px;
border: none;
border-radius: var(--fastlink-radius-small);
cursor: pointer;
font-size: var(--fastlink-font-size);
font-weight: 500;
transition: var(--fastlink-transition);
min-width: 80px;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.fastlink-btn:hover {
box-shadow: var(--fastlink-shadow-light);
}
.fastlink-btn:active {
transition: var(--fastlink-transition-fast);
}
.fastlink-btn-primary {
background: var(--fastlink-primary-color) !important;
color: white !important;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.04);
}
.fastlink-btn-primary:hover {
background: var(--fastlink-primary-light);
}
.fastlink-btn-success {
background: var(--fastlink-success-color);
color: white;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.04);
}
.fastlink-btn-success:hover {
background: var(--fastlink-success-light);
}
.fastlink-btn-default {
background: var(--fastlink-bg-color);
color: var(--fastlink-text-color);
border: 1px solid var(--fastlink-border-color);
}
.fastlink-btn-default:hover {
background: var(--fastlink-bg-light);
border-color: var(--fastlink-primary-light);
}
/* 进度条样式 */
.fastlink-progress-container {
margin: var(--fastlink-spacing) 0;
}
.fastlink-progress-text {
font-size: var(--fastlink-font-size-small);
color: var(--fastlink-text-secondary);
font-weight: 500;
margin-bottom: var(--fastlink-spacing-small);
}
.fastlink-progress-bar-container {
width: 100%;
height: 10px;
background: var(--fastlink-bg-lighter);
border-radius: var(--fastlink-radius-small);
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
}
.fastlink-progress-bar {
width: 0%;
height: 100%;
background: linear-gradient(90deg, var(--fastlink-primary-color), var(--fastlink-primary-light));
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: var(--fastlink-radius-small);
position: relative;
overflow: hidden;
}
.fastlink-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: fastlink-progress-shine 1.5s infinite;
}
@keyframes fastlink-progress-shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* 日志样式 */
.fastlink-log-container {
margin-top: var(--fastlink-spacing);
text-align: left;
display: none;
}
.fastlink-log {
max-height: 200px;
overflow-y: auto;
font-size: var(--fastlink-font-size-small);
font-family: monospace;
background: var(--fastlink-bg-light);
padding: 10px 16px;
border-radius: 4px;
border: 1px solid var(--fastlink-border-light);
}
.fastlink-log-item {
display: flex;
align-items: flex-start;
margin-bottom: 4px;
padding-bottom: 4px;
border-bottom: 1px solid var(--fastlink-border-light);
}
.fastlink-log-time {
color: var(--fastlink-text-light);
margin-right: var(--fastlink-spacing-small);
min-width: 60px;
}
.fastlink-log-file {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fastlink-log-status {
margin-left: var(--fastlink-spacing-small);
font-weight: 500;
}
.fastlink-log-status.success {
color: var(--fastlink-success-color);
}
.fastlink-log-status.error {
color: var(--fastlink-error-color);
}
/* 表单样式 */
.fastlink-form-item {
margin-bottom: var(--fastlink-spacing);
}
.fastlink-form-label {
display: block;
margin-bottom: var(--fastlink-spacing-small);
font-weight: 500;
color: var(--fastlink-text-color);
}
.fastlink-form-input {
width: 100%;
padding: 8px;
border: 1px solid var(--fastlink-border-color);
border-radius: 4px;
font-size: var(--fastlink-font-size);
transition: var(--fastlink-transition);
}
.fastlink-form-input:focus {
outline: none;
border-color: var(--fastlink-primary-color);
box-shadow: 0 0 0 2px rgba(13, 83, 255, 0.1);
}
.fastlink-form-textarea {
width: 100%;
min-height: 200px;
padding: var(--fastlink-spacing-small);
border: 1px solid var(--fastlink-border-color);
border-radius: 4px;
font-size: var(--fastlink-font-size-small);
font-family: monospace;
resize: vertical;
transition: var(--fastlink-transition);
}
.fastlink-form-textarea:focus {
outline: none;
border-color: var(--fastlink-primary-color);
box-shadow: 0 0 0 2px rgba(13, 83, 255, 0.1);
}
/* 错误对话框样式 */
.fastlink-error-icon {
color: var(--fastlink-error-color);
margin-bottom: var(--fastlink-spacing);
text-align: center;
}
.fastlink-error-title {
font-size: var(--fastlink-font-size-large);
font-weight: 600;
margin-bottom: var(--fastlink-spacing-small);
color: var(--fastlink-text-color);
text-align: center;
}
.fastlink-error-message {
font-size: var(--fastlink-font-size);
color: var(--fastlink-text-secondary);
margin-bottom: var(--fastlink-spacing-large);
text-align: center;
white-space: pre-line;
}
`;
document.head.appendChild(style);
};
const utils = {
// 对话框缓存
_dialogCache: {
loading: null,
auth: null,
cookie: null,
error: null,
currentId: 0,
},
// 初始化样式
initStyles() {
addStyles();
},
getCachedCookie() {
return GM_getValue("quark_cookie", "");
},
saveCookie(cookie) {
GM_setValue("quark_cookie", cookie);
},
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
return null;
},
// 123云盘认证信息管理
get123PanAuth() {
return {
authToken: GM_getValue("pan123_authToken", ""),
loginUuid: GM_getValue("pan123_loginUuid", ""),
};
},
save123PanAuth(authToken, loginUuid) {
// 只使用GM_setValue存储认证信息,不使用localStorage,提高安全性
GM_setValue("pan123_authToken", authToken);
GM_setValue("pan123_loginUuid", loginUuid);
},
show123PanAuthDialog(onSave, currentAuth = null) {
// 初始化样式
this.initStyles();
const auth = currentAuth || this.get123PanAuth();
const dialog = document.createElement("div");
dialog.id = "pan123-auth-input-dialog";
dialog.className = "fastlink-dialog-overlay";
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
document.getElementById("pan123-auth-save-btn").onclick = () => {
const authToken = document
.getElementById("pan123-auth-token")
.value.trim();
const loginUuid = document
.getElementById("pan123-login-uuid")
.value.trim();
if (!authToken || !loginUuid) {
alert("认证信息不能为空");
return;
}
this.save123PanAuth(authToken, loginUuid);
dialog.remove();
GM_notification({
text: "123云盘认证信息已保存",
timeout: 2000,
});
if (onSave) {
onSave({ authToken, loginUuid });
}
};
document.getElementById("pan123-auth-cancel-btn").onclick = () => {
dialog.remove();
};
},
showCookieInputDialog(onSave, currentCookie = "") {
// 初始化样式
this.initStyles();
const dialog = document.createElement("div");
dialog.id = "quark-cookie-input-dialog";
dialog.className = "fastlink-dialog-overlay";
dialog.innerHTML = `
设置夸克网盘Cookie
请打开浏览器开发者工具(F12) → Network → 找到任意请求 → 复制完整的Cookie值
必须包含:__puus、__pus、ctoken 等关键Cookie
`;
document.body.appendChild(dialog);
document.getElementById("quark-cookie-save-btn").onclick = () => {
const cookie = document
.getElementById("quark-cookie-input")
.value.trim();
if (!cookie) {
alert("Cookie不能为空");
return;
}
this.saveCookie(cookie);
dialog.remove();
GM_notification({
text: "Cookie已保存",
timeout: 2000,
});
if (onSave) {
onSave(cookie);
}
};
document.getElementById("quark-cookie-cancel-btn").onclick = () => {
dialog.remove();
};
},
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
findReact(dom, traverseUp = 0) {
let key = Object.keys(dom).find((key) => {
return (
key.startsWith("__reactFiber$") ||
key.startsWith("__reactInternalInstance$")
);
});
let domFiber = dom[key];
if (domFiber == null) {
return null;
}
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber._currentElement._owner;
}
return compFiber._instance;
}
const GetCompFiber = (fiber) => {
let parentFiber = fiber.return;
while (typeof parentFiber.type === "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode || compFiber;
},
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 = [];
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() {
try {
const fileListDom = document.getElementsByClassName("file-list")[0];
if (!fileListDom) {
return [];
}
const reactObj = this.findReact(fileListDom);
const props = reactObj?.props;
if (props) {
const fileList = props.list || [];
const selectedKeys = props.selectedRowKeys || [];
const selectedList = [];
fileList.forEach(function (val) {
if (selectedKeys.includes(val.fid)) {
selectedList.push(val);
}
});
return selectedList;
}
return [];
} catch (e) {
return [];
}
},
post(url, data, headers = {}) {
return new Promise((resolve, reject) => {
try {
// 验证URL
if (!url || typeof url !== "string" || !url.startsWith("http")) {
reject(new Error("无效的请求URL"));
return;
}
// 验证数据
if (data === undefined || data === null) {
reject(new Error("请求数据不能为空"));
return;
}
const requestData = JSON.stringify(data);
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 defaultHeaders = {
"Content-Type": "application/json;charset=utf-8",
"User-Agent": QUARK_UA,
Origin: location.origin,
Referer: `${location.origin}/`,
Dnt: "",
"Cache-Control": "no-cache",
Pragma: "no-cache",
Expires: "0",
};
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: { ...defaultHeaders, ...headers },
data: requestData,
onload: function (response) {
try {
// 检查响应状态
if (response.status < 200 || response.status >= 300) {
reject(new Error(`请求失败,状态码:${response.status}`));
return;
}
// 尝试解析响应
let result;
try {
result = JSON.parse(response.responseText);
} catch (e) {
reject(new Error("响应解析失败"));
return;
}
resolve(result);
} catch (e) {
reject(new Error("处理响应时出错"));
}
},
onerror: function (error) {
reject(new Error("网络请求失败"));
},
ontimeout: function () {
reject(new Error("请求超时"));
},
});
} catch (e) {
reject(new Error(`请求准备失败:${e.message}`));
}
});
},
get(url, headers = {}) {
return new Promise((resolve, reject) => {
try {
// 验证URL
if (!url || typeof url !== "string" || !url.startsWith("http")) {
reject(new Error("无效的请求URL"));
return;
}
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: headers,
onload: function (response) {
try {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
} else {
reject(new Error(`请求失败: ${response.status}`));
}
} catch (e) {
reject(new Error("处理响应时出错"));
}
},
onerror: function (error) {
reject(new Error("网络请求失败"));
},
ontimeout: function () {
reject(new Error("请求超时"));
},
});
} catch (e) {
reject(new Error(`请求准备失败:${e.message}`));
}
});
},
async getFolderFiles(folderId, folderPath = "", onProgress) {
const API_URL = "https://drive-pc.quark.cn/1/clouddrive/file/sort?pr=ucpro&fr=pc";
const allFiles = [];
let page = 1;
const pageSize = 50;
while (true) {
const url = `${API_URL}&pdir_fid=${folderId}&_page=${page}&_size=${pageSize}&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc`;
const result = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function (response) {
try {
resolve(JSON.parse(response.responseText));
} catch (e) {
reject(new Error("响应解析失败"));
}
},
onerror: () => reject(new Error("网络请求失败")),
});
});
if (result?.code !== 0 || !result?.data?.list) {
break;
}
const items = result.data.list;
for (const item of items) {
const itemPath = folderPath
? `${folderPath}/${item.file_name}`
: item.file_name;
if (item.dir) {
const subFiles = await this.getFolderFiles(
item.fid,
itemPath,
onProgress
);
allFiles.push(...subFiles);
} else if (item.file) {
allFiles.push({ ...item, path: itemPath });
if (onProgress) {
onProgress();
}
}
}
if (items.length < pageSize) {
break;
}
page++;
}
return allFiles;
},
async getShareToken(shareId, passcode = "", cookie = "") {
const API_URL = "https://pc-api.uc.cn/1/clouddrive/share/sharepage/token";
try {
const result = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: API_URL,
headers: {
"Content-Type": "application/json",
Cookie: cookie,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Referer: "https://pan.quark.cn/",
},
data: JSON.stringify({
pwd_id: shareId,
passcode: passcode,
}),
onload: function (response) {
try {
resolve(JSON.parse(response.responseText));
} catch (e) {
reject(new Error("响应解析失败"));
}
},
onerror: () => reject(new Error("网络请求失败")),
});
});
if (result?.code === 31001) {
throw new Error("请先登录网盘");
}
if (result?.code !== 0) {
throw new Error(
`获取token失败,代码:${result.code},消息:${result.message}`
);
}
return {
stoken: result.data.stoken,
title: result.data.title || "",
};
} catch (error) {
throw error;
}
},
async getFilesWithMd5(fileList, onProgress) {
const API_URL = "https://drive.quark.cn/1/clouddrive/file/download?pr=ucpro&fr=pc";
const BATCH_SIZE = 15;
const MAX_PARALLEL = 3;
const validFiles = fileList.filter((item) => item.file === true);
const pathMap = this._buildPathMap(validFiles);
const data = [];
let processed = 0;
// 分批处理,每批并行发送多个请求
const batches = [];
for (let i = 0; i < validFiles.length; i += BATCH_SIZE) {
batches.push(validFiles.slice(i, i + BATCH_SIZE));
}
// 并行处理批次
for (let i = 0; i < batches.length; i += MAX_PARALLEL) {
const parallelBatches = batches.slice(i, i + MAX_PARALLEL);
const batchPromises = parallelBatches.map((batch, batchIndex) => {
const batchProcessed = processed + batchIndex * BATCH_SIZE;
return this._processFileBatch(
batch,
pathMap,
API_URL,
data,
batchProcessed,
validFiles.length,
onProgress
);
});
await Promise.all(batchPromises);
processed += parallelBatches.reduce(
(sum, batch) => sum + batch.length,
0
);
// 批次之间添加较小的延迟,避免请求过于密集
if (i + MAX_PARALLEL < batches.length) {
await this.sleep(500);
}
}
return data;
},
_buildPathMap(files) {
const pathMap = {};
files.forEach((file) => {
pathMap[file.fid] = file.path;
});
return pathMap;
},
async _processFileBatch(
batch,
pathMap,
apiUrl,
data,
processed,
total,
onProgress
) {
const fids = batch.map((item) => item.fid);
try {
const result = await this.post(apiUrl, { fids });
this._validateApiResponse(result);
if (result?.data) {
const filesWithPath = this._processFileData(result.data, pathMap);
data.push(...filesWithPath);
}
if (onProgress) {
onProgress(processed + batch.length, total);
}
} catch (error) {
throw error;
}
},
_validateApiResponse(result) {
if (result?.code === 31001) {
throw new Error("请先登录网盘");
}
if (result?.code !== 0) {
throw new Error(
`获取链接失败,代码:${result.code},消息:${result.message}`
);
}
},
_processFileData(fileData, pathMap) {
return fileData.map((file) => {
const newFile = {
...file,
path: pathMap[file.fid] || file.file_name,
};
let md5 = newFile.md5 || newFile.hash || newFile.etag || "";
md5 = this.decodeMd5(md5);
if (md5) {
newFile.md5 = md5;
}
return newFile;
});
},
async scanQuarkShareFiles(
shareId,
stoken,
cookie,
parentFileId = 0,
path = "",
recursive = true
) {
const fileItems = [];
let page = 1;
while (true) {
const url = this._buildShareDetailUrl(
shareId,
stoken,
parentFileId,
page
);
const result = await this._fetchShareDetail(url, cookie);
if (!this._isValidShareResponse(result)) break;
await this._processShareItems(
result.data.list,
shareId,
stoken,
cookie,
path,
recursive,
fileItems
);
if (this._shouldStopPaging(result.data.list)) break;
page++;
}
return fileItems;
},
_buildShareDetailUrl(shareId, stoken, parentFileId, page) {
return `https://pc-api.uc.cn/1/clouddrive/share/sharepage/detail?pwd_id=${shareId}&stoken=${encodeURIComponent(
stoken
)}&pdir_fid=${parentFileId}&_page=${page}&_size=100&pr=ucpro&fr=pc`;
},
async _fetchShareDetail(url, cookie) {
return await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
Cookie: cookie,
"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",
Referer: "https://pan.quark.cn/",
},
onload: function (response) {
try {
resolve(JSON.parse(response.responseText));
} catch (e) {
reject(new Error("响应解析失败"));
}
},
onerror: () => reject(new Error("网络请求失败")),
});
});
},
_isValidShareResponse(result) {
return result.code === 0 && result.data?.list;
},
_shouldStopPaging(items) {
return items.length < 100;
},
async _processShareItems(
items,
shareId,
stoken,
cookie,
path,
recursive,
fileItems
) {
for (const item of items) {
const itemPath = path ? `${path}/${item.file_name}` : item.file_name;
if (item.dir) {
if (recursive) {
const subFiles = await this.scanQuarkShareFiles(
shareId,
stoken,
cookie,
item.fid,
itemPath,
true
);
fileItems.push(...subFiles);
}
} else {
fileItems.push({
fid: item.fid,
token: item.share_fid_token,
name: item.file_name,
size: item.size,
path: itemPath,
});
}
}
},
async batchGetShareFilesMd5(
shareId,
stoken,
cookie,
fileItems,
onProgress
) {
const md5Map = {};
const batchSize = 10;
const MAX_PARALLEL = 3;
let totalProcessed = 0;
// 分批处理,每批并行发送多个请求
const batches = [];
for (let i = 0; i < fileItems.length; i += batchSize) {
batches.push(fileItems.slice(i, i + batchSize));
}
// 并行处理批次
for (let i = 0; i < batches.length; i += MAX_PARALLEL) {
const parallelBatches = batches.slice(i, i + MAX_PARALLEL);
const batchPromises = parallelBatches.map((batch) =>
this._processMd5Batch(batch, shareId, stoken, cookie, md5Map)
);
await Promise.all(batchPromises);
totalProcessed += parallelBatches.reduce(
(sum, batch) => sum + batch.length,
0
);
if (onProgress) {
onProgress(totalProcessed, fileItems.length);
}
// 批次之间添加较小的延迟,避免请求过于密集
if (i + MAX_PARALLEL < batches.length) {
await this.sleep(500);
}
}
return md5Map;
},
async _processMd5Batch(batch, shareId, stoken, cookie, md5Map) {
const fids = batch.map((item) => item.fid);
const tokens = batch.map((item) => item.token);
try {
const requestBody = this._buildMd5RequestBody(
fids,
tokens,
shareId,
stoken
);
const md5Result = await this._fetchMd5Data(requestBody, cookie);
this._processMd5Result(md5Result, fids, md5Map);
} catch (e) {
fids.forEach((fid) => (md5Map[fid] = ""));
}
},
_buildMd5RequestBody(fids, tokens, shareId, stoken) {
return {
fids,
pwd_id: shareId,
stoken,
fids_token: tokens,
};
},
async _fetchMd5Data(requestBody, cookie) {
const url = `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()}`;
return await new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: {
"Content-Type": "application/json",
Cookie: cookie,
"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",
Referer: "https://pan.quark.cn/",
Accept: "application/json, text/plain, */*",
Origin: "https://pan.quark.cn",
},
data: JSON.stringify(requestBody),
onload: function (response) {
try {
const parsed = JSON.parse(response.responseText);
resolve(parsed);
} catch (e) {
resolve({ code: -1, message: "解析失败" });
}
},
onerror: (error) => {
resolve({ code: -1, message: "网络错误" });
},
});
});
},
_processMd5Result(md5Result, fids, md5Map) {
if (md5Result.code === 0 && md5Result.data) {
const dataList = Array.isArray(md5Result.data)
? md5Result.data
: [md5Result.data];
dataList.forEach((item, idx) => {
const fid = fids[idx];
if (!fid) return;
let md5 = item.md5 || item.hash || "";
md5 = utils.decodeMd5(md5);
md5Map[fid] = md5;
});
} else {
fids.forEach((fid) => (md5Map[fid] = ""));
}
},
generateRapidTransferJson(filesData) {
const files = filesData.map((file) => ({
path: file.path || file.file_name,
etag: (file.etag || file.md5 || "").toLowerCase(),
size: file.size,
}));
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
return {
scriptVersion: "3.0.3",
exportVersion: "1.0",
usesBase62EtagsInExport: false,
commonPath: "",
files: files,
totalFilesCount: files.length,
totalSize: totalSize,
};
},
showLoadingDialog(title = "正在转存文件") {
// 初始化样式
this.initStyles();
// 检查缓存中是否已有对话框
if (this._dialogCache.loading) {
// 更新标题
const titleEl = this._dialogCache.loading.querySelector(
".fastlink-dialog-title"
);
if (titleEl) {
titleEl.textContent = title;
}
// 更新进度为0
this.updateProgress(0, 1);
// 清空日志
const logEl = this._dialogCache.loading.querySelector(
"#fastlink-loading-log"
);
if (logEl) {
logEl.innerHTML = "";
}
// 显示对话框
this._dialogCache.loading.style.display = "flex";
return this._dialogCache.loading;
}
const dialog = document.createElement("div");
dialog.id = "fastlink-loading-dialog";
dialog.className = "fastlink-dialog-overlay";
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
dialog.querySelector("#fastlink-loading-close-btn").onclick = () => {
this.closeLoadingDialog();
};
dialog.querySelector("#fastlink-loading-open-123pan-btn").onclick = () => {
window.open(`https://www.123pan.com/?homeFilePath=${utils._dialogCache.currentId}`, "_blank");
};
// 缓存对话框
this._dialogCache.loading = dialog;
return dialog;
},
updateProgress(processed, total, phase = "转存") {
const titleEl = document.querySelector(
"#fastlink-loading-dialog .fastlink-dialog-title"
);
const progressBar = document.getElementById(
"fastlink-loading-progress-bar"
);
const progressText = document.getElementById(
"fastlink-loading-progress-text"
);
// 更新标题
if (titleEl) {
if (phase.includes("转存") || phase.includes("保存")) {
titleEl.textContent = "正在转存文件";
} else if (phase.includes("完成")) {
titleEl.textContent = "文件转存完成";
}
}
// 更新进度条和进度文本
if (progressBar && progressText) {
const percent = total > 0 ? ((processed / total) * 100).toFixed(1) : 0;
progressBar.style.width = `${percent}%`;
progressText.style.display = "block";
progressText.textContent = `正在转存 ${processed} / ${total} (${percent}%)`;
}
},
addSaveLog(fileName, status, error = "") {
const logContainer = document.getElementById(
"fastlink-loading-log-container"
);
const logEl = document.getElementById("fastlink-loading-log");
if (logEl) {
// 显示日志容器
if (logContainer) {
logContainer.style.display = "block";
}
const logItem = document.createElement("div");
logItem.className = "fastlink-log-item";
const timestamp = new Date().toLocaleTimeString();
let statusText = status === "success" ? "✅ 成功" : "❌ 失败";
let statusClass = status === "success" ? "success" : "error";
if (error) {
statusText += `: ${error}`;
}
logItem.innerHTML = `
[${timestamp}]
${fileName}
${statusText}
`;
logEl.appendChild(logItem);
logEl.scrollTop = logEl.scrollHeight;
}
},
closeLoadingDialog() {
if (this._dialogCache.loading) {
// 隐藏对话框而不是删除,以便后续重用
this._dialogCache.loading.style.display = "none";
}
},
showError(message, showCookieButton = false) {
// 初始化样式
this.initStyles();
// 检查缓存中是否已有对话框
if (this._dialogCache.error) {
// 更新消息
const messageEl = this._dialogCache.error.querySelector(
".fastlink-error-message"
);
if (messageEl) {
messageEl.textContent = message;
}
// 更新按钮状态
const cookieBtn = this._dialogCache.error.querySelector(
"#fastlink-error-cookie-btn"
);
if (cookieBtn) {
cookieBtn.style.display = showCookieButton ? "block" : "none";
}
// 显示对话框
this._dialogCache.error.style.display = "flex";
return this._dialogCache.error;
}
const dialog = document.createElement("div");
dialog.id = "fastlink-error-dialog";
dialog.className = "fastlink-dialog-overlay";
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
if (showCookieButton) {
dialog.querySelector("#fastlink-error-cookie-btn").onclick = () => {
dialog.style.display = "none";
this.showCookieInputDialog(null, this.getCachedCookie());
};
}
dialog.querySelector("#fastlink-error-close-btn").onclick = () => {
dialog.style.display = "none";
};
// 缓存对话框
this._dialogCache.error = dialog;
return dialog;
},
showFolderSelectDialog(apiClient, onSelect) {
// 初始化样式
this.initStyles();
const dialog = document.createElement("div");
dialog.id = "fastlink-folder-select-dialog";
dialog.className = "fastlink-dialog-overlay";
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
let currentFolderId = "0";
let folderTree = [];
// 加载文件夹结构
async function loadFolders(folderId = "0", showLoading = true) {
if (showLoading) {
document.getElementById("fastlink-folder-loading").style.display = "block";
document.getElementById("fastlink-folder-content").style.display = "none";
}
try {
const folders = await apiClient.getFolderList(folderId);
folderTree = folders;
renderFolders(folders);
} catch (error) {
console.error("[123Link] [PanApiClient]", "加载文件夹失败:", error);
utils.showError("加载文件夹失败");
} finally {
document.getElementById("fastlink-folder-loading").style.display = "none";
document.getElementById("fastlink-folder-content").style.display = "block";
}
}
// 渲染文件夹列表
function renderFolders(folders) {
const contentEl = document.getElementById("fastlink-folder-content");
contentEl.innerHTML = "";
if (folders.length === 0) {
contentEl.innerHTML = '当前目录无文件夹
';
return;
}
folders.forEach(folder => {
const folderEl = document.createElement("div");
folderEl.className = "fastlink-folder-item";
folderEl.style = `
padding: 8px 12px;
border-radius: var(--fastlink-radius-small);
cursor: pointer;
transition: var(--fastlink-transition);
display: flex;
align-items: center;
gap: 8px;
`;
folderEl.dataset.fid = folder.fileId;
folderEl.dataset.name = folder.fileName;
folderEl.innerHTML = `
${folder.fileName}
`;
folderEl.onclick = () => {
currentFolderId = folder.fileId;
updateBreadcrumb(folder.fileId, folder.fileName);
loadFolders(folder.fileId);
};
folderEl.onmouseenter = () => {
folderEl.style.background = "var(--fastlink-bg-light)";
};
folderEl.onmouseleave = () => {
folderEl.style.background = "transparent";
};
contentEl.appendChild(folderEl);
});
}
// 更新面包屑导航
const breadcrumbs = [{ fid: "0", name: "123云盘" }];
function updateBreadcrumb(folderId, folderName) {
utils._dialogCache.currentId = folderId;
// 判断当前是否包含folderId
const folderIndex = breadcrumbs.findIndex(item => item.fid === folderId);
if (folderIndex !== -1) {
breadcrumbs.splice(folderIndex + 1);
} else {
breadcrumbs.push({ fid: folderId, name: folderName });
}
// 清空并重新构建面包屑
const breadcrumbEl = document.getElementById("fastlink-folder-breadcrumb");
breadcrumbEl.innerHTML = "";
breadcrumbs.forEach((item, index) => {
const isLastItem = index === breadcrumbs.length - 1;
const breadcrumbItem = document.createElement("span");
breadcrumbItem.className = "fastlink-breadcrumb-item";
breadcrumbItem.dataset.fid = item.fid;
breadcrumbItem.textContent = item.name;
breadcrumbItem.style = `
transition: var(--fastlink-transition);
border-radius: var(--fastlink-radius-small);
cursor: ${isLastItem ? "default" : "pointer"};
max-width: ${isLastItem ? "100px" : "60px"};
color: ${isLastItem ? "var(--fastlink-text)" : "var(--fastlink-primary-color)"};
`;
if (!isLastItem) {
breadcrumbItem.onclick = () => {
loadFolders(item.fid);
updateBreadcrumb(item.fid, item.name);
};
}
breadcrumbEl.appendChild(breadcrumbItem);
if (index < breadcrumbs.length - 1) {
const separator = document.createElement("span");
separator.textContent = "/";
separator.style.color = "var(--fastlink-primary-color)";
breadcrumbEl.appendChild(separator);
}
});
}
// 初始化加载根目录
loadFolders();
// 新建文件夹按钮
document.getElementById("fastlink-create-folder-btn").onclick = async () => {
const folderName = document.getElementById("fastlink-new-folder-name").value.trim();
if (!folderName) {
alert("请输入文件夹名称");
return;
}
try {
const result = await apiClient.mkdir(currentFolderId, folderName);
if (result.success) {
document.getElementById("fastlink-new-folder-name").value = "";
loadFolders(currentFolderId);
GM_notification({ text: "文件夹创建成功", timeout: 2000 });
} else {
throw new Error("创建文件夹失败");
}
} catch (error) {
console.error("创建文件夹失败:", error);
utils.showError("创建文件夹失败");
}
};
// 取消按钮
document.getElementById("fastlink-folder-cancel-btn").onclick = () => {
dialog.remove();
};
// 选择按钮
document.getElementById("fastlink-folder-select-btn").onclick = () => {
if (onSelect) {
onSelect(currentFolderId);
}
dialog.remove();
};
// 搜索功能
const searchInput = document.getElementById("fastlink-folder-search");
searchInput.oninput = () => {
const searchTerm = searchInput.value.toLowerCase();
const folderItems = document.querySelectorAll(".fastlink-folder-item");
folderItems.forEach(item => {
const folderName = item.dataset.name.toLowerCase();
if (folderName.includes(searchTerm)) {
item.style.display = "flex";
} else {
item.style.display = "none";
}
});
};
return dialog;
},
decodeMd5(md5) {
if (!md5 || !md5.includes("==")) {
return md5 || "";
}
try {
const binaryString = atob(md5);
if (binaryString.length === 16) {
return Array.from(binaryString, (char) =>
char.charCodeAt(0).toString(16).padStart(2, "0")
).join("");
}
return "";
} catch (e) {
return "";
}
},
// 123云盘相关功能
pan123: {
// 123云盘API客户端
apiClient: null,
// 初始化API客户端
initApiClient() {
const auth = utils.get123PanAuth();
this.apiClient = {
host: "https://www.123pan.com",
authToken: auth.authToken,
loginUuid: auth.loginUuid,
appVersion: "3",
referer: document.location.href,
buildURL(path, queryParams) {
const queryString = new URLSearchParams(
queryParams || {}
).toString();
return `${this.host}${path}?${queryString}`;
},
sendRequest(method, path, queryParams, body) {
return new Promise((resolve, reject) => {
const headers = {
"Content-Type": "application/json;charset=UTF-8",
Authorization: "Bearer " + this.authToken,
platform: "web",
"App-Version": this.appVersion,
LoginUuid: this.loginUuid,
Origin: this.host,
Referer: this.referer,
};
try {
GM_xmlhttpRequest({
method: method,
url: this.buildURL(path, queryParams),
headers: headers,
data: body,
withCredentials: true,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.code === 401) {
utils.show123PanAuthDialog(async (newAuth) => {
setTimeout(() => generateAndSaveTo123Pan(), 100);
})
return;
}
if (data.code !== 0) {
reject(new Error(data.message));
return;
}
resolve(data);
} catch (e) {
reject(new Error("解析响应失败: " + e.message));
}
},
onerror: (error) => {
console.error(
"[123Link] [PanApiClient]",
"API请求失败:",
error
);
reject(new Error("网络请求失败: " + error.message));
},
ontimeout: () => {
reject(new Error("请求超时"));
},
});
} catch (e) {
console.error("[123Link] [PanApiClient]", "API请求失败:", e);
reject(e);
}
});
},
async getParentFileId() {
return "0";
},
async getFolderList(parentFileId = "0", page = 1, pageSize = 50) {
try {
const response = await this.sendRequest(
"GET",
"/b/api/file/list/new",
{
driveId: "0",
parentFileId: parentFileId.toString(),
Page: page.toString(),
pageSize: pageSize.toString(),
limit: pageSize.toString(),
next: "0",
orderBy: "file_name",
orderDirection: "asc",
trashed: "false",
SearchData: "",
OnlyLookAbnormalFile: "0",
event: "homeListFile",
operateType: "1",
inDirectSpace: "false"
}
);
if (response.code === 0 && response.data) {
const folders = response.data.InfoList || [];
return folders.filter(item => item.Type === 1).map(item => ({
fileId: item.FileId,
fileName: item.FileName,
type: item.Type,
parentFileId: item.ParentFileId || parentFileId
}));
}
return [];
} catch (error) {
console.error("[123Link] [PanApiClient]", "获取文件夹列表失败:", error);
return [];
}
},
async getFolderTree(parentFileId = "0", depth = 2) {
const folderTree = [];
const visited = new Set();
async function traverse(currentId, currentDepth) {
if (currentDepth > depth || visited.has(currentId)) {
return;
}
visited.add(currentId);
const folders = await this.getFolderList(currentId);
for (const folder of folders) {
const folderNode = {
...folder,
children: []
};
folderTree.push(folderNode);
if (currentDepth < depth) {
folderNode.children = await traverse.call(this, folder.fileId, currentDepth + 1);
}
}
return folderTree;
}
return await traverse.call(this, parentFileId, 1);
},
async uploadRequest(fileInfo) {
try {
const response = await this.sendRequest(
"POST",
"/b/api/file/upload_request",
{},
JSON.stringify({
...fileInfo,
RequestSource: null,
})
);
const reuse = response["data"]["Reuse"];
if (response["code"] !== 0) {
return [false, response["message"]];
}
if (!reuse) {
console.error(
"[123Link] [PanApiClient]",
"保存文件失败:",
fileInfo.fileName,
"response:",
response
);
return [false, null];
} else {
return [true, null];
}
} catch (error) {
console.error("[123Link] [PanApiClient]", "上传请求失败:", error);
return [false, "请求失败"];
}
},
async getFile(fileInfo, parentFileId) {
if (!parentFileId) {
parentFileId = await this.getParentFileId();
}
return await this.uploadRequest({
driveId: 0,
etag: fileInfo.etag,
fileName: fileInfo.fileName,
parentFileId,
size: fileInfo.size,
type: 0,
duplicate: 1,
});
},
async mkdir(parentFileId, folderName = "New Folder") {
let folderFileId = null;
try {
const response = await this.sendRequest(
"POST",
"/b/api/file/upload_request",
{},
JSON.stringify({
driveId: 0,
etag: "",
fileName: folderName,
parentFileId,
size: 0,
type: 1,
duplicate: 1,
NotReuse: true,
event: "newCreateFolder",
operateType: 1,
RequestSource: null,
})
);
folderFileId = response["data"]["Info"]["FileId"];
} catch (error) {
console.error(
"[123Link] [PanApiClient]",
"创建文件夹失败:",
error
);
return {
folderFileId: null,
folderName: folderName,
success: false,
};
}
return {
folderFileId: folderFileId,
folderName: folderName,
success: true,
};
},
};
return this.apiClient;
},
// 保存JSON格式的秒链到123云盘
async saveJsonShareLink(jsonData, onProgress, targetFolderId = "0") {
try {
// 初始化API客户端
const apiClient = this.initApiClient();
// 检查是否登录
if (!apiClient.authToken) {
throw new Error("请先登录123云盘");
}
// 解析JSON数据
const shareFileList = this._parseJsonShareLink(jsonData);
// 创建文件夹结构
const filesWithParentId = await this._makeDirForFiles(
shareFileList,
apiClient,
onProgress,
targetFolderId
);
// 保存文件
const saveResult = await this._saveFileList(
filesWithParentId,
apiClient,
onProgress
);
return saveResult;
} catch (error) {
console.error("[123Link] [pan123]", "保存失败:", error);
throw error;
}
},
// 解析JSON格式的秒链
_parseJsonShareLink(jsonData) {
const commonPath = jsonData["commonPath"] || "";
const shareFileList = jsonData["files"];
if (jsonData["usesBase62EtagsInExport"]) {
shareFileList.forEach((file) => {
file.etag = this._base62ToHex(file.etag);
});
}
shareFileList.forEach((file) => {
file.fileName = file.path.split("/").pop();
});
return {
files: shareFileList,
commonPath: commonPath,
};
},
// 创建文件夹结构
async _makeDirForFiles(shareData, apiClient, onProgress, targetFolderId = "0") {
const { files, commonPath } = shareData;
const total = files.length;
const folder = {};
// 使用指定的目标文件夹ID或默认根文件夹ID
const rootFolderId = targetFolderId;
// 创建commonPath对应的文件夹结构
if (commonPath) {
const commonPathParts = commonPath
.split("/")
.filter((part) => part !== "");
let currentParentId = rootFolderId;
for (let i = 0; i < commonPathParts.length; i++) {
const currentPath = commonPathParts.slice(0, i + 1).join("/");
const folderName = commonPathParts[i];
if (!folder[currentPath]) {
const newFolder = await apiClient.mkdir(
currentParentId,
folderName
);
await new Promise((resolve) => setTimeout(resolve, 100));
folder[currentPath] = newFolder.folderFileId;
}
currentParentId = folder[currentPath];
}
} else {
folder[""] = rootFolderId;
}
// 为每个文件创建对应的文件夹结构并添加parentFolderId
for (let i = 0; i < files.length; i++) {
const item = files[i];
const itemPath = item.path.split("/").slice(0, -1);
// 从commonPath或根文件夹开始
let nowParentFolderId =
folder[commonPath.slice(0, -1)] || rootFolderId;
// 创建文件路径对应的文件夹结构
for (let j = 0; j < itemPath.length; j++) {
const path = itemPath.slice(0, j + 1).join("/");
if (!folder[path]) {
const newFolderID = await apiClient.mkdir(
nowParentFolderId,
itemPath[j]
);
await new Promise((resolve) => setTimeout(resolve, 100));
folder[path] = newFolderID.folderFileId;
nowParentFolderId = newFolderID.folderFileId;
} else {
nowParentFolderId = folder[path];
}
}
// 添加parentFolderId到文件信息
files[i].parentFolderId = nowParentFolderId;
// 调用进度回调
if (onProgress) {
onProgress(i + 1, total, item.fileName, 0, 0);
}
}
return files;
},
// 保存文件列表
async _saveFileList(shareFileList, apiClient, onProgress) {
const total = shareFileList.length;
const successList = [];
const failedList = [];
for (let i = 0; i < shareFileList.length; i++) {
const fileInfo = shareFileList[i];
if (i > 0) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
await this._saveSingleFile(
fileInfo,
apiClient,
successList,
failedList,
i,
total,
onProgress
);
}
utils.updateProgress(1, 1, "完成");
return {
success: successList,
failed: failedList,
};
},
async _saveSingleFile(
fileInfo,
apiClient,
successList,
failedList,
index,
total,
onProgress
) {
try {
const reuse = await apiClient.getFile(
{
etag: fileInfo.etag,
size: fileInfo.size,
fileName: fileInfo.fileName,
},
fileInfo.parentFolderId
);
if (reuse[0]) {
this._handleSaveSuccess(fileInfo, successList);
} else {
this._handleSaveFailure(fileInfo, failedList, reuse[1]);
}
// 调用进度回调
if (onProgress) {
const completed = index + 1;
const success = successList.length;
const failed = failedList.length;
utils.updateProgress(completed, total);
onProgress(completed, total, fileInfo.fileName, success, failed);
}
} catch (error) {
this._handleSaveFailure(fileInfo, failedList, error.message);
console.error(
"[123Link] [pan123]",
"保存文件异常:",
fileInfo.fileName,
error
);
}
},
_handleSaveSuccess(fileInfo, successList) {
successList.push(fileInfo);
utils.addSaveLog(fileInfo.fileName, "success");
},
_handleSaveFailure(fileInfo, failedList, error) {
fileInfo.error = error;
failedList.push(fileInfo);
utils.addSaveLog(fileInfo.fileName, "failed", error);
},
// Base62转换相关方法
_base62chars() {
return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
},
_base62ToHex(base62) {
if (!base62) return "";
const chars = this._base62chars();
let num = 0n;
for (let i = 0; i < base62.length; i++) {
num = num * 62n + BigInt(chars.indexOf(base62[i]));
}
let hex = num.toString(16);
if (hex.length % 2) hex = "0" + hex;
while (hex.length < 32) hex = "0" + hex;
return hex;
},
},
};
const tianyiService = {
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 (e) {
// 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) {
if (
!selectedItems.some(
(item) => item.fileId === (fileData.id || fileData.fileId)
)
) {
const normalizedItem = {
fileId: fileData.id || fileData.fileId,
fileName: fileData.name || fileData.fileName,
isFolder: fileData.isFolder || fileData.fileCata === 2,
md5: fileData.md5,
size: fileData.size,
};
selectedItems.push(normalizedItem);
}
}
}
});
return selectedItems;
},
async getPersonalFolderFiles(folderId, path = "", onProgress = null) {
const files = [];
let pageNum = 1;
const pageSize = 100;
while (true) {
const appKey = "600100422";
const timestamp = Date.now().toString();
const urlParams = {
folderId: folderId,
pageNum: pageNum,
pageSize: pageSize,
orderBy: "lastOpTime",
descending: "true",
};
const signParams = {
...urlParams,
Timestamp: timestamp,
AppKey: appKey,
};
const signature = this.get189Signature(signParams);
const url = `https://cloud.189.cn/api/open/file/listFiles.action?${new URLSearchParams(
urlParams
)}`;
const text = await utils.get(url, {
Accept: "application/json;charset=UTF-8",
"Sign-Type": "1",
Signature: signature,
Timestamp: timestamp,
AppKey: appKey,
});
const data = JSON.parse(text);
if (data.res_code !== 0) break;
const fileList = data.fileListAO?.fileList || [];
const folderList = data.fileListAO?.folderList || [];
if (fileList.length === 0 && folderList.length === 0) break;
for (const file of fileList) {
const filePath = path ? `${path}/${file.name}` : file.name;
files.push({
path: filePath,
etag: (file.md5 || "").toLowerCase(),
size: file.size,
fileId: file.id,
});
if (onProgress) onProgress();
}
for (const folder of folderList) {
const folderPath = path ? `${path}/${folder.name}` : folder.name;
const subFiles = await this.getPersonalFolderFiles(
folder.id,
folderPath,
onProgress
);
files.push(...subFiles);
}
if (fileList.length + folderList.length < pageSize) break;
pageNum++;
}
return files;
},
async getBaseShareInfo(shareUrl, sharePwd) {
let 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 = utils.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 (e) {
/* ignore decoding errors */
}
}
}
let shareId = shareCode;
if (accessCode) {
const checkUrl = `https://cloud.189.cn/api/open/share/checkAccessCode.action?shareCode=${shareCode}&accessCode=${accessCode}`;
try {
const checkText = await utils.get(checkUrl, {
Accept: "application/json;charset=UTF-8",
Referer: "https://cloud.189.cn/web/main/",
});
const checkData = JSON.parse(checkText);
if (checkData.shareId) shareId = checkData.shareId;
} catch (e) {
/* ignore */
}
}
const params = { shareCode, accessCode: accessCode };
const timestamp = Date.now().toString();
const appKey = "600100422";
const signData = { ...params, Timestamp: timestamp, AppKey: appKey };
const signature = this.get189Signature(signData);
const apiUrl = `https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action?${new URLSearchParams(
params
)}`;
const text = await utils.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 = JSON.parse(
text.replace(
/"(id|fileId|parentId|shareId)":"?(\d{15,})"?/g,
'"$1":"$2"'
)
);
} catch (e) {
throw new Error("解析分享信息失败");
}
if (data.res_code !== 0) {
if (data.res_code === 40401 && !accessCode)
throw new Error("该分享需要提取码,请输入提取码");
throw new Error(`获取分享信息失败: ${data.res_message || "未知错误"}`);
}
return {
shareId: data.shareId || shareId,
shareMode: data.shareMode || "0",
accessCode: accessCode,
shareCode: shareCode,
title: data.fileName || "",
};
},
async get189ShareFiles(
shareId,
shareDirFileId,
fileId,
path = "",
shareMode = "0",
accessCode = "",
shareCode = "",
onProgress = null
) {
const files = [];
let page = 1;
while (true) {
const params = {
pageNum: page.toString(),
pageSize: "100",
fileId: fileId.toString(),
shareDirFileId: shareDirFileId.toString(),
isFolder: "true",
shareId: shareId.toString(),
shareMode: shareMode,
iconOption: "5",
orderBy: "lastOpTime",
descending: "true",
accessCode: accessCode || "",
};
const queryString = new URLSearchParams(params).toString();
const url = `https://cloud.189.cn/api/open/share/listShareDir.action?${queryString}`;
const headers = {
Accept: "application/json;charset=UTF-8",
Referer: "https://cloud.189.cn/web/main/",
};
if (shareCode && accessCode) {
headers["Cookie"] = `share_${shareCode}=${accessCode}`;
}
const text = await utils.get(url, headers);
let data;
try {
const fixedText = text.replace(
/"(id|fileId|parentId|shareId)":(\d{15,})/g,
'"$1":"$2"'
);
data = JSON.parse(fixedText);
} catch (e) {
break;
}
if (data.res_code !== 0) {
break;
}
const fileList = data.fileListAO?.fileList || [];
const folderList = data.fileListAO?.folderList || [];
for (const file of fileList) {
const filePath = path ? `${path}/${file.name}` : file.name;
files.push({
path: filePath,
etag: (file.md5 || "").toLowerCase(),
size: file.size,
});
if (onProgress) onProgress();
}
for (const folder of folderList) {
const folderPath = path ? `${path}/${folder.name}` : folder.name;
const subFiles = await this.get189ShareFiles(
shareId,
folder.id,
folder.id,
folderPath,
shareMode,
accessCode,
shareCode,
onProgress
);
files.push(...subFiles);
}
if (fileList.length + folderList.length < 100) {
break;
}
page++;
}
return files;
},
get189Signature(params) {
const sortedKeys = Object.keys(params).sort();
const sortedParams = sortedKeys
.map((key) => `${key}=${params[key]}`)
.join("&");
return this.simpleMD5(sortedParams);
},
simpleMD5(str) {
function rotateLeft(value, shift) {
return (value << shift) | (value >>> (32 - shift));
}
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);
}
function convertToWordArray(str) {
const lWordCount = ((str.length + 8) >>> 6) + 1;
const lMessageLength = lWordCount * 16;
const lWordArray = new Array(lMessageLength - 1);
let lBytePosition = 0;
let lByteCount = 0;
while (lByteCount < str.length) {
const lWordIndex = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordIndex] =
lWordArray[lWordIndex] |
(str.charCodeAt(lByteCount) << lBytePosition);
lByteCount++;
}
const lWordIndex = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordIndex] =
lWordArray[lWordIndex] | (0x80 << lBytePosition);
lWordArray[lMessageLength - 2] = str.length << 3;
lWordArray[lMessageLength - 1] = str.length >>> 29;
return lWordArray;
}
function wordToHex(value) {
let result = "";
for (let i = 0; i <= 3; i++) {
const byte = (value >>> (i * 8)) & 255;
result += ("0" + byte.toString(16)).slice(-2);
}
return result;
}
const x = convertToWordArray(str);
let a = 0x67452301,
b = 0xefcdab89,
c = 0x98badcfe,
d = 0x10325476;
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 (
wordToHex(a) +
wordToHex(b) +
wordToHex(c) +
wordToHex(d)
).toLowerCase();
},
};
async function generateAndSaveTo123Pan() {
try {
// 先检查123云盘认证信息
const auth = utils.get123PanAuth();
if (!auth.authToken || !auth.loginUuid) {
utils.closeLoadingDialog();
if (
confirm(
"未检测到123云盘认证信息,请先设置认证信息后再转存。\n\n点击确定进入设置,点击取消取消转存操作。"
)
) {
utils.show123PanAuthDialog(async (newAuth) => {
setTimeout(() => generateAndSaveTo123Pan(), 100);
});
}
return;
}
// 先检查是否有已选择的文件或文件夹
const hostname = location.hostname;
let hasSelectedFiles = false;
if (hostname.includes("cloud.189.cn")) {
const selectedFiles = tianyiService.getSelectedFiles();
hasSelectedFiles = selectedFiles.length > 0;
} else if (hostname.includes("quark.cn")) {
const selectedItems = utils.getSelectedList();
hasSelectedFiles = selectedItems.length > 0;
}
if (!hasSelectedFiles) {
utils.showError("请选择要转存的文件或文件夹");
return;
}
// 初始化API客户端以获取文件夹结构
const apiClient = utils.pan123.initApiClient();
// 先显示文件夹选择对话框
utils.showFolderSelectDialog(apiClient, async (targetFolderId) => {
try {
const path = location.pathname;
let json = null;
let shareTitle = "";
if (hostname.includes("cloud.189.cn")) {
if (path.startsWith("/web/main")) {
json = await generateTianyiHomeJsonInternal();
} else {
const result = await generateTianyiShareJsonInternal();
json = result.json;
shareTitle = result.title;
}
} else if (hostname.includes("quark.cn")) {
const isSharePage = /^\/(s|share)\//.test(path);
if (isSharePage) {
const match = location.pathname.match(/\/(s|share)\/([a-zA-Z0-9]+)/);
if (!match) {
throw new Error("无法获取分享ID");
}
const shareId = match[2];
let cookie = utils.getCachedCookie();
if (!cookie || cookie.length < 10) {
utils.showCookieInputDialog((newCookie) => {
setTimeout(() => generateAndSaveTo123Pan(), 100);
});
return;
}
const result = await generateShareJsonInternal(shareId, cookie);
json = result.json;
shareTitle = result.title;
} else {
json = await generateHomeJsonInternal();
}
}
if (json) {
// 修改saveTo123PanInternal,使其接受targetFolderId参数
await saveTo123PanInternal(json, shareTitle, targetFolderId);
}
} catch (error) {
utils.closeLoadingDialog();
utils.showError(error.message || "转存到123云盘失败");
}
});
} catch (error) {
utils.closeLoadingDialog();
utils.showError(error.message || "转存到123云盘失败");
}
}
async function saveTo123PanInternal(json, shareTitle = "", targetFolderId = "0") {
try {
const auth = utils.get123PanAuth();
if (!auth.authToken || !auth.loginUuid) {
utils.closeLoadingDialog();
if (
confirm(
"未检测到123云盘认证信息,请先设置认证信息后再转存。\n\n点击确定进入设置,点击取消取消转存操作。"
)
) {
utils.show123PanAuthDialog(async (newAuth) => {
await saveTo123PanInternal(json, shareTitle, targetFolderId);
});
}
return;
}
try {
let percent = 0;
const saveResult = await utils.pan123.saveJsonShareLink(
json,
(completed, total) => {
percent = total > 0 ? Math.round((completed / total) * 100) : 100;
utils.updateProgress(completed, total);
},
targetFolderId
);
const successCount = saveResult.success.length;
const failedCount = saveResult.failed.length;
if (percent === 100) {
const progressText = document.getElementById(
"fastlink-loading-progress-text"
);
progressText.textContent = `✅ 成功: ${successCount}, ❌ 失败: ${failedCount}`;
}
} catch (error) {
console.error("转存到123云盘失败:", error);
utils.showError(`转存失败: ${error.message}`);
}
} catch (error) {
console.error("转存到123云盘失败:", error);
utils.showError(`转存失败: ${error.message}`);
}
}
async function generateTianyiShareJsonInternal() {
utils.showLoadingDialog("正在转存文件", "准备中...");
try {
const selectedFiles = tianyiService.getSelectedFiles();
if (selectedFiles.length === 0) {
utils.closeLoadingDialog();
throw new Error("请选择要转存的文件或文件夹");
}
const shareUrl = window.location.href;
let sharePwd = "";
const allFiles = [];
let filesFound = 0;
const onProgress = () => {
filesFound++;
};
const { shareId, shareMode, accessCode, shareCode, title } =
await tianyiService.getBaseShareInfo(shareUrl, sharePwd);
for (const item of selectedFiles) {
if (item.isFolder) {
const folderPath = item.fileName;
const subFiles = await tianyiService.get189ShareFiles(
shareId,
item.fileId,
item.fileId,
folderPath,
shareMode,
accessCode,
shareCode,
onProgress
);
allFiles.push(...subFiles);
} else {
allFiles.push({
path: item.fileName,
etag: (item.md5 || "").toLowerCase(),
size: item.size,
});
onProgress();
}
}
await utils.sleep(300);
const finalJson = utils.generateRapidTransferJson(allFiles);
return { json: finalJson, title };
} catch (error) {
utils.closeLoadingDialog();
throw error;
}
}
async function generateTianyiHomeJsonInternal() {
utils.showLoadingDialog("正在转存文件", "准备中...");
try {
const selectedFiles = tianyiService.getSelectedFiles();
if (selectedFiles.length === 0) {
utils.closeLoadingDialog();
throw new Error("请先勾选要生成JSON的文件或文件夹");
}
const allFiles = [];
let filesFound = 0;
const onProgress = () => {
filesFound++;
};
for (const item of selectedFiles) {
if (item.isFolder) {
const subFiles = await tianyiService.getPersonalFolderFiles(
item.fileId,
item.fileName,
onProgress
);
allFiles.push(...subFiles);
} else {
allFiles.push({
path: item.fileName,
size: item.size,
fileId: item.fileId,
etag: (item.md5 || "").toLowerCase(),
});
onProgress();
}
}
await utils.sleep(300);
const finalJson = utils.generateRapidTransferJson(allFiles);
return finalJson;
} catch (error) {
utils.closeLoadingDialog();
throw error;
}
}
async function generateHomeJsonInternal() {
utils.showLoadingDialog("正在转存文件", "准备中...");
try {
const selectedItems = utils.getSelectedList();
if (selectedItems.length === 0) {
throw new Error("请选择要转存的文件或文件夹");
}
const currentPath = utils.getCurrentPath();
const allFiles = [];
let totalFilesFound = 0;
for (const item of selectedItems) {
if (item.file) {
const filePath = currentPath
? `${currentPath}/${item.file_name}`
: item.file_name;
allFiles.push({ ...item, path: filePath });
totalFilesFound++;
} else if (item.dir) {
const folderPath = currentPath
? `${currentPath}/${item.file_name}`
: item.file_name;
const folderFiles = await utils.getFolderFiles(
item.fid,
folderPath,
() => {
totalFilesFound++;
}
);
allFiles.push(...folderFiles);
}
}
if (allFiles.length === 0) {
throw new Error("没有找到任何文件");
}
const filesData = await utils.getFilesWithMd5(allFiles);
const json = utils.generateRapidTransferJson(filesData);
return json;
} catch (error) {
utils.closeLoadingDialog();
throw error;
}
}
async function generateShareJsonInternal(shareId, cookie) {
utils.showLoadingDialog("正在转存文件", "准备中...");
try {
const { stoken, title } = await utils.getShareToken(shareId, "", cookie);
const selectedItems = utils.getSelectedList();
if (selectedItems.length === 0) {
throw new Error("请选择要转存的文件或文件夹");
}
const allFileItems = [];
let totalFilesFound = 0;
for (const item of selectedItems) {
if (item.file) {
const parentFid = item.pdir_fid;
const filesInParent = await utils.scanQuarkShareFiles(
shareId,
stoken,
cookie,
parentFid,
"",
false
);
const fileInfo = filesInParent.find((f) => f.fid === item.fid);
if (fileInfo) {
const fileItem = {
fid: item.fid,
token: fileInfo.token,
name: item.file_name,
size: item.size,
path: item.file_name,
};
allFileItems.push(fileItem);
} else {
const fileItem = {
fid: item.fid,
token: item.share_fid_token,
name: item.file_name,
size: item.size,
path: item.file_name,
};
allFileItems.push(fileItem);
}
totalFilesFound++;
} else if (item.dir) {
const folderFiles = await utils.scanQuarkShareFiles(
shareId,
stoken,
cookie,
item.fid,
item.file_name
);
allFileItems.push(...folderFiles);
totalFilesFound += folderFiles.length;
}
}
if (allFileItems.length === 0) {
throw new Error("没有找到任何文件");
}
await utils.sleep(300);
const md5Map = await utils.batchGetShareFilesMd5(
shareId,
stoken,
cookie,
allFileItems
);
const files = allFileItems.map((item) => ({
path: item.path,
etag: (md5Map[item.fid] || "").toLowerCase(),
size: item.size,
}));
const json = {
scriptVersion: "3.0.3",
exportVersion: "1.0",
usesBase62EtagsInExport: false,
commonPath: "",
files,
totalFilesCount: files.length,
totalSize: files.reduce((sum, f) => sum + f.size, 0),
};
return { json, title };
} catch (error) {
utils.closeLoadingDialog();
throw error;
}
}
function addButton() {
const hostname = location.hostname;
let container;
if (document.getElementById("quark-json-generator-btn")) {
return;
}
if (hostname.includes("cloud.189.cn")) {
const isMainPage = location.pathname.startsWith("/web/main");
if (isMainPage) {
container = document.querySelector(
'[class*="FileHead_file-head-left"]'
);
} else {
container = document.querySelector(".file-operate");
}
if (!container) return;
const button = document.createElement("a");
button.id = "quark-json-generator-btn";
button.className = "btn";
button.href = "javascript:;";
button.textContent = "转存到123云盘";
if (isMainPage) {
button.style.cssText =
"width: 100px; height: 30px; padding: 0; border-radius: 4px; line-height: 30px; color: #fff; text-align: center; font-size: 12px; background: #52c41a; border: 1px solid #43a413; position: relative; display: block; margin-right: 12px;";
} else {
button.style.cssText =
"width: 140px; height: 36px; padding: 0; border-radius: 4px; line-height: 36px; color: #fff; text-align: center; font-size: 14px; background: #52c41a; border: 1px solid #43a413; position: relative; display: block;margin-right:20px;";
}
container.insertBefore(button, container.firstChild);
if (!isMainPage) {
const styleId = "quark-json-flex-style";
if (!document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
.outlink-box-b .file-operate {
display: flex !important;
flex-wrap: nowrap !important;
justify-content: flex-end !important;
align-items: center !important;
/* Override conflicting styles */
float: none !important;
text-align: unset !important;
}
.btn-save-as{
margin-left: 0 !important;
}
`;
document.head.appendChild(style);
}
}
button.onclick = generateAndSaveTo123Pan;
} else if (hostname.includes("quark.cn")) {
const path = location.pathname;
const isSharePage = /^\/(s|share)\//.test(path);
if (isSharePage) {
container = document.querySelector(".share-btns");
if (!container) {
const alternatives = [
".ant-layout-content .operate-bar",
".share-detail-header .operate-bar",
".share-header-btns",
".share-operate-btns",
"[class*='share'][class*='btn']",
".ant-btn-group",
];
for (const selector of alternatives) {
container = document.querySelector(selector);
if (container) break;
}
}
} else {
container = document.querySelector(".btn-operate .btn-main");
}
if (!container) return;
const buttonWrapper = document.createElement("div");
buttonWrapper.id = "quark-json-generator-btn";
buttonWrapper.className = "ant-dropdown-trigger pl-button-json";
const isSharePageQuark = /^\/(s|share)\//.test(location.pathname);
if (isSharePageQuark) {
buttonWrapper.style.cssText = "display: inline-block; margin-left: 16px;";
buttonWrapper.innerHTML = `
`;
container.appendChild(buttonWrapper);
} else {
buttonWrapper.style.cssText = "display: inline-block; margin-right: 16px;";
buttonWrapper.innerHTML = `
`;
container.insertBefore(buttonWrapper, container.firstChild);
}
buttonWrapper.querySelector("button").onclick = generateAndSaveTo123Pan;
}
}
// 全局变量
let mutationObserver = null;
function init() {
const SCRIPT_VERSION = GM_info.script.version;
const LAST_VERSION = GM_getValue("last_version", "0");
if (SCRIPT_VERSION > LAST_VERSION) {
GM_setValue("last_version", SCRIPT_VERSION);
}
// 检查123云盘认证信息
const auth = utils.get123PanAuth();
if (!auth.authToken || !auth.loginUuid) {
// 首次使用或认证信息为空,提示用户设置
const hasPrompted = GM_getValue("pan123_auth_prompted", false);
if (!hasPrompted) {
setTimeout(() => {
if (
confirm(
"首次使用转存到123云盘功能,请先设置认证信息。\n\n点击确定进入设置,点击取消稍后在需要时设置。"
)
) {
utils.show123PanAuthDialog();
}
GM_setValue("pan123_auth_prompted", true);
}, 1000);
}
}
const hostname = location.hostname;
if (hostname.includes("quark.cn") || hostname.includes("cloud.189.cn")) {
// 创建MutationObserver并保存引用
mutationObserver = new MutationObserver(() => {
addButton();
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
addButton();
}
}
// 清理函数,在脚本卸载时调用
function cleanup() {
// 断开MutationObserver
if (mutationObserver) {
mutationObserver.disconnect();
mutationObserver = null;
}
// 清理对话框缓存
if (utils._dialogCache) {
Object.values(utils._dialogCache).forEach((dialog) => {
if (dialog && dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
});
utils._dialogCache = {};
}
}
// 监听页面卸载事件,清理资源
window.addEventListener("unload", cleanup);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();