// sidepanel/ip-proxy-panel.js — IP代理面板(轻量解耦) function normalizeIpProxyService(value = '') { const normalized = String(value || '').trim().toLowerCase(); const enabledServices = Array.isArray(globalThis.IP_PROXY_ENABLED_SERVICES) ? globalThis.IP_PROXY_ENABLED_SERVICES : [DEFAULT_IP_PROXY_SERVICE]; if (enabledServices.includes(normalized)) { return normalized; } return DEFAULT_IP_PROXY_SERVICE; } const ipProxyActionState = { busy: false, action: '', startedAt: 0, }; const IP_PROXY_ACTION_LOCK_TIMEOUT_MS = 25000; let ipProxyDeferredProbeTimer = 0; const IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded'; let ipProxySectionExpanded = false; function readIpProxySectionExpanded() { try { return globalThis.localStorage?.getItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY) === '1'; } catch (err) { return false; } } function persistIpProxySectionExpanded(expanded) { try { if (expanded) { globalThis.localStorage?.setItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY, '1'); } else { globalThis.localStorage?.removeItem(IP_PROXY_SECTION_EXPANDED_STORAGE_KEY); } } catch (err) { // Ignore storage errors; the in-memory collapsed state is still enough for this session. } } function setIpProxySectionExpanded(expanded) { ipProxySectionExpanded = Boolean(expanded); persistIpProxySectionExpanded(ipProxySectionExpanded); if (typeof updateIpProxyUI === 'function') { updateIpProxyUI(latestState); } } function toggleIpProxySectionExpanded() { setIpProxySectionExpanded(!ipProxySectionExpanded); } function initIpProxySectionExpandedState() { ipProxySectionExpanded = readIpProxySectionExpanded(); if (typeof updateIpProxyUI === 'function') { updateIpProxyUI(latestState); } } function normalizeIpProxyActionType(value = '') { const normalized = String(value || '').trim().toLowerCase(); return normalized || 'action'; } function getIpProxyActionLabel(action = '') { const normalized = normalizeIpProxyActionType(action); if (normalized === 'refresh') return '同步代理'; if (normalized === 'next') return '切换代理'; if (normalized === 'change') return '会话换出口'; if (normalized === 'probe') return '检测出口'; return '代理操作'; } function isIpProxyActionBusy() { return Boolean(ipProxyActionState.busy); } function getIpProxyActionState() { return { busy: Boolean(ipProxyActionState.busy), action: normalizeIpProxyActionType(ipProxyActionState.action), startedAt: Number(ipProxyActionState.startedAt) || 0, }; } function setIpProxyActionBusy(action = '', busy = false) { ipProxyActionState.busy = Boolean(busy); ipProxyActionState.action = ipProxyActionState.busy ? normalizeIpProxyActionType(action) : ''; ipProxyActionState.startedAt = ipProxyActionState.busy ? Date.now() : 0; } async function runIpProxyActionWithLock(action = '', runner) { if (typeof runner !== 'function') { throw new Error('代理操作执行器无效。'); } const nextAction = normalizeIpProxyActionType(action); const currentState = getIpProxyActionState(); if (currentState.busy) { if (typeof showToast === 'function') { showToast(`${getIpProxyActionLabel(currentState.action)}进行中,请稍候。`, 'info', 1600); } return { skipped: true }; } setIpProxyActionBusy(nextAction, true); if (typeof updateIpProxyUI === 'function') { updateIpProxyUI(latestState); } const actionLabel = getIpProxyActionLabel(nextAction); const timeoutMs = Math.max(5000, Number(IP_PROXY_ACTION_LOCK_TIMEOUT_MS) || 25000); let timeoutId = 0; try { const value = await Promise.race([ Promise.resolve().then(() => runner()), new Promise((_, reject) => { timeoutId = globalThis.setTimeout(() => { reject(new Error(`${actionLabel}超时(${Math.round(timeoutMs / 1000)} 秒),已自动解锁,请重试。`)); }, timeoutMs); }), ]); return { skipped: false, value }; } finally { if (timeoutId) { globalThis.clearTimeout(timeoutId); } setIpProxyActionBusy(nextAction, false); if (typeof updateIpProxyUI === 'function') { updateIpProxyUI(latestState); } } } function scheduleIpProxyExitProbe(options = {}) { const { silent = true, delayMs = 80, } = options; const delay = Math.max(0, Number(delayMs) || 0); if (ipProxyDeferredProbeTimer) { globalThis.clearTimeout(ipProxyDeferredProbeTimer); ipProxyDeferredProbeTimer = 0; } ipProxyDeferredProbeTimer = globalThis.setTimeout(() => { ipProxyDeferredProbeTimer = 0; Promise.resolve() .then(() => probeIpProxyExit({ silent })) .catch(() => {}); }, delay); } function normalizeIpProxyMode(value = '') { const normalized = String(value || '').trim().toLowerCase(); return SUPPORTED_IP_PROXY_MODES.includes(normalized) ? normalized : DEFAULT_IP_PROXY_MODE; } function isIpProxyApiModeAvailable() { return Boolean(typeof IP_PROXY_API_MODE_ENABLED === 'undefined' ? false : IP_PROXY_API_MODE_ENABLED); } function isIpProxyAccountListAvailable() { return Boolean(typeof IP_PROXY_ACCOUNT_LIST_ENABLED === 'undefined' ? true : IP_PROXY_ACCOUNT_LIST_ENABLED); } function normalizeIpProxyModeForCurrentRelease(value = '') { const normalized = normalizeIpProxyMode(value); if (!isIpProxyApiModeAvailable() && normalized === 'api') { return 'account'; } return normalized; } function normalizeIpProxyProtocol(value = '') { const normalized = String(value || '').trim().toLowerCase(); return SUPPORTED_IP_PROXY_PROTOCOLS.includes(normalized) ? normalized : DEFAULT_IP_PROXY_PROTOCOL; } function normalizeIpProxyPort(value = '') { const numeric = Number.parseInt(String(value || '').trim(), 10); if (!Number.isInteger(numeric) || numeric <= 0 || numeric > 65535) { return 0; } return numeric; } function normalizeIpProxyPoolTargetCount(value = '', fallback = 20) { const rawValue = String(value ?? '').trim(); if (!rawValue) { return String(Math.max(1, Math.min(500, Number(fallback) || 20))); } const numeric = Number.parseInt(rawValue, 10); if (!Number.isInteger(numeric)) { return String(Math.max(1, Math.min(500, Number(fallback) || 20))); } return String(Math.max(1, Math.min(500, numeric))); } function normalizeIpProxyAccountLifeMinutes(value = '', fallback = '') { const rawValue = String(value ?? '').trim(); if (!rawValue) { return String(fallback || '').trim(); } const numeric = Number.parseInt(rawValue, 10); if (!Number.isInteger(numeric)) { return String(fallback || '').trim(); } return String(Math.max(1, Math.min(1440, numeric))); } function normalizeIpProxyAccountSessionPrefix(value = '') { return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 32); } function normalizeIpProxyAccountList(value = '') { return String(value || '') .replace(/\r/g, '') .split('\n') .map((line) => line.trim()) .filter((line) => { if (!line) { return false; } // 与后台保持一致:支持 # / // / ; 注释行,注释行不参与生效。 if (/^(?:#|\/\/|;)/.test(line)) { return false; } return true; }) .join('\n'); } function normalizeIpProxyServiceProfile(rawValue = {}) { const raw = (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) ? rawValue : {}; return { mode: normalizeIpProxyModeForCurrentRelease(raw.mode), apiUrl: String(raw.apiUrl || '').trim(), accountList: normalizeIpProxyAccountList(raw.accountList || ''), accountSessionPrefix: normalizeIpProxyAccountSessionPrefix(raw.accountSessionPrefix || ''), accountLifeMinutes: normalizeIpProxyAccountLifeMinutes(raw.accountLifeMinutes || ''), poolTargetCount: normalizeIpProxyPoolTargetCount(raw.poolTargetCount || '', 20), autoSyncEnabled: Boolean(raw.autoSyncEnabled), autoSyncIntervalMinutes: String(Math.max(1, Math.min(1440, Number.parseInt(String(raw.autoSyncIntervalMinutes ?? '').trim(), 10) || 15))), host: String(raw.host || '').trim(), port: String(normalizeIpProxyPort(raw.port || '') || ''), protocol: normalizeIpProxyProtocol(raw.protocol), username: String(raw.username || '').trim(), password: String(raw.password || ''), region: String(raw.region || '').trim(), }; } function buildIpProxyServiceProfileFromFlatState(state = {}) { return normalizeIpProxyServiceProfile({ mode: state?.ipProxyMode, apiUrl: state?.ipProxyApiUrl, accountList: state?.ipProxyAccountList, accountSessionPrefix: state?.ipProxyAccountSessionPrefix, accountLifeMinutes: state?.ipProxyAccountLifeMinutes, poolTargetCount: state?.ipProxyPoolTargetCount, autoSyncEnabled: state?.ipProxyAutoSyncEnabled, autoSyncIntervalMinutes: state?.ipProxyAutoSyncIntervalMinutes, host: state?.ipProxyHost, port: state?.ipProxyPort, protocol: state?.ipProxyProtocol, username: state?.ipProxyUsername, password: state?.ipProxyPassword, region: state?.ipProxyRegion, }); } function normalizeIpProxyServiceProfiles(rawValue = {}, fallbackState = {}) { const raw = (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) ? rawValue : {}; const fallbackProfile = buildIpProxyServiceProfileFromFlatState(fallbackState); const result = {}; SUPPORTED_IP_PROXY_SERVICES.forEach((service) => { const serviceRaw = raw[service]; if (serviceRaw && typeof serviceRaw === 'object' && !Array.isArray(serviceRaw)) { result[service] = normalizeIpProxyServiceProfile(serviceRaw); return; } result[service] = normalizeIpProxyServiceProfile(fallbackProfile); }); return result; } function getIpProxyServiceProfilesFromState(state = latestState) { return normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {}); } function getIpProxyServiceProfile(service = '', state = latestState) { const selectedService = normalizeIpProxyService(service || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE); const profiles = getIpProxyServiceProfilesFromState(state); const profile = normalizeIpProxyServiceProfile(profiles[selectedService] || {}); const resolvedRegion = resolveIpProxyRegionFromInputs({ service: selectedService, mode: profile.mode, host: profile.host, username: profile.username, region: profile.region, }); if (resolvedRegion) { profile.region = resolvedRegion; } return profile; } function inferRegionFromProxyUsernameForPanel(username = '') { const text = String(username || '').trim(); if (!text) return ''; const match = text.match(/(?:^|[-_])(?:region|area|country)[-_:]?([A-Za-z]{2})\b/i); if (!match) return ''; return String(match[1] || '').trim().toUpperCase(); } function inferRegionFromProxyHostForPanel(host = '') { const text = String(host || '').trim().toLowerCase().replace(/\.$/, ''); if (!text || !text.includes('.')) return ''; const firstLabel = String(text.split('.')[0] || '').trim(); if (!/^[a-z]{2}$/.test(firstLabel)) return ''; return firstLabel.toUpperCase(); } function normalizeExplicitRegionForPanel(region = '') { const text = String(region || '').trim().toUpperCase().replace(/[^A-Z]/g, ''); return /^[A-Z]{2}$/.test(text) ? text : ''; } function resolveIpProxyRegionFromInputs(options = {}) { const selectedService = normalizeIpProxyService(options?.service || DEFAULT_IP_PROXY_SERVICE); const selectedMode = normalizeIpProxyModeForCurrentRelease(options?.mode || DEFAULT_IP_PROXY_MODE); if (selectedMode !== 'account') { return ''; } const host = String(options?.host || '').trim(); const username = String(options?.username || '').trim(); const region = String(options?.region || '').trim(); if (selectedService === '711proxy') { const resolvedCode = typeof resolve711ProxyRegionFromInputs === 'function' ? resolve711ProxyRegionFromInputs({ host, username, region }) : ''; if (resolvedCode) { return String(resolvedCode || '').trim().toUpperCase(); } } const fromUsername = inferRegionFromProxyUsernameForPanel(username); if (fromUsername) { return fromUsername; } const fromHost = inferRegionFromProxyHostForPanel(host); if (fromHost) { return fromHost; } return normalizeExplicitRegionForPanel(region); } function hasCurrentInputAccountListEntries() { if (!isIpProxyAccountListAvailable()) { return false; } return normalizeIpProxyAccountList(inputIpProxyAccountList?.value || '') .split('\n') .map((line) => line.trim()) .filter(Boolean) .length > 0; } function syncIpProxyRegionInputFromCredentials(options = {}) { if (!inputIpProxyRegion) { return ''; } const selectedService = normalizeIpProxyService( selectIpProxyService?.value || latestState?.ipProxyService || DEFAULT_IP_PROXY_SERVICE ); const selectedMode = normalizeIpProxyModeForCurrentRelease(getSelectedIpProxyMode()); if (selectedMode !== 'account' || hasCurrentInputAccountListEntries()) { return ''; } const force = Boolean(options?.force); const currentRegion = String(inputIpProxyRegion.value || '').trim(); const resolvedRegion = resolveIpProxyRegionFromInputs({ service: selectedService, mode: selectedMode, host: inputIpProxyHost?.value || '', username: inputIpProxyUsername?.value || '', region: currentRegion, }); if (!resolvedRegion) { return ''; } const normalizedCurrent = normalizeExplicitRegionForPanel(currentRegion); if (force || !normalizedCurrent) { inputIpProxyRegion.value = resolvedRegion; } return resolvedRegion; } function buildCurrentIpProxyServiceProfileFromInputs() { const selectedService = normalizeIpProxyService( selectIpProxyService?.value || latestState?.ipProxyService || DEFAULT_IP_PROXY_SERVICE ); const selectedMode = normalizeIpProxyMode(getSelectedIpProxyMode()); const effectiveMode = normalizeIpProxyModeForCurrentRelease(selectedMode); const rawRegion = String(inputIpProxyRegion?.value || '').trim(); const finalRegion = resolveIpProxyRegionFromInputs({ service: selectedService, mode: effectiveMode, host: inputIpProxyHost?.value || '', username: inputIpProxyUsername?.value || '', region: rawRegion, }) || rawRegion; return normalizeIpProxyServiceProfile({ mode: effectiveMode, apiUrl: inputIpProxyApiUrl?.value || '', accountList: isIpProxyAccountListAvailable() ? (inputIpProxyAccountList?.value || '') : '', accountSessionPrefix: inputIpProxyAccountSessionPrefix?.value || '', accountLifeMinutes: inputIpProxyAccountLifeMinutes?.value || '', poolTargetCount: inputIpProxyPoolTargetCount?.value || '', autoSyncEnabled: Boolean(inputIpProxyAutoSyncEnabled?.checked), autoSyncIntervalMinutes: inputIpProxyAutoSyncIntervalMinutes?.value || '', host: inputIpProxyHost?.value || '', port: inputIpProxyPort?.value || '', protocol: selectIpProxyProtocol?.value || '', username: inputIpProxyUsername?.value || '', password: inputIpProxyPassword?.value || '', region: finalRegion, }); } function buildIpProxyServiceProfilesPatch(selectedService = '', state = latestState) { const nextService = normalizeIpProxyService( selectedService || selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE ); const profiles = getIpProxyServiceProfilesFromState(state || {}); profiles[nextService] = buildCurrentIpProxyServiceProfileFromInputs(); return normalizeIpProxyServiceProfiles(profiles, state || {}); } function buildIpProxyStatePatchFromServiceProfile(service = '', profile = {}) { const normalizedService = normalizeIpProxyService(service || DEFAULT_IP_PROXY_SERVICE); const normalizedProfile = normalizeIpProxyServiceProfile(profile); return { ipProxyService: normalizedService, ipProxyMode: normalizedProfile.mode, ipProxyApiUrl: normalizedProfile.apiUrl, ipProxyAccountList: normalizedProfile.accountList, ipProxyAccountSessionPrefix: normalizedProfile.accountSessionPrefix, ipProxyAccountLifeMinutes: normalizedProfile.accountLifeMinutes, ipProxyPoolTargetCount: normalizedProfile.poolTargetCount, ipProxyAutoSyncEnabled: normalizedProfile.autoSyncEnabled, ipProxyAutoSyncIntervalMinutes: Number.parseInt(String(normalizedProfile.autoSyncIntervalMinutes || '15').trim(), 10) || 15, ipProxyHost: normalizedProfile.host, ipProxyPort: normalizedProfile.port, ipProxyProtocol: normalizedProfile.protocol, ipProxyUsername: normalizedProfile.username, ipProxyPassword: normalizedProfile.password, ipProxyRegion: normalizedProfile.region, }; } function applyIpProxyServiceProfileToInputs(profile = {}, options = {}) { const { keepMode = false } = options; const normalizedProfile = normalizeIpProxyServiceProfile(profile); if (!keepMode) { setIpProxyMode(normalizedProfile.mode); } if (inputIpProxyApiUrl) { inputIpProxyApiUrl.value = normalizedProfile.apiUrl; } if (inputIpProxyAccountList) { inputIpProxyAccountList.value = normalizedProfile.accountList; } if (inputIpProxyAccountSessionPrefix) { inputIpProxyAccountSessionPrefix.value = normalizedProfile.accountSessionPrefix; } if (inputIpProxyAccountLifeMinutes) { inputIpProxyAccountLifeMinutes.value = normalizedProfile.accountLifeMinutes; } if (inputIpProxyPoolTargetCount) { inputIpProxyPoolTargetCount.value = normalizedProfile.poolTargetCount; } if (inputIpProxyAutoSyncEnabled) { inputIpProxyAutoSyncEnabled.checked = Boolean(normalizedProfile.autoSyncEnabled); } if (inputIpProxyAutoSyncIntervalMinutes) { inputIpProxyAutoSyncIntervalMinutes.value = String(normalizedProfile.autoSyncIntervalMinutes || '15'); } if (inputIpProxyHost) { inputIpProxyHost.value = normalizedProfile.host; } if (inputIpProxyPort) { inputIpProxyPort.value = normalizedProfile.port; } if (selectIpProxyProtocol) { selectIpProxyProtocol.value = normalizedProfile.protocol; } if (inputIpProxyUsername) { inputIpProxyUsername.value = normalizedProfile.username; } if (inputIpProxyPassword) { inputIpProxyPassword.value = normalizedProfile.password; } if (inputIpProxyRegion) { inputIpProxyRegion.value = normalizedProfile.region; } } function getSelectedIpProxyEnabled() { if (inputIpProxyEnabled) { return Boolean(inputIpProxyEnabled.checked); } const activeButton = ipProxyEnabledButtons.find((button) => button.classList.contains('is-active')); return String(activeButton?.dataset?.ipProxyEnabled) === 'true'; } function setIpProxyEnabled(enabled) { const nextEnabled = Boolean(enabled); if (inputIpProxyEnabled) { inputIpProxyEnabled.checked = nextEnabled; } ipProxyEnabledButtons.forEach((button) => { const buttonValue = String(button?.dataset?.ipProxyEnabled) === 'true'; button.classList.toggle('is-active', buttonValue === nextEnabled); button.setAttribute('aria-pressed', String(buttonValue === nextEnabled)); }); } function getSelectedIpProxyMode() { const activeButton = ipProxyModeButtons.find((button) => button.classList.contains('is-active')); return normalizeIpProxyModeForCurrentRelease(activeButton?.dataset?.ipProxyMode || DEFAULT_IP_PROXY_MODE); } function setIpProxyMode(mode) { const nextMode = normalizeIpProxyModeForCurrentRelease(mode); ipProxyModeButtons.forEach((button) => { const buttonMode = normalizeIpProxyMode(button?.dataset?.ipProxyMode || DEFAULT_IP_PROXY_MODE); button.classList.toggle('is-active', buttonMode === nextMode); button.setAttribute('aria-pressed', String(buttonMode === nextMode)); }); } function getIpProxyRuntimeFieldNames(mode = DEFAULT_IP_PROXY_MODE) { const normalizedMode = normalizeIpProxyModeForCurrentRelease(mode); if (normalizedMode === 'account') { return { poolKey: 'ipProxyAccountPool', indexKey: 'ipProxyAccountCurrentIndex', currentKey: 'ipProxyAccountCurrent', }; } return { poolKey: 'ipProxyApiPool', indexKey: 'ipProxyApiCurrentIndex', currentKey: 'ipProxyApiCurrent', }; } function getIpProxyRuntimeSnapshot(state = latestState, mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode)) { const normalizedMode = normalizeIpProxyModeForCurrentRelease(mode); const fields = getIpProxyRuntimeFieldNames(normalizedMode); const hasAccountListConfigured = normalizedMode === 'account' && normalizeIpProxyAccountList(state?.ipProxyAccountList || '').split('\n').filter(Boolean).length > 0; const hasModePool = Array.isArray(state?.[fields.poolKey]); const hasModeCurrent = state?.[fields.currentKey] !== undefined && state?.[fields.currentKey] !== null; const hasModeIndex = state?.[fields.indexKey] !== undefined && state?.[fields.indexKey] !== null; const modePool = Array.isArray(state?.[fields.poolKey]) ? state[fields.poolKey] : []; // 账号模式在“固定账号”场景下不应读取历史 runtime 缓存,否则会出现 // “状态显示 A 节点、当前节点显示 B 节点”的错位。 const allowAccountRuntimeCache = normalizedMode !== 'account' || hasAccountListConfigured; const pool = hasModePool && allowAccountRuntimeCache ? modePool : []; const current = hasModeCurrent && allowAccountRuntimeCache ? state?.[fields.currentKey] : null; const rawIndex = hasModeIndex && allowAccountRuntimeCache ? state?.[fields.indexKey] : 0; const index = Number.isFinite(Number(rawIndex)) ? Math.max(0, Math.floor(Number(rawIndex))) : 0; return { mode: normalizedMode, ...fields, hasModePool: hasModePool && allowAccountRuntimeCache, hasModeCurrent: hasModeCurrent && allowAccountRuntimeCache, hasModeIndex: hasModeIndex && allowAccountRuntimeCache, hasAccountListConfigured, pool, current, index, }; } function buildIpProxyRuntimeStatePatchForMode(mode = DEFAULT_IP_PROXY_MODE, response = {}) { const normalizedMode = normalizeIpProxyModeForCurrentRelease(mode); const fields = getIpProxyRuntimeFieldNames(normalizedMode); const patch = {}; if (response?.pool !== undefined) { patch[fields.poolKey] = Array.isArray(response.pool) ? response.pool : []; // 兼容旧字段:始终反映当前激活模式的运行态。 patch.ipProxyPool = patch[fields.poolKey]; } if (response?.index !== undefined) { const index = Number.isFinite(Number(response.index)) ? Math.max(0, Math.floor(Number(response.index))) : 0; patch[fields.indexKey] = index; patch.ipProxyCurrentIndex = index; } if (response?.current !== undefined) { patch[fields.currentKey] = response.current || null; patch.ipProxyCurrent = response.current || null; } return patch; } function getIpProxyCurrentEntry(state = latestState) { const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode); if (mode === 'account') { const runtime = getIpProxyRuntimeSnapshot(state, mode); const poolEntry = runtime.hasModePool && runtime.pool.length ? runtime.pool[runtime.index % runtime.pool.length] : null; const modeCurrent = runtime.hasModeCurrent ? runtime.current : null; const hasRuntimeEntry = Boolean(poolEntry || modeCurrent); const current = poolEntry || modeCurrent || {}; const host = String(current?.host || state?.ipProxyHost || '').trim(); const port = Number(current?.port || normalizeIpProxyPort(state?.ipProxyPort)); if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) { return null; } return { host, port, username: hasRuntimeEntry ? String(current?.username || '').trim() : String(current?.username || state?.ipProxyUsername || '').trim(), password: hasRuntimeEntry ? String(current?.password || '') : String(current?.password || state?.ipProxyPassword || ''), protocol: normalizeIpProxyProtocol(current?.protocol || state?.ipProxyProtocol), region: hasRuntimeEntry ? String(current?.region || '').trim() : String(current?.region || state?.ipProxyRegion || '').trim(), provider: normalizeIpProxyService(state?.ipProxyService), }; } const runtime = getIpProxyRuntimeSnapshot(state, mode); if (runtime.pool.length) { const index = runtime.index % runtime.pool.length; const current = runtime.pool[index]; if (current?.host && current?.port) { return current; } } const current = runtime.current; return current && current.host && current.port ? current : null; } function has711SessionTokenForPanel(username = '') { const text = String(username || '').trim(); if (!text) { return false; } return /(?:^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i.test(text); } function normalize711SessionIdForPanel(value = '') { return String(value || '').trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64); } function normalize711SessTimeForPanel(value = '') { const raw = String(value ?? '').trim(); if (!raw) { return ''; } const numeric = Number.parseInt(raw, 10); if (!Number.isInteger(numeric)) { return ''; } return String(Math.max(1, Math.min(180, numeric))); } function normalize711RegionCodeForPanel(value = '') { const text = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, ''); return /^[A-Z]{2}$/.test(text) ? text : ''; } function shouldAutoSync711SessionFieldsForPanel(state = latestState) { const service = normalizeIpProxyService( selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE ); const mode = normalizeIpProxyModeForCurrentRelease(getSelectedIpProxyMode()); if (service !== '711proxy' || mode !== 'account') { return false; } return !hasCurrentInputAccountListEntries(); } function parse711RegionFromUsernameForPanel(username = '') { const text = String(username || '').trim(); if (!text) { return ''; } const match = text.match(/(?:^|[-_])region[-_:]?([A-Za-z]{2})\b/i); return normalize711RegionCodeForPanel(match ? match[1] : ''); } function apply711RegionToUsernameForPanel(username = '', regionCode = '', options = {}) { const text = String(username || '').trim(); if (!text) { return ''; } const normalizedRegion = normalize711RegionCodeForPanel(regionCode); const removeWhenEmpty = Boolean(options?.removeWhenEmpty); if (normalizedRegion) { if (/(?:^|[-_])region[-_:]?[A-Za-z]{2}\b/i.test(text)) { return text.replace(/((?:^|[-_])region[-_:]?)([A-Za-z]{2})\b/i, `$1${normalizedRegion}`); } return `${text}-region-${normalizedRegion}`; } if (!removeWhenEmpty) { return text; } return String(text.replace(/(^|[-_])region[-_:]?[A-Za-z]{2}\b/ig, '$1') || '') .replace(/[-_]{2,}/g, '-') .replace(/^[-_]+|[-_]+$/g, '') .trim(); } function parse711SessionConfigFromUsernameForPanel(username = '') { const text = String(username || '').trim(); if (!text) { return { sessionId: '', sessTime: '' }; } const sessionMatch = text.match(/(?:^|[-_])session[-_:]([A-Za-z0-9_-]+?)(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i); const sessTimeMatch = text.match(/(?:^|[-_])sessTime[-_:]?(\d+)\b/i); const lifeMatch = text.match(/(?:^|[-_])life[-_:]?(\d+)\b/i); return { sessionId: normalize711SessionIdForPanel(sessionMatch ? sessionMatch[1] : ''), sessTime: normalize711SessTimeForPanel( sessTimeMatch ? sessTimeMatch[1] : (lifeMatch ? lifeMatch[1] : '') ), }; } function apply711SessionConfigToUsernameForPanel(username = '', options = {}) { const text = String(username || '').trim(); if (!text) { return ''; } const sessionId = normalize711SessionIdForPanel(options?.sessionId || ''); const sessTime = normalize711SessTimeForPanel(options?.sessTime || ''); const removeWhenEmpty = Boolean(options?.removeWhenEmpty); let next = text; if (sessionId) { if (/(?:^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i.test(next)) { next = next.replace( /((?:^|[-_])session[-_:])([A-Za-z0-9_-]+?)(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/i, `$1${sessionId}` ); } else { next = `${next}-session-${sessionId}`; } } else if (removeWhenEmpty) { next = next.replace( /(^|[-_])session[-_:][A-Za-z0-9_-]+?(?=(?:[-_](?:sessTime|sessAuto|region|life|zone|ptype|country|area)\b)|$)/ig, '$1' ); } if (sessTime) { if (/(?:^|[-_])sessTime[-_:]?\d+\b/i.test(next)) { next = next.replace(/((?:^|[-_])sessTime[-_:]?)(\d+)\b/i, `$1${sessTime}`); } else if (/(?:^|[-_])life[-_:]?\d+\b/i.test(next)) { next = next.replace(/((?:^|[-_])life[-_:]?)(\d+)\b/i, `$1${sessTime}`); } else { next = `${next}-sessTime-${sessTime}`; } } else if (removeWhenEmpty) { next = next.replace(/(^|[-_])sessTime[-_:]?\d+\b/ig, '$1'); next = next.replace(/(^|[-_])life[-_:]?\d+\b/ig, '$1'); } return String(next || '') .replace(/[-_]{2,}/g, '-') .replace(/^[-_]+|[-_]+$/g, '') .trim(); } function sync711SessionFieldsFromUsernameForPanel(options = {}) { if (!shouldAutoSync711SessionFieldsForPanel(options?.state || latestState)) { return { updated: false, sessionId: '', sessTime: '' }; } const username = String( options?.username !== undefined ? options.username : (inputIpProxyUsername?.value || '') ).trim(); if (!username) { return { updated: false, sessionId: '', sessTime: '' }; } const parsed = parse711SessionConfigFromUsernameForPanel(username); let updated = false; if (parsed.sessionId && inputIpProxyAccountSessionPrefix) { const currentSession = normalizeIpProxyAccountSessionPrefix( inputIpProxyAccountSessionPrefix.value || '' ); if (currentSession !== parsed.sessionId) { inputIpProxyAccountSessionPrefix.value = parsed.sessionId; updated = true; } } if (parsed.sessTime && inputIpProxyAccountLifeMinutes) { const currentLife = normalizeIpProxyAccountLifeMinutes( inputIpProxyAccountLifeMinutes.value || '' ); if (currentLife !== parsed.sessTime) { inputIpProxyAccountLifeMinutes.value = parsed.sessTime; updated = true; } } return { updated, sessionId: parsed.sessionId, sessTime: parsed.sessTime, }; } function sync711RegionFieldFromUsernameForPanel(options = {}) { if (!shouldAutoSync711SessionFieldsForPanel(options?.state || latestState)) { return { updated: false, region: '' }; } if (!inputIpProxyRegion) { return { updated: false, region: '' }; } const username = String( options?.username !== undefined ? options.username : (inputIpProxyUsername?.value || '') ).trim(); const parsedRegion = parse711RegionFromUsernameForPanel(username); if (!parsedRegion) { return { updated: false, region: '' }; } const currentRegion = normalize711RegionCodeForPanel(inputIpProxyRegion.value || ''); if (currentRegion === parsedRegion) { return { updated: false, region: parsedRegion }; } inputIpProxyRegion.value = parsedRegion; return { updated: true, region: parsedRegion }; } function sync711UsernameFromSessionFieldsForPanel(options = {}) { if (!shouldAutoSync711SessionFieldsForPanel(options?.state || latestState)) { return { updated: false, username: '' }; } if (!inputIpProxyUsername) { return { updated: false, username: '' }; } const currentUsername = String(inputIpProxyUsername.value || '').trim(); if (!currentUsername) { return { updated: false, username: '' }; } const sessionId = normalize711SessionIdForPanel( options?.sessionId !== undefined ? options.sessionId : (inputIpProxyAccountSessionPrefix?.value || '') ); const lifeMinutes = normalizeIpProxyAccountLifeMinutes( options?.sessTime !== undefined ? options.sessTime : (inputIpProxyAccountLifeMinutes?.value || '') ); const sessTime = normalize711SessTimeForPanel(lifeMinutes); const nextUsername = apply711SessionConfigToUsernameForPanel(currentUsername, { sessionId, sessTime, removeWhenEmpty: Boolean(options?.removeWhenEmpty), }); if (!nextUsername || nextUsername === currentUsername) { return { updated: false, username: currentUsername }; } inputIpProxyUsername.value = nextUsername; return { updated: true, username: nextUsername }; } function sync711UsernameFromRegionForPanel(options = {}) { if (!shouldAutoSync711SessionFieldsForPanel(options?.state || latestState)) { return { updated: false, username: '' }; } if (!inputIpProxyUsername) { return { updated: false, username: '' }; } const currentUsername = String(inputIpProxyUsername.value || '').trim(); if (!currentUsername) { return { updated: false, username: '' }; } const rawRegion = String( options?.region !== undefined ? options.region : (inputIpProxyRegion?.value || '') ).trim(); const normalizedRegion = normalize711RegionCodeForPanel(rawRegion); const nextUsername = apply711RegionToUsernameForPanel( currentUsername, normalizedRegion, { removeWhenEmpty: Boolean(options?.removeWhenEmpty) } ); if (!nextUsername || nextUsername === currentUsername) { return { updated: false, username: currentUsername }; } inputIpProxyUsername.value = nextUsername; return { updated: true, username: nextUsername }; } function canChangeIpProxyExitWithCurrentSession(state = latestState) { const mode = getSelectedIpProxyMode(); const service = normalizeIpProxyService( selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE ); if (mode !== 'account' || service !== '711proxy') { return false; } const currentEntry = getIpProxyCurrentEntry(state); const username = String(currentEntry?.username || inputIpProxyUsername?.value || '').trim(); return has711SessionTokenForPanel(username); } function buildIpProxyActionHintText(options = {}) { const mode = normalizeIpProxyModeForCurrentRelease(options?.mode || DEFAULT_IP_PROXY_MODE); const poolCount = Math.max(0, Number(options?.poolCount) || 0); const changeAvailable = Boolean(options?.changeAvailable); const dynamicPoolCount = poolCount > 0 ? poolCount : 1; if (mode === 'api') { const nextPart = poolCount > 1 ? `下一条:当前共 ${dynamicPoolCount} 条节点,切到已拉取代理池的下一条节点。` : `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`; return `${nextPart} Change:仅账号模式可用。`; } const nextPart = poolCount > 1 ? `下一条:当前共 ${dynamicPoolCount} 条节点,切到代理池的下一条节点。` : `下一条:当前仅 ${dynamicPoolCount} 条节点,执行重绑复测(不保证更换出口)。`; const changePart = changeAvailable ? 'Change:保持当前 session 重绑链路并复测出口。' : 'Change:需 711 账号模式且用户名包含 session。'; return `${nextPart} ${changePart}`; } function setIpProxyCurrentDisplay(text = '', hasValue = false) { if (!ipProxyCurrent) return; ipProxyCurrent.textContent = text || '暂无可用代理'; ipProxyCurrent.title = text || '暂无可用代理'; ipProxyCurrent.classList.toggle('has-value', Boolean(hasValue)); } function formatIpProxyCurrentDisplay(state = latestState) { const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode); if (mode === 'account') { const current = getIpProxyCurrentEntry(state); if (!current) { return { text: '账号模式:请填写代理列表,或填写 Host / Port', hasValue: false, }; } return { text: `${current.host}:${current.port}${current.region ? ` [${current.region}]` : ''}`, hasValue: true, }; } const runtime = getIpProxyRuntimeSnapshot(state, mode); const current = getIpProxyCurrentEntry(state); const count = runtime.pool.length; const index = runtime.index; if (!current) { return { text: count ? `已拉取 ${count} 条,点击“下一条”切换` : '暂无可用代理', hasValue: false, }; } const region = String(current.region || '').trim(); const label = region ? `${current.host}:${current.port} [${region}]` : `${current.host}:${current.port}`; return { text: label, hasValue: true, }; } function extractIpProxyEndpointToken(text = '') { const raw = String(text || '').trim(); if (!raw) return ''; const match = raw.match(/^([^\s\[]+:\d+)/); return match ? String(match[1]).toLowerCase() : ''; } function extractIpProxyIndexToken(text = '') { const raw = String(text || '').trim(); if (!raw) return ''; const match = raw.match(/\((\d+\s*\/\s*\d+)\)\s*$/); if (!match) return ''; return String(match[1]).replace(/\s+/g, ''); } function buildIpProxyCurrentDisplayText(display = {}, runtimeStatus = {}) { const rawText = String(display?.text || '').trim(); const hasValue = Boolean(display?.hasValue); if (!hasValue || !rawText) { return rawText; } return rawText; } function formatIpProxyRuntimeStatus(state = latestState) { const enabled = Boolean(state?.ipProxyEnabled); const mode = normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode); const hasAccountListConfigured = mode === 'account' && normalizeIpProxyAccountList(state?.ipProxyAccountList || '').split('\n').filter(Boolean).length > 0; const accountSourceTag = mode === 'account' ? (hasAccountListConfigured ? '(账号列表)' : '(固定账号)') : ''; const accountSourceDetail = mode === 'account' ? ( hasAccountListConfigured ? '当前生效来源:账号列表(固定账号字段已忽略)。' : '当前生效来源:固定账号字段。' ) : ''; const activeEntry = getIpProxyCurrentEntry(state); const applied = Boolean(state?.ipProxyApplied); const reason = String(state?.ipProxyAppliedReason || '').trim().toLowerCase(); const host = String( state?.ipProxyAppliedHost || activeEntry?.host || (mode === 'account' ? state?.ipProxyHost : '') || '' ).trim(); const portValue = Number( state?.ipProxyAppliedPort || activeEntry?.port || (mode === 'account' ? normalizeIpProxyPort(state?.ipProxyPort) : 0) ); const port = Number.isInteger(portValue) && portValue > 0 && portValue <= 65535 ? portValue : 0; const region = String( state?.ipProxyAppliedRegion || activeEntry?.region || (mode === 'account' ? state?.ipProxyRegion : '') || '' ).trim(); const endpoint = host && port > 0 ? `${host}:${port}` : ''; const endpointWithRegion = endpoint ? `${endpoint}${region ? ` [${region}]` : ''}` : ''; const hasAuth = Boolean(state?.ipProxyAppliedHasAuth); const authSuffix = hasAuth ? '(需要鉴权)' : ''; const errorText = String(state?.ipProxyAppliedError || '').trim(); const warningText = String(state?.ipProxyAppliedWarning || '').trim(); const exitIp = String(state?.ipProxyAppliedExitIp || '').trim(); const exitRegion = String(state?.ipProxyAppliedExitRegion || '').trim(); const exitDetecting = Boolean(state?.ipProxyAppliedExitDetecting); const exitError = String(state?.ipProxyAppliedExitError || '').trim(); const exitSource = String(state?.ipProxyAppliedExitSource || '').trim().toLowerCase(); const exitSourceSuffix = exitSource === 'page_context' ? '(页面探测)' : (exitSource === 'background_fallback' ? '(后台兜底,可能受全局代理影响)' : ''); const endpointSummary = endpointWithRegion ? `${endpointWithRegion}${authSuffix}` : (endpoint ? `${endpoint}${authSuffix}` : ''); const exitSummary = exitIp ? `${exitIp}${exitRegion ? ` [${exitRegion}]` : ''}${exitSourceSuffix}` : (exitDetecting ? '检测中...' : '未检测到'); const details = [accountSourceDetail, errorText, warningText, exitError] .map((item) => String(item || '').trim()) .filter(Boolean) .join('\n'); if (!enabled) { return { stateClass: 'state-idle', text: '未启用,沿用浏览器默认/全局代理。', details: '', hideCurrentDisplay: false, }; } if (applied) { const statusPrefix = endpointSummary ? `当前代理:${endpointSummary}${accountSourceTag}` : '当前代理:已启用'; const statusText = `${statusPrefix};当前出口:${exitSummary}`; const briefWarning = warningText ? ';地区校验未通过(详情可展开查看)' : ''; return { stateClass: warningText ? 'state-warning' : 'state-applied', text: `${statusText}${briefWarning}`, details, hideCurrentDisplay: true, }; } if (reason === 'missing_proxy_entry') { if (errorText) { return { stateClass: 'state-warning', text: '已启用,但当前没有可用代理(已阻断直连)。', details: errorText, hideCurrentDisplay: false, }; } return { stateClass: 'state-warning', text: mode === 'account' ? '已启用,但账号模式没有可用代理。请先填写代理列表,或填写 Host/Port。已阻断所有网站直连。' : '已启用,但当前没有可用代理。请先点击“拉取”获取 IP 列表。已阻断所有网站直连。', details: '', hideCurrentDisplay: false, }; } if (reason === 'proxy_api_unavailable') { return { stateClass: 'state-error', text: '已启用,但当前浏览器不支持扩展代理 API,无法应用。', details, hideCurrentDisplay: false, }; } if (reason === 'apply_failed') { return { stateClass: 'state-error', text: '已启用,但代理应用失败(已回退默认代理)。', details: errorText || details, hideCurrentDisplay: false, }; } if (reason === 'connectivity_failed') { const prefix = endpointSummary ? `当前代理:${endpointSummary}${accountSourceTag}` : '当前代理:未知'; const targetUnreachable = /真实目标|chatgpt\.com 不可达|target:page_context/i.test(errorText || details); const exitPart = exitIp ? `;当前出口:${exitSummary}` : ''; return { stateClass: 'state-error', text: targetUnreachable && exitIp ? `${prefix}${exitPart};ChatGPT 目标不可达,请切换支持 ChatGPT 的节点。` : `${prefix}${exitPart};连通性失败,请切换节点或重试。`, details: errorText || details, hideCurrentDisplay: true, }; } return { stateClass: 'state-warning', text: endpointWithRegion ? `已启用,等待生效:${endpointWithRegion}${authSuffix}` : '已启用,等待拉取并应用代理。', details, hideCurrentDisplay: false, }; } function setIpProxyRuntimeStatusDisplay(status = {}) { if (!ipProxyRuntimeStatus || !ipProxyRuntimeText) { return; } const text = String(status?.text || '未启用,沿用浏览器默认/全局代理。').trim(); const stateClass = String(status?.stateClass || 'state-idle').trim() || 'state-idle'; ipProxyRuntimeStatus.classList.remove('state-idle', 'state-applied', 'state-warning', 'state-error'); ipProxyRuntimeStatus.classList.add(stateClass); ipProxyRuntimeText.textContent = text; ipProxyRuntimeText.title = text; if (ipProxyRuntimeDot) { ipProxyRuntimeDot.title = text; } const runtimeMeta = ipProxyCurrent?.closest?.('.ip-proxy-runtime-meta') || null; if (runtimeMeta) { runtimeMeta.style.display = status?.hideCurrentDisplay ? 'none' : ''; } const detailsText = String(status?.details || '').trim(); if (ipProxyRuntimeDetails && ipProxyRuntimeDetailsText) { if (detailsText) { ipProxyRuntimeDetails.hidden = false; ipProxyRuntimeDetailsText.textContent = detailsText; } else { ipProxyRuntimeDetails.hidden = true; ipProxyRuntimeDetails.open = false; ipProxyRuntimeDetailsText.textContent = ''; } } } function normalizeIpProxyEnabledInlineRegion(value = '') { const normalized = String(value || '').trim(); return normalized ? normalized.toUpperCase() : ''; } function setIpProxyEnabledInlineStatus(state = {}, enabled = getSelectedIpProxyEnabled()) { if (!ipProxyEnabledStatus || !ipProxyEnabledStatusText) { return; } ipProxyEnabledStatus.classList.remove('is-off', 'is-on'); if (!enabled) { const text = '未开启'; ipProxyEnabledStatus.classList.add('is-off'); ipProxyEnabledStatusText.textContent = text; ipProxyEnabledStatusText.title = text; ipProxyEnabledStatus.title = text; if (ipProxyEnabledStatusDot) { ipProxyEnabledStatusDot.title = text; } return; } const region = normalizeIpProxyEnabledInlineRegion(state?.ipProxyAppliedExitRegion) || normalizeIpProxyEnabledInlineRegion(state?.ipProxyAppliedRegion) || normalizeIpProxyEnabledInlineRegion(state?.ipProxyRegion); const text = region ? `已开启 · ${region}` : '已开启'; ipProxyEnabledStatus.classList.add('is-on'); ipProxyEnabledStatusText.textContent = text; ipProxyEnabledStatusText.title = text; ipProxyEnabledStatus.title = text; if (ipProxyEnabledStatusDot) { ipProxyEnabledStatusDot.title = text; } } function updateIpProxyUI(state = latestState) { const enabled = getSelectedIpProxyEnabled(); const showSettings = enabled && ipProxySectionExpanded; const mode = getSelectedIpProxyMode(); const service = normalizeIpProxyService(selectIpProxyService?.value || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE); const apiModeAvailable = isIpProxyApiModeAvailable(); const accountListAvailable = isIpProxyAccountListAvailable(); const isApiMode = mode === 'api' && apiModeAvailable; const isAccountMode = mode === 'account'; const showSessionOptions = isAccountMode && service === '711proxy'; const hasAccountListConfigured = accountListAvailable && isAccountMode && hasCurrentInputAccountListEntries(); const canOperate = !isAutoRunLockedPhase(); const actionState = getIpProxyActionState(); const actionBusy = Boolean(actionState.busy); const busyAction = normalizeIpProxyActionType(actionState.action); const runtimeState = state || latestState || {}; if (rowIpProxyEnabled) { rowIpProxyEnabled.style.display = ''; } if (btnToggleIpProxySection) { btnToggleIpProxySection.disabled = !enabled; btnToggleIpProxySection.textContent = showSettings ? '收起设置' : '展开设置'; btnToggleIpProxySection.title = enabled ? (showSettings ? '收起 IP 代理设置' : '展开 IP 代理设置') : '开启 IP 代理后可展开设置'; btnToggleIpProxySection.setAttribute('aria-expanded', String(showSettings)); } if (rowIpProxyFold) { rowIpProxyFold.style.display = showSettings ? '' : 'none'; } if (rowIpProxyService) { rowIpProxyService.style.display = showSettings ? '' : 'none'; } if (rowIpProxyMode) { rowIpProxyMode.style.display = showSettings ? '' : 'none'; } if (rowIpProxyLayout) { rowIpProxyLayout.style.display = showSettings ? '' : 'none'; } if (rowIpProxyApiUrl) { rowIpProxyApiUrl.style.display = showSettings && apiModeAvailable && isApiMode ? '' : 'none'; } if (rowIpProxyAccountList) { rowIpProxyAccountList.style.display = showSettings && isAccountMode && accountListAvailable ? '' : 'none'; } if (rowIpProxyAccountSessionPrefix) { rowIpProxyAccountSessionPrefix.style.display = showSettings && showSessionOptions ? '' : 'none'; } if (rowIpProxyAccountLifeMinutes) { rowIpProxyAccountLifeMinutes.style.display = showSettings && showSessionOptions ? '' : 'none'; } if (rowIpProxyPoolTargetCount) { rowIpProxyPoolTargetCount.style.display = showSettings ? '' : 'none'; } const autoSyncEnabledRow = typeof rowIpProxyAutoSyncEnabled !== 'undefined' ? rowIpProxyAutoSyncEnabled : null; const autoSyncIntervalRow = typeof rowIpProxyAutoSyncInterval !== 'undefined' ? rowIpProxyAutoSyncInterval : null; if (autoSyncEnabledRow) { autoSyncEnabledRow.style.display = showSettings ? '' : 'none'; } if (autoSyncIntervalRow) { autoSyncIntervalRow.style.display = showSettings ? '' : 'none'; } if (rowIpProxyHost) { rowIpProxyHost.style.display = showSettings && isAccountMode ? '' : 'none'; } if (rowIpProxyPort) { rowIpProxyPort.style.display = showSettings && isAccountMode ? '' : 'none'; } if (rowIpProxyProtocol) { rowIpProxyProtocol.style.display = showSettings ? '' : 'none'; } if (rowIpProxyUsername) { rowIpProxyUsername.style.display = showSettings && isAccountMode ? '' : 'none'; } if (rowIpProxyPassword) { rowIpProxyPassword.style.display = showSettings && isAccountMode ? '' : 'none'; } if (rowIpProxyRegion) { rowIpProxyRegion.style.display = showSettings && isAccountMode ? '' : 'none'; } if (rowIpProxyActions) { rowIpProxyActions.style.display = showSettings ? '' : 'none'; } if (ipProxyActionButtons) { ipProxyActionButtons.style.display = showSettings ? '' : 'none'; } if (rowIpProxyRuntimeStatus) { rowIpProxyRuntimeStatus.style.display = showSettings ? '' : 'none'; } if (ipProxyLayout) { ipProxyLayout.classList.toggle('is-account-only', !apiModeAvailable); } if (selectIpProxyService) { selectIpProxyService.value = service; selectIpProxyService.disabled = true; } if (typeof updateIpProxyServiceLoginButtonState === 'function') { updateIpProxyServiceLoginButtonState({ service, enabled, }); } ipProxyModeButtons.forEach((button) => { const buttonMode = normalizeIpProxyMode(button?.dataset?.ipProxyMode || DEFAULT_IP_PROXY_MODE); const apiButton = buttonMode === 'api'; button.disabled = apiButton && !apiModeAvailable; if (apiButton) { button.hidden = false; } }); if (ipProxyApiPanel) { ipProxyApiPanel.classList.toggle('is-disabled', !apiModeAvailable); ipProxyApiPanel.setAttribute('aria-disabled', String(!apiModeAvailable)); ipProxyApiPanel.hidden = !apiModeAvailable; ipProxyApiPanel.style.display = showSettings && apiModeAvailable ? '' : 'none'; } if (inputIpProxyApiUrl) { inputIpProxyApiUrl.disabled = !enabled || !apiModeAvailable; } if (btnToggleIpProxyApiUrl) { btnToggleIpProxyApiUrl.disabled = !enabled || !apiModeAvailable; } if (inputIpProxyHost) { inputIpProxyHost.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyPort) { inputIpProxyPort.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (selectIpProxyProtocol) { selectIpProxyProtocol.disabled = !enabled || (isAccountMode && hasAccountListConfigured); } if (inputIpProxyUsername) { inputIpProxyUsername.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyPassword) { inputIpProxyPassword.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (btnToggleIpProxyUsername) { btnToggleIpProxyUsername.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (btnToggleIpProxyPassword) { btnToggleIpProxyPassword.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyRegion) { inputIpProxyRegion.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyAccountSessionPrefix) { inputIpProxyAccountSessionPrefix.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyAccountLifeMinutes) { inputIpProxyAccountLifeMinutes.disabled = !enabled || !isAccountMode || hasAccountListConfigured; } if (inputIpProxyAccountList) { inputIpProxyAccountList.disabled = !enabled || !isAccountMode || !accountListAvailable; } const autoSyncEnabledInput = typeof inputIpProxyAutoSyncEnabled !== 'undefined' ? inputIpProxyAutoSyncEnabled : null; const autoSyncIntervalInput = typeof inputIpProxyAutoSyncIntervalMinutes !== 'undefined' ? inputIpProxyAutoSyncIntervalMinutes : null; if (autoSyncEnabledInput) { autoSyncEnabledInput.disabled = !enabled; } if (autoSyncIntervalInput) { const autoSyncEnabled = Boolean(autoSyncEnabledInput?.checked); if (!Number.isFinite(Number.parseInt(String(autoSyncIntervalInput.value || '').trim(), 10))) { autoSyncIntervalInput.value = '15'; } autoSyncIntervalInput.disabled = !enabled || !autoSyncEnabled; } const runtimeStatus = formatIpProxyRuntimeStatus(runtimeState); setIpProxyRuntimeStatusDisplay(runtimeStatus); const currentDisplay = formatIpProxyCurrentDisplay(runtimeState); const currentDisplayText = buildIpProxyCurrentDisplayText(currentDisplay, runtimeStatus); setIpProxyCurrentDisplay(currentDisplayText, currentDisplay.hasValue); const runtimeSnapshot = getIpProxyRuntimeSnapshot(runtimeState, mode, service); const runtimePoolCount = Array.isArray(runtimeSnapshot?.pool) ? runtimeSnapshot.pool.length : 0; const runtimePoolCountForDisplay = runtimePoolCount > 0 ? runtimePoolCount : 1; const hasCurrentEntry = Boolean(getIpProxyCurrentEntry(runtimeState)); const changeAvailable = canChangeIpProxyExitWithCurrentSession(runtimeState); const nextActionTitle = runtimePoolCount > 1 ? `切换到代理池下一条节点并应用(当前共 ${runtimePoolCountForDisplay} 条)` : `当前仅 ${runtimePoolCountForDisplay} 条节点:重绑当前节点并复测连通性(不保证更换出口)`; if (btnIpProxyRefresh) { btnIpProxyRefresh.disabled = actionBusy || !enabled || !canOperate; btnIpProxyRefresh.textContent = busyAction === 'refresh' ? (isApiMode ? '拉取中...' : '同步中...') : (isApiMode ? '拉取' : '同步'); btnIpProxyRefresh.title = isApiMode ? '拉取代理池并应用当前代理' : '同步账号代理列表并应用当前代理'; } if (btnIpProxyNext) { btnIpProxyNext.disabled = actionBusy || !enabled || !canOperate || !hasCurrentEntry; btnIpProxyNext.textContent = busyAction === 'next' ? '切换中...' : '下一条'; btnIpProxyNext.title = nextActionTitle; } if (btnIpProxyChange) { btnIpProxyChange.disabled = actionBusy || !enabled || !canOperate || !changeAvailable; btnIpProxyChange.textContent = busyAction === 'change' ? 'Change中...' : 'Change'; btnIpProxyChange.title = changeAvailable ? '保持当前会话并刷新出口(仅 711 + session)' : '当前模式不支持 Change(需 711 账号模式且用户名包含 session)'; } if (btnIpProxyProbe) { btnIpProxyProbe.disabled = actionBusy || !enabled || !canOperate; btnIpProxyProbe.textContent = busyAction === 'probe' ? '检测中...' : '检测出口'; } if (btnIpProxyCheckIp) { btnIpProxyCheckIp.disabled = false; btnIpProxyCheckIp.title = '打开公网 IP 检测页'; } if (ipProxyActionHint) { const actionHint = buildIpProxyActionHintText({ mode, poolCount: runtimePoolCount, changeAvailable, }); ipProxyActionHint.textContent = actionHint; ipProxyActionHint.title = actionHint; } } async function refreshIpProxyPoolByApi(options = {}) { const { silent = false } = options; const mode = normalizeIpProxyModeForCurrentRelease(getSelectedIpProxyMode()); const response = await chrome.runtime.sendMessage({ type: 'REFRESH_IP_PROXY_POOL', source: 'sidepanel', payload: { mode, skipExitProbe: true, }, }); if (response?.error) { throw new Error(response.error); } const responseMode = normalizeIpProxyModeForCurrentRelease(response?.mode || mode); const patch = {}; Object.assign(patch, buildIpProxyRuntimeStatePatchForMode(responseMode, response)); if (response?.proxyRouting && typeof response.proxyRouting === 'object') { patch.ipProxyApplied = Boolean(response.proxyRouting.applied); patch.ipProxyAppliedReason = String(response.proxyRouting.reason || '').trim().toLowerCase(); patch.ipProxyAppliedHost = String(response.proxyRouting.host || '').trim(); patch.ipProxyAppliedPort = Number(response.proxyRouting.port) || 0; patch.ipProxyAppliedRegion = String(response.proxyRouting.region || '').trim(); patch.ipProxyAppliedHasAuth = Boolean(response.proxyRouting.hasAuth); patch.ipProxyAppliedProvider = normalizeIpProxyService(response.proxyRouting.provider || ''); patch.ipProxyAppliedError = String(response.proxyRouting.error || '').trim(); patch.ipProxyAppliedWarning = String(response.proxyRouting.warning || '').trim(); patch.ipProxyAppliedExitIp = String(response.proxyRouting.exitIp || '').trim(); patch.ipProxyAppliedExitRegion = String(response.proxyRouting.exitRegion || '').trim(); patch.ipProxyAppliedExitDetecting = Boolean(response.proxyRouting.exitDetecting); patch.ipProxyAppliedExitError = String(response.proxyRouting.exitError || '').trim(); patch.ipProxyAppliedExitSource = String(response.proxyRouting.exitSource || '').trim().toLowerCase(); patch.ipProxyAppliedAt = Date.now(); } if (Object.keys(patch).length) { syncLatestState(patch); } updateIpProxyUI(latestState); scheduleIpProxyExitProbe({ silent: true }); if (!silent) { if (mode === 'account') { showToast(`已同步账号代理:${response?.display || formatIpProxyCurrentDisplay(latestState).text}`, 'success', 1800); } else { showToast(`已拉取代理池:${Number(response?.count) || 0} 条`, 'success', 1800); } } return response; } async function switchIpProxyToNext(options = {}) { const { silent = false } = options; const mode = normalizeIpProxyModeForCurrentRelease(getSelectedIpProxyMode()); const response = await chrome.runtime.sendMessage({ type: 'SWITCH_IP_PROXY', source: 'sidepanel', payload: { direction: 'next', mode, forceRefresh: false, skipExitProbe: true, }, }); if (response?.error) { throw new Error(response.error); } const responseMode = normalizeIpProxyModeForCurrentRelease(response?.mode || mode); const patch = {}; Object.assign(patch, buildIpProxyRuntimeStatePatchForMode(responseMode, response)); if (response?.proxyRouting && typeof response.proxyRouting === 'object') { patch.ipProxyApplied = Boolean(response.proxyRouting.applied); patch.ipProxyAppliedReason = String(response.proxyRouting.reason || '').trim().toLowerCase(); patch.ipProxyAppliedHost = String(response.proxyRouting.host || '').trim(); patch.ipProxyAppliedPort = Number(response.proxyRouting.port) || 0; patch.ipProxyAppliedRegion = String(response.proxyRouting.region || '').trim(); patch.ipProxyAppliedHasAuth = Boolean(response.proxyRouting.hasAuth); patch.ipProxyAppliedProvider = normalizeIpProxyService(response.proxyRouting.provider || ''); patch.ipProxyAppliedError = String(response.proxyRouting.error || '').trim(); patch.ipProxyAppliedWarning = String(response.proxyRouting.warning || '').trim(); patch.ipProxyAppliedExitIp = String(response.proxyRouting.exitIp || '').trim(); patch.ipProxyAppliedExitRegion = String(response.proxyRouting.exitRegion || '').trim(); patch.ipProxyAppliedExitDetecting = Boolean(response.proxyRouting.exitDetecting); patch.ipProxyAppliedExitError = String(response.proxyRouting.exitError || '').trim(); patch.ipProxyAppliedExitSource = String(response.proxyRouting.exitSource || '').trim().toLowerCase(); patch.ipProxyAppliedAt = Date.now(); } if (Object.keys(patch).length) { syncLatestState(patch); } updateIpProxyUI(latestState); scheduleIpProxyExitProbe({ silent: true }); if (!silent) { showToast(`已切换代理:${response?.display || formatIpProxyCurrentDisplay(latestState).text}`, 'success', 1800); } return response; } async function changeIpProxyExitBySession(options = {}) { const { silent = false } = options; const mode = normalizeIpProxyModeForCurrentRelease(getSelectedIpProxyMode()); const response = await chrome.runtime.sendMessage({ type: 'CHANGE_IP_PROXY_EXIT', source: 'sidepanel', payload: { mode, skipExitProbe: true, }, }); if (response?.error) { throw new Error(response.error); } const responseMode = normalizeIpProxyModeForCurrentRelease(response?.mode || mode); const patch = {}; Object.assign(patch, buildIpProxyRuntimeStatePatchForMode(responseMode, response)); if (response?.proxyRouting && typeof response.proxyRouting === 'object') { patch.ipProxyApplied = Boolean(response.proxyRouting.applied); patch.ipProxyAppliedReason = String(response.proxyRouting.reason || '').trim().toLowerCase(); patch.ipProxyAppliedHost = String(response.proxyRouting.host || '').trim(); patch.ipProxyAppliedPort = Number(response.proxyRouting.port) || 0; patch.ipProxyAppliedRegion = String(response.proxyRouting.region || '').trim(); patch.ipProxyAppliedHasAuth = Boolean(response.proxyRouting.hasAuth); patch.ipProxyAppliedProvider = normalizeIpProxyService(response.proxyRouting.provider || ''); patch.ipProxyAppliedError = String(response.proxyRouting.error || '').trim(); patch.ipProxyAppliedWarning = String(response.proxyRouting.warning || '').trim(); patch.ipProxyAppliedExitIp = String(response.proxyRouting.exitIp || '').trim(); patch.ipProxyAppliedExitRegion = String(response.proxyRouting.exitRegion || '').trim(); patch.ipProxyAppliedExitDetecting = Boolean(response.proxyRouting.exitDetecting); patch.ipProxyAppliedExitError = String(response.proxyRouting.exitError || '').trim(); patch.ipProxyAppliedExitSource = String(response.proxyRouting.exitSource || '').trim().toLowerCase(); patch.ipProxyAppliedAt = Date.now(); } if (Object.keys(patch).length) { syncLatestState(patch); } updateIpProxyUI(latestState); scheduleIpProxyExitProbe({ silent: true }); if (!silent) { showToast(`已执行 Change:${response?.display || formatIpProxyCurrentDisplay(latestState).text}`, 'success', 1800); } return response; } async function probeIpProxyExit(options = {}) { const { silent = false } = options; const sendMessage = typeof sendSidepanelMessage === 'function' ? sendSidepanelMessage : (message) => chrome.runtime.sendMessage(message); const response = await sendMessage({ type: 'PROBE_IP_PROXY_EXIT', source: 'sidepanel', payload: {}, }); if (response?.error) { throw new Error(response.error); } const routing = response?.proxyRouting || {}; syncLatestState({ ipProxyApplied: Boolean(routing.applied), ipProxyAppliedReason: String(routing.reason || '').trim().toLowerCase(), ipProxyAppliedHost: String(routing.host || '').trim(), ipProxyAppliedPort: Number(routing.port) || 0, ipProxyAppliedRegion: String(routing.region || '').trim(), ipProxyAppliedHasAuth: Boolean(routing.hasAuth), ipProxyAppliedProvider: normalizeIpProxyService(routing.provider || ''), ipProxyAppliedError: String(routing.error || '').trim(), ipProxyAppliedWarning: String(routing.warning || '').trim(), ipProxyAppliedExitIp: String(routing.exitIp || '').trim(), ipProxyAppliedExitRegion: String(routing.exitRegion || '').trim(), ipProxyAppliedExitDetecting: Boolean(routing.exitDetecting), ipProxyAppliedExitError: String(routing.exitError || '').trim(), ipProxyAppliedExitSource: String(routing.exitSource || '').trim().toLowerCase(), ipProxyAppliedAt: Date.now(), }); updateIpProxyUI(latestState); if (!silent) { const exitIp = String(routing?.exitIp || '').trim(); const exitRegion = String(routing?.exitRegion || '').trim(); const exitSource = String(routing?.exitSource || '').trim().toLowerCase(); const sourceHint = exitSource === 'background_fallback' ? '(后台兜底,可能受全局代理影响)' : ''; if (exitIp) { showToast(`出口检测成功:${exitIp}${exitRegion ? ` [${exitRegion}]` : ''}${sourceHint}`, 'success', 2600); } else { showToast('出口检测完成,但未获取到出口 IP', 'warn', 2200); } } return response; }