115Aria

115.com OpenList直链发送到aria2 RPC

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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(/&amp;/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(/&amp;/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(/&amp;/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();
        }
    }
})();