) */
function confirmBigDownloadModal(modal, statusEl, count, totalBytes) {
return new Promise(resolve => {
try {
setStatus(statusEl, `${count} 个文件服务器未提供 md5(多为超过分片阈值的大文件,CDN 无哈希头),可下载计算(共 ${formatSize(totalBytes)},需完整下载全部内容,较慢)。`, 'warn');
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="skip"]').onclick = () => resolve(false);
modal.footerEl.querySelector('[data-act="dl"]').onclick = () => resolve(true);
} catch (e) { resolve(false); }
});
}
/** 字节数组 → md5 hex(原始字节串) */
function baiduBytesToMd5Hex(bytes) {
let bin = '';
const step = 0x8000;
for (let i = 0; i < bytes.length; i += step) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + step));
}
return BAIDU.md5Hex(bin);
}
// ============================== UI(参照 123FastLink 风格)==============================
const STYLES = `
:root{
--gy-primary:#10b981;--gy-primary-hover:#059669;
--gy-danger:#ef4444;--gy-danger-hover:#dc2626;
--gy-warning:#f59e0b;--gy-warning-hover:#d97706;
--gy-info:#3b82f6;--gy-info-hover:#2563eb;
--gy-bg:#ffffff;--gy-surface:#f8fafc;--gy-border:#e2e8f0;
--gy-text:#1e293b;--gy-text2:#64748b;--gy-text3:#94a3b8;
--gy-radius:12px;--gy-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05);
}
.gy-ms-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:2147483646;animation:gy-ms-fade .18s ease-out}
.gy-ms-modal{background:var(--gy-bg);border-radius:var(--gy-radius);box-shadow:var(--gy-shadow);width:90%;max-width:560px;max-height:88vh;overflow:hidden;border:1px solid var(--gy-border);animation:gy-ms-slide .22s cubic-bezier(0.4,0,0.2,1)}
.gy-ms-header{padding:18px 22px 14px;border-bottom:1px solid var(--gy-border);display:flex;align-items:center;justify-content:space-between}
.gy-ms-title{font-size:17px;font-weight:600;color:var(--gy-text);display:flex;align-items:center;gap:8px}
.gy-ms-title svg{width:18px;height:18px;color:var(--gy-primary)}
.gy-ms-close{background:none;border:none;height:30px;width:30px;display:flex;align-items:center;justify-content:center;color:var(--gy-text2);cursor:pointer;border-radius:6px;font-size:18px;line-height:1}
.gy-ms-close:hover{background:var(--gy-surface);color:var(--gy-text)}
.gy-ms-content{padding:18px 22px;overflow:auto;max-height:calc(88vh - 120px)}
.gy-ms-footer{padding:12px 22px 18px;border-top:1px solid var(--gy-border);display:flex;gap:10px;justify-content:flex-end;align-items:center;flex-wrap:wrap}
.gy-ms-btn{padding:8px 18px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:500;transition:all .15s}
.gy-ms-btn-primary{background:var(--gy-primary);color:#fff}
.gy-ms-btn-primary:hover{background:var(--gy-primary-hover)}
.gy-ms-btn-secondary{background:#e6f4ff;color:#1677ff;border:1px solid #91caff}
.gy-ms-btn-outline{background:#fff;border:1px solid var(--gy-border);color:var(--gy-text)}
.gy-ms-btn-outline:hover{background:var(--gy-surface)}
.gy-ms-btn-danger{background:var(--gy-danger);color:#fff}
.gy-ms-btn:disabled{opacity:.6;cursor:not-allowed}
.gy-ms-status{font-size:12px;min-height:18px;white-space:pre-wrap;margin-top:8px}
.gy-ms-status-ok{color:#16a34a}
.gy-ms-status-err{color:#dc2626}
.gy-ms-status-warn{color:#d97706}
.gy-ms-status-info{color:#2563eb}
.gy-ms-textarea{width:100%;box-sizing:border-box;min-height:150px;padding:10px;border:1px solid #d9d9d9;border-radius:8px;font-size:12px;font-family:ui-monospace,monospace;resize:vertical;color:var(--gy-text)}
.gy-ms-pre{background:#f7f7f7;padding:12px;border-radius:8px;font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;margin:0;max-height:280px;overflow:auto}
.gy-ms-toast{position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:2147483647;padding:10px 22px;border-radius:10px;color:#fff;font-size:13px;box-shadow:0 6px 20px rgba(0,0,0,.25);animation:gy-ms-fade .2s ease-out;max-width:80vw;word-break:break-all}
.gy-ms-toast-success{background:#16a34a}
.gy-ms-toast-error{background:#dc2626}
.gy-ms-toast-warning{background:#d97706}
.gy-ms-toast-info{background:#2563eb}
.gy-ms-file-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0 8px;font-size:12px;color:var(--gy-text2)}
.gy-ms-bar{height:6px;border-radius:3px;background:var(--gy-surface);overflow:hidden;margin-top:10px}
.gy-ms-bar-fill{height:100%;background:var(--gy-primary);border-radius:3px;transition:width .2s}
@keyframes gy-ms-fade{from{opacity:0}to{opacity:1}}
@keyframes gy-ms-slide{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
`;
const ICONS = {
bolt: ``,
generate: ``,
import: ``,
settings: ``,
};
// ---- 模态框基础 ----
function openModal({ title, icon, content, footer, width }) {
closeAllModals();
const overlay = document.createElement('div');
overlay.className = 'gy-ms-overlay';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
const contentEl = overlay.querySelector('.gy-ms-content');
const footerEl = overlay.querySelector('.gy-ms-footer');
if (typeof content === 'function') content(contentEl, overlay);
else if (content) contentEl.innerHTML = content;
if (typeof footer === 'function') footer(footerEl, overlay);
else if (footer) footerEl.innerHTML = footer;
overlay.querySelector('.gy-ms-close').onclick = () => overlay.remove();
overlay.addEventListener('mousedown', e => { if (e.target === overlay) overlay.remove(); });
return { overlay, contentEl, footerEl };
}
function closeAllModals() {
document.querySelectorAll('.gy-ms-overlay').forEach(o => o.remove());
}
function showToast(message, type = 'success', duration = 2600) {
const t = document.createElement('div');
t.className = `gy-ms-toast gy-ms-toast-${type}`;
t.textContent = message;
document.body.appendChild(t);
setTimeout(() => t.remove(), duration);
}
function setStatus(el, text, type = 'info') {
if (!el) return;
el.textContent = text;
el.className = `gy-ms-status gy-ms-status-${type}`;
}
function updateProgress(el, percent, text) {
if (!el) return;
const fill = el.querySelector('.gy-ms-bar-fill');
if (fill) fill.style.width = `${Math.max(0, Math.min(100, percent))}%`;
const label = el.querySelector('.gy-ms-bar-label');
if (label) label.textContent = text || '';
}
// ---- 平台适配:获取勾选项与目录遍历 ----
function getPlatformApi() {
return isGuangya() ? new GuangyaApi() : null;
}
function getCheckedItems() {
if (isGuangya()) return guangyaGetCheckedItems();
if (isXunlei()) return xunleiGetCheckedItems();
if (isBaidu()) {
const s = BAIDU.getCheckedItems();
return { all: s.headState === 'checked', items: s.ids, headState: s.headState };
}
if (isAliyun()) return ALIYUN.getCheckedItems();
if (isQuark()) return QUARK.getCheckedItems();
if (isUc()) return UC.getCheckedItems();
if (isWeiyun()) {
// 微云为异步(DOM checked → API 匹配),同步包装为 Promise 由调用方 await
return WEIYUN.getCheckedItems();
}
if (isFeejii()) return FEEJII.getCheckedItems();
if (isCtfile()) {
const s = ctfileGetCheckedItems();
return { all: s.all, items: s.names.map(name => ({ name })) };
}
if (isY139()) return y139GetCheckedItems();
if (isP123()) return p123GetCheckedItems();
return { all: false, items: [] };
}
function getLastParentId() {
if (isGuangya()) return guangyaGetLastParentId();
if (isXunlei()) return xunleiGetLastParentId();
if (isBaidu()) return BAIDU.getCurrentDir();
if (isAliyun()) return ALIYUN.getCurrentParentId();
if (isQuark()) return QUARK.getCurrentParentId();
if (isUc()) return UC.getCurrentParentId();
if (isWeiyun()) return WEIYUN.getLastParentId();
if (isFeejii()) return FEEJII.getCurrentParentId();
if (isCtfile()) return CTFILE.getCurrentFolderId();
if (isY139()) return y139GetCurrentFolderId();
if (isP123()) return p123GetCurrentParentId();
return '';
}
/** 统一:获取文件详情(md5 + gcid + size);迅雷条目哈希缺失时调详情接口补齐 */
async function getFileDetailPlatform(api, item) {
if (isGuangya()) return api.getFileDetail(item.fileId);
if (isAliyun()) {
// 阿里云盘列表条目已含 content_hash(SHA1) + size,无需额外接口
const sha1 = String(item.contentHash || item.content_hash || '').toUpperCase();
return { md5: '', gcid: '', sha1: /^[0-9A-F]{40}$/.test(sha1) ? sha1 : '', fileSize: Number(item.size || 0) };
}
if (isXunlei()) {
const md5 = item.md5_checksum ? String(item.md5_checksum).toLowerCase() : '';
const gcid = item.hash ? normalizeGcid(item.hash) : '';
const size = Number(item.size || 0);
// drive.all 条目通常不带 md5/hash(字段为空),需要调详情接口补齐
if (!md5 && !gcid) {
try {
const d = await xunleiGetFileDetail(item.id || item.fileId);
return {
md5: d.md5 || '',
gcid: d.gcid || '',
fileSize: d.fileSize || size,
};
} catch (e) {
// 详情失败时返回原始字段(可能只有 size)
return { md5, gcid, fileSize: size };
}
}
return { md5, gcid, fileSize: size };
}
if (isQuark()) {
// 夸克列表条目无 hash,需批量调 download 接口(由调用方按批取,此处单条兜底)
const md5 = String(item.md5 || item.md5_checksum || '').toLowerCase();
const sha1 = String(item.sha1 || item.content_hash || '').toLowerCase();
if (!md5 && !sha1 && item.fid) {
try {
const map = await QUARK.getFileHashes([item.fid]);
const h = map[String(item.fid)] || {};
return { md5: h.md5 || '', sha1: h.sha1 || '', gcid: '', fileSize: Number(h.size || item.size || 0) };
} catch (e) {
return { md5, sha1, gcid: '', fileSize: Number(item.size || 0) };
}
}
return { md5, sha1, gcid: '', fileSize: Number(item.size || 0) };
}
if (isWeiyun()) {
// 微云列表条目已含 file_sha(完整SHA1) + size;file_md5 恒为全 0(微云不存 MD5)
const sha1 = String(item.sha1 || item.file_sha || '').toLowerCase();
return { md5: '', sha1: /^[0-9a-f]{40}$/.test(sha1) ? sha1 : '', gcid: '', fileSize: Number(item.size || 0) };
}
if (isCtfile()) {
// 城通列表条目 file_real_name="{md5(前1MB)}-{size}";≤1MB 即完整内容 MD5,>1MB 仅前1MB 哈希
const meta = CTFILE.parseItemMeta(item);
const size = meta.size || Number(item.size || 0);
return { md5: size <= CTFILE.CHECKSUM_SLICE ? meta.checksum : '', ctChecksum: meta.checksum, gcid: '', fileSize: size };
}
return { md5: '', gcid: '', fileSize: 0 };
}
/** 统一:判断条目是否为目录 */
function isDirPlatform(item) {
if (isGuangya()) return Number(item.resType) === 2; // 光鸭目录标记 resType===2
if (isXunlei()) return String(item.kind) === 'drive#folder'; // 迅雷目录 kind===drive#folder
if (isAliyun()) return String(item.type) === 'folder' || String(item.type) === 'backupfolder'; // 阿里目录 type===folder
if (isQuark()) return item.isDir === true || item.dir === true; // 夸克目录 dir===true
if (isUc()) return item.isDir === true || item.dir === true;
if (isWeiyun()) return String(item.kind) === 'dir'; // 微云目录 kind==='dir'
if (isFeejii()) return Number(item.fileType) === 2; // 小飞机目录 fileType===2
if (isCtfile()) return String(item.ext) === 'folder'; // 城通目录 ext==='folder'
return false;
}
/** 统一:获取目录子项 */
async function getListPlatform(api, folderId, page) {
if (isGuangya()) return { list: await api.getFileList(folderId, page), next: '' };
if (isAliyun()) {
const driveId = await ALIYUN.getDriveId();
const r = await ALIYUN.getFileList(driveId, folderId, page || '');
return { list: r.list, next: r.nextMarker };
}
if (isQuark()) {
const r = await QUARK.getFileList(folderId, page || 1);
// 规范化条目:与统一 item 形态对齐
const list = (r.list || []).map(it => ({
fileId: String(it.fid || ''),
fid: String(it.fid || ''),
name: String(it.file_name || ''),
fileName: String(it.file_name || ''),
isDir: it.dir === true,
type: it.dir === true ? 'folder' : 'file',
size: Number(it.size || 0),
md5: String(it.md5 || ''),
sha1: String(it.sha1 || ''),
}));
return { list, next: r.hasMore ? (Number(page || 1) + 1) : '' };
}
if (isXunlei()) {
const r = await xunleiGetFileList(folderId, page || '');
return { list: r.list, next: r.nextPageToken };
}
if (isWeiyun()) {
const { dirs, files, finishFlag } = await WEIYUN.getFileList(folderId, 0, 500);
const list = [...dirs, ...files];
return { list, next: finishFlag ? '' : (Number(page || 0) + 500) };
}
if (isFeejii()) {
const r = await FEEJII.getFileList(folderId, page || 1);
const list = (r.list || []).map(it => ({
fileId: String(it.fileId || ''),
fileName: String(it.fileName || ''),
name: String(it.fileName || ''),
isDir: Number(it.fileType) === 2,
type: Number(it.fileType) === 2 ? 'folder' : 'file',
fileType: Number(it.fileType),
size: Number(it.fileSize || 0),
folderId: String(it.folderId || ''),
}));
return { list, next: r.hasMore ? (Number(page || 1) + 1) : '' };
}
if (isCtfile()) {
const wsId = await CTFILE.getWorkspaceId();
const r = await CTFILE.getFileList(wsId, folderId, page || 0);
const list = (r.list || []).map(it => ({
fileId: String(it.fid || ''),
fid: String(it.fid || ''),
name: String(it.name || ''),
fileName: String(it.name || ''),
isDir: String(it.ext) === 'folder',
type: String(it.ext) === 'folder' ? 'folder' : 'file',
ext: String(it.ext || ''),
size: Number(it.size || 0),
file_real_name: String(it.file_real_name || ''),
}));
return { list, next: '' };
}
return { list: [], next: '' };
}
// ---- 生成秒传 JSON(导出,双哈希:MD5 + GCID)----
async function generateJson() {
const { items, all } = getCheckedItems();
if (!items.length) {
showToast('请先在页面勾选要导出的文件或文件夹', 'warning');
return;
}
if (isXunlei() && !(await xunleiGetCredentials()).accessToken) {
showToast('未检测到迅雷云盘登录态,请先刷新页面', 'warning');
return;
}
const api = getPlatformApi();
const modal = openModal({
title: '生成秒传 JSON',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() {
return '';
},
});
const statusEl = modal.contentEl.querySelector('#gy-ms-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
let done = 0;
let total = items.length;
const collectItem = async (item, prefix) => {
try {
const detail = await getFileDetailPlatform(api, item);
const md5 = detail.md5;
const gcid = detail.gcid;
if (md5 || gcid) {
files.push({
path: prefix + (item.fileName || item.name || ''),
etag: md5 || '',
gcid: gcid || '',
size: detail.fileSize || Number(item.fileSize || item.size || 0),
});
} else {
warnMissing.push(prefix + (item.fileName || item.name || ''));
}
} catch (e) {
warnMissing.push(`${prefix}${item.fileName || item.name || ''}(${e.message})`);
}
};
const walkFolder = async (folderId, prefix) => {
let page = '';
for (;;) {
let list, next;
try {
const r = await getListPlatform(api, folderId, page);
list = r.list;
next = r.next;
} catch (e) {
setStatus(statusEl, `获取目录失败:${prefix} ${e.message}`, 'err');
return;
}
for (const it of list) {
if (isDirPlatform(it)) {
await walkFolder(it.fileId || it.id, prefix + (it.fileName || it.name) + '/');
} else {
await collectItem(it, prefix);
done++;
updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${it.fileName || it.name}`);
await sleep(CONFIG.md5Delay);
}
}
if (isXunlei()) {
if (!next) break;
page = next;
} else {
if (list.length < CONFIG.fileListPageSize) break;
page = Number(page || 0) + 1;
}
await sleep(CONFIG.listPageDelay);
}
};
try {
const platformName = isGuangya() ? '光鸭' : '迅雷';
setStatus(statusEl, all ? `已全选当前目录 ${items.length} 项,正在获取文件信息...` : `正在获取 ${items.length} 项的文件信息...`, 'info');
for (const item of items) {
if (isDirPlatform(item)) {
await walkFolder(item.fileId || item.id, (item.fileName || item.name) + '/');
} else {
if (!(item.fileId || item.id)) {
warnMissing.push(`${item.fileName || item.name}(未能匹配到文件ID)`);
done++;
continue;
}
await collectItem(item, '');
done++;
updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${item.fileName || item.name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!files.length) {
setStatus(statusEl, '未获取到任何文件的哈希(MD5/GCID),无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: platformName,
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files,
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '.json';
const dual = files.filter(f => f.etag && f.gcid).length;
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(${dual} 项双哈希 MD5+GCID)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try {
GM_setClipboard(jsonStr);
showToast('秒传 JSON 已复制到剪贴板');
} catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = exportName;
a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
function escapeHtml(str) {
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
// ---- 百度导出(生成秒传 JSON,含 contentMd5 + 云服务器注册)----
async function baiduGenerateJson() {
const bdstoken = BAIDU.getBdstoken();
if (!bdstoken) {
showToast('未能获取百度 bdstoken,请刷新百度网盘页面后重试', 'warning');
return;
}
// 需要下载凭证 token(dlink 下载/嵌入字节/云注册依赖)
await BAIDU.ensureAccessToken();
const sel = BAIDU.getCheckedItems();
const currentDir = BAIDU.getCurrentDir();
let fsIds = sel.ids.slice();
if (sel.headState === 'checked' && !fsIds.length) {
showToast('检测到全选,正在拉取当前目录全部条目...', 'info', 2000);
let page = 1;
try {
for (;;) {
const r = await BAIDU.listDir(currentDir, page);
for (const it of r.list) fsIds.push(String(it.fs_id));
if (!r.hasMore) break;
page++;
await sleep(CONFIG.listPageDelay);
}
} catch (e) {
showToast(`拉取目录失败:${e.message}`, 'error');
return;
}
}
fsIds = Array.from(new Set(fsIds));
if (!fsIds.length) {
showToast('请先在页面勾选要导出的文件或文件夹', 'warning');
return;
}
const modal = openModal({
title: '生成秒传 JSON(百度网盘)',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-bd-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
try {
setStatus(statusEl, `正在获取 ${fsIds.length} 项的文件信息...`, 'info');
const metas = await BAIDU.getFileMetas(fsIds);
const folderMetas = [];
for (const id of fsIds) {
const m = metas[id];
if (!m) { warnMissing.push(`fs_id=${id}(未取到元数据)`); continue; }
if (m.isdir) {
folderMetas.push(m);
} else {
const md5 = m.md5;
if (md5 && /^[0-9a-f]{32}$/.test(md5)) {
const entry = { path: String(m.path || m.name).replace(/^\//, ''), etag: md5, contentMd5: BAIDU.encryptMd5(md5), size: m.size, fsId: m.fsId || '' };
// 大文件(分块上传)md5 字段可能非内容哈希:尝试从 dlink 响应头探测真实 md5 覆盖
// (注:小巧数据不内嵌进 JSON——JSON 需保持轻量可发社交平台,验证块一律走云服务器)
if (Number(m.size) > 262144 && m.fsId) {
try {
const dl = await BAIDU.getDlink(m.fsId, m.path);
if (dl) {
const realMd5 = await BAIDU.probeDlinkMd5(dl);
if (realMd5 && /^[0-9a-f]{32}$/.test(realMd5) && realMd5 !== entry.etag) {
entry.etag = realMd5;
entry.contentMd5 = BAIDU.encryptMd5(realMd5);
entry.md5Fixed = true;
setStatus(statusEl, `已从下载响应头修复真实 MD5:${m.name}`, 'info');
}
}
} catch (e) { /* 探测失败则保留 filemetas 值 */ }
await sleep(CONFIG.md5Delay);
}
files.push(entry);
} else {
warnMissing.push((m.path || m.name) + ':未能取得合法 MD5');
}
}
setStatus(statusEl, `已处理 ${files.length} 项...`, 'info');
await sleep(CONFIG.md5Delay);
}
const counter = { done: 0 };
for (const f of folderMetas) {
setStatus(statusEl, `正在扫描文件夹:${f.path}...`, 'info');
await baiduWalkFolder(f.path, files, warnMissing, statusEl, barEl, counter);
}
if (!files.length) {
setStatus(statusEl, '未获取到任何文件的哈希(MD5),无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const bigCount = files.filter(f => (Number(f.size) || 0) > 4 * 1024 * 1024).length;
// ---- 云块服务器注册(仅「同意提供令牌」时;仅导出模式不采集令牌)----
const cloudNotes = [];
let tracker = null;
if (!CONFIG.cloudServer || !CONFIG.cloudRegister) {
cloudNotes.push('未配置云服务器或未启用注册');
} else if (!BAIDU.cloudConsentEnabled()) {
mcLog('export', '仅导出模式:不采集令牌,跳过云注册');
} else {
try {
await BAIDU.ensureAccessToken();
setStatus(statusEl, '正在注册云块服务器...', 'info');
const token = BAIDU.getAccessTokenCached();
const reg = await BAIDU.cloudRegister(CONFIG.cloudServer, token, files);
if (reg && reg.ok) {
tracker = { cloud: { server: CONFIG.cloudServer } };
mcLog('export', '云服务器注册成功,' + (reg.registered != null ? reg.registered + ' 个文件归并' : ''));
} else {
cloudNotes.push('云服务器注册失败(' + (reg && reg.err || '未知错误') + ')');
mcLog('export', 'ERROR 云注册失败:' + (reg && reg.err));
}
} catch (e) {
cloudNotes.push('云服务器注册异常(' + e.message + ')');
mcLog('export', 'ERROR 云注册异常:' + e.message);
}
}
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '百度',
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
tracker,
files,
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_百度.json';
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}${cloudNotes.length ? `;${cloudNotes.length} 项云注册未成功` : ''}
${tracker ? `云块服务器已注册,导入者可经服务器取验证块。
` : ''}
${cloudNotes.length ? `${cloudNotes.map(n => '· ' + n).join('\n')}
` : ''}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
// 导出日志按钮(本地,便于排查问题)
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginLeft = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
modal.footerEl.appendChild(logBtn);
} catch (e) { /* ignore */ }
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 递归收集目录下所有文件(path 用服务器绝对路径,etag 为解密 md5) */
async function baiduWalkFolder(dir, files, warnMissing, statusEl, barEl, counter) {
let page = 1;
for (;;) {
let list, hasMore;
try {
const r = await BAIDU.listDir(dir, page);
list = r.list;
hasMore = r.hasMore;
} catch (e) {
setStatus(statusEl, `列出目录失败:${dir}(${e.message})`, 'err');
return;
}
for (const it of list) {
if (Number(it.isdir) === 1) {
await baiduWalkFolder(it.path, files, warnMissing, statusEl, barEl, counter);
} else {
const md5 = BAIDU.decryptMd5(it.md5 || '');
const name = String(it.server_filename || '');
if (md5 && /^[0-9a-f]{32}$/.test(md5)) {
files.push({ path: String(it.path || name).replace(/^\//, ''), etag: md5, contentMd5: BAIDU.encryptMd5(md5), size: Number(it.size || 0), fsId: String(it.fs_id || '') });
} else {
warnMissing.push(String(it.path || name) + ':未能取得合法 MD5');
}
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${dir}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!hasMore) break;
page++;
await sleep(CONFIG.listPageDelay);
}
}
/** 百度云服务器导入(协作秒传主流程) */
async function baiduCloudImportRow(row, cloudServer, uk, currentDir, onProgress) {
const SIZE256 = 262144;
const bdstoken = BAIDU.getBdstoken();
const dataTime = Math.floor(Date.now() / 1000);
const size = row.size;
const offset = size <= SIZE256 ? 0 : BAIDU.computeDataOffset(uk, row.contentMd5, dataTime, size);
mcLog('import', '云分支:size=' + size + ' offset=' + offset + ' contentMd5=' + row.contentMd5);
// 经云服务器取验证块(服务器按 md5 查池、轮询凭据拉块)
mcLog('import', '经 ' + cloudServer + ' 取验证块');
let sliceBytes = null;
if (offset !== 0 && size > SIZE256) {
if (onProgress) onProgress('正在经云服务器取 slice 块...');
sliceBytes = await BAIDU.cloudBlock(cloudServer, row.contentMd5, 0, SIZE256);
if (!sliceBytes || sliceBytes.length < SIZE256) {
mcLog('import', 'ERROR 云服务器 slice 块获取失败');
return { ok: false, errno: -1, reason: '云服务器获取 slice 块失败(该 md5 未注册或凭据不可用)' };
}
}
if (onProgress) onProgress('正在经云服务器取验证块...');
let contentBytes = await BAIDU.cloudBlock(cloudServer, row.contentMd5, offset, SIZE256);
if (!contentBytes && sliceBytes && offset === 0) contentBytes = sliceBytes;
if (!contentBytes || contentBytes.length < SIZE256) {
mcLog('import', 'ERROR 云服务器验证块获取失败(offset=' + offset + ' len=' + (contentBytes && contentBytes.length) + ')');
return { ok: false, errno: -1, reason: '云服务器获取验证块失败(该 md5 无人注册或凭据不可用)' };
}
contentBytes = contentBytes.subarray(0, SIZE256);
mcLog('import', '云服务器取块成功(slice=' + (sliceBytes ? sliceBytes.length : 0) + ' content=' + contentBytes.length + ')');
const sliceMd5 = size <= SIZE256 ? row.contentMd5 : BAIDU.encryptMd5(baiduBytesToMd5Hex(sliceBytes || contentBytes));
// rapidupload 秒传(目标路径 = 当前目录 + 原文件名,与同账号导入一致)
const targetPath = (currentDir === '/' || !currentDir ? '/' : String(currentDir).replace(/\/+$/, '') + '/') + String(row.path || '').replace(/^\/+/, '');
const pcBody = new URLSearchParams({ path: targetPath, autoinit: '1', block_list: JSON.stringify(['00000000000000000000000000000000', '11111111111111111111111111111111']), target_path: '/', local_mtime: String(Math.floor(Date.now() / 1000)) });
const pcr = await fetch(BAIDU.API_BASE + '/api/precreate?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(),
{ method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: pcBody.toString() });
const pcj = await pcr.json();
if (!pcj.uploadid) {
mcLog('import', 'ERROR 云模式 precreate 失败 errno=' + pcj.errno);
return { ok: false, errno: pcj.errno, reason: 'precreate 失败' };
}
let bin = '';
for (let i = 0; i < contentBytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, contentBytes.subarray(i, i + 0x8000));
const ruBody = new URLSearchParams({
uploadid: pcj.uploadid, path: targetPath, 'content-length': String(size),
'content-md5': row.contentMd5, 'slice-md5': sliceMd5,
target_path: '/', local_mtime: String(Math.floor(Date.now() / 1000)),
data_time: String(dataTime), data_offset: String(offset), data_content: btoa(bin),
});
const rur = await fetch(BAIDU.API_BASE + '/api/rapidupload?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(),
{ method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: ruBody.toString() });
let ruj = null;
try { ruj = await rur.json(); } catch (e) { /* ignore */ }
mcLog('import', '云模式 rapidupload 结果 errno=' + (ruj && ruj.errno));
// 成功 → 滚雪球:定位秒传后的文件 fsId,把当前账号凭据加入云池(后续导入者可复用)
if (ruj && ruj.errno === 0) {
try {
const myFsId = await BAIDU.getFsidByPath(targetPath);
if (myFsId) {
BAIDU.cloudPool(cloudServer, BAIDU.getAccessTokenCached(), row.contentMd5, myFsId).catch(() => {});
mcLog('import', '云模式秒传成功,凭据已入池(fsId=' + myFsId + ')');
} else {
mcLog('import', '云模式秒传成功,但未定位到新文件 fsId');
}
} catch (e) { mcLog('import', '云模式入池异常:' + e.message); }
}
return { ok: ruj && ruj.errno === 0, errno: ruj && ruj.errno, reason: ruj && ruj.errno === 0 ? '' : ('errno=' + ruj.errno) };
}
// ---- 百度导入(同账号 dlink / 嵌入数据 / 云服务器协作)----
async function baiduImportJson(rawJson, onProgress) {
if (!BAIDU.cloudConsentEnabled()) {
throw new Error('已选择「仅导出」模式:不采集令牌,导入功能已禁用(设置里可切换提供令牌恢复)');
}
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const md5 = normalizeMd5(f.etag || f.md5);
const size = Number(f.size != null ? f.size : 0);
if (!path || !md5) continue;
rows.push({
path,
md5,
contentMd5: String(f.contentMd5 || '').trim() || BAIDU.encryptMd5(md5),
size,
});
}
if (!rows.length) throw new Error('JSON 中没有可导入的文件条目');
mcLog('import', '开始导入,共 ' + rows.length + ' 条,当前目录 ' + (BAIDU.getCurrentDir ? BAIDU.getCurrentDir() : '/'));
const currentDir = BAIDU.getCurrentDir();
const bdstoken = BAIDU.getBdstoken();
if (!bdstoken) throw new Error('未能获取百度 bdstoken,请刷新页面后重试');
const uk = await BAIDU.getUk();
if (!uk) throw new Error('未能获取当前账号 uk(oper_id),请刷新页面后重试');
await BAIDU.ensureAccessToken(); // 同账号 dlink 下载需要 access_token
mcLog('import', 'uk=' + uk + ' token=' + (BAIDU.getAccessTokenCached ? '有' : '无'));
const cloudServer = (obj && obj.tracker && obj.tracker.cloud && obj.tracker.cloud.server) ? String(obj.tracker.cloud.server) : (CONFIG.cloudServer || '');
if (cloudServer) mcLog('import', '云服务器:' + cloudServer);
const SIZE256 = 262144;
let success = 0, fail = 0, skipped = 0;
const failures = [];
const skippedDetail = [];
const total = rows.length;
async function locateSource(pathStr) {
const p = '/' + String(pathStr || '').replace(/^\/+/, '');
const idx = p.lastIndexOf('/');
const parent = idx <= 0 ? '/' : p.slice(0, idx);
const name = p.slice(idx + 1);
let page = 1;
for (;;) {
let r;
try { r = await BAIDU.listDir(parent, page); } catch (e) { return null; }
const hit = (r.list || []).find(x => String(x.server_filename) === name && String(x.path) === p);
if (hit) return { fsId: String(hit.fs_id), size: Number(hit.size || 0), isdir: Number(hit.isdir) === 1, path: String(hit.path || p) };
if (!r.hasMore) break;
page++;
await sleep(CONFIG.listPageDelay);
}
return null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `百度秒传 ${i + 1}/${total}:${row.path}` });
try {
const dataTime = Math.floor(Date.now() / 1000);
const size = row.size > 0 ? row.size : 0;
const offset = size <= SIZE256 ? 0 : BAIDU.computeDataOffset(uk, row.contentMd5, dataTime, size);
let sliceBytes = null;
let contentBytes = null;
// 取块优先级:云服务器(跨账号)→ 同账号网盘内定位源文件 dlink(同账号/自秒)
if (cloudServer) {
try {
mcLog('import', '进入云导入分支(row=' + row.path + ')');
const res = await baiduCloudImportRow(row, cloudServer, uk, currentDir, (t) => onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: t + `:${row.path}` }));
if (res && res.ok) { mcLog('import', '云秒传成功:' + row.path); success++; await sleep(CONFIG.transferDelay); continue; }
if (res && res.reason) { skippedDetail.push(`${row.path}:云秒传未成(${res.reason})`); mcLog('import', '云秒传未成:' + res.reason); }
} catch (e) {
mcLog('import', 'ERROR 云秒传异常:' + e.message);
skippedDetail.push(`${row.path}:云秒传异常(${e.message})`);
}
}
const src = await locateSource(row.path);
if (!src || src.isdir) {
skipped++;
skippedDetail.push(`${row.path}:经云服务器未命中且当前账号云端未找到源文件(跨账号 JSON 无文件本体,无法秒传)`);
continue;
}
const dlink = await BAIDU.getDlink(src.fsId, src.path || row.path);
if (!dlink) {
fail++;
failures.push(`${row.path}:未能获取下载链接`);
continue;
}
const realSize = row.size > 0 ? row.size : src.size;
if (realSize <= SIZE256) {
sliceBytes = await BAIDU.downloadSlice(dlink, 0, realSize);
contentBytes = sliceBytes;
} else if (offset === 0) {
sliceBytes = await BAIDU.downloadSlice(dlink, 0, SIZE256);
contentBytes = sliceBytes;
} else {
sliceBytes = await BAIDU.downloadSlice(dlink, 0, SIZE256);
contentBytes = await BAIDU.downloadSlice(dlink, offset, SIZE256);
}
const sliceMd5 = size <= SIZE256 ? row.contentMd5 : BAIDU.encryptMd5(baiduBytesToMd5Hex(sliceBytes));
const targetPath = (currentDir === '/' ? '/' : currentDir + '/') + row.path.replace(/^\/+/, '');
const pcBody = new URLSearchParams({ path: targetPath, autoinit: '1', block_list: JSON.stringify(['00000000000000000000000000000000', '11111111111111111111111111111111']), target_path: currentDir === '/' ? '/' : currentDir, local_mtime: String(Math.floor(Date.now() / 1000)) });
const pcr = await fetch(BAIDU.API_BASE + '/api/precreate?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(),
{ method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: pcBody.toString() });
const pcj = await pcr.json();
if (!pcj.uploadid) {
fail++;
failures.push(`${row.path}:precreate 失败(${pcj.errmsg || ('errno=' + pcj.errno)})`);
continue;
}
let bin = '';
for (let j = 0; j < contentBytes.length; j += 0x8000) bin += String.fromCharCode.apply(null, contentBytes.subarray(j, j + 0x8000));
const ruBody = new URLSearchParams({
uploadid: pcj.uploadid, path: targetPath, 'content-length': String(size),
'content-md5': row.contentMd5, 'slice-md5': sliceMd5,
target_path: currentDir === '/' ? '/' : currentDir,
local_mtime: String(Math.floor(Date.now() / 1000)),
data_time: String(dataTime), data_offset: String(offset), data_content: btoa(bin),
});
const rur = await fetch(BAIDU.API_BASE + '/api/rapidupload?rtype=1&bdstoken=' + encodeURIComponent(bdstoken) + '&app_id=' + BAIDU.APP_ID + '&channel=' + BAIDU.CHANNEL + '&web=1&clienttype=0&dp-logid=' + Date.now(),
{ method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': location.origin + '/disk/main' }, body: ruBody.toString() });
let ruj = null;
try { ruj = await rur.json(); } catch (e) { /* ignore */ }
const errno = ruj && ruj.errno;
if (errno === 0) {
success++;
} else if (errno === 404) {
fail++;
failures.push(`${row.path}:未命中(errno=404)。可能原因:content-md5 与云端不一致(大文件分块上传的 md5 问题),或嵌入字节与源文件不符,或源文件已变动`);
} else {
fail++;
failures.push(`${row.path}:百度秒传未成功(errno=${errno}${ruj && ruj.errmsg ? ',' + ruj.errmsg : ''})`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
// ============================== 夸克网盘导出/导入(v1.2) ==============================
// 设计要点:
// - 导出:列表无 hash → 批量调 /1/clouddrive/file/download 取明文 md5+sha1
// - JSON 轻量:{path, sha1, md5, hashName:"sha1", etag:"", size},不内嵌文件字节
// - 导入:夸克秒传(upload/pre)**不需要 proof/验证块**——带 sha1+md5 直接命中 finish:true,
// PDS 全局去重跨账号可用,因此导入**不需要云服务器**(与阿里云盘不同,这是夸克的最大优势)
// - 建目录:/1/clouddrive/file {pdir_fid, file_name};path 中的目录逐级创建
/** 夸克导出:勾选 → 递归列表 → 批量取 hash → 轻量 JSON(含 md5+sha1) */
async function quarkGenerateJson() {
const sel = QUARK.getCheckedItems();
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(夸克网盘)',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-qk-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const sha1Check = modal.contentEl.querySelector('#gy-ms-qk-gen-sha1');
const wantSha1 = () => sha1Check && sha1Check.checked;
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
// 递归收集(勾选项:文件直接收,目录递归)
const rootParent = QUARK.getCurrentParentId();
setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info');
for (const item of sel.items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (item.isDir && item.fid) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await quarkWalkFolder(item.fid, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fid) {
await quarkCollectFile(item, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
// 批量取 hash(按 fid)
const fids = files.filter(f => f.fileId).map(f => f.fileId);
const hashMap = await QUARK.getFileHashes(fids);
for (const f of files) {
const h = hashMap[String(f.fileId)] || {};
if (h.md5 || h.sha1) {
f.md5 = h.md5 || '';
f.sha1 = h.sha1 || '';
f.etag = h.md5 || '';
f.size = h.size || f.size || 0;
// 有哈希才保留;否则进 warnMissing
if (!f.md5 && !f.sha1) warnMissing.push(f.path + ':未能取得哈希');
} else {
warnMissing.push(f.path + ':download 接口未返回哈希');
}
}
// 夸克 download 接口不返回 sha1;勾选后下载文件流式计算真实 SHA1(跨账号秒传的全局键)
if (wantSha1()) {
const needSha1 = files.filter(f => f.fileId && !f.sha1);
if (needSha1.length) {
setStatus(statusEl, `正在下载计算 SHA1(${needSha1.length} 个文件)...`, 'info');
for (let si = 0; si < needSha1.length; si++) {
const f = needSha1[si];
setStatus(statusEl, `计算 SHA1 ${si + 1}/${needSha1.length}:${f.path}(${formatSize(f.size || 0)})`, 'info');
const sha1 = await QUARK.getFileSha1ByDownload(f.fileId, f.size, (pct) => {
updateProgress(barEl, Math.round(pct), `SHA1 ${si + 1}/${needSha1.length}:${f.path} ${pct}%`);
});
if (sha1 && /^[0-9a-f]{40}$/.test(sha1)) {
f.sha1 = sha1;
} else {
warnMissing.push(f.path + ':SHA1 计算失败(文件下载受限),该条目仅含 MD5');
}
}
}
}
const finalFiles = files.filter(f => (f.md5 || f.sha1));
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的哈希(MD5/SHA1),无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '夸克网盘',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({ path: f.path, sha1: f.sha1 || '', md5: f.md5 || '', hashName: 'sha1', etag: f.md5 || '', size: Number(f.size) || 0 })),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_夸克.json';
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(MD5+SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 夸克单文件收集(暂存 fid,hash 待批量取) */
async function quarkCollectFile(it, prefix, files, warnMissing) {
const name = String(it.name || it.file_name || '');
if (!it.fid) { warnMissing.push(prefix + name + ':无 fid'); return; }
files.push({
path: prefix + name,
fileId: String(it.fid),
md5: String(it.md5 || '').toLowerCase(),
sha1: String(it.sha1 || '').toLowerCase(),
size: Number(it.size || 0),
});
}
/** 夸克递归收集目录(file/sort 分页) */
async function quarkWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let page = 1;
for (;;) {
let r;
try {
r = await QUARK.getFileList(folderId, page, 100);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
const list = r.list || [];
for (const it of list) {
const isDir = it.dir === true;
const name = String(it.file_name || '');
if (isDir) {
if (it.fid) await quarkWalkFolder(String(it.fid), prefix + name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await quarkCollectFile({ fid: it.fid, name: it.file_name, size: it.size }, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!r.hasMore) break;
page++;
await sleep(CONFIG.listPageDelay);
}
}
/** 夸克导入(upload/pre 秒传,无需云服务器/proof) */
async function quarkImportJson(rawJson, onProgress) {
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
// 夸克秒传优先 sha1(40hex);也兼容纯 md5(32hex)
const rawSha1 = String(f.sha1 || '').trim();
const rawMd5 = String(f.md5 || f.etag || '').trim();
const sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : '';
const md5 = /^[0-9a-f]{32}$/i.test(rawMd5) ? rawMd5.toLowerCase() : '';
const size = Number(f.size != null ? f.size : 0);
if (!path || (!sha1 && !md5)) continue;
rows.push({ path, sha1, md5, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1 或 md5)');
const rootParent = QUARK.getCurrentParentId();
let success = 0, fail = 0, skipped = 0;
const failures = [];
const skippedDetail = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `夸克秒传 ${i + 1}/${total}:${row.path}` });
try {
// 逐级建目录
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootParent;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await QUARK.createDir(parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
// 已存在目录可能报错,尝试直接秒传(父目录已建则跳过)
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
// 秒传
const r = await QUARK.rapidUpload(parent, fileName, row.size, row.sha1, row.md5, '');
if (r && r.ok && r.rapid) {
success++;
} else {
fail++;
failures.push(`${row.path}:夸克未命中快速上传(flash),服务器上可能不存在该内容(${r && r.err || ''})`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '夸克导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 夸克导入模态框 */
function showQuarkImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到夸克网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、md5=32 位、path、size)。
秒传优先使用 sha1,缺失时用 md5;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-qk-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-qk-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-qk-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-qk-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await quarkImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== UC 网盘导出/导入(v1.3) ==============================
// 与夸克同源 PDS,差异:API host=pc-api.uc.cn / pr=UCBrowser / download 的 md5 为 base64 编码
// - 导出:列表无 hash → 批量调 download 取 base64 md5(JSON 保持 base64,与 UC upload/pre 一致)
// - 导入:upload/pre 用 base64 md5 秒传(实测 hex 不命中、base64 命中);小文件 md5 即可
// - JSON 轻量:{path, md5, hashName:"md5", etag:md5, size}
/** UC 导出 */
async function ucGenerateJson() {
const sel = UC.getCheckedItems();
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(UC网盘)',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-uc-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const sha1Check = modal.contentEl.querySelector('#gy-ms-uc-gen-sha1');
const wantSha1 = () => sha1Check && sha1Check.checked;
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
const rootParent = UC.getCurrentParentId();
setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info');
for (const item of sel.items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (item.isDir && item.fid) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await ucWalkFolder(item.fid, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fid) {
await ucCollectFile(item, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
const fids = files.filter(f => f.fileId).map(f => f.fileId);
const hashMap = await UC.getFileHashes(fids);
for (const f of files) {
const h = hashMap[String(f.fileId)] || {};
if (h.md5) {
f.md5 = h.md5 || '';
f.etag = h.md5 || '';
f.size = h.size || f.size || 0;
if (!f.md5) warnMissing.push(f.path + ':未能取得哈希');
} else {
warnMissing.push(f.path + ':download 接口未返回哈希');
}
}
// UC download 接口同样不返回 sha1;勾选后下载文件流式计算真实 SHA1(跨账号秒传的全局键)
if (wantSha1()) {
const needSha1 = files.filter(f => f.fileId && !f.sha1);
if (needSha1.length) {
setStatus(statusEl, `正在下载计算 SHA1(${needSha1.length} 个文件)...`, 'info');
for (let si = 0; si < needSha1.length; si++) {
const f = needSha1[si];
setStatus(statusEl, `计算 SHA1 ${si + 1}/${needSha1.length}:${f.path}(${formatSize(f.size || 0)})`, 'info');
const sha1 = await UC.getFileSha1ByDownload(f.fileId, f.size, (pct) => {
updateProgress(barEl, Math.round(pct), `SHA1 ${si + 1}/${needSha1.length}:${f.path} ${pct}%`);
});
if (sha1 && /^[0-9a-f]{40}$/.test(sha1)) {
f.sha1 = sha1;
} else {
warnMissing.push(f.path + ':SHA1 计算失败(文件下载受限),该条目仅含 MD5');
}
}
}
}
const finalFiles = files.filter(f => (f.md5 || f.sha1));
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的哈希(MD5),无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: 'UC网盘',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({ path: f.path, sha1: f.sha1 || '', md5: f.md5 || '', hashName: f.sha1 ? 'sha1' : 'md5', etag: f.md5 || '', size: Number(f.size) || 0 })),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_UC.json';
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(MD5+SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** UC 单文件收集 */
async function ucCollectFile(it, prefix, files, warnMissing) {
const name = String(it.name || it.file_name || '');
if (!it.fid) { warnMissing.push(prefix + name + ':无 fid'); return; }
files.push({ path: prefix + name, fileId: String(it.fid), md5: String(it.md5 || ''), sha1: String(it.sha1 || '').toLowerCase(), size: Number(it.size || 0) });
}
/** UC 递归收集目录 */
async function ucWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let page = 1;
for (;;) {
let r;
try {
r = await UC.getFileList(folderId, page, 100);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
const list = r.list || [];
for (const it of list) {
const isDir = it.dir === true;
const name = String(it.file_name || '');
if (isDir) {
if (it.fid) await ucWalkFolder(String(it.fid), prefix + name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await ucCollectFile({ fid: it.fid, name: it.file_name, size: it.size }, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!r.hasMore) break;
page++;
await sleep(CONFIG.listPageDelay);
}
}
/** UC 导入(upload/pre 秒传,md5 用 base64) */
async function ucImportJson(rawJson, onProgress) {
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
// UC md5 为 base64(download 原样);兼容 hex md5(转换成 base64)与 sha1
const rawMd5 = String(f.md5 || f.etag || '').trim();
const rawSha1 = String(f.sha1 || '').trim();
let md5 = '';
if (/^[0-9a-f]{32}$/i.test(rawMd5)) {
// hex → base64
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) bytes[i] = parseInt(rawMd5.substr(i * 2, 2), 16);
md5 = btoa(String.fromCharCode.apply(null, bytes));
} else if (/^[A-Za-z0-9+/]{22}==$/.test(rawMd5) || /^[A-Za-z0-9+/]{22}=$/.test(rawMd5)) {
md5 = rawMd5; // 已是 base64
}
const size = Number(f.size != null ? f.size : 0);
if (!path || (!md5 && !rawSha1)) continue;
rows.push({ path, md5, sha1: rawSha1.toLowerCase(), size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 md5 或 sha1)');
const rootParent = UC.getCurrentParentId();
let success = 0, fail = 0, skipped = 0;
const failures = [];
const skippedDetail = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `UC秒传 ${i + 1}/${total}:${row.path}` });
try {
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootParent;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await UC.createDir(parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
// 有 sha1 用 sha1(大文件必需),否则 base64 md5
let r;
if (row.sha1 && /^[0-9a-f]{40}$/i.test(row.sha1)) {
r = await UC.rapidUploadWithSha1(parent, fileName, row.size, row.sha1, row.md5, '');
} else {
r = await UC.rapidUpload(parent, fileName, row.size, row.md5, '');
}
if (r && r.ok && r.rapid) {
success++;
} else {
fail++;
failures.push(`${row.path}:UC未命中快速上传(flash),服务器上可能不存在该内容(${r && r.err || ''})`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', 'UC导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** UC 导入模态框 */
function showUcImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到 UC网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=base64 或 32 位 hex、sha1=40 位、path、size)。
UC 秒传优先使用 sha1(大文件必需),缺失时用 md5(小文件可用);path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-uc-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-uc-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-uc-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-uc-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await ucImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 腾讯微云导出/导入(v1.4) ==============================
// 设计要点:
// - 列表自带 file_sha(完整 SHA1),JSON 输出 {path, sha1, hashName:'sha1', size}
// - 微云秒传(ftn_pre_upload)需要"1MB 分块 SHA1 状态 + check_sha + check_data(末尾 ≤128 字节)",
// 这些只能由导出方(持有原文件)下载计算后内嵌 JSON(wyBlocks/wyCheckSha/wyCheckData),导入方零下载直接提交;
// - 未填写分块信息的 JSON(如夸克/UC 导出的)无法导入微云(微云不认纯 sha1 秒传),会提示;
// - 导入未命中(file_exist=false)时微云会创建空壳条目 → 自动清理并报"未命中"。
/** 微云导出:勾选 → 递归列表(sha1 自带)→ 可选下载计算分块信息 → JSON */
async function weiyunGenerateJson() {
let sel;
try {
sel = await WEIYUN.getCheckedItems();
} catch (e) {
showToast('读取勾选失败:' + e.message, 'error');
return;
}
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(腾讯微云)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-wy-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const blocksCheck = modal.contentEl.querySelector('#gy-ms-wy-gen-blocks');
const wantBlocks = () => blocksCheck && blocksCheck.checked;
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info');
const curDirKey = await WEIYUN.resolveDirKey(WEIYUN.getCurrentDirKey());
for (const item of sel.items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (item.kind === 'dir' && item.dirKey) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await weiyunWalkFolder(item.dirKey, name + '/', curDirKey, files, warnMissing, statusEl, barEl, counter);
} else if (item.kind === 'file' && item.fileId) {
await weiyunCollectFile(item, '', files, warnMissing, item.pdirKey || curDirKey);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
const finalFiles = files.filter(f => f.sha1);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的 SHA1,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
// 下载计算微云秒传分块信息(每文件 Range 流式;用户可关闭——关闭后 JSON 仅含 sha1,供夸克/UC/阿里导入)
const want = wantBlocks();
if (want) {
setStatus(statusEl, `正在下载计算微云分块信息(${finalFiles.length} 个文件)...`, 'info');
for (let i = 0; i < finalFiles.length; i++) {
const f = finalFiles[i];
if (!f.fileId) continue;
setStatus(statusEl, `计算分块 ${i + 1}/${finalFiles.length}:${f.path}(${formatSize(f.size || 0)})`, 'info');
const binfo = await WEIYUN.computeBlockInfo(f.pdirKey || curDirKey, { fileId: f.fileId, size: f.size }, (pct) => {
updateProgress(barEl, Math.round(pct), `分块 ${i + 1}/${finalFiles.length}:${f.path} ${pct}%`);
});
if (binfo && binfo.blockInfoList && binfo.checkData) {
f.wyBlocks = binfo.blockInfoList;
f.wyCheckSha = binfo.checkSha;
f.wyCheckData = binfo.checkData;
f.sha1 = f.sha1 || binfo.fileHash;
} else {
warnMissing.push(f.path + ':分块信息计算失败(下载受限),该条目仅含 SHA1');
}
}
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '腾讯微云',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({
path: f.path,
sha1: f.sha1 || '',
hashName: 'sha1',
etag: '',
size: Number(f.size) || 0,
...(f.fileId ? { fileId: f.fileId } : {}),
...(f.wyBlocks ? { wyBlocks: f.wyBlocks, wyCheckSha: f.wyCheckSha, wyCheckData: f.wyCheckData } : {}),
})),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_微云.json';
const withBlocks = finalFiles.filter(f => f.wyBlocks).length;
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(SHA1${withBlocks ? `,${withBlocks} 项含微云分块信息` : ''})${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
${withBlocks ? '含分块信息的 JSON 可被**任意微云账号**秒传导入;' : ''}SHA1 字段可被夸克/UC 导入。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 微云单文件收集(sha1 来自列表条目;pdirKey=文件所在目录,下载分块时用) */
async function weiyunCollectFile(it, prefix, files, warnMissing, pdirKey) {
const name = String(it.name || it.file_name || '');
const sha1 = String(it.sha1 || it.file_sha || '').toLowerCase();
if (!it.fileId) { warnMissing.push(prefix + name + ':无 file_id'); return; }
files.push({
path: prefix + name,
fileId: String(it.fileId),
pdirKey: String(pdirKey || ''),
sha1: /^[0-9a-f]{40}$/.test(sha1) ? sha1 : '',
size: Number(it.size || 0),
});
}
/** 微云递归收集目录(DiskDirBatchList 分页) */
async function weiyunWalkFolder(folderId, prefix, curDirKey, files, warnMissing, statusEl, barEl, counter) {
let offset = 0;
for (;;) {
let r;
try {
r = await WEIYUN.getFileList(folderId, offset, 500);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
for (const d of r.dirs) {
if (d.dirKey) await weiyunWalkFolder(d.dirKey, prefix + d.name + '/', curDirKey, files, warnMissing, statusEl, barEl, counter);
}
for (const f of r.files) {
await weiyunCollectFile(f, prefix, files, warnMissing, String(folderId));
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${f.name}`);
await sleep(CONFIG.md5Delay);
}
if (r.finishFlag || (!r.dirs.length && !r.files.length)) break;
offset += 500;
await sleep(CONFIG.listPageDelay);
}
}
/** 微云导入(ftn_pre_upload 秒传;需 JSON 内嵌微云分块信息) */
async function weiyunImportJson(rawJson, onProgress) {
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const rawSha1 = String(f.sha1 || '').trim();
const sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : '';
const blocks = Array.isArray(f.wyBlocks) && f.wyBlocks.length ? f.wyBlocks : null;
const checkSha = String(f.wyCheckSha || '').trim() || '';
const checkData = String(f.wyCheckData || '').trim() || '';
const size = Number(f.size != null ? f.size : 0);
if (!path || !sha1) continue;
rows.push({ path, sha1, size, blocks, checkSha, checkData });
}
if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1)');
const noBlocks = rows.filter(r => !r.blocks).length;
if (noBlocks === rows.length) {
throw new Error('微云秒传需要分块信息(wyBlocks/wyCheckSha/wyCheckData)——该 JSON 未包含。请由「腾讯微云」页面导出(勾选"计算微云秒传分块信息")后再导入微云;含 sha1 的 JSON 可直接导入夸克/UC。');
}
const rootParent = await WEIYUN.resolveDirKey(WEIYUN.getCurrentDirKey());
let success = 0, fail = 0, skipped = 0;
const failures = [];
const skippedDetail = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `微云秒传 ${i + 1}/${total}:${row.path}` });
try {
if (!row.blocks) {
skipped++;
skippedDetail.push(`${row.path}:无微云分块信息,跳过(可导入夸克/UC)`);
continue;
}
// 逐级建目录(同夸克/UC 逻辑)
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootParent;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await WEIYUN.createDir(parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
// 秒传(PreUpload)
const pair = await WEIYUN.getPdirPair(parent);
const r = await WEIYUN.preUpload(pair, fileName, row.size, 1048576, row.checkSha, row.checkData, row.blocks, 2);
if (r.hit) {
success++;
} else {
// 未命中 → 清理微云创建的空壳条目
if (r.fileId) {
WEIYUN.deleteFiles([{ ppdir_key: pair.ppdir_key, pdir_key: parent, file_id: r.fileId, filename: fileName }], []).catch(() => {});
}
fail++;
failures.push(`${row.path}:微云未命中秒传(云端无该内容),空壳条目已清理`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '微云导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 微云导入模态框 */
function showWeiyunImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到腾讯微云',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、wyBlocks/wyCheckSha/wyCheckData=微云分块信息、path、size)。
微云秒传需要 JSON 携带微云分块信息(仅由微云页面导出时勾选"计算分块信息"生成);夸克/UC 导出的 JSON(只有 sha1)无法导入微云,可导入夸克/UC。
path 中的目录会自动创建;未命中时自动清理空壳条目。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-wy-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-wy-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-wy-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-wy-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await weiyunImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无分块信息) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 小飞机网盘导出/导入(v1.5) ==============================
// 设计要点:
// - 导出【零下载】:每文件发 Range bytes=0-1 的极小请求,读 COS 对象元数据 x-amz-meta-callback
// 直接取服务器记录的 md5(=小飞机去重池键,实测可秒传命中);列表不提供哈希,故无需下载整个文件。
// - 导入:getUpToken 秒传,upToken==='-1' 即命中;只认 md5(32hex),无 md5 的条目跳过并提示。
/** 小飞机导出:勾选 → 递归列表 → 服务器元数据取 md5(零下载)→ JSON */
async function feejiiGenerateJson() {
const c = FEEJII.readCommon();
if (!c) { showToast('未检测到小飞机网盘登录态,请刷新页面', 'warning'); return; }
let sel;
try { sel = FEEJII.getCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; }
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(小飞机网盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
md5 从小飞机服务器直接获取(零下载、快,无需下载文件计算哈希)。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-fj-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info');
for (const item of sel.items) {
const name = String(item.fileName || item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (Number(item.fileType) === 2 && item.fileId) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await feejiiWalkFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fileId) {
await feejiiCollectFile(item, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
// 零下载:从服务器元数据取 md5
const withMd5 = files.filter(f => f.fileId);
setStatus(statusEl, `正在从服务器获取 md5(${withMd5.length} 个文件,零下载)...`, 'info');
const pending = [];
for (let i = 0; i < withMd5.length; i++) {
const f = withMd5[i];
const md5 = await FEEJII.getServerMd5(f.fileId);
if (md5 && /^[0-9a-f]{32}$/.test(md5)) {
f.md5 = md5;
} else {
pending.push(f);
}
setStatus(statusEl, `获取 md5 ${i + 1}/${withMd5.length}:${f.path}`, 'info');
await sleep(60);
}
// 大文件/分片文件回退:CDN 无哈希头 → 询问是否下载计算
if (pending.length) {
const totalBytes = pending.reduce((s, f) => s + (Number(f.size) || 0), 0);
if (await confirmBigDownloadModal(modal, statusEl, pending.length, totalBytes)) {
for (let i = 0; i < pending.length; i++) {
const f = pending[i];
setStatus(statusEl, `下载计算 md5 ${i + 1}/${pending.length}:${f.path}(${formatSize(Number(f.size) || 0)})`, 'info');
const m = await FEEJII.computeMd5ByDownload(f.fileId, f.size, (pct) => updateProgress(barEl, Math.round(pct * 0.9), `计算 ${f.path}:${pct}%`));
if (m && /^[0-9a-f]{32}$/.test(m)) {
f.md5 = m;
} else {
warnMissing.push(f.path + ':下载计算 md5 失败');
}
updateProgress(barEl, 90, `下载计算 ${i + 1}/${pending.length}`);
await sleep(60);
}
} else {
pending.forEach(f => warnMissing.push(f.path + ':服务器无 md5(大文件/分片),已跳过'));
}
}
const finalFiles = files.filter(f => f.md5);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '小飞机网盘',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({
path: f.path,
md5: f.md5,
hashName: 'md5',
etag: f.md5,
size: Number(f.size) || 0,
})),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_小飞机.json';
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
md5 可直接导入小飞机/百度/夸克/UC;size 已换算为字节(与其他平台 JSON 一致)。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 小飞机单文件收集(暂存 fileId;注意列表 fileSize 单位是 KB,导出统一换算为字节与其他平台 JSON 一致) */
async function feejiiCollectFile(it, prefix, files, warnMissing) {
const name = String(it.fileName || it.file_name || it.name || '');
if (!it.fileId) { warnMissing.push(prefix + name + ':无 fileId'); return; }
const kb = Number(it.fileSize || it.size || 0);
files.push({
path: prefix + name,
fileId: String(it.fileId),
size: kb > 0 ? kb * 1024 : 0,
folderId: String(it.folderId || ''),
md5: '',
sha1: '',
});
}
/** 小飞机递归收集目录(file/list 分页) */
async function feejiiWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let offset = 1;
for (;;) {
let r;
try {
r = await FEEJII.getFileList(folderId, offset);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
const list = r.list || [];
for (const it of list) {
const isDir = Number(it.fileType) === 2;
const name = String(it.fileName || '');
if (isDir) {
if (it.fileId) await feejiiWalkFolder(String(it.fileId), prefix + name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await feejiiCollectFile({ fileId: it.fileId, fileName: it.fileName, fileSize: it.fileSize, folderId: it.folderId }, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!r.hasMore) break;
offset++;
await sleep(CONFIG.listPageDelay);
}
}
/** 小飞机导入(getUpToken 秒传,只认 md5) */
async function feejiiImportJson(rawJson, onProgress) {
const c = FEEJII.readCommon();
if (!c) throw new Error('未检测到小飞机网盘登录态,请刷新页面');
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
const skippedDetail = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const md5 = normalizeMd5(f.md5 || f.etag);
const size = Number(f.size != null ? f.size : 0);
if (!path) continue;
if (!md5) {
skippedDetail.push(`${path}:无 md5(小飞机秒传只认 md5;阿里导出的仅含 sha1 无法导入)`);
continue;
}
rows.push({ path, md5, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的条目(小飞机秒传需要 md5,32 位 hex)');
const rootParent = FEEJII.getCurrentParentId();
let success = 0, fail = 0, skipped = 0;
const failures = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `小飞机秒传 ${i + 1}/${total}:${row.path}` });
try {
// 逐级建目录
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootParent;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await FEEJII.createDir(parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
// 秒传
const r = await FEEJII.rapidUpload(fileName, row.md5, row.size, parent);
if (r.hit) {
success++;
} else {
fail++;
failures.push(`${row.path}:小飞机未命中秒传(云端不存在该 md5 内容)`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '小飞机导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 小飞机导入模态框 */
function showFeejiiImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到小飞机网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位、path、size)。
小飞机秒传只认 md5:夸克/UC/百度/光鸭导出的 JSON(含 md5)可直接秒传;阿里云盘导出的(仅 sha1)将被跳过并提示。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-fj-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-fj-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-fj-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-fj-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await feejiiImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 蓝奏云导出(v1.6:分享链 + CDN Content-Md5 零下载取 md5) ==============================
// 蓝奏云网页无多选机制(行尾菜单单文件操作),故导出流程为:弹窗列出当前目录(文件夹+文件)→ 勾选(勾文件夹=递归导出)→
// 逐文件 task=22 分享信息 → 分享页解析签名 → ajaxfile.php 取直链 → CDN 响应头 Content-Md5 取 md5(零下载)。
// ============================== 蓝奏云 iframe 勾选注入(v1.6) ==============================
// 蓝奏云网页无原生多选(老式 UI,行尾仅单文件操作菜单),这里向文件管理 iframe 注入:
// - 工具栏「秒传」按钮(挂到 .mydisk_file_bar,与 上传文件/新建文件夹 并排)
// - 每行 .f_tb 前勾选框(文件行 id=f{fileId} / 文件夹行 id=fol{folId})
// - 表头 .f_th 文件列「全选」勾选框(覆盖当前已加载行;先点「显示更多文件」可加载更多)
// - MutationObserver 跟随列表重绘自动给新行补勾选框
const LZ_CB_STYLE = 'margin:0 5px 0 0;vertical-align:middle;cursor:pointer;accent-color:#10b981;width:14px;height:14px;';
const LZ_STYLE_CSS = `
.f_tb input.lzms-cb, .f_th input.lzms-all { ${LZ_CB_STYLE} }
.f_tb .f_name, .f_tb .f_name2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
`;
/** 更新全选勾选框状态(indeterminate) */
function lzUpdateAllState(doc) {
try {
const all = doc.querySelector('input.lzms-all');
if (!all) return;
const cbs = doc.querySelectorAll('.f_tb input.lzms-cb');
const checked = doc.querySelectorAll('.f_tb input.lzms-cb:checked');
all.checked = cbs.length > 0 && checked.length === cbs.length;
all.indeterminate = checked.length > 0 && checked.length < cbs.length;
} catch (e) { /* ignore */ }
}
/** 给 iframe 当前已渲染的行 + 表头补勾选框(幂等) */
function lanzouInjectCheckboxes(doc) {
if (!doc || !doc.body) return;
// 行勾选框
doc.querySelectorAll('div.f_tb').forEach(row => {
try {
if (row.querySelector('input.lzms-cb')) return;
const id = String(row.id || '');
if (!/^f\d+$/.test(id) && !/^fol\d+$/.test(id)) return;
const cb = doc.createElement('input');
cb.type = 'checkbox';
cb.className = 'lzms-cb';
cb.title = '勾选后点「秒传 → 生成秒传JSON」导出';
cb.addEventListener('change', () => lzUpdateAllState(doc));
const anchor = row.querySelector('.f_name, .f_name2');
if (anchor) anchor.insertBefore(cb, anchor.firstChild);
} catch (e) { /* ignore */ }
});
// 表头全选
const th = doc.querySelector('div.f_th');
if (th && !th.querySelector('input.lzms-all')) {
try {
const nameCol = th.querySelector('.f_name');
if (nameCol) {
const cb = doc.createElement('input');
cb.type = 'checkbox';
cb.className = 'lzms-all';
cb.title = '全选当前已加载项(先点「显示更多文件」可加载更多)';
cb.addEventListener('change', () => {
doc.querySelectorAll('.f_tb input.lzms-cb').forEach(c => c.checked = cb.checked);
lzUpdateAllState(doc);
});
nameCol.insertBefore(cb, nameCol.firstChild);
}
} catch (e) { /* ignore */ }
}
lzUpdateAllState(doc);
}
/** 从 iframe 页面勾选读取 {id, name, size, isDir}[](文件 id=fileId / 文件夹 id=folderId) */
function lanzouGetCheckedRows() {
const doc = LANZOU.frameDoc();
if (!doc || !doc.body) return [];
const out = [];
doc.querySelectorAll('div.f_tb').forEach(row => {
try {
const cb = row.querySelector('input.lzms-cb');
if (!cb || !cb.checked) return;
const id = String(row.id || '');
let item = null;
if (/^f\d+$/.test(id)) {
const nameEl = row.querySelector('.f_name_title');
const sizeEl = row.querySelector('.f_size');
item = {
id: id.slice(1),
name: nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '',
size: sizeEl ? sizeEl.textContent.replace(/\s+/g, ' ').trim() : '0 B',
isDir: false,
};
} else if (/^fol\d+$/.test(id)) {
const nameEl = row.querySelector('.f_name2, .f_name');
item = {
id: id.slice(3),
name: nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '',
size: '',
isDir: true,
};
}
if (item && item.name) out.push(item);
} catch (e) { /* ignore */ }
});
return out;
}
/** 蓝奏云顶层:等待文件管理 iframe 加载后注入(按钮 + 勾选框 + MutationObserver) */
function lanzouSetupIframe() {
let frame = null;
try { frame = document.querySelector('iframe[name="mainframe"], iframe[src*="item=files"], iframe[src*="mydisk.php"]'); } catch (e) { /* ignore */ }
if (!frame) { setTimeout(lanzouSetupIframe, 600); return; }
let doc = null;
try { doc = frame.contentDocument; } catch (e) { /* ignore */ }
if (!doc || doc.readyState !== 'complete' || !doc.body) { setTimeout(lanzouSetupIframe, 600); return; }
// iframe 内样式
try {
if (!doc.getElementById('gy-ms-lz-style')) {
const st = doc.createElement('style');
st.id = 'gy-ms-lz-style';
st.textContent = LZ_STYLE_CSS;
(doc.head || doc.documentElement).appendChild(st);
}
} catch (e) { /* ignore */ }
// 工具栏按钮(与 上传文件/新建文件夹 并排)
try {
if (!doc.querySelector('.gy-ms-toolbar-btn')) {
const bar = doc.querySelector('div.mydisk_file_bar, .f_upb');
if (bar) {
const wrap = doc.createElement('div');
wrap.className = 'gy-ms-toolbar-btn';
wrap.style.cssText = 'display:inline-flex;margin-right:8px;vertical-align:middle;position:relative;z-index:100;';
wrap.innerHTML = buildButtonHtml();
bar.appendChild(wrap);
bindDropdown(wrap);
}
}
} catch (e) { /* ignore */ }
// 勾选框(全选 + 行)
lanzouInjectCheckboxes(doc);
// 跟随列表重绘维护勾选框
try {
if (!doc.__lzmsMo) {
const MO = (doc.defaultView && doc.defaultView.MutationObserver) || window.MutationObserver;
if (MO) {
const mo = new MO(() => { lanzouInjectCheckboxes(doc); });
mo.observe(doc.body, { childList: true, subtree: true });
doc.__lzmsMo = mo;
}
}
} catch (e) { /* ignore */ }
// iframe 重新加载后重建
try {
if (frame && !frame.__lzmsBoundLoad) {
frame.__lzmsBoundLoad = true;
frame.addEventListener('load', () => { setTimeout(lanzouSetupIframe, 400); });
}
} catch (e) { /* ignore */ }
}
/** 蓝奏云大小字符串 → 字节("526.0 B"/"12.5 MB"/"1.2 GB") */
function parseLanzouSizeStr(str) {
const m = String(str || '').match(/([\d.]+)\s*([BKMGT]?)/i);
if (!m) return 0;
const v = parseFloat(m[1]) || 0;
const unit = (m[2] || 'B').toUpperCase();
const f = { B: 1, K: 1024, M: 1048576, G: 1073741824, T: 1099511627776 }[unit] || 1;
return Math.round(v * f);
}
/** 蓝奏云递归收集目录(folderId 下全部文件) */
async function lanzouWalkFolder(folderId, prefix, files, warnMissing, statusEl, counter) {
let r;
try {
r = await LANZOU.getFileList(folderId);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
for (const fol of r.folders) {
await lanzouWalkFolder(fol.id, prefix + fol.name + '/', files, warnMissing, statusEl, counter);
}
for (const f of r.files) {
files.push({ path: prefix + f.name, fileId: f.id, size: parseLanzouSizeStr(f.size), md5: '', sha1: '' });
if (counter) counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
await sleep(CONFIG.md5Delay);
}
}
/** 蓝奏云导出(优先读取页面勾选 checkbox;无勾选时走弹窗选择兜底) */
async function lanzouGenerateJson() {
// 1) 页面勾选优先(脚本注入的 .lzms-cb checkbox)
const selRows = lanzouGetCheckedRows();
if (selRows.length) {
await lanzouRunExport(selRows);
return;
}
// 2) 兜底:弹窗选择器
let folderList;
try {
folderList = await LANZOU.getFileList(LANZOU.getCurrentFolderId());
} catch (e) {
showToast('读取蓝奏云当前目录失败:' + e.message, 'error');
return;
}
const all = [...folderList.folders.map(f => ({ ...f, isDir: true })), ...folderList.files.map(f => ({ ...f, isDir: false }))];
if (!all.length) { showToast('当前目录为空,没有可导出的文件', 'warning'); return; }
// 弹窗选择
const modal = openModal({
title: '选择要导出的蓝奏云文件',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
当前目录 ${folderList.folders.length} 个文件夹、${folderList.files.length} 个文件。勾选后导出(勾选文件夹=递归导出其全部文件)。
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const listEl = modal.contentEl.querySelector('#gy-ms-lz-list');
const rowHtml = (it) => `
`;
listEl.innerHTML = all.map(rowHtml).join('');
modal.contentEl.querySelector('[data-act="all"]').onclick = () => listEl.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = true);
modal.contentEl.querySelector('[data-act="none"]').onclick = () => listEl.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
modal.footerEl.querySelector('[data-act="run"]').onclick = async () => {
const selected = [];
listEl.querySelectorAll('input[type=checkbox]:checked').forEach(cb => {
const it = all.find(x => String(x.id) === cb.getAttribute('data-id'));
if (it) selected.push(it);
});
if (!selected.length) { showToast('请至少勾选一个文件或文件夹', 'warning'); return; }
modal.overlay.remove();
await lanzouRunExport(selected);
};
}
/** 蓝奏云导出主流程(收集 + CDN Content-Md5 零下载取 md5) */
async function lanzouRunExport(selected) {
const modal = openModal({
title: '生成秒传 JSON(蓝奏云)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
md5 经蓝奏云分享链路从 CDN 响应头直接读取(零下载、快)。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-lz-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
setStatus(statusEl, `正在收集文件信息(${selected.length} 项)...`, 'info');
for (const it of selected) {
if (it.isDir) {
setStatus(statusEl, `正在扫描文件夹:${it.name}...`, 'info');
await lanzouWalkFolder(it.id, it.name + '/', files, warnMissing, statusEl, counter);
} else {
files.push({ path: it.name, fileId: it.id, size: parseLanzouSizeStr(it.size), md5: '', sha1: '' });
counter.done++;
}
}
if (!files.length) { setStatus(statusEl, '未收集到任何文件(勾选的文件夹为空?)', 'err'); modal.footerEl.innerHTML = ``; return; }
setStatus(statusEl, `正在从蓝奏云 CDN 获取 md5(${files.length} 个文件,零下载)...`, 'info');
const pending = [];
for (let i = 0; i < files.length; i++) {
const f = files[i];
let md5 = '';
try { md5 = await LANZOU.getServerMd5(f.fileId); } catch (e) { /* 计入 pending 走回退 */ }
if (md5 && /^[0-9a-f]{32}$/.test(md5)) {
f.md5 = md5;
} else {
pending.push(f);
}
updateProgress(barEl, Math.round(((i + 1) / files.length) * 100), `获取 md5 ${i + 1}/${files.length}:${f.path}`);
await sleep(CONFIG.md5Delay);
}
// 蓝奏云仅做导出:CDN 未返回 md5 的直接报错(蓝奏云文件上限 550MB,正常均带头)
if (pending.length) {
for (const f of pending) {
warnMissing.push(`${f.path}:未获取到服务器 md5(CDN 无哈希头或分享链路失败),无法导出`);
}
}
const finalFiles = files.filter(f => f.md5);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '蓝奏云',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({
path: f.path,
md5: f.md5,
hashName: 'md5',
etag: f.md5,
size: Number(f.size) || 0,
})),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_蓝奏云.json';
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
md5 可直接导入小飞机/蓝奏云优享版等支持 md5 秒传的平台;蓝奏云本身无秒传接口,无法导入。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
// ============================== 蓝奏云优享版导出/导入(v1.6:ilanzou.com,同 openware 框架) ==============================
// 设计要点:
// - 导出【零下载】:redirect 302 后的 CDN 响应头 Content-Md5(base64 文件 md5)直接取;
// 列表不提供哈希,无需下载整个文件。
// - 导入:getUpToken 秒传,upToken==='-1' 创建记录(map.fileId);只认 md5(32hex)。
// 实测任意 md5 都返回 -1 —— md5 不在内容池时记录会指向无关对象(幽灵文件),
// 故导入后零下载校验(CDN Content-Md5 对比),不一致则删除记录并报"未命中"。
/** 优享版单文件收集(列表 fileSize 单位 KB,导出统一换算为字节) */
async function ilanzouCollectFile(it, prefix, files, warnMissing) {
const name = String(it.fileName || it.file_name || it.name || '');
if (!it.fileId) { warnMissing.push(prefix + name + ':无 fileId'); return; }
const kb = Number(it.fileSize || it.size || 0);
files.push({
path: prefix + name,
fileId: String(it.fileId),
size: kb > 0 ? kb * 1024 : 0,
folderId: String(it.folderId || ''),
md5: '',
sha1: '',
});
}
/** 优享版递归收集目录(file/list 分页) */
async function ilanzouWalkFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let offset = 1;
for (;;) {
let r;
try {
r = await ILANZOU.getFileList(folderId, offset);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
const list = r.list || [];
for (const it of list) {
const isDir = Number(it.fileType) === 2;
const name = String(it.fileName || '');
if (isDir) {
if (it.fileId) await ilanzouWalkFolder(String(it.fileId), prefix + name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await ilanzouCollectFile({ fileId: it.fileId, fileName: it.fileName, fileSize: it.fileSize, folderId: it.folderId }, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!r.hasMore) break;
offset++;
await sleep(CONFIG.listPageDelay);
}
}
/** 优享版导出:勾选 → 递归列表 → CDN Content-Md5 取 md5(零下载)→ JSON */
async function ilanzouGenerateJson() {
const c = ILANZOU.readCommon();
if (!c) { showToast('未检测到蓝奏云优享版登录态,请刷新页面', 'warning'); return; }
let sel;
try { sel = ILANZOU.getCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; }
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(蓝奏云优享版)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
md5 从优享版 CDN 响应头直接获取(零下载、快)。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-ilz-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info');
for (const item of sel.items) {
const name = String(item.fileName || item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (Number(item.fileType) === 2 && item.fileId) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await ilanzouWalkFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fileId) {
await ilanzouCollectFile(item, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, sel.items.length)) * 100), `获取文件信息 ${counter.done}/${sel.items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
const withMd5 = files.filter(f => f.fileId);
setStatus(statusEl, `正在从 CDN 获取 md5(${withMd5.length} 个文件,零下载)...`, 'info');
const pending = [];
for (let i = 0; i < withMd5.length; i++) {
const f = withMd5[i];
const md5 = await ILANZOU.getServerMd5(f.fileId);
if (md5 && /^[0-9a-f]{32}$/.test(md5)) f.md5 = md5;
else pending.push(f);
setStatus(statusEl, `获取 md5 ${i + 1}/${withMd5.length}:${f.path}`, 'info');
await sleep(60);
}
// 优享版仅支持 ≤512MB 零下载取 md5(分片大文件 CDN 无哈希头,不做下载计算)
if (pending.length) {
for (const f of pending) {
const bytes = Number(f.size) || 0;
if (bytes > 512 * 1024 * 1024) {
warnMissing.push(`${f.path}:超过 512MB 使用限制(${formatSize(bytes)}),优享版导出仅支持 ≤512MB 文件`);
} else {
warnMissing.push(`${f.path}:CDN 未返回 md5(≤512MB 但无哈希头,无法导出)`);
}
}
}
const finalFiles = files.filter(f => f.md5);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '蓝奏云优享版',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({
path: f.path,
md5: f.md5,
hashName: 'md5',
etag: f.md5,
size: Number(f.size) || 0,
})),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_优享版.json';
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(md5,零下载)${warnMissing.length ? `;${warnMissing.length} 项未获取到 md5` : ''}
优享版导出仅支持 ≤512MB 文件(零下载取 md5);md5 可直接导入小飞机/百度/夸克/UC 等支持 md5 秒传的平台;size 已换算为字节(与其他平台 JSON 一致)。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { const ta = document.createElement('textarea'); ta.value = jsonStr; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); showToast('秒传 JSON 已复制到剪贴板'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 优享版零下载校验:CDN Content-Md5 与期望 md5 是否一致;返回 '' 表示无法校验(不判幽灵) */
async function ilanzouVerifyMd5(fileId, expectMd5) {
try {
const real = await ILANZOU.getServerMd5(fileId);
return real ? String(real).toLowerCase() === String(expectMd5 || '').toLowerCase() : null;
} catch (e) { return null; }
}
/** 优享版导入(getUpToken 秒传 + CDN Content-Md5 幽灵校验,只认 md5;仅支持 ≤8MB 小文件) */
async function ilanzouImportJson(rawJson, onProgress) {
const c = ILANZOU.readCommon();
if (!c) throw new Error('未检测到蓝奏云优享版登录态,请刷新页面');
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
const skippedDetail = [];
const ILZ_IMPORT_MAX_BYTES = 8 * 1024 * 1024; // 优享版秒传池仅收录单请求直传的小文件(>8MB 分片不入池)
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const md5 = normalizeMd5(f.md5 || f.etag);
const size = Number(f.size != null ? f.size : 0);
if (!path) continue;
if (!md5) {
skippedDetail.push(`${path}:无 md5(优享版秒传只认 md5;阿里导出的仅含 sha1 无法导入)`);
continue;
}
if (size > ILZ_IMPORT_MAX_BYTES) {
skippedDetail.push(`${path}:超小文件导入限制(${formatSize(size)} > 8MB)。优享版秒传池只收录单请求直传的小文件,>8MB 分片文件无法秒传导入`);
continue;
}
rows.push({ path, md5, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的条目(优享版秒传需要 md5,32 位 hex,且文件需 ≤8MB)');
const rootParent = ILANZOU.getCurrentParentId();
let success = 0, fail = 0, skipped = 0;
const failures = [];
const warns = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `优享版秒传 ${i + 1}/${total}:${row.path}` });
try {
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootParent;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await ILANZOU.createDir(parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
const r = await ILANZOU.rapidUpload(fileName, row.md5, row.size, parent);
if (r.hit && r.fileId) {
// 幽灵校验:CDN Content-Md5 对比(零下载)
const verified = await ilanzouVerifyMd5(r.fileId, row.md5);
if (verified === null) {
success++;
warns.push(`${row.path}:已创建但无法校验(CDN 未返回 md5 头,请自行确认)`);
} else if (verified) {
success++;
} else {
try { await ILANZOU.deleteItems([], [r.fileId]); } catch (e) { /* ignore */ }
fail++;
failures.push(`${row.path}:优享版内容池无该 md5 内容(记录的 CDN md5 不一致,已清理)`);
}
} else if (r.hit) {
fail++;
failures.push(`${row.path}:getUpToken 返回命中但无 fileId`);
} else {
fail++;
failures.push(`${row.path}:优享版秒传池未收录该内容(仅服务器有记录的直传小文件可秒传;分片/大文件或非近期上传的内容不在池中,无法秒传导入)`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '优享版导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail, warns };
}
/** 优享版导入模态框 */
function showIlanzouImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到蓝奏云优享版',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位、path、size)。
优享版秒传只认 md5,且仅支持 ≤8MB 小文件(秒传池只收录单请求直传的小文件,>8MB 分片文件无法秒传导入,超出将报错跳过)。夸克/UC/百度/光鸭/蓝奏云/小飞机导出的 JSON(含 md5)可直接秒传;阿里云盘导出的(仅 sha1)将被跳过并提示。
path 中的目录会自动创建;导入后脚本会零下载校验 CDN md5,内容池不存在的会清理并报未命中。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-ilz-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-ilz-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-ilz-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-ilz-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await ilanzouImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.warns && summary.warns.length) { detailLines.push('', '========== 提示(无法校验) =========='); detailLines.push(...summary.warns); }
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0 || (summary.warns && summary.warns.length > 0);
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 城通网盘平台(v1.8:drive.ctfile.com 标准版 v4 API,实测 2026-10) ==============================
// 实测逆向结论:
// - 凭证:sessionStorage['ctfile_basic_token'](Bearer,15 分钟过期);刷新 POST /v4/user/auth/refresh-token(body {},凭 cookie 会话)
// - 工作区:POST /v4/workspace/manage/list → workspaces[];路由 #/public=共享(is_private 0)、#/private=私密(is_private 1,超级会员)
// - 列表:POST /v4/file/browse/list {workspace_id, folder_id, orderby:'new', filter:'all', keyword:'', start, pagesize:100}
// 文件 fid 'f...'、size、file_real_name="{checksum}-{size}";目录 fid 'd...'、ext='folder'、size=0
// 接口 folder_id 参数 = fid 去掉 'd'/'f' 前缀(根为 '0')
// - checksum 算法:md5(文件前 1,000,000 字节)(前端 Ua(): FileReader.readAsArrayBuffer(file.slice(0,1e6)) → SparkMD5.ArrayBuffer)
// ≤1MB 文件:checksum == 完整内容 MD5(file_real_name 可直接作 etag)
// >1MB 文件:checksum ≠ 完整 MD5(不下载计算完整 MD5;ctChecksum 零下载、供城通→城通秒传)
// - 建目录:POST /v4/file/manage/create-folder {workspace_id, folder_id, name} → data.folder_id:"d..."
// - 秒传:POST /v4/file/manage/upload {workspace_id, folder_id, path, size, checksum, carrier:0}
// data.exists===1 / instant_upload===true → 「文件已存在,秒传成功」直接入库(零上传,实测验证)
// 实测池收录规则:按 (checksum=md5前1MB, size) 收录网页上传的 ≥约10MB 文件(即时收录无延迟);
// 8MB 以下不入池;完整 MD5 永远对不上池键(16/32/64MB 用完整 md5 查询全部 exists:0)
// - 删除:POST /v4/file/manage/delete {workspace_id, ids:"fid1,fid2"}(ids 为逗号分隔字符串,实测数组会 500)
const CTFILE = {
API_BASE: 'https://api.ctfile.com',
CHECKSUM_SLICE: 1000000, // 前端只哈希文件前 100 万字节
/** Bearer token:sessionStorage ctfile_basic_token */
getToken() {
try {
const t = String(sessionStorage.getItem('ctfile_basic_token') || '').trim();
if (t) return t;
} catch (e) { /* ignore */ }
return '';
},
/** 刷新 Bearer token(cookie 会话有效时直接换新 token) */
async refreshToken() {
const j = await CTFILE.request('/v4/user/auth/refresh-token', {}, { noToken: true, autoRetry: false });
const t = j && j.data && j.data.token;
if (t) {
try { sessionStorage.setItem('ctfile_basic_token', t); } catch (e) { /* ignore */ }
}
return t || '';
},
/** POST JSON 请求(同源 fetch,cookie 会话 + Bearer;401/403 token 类错误自动刷新重试一次) */
async request(path, body, opts = {}) {
const headers = { 'Content-Type': 'application/json', 'Origin': 'https://drive.ctfile.com', 'Referer': 'https://drive.ctfile.com/' };
if (!opts.noToken) {
let token = CTFILE.getToken();
if (!token) token = await CTFILE.refreshToken();
if (token) headers['Authorization'] = 'Bearer ' + token;
}
let resp;
try {
resp = await fetch(CTFILE.API_BASE + path, { method: 'POST', headers, credentials: 'include', body: JSON.stringify(body || {}) });
} catch (e) {
throw new Error('城通网络请求失败: ' + e.message);
}
let j;
try { j = await resp.json(); } catch (e) { throw new Error('城通 HTTP ' + resp.status + ' 响应解析失败'); }
const badAuth = resp.status === 401 || (resp.status === 403 && j && /token|auth|session|login/i.test(String(j.message || '')));
if (resp.status < 200 || resp.status >= 300 || (j && j.code && j.code !== 200)) {
if (!opts.noToken && opts.autoRetry !== false && !opts.retried && badAuth) {
const t2 = await CTFILE.refreshToken();
if (t2) return CTFILE.request(path, body, Object.assign({}, opts, { retried: true }));
}
const err = new Error((j && j.message) || ('城通接口错误 code=' + (j && j.code) + ' HTTP ' + resp.status));
err.code = j && j.code;
throw err;
}
return j;
},
/** 工作区列表 → {shared, private}(is_private 0=共享空间,1=私密空间) */
async getWorkspaces() {
const j = await CTFILE.request('/v4/workspace/manage/list', {});
const list = Array.isArray(j.data && j.data.workspaces) ? j.data.workspaces : [];
return {
shared: list.find(w => Number(w.is_private) === 0) || null,
private: list.find(w => Number(w.is_private) === 1) || null,
list,
};
},
/** 当前路由对应的工作区 id(#/public→共享、#/private→私密) */
async getWorkspaceId() {
const ws = await CTFILE.getWorkspaces();
const priv = /#\/private/.test(location.hash || '');
const w = priv ? ws.private : ws.shared;
if (!w) throw new Error('未获取到城通工作区' + (priv ? '(私密空间为超级会员功能,免费账号不可用)' : ',请确认已登录并刷新页面'));
return String(w.id);
},
/** 当前目录 folder_id(路由 #/public/{id} 或 #/private/{id};根为 '0') */
getCurrentFolderId() {
try {
const m = (location.hash || '').match(/(?:\/public|\/private)\/(\d+)/);
return m ? m[1] : '0';
} catch (e) { return '0'; }
},
/** folder_id 规范化:fid('d...'/'f...') → 纯数字(接口参数);根 → '0' */
normFolderId(folderId) {
const s = String(folderId == null || folderId === '' ? '0' : folderId).trim();
return s === '0' || /^\d+$/.test(s) ? s : s.replace(/^[df]/, '');
},
/** 文件列表(分页):返回 {list, total};条目为原始结果 */
async getFileList(workspaceId, folderId, start) {
const j = await CTFILE.request('/v4/file/browse/list', {
workspace_id: Number(workspaceId || 0),
folder_id: CTFILE.normFolderId(folderId),
orderby: 'new', filter: 'all', keyword: '',
start: Number(start || 0), pagesize: 100,
});
const d = (j && j.data) || {};
return {
list: Array.isArray(d.results) ? d.results : [],
total: Number(d.totalNum || 0),
};
},
/** 从列表条目取 checksum(file_real_name="{checksum}-{size}")+ size */
parseItemMeta(it) {
const m = String(it.file_real_name || '').match(/^([0-9a-fA-F]{32})-(\d+)$/);
if (m) return { checksum: m[1].toLowerCase(), size: Number(m[2]) };
return { checksum: '', size: Number(it.size || 0) };
},
/** 建目录:返回 folder_id('d...',用于后续列表需去掉前缀) */
async createFolder(workspaceId, folderId, name) {
const j = await CTFILE.request('/v4/file/manage/create-folder', {
workspace_id: Number(workspaceId || 0),
folder_id: CTFILE.normFolderId(folderId),
name: String(name),
});
const id = j && j.data && j.data.folder_id;
if (!id) throw new Error('城通创建目录未返回 ID');
return String(id);
},
/** 秒传(导入):checksum 用 ctChecksum 优先、否则完整 md5;返回 {instant, exists, uploadUrl, fileId, name} */
async uploadInit(workspaceId, folderId, name, size, checksum) {
const j = await CTFILE.request('/v4/file/manage/upload', {
workspace_id: Number(workspaceId || 0),
folder_id: CTFILE.normFolderId(folderId),
path: String(name),
size: Number(size || 0),
checksum: String(checksum || '').toLowerCase(),
carrier: 0,
});
const d = (j && j.data) || {};
return {
instant: Number(d.exists) === 1 || d.instant_upload === true,
exists: Number(d.exists || 0),
uploadUrl: String(d.uploadUrl || ''),
fileId: String(d.file_id || ''),
name: String(d.file_name || name || ''),
};
},
/** 删除(ids 为逗号分隔字符串) */
async deleteItems(workspaceId, idsStr) {
const j = await CTFILE.request('/v4/file/manage/delete', { workspace_id: Number(workspaceId || 0), ids: String(idsStr || '') });
return j;
},
};
// ---- 城通导出(勾选 → 递归列表 → file_real_name 取 checksum;≤1MB 即完整 md5,>1MB 仅 ctChecksum)----
/** 城通行文件名提取:.ct-entry-name > .ct-entry-text > button.ct-entry(去掉行内 svg 类型徽标文本) */
function ctfileRowName(row) {
const el = row.querySelector('.ct-cell-name .ct-entry-name') ||
row.querySelector('.ct-cell-name .ct-entry-text') ||
row.querySelector('.ct-cell-name button.ct-entry');
if (!el) return '';
let t = (el.textContent || '').replace(/\s+/g, ' ').trim();
// 兜底(仅 button 路径):去掉文件名前的类型徽标(如 BIN/TXT/MP4 等大写短词)
if (!row.querySelector('.ct-cell-name .ct-entry-name, .ct-cell-name .ct-entry-text')) {
t = t.replace(/^[A-Z0-9]{1,8}(?=.)/, '');
}
return t;
}
/** 城通勾选读取:行复选框(.ct-row:not(.is-head) input.ct-check)+ 表头全选 */
function ctfileGetCheckedItems() {
const names = [];
document.querySelectorAll('.ct-row:not(.is-head)').forEach(row => {
const cb = row.querySelector('input.ct-check');
if (cb && cb.checked) {
const name = ctfileRowName(row);
if (name) names.push(name);
}
});
const headCb = document.querySelector('.ct-row.is-head input.ct-check');
const all = !!(headCb && headCb.checked);
return { names, all };
}
/** 按名称匹配当前目录 API 列表 → 勾选项条目(含 fid/ext/size/file_real_name) */
async function ctfileResolveCheckedItems(workspaceId, folderId, names, all) {
const items = [];
let start = 0;
for (;;) {
const r = await CTFILE.getFileList(workspaceId, folderId, start);
const list = r.list || [];
for (const it of list) {
const nm = String(it.name || '');
if (all || names.includes(nm)) items.push(it);
}
if (!list.length || start + list.length >= r.total) break;
start += list.length;
}
return items;
}
/** 城通单文件收集 */
async function ctfileCollectFile(it, prefix, files, warnMissing) {
const name = String(it.name || '');
const meta = CTFILE.parseItemMeta(it);
if (meta.checksum) {
files.push({
path: prefix + name,
fileId: String(it.fid || ''),
size: meta.size || Number(it.size || 0),
ctChecksum: meta.checksum, // md5(前1MB),零下载;城通→城通秒传键
etag: '', // 完整内容 MD5(仅 ≤1MB:ctChecksum 即完整 MD5,由主流程补齐)
});
} else {
warnMissing.push(prefix + name + ':无 file_real_name,无法取哈希');
}
}
/** 城通递归收集目录(browse/list 分页) */
async function ctfileWalkFolder(workspaceId, folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let start = 0;
for (;;) {
let r;
try {
r = await CTFILE.getFileList(workspaceId, folderId, start);
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
const list = r.list || [];
for (const it of list) {
const isDir = String(it.ext) === 'folder';
const name = String(it.name || '');
if (isDir) {
if (it.fid) await ctfileWalkFolder(workspaceId, it.fid, prefix + name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await ctfileCollectFile(it, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!list.length || start + list.length >= r.total) break;
start += list.length;
await sleep(CONFIG.listPageDelay);
}
}
/** 城通导出主流程 */
async function ctfileGenerateJson() {
const workspaceId = await CTFILE.getWorkspaceId();
const folderId = CTFILE.getCurrentFolderId();
const sel = ctfileGetCheckedItems();
if (!sel.names.length && !sel.all) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(城通网盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
城通哈希 = md5(文件前1MB):≤1MB 即完整内容 MD5(同时带 etag,可导入其他盘);>1MB 仅 ctChecksum(零下载),可直接导入城通秒传。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-ct-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
const counter = { done: 0 };
try {
setStatus(statusEl, `正在匹配勾选项(当前目录 ${folderId === '0' ? '根目录' : folderId})...`, 'info');
const items = await ctfileResolveCheckedItems(workspaceId, folderId, sel.names, sel.all);
if (!items.length) {
setStatus(statusEl, '未能匹配到勾选条目,请刷新页面重试', 'err');
modal.footerEl.innerHTML = ``;
return;
}
setStatus(statusEl, sel.all ? `已全选当前目录 ${items.length} 项,正在收集信息...` : `正在收集 ${items.length} 项...`, 'info');
for (const item of items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (String(item.ext) === 'folder' && item.fid) {
setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await ctfileWalkFolder(workspaceId, item.fid, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fid) {
await ctfileCollectFile(item, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, items.length)) * 100), `获取文件信息 ${counter.done}/${items.length}:${name}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':未能匹配到文件ID');
}
}
// ≤1MB:ctChecksum 即完整内容 MD5,直接补 etag;>1MB 仅 ctChecksum(城通秒传专用键,不下载计算完整 MD5)
for (const f of files) {
if (f.size <= CTFILE.CHECKSUM_SLICE) {
f.etag = f.ctChecksum;
}
}
const finalFiles = files.filter(f => f.ctChecksum);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何文件的哈希,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '城通网盘',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => {
const o = { path: f.path, size: Number(f.size) || 0 };
if (f.etag) { o.md5 = f.etag; o.hashName = 'md5'; o.etag = f.etag; }
o.ctChecksum = f.ctChecksum; // 城通秒传键:md5(前1MB)
return o;
}),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_城通.json';
const bigMissing = finalFiles.filter(f => f.size > CTFILE.CHECKSUM_SLICE && !f.etag).length;
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON(ctChecksum 零下载)${warnMissing.length ? `;${warnMissing.length} 项异常` : ''}
${bigMissing ? `${bigMissing} 项 >1MB 仅含 ctChecksum(可导入城通秒传;不含完整 MD5,无法导入其他盘)` : '含完整 MD5(etag),可导入百度/夸克/UC/小飞机等支持 md5 的平台'}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 城通导入(/v4/file/manage/upload checksum 秒传;优先 ctChecksum,否则 etag/md5;未命中如实报告) */
async function ctfileImportJson(rawJson, onProgress) {
const workspaceId = await CTFILE.getWorkspaceId();
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
const skippedDetail = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const md5 = normalizeMd5(f.md5 || f.etag);
const ctChecksum = normalizeMd5(f.ctChecksum);
const size = Number(f.size != null ? f.size : 0);
if (!path) continue;
if (!md5 && !ctChecksum) {
skippedDetail.push(`${path}:无 md5/ctChecksum(城通秒传需要 md5 或 ctChecksum;阿里导出的仅含 sha1 无法导入)`);
continue;
}
rows.push({ path, md5, ctChecksum, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的条目(城通秒传需要 md5/ctChecksum)');
const rootFolder = CTFILE.getCurrentFolderId();
let success = 0, fail = 0, skipped = 0;
const failures = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `城通秒传 ${i + 1}/${total}:${row.path}` });
try {
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootFolder;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dirId = await CTFILE.createFolder(workspaceId, parent, dirName);
dirCache.set(key, dirId);
parent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
// 秒传:优先 ctChecksum(城通导出的前1MB哈希),否则完整 md5(仅 ≤1MB 文件与池键一致)
const checksum = row.ctChecksum || row.md5;
const r = await CTFILE.uploadInit(workspaceId, parent, fileName, row.size, checksum);
if (r.instant) {
success++;
} else {
fail++;
failures.push(`${row.path}:城通秒传池未命中(池按 md5(前1MB) 收录 ≥约10MB 的已上传文件;小文件/未收录内容需真实上传,JSON 不含文件本体无法上传)`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '城通导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 城通导入模态框 */
function showCtfileImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到城通网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5/etag=32 位完整 MD5、ctChecksum=城通前1MB 哈希、path、size)。
城通秒传优先 ctChecksum(城通导出的 JSON 自带),其次 md5。实测池按 md5(前1MB) 收录 ≥约10MB 的已上传文件——城通导出的大文件(ctChecksum)可秒传;<1MB 或新内容一般未命中。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-ct-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-ct-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-ct-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-ct-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await ctfileImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 md5/ctChecksum) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 阿里云盘导出/导入(v1.1) ==============================
// 设计要点:JSON 保持轻量(只含 sha1/size/path/fileId,不内嵌文件字节,可直接发社交平台);
// 验证块(proof 8 字节)经夸克/UC 中转获取(或同账号直取)——导入者用自己 token 算 offset 后取块,零服务器依赖。
/** 阿里云盘导出:勾选 → 列表(含 content_hash=SHA1) → 轻量 JSON(同账号直取/夸克 UC 中转取块) */
async function aliyunGenerateJson() {
const t = ALIYUN.getToken();
if (!t) { showToast('未检测到阿里云盘登录态,请刷新页面', 'warning'); return; }
const driveId = await ALIYUN.getDriveId();
if (!driveId) { showToast('未能获取阿里云盘 drive_id,请刷新页面', 'warning'); return; }
const sel = ALIYUN.getCheckedItems();
let items = sel.items.slice();
if (sel.all) {
showToast('检测到全选,正在拉取当前目录全部条目...', 'info', 2000);
const parentId = ALIYUN.getCurrentParentId();
try {
let marker = '';
for (;;) {
const r = await ALIYUN.getFileList(driveId, parentId, marker);
for (const it of r.list) {
items.push(ALIYUN.normalizeListItem(it));
}
if (!r.nextMarker) break;
marker = r.nextMarker;
await sleep(CONFIG.listPageDelay);
}
} catch (e) {
showToast(`拉取目录失败:${e.message}`, 'error');
return;
}
}
// 去重
const seen = new Set();
items = items.filter(it => { const k = String(it.fileId || it.name); if (!k || seen.has(k)) return false; seen.add(k); return true; });
if (!items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(阿里云盘)',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-al-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
const counter = { done: 0 };
const total = items.length;
try {
setStatus(statusEl, `正在获取 ${items.length} 项的文件信息...`, 'info');
for (const it of items) {
if (it.type === 'folder') {
setStatus(statusEl, `正在扫描文件夹:${it.name}...`, 'info');
await aliyunWalkFolder(driveId, it.fileId, it.name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await aliyunCollectFile(driveId, it, '', files, warnMissing);
counter.done++;
updateProgress(barEl, Math.round((counter.done / Math.max(1, total)) * 100), `获取文件信息 ${counter.done}/${total}:${it.name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!files.length) {
setStatus(statusEl, '未获取到任何文件的 SHA1 哈希,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '阿里云盘',
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files,
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_阿里云盘.json';
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(SHA1)${warnMissing.length ? `;${warnMissing.length} 项未获取到 SHA1` : ''}
JSON 含 fileId:同账号导入可直接秒传;跨盘导入在阿里导入框粘贴夸克/UC Cookie 即可(不需云服务器)。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr; document.body.appendChild(ta); ta.select();
document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
/** 阿里云盘单文件收集:content_hash(SHA1) + size + fileId → 轻量条目 */
async function aliyunCollectFile(driveId, it, prefix, files, warnMissing) {
const sha1 = String(it.contentHash || it.content_hash || '').toUpperCase();
const name = String(it.name || '');
if (sha1 && /^[0-9A-F]{40}$/.test(sha1)) {
files.push({
path: prefix + name,
sha1: sha1.toLowerCase(),
hashName: 'sha1',
etag: '',
size: Number(it.size || 0),
driveId: String(it.driveId || driveId || ''),
fileId: String(it.fileId || ''),
});
} else {
warnMissing.push(prefix + name + ':未能取得合法 SHA1(可能仍在转码/未完成)');
}
}
/** 阿里云盘递归收集目录下所有文件(v3 list 分页) */
async function aliyunWalkFolder(driveId, folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
let marker = '';
for (;;) {
let list, next;
try {
const r = await ALIYUN.getFileList(driveId, folderId, marker);
list = r.list;
next = r.nextMarker;
} catch (e) {
setStatus(statusEl, `列出目录失败:${prefix}(${e.message})`, 'err');
return;
}
for (const item of list) {
const it = ALIYUN.normalizeListItem(item);
if (it.type === 'folder') {
if (it.fileId) await aliyunWalkFolder(driveId, it.fileId, prefix + it.name + '/', files, warnMissing, statusEl, barEl, counter);
} else {
await aliyunCollectFile(driveId, it, prefix, files, warnMissing);
counter.done++;
if (statusEl) setStatus(statusEl, `正在扫描:${prefix}(已收集 ${files.length} 个文件)`, 'info');
if (barEl) updateProgress(barEl, 0, `已收集 ${files.length} 个文件:${it.name}`);
await sleep(CONFIG.md5Delay);
}
}
if (!next) break;
marker = next;
await sleep(CONFIG.listPageDelay);
}
}
/**
* 阿里导入时经「夸克/UC 中转」取 proof 块(用户提供夸克或 UC 的完整 Cookie):
* 1) 用 sha1(全局键)调夸克/UC upload/pre 秒传 → 命中返回 fid(不下载原文件)
* 2) 用 fid 调 download 拿 download_url → Range 取 [offset, offset+8) 8 字节 = proof
* 这样阿里导入完全不需要云服务器,跨账号秒传由夸克/UC 的 PDS 全局去重兜底。
* cookie 决定走夸克还是 UC(按 hostname 判断)。
*/
/**
* 夸克/UC(阿里云 PDS 生态)通用中转取字节:先把文件(按 sha1 全局键)丢给夸克/UC 秒传
* (夸克/UC 秒传宽松,只需 sha1+size 即命中,内容在全库即登记成功)→ download 拿临时直链 →
* Range 取 [start,end] 任意范围字节。115 二次验证的 sign_check 范围即用此中转;
* 所有底层用阿里云的网盘(夸克/UC 等)都可当此"sha1 登记中转站"。
* cookie 前缀 QK:/UC: 区分平台;直链只在内存、用完即弃,全程不产生分享链接。
*/
async function pdsProxyFetchRange(cookie, sha1, size, start, end) {
if (!cookie) throw new Error('未提供夸克/UC Cookie');
// 平台判定:前缀 QK:/UC: 显式区分(cookie 内无法可靠区分夸克/UC)
let platform = 'quark';
if (/^UC:/i.test(cookie)) { platform = 'uc'; cookie = cookie.replace(/^UC:/i, '').trim(); }
else if (/^QK:/i.test(cookie)) { platform = 'quark'; cookie = cookie.replace(/^QK:/i, '').trim(); }
const host = platform === 'uc' ? 'https://pc-api.uc.cn' : 'https://drive-pc.quark.cn';
const pr = platform === 'uc' ? 'UCBrowser' : 'ucpro';
const referer = platform === 'uc' ? 'https://drive.uc.cn/' : 'https://pan.quark.cn/';
const ua = navigator.userAgent;
// 统一请求(夸克/UC API 跨域 → 必须 GM_xmlhttpRequest,页面 fetch 会被 CORS 拦截)
const gmReq = (opts) => new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('无 GM_xmlhttpRequest')); return; }
GM_xmlhttpRequest(Object.assign({
timeout: 60000,
withCredentials: true,
onload: (r) => resolve(r),
onerror: () => reject(new Error('网络错误')),
ontimeout: () => reject(new Error('请求超时')),
}, opts));
});
// 1) + 2) 秒传 → 取 fid → download 拿直链(可重试单元):
// 实测:秒传响应必有 fid(finish:true + upload_id:rapid-*),download 按 fid 可取直链;
// 但偶发「file not found」(秒传新建文件尚未就绪/被清理)→ 用新文件名重新秒传换新 fid 重试。
// 文件名带随机后缀,避免同名 auto_rename 干扰。
const doPre = async (name) => {
const body = {
pdir_fid: '0', dir_name: '', size: Number(size) || 0,
file_name: name,
format_type: 'application/octet-stream',
l_updated_at: Date.now(), l_created_at: Date.now(),
ccp_hash_update: true, parallel_upload: true,
sha1: String(sha1).toLowerCase(),
};
const resp = await gmReq({
method: 'POST',
url: host + '/1/clouddrive/file/upload/pre?pr=' + pr + '&fr=pc',
headers: { 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer.replace(/\/$/, ''), 'Cookie': cookie, 'User-Agent': ua },
data: JSON.stringify(body),
});
const j = JSON.parse(resp.responseText || '{}');
if (j.code !== 0 && j.code !== 200 && !j.data) throw new Error('夸克/UC 秒传失败: ' + (j.message || ('code ' + j.code)));
return j.data || {};
};
const doDl = async (fidNow) => {
const resp = await gmReq({
method: 'POST',
url: host + '/1/clouddrive/file/download?pr=' + pr + '&fr=pc',
headers: { 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer.replace(/\/$/, ''), 'Cookie': cookie, 'User-Agent': ua },
data: JSON.stringify({ fids: [fidNow] }),
});
const j = JSON.parse(resp.responseText || '{}');
if (j.code !== undefined && j.code !== 0 && j.code !== 200) {
return { code: j.code, message: String(j.message || '') };
}
const arr = Array.isArray(j.data) ? j.data : [];
if (!arr.length) return { code: 21001, message: 'empty data' };
const dd = arr[0] || {};
return { code: 0, url: dd.download_url || '', message: '' };
};
let url = '';
let lastPreErr = '';
for (let attempt = 1; attempt <= 3 && !url; attempt++) {
const tmpName = 'proof_proxy_' + String(sha1).slice(0, 8) + '_' + Date.now().toString(36) + '.bin';
let pd;
try { pd = await doPre(tmpName); }
catch (e) { lastPreErr = e.message; await sleep(500); continue; }
const fid = String(pd.fid || pd.file_id || '');
if (!fid) { lastPreErr = '秒传未返回 fid(finish=' + pd.finish + ')'; await sleep(500); continue; }
let dl = await doDl(fid);
if (dl.code !== 0) {
if (dl.code === 21001 || dl.code === 30107 || dl.message === 'empty data') {
lastPreErr = dl.message;
await sleep(600);
continue;
}
if (/login|auth|token|31001|401/i.test(dl.message)) {
throw new Error('夸克/UC Cookie 无效或已过期(download 接口拒绝:' + dl.message + '),请重新获取 Cookie 后重试');
}
throw new Error('夸克/UC download 接口错误: ' + dl.message + '(code ' + dl.code + ')');
}
if (!dl.url) { lastPreErr = 'download 未返回直链'; await sleep(600); continue; }
url = dl.url;
}
if (!url) {
throw new Error('夸克/UC 中转取字节失败:' + (lastPreErr || '未知错误') + ' —— 请确认 Cookie 有效且内容在夸克/UC 池中后重试');
}
// 3) Range 取 [start, end] 字节
// 关键:夸克/UC 的 OSS 直链(Tengine)拒绝 XMLHttpRequest 请求(一律 412 Precondition Failed),
// 只接受 fetch 发起的请求(实测 fetch 206 vs XHR 412,与 cookie/头无关)。
// 因此这里优先用页面 fetch(CDN 下载链接通常开放 CORS,跨域可访问);若被 CORS 拦截
// 再回退 GM_xmlhttpRequest(部分环境可通)。
const rEnd = Math.min(Number(end), Math.max(Number(size) - 1, Number(start)));
let rng = null;
try {
const fr = await fetch(String(url), {
method: 'GET', credentials: 'include',
headers: { 'Range': `bytes=${start}-${rEnd}`, 'Referer': referer },
});
if (fr.status >= 200 && fr.status < 300) {
const ab = await fr.arrayBuffer();
return new Uint8Array(ab);
}
} catch (e) { /* 回退 GM */ }
try {
rng = await gmReq({
method: 'GET',
url: String(url),
headers: { 'Range': `bytes=${start}-${rEnd}`, 'Referer': referer },
responseType: 'arraybuffer',
});
} catch (e) { throw new Error('夸克/UC 直链下载失败: ' + e.message); }
if (rng.status < 200 || rng.status >= 300) throw new Error('夸克/UC 直链下载 HTTP ' + rng.status);
const raw = rng.response;
let bytes;
if (raw instanceof ArrayBuffer) bytes = new Uint8Array(raw);
else if (Array.isArray(raw)) bytes = new Uint8Array(raw);
else if (raw && raw.byteLength != null) bytes = new Uint8Array(raw);
else if (typeof raw === 'string') bytes = new Uint8Array(raw.split('').map(c => c.charCodeAt(0) & 0xFF));
else throw new Error('夸克/UC 直链下载返回类型异常');
return bytes;
}
/** 兼容旧调用:取 8 字节 proof 块(阿里导入用;固定 [offset, offset+7]) */
async function pdsProxyFetchProof(cookie, sha1, size, offset) {
return pdsProxyFetchRange(cookie, sha1, size, offset, Math.min(offset + 7, Math.max(Number(size) - 1, offset)));
}
function pdsCookieAutoFetch() {
if (!(isQuark() || isUc())) return;
let pending = '';
try { pending = String(GM_getValue('__pds_cookie_pending', '') || ''); } catch (e) { /* ignore */ }
if (!pending) return;
// 清 pending(一次性),无论成功失败
try { GM_setValue('__pds_cookie_pending', ''); } catch (e) { /* ignore */ }
const platform = pending === 'uc' ? 'uc' : 'quark';
if ((platform === 'uc' && !isUc()) || (platform === 'quark' && !isQuark())) return; // 开错了站
const take = () => {
const isLoggedIn = (() => {
try {
// 登录态判断:关键 cookie 存在(b-user-id/ctoken/__puus),且尝试取列表成功
const ck = document.cookie || '';
if (!/b-user-id=/.test(ck) && !/ctoken=/.test(ck) && !/__puus=/.test(ck)) return false;
return true;
} catch (e) { return false; }
})();
if (!isLoggedIn) {
showToast('未登录:请先在' + (platform === 'uc' ? 'UC网盘' : '夸克网盘') + '登录,登录后会自动获取 Cookie', 'warning', 6000);
return;
}
// 已登录:取 document.cookie(夸克/UC 的关键会话 cookie 均为非 HttpOnly,前实测可见)
const cookieStr = (document.cookie || '').trim();
if (!cookieStr) { showToast('未获取到 Cookie,请刷新页面后重试', 'warning'); return; }
const prefix = platform === 'uc' ? 'UC:' : 'QK:';
try { GM_setValue('__pds_cookie_result', prefix + cookieStr); } catch (e) { /* ignore */ }
showToast('Cookie 已获取,正在返回阿里云盘导入页...', 'ok', 4000);
// 尝试自动切回:本标签由 window.open 打开且非用户主动 → 可尝试 close;失败则提示
setTimeout(() => {
try {
if (window.opener && !window.opener.closed) {
window.close();
}
} catch (e) { /* ignore */ }
}, 1200);
};
// 稍等页面就绪(SPA 加载后 cookie 完整)
setTimeout(take, 1200);
}
/** 阿里云盘导入(证明块经夸克/UC 中转或同账号直取,createWithFolders rapid_upload) */
async function aliyunImportJson(rawJson, opts, onProgress) {
const proxyCookie = opts && opts.proxyCookie ? String(opts.proxyCookie).trim() : '';
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
// 优先 sha1(hashName=sha1 或 sha1 字段);兼容阿里导出的轻量 JSON。
// 注意:sha1 是 40 位 hex,不能走 MD5 专用的 normalizeMd5(会把 sha1 清空)
const rawSha1 = String(f.sha1 || '').trim();
const rawEtag = String(f.etag || '').trim();
let sha1 = /^[0-9a-f]{40}$/i.test(rawSha1) ? rawSha1.toLowerCase() : '';
if (!sha1 && String(f.hashName || '') === 'sha1' && /^[0-9a-f]{40}$/i.test(rawEtag)) {
sha1 = rawEtag.toLowerCase();
}
const size = Number(f.size != null ? f.size : 0);
if (!path || !/^[0-9a-f]{40}$/.test(sha1)) continue;
rows.push({ path, sha1: sha1.toUpperCase(), size, fileId: String(f.fileId || f.file_id || '').trim() });
}
if (!rows.length) throw new Error('JSON 中没有可导入的文件条目(需要 sha1 + hashName=sha1 或 sha1 字段)');
const t = ALIYUN.getToken();
if (!t) throw new Error('未检测到阿里云盘登录态,请刷新页面');
const driveId = await ALIYUN.getDriveId();
if (!driveId) throw new Error('未能获取阿里云盘 drive_id,请刷新页面');
const currentParent = ALIYUN.getCurrentParentId();
const accessToken = t.access_token;
let success = 0, fail = 0, skipped = 0;
const failures = [];
const skippedDetail = [];
const total = rows.length;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `阿里秒传 ${i + 1}/${total}:${row.path}` });
try {
const size = row.size > 0 ? row.size : 0;
// 1) 导入者用自己的 token 算 offset(与创建请求的 Authorization 一致)
const offset = ALIYUN.computeProofOffset(accessToken, size);
// 2) 取 8 字节 proof 块(零服务器依赖):
// a) JSON 自带 fileId(阿里同账号导出的 JSON)→ 直接从自己网盘取块
// b) 否则 → 经夸克/UC 中转(用户提供 cookie,先把文件按 sha1 秒传到夸克/UC 再取块)
let proofBytes = null;
let proofSource = '';
if (row.fileId) {
try {
proofBytes = await ALIYUN.downloadProofRange(driveId, row.fileId, offset);
proofSource = '同账号直取';
} catch (e) { proofBytes = null; }
}
if (!proofBytes || !proofBytes.length) {
if (proxyCookie) {
try {
proofBytes = await pdsProxyFetchProof(proxyCookie, row.sha1, size, offset);
proofSource = '夸克/UC中转';
} catch (e) {
proofBytes = null;
skippedDetail.push(`${row.path}:夸克/UC 中转取块失败(${e.message})`);
skipped++;
continue;
}
}
}
if (!proofBytes || !proofBytes.length) {
skipped++;
skippedDetail.push(`${row.path}:无法获取 proof 块。阿里同账号导入请导出的 JSON 需带 fileId;跨盘导入请在输入框粘贴夸克或 UC 的完整 Cookie(夸克/UC 中转取块,不需服务器)。`);
continue;
}
const proof = ALIYUN.proofFromBytes(proofBytes);
// 3) createWithFolders 快速上传(name 含 "/" 时自动创建父目录,实测支持)
const r = await ALIYUN.rapidUpload(driveId, currentParent, row.path, size, row.sha1, proof);
if (r.rapid) {
success++;
} else {
fail++;
failures.push(`${row.path}:阿里云盘未命中快速上传(rapid_upload=false),服务器上不存在该 sha1 内容(proof 来源:${proofSource || '无'})`);
}
} catch (e) {
// 特判:proof 无效 / 需要 proof
const msg = String(e.message || '');
if (msg.includes('InvalidRapidProof')) {
fail++;
failures.push(`${row.path}:proof 校验失败(取到的验证块与该文件不匹配,或 offset 算法需更新)`);
} else if (msg.includes('RapidProofNeed')) {
fail++;
failures.push(`${row.path}:缺少 proof 验证块(未能获取验证块)`);
} else {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('import', '阿里导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('import', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 阿里云盘导入模态框 */
function showAliyunImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到阿里云盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、hashName=sha1、path、size)。
proof 块经夸克/UC 中转获取(不需云服务器):阿里导入时填入夸克或 UC 网的完整 Cookie,脚本先把文件按 sha1 秒传到夸克/UC,再从云端取 proof 字节;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
夸克/UC Cookie:
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-al-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-al-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-al-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-al-import-detail');
const cookieEl = modal.contentEl.querySelector('#gy-ms-al-proxy-cookie');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
// ========== 一键获取夸克/UC Cookie(跨标签通道) ==========
const qkBtn = modal.contentEl.querySelector('#gy-ms-al-proxy-qk');
const ucBtn = modal.contentEl.querySelector('#gy-ms-al-proxy-uc');
let pendingTimer = null;
function startPendingPoll() {
if (pendingTimer) return;
pendingTimer = setInterval(() => {
let result = '';
try { result = GM_getValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
if (result) {
try { GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
cookieEl.value = result;
setStatus(statusEl, '已自动填入 ' + (result.indexOf('QK:') === 0 ? '夸克' : 'UC') + ' Cookie,可以开始导入。', 'ok');
clearInterval(pendingTimer);
pendingTimer = null;
}
}, 500);
}
qkBtn.onclick = () => {
try { GM_setValue('__pds_cookie_pending', 'quark'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
showToast('正在打开夸克网盘获取 Cookie...', 'info');
window.open('https://pan.quark.cn/', '_blank');
startPendingPoll();
};
ucBtn.onclick = () => {
try { GM_setValue('__pds_cookie_pending', 'uc'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
showToast('正在打开 UC 网盘获取 Cookie...', 'info');
window.open('https://drive.uc.cn/', '_blank');
startPendingPoll();
};
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await aliyunImportJson(ta.value, {
proxyCookie: cookieEl ? cookieEl.value.trim() : '',
}, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
function showBaiduImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到百度网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=32 位 MD5、contentMd5=加密 MD5、path、size)。
导入优先经云服务器取验证块;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-bd-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-bd-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-bd-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-bd-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await baiduImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
// 导出日志按钮(本地,便于排查)
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
/** 首启提示:云令牌授权(仅导出 / 继续) */
function showCloudConsentModal() {
const modal = openModal({
title: '百度网盘秒传 - 云令牌授权',
icon: ICONS.settings,
width: 560,
content(el) {
el.innerHTML = `
百度网盘 23 年后加强了秒传验证,现在秒传需要一个绑定账号的随机值对应的一段原文件随机位置的 256KB 数据块。
我们默认提供一个云服务器来存储您的账号令牌,以用于
在不向其他用户暴露您账号真实令牌的情况下,为导入您秒传文件的用户提供这个随机数据块以实现秒传。
我们
承诺只会将令牌用于秒传的实现。
如果您
不想将这个令牌提供给我们以用于秒传,请选择「仅导出」;如果您愿意,请选择「继续」。
选择「仅导出」将导致您无法使用导入功能,导出的秒传文件也仅能用于其他网盘的秒传。设置里可以切换是否提供令牌以恢复完整功能。
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
modal.footerEl.querySelector('[data-act="no"]').onclick = () => {
BAIDU.setCloudConsent(false);
mcLog('consent', '用户选择「仅导出」(不采集令牌)');
modal.overlay.remove();
showToast('已选择「仅导出」:不采集令牌,导入功能已禁用;设置里可切换恢复');
};
modal.footerEl.querySelector('[data-act="yes"]').onclick = () => {
BAIDU.setCloudConsent(true);
mcLog('consent', '用户同意提供令牌(完整功能)');
modal.overlay.remove();
showToast('已授权:云秒传完整功能可用');
};
}
// ---- 保存秒传 JSON(导入,仅光鸭;迅雷无秒传接口)----
function validateImportJson(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 || typeof obj !== 'object' || Array.isArray(obj)) return { ok: false, message: 'JSON 顶层须为对象' };
if (!Array.isArray(obj.files)) return { ok: false, message: '须包含 files 数组' };
if (!obj.files.length) return { ok: false, message: 'files 数组为空' };
return { ok: true, fileCount: obj.files.length };
}
/**
* 给导入模态框挂「选择 .json 文件」功能(各平台共用;光鸭原有实现抽取)。
* modal:openModal 返回对象;ta:主 textarea;fileRowEl:含 input 的行容器;onFileLoaded(text) 回调。
*/
function attachImportFilePicker(modal, ta, fileNameEl, statusEl, onFileLoaded) {
const fileInput = modal.contentEl.querySelector('#gy-ms-import-file');
if (!fileInput) return;
fileInput.addEventListener('change', async () => {
const f = fileInput.files && fileInput.files[0];
if (!f) return;
try {
const text = await f.text();
const vr = validateImportJson(text);
if (!vr.ok) {
if (statusEl) setStatus(statusEl, `格式不符:${vr.message}`, 'err');
return;
}
ta.value = text;
if (fileNameEl) fileNameEl.textContent = `${f.name}(${(f.size / 1024).toFixed(1)} KB,共 ${vr.fileCount} 条)`;
if (statusEl) setStatus(statusEl, '', 'info');
if (onFileLoaded) onFileLoaded(text, f);
} catch (e) {
if (statusEl) setStatus(statusEl, '读取文件失败,请改用粘贴', 'err');
}
});
const clearBtn = modal.contentEl.querySelector('#gy-ms-import-clear');
if (clearBtn) {
clearBtn.onclick = () => {
fileInput.value = '';
ta.value = '';
if (fileNameEl) fileNameEl.textContent = '';
if (statusEl) setStatus(statusEl, '', 'info');
};
}
}
/** 导入框公共说明:粘贴或选择 .json 文件(跨平台一致) */
const IMPORT_FILE_ROW_HTML = `
选择文件:
`;
function showImportModal() {
if (isXunlei()) {
showToast('迅雷云盘无秒传接口,请到光鸭云盘页面导入', 'warning');
return;
}
const modal = openModal({
title: '保存秒传 JSON 到当前账号',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=32 位十六进制 MD5、gcid=40 位 GCID、path、size)。
秒传优先使用 MD5,缺失时尝试 GCID;path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-import-ta');
const fileNameEl = modal.contentEl.querySelector('#gy-ms-import-filename');
const statusEl = modal.contentEl.querySelector('#gy-ms-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, fileNameEl, statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) {
setStatus(statusEl, `校验失败:${vr.message}`, 'err');
return;
}
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
const summary = await runImport(ta.value, (p) => {
updateProgress(barEl, p.percent, p.text);
setStatus(statusEl, p.text, 'info');
});
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) {
detailLines.push('', '========== 已跳过(未提交接口) ==========');
detailLines.push(...summary.skippedDetail);
}
if (detailLines.length) {
detailEl.value = detailLines.join('\n');
detailEl.style.display = 'block';
}
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(
statusEl,
`秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`,
warn ? 'warn' : 'ok'
);
};
}
async function runImport(rawJson, onProgress) {
const api = new GuangyaApi();
const obj = JSON.parse(rawJson);
const list = Array.isArray(obj.files) ? obj.files : [];
const rows = [];
const skipped = [];
for (let i = 0; i < list.length; i++) {
const f = list[i];
const path = String(f.path || f.name || '').trim() || 'file';
const size = Number(f.size != null ? f.size : 0);
const gcid = normalizeGcid(f.gcid);
const md5 = normalizeMd5(f.etag || f.md5);
if (!path) { skipped.push('(无路径条目)'); continue; }
if (!md5 && !gcid) {
skipped.push(`${path}:etag/md5 与 gcid 均缺失,无法秒传`);
continue;
}
rows.push({ path, md5, gcid, size });
}
const parentId = getLastParentId();
const dirCache = new Map();
let success = 0;
let fail = 0;
const failures = [];
const total = rows.length;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `秒传 ${i + 1}/${total}:${row.path}` });
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let targetParent = parentId;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${targetParent}::${dirName}`;
if (dirCache.has(key)) {
targetParent = dirCache.get(key);
continue;
}
try {
const dirId = await api.createDir(targetParent, dirName);
dirCache.set(key, dirId);
targetParent = dirId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
try {
const r = await api.flashUpload(fileName, row.md5, row.gcid, row.size, targetParent);
if (r.hit) {
success++;
} else {
fail++;
failures.push(`${row.path}:未命中秒传库存(服务器上不存在该文件)`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传接口失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped.length ? `,跳过 ${skipped.length}` : ''}` });
return { total, success, fail, skipped: skipped.length, failures, skippedDetail: skipped };
}
// ---- 设置 ----
function showSettingsModal() {
const modal = openModal({
title: '设置',
icon: ICONS.settings,
width: 460,
content(el) {
const fields = [
['debug', '调试模式(控制台日志)', 'checkbox', CONFIG.debug],
['md5Delay', '获取MD5间隔 (ms)', 'number', CONFIG.md5Delay],
['mkdirDelay', '创建目录间隔 (ms)', 'number', CONFIG.mkdirDelay],
['transferDelay', '秒传间隔 (ms)', 'number', CONFIG.transferDelay],
['listPageDelay', '分页间隔 (ms)', 'number', CONFIG.listPageDelay],
['md5BatchSize', 'MD5批量大小', 'number', CONFIG.md5BatchSize],
['defaultExportName', '默认导出文件名', 'text', CONFIG.defaultExportName],
];
if (isBaidu()) {
fields.push(['baiduCloudConsent', '提供令牌给云服务器(完整秒传功能)', 'checkbox', BAIDU.cloudConsentEnabled()]);
}
el.innerHTML = fields.map(([key, label, type, value]) => `
${type === 'checkbox'
? ``
: ``}
`).join('');
},
footer(el) {
el.innerHTML = `
${isGuangya() ? `` : ''}
`;
},
});
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
modal.footerEl.querySelector('[data-act="save"]').onclick = () => {
modal.contentEl.querySelectorAll('[data-field]').forEach(inp => {
const key = inp.getAttribute('data-field');
if (key === 'baiduCloudConsent') {
BAIDU.setCloudConsent(inp.checked);
return;
}
if (inp.type === 'checkbox') CONFIG[key] = inp.checked;
else if (key === 'defaultExportName') CONFIG[key] = inp.value.trim();
else CONFIG[key] = Number(inp.value) || 0;
});
saveSettings();
modal.overlay.remove();
showToast('设置已保存');
};
const clearBtn = modal.footerEl.querySelector('[data-act="cleartoken"]');
if (clearBtn) {
clearBtn.onclick = () => {
if (isGuangya()) {
new GuangyaApi().clearToken();
showToast('已清除保存的 access_token', 'info');
}
};
}
}
// ---- 工具栏按钮(按平台挂载) ----
function isUploadLabel(text) {
const t = String(text || '').replace(/\s+/g, '');
return t === '上传' || /^upload$/i.test(t);
}
function rowLooksLikeToolbar(row, uploadBtn) {
if (!row || !uploadBtn) return false;
const n = (row.textContent || '').replace(/\s+/g, '');
if (isGuangya() && (n.includes('新建文件夹') || n.includes('云添加'))) return true;
if (isXunlei() && n.includes('云添加')) return true;
if (isBaidu() && (n.includes('上传') || n.includes('新建文件夹') || n.includes('云添加'))) return true;
const primaries = Array.from(row.querySelectorAll('button.ant-btn-primary')).filter((b) => !b.closest('.gy-ms-toolbar-btn'));
return primaries.length === 1 && primaries[0] === uploadBtn;
}
function resolveToolbarRow() {
if (isP115()) {
// 115 新版网页端(115.com/storage/allfiles):上传按钮的二级父容器即工具栏行(含 上传/新建)
const upBtn = Array.from(document.querySelectorAll('button')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
return (b.textContent || '').trim() === '上传';
});
if (upBtn && upBtn.parentElement && upBtn.parentElement.parentElement) {
const toolbar = upBtn.parentElement.parentElement;
return { row: toolbar, uploadAnchor: upBtn, toolbar };
}
return null;
}
if (isY139()) {
// 移动云盘(yun.139.com):上传按钮的父容器即工具栏行(document_top_upload_button → 父容器 / .document_main .top_button)
const upWrap = document.querySelector('.document_top_upload_button');
const row = upWrap ? upWrap.parentElement : document.querySelector('.document_main .top_button');
if (row) return { row, uploadAnchor: upWrap || row, toolbar: row };
return null;
}
if (isP123()) {
// 123云盘:.home-operator-button-group 即工具栏容器(上传/新建文件夹所在行)
const container = document.querySelector('.home-operator-button-group');
if (container) return { row: container, uploadAnchor: container.firstElementChild || container, toolbar: container };
return null;
}
if (isBaidu()) {
// 精确选中工具栏上传按钮(u-button nd-file-list-toolbar-action-item 且文本为「上传」)
const uploadBtn = Array.from(document.querySelectorAll('button.nd-file-list-toolbar-action-item')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const t = (b.textContent || '').trim();
return t === '上传' || t.indexOf('上传') === 0;
});
if (uploadBtn) {
// 工具栏容器 = .wp-s-agile-tool-bar__header(直链助手同容器;秒传按钮 prepend 到此层,
// 与上传按钮平级分离,绝不嵌入上传内部)
const toolbar = uploadBtn.closest('.wp-s-agile-tool-bar__header, .nd-file-list-toolbar') || uploadBtn.parentElement;
return { row: toolbar, uploadAnchor: uploadBtn, toolbar };
}
return null;
}
if (isAliyun()) {
// 阿里云盘:优先复用「直链下载助手」同款容器 actions--xxx(header 右侧操作区,与下载助手按钮并列)。
const actionsEl = Array.from(document.querySelectorAll('[class*="actions--"], [class*="action--"], [class*="actions-"]')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const r = b.getBoundingClientRect();
// 右侧工具栏:宽 > 20、高 > 20、且在页面顶部工具栏区域(y < 150)
return r.width > 20 && r.height > 20 && r.y > 0 && r.y < 150;
});
if (actionsEl) {
return { row: actionsEl, uploadAnchor: actionsEl, toolbar: actionsEl };
}
// 回退:挂到阿里原生 header(header--xxx / header / [class*="navbar"]),append 到末尾即右上角。
// 不依赖「新建/上传」按钮(部分视图无该按钮)或第三方脚本容器。
const headerEl = document.querySelector('header, .header--wVY7B, [class*="header--"], [class*="navbar"]');
if (headerEl) {
return { row: headerEl, uploadAnchor: headerEl, toolbar: headerEl };
}
return null;
}
if (isQuark()) {
// 夸克网盘:直接定位工具栏按钮组容器(btn-main,含 下载助手/上传/新建文件夹/备份),
// 秒传按钮 append 到 btn-main 末尾。绝不能挂进 ant-upload 容器
// (Ant Design Upload 内点击任意处会触发隐藏 input → 唤起文件选择器)。
const btnMain = Array.from(document.querySelectorAll('.btn-main, [class*="btn-main"]')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const r = b.getBoundingClientRect();
// 工具栏:宽 > 100、高 > 20、顶部区域 y < 150
return r.width > 100 && r.height > 20 && r.y > 0 && r.y < 150;
});
if (btnMain) {
return { row: btnMain, uploadAnchor: btnMain, toolbar: btnMain };
}
// 回退:以「上传文件」按钮为锚点
const uploadBtn = Array.from(document.querySelectorAll('button.upload-btn, button[class*="upload"], button.btn-file')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const r = b.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}) || Array.from(document.querySelectorAll('button')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const t = (b.textContent || '').replace(/\s+/g, '');
const r = b.getBoundingClientRect();
return /^上传文件$/.test(t) && r.width > 0 && r.height > 0;
});
if (uploadBtn) {
let toolbar = uploadBtn.closest('.btn-main, .btn-operate, .section-header.list-header, [class*="toolbar"]') ||
uploadBtn.parentElement;
// 兜底:若父级仍在 ant-upload 内则继续上跳一层
if (toolbar && toolbar.closest('.ant-upload')) {
toolbar = toolbar.parentElement;
}
return { row: toolbar, uploadAnchor: uploadBtn, toolbar };
}
return null;
}
if (isUc()) {
// UC 网盘(与夸克同源前端):同样挂到 btn-main 工具栏组,避开 ant-upload(点击会唤起文件选择器)
const btnMain = Array.from(document.querySelectorAll('.btn-main, [class*="btn-main"]')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const r = b.getBoundingClientRect();
return r.width > 100 && r.height > 20 && r.y > 0 && r.y < 150;
});
if (btnMain) {
return { row: btnMain, uploadAnchor: btnMain, toolbar: btnMain };
}
// 回退:以「上传文件」按钮为锚点
const uploadBtn = Array.from(document.querySelectorAll('button.upload-btn, button[class*="upload"], button.btn-file')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const r = b.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}) || Array.from(document.querySelectorAll('button')).find((b) => {
if (b.closest('.gy-ms-toolbar-btn')) return false;
const t = (b.textContent || '').replace(/\s+/g, '');
const r = b.getBoundingClientRect();
return /^上传文件$/.test(t) && r.width > 0 && r.height > 0;
});
if (uploadBtn) {
let toolbar = uploadBtn.closest('.btn-main, .btn-operate, .section-header.list-header, [class*="toolbar"]') ||
uploadBtn.parentElement;
if (toolbar && toolbar.closest('.ant-upload')) {
toolbar = toolbar.parentElement;
}
return { row: toolbar, uploadAnchor: uploadBtn, toolbar };
}
return null;
}
if (isWeiyun()) {
// 腾讯微云:挂到 .mod-nav(上传/新建/更多 按钮组的外层布局容器)末尾,
// 与"新建"同排右侧。绝不能挂 .mod-action-wrap*(mod-action-wrap-upload 内含 #formFileInputCt
// 上传按钮容器,点击其内部任意位置会冒泡触发 uploadFile → 唤起文件选择器,与夸克/UC ant-upload 陷阱同类)。
const nav = document.querySelector('.mod-nav, [class*="mod-nav"]');
if (nav) {
return { row: nav, uploadAnchor: nav, toolbar: nav };
}
// 回退:最外层工具栏(安全但不与按钮组同排)
const toolbar = document.querySelector('.layout-toolbar, [class*="layout-toolbar"]');
if (toolbar) {
return { row: toolbar, uploadAnchor: toolbar, toolbar };
}
return null;
}
if (isFeejii() || isIlanzou()) {
// 小飞机/蓝奏云优享版控制台:顶部 .title-bar 工具栏容器(含 上传/新建 等按钮组),秒传按钮 append 到末尾
const titleBar = document.querySelector('.title-bar, [class*="title-bar"]');
if (titleBar) {
return { row: titleBar, uploadAnchor: titleBar, toolbar: titleBar };
}
// 回退:整个文件列表区顶部
const files = document.querySelector('.files, [class*="file-list"]');
if (files) {
return { row: files, uploadAnchor: files, toolbar: files };
}
return null;
}
if (isLanzou()) {
// 蓝奏云为 iframe 结构,顶层仅悬浮按钮(fixed 兜底)
return null;
}
if (isCtfile()) {
// 城通网盘标准版:顶部 .ct-toolbar 工具栏(含 上传/上传文件夹/新建文件夹/搜索/排序),秒传按钮 append 到末尾
const toolbar = document.querySelector('.ct-toolbar, [class*="ct-toolbar"]');
if (toolbar) {
return { row: toolbar, uploadAnchor: toolbar, toolbar };
}
// 回退:整个文件列表容器
const listEl = document.querySelector('.ct-list, [class*="ct-list"], [class*="file-list"]');
if (listEl) {
return { row: listEl, uploadAnchor: listEl, toolbar: listEl };
}
return null;
}
if (isTianyi()) {
// 天翼云盘:以「新建文件夹」文本按钮为锚(scoped hash 类名不稳定,锚文本最稳),
// 其父级即左侧按钮组容器(上传/新建文件夹/刷新一组),秒传按钮 append 到该组末尾
const btns = Array.from(document.querySelectorAll('div,span,a')).filter(el => (el.textContent || '').trim() === '新建文件夹' && el.children.length === 0);
if (btns.length && btns[0].parentElement) {
const group = btns[0].parentElement;
return { row: group, uploadAnchor: group, toolbar: group };
}
// 回退:整个文件区容器
const panel = document.querySelector('.cloud-file, .p-file, [class*="File_p-file"], [class*="file-list"], [class*="c-file-list"]');
if (panel) return { row: panel, uploadAnchor: panel, toolbar: panel };
return null;
}
if (isWopan()) {
// 联通云盘:以「新建」文本按钮为锚,其父级 .actions 即顶部按钮组(上传/新建),秒传按钮 append 到末尾。
// 注意「上传」是 el-dropdown(点击展开菜单),秒传按钮作为独立兄弟节点不受影响。
const btns = Array.from(document.querySelectorAll('button,span,div,a')).filter(el => (el.textContent || '').trim() === '新建' && /el-button/.test(el.className || ''));
if (btns.length && btns[0].parentElement) {
const group = btns[0].parentElement;
return { row: group, uploadAnchor: group, toolbar: group };
}
// 回退:左侧操作区/整个文件列表容器
const op = document.querySelector('[class*="operation-container"], [class*="left-operation"], .file-list-container');
if (op) return { row: op, uploadAnchor: op, toolbar: op };
return null;
}
const triggers = document.querySelectorAll('.ant-dropdown-trigger');
for (const trig of triggers) {
const inner = trig.matches('button.ant-btn, .ant-btn') ? trig : trig.querySelector('button.ant-btn, .ant-btn');
if (!inner || !inner.classList.contains('ant-btn-primary')) continue;
if (!isUploadLabel(inner.textContent)) continue;
const row = trig.parentElement;
if (!row || !rowLooksLikeToolbar(row, inner)) continue;
return { row, uploadAnchor: trig };
}
const primaries = document.querySelectorAll('button.ant-btn-primary');
for (const inner of primaries) {
if (inner.closest('.gy-ms-toolbar-btn')) continue;
if (!isUploadLabel(inner.textContent)) continue;
const row = inner.parentElement;
if (!row || !rowLooksLikeToolbar(row, inner)) continue;
return { row, uploadAnchor: inner };
}
return null;
}
function addToolbarButton() {
// 115 旧版 UI:不挂秒传按钮(p115PlatformGate 已显示提示条)
if (isP115() && !p115IsNewUi()) return;
const existing = document.querySelector('.gy-ms-toolbar-btn');
if (existing) {
// 菜单打开时绝不触碰(防止 MutationObserver 重建导致菜单消失)
const dd = existing.querySelector('.gy-ms-dropdown');
if (dd && dd.style.display === 'block') return;
const hitCheck = resolveToolbarRow();
if (hitCheck) {
// 幂等:只要按钮仍在期望容器内(或是其子孙)就直接返回,不重建;
// 优享版/小飞机等 append 到工具栏末尾,百度 prepend 到最前,其它平台在 uploadAnchor 前
if (existing.parentElement && (existing.parentElement === hitCheck.toolbar || (hitCheck.toolbar && hitCheck.toolbar.contains(existing)))) {
return;
}
if (hitCheck.uploadAnchor && hitCheck.uploadAnchor.previousElementSibling === existing) return;
} else {
// 无目标容器(fixed 悬浮兜底):只要还挂在 body 上就不重建
if (existing.parentElement === document.body) return;
}
existing.remove();
}
const hit = resolveToolbarRow();
if (hit) {
const wrap = document.createElement('div');
wrap.className = 'gy-ms-toolbar-btn';
wrap.style.cssText = 'display:inline-flex;margin-right:8px;vertical-align:middle;position:relative;z-index:100;';
wrap.innerHTML = buildButtonHtml();
try {
const tb = hit.toolbar;
if (tb && typeof tb.appendChild === 'function') {
if (isAliyun() || isQuark() || isUc() || isWeiyun() || isFeejii() || isP115() || isCtfile() || isTianyi() || isWopan()) {
// 阿里:append 到 actions 容器末尾(与直链下载助手并列);夸克/UC/微云/小飞机/优享版/城通/天翼:append 到工具栏末尾(与新建文件夹并列)
tb.appendChild(wrap);
} else {
// 百度及其它平台:prepend 到工具栏最前 / uploadAnchor 前
tb.insertBefore(wrap, tb.firstChild);
}
} else {
(hit.uploadAnchor.parentElement || document.body).insertBefore(wrap, hit.uploadAnchor);
}
} catch (e) {
(hit.uploadAnchor.parentElement || document.body).insertBefore(wrap, hit.uploadAnchor);
}
bindDropdown(wrap);
return;
}
const fab = document.createElement('div');
fab.className = 'gy-ms-toolbar-btn';
fab.style.cssText = 'position:fixed;left:24px;top:24px;z-index:2147483645;';
fab.innerHTML = buildButtonHtml();
document.body.appendChild(fab);
bindDropdown(fab);
}
function buildButtonHtml() {
return `
${ICONS.generate}生成秒传JSON
${ICONS.import}保存秒传JSON
${ICONS.settings}设置
`;
}
function bindDropdown(container) {
const mainBtn = container.querySelector('.gy-ms-main-btn');
const dropdown = container.querySelector('.gy-ms-dropdown');
const show = () => { dropdown.style.display = 'block'; };
const hide = () => { dropdown.style.display = 'none'; };
// 记录鼠标坐标:绕过按钮与菜单间空隙导致的 mouseleave 误判。
// 注意:按钮可能在 iframe 内(蓝奏云),mousemove 必须监听按钮所属 document,
// iframe 内的鼠标移动不会冒泡到顶层 document。
const ownerDoc = (container.ownerDocument) || document;
const ownerWin = (ownerDoc.defaultView) || window;
let lastX = 0, lastY = 0;
try {
ownerDoc.addEventListener('mousemove', (e) => { lastX = e.clientX; lastY = e.clientY; }, { passive: true });
} catch (e) { /* ignore */ }
const pointerIn = (el) => {
try {
const r = el.getBoundingClientRect();
return lastX >= r.left && lastX <= r.right && lastY >= r.top && lastY <= r.bottom;
} catch (e) { return false; }
};
const scheduleHide = () => {
setTimeout(() => {
if (pointerIn(container) || pointerIn(dropdown)) { show(); return; }
if (!container.matches(':hover') && !dropdown.matches(':hover')) hide();
}, 300);
};
mainBtn.addEventListener('mouseenter', show);
mainBtn.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
});
container.addEventListener('mouseleave', scheduleHide);
dropdown.addEventListener('mouseleave', scheduleHide);
dropdown.addEventListener('mouseenter', show);
dropdown.addEventListener('click', (e) => {
const item = e.target.closest('.gy-ms-dropdown-item');
if (!item) return;
const act = item.getAttribute('data-act');
hide();
if (act === 'generate') {
const job = (isP115() ? p115GenerateJson()
: isBaidu() ? baiduGenerateJson()
: isAliyun() ? aliyunGenerateJson()
: isQuark() ? quarkGenerateJson()
: isUc() ? ucGenerateJson()
: isWeiyun() ? weiyunGenerateJson()
: isFeejii() ? feejiiGenerateJson()
: isIlanzou() ? ilanzouGenerateJson()
: isLanzou() ? lanzouGenerateJson()
: isCtfile() ? ctfileGenerateJson()
: isTianyi() ? tianyiGenerateJson()
: isWopan() ? wopanGenerateJson()
: isY139() ? y139GenerateJson()
: isP123() ? p123GenerateJson()
: generateJson());
job.catch(err => showToast(err.message, 'error'));
}
else if (act === 'import') {
if (isP115()) {
showP115ImportModal();
} else if (isBaidu()) {
if (!BAIDU.cloudConsentEnabled()) {
showToast('已选择「仅导出」:导入功能已禁用(设置里可切换提供令牌恢复)', 'warning');
return;
}
showBaiduImportModal();
} else if (isAliyun()) {
showAliyunImportModal();
} else if (isQuark()) {
showQuarkImportModal();
} else if (isUc()) {
showUcImportModal();
} else if (isWeiyun()) {
showWeiyunImportModal();
} else if (isFeejii()) {
showFeejiiImportModal();
} else if (isIlanzou()) {
showIlanzouImportModal();
} else if (isLanzou()) {
showToast('蓝奏云无哈希秒传接口(html5up.php 必须携带文件本体,实测 md5-only 被拒、服务器不做内容去重),无法秒传导入;该盘导出的 JSON 可在优享版/小飞机等支持 md5 的平台导入', 'warning', 6000);
} else if (isCtfile()) {
showCtfileImportModal();
} else if (isTianyi()) {
showTianyiImportModal();
} else if (isWopan()) {
showToast('联通云盘无哈希秒传接口(上传必须携带文件本体,服务器不做内容去重,列表也不返回哈希),无法秒传导入;该盘导出的 md5 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入', 'warning', 6000);
} else if (isY139()) {
showY139ImportModal();
} else if (isP123()) {
showP123ImportModal();
} else showImportModal();
}
else if (act === 'settings') showSettingsModal();
});
}
// ============================== 115 平台(v1.0:导出秒传JSON + 导入秒传,含二次验证云端中转) ==============================
// 协议来源:2026-10 逆向实测(uplb.115.com/4.0/initupload.php 固定 AES 方案 + proapi chrome/downurl rsa 加密直链)
// 边界:<1MB 纯 sha1 秒传;≥1MB 必触发二次验证(sign_check 随机范围)→ 115→115 导入用导出内嵌的 pc 走 CDN Range 中转
const P115_API = {
UPLOADINFO: 'https://proapi.115.com/app/uploadinfo',
INIT: 'https://uplb.115.com/4.0/initupload.php',
LIST: 'https://webapi.115.com/files',
DIR_ADD: 'https://webapi.115.com/files/add',
DELETE: 'https://webapi.115.com/rb/delete',
DOWNURL: 'https://proapi.115.com/app/chrome/downurl',
UA_DISK: 'Mozilla/5.0 115disk/36.2.28 115Browser/36.2.28 115wangpan_android/36.2.28',
APP_VERSION: '36.2.28',
};
// ---- 115 加密栈(k_ec / AES-CBC / LZ4 / sig / token / RSA 直链)----
const P115_CRYPTO = (() => {
const enc = new TextEncoder();
const dec = new TextDecoder('utf-8');
const hexOf = (u8) => Array.from(u8, b => b.toString(16).padStart(2, '0')).join('');
const toU8 = (arr) => new Uint8Array(arr);
// MD5(blueimp 风格,ASCII/字节串)
function md5Hex(str) {
const s = String(str);
const safeAdd = (x, y) => { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); };
const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt));
const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t);
const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t);
const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t);
const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t);
const binlMD5 = (x, len) => {
x[len >> 5] |= 0x80 << (len % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
let i, oa, ob, oc, od;
let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;
for (i = 0; i < x.length; i += 16) {
oa = a; ob = b; oc = c; od = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, oa); b = safeAdd(b, ob); c = safeAdd(c, oc); d = safeAdd(d, od);
}
return [a, b, c, d];
};
const rstr2binl = (input) => {
const output = []; output[(input.length >> 2) - 1] = undefined;
for (let i = 0; i < output.length; i += 1) output[i] = 0;
const length8 = input.length * 8;
for (let i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32);
return output;
};
const binl2rstr = (input) => {
let output = ''; const length32 = input.length * 32;
for (let i = 0; i < length32; i += 8) output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff);
return output;
};
const rstr2hex = (input) => {
const hexTab = '0123456789abcdef'; let output = '';
for (let i = 0; i < input.length; i += 1) { const x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); }
return output;
};
const rstrMD5 = (ss) => binl2rstr(binlMD5(rstr2binl(ss), ss.length * 8));
return rstr2hex(rstrMD5(s));
}
// SHA1 hex(WebCrypto;失败回退纯 JS SHA-1)
const sha1Hex = (str) => {
try {
return crypto.subtle.digest('SHA-1', enc.encode(str)).then((buf) => hexOf(new Uint8Array(buf)));
} catch (e) {
return Promise.resolve(sha1HexFallback(str));
}
};
// 纯 JS SHA-1 回退(FIPS 180-4)
function sha1HexFallback(str) {
const bytes = [];
for (let i = 0; i < str.length; i++) bytes.push(str.charCodeAt(i) & 0xff);
const ml = bytes.length * 8;
bytes.push(0x80);
while (bytes.length % 64 !== 56) bytes.push(0);
for (let i = 7; i >= 0; i--) bytes.push((ml / Math.pow(2, i * 8)) & 0xff);
let h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0;
const rol = (n, c) => ((n << c) | (n >>> (32 - c))) >>> 0;
for (let i = 0; i < bytes.length; i += 64) {
const w = new Array(80);
for (let t = 0; t < 16; t++) w[t] = ((bytes[i + t * 4] << 24) | (bytes[i + t * 4 + 1] << 16) | (bytes[i + t * 4 + 2] << 8) | bytes[i + t * 4 + 3]) >>> 0;
for (let t = 16; t < 80; t++) w[t] = rol(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1);
let a = h0, b = h1, c = h2, d = h3, e = h4;
for (let t = 0; t < 80; t++) {
const f = t < 20 ? ((b & c) | (~b & d)) : t < 40 ? (b ^ c ^ d) : t < 60 ? ((b & c) | (b & d) | (c & d)) : (b ^ c ^ d);
const k = t < 20 ? 0x5A827999 : t < 40 ? 0x6ED9EBA1 : t < 60 ? 0x8F1BBCDC : 0xCA62C1D6;
const tmp = (rol(a, 5) + f + e + k + w[t]) >>> 0;
e = d; d = c; c = rol(b, 30) >>> 0; b = a; a = tmp;
}
h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0; h4 = (h4 + e) >>> 0;
}
return [h0, h1, h2, h3, h4].map(h => ('00000000' + h.toString(16)).slice(-8)).join('');
}
// CRC32
const crcTable = (() => { const t = new Int32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); t[i] = c; } return t; })();
const crc32 = (bytes) => { let c = -1; for (const b of bytes) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ -1) >>> 0; };
// ---- 常量 ----
const AES_PUBKEY = toU8([0x1d, 0x03, 0x0e, 0x80, 0xa1, 0x78, 0xdc, 0xee, 0xce, 0xcd, 0xa3, 0x77, 0xde, 0x12, 0x8d, 0x8e, 0xd9, 0xdd, 0xcf, 0x55, 0xae, 0x61, 0xed, 0x46, 0xea, 0x12, 0x1a, 0x1c, 0xfc, 0x81]);
const AES_KEY = toU8([0xfb, 0x1a, 0x19, 0xd6, 0x52, 0xf5, 0xaa, 0xf7, 0xbc, 0x65, 0x1d, 0x0f, 0x69, 0xbf, 0x42, 0x2f]);
const AES_IV = toU8([0x69, 0xbf, 0x42, 0x2f, 0x49, 0x96, 0x05, 0x50, 0xa0, 0xad, 0x44, 0xec, 0x34, 0x46, 0xcb, 0x4c]);
const CRC_SALT = toU8([0x5e, 0x6a, 0x3e, 0x57, 0x44, 0x33, 0x4b, 0x72, 0x3f, 0x4a, 0x32, 0x67, 0x4c, 0x46, 0x6a, 0x44, 0x34, 0x57, 0x32, 0x79, 0x40]);
const MD5_SALT = 'Qclm8MGWUv59TnrR0XPg';
// RSA(下载直链,p115cipher 风格)
const RSA_N = BigInt('0x' + '8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1cf88f09b86021' + '36477198a6e2683149659bd122c33592fdb5ad47944ad1ea4d36c6b172aad633' + '8c3bb6ac6227502d010993ac967d1aef00f0c8e038de2e4d3bc2ec368af2e9f1' + '0a6f1eda4f7262f136420c07c331b871bf139f74f3010e3c4fe57df3afb71683');
const RSA_E = 65537n;
const RSA_KEY = toU8([0x8d, 0xa5, 0xa5, 0x8d]);
const G_KEY_L = toU8([0x78, 0x06, 0xad, 0x4c, 0x33, 0x86, 0x5d, 0x18, 0x4c, 0x01, 0x3f, 0x46]);
const G_KTS = toU8([0xf0,0xe5,0x69,0xae,0xbf,0xdc,0xbf,0x8a,0x1a,0x45,0xe8,0xbe,0x7d,0xa6,0x73,0xb8,0xde,0x8f,0xe7,0xc4,0x45,0xda,0x86,0xc4,0x9b,0x64,0x8b,0x14,0x6a,0xb4,0xf1,0xaa,0x38,0x01,0x35,0x9e,0x26,0x69,0x2c,0x86,0x00,0x6b,0x4f,0xa5,0x36,0x34,0x62,0xa6,0x2a,0x96,0x68,0x18,0xf2,0x4a,0xfd,0xbd,0x6b,0x97,0x8f,0x4d,0x8f,0x89,0x13,0xb7,0x6c,0x8e,0x93,0xed,0x0e,0x0d,0x48,0x3e,0xd7,0x2f,0x88,0xd8,0xfe,0xfe,0x7e,0x86,0x50,0x95,0x4f,0xd1,0xeb,0x83,0x26,0x34,0xdb,0x66,0x7b,0x9c,0x7e,0x9d,0x7a,0x81,0x32,0xea,0xb6,0x33,0xde,0x3a,0xa9,0x59,0x34,0x66,0x3b,0xaa,0xba,0x81,0x60,0x48,0xb9,0xd5,0x81,0x9c,0xf8,0x6c,0x84,0x77,0xff,0x54,0x78,0x26,0x5f,0xbe,0xe8,0x1e,0x36,0x9f,0x34,0x80,0x5c,0x45,0x2c,0x9b,0x76,0xd5,0x1b,0x8f,0xcc,0xc3,0xb8,0xf5]);
// ---- AES-128-CBC(手写,无 PKCS7 自动校验)----
const SBOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16];
const INV_SBOX = new Uint8Array(256);
for (let i = 0; i < 256; i++) INV_SBOX[SBOX[i]] = i;
const RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
const xtime = (x) => ((x << 1) ^ ((x >> 7) * 0x1b)) & 0xff;
const gmul = (a, b) => { let p = 0; for (let i = 0; i < 8; i++) { if (b & 1) p ^= a; a = xtime(a); b >>= 1; } return p & 0xff; };
const expandKey = (key) => {
const w = new Uint8Array(176);
w.set(key, 0);
for (let i = 4; i < 44; i++) {
let t0 = w[4 * (i - 1)], t1 = w[4 * (i - 1) + 1], t2 = w[4 * (i - 1) + 2], t3 = w[4 * (i - 1) + 3];
if (i % 4 === 0) { const tmp = t0; t0 = SBOX[t1] ^ RCON[i / 4 - 1]; t1 = SBOX[t2]; t2 = SBOX[t3]; t3 = SBOX[tmp]; }
w[4 * i] = w[4 * (i - 4)] ^ t0; w[4 * i + 1] = w[4 * (i - 4) + 1] ^ t1; w[4 * i + 2] = w[4 * (i - 4) + 2] ^ t2; w[4 * i + 3] = w[4 * (i - 4) + 3] ^ t3;
}
return w;
};
const addRoundKey = (st, w, r) => { for (let c = 0; c < 4; c++) for (let rw = 0; rw < 4; rw++) st[rw + 4 * c] ^= w[16 * r + 4 * c + rw]; };
const shiftRows = (st) => { for (let r = 1; r < 4; r++) { const row = [st[r], st[r + 4], st[r + 8], st[r + 12]]; for (let c = 0; c < 4; c++) st[r + 4 * c] = row[(c + r) % 4]; } };
const invShiftRows = (st) => { for (let r = 1; r < 4; r++) { const row = [st[r], st[r + 4], st[r + 8], st[r + 12]]; for (let c = 0; c < 4; c++) st[r + 4 * c] = row[(c - r + 4) % 4]; } };
const mixColumns = (st) => { for (let c = 0; c < 4; c++) { const a = [st[4 * c], st[4 * c + 1], st[4 * c + 2], st[4 * c + 3]]; st[4 * c] = gmul(a[0], 2) ^ gmul(a[1], 3) ^ a[2] ^ a[3]; st[4 * c + 1] = a[0] ^ gmul(a[1], 2) ^ gmul(a[2], 3) ^ a[3]; st[4 * c + 2] = a[0] ^ a[1] ^ gmul(a[2], 2) ^ gmul(a[3], 3); st[4 * c + 3] = gmul(a[0], 3) ^ a[1] ^ a[2] ^ gmul(a[3], 2); } };
const invMixColumns = (st) => { for (let c = 0; c < 4; c++) { const a = [st[4 * c], st[4 * c + 1], st[4 * c + 2], st[4 * c + 3]]; st[4 * c] = gmul(a[0], 14) ^ gmul(a[1], 11) ^ gmul(a[2], 13) ^ gmul(a[3], 9); st[4 * c + 1] = gmul(a[0], 9) ^ gmul(a[1], 14) ^ gmul(a[2], 11) ^ gmul(a[3], 13); st[4 * c + 2] = gmul(a[0], 13) ^ gmul(a[1], 9) ^ gmul(a[2], 14) ^ gmul(a[3], 11); st[4 * c + 3] = gmul(a[0], 11) ^ gmul(a[1], 13) ^ gmul(a[2], 9) ^ gmul(a[3], 14); } };
const encryptBlock = (st, w) => { addRoundKey(st, w, 0); for (let r = 1; r <= 9; r++) { for (let i = 0; i < 16; i++) st[i] = SBOX[st[i]]; shiftRows(st); mixColumns(st); addRoundKey(st, w, r); } for (let i = 0; i < 16; i++) st[i] = SBOX[st[i]]; shiftRows(st); addRoundKey(st, w, 10); };
const decryptBlock = (st, w) => { addRoundKey(st, w, 10); for (let r = 9; r >= 1; r--) { invShiftRows(st); for (let i = 0; i < 16; i++) st[i] = INV_SBOX[st[i]]; addRoundKey(st, w, r); invMixColumns(st); } invShiftRows(st); for (let i = 0; i < 16; i++) st[i] = INV_SBOX[st[i]]; addRoundKey(st, w, 0); };
// ---- AES-128-ECB/PKCS7(天翼云盘上传参数加密,标准 PKCS7 尾块)----
const aesEcbEncrypt = (plain, key) => {
const w = expandKey(key);
const pad = 16 - (plain.length % 16);
const n = plain.length + pad;
const out = new Uint8Array(n);
out.set(plain, 0);
for (let i = plain.length; i < n; i++) out[i] = pad;
for (let off = 0; off < n; off += 16) {
const block = out.slice(off, off + 16);
encryptBlock(block, w);
out.set(block, off);
}
return out;
};
// ---- RSA PKCS#1 v1.5(任意 SPKI 公钥;天翼 generateRsaKey 公钥 DER 解析 + BigInt 模幂)----
const derReadLen = (bytes, i) => {
let len = bytes[i]; i++;
if (len & 0x80) {
const n = len & 0x7f; len = 0;
for (let k = 0; k < n; k++) { len = (len << 8) | bytes[i]; i++; }
}
return { len, next: i };
};
const rsaEncryptWithKey = (pubKeyB64, data) => {
const der = b64Decode(pubKeyB64);
let i = 0;
if (der[i++] !== 0x30) throw new Error('bad SPKI');
const { next: i1 } = derReadLen(der, i); i = i1;
if (der[i++] !== 0x30) throw new Error('bad alg seq');
const alg = derReadLen(der, i); i = alg.next + alg.len; // 跳过算法 SEQUENCE 全部内容
if (der[i++] !== 0x03) throw new Error('bad bitstring');
const { next: i3 } = derReadLen(der, i); i = i3; // BIT STRING 内容起点(含未用位字节)
if (der[i] === 0x00) i++;
if (der[i++] !== 0x30) throw new Error('bad rsa seq');
const { next: i4 } = derReadLen(der, i); i = i4;
const readInt = () => {
if (der[i++] !== 0x02) throw new Error('bad int');
const { len, next } = derReadLen(der, i);
let v = 0n;
for (let k = next; k < next + len; k++) v = (v << 8n) | BigInt(der[k]);
i = next + len;
return v;
};
const n = readInt();
const e = readInt();
const blockLen = (n.toString(16).length + 1) >> 1;
const maxMsg = blockLen - 11;
const out = [];
const src = data instanceof Uint8Array ? data : new Uint8Array(data);
for (let off = 0; off < src.length; off += maxMsg) {
const slice = src.subarray(off, Math.min(off + maxMsg, src.length));
const padCount = blockLen - 2 - slice.length;
const buf = new Uint8Array(blockLen);
buf[0] = 0;
for (let k = 1; k <= padCount; k++) buf[k] = 0x02;
buf[padCount + 1] = 0;
buf.set(slice, padCount + 2);
let msg = 0n;
for (const b of buf) msg = (msg << 8n) | BigInt(b);
const c = modpow(msg, e, n);
const cb = toBytes(c, blockLen);
for (const b of cb) out.push(b);
}
return b64Encode(new Uint8Array(out));
};
const aesCbcEncrypt = (plain, key, iv) => {
// p115client 式填充:不足 16 对齐时补 1..15(对齐时不补)
let data = plain;
const mod = plain.length % 16;
if (mod !== 0) {
const pad = 16 - mod;
data = new Uint8Array(plain.length + pad);
data.set(plain, 0);
for (let i = plain.length; i < data.length; i++) data[i] = pad;
}
const w = expandKey(key);
const n = data.length;
const out = new Uint8Array(n);
let prev = iv;
for (let off = 0; off < n; off += 16) {
const block = data.slice(off, off + 16).slice();
for (let i = 0; i < 16; i++) block[i] ^= prev[i];
encryptBlock(block, w);
out.set(block, off);
prev = block;
}
return out;
};
const aesCbcDecrypt = (cipher, key, iv) => {
const w = expandKey(key);
const n = cipher.length - (cipher.length % 16);
const out = new Uint8Array(n);
let prev = iv;
for (let off = 0; off < n; off += 16) {
const block = cipher.slice(off, off + 16).slice();
decryptBlock(block, w);
for (let i = 0; i < 16; i++) out[off + i] = block[i] ^ prev[i];
prev = cipher.slice(off, off + 16);
}
return out;
};
const unpad115 = (v) => {
if (v.length && v[v.length - 1] > 0 && v[v.length - 1] < 16) {
const p = v[v.length - 1];
let all = true;
for (let i = v.length - p; i < v.length; i++) if (v[i] !== p) { all = false; break; }
if (all) return v.slice(0, v.length - p);
}
return v;
};
// ---- LZ4 块解压(2 字节 LE 长度头 + LZ4 block 循环)----
const lz4BlockDecompress = (src) => {
const dest = []; let srcPtr = 0; const srcLen = src.length;
while (srcPtr < srcLen) {
const token = src[srcPtr++];
let litLen = token >> 4;
if (litLen === 15) { let sp = srcPtr; try { while (src[srcPtr] === 255) srcPtr++; litLen += (srcPtr - sp) * 255 + src[srcPtr]; srcPtr++; } catch (e) { } }
if (litLen) { for (let i = 0; i < litLen; i++) dest.push(src[srcPtr + i]); srcPtr += litLen; }
if (srcPtr >= srcLen) break;
const offset = src[srcPtr] | (src[srcPtr + 1] << 8);
srcPtr += 2;
let matchLen = token & 0x0F;
if (matchLen === 15) { let sp = srcPtr; try { while (src[srcPtr] === 255) srcPtr++; matchLen += (srcPtr - sp) * 255 + src[srcPtr]; srcPtr++; } catch (e) { } }
matchLen += 4;
const matchPos = dest.length - offset;
if (offset >= matchLen) { for (let i = 0; i < matchLen; i++) dest.push(dest[matchPos + i]); }
else if (offset === 1) { for (let i = 0; i < matchLen; i++) dest.push(dest[matchPos]); }
else {
const endPtr = dest.length + matchLen;
let dp = dest.length;
while (dp < endPtr) {
const copySize = offset < (endPtr - dp) ? offset : (endPtr - dp);
for (let i = 0; i < copySize; i++) dest.push(dest[matchPos + i]);
dp += copySize;
}
}
}
return dest;
};
const lz4Decompress = (src) => {
const out = []; let p = 0;
while (src.length - p > 2 && ((src[p] + (src[p + 1] << 8)) !== 0)) {
const blkLen = src[p] + (src[p + 1] << 8);
const block = src.slice(p + 2, p + 2 + blkLen);
const d = lz4BlockDecompress(block);
for (const b of d) out.push(b);
p += 2 + blkLen;
}
return out;
};
// ---- k_ec ----
const le32 = (n) => [n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff];
const ecdhEncodeToken = (timestamp) => {
const body = [];
for (let i = 0; i < 15; i++) body.push(AES_PUBKEY[i]);
body.push(0x00, 0x73, 0x00, 0x00, 0x00);
for (const b of le32(timestamp >>> 0)) body.push(b);
for (let i = 15; i < AES_PUBKEY.length; i++) body.push(AES_PUBKEY[i]);
body.push(0x00, 0x01, 0x00, 0x00, 0x00);
const crcInput = new Uint8Array(CRC_SALT.length + body.length);
crcInput.set(CRC_SALT, 0);
crcInput.set(body, CRC_SALT.length);
for (const b of le32(crc32(crcInput))) body.push(b);
let s = ''; for (const b of body) s += String.fromCharCode(b);
return btoa(s);
};
// ---- 生成 init 加密请求体 ----
async function makeInitPayload(payload) {
const t = Math.floor(Date.now() / 1000);
payload.t = String(t);
const userid = String(payload.userid);
const inner = await sha1Hex(userid + payload.fileid + payload.target + '0');
const sig = (await sha1Hex(payload.userkey + inner + '000000')).toUpperCase();
payload.sig = sig;
const token = md5Hex(MD5_SALT + payload.fileid + payload.filesize + (payload.sign_key || '') + (payload.sign_val || '') + userid + t + md5Hex(userid) + payload.appversion);
payload.token = token;
const sorted = Object.keys(payload).filter(k => payload[k]).sort();
const parts = sorted.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(payload[k])));
const data = parts.join('&') + '&';
const body = aesCbcEncrypt(toU8(Array.from(data, ch => ch.charCodeAt(0) & 0xff)), AES_KEY, AES_IV);
return { k_ec: ecdhEncodeToken(t), body };
}
// ---- 解密 init 响应 ----
async function decryptInitResp(u8) {
const raw = unpad115(aesCbcDecrypt(u8, AES_KEY, AES_IV));
const head = String.fromCharCode(raw[0], raw[1]);
let text;
if (head === '{') text = dec.decode(raw);
else text = dec.decode(new Uint8Array(lz4Decompress(raw)));
return JSON.parse(text);
}
// ---- RSA(下载直链,p115cipher 风格)----
const modpow = (base, exp, mod) => { let result = 1n; base %= mod; while (exp > 0n) { if (exp & 1n) result = (result * base) % mod; base = (base * base) % mod; exp >>= 1n; } return result; };
const toBytes = (big, size) => { const b = []; while (big > 0n) { b.unshift(Number(big & 0xffn)); big >>= 8n; } while (b.length < size) b.unshift(0); return b; };
const rsaEncrypt = (input) => {
const out = [];
const BLOCK = 128;
for (let off = 0; off < input.length; off += BLOCK - 11) {
const slice = input.subarray(off, Math.min(off + BLOCK - 11, input.length));
const padCount = BLOCK - 2 - slice.length;
const buf = new Uint8Array(BLOCK);
buf[0] = 0;
for (let i = 1; i <= padCount; i++) buf[i] = 0x02;
buf[padCount + 1] = 0;
buf.set(slice, padCount + 2);
let msg = 0n;
for (const b of buf) msg = (msg << 8n) | BigInt(b);
for (const b of toBytes(modpow(msg, RSA_E, RSA_N), BLOCK)) out.push(b);
}
return new Uint8Array(out);
};
const rsaDecrypt = (input) => {
const out = [];
const BLOCK = 128;
for (let off = 0; off < input.length; off += BLOCK) {
const slice = input.subarray(off, Math.min(off + BLOCK, input.length));
let msg = 0n;
for (const b of slice) msg = (msg << 8n) | BigInt(b);
const ret = modpow(msg, RSA_E, RSA_N);
const b = toBytes(ret, Math.max(1, Math.ceil(ret.toString(16).length / 2)));
let idx = b.findIndex(x => x === 0);
if (idx < 0) { for (const x of b) out.push(x); } else { for (let i = idx + 1; i < b.length; i++) out.push(b[i]); }
}
return new Uint8Array(out);
};
const xorTransform = (data, key) => {
const dataSize = data.length, keySize = key.length;
const mod = dataSize % 4;
if (mod > 0) for (let i = 0; i < mod; i++) data[i] ^= key[i % keySize];
for (let i = mod; i < dataSize; i++) data[i] ^= key[(i - mod) % keySize];
};
const reverseBytes = (data) => { for (let i = 0, j = data.length - 1; i < j; i++, j--) { const t = data[i]; data[i] = data[j]; data[j] = t; } };
const rsaGenKey = (randKey, skLen) => {
const k = new Uint8Array(skLen);
let length = skLen * (skLen - 1);
let index = 0;
for (let i = 0; i < skLen; i++) { const x = (randKey[i] + G_KTS[index]) & 0xff; k[i] = G_KTS[length] ^ x; length -= skLen; index += skLen; }
return k;
};
const b64Encode = (u8) => { let s = ''; for (const b of u8) s += String.fromCharCode(b); return btoa(s); };
const b64Decode = (s) => { const bin = atob(s); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); return u8; };
const rsaEncEncode = (inputBytes) => {
const tmp = inputBytes.slice();
xorTransform(tmp, RSA_KEY);
reverseBytes(tmp);
const xt = tmp.slice();
xorTransform(xt, G_KEY_L);
const xorData = new Uint8Array(16 + xt.length);
xorData.set(xt, 16);
return b64Encode(rsaEncrypt(xorData));
};
const rsaEncDecode = (inputB64) => {
const data = rsaDecrypt(b64Decode(inputB64));
const randkey = data.subarray(0, 16);
const keyL = rsaGenKey(randkey, 12);
const body = data.subarray(16).slice();
xorTransform(body, keyL);
reverseBytes(body);
const out = body.slice();
xorTransform(out, RSA_KEY);
return out;
};
return {
md5Hex, sha1Hex, makeInitPayload, decryptInitResp,
rsaEncEncode, rsaEncDecode, ecdhEncodeToken,
aesEcbEncrypt, rsaEncryptWithKey,
// 测试钩子
_test: { aesCbcEncrypt, aesCbcDecrypt, lz4Decompress, crc32, sha1HexFallback },
};
})();
// ---- 115 API 封装(GM_xmlhttpRequest 跨域 + 自定义 UA;Cookie 由油猴按域名自动携带)----
const ONE15 = {
state: { userid: '', userkey: '', ua: P115_API.UA_DISK },
listCache: [], // 拦截的当前目录列表(配合 DOM 勾选读取)
gmRequest(method, url, opts = {}) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; }
const headers = Object.assign({ 'User-Agent': this.state.ua, 'Referer': 'https://115.com/' }, opts.headers || {});
GM_xmlhttpRequest({
method,
url,
headers,
data: opts.data,
responseType: opts.responseType || 'text',
timeout: opts.timeout || 20000,
onload: (r) => {
if (r.status < 200 || r.status >= 300) reject(new Error(`HTTP ${r.status} @ ${url}`));
else resolve(r);
},
onerror: () => reject(new Error(`网络请求失败 @ ${url}`)),
ontimeout: () => reject(new Error(`请求超时 @ ${url}`)),
});
});
},
async gmText(method, url, body) {
const r = await this.gmRequest(method, url, { data: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
return r.responseText;
},
/** 获取 user_id/userkey(缓存) */
async ensureUploadInfo() {
if (this.state.userid && this.state.userkey) return this.state;
const text = await this.gmText('POST', P115_API.UPLOADINFO, '');
const j = JSON.parse(text);
if (!j.userkey) throw new Error('获取 115 上传凭证失败(请确认已登录 115 网页版)');
this.state.userid = String(j.user_id);
this.state.userkey = j.userkey;
return this.state;
},
/** 文件列表(分页) */
async getFileList(cid, offset = 0, limit = 115) {
const qs = new URLSearchParams({
aid: '1', cid: String(cid), offset: String(offset), limit: String(limit),
type: '0', show_dir: '1', fc_mix: '0', natsort: '1', count_folders: '1', format: 'json', custom_order: '0',
});
const text = await this.gmText('GET', `${P115_API.LIST}?${qs.toString()}`);
const j = JSON.parse(text);
if (!j.state) throw new Error(j.error || j.errNo || '列表接口错误');
return { list: j.data || [], count: Number(j.count || 0) };
},
/** 建目录;同名已存在时查回既有目录 cid(幂等重导入) */
async mkdir(pid, name) {
const body = new URLSearchParams({ pid: String(pid), cname: String(name) }).toString();
const text = await this.gmText('POST', P115_API.DIR_ADD, body);
const j = JSON.parse(text);
if (!j.state) {
if (/已存在|exist/i.test(String(j.error || j.errno || ''))) {
const page = await this.getFileList(pid, 0, 115);
const hit = (page.list || []).find(f => !f.sha && f.n === String(name));
if (hit && hit.cid) return String(hit.cid);
}
throw new Error(j.error || j.errno || '创建目录失败');
}
return String(j.file_id || '');
},
/** 删除文件 */
async deleteFiles(fids) {
const body = new URLSearchParams();
fids.forEach((f, i) => body.append(`fid[${i}]`, String(f)));
const text = await this.gmText('POST', P115_API.DELETE, body.toString());
const j = JSON.parse(text);
return !!j.state;
},
/** 秒传初始化(加密请求体),返回 {k_ec, body, url} */
async prepareInit({ fileid, filesize, filename, target, signKey, signVal }) {
await this.ensureUploadInfo();
const payload = {
appid: '0', appversion: P115_API.APP_VERSION,
userid: this.state.userid, userkey: this.state.userkey,
filename, filesize: String(filesize), fileid: String(fileid).toUpperCase(),
target, topupload: 'true',
};
if (signKey) { payload.sign_key = signKey; payload.sign_val = signVal; }
const { k_ec, body } = await P115_CRYPTO.makeInitPayload(payload);
return { url: `${P115_API.INIT}?k_ec=${encodeURIComponent(k_ec)}`, body };
},
/** 执行 init 并解密响应 */
async initUpload(args) {
const { url, body } = await this.prepareInit(args);
const r = await this.gmRequest('POST', url, {
data: body, responseType: 'arraybuffer', binary: true,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
const buf = new Uint8Array(r.response);
try {
return await P115_CRYPTO.decryptInitResp(buf);
} catch (e) {
if (r.status !== 200) return { status: 4, statuscode: r.status, statusmsg: String(e) };
throw new Error(`秒传响应解密失败: ${e.message}`);
}
},
/** 获取下载直链(rsa 加密;UA 必须为 115disk 组合,否则 CDN 拒签) */
async getDownloadUrl(pickcode) {
await this.ensureUploadInfo();
const enc = new TextEncoder();
const data = P115_CRYPTO.rsaEncEncode(enc.encode(JSON.stringify({ pickcode, user_id: this.state.userid })));
const t = Math.floor(Date.now() / 1000);
const r = await this.gmRequest('POST', `${P115_API.DOWNURL}?t=${t}`, {
data: 'data=' + encodeURIComponent(data),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'arraybuffer', binary: true,
});
const raw = new Uint8Array(r.response);
let j = null;
try { j = JSON.parse(new TextDecoder().decode(raw)); } catch (e) {
// 响应可能整体 ECDH-AES 加密(不同请求上下文形态不一)
j = await P115_CRYPTO.decryptInitResp(raw);
}
if (!j.state || !j.data) throw new Error(j.msg || j.error || '获取下载直链失败');
const decoded = P115_CRYPTO.rsaEncDecode(j.data);
const obj = JSON.parse(new TextDecoder().decode(decoded));
for (const k of Object.keys(obj)) {
const info = obj[k];
if (info && info.url && info.url.url) return info.url.url;
}
throw new Error('下载直链响应结构异常');
},
/** CDN Range 取字节(只依赖 UA,无需 Cookie) */
async fetchRangeBytes(url, start, end) {
const r = await this.gmRequest('GET', url, {
headers: { 'Range': `bytes=${start}-${end}` },
responseType: 'arraybuffer', binary: true, timeout: 60000,
});
return new Uint8Array(r.response);
},
};
// ---- 勾选读取:拦截 115 webapi 列表响应 + DOM 复选框 ----
function p115InjectPageHook() {
try {
if (window.__P115_LIST__) return;
} catch (e) { /* ignore */ }
const script = document.createElement('script');
script.textContent = `(${p115PageHookSource.toString()})();`;
(document.documentElement || document.head || document.body).appendChild(script);
script.remove();
}
function p115PageHookSource() {
const st = window.__P115_LIST__ = window.__P115_LIST__ || { lists: [], cid: '', at: 0 };
const looksList = (u) => /^https:\/\/webapi\.115\.com\/files\?/i.test(String(u));
const push = (j) => {
try {
if (!j || !j.state || !Array.isArray(j.data)) return;
st.cid = String(j.cid != null ? j.cid : st.cid);
const items = j.data.map(f => ({
fid: String(f.fid || ''), pid: String(f.pid || ''), cid: String(f.cid || ''),
n: String(f.n || ''), s: Number(f.s || 0), sha: String(f.sha || '').toLowerCase(),
pc: String(f.pc || ''), t: String(f.t || ''), ico: String(f.ico || ''),
}));
st.lists = st.lists.concat(items);
if (st.lists.length > 30000) st.lists.splice(0, st.lists.length - 30000);
st.at = Date.now();
} catch (e) { /* ignore */ }
};
const origFetch = window.fetch ? window.fetch.bind(window) : null;
if (origFetch) {
window.fetch = async function (input, init) {
const resp = await origFetch(input, init);
try {
const url = typeof input === 'string' ? input : (input && (input.url || input.href)) || '';
if (looksList(resp.url || url) && resp.ok && typeof resp.clone === 'function') {
resp.clone().json().then(push).catch(() => { });
}
} catch (e) { /* ignore */ }
return resp;
};
}
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) { this.__p115Url = url; return origOpen.apply(this, arguments); };
XMLHttpRequest.prototype.send = function () {
this.addEventListener('load', function () {
try {
if (looksList(this.__p115Url) && this.status >= 200 && this.status < 300 && this.responseText) {
push(JSON.parse(this.responseText));
}
} catch (e) { /* ignore */ }
});
return origSend.apply(this, arguments);
};
}
function p115GetPageState() {
try {
const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window;
return w.__P115_LIST__ || {};
} catch (e) { return {}; }
}
/** 读取勾选:从勾选框向上爬祖先,用拦截列表的已知文件名匹配(img[alt]/file-name 类/文本)。
* 不按父类排除(真实文件行勾选框也在 checkbox-area 内);表头/页脚全选框爬升时匹配不到
* 任何已知文件名(无 img[alt]/file-name 文本),天然被排除;全选时所有行勾选框同步 checked,
* 按行匹配即覆盖"全选当前页"。 */
function p115GetCheckedItems() {
const st = p115GetPageState();
const curCid = p115GetLastCid();
const known = new Set((Array.isArray(st.lists) ? st.lists : []).map(f => f.n));
const names = new Set();
const cbs = document.querySelectorAll('input[type="checkbox"]');
for (const cb of cbs) {
if (!(cb.checked || cb.getAttribute('aria-checked') === 'true')) continue;
let el = cb;
let found = false;
for (let i = 0; i < 6 && el && !found; i++, el = el.parentElement) {
if (!el.querySelector) continue;
// 行边界判定:当前作用域内必须恰好只有这一个勾选框(容器内多个 → 跳过,避免误配其它行)
if (el.querySelectorAll('input[type="checkbox"]').length !== 1) continue;
const img = el.querySelector('img[alt]');
if (img && img.alt && known.has(img.alt)) { names.add(img.alt); found = true; break; }
const cands = el.querySelectorAll('[class*="file-name"], [class*="truncate"], [class*="title"], [class*="name"]');
for (const c of cands) {
const t = (c.textContent || '').replace(/\s+/g, ' ').trim();
if (t && known.has(t)) { names.add(t); found = true; break; }
}
}
}
const source = Array.isArray(st.lists) ? st.lists : [];
// 限定当前目录 + 名字匹配 + 按 fid 去重(拦截列表可能跨目录/跨页累积)
const seen = new Set();
const items = [];
for (const f of source) {
if (curCid && f.cid && f.cid !== curCid) continue;
if (!names.has(f.n)) continue;
const key = f.fid || (f.n + ':' + f.s);
if (seen.has(key)) continue;
seen.add(key);
items.push(f);
}
return { items, names, all: items.length > 0 && items.length === source.filter(f => !curCid || !f.cid || f.cid === curCid).length };
}
/** 列表数据兜底:拦截缓存缺失/过期/目录不符时,主动拉取当前目录列表(分页)写入缓存 */
async function p115EnsureListData() {
const st = p115GetPageState();
const cur = p115GetLastCid();
const fresh = Date.now() - (Number(st.at) || 0) < 15000;
const sameDir = !cur || !st.cid || st.cid === cur;
if (Array.isArray(st.lists) && st.lists.length && fresh && sameDir) return;
try {
st.lists = [];
st.cid = cur || '0';
let offset = 0;
for (;;) {
const page = await ONE15.getFileList(cur || '0', offset, 115);
st.lists = st.lists.concat(page.list);
if (!page.list.length || page.list.length < 115) break;
offset += page.list.length;
}
st.at = Date.now();
} catch (e) { mcLog('p115', '列表兜底拉取失败: ' + e.message); }
}
/** 当前目录 cid(URL 参数优先,其次拦截列表) */
function p115GetLastCid() {
try {
const m = location.search.match(/[?&]cid=([0-9]+)/);
if (m && m[1]) return m[1];
} catch (e) { /* ignore */ }
const st = p115GetPageState();
return st.cid || '0';
}
/** 新版 UI 判定:新版网盘路由为 /storage/...(如 /storage/allfiles);旧版为 115.com/?mode=wangpan(pathname=/)或 ?old=1 */
function p115IsNewUi() {
try { return /^\/storage\//.test(location.pathname); } catch (e) { return true; }
}
/** 旧版 UI 顶部提示条:提醒切换到新版 */
function p115ShowOldUiHint() {
try {
if (document.getElementById('p115-old-ui-hint')) return;
const bar = document.createElement('div');
bar.id = 'p115-old-ui-hint';
bar.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#fff7ed;border:1px solid #fdba74;color:#9a3412;padding:10px 16px;border-radius:10px;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.15);font-family:system-ui,sans-serif;display:flex;align-items:center;gap:12px;max-width:92vw;';
bar.innerHTML = `
🔄 115 秒传助手仅支持新版网盘界面(全部文件 /storage 页面)。当前为旧版界面,请切换后再使用秒传。 立即切换到新版 →
`;
document.body.appendChild(bar);
bar.querySelector('#p115-old-ui-close').onclick = () => bar.remove();
} catch (e) { /* ignore */ }
}
/** 平台门禁:新版返回 true(继续初始化);旧版显示提示并返回 false(不挂秒传功能) */
function p115PlatformGate() {
if (p115IsNewUi()) return true;
setTimeout(p115ShowOldUiHint, 800);
return false;
}
// ---- 导出(生成秒传 JSON,files 内嵌 pc 供 115→115 二次验证中转)----
async function p115GenerateJson() {
// 列表兜底:页面钩子可能落后于 SPA 首次列表请求,主动确保当前目录列表数据存在
await p115EnsureListData();
const sel = p115GetCheckedItems();
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(115 网盘)',
icon: ICONS.generate,
width: 620,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#p115-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
let done = 0;
const total = sel.items.length;
const collect = async (item, prefix) => {
if (!item.sha) { warnMissing.push(prefix + item.n + '(未取到 SHA1)'); return; }
files.push({
path: prefix + item.n,
etag: item.sha,
size: item.s,
pc: item.pc || '',
});
};
const walkFolder = async (cid, prefix) => {
let offset = 0;
for (;;) {
let page;
try { page = await ONE15.getFileList(cid, offset); } catch (e) {
setStatus(statusEl, `获取目录失败:${prefix} ${e.message}`, 'err');
return;
}
for (const it of page.list) {
if (!it.sha) {
await walkFolder(String(it.cid || it.fid), prefix + it.n + '/');
} else {
await collect({ n: it.n, s: it.s, sha: it.sha, pc: it.pc }, prefix);
done++;
updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${it.n}`);
await sleep(CONFIG.md5Delay);
}
}
if (page.list.length < 115) break;
offset += page.list.length;
await sleep(CONFIG.listPageDelay);
}
};
try {
setStatus(statusEl, sel.all ? `已全选当前目录 ${total} 项,正在获取文件信息...` : `正在获取 ${total} 项的文件信息...`, 'info');
for (const item of sel.items) {
if (!item.sha) {
await walkFolder(String(item.cid || item.fid), item.n + '/');
} else {
await collect(item, '');
done++;
updateProgress(barEl, Math.round((done / Math.max(1, total)) * 100), `获取文件信息 ${done}/${total}:${item.n}`);
}
await sleep(CONFIG.md5Delay);
}
if (!files.length) {
setStatus(statusEl, '未获取到任何文件的 SHA1,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '115',
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files,
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = '115秒传_Export.json';
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON(115 导出内嵌 pc,可支持大文件同账号二次验证中转)${warnMissing.length ? `;${warnMissing.length} 项未获取到哈希` : ''}
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) {
const ta = document.createElement('textarea');
ta.value = jsonStr;
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
showToast('秒传 JSON 已复制到剪贴板');
}
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = exportName; a.click();
URL.revokeObjectURL(url);
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
} catch (e) {
setStatus(statusEl, `生成失败:${e.message}`, 'err');
modal.footerEl.innerHTML = ``;
}
}
// ---- 导入(秒传 + 二次验证云端中转)----
function p115NormalizeFile(f) {
const sha1 = String(f.sha1 || f.etag || f.hashName || '').toLowerCase();
if (!/^[0-9a-f]{40}$/.test(sha1)) return null;
const size = Number(f.size != null ? f.size : f.fileSize || 0);
const name = basenameFromPath(f.path) || f.fileName || 'file';
const dir = dirSegmentsFromPath(f.path).join('/');
return { sha1, size, name, dir, pc: String(f.pc || '') };
}
function showP115ImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到 115 网盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:sha1=40 位、hashName=sha1、path、size)。
≥1MB 的文件会触发 115 二次验证(服务器要求随机字节范围的 SHA1):经夸克/UC 中转获取——填写夸克或 UC 的完整 Cookie,脚本先把文件按 sha1 秒传到夸克/UC(宽松秒传),再从云端取验证块;115 同账号导出的 JSON(内嵌 pc)可留空自动中转。path 中的目录会自动创建;全程不产生分享链接。
${IMPORT_FILE_ROW_HTML}
目标目录 cid:
夸克/UC Cookie:
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#p115-import-ta');
const statusEl = modal.contentEl.querySelector('#p115-import-status');
const barEl = modal.contentEl.querySelector('#p115-import-bar');
const barLabelEl = modal.contentEl.querySelector('#p115-import-bar-label');
const detailEl = modal.contentEl.querySelector('#p115-import-detail');
const cookieEl = modal.contentEl.querySelector('#p115-proxy-cookie');
const cidEl = modal.contentEl.querySelector('#p115-target-cid');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
modal.contentEl.querySelector('#p115-cid-cur').onclick = () => { cidEl.value = p115GetLastCid(); };
// ========== 一键获取夸克/UC Cookie(跨标签通道,与阿里导入一致) ==========
const qkBtn = modal.contentEl.querySelector('#p115-proxy-qk');
const ucBtn = modal.contentEl.querySelector('#p115-proxy-uc');
let pendingTimer = null;
function startPendingPoll() {
if (pendingTimer) return;
pendingTimer = setInterval(() => {
let result = '';
try { result = GM_getValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
if (result) {
try { GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
cookieEl.value = result;
setStatus(statusEl, '已自动填入 ' + (result.indexOf('QK:') === 0 ? '夸克' : 'UC') + ' Cookie,可以开始导入。', 'ok');
clearInterval(pendingTimer);
pendingTimer = null;
}
}, 500);
}
qkBtn.onclick = () => {
try { GM_setValue('__pds_cookie_pending', 'quark'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
showToast('正在打开夸克网盘获取 Cookie...', 'info');
window.open('https://pan.quark.cn/', '_blank');
startPendingPoll();
};
ucBtn.onclick = () => {
try { GM_setValue('__pds_cookie_pending', 'uc'); GM_setValue('__pds_cookie_result', ''); } catch (e) { /* ignore */ }
showToast('正在打开 UC 网盘获取 Cookie...', 'info');
window.open('https://drive.uc.cn/', '_blank');
startPendingPoll();
};
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
const cid = cidEl.value.trim();
let json;
try { json = JSON.parse(ta.value); } catch (e) { setStatus(statusEl, `校验失败:${e.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await p115ImportJson(json.files || [], cid, (pct, label) => {
updateProgress(barEl, pct, label);
if (barLabelEl) barLabelEl.textContent = label;
setStatus(statusEl, label, 'info');
}, { sourceCookie: cookieEl ? cookieEl.value.trim() : '' });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(未提交接口) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
/** 核心导入:逐文件秒传;≥1MB 触发二次验证时经「PDS 中转站」取范围字节(先 sha1 丢给夸克/UC 宽松秒传 → 临时直链 → Range),
* 或 115 同账号 pc 中转。全程不产生分享链接,直链内存瞬存。 */
async function p115ImportJson(files, rootCid, onProgress, opts) {
const sourceCookie = (opts && opts.sourceCookie ? String(opts.sourceCookie).trim() : '');
await ONE15.ensureUploadInfo();
const dirCache = new Map();
dirCache.set('', String(rootCid || '0'));
const ensureDir = async (dirPath) => {
if (!dirPath) return dirCache.get('');
if (dirCache.has(dirPath)) return dirCache.get(dirPath);
const segs = dirPath.split('/').filter(Boolean);
let cid = dirCache.get('');
let cur = '';
for (const seg of segs) {
cur = cur ? cur + '/' + seg : seg;
if (dirCache.has(cur)) { cid = dirCache.get(cur); continue; }
cid = await ONE15.mkdir(cid, seg);
dirCache.set(cur, cid);
await sleep(CONFIG.mkdirDelay);
}
dirCache.set(dirPath, cid);
return cid;
};
const importOne = async (f) => {
const norm = p115NormalizeFile(f);
if (!norm) return { name: (f.path || f.fileName || '?'), ok: false, kind: 'skip', reason: '缺少 SHA1(40位hex)' };
const dirId = await ensureDir(norm.dir);
const target = `U_1_${dirId}`;
let firstResp = await ONE15.initUpload({
fileid: norm.sha1, filesize: norm.size, filename: norm.name, target,
});
if (firstResp.status === 2) {
return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(小文件)' };
}
if (firstResp.status === 7) {
// 二次验证:需读取 sign_check 范围的字节。字节来源优先级:
// 1) 填了夸克/UC Cookie(QK:/UC:)→ PDS 中转:先把 sha1 丢给夸克/UC 宽松秒传(内容在全库即登记)→ download 临时直链 → Range 取字节
// (覆盖跨账号 115→115 与跨盘导入;所有底层用阿里云的网盘都可当此中转站)
// 2) JSON 内嵌 pc(115 同账号导出)→ 当前账号 chrome/downurl → Range 取字节
// 3) 否则跳过并给出指引
const signKey = firstResp.sign_key;
const signCheck = firstResp.sign_check;
const m = String(signCheck).match(/^(\d+)-(\d+)$/);
if (!m) return { name: norm.name, ok: false, kind: 'skip', reason: 'sign_check 格式异常' };
const start = Number(m[1]);
const end = Number(m[2]);
let bytes = null;
let via = '';
if (/^QK:/i.test(sourceCookie) || /^UC:/i.test(sourceCookie)) {
try {
bytes = await pdsProxyFetchRange(sourceCookie, norm.sha1, norm.size, start, end);
via = '夸克/UC 中转';
} catch (e) {
return { name: norm.name, ok: false, kind: 'skip', reason: '夸克/UC 中转失败(' + e.message + ')——请确认 Cookie 有效且内容在夸克/UC 池中' };
}
} else if (norm.pc) {
try {
const dlUrl = await ONE15.getDownloadUrl(norm.pc);
bytes = await ONE15.fetchRangeBytes(dlUrl, start, end);
via = '115 同账号中转';
} catch (e) {
return { name: norm.name, ok: false, kind: 'skip', reason: '115 同账号中转失败(' + e.message + ')——若为跨账号导入,请填写夸克或 UC 的完整 Cookie 走 PDS 中转' };
}
} else {
return { name: norm.name, ok: false, kind: 'skip', reason: '≥1MB 触发二次验证且无可用中转:请在导入框填写夸克或 UC 的完整 Cookie(跨盘/跨账号),或使用 115 导出的 JSON(同账号自动中转)' };
}
const digest = await crypto.subtle.digest('SHA-1', bytes);
const signVal = Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
let retryResp = await ONE15.initUpload({
fileid: norm.sha1, filesize: norm.size, filename: norm.name, target,
signKey, signVal,
});
if (retryResp.status === 2) {
return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(二次验证通过,' + via + ')' };
}
if (retryResp.status === 1) {
return { name: norm.name, ok: false, kind: 'miss', reason: '未命中(该 sha1 内容在 115 全库不存在)' };
}
if (retryResp.status === 8 || retryResp.status === 4) {
return { name: norm.name, ok: false, kind: 'fail', reason: `二次验证失败(status ${retryResp.status} ${retryResp.statusmsg || ''},中转来源 ${via})` };
}
return { name: norm.name, ok: false, kind: 'fail', reason: `异常状态 ${retryResp.status}` };
}
if (firstResp.status === 1) {
return { name: norm.name, ok: false, kind: 'miss', reason: '未命中(该 sha1 内容在 115 全库不存在)' };
}
if (firstResp.status === 4 || firstResp.status === 8) {
// 偶发 401/参数问题 → 重试一次
const retry = await ONE15.initUpload({
fileid: norm.sha1, filesize: norm.size, filename: norm.name, target,
});
if (retry.status === 2) return { name: norm.name, ok: true, kind: 'hit', note: '秒传命中(重试)' };
return { name: norm.name, ok: false, kind: 'fail', reason: `status ${firstResp.status} ${firstResp.statusmsg || ''}` };
}
return { name: norm.name, ok: false, kind: 'fail', reason: `异常状态 ${firstResp.status} ${firstResp.statusmsg || ''}` };
};
const results = [];
let done = 0;
for (const f of files) {
try {
results.push(await importOne(f));
} catch (e) {
results.push({ name: (f.path || f.fileName || '?'), ok: false, kind: 'fail', reason: e.message });
}
done++;
if (onProgress) onProgress(Math.round((done / files.length) * 100), `秒传 ${done}/${files.length}:${results[results.length - 1].name} → ${results[results.length - 1].ok ? '命中' : results[results.length - 1].reason}`);
await sleep(CONFIG.transferDelay);
}
const failures = results.filter(r => !r.ok && r.kind !== 'skip').map(r => `${r.name}:${r.reason}`);
const skippedDetail = results.filter(r => !r.ok && r.kind === 'skip').map(r => `${r.name}:${r.reason}`);
const summary = {
total: results.length,
success: results.filter(r => r.ok).length,
fail: results.filter(r => !r.ok && r.kind !== 'skip').length,
skipped: results.filter(r => !r.ok && r.kind === 'skip').length,
failures,
skippedDetail,
};
showToast(`115 秒传完成:${summary.success}/${summary.total} 命中`, summary.fail || summary.skipped ? 'warning' : 'ok');
mcLog('p115', `导入完成 ${summary.success}/${summary.total}`);
return summary;
}
// ============================== 天翼云盘平台(v1.9:189.cn 秒传,个人+家庭云,接口已实测 2026-10) ==============================
// 协议要点(已实测验证):
// - 导出:GET cloud.189.cn/api/open/file/listFiles.action(Cookie 鉴权)返回 name/size/md5(id),**不返回 sliceMd5**。
// ≤10MB 时 sliceMd5=md5(服务端单分片规则,实测 fileDataExists:1 命中);>10MB 必须正确 sliceMd5
// (= MD5(各 10MB 分片 MD5 大写 hex 以 "\n" 连接),openlist 规范实现),只能下载文件计算 → 导出弹窗提供"计算分片哈希"复选(默认勾选,大文件耗时)。
// - 导入:upload.cloud.189.cn 上传接口全程 header 鉴权(SessionKey + EncryptionText(RSA PKCS1v15) + PkId + Signature(HMAC-SHA1)
// + X-Request-Date + X-Request-ID),参数 AES-128-ECB/PKCS7 加密(本地实现,零 CDN 依赖)。流程 generateRsaKey →
// initMultiUpload(fileMd5+sliceMd5) → data.fileDataExists==1 命中 → commitMultiUploadFile 提交即秒传(实测命中)。
// - 家庭云:列表/建目录走 api.cloud.189.cn/open/family/ 带 AccessToken + Browser-Id + Signature(md5 排序参数) 签名
// (Browser-Id/AccessToken 由页面请求头捕获钩子持久化);上传走 upload.cloud.189.cn 的 /family/ 前缀接口(params 带 familyId),
// 同一套 AES/RSA/HMAC 加密。
// - JSON 字段:{path, md5, slice_md5, size};md5 可被百度/夸克/UC/小飞机/光鸭等支持 md5 的平台导入。
const TIANYI = {
WEB: 'https://cloud.189.cn',
API: 'https://api.cloud.189.cn',
UPLOAD: 'https://upload.cloud.189.cn',
UPLOAD_APPKEY: '600100422',
FAMILY_CTX_KEY: 'cloud189_family_request_context',
SLICE: 10485760, // 10MB 标准分片
_sk: '',
_skAt: 0,
_rsa: null,
_familyId: '',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
gmRequest(method, url, opts = {}) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; }
GM_xmlhttpRequest({
method,
url,
headers: Object.assign({ 'User-Agent': TIANYI.userAgent, 'Accept': 'application/json, text/plain, */*' }, opts.headers || {}),
data: opts.data,
responseType: opts.responseType || 'text',
timeout: opts.timeout || 60000,
onload: (r) => resolve(r),
onerror: () => reject(new Error('网络请求失败 @ ' + url)),
ontimeout: () => reject(new Error('请求超时 @ ' + url)),
});
});
},
isFamilySpace() {
return /\/web\/family\//.test(location.pathname);
},
getCurrentFolderId() {
if (this.isFamilySpace()) {
const m = location.pathname.match(/\/web\/family\/file\/folder\/([^/?#]+)/);
const v = m ? m[1] : '';
return (v && v !== 'home') ? v : '';
}
const m = location.pathname.match(/\/folder\/([^/?#]+)/);
const v = m ? m[1] : '';
return (!v || v === 'home') ? '-11' : v;
},
strToU8(str) {
const u8 = new Uint8Array(String(str).length);
for (let i = 0; i < String(str).length; i++) u8[i] = String(str).charCodeAt(i) & 0xff;
return u8;
},
/** UTF-8 编码的 md5(中文文件名/目录签名必须 UTF-8 字节;BAIDU.md5Hex 是 Latin-1 字节语义) */
md5Utf8(str) {
const u8 = new TextEncoder().encode(String(str));
let bin = '';
for (let i = 0; i < u8.length; i += 0x8000) bin += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000));
return BAIDU.md5Hex(bin);
},
uuid4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
},
async getSessionKey(force = false) {
if (!force && this._sk && Date.now() - this._skAt < 30 * 60 * 1000) return this._sk;
// 1) getUserBriefInfo 返回当前账号权威 sessionKey(已实测)
try {
const resp = await this.gmRequest('GET', this.WEB + '/v2/getUserBriefInfo.action', { headers: { 'Accept': 'application/json;charset=UTF-8' } });
const j = JSON.parse(resp.responseText);
if (j && j.sessionKey) { this._sk = String(j.sessionKey); this._skAt = Date.now(); return this._sk; }
} catch (e) { /* ignore */ }
// 2) sessionStorage / localStorage / cookie 兜底
try { const s = sessionStorage.getItem('sessionKey'); if (s) { this._sk = String(s); this._skAt = Date.now(); return this._sk; } } catch (e) { /* ignore */ }
try {
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
const v = localStorage.getItem(k);
if (v && String(k).toLowerCase().includes('sessionkey')) { this._sk = String(v); this._skAt = Date.now(); return this._sk; }
}
} catch (e) { /* ignore */ }
try {
for (const c of document.cookie.split(';')) {
const p = c.trim().split('=');
if (/^SESSION_KEY$/i.test(p[0])) { this._sk = p.slice(1).join('='); this._skAt = Date.now(); return this._sk; }
}
} catch (e) { /* ignore */ }
return this._sk || '';
},
async getRsaKey(force = false) {
if (!force && this._rsa && this._rsa.expire > Date.now() + 60000) return this._rsa;
const sk = await this.getSessionKey();
if (!sk) throw new Error('无法获取 SessionKey,请刷新天翼云盘页面');
const ts = String(Date.now());
const sign = BAIDU.md5Hex('AppKey=' + this.UPLOAD_APPKEY + '&Timestamp=' + ts);
const resp = await this.gmRequest('GET', this.WEB + '/api/security/generateRsaKey.action?sessionKey=' + encodeURIComponent(sk), {
headers: { 'Sign-Type': '1', 'Signature': sign, 'Timestamp': ts, 'AppKey': this.UPLOAD_APPKEY, 'SessionKey': sk, 'Accept': 'application/json' },
});
const text = resp.responseText;
if (resp.status < 200 || resp.status >= 300) throw new Error('generateRsaKey HTTP ' + resp.status);
let pubKey = '', pkId = '', expire = 0;
try { const j = JSON.parse(text); pubKey = j.pubKey || ''; pkId = j.pkId || ''; expire = parseInt(j.expire) || 0; } catch (e) { /* ignore */ }
if (!pubKey) {
pubKey = (text.match(/([^<]+)) || [])[1] || '';
pkId = pkId || (text.match(/([^<]+)) || [])[1] || '';
const em = text.match(/([^<]+));
if (em) expire = parseInt(em[1]) || 0;
}
if (!pubKey || !pkId) throw new Error('generateRsaKey 失败: ' + text.slice(0, 120));
this._rsa = {
pubKey: String(pubKey).replace(/\s+/g, ''),
pkId: String(pkId),
expire: expire || (Date.now() + 5 * 60 * 1000),
};
return this._rsa;
},
/** AES-128-ECB/PKCS7 加密 → hex 大写(复用 115 手写 AES 块加密) */
aesEcbHex(plainStr, keyStr) {
const out = P115_CRYPTO.aesEcbEncrypt(this.strToU8(plainStr), this.strToU8(keyStr));
let hex = '';
for (const b of out) hex += b.toString(16).padStart(2, '0');
return hex.toUpperCase();
},
/** HMAC-SHA1 → hex 大写(复用 115 纯 JS SHA-1 回退,原始字节;inner 需转回二进制再进 outer) */
hmacSha1Hex(keyStr, msgStr) {
const fallback = P115_CRYPTO._test.sha1HexFallback;
const keyBytes = this.strToU8(keyStr);
let k = keyBytes;
if (keyBytes.length > 64) {
const h = fallback(keyStr);
k = new Uint8Array(20);
for (let i = 0; i < 20; i++) k[i] = parseInt(h.substr(i * 2, 2), 16);
}
const ipad = new Uint8Array(64), opad = new Uint8Array(64);
for (let i = 0; i < 64; i++) { const b = i < k.length ? k[i] : 0; ipad[i] = b ^ 0x36; opad[i] = b ^ 0x5c; }
const bin = (u8) => { let s = ''; for (let i = 0; i < u8.length; i += 0x8000) s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000)); return s; };
const innerHex = fallback(bin(ipad) + msgStr);
let innerBin = '';
for (let i = 0; i < innerHex.length; i += 2) innerBin += String.fromCharCode(parseInt(innerHex.substr(i, 2), 16));
return fallback(bin(opad) + innerBin).toUpperCase();
},
/** upload.cloud.189.cn 加密请求(AES 参数 + RSA 会话钥 + HMAC 签名;纯 header 鉴权) */
async uploadRequest(uri, paramsObj) {
const sk = await this.getSessionKey();
if (!sk) throw new Error('无法获取 SessionKey,请刷新天翼云盘页面');
const rsa = await this.getRsaKey();
const uuid = Array.from({ length: 16 }, () => '0123456789abcdef'.charAt(Math.floor(Math.random() * 16))).join('');
const paramStr = Object.entries(paramsObj).map(([k, v]) => k + '=' + v).join('&');
const enc = this.aesEcbHex(paramStr, uuid);
const ts = String(Date.now());
const signature = this.hmacSha1Hex(uuid, 'SessionKey=' + sk + '&Operate=GET&RequestURI=' + uri + '&Date=' + ts + '¶ms=' + enc);
const encryptionText = P115_CRYPTO.rsaEncryptWithKey(rsa.pubKey, this.strToU8(uuid));
const resp = await this.gmRequest('GET', this.UPLOAD + uri + '?params=' + enc, {
headers: {
'X-Request-Date': ts,
'X-Request-ID': this.uuid4(),
'SessionKey': sk,
'EncryptionText': encryptionText,
'PkId': rsa.pkId,
'Signature': signature,
'Accept': 'application/json;charset=UTF-8',
},
});
let j;
try { j = JSON.parse(resp.responseText); } catch (e) { throw new Error('上传接口响应解析失败: ' + String(resp.responseText).slice(0, 150)); }
if (j.errorCode) throw new Error(j.errorCode === 'InvalidSessionKey' ? '登录已过期,请刷新天翼云盘页面' : (j.errorMsg || j.errorCode));
if (j.code !== 'SUCCESS') throw new Error(j.msg || j.errorMsg || j.error || ('上传接口失败 ' + (j.code || '') + ' ' + String(resp.responseText).slice(0, 120)));
return j;
},
/** 个人文件列表(Cookie 鉴权),返回 fileListAO */
async listFilesPersonal(folderId, pageNum = 1, pageSize = 60) {
const qs = new URLSearchParams({
noCache: String(Math.random()),
pageSize: String(pageSize),
pageNum: String(pageNum),
mediaType: '0',
folderId: String(folderId),
iconOption: '5',
orderBy: 'lastOpTime',
descending: 'true',
});
const resp = await this.gmRequest('GET', this.WEB + '/api/open/file/listFiles.action?' + qs.toString(), { headers: { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' } });
const j = JSON.parse(resp.responseText);
if (j.res_code !== 0) throw new Error(j.res_message || '获取文件列表失败');
return j.fileListAO || { fileList: [], folderList: [], count: 0 };
},
/** 家庭文件列表(AccessToken 签名),返回 fileListAO */
async listFilesFamily(folderId, pageNum = 1, pageSize = 60) {
const familyId = await this.getCurrentFamilyId();
if (!familyId) throw new Error('无法获取 familyId');
const url = this.API + '/open/family/file/listFiles.action?pageSize=' + pageSize + '&pageNum=' + pageNum +
'&mediaType=0&familyId=' + encodeURIComponent(familyId) + '&folderId=' + encodeURIComponent(folderId || '') +
'&iconOption=5&orderBy=3&descending=true';
const j = await this.familyFetchJson(url);
return j.fileListAO || { fileList: [], folderList: [], count: 0 };
},
async listFiles(folderId, pageNum, pageSize) {
return this.isFamilySpace() ? this.listFilesFamily(folderId, pageNum, pageSize) : this.listFilesPersonal(folderId, pageNum, pageSize);
},
// ---- 家庭云签名 ----
getFamilyContext() {
try {
const raw = sessionStorage.getItem(this.FAMILY_CTX_KEY) || localStorage.getItem(this.FAMILY_CTX_KEY);
if (raw) {
const j = JSON.parse(raw);
return { browserId: String(j.browserId || ''), accessToken: String(j.accessToken || '') };
}
} catch (e) { /* ignore */ }
return { browserId: '', accessToken: '' };
},
getAccessToken() {
const ctx = this.getFamilyContext();
if (ctx.accessToken) return ctx.accessToken;
try {
const t = localStorage.getItem('accessToken');
if (t) return t;
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
const v = localStorage.getItem(k);
if (k && String(k).toLowerCase().includes('accesstoken') && v) return v;
}
} catch (e) { /* ignore */ }
return '';
},
buildFamilySignature(url, extraParams) {
try {
const entries = [];
try { const u = new URL(url); for (const [k, v] of u.searchParams.entries()) entries.push([k, v]); } catch (e) { /* ignore */ }
if (extraParams && typeof extraParams === 'object') {
for (const [k, v] of Object.entries(extraParams)) entries.push([k, v == null ? '' : String(v)]);
}
entries.sort((a, b) => a[0].localeCompare(b[0]));
const timestamp = String(Date.now());
const parts = ['AccessToken=' + this.getAccessToken(), 'Timestamp=' + timestamp];
for (const [k, v] of entries) parts.push(k + '=' + v);
return { timestamp, signature: this.md5Utf8(parts.join('&')) };
} catch (e) {
return { timestamp: '', signature: '' };
}
},
familyHeaders(url, extraParams) {
const ctx = this.getFamilyContext();
const h = { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' };
const at = this.getAccessToken();
if (at) h.AccessToken = at;
if (ctx.browserId) h['Browser-Id'] = ctx.browserId;
const sig = this.buildFamilySignature(url, extraParams);
if (sig && sig.signature) { h.Signature = sig.signature; h.Timestamp = sig.timestamp; }
return h;
},
async familyFetchJson(url, extraParams, method = 'GET', body) {
const headers = this.familyHeaders(url, extraParams);
const resp = await this.gmRequest(method, url, { headers, data: body });
const j = JSON.parse(resp.responseText);
if ((j.res_code != null && j.res_code !== 0) || j.errorCode) {
throw new Error(j.errorMsg || j.res_message || '家庭接口失败');
}
return j;
},
async getCurrentFamilyId() {
if (this._familyId) return this._familyId;
const j = await this.familyFetchJson(this.API + '/open/family/manage/getFamilyList.action');
const list = j.familyInfoResp || [];
const cur = list.find(x => Number(x.useFlag) === 1) || list[0];
this._familyId = cur && cur.familyId ? String(cur.familyId) : '';
return this._familyId;
},
// ---- 目录 ----
async getFolderIdByNamePersonal(parentFolderId, folderName) {
const ao = await this.listFilesPersonal(parentFolderId, 1, 60);
const f = (ao.folderList || []).find(x => x.name === folderName);
return f ? String(f.id) : null;
},
async createFolderPersonal(parentFolderId, folderName) {
const resp = await this.gmRequest('POST', this.WEB + '/api/open/file/createFolder.action?noCache=' + Math.random(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' },
data: 'parentFolderId=' + encodeURIComponent(String(parentFolderId == null ? '' : parentFolderId)) + '&folderName=' + encodeURIComponent(folderName),
});
let j;
const t = resp.responseText;
try { j = JSON.parse(t); } catch (e) { j = {}; }
if (j.res_code === 0 && (j.id != null)) return String(j.id);
if (String(j.res_message || '').includes('已存在')) {
const id = await this.getFolderIdByNamePersonal(parentFolderId, folderName);
if (id) return id;
}
if (j.id != null) return String(j.id);
const id = await this.getFolderIdByNamePersonal(parentFolderId, folderName);
if (id) return id;
throw new Error('创建目录失败(文件夹名: ' + folderName + ')');
},
async getFamilyFolderIdByName(parentFolderId, folderName) {
try {
const familyId = await this.getCurrentFamilyId();
if (!familyId) return null;
const url = this.API + '/open/family/file/listFiles.action?pageSize=60&pageNum=1&mediaType=0&familyId=' + encodeURIComponent(familyId) +
'&folderId=' + encodeURIComponent(parentFolderId || '') + '&iconOption=5&orderBy=3&descending=true';
const j = await this.familyFetchJson(url);
const f = ((j.fileListAO && j.fileListAO.folderList) || []).find(x => x.name === folderName);
return f ? String(f.id || f.fileId) : null;
} catch (e) { return null; }
},
async createFolderFamily(parentFolderId, folderName) {
const familyId = await this.getCurrentFamilyId();
if (!familyId) throw new Error('无法获取 familyId');
const url = this.API + '/open/family/file/createFolder.action?noCache=' + Math.random();
const params = { folderName, familyId: String(familyId), parentId: String(parentFolderId == null ? '' : parentFolderId) };
const headers = this.familyHeaders(url, params);
headers['Content-Type'] = 'application/x-www-form-urlencoded';
const body = Object.entries(params).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
const resp = await this.gmRequest('POST', url, { headers, data: body });
const j = JSON.parse(resp.responseText);
if (j.id != null) return String(j.id);
if ((j.res_code != null && j.res_code !== 0) || j.errorCode) {
if (String(j.res_message || '').includes('已存在')) {
const id = await this.getFamilyFolderIdByName(parentFolderId, folderName);
if (id) return id;
}
throw new Error(j.errorMsg || j.res_message || '家庭目录创建失败');
}
const id = await this.getFamilyFolderIdByName(parentFolderId, folderName);
if (id) return id;
throw new Error('创建家庭目录失败(文件夹名: ' + folderName + ')');
},
async createFolder(parentFolderId, folderName) {
return this.isFamilySpace() ? this.createFolderFamily(parentFolderId, folderName) : this.createFolderPersonal(parentFolderId, folderName);
},
// ---- 下载链接 + Range(分片哈希计算用)----
async getFileDownloadUrl(fileId) {
try {
if (this.isFamilySpace()) {
const familyId = await this.getCurrentFamilyId();
if (!familyId) return '';
const url = this.API + '/open/family/file/getFileDownloadUrl.action?fileId=' + encodeURIComponent(String(fileId)) +
'&familyId=' + encodeURIComponent(familyId) + '&type=1';
const j = await this.familyFetchJson(url);
return j.fileDownloadUrl ? String(j.fileDownloadUrl).replace(/&/g, '&') : '';
}
const resp = await this.gmRequest('GET', this.WEB + '/api/open/file/getFileDownloadUrl.action?fileId=' + encodeURIComponent(String(fileId)), { headers: { 'Accept': 'application/json;charset=UTF-8', 'Sign-Type': '1' } });
const j = JSON.parse(resp.responseText);
return j.fileDownloadUrl || '';
} catch (e) { return ''; }
},
async downloadRange(url, offset, length) {
const resp = await this.gmRequest('GET', url, {
headers: { 'Range': 'bytes=' + offset + '-' + (offset + length - 1), 'Referer': location.href },
responseType: 'arraybuffer',
timeout: 120000,
});
if (resp.status < 200 || resp.status >= 300) throw new Error('下载片段失败 HTTP ' + resp.status);
try { return new Uint8Array(resp.response); }
catch (e) { return new Uint8Array(resp.responseText.split('').map(ch => ch.charCodeAt(0) & 0xff)); }
},
// ---- 秒传(2026 实测:必须 init(lazyCheck) → checkTransSecond 命中 → commit 三步流程;
// 直接带哈希 init+commit 会被 InfoSecurityError「md5 in black list, security check not pass」拒绝)----
async rapidUploadFile(fileName, fileSize, fileMd5, sliceMd5, parentFolderId) {
const family = this.isFamilySpace();
const md5u = String(fileMd5 || '').toUpperCase();
const smu = String(sliceMd5 || '').toUpperCase();
const initParams = {
parentFolderId: String((family && (parentFolderId == null || parentFolderId === '')) ? '0' : (parentFolderId == null ? '' : parentFolderId)),
fileName: encodeURIComponent(String(fileName || '')),
fileSize: String(Number(fileSize) || 0),
sliceSize: String(this.SLICE),
lazyCheck: '1',
};
if (family) {
const famId = await this.getCurrentFamilyId();
if (famId) initParams.familyId = String(famId);
}
const initUri = family ? '/family/initMultiUpload' : '/person/initMultiUpload';
const checkUri = family ? '/family/checkTransSecond' : '/person/checkTransSecond';
const commitUri = family ? '/family/commitMultiUploadFile' : '/person/commitMultiUploadFile';
const init = await this.uploadRequest(initUri, initParams);
const uploadFileId = (init.data && init.data.uploadFileId) || '';
if (!uploadFileId) return { hit: false, reason: '初始化未返回 uploadFileId' };
const chk = await this.uploadRequest(checkUri, { fileMd5: md5u, sliceMd5: smu, uploadFileId: String(uploadFileId) });
const exists = Number((chk.data && chk.data.fileDataExists) || 0);
if (exists !== 1) return { hit: false, reason: '未命中秒传池(服务器上不存在该内容哈希)' };
const commit = await this.uploadRequest(commitUri, {
uploadFileId: String(uploadFileId),
fileMd5: md5u,
sliceMd5: smu,
lazyCheck: '1',
opertype: '3',
});
const fid = (commit.file && (commit.file.userFileId || commit.file.fileId)) || (commit.data && commit.data.fileId) || '';
return { hit: true, fileId: String(fid || '') };
},
};
/** 安装家庭接口请求头捕获钩子(Browser-Id/AccessToken → sessionStorage/localStorage) */
function tianyiInstallFamilyHook() {
const persist = (detail = {}) => {
const headers = detail.headers || {};
const context = {
browserId: headers['Browser-Id'] || headers['browser-id'] || headers.browserId || '',
accessToken: headers.accesstoken || headers.AccessToken || '',
};
if (!context.browserId && !context.accessToken) return;
try {
const serialized = JSON.stringify({ ...context, url: detail.url || '', updatedAt: Date.now() });
sessionStorage.setItem(TIANYI.FAMILY_CTX_KEY, serialized);
localStorage.setItem(TIANYI.FAMILY_CTX_KEY, serialized);
} catch (e) { /* ignore */ }
};
document.addEventListener('cloud189-family-request', (event) => persist(event.detail || {}));
if (window.__gyMS189FamilyHookInstalled) return;
window.__gyMS189FamilyHookInstalled = true;
const script = document.createElement('script');
script.textContent = `(${function () {
if (window.__cloud189FamilyHookInstalled) return;
window.__cloud189FamilyHookInstalled = true;
function shouldCapture(url) { return typeof url === 'string' && url.indexOf('/open/family/') !== -1; }
function normalizeHeaders(input) {
const result = {};
if (!input) return result;
try {
if (typeof Headers !== 'undefined' && input instanceof Headers) { input.forEach(function (value, key) { result[key] = value; }); return result; }
} catch (e) { /* ignore */ }
if (Array.isArray(input)) { input.forEach(function (entry) { if (Array.isArray(entry) && entry.length >= 2) result[String(entry[0])] = entry[1]; }); return result; }
if (typeof input === 'object') { Object.keys(input).forEach(function (key) { result[key] = input[key]; }); return result; }
return result;
}
function emit(url, headers) {
try { document.dispatchEvent(new CustomEvent('cloud189-family-request', { detail: { url: url, headers: normalizeHeaders(headers) } })); } catch (e) { /* ignore */ }
}
if (window.fetch) {
const originalFetch = window.fetch;
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url) || '';
if (shouldCapture(url)) {
const requestHeaders = {};
if (input && input.headers) Object.assign(requestHeaders, normalizeHeaders(input.headers));
if (init && init.headers) Object.assign(requestHeaders, normalizeHeaders(init.headers));
emit(url, requestHeaders);
}
return originalFetch.apply(this, arguments);
};
}
const originalOpen = XMLHttpRequest.prototype.open;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this.__gyMs189Url = url;
this.__gyMs189Headers = {};
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
try { if (this.__gyMs189Headers) this.__gyMs189Headers[key] = value; } catch (e) { /* ignore */ }
return originalSetRequestHeader.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
if (shouldCapture(this.__gyMs189Url)) emit(this.__gyMs189Url, this.__gyMs189Headers || {});
return originalSend.apply(this, arguments);
};
}})();`;
try {
(document.documentElement || document.head || document.body).appendChild(script);
script.remove();
} catch (e) { /* ignore */ }
}
/** 勾选读取:.c-file-item 行内 ant-checkbox-input(悬停显示)+ 列表头部全选框 */
function tianyiGetCheckedItems() {
const items = [];
const rows = Array.from(document.querySelectorAll('.c-file-item'));
let headChecked = false;
try {
const headCb = Array.from(document.querySelectorAll('input.ant-checkbox-input, input[type="checkbox"]')).find(cb => !cb.closest('.c-file-item'));
if (headCb && headCb.checked && headCb.offsetParent !== null) headChecked = true;
} catch (e) { /* ignore */ }
for (const row of rows) {
try {
const cb = row.querySelector('input.ant-checkbox-input, input[type="checkbox"]');
const fileId = row.getAttribute('data-fileid') || '';
const isFolder = row.getAttribute('data-isfolder') === 'true';
const nameEl = row.querySelector('.file-item-name-div, .file-item-name');
const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '';
if (!name) continue;
if (headChecked || (cb && cb.checked)) items.push({ fileId: String(fileId), name, isFolder });
} catch (e) { /* ignore */ }
}
if (!items.length) {
// 兜底:Vue 组件内可能已把勾选状态渲染成 ant-checkbox checked(防 hover 未触发)
try {
for (const row of rows) {
const cb = row.querySelector('.ant-checkbox, .ant-checkbox-input');
if (cb && (cb.classList.contains('ant-checkbox-checked') || cb.checked)) {
const fileId = row.getAttribute('data-fileid') || '';
const isFolder = row.getAttribute('data-isfolder') === 'true';
const nameEl = row.querySelector('.file-item-name-div, .file-item-name');
const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '';
if (name) items.push({ fileId: String(fileId), name, isFolder });
}
}
} catch (e) { /* ignore */ }
}
return { all: headChecked, items };
}
/** 拉取目录全部页,建 id→信息 map */
async function tianyiListFolderMap(folderId) {
const filesMap = new Map();
const foldersMap = new Map();
let pageNum = 1;
let seen = 0;
while (true) {
const ao = await TIANYI.listFiles(folderId, pageNum, 60);
const fl = ao.fileList || [];
const dl = ao.folderList || [];
for (const f of fl) filesMap.set(String(f.id), { name: String(f.name || ''), size: Number(f.size || 0), md5: String(f.md5 || '') });
for (const d of dl) foldersMap.set(String(d.id), String(d.name || ''));
seen += fl.length + dl.length;
const count = Number(ao.count || 0);
if (!fl.length && !dl.length) break;
if (count > 0 && seen >= count) break;
pageNum++;
await sleep(CONFIG.listPageDelay);
}
return { filesMap, foldersMap };
}
/** 递归导出目录(勾选文件夹=递归) */
async function tianyiCollectFolder(folderId, prefix, files, warnMissing, statusEl, barEl, counter) {
const map = await tianyiListFolderMap(folderId);
for (const [id, d] of map.filesMap) {
const md5 = String(d.md5 || '').toUpperCase();
if (!/^[0-9A-F]{32}$/.test(md5)) { warnMissing.push(prefix + d.name + ':无 md5(可能仍在处理中)'); continue; }
const size = Number(d.size) || 0;
const f = { path: prefix + d.name, fileId: String(id), size, md5, sliceMd5: '' };
if (size <= TIANYI.SLICE) f.sliceMd5 = md5;
files.push(f);
counter.done++;
if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${f.path}`, 'info');
if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`);
await sleep(CONFIG.md5Delay);
}
for (const id of map.foldersMap.keys()) {
const fname = map.foldersMap.get(id);
await tianyiCollectFolder(id, prefix + fname + '/', files, warnMissing, statusEl, barEl, counter);
}
}
/** 勾选项 → 收集文件(当前目录文件查 map;文件夹递归) */
async function tianyiExportCollect(items, curFilesMap, files, warnMissing, statusEl, barEl, counter) {
for (const item of items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); continue; }
if (item.isFolder && item.fileId) {
if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
await tianyiCollectFolder(item.fileId, name + '/', files, warnMissing, statusEl, barEl, counter);
} else if (item.fileId) {
const d = curFilesMap.get(String(item.fileId));
if (!d) { warnMissing.push(name + ':未能匹配到列表信息'); continue; }
const md5 = String(d.md5 || '').toUpperCase();
if (!/^[0-9A-F]{32}$/.test(md5)) { warnMissing.push(name + ':无 md5(可能仍在处理中)'); continue; }
const size = Number(d.size) || 0;
const f = { path: d.name || name, fileId: String(item.fileId), size, md5, sliceMd5: '' };
if (size <= TIANYI.SLICE) f.sliceMd5 = md5;
files.push(f);
counter.done++;
if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${f.path}`, 'info');
if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(name + ':缺少文件 ID');
}
}
}
/** 下载文件计算天翼分片哈希(10MB 分片 md5 → sliceMd5;内存恒定,每片瞬存) */
async function tianyiComputeSliceMd5(file, onProgress) {
try {
const size = Number(file.size);
if (size <= TIANYI.SLICE) return file.md5 || '';
const url = await TIANYI.getFileDownloadUrl(file.fileId);
if (!url) return '';
const md5s = [];
let offset = 0;
while (offset < size) {
const len = Math.min(TIANYI.SLICE, size - offset);
const buf = await TIANYI.downloadRange(url, offset, len);
if (!buf || !buf.length) return '';
md5s.push(baiduBytesToMd5Hex(buf).toUpperCase());
offset += len;
if (onProgress) onProgress(Math.min(99, Math.round(offset / size * 100)));
}
if (onProgress) onProgress(100);
return BAIDU.md5Hex(md5s.join('\n')).toUpperCase();
} catch (e) {
mcLog('tianyi', '分片哈希计算失败: ' + e.message);
return '';
}
}
/** 天翼导出:勾选 → 收集(md5 零下载)→ 可选下载计算 sliceMd5 → JSON */
async function tianyiGenerateJson() {
let sel;
try { sel = tianyiGetCheckedItems(); } catch (e) { showToast('读取勾选失败:' + e.message, 'error'); return; }
if (!sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(天翼云盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-189-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const sliceCheck = modal.contentEl.querySelector('#gy-ms-189-gen-slice');
const wantSlice = () => sliceCheck && sliceCheck.checked;
const files = [];
const warnMissing = [];
const counter = { done: 0, total: sel.items.length };
try {
setStatus(statusEl, sel.all ? `已全选当前目录 ${sel.items.length} 项,正在收集信息...` : `正在收集 ${sel.items.length} 项...`, 'info');
const curFolderId = TIANYI.getCurrentFolderId();
const curMap = await tianyiListFolderMap(curFolderId);
await tianyiExportCollect(sel.items, curMap.filesMap, files, warnMissing, statusEl, barEl, counter);
if (!files.length) {
setStatus(statusEl, '未获取到任何文件的 md5,无法生成 JSON。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err');
modal.footerEl.innerHTML = ``;
return;
}
const need = files.filter(f => !f.sliceMd5 && Number(f.size) > TIANYI.SLICE);
if (wantSlice() && need.length) {
setStatus(statusEl, `正在下载计算天翼分片哈希(${need.length} 个文件)...`, 'info');
for (let i = 0; i < need.length; i++) {
const f = need[i];
setStatus(statusEl, `计算分片 ${i + 1}/${need.length}:${f.path}(${formatSize(f.size)})`, 'info');
const sm = await tianyiComputeSliceMd5(f, (pct) => updateProgress(barEl, Math.round(pct), `分片 ${i + 1}/${need.length}:${f.path} ${pct}%`));
if (sm) f.sliceMd5 = sm;
else warnMissing.push(f.path + ':分片哈希计算失败(下载受限),该条目仅有 md5');
}
} else if (need.length) {
for (const f of need) warnMissing.push(f.path + ':>10MB 未计算分片哈希(未勾选/取消计算),天翼秒传不可用,md5 可导入其他盘');
}
const finalFiles = files.filter(f => f.md5);
if (!finalFiles.length) {
setStatus(statusEl, '未获取到任何可用条目,无法生成 JSON。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = finalFiles.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '天翼云盘',
totalFilesCount: finalFiles.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: finalFiles.map(f => ({
path: f.path,
md5: f.md5 || '',
slice_md5: f.sliceMd5 || '',
size: Number(f.size) || 0,
})),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_天翼.json';
const withSlice = finalFiles.filter(f => f.sliceMd5).length;
modal.contentEl.innerHTML = `
已生成 ${finalFiles.length} 项秒传 JSON${withSlice ? `(${withSlice} 项含分片哈希)` : ''}${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
含 slice_md5 的 JSON 可被任意天翼账号秒传导入(>10MB 需分片哈希,≤10MB 自动=md5);md5 字段可导入百度/夸克/UC/小飞机等支持 md5 的平台。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { showToast('复制失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
try {
const blob = new Blob([jsonStr], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = exportName;
a.click();
URL.revokeObjectURL(a.href);
showToast('JSON 已下载');
} catch (e) { showToast('下载失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
mcLog('tianyi', `导出完成 ${finalFiles.length} 项`);
} catch (e) {
setStatus(statusEl, '导出失败:' + e.message, 'err');
modal.footerEl.innerHTML = ``;
mcLog('tianyi', '导出失败: ' + e.message);
}
}
/** JSON 条目解析(天翼导入专用;≤10MB 缺 slice_md5 时用 md5 顶替) */
function tianyiParseImportRows(text) {
const obj = JSON.parse(text);
const list = Array.isArray(obj.files) ? obj.files : [];
const rows = [];
const skipped = [];
for (const f of list) {
const path = String(f.path || f.name || '').trim() || 'file';
const size = Number(f.size != null ? f.size : 0);
const md5 = normalizeMd5(f.etag || f.md5 || f.fileMd5 || '');
let sliceMd5 = normalizeMd5(f.slice_md5 || f.sliceMd5 || '');
if (!path || !md5) { skipped.push(path + ':缺少 md5(32 位 hex),无法天翼秒传'); continue; }
if (!sliceMd5) {
if (size <= TIANYI.SLICE) sliceMd5 = md5;
else { skipped.push(path + ':>10MB 缺少 slice_md5(天翼秒传必需;请由天翼云盘导出并勾选"计算分片哈希",或该 JSON 仅用于其他盘导入)'); continue; }
}
rows.push({ path, md5, sliceMd5, size });
}
return { rows, skipped };
}
/** 天翼导入:目录自动创建 + initMultiUpload/commitMultiUploadFile 秒传 */
async function tianyiImportJson(rows, onProgress) {
const rootId = TIANYI.getCurrentFolderId();
const dirCache = new Map();
dirCache.set('', String(rootId == null ? '' : rootId));
const ensureDir = async (dirPath) => {
if (!dirPath) return dirCache.get('');
if (dirCache.has(dirPath)) return dirCache.get(dirPath);
const segs = dirPath.split('/').filter(Boolean);
let cur = '';
let parent = dirCache.get('');
for (const seg of segs) {
cur = cur ? cur + '/' + seg : seg;
if (dirCache.has(cur)) { parent = dirCache.get(cur); continue; }
const id = await TIANYI.createFolder(parent || '', seg);
if (!id) throw new Error('创建目录失败:' + seg);
dirCache.set(cur, id);
parent = id;
await sleep(CONFIG.mkdirDelay);
}
dirCache.set(dirPath, parent);
return parent;
};
const results = [];
let done = 0;
for (const row of rows) {
try {
const segs = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
const parent = await ensureDir(segs.join('/'));
const r = await TIANYI.rapidUploadFile(fileName, row.size, row.md5, row.sliceMd5, parent);
results.push({ name: row.path, ok: r.hit, kind: r.hit ? 'hit' : 'miss', note: r.reason || '秒传命中' });
} catch (e) {
results.push({ name: row.path, ok: false, kind: 'fail', reason: e.message });
}
done++;
const last = results[results.length - 1];
if (onProgress) onProgress(Math.round((done / rows.length) * 100), `秒传 ${done}/${rows.length}:${last.name} → ${last.ok ? '命中' : (last.reason || last.note)}`);
await sleep(CONFIG.transferDelay);
}
const failures = results.filter(r => !r.ok && r.kind !== 'skip').map(r => `${r.name}:${r.reason || r.note}`);
const skippedDetail = results.filter(r => !r.ok && r.kind === 'skip').map(r => `${r.name}:${r.reason}`);
const summary = {
total: results.length,
success: results.filter(r => r.ok).length,
fail: results.filter(r => !r.ok && r.kind !== 'skip').length,
skipped: results.filter(r => !r.ok && r.kind === 'skip').length,
failures,
skippedDetail,
};
showToast(`天翼秒传完成:${summary.success}/${summary.total} 命中`, summary.fail || summary.skipped ? 'warning' : 'ok');
mcLog('tianyi', `导入完成 ${summary.success}/${summary.total}`);
return summary;
}
/** 天翼导入模态框 */
function showTianyiImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到天翼云盘(当前目录)',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:md5=32 位 hex、slice_md5=天翼分片哈希、path、size)。
天翼秒传需要 md5 + slice_md5:≤10MB 文件 slice_md5 自动 = md5(其他盘带 md5 的 JSON 即可秒传);>10MB 缺少 slice_md5 的条目将跳过(需由天翼导出时勾选"计算分片哈希")。path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-189-import-ta');
const fileNameEl = modal.contentEl.querySelector('#gy-ms-189-import-filename');
const statusEl = modal.contentEl.querySelector('#gy-ms-189-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-189-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-189-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
try { attachImportFilePicker(modal, ta, fileNameEl, statusEl); } catch (e) { /* ignore */ }
runBtn.onclick = async () => {
let rows = [], skipped = [];
try {
const r = tianyiParseImportRows(ta.value);
rows = r.rows;
skipped = r.skipped;
} catch (e) {
setStatus(statusEl, 'JSON 解析失败:' + e.message, 'err');
return;
}
if (!rows.length) {
setStatus(statusEl, '没有可导入的条目(需要 md5;>10MB 还需 slice_md5)' + (skipped.length ? '\n示例提示:' + skipped.slice(0, 3).join('\n') : ''), 'err');
return;
}
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await tianyiImportJson(rows, (pct, label) => {
updateProgress(barEl, pct, label);
setStatus(statusEl, label, 'info');
});
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) {
detailLines.push('', '========== 跳过(未提交接口) ==========');
detailLines.push(...summary.skippedDetail);
}
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
if (warn) {
try {
const logBtn = document.createElement('button');
logBtn.className = 'gy-ms-btn gy-ms-btn-outline';
logBtn.textContent = '导出日志';
logBtn.style.marginTop = '8px';
logBtn.onclick = () => { exportMcLog(); showToast('日志已导出'); };
statusEl.parentNode.insertBefore(logBtn, statusEl.nextSibling);
} catch (e) { /* ignore */ }
}
};
}
// ============================== 联通云盘(pan.wo.cn / 沃盘)v1.10:零下载导出 md5 ==============================
// 协议来源:2026-10 实测(wohome dispatcher:AES-CBC(accessToken[:16] + 固定IV) + header md5 签名 + Accesstoken 头)
// 哈希来源:GetDownloadUrlV2 → 下载 URL → Range: bytes=0-0 读响应头 ETag(单分片文件 ≤~10MB 时 = 完整内容 MD5;多分片大文件为 OSS 分片格式 md5hex-N,原样记录 etag 字段)
// 边界:联通无哈希秒传接口(上传 upload2C 必须携带文件本体、无预检;列表 fileUniqueValue/sha256 恒空),故仅支持导出,导入提示不支持
const WOPAN = {
BASE: 'https://panservice.mail.wo.cn',
CLIENT_ID: '1001000021',
IV: 'wNSOYIB1k1DjY5lA', // AES-CBC 固定 IV
UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
_token: '',
_tokenAt: 0,
/** 从 localStorage.wocloudinfo(base64 → JSON字符串 → JSON)取 accessToken,缓存 30 分钟 */
getToken() {
if (this._token && Date.now() - this._tokenAt < 30 * 60 * 1000) return this._token;
try {
const raw = localStorage.getItem('wocloudinfo');
if (!raw) return '';
const decoded = decodeURIComponent(escape(atob(raw)));
const obj = JSON.parse(JSON.parse(decoded));
this._token = (obj && obj.user && obj.user.token) || '';
this._tokenAt = Date.now();
} catch (e) { this._token = ''; }
return this._token;
},
gmReq(method, url, opts = {}) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('当前环境无 GM_xmlhttpRequest')); return; }
GM_xmlhttpRequest({
method,
url,
headers: Object.assign({
'User-Agent': WOPAN.UA,
'Origin': 'https://pan.wo.cn',
'Referer': 'https://pan.wo.cn/',
'Accept': 'application/json, text/plain, */*',
}, opts.headers || {}),
data: opts.data,
responseType: opts.responseType || 'text',
timeout: opts.timeout || 60000,
onload: (r) => resolve(r),
onerror: () => reject(new Error('网络请求失败 @ ' + url)),
ontimeout: () => reject(new Error('请求超时 @ ' + url)),
});
});
},
/** 标准 PKCS7 填充(无论是否对齐都补 1..16) */
pkcs7(data) {
const mod = data.length % 16;
const pad = 16 - mod;
const out = new Uint8Array(data.length + pad);
out.set(data, 0);
for (let i = data.length; i < out.length; i++) out[i] = pad;
return out;
},
unpad(data) {
if (!data || !data.length) return data;
const p = data[data.length - 1];
if (p >= 1 && p <= 16) {
let ok = true;
for (let i = data.length - p; i < data.length; i++) { if (data[i] !== p) { ok = false; break; } }
if (ok) return data.slice(0, data.length - p);
}
return data;
},
keyBytes() {
const tok = this.getToken();
const u8 = new Uint8Array(16);
for (let i = 0; i < 16; i++) u8[i] = (tok.charCodeAt(i) || 0) & 0xff;
return u8;
},
ivBytes() {
return new TextEncoder().encode(WOPAN.IV);
},
b64ToBytes(b64) {
const bin = atob(b64);
const u8 = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
return u8;
},
/** 加密请求参数:JSON → UTF-8 → PKCS7 → AES-CBC → base64 */
encryptParam(json) {
const data = this.pkcs7(new TextEncoder().encode(JSON.stringify(json)));
const enc = P115_CRYPTO._test.aesCbcEncrypt(data, this.keyBytes(), this.ivBytes());
let s = '';
for (const b of enc) s += String.fromCharCode(b);
return btoa(s);
},
/** 解密响应 DATA:base64 → AES-CBC → 去 PKCS7 → UTF-8 */
decryptData(b64) {
const plain = P115_CRYPTO._test.aesCbcDecrypt(this.b64ToBytes(b64), this.keyBytes(), this.ivBytes());
return new TextDecoder('utf-8').decode(this.unpad(plain));
},
/** wohome dispatcher 通用调用(本地实现加密+签名,同浏览器前端协议) */
async dispatcher(key, param) {
const token = this.getToken();
if (!token) throw new Error('未获取到联通云盘令牌(请先登录并刷新页面)');
const resTime = Date.now();
const reqSeq = Math.floor(Math.random() * 8999) + 1e5;
const sign = BAIDU.md5Hex(String(key) + resTime + reqSeq + 'wohome' + '');
const bodyObj = {
header: { key, resTime, reqSeq, channel: 'wohome', version: '', sign },
body: { param: param ? this.encryptParam(param) : '', secret: true },
};
if (!param) delete bodyObj.body.param;
const r = await this.gmReq('POST', this.BASE + '/wohome/dispatcher', {
headers: { 'Accesstoken': token, 'Content-Type': 'application/json;charset=UTF-8' },
data: JSON.stringify(bodyObj),
});
let j;
try { j = JSON.parse(r.responseText); } catch (e) { throw new Error('联通接口响应解析失败: ' + String(r.responseText).slice(0, 150)); }
if (j.STATUS !== '200') throw new Error('联通接口状态异常: ' + (j.MSG || j.STATUS));
const rsp = j.RSP || {};
if (rsp.RSP_CODE !== '0000') {
if (rsp.RSP_CODE === '9999') throw new Error('令牌已过期,请刷新联通云盘页面后重试');
throw new Error('联通接口失败: ' + (rsp.RSP_DESC || rsp.RSP_CODE));
}
const data = rsp.DATA;
if (data === undefined || data === null || data === '') return data;
if (typeof data === 'string') {
const b64 = (data.startsWith('"') && data.endsWith('"')) ? data.slice(1, -1) : data;
try { return JSON.parse(this.decryptData(b64)); } catch (e) { /* 明文 DATA */ }
try { return JSON.parse(data); } catch (e2) { return data; }
}
return data;
},
/** 文件列表(pageNum 从 0 开始,100/页) */
async listFiles(dirId, spaceType, familyId, pageNum) {
const param = {
spaceType: String(spaceType || '0'),
parentDirectoryId: String(dirId || '0'),
pageNum: pageNum || 0,
pageSize: 100,
sortRule: 0,
clientId: WOPAN.CLIENT_ID,
};
if (String(spaceType) === '1') param.familyId = String(familyId || '');
const data = await this.dispatcher('QueryAllFiles', param);
return (data && data.files) || [];
},
/** 批量取下载链接:fidList → { fid: downloadUrl } */
async getDownloadUrls(fidList) {
const data = await this.dispatcher('GetDownloadUrlV2', {
type: '1', fidList: fidList, clientId: WOPAN.CLIENT_ID,
});
const list = (data && data.list) || (Array.isArray(data) ? data : []);
const map = {};
for (const it of list) if (it && it.fid && it.downloadUrl) map[it.fid] = it.downloadUrl;
return map;
},
/** 对下载 URL 发 Range: bytes=0-0,读响应头 ETag(零下载)
* - 单分片文件(≤~10MB):ETag = 完整内容 MD5(32 hex)
* - 多分片大文件:ETag = OSS 分片格式 "md5hex-N"(非内容 MD5,原样记录到 etag 字段供兼容)
*/
async getEtag(url) {
const r = await this.gmReq('GET', url, {
headers: { 'Range': 'bytes=0-0', 'Accept': '*/*' },
timeout: 30000,
});
const hdrs = String(r.responseHeaders || '');
const m = hdrs.match(/etag:\s*"?([0-9a-fA-F-]+)"?/i);
if (m) return m[1].toLowerCase();
return '';
},
};
/** 找到文件列表页 Vue 组件(含 tableData/multipleSelection/cloudType/familyId) */
function wopanFindPageVm() {
const walk = (seed) => {
let vm = seed;
for (let i = 0; i < 12 && vm; i++) {
if (vm.$data && Array.isArray(vm.$data.tableData)) return vm;
vm = vm.$parent;
}
return null;
};
try {
const tr = document.querySelector('tr.el-table__row');
if (tr) {
const v = walk(tr.__vue__ || (tr.firstElementChild && tr.firstElementChild.__vue__));
if (v) return v;
}
} catch (e) { /* ignore */ }
try {
const table = document.querySelector('.el-table');
if (table) {
const v = walk(table.__vue__ || (table.firstElementChild && table.firstElementChild.__vue__));
if (v) return v;
}
} catch (e) { /* ignore */ }
try {
const cont = document.querySelector('[class*="file-list-container"], [class*="operation-container"]');
if (cont) {
const v = walk(cont.__vue__ || (cont.firstElementChild && cont.firstElementChild.__vue__));
if (v) return v;
}
} catch (e) { /* ignore */ }
return null;
}
/** 勾选读取:页面 Vue 组件的 multipleSelection(el-table 选中行,含表头全选)+ allChecked/selectALlFileModel */
function wopanGetCheckedItems() {
const vm = wopanFindPageVm();
if (!vm) return { all: false, items: [], state: null };
const d = vm.$data;
let items = [];
try { items = (d.multipleSelection || []).filter(x => x && x.id); } catch (e) { /* ignore */ }
if (!items.length) {
try {
const tr = document.querySelector('tr.el-table__row');
let b = tr && tr.__vue__ && tr.__vue__.$parent;
if (b && b.store && b.store.states && b.store.states.selection && b.store.states.selection.length) {
items = b.store.states.selection.filter(x => x && x.id);
}
} catch (e) { /* ignore */ }
}
let all = false;
try { all = !!(d.allChecked || d.selectALlFileModel); } catch (e) { /* ignore */ }
if (!all && !items.length) {
try {
const headInput = document.querySelector('.el-table__header input[type="checkbox"]');
if (headInput && headInput.checked) all = true;
} catch (e) { /* ignore */ }
}
let state = { spaceType: '0', familyId: '', currentDirId: '0' };
try {
state.spaceType = String(d.cloudType != null ? d.cloudType : '0');
state.familyId = String(d.familyId || '');
const first = (d.tableData || []).find(x => x && x.parentDirectoryId);
state.currentDirId = first ? String(first.parentDirectoryId) : '0';
} catch (e) { /* ignore */ }
return { all, items, state };
}
/** 拉取目录全部页(文件+文件夹) */
async function wopanListDirAll(dirId, state) {
const out = [];
let pageNum = 0;
for (;;) {
const list = await WOPAN.listFiles(dirId, state.spaceType, state.familyId, pageNum);
out.push(...list);
if (list.length < 100) break;
pageNum++;
await sleep(CONFIG.listPageDelay);
}
return out;
}
/** 导出收集:顶层条目(文件→记录 fid;文件夹→递归扫描) */
async function wopanExportCollect(items, files, warnMissing, state, statusEl, barEl, counter, prefix) {
for (const item of items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; }
const path = (prefix || '') + name;
if (Number(item.type) === 0) {
if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
try {
const sub = await wopanListDirAll(item.id, state);
await wopanExportCollect(sub, files, warnMissing, state, statusEl, barEl, counter, path + '/');
} catch (e) {
warnMissing.push(path + ':扫描文件夹失败 ' + e.message);
}
} else if (item.fid) {
files.push({ path, fid: String(item.fid), size: Number(item.size) || 0 });
counter.done++;
if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info');
if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(path + ':缺少 fid');
counter.done++;
}
}
}
/** 联通导出:勾选/全选 → 收集(目录递归)→ 批量取下载链接 → Range 读 ETag 取 md5(零下载)→ JSON */
async function wopanGenerateJson() {
const sel = wopanGetCheckedItems();
if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹(可勾选列表左上角全选框)', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(联通云盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
零下载取哈希:文件列表 → 下载链接 → 响应头 ETag。单分片文件(≤~10MB)ETag = 完整内容 MD5;多分片大文件 ETag 为 OSS 分片格式,原样记录到 JSON 的 etag 字段。
注意:联通云盘无哈希秒传接口(上传必须携带文件本体、列表不返回哈希),本 JSON 的 md5 可用于百度/夸克/UC/小飞机等支持 md5 的平台导入。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-wo-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
try {
let topItems = [];
if (sel.all) {
setStatus(statusEl, `已全选当前目录,正在拉取全部文件列表(${sel.state.currentDirId === '0' ? '根目录' : '当前目录'})...`, 'info');
topItems = await wopanListDirAll(sel.state.currentDirId, sel.state);
setStatus(statusEl, `当前目录共 ${topItems.length} 项,正在收集...`, 'info');
} else {
topItems = sel.items;
setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info');
}
if (!topItems.length) {
setStatus(statusEl, '当前目录没有可导出的内容。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const counter = { done: 0, total: topItems.length };
await wopanExportCollect(topItems, files, warnMissing, sel.state, statusEl, barEl, counter);
if (!files.length) {
setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err');
modal.footerEl.innerHTML = ``;
return;
}
setStatus(statusEl, `已收集 ${files.length} 个文件,正在批量获取下载链接并读取 ETag(零下载)...`, 'info');
updateProgress(barEl, 0, '取哈希 0/' + files.length);
const BATCH = 20;
const withMd5 = [];
const withEtag = [];
for (let i = 0; i < files.length; i += BATCH) {
const batch = files.slice(i, i + BATCH);
let urlMap = {};
try { urlMap = await WOPAN.getDownloadUrls(batch.map(f => f.fid)); }
catch (e) { for (const f of batch) warnMissing.push(f.path + ':获取下载链接失败 ' + e.message); continue; }
for (const f of batch) {
const url = urlMap[f.fid];
if (!url) { warnMissing.push(f.path + ':无下载链接'); continue; }
try {
const etag = await WOPAN.getEtag(url);
if (/^[0-9a-f]{32}$/.test(etag)) {
f.md5 = etag;
f.etag = '';
withMd5.push(f);
setStatus(statusEl, `取 md5 ${withMd5.length}/${files.length}:${f.path}(零下载)`, 'info');
updateProgress(barEl, Math.round((withMd5.length + withEtag.length) / files.length * 100), `取哈希 ${withMd5.length + withEtag.length}/${files.length}`);
} else if (/^[0-9a-f]{32}-[0-9]+$/.test(etag)) {
// 多分片大文件:ETag 为 OSS 分片格式,非内容 MD5 → 原样记录 etag 字段
f.md5 = '';
f.etag = etag;
withEtag.push(f);
warnMissing.push(f.path + ':>10MB 分片文件,ETag 为分片格式(' + etag + ')非内容 MD5,已记录 etag 字段;如需跨盘秒传请先在支持下载计算的平台导出');
setStatus(statusEl, `取 etag ${withMd5.length + withEtag.length}/${files.length}:${f.path}(分片格式)`, 'info');
updateProgress(barEl, Math.round((withMd5.length + withEtag.length) / files.length * 100), `取哈希 ${withMd5.length + withEtag.length}/${files.length}`);
} else {
warnMissing.push(f.path + ':响应头无有效 ETag');
}
} catch (e) {
warnMissing.push(f.path + ':取哈希失败 ' + e.message);
}
await sleep(CONFIG.md5Delay);
}
}
if (!withMd5.length && !withEtag.length) {
setStatus(statusEl, '未获取到任何文件的哈希,无法生成 JSON。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = (withMd5.concat(withEtag)).reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '联通云盘',
totalFilesCount: withMd5.length + withEtag.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: (withMd5.concat(withEtag)).map(f => {
const o = {
path: f.path,
md5: f.md5 || '',
size: Number(f.size) || 0,
};
if (f.etag) o.etag = f.etag; // 大文件分片 ETag(OSS 格式 md5hex-N,非内容 MD5,仅供兼容尝试)
return o;
}),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_联通.json';
modal.contentEl.innerHTML = `
已生成 ${withMd5.length + withEtag.length} 项秒传 JSON:${withMd5.length} 项含内容 MD5(零下载)${withEtag.length ? `,${withEtag.length} 项仅记录分片 ETag` : ''}${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
md5 = 完整内容 MD5(下载链接响应头 ETag,仅单分片文件 ≤~10MB 可用);大文件(多分片)ETag 为 OSS 分片格式(md5hex-N),已写入 etag 字段供各盘兼容尝试,非内容 MD5。
联通云盘无秒传接口,本 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入(仅 md5 字段条目)。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { showToast('复制失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
try {
const blob = new Blob([jsonStr], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = exportName;
a.click();
URL.revokeObjectURL(a.href);
showToast('JSON 已下载');
} catch (e) { showToast('下载失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
mcLog('wopan', `导出完成 ${withMd5.length + withEtag.length} 项`);
} catch (e) {
setStatus(statusEl, '导出失败:' + e.message, 'err');
modal.footerEl.innerHTML = ``;
mcLog('wopan', '导出失败: ' + e.message);
}
}
// ---- 样式注入 ----
function insertStyle() {
if (document.getElementById('gy-ms-style')) return;
const style = document.createElement('style');
style.id = 'gy-ms-style';
style.textContent = STYLES;
(document.head || document.documentElement).appendChild(style);
style.textContent += `
.gy-ms-dropdown-item:hover{background:#f0fdf4;color:#059669;}
.gy-ms-main-btn:hover{background:#059669;}
`;
}
// ============================== 移动云盘平台(v1.11:yun.139.com,SHA256 秒传,协议实测 2026) ==============================
// 协议来源:139FastLink(个人版,MIT)移植——/file/list 列表自带 contentHash(SHA256);/file/create 秒传(exist/rapidUpload 命中)
// 边界:列表自带 64hex SHA256 零下载导出;导入仅支持 SHA256(32 位 MD5 JSON 无法在移动云盘秒传)
// JSON 说明:etag 字段存 64 位 SHA256(仅供移动云盘自身导入;其他平台按 md5 解析会跳过该条目)
const Y139 = (() => {
// ---- MD5(mcloud-sign 签名,移植自 139FastLink)----
function md5(inputString) {
const hc = "0123456789abcdef";
function rh(n) { let j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; }
function ad(x, y) { const l = (x & 0xFFFF) + (y & 0xFFFF), m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); }
function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); }
function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); }
function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); }
function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); }
function sb(x) {
const bytes = new TextEncoder().encode(x);
let i;
const nblk = ((bytes.length + 8) >> 6) + 1;
const blks = new Array(nblk * 16);
for (i = 0; i < nblk * 16; i++) blks[i] = 0;
for (i = 0; i < bytes.length; i++) blks[i >> 2] |= bytes[i] << ((i % 4) * 8);
blks[i >> 2] |= 0x80 << ((i % 4) * 8);
blks[nblk * 16 - 2] = bytes.length * 8;
return blks;
}
let i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd;
for (i = 0; i < x.length; i += 16) {
olda = a; oldb = b; oldc = c; oldd = d;
a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); b = ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983);
a = ff(a, b, c, d, x[i + 8], 7, 1770035416); d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); b = ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302);
a = gg(a, b, c, d, x[i + 5], 5, -701558691); d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); b = gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = hh(a, b, c, d, x[i + 5], 4, -378558); d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); b = hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222); c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189);
a = hh(a, b, c, d, x[i + 9], 4, -640364487); d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); b = ii(b, c, d, a, x[i + 5], 21, -57434055);
a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = ii(a, b, c, d, x[i + 8], 6, 1873313359); d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); b = ii(b, c, d, a, x[i + 9], 21, -343485551);
a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd);
}
return rh(a) + rh(b) + rh(c) + rh(d);
}
function encodeURIComponent2(str) {
return encodeURIComponent(str)
.replace(/\+/g, "%20").replace(/!/g, "%21").replace(/'/g, "%27")
.replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A");
}
function calSign(body, ts, randStr) {
let b = encodeURIComponent2(body);
b = b.split("").sort().join("");
b = btoa(unescape(encodeURIComponent(b)));
const res = md5(b) + md5(ts + ":" + randStr);
return md5(res).toUpperCase();
}
function getCookie(name) {
const m = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith(name + '='));
return m ? decodeURIComponent(m.slice(name.length + 1)) : "";
}
function getLS(key) { try { return localStorage.getItem(key) || ""; } catch (e) { return ""; } }
function parseLS(key) { try { return JSON.parse(getLS(key)); } catch (e) { return null; } }
function randomString(len) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let s = "";
for (let i = 0; i < len; i++) s += chars.charAt(Math.floor(Math.random() * chars.length));
return s;
}
class Api {
constructor() { this.init(); }
init() {
this.authorization = getCookie("authorization");
this.account = this.extractAccount();
const router = parseLS("routerInfo") || [];
const personal = router.find(r => r.modName === "personal");
this.host = (personal && personal.httpsUrl) || "https://personal-kd-njs.yun.139.com/hcy";
this.clientInfo = "||9|7.18.0|edge||" + (getLS("TY_DEVICE_N_ID") || "unknown") + "||windows 10||zh-CN|||ZWRnZQ==||";
this.appChannel = "10000034";
}
extractAccount() {
try {
const b64 = this.authorization.replace(/^Basic\s+/i, "");
const parts = atob(b64).split(":");
if (parts.length >= 2) return parts[1];
} catch (e) { /* ignore */ }
try { return atob(getLS("encryptAccount") || ""); } catch (e) { return ""; }
}
getCurrentFolderId() {
const id = getLS("currentCatalogID");
return id && id !== "root" ? id : "/";
}
async sendRequest(method, path, body) {
const bodyStr = body ? JSON.stringify(body) : "";
const now = new Date();
const pad = n => String(n).padStart(2, "0");
const tsStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const rand = randomString(16);
const sign = calSign(bodyStr, tsStr, rand);
const resp = await fetch(this.host + path, {
method, headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": this.authorization,
"x-yun-api-version": "v1",
"x-yun-client-info": this.clientInfo,
"x-yun-app-channel": this.appChannel,
"mcloud-sign": `${tsStr},${rand},${sign}`,
}, body: bodyStr, credentials: "include",
});
let data;
try { data = await resp.json(); } catch (e) { throw new Error("响应不是 JSON: " + (await resp.text()).slice(0, 200)); }
if (data.success !== true && data.code !== "0000") {
throw new Error(`接口错误(${path}): ${data.code} ${data.message || data.resultMsg}`);
}
return data;
}
async getOnePageFileList(parentFileId, pageCursor) {
const data = await this.sendRequest("POST", "/file/list", {
pageInfo: { pageSize: 100, pageCursor: pageCursor || null },
orderBy: "updated_at", orderDirection: "DESC", parentFileId,
imageThumbnailStyleList: ["Small", "Large"],
});
const items = (data.data && data.data.items) || [];
return { items, nextPageCursor: (data.data && data.data.nextPageCursor) || null };
}
async getFileList(parentFileId) {
const all = [];
let cursor = null;
do {
const page = await this.getOnePageFileList(parentFileId, cursor);
all.push(...page.items);
cursor = page.nextPageCursor;
if (cursor) await sleep(CONFIG.listPageDelay);
} while (cursor);
return all;
}
async mkdir(parentFileId, folderName) {
try {
const data = await this.sendRequest("POST", "/file/create", {
fileRenameMode: "force_rename", type: "folder", name: folderName, parentFileId, description: "",
commonAccountInfo: { account: this.account, accountType: 1 },
});
const fileId = data.data && (data.data.fileId || data.data.existFileId);
return { fileId, folderName, success: !!fileId };
} catch (e) {
return { fileId: null, folderName, success: false, error: e.message };
}
}
// 秒传:按 SHA256 hash 创建文件,exist/rapidUpload 为 true 即成功
async getFile(fileInfo, parentFileId) {
try {
const data = await this.sendRequest("POST", "/file/create", {
fileRenameMode: "auto_rename", contentType: "application/oct-stream", type: "file",
name: fileInfo.fileName, size: fileInfo.size,
contentHashAlgorithm: "SHA256", contentHash: fileInfo.etag,
partInfos: [{ parallelHashCtx: { partOffset: 0 }, partNumber: 1, partSize: fileInfo.size }],
parentFileId,
commonAccountInfo: { account: this.account, accountType: 1 },
});
const d = data.data || {};
if (d.exist === true || d.rapidUpload === true) {
return [true, null, d.fileId || null];
}
return [false, "云端无此文件,无法秒传", null];
} catch (e) {
return [false, "请求失败: " + e.message, null];
}
}
}
return { Api };
})();
/** 找到移动云盘文件列表 Vue 组件($data.dataList 含 contentID 的真实列表,优先当前目录) */
function y139FindPageVm() {
let currentId = '';
try { currentId = localStorage.getItem('currentCatalogID') || ''; } catch (e) { /* ignore */ }
const seen = new Set();
let best = null, bestScore = -1;
const walk = (obj, depth) => {
if (depth > 8 || !obj || seen.has(obj)) return;
seen.add(obj);
try {
const dl = obj.$data && obj.$data.dataList;
if (Array.isArray(dl)) {
const real = dl.filter(i => i && i.contentID);
let score = real.length;
if (obj.$data && obj.$data.catalogID === currentId) score += 100000;
if (score > bestScore) { bestScore = score; best = obj; }
}
} catch (e) { /* ignore */ }
if (obj.$children) obj.$children.forEach(c => walk(c, depth + 1));
};
try {
const container = document.querySelector(".document_main, .body_main");
if (container && container.__vue__) walk(container.__vue__, 0);
if (!best) {
for (const el of document.querySelectorAll("*")) {
if (el.__vue__) { seen.clear(); walk(el.__vue__, 0); if (best) break; }
}
}
} catch (e) { /* ignore */ }
return best || null;
}
/** 移动云盘勾选读取:dataList 中 isSelected===true 的条目(Vue 内存态) */
function y139GetCheckedItems() {
const vm = y139FindPageVm();
if (!vm) return { all: false, items: [], state: null };
const dl = (vm.$data && vm.$data.dataList) || [];
const items = dl.filter(i => i && i.isSelected === true && i.contentID).map(i => ({
contentID: i.contentID, contentName: i.contentName, contentSize: i.contentSize,
contentType: i.contentType, isSelected: true,
}));
let all = false;
try { all = !!(vm.$data && vm.$data.isAllSelected); } catch (e) { /* ignore */ }
if (!all && items.length && dl.length && items.length === dl.length) all = true;
return { all, items, state: { currentDirId: y139GetCurrentFolderId() } };
}
/** 移动云盘当前目录 ID(localStorage.currentCatalogID,根为 '/') */
function y139GetCurrentFolderId() {
try {
const id = localStorage.getItem('currentCatalogID');
return id && id !== 'root' ? id : '/';
} catch (e) { return '/'; }
}
/** 拉取移动云盘目录全部页(items 含 name/type/contentHash/fileId/size) */
async function y139ListDirAll(api, parentFileId) {
const out = [];
let cursor = null;
for (;;) {
const page = await api.getOnePageFileList(parentFileId, cursor);
out.push(...page.items);
cursor = page.nextPageCursor;
if (!cursor) break;
await sleep(CONFIG.listPageDelay);
}
return out;
}
/** 移动云盘导出收集:顶层文件→SHA256;文件夹→API 递归扫描 */
async function y139ExportCollect(api, items, files, warnMissing, statusEl, barEl, counter, prefix) {
for (const item of items) {
const name = String(item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; }
const path = (prefix || '') + name;
if (item.type === 'folder') {
if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
try {
const sub = await y139ListDirAll(api, item.fileId);
await y139ExportCollect(api, sub, files, warnMissing, statusEl, barEl, counter, path + '/');
} catch (e) {
warnMissing.push(path + ':扫描文件夹失败 ' + e.message);
}
} else {
const sha256 = normalizeSha256(item.contentHash);
if (sha256) {
files.push({ path, etag: sha256, size: Number(item.size) || 0 });
counter.done++;
if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info');
if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(path + ':缺少 contentHash(SHA256)');
counter.done++;
}
}
}
}
/** 移动云盘导出:勾选 → 顶层用当前目录 API 列表补齐哈希 → 文件夹递归 → JSON */
async function y139GenerateJson() {
const sel = y139GetCheckedItems();
if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(移动云盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
零下载取哈希:文件列表接口自带 contentHash(SHA256,64位hex)。
注意:移动云盘秒传使用 SHA256,本 JSON 的 etag 字段为 64 位 SHA256,仅供移动云盘自身导入;其他平台按 32 位 MD5 解析会跳过这些条目。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-y139-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
try {
const api = new Y139.Api();
let topItems = [];
if (sel.all) {
setStatus(statusEl, '已全选当前目录,正在拉取全部文件列表...', 'info');
topItems = await y139ListDirAll(api, y139GetCurrentFolderId());
setStatus(statusEl, `当前目录共 ${topItems.length} 项,正在收集...`, 'info');
} else {
// 顶层条目:用当前目录 API 列表按 contentID 补齐 type/contentHash(页面 dataList 无哈希)
setStatus(statusEl, '正在获取当前目录文件信息(补齐 SHA256)...', 'info');
const curDirId = y139GetCurrentFolderId();
let apiMap = {};
try {
const items = await y139ListDirAll(api, curDirId);
apiMap = {};
items.forEach(i => { if (i && i.fileId) apiMap[i.fileId] = i; });
} catch (e) {
warnMissing.push('获取当前目录列表失败:' + e.message);
}
topItems = sel.items.map(s => {
const meta = apiMap[s.contentID];
if (meta) return meta;
const isFolder = String(s.contentType) === '1';
return isFolder
? { name: s.contentName, type: 'folder', fileId: s.contentID, contentHash: '', size: Number(s.contentSize) || 0 }
: { name: s.contentName, type: 'file', fileId: s.contentID, contentHash: '', size: Number(s.contentSize) || 0 };
});
setStatus(statusEl, `正在收集 ${topItems.length} 项...`, 'info');
}
if (!topItems.length) {
setStatus(statusEl, '当前目录没有可导出的内容。', 'err');
modal.footerEl.innerHTML = ``;
return;
}
const counter = { done: 0, total: topItems.length };
await y139ExportCollect(api, topItems, files, warnMissing, statusEl, barEl, counter);
if (!files.length) {
setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '移动云盘',
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: files.map(f => ({ path: f.path, etag: f.etag, gcid: '', size: Number(f.size) || 0 })),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_移动云盘.json';
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
etag = 64 位 SHA256(列表接口 contentHash,零下载)。
移动云盘秒传使用 SHA256,本 JSON 仅供移动云盘自身导入(其他平台按 32 位 MD5 解析会跳过)。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { showToast('复制失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
try {
const blob = new Blob([jsonStr], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = exportName;
a.click();
URL.revokeObjectURL(a.href);
showToast('JSON 已下载');
} catch (e) { showToast('下载失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
mcLog('y139', `导出完成 ${files.length} 项`);
} catch (e) {
setStatus(statusEl, '导出失败:' + e.message, 'err');
modal.footerEl.innerHTML = ``;
mcLog('y139', '导出失败: ' + e.message);
}
}
/** 移动云盘导入:JSON(files 的 etag=64位SHA256)→ 建目录 → /file/create 秒传 */
async function y139ImportJson(rawJson, onProgress) {
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
const skippedDetail = [];
let skipped = 0;
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const sha256 = normalizeSha256(f.etag || f.sha256);
const size = Number(f.size != null ? f.size : 0);
if (!path) continue;
if (!sha256) {
skippedDetail.push(`${path}:无 64 位 SHA256(移动云盘秒传仅支持 SHA256;百度等导出的 32 位 MD5 无法在此导入)`);
skipped++;
continue;
}
rows.push({ path, sha256, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的条目(需要 etag=64 位 SHA256)');
const api = new Y139.Api();
const rootFolder = api.getCurrentFolderId();
let success = 0, fail = 0;
const failures = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `移动云盘秒传 ${i + 1}/${total}:${row.path}` });
try {
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootFolder;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dir = await api.mkdir(parent, dirName);
if (!dir.fileId) throw new Error(dir.error || '创建目录失败');
dirCache.set(key, dir.fileId);
parent = dir.fileId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
const r = await api.getFile({ etag: row.sha256, fileName, size: row.size }, parent);
if (r[0]) {
success++;
} else {
fail++;
failures.push(`${row.path}:${r[1] || '未命中秒传库存'}`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('y139', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('y139', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 移动云盘导入模态框 */
function showY139ImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到移动云盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag=64 位 SHA256(移动云盘列表 contentHash)、path、size)。
移动云盘秒传使用 SHA256:仅本平台导出的 JSON(etag 为 64 位 hex)可秒传;百度/123 等导出的 32 位 MD5 条目会跳过。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-y139-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-y139-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-y139-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-y139-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await y139ImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 64 位 SHA256) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
};
}
// ============================== 123云盘平台(v1.11:123pan.com,MD5 秒传,协议实测 2026) ==============================
// 协议来源:123FastLink(Baoqing/Chaofan/lipkiat/JiangKaslana,MIT)移植——/b/api/file/list/new 自带 Etag(MD5);/b/api/file/upload_request 秒传(Reuse 命中)
// 边界:列表自带 32hex MD5 零下载导出;导入走 upload_request(etag+size 命中即秒传)
// JSON 说明:etag 存 32 位 MD5——123 导出的 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入
const P123 = (() => {
class Api {
constructor() {
this.host = 'https://' + location.host;
try { this.authToken = localStorage['authorToken'] || ''; } catch (e) { this.authToken = ''; }
try { this.loginUuid = localStorage['LoginUuid'] || ''; } catch (e) { this.loginUuid = ''; }
}
buildURL(path, queryParams) {
const qs = new URLSearchParams(queryParams || {}).toString();
return `${this.host}${path}?${qs}`;
}
async sendRequest(method, path, queryParams, body) {
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': document.location.href,
};
const response = await fetch(this.buildURL(path, queryParams), { method, headers, body, credentials: 'include' });
let data;
try { data = await response.json(); } catch (e) { throw new Error('123接口响应不是 JSON'); }
if (data.code !== 0) throw new Error(data.message || ('123接口错误 code=' + data.code));
return data;
}
async getOnePageFileList(parentFileId, page) {
const urlParams = {
driveId: '0', limit: '100', next: '0', orderBy: 'file_name', orderDirection: 'asc',
parentFileId: String(parentFileId || 0), trashed: 'false', SearchData: '', Page: String(page || 1),
OnlyLookAbnormalFile: '0', event: 'homeListFile', operateType: '1', inDirectSpace: 'false',
};
const data = await this.sendRequest('GET', '/b/api/file/list/new', urlParams);
return { items: (data.data && data.data.InfoList) || [], total: (data.data && data.data.Total) || 0 };
}
/** 秒传:etag+size 命中 Reuse 即成功 */
async fastUpload(fileInfo) {
try {
const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({ ...fileInfo, RequestSource: null }));
const d = response.data || {};
if (!d.Reuse) return [false, '云端无此文件,无法秒传', null];
return [true, null, (d.Info && d.Info.FileId) || null];
} catch (error) {
return [false, '请求失败: ' + error.message, null];
}
}
async mkdir(parentFileId, folderName) {
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,
}));
const folderFileId = response.data && response.data.Info && response.data.Info.FileId;
return { folderFileId, folderName, success: !!folderFileId };
} catch (e) {
return { folderFileId: null, folderName, success: false, error: e.message };
}
}
/** 当前父目录 ID:优先表格 dataSource 的 ParentFileId(yun.123pan.cn 新站无 sessionStorage.filePath),兜底 URL/sessionStorage 旧方案 */
getParentFileId() {
// 1. 新站:表格 dataSource 行自带 ParentFileId(当前目录 = 任一行 ParentFileId;根目录为 0)
try {
const fiber = p123FindTableFiber();
if (fiber) {
const ds = fiber.memoizedProps && fiber.memoizedProps.dataSource;
if (Array.isArray(ds) && ds.length) {
const pid = ds[0] && ds[0].ParentFileId;
if (pid != null) return String(pid);
}
}
} catch (e) { /* ignore */ }
// 2. URL homeFilePath 参数(旧方案兜底)
try {
const urlHomeFilePath = new URLSearchParams(window.location.search).get('homeFilePath');
if (urlHomeFilePath) {
const arr = urlHomeFilePath.split(',').filter(Boolean);
if (arr.length) return String(arr[arr.length - 1]);
}
} catch (e) { /* ignore */ }
// 3. sessionStorage.filePath(更旧方案兜底)
try {
const rawFilePath = sessionStorage.getItem('filePath') || sessionStorage['filePath'];
if (rawFilePath) {
const parsed = typeof rawFilePath === 'string' ? JSON.parse(rawFilePath) : rawFilePath;
if (parsed && Array.isArray(parsed.homeFilePath) && parsed.homeFilePath.length) {
return String(parsed.homeFilePath[parsed.homeFilePath.length - 1]);
}
}
} catch (e) { /* ignore */ }
return '0';
}
}
return { Api };
})();
/** 定位 123 云盘 antd 表格 fiber(memoizedProps 含 dataSource/rowSelection/columns) */
function p123FindTableFiber() {
const isTableFiber = (node) => {
const p = node && node.memoizedProps;
return !!(p && Array.isArray(p.dataSource) && p.rowSelection && Array.isArray(p.columns));
};
const findInElement = (el) => {
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) return null;
let node = el[fiberKey] && el[fiberKey].return;
for (let i = 0; i < 12 && node; i++, node = node.return) {
if (isTableFiber(node)) return node;
}
return null;
};
// 1. 语义化容器快速定位
const containerSelectors = [
'.ant-table-wrapper', '.file-table-list-wrapper',
'[class*="table-list-wrapper"]', '[class*="table-module"]', '[class*="ant-table"]',
];
for (const selector of containerSelectors) {
for (const el of document.querySelectorAll(selector)) {
const fiber = findInElement(el);
if (fiber) return fiber;
}
}
// 2. 兜底:从 React 根容器沿 fiber 树遍历(与 class 名完全解耦)
const roots = [];
const pushRoot = (el) => {
if (!el) return;
const containerKey = Object.keys(el).find(k => k.startsWith('__reactContainer$'));
if (containerKey && el[containerKey]) roots.push(el[containerKey]);
};
for (const selector of ['#app', '#root', '#__next', '#main', '#content']) pushRoot(document.querySelector(selector));
if (!roots.length) {
for (const el of document.querySelectorAll('*')) { pushRoot(el); if (roots.length >= 3) break; }
}
for (const root of roots) {
const stack = [root];
let visited = 0;
while (stack.length && visited < 100000) {
const node = stack.pop();
visited++;
if (isTableFiber(node)) return node;
if (node.child) stack.push(node.child);
if (node.sibling) stack.push(node.sibling);
}
}
return null;
}
/** 123 勾选读取:表格内存态 dataSource + selectedRowKeys(antd rowSelection,含虚拟列表未渲染行) */
function p123GetCheckedItems() {
const fiber = p123FindTableFiber();
if (!fiber) return { all: false, items: [] };
const p = fiber.memoizedProps;
const dataSource = Array.isArray(p.dataSource) ? p.dataSource : [];
const selectedRowKeys = (p.rowSelection && Array.isArray(p.rowSelection.selectedRowKeys)) ? p.rowSelection.selectedRowKeys : [];
const rowKey = p.rowKey;
const keyOf = (item) => {
if (typeof rowKey === 'string') return item[rowKey];
if (typeof rowKey === 'function') return rowKey(item);
return item.FileId != null ? item.FileId : (item.keys != null ? item.keys : item.id);
};
let items = [];
if (selectedRowKeys.length && dataSource.length) {
const keySet = new Set(selectedRowKeys);
items = dataSource.filter(it => keySet.has(keyOf(it)));
} else {
items = dataSource.filter(it => it.checked);
}
items = items.filter(it => it && it.FileId != null).map(it => ({
FileId: it.FileId, FileName: it.FileName, Size: it.Size, Type: it.Type, Etag: it.Etag,
}));
return { all: false, items };
}
/** 123 当前父目录 ID(同步,sessionStorage/URL) */
function p123GetCurrentParentId() {
try { return new P123.Api().getParentFileId(); } catch (e) { return '0'; }
}
/** 拉取 123 目录全部页(InfoList 含 FileName/FileId/Type/Size/Etag) */
async function p123ListDirAll(api, parentFileId) {
const out = [];
const first = await api.getOnePageFileList(parentFileId, 1);
out.push(...first.items);
const total = first.total;
if (total > 100) {
const times = Math.ceil(total / 100);
for (let page = 2; page <= times; page++) {
const p = await api.getOnePageFileList(parentFileId, page);
out.push(...p.items);
await sleep(CONFIG.listPageDelay);
}
}
return out;
}
/** 123 导出收集:顶层文件→Etag(MD5);文件夹→API 递归扫描 */
async function p123ExportCollect(api, items, files, warnMissing, statusEl, barEl, counter, prefix) {
for (const item of items) {
const name = String(item.FileName || item.name || '');
if (!name) { warnMissing.push('(未命名条目)'); counter.done++; continue; }
const path = (prefix || '') + name;
if (Number(item.Type) === 1) {
if (statusEl) setStatus(statusEl, `正在扫描文件夹:${name}...`, 'info');
try {
const sub = await p123ListDirAll(api, item.FileId);
await p123ExportCollect(api, sub, files, warnMissing, statusEl, barEl, counter, path + '/');
} catch (e) {
warnMissing.push(path + ':扫描文件夹失败 ' + e.message);
}
} else {
const md5 = normalizeMd5(item.Etag);
if (md5) {
files.push({ path, etag: md5, size: Number(item.Size) || 0 });
counter.done++;
if (statusEl) setStatus(statusEl, `收集 ${counter.done}/${counter.total}:${path}`, 'info');
if (barEl) updateProgress(barEl, Math.round(counter.done / Math.max(1, counter.total) * 100), `收集 ${counter.done}/${counter.total}`);
await sleep(CONFIG.md5Delay);
} else {
warnMissing.push(path + ':缺少 Etag(MD5)');
counter.done++;
}
}
}
}
/** 123 导出:勾选(页面 dataSource 自带 Etag)→ 文件夹递归 → JSON */
async function p123GenerateJson() {
const sel = p123GetCheckedItems();
if (!sel.all && !sel.items.length) { showToast('请先在页面勾选要导出的文件或文件夹', 'warning'); return; }
const modal = openModal({
title: '生成秒传 JSON(123云盘)',
icon: ICONS.generate,
width: 640,
content(el) {
el.innerHTML = `
准备中...
零下载取哈希:文件列表自带 Etag(MD5,32位hex)。
跨盘秒传:本 JSON 的 etag 为 32 位 MD5,可在百度/夸克/UC/小飞机等支持 md5 的平台导入。
`;
},
footer() { return ''; },
});
const statusEl = modal.contentEl.querySelector('#gy-ms-p123-gen-status');
const barEl = modal.contentEl.querySelector('.gy-ms-bar');
const files = [];
const warnMissing = [];
try {
const api = new P123.Api();
setStatus(statusEl, `正在收集 ${sel.items.length} 项...`, 'info');
const counter = { done: 0, total: sel.items.length };
await p123ExportCollect(api, sel.items, files, warnMissing, statusEl, barEl, counter);
if (!files.length) {
setStatus(statusEl, '未获取到任何文件(可能勾选的全是空文件夹)。' + (warnMissing.length ? '\n' + warnMissing.slice(0, 5).join('\n') : ''), 'err');
modal.footerEl.innerHTML = ``;
return;
}
const totalSize = files.reduce((s, f) => s + (Number(f.size) || 0), 0);
const json = {
scriptVersion: SCRIPT_VERSION,
platform: '123云盘',
totalFilesCount: files.length,
totalSize,
formattedTotalSize: formatSize(totalSize),
files: files.map(f => ({ path: f.path, etag: f.etag, gcid: '', size: Number(f.size) || 0 })),
};
const jsonStr = JSON.stringify(json, null, 2);
const exportName = CONFIG.defaultExportName + '_123.json';
modal.contentEl.innerHTML = `
已生成 ${files.length} 项秒传 JSON${warnMissing.length ? `;${warnMissing.length} 项未完整` : ''}
etag = 32 位 MD5(列表 Etag,零下载)。
123 导出的 JSON 可在百度/夸克/UC/小飞机等支持 md5 的平台导入。
${escapeHtml(jsonStr)}
`;
modal.footerEl.innerHTML = `
`;
modal.footerEl.querySelector('[data-act="copy"]').onclick = () => {
try { GM_setClipboard(jsonStr); showToast('秒传 JSON 已复制到剪贴板'); }
catch (e) { showToast('复制失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="download"]').onclick = () => {
try {
const blob = new Blob([jsonStr], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = exportName;
a.click();
URL.revokeObjectURL(a.href);
showToast('JSON 已下载');
} catch (e) { showToast('下载失败:' + e.message, 'error'); }
};
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
mcLog('p123', `导出完成 ${files.length} 项`);
} catch (e) {
setStatus(statusEl, '导出失败:' + e.message, 'err');
modal.footerEl.innerHTML = ``;
mcLog('p123', '导出失败: ' + e.message);
}
}
/** 123 导入:JSON(files 的 etag/md5=32位MD5)→ 建目录 → upload_request 秒传 */
async function p123ImportJson(rawJson, onProgress) {
let obj;
try { obj = JSON.parse(rawJson); } catch (e) { throw new Error('不是合法 JSON'); }
const rows = [];
const skippedDetail = [];
let skipped = 0;
for (const f of (obj.files || [])) {
const path = String(f.path || f.name || '').trim();
const md5 = normalizeMd5(f.etag || f.md5);
const size = Number(f.size != null ? f.size : 0);
if (!path) continue;
if (!md5) {
skippedDetail.push(`${path}:无 32 位 MD5(123 秒传需要 MD5;阿里导出的仅含 sha1 无法导入)`);
skipped++;
continue;
}
rows.push({ path, md5, size });
}
if (!rows.length) throw new Error('JSON 中没有可导入的条目(需要 etag/md5=32 位 MD5)');
const api = new P123.Api();
const rootFolder = api.getParentFileId();
let success = 0, fail = 0;
const failures = [];
const total = rows.length;
const dirCache = new Map();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
onProgress({ percent: Math.round((i / Math.max(1, total)) * 100), text: `123秒传 ${i + 1}/${total}:${row.path}` });
try {
const dirSegments = dirSegmentsFromPath(row.path);
const fileName = basenameFromPath(row.path);
let parent = rootFolder;
let mkdirOk = true;
for (const dirName of dirSegments) {
const key = `${parent}::${dirName}`;
if (dirCache.has(key)) { parent = dirCache.get(key); continue; }
try {
const dir = await api.mkdir(parent, dirName);
if (!dir.success || !dir.folderFileId) throw new Error(dir.error || '创建目录失败');
dirCache.set(key, dir.folderFileId);
parent = dir.folderFileId;
await sleep(CONFIG.mkdirDelay);
} catch (e) {
mkdirOk = false;
failures.push(`${row.path}:创建目录 ${dirName} 失败(${e.message})`);
break;
}
}
if (!mkdirOk) { fail++; continue; }
const r = await api.fastUpload({
driveId: 0, etag: row.md5, fileName, parentFileId: parent, size: row.size, type: 0, duplicate: 1,
});
if (r[0]) {
success++;
} else {
fail++;
failures.push(`${row.path}:${r[1] || '未命中秒传库存'}`);
}
} catch (e) {
fail++;
failures.push(`${row.path}:秒传失败(${e.message})`);
}
await sleep(CONFIG.transferDelay);
}
onProgress({ percent: 100, text: `完成:成功 ${success},失败 ${fail}${skipped ? `,跳过 ${skipped}` : ''}` });
mcLog('p123', '导入完成:成功 ' + success + ' 失败 ' + fail + ' 跳过 ' + skipped);
if (failures.length) mcLog('p123', '失败明细:' + failures.slice(0, 8).join(' | '));
return { total, success, fail, skipped, failures, skippedDetail };
}
/** 123 导入模态框 */
function showP123ImportModal() {
const modal = openModal({
title: '保存秒传 JSON 到 123云盘',
icon: ICONS.import,
width: 640,
content(el) {
el.innerHTML = `
可粘贴 JSON 或选择单个 .json 文件(files:etag/md5=32 位 MD5、path、size)。
123 秒传使用 MD5:本平台导出的 JSON(etag=32 位 MD5)、以及百度/夸克/UC/小飞机/城通等导出的含 md5 的 JSON 均可导入;仅含 sha1 的条目(如阿里导出)会跳过。
path 中的目录会自动创建。
${IMPORT_FILE_ROW_HTML}
`;
},
footer(el) {
el.innerHTML = `
`;
},
});
const ta = modal.contentEl.querySelector('#gy-ms-p123-import-ta');
const statusEl = modal.contentEl.querySelector('#gy-ms-p123-import-status');
const barEl = modal.contentEl.querySelector('#gy-ms-p123-import-bar');
const detailEl = modal.contentEl.querySelector('#gy-ms-p123-import-detail');
const runBtn = modal.footerEl.querySelector('[data-act="run"]');
modal.footerEl.querySelector('[data-act="close"]').onclick = () => modal.overlay.remove();
attachImportFilePicker(modal, ta, modal.contentEl.querySelector('#gy-ms-import-filename'), statusEl);
runBtn.onclick = async () => {
const vr = validateImportJson(ta.value);
if (!vr.ok) { setStatus(statusEl, `校验失败:${vr.message}`, 'err'); return; }
runBtn.disabled = true;
runBtn.textContent = '导入中...';
barEl.style.display = 'block';
detailEl.style.display = 'none';
let summary;
try {
summary = await p123ImportJson(ta.value, (p) => { updateProgress(barEl, p.percent, p.text); setStatus(statusEl, p.text, 'info'); });
} catch (e) {
setStatus(statusEl, `导入失败:${e.message}`, 'err');
runBtn.disabled = false;
runBtn.textContent = '开始导入';
return;
}
runBtn.disabled = false;
runBtn.textContent = '开始导入';
const detailLines = [];
if (summary.failures.length) detailLines.push(...summary.failures);
if (summary.skippedDetail && summary.skippedDetail.length) { detailLines.push('', '========== 跳过(无 32 位 MD5) =========='); detailLines.push(...summary.skippedDetail); }
if (detailLines.length) { detailEl.value = detailLines.join('\n'); detailEl.style.display = 'block'; }
const warn = summary.fail > 0 || summary.skipped > 0;
setStatus(statusEl, `秒传结果:共 ${summary.total} 条,成功 ${summary.success} 条,失败 ${summary.fail} 条${summary.skipped ? `,跳过 ${summary.skipped} 条` : ''}。`, warn ? 'warn' : 'ok');
};
}
// ============================== 初始化 ==============================
function init() {
// openapi 授权页:OAuth 自动处理(不依赖 PLATFORM)
try { if (typeof BAIDU !== 'undefined') BAIDU.oauthPageHook(); } catch (e) { /* ignore */ }
if (!PLATFORM) return; // 非目标平台不运行
loadSettings();
insertStyle();
// 115:新版注入页面钩子拦截 webapi 列表响应(勾选匹配数据源);旧版显示提示切新版
if (isP115()) { if (p115PlatformGate()) p115InjectPageHook(); }
// 天翼:安装家庭接口请求头捕获钩子(Browser-Id/AccessToken 持久化,家庭云签名用)
if (isTianyi()) { try { tianyiInstallFamilyHook(); } catch (e) { /* ignore */ } }
// 勾选状态追踪(用户脚本层,与 pageHook 解耦)
document.addEventListener('change', function (e) {
try {
if (isGuangya()) {
const input = e.target;
if (!input || input.tagName !== 'INPUT' || !/swangpan-checkbox__input/.test(input.className || '')) return;
const row = input.closest('.swangpan-file-list-table__row');
const checked = !!(input.checked || input.getAttribute('aria-checked') === 'true');
const st = guangyaGetPageState();
if (!st) return;
if (!row) {
st.selectAll = checked;
st.selectedNames = {};
} else {
const nameEl = row.querySelector('.swangpan-file-list-table__titleCell');
const name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '';
if (name) {
st.selectedNames = st.selectedNames || {};
st.selectedNames[name] = checked ? 1 : 0;
}
}
st.selectionVersion = (st.selectionVersion || 0) + 1;
} else if (isXunlei()) {
// 迅雷勾选记录到本地 state(DOM 兜底由 xunleiGetCheckedItems 直接读 checkbox)
const input = e.target;
const st = xunleiGetPageState();
if (input && input.tagName === 'INPUT' && input.type === 'checkbox') {
st.selectionVersion = (st.selectionVersion || 0) + 1;
}
}
} catch (err) { /* ignore */ }
}, true);
// 幂等挂载按钮(蓝奏云走独立 iframe 注入流程,其余平台走通用工具栏注入)
if (isLanzou()) {
lanzouSetupIframe();
setTimeout(lanzouSetupIframe, 1200);
setTimeout(lanzouSetupIframe, 3000);
} else {
const tryAdd = () => {
if (document.querySelector('button img[alt="upload"], [class*="file-list-table"], .ant-dropdown-trigger, [class*="SourceListItem"], [class*="file-list-toolbar"], [class*="wp-s-pan-table"], [class*="node-card"], .adrive-create-button, [class*="actions--"], [class*="actions-"], button.upload-btn, button.btn-file, .file-list, [class*="mod-action-wrap"], [class*="layout-toolbar"], .title-bar, [class*="title-bar"], iframe[name="mainframe"], iframe[src*="item=files"], iframe[src*="mydisk.php"], [class*="file-name-responsive"], .ct-toolbar, .ct-row, .ct-list, .c-file-item, [class*="file-head"], [class*="operation-container"], .el-table, .document_main, .document_top_upload_button, .home-operator-button-group')) {
addToolbarButton();
return;
}
setTimeout(tryAdd, 600);
};
let obsTimer = null;
const obs = new MutationObserver(() => {
if (obsTimer) return;
obsTimer = setTimeout(() => {
obsTimer = null;
addToolbarButton();
}, 500);
});
try {
obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
} catch (e) { /* ignore */ }
setTimeout(tryAdd, 800);
setTimeout(tryAdd, 2500);
setTimeout(tryAdd, 5000);
}
// 暴露调试对象
try {
const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window;
w.__GYMS = {
CONFIG, generateJson, showImportModal, getCheckedItems, getLastParentId,
getPageState: isGuangya() ? guangyaGetPageState : (isBaidu() ? (() => ({})) : xunleiGetPageState),
SCRIPT_VERSION, PLATFORM,
};
if (isXunlei()) {
w.__GYMS.xunleiGetCheckedItems = xunleiGetCheckedItems;
w.__GYMS.xunleiGetFileList = xunleiGetFileList;
}
if (isBaidu()) {
w.__GYMS.BAIDU = BAIDU;
w.__GYMS.baiduGenerateJson = baiduGenerateJson;
w.__GYMS.baiduImportJson = baiduImportJson;
w.__GYMS.getBdstoken = () => BAIDU.getBdstoken();
}
if (isAliyun()) {
w.__GYMS.ALIYUN = ALIYUN;
w.__GYMS.aliyunGenerateJson = aliyunGenerateJson;
w.__GYMS.aliyunImportJson = aliyunImportJson;
w.__GYMS.aliyunWalkFolder = aliyunWalkFolder;
}
if (isQuark()) {
w.__GYMS.QUARK = QUARK;
w.__GYMS.quarkGenerateJson = quarkGenerateJson;
w.__GYMS.quarkImportJson = quarkImportJson;
w.__GYMS.quarkWalkFolder = quarkWalkFolder;
}
if (isUc()) {
w.__GYMS.UC = UC;
w.__GYMS.ucGenerateJson = ucGenerateJson;
w.__GYMS.ucImportJson = ucImportJson;
w.__GYMS.ucWalkFolder = ucWalkFolder;
}
if (isWeiyun()) {
w.__GYMS.WEIYUN = WEIYUN;
w.__GYMS.weiyunGenerateJson = weiyunGenerateJson;
w.__GYMS.weiyunImportJson = weiyunImportJson;
w.__GYMS.weiyunWalkFolder = weiyunWalkFolder;
}
if (isFeejii()) {
w.__GYMS.FEEJII = FEEJII;
w.__GYMS.feejiiGenerateJson = feejiiGenerateJson;
w.__GYMS.feejiiImportJson = feejiiImportJson;
w.__GYMS.feejiiWalkFolder = feejiiWalkFolder;
w.__GYMS.showFeejiiImportModal = showFeejiiImportModal;
}
if (isIlanzou()) {
w.__GYMS.ILANZOU = ILANZOU;
w.__GYMS.ilanzouGenerateJson = ilanzouGenerateJson;
w.__GYMS.ilanzouImportJson = ilanzouImportJson;
w.__GYMS.ilanzouWalkFolder = ilanzouWalkFolder;
w.__GYMS.showIlanzouImportModal = showIlanzouImportModal;
}
if (isLanzou()) {
w.__GYMS.LANZOU = LANZOU;
w.__GYMS.lanzouGenerateJson = lanzouGenerateJson;
w.__GYMS.lanzouRunExport = lanzouRunExport;
w.__GYMS.parseLanzouSizeStr = parseLanzouSizeStr;
w.__GYMS.lanzouSetupIframe = lanzouSetupIframe;
w.__GYMS.lanzouGetCheckedRows = lanzouGetCheckedRows;
w.__GYMS.lanzouInjectCheckboxes = lanzouInjectCheckboxes;
}
if (isP115()) {
w.__GYMS.ONE15 = ONE15;
w.__GYMS.P115_CRYPTO = P115_CRYPTO;
w.__GYMS.p115GenerateJson = p115GenerateJson;
w.__GYMS.showP115ImportModal = showP115ImportModal;
w.__GYMS.p115ImportJson = p115ImportJson;
w.__GYMS.p115GetCheckedItems = p115GetCheckedItems;
w.__GYMS.p115GetLastCid = p115GetLastCid;
}
if (isCtfile()) {
w.__GYMS.CTFILE = CTFILE;
w.__GYMS.ctfileGenerateJson = ctfileGenerateJson;
w.__GYMS.ctfileImportJson = ctfileImportJson;
w.__GYMS.ctfileGetCheckedItems = ctfileGetCheckedItems;
w.__GYMS.showCtfileImportModal = showCtfileImportModal;
}
if (isTianyi()) {
w.__GYMS.TIANYI = TIANYI;
w.__GYMS.tianyiGenerateJson = tianyiGenerateJson;
w.__GYMS.tianyiImportJson = tianyiImportJson;
w.__GYMS.tianyiGetCheckedItems = tianyiGetCheckedItems;
w.__GYMS.showTianyiImportModal = showTianyiImportModal;
}
if (isWopan()) {
w.__GYMS.WOPAN = WOPAN;
w.__GYMS.wopanGenerateJson = wopanGenerateJson;
w.__GYMS.wopanGetCheckedItems = wopanGetCheckedItems;
w.__GYMS.wopanFindPageVm = wopanFindPageVm;
}
if (isY139()) {
w.__GYMS.Y139 = Y139;
w.__GYMS.y139GenerateJson = y139GenerateJson;
w.__GYMS.y139GetCheckedItems = y139GetCheckedItems;
w.__GYMS.y139FindPageVm = y139FindPageVm;
w.__GYMS.y139ImportJson = y139ImportJson;
}
if (isP123()) {
w.__GYMS.P123 = P123;
w.__GYMS.p123GenerateJson = p123GenerateJson;
w.__GYMS.p123GetCheckedItems = p123GetCheckedItems;
w.__GYMS.p123FindTableFiber = p123FindTableFiber;
w.__GYMS.p123ImportJson = p123ImportJson;
}
w.__GYMS.pdsProxyFetchProof = pdsProxyFetchProof; // 阿里导入夸克/UC 中转取 proof 块(调试/复用)
w.__GYMS.pdsCookieAutoFetch = pdsCookieAutoFetch; // 夸克/UC 一键取 Cookie 回填(调试/复用)
w.__GYMS.showAliyunImportModal = showAliyunImportModal; // 调试/复用
} catch (e) { /* ignore */ }
// 夸克/UC 页:检测"阿里导入一键获取 Cookie"通道(在目标站取登录态后回填)
try { setTimeout(pdsCookieAutoFetch, 600); } catch (e) { /* ignore */ }
// 首启云令牌授权提示(百度网盘页,未选择过才弹)
if (isBaidu() && BAIDU.needsConsentPrompt()) {
try {
setTimeout(() => showCloudConsentModal(), 1200);
} catch (e) { /* ignore */ }
}
}
// 尽早注入页面拦截脚本(仅光鸭需要)
if (isGuangya()) {
if (document.documentElement) {
guangyaInjectPageHook();
} else {
document.addEventListener('DOMContentLoaded', guangyaInjectPageHook, { once: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();