115.com OpenList直链发送到aria2 RPC
// ==UserScript==
// @name 115Aria
// @namespace http://tampermonkey.net/
// @version 0.45.12
// @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
// ==/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 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 = '<span class="flex items-center" style="margin-right:5px;"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><g fill="none" fill-rule="evenodd"><path d="M0 0h20v20H0z"></path><path fill="#2777F8" fill-rule="nonzero" d="M7 2h6a1 1 0 0 1 1 1v1h3v2H3V4h3V3a1 1 0 0 1 1-1m1 2h4V3H8zm-3 4h10l-.7 8.4A2 2 0 0 1 12.3 18H7.7a2 2 0 0 1-1.99-1.6zm3 2v5h2v-5zm3 0v5h2v-5z"></path></g></svg></span><span>删除</span>';
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 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 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 = '<h3>115Aria 设置</h3>';
const body = document.createElement('div');
body.className = 'aria115-body';
const olistSection = document.createElement('div');
olistSection.className = 'aria115-section';
const olistHead = document.createElement('div');
olistHead.className = 'aria115-section-head';
const olistLabel = document.createElement('div');
olistLabel.className = 'aria115-label';
olistLabel.textContent = 'OpenList 直链配置';
olistHead.appendChild(olistLabel);
const olistRow = document.createElement('div');
olistRow.className = 'aria115-olist-row';
const openlistHostInput = createInput(draft.openlistHost || DEFAULT_OPENLIST_HOST, '');
const openlistMountInput = createInput(draft.openlistMountPath || DEFAULT_OPENLIST_MOUNT_PATH, '例如 115');
const olistTip = document.createElement('div');
olistTip.className = 'aria115-tip';
olistTip.textContent = '需openlist关闭签名。填写openlist主机,填写挂载路径 如:媒体/115。';
olistRow.appendChild(createField('OpenList 主机', openlistHostInput));
olistRow.appendChild(createField('115 挂载路径', openlistMountInput));
olistSection.appendChild(olistHead);
olistSection.appendChild(olistRow);
olistSection.appendChild(olistTip);
const transferSection = document.createElement('div');
transferSection.className = 'aria115-section';
const transferHead = document.createElement('div');
transferHead.className = 'aria115-section-head';
const transferLabel = document.createElement('div');
transferLabel.className = 'aria115-label';
transferLabel.textContent = '115 分享转存';
transferHead.appendChild(transferLabel);
const transferRow = document.createElement('div');
transferRow.className = 'aria115-transfer-row';
const transferTargetCidInput = createInput(draft.transferTargetCid || DEFAULT_TRANSFER_TARGET_CID, DEFAULT_TRANSFER_TARGET_CID);
const transferCookieInput = createInput('', draft.transferCookie ? '已保存,留空不修改' : '可选,HDHive跨站转存建议填写', 'password');
const transferTip = document.createElement('div');
transferTip.className = 'aria115-tip';
transferTip.textContent = '可填纯 CID、cid=xxx,或带 cid 参数的115目录链接。HDLive/HDHive 转存按 1.js 方式执行,需要填写 115 Cookie。';
transferCookieInput.oninput = () => {
draft._transferCookieEdited = true;
draft._transferCookieValue = transferCookieInput.value;
};
transferRow.appendChild(createField('目标文件夹 CID', transferTargetCidInput));
transferRow.appendChild(createField('115 Cookie', transferCookieInput));
transferSection.appendChild(transferHead);
transferSection.appendChild(transferRow);
transferSection.appendChild(transferTip);
const rpcSection = document.createElement('div');
rpcSection.className = 'aria115-section';
const rpcHead = document.createElement('div');
rpcHead.className = 'aria115-section-head';
const rpcLabel = document.createElement('div');
rpcLabel.className = 'aria115-label';
rpcLabel.textContent = 'RPC 服务器';
const rpcAdd = createButton('+ 新增RPC');
const rpcWrap = document.createElement('div');
rpcHead.appendChild(rpcLabel);
rpcHead.appendChild(rpcAdd);
rpcSection.appendChild(rpcHead);
rpcSection.appendChild(rpcWrap);
const pathSection = document.createElement('div');
pathSection.className = 'aria115-section';
const pathHead = document.createElement('div');
pathHead.className = 'aria115-section-head';
const pathLabel = document.createElement('div');
pathLabel.className = 'aria115-label';
pathLabel.textContent = '下载路径';
const pathAdd = createButton('+ 新增路径');
const pathWrap = document.createElement('div');
pathHead.appendChild(pathLabel);
pathHead.appendChild(pathAdd);
pathSection.appendChild(pathHead);
pathSection.appendChild(pathWrap);
const errorBox = document.createElement('div');
errorBox.className = 'aria115-error';
const footer = document.createElement('div');
footer.className = 'aria115-footer';
const reset = createButton('恢复默认');
const cancel = createButton('取消');
const save = createButton('保存', 'aria115-primary');
body.appendChild(olistSection);
body.appendChild(transferSection);
body.appendChild(rpcSection);
body.appendChild(pathSection);
body.appendChild(errorBox);
footer.appendChild(reset);
footer.appendChild(cancel);
footer.appendChild(save);
panel.appendChild(head);
panel.appendChild(body);
panel.appendChild(footer);
overlay.appendChild(panel);
document.body.appendChild(overlay);
function showError(message) {
errorBox.textContent = message;
errorBox.style.display = 'block';
}
function renderRpcRows() {
rpcWrap.innerHTML = '';
draft.rpcConfigs.forEach((rpc, index) => {
const row = document.createElement('div');
row.className = 'aria115-rpc-row';
const name = createInput(rpc.name, '名称');
const endpoint = createInput(rpc.endpoint, 'http://127.0.0.1:6800');
const token = createInput('', rpc.token ? '已保存,留空不修改' : 'Token 可留空', 'password');
const rpcPath = createInput(rpc.path || '/jsonrpc', '/jsonrpc');
const del = createButton('删除');
name.oninput = () => { draft.rpcConfigs[index].name = name.value; };
endpoint.oninput = () => { draft.rpcConfigs[index].endpoint = endpoint.value; };
token.oninput = () => {
draft.rpcConfigs[index]._tokenEdited = true;
draft.rpcConfigs[index]._tokenValue = token.value;
};
rpcPath.oninput = () => { draft.rpcConfigs[index].path = rpcPath.value; };
del.onclick = () => {
const removed = draft.rpcConfigs[index];
draft.rpcConfigs.splice(index, 1);
if (removed && removed.id === draft.activeRpcId) {
draft.activeRpcId = draft.rpcConfigs[0] ? draft.rpcConfigs[0].id : '';
}
renderRpcRows();
};
row.appendChild(createField('名称', name));
row.appendChild(createField('地址', endpoint));
row.appendChild(createField('Token', token));
row.appendChild(createField('路径', rpcPath));
row.appendChild(del);
rpcWrap.appendChild(row);
});
}
function renderPathRows() {
pathWrap.innerHTML = '';
draft.downloadPaths.forEach((path, index) => {
const row = document.createElement('div');
row.className = 'aria115-path-row';
const input = createInput(path, '/Users/Administrator/Downloads');
const del = createButton('删除');
input.oninput = () => { draft.downloadPaths[index] = input.value; };
del.onclick = () => {
draft.downloadPaths.splice(index, 1);
if (!draft.downloadPaths.includes(draft.activePath)) {
draft.activePath = draft.downloadPaths[0] || '';
}
renderPathRows();
};
row.appendChild(input);
row.appendChild(del);
pathWrap.appendChild(row);
});
}
reset.onclick = () => {
draft = clone(DEFAULT_SETTINGS);
openlistHostInput.value = draft.openlistHost;
openlistMountInput.value = draft.openlistMountPath;
transferTargetCidInput.value = draft.transferTargetCid;
transferCookieInput.value = '';
errorBox.style.display = 'none';
renderRpcRows();
renderPathRows();
};
cancel.onclick = () => overlay.remove();
overlay.addEventListener('click', (event) => {
if (event.target === overlay) overlay.remove();
});
save.onclick = () => {
const errors = [];
draft.openlistHost = normalizeOpenlistHost(openlistHostInput.value);
draft.openlistMountPath = normalizeOpenlistMountPath(openlistMountInput.value);
draft.transferTargetCid = normalizeTransferTargetCid(transferTargetCidInput.value);
draft.transferCookie = draft._transferCookieEdited ? String(draft._transferCookieValue || '') : String(draft.transferCookie || '');
draft.rpcConfigs = draft.rpcConfigs.map((rpc, index) => ({
id: rpc.id || makeId(`rpc${index}`),
name: normalizeText(rpc.name) || `RPC-${index + 1}`,
endpoint: normalizePath(rpc.endpoint),
token: rpc._tokenEdited ? String(rpc._tokenValue || '') : String(rpc.token || ''),
path: normalizePath(rpc.path || '/jsonrpc') || '/jsonrpc'
}));
draft.downloadPaths = Array.from(new Set(draft.downloadPaths.map(normalizePath).filter(Boolean)));
if (!parseOpenlistHost(openlistHostInput.value)) {
errors.push('OpenList 主机必须是 http/https 地址。');
}
if (!normalizePath(openlistMountInput.value)) {
errors.push('115 挂载路径不能为空。');
}
if (draft.rpcConfigs.length === 0) {
errors.push('至少需要 1 个 RPC 配置。');
}
draft.rpcConfigs.forEach((rpc, index) => {
const parsed = splitEndpoint(rpc.endpoint);
if (!parsed) errors.push(`RPC #${index + 1} 地址格式错误。`);
else rpc.endpoint = parsed.endpoint;
if (!rpc.path.startsWith('/')) errors.push(`RPC #${index + 1} JSON-RPC 路径需以 / 开头。`);
});
if (draft.downloadPaths.length === 0) {
errors.push('至少需要 1 个下载路径。');
}
if (errors.length > 0) {
showError(errors.join(' '));
return;
}
if (!draft.rpcConfigs.some((rpc) => rpc.id === draft.activeRpcId)) {
draft.activeRpcId = draft.rpcConfigs[0].id;
}
if (!draft.downloadPaths.includes(draft.activePath)) {
draft.activePath = draft.downloadPaths[0];
}
onSave(sanitizeSettings(draft));
overlay.remove();
};
rpcAdd.onclick = () => {
draft.rpcConfigs.push({
id: makeId('rpc'),
name: '新RPC',
endpoint: 'http://127.0.0.1:6800',
token: '',
path: '/jsonrpc'
});
renderRpcRows();
};
pathAdd.onclick = () => {
draft.downloadPaths.push(DEFAULT_DIR);
renderPathRows();
};
renderRpcRows();
renderPathRows();
}
function mountUI() {
if (!document.body) {
window.setTimeout(mountUI, 200);
return;
}
ensureStyle();
let settings = loadSettings();
let summaryTimer = 0;
let container = document.getElementById(CONTAINER_ID);
if (container) container.remove();
container = document.createElement('div');
container.id = CONTAINER_ID;
const top = document.createElement('div');
top.className = 'aria115-top';
const brand = document.createElement('div');
brand.className = 'aria115-brand';
brand.innerHTML = '<span class="aria115-logo">A2</span><span>115Aria</span>';
const status = document.createElement('div');
status.className = 'aria115-status';
const controls = document.createElement('div');
controls.className = 'aria115-controls';
const settingsButton = createButton('设置');
const rpcSelect = document.createElement('select');
rpcSelect.className = 'aria115-select';
const pathSelect = document.createElement('select');
pathSelect.className = 'aria115-select';
const previewButton = createButton('预览');
const testButton = createButton('测试RPC');
const sendButton = createButton('发送RPC', 'aria115-primary');
function updateSummary() {
const folderPath = getCurrentFolderPath();
const selectedCount = getSelectedEntriesFromDom().length;
const prefix = buildOlistPrefix(settings);
status.textContent = `目录: ${folderPath} · 已选 ${selectedCount} 项 · ${prefix}`;
status.title = status.textContent;
}
function refresh() {
renderSelectOptions(rpcSelect, settings.rpcConfigs, settings.activeRpcId, (item) => item.name || item.endpoint);
renderSelectOptions(pathSelect, settings.downloadPaths, settings.activePath);
saveSettings(settings);
updateSummary();
}
settingsButton.onclick = () => {
openSettingsModal(settings, (next) => {
settings = next;
refresh();
});
};
rpcSelect.onchange = () => {
settings.activeRpcId = rpcSelect.value;
saveSettings(settings);
updateSummary();
};
pathSelect.onchange = () => {
settings.activePath = pathSelect.value;
saveSettings(settings);
updateSummary();
};
previewButton.onclick = async () => {
if (previewButton.disabled) return;
previewButton.disabled = true;
previewButton.textContent = '扫描中...';
try {
const filePaths = await collectSelectedFilePaths();
if (filePaths.length === 0) {
showToast('[115Aria] 当前没有识别到可发送文件。', 'warning');
return;
}
const urls = filePaths.map((filePath) => buildOlistUrl(filePath, settings));
showToast(urls.slice(0, 20).join('\n') + (urls.length > 20 ? `\n... 还有 ${urls.length - 20} 个` : ''), 'info', 12000);
} catch (err) {
showToast(`[115Aria] ${err.message || err}`, 'error');
} finally {
previewButton.disabled = false;
previewButton.textContent = '预览';
updateSummary();
}
};
testButton.onclick = async () => {
if (testButton.disabled) return;
testButton.disabled = true;
testButton.textContent = '测试中...';
try {
const version = await testAria2(getActiveRpc(settings));
showToast(`RPC连接正常,aria2 ${version}`, 'success');
} catch (err) {
showToast(`[115Aria] ${err.message || err}`, 'error');
} finally {
testButton.disabled = false;
testButton.textContent = '测试RPC';
}
};
sendButton.onclick = async () => {
if (sendButton.disabled) return;
sendButton.disabled = true;
sendButton.textContent = '识别中...';
try {
const filePaths = await collectSelectedFilePaths();
if (filePaths.length === 0) throw new Error('没有可发送的115文件路径。');
const rpc = getActiveRpc(settings);
const errors = [];
let success = 0;
for (let index = 0; index < filePaths.length; index += 1) {
const filePath = filePaths[index];
sendButton.textContent = `发送 ${index + 1}/${filePaths.length}`;
try {
await sendToAria2(rpc, settings.activePath, buildOlistUrl(filePath, settings));
success += 1;
} catch (err) {
errors.push(`${filePath}: ${err.message || err}`);
}
}
if (errors.length > 0) {
showToast(`已发送 ${success}/${filePaths.length} 个任务,失败 ${errors.length} 个:\n${errors.slice(0, 5).join('\n')}`, 'error', 10000);
} else {
sendButton.textContent = `已发送${success}个`;
showToast(`已发送 ${success} 个 aria2 任务`, 'success');
window.setTimeout(() => { sendButton.textContent = '发送RPC'; }, 1600);
}
updateSummary();
} catch (err) {
showToast(`[115Aria] ${err.message || err}`, 'error');
} finally {
sendButton.disabled = false;
if (!/^已发送/.test(sendButton.textContent)) sendButton.textContent = '发送RPC';
}
};
top.appendChild(brand);
top.appendChild(status);
controls.appendChild(settingsButton);
controls.appendChild(rpcSelect);
controls.appendChild(pathSelect);
controls.appendChild(previewButton);
controls.appendChild(testButton);
controls.appendChild(sendButton);
container.appendChild(top);
container.appendChild(controls);
document.body.appendChild(container);
refresh();
summaryTimer = window.setInterval(() => {
if (!document.body.contains(container)) {
window.clearInterval(summaryTimer);
return;
}
updateSummary();
}, 1200);
}
async function run115TransferAction(button, shareUrl, getSettings, setStatus) {
const originalText = button.textContent;
button.disabled = true;
button.textContent = '转存中...';
if (setStatus) setStatus('正在转存到115...');
try {
const result = await transfer115Share(shareUrl, getSettings());
button.textContent = '转存成功';
if (setStatus) setStatus(result.message);
showToast(`[115Aria] ${result.message}`, 'success');
window.setTimeout(() => {
button.disabled = false;
button.textContent = originalText;
}, 1800);
} catch (err) {
button.disabled = false;
button.textContent = originalText;
const message = err.message || err;
if (setStatus) setStatus(formatTransferFailure(message));
showToast(`[115Aria] ${formatTransferFailure(message)}`, 'error');
}
}
function mount115ShareTransferPanel() {
if (!document.body) {
window.setTimeout(mount115ShareTransferPanel, 200);
return;
}
ensureStyle();
let settings = loadSettings();
const old = document.getElementById('aria115-share-panel');
if (old) old.remove();
const panel = document.createElement('div');
panel.id = 'aria115-share-panel';
panel.className = 'aria115-share-panel';
const head = document.createElement('div');
head.className = 'aria115-mini-head';
head.textContent = '115Aria 分享转存';
const status = document.createElement('div');
status.className = 'aria115-mini-status';
const actions = document.createElement('div');
actions.className = 'aria115-mini-actions';
const open115Button = createButton('返回115');
const settingsButton = createButton('设置');
const transferButton = createButton('转存到115', 'aria115-primary');
function setStatus(message) {
status.textContent = `${message} · 目标CID: ${settings.transferTargetCid || '0'}`;
}
function getSettings() {
return settings;
}
settingsButton.onclick = () => {
openSettingsModal(settings, (next) => {
settings = next;
saveSettings(settings);
setStatus('设置已保存');
});
};
transferButton.onclick = () => run115TransferAction(transferButton, location.href, getSettings, setStatus);
open115Button.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
window.location.href = 'https://115.com/storage/netdisk?cid=0&mode=wangpan';
};
actions.appendChild(open115Button);
actions.appendChild(settingsButton);
actions.appendChild(transferButton);
panel.appendChild(head);
panel.appendChild(status);
panel.appendChild(actions);
document.body.appendChild(panel);
const parsed = parse115ShareLink(location.href);
setStatus(parsed.success ? '检测到115分享链接,准备转存' : '当前链接缺少115提取码,无法自动转存');
if (parsed.success) window.setTimeout(() => transferButton.click(), 700);
}
function relay115ShareToHDHiveOpener() {
if (!String(window.name || '').startsWith(HDHIVE_WINDOW_NAME_PREFIX)) return false;
const parsed = parse115ShareLink(location.href);
if (!parsed.success) return false;
const message = { type: HDHIVE_MESSAGE_TYPE, status: 'success', url: parsed.url };
if (window.opener) {
window.opener.postMessage(message, '*');
window.setTimeout(() => window.close(), 1000);
}
if (window.parent && window.parent !== window) window.parent.postMessage(message, '*');
return true;
}
function isHDHiveResourcePage() {
return /\/resource\/(115\/)?[\w-]+/i.test(location.pathname);
}
function isSafariBrowser() {
try {
const ua = navigator.userAgent;
return /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR|Android/.test(ua);
} catch (err) {
return false;
}
}
function verifyAndFormat115ShareUrl(rawUrl) {
const parsed = parse115ShareLink(rawUrl);
if (!parsed.success) return { success: false, msg: '无法解析115分享链接或提取码' };
return { success: true, url: parsed.url, msg: '解析成功' };
}
function is115ResourceLink(link) {
try {
const url = new URL(link.href || link.getAttribute('href') || '', location.origin);
return /\/resource\/115\//i.test(url.pathname);
} catch (err) {
return false;
}
}
function getHDHiveResourceType(container) {
const text = normalizeText(container ? container.textContent : '');
if (text.includes('免费') || text.includes('已解锁')) return 'free';
if (text.includes('积分')) return 'paid';
return 'user_auto';
}
function getHDHiveResourceUrl(link) {
return new URL(link.getAttribute('href') || link.href, location.origin).toString();
}
function getCleanHDHiveResourceUrl() {
try {
const url = new URL(location.href);
url.searchParams.delete(HDHIVE_AUTOTRANSFER_PARAM);
url.searchParams.delete(HDHIVE_RESOURCE_TYPE_PARAM);
url.searchParams.delete(HDHIVE_SOURCE_PARAM);
return url.toString();
} catch (err) {
return location.href;
}
}
function appendHDHiveTransferButton(link, startTransfer) {
const container = link.closest('a[class*="MuiBox-root"]') || link.closest('[class*="MuiGrid-root"]') || link.parentElement;
if (!container || container.querySelector('.aria115-hd-card-btn')) return false;
const resourceUrl = getHDHiveResourceUrl(link);
const resourceType = getHDHiveResourceType(container);
const button = document.createElement('button');
button.type = 'button';
button.className = 'aria115-hd-card-btn';
button.textContent = '一键转存';
button.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
startTransfer(resourceUrl, resourceType, button);
};
const wrapper = document.createElement('div');
wrapper.style.cssText = 'margin-top:8px;width:100%;display:flex;justify-content:flex-end;align-items:center;';
wrapper.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
});
wrapper.appendChild(button);
const description = container.querySelector('p[class*="MuiTypography-root"][class*="MuiTypography-body2"][aria-label], p[class*="MuiTypography-root"][class*="MuiTypography-body2"]');
if (description && description.parentElement) description.parentElement.appendChild(wrapper);
else container.appendChild(wrapper);
return true;
}
function scanHDHiveResourceButtons(startTransfer, setStatus) {
let added = 0;
document.querySelectorAll('a[href*="/resource/115/"]').forEach((link) => {
if (is115ResourceLink(link) && appendHDHiveTransferButton(link, startTransfer)) added += 1;
});
const total = document.querySelectorAll('.aria115-hd-card-btn').length;
if (added > 0 || total === 0) setStatus(total > 0 ? `已识别 ${total} 个 115 资源` : '未识别到 115 资源');
return added;
}
function startHDHiveResourceTransfer(resourceUrl, resourceType, button, getSettings, setStatus, openTransferSettings) {
const settings = getSettings();
if (!normalizePath(settings.transferCookie || '')) {
openTransferSettings();
setStatus('请先填写 115 Cookie');
return;
}
if (hdHiveProcessedResources.has(resourceUrl)) {
setStatus('该资源正在处理或已处理');
return;
}
hdHiveProcessedResources.add(resourceUrl);
if (button) {
button.disabled = true;
button.textContent = '获取中...';
hdHiveProcessingButtons.set(resourceUrl, button);
}
setStatus('正在打开资源页获取 115 链接...');
const url = new URL(resourceUrl);
url.searchParams.set(HDHIVE_AUTOTRANSFER_PARAM, '1');
url.searchParams.set(HDHIVE_RESOURCE_TYPE_PARAM, resourceType || 'user_auto');
url.searchParams.set(HDHIVE_SOURCE_PARAM, resourceUrl);
const childName = `${HDHIVE_WINDOW_NAME_PREFIX}${Date.now()}-${Math.random().toString(16).slice(2)}`;
const child = window.open(url.toString(), childName, 'width=320,height=220,left=0,top=100,resizable=yes,scrollbars=yes');
if (!child) {
hdHiveProcessedResources.delete(resourceUrl);
hdHiveProcessingButtons.delete(resourceUrl);
if (button) {
button.disabled = false;
button.textContent = '一键转存';
}
setStatus('浏览器阻止了资源页窗口');
}
}
function getHDHiveMessageButtons(resourceUrl) {
const button = resourceUrl ? hdHiveProcessingButtons.get(resourceUrl) : null;
if (button) return [button];
return Array.from(document.querySelectorAll('.aria115-hd-card-btn:disabled'));
}
function initHDHiveResourceChild() {
const params = new URLSearchParams(location.search);
if (!isHDHiveResourcePage() || !params.has(HDHIVE_AUTOTRANSFER_PARAM)) return false;
const resourceType = params.get(HDHIVE_RESOURCE_TYPE_PARAM) || 'user_auto';
const resourceUrl = params.get(HDHIVE_SOURCE_PARAM) || getCleanHDHiveResourceUrl();
const target = unsafeWindow || window;
const safari = isSafariBrowser();
let finished = false;
let observer = null;
const timers = [];
const send = (data) => {
if (window.opener) window.opener.postMessage({ type: HDHIVE_MESSAGE_TYPE, resourceUrl, ...data }, '*');
};
const process = (step) => send({ status: 'process', step });
const clearTimers = () => {
timers.forEach((timer) => {
try { window.clearInterval(timer); } catch (err) {}
try { window.clearTimeout(timer); } catch (err) {}
});
if (observer) observer.disconnect();
};
const fail = (message) => {
if (finished) return;
finished = true;
clearTimers();
send({ status: 'error', error: message });
window.setTimeout(() => window.close(), HDHIVE_AUTO_CLOSE_DELAY);
};
const success = (rawUrl) => {
if (finished) return;
const checked = verifyAndFormat115ShareUrl(rawUrl);
if (!checked.success) return;
finished = true;
clearTimers();
send({ status: 'success', url: checked.url });
window.setTimeout(() => window.close(), HDHIVE_AUTO_CLOSE_DELAY);
};
function clickUnlockIfNeeded() {
const buttons = Array.from(document.querySelectorAll('button'));
const unlockButton = buttons.find((item) => {
const text = normalizeText(item.textContent).replace(/\s+/g, '');
return text.includes('确定解锁') || text.includes('确认解锁') || (text.includes('解锁') && !text.includes('取消'));
});
if (!unlockButton || unlockButton.dataset.aria115TransferClicked) return false;
unlockButton.dataset.aria115TransferClicked = '1';
process('找到解锁按钮,正在点击...');
if (safari) unlockButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
else {
try { unlockButton.click(); }
catch (err) { unlockButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); }
}
return true;
}
function scanDom() {
if (finished || !document.body) return;
const anchors = Array.from(document.querySelectorAll('a[href]'));
for (const anchor of anchors) {
if (anchor.href && anchor.href.includes('115')) {
success(anchor.href);
if (finished) return;
}
}
const link = find115ShareLinkFromText(document.body.innerHTML || document.body.textContent || '');
if (link) success(link);
}
function startDomScan() {
if (!document.body) {
timers.push(window.setTimeout(startDomScan, 100));
return;
}
process('正在扫描资源页...');
timers.push(window.setTimeout(() => fail('获取 115 分享链接超时'), HDHIVE_MAX_WAIT_TIME));
scanDom();
observer = new MutationObserver(scanDom);
observer.observe(document.body, { childList: true, subtree: true });
timers.push(window.setTimeout(() => {
if (observer) observer.disconnect();
}, HDHIVE_MAX_WAIT_TIME));
}
if (resourceType === 'paid' || resourceType === 'user_auto') {
timers.push(window.setInterval(() => {
if (!finished) clickUnlockIfNeeded();
}, 300));
}
const XHR = target.XMLHttpRequest;
if (XHR && XHR.prototype && !XHR.prototype.__aria115HDHiveSniffed) {
const rawXhrOpen = XHR.prototype.open;
XHR.prototype.open = function() {
this.addEventListener('load', function() {
if (finished || !this.responseText) return;
const link = find115ShareLinkFromText(this.responseText);
if (link) {
process('从接口响应中发现 115 链接');
success(link);
}
});
return rawXhrOpen.apply(this, arguments);
};
XHR.prototype.__aria115HDHiveSniffed = true;
}
if (typeof target.fetch === 'function' && !target.fetch.__aria115HDHiveSniffed) {
const rawFetch = target.fetch;
const wrappedFetch = function() {
const promise = rawFetch.apply(this, arguments);
Promise.resolve(promise).then((response) => {
if (finished || !response || typeof response.clone !== 'function') return;
response.clone().text().then((text) => {
const link = find115ShareLinkFromText(text);
if (link) {
process('从接口响应中发现 115 链接');
success(link);
}
}).catch(() => {});
}).catch(() => {});
return promise;
};
wrappedFetch.__aria115HDHiveSniffed = true;
target.fetch = wrappedFetch;
}
if (typeof target.open === 'function' && !target.open.__aria115HDHiveSniffed) {
const rawWindowOpen = target.open;
const wrappedOpen = function(url) {
if (url && String(url).includes('115')) {
const checked = verifyAndFormat115ShareUrl(url);
if (checked.success) {
process('拦截到 115 跳转');
success(checked.url);
return null;
}
}
return rawWindowOpen.apply(this, arguments);
};
wrappedOpen.__aria115HDHiveSniffed = true;
target.open = wrappedOpen;
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', startDomScan, { once: true });
else startDomScan();
return true;
}
function initHDHiveTransfer() {
if (initHDHiveResourceChild()) return;
if (!document.body) {
window.setTimeout(initHDHiveTransfer, 200);
return;
}
ensureStyle();
let settings = loadSettings();
const old = document.getElementById('aria115-hdhive-panel');
if (old) old.remove();
const panel = document.createElement('div');
panel.id = 'aria115-hdhive-panel';
panel.className = 'aria115-hdhive-panel';
const head = document.createElement('div');
head.className = 'aria115-mini-head';
head.textContent = '115Aria HDHive/HDLive';
const status = document.createElement('div');
status.className = 'aria115-mini-status';
const actions = document.createElement('div');
actions.className = 'aria115-mini-actions';
const open115Button = createButton('返回115');
const settingsButton = createButton('设置');
function setStatus(message) {
status.textContent = `${message} · 目标CID: ${settings.transferTargetCid || '0'}`;
}
function getSettings() {
return settings;
}
function openTransferSettings() {
openSettingsModal(settings, (next) => {
settings = next;
saveSettings(settings);
setStatus('设置已保存');
scan();
});
}
function startTransfer(resourceUrl, resourceType, button) {
startHDHiveResourceTransfer(resourceUrl, resourceType, button, getSettings, setStatus, openTransferSettings);
}
function scan() {
scanHDHiveResourceButtons(startTransfer, setStatus);
}
settingsButton.onclick = openTransferSettings;
open115Button.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
window.location.href = 'https://115.com/storage/netdisk?cid=0&mode=wangpan';
};
window.addEventListener('message', async (event) => {
const data = event.data || {};
if (data.type !== HDHIVE_MESSAGE_TYPE) return;
if (event.origin && !isHDHiveOrigin(event.origin) && !is115ShareOrigin(event.origin)) return;
const buttons = getHDHiveMessageButtons(data.resourceUrl);
if (data.status === 'process') {
setStatus(data.step || '正在获取 115 链接...');
return;
}
if (data.status === 'error') {
setStatus(data.error || '获取 115 链接失败');
buttons.forEach((button) => {
button.disabled = false;
button.textContent = '一键转存';
});
if (data.resourceUrl) {
hdHiveProcessedResources.delete(data.resourceUrl);
hdHiveProcessingButtons.delete(data.resourceUrl);
}
return;
}
if (data.status === 'success' && data.url) {
setStatus('已获取 115 链接,正在转存...');
buttons.forEach((button) => { button.textContent = '转存中...'; });
try {
const result = await transfer115Share(data.url, settings);
setStatus(result.message);
showToast(`[115Aria] ${result.message}`, 'success');
buttons.forEach((button) => {
button.disabled = false;
button.textContent = '已转存';
});
if (data.resourceUrl) hdHiveProcessingButtons.delete(data.resourceUrl);
} catch (err) {
const message = err.message || err;
setStatus(formatTransferFailure(message));
buttons.forEach((button) => {
button.disabled = false;
button.textContent = '一键转存';
});
if (data.resourceUrl) {
hdHiveProcessedResources.delete(data.resourceUrl);
hdHiveProcessingButtons.delete(data.resourceUrl);
}
showToast(`[115Aria] ${formatTransferFailure(message)}`, 'error');
}
}
});
actions.appendChild(open115Button);
actions.appendChild(settingsButton);
panel.appendChild(head);
panel.appendChild(status);
panel.appendChild(actions);
document.body.appendChild(panel);
scan();
let timer = 0;
const observer = new MutationObserver(() => {
window.clearTimeout(timer);
timer = window.setTimeout(scan, 300);
});
observer.observe(document.body, { childList: true, subtree: true });
}
unsafeWindow.Aria115 = {
buildOlistUrl,
collectSelectedFilePaths,
folderCoverCidCache,
get115FolderParts,
loadSettings,
parse115ShareLink,
saveSettings,
sendToAria2,
transfer115Share
};
if (is115CdnSharePage()) {
if (!relay115ShareToHDHiveOpener()) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount115ShareTransferPanel, { once: true });
else mount115ShareTransferPanel();
}
} else if (isHDHiveHost()) {
initHDHiveTransfer();
} else {
install115OperationCleaner();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountUI, { once: true });
} else {
mountUI();
}
}
})();