// ==UserScript== // @name 115Aria // @namespace http://tampermonkey.net/ // @version 0.45.14 // @description 115.com OpenList直链发送到aria2 RPC // @author jiemo // @match *://115.com/* // @match *://*.115.com/* // @match *://115cdn.com/s/* // @match *://*.115cdn.com/s/* // @match *://hdhive.com/* // @match *://www.hdhive.com/* // @match *://*.hdhive.com/* // @match *://hdlive.com/* // @match *://*.hdlive.com/* // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect * // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/575977/115Aria.user.js // @updateURL https://update.greasyfork.icu/scripts/575977/115Aria.meta.js // ==/UserScript== (function() { 'use strict'; if (location.origin === 'https://115.com' && (location.pathname === '/' || location.pathname === '') && !location.search && !location.hash) { location.replace('https://115.com/storage/netdisk?cid=0&mode=wangpan'); return; } const STORE_PREFIX = 'aria115_'; const SETTINGS_KEY = 'aria115_settings_v1'; const DEFAULT_OPENLIST_HOST = 'https://abc.com'; const DEFAULT_OPENLIST_MOUNT_PATH = '115'; const DEFAULT_TRANSFER_TARGET_CID = '0'; const BREADCRUMB_CLASS = 'flex items-center text-xs overflow-x-auto gap-3 flex-wrap pr-6'; const DEFAULT_DIR = '/Users/Administrator/Downloads'; const CONTAINER_ID = 'aria115-container'; const MODAL_ID = 'aria115-settings-modal'; const STYLE_ID = 'aria115-style'; const FOLDER_SCAN_LIMIT = 5000; const folderCoverCidCache = []; const SHARE_RECEIVE_API = 'https://115cdn.com/webapi/share/receive'; const SHARE_SNAP_API = 'https://115cdn.com/webapi/share/snap'; const HDHIVE_AUTOTRANSFER_PARAM = 'autotransfer'; const HDHIVE_RESOURCE_TYPE_PARAM = 'type'; const HDHIVE_SOURCE_PARAM = 'aria115_source'; const HDHIVE_MESSAGE_TYPE = 'HDHIVE_115_TRANSFER_RESULT'; const HDHIVE_WINDOW_NAME_PREFIX = 'aria115-hdhive-'; const HDHIVE_AUTO_CLOSE_DELAY = 1200; const HDHIVE_MAX_WAIT_TIME = 20000; const AD_OVERLAY_CLASS_SETS = [ ['fixed', 'inset-0', 'bg-black/50', 'z-[9998]'], ['fixed', 'inset-0', 'z-[9999]', 'flex', 'items-center', 'justify-center', 'pointer-events-none'] ]; const hdHiveProcessedResources = new Set(); const hdHiveProcessingButtons = new Map(); const DEFAULT_SETTINGS = { version: 1, openlistHost: DEFAULT_OPENLIST_HOST, openlistMountPath: DEFAULT_OPENLIST_MOUNT_PATH, activeRpcId: 'local-win', activePath: DEFAULT_DIR, transferTargetCid: DEFAULT_TRANSFER_TARGET_CID, transferCookie: '', rpcConfigs: [ { id: 'local-win', name: '本地Win', endpoint: 'http://127.0.0.1:6800', token: '', path: '/jsonrpc' }, { id: 'remote-linux', name: '远程Linux', endpoint: 'https://your-linux-server.example.com:443', token: '', path: '/jsonrpc' } ], downloadPaths: [ DEFAULT_DIR, '/Users/Administrator/Desktop', '/root/downloads' ] }; const BAD_NAME_TEXT = new Set([ '下载', '分享', '删除', '移动', '复制', '重命名', '更多', '选择', '全选', '文件名', '大小', '时间', '拖拽移动', '置顶', 'download', 'share', 'delete', 'move', 'copy', 'rename', 'more', 'select' ]); const HIDDEN_115_OPERATION_TEXT = new Set([ '置顶', '标签', '备注', '星标', '显示时长', '加密隐藏', '设为快捷入口' ]); function clone(value) { return JSON.parse(JSON.stringify(value)); } function normalizeText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function normalizePath(value) { return String(value || '').trim(); } function stripSlashes(value) { return String(value || '').replace(/^\/+|\/+$/g, ''); } function parseOpenlistHost(value) { const raw = normalizePath(value); if (!raw) return null; const urlText = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`; try { const url = new URL(urlText); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; return url.origin; } catch (err) { return null; } } function normalizeOpenlistHost(value) { return parseOpenlistHost(value) || DEFAULT_OPENLIST_HOST; } function normalizeOpenlistMountPath(value) { const raw = stripSlashes(normalizePath(value) || DEFAULT_OPENLIST_MOUNT_PATH).replace(/^d\//i, ''); return raw || DEFAULT_OPENLIST_MOUNT_PATH; } function normalizeTransferTargetCid(value) { const raw = normalizePath(value); if (!raw) return DEFAULT_TRANSFER_TARGET_CID; try { const url = new URL(raw, location.origin); const cid = url.searchParams.get('cid') || url.searchParams.get('parent_id'); if (cid && /^\d+$/.test(cid)) return cid; } catch (err) {} const cidMatch = raw.match(/(?:^|[?\s])(?:cid|parent_id)=([0-9]+)/i); if (cidMatch) return cidMatch[1]; const digitMatch = raw.match(/\d+/); return digitMatch ? digitMatch[0] : DEFAULT_TRANSFER_TARGET_CID; } function safeDecode(value) { try { return decodeURIComponent(value); } catch (err) { return value; } } function parseRequestUrl(input) { if (!input) return ''; if (typeof input === 'string') return input; if (input.url) return input.url; return String(input || ''); } function getFolderCoverCidFromUrl(rawUrl) { const value = parseRequestUrl(rawUrl); if (!value) return ''; try { const url = new URL(value, location.origin); const isDirectCover = url.pathname.endsWith('/files/cover'); const isProxyCover = url.pathname === '/api/proxy/115' && url.searchParams.get('domain') === 'webapi' && url.searchParams.get('path') === '/files/cover'; if (!isDirectCover && !isProxyCover) return ''; if (url.searchParams.get('folder_as_file') !== '1') return ''; return normalizePath(url.searchParams.get('file_id')); } catch (err) { return ''; } } function rememberFolderCoverCid(rawUrl) { const cid = getFolderCoverCidFromUrl(rawUrl); if (!cid) return; const now = Date.now(); const existing = folderCoverCidCache.find((item) => item.cid === cid); if (existing) { existing.pageCid = getCurrentCid(); existing.time = now; return; } folderCoverCidCache.push({ cid, pageCid: getCurrentCid(), time: now }); if (folderCoverCidCache.length > 50) folderCoverCidCache.splice(0, folderCoverCidCache.length - 50); } function rememberFolderCoverFromPerformance() { if (!performance || typeof performance.getEntriesByType !== 'function') return; performance.getEntriesByType('resource').forEach((entry) => rememberFolderCoverCid(entry && entry.name)); } function getRecentFolderCoverCid(usedCids) { rememberFolderCoverFromPerformance(); const used = usedCids || new Set(); const pageCid = getCurrentCid(); const recent = folderCoverCidCache .filter((item) => item && item.cid && item.pageCid === pageCid && !used.has(item.cid)) .sort((a, b) => b.time - a.time)[0]; if (!recent) return ''; used.add(recent.cid); return recent.cid; } function installFolderCoverSniffer() { const target = unsafeWindow || window; if (!target || target.__aria115FolderCoverSniffer) return; target.__aria115FolderCoverSniffer = true; const rawFetch = target.fetch; if (typeof rawFetch === 'function') { target.fetch = function(input, init) { rememberFolderCoverCid(input); return rawFetch.apply(this, arguments); }; } const XHR = target.XMLHttpRequest; if (XHR && XHR.prototype && typeof XHR.prototype.open === 'function') { const rawOpen = XHR.prototype.open; XHR.prototype.open = function(method, url) { rememberFolderCoverCid(url); return rawOpen.apply(this, arguments); }; } } function is115OperationToolbar(button) { const toolbar = button && button.closest && button.closest('div.flex.items-center.bg-white.overflow-hidden'); if (!toolbar) return false; const text = normalizeText(toolbar.textContent); return text.includes('下载') && text.includes('移动') && text.includes('复制') && text.includes('更多'); } function is115OperationToolbarElement(toolbar) { if (!toolbar) return false; const text = normalizeText(toolbar.textContent); return text.includes('下载') && text.includes('移动') && text.includes('复制') && text.includes('更多'); } function has115OperationToolbar() { return Array.from(document.querySelectorAll('div.flex.items-center.bg-white.overflow-hidden')).some(is115OperationToolbarElement); } function isVisibleNode(element) { if (!element || typeof element.getClientRects !== 'function') return false; return element.getClientRects().length > 0; } function isElementActuallyVisible(element) { if (!element || !element.isConnected || typeof element.getBoundingClientRect !== 'function') return false; const rects = Array.from(element.getClientRects ? element.getClientRects() : []); if (rects.length === 0) return false; if (!rects.some((rect) => rect.width > 0 && rect.height > 0)) return false; let node = element; while (node && node.nodeType === 1 && node !== document.body) { const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') return false; node = node.parentElement; } return true; } function isAria115OwnedElement(element) { return Boolean(element && element.closest && element.closest(`#${CONTAINER_ID}, #${MODAL_ID}, #aria115-toast-wrap, #aria115-share-panel, #aria115-hdhive-panel`)); } function is115MoreMenuItem(element) { if (!has115OperationToolbar() || !isVisibleNode(element) || isAria115OwnedElement(element)) return false; if (element.closest('div.flex.items-center.bg-white.overflow-hidden')) return false; const popup = element.closest('[role="menu"], [role="listbox"], [data-radix-popper-content-wrapper], .ant-dropdown, .el-popper, .MuiPopover-root, .MuiMenu-paper'); if (popup) return true; let node = element.parentElement; for (let depth = 0; node && node !== document.body && depth < 6; depth += 1, node = node.parentElement) { const style = window.getComputedStyle(node); if ((style.position === 'absolute' || style.position === 'fixed') && isVisibleNode(node)) return true; } return false; } function hide115OperationElement(element, toolbarItem) { const wrapper = toolbarItem ? (element.closest('.relative.flex.items-center') || element) : (element.closest('[role="menuitem"], li') || element); wrapper.dataset.aria115HiddenOperation = '1'; wrapper.style.display = 'none'; wrapper.setAttribute('aria-hidden', 'true'); } function get115OperationButtonLabel(button) { const spans = button ? Array.from(button.querySelectorAll('span')) : []; const last = spans.map((span) => normalizeText(span.textContent)).filter(Boolean).pop(); return last || normalizeText(button ? button.textContent : ''); } function find115ToolbarButton(toolbar, label) { if (!toolbar) return null; return Array.from(toolbar.querySelectorAll('button')).find((button) => get115OperationButtonLabel(button) === label) || null; } function find115ToolbarButtonWrapper(toolbar, label) { const button = find115ToolbarButton(toolbar, label); return button ? (button.closest('.relative.flex.items-center') || button) : null; } function hideFirstToolbarDivider(wrapper) { if (!wrapper) return; const divider = Array.from(wrapper.children).find((child) => { const style = window.getComputedStyle(child); return style.position === 'absolute' && /(^|\s)left-0(\s|$)/.test(child.className || ''); }); if (divider) divider.style.display = 'none'; } function find115MoreMenuItemByLabel(label) { return Array.from(document.querySelectorAll('button, a, [role="menuitem"], li, [tabindex], div[class*="cursor-pointer"]')).find((item) => { if (isAria115OwnedElement(item)) return false; if (normalizeText(item.textContent) !== label) return false; return is115MoreMenuItem(item); }) || null; } function click115MoreMenuItem(toolbar, label) { const existing = find115MoreMenuItemByLabel(label); if (existing) { existing.click(); return; } const moreButton = find115ToolbarButton(toolbar, '更多'); if (!moreButton) { showToast(`未找到${label}入口`, 'warning'); return; } moreButton.click(); const startedAt = Date.now(); const tryClick = () => { const item = find115MoreMenuItemByLabel(label); if (item) { item.click(); return; } if (Date.now() - startedAt < 1000) window.setTimeout(tryClick, 50); else showToast(`未找到${label}入口`, 'warning'); }; window.setTimeout(tryClick, 80); } function create115DeleteProxyButton(toolbar) { const wrapper = document.createElement('div'); wrapper.className = 'relative flex items-center'; wrapper.dataset.aria115DeleteProxy = '1'; const button = document.createElement('button'); button.type = 'button'; button.className = 'flex items-center px-3 py-[5px] transition-colors whitespace-nowrap flex-shrink-0 text-theme hover:bg-[#F7F9FA]'; button.style.fontSize = '14px'; button.style.fontWeight = '500'; button.innerHTML = '删除'; button.onclick = (event) => { event.preventDefault(); event.stopPropagation(); click115MoreMenuItem(toolbar, '删除'); }; wrapper.appendChild(button); return wrapper; } function promote115DeleteButton() { document.querySelectorAll('div.flex.items-center.bg-white.overflow-hidden').forEach((toolbar) => { if (!is115OperationToolbarElement(toolbar)) return; const firstChild = toolbar.firstElementChild; const deleteWrapper = find115ToolbarButtonWrapper(toolbar, '删除'); if (deleteWrapper && deleteWrapper.parentElement === toolbar) { if (firstChild !== deleteWrapper) toolbar.insertBefore(deleteWrapper, firstChild); hideFirstToolbarDivider(deleteWrapper); return; } let proxy = toolbar.querySelector('[data-aria115-delete-proxy="1"]'); if (!proxy) proxy = create115DeleteProxyButton(toolbar); if (toolbar.firstElementChild !== proxy) toolbar.insertBefore(proxy, toolbar.firstElementChild); }); } function hide115OperationButtons() { if (!document.body) return; promote115DeleteButton(); document.querySelectorAll('button, a, [role="menuitem"], li, [tabindex], div[class*="cursor-pointer"]').forEach((button) => { if (isAria115OwnedElement(button)) return; const label = normalizeText(button.textContent); if (!HIDDEN_115_OPERATION_TEXT.has(label)) return; if (is115OperationToolbar(button)) { hide115OperationElement(button, true); return; } if (is115MoreMenuItem(button)) hide115OperationElement(button, false); }); } function install115OperationCleaner() { const host = location.hostname.toLowerCase(); if (host !== '115.com' && !host.endsWith('.115.com')) return; if (!document.body) { window.setTimeout(install115OperationCleaner, 200); return; } const target = unsafeWindow || window; if (target.__aria115OperationCleaner) return; target.__aria115OperationCleaner = true; let timer = 0; const scan = () => { window.clearTimeout(timer); timer = window.setTimeout(hide115OperationButtons, 120); }; hide115OperationButtons(); const observer = new MutationObserver(scan); observer.observe(document.body, { childList: true, subtree: true }); } function buildOlistPrefix(settings) { const source = settings || DEFAULT_SETTINGS; const host = normalizeOpenlistHost(source.openlistHost); const mountPath = normalizeOpenlistMountPath(source.openlistMountPath); return `${host}/d/${encodePath(mountPath)}/`; } function parseStoredValue(raw, fallbackValue) { if (raw === undefined || raw === null || raw === '') return fallbackValue; if (typeof raw !== 'string') return raw; try { return JSON.parse(raw); } catch (err) { return raw; } } function readStorage(key, fallbackValue) { try { if (typeof GM_getValue === 'function') { const gmValue = GM_getValue(key); if (gmValue !== undefined && gmValue !== null && gmValue !== '') { return parseStoredValue(gmValue, fallbackValue); } } } catch (err) { console.warn('[115Aria] GM_getValue failed:', err); } try { const prefixedValue = localStorage.getItem(STORE_PREFIX + key); if (prefixedValue !== undefined && prefixedValue !== null && prefixedValue !== '') { return parseStoredValue(prefixedValue, fallbackValue); } return fallbackValue; } catch (err) { console.warn('[115Aria] localStorage read failed:', err); return fallbackValue; } } function writeStorage(key, value) { const data = JSON.stringify(value); try { if (typeof GM_setValue === 'function') { GM_setValue(key, data); return true; } } catch (err) { console.warn('[115Aria] GM_setValue failed:', err); } try { localStorage.setItem(STORE_PREFIX + key, data); return true; } catch (err) { console.warn('[115Aria] localStorage write failed:', err); return false; } } function makeId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } function splitEndpoint(endpoint) { const raw = String(endpoint || '').trim(); if (!raw) return null; try { const url = new URL(raw); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; return { domain: `${url.protocol}//${url.hostname}`, port: String(url.port || (url.protocol === 'https:' ? '443' : '80')), endpoint: `${url.protocol}//${url.hostname}:${url.port || (url.protocol === 'https:' ? '443' : '80')}` }; } catch (err) { return null; } } function sanitizeRpcConfig(input, index) { const source = input && typeof input === 'object' ? input : {}; const parsed = splitEndpoint(source.endpoint || `${source.domain || ''}${source.port ? `:${source.port}` : ''}`); const fallback = DEFAULT_SETTINGS.rpcConfigs[index] || DEFAULT_SETTINGS.rpcConfigs[0]; return { id: String(source.id || fallback.id || makeId('rpc')), name: String(source.name || fallback.name || `RPC-${index + 1}`).trim(), endpoint: parsed ? parsed.endpoint : fallback.endpoint, token: String(source.token || ''), path: normalizePath(source.path || fallback.path || '/jsonrpc') || '/jsonrpc' }; } function sanitizeSettings(rawInput) { const source = rawInput && typeof rawInput === 'object' ? rawInput : {}; let rpcConfigs = Array.isArray(source.rpcConfigs) ? source.rpcConfigs : DEFAULT_SETTINGS.rpcConfigs; rpcConfigs = rpcConfigs.map(sanitizeRpcConfig).filter((item) => splitEndpoint(item.endpoint)); if (rpcConfigs.length === 0) rpcConfigs = clone(DEFAULT_SETTINGS.rpcConfigs); let downloadPaths = Array.isArray(source.downloadPaths) ? source.downloadPaths : DEFAULT_SETTINGS.downloadPaths; downloadPaths = downloadPaths.map(normalizePath).filter(Boolean); downloadPaths = Array.from(new Set(downloadPaths)); if (downloadPaths.length === 0) downloadPaths = clone(DEFAULT_SETTINGS.downloadPaths); const activeRpcId = rpcConfigs.some((item) => item.id === source.activeRpcId) ? source.activeRpcId : rpcConfigs[0].id; const activePath = downloadPaths.includes(source.activePath) ? source.activePath : downloadPaths[0]; return { version: 1, openlistHost: normalizeOpenlistHost(source.openlistHost), openlistMountPath: normalizeOpenlistMountPath(source.openlistMountPath), activeRpcId, activePath, transferTargetCid: normalizeTransferTargetCid(source.transferTargetCid), transferCookie: String(source.transferCookie || ''), rpcConfigs, downloadPaths }; } function loadSettings() { const settings = sanitizeSettings(readStorage(SETTINGS_KEY)); saveSettings(settings); return settings; } function saveSettings(settings) { writeStorage(SETTINGS_KEY, settings); } function getActiveRpc(settings) { return settings.rpcConfigs.find((item) => item.id === settings.activeRpcId) || settings.rpcConfigs[0]; } function buildRpcUrl(rpc) { const endpoint = splitEndpoint(rpc.endpoint); if (!endpoint) throw new Error('RPC服务器未配置。'); const url = new URL(endpoint.endpoint); url.pathname = normalizePath(rpc.path || '/jsonrpc') || '/jsonrpc'; url.search = ''; url.hash = ''; return url.toString(); } function parseResponseJson(response) { if (response.response && typeof response.response === 'object') return response.response; const text = response.responseText || ''; return text ? JSON.parse(text) : null; } function gmRequestJson(options) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest不可用。')); return; } GM_xmlhttpRequest({ method: options.method || 'GET', url: options.url, headers: options.headers || {}, data: options.data, responseType: 'json', timeout: options.timeout || 30000, onload(response) { if (response.status >= 400) { reject(new Error(`HTTP ${response.status}: ${response.responseText || response.statusText}`)); return; } try { resolve(parseResponseJson(response)); } catch (err) { reject(err); } }, onerror() { reject(new Error('网络请求失败。')); }, ontimeout() { reject(new Error('网络请求超时。')); } }); }); } async function postJson(url, payload) { const data = JSON.stringify(payload); const headers = { 'Content-Type': 'application/json' }; if (typeof GM_xmlhttpRequest === 'function') { return gmRequestJson({ method: 'POST', url, headers, data }); } const response = await fetch(url, { method: 'POST', headers, body: data }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function getJson(url) { try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (err) { if (typeof GM_xmlhttpRequest !== 'function') throw err; return gmRequestJson({ method: 'GET', url }); } } function isSameOriginUrl(url) { try { return new URL(url, location.href).origin === location.origin; } catch (err) { return false; } } function is115CdnSharePage() { const host = location.hostname.toLowerCase(); return (host === '115.com' || host.endsWith('.115.com') || host === '115cdn.com' || host.endsWith('.115cdn.com')) && location.pathname.startsWith('/s/'); } function is115Host(hostname) { const host = String(hostname || location.hostname).toLowerCase(); return host === '115.com' || host.endsWith('.115.com') || host === '115cdn.com' || host.endsWith('.115cdn.com'); } function isHDHiveHost(hostname) { const host = String(hostname || location.hostname).toLowerCase(); return host === 'hdhive.com' || host.endsWith('.hdhive.com') || host === 'hdlive.com' || host.endsWith('.hdlive.com'); } function isBlockingAdOverlayElement(element) { return Boolean(element && element.tagName === 'DIV' && AD_OVERLAY_CLASS_SETS.some((classSet) => { return classSet.every((className) => element.classList.contains(className)); })); } function removeBlockingAdOverlays(root) { const scope = root || document.documentElement || document.body; if (!scope) return; if (scope.nodeType === 1 && isBlockingAdOverlayElement(scope)) { scope.remove(); return; } if (typeof scope.querySelectorAll !== 'function') return; scope.querySelectorAll('div').forEach((element) => { if (isBlockingAdOverlayElement(element)) element.remove(); }); } function installBlockingAdOverlayCleaner() { if (!is115Host() && !isHDHiveHost()) return; if (window.__aria115BlockingAdOverlayCleaner) return; window.__aria115BlockingAdOverlayCleaner = true; const scan = (root) => removeBlockingAdOverlays(root); const start = () => { const target = document.documentElement || document.body; if (!target) { window.setTimeout(start, 50); return; } scan(target); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => scan(node)); }); }); observer.observe(target, { childList: true, subtree: true }); }; start(); } function isHDHiveOrigin(origin) { try { return isHDHiveHost(new URL(origin).hostname); } catch (err) { return false; } } function is115ShareOrigin(origin) { try { const host = new URL(origin).hostname.toLowerCase(); return host === '115.com' || host.endsWith('.115.com') || host === '115cdn.com' || host.endsWith('.115cdn.com'); } catch (err) { return false; } } function parse115ShareLink(shareLink) { const raw = String(shareLink || '').trim().replace(/&/g, '&').replace(/\\\//g, '/'); if (!raw) return { success: false }; const directMatch = raw.match(/https?:\/\/(?:[^/\s"'<>]+\.)?(?:115cdn\.com|115\.com)\/s\/([^?/\s"'<>]+)[^\s"'<>]*/i); const candidate = directMatch ? directMatch[0] : raw; const urlText = /^https?:\/\//i.test(candidate) ? candidate : `https://${candidate.replace(/^\/\//, '')}`; try { const url = new URL(urlText); const host = url.hostname.toLowerCase(); if (host !== '115.com' && !host.endsWith('.115.com') && host !== '115cdn.com' && !host.endsWith('.115cdn.com')) { return { success: false }; } const codeMatch = url.pathname.match(/\/s\/([^/?#]+)/i); const hashPassword = (url.hash.match(/(?:password|pwd|pass)=(\w{4})/i) || [])[1] || ''; const rawPassword = (raw.match(/(?:password|pwd|pass)=(\w{4})/i) || [])[1] || ''; const textPassword = (raw.match(/(?:提取码|访问码|验证码|密码|口令)\s*[::=]?\s*([a-z0-9]{4})/i) || [])[1] || ''; const receiveCode = url.searchParams.get('password') || url.searchParams.get('pwd') || url.searchParams.get('pass') || hashPassword || rawPassword || textPassword || ''; if (!codeMatch || !/^\w{4}$/.test(receiveCode)) return { success: false }; return { success: true, shareCode: codeMatch[1], receiveCode, url: `https://115cdn.com/s/${codeMatch[1]}?password=${receiveCode}` }; } catch (err) { const codeMatch = raw.match(/\/s\/([^?/\s"'<>]+)/i); const passwordMatch = raw.match(/[?](?:password|pwd|pass)=(\w{4})/i) || raw.match(/(?:password|pwd|pass)=(\w{4})/i) || raw.match(/(?:提取码|访问码|验证码|密码|口令)\s*[::=]?\s*([a-z0-9]{4})/i); if (!codeMatch || !passwordMatch) return { success: false }; return { success: true, shareCode: codeMatch[1], receiveCode: passwordMatch[1], url: `https://115cdn.com/s/${codeMatch[1]}?password=${passwordMatch[1]}` }; } } function extract115ShareLinks(text) { const normalized = String(text || '').replace(/&/g, '&').replace(/\\\//g, '/'); const matches = normalized.match(/https?:\/\/(?:[^/\s"'<>]+\.)?(?:115cdn\.com|115\.com)\/s\/[^\s"'<>]+/gi) || []; return dedupe(matches.map((item) => { const cleaned = item.replace(/[\])}>,.;,。;、]+$/g, ''); const parsed = parse115ShareLink(cleaned); return parsed.success ? parsed.url : ''; })); } function find115ShareLinkFromText(text) { const normalized = String(text || '').replace(/&/g, '&').replace(/\\\//g, '/'); const matches = normalized.match(/https?:\/\/(?:[^/\s"'<>]+\.)?(?:115cdn\.com|115\.com)\/s\/[^\s"'<>]+/gi) || []; for (const item of matches) { const cleaned = item.replace(/[\])}>,.;,。;、]+$/g, ''); const parsed = parse115ShareLink(`${cleaned} ${normalized}`); if (parsed.success) return parsed.url; } return ''; } function gmRequestText(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest不可用。')); return; } GM_xmlhttpRequest({ method: 'GET', url, timeout: 30000, onload(response) { if (response.status >= 400) { reject(new Error(`HTTP ${response.status}: ${response.statusText || response.responseText || ''}`)); return; } resolve(response.responseText || ''); }, onerror() { reject(new Error('网络请求失败。')); }, ontimeout() { reject(new Error('网络请求超时。')); } }); }); } async function getText(url) { try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.text(); } catch (err) { return gmRequestText(url); } } function humanReadable(size) { const value = Number(size) || 0; if (value < 1024) return `${value}B`; if (value < 1024 ** 2) return `${(value / 1024).toFixed(2)}KB`; if (value < 1024 ** 3) return `${(value / 1024 / 1024).toFixed(2)}MB`; if (value < 1024 ** 4) return `${(value / 1024 / 1024 / 1024).toFixed(2)}GB`; return `${(value / 1024 / 1024 / 1024 / 1024).toFixed(2)}TB`; } function showToast(message, type, duration) { const text = String(message || ''); if (!text) return; if (!document.body) { window.setTimeout(() => showToast(text, type, duration), 200); return; } if (document.head) ensureStyle(); let wrap = document.getElementById('aria115-toast-wrap'); if (!wrap) { wrap = document.createElement('div'); wrap.id = 'aria115-toast-wrap'; document.body.appendChild(wrap); } const toast = document.createElement('div'); toast.className = `aria115-toast aria115-toast-${type || 'info'}`; toast.textContent = text; toast.title = '点击关闭'; toast.onclick = () => close(); let timer = 0; function close() { window.clearTimeout(timer); toast.classList.add('aria115-toast-out'); window.setTimeout(() => toast.remove(), 220); } wrap.appendChild(toast); timer = window.setTimeout(close, duration || (text.length > 80 || text.includes('\n') ? 9000 : 3200)); } async function request115Json(method, url, data, cookie) { const headers = {}; if (method === 'POST') headers['Content-Type'] = 'application/x-www-form-urlencoded'; if (cookie) headers.Cookie = cookie; if (!cookie && isSameOriginUrl(url) && typeof fetch === 'function') { const response = await fetch(url, { method, credentials: 'include', headers, body: method === 'POST' ? data : undefined }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } return gmRequestJson({ method, url, headers, data }); } async function get115ShareSize(shareCode, receiveCode, cookie) { const snapshot = await get115ShareSnapshot(shareCode, receiveCode, cookie); return snapshot.size; } async function get115ShareSnapshot(shareCode, receiveCode, cookie) { const params = new URLSearchParams({ _v: '2', share_code: shareCode, receive_code: receiveCode, offset: '0', limit: String(FOLDER_SCAN_LIMIT), cid: '' }); const payload = await request115Json('GET', `${SHARE_SNAP_API}?${params.toString()}`, undefined, cookie); const list = payload && payload.data && Array.isArray(payload.data.list) ? payload.data.list : []; const fallbackList = payload && payload.data && Array.isArray(payload.data) ? payload.data : []; const items = list.length > 0 ? list : fallbackList; const fileIds = dedupe(items.map((item) => { if (!item) return ''; if (String(item.p) === '0') return normalizePath(item.cid || item.file_id || item.fid || item.id); return normalizePath(item.fid || item.file_id || item.id || item.cid); }).filter(Boolean)); const first = list[0] || fallbackList[0]; return { payload, list: items, fileIds, size: first ? humanReadable(first.s || first.size || 0) : '' }; } function get115TransferFailure(payload) { const code = String(payload && (payload.errno || payload.errNo || payload.code || '')); const messages = { 4100024: '已经转存过该文件。可尝试清空 115 回收站后重试。', 4100008: '分享链接不存在或参数错误。', 4100010: '分享已取消。', 4100018: '分享已过期。' }; return messages[code] || ((payload && (payload.error || payload.message || payload.msg)) || '未知错误'); } function is115AlreadyTransferred(payload) { return String(payload && (payload.errno || payload.errNo || payload.code || '')) === '4100024'; } function build115ReceiveData(parsed, targetCid, fileId, isCheck) { const data = new URLSearchParams({ share_code: parsed.shareCode, receive_code: parsed.receiveCode }); if (fileId) data.append('file_id', fileId); if (targetCid) data.append('cid', targetCid); if (typeof isCheck !== 'undefined') data.append('is_check', String(isCheck)); return data.toString(); } function formatTransferFailure(message) { const text = String(message || '未知错误'); return /^转存失败/.test(text) ? text : `转存失败:${text}`; } async function transfer115Share(shareLink, settings) { const parsed = parse115ShareLink(shareLink); if (!parsed.success) throw new Error('无法解析115分享链接或提取码。'); const source = settings || loadSettings(); const cookie = normalizePath(source.transferCookie || ''); const targetCid = normalizeTransferTargetCid(source.transferTargetCid); let snapshot = null; try { snapshot = await get115ShareSnapshot(parsed.shareCode, parsed.receiveCode, cookie); } catch (err) { snapshot = null; } const fileId = snapshot && snapshot.fileIds.length > 0 ? snapshot.fileIds.join(',') : ''; let payload = await request115Json('POST', SHARE_RECEIVE_API, build115ReceiveData(parsed, targetCid, fileId), cookie); if ((!payload || payload.state !== true) && is115AlreadyTransferred(payload) && fileId) { payload = await request115Json('POST', SHARE_RECEIVE_API, build115ReceiveData(parsed, targetCid, fileId, 0), cookie); } if ((!payload || payload.state !== true) && !fileId) { payload = await request115Json('POST', SHARE_RECEIVE_API, build115ReceiveData(parsed, targetCid, '', 0), cookie); } if (!payload || payload.state !== true) throw new Error(get115TransferFailure(payload)); const size = snapshot ? snapshot.size : ''; return { success: true, message: size ? `115转存成功 [${size}]` : '115转存成功', fileSize: size, targetCid }; } async function sendToAria2(rpc, dir, url) { const params = []; const token = normalizePath(rpc.token); if (token) params.push(`token:${token}`); params.push([url]); params.push(dir ? { dir } : {}); const result = await postJson(buildRpcUrl(rpc), { jsonrpc: '2.0', id: `aria115-${Date.now()}-${Math.random().toString(16).slice(2)}`, method: 'aria2.addUri', params }); if (result && result.error) { const message = result.error.message || JSON.stringify(result.error); if (/Unauthorized/i.test(message)) { throw new Error('RPC认证失败,请在“设置”里填写正确的 aria2 token。'); } throw new Error(message); } return result ? result.result : null; } async function testAria2(rpc) { const params = []; const token = normalizePath(rpc.token); if (token) params.push(`token:${token}`); const result = await postJson(buildRpcUrl(rpc), { jsonrpc: '2.0', id: `aria115-test-${Date.now()}-${Math.random().toString(16).slice(2)}`, method: 'aria2.getVersion', params }); if (result && result.error) { const message = result.error.message || JSON.stringify(result.error); if (/Unauthorized/i.test(message)) { throw new Error('RPC认证失败,请检查 aria2 token。'); } throw new Error(message); } return result && result.result ? result.result.version || 'OK' : 'OK'; } function encodePath(path) { return stripSlashes(path) .split('/') .filter(Boolean) .map((part) => encodeURIComponent(part)) .join('/'); } function buildOlistUrl(filePath, source) { const prefix = buildOlistPrefix(source || DEFAULT_SETTINGS); return prefix + encodePath(filePath); } function getUrlParam(name) { const sources = [location.search, location.hash, location.href]; const matcher = new RegExp(`[?]${name}=([^]+)`); for (const source of sources) { const match = String(source || '').match(matcher); if (match) return safeDecode(match[1]); } return ''; } function getCurrentCid() { return getUrlParam('cid') || '0'; } function buildFilesApiUrl(cid, offset, limit) { const url = new URL('/api/proxy/115', location.origin); const params = new URLSearchParams({ domain: 'webapi', path: '/files', aid: '1', cid: cid || '0', offset: String(offset || 0), limit: String(limit || 1150), type: '0', show_dir: '1', fc_mix: '0', natsort: '1', count_folders: '1', record_open_time: '1', format: 'json', o: 'user_ptime', asc: '0' }); url.search = params.toString(); return url.toString(); } function buildPathIdApiUrl(parentCid, pathValue) { const url = new URL('/api/proxy/115', location.origin); const params = new URLSearchParams(); params.append('domain', 'webapi'); params.append('path', '/files/get_path_id'); params.append('path', stripSlashes(pathValue)); params.append('parent_id', parentCid || '0'); params.append('is_create', '0'); params.append('format', 'json'); url.search = params.toString(); return url.toString(); } function getApiItems(payload) { if (!payload || typeof payload !== 'object') return []; if (Array.isArray(payload.data)) return payload.data; if (payload.data && Array.isArray(payload.data.list)) return payload.data.list; if (payload.data && Array.isArray(payload.data.data)) return payload.data.data; if (payload.data && Array.isArray(payload.data.files)) return payload.data.files; if (Array.isArray(payload.list)) return payload.list; if (Array.isArray(payload.files)) return payload.files; if (Array.isArray(payload.items)) return payload.items; return []; } function getApiItemName(item) { if (!item || typeof item !== 'object') return ''; return normalizeText(item.n || item.name || item.file_name || item.filename || item.title || ''); } function getApiItemCid(item) { if (!item || typeof item !== 'object') return ''; const value = item.cid || item.folder_id || item.category_id || item.file_id || item.id; return value === undefined || value === null ? '' : String(value); } function isApiFolder(item) { if (!item || typeof item !== 'object') return false; if (item.fid || item.file_id || item.pick_code || item.pickcode) return false; if (String(item.is_dir || item.isdir || item.isFolder || item.fc || '') === '1') return true; if (String(item.type || '').toLowerCase() === 'dir') return true; if (String(item.type || '').toLowerCase() === 'folder') return true; if (item.cid || item.folder_id || item.category_id) return true; return false; } async function fetchFolderItems(cid) { const result = []; const limit = 1150; let offset = 0; while (true) { const payload = await getJson(buildFilesApiUrl(cid, offset, limit)); const items = getApiItems(payload); result.push(...items); if (items.length < limit) break; offset += items.length; if (offset > FOLDER_SCAN_LIMIT) break; } return result; } function getPathIdFromPayload(payload) { if (!payload || typeof payload !== 'object') return ''; const data = payload.data && typeof payload.data === 'object' ? payload.data : payload; const value = data.file_id || data.cid || data.folder_id || data.category_id || data.id; return value === undefined || value === null ? '' : String(value); } async function fetchFolderCidByPath(parentCid, pathValue) { const payload = await getJson(buildPathIdApiUrl(parentCid, pathValue)); if (payload && (payload.state === false || payload.errNo || payload.errno)) { return ''; } return getPathIdFromPayload(payload); } async function collectFilesFromFolder(folderCid, folderParts, output) { if (output.length >= FOLDER_SCAN_LIMIT) return; const items = await fetchFolderItems(folderCid); for (const item of items) { if (output.length >= FOLDER_SCAN_LIMIT) return; const name = getApiItemName(item); if (!name) continue; if (isApiFolder(item)) { const childCid = getApiItemCid(item); if (childCid) await collectFilesFromFolder(childCid, folderParts.concat(name), output); } else { output.push(folderParts.concat(name).map(stripSlashes).filter(Boolean).join('/')); } } } function escapeCssIdent(value) { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value); return String(value).replace(/([^a-zA-Z0-9_-])/g, '\\$1'); } function exactClassSelector(className) { return className.split(/\s+/).filter(Boolean).map((item) => `.${escapeCssIdent(item)}`).join(''); } function cleanBreadcrumbPart(value) { return normalizeText(value) .replace(/^根目录\s*/, '') .replace(/^[/\\>›»]+|[/\\>›»]+$/g, '') .trim(); } function get115FolderParts() { const selector = `div${exactClassSelector(BREADCRUMB_CLASS)}`; const nodes = Array.from(document.querySelectorAll(selector)); const target = nodes.find((node) => Array.from(node.querySelectorAll('button')).some((button) => normalizeText(button.getAttribute('title') || button.textContent) === '根目录')) || nodes.find((node) => normalizeText(node.innerText || node.textContent).includes('根目录')) || nodes[0]; if (!target) return []; const buttonParts = Array.from(target.querySelectorAll('button')) .map((node) => cleanBreadcrumbPart(node.getAttribute('title') || node.innerText || node.textContent)) .filter((text) => text && text !== '根目录'); if (buttonParts.length > 0) return buttonParts; return normalizeText(target.innerText || target.textContent) .split(/[\n/>›»]+/) .map(cleanBreadcrumbPart) .filter((text) => text && text !== '根目录'); } function isUsefulName(value) { const name = normalizeText(value); if (!name || BAD_NAME_TEXT.has(name.toLowerCase())) return false; if (/^\d+(\.\d+)?\s*(B|KB|MB|GB|TB)$/i.test(name)) return false; if (/^\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(name)) return false; return true; } function readAttrDeep(element, names) { for (const name of names) { const value = element.getAttribute && element.getAttribute(name); if (value && isUsefulName(value)) return normalizeText(value); } const selector = names.map((name) => `[${name}]`).join(','); const child = selector ? element.querySelector(selector) : null; if (!child) return ''; for (const name of names) { const value = child.getAttribute(name); if (value && isUsefulName(value)) return normalizeText(value); } return ''; } function readNameFromElement(element) { const attrName = readAttrDeep(element, ['data-name', 'data-filename', 'data-file-name', 'file_name', 'filename']); if (attrName) return attrName; const nameNode = element.querySelector('.file-name-responsive[title], [class*="file-name"][title], [class*="filename"][title], [class*="name"][title], [class*="Name"][title]'); if (nameNode) { const value = normalizeText(nameNode.getAttribute('title') || nameNode.innerText || nameNode.textContent); if (isUsefulName(value)) return value; } const imgNode = element.querySelector('img[title], img[alt]'); if (imgNode) { const value = normalizeText(imgNode.getAttribute('title') || imgNode.getAttribute('alt')); if (isUsefulName(value)) return value; } const titleName = readAttrDeep(element, ['title', 'aria-label']); if (titleName) return titleName; return ''; } function isFolderElement(element) { const text = normalizeText(element.innerText || element.textContent); if (text.includes('文件夹')) return true; const icon = element.querySelector('img[src*="folder"], img[src*="dir"], img[alt*="文件夹"], img[title*="文件夹"], i[class*="folder"], i[class*="Folder"], i[class*="dir"], i[class*="Dir"]'); return Boolean(icon); } function hasFileLikeExtension(name) { const cleanName = stripSlashes(name).split('/').pop() || ''; return /\.[a-z0-9]{1,10}$/i.test(cleanName); } function isFolderLikeEntry(entry) { return Boolean(entry && (entry.isFolder || !hasFileLikeExtension(entry.name))); } function normalizeSelectedElement(element) { const indexedRow = element.closest('[data-index]'); if (indexedRow) return indexedRow; return element.closest('[data-fid], [data-file-id], [data-pickcode], [data-pick-code], [data-id], tr, li, [role="row"], [class*="file"], [class*="File"], [class*="row"], [class*="Row"], [class*="item"], [class*="Item"]') || element; } function getSelectedDomIndex(element) { const indexedRow = element && element.closest ? element.closest('[data-index]') : null; const value = indexedRow ? indexedRow.getAttribute('data-index') : ''; if (!/^\d+$/.test(value || '')) return -1; return Number(value); } function getSelectedEntriesFromDom() { const selected = new Set(); const selectors = [ 'tr.selected', 'tr[aria-selected="true"]', 'li.selected', 'li[aria-selected="true"]', '[role="row"].selected', '[role="row"][aria-selected="true"]', '.file-item.selected', '.file-item[aria-selected="true"]', '.list-item.selected', '.list-item[aria-selected="true"]', '[data-fid].selected', '[data-file-id].selected', '[data-pickcode].selected', '[data-pick-code].selected', '.file-list-item.bg-blue-100', '.file-list-item .bg-blue-100', '[class*="selected"][data-fid]', '[class*="selected"][data-file-id]', '[class*="selected"][data-id]' ]; selectors.forEach((selector) => { document.querySelectorAll(selector).forEach((item) => selected.add(normalizeSelectedElement(item))); }); document.querySelectorAll('input[type="checkbox"]:checked').forEach((item) => { selected.add(normalizeSelectedElement(item)); }); return Array.from(selected) .filter((element) => !isAria115OwnedElement(element) && isElementActuallyVisible(element)) .map((element) => ({ element, name: readNameFromElement(element), isFolder: isFolderElement(element), domIndex: getSelectedDomIndex(element) })) .filter((item) => isUsefulName(item.name)); } function getSelectedFileNamesFromDom() { return getSelectedEntriesFromDom().map((item) => item.name); } function buildRelativePath(folderParts, fileName) { const name = stripSlashes(fileName); if (!name) return ''; if (name.includes('/')) return name; return folderParts.concat(name).map(stripSlashes).filter(Boolean).join('/'); } function dedupe(list) { return Array.from(new Set(list.filter(Boolean))); } function promptManualPaths(folderParts) { const folderPath = folderParts.join('/') || '/'; const input = window.prompt(`未能识别115选中文件。\n当前目录: ${folderPath}\n请输入115文件路径或当前目录下文件名,每行一个:`, ''); if (!input) return []; return input .split(/\r?\n/) .map(normalizeText) .filter(Boolean) .map((line) => buildRelativePath(folderParts, line)); } function getCurrentFolderPath() { return get115FolderParts().join('/') || '根目录'; } function collectDetectedFilePaths() { const folderParts = get115FolderParts(); const fileNames = getSelectedEntriesFromDom() .filter((item) => !item.isFolder) .map((item) => item.name); return dedupe(fileNames.map((name) => buildRelativePath(folderParts, name))); } function matchApiItemForSelection(selectedItem, items, preferredFolder) { const targetName = normalizeText(selectedItem.name); const domIndex = selectedItem.domIndex; if (Number.isInteger(domIndex) && domIndex >= 0 && domIndex < items.length) { const indexedItem = items[domIndex]; const indexedFolder = isApiFolder(indexedItem); if (normalizeText(getApiItemName(indexedItem)) === targetName && (preferredFolder ? indexedFolder : !indexedFolder)) { return { item: indexedItem, ambiguous: false }; } } const sameName = items.filter((item) => normalizeText(getApiItemName(item)) === targetName); if (sameName.length === 0) return { item: null, ambiguous: false }; const typedMatches = sameName.filter((item) => preferredFolder ? isApiFolder(item) : !isApiFolder(item)); const candidates = typedMatches.length > 0 ? typedMatches : sameName; if (candidates.length === 1) return { item: candidates[0], ambiguous: false }; return { item: null, ambiguous: true }; } async function collectSelectedFilePaths() { const folderParts = get115FolderParts(); const currentCid = getCurrentCid(); const selected = getSelectedEntriesFromDom(); if (selected.length === 0) { return dedupe(promptManualPaths(folderParts)); } let currentItems = []; let apiError = null; try { currentItems = await fetchFolderItems(currentCid); } catch (err) { apiError = err; } const preparedSelected = selected.map((selectedItem) => { const initialFolderLike = isFolderLikeEntry(selectedItem); const matchResult = currentItems.length > 0 ? matchApiItemForSelection(selectedItem, currentItems, initialFolderLike) : { item: null, ambiguous: false }; const matched = matchResult.item; const folderLike = matched ? isApiFolder(matched) : initialFolderLike; return { selectedItem, folderLike, matched, ambiguous: matchResult.ambiguous }; }); const hasAnyCurrentMatch = currentItems.length > 0 && preparedSelected.some((item) => item.matched); const output = []; for (const item of preparedSelected) { const { selectedItem, folderLike, matched, ambiguous } = item; if (ambiguous) { throw new Error(`当前目录存在多个同名项目:${selectedItem.name},无法安全判断选中项,请刷新页面后重试。`); } if (hasAnyCurrentMatch && !matched) { continue; } if (matched && isApiFolder(matched)) { const folderCid = getApiItemCid(matched); if (!folderCid) throw new Error(`文件夹 ${selectedItem.name} 未取到 cid,无法递归。`); await collectFilesFromFolder(folderCid, folderParts.concat(selectedItem.name), output); continue; } if (folderLike) { let folderCid = ''; try { folderCid = await fetchFolderCidByPath(currentCid, selectedItem.name); } catch (err) { apiError = err; } if (!folderCid) { throw new Error(`文件夹 ${selectedItem.name} 未能取到 cid,已停止发送目录直链。${apiError ? ` ${apiError.message || apiError}` : ''}`); } await collectFilesFromFolder(folderCid, folderParts.concat(selectedItem.name), output); continue; } output.push(buildRelativePath(folderParts, selectedItem.name)); } return dedupe(output); } function ensureStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${CONTAINER_ID} { position: fixed; bottom: 22px; right: 22px; z-index: 999998; display: flex; flex-direction: column; align-items: center; gap: 8px; width: min(760px, calc(100vw - 44px)); padding: 10px; border-radius: 18px; background: linear-gradient(135deg, rgba(250, 253, 255, 0.96), rgba(236, 245, 255, 0.92)); border: 1px solid rgba(39, 119, 248, 0.22); box-shadow: 0 18px 44px rgba(15, 23, 42, 0.18); backdrop-filter: blur(14px); color: #172033; font-size: 13px; } .aria115-top { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .aria115-brand { display: flex; align-items: center; gap: 10px; min-width: 180px; font-weight: 800; color: #1d4ed8; } .aria115-logo { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 10px; color: #fff; background: linear-gradient(135deg, #2777f8, #74b8ff); box-shadow: 0 8px 18px rgba(39, 119, 248, 0.28); font-weight: 900; } .aria115-status { flex: 1; min-width: 0; color: #50627c; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } .aria115-controls { width: 100%; display: grid; grid-template-columns: auto minmax(120px, 1fr) minmax(180px, 1.4fr) auto auto auto; gap: 8px; align-items: center; } .aria115-select { height: 34px; min-width: 0; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 10px; background: #fff; color: #172033; outline: none; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); } .aria115-select:focus { border-color: #2777f8; box-shadow: 0 0 0 3px rgba(39, 119, 248, 0.12); } .aria115-btn { height: 34px; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 12px; background: rgba(255, 255, 255, 0.94); color: #24415f; cursor: pointer; font-size: 13px; transition: all .16s ease; white-space: nowrap; } .aria115-btn:hover { border-color: #2777f8; color: #0d67e6; transform: translateY(-1px); } .aria115-primary { background: linear-gradient(135deg, #2777f8, #0d67e6); border-color: #2777f8; color: #fff; box-shadow: 0 8px 18px rgba(39, 119, 248, 0.24); } .aria115-primary:hover { border-color: #0d67e6; color: #fff; } .aria115-primary:disabled { cursor: wait; opacity: 0.72; } #${MODAL_ID} { position: fixed; inset: 0; z-index: 999999; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(15, 23, 42, 0.45); } .aria115-panel { width: 980px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; background: #fff; border-radius: 18px; border: 1px solid #d4e2f4; box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18); color: #213547; overflow: hidden; } .aria115-head, .aria115-footer { padding: 16px 18px; background: #f9fbff; } .aria115-head { border-bottom: 1px solid #e6edf6; } .aria115-head h3 { margin: 0; font-size: 18px; color: #17324d; } .aria115-head p { margin: 6px 0 0; color: #6b7f96; font-size: 12px; } .aria115-body { padding: 16px 18px; display: grid; gap: 14px; overflow: auto; } .aria115-section { border: 1px solid #e3edf8; border-radius: 14px; background: #fbfdff; padding: 12px; } .aria115-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; } .aria115-label { font-weight: 800; font-size: 14px; color: #27486b; } .aria115-tip { margin-top: 4px; color: #6b7280; font-size: 12px; } .aria115-input { width: 100%; height: 34px; box-sizing: border-box; border: 1px solid #c7d6e8; border-radius: 10px; padding: 0 10px; outline: none; background: #fff; color: #1f2937; font-size: 13px; } .aria115-input:focus { border-color: #2777f8; box-shadow: 0 0 0 3px rgba(39, 119, 248, 0.12); } .aria115-rpc-row, .aria115-path-row { display: grid; gap: 8px; margin-top: 8px; padding: 8px; border: 1px solid #e6edf6; border-radius: 12px; background: #fff; } .aria115-olist-row, .aria115-transfer-row { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 10px; } .aria115-rpc-row { grid-template-columns: 120px minmax(210px, 1.4fr) minmax(120px, 1fr) 100px auto; } .aria115-path-row { grid-template-columns: 1fr auto; } .aria115-field-title { margin: 0 0 5px; color: #64748b; font-size: 11px; } .aria115-error { display: none; padding: 10px; border-radius: 8px; background: #fff4f2; border: 1px solid #ffb3ab; color: #b42318; font-size: 12px; } .aria115-footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid #e6edf6; } .aria115-share-panel, .aria115-hdhive-panel { position: fixed; right: 20px; bottom: 20px; z-index: 999998; min-width: 270px; max-width: min(420px, calc(100vw - 40px)); padding: 12px; border-radius: 16px; background: rgba(255, 255, 255, 0.96); border: 1px solid rgba(39, 119, 248, 0.22); box-shadow: 0 18px 44px rgba(15, 23, 42, 0.18); color: #172033; font-size: 13px; } .aria115-mini-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; font-weight: 800; color: #1d4ed8; } .aria115-mini-status { margin-bottom: 10px; color: #50627c; font-size: 12px; line-height: 1.5; word-break: break-all; } .aria115-mini-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; } .aria115-hd-card-btn { display: inline-flex; align-items: center; justify-content: center; height: 32px; margin-left: auto; padding: 0 12px; border: 1px solid rgba(13, 71, 161, 0.24); border-radius: 999px; background: rgba(227, 242, 253, 0.98); color: #0d47a1; cursor: pointer; font-size: 14px; font-weight: 700; line-height: 32px; box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12); user-select: none; } .aria115-hd-card-btn:hover { background: #1976d2; color: #fff; } .aria115-hd-card-btn:disabled { cursor: wait; opacity: 0.72; } #aria115-toast-wrap { position: fixed; top: 18px; right: 18px; z-index: 1000002; display: flex; flex-direction: column; gap: 10px; width: min(420px, calc(100vw - 36px)); pointer-events: none; } .aria115-toast { pointer-events: auto; padding: 12px 14px; border-radius: 12px; background: rgba(15, 23, 42, 0.94); color: #fff; box-shadow: 0 12px 28px rgba(15, 23, 42, 0.22); font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; cursor: pointer; animation: aria115ToastIn .22s ease-out; } .aria115-toast-success { background: linear-gradient(135deg, #16a34a, #15803d); } .aria115-toast-error { background: linear-gradient(135deg, #ef4444, #b91c1c); } .aria115-toast-warning { background: linear-gradient(135deg, #f59e0b, #b45309); } .aria115-toast-out { opacity: 0; transform: translateX(18px); transition: all .2s ease; } @keyframes aria115ToastIn { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); } } @media (max-width: 860px) { #${CONTAINER_ID} { left: 8px; right: 8px; bottom: 8px; width: auto; } .aria115-top { align-items: flex-start; flex-direction: column; } .aria115-status { width: 100%; text-align: left; } .aria115-controls { grid-template-columns: 1fr 1fr; } .aria115-olist-row, .aria115-transfer-row, .aria115-rpc-row, .aria115-path-row { grid-template-columns: 1fr; } .aria115-share-panel, .aria115-hdhive-panel { left: 8px; right: 8px; bottom: 8px; max-width: none; } #aria115-toast-wrap { left: 8px; right: 8px; top: 8px; width: auto; } } `; document.head.appendChild(style); } function createButton(text, className) { const button = document.createElement('button'); button.type = 'button'; button.className = `aria115-btn ${className || ''}`.trim(); button.textContent = text; return button; } function createInput(value, placeholder, type) { const input = document.createElement('input'); input.className = 'aria115-input'; input.type = type || 'text'; input.value = value || ''; input.placeholder = placeholder || ''; if (input.type === 'password') input.autocomplete = 'new-password'; return input; } function createField(title, input) { const wrap = document.createElement('label'); const label = document.createElement('div'); label.className = 'aria115-field-title'; label.textContent = title; wrap.appendChild(label); wrap.appendChild(input); return wrap; } function renderSelectOptions(select, values, selectedValue, getText) { select.innerHTML = ''; values.forEach((item) => { const option = document.createElement('option'); option.value = typeof item === 'string' ? item : item.id; option.text = getText ? getText(item) : option.value; select.appendChild(option); }); select.value = selectedValue; } function openSettingsModal(settings, onSave) { const old = document.getElementById(MODAL_ID); if (old) old.remove(); let draft = clone(settings); const overlay = document.createElement('div'); overlay.id = MODAL_ID; const panel = document.createElement('div'); panel.className = 'aria115-panel'; const head = document.createElement('div'); head.className = 'aria115-head'; head.innerHTML = '