// ==UserScript== // @name RPC切换 & 路径选择 (LinkSwift适配版) // @namespace http://tampermonkey.net/ // @version 1.6 // @description RPC与路径分离配置 + 绑定关系,支持每RPC多路径与默认路径 // @author jiemo // @match *://pan.quark.cn/* // @match *://drive.uc.cn/* // @match *://cloud.189.cn/* // @match *://pan.baidu.com/* // @grant unsafeWindow // @run-at document-end // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/541319/RPC%E5%88%87%E6%8D%A2%20%20%E8%B7%AF%E5%BE%84%E9%80%89%E6%8B%A9%20%28LinkSwift%E9%80%82%E9%85%8D%E7%89%88%29.user.js // @updateURL https://update.greasyfork.icu/scripts/541319/RPC%E5%88%87%E6%8D%A2%20%20%E8%B7%AF%E5%BE%84%E9%80%89%E6%8B%A9%20%28LinkSwift%E9%80%82%E9%85%8D%E7%89%88%29.meta.js // ==/UserScript== (function() { 'use strict'; const LINKSWIFT_RPC_KEY = 'setting_aria2_rpc'; const SETTINGS_KEY = 'rpc_helper_settings_v3'; const LEGACY_SETTINGS_KEY = 'rpc_helper_settings_v2'; const CONTAINER_ID = 'rpc-helper-container'; const MODAL_ID = 'rpc-helper-settings-modal'; const STYLE_ID = 'rpc-helper-style'; const WIN_DEFAULT_PATH = '/Users/Administrator/Downloads'; const DEFAULT_SETTINGS = { version: 3, rpcConfigs: [ { id: 'local-win', name: '本地Win', domain: 'http://127.0.0.1', port: '6800', token: '', path: '/jsonrpc' }, { id: 'remote-linux', name: '远程Linux', domain: 'https://your-linux-server.example.com', port: '443', token: '', path: '/jsonrpc' } ], downloadPaths: [ { id: 'p-win-downloads', path: '/Users/Administrator/Downloads' }, { id: 'p-win-desktop', path: '/Users/Administrator/Desktop' }, { id: 'p-linux-downloads', path: '/root/downloads' }, { id: 'p-linux-s26', path: '/root/downloads/s25' }, { id: 'p-linux-x25', path: '/root/downloads/x25' }, { id: 'p-linux-26', path: '/root/downloads/s26' } ], rpcPathBindings: { 'local-win': { pathIds: ['p-win-downloads', 'p-win-desktop'], defaultPathId: 'p-win-downloads' }, 'remote-linux': { pathIds: ['p-linux-downloads', 'p-linux-s26', 'p-linux-x25', 'p-linux-26'], defaultPathId: 'p-linux-downloads' } } }; function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } function normalizePath(value) { return typeof value === 'string' ? value.trim() : ''; } function makeId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } function isValidHttpUrl(value) { try { const url = new URL(value); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (err) { return false; } } function splitServerEndpoint(endpoint, fallbackPort) { const value = String(endpoint || '').trim(); if (!value) return null; if (!isValidHttpUrl(value)) return null; const url = new URL(value); const port = String(url.port || fallbackPort || (url.protocol === 'https:' ? '443' : '80')).trim(); return { domain: `${url.protocol}//${url.hostname}`, port }; } function composeServerEndpoint(domain, port) { const d = String(domain || '').trim(); const p = String(port || '').trim(); if (!d) return ''; if (!isValidHttpUrl(d)) return d; const parsed = splitServerEndpoint(d, p || undefined); if (!parsed) return d; return `${parsed.domain}:${parsed.port}`; } function ensureStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #${CONTAINER_ID} { display: inline-flex; gap: 6px; align-items: center; margin-right: 10px; padding: 3px; border-radius: 10px; background: linear-gradient(135deg, #f8fafc, #eef6ff); border: 1px solid #d7e3f4; box-shadow: 0 2px 8px rgba(23, 89, 168, 0.08); vertical-align: middle; } .rpc-helper-select { height: 28px; border: 1px solid #c7d6e8; border-radius: 8px; padding: 0 8px; background: #fff; color: #24364b; font-size: 13px; outline: none; } .rpc-helper-select:hover { border-color: #7ba7d8; } .rpc-helper-select:focus { border-color: #3f84cc; box-shadow: 0 0 0 2px rgba(63, 132, 204, 0.15); } .rpc-helper-btn { height: 28px; border: 1px solid #c7d6e8; border-radius: 8px; padding: 0 10px; background: #fff; color: #1f3c5b; cursor: pointer; transition: all .15s ease; font-size: 13px; } .rpc-helper-btn:hover { border-color: #3f84cc; color: #1f5c96; background: #f3f9ff; } .rpc-helper-btn-primary { background: #1677ff; border-color: #1677ff; color: #fff; } .rpc-helper-btn-primary:hover { background: #0d67e6; border-color: #0d67e6; color: #fff; } #${MODAL_ID} { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45); z-index: 999999; display: flex; align-items: center; justify-content: center; padding: 16px; box-sizing: border-box; } .rpc-helper-panel { width: 1080px; max-width: 96vw; max-height: 90vh; overflow: auto; background: #fff; border-radius: 14px; border: 1px solid #d4e2f4; box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18); color: #213547; } .rpc-helper-head { padding: 16px 18px; border-bottom: 1px solid #e6edf6; background: linear-gradient(180deg, #f9fcff, #f4f9ff); } .rpc-helper-title { margin: 0; font-size: 18px; } .rpc-helper-desc { margin: 6px 0 0; color: #5a6f88; font-size: 12px; } .rpc-helper-body { padding: 14px 18px 10px; } .rpc-helper-section { border: 1px solid #e3edf8; border-radius: 12px; margin-bottom: 14px; background: #fbfdff; padding: 10px; } .rpc-helper-section-title { font-weight: 700; font-size: 14px; color: #27486b; margin-bottom: 8px; } .rpc-helper-row { display: grid; gap: 6px; margin-bottom: 8px; padding: 8px; border: 1px solid #e6edf6; border-radius: 10px; background: #fff; } .rpc-helper-row.rpc-row { grid-template-columns: 130px 2fr 0.9fr 110px 70px; } .rpc-helper-row.path-row { grid-template-columns: 1fr 70px; } .rpc-helper-row.bind-row { grid-template-columns: 120px 1fr 220px; align-items: center; } .rpc-helper-input, .rpc-helper-multi { height: 32px; border: 1px solid #c7d6e8; border-radius: 8px; padding: 0 10px; outline: none; box-sizing: border-box; width: 100%; font-size: 13px; color: #26384d; background: #fff; } .rpc-helper-multi { min-height: 72px; height: 72px; padding: 4px; } .rpc-helper-input:focus, .rpc-helper-multi:focus { border-color: #3f84cc; box-shadow: 0 0 0 2px rgba(63, 132, 204, 0.15); } .rpc-helper-label { font-size: 13px; color: #35516f; font-weight: 600; } .rpc-helper-empty { color: #6c8198; font-size: 12px; padding: 6px 2px; } .rpc-helper-error { margin: 0 18px 10px; padding: 10px; border-radius: 8px; border: 1px solid #ffb3ab; background: #fff4f2; color: #b42318; font-size: 12px; display: none; } .rpc-helper-footer { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 14px 18px; border-top: 1px solid #e6edf6; background: #f9fbff; position: sticky; bottom: 0; } .rpc-helper-group { display: flex; gap: 8px; } .rpc-helper-tip { color: #5a6f88; font-size: 12px; } @media (max-width: 960px) { .rpc-helper-row.rpc-row, .rpc-helper-row.path-row, .rpc-helper-row.bind-row { grid-template-columns: 1fr; } } `; document.head.appendChild(style); } function getPathIdByPath(pathList, pathValue) { const value = normalizePath(pathValue); if (!value) return ''; const found = pathList.find((item) => item.path === value); return found ? found.id : ''; } function sanitizePathList(input) { const src = Array.isArray(input) ? input : []; const result = []; src.forEach((item, index) => { const obj = item && typeof item === 'object' ? item : {}; const path = normalizePath(obj.path || item); if (!path) return; const id = String(obj.id || makeId(`path${index}`)); if (result.some((x) => x.path === path)) return; result.push({ id, path }); }); return result; } function sanitizeRpcConfig(input, index) { const cfg = input && typeof input === 'object' ? input : {}; const endpointRaw = String(cfg._serverEndpoint || '').trim(); const endpointParsed = endpointRaw ? splitServerEndpoint(endpointRaw) : null; const domain = endpointParsed ? endpointParsed.domain : String(cfg.domain || '').trim(); const port = endpointParsed ? endpointParsed.port : String(cfg.port || '').trim(); return { id: String(cfg.id || makeId(`rpc${index}`)), name: String(cfg.name || `RPC-${index + 1}`).trim(), domain, port, token: String(cfg.token || ''), path: normalizePath(cfg.path || '/jsonrpc') || '/jsonrpc' }; } function buildDefaultBinding(pathIds) { return { pathIds: pathIds.length > 0 ? pathIds : [], defaultPathId: pathIds.length > 0 ? pathIds[0] : '' }; } function sanitizeSettings(rawInput) { const defaults = deepClone(DEFAULT_SETTINGS); const source = rawInput && typeof rawInput === 'object' ? rawInput : {}; let downloadPaths = sanitizePathList(source.downloadPaths); if (downloadPaths.length === 0) { downloadPaths = sanitizePathList(defaults.downloadPaths); } const ensurePath = (pathValue) => { const p = normalizePath(pathValue); if (!p) return ''; const existing = downloadPaths.find((item) => item.path === p); if (existing) return existing.id; const id = makeId('path'); downloadPaths.push({ id, path: p }); return id; }; const rawRpcs = Array.isArray(source.rpcConfigs) ? source.rpcConfigs : defaults.rpcConfigs; let rpcConfigs = rawRpcs.map((item, index) => sanitizeRpcConfig(item, index)); rpcConfigs = rpcConfigs.filter((rpc) => rpc.domain && rpc.port); if (rpcConfigs.length === 0) { rpcConfigs = defaults.rpcConfigs.map((item, index) => sanitizeRpcConfig(item, index)); } const pathIdSet = () => new Set(downloadPaths.map((item) => item.id)); const rawBindings = source.rpcPathBindings && typeof source.rpcPathBindings === 'object' ? source.rpcPathBindings : {}; const bindings = {}; rpcConfigs.forEach((rpc) => { const rawBinding = rawBindings[rpc.id] && typeof rawBindings[rpc.id] === 'object' ? rawBindings[rpc.id] : {}; const idsFromBinding = Array.isArray(rawBinding.pathIds) ? rawBinding.pathIds.map(String) : []; const validIds = idsFromBinding.filter((id) => pathIdSet().has(id)); const legacyRpcRaw = rawRpcs.find((item) => item && typeof item === 'object' && String(item.id || '') === rpc.id) || {}; const legacyPaths = Array.isArray(legacyRpcRaw.downloadPaths) ? legacyRpcRaw.downloadPaths : []; legacyPaths.forEach((p) => { ensurePath(p); }); let finalIds = validIds; if (finalIds.length === 0 && legacyPaths.length > 0) { finalIds = legacyPaths.map((p) => ensurePath(p)).filter((id) => Boolean(id)); } const legacyDefault = normalizePath(legacyRpcRaw.defaultPath || ''); let defaultPathId = String(rawBinding.defaultPathId || ''); if (!pathIdSet().has(defaultPathId) && legacyDefault) { defaultPathId = ensurePath(legacyDefault); } if (finalIds.length === 0) { const sameDefault = defaults.rpcPathBindings[rpc.id]; if (sameDefault) { finalIds = sameDefault.pathIds.filter((id) => pathIdSet().has(id)); if (finalIds.length === 0) { finalIds = [ensurePath(WIN_DEFAULT_PATH)]; } defaultPathId = pathIdSet().has(sameDefault.defaultPathId) ? sameDefault.defaultPathId : finalIds[0]; } else { finalIds = [ensurePath(WIN_DEFAULT_PATH)]; defaultPathId = finalIds[0]; } } if (!finalIds.includes(defaultPathId)) { defaultPathId = finalIds[0]; } bindings[rpc.id] = { pathIds: Array.from(new Set(finalIds)), defaultPathId }; }); return { version: 3, rpcConfigs, downloadPaths, rpcPathBindings: bindings }; } function loadSettings() { let raw = unsafeWindow.base.getValue(SETTINGS_KEY); if (!raw) raw = unsafeWindow.base.getValue(LEGACY_SETTINGS_KEY); if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch (err) { raw = null; } } const normalized = sanitizeSettings(raw); unsafeWindow.base.setValue(SETTINGS_KEY, normalized); return normalized; } function saveSettings(settings) { unsafeWindow.base.setValue(SETTINGS_KEY, settings); } function waitForBaseAndInit() { let retry = 0; const timer = setInterval(() => { retry += 1; if (unsafeWindow.base && typeof unsafeWindow.base.getValue === 'function' && typeof unsafeWindow.base.setValue === 'function') { clearInterval(timer); initHelper(); return; } if (retry > 240) { clearInterval(timer); console.warn('[RPC Helper] LinkSwift base 未就绪,已停止初始化。'); } }, 500); } function initHelper() { ensureStyle(); let settings = loadSettings(); const container = document.createElement('div'); container.id = CONTAINER_ID; const selectServer = document.createElement('select'); selectServer.className = 'rpc-helper-select'; selectServer.style.width = '80px'; const selectPath = document.createElement('select'); selectPath.className = 'rpc-helper-select'; selectPath.style.width = '200px'; const btnSettings = document.createElement('button'); btnSettings.type = 'button'; btnSettings.className = 'rpc-helper-btn'; btnSettings.textContent = '设置'; container.appendChild(btnSettings); container.appendChild(selectServer); container.appendChild(selectPath); function getActiveRpcConfigFromLinkSwift() { const list = unsafeWindow.base.getValue(LINKSWIFT_RPC_KEY); if (!Array.isArray(list) || list.length === 0) return null; let index = list.findIndex((item) => item && item.default); if (index === -1) index = 0; return { list, index, item: list[index] }; } function findRpcByActive(active) { if (!active || !active.item) return { index: -1, rpc: null }; const idx = settings.rpcConfigs.findIndex((cfg) => cfg.domain === active.item.domain && String(cfg.port) === String(active.item.port)); if (idx === -1) return { index: -1, rpc: null }; return { index: idx, rpc: settings.rpcConfigs[idx] }; } function getPathById(pathId) { return settings.downloadPaths.find((item) => item.id === pathId) || null; } function getBindingForRpc(rpcId) { const b = settings.rpcPathBindings && settings.rpcPathBindings[rpcId]; if (!b || !Array.isArray(b.pathIds)) return buildDefaultBinding([]); const pathIds = b.pathIds.filter((id) => getPathById(id)); const defaultPathId = pathIds.includes(b.defaultPathId) ? b.defaultPathId : (pathIds[0] || ''); return { pathIds, defaultPathId }; } function getBoundPathValues(rpcId) { const binding = getBindingForRpc(rpcId); return binding.pathIds .map((id) => getPathById(id)) .filter((item) => Boolean(item)) .map((item) => item.path); } function renderServerOptions() { selectServer.innerHTML = ''; const empty = document.createElement('option'); empty.value = ''; empty.text = '选择RPC服务器...'; selectServer.appendChild(empty); settings.rpcConfigs.forEach((rpc, index) => { const option = document.createElement('option'); option.value = String(index); option.text = rpc.name || rpc.domain; selectServer.appendChild(option); }); } function renderPathOptions(rpcId, currentDir) { selectPath.innerHTML = ''; const empty = document.createElement('option'); empty.value = ''; empty.text = '选择下载路径...'; selectPath.appendChild(empty); const paths = rpcId ? getBoundPathValues(rpcId) : []; paths.forEach((p) => { const option = document.createElement('option'); option.value = p; option.text = p; selectPath.appendChild(option); }); if (currentDir && !paths.includes(currentDir)) { const temp = document.createElement('option'); temp.value = currentDir; temp.text = `当前: ${currentDir}`; selectPath.appendChild(temp); } } function syncUIState() { const active = getActiveRpcConfigFromLinkSwift(); const currentDir = active && active.item ? String(active.item.dir || '') : ''; renderServerOptions(); const matched = findRpcByActive(active); const activeRpc = matched.rpc || settings.rpcConfigs[0] || null; renderPathOptions(activeRpc ? activeRpc.id : '', currentDir); if (matched.index >= 0) { selectServer.value = String(matched.index); } else { selectServer.value = ''; } if (currentDir) { selectPath.value = currentDir; } else { selectPath.value = ''; } } function makeInput(value, placeholder, type) { const input = document.createElement('input'); input.className = 'rpc-helper-input'; input.value = value || ''; input.placeholder = placeholder; input.type = type || 'text'; if (input.type === 'password') input.autocomplete = 'new-password'; return input; } function ensureDraftBinding(draft, rpcId) { if (!draft.rpcPathBindings || typeof draft.rpcPathBindings !== 'object') { draft.rpcPathBindings = {}; } if (!draft.rpcPathBindings[rpcId]) { const firstPathId = draft.downloadPaths[0] ? draft.downloadPaths[0].id : ''; draft.rpcPathBindings[rpcId] = { pathIds: firstPathId ? [firstPathId] : [], defaultPathId: firstPathId }; } const binding = draft.rpcPathBindings[rpcId]; if (!Array.isArray(binding.pathIds)) binding.pathIds = []; const pathIdSet = new Set(draft.downloadPaths.map((item) => item.id)); binding.pathIds = binding.pathIds.filter((id) => pathIdSet.has(id)); if (binding.pathIds.length === 0 && draft.downloadPaths[0]) { binding.pathIds = [draft.downloadPaths[0].id]; } if (!binding.pathIds.includes(binding.defaultPathId)) { binding.defaultPathId = binding.pathIds[0] || ''; } return binding; } function openSettingsModal() { const old = document.getElementById(MODAL_ID); if (old) old.remove(); const overlay = document.createElement('div'); overlay.id = MODAL_ID; const panel = document.createElement('div'); panel.className = 'rpc-helper-panel'; const head = document.createElement('div'); head.className = 'rpc-helper-head'; head.innerHTML = `
服务器与路径分离管理,再通过绑定关系关联;每个RPC可绑定多个路径并设置默认路径。
`; const body = document.createElement('div'); body.className = 'rpc-helper-body'; const rpcSection = document.createElement('div'); rpcSection.className = 'rpc-helper-section'; const rpcTitle = document.createElement('div'); rpcTitle.className = 'rpc-helper-section-title'; rpcTitle.textContent = 'RPC服务器'; const rpcWrap = document.createElement('div'); const rpcAdd = document.createElement('button'); rpcAdd.type = 'button'; rpcAdd.className = 'rpc-helper-btn'; rpcAdd.textContent = '+ 新增RPC'; rpcSection.appendChild(rpcTitle); rpcSection.appendChild(rpcWrap); rpcSection.appendChild(rpcAdd); const pathSection = document.createElement('div'); pathSection.className = 'rpc-helper-section'; const pathTitle = document.createElement('div'); pathTitle.className = 'rpc-helper-section-title'; pathTitle.textContent = '路径库(全局)'; const pathWrap = document.createElement('div'); const pathAdd = document.createElement('button'); pathAdd.type = 'button'; pathAdd.className = 'rpc-helper-btn'; pathAdd.textContent = '+ 新增路径'; pathSection.appendChild(pathTitle); pathSection.appendChild(pathWrap); pathSection.appendChild(pathAdd); const bindSection = document.createElement('div'); bindSection.className = 'rpc-helper-section'; const bindTitle = document.createElement('div'); bindTitle.className = 'rpc-helper-section-title'; bindTitle.textContent = '绑定关系'; const bindWrap = document.createElement('div'); bindSection.appendChild(bindTitle); bindSection.appendChild(bindWrap); const errorBox = document.createElement('div'); errorBox.className = 'rpc-helper-error'; const footer = document.createElement('div'); footer.className = 'rpc-helper-footer'; const leftTip = document.createElement('div'); leftTip.className = 'rpc-helper-tip'; leftTip.textContent = '同一个路径可被多个RPC复用;绑定关系决定下拉显示与默认路径。'; const actions = document.createElement('div'); actions.className = 'rpc-helper-group'; const btnReset = document.createElement('button'); btnReset.type = 'button'; btnReset.className = 'rpc-helper-btn'; btnReset.textContent = '恢复默认'; const btnCancel = document.createElement('button'); btnCancel.type = 'button'; btnCancel.className = 'rpc-helper-btn'; btnCancel.textContent = '取消'; const btnSave = document.createElement('button'); btnSave.type = 'button'; btnSave.className = 'rpc-helper-btn rpc-helper-btn-primary'; btnSave.textContent = '保存'; actions.appendChild(btnReset); actions.appendChild(btnCancel); actions.appendChild(btnSave); footer.appendChild(leftTip); footer.appendChild(actions); body.appendChild(rpcSection); body.appendChild(pathSection); body.appendChild(bindSection); panel.appendChild(head); panel.appendChild(body); panel.appendChild(errorBox); panel.appendChild(footer); overlay.appendChild(panel); let draft = deepClone(settings); function renderBindings() { bindWrap.innerHTML = ''; if (!Array.isArray(draft.rpcConfigs) || draft.rpcConfigs.length === 0) { const empty = document.createElement('div'); empty.className = 'rpc-helper-empty'; empty.textContent = '请先添加RPC服务器。'; bindWrap.appendChild(empty); return; } draft.rpcConfigs.forEach((rpc) => { const binding = ensureDraftBinding(draft, rpc.id); const row = document.createElement('div'); row.className = 'rpc-helper-row bind-row'; const label = document.createElement('div'); label.className = 'rpc-helper-label'; label.textContent = rpc.name || rpc.domain; const multi = document.createElement('select'); multi.className = 'rpc-helper-multi'; multi.multiple = true; draft.downloadPaths.forEach((item) => { const opt = document.createElement('option'); opt.value = item.id; opt.text = item.path; if (binding.pathIds.includes(item.id)) opt.selected = true; multi.appendChild(opt); }); const defaultSelect = document.createElement('select'); defaultSelect.className = 'rpc-helper-input'; function refreshDefaultSelect() { defaultSelect.innerHTML = ''; binding.pathIds.forEach((id) => { const pathObj = draft.downloadPaths.find((p) => p.id === id); if (!pathObj) return; const opt = document.createElement('option'); opt.value = id; opt.text = pathObj.path; defaultSelect.appendChild(opt); }); if (!binding.pathIds.includes(binding.defaultPathId)) { binding.defaultPathId = binding.pathIds[0] || ''; } defaultSelect.value = binding.defaultPathId || ''; } multi.onchange = () => { const selected = Array.from(multi.selectedOptions).map((opt) => opt.value); binding.pathIds = selected; if (binding.pathIds.length === 0 && draft.downloadPaths[0]) { binding.pathIds = [draft.downloadPaths[0].id]; const fallbackOpt = Array.from(multi.options).find((opt) => opt.value === draft.downloadPaths[0].id); if (fallbackOpt) fallbackOpt.selected = true; } refreshDefaultSelect(); }; defaultSelect.onchange = () => { binding.defaultPathId = defaultSelect.value; }; refreshDefaultSelect(); row.appendChild(label); row.appendChild(multi); row.appendChild(defaultSelect); bindWrap.appendChild(row); }); } function renderRpcRows() { rpcWrap.innerHTML = ''; draft.rpcConfigs.forEach((rpc, idx) => { const row = document.createElement('div'); row.className = 'rpc-helper-row rpc-row'; const name = makeInput(rpc.name, '名称'); const server = makeInput(composeServerEndpoint(rpc.domain, rpc.port), '服务器(含端口) 例如 http://127.0.0.1:6800'); const token = makeInput(rpc.token, 'Token', 'password'); const rpcPath = makeInput(rpc.path, '/jsonrpc'); const del = document.createElement('button'); del.type = 'button'; del.className = 'rpc-helper-btn'; del.textContent = '删除'; name.oninput = () => { draft.rpcConfigs[idx].name = name.value; }; server.oninput = () => { draft.rpcConfigs[idx]._serverEndpoint = server.value; }; token.oninput = () => { draft.rpcConfigs[idx].token = token.value; }; rpcPath.oninput = () => { draft.rpcConfigs[idx].path = rpcPath.value; }; del.onclick = () => { const rpcId = draft.rpcConfigs[idx].id; draft.rpcConfigs.splice(idx, 1); if (draft.rpcPathBindings && draft.rpcPathBindings[rpcId]) { delete draft.rpcPathBindings[rpcId]; } renderRpcRows(); renderBindings(); }; row.appendChild(name); row.appendChild(server); row.appendChild(token); row.appendChild(rpcPath); row.appendChild(del); rpcWrap.appendChild(row); }); } function renderPathRows() { pathWrap.innerHTML = ''; draft.downloadPaths.forEach((item, idx) => { const row = document.createElement('div'); row.className = 'rpc-helper-row path-row'; const pathInput = makeInput(item.path, '路径'); const del = document.createElement('button'); del.type = 'button'; del.className = 'rpc-helper-btn'; del.textContent = '删除'; pathInput.oninput = () => { draft.downloadPaths[idx].path = pathInput.value; renderBindings(); }; del.onclick = () => { const pathId = draft.downloadPaths[idx].id; draft.downloadPaths.splice(idx, 1); if (draft.rpcPathBindings) { Object.keys(draft.rpcPathBindings).forEach((rpcId) => { const binding = ensureDraftBinding(draft, rpcId); binding.pathIds = binding.pathIds.filter((id) => id !== pathId); if (!binding.pathIds.includes(binding.defaultPathId)) { binding.defaultPathId = binding.pathIds[0] || ''; } }); } renderPathRows(); renderBindings(); }; row.appendChild(pathInput); row.appendChild(del); pathWrap.appendChild(row); }); } function closeModal() { overlay.remove(); } rpcAdd.onclick = () => { const rpcId = makeId('rpc'); draft.rpcConfigs.push({ id: rpcId, name: '', domain: 'http://127.0.0.1', port: '6800', token: '', path: '/jsonrpc' }); ensureDraftBinding(draft, rpcId); renderRpcRows(); renderBindings(); }; pathAdd.onclick = () => { const id = makeId('path'); draft.downloadPaths.push({ id, path: WIN_DEFAULT_PATH }); if (draft.rpcPathBindings) { Object.keys(draft.rpcPathBindings).forEach((rpcId) => { const binding = ensureDraftBinding(draft, rpcId); if (binding.pathIds.length === 0) { binding.pathIds = [id]; binding.defaultPathId = id; } }); } renderPathRows(); renderBindings(); }; btnCancel.onclick = closeModal; overlay.addEventListener('click', (event) => { if (event.target === overlay) closeModal(); }); btnReset.onclick = () => { if (!window.confirm('确定恢复默认配置吗?')) return; draft = deepClone(DEFAULT_SETTINGS); errorBox.style.display = 'none'; renderRpcRows(); renderPathRows(); renderBindings(); }; btnSave.onclick = () => { const errors = []; if (!Array.isArray(draft.rpcConfigs) || draft.rpcConfigs.length === 0) { errors.push('至少保留 1 个 RPC 配置。'); } if (!Array.isArray(draft.downloadPaths) || draft.downloadPaths.length === 0) { errors.push('至少保留 1 个下载路径。'); } draft.rpcConfigs.forEach((rpc, idx) => { const endpointText = String(rpc._serverEndpoint || composeServerEndpoint(rpc.domain, rpc.port)).trim(); const endpoint = splitServerEndpoint(endpointText); if (!endpoint) { errors.push(`RPC #${idx + 1} 服务器地址格式错误,示例: http://127.0.0.1:6800`); return; } rpc.domain = endpoint.domain; rpc.port = endpoint.port; if (!normalizePath(rpc.path).startsWith('/')) { errors.push(`RPC #${idx + 1} JSON-RPC 路径需以 / 开头。`); } const binding = ensureDraftBinding(draft, rpc.id); if (!Array.isArray(binding.pathIds) || binding.pathIds.length === 0) { errors.push(`RPC #${idx + 1} 至少需要绑定 1 个下载路径。`); } if (!binding.pathIds.includes(binding.defaultPathId)) { errors.push(`RPC #${idx + 1} 默认路径必须在绑定路径中。`); } }); const pathSet = new Set(); draft.downloadPaths.forEach((item, idx) => { item.path = normalizePath(item.path); if (!item.path) { errors.push(`路径 #${idx + 1} 不能为空。`); return; } if (pathSet.has(item.path)) { errors.push(`路径 #${idx + 1} 与其他路径重复。`); return; } pathSet.add(item.path); }); if (errors.length > 0) { errorBox.innerHTML = errors.map((x) => `