// ==UserScript== // @name 光鸭云盘 秒传工具 (txt 解析) // @namespace https://github.com/anomalyco/opencode // @version 1.9.17 // @description 导航栏"云添加"右侧按钮;仅当当前目录存在以 秒传.txt 结尾的文档时才显示并高亮提示引导秒传;点开解析条目勾选后"开始秒传"(新版两步流程)。弹窗内可一键复制 credentials.json 供上传工具使用。弹窗内可打开「秒传分享清单」(HTTP 服务提供),点选某剧自动填入页面云添加转存。 // @author opencode // @supportURL https://wpa.qq.com/msgrd?v=3&uin=2223515969&site=qq&menu=yes // @match https://www.guangyapan.com/* // @match https://gy.airp0.pw/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setClipboard // @connect api.guangyapan.com // @connect h5.guangyapan.com // @connect guangyacdn.com // @connect 127.0.0.1 // @connect localhost // @connect * // @run-at document-end // ==/UserScript== (function () { 'use strict'; const API_BASE = 'https://api.guangyapan.com'; const CONCURRENCY = 4; const FLASH_DOC_FILE_RE = /秒传\.txt$/i; // 秒传文档文件名匹配模式(以 秒传.txt 结尾即可) const FLASH_DOC_EXTS = ['.txt']; // 光鸭云盘 client_id(与账号绑定,存在 localStorage key `credentials_` 里)。 // 换账号时改这一个地方即可。获取方式:登录后浏览器 Console 跑 // Object.keys(localStorage).find(k => k.startsWith('credentials_'))?.replace('credentials_','') const CLIENT_ID = 'aMe-8VSlkrbQXpUR'; const LS_CRED_KEY = 'credentials_' + CLIENT_ID; // 秒传分享清单来源:HTTP 服务返回 JSON 数组 [{name, url}, ...]。 // 用户没有秒传文档时,可从弹窗「📚 秒传分享清单」里选一部,自动填入页面「云添加」转存到当前目录再秒传。 const FLASH_SHARE_LIST_URL = 'http://ali.ismewen.com:16666/sharelist'; // ===================================================================== // Auth // ===================================================================== function getAuthHeaders() { const credRaw = localStorage.getItem(LS_CRED_KEY); if (!credRaw) throw new Error('未登录:localStorage 没有 ' + LS_CRED_KEY); let token; try { const cred = JSON.parse(credRaw); token = cred.access_token; } catch (e) { throw new Error('credentials JSON 解析失败: ' + e.message); } if (!token) throw new Error('credentials.access_token 为空'); const did = localStorage.getItem('swangpan_web_device_id'); const smid = localStorage.getItem('shumei_id:v1'); if (!did || !smid) throw new Error('未登录或 localStorage 缺 did/smid'); return { 'authorization': 'Bearer ' + token, 'did': did, 'smid': smid, 'dt': '4', 'content-type': 'application/json', 'accept': 'application/json, text/plain, */*', 'origin': location.origin, 'referer': location.origin + '/' }; } function apiPost(path, body) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: API_BASE + path, headers: getAuthHeaders(), data: JSON.stringify(body), responseType: 'json', onload: (r) => { try { const j = typeof r.response === 'string' ? JSON.parse(r.response) : r.response; if (r.status !== 200) return reject(new Error('HTTP ' + r.status + ': ' + (j && j.msg || r.responseText))); if (j && j.code !== undefined && j.code !== 0 && j.code !== 156) { return reject(new Error('server code=' + j.code + ' msg=' + j.msg)); } resolve(j); } catch (e) { reject(e); } }, onerror: (e) => reject(new Error('network: ' + (e.error || e.responseText || 'unknown'))) }); }); } function apiGet(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, headers: { 'referer': location.origin + '/', 'accept': '*/*' }, responseType: 'text', onload: (r) => { if (r.status >= 200 && r.status < 300) resolve(r.response); else reject(new Error('HTTP ' + r.status + ': ' + (r.responseText || '').slice(0, 100))); }, onerror: (e) => reject(new Error('network: ' + (e.error || 'unknown'))) }); }); } async function tryFlash(entry, parentId) { const r1 = await apiPost('/userres/v1/get_res_center_token', { capacity: 2, name: entry.name, res: { fileSize: entry.size }, parentId: parentId || '' }); const taskId = r1.data.taskId; const r2 = await apiPost('/userres/v1/check_can_flash_upload', { taskId, gcid: entry.gcid, cid: entry.cid }); return { instant: !!r2.data.canFlashUpload, taskId }; } // ===================================================================== // Context: URL hash → parentId / kind // ===================================================================== function getCurrentCtx() { const hash = location.hash || ''; // /home//-name[/-name...]* — match 最末尾的数字 ID let m = hash.match(/^\/?#?\/(?:home|folder|drive)\/(?:[^\/]+\/)*(\d+)/); if (m) return { parentId: m[1], kind: 'folder', label: '当前文件夹' }; const cat = hash.match(/\/home\/(all|video|img|audio|doc|installer|recent)(?:\/|$)/); if (cat) return { parentId: '', kind: 'category', label: '当前: ' + cat[1] + '(分类视图)' }; const sp = hash.match(/\/(transfer|publish|trash)/); if (sp) return { parentId: '', kind: 'system', label: '当前: ' + sp[1] }; const pv = hash.match(/\/preview\/(\d+)/); if (pv) return { parentId: '', kind: 'preview', fileId: pv[1] }; return { parentId: '', kind: 'root', label: '根目录' }; } // ===================================================================== // Folder utilities // ===================================================================== async function listFolder(parentId) { // orderBy:3 按修改时间(最新在前),与光鸭 UI 默认一致——刚上传的秒传文档排最前, // 一次 API 即命中;分页遍历兜底,避免大目录(>200 文件)漏检。 const all = []; let page = 0; while (true) { const r = await apiPost('/userres/v1/file/get_file_list', { parentId: parentId || '', pageSize: 200, page: page, orderBy: 3, sortType: 1 }); const list = (r && r.data && r.data.list) || []; all.push(...list); if (list.length < 200) break; page++; if (page > 20) break; // 安全上限,避免死循环 } return all; } // ===================================================================== // Concurrency pool // ===================================================================== async function runPool(items, limit, worker, onItemDone) { let i = 0; const runners = Array.from({ length: limit }, async () => { while (true) { const idx = i++; if (idx >= items.length) return; try { const res = await worker(items[idx], idx); onItemDone(idx, { ok: true, ...res }); } catch (e) { onItemDone(idx, { ok: false, error: e.message || String(e) }); } } }); await Promise.all(runners); } // ===================================================================== // CSS // ===================================================================== GM_addStyle(` #gy-fab { margin-left: 8px; height: 36px; padding: 0 15px; border-radius: 8px; background: #fff; color: #222; border: 1px solid #e8e8e8; cursor: pointer; font-size: 14px; font-weight: 400; display: inline-flex; align-items: center; justify-content: center; box-shadow: none; transition: border-color .15s ease, color .15s ease; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } #gy-fab:hover { border-color: #4f46e5; color: #4f46e5; } #gy-fab .fab-icon { margin-right: 6px; font-size: 18px; line-height: 0; display: inline-flex; align-items: center; } /* 检测到秒传文档时的强调状态 */ #gy-fab.gy-fab-active { border-color: #7c3aed; color: #4f46e5; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); box-shadow: 0 0 0 3px rgba(124,58,237,.18), 0 2px 10px rgba(124,58,237,.25); animation: gy-fab-pulse 1.8s ease-in-out infinite; font-weight: 600; } @keyframes gy-fab-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(124,58,237,.18), 0 2px 10px rgba(124,58,237,.25); } 50% { box-shadow: 0 0 0 6px rgba(124,58,237,.28), 0 2px 14px rgba(124,58,237,.4); } } #gy-fab .gy-fab-badge { margin-left: 7px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: #dc2626; color: #fff; font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; display: none; } #gy-fab.gy-fab-active .gy-fab-badge { display: inline-block; } /* ---- 获取秒传文档小圆点(悬浮于表头「类型」列上方角落) ---- */ #gy-share-fab { position: absolute; z-index: 9999; width: 30px; height: 30px; border-radius: 50%; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); border: 2px solid #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 15px; line-height: 1; box-shadow: 0 0 0 3px rgba(124,58,237,.25), 0 2px 10px rgba(124,58,237,.4); animation: gy-share-pulse 2s ease-in-out infinite; transform: translate(-50%, -50%); transition: transform .15s ease, box-shadow .15s ease; } #gy-share-fab:hover { transform: translate(-50%, -50%) scale(1.15); box-shadow: 0 0 0 5px rgba(124,58,237,.35), 0 4px 16px rgba(124,58,237,.5); } #gy-share-fab:active { transform: translate(-50%, -50%) scale(.95); } /* JS 生成的多行气泡 tooltip */ #gy-share-tt { position: fixed; z-index: 2147483647; padding: 10px 14px; background: #1f1f23; color: #e5e7eb; border-radius: 10px; border: 1px solid #3f3f46; box-shadow: 0 10px 30px rgba(0,0,0,.45); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 12px; line-height: 1.55; font-weight: 500; pointer-events: none; white-space: nowrap; opacity: 0; transition: opacity .12s ease, transform .12s ease; transform: translateY(-4px); } #gy-share-tt.gy-tt-show { opacity: 1; transform: translateY(0); } #gy-share-tt .gy-tt-main { font-size: 13px; font-weight: 700; color: #a5b4fc; } #gy-share-tt .gy-tt-sub { color: #9ca3af; font-size: 12px; margin-top: 2px; } #gy-share-tt::before { content: ''; position: absolute; top: 50%; left: -6px; width: 10px; height: 10px; margin-top: -5px; background: #1f1f23; border-left: 1px solid #3f3f46; border-bottom: 1px solid #3f3f46; transform: rotate(45deg); } /* 手指引导:小圆点上方的手指弹跳 */ .gy-sl-hand { position: fixed; z-index: 9999; font-size: 22px; line-height: 1; pointer-events: none; filter: drop-shadow(0 2px 4px rgba(124,58,237,.4)); animation: gy-sl-tap 1.1s ease-in-out infinite; transform-origin: bottom center; } @keyframes gy-sl-tap { 0%, 100% { transform: translate(-50%, 0) rotate(-10deg); } 55% { transform: translate(-50%, 26px) rotate(0deg) scale(.9); } 70% { transform: translate(-50%, 6px) rotate(0deg) scale(1); } } @keyframes gy-share-pulse { 0%, 100% { box-shadow: 0 0 0 3px rgba(124,58,237,.25), 0 2px 10px rgba(124,58,237,.4); } 50% { box-shadow: 0 0 0 6px rgba(124,58,237,.35), 0 2px 16px rgba(124,58,237,.5); } } /* 云朵提示词(常驻显示在小圆点上方) */ #gy-share-cloud { position: absolute; z-index: 9998; padding: 10px 16px 12px 16px; background: linear-gradient(180deg, #ffffff 0%, #ede9fe 100%); border-radius: 22px; box-shadow: 0 6px 20px rgba(124,58,237,.25); text-align: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; pointer-events: none; animation: gy-cloud-float 2.4s ease-in-out infinite; transform: translateX(-50%); } /* 云朵的圆润凸起(左右小圆) */ #gy-share-cloud::before, #gy-share-cloud::after { content: ''; position: absolute; background: inherit; border-radius: 50%; } #gy-share-cloud::before { width: 22px; height: 22px; top: -8px; left: 18px; } #gy-share-cloud::after { width: 16px; height: 16px; top: -4px; right: 20px; } /* 云朵尾巴(指向下方圆点) */ #gy-share-cloud .gy-cloud-tail { position: absolute; left: 50%; bottom: -8px; width: 16px; height: 16px; margin-left: -8px; background: inherit; transform: rotate(45deg); border-bottom: none; border-right: none; } #gy-share-cloud .gy-cloud-main { font-size: 13px; font-weight: 700; color: #6d28d9; position: relative; z-index: 2; } #gy-share-cloud .gy-cloud-sub { font-size: 11px; color: #8b5cf6; margin-top: 2px; position: relative; z-index: 2; } @keyframes gy-cloud-float { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-4px); } } #gy-modal-bg { position: fixed; inset: 0; z-index: 2147483646; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; } #gy-modal { background: #1f1f23; color: #e5e7eb; width: min(920px, 96vw); max-height: 90vh; border-radius: 12px; box-shadow: 0 24px 60px rgba(0,0,0,.5); display: flex; flex-direction: column; overflow: hidden; } #gy-modal header { padding: 16px 20px; border-bottom: 1px solid #2c2c30; display: flex; justify-content: space-between; align-items: center; } #gy-modal header h3 { margin: 0; font-size: 16px; font-weight: 600; } #gy-modal header .close { background: transparent; border: 0; color: #9ca3af; font-size: 22px; cursor: pointer; padding: 0 4px; line-height: 1; } #gy-modal header .close:hover { color: #fff; } #gy-modal .body { padding: 16px 20px; overflow: auto; flex: 1; } #gy-modal footer { padding: 12px 20px; border-top: 1px solid #2c2c30; display: flex; gap: 10px; justify-content: flex-end; align-items: center; } #gy-modal footer .right { margin-left: auto; display: flex; gap: 10px; } .gy-btn { padding: 7px 14px; border-radius: 6px; border: 0; font-size: 13px; font-weight: 600; cursor: pointer; background: #353539; color: #d1d5db; } .gy-btn:hover { background: #42424a; } .gy-btn-primary { background: #4f46e5; color: #fff; } .gy-btn-primary:hover { background: #4338ca; } .gy-btn-primary:disabled { background: #353539; color: #6b7280; cursor: not-allowed; } /* ---- 复制凭证常驻按钮(导航栏,位于秒传按钮之后) ---- */ .gy-cred-btn { position: relative; margin-left: 8px; height: 36px; padding: 0 14px; border-radius: 8px; background: #fff; color: #222; border: 1px solid #e8e8e8; cursor: pointer; font-size: 14px; font-weight: 400; display: inline-flex; align-items: center; justify-content: center; box-shadow: none; transition: border-color .15s ease, color .15s ease, background .15s ease; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } .gy-cred-btn:hover { border-color: #4f46e5; color: #4f46e5; } .gy-cred-btn:active { background: #f5f3ff; } .gy-cred-btn.gy-cred-ok { border-color: #10b981; color: #059669; background: #ecfdf5; font-weight: 600; } .gy-cred-btn.gy-cred-err { border-color: #ef4444; color: #dc2626; background: #fef2f2; font-weight: 600; } /* ---- 复制凭证按钮气泡 tooltip(悬停立即显示) ---- */ .gy-cred-btn[data-tooltip]::before, .gy-cred-btn[data-tooltip]::after { content: ''; opacity: 0; pointer-events: none; transition: opacity .15s ease, transform .15s ease; z-index: 2147483647; } .gy-cred-btn[data-tooltip]::before { position: absolute; top: calc(100% + 8px); right: 16px; width: 8px; height: 8px; transform: rotate(45deg) scale(.6); background: #1f1f23; border-left: 1px solid #3f3f46; border-top: 1px solid #3f3f46; } .gy-cred-btn[data-tooltip]::after { content: attr(data-tooltip); position: absolute; top: calc(100% + 12px); right: 0; transform: translateY(-4px); padding: 9px 12px; background: #1f1f23; color: #e5e7eb; font-size: 12px; line-height: 1.55; white-space: nowrap; font-weight: 500; border-radius: 10px; border: 1px solid #3f3f46; box-shadow: 0 10px 30px rgba(0,0,0,.35); } .gy-cred-btn[data-tooltip]:hover::before, .gy-cred-btn[data-tooltip]:hover::after { opacity: 1; transform: translateY(0); } .gy-cred-btn[data-tooltip]:hover::before { transform: translateY(0) rotate(45deg) scale(1); } .gy-summary { padding: 10px 12px; margin: 12px 0; background: #14141a; border-radius: 6px; font-size: 13px; color: #d1d5db; } .gy-doc-row { display: flex; gap: 8px; align-items: center; padding: 6px 10px; font-size: 13px; border-bottom: 1px solid #2c2c30; cursor: pointer; } .gy-doc-row:hover { background: #2c2c30; } .gy-doc-row input { transform: scale(1.15); cursor: pointer; } .gy-doc-row .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .gy-doc-row .sz { color: #9ca3af; font-size: 12px; } table.gy-list { width: 100%; border-collapse: collapse; font-size: 13px; font-family: ui-monospace, "Cascadia Code", Consolas, monospace; } table.gy-list th { background: #2c2c30; color: #d1d5db; text-align: left; padding: 8px 10px; font-weight: 500; position: sticky; top: 0; border-bottom: 1px solid #353539; } table.gy-list td { padding: 6px 10px; border-bottom: 1px solid #2c2c30; color: #d1d5db; vertical-align: middle; } table.gy-list td.cb-col { width: 30px; text-align: center; } table.gy-list td.cb-col input { transform: scale(1.15); cursor: pointer; } table.gy-list td.size { text-align: right; width: 90px; } table.gy-list td.status { width: 200px; } .gy-status-pending { color: #9ca3af; } .gy-status-running { color: #fbbf24; } .gy-status-ok { color: #34d399; } .gy-status-fail { color: #f87171; } .gy-status-error { color: #f87171; font-weight: 600; } .gy-bar { height: 6px; background: #2c2c30; border-radius: 3px; overflow: hidden; margin-top: 8px; } .gy-bar > div { height: 100%; background: linear-gradient(90deg, #4f46e5, #7c3aed); width: 0%; transition: width .25s ease; } `); // ===================================================================== // Modal state // ===================================================================== let modalState = null; function openFlashUI() { if (document.getElementById('gy-modal-bg')) return; const ctx = getCurrentCtx(); if (!ctx.parentId) { alert('请先进入一个文件夹目录,再点秒传按钮'); return; } const bg = document.createElement('div'); bg.id = 'gy-modal-bg'; bg.addEventListener('click', (e) => { if (e.target === bg) closeModal(); }); const m = document.createElement('div'); m.id = 'gy-modal'; m.innerHTML = `

🐤 秒传工具

📄 正在扫描当前目录的秒传文档…
`; bg.appendChild(m); document.body.appendChild(bg); modalState = { bg, modal: m, parentId: ctx.parentId, docs: [], // 目录里的秒传文档 entries: [], // 解析出的秒传条目 selected: new Set(), // 勾选的条目 index results: [], running: false, done: false, step: 'docs', // 'docs' → 'entries' → running/done }; m.querySelector('.close').addEventListener('click', closeModal); m.querySelector('#gy-cancel').addEventListener('click', closeModal); m.querySelector('#gy-start').addEventListener('click', onStartClick); const onKey = (e) => { if (e.key === 'Escape') closeModal(); }; document.addEventListener('keydown', onKey); bg._escHandler = onKey; loadDocList(); } // ===================================================================== // 独立的「获取秒传文档分享链接」弹窗:只做转存(把分享链接转存到当前目录) // ===================================================================== function openShareUI() { if (document.getElementById('gy-share-bg')) return; const bg = document.createElement('div'); bg.id = 'gy-share-bg'; bg.style.cssText = 'position:fixed; inset:0; z-index:2147483646; background:rgba(0,0,0,.5); display:flex; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;'; bg.addEventListener('click', (e) => { if (e.target === bg) bg.remove(); }); const m = document.createElement('div'); m.style.cssText = 'background:#1f1f23; color:#e5e7eb; width:min(720px,95vw); max-height:90vh; border-radius:12px; box-shadow:0 24px 60px rgba(0,0,0,.5); display:flex; flex-direction:column; overflow:hidden;'; m.innerHTML = `

📚 获取秒传文档分享链接

`; bg.appendChild(m); document.body.appendChild(bg); m.querySelector('.close').addEventListener('click', () => bg.remove()); m.querySelector('#gy-share-close').addEventListener('click', () => bg.remove()); bg.addEventListener('keydown', (e) => { if (e.key === 'Escape') bg.remove(); }); renderShareList(bg); } // 渲染秒传分享清单(来自 HTTP 服务)——独立转存弹窗用 const SHARE_DEFAULT_SHOW = 2; // 默认展示条数 const SHARE_MAX_ALL = 5; // 展开全部最多展示条数 async function renderShareList(bg) { const listEl = bg.querySelector('#gy-share-list'); const searchEl = bg.querySelector('#gy-share-search'); listEl.innerHTML = '
⏳ 正在拉取分享清单…
'; let items; try { items = await fetchShareList(); } catch (e) { listEl.innerHTML = `
❌ 拉取清单失败:${escapeHtml(e.message || String(e))}
`; return; } // 渲染当前 items(q 为搜索词,null=无搜索,'__all__'=展开全部) function renderList(q) { if (!items.length) { listEl.innerHTML = q ? '
没有匹配的分享
' : '
清单为空
'; return; } const showCount = q ? Math.min(items.length, SHARE_MAX_ALL) : Math.min(items.length, SHARE_DEFAULT_SHOW); const visible = items.slice(0, showCount); const hasMore = items.length > showCount; listEl.innerHTML = visible.map((s) => { const hasChildren = Array.isArray(s.children) && s.children.length; const childrenHtml = hasChildren ? `` : ''; return `
${escapeHtml(s.name)}
${escapeHtml(s.desc || (hasChildren ? s.children.length + ' 个子项' : ''))}
${childrenHtml}
`; }).join(''); // 展开/收起控制 if (hasMore && !q) { listEl.insertAdjacentHTML('beforeend', `
展开全部 ${items.length} 个 ▾
`); listEl.querySelector('#gy-share-more').addEventListener('click', () => renderList('__all__')); } else if (q === '__all__' && items.length > SHARE_DEFAULT_SHOW) { listEl.insertAdjacentHTML('beforeend', `
收起(显示前 ${SHARE_DEFAULT_SHOW} 个)▲
`); listEl.querySelector('#gy-share-less').addEventListener('click', () => renderList('')); } listEl.querySelectorAll('.gy-share-restore').forEach((btn) => { btn.addEventListener('click', () => { const idx = [...listEl.querySelectorAll('.gy-share-item')].indexOf(btn.closest('.gy-share-item')); onShareRestore(bg, items[idx]); }); }); // 「查看详情」展开/收起 children listEl.querySelectorAll('.gy-share-detail').forEach((btn) => { btn.addEventListener('click', () => { const itemEl = btn.closest('.gy-share-item'); const childrenEl = itemEl.querySelector('.gy-share-children'); if (!childrenEl) return; const open = childrenEl.style.display !== 'block'; childrenEl.style.display = open ? 'block' : 'none'; btn.textContent = open ? '收起详情 ▴' : '查看详情 ▾'; }); }); } renderList(''); // 搜索:服务端实现(去抖 300ms) let debounceTimer = null; searchEl.addEventListener('input', () => { clearTimeout(debounceTimer); const q = searchEl.value.trim(); debounceTimer = setTimeout(async () => { listEl.innerHTML = '
⏳ 搜索中…
'; try { items = q ? await fetchShareList(q) : await fetchShareList(); } catch (e) { listEl.innerHTML = `
❌ 搜索失败:${escapeHtml(e.message || String(e))}
`; return; } renderList(q); }, 300); }); } // 点击「转存分享」:关闭弹窗,打开页面「云添加」并填入链接,由页面完成转存 async function onShareRestore(bg, item) { // 先关闭分享弹窗(云添加会接管) if (bg && bg.parentNode) bg.remove(); try { await cloudAddShare(item.url); // 已填入云添加输入框,无需额外提示(弹窗已关) } catch (e) { alert('转存失败:' + (e.message || String(e)) + '\n请手动点击页面「云添加」粘贴链接转存。'); } } // 从 HTTP 服务拉取秒传分享清单(GM_xmlhttpRequest 绕过 CORS / 混合内容限制) // q 可选:服务端搜索关键词(匹配 name / desc / children.name) function fetchShareList(q) { return new Promise((resolve, reject) => { const url = q ? FLASH_SHARE_LIST_URL + '?q=' + encodeURIComponent(q) : FLASH_SHARE_LIST_URL; GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'json', timeout: 8000, onload: (r) => { try { const j = typeof r.response === 'string' ? JSON.parse(r.response) : r.response; if (r.status !== 200) return reject(new Error('清单服务 HTTP ' + r.status)); if (!Array.isArray(j)) return reject(new Error('清单格式错误:应为 JSON 数组')); resolve(j.filter((x) => x && x.name && x.url)); } catch (e) { reject(e); } }, onerror: (e) => reject(new Error('network: ' + (e.error || e.responseText || 'unknown'))) }); }); } // cloudAddShare: 模拟点击页面「云添加」按钮,把分享链接填入 URL 输入框。 // 用原生 value setter 赋值(兼容 React/Vue 受控组件),避免聚焦后链接消失。 // 不自动确认——目标目录由云添加自己选择,让用户去云添加里选目录并确认转存。 function cloudAddShare(shareUrl) { return new Promise((resolve, reject) => { const addBtn = findCloudAddBtn(); if (!addBtn) return reject(new Error('页面没有「云添加」按钮')); addBtn.click(); // 等待对话框/输入框出现,填入链接 const deadline = Date.now() + 8000; const tryFill = () => { if (Date.now() > deadline) return reject(new Error('未找到云添加输入框(超时)')); const input = findCloudAddInput(); if (!input) return setTimeout(tryFill, 300); setNativeValue(input, shareUrl); // 触发完整输入事件链 input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); resolve(); // 链接已填入,由用户在云添加里选目录并确认 }; setTimeout(tryFill, 400); }); } // setNativeValue: 用原生 setter 设置受控 input 的值(React/Vue 兼容) function setNativeValue(el, value) { const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const desc = Object.getOwnPropertyDescriptor(proto, 'value'); if (desc && desc.set) { desc.set.call(el, value); } else { el.value = value; } } // 查找云添加对话框里的链接输入框 function findCloudAddInput() { const cands = document.querySelectorAll('input[type=text], input:not([type]), textarea'); for (const el of cands) { const ph = (el.placeholder || '').toLowerCase(); const vis = el.offsetParent !== null; if (vis && (ph.includes('http') || ph.includes('链接') || ph.includes('url') || ph.includes('地址'))) return el; } return null; } function closeModal() { if (!modalState) return; if (modalState.bg._escHandler) document.removeEventListener('keydown', modalState.bg._escHandler); modalState.bg.remove(); modalState = null; } // ===================================================================== // 一键复制接口凭证(供 AI 直接调用光鸭 web API) // ===================================================================== function copyCredentials() { const btn = document.getElementById('gy-copy-cred-btn'); const flashBtn = (text, ok) => { if (!btn) return; btn.classList.remove('gy-cred-ok', 'gy-cred-err'); btn.textContent = text; if (ok) btn.classList.add('gy-cred-ok'); else btn.classList.add('gy-cred-err'); setTimeout(() => { btn.textContent = '🔑 复制接口凭证'; btn.classList.remove('gy-cred-ok', 'gy-cred-err'); }, 1800); }; try { const credRaw = localStorage.getItem(LS_CRED_KEY); if (!credRaw) throw new Error('未登录:localStorage 没有 ' + LS_CRED_KEY); let cred; try { cred = JSON.parse(credRaw); } catch (e) { throw new Error('credentials JSON 解析失败: ' + e.message); } const access = cred.access_token; const refresh = cred.refresh_token; const did = localStorage.getItem('swangpan_web_device_id'); const smid = localStorage.getItem('shumei_id:v1'); if (!access || !refresh) throw new Error('缺少 access_token 或 refresh_token'); if (!did || !smid) throw new Error('缺少 did 或 smid,请确认已登录'); const out = JSON.stringify({ access_token: access, refresh_token: refresh, client_id: CLIENT_ID, did, smid }, null, 2); GM_setClipboard(out); flashBtn('✅ 已复制', true); } catch (e) { flashBtn('❌ ' + e.message, false); } } // ===================================================================== // Step 1: 列出当前目录的秒传文档,供勾选 // ===================================================================== async function loadDocList() { const summary = modalState.modal.querySelector('#gy-doc-summary'); const wrap = modalState.modal.querySelector('#gy-doc-wrap'); try { const list = await listFolder(modalState.parentId); const docs = list.filter((f) => { if (f.dirType !== 1) return false; const ext = (f.fileName.match(/\.[^.]+$/) || [''])[0].toLowerCase(); return FLASH_DOC_EXTS.includes(ext) && FLASH_DOC_FILE_RE.test(f.fileName); }); modalState.docs = docs; if (!docs.length) { summary.textContent = '❌ 当前目录没有找到秒传文档(文件名含"秒传"的 .txt/.json)'; return; } summary.innerHTML = `📄 找到 ${docs.length} 个秒传文档,勾选要解析的:`; renderDocList(); } catch (e) { summary.textContent = '⚠️ 扫描失败:' + e.message; } } function renderDocList() { const wrap = modalState.modal.querySelector('#gy-doc-wrap'); wrap.innerHTML = modalState.docs.map((d, i) => `
${escapeHtml(d.fileName)} ${fmtSize(d.fileSize != null ? d.fileSize : (d.size || 0))}
`).join(''); const updateBtn = () => { const anyChecked = wrap.querySelectorAll('input:checked').length > 0; modalState.modal.querySelector('#gy-start').disabled = !anyChecked; }; wrap.querySelectorAll('input').forEach((cb) => { cb.addEventListener('change', () => { cb.closest('.gy-doc-row').style.background = cb.checked ? 'rgba(99,102,241,.15)' : ''; updateBtn(); }); }); wrap.querySelectorAll('input:checked').forEach((cb) => { cb.closest('.gy-doc-row').style.background = 'rgba(99,102,241,.15)'; }); updateBtn(); } // ===================================================================== // Step 2: 解析所选文档 → 条目列表(可勾选) // ===================================================================== async function resolveDocs() { if (modalState.running || modalState.done) return; const wrap = modalState.modal.querySelector('#gy-doc-wrap'); const docIdxs = [...wrap.querySelectorAll('input:checked')].map((cb) => parseInt(cb.dataset.docIdx, 10)); if (!docIdxs.length) { alert('请先勾选要解析的秒传文档'); return; } const startBtn = modalState.modal.querySelector('#gy-start'); startBtn.disabled = true; startBtn.textContent = '解析中…'; const allEntries = []; const docNames = []; let errors = []; for (const idx of docIdxs) { const doc = modalState.docs[idx]; docNames.push(doc.fileName); try { const dl = await apiPost('/userres/v1/get_res_download_url', { fileId: doc.fileId }); const text = await apiGet(dl.data.signedURL); const arr = JSON.parse(text); if (Array.isArray(arr) && arr.length) allEntries.push(...arr); else errors.push(doc.fileName + ': 内容不是数组或为空'); } catch (e) { errors.push(doc.fileName + ': ' + e.message); } } if (!allEntries.length) { startBtn.disabled = false; startBtn.textContent = '解析所选文档'; alert('解析失败:\n' + errors.join('\n')); return; } loadEntries(allEntries, docNames.join('、')); modalState.step = 'entries'; startBtn.disabled = false; startBtn.textContent = '开始秒传'; if (errors.length) { modalState.modal.querySelector('#gy-summary').innerHTML += `
⚠️ 部分文档解析失败:${escapeHtml(errors.join(';'))}
`; } } // ===================================================================== // Helpers // ===================================================================== function fmtSize(n) { n = Number(n) || 0; if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'; if (n < 1073741824) return (n / 1048576).toFixed(2) + ' MB'; return (n / 1073741824).toFixed(2) + ' GB'; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); } function loadEntries(data, name) { const bad = data.filter((x) => !x || !x.name || !x.size || !x.gcid || !x.cid); if (bad.length) throw new Error(`有 ${bad.length} 条目缺 name/size/gcid/cid`); modalState.entries = data; modalState.results = data.map(() => ({ status: 'pending' })); modalState.selected = new Set(data.map((_, i) => i)); renderTable(); const totalSize = data.reduce((s, x) => s + (Number(x.size) || 0), 0); const summary = modalState.modal.querySelector('#gy-summary'); summary.style.display = ''; summary.innerHTML = ` 来自 ${escapeHtml((name || '秒传文档').slice(0, 60))},共 ${data.length} 个文件,总计 ${fmtSize(totalSize)}, 已选 ${data.length} 个 `; } // ----- Table render ----- function renderTable() { const wrap = modalState.modal.querySelector('#gy-table-wrap'); if (!modalState.entries.length) { wrap.innerHTML = '
还没有条目
'; return; } const rows = modalState.entries.map((e, i) => { const r = modalState.results[i] || { status: 'pending' }; const st = r.status; const cls = 'gy-status-' + (st === 'pending' ? 'pending' : st); const txt = ({ pending: '⏳ 排队', running: '🔄 探测中…', ok: '✅ 秒传命中', no_match: '❌ 服务端未命中', error: '⚠️ 错误' })[st] || st; const msg = r.message ? `
${escapeHtml(r.message)}` : ''; const checked = modalState.selected.has(i); return ` ${i + 1} ${escapeHtml(e.name)} ${fmtSize(e.size)} ${txt}${msg} `; }).join(''); wrap.innerHTML = ` ${rows}
# name size 状态
`; wrap.querySelectorAll('input.gy-cb').forEach((cb) => { cb.addEventListener('change', (e) => { const idx = parseInt(e.target.getAttribute('data-idx'), 10); if (e.target.checked) modalState.selected.add(idx); else modalState.selected.delete(idx); updateSelCount(); }); }); const headCb = wrap.querySelector('#gy-cb-head'); if (headCb) { headCb.addEventListener('change', (e) => { const all = e.target.checked; modalState.selected = new Set(); wrap.querySelectorAll('input.gy-cb').forEach((cb) => { cb.checked = all; if (all) modalState.selected.add(parseInt(cb.getAttribute('data-idx'), 10)); }); updateSelCount(); }); } } function updateSelCount() { const el = modalState.modal.querySelector('#gy-sel-count'); if (el) el.textContent = modalState.selected.size; } function renderRow(idx) { const wrap = modalState.modal.querySelector('#gy-table-wrap'); const tr = wrap.querySelectorAll('tbody tr')[idx]; if (!tr) return; const r = modalState.results[idx] || { status: 'pending' }; const st = r.status; const cls = 'gy-status-' + (st === 'pending' ? 'pending' : st); const txt = ({ pending: '⏳ 排队', running: '🔄 探测中…', ok: '✅ 秒传命中', no_match: '❌ 服务端未命中', error: '⚠️ 错误' })[st] || st; const msg = r.message ? `
${escapeHtml(r.message)}` : ''; tr.querySelector('.status').innerHTML = `${txt}${msg}`; } function updateProgress(done, total, ok, fail) { const pct = Math.round(done * 100 / total); const el = modalState.modal.querySelector('#gy-summary'); el.innerHTML = ` ${done}/${total} 完成 · ✅ ${ok} 秒传 · ❌ ${fail} 未命中/错
`; } // ===================================================================== // Main action button // ===================================================================== function onStartClick() { if (!modalState) return; if (modalState.done) { closeModal(); return; } if (modalState.running) return; if (modalState.step === 'docs') { resolveDocs(); return; } startFlash(); } // ===================================================================== // Flash run // ===================================================================== async function startFlash() { if (!modalState || modalState.running || modalState.done) return; const indices = [...modalState.selected].sort((a, b) => a - b); if (!indices.length) { alert('请先勾选要秒传的条目'); return; } modalState.running = true; const startBtn = modalState.modal.querySelector('#gy-start'); startBtn.disabled = true; startBtn.textContent = '秒传中…'; modalState.results = modalState.entries.map(() => ({ status: 'pending' })); renderTable(); let done = 0, ok = 0, fail = 0; const total = indices.length; await runPool(indices, CONCURRENCY, async (idx) => { const entry = modalState.entries[idx]; modalState.results[idx] = { status: 'running' }; renderRow(idx); try { const res = await tryFlash(entry, modalState.parentId); modalState.results[idx] = { status: res.instant ? 'ok' : 'no_match', message: res.instant ? 'taskId=' + res.taskId : '服务端没这个 gcid+cid' }; if (res.instant) ok++; else fail++; } catch (e) { modalState.results[idx] = { status: 'error', message: e.message }; fail++; } done++; renderRow(idx); updateProgress(done, total, ok, fail); }, () => {}); modalState.running = false; modalState.done = true; startBtn.disabled = false; startBtn.textContent = '✅ 完成,点此关闭'; updateProgress(done, total, ok, fail); const reloadChecked = modalState.modal.querySelector('#gy-reload-after'); if (reloadChecked && reloadChecked.checked && ok > 0) { setTimeout(() => { try { location.reload(); } catch (e) {} }, 600); } } // ===================================================================== // Boot: 注入到"云添加"按钮右侧 // ===================================================================== function findCloudAddBtn() { const btns = [...document.querySelectorAll('button')]; return btns.find((b) => (b.textContent || '').replace(/\s+/g, '').includes('云添加')); } function ensureFab() { const cloudAdd = findCloudAddBtn(); if (!cloudAdd) return; const btn = document.getElementById('gy-fab'); if (!btn) { const fab = document.createElement('button'); fab.id = 'gy-fab'; fab.type = 'button'; fab.title = '秒传工具'; fab.style.display = 'none'; fab.innerHTML = '⚡秒传0'; fab.addEventListener('click', openFlashUI); cloudAdd.insertAdjacentElement('afterend', fab); } // 复制凭证常驻按钮(位于秒传按钮之后,始终显示) if (!document.getElementById('gy-copy-cred-btn')) { const credBtn = document.createElement('button'); credBtn.id = 'gy-copy-cred-btn'; credBtn.type = 'button'; credBtn.className = 'gy-cred-btn'; credBtn.setAttribute('data-tooltip', '一键复制 access_token + refresh_token + did + smid,AI 可直接调用光鸭 web API'); credBtn.textContent = '🔑 复制接口凭证'; credBtn.addEventListener('click', copyCredentials); const fab = document.getElementById('gy-fab'); fab.insertAdjacentElement('afterend', credBtn); } // 「获取秒传文档」悬浮按钮:跟随文件列表表头「类型」列上方(fixed 定位) ensureSharelistFloat(); } // 创建/定位「获取秒传文档」悬浮小圆点(表头「类型」列上方角落) // 挂到 swangpan-file-list__root(relative),用 absolute 定位——随文件列表自然滚动,与表头永远一致 function ensureSharelistFloat() { if (!document.getElementById('gy-share-fab')) { const fab = document.createElement('button'); fab.id = 'gy-share-fab'; fab.type = 'button'; fab.textContent = '📥'; fab.addEventListener('click', () => openShareUI()); // 等列表 root 出现后挂载 const root = document.querySelector('.swangpan-file-list__root'); if (root) root.appendChild(fab); else document.body.appendChild(fab); // 多行气泡 tooltip if (!document.getElementById('gy-share-tt')) { const tt = document.createElement('div'); tt.id = 'gy-share-tt'; tt.innerHTML = '
点击获取秒传文档分享链接
从分享清单选一部,自动填入云添加转存
'; document.body.appendChild(tt); } // hover 显示/隐藏 tooltip const tt = document.getElementById('gy-share-tt'); fab.addEventListener('mouseenter', () => { const fr = fab.getBoundingClientRect(); tt.classList.add('gy-tt-show'); tt.style.left = (fr.right + 12) + 'px'; tt.style.top = (fr.top + fr.height / 2) + 'px'; }); fab.addEventListener('mouseleave', () => tt.classList.remove('gy-tt-show')); // 云朵提示词(常驻显示在小圆点上方) if (!document.getElementById('gy-share-cloud')) { const cloud = document.createElement('div'); cloud.id = 'gy-share-cloud'; cloud.innerHTML = '
点击获取秒传文档
'; const root = document.querySelector('.swangpan-file-list__root'); if (root) root.appendChild(cloud); else document.body.appendChild(cloud); } } positionSharelistFloat(); } // 把悬浮小圆点定位到「类型」列上方角落(云朵在其上方) function positionSharelistFloat() { const fab = document.getElementById('gy-share-fab'); const cloud = document.getElementById('gy-share-cloud'); const typeBtn = findTypeBtn(); if (!fab || !typeBtn) { if (fab) fab.style.display = 'none'; if (cloud) cloud.style.display = 'none'; return; } const root = fab.parentElement; // 定位上下文(.swangpan-file-list__root 或 body) const rr = root.getBoundingClientRect(); const r = typeBtn.getBoundingClientRect(); // 相对 root 的坐标(绝对定位) const offset = -70; const cx = (r.right - rr.left) - 16 + offset; const cy = (r.top - rr.top) - 24; fab.style.display = 'flex'; fab.style.left = cx + 'px'; fab.style.top = cy + 'px'; if (cloud) { cloud.style.display = 'block'; cloud.style.left = cx + 'px'; cloud.style.top = (cy - 52) + 'px'; } } // 查找文件列表表头的「类型」按钮 function findTypeBtn() { const btns = [...document.querySelectorAll('button')]; return btns.find((b) => (b.textContent || '').trim() === '类型'); } // ---- 检测当前目录是否有秒传文档,控制按钮显隐 + 提示 ---- let fabDetecting = false; let lastCheckHash = null; let lastFabParent = null; function getFabBtn() { return document.getElementById('gy-fab'); } async function checkFlashDocs() { if (fabDetecting) return; const btn = getFabBtn(); if (!btn) return; const ctx = getCurrentCtx(); if (!ctx.parentId) { // 非文件夹视图(根目录/分类/系统页)→ 隐藏按钮,不提示 btn.style.display = 'none'; btn.classList.remove('gy-fab-active'); lastFabParent = null; return; } // 同一目录且刚检测过(<3s)→ 跳过,避免频繁 API const now = Date.now(); if (lastFabParent === ctx.parentId && lastCheckHash === location.hash && now - (fabLastTs || 0) < 3000) return; fabDetecting = true; try { const list = await listFolder(ctx.parentId); const docs = list.filter((f) => { if (f.dirType !== 1) return false; const ext = (f.fileName.match(/\.[^.]+$/) || [''])[0].toLowerCase(); return FLASH_DOC_EXTS.includes(ext) && FLASH_DOC_FILE_RE.test(f.fileName); }); lastFabParent = ctx.parentId; lastCheckHash = location.hash; fabLastTs = Date.now(); if (docs.length) { btn.style.display = 'inline-flex'; btn.classList.add('gy-fab-active'); btn.querySelector('[data-badge]').textContent = docs.length; btn.title = `发现 ${docs.length} 个秒传文档,点击开始秒传`; btn.querySelector('.fab-text').textContent = '秒传'; } else { btn.style.display = 'none'; btn.classList.remove('gy-fab-active'); lastFabParent = null; } } catch (e) { // API 失败:保守隐藏,避免误引导 btn.style.display = 'none'; btn.classList.remove('gy-fab-active'); } finally { fabDetecting = false; } } // SPA 路由切换会重渲染导航栏:定时确保按钮存在 + 检测秒传文档 let fabWatcher = null; let fabLastTs = 0; function startFabWatcher() { if (fabWatcher) return; fabWatcher = setInterval(() => { if (!document.getElementById('gy-fab')) { ensureFab(); // 刚注入的按钮,立即检测一次 checkFlashDocs(); return; } positionSharelistFloat(); // 持续跟随「类型」列上方 checkFlashDocs(); }, 1500); } function boot() { if (!document.body) { setTimeout(boot, 100); return; } ensureFab(); startFabWatcher(); // 滚动时立即跟随「类型」列(rAF 节流,避免滚动完才跳) let rafId = null; const onScroll = () => { if (rafId) return; rafId = requestAnimationFrame(() => { rafId = null; positionSharelistFloat(); }); }; document.addEventListener('scroll', onScroll, { capture: true, passive: true }); window.addEventListener('scroll', onScroll, { passive: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();