// background/ip-proxy-core.js — IP代理核心(解析/应用/切换/探测) let ipProxyAuthListenerInstalled = false; let ipProxyErrorListenerInstalled = false; let currentIpProxyAuthEntry = null; let ipProxyExitDetectionToken = 0; let ipProxyProbeInFlightPromise = null; let lastAppliedIpProxyEntrySignature = ''; let ipProxyAuthHostVariantToggle = false; const ipProxyHostResolveCache = new Map(); const IP_PROXY_HOST_RESOLVE_CACHE_TTL_MS = 60 * 1000; const ipProxyHostResolveCursor = new Map(); let lastAppliedIpProxyAuthSnapshot = { host: '', port: 0, username: '', password: '', }; let ipProxyAuthDiagnostics = { challengeCount: 0, providedCount: 0, lastIsProxy: null, lastStatusCode: 0, lastHost: '', lastPort: 0, lastAt: 0, }; let ipProxyLastRuntimeError = { error: '', details: '', fatal: false, at: 0, }; function normalizeAutomationWindowId(value) { if (value === null || value === undefined || value === '') { return null; } const numeric = Number(value); return Number.isInteger(numeric) && numeric >= 0 ? numeric : null; } function buildAutomationWindowUnavailableError(error) { const suffix = error?.message ? ` 原因:${error.message}` : ''; return new Error(`自动任务窗口已不可用,请在目标 Chrome 窗口重新打开侧边栏并启动任务。${suffix}`); } async function createAutomationScopedTab(createProperties = {}, options = {}) { const windowId = normalizeAutomationWindowId( options?.automationWindowId ?? options?.windowId ?? options?.state?.automationWindowId ?? null ); if (windowId === null) { return chrome.tabs.create(createProperties || {}); } try { return await chrome.tabs.create({ ...(createProperties || {}), windowId, }); } catch (error) { throw buildAutomationWindowUnavailableError(error); } } async function queryAutomationScopedTabs(queryInfo = {}, options = {}) { const windowId = normalizeAutomationWindowId( options?.automationWindowId ?? options?.windowId ?? options?.state?.automationWindowId ?? null ); if (windowId === null) { return chrome.tabs.query(queryInfo || {}); } const scopedQuery = { ...(queryInfo || {}), windowId, }; delete scopedQuery.currentWindow; try { return await chrome.tabs.query(scopedQuery); } catch (error) { throw buildAutomationWindowUnavailableError(error); } } const IP_PROXY_EXIT_PROBE_ENDPOINTS = [ 'http://ip-api.com/json?lang=en', 'http://ipinfo.io/json', 'https://ipinfo.io/json', 'http://myip.ipip.net', 'https://chatgpt.com/cdn-cgi/trace', 'https://ipwho.is/', 'https://ipapi.co/json/', 'https://api.ipify.org?format=json', 'https://api64.ipify.org?format=json', 'https://httpbin.org/ip', 'https://checkip.amazonaws.com', 'https://ident.me', ]; const IP_PROXY_EXIT_PROBE_ENDPOINTS_711_STICKY = [ // 与 curl 口径对齐,保持单端点,避免多站点探测引入额外波动与耗时。 'https://ipinfo.io/json', ]; const IP_PROXY_TARGET_REACHABILITY_ENDPOINTS = [ // Step 1 的真实目标站点;出口 IP 可用不代表该站点的 CONNECT/TLS 链路可用。 'https://chatgpt.com/', ]; const IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS = 8000; const IP_PROXY_BACKGROUND_PROBE_MAX_ENDPOINTS = 4; const IP_PROXY_BACKGROUND_PROBE_PER_ENDPOINT_TIMEOUT_MS = 3500; const IP_PROXY_PAGE_CONTEXT_PROBE_URL = 'https://example.com/'; const IP_PROXY_PAGE_CONTEXT_BASELINE_URL = 'https://ifconfig.co/json'; const IP_PROXY_PAGE_CONTEXT_READY_TIMEOUT_MS = 8000; const IP_PROXY_PAGE_CONTEXT_BASELINE_TIMEOUT_MS = 6000; const IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS = 8; const IP_PROXY_GUARD_BLOCK_RULE_ID = 10991; const IP_PROXY_GUARD_REGEX = '^https?:\\/\\/([^\\/]+\\.)?(chatgpt\\.com|openai\\.com)(\\/|$)'; function normalizeIpProxyProviderValue(value = '') { const normalized = String(value || '').trim().toLowerCase(); const enabledValues = Array.isArray(globalThis.IP_PROXY_ENABLED_SERVICE_VALUES) ? globalThis.IP_PROXY_ENABLED_SERVICE_VALUES : []; if (enabledValues.includes(normalized)) { return normalized; } if (IP_PROXY_SERVICE_VALUES.includes(normalized)) { return DEFAULT_IP_PROXY_SERVICE; } return DEFAULT_IP_PROXY_SERVICE; } function normalizeProxyHostForCompare(value = '') { return String(value || '').trim().toLowerCase().replace(/\.$/, ''); } function stripProxyHostTrailingDot(value = '') { return String(value || '').trim().replace(/\.$/, ''); } function isIpv4LikeHost(value = '') { const text = String(value || '').trim(); if (!text) { return false; } return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(text); } function isIpv6LikeHost(value = '') { const text = String(value || '').trim(); if (!text) { return false; } const compact = text.replace(/^\[/, '').replace(/\]$/, ''); return compact.includes(':'); } function canUseProxyHostVariant(value = '') { const host = stripProxyHostTrailingDot(value); if (!host) { return false; } if (isIpv4LikeHost(host) || isIpv6LikeHost(host)) { return false; } return /[A-Za-z]/.test(host) && host.includes('.'); } function isValidIpv4Address(value = '') { const text = String(value || '').trim(); if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(text)) { return false; } const parts = text.split('.'); return parts.length === 4 && parts.every((part) => { const numeric = Number.parseInt(part, 10); return Number.isInteger(numeric) && numeric >= 0 && numeric <= 255; }); } function readCachedResolvedProxyHosts(host = '') { const key = stripProxyHostTrailingDot(host).toLowerCase(); if (!key) { return []; } const cached = ipProxyHostResolveCache.get(key); if (!cached || !Array.isArray(cached?.ips) || !cached?.expiresAt) { return []; } if (cached.expiresAt <= Date.now()) { ipProxyHostResolveCache.delete(key); return []; } return cached.ips; } function writeCachedResolvedProxyHosts(host = '', ips = []) { const key = stripProxyHostTrailingDot(host).toLowerCase(); if (!key) { return; } const normalized = Array.from(new Set((Array.isArray(ips) ? ips : []) .map((item) => String(item || '').trim()) .filter((item) => isValidIpv4Address(item)))); if (!normalized.length) { return; } ipProxyHostResolveCache.set(key, { ips: normalized, expiresAt: Date.now() + IP_PROXY_HOST_RESOLVE_CACHE_TTL_MS, }); if (!ipProxyHostResolveCursor.has(key)) { ipProxyHostResolveCursor.set(key, 0); } } function pickNextResolvedProxyHost(host = '', candidates = []) { const key = stripProxyHostTrailingDot(host).toLowerCase(); const list = Array.isArray(candidates) ? candidates.map((item) => String(item || '').trim()).filter((item) => isValidIpv4Address(item)) : []; if (!key || !list.length) { return ''; } const cursor = Number(ipProxyHostResolveCursor.get(key) || 0); const index = Math.abs(cursor) % list.length; ipProxyHostResolveCursor.set(key, cursor + 1); return String(list[index] || '').trim(); } async function resolveProxyHostIpv4Candidates(host = '', timeoutMs = 3500) { const normalizedHost = stripProxyHostTrailingDot(host); if (!canUseProxyHostVariant(normalizedHost)) { return []; } const cached = readCachedResolvedProxyHosts(normalizedHost); if (cached.length) { return cached; } const endpoint = `https://dns.google/resolve?name=${encodeURIComponent(normalizedHost)}&type=A`; const response = await fetchWithTimeout(endpoint, { method: 'GET', cache: 'no-store', headers: { Accept: 'application/dns-json, application/json, text/plain, */*', }, }, timeoutMs); if (!response.ok) { return []; } const payload = await response.json().catch(() => null); if (!payload || typeof payload !== 'object') { return []; } const answers = Array.isArray(payload?.Answer) ? payload.Answer : []; const ips = answers .filter((item) => Number(item?.type) === 1) .map((item) => String(item?.data || '').trim()) .filter((ip) => isValidIpv4Address(ip)); const uniqueIps = Array.from(new Set(ips)); if (!uniqueIps.length) { return []; } writeCachedResolvedProxyHosts(normalizedHost, uniqueIps); return uniqueIps; } async function maybeResolveProxyHostVariantForAuthSwitch(entry = {}, options = {}) { const force = Boolean(options?.force); if (!force) { return entry; } const provider = normalizeIpProxyProviderValue(entry?.provider || DEFAULT_IP_PROXY_SERVICE); const allow711ResolvedIp = Boolean(options?.allow711ResolvedIp); // 711 默认仍保持域名路由;仅在“多账号列表切换”场景下允许解析 IP 变体, // 用于强制打断同 host:port 连接复用,提升触发 407 挑战的概率。 if (provider === '711proxy' && !allow711ResolvedIp) { return entry; } const protocol = normalizeIpProxyProtocol(entry?.protocol || DEFAULT_IP_PROXY_PROTOCOL); if (protocol !== 'http') { return entry; } if (!String(entry?.username || '').trim()) { return entry; } const host = stripProxyHostTrailingDot(entry?.host || ''); if (!canUseProxyHostVariant(host)) { return entry; } const candidates = await resolveProxyHostIpv4Candidates(host, Number(options?.timeoutMs) || 3500).catch(() => []); if (!candidates.length) { return entry; } const selectedHost = pickNextResolvedProxyHost(host, candidates); if (!selectedHost || !isValidIpv4Address(selectedHost)) { return entry; } return { ...entry, host: selectedHost, }; } function buildHostVariantForProxy(value = '', useTrailingDot = false) { const host = stripProxyHostTrailingDot(value); if (!canUseProxyHostVariant(host)) { return host; } return useTrailingDot ? `${host}.` : host; } function shouldForceProxyConnectionDrainForEntry(entry = {}) { const nextHost = normalizeProxyHostForCompare(entry?.host || ''); const nextPort = normalizeIpProxyPort(entry?.port); const nextUser = String(entry?.username || '').trim(); const nextPass = String(entry?.password || ''); if (!nextHost || !nextPort || !nextUser) { return false; } const previousHost = normalizeProxyHostForCompare(lastAppliedIpProxyAuthSnapshot?.host || ''); const previousPort = normalizeIpProxyPort(lastAppliedIpProxyAuthSnapshot?.port); if (!previousHost || !previousPort) { return false; } const sameEndpoint = nextHost === previousHost && nextPort === previousPort; if (!sameEndpoint) { return false; } const previousUser = String(lastAppliedIpProxyAuthSnapshot?.username || '').trim(); const previousPass = String(lastAppliedIpProxyAuthSnapshot?.password || ''); return nextUser !== previousUser || nextPass !== previousPass; } function buildEffectiveProxyEntryForApply(entry = {}, options = {}) { const forceRotateVariant = Boolean(options?.forceRotateVariant); const next = { ...(entry || {}) }; const provider = normalizeIpProxyProviderValue(next?.provider || DEFAULT_IP_PROXY_SERVICE); const originalHost = String(next?.host || '').trim(); if (provider === '711proxy') { const allow711HostVariant = Boolean(options?.allow711HostVariant) && forceRotateVariant; if (allow711HostVariant && canUseProxyHostVariant(originalHost)) { ipProxyAuthHostVariantToggle = !ipProxyAuthHostVariantToggle; next.host = buildHostVariantForProxy(originalHost, ipProxyAuthHostVariantToggle); return next; } // 单账号场景下保持 host 稳定,避免不必要的链路扰动。 next.host = stripProxyHostTrailingDot(originalHost); return next; } const canVariant = canUseProxyHostVariant(originalHost); if (!canVariant) { next.host = stripProxyHostTrailingDot(originalHost); return next; } if (forceRotateVariant) { ipProxyAuthHostVariantToggle = !ipProxyAuthHostVariantToggle; } next.host = buildHostVariantForProxy(originalHost, ipProxyAuthHostVariantToggle); return next; } function resetIpProxyAuthDiagnostics() { ipProxyAuthDiagnostics = { challengeCount: 0, providedCount: 0, lastIsProxy: null, lastStatusCode: 0, lastHost: '', lastPort: 0, lastAt: 0, }; } function recordIpProxyAuthChallenge(details = {}, provided = false) { const host = String(details?.challenger?.host || '').trim(); const port = Number.parseInt(String(details?.challenger?.port || ''), 10) || 0; const statusCode = Number.parseInt(String(details?.statusCode || ''), 10) || 0; const isProxy = details?.isProxy === true; ipProxyAuthDiagnostics.challengeCount += 1; if (provided) { ipProxyAuthDiagnostics.providedCount += 1; } ipProxyAuthDiagnostics.lastIsProxy = isProxy; ipProxyAuthDiagnostics.lastStatusCode = statusCode; ipProxyAuthDiagnostics.lastHost = host; ipProxyAuthDiagnostics.lastPort = port; ipProxyAuthDiagnostics.lastAt = Date.now(); } function getIpProxyAuthDiagnosticsSummary() { const lastHost = String(ipProxyAuthDiagnostics?.lastHost || '').trim(); const lastPort = Number(ipProxyAuthDiagnostics?.lastPort || 0); const hostPort = lastHost ? `${lastHost}${lastPort > 0 ? `:${lastPort}` : ''}` : 'unknown'; const isProxyText = ipProxyAuthDiagnostics?.lastIsProxy === null ? 'n/a' : (ipProxyAuthDiagnostics.lastIsProxy ? 'true' : 'false'); return `auth(challenge=${Number(ipProxyAuthDiagnostics?.challengeCount || 0)},provided=${Number(ipProxyAuthDiagnostics?.providedCount || 0)},isProxy=${isProxyText},status=${Number(ipProxyAuthDiagnostics?.lastStatusCode || 0)},host=${hostPort})`; } function appendIpProxyAuthDiagnosticsToErrors(errors = []) { if (!Array.isArray(errors)) { return; } errors.push(`probe:${getIpProxyAuthDiagnosticsSummary()}`); } function parseIpProxyAuthDiagnosticsSummary(summary = '') { const text = String(summary || '').trim(); if (!text) { return null; } const challenge = Number.parseInt(String(text.match(/challenge=(\d+)/i)?.[1] || ''), 10) || 0; const provided = Number.parseInt(String(text.match(/provided=(\d+)/i)?.[1] || ''), 10) || 0; return { challenge, provided }; } function resetIpProxyRuntimeErrorDiagnostics() { ipProxyLastRuntimeError = { error: '', details: '', fatal: false, at: 0, }; } function getIpProxyRuntimeErrorSummary() { const error = String(ipProxyLastRuntimeError?.error || '').trim(); const details = String(ipProxyLastRuntimeError?.details || '').trim(); const fatal = Boolean(ipProxyLastRuntimeError?.fatal); if (!error && !details) { return ''; } return `proxy_error(error=${error || 'unknown'},fatal=${fatal ? 1 : 0},details=${details || 'n/a'})`; } function appendIpProxyRuntimeErrorDiagnosticsToErrors(errors = [], maxAgeMs = 30000) { if (!Array.isArray(errors)) { return; } const summary = getIpProxyRuntimeErrorSummary(); if (!summary) { return; } const ageMs = Date.now() - Number(ipProxyLastRuntimeError?.at || 0); if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > Math.max(1000, Number(maxAgeMs) || 30000)) { return; } errors.push(`probe:${summary}`); } function buildProbeDiagnosticsSummary(errors = [], maxItems = 4) { if (!Array.isArray(errors) || !errors.length) { return ''; } const normalized = errors .map((item) => String(item || '').trim()) .filter(Boolean); if (!normalized.length) { return ''; } const authItem = normalized.find((item) => /probe:auth\(/i.test(item)) || ''; const proxyErrorItem = normalized.find((item) => /probe:proxy_error\(/i.test(item)) || ''; const navigationItem = normalized.find((item) => /probe:page_context:navigation_error:/i.test(item)) || ''; const pageContextItem = normalized.find((item) => /probe:page_context:/i.test(item)) || ''; const slowRetryItem = normalized.find((item) => /probe:bg:slow_retry:/i.test(item)) || ''; const retryItem = normalized.find((item) => /probe:bg:retry:/i.test(item)) || ''; const abortFallbackItem = normalized.find((item) => /probe:bg:abort_storm_page_fallback/i.test(item)) || ''; const headLimit = Math.max(1, Number(maxItems) || 4); const headItems = normalized.slice(0, headLimit); const mustKeepItems = [ authItem, proxyErrorItem, navigationItem, pageContextItem, slowRetryItem, retryItem, abortFallbackItem, ].filter(Boolean); for (const mustKeepItem of mustKeepItems) { if (headItems.includes(mustKeepItem)) { continue; } if (headItems.length >= headLimit) { headItems[headItems.length - 1] = mustKeepItem; } else { headItems.push(mustKeepItem); } } return headItems.join(' | '); } function normalizeIpProxyMode(value = '') { const normalized = String(value || '').trim().toLowerCase(); return IP_PROXY_MODE_VALUES.includes(normalized) ? normalized : DEFAULT_IP_PROXY_MODE; } function normalizeIpProxyProtocol(value = '') { const normalized = String(value || '').trim().toLowerCase(); return IP_PROXY_PROTOCOL_VALUES.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 normalizeIpProxyCurrentIndex(value, fallback = 0) { const numeric = Number.parseInt(String(value ?? ''), 10); if (!Number.isInteger(numeric) || numeric < 0) { return Math.max(0, Number(fallback) || 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 = '') { 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(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 inferRegionFromProxyUsername(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 inferRegionFromProxyHost(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 shouldApplyConfiguredRegionFallbackToEntry(entry = {}, context = {}) { const provider = normalizeIpProxyProviderValue( context?.provider || entry?.provider || DEFAULT_IP_PROXY_SERVICE ); const configuredRegion = String(context?.configuredRegion || '').trim(); if (!configuredRegion) { return false; } if (Boolean(context?.hasAccountList)) { return false; } const entryRegion = String(entry?.region || '').trim(); if (entryRegion) { return false; } if (provider !== '711proxy') { return true; } const usernameRegion = inferRegionFromProxyUsername(entry?.username || ''); if (usernameRegion) { return true; } const hostRegion = inferRegionFromProxyHost(entry?.host || ''); if (hostRegion) { return true; } // 711 固定账号在 zone-custom 混播场景下,不使用旧的配置地区兜底,避免误报“卡在 AF”。 return false; } function resolveIpProxyAccountEntrySource(state = {}, mode = normalizeIpProxyMode(state?.ipProxyMode)) { const normalizedMode = normalizeIpProxyMode(mode); if (normalizedMode !== 'account') { return 'non_account'; } return hasConfiguredAccountListEntries(state) ? 'account_list' : 'fixed_account'; } function isIpProxyAccountListEnabled() { return Boolean(typeof IP_PROXY_ACCOUNT_LIST_ENABLED === 'undefined' ? true : IP_PROXY_ACCOUNT_LIST_ENABLED); } function normalizeIpProxyServiceProfile(rawValue = {}) { const raw = (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) ? rawValue : {}; const normalizeAutoSyncInterval = (value = '', fallback = 15) => { const rawValue = String(value ?? '').trim(); if (!rawValue) { return Math.max(1, Math.min(1440, Number(fallback) || 15)); } const numeric = Number.parseInt(rawValue, 10); if (!Number.isFinite(numeric)) { return Math.max(1, Math.min(1440, Number(fallback) || 15)); } return Math.max(1, Math.min(1440, numeric)); }; return { mode: normalizeIpProxyMode(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: normalizeAutoSyncInterval(raw.autoSyncIntervalMinutes, 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 buildIpProxyServiceProfileFromState(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 = buildIpProxyServiceProfileFromState(fallbackState); const result = {}; IP_PROXY_SERVICE_VALUES.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 normalizeIpProxyEntriesFromObjectCandidate(candidate, provider = DEFAULT_IP_PROXY_SERVICE) { if (!candidate || typeof candidate !== 'object') { return null; } const directProxyText = String( candidate.proxy || candidate.proxy_url || candidate.proxyUrl || candidate.proxyAddress || '' ).trim(); if (directProxyText) { const parsed = parseIpProxyLine(directProxyText, provider); if (parsed) { return { ...parsed, region: String(candidate.region || candidate.country || candidate.city || parsed.region || '').trim(), }; } } const host = String( candidate.host || candidate.ip || candidate.server || candidate.address || candidate.proxy_host || candidate.proxyHost || '' ).trim(); const port = normalizeIpProxyPort( candidate.port || candidate.proxy_port || candidate.proxyPort || '' ); if (!host || !port) { return null; } const username = String( candidate.username || candidate.user || candidate.account || candidate.proxy_user || candidate.proxyUser || '' ).trim(); const password = String( candidate.password || candidate.pass || candidate.pwd || candidate.proxy_pass || candidate.proxyPass || '' ); const protocol = normalizeIpProxyProtocol( candidate.protocol || candidate.schema || candidate.scheme || candidate.type || '' ); const region = String(candidate.region || candidate.country || candidate.city || '').trim(); return { host, port, username, password, protocol, region, provider: normalizeIpProxyProviderValue(candidate.provider || provider), }; } function parseIpProxyLine(line, provider = DEFAULT_IP_PROXY_SERVICE) { const text = String(line || '').trim(); if (!text) { return null; } const schemaMatch = text.match(/^(https?|socks4|socks5):\/\/(.+)$/i); const protocol = normalizeIpProxyProtocol(schemaMatch ? schemaMatch[1] : ''); const rawBody = schemaMatch ? schemaMatch[2] : text; const firstSegment = rawBody.split(/[/?#]/)[0]; const parts = firstSegment.split(':'); if (parts.length < 2) { return null; } const host = String(parts[0] || '').trim(); const port = normalizeIpProxyPort(parts[1]); if (!host || !port) { return null; } const username = String(parts[2] || '').trim(); const password = parts.length >= 4 ? String(parts.slice(3).join(':') || '') : ''; return { host, port, username, password, protocol, region: '', provider: normalizeIpProxyProviderValue(provider), }; } function enumerateContiguousIpv4WithColonCandidates(text = '', startIndex = 0) { const source = String(text || ''); const candidates = []; const sourceLength = source.length; const isDigitCode = (code) => code >= 48 && code <= 57; const readOctet = (offset, maxLen = 3) => { let end = offset; while (end < sourceLength && end < offset + maxLen) { const code = source.charCodeAt(end); if (!isDigitCode(code)) { break; } end += 1; } if (end <= offset) { return null; } const value = Number.parseInt(source.slice(offset, end), 10); if (!Number.isInteger(value) || value < 0 || value > 255) { return null; } return { value, end }; }; for (let l1 = 1; l1 <= 3; l1 += 1) { const o1 = readOctet(startIndex, l1); if (!o1 || source[o1.end] !== '.') continue; for (let l2 = 1; l2 <= 3; l2 += 1) { const o2 = readOctet(o1.end + 1, l2); if (!o2 || source[o2.end] !== '.') continue; for (let l3 = 1; l3 <= 3; l3 += 1) { const o3 = readOctet(o2.end + 1, l3); if (!o3 || source[o3.end] !== '.') continue; for (let l4 = 1; l4 <= 3; l4 += 1) { const o4 = readOctet(o3.end + 1, l4); if (!o4 || source[o4.end] !== ':') continue; const host = `${o1.value}.${o2.value}.${o3.value}.${o4.value}`; candidates.push({ host, hostEnd: o4.end, portStart: o4.end + 1, }); } } } } return candidates; } function parseContiguousProxyStream(rawText = '', provider = DEFAULT_IP_PROXY_SERVICE) { const source = String(rawText || '').trim(); if (!source) { return []; } if (!/^\d/.test(source) || !source.includes(':')) { return []; } const memo = new Map(); const solve = (offset = 0, previousHost = '') => { const key = `${offset}|${previousHost}`; if (memo.has(key)) { return memo.get(key); } if (offset >= source.length) { const terminal = { score: 0, tokens: [] }; memo.set(key, terminal); return terminal; } let best = null; const hostCandidates = enumerateContiguousIpv4WithColonCandidates(source, offset); for (const candidate of hostCandidates) { const { host, portStart } = candidate; for (let portLen = 2; portLen <= 5; portLen += 1) { const portEnd = portStart + portLen; if (portEnd > source.length) { break; } const portText = source.slice(portStart, portEnd); if (!/^\d+$/.test(portText)) { break; } const port = Number.parseInt(portText, 10); if (!Number.isInteger(port) || port <= 0 || port > 65535) { continue; } const next = solve(portEnd, host); if (!next) { continue; } const hostContinuityBonus = previousHost ? (previousHost === host ? 8 : -4) : 0; const portLengthBonus = portLen * 2; const tinyPortPenalty = port < 1000 ? -40 : 0; const score = next.score + 100 + hostContinuityBonus + portLengthBonus + tinyPortPenalty; if (!best || score > best.score) { best = { score, tokens: [{ host, port, provider: normalizeIpProxyProviderValue(provider), }, ...next.tokens], }; } } } memo.set(key, best); return best; }; const solved = solve(0, ''); if (!solved || !Array.isArray(solved.tokens) || solved.tokens.length <= 0) { return []; } return solved.tokens.map((item) => ({ host: String(item.host || '').trim(), port: normalizeIpProxyPort(item.port), username: '', password: '', protocol: DEFAULT_IP_PROXY_PROTOCOL, region: '', provider: normalizeIpProxyProviderValue(item.provider || provider), })).filter((item) => item.host && item.port); } function normalizeProxyPoolEntries(value = [], provider = DEFAULT_IP_PROXY_SERVICE) { const result = []; const seen = new Set(); const append = (entry) => { if (!entry || !entry.host || !entry.port) { return; } const normalizedEntry = { host: String(entry.host || '').trim(), port: normalizeIpProxyPort(entry.port), username: String(entry.username || '').trim(), password: String(entry.password || ''), protocol: normalizeIpProxyProtocol(entry.protocol || ''), region: String(entry.region || '').trim(), provider: normalizeIpProxyProviderValue(entry.provider || provider), }; if (!normalizedEntry.host || !normalizedEntry.port) { return; } const dedupKey = `${normalizedEntry.host}:${normalizedEntry.port}:${normalizedEntry.username}:${normalizedEntry.password}:${normalizedEntry.protocol}`; if (seen.has(dedupKey)) { return; } seen.add(dedupKey); result.push(normalizedEntry); }; if (typeof value === 'string') { const rawText = String(value || ''); normalizeIpProxyAccountList(rawText).split('\n').forEach((line) => { append(parseIpProxyLine(line, provider)); }); if (!result.length) { const contiguousEntries = parseContiguousProxyStream(rawText, provider); contiguousEntries.forEach((entry) => append(entry)); } if (!result.length) { // Lumi API 在 lb 参数异常时,可能返回无分隔的 host:port 串,这里做兜底拆分。 const contiguousMatches = rawText.match(/\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}/g) || []; contiguousMatches.forEach((token) => { append(parseIpProxyLine(token, provider)); }); } return result; } if (Array.isArray(value)) { value.forEach((item) => { if (typeof item === 'string') { append(parseIpProxyLine(item, provider)); return; } append(normalizeIpProxyEntriesFromObjectCandidate(item, provider)); }); return result; } if (value && typeof value === 'object') { append(normalizeIpProxyEntriesFromObjectCandidate(value, provider)); } return result; } function normalizeIpProxyListFromPayload(payload, provider = DEFAULT_IP_PROXY_SERVICE) { if (Array.isArray(payload)) { return normalizeProxyPoolEntries(payload, provider); } if (!payload || typeof payload !== 'object') { if (typeof payload === 'string') { return normalizeProxyPoolEntries(payload, provider); } return []; } const candidateArrays = [ payload.data, payload.list, payload.items, payload.proxies, payload.result, payload.rows, ]; for (const candidate of candidateArrays) { if (Array.isArray(candidate) && candidate.length > 0) { return normalizeProxyPoolEntries(candidate, provider); } } const singleCandidate = normalizeIpProxyEntriesFromObjectCandidate(payload, provider); return singleCandidate ? [singleCandidate] : []; } function getAccountModeProxyPoolFromState(state = {}, provider = DEFAULT_IP_PROXY_SERVICE) { const normalizedProvider = normalizeIpProxyProviderValue(provider || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE); const accountListEnabled = isIpProxyAccountListEnabled(); const rawLines = normalizeIpProxyAccountList(state?.ipProxyAccountList || '').split('\n').filter(Boolean); const lines = accountListEnabled ? rawLines : []; const hasAccountList = lines.length > 0; const transformState = hasAccountList ? { ...state, ipProxyHost: '', ipProxyPort: '', ipProxyUsername: '', ipProxyPassword: '', ipProxyRegion: '', ipProxyAccountSessionPrefix: '', ipProxyAccountLifeMinutes: '', } : state; let pool = lines.map((line) => parseIpProxyLine(line, normalizedProvider)).filter(Boolean); if (!pool.length && !hasAccountList) { const host = String(state?.ipProxyHost || '').trim(); const port = normalizeIpProxyPort(state?.ipProxyPort); if (host && port) { pool = [{ host, port, username: String(state?.ipProxyUsername || '').trim(), password: String(state?.ipProxyPassword || ''), protocol: normalizeIpProxyProtocol(state?.ipProxyProtocol), region: '', provider: normalizedProvider, }]; } else if (!accountListEnabled && rawLines.length) { const legacyEntry = parseIpProxyLine(rawLines[0], normalizedProvider); if (legacyEntry) { pool = [legacyEntry]; } } } const configuredRegion = String(state?.ipProxyRegion || '').trim(); if (!pool.length) { return []; } return pool.map((entry, index) => { let nextEntry = { ...entry }; if (typeof transformIpProxyAccountEntryByProvider === 'function') { const transformed = transformIpProxyAccountEntryByProvider(normalizedProvider, nextEntry, { state: transformState, index, hasAccountList, }); if (transformed && typeof transformed === 'object') { nextEntry = transformed; } } const inferredRegion = inferRegionFromProxyUsername(nextEntry.username); if (inferredRegion) { nextEntry.region = inferredRegion; } else if (!String(nextEntry.region || '').trim()) { const inferredHostRegion = inferRegionFromProxyHost(nextEntry.host); if (inferredHostRegion) { nextEntry.region = inferredHostRegion; } } if (shouldApplyConfiguredRegionFallbackToEntry(nextEntry, { provider: normalizedProvider, configuredRegion, hasAccountList, })) { nextEntry.region = configuredRegion; } return nextEntry; }); } function hasConfiguredAccountListEntries(state = {}) { if (!isIpProxyAccountListEnabled()) { return false; } const lines = normalizeIpProxyAccountList(state?.ipProxyAccountList || '').split('\n').filter(Boolean); return lines.length > 0; } function getAccountListParseFailureHint(state = {}, provider = DEFAULT_IP_PROXY_SERVICE) { if (!isIpProxyAccountListEnabled()) { return ''; } const normalizedProvider = normalizeIpProxyProviderValue(provider || state?.ipProxyService || DEFAULT_IP_PROXY_SERVICE); const lines = normalizeIpProxyAccountList(state?.ipProxyAccountList || '').split('\n').filter(Boolean); if (!lines.length) { return ''; } const parsedCount = lines .map((line) => parseIpProxyLine(line, normalizedProvider)) .filter(Boolean) .length; if (parsedCount > 0) { return ''; } if (normalizedProvider === '711proxy') { return '账号列表已填写,但未解析出有效条目。请按每行 host:port:username:password 填写。'; } return '账号列表已填写,但未解析出有效条目。请检查列表格式。'; } function resolveIpProxyPoolTargetCountForMode(state = {}, mode = normalizeIpProxyMode(state?.ipProxyMode)) { const normalizedMode = normalizeIpProxyMode(mode); // `ipProxyPoolTargetCount` 语义是“任务成功轮次阈值”, // 拉取/切换代理池条数不再复用该字段,避免语义混淆。 if (normalizedMode === 'account') { return 500; } return 100; } function resolveIpProxyAutoSwitchThreshold(state = {}) { return Math.max( 1, Math.min(500, Number(normalizeIpProxyPoolTargetCount(state?.ipProxyPoolTargetCount, 20)) || 20) ); } function buildProxyPoolSummary(pool = [], preferredIndex = 0) { const normalizedPool = Array.isArray(pool) ? pool : []; if (!normalizedPool.length) { return { count: 0, index: 0, current: null, display: '暂无可用代理', }; } const index = normalizeIpProxyCurrentIndex(preferredIndex, 0) % normalizedPool.length; const current = normalizedPool[index]; const region = String(current?.region || '').trim(); return { count: normalizedPool.length, index, current, display: `${current.host}:${current.port}${region ? ` [${region}]` : ''} (${index + 1}/${normalizedPool.length})`, }; } function getIpProxyRuntimeFieldNames(mode = DEFAULT_IP_PROXY_MODE) { const normalizedMode = normalizeIpProxyMode(mode); if (normalizedMode === 'account') { return { poolKey: 'ipProxyAccountPool', indexKey: 'ipProxyAccountCurrentIndex', currentKey: 'ipProxyAccountCurrent', }; } return { poolKey: 'ipProxyApiPool', indexKey: 'ipProxyApiCurrentIndex', currentKey: 'ipProxyApiCurrent', }; } function getIpProxyRuntimeSnapshot( state = {}, mode = normalizeIpProxyMode(state?.ipProxyMode), provider = normalizeIpProxyProviderValue(state?.ipProxyService) ) { const normalizedMode = normalizeIpProxyMode(mode); const normalizedProvider = normalizeIpProxyProviderValue(provider); const fields = getIpProxyRuntimeFieldNames(normalizedMode); 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 = normalizeProxyPoolEntries(state?.[fields.poolKey], normalizedProvider); const modeCurrent = normalizeProxyPoolEntries( state?.[fields.currentKey] ? [state[fields.currentKey]] : [], normalizedProvider )[0] || null; const pool = hasModePool ? modePool : []; const current = hasModeCurrent ? modeCurrent : null; const indexSource = hasModeIndex ? state?.[fields.indexKey] : 0; return { mode: normalizedMode, provider: normalizedProvider, ...fields, hasModePool, hasModeCurrent, hasModeIndex, pool, current, index: normalizeIpProxyCurrentIndex(indexSource, 0), }; } function isApiModeProxyConfigAvailable(state = {}) { return Boolean(String(state?.ipProxyApiUrl || '').trim()); } function buildIpProxyRuntimeStatePatch(mode = DEFAULT_IP_PROXY_MODE, runtime = {}, provider = DEFAULT_IP_PROXY_SERVICE) { const normalizedMode = normalizeIpProxyMode(mode); const normalizedProvider = normalizeIpProxyProviderValue(provider); const fields = getIpProxyRuntimeFieldNames(normalizedMode); const pool = normalizeProxyPoolEntries(runtime?.pool, normalizedProvider); const summary = buildProxyPoolSummary(pool, runtime?.index); const explicitCurrent = normalizeProxyPoolEntries( runtime?.current ? [runtime.current] : [], normalizedProvider )[0] || null; const current = explicitCurrent || summary.current || null; return { [fields.poolKey]: pool, [fields.indexKey]: summary.index, [fields.currentKey]: current, // 兼容旧状态字段,保持当前激活模式的摘要可见。 ipProxyPool: pool, ipProxyCurrentIndex: summary.index, ipProxyCurrent: current, }; } function getIpProxyCurrentEntryFromState(state = {}) { const mode = normalizeIpProxyMode(state?.ipProxyMode); const provider = normalizeIpProxyProviderValue(state?.ipProxyService); if (mode === 'api' && !isApiModeProxyConfigAvailable(state)) { return null; } const runtime = getIpProxyRuntimeSnapshot(state, mode, provider); if (mode === 'account') { const hasAccountList = hasConfiguredAccountListEntries(state); const accountPool = getAccountModeProxyPoolFromState(state, provider); if (hasAccountList) { if (!accountPool.length) { return null; } const index = normalizeIpProxyCurrentIndex(runtime.index, 0) % accountPool.length; return accountPool[index]; } if (accountPool.length) { const index = normalizeIpProxyCurrentIndex(runtime.index, 0) % accountPool.length; return accountPool[index]; } const host = String(state?.ipProxyHost || '').trim(); const port = normalizeIpProxyPort(state?.ipProxyPort); if (!host || !port) return null; const entry = { host, port, username: String(state?.ipProxyUsername || '').trim(), password: String(state?.ipProxyPassword || ''), protocol: normalizeIpProxyProtocol(state?.ipProxyProtocol), region: '', provider, }; const inferredRegionFromUsername = inferRegionFromProxyUsername(entry.username); if (inferredRegionFromUsername) { entry.region = inferredRegionFromUsername; } else { const inferredRegionFromHost = inferRegionFromProxyHost(entry.host); if (inferredRegionFromHost) { entry.region = inferredRegionFromHost; } } const configuredRegion = String(state?.ipProxyRegion || '').trim(); if (shouldApplyConfiguredRegionFallbackToEntry(entry, { provider, configuredRegion, hasAccountList: false, })) { entry.region = configuredRegion; } return entry; } const pool = runtime.pool; if (pool.length) { const index = normalizeIpProxyCurrentIndex(runtime.index, 0) % pool.length; return pool[index]; } const fallback = normalizeProxyPoolEntries(runtime.current ? [runtime.current] : [], provider)[0]; return fallback || null; } function buildIpProxyRoutingStatePatch(status = {}) { const provider = normalizeIpProxyProviderValue(status?.provider || DEFAULT_IP_PROXY_SERVICE); const host = String(status?.host || '').trim(); const port = normalizeIpProxyPort(status?.port); const region = String(status?.region || '').trim(); const hasAuth = Boolean(status?.hasAuth); const applied = Boolean(status?.applied); const reason = String(status?.reason || (applied ? 'applied' : 'disabled')).trim().toLowerCase(); const error = String(status?.error || '').trim(); const warning = String(status?.warning || '').trim(); const exitIp = String(status?.exitIp || '').trim(); const exitRegion = String(status?.exitRegion || '').trim(); const exitDetecting = Boolean(status?.exitDetecting); const exitError = String(status?.exitError || '').trim(); const exitSource = String(status?.exitSource || '').trim().toLowerCase(); const exitEndpoint = String(status?.exitEndpoint || status?.endpoint || '').trim(); return { ipProxyApplied: applied, ipProxyAppliedReason: reason, ipProxyAppliedAt: Date.now(), ipProxyAppliedHost: host, ipProxyAppliedPort: port, ipProxyAppliedRegion: region, ipProxyAppliedHasAuth: hasAuth, ipProxyAppliedProvider: provider, ipProxyAppliedError: error, ipProxyAppliedWarning: warning, ipProxyAppliedExitIp: exitIp, ipProxyAppliedExitRegion: exitRegion, ipProxyAppliedExitDetecting: exitDetecting, ipProxyAppliedExitError: exitError, ipProxyAppliedExitSource: exitSource, ipProxyAppliedExitEndpoint: exitEndpoint, }; } async function setIpProxyLeakGuardEnabled(enabled) { if (!chrome.declarativeNetRequest?.updateDynamicRules) { return; } const removeRuleIds = [IP_PROXY_GUARD_BLOCK_RULE_ID]; const addRules = enabled ? [{ id: IP_PROXY_GUARD_BLOCK_RULE_ID, priority: 100, action: { type: 'block', }, condition: { regexFilter: IP_PROXY_GUARD_REGEX, resourceTypes: [ 'main_frame', 'sub_frame', 'xmlhttprequest', 'websocket', 'other', ], }, }] : []; await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds, addRules, }).catch(() => { }); } function shouldEnableIpProxyLeakGuardForStatus(status = {}) { if (!status?.enabled) { return false; } if (status?.applied) { return false; } const reason = String(status?.reason || '').trim().toLowerCase(); // connectivity_failed 表示 PAC/代理接管已经下发,只是探测或目标站点失败。 // 此时继续启用 DNR 会把 chatgpt.com 变成 ERR_BLOCKED_BY_CLIENT,掩盖真实代理链路结果。 return reason !== 'connectivity_failed'; } async function syncIpProxyLeakGuardForStatus(status = {}) { await setIpProxyLeakGuardEnabled(shouldEnableIpProxyLeakGuardForStatus(status)); } async function fetchWithTimeout(url, options = {}, timeoutMs = IP_PROXY_FETCH_TIMEOUT_MS) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || IP_PROXY_FETCH_TIMEOUT_MS)); try { return await fetch(url, { ...options, signal: controller.signal, }); } finally { clearTimeout(timer); } } function extractIpv4FromText(value = '') { const text = String(value || '').trim(); if (!text) { return ''; } const ipv4Match = text.match(/\b(\d{1,3}(?:\.\d{1,3}){3})\b/); if (ipv4Match) { return ipv4Match[1]; } const ipv6Match = text.match(/\b([0-9a-f]{0,4}(?::[0-9a-f]{0,4}){2,7})\b/i); return ipv6Match ? String(ipv6Match[1] || '').trim() : ''; } function normalizeProbeRegionCode(value = '') { const code = String(value || '') .trim() .toUpperCase() .replace(/[^A-Z]/g, ''); return /^[A-Z]{2}$/.test(code) ? code : ''; } function pickProbeRegion(parsed = {}) { const countryCodeCandidates = [ parsed?.countryCode, parsed?.country_code, parsed?.countrycode, parsed?.countryAlpha2, parsed?.country_alpha2, parsed?.countryISO, parsed?.countryIso, parsed?.iso_country, parsed?.geo?.countryCode, parsed?.geo?.country_code, parsed?.location?.countryCode, parsed?.location?.country_code, ]; for (const candidate of countryCodeCandidates) { const code = normalizeProbeRegionCode(candidate); if (code) { return code; } } const fallbackCandidates = [ parsed?.regionCode, parsed?.region_code, parsed?.country, parsed?.country_name, parsed?.countryName, parsed?.regionName, parsed?.region, parsed?.city, ]; for (const candidate of fallbackCandidates) { const text = String(candidate || '').trim(); if (text) { return text; } } return ''; } function parseProxyExitProbePayload(rawText = '', contentType = '') { const text = String(rawText || '').trim(); if (!text) { return { ip: '', region: '' }; } if (/^[-_a-zA-Z0-9]+=/m.test(text)) { const kv = {}; text.split(/\r?\n+/).forEach((line) => { const index = line.indexOf('='); if (index <= 0) return; const key = String(line.slice(0, index) || '').trim().toLowerCase(); if (!key) return; kv[key] = String(line.slice(index + 1) || '').trim(); }); const kvIp = extractIpv4FromText( kv.ip || kv.query || kv.clientip || kv.client_ip || '' ); const kvRegion = normalizeProbeRegionCode(kv.loc || kv.country_code || kv.countrycode || ''); if (kvIp) { return { ip: kvIp, region: kvRegion, }; } } const normalizedContentType = String(contentType || '').toLowerCase(); if (normalizedContentType.includes('application/json') || text.startsWith('{') || text.startsWith('[')) { try { const parsed = JSON.parse(text); const ip = extractIpv4FromText( parsed?.ip || parsed?.query || parsed?.origin || parsed?.address || parsed?.ipAddress || parsed?.ip_address || '' ); const region = pickProbeRegion(parsed); if (ip) { return { ip, region }; } } catch { // fall through to regex parse } } const ip = extractIpv4FromText(text); if (!ip) { return { ip: '', region: '' }; } return { ip, region: '', }; } function normalizeRegionToken(value = '') { return String(value || '') .trim() .toUpperCase() .replace(/[^A-Z0-9]/g, ''); } function hasProxyExitRegionMismatch(expectedRegion = '', detectedRegion = '') { const expected = normalizeRegionToken(expectedRegion); const detected = normalizeRegionToken(detectedRegion); if (!expected || !detected) { return false; } if (expected === detected) { return false; } if (expected.length >= 2 && detected.includes(expected)) { return false; } if (detected.length >= 2 && expected.includes(detected)) { return false; } return true; } function has711SessionToken(username = '') { const text = String(username || '').trim(); if (!text) { return false; } return /(?:^|[-_])session[-_:][A-Za-z0-9_-]+\b/i.test(text); } function resolveExitProbeEndpoints(options = {}) { const provider = normalizeIpProxyProviderValue(options?.provider || ''); const username = String(options?.username || '').trim(); if (provider === '711proxy' && has711SessionToken(username)) { return IP_PROXY_EXIT_PROBE_ENDPOINTS_711_STICKY.slice(); } return IP_PROXY_EXIT_PROBE_ENDPOINTS.slice(); } function resolveTargetReachabilityEndpoints(options = {}) { const configured = Array.isArray(options?.targetReachabilityEndpoints) ? options.targetReachabilityEndpoints.map((item) => String(item || '').trim()).filter(Boolean) : []; return configured.length ? configured : IP_PROXY_TARGET_REACHABILITY_ENDPOINTS.slice(); } function applyExitRegionExpectation(status = {}, expectedRegion = '') { const exitIp = String(status?.exitIp || '').trim(); const exitError = String(status?.exitError || '').trim(); if (!exitIp) { const hasBackgroundAbortStorm = /probe:bg:/.test(exitError) && /signal is aborted without reason/i.test(exitError); const hasBackgroundAbortFallback = /probe:bg:abort_storm_page_fallback/i.test(exitError); const hasProxyRuntimeError = /probe:proxy_error\(/i.test(exitError); const hasAuth = Boolean(status?.hasAuth); const challengeMatch = exitError.match(/challenge=(\d+)/i); const providedMatch = exitError.match(/provided=(\d+)/i); const challengeCount = Number.parseInt(String(challengeMatch?.[1] || ''), 10) || 0; const providedCount = Number.parseInt(String(providedMatch?.[1] || ''), 10) || 0; const missingProxyChallenge = hasAuth && challengeCount <= 0 && providedCount <= 0; const hasErrorPageFailure = /Frame with ID 0 is showing error page|Cannot access contents of the page/i.test(exitError); const netErrorCode = exitError.match(/net::ERR_[A-Z_]+/i)?.[0] || ''; const navigationHint = netErrorCode ? `导航错误:${netErrorCode}。` : ''; const tunnelFailedWithoutChallenge = missingProxyChallenge && /ERR_TUNNEL_CONNECTION_FAILED/i.test(netErrorCode || exitError); const humanizedError = hasErrorPageFailure ? ( hasAuth ? '探测页已进入浏览器错误页(常见于代理鉴权失败、代理节点不可用或网络被拦截)。请先检查代理账号/密码与节点可用性,再重试检测。' : '探测页已进入浏览器错误页(常见于代理节点不可用、代理协议不匹配或网络被拦截)。请先检查代理节点、端口与协议配置,再重试检测。' ) : ''; const authDiagnostics = exitError.match(/probe:auth\([^)]+\)/i)?.[0] || ''; const challengeHint = missingProxyChallenge ? '当前链路未收到 407 代理鉴权挑战(可能是代理端返回非标准拒绝,例如 630 no Proxy-Authorization),浏览器无法触发扩展鉴权回填。建议切换 API 模式,或更换会返回标准 407 的账号节点。' : ''; const compatibilityHint = tunnelFailedWithoutChallenge ? '当前账号模式节点可能采用非标准代理鉴权链路(未发起 407 挑战),该链路在 Chrome 扩展代理 API 下无法自动回填凭据。' : ''; const mergedErrorParts = [ hasBackgroundAbortStorm ? ( hasBackgroundAbortFallback ? '后台探测请求连续超时中断(代理隧道未在时限内建立),且已自动回退页面探测但仍未拿到可用出口。' : '后台探测请求连续超时中断(代理隧道未在时限内建立)。这通常是代理节点链路不可用或被上游丢弃,并非探测页面本身异常。' ) : '', hasProxyRuntimeError ? '已捕获浏览器代理栈错误事件(chrome.proxy.onProxyError),请优先依据诊断中的 proxy_error 字段排查节点/协议兼容性。' : '', humanizedError, navigationHint, compatibilityHint, challengeHint, ].filter(Boolean); const mergedError = mergedErrorParts.length ? `${mergedErrorParts.join(' ')}${authDiagnostics ? ` 诊断:${authDiagnostics}` : ''}` : ''; const mergedWithDiagnostics = mergedError ? ( exitError && !mergedError.includes(exitError) ? `${mergedError} 诊断:${exitError}` : mergedError ) : ''; return { ...status, applied: false, reason: 'connectivity_failed', error: mergedWithDiagnostics || exitError || '未检测到代理出口 IP,无法确认代理已生效。', }; } const expected = String(expectedRegion || '').trim(); const entrySource = String(status?.entrySource || '').trim(); const normalizedProvider = normalizeIpProxyProviderValue(status?.provider || DEFAULT_IP_PROXY_SERVICE); const authSummary = String(status?.authDiagnostics || '').trim(); const parsedAuthSummary = parseIpProxyAuthDiagnosticsSummary(authSummary); const missingAuthChallenge = Boolean(status?.hasAuth) && parsedAuthSummary && parsedAuthSummary.challenge <= 0 && parsedAuthSummary.provided <= 0; if (!expected) { return status; } const detected = String(status?.exitRegion || '').trim(); if (!detected) { return { ...status, applied: false, reason: 'connectivity_failed', error: `已检测到出口 IP ${exitIp},但地区探测未返回国家/地区代码,暂无法校验期望地区 ${expected}。`, }; } if (!hasProxyExitRegionMismatch(expected, detected)) { return status; } const sourceHint = entrySource === 'account_list' ? '(来源:账号列表)' : (entrySource === 'fixed_account' ? '(来源:固定账号)' : ''); const usernameRegion = inferRegionFromProxyUsername(status?.username || ''); const usernameHint = usernameRegion ? ( hasProxyExitRegionMismatch(expected, usernameRegion) ? ` 当前账号用户名地区标记为 ${usernameRegion},与期望不一致。` : ` 当前账号用户名地区标记为 ${usernameRegion}。` ) : ''; const missingAuthChallengeHint = Boolean(status?.hasAuth) && parsedAuthSummary && parsedAuthSummary.challenge <= 0 && parsedAuthSummary.provided <= 0 ? ' 当前链路未触发代理鉴权挑战(407),可能复用了历史代理连接/鉴权缓存,或代理端未返回标准 407 导致账号参数未被浏览器回填(可能走匿名链路)。可先关闭再开启代理,或切换到下一条节点后重试。' : ''; const authHint = authSummary ? ` 诊断:probe:${authSummary}` : ''; const mismatchMessage = `代理出口地区与预期不一致:期望 ${expected},实际 ${detected || 'unknown'}${sourceHint}。${usernameHint}${missingAuthChallengeHint}${authHint}`.trim(); if (normalizedProvider === '711proxy') { const warningPrefix = missingAuthChallenge ? '地区校验未通过且未触发代理鉴权挑战,疑似匿名链路;先保留代理接管并给出强告警:' : '地区校验未通过,但已保留代理接管:'; return { ...status, applied: true, reason: 'applied_with_warning', warning: `${warningPrefix}${mismatchMessage}`, error: '', }; } return { ...status, applied: false, reason: 'connectivity_failed', error: mismatchMessage, }; } function applyExitBaselineExpectation(status = {}) { const exitIp = String(status?.exitIp || '').trim(); const baselineIp = String(status?.exitBaselineIp || '').trim(); const exitSource = String(status?.exitSource || '').trim().toLowerCase(); if (exitIp && exitSource.includes('background') && !baselineIp) { return { ...status, applied: false, reason: 'connectivity_failed', error: `已检测到出口 IP ${exitIp},但系统基线网络探测失败,无法确认是否经过插件代理链路。`, }; } if (!exitIp || !baselineIp) { return status; } if (exitIp !== baselineIp) { return status; } return { ...status, applied: false, reason: 'connectivity_failed', error: `检测到出口 IP ${exitIp} 与系统基线网络一致,疑似未经过插件代理链路。`, }; } function shouldVerifyIpProxyTargetReachability(status = {}) { if (!String(status?.exitIp || '').trim()) { return false; } if (status?.applied === false && String(status?.reason || '').trim().toLowerCase() === 'connectivity_failed') { return false; } return true; } function buildTargetReachabilityFailureMessage(status = {}, reachability = {}) { const exitIp = String(status?.exitIp || '').trim(); const exitRegion = String(status?.exitRegion || '').trim(); const endpoint = String(reachability?.endpoint || reachability?.url || IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0] || '').trim(); const targetHost = extractProbeHostFromTabUrl(endpoint) || endpoint || 'chatgpt.com'; const diagnostic = String(reachability?.error || reachability?.diagnostics || '').trim(); const diagnosticSuffix = diagnostic ? ` 诊断:${diagnostic}` : ''; const exitRegionSuffix = exitRegion ? ` [${exitRegion}]` : ''; return `已检测到出口 IP ${exitIp}${exitRegionSuffix},但真实目标 ${targetHost} 不可达。` + `这说明代理只通过了 IP 探测,无法打开步骤 1 的 ChatGPT 页面;请更换支持 chatgpt.com CONNECT/TLS 的节点。` + diagnosticSuffix; } function applyTargetReachabilityExpectation(status = {}, reachability = {}) { if (!shouldVerifyIpProxyTargetReachability(status)) { return status; } if (reachability?.reachable === true) { return status; } if (reachability?.skipped === true) { return status; } return { ...status, applied: false, reason: 'connectivity_failed', warning: '', error: buildTargetReachabilityFailureMessage(status, reachability), }; } async function detectProxyExitInfoByBackgroundFetch(options = {}) { const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000; const errors = Array.isArray(options?.errors) ? options.errors : []; const configuredEndpoints = Array.isArray(options?.probeEndpoints) ? options.probeEndpoints.map((item) => String(item || '').trim()).filter(Boolean) : []; const availableEndpoints = configuredEndpoints.length ? configuredEndpoints : IP_PROXY_EXIT_PROBE_ENDPOINTS; const requestedPerEndpointTimeoutMs = Number(options?.backgroundPerEndpointTimeoutMs); const perRequestTimeoutMs = Math.max( 1200, Math.min( Number.isFinite(requestedPerEndpointTimeoutMs) && requestedPerEndpointTimeoutMs > 0 ? requestedPerEndpointTimeoutMs : IP_PROXY_BACKGROUND_PROBE_PER_ENDPOINT_TIMEOUT_MS, timeoutMs ) ); const maxEndpoints = Math.max( 1, Math.min( availableEndpoints.length, Number.isFinite(Number(options?.backgroundMaxEndpoints)) ? Number(options.backgroundMaxEndpoints) : IP_PROXY_BACKGROUND_PROBE_MAX_ENDPOINTS ) ); const probeEndpoints = availableEndpoints.slice(0, maxEndpoints); let baselineIp = ''; try { const baselineUrl = `${IP_PROXY_PAGE_CONTEXT_BASELINE_URL}?_multipage_proxy_baseline=${Date.now()}`; const baselineResponse = await fetchWithTimeout(baselineUrl, { method: 'GET', cache: 'no-store', headers: { Accept: 'application/json, text/plain, */*' }, }, perRequestTimeoutMs); if (!baselineResponse.ok) { errors.push(`probe:bg:baseline:status:${baselineResponse.status}`); } else { const baselineText = await baselineResponse.text(); const parsedBaseline = parseProxyExitProbePayload( baselineText, baselineResponse.headers.get('content-type') ); baselineIp = String(parsedBaseline?.ip || '').trim(); if (!baselineIp) { errors.push('probe:bg:baseline:empty_parse'); } } } catch (baselineError) { errors.push(`probe:bg:baseline:${baselineError?.message || baselineError}`); } const runProbePass = async (endpoints, requestTimeoutMs, diagnosticPrefix = 'probe:bg') => { let bestEffortResult = null; let abortLikeCount = 0; for (const endpoint of endpoints) { try { const url = endpoint.includes('?') ? `${endpoint}&_t=${Date.now()}` : `${endpoint}?_t=${Date.now()}`; const response = await fetchWithTimeout(url, { method: 'GET', cache: 'no-store', headers: { Accept: 'application/json, text/plain, */*' }, }, requestTimeoutMs); if (!response.ok) { errors.push(`${diagnosticPrefix}:${endpoint}:status:${response.status}`); continue; } const text = await response.text(); const parsed = parseProxyExitProbePayload(text, response.headers.get('content-type')); if (parsed.ip) { const result = { ip: parsed.ip, region: parsed.region, source: 'background_fallback', endpoint, baselineIp, }; if (parsed.region) { return { result, bestEffortResult: result, abortLikeCount, }; } if (!bestEffortResult) { bestEffortResult = result; } errors.push(`${diagnosticPrefix}:${endpoint}:missing_region`); continue; } errors.push(`${diagnosticPrefix}:${endpoint}:empty_parse`); } catch (error) { const message = String(error?.message || error || '').trim(); if (/signal is aborted without reason|abort/i.test(message)) { abortLikeCount += 1; } errors.push(`${diagnosticPrefix}:${endpoint}:${message}`); } } return { result: bestEffortResult, bestEffortResult, abortLikeCount, }; }; const firstPass = await runProbePass(probeEndpoints, perRequestTimeoutMs, 'probe:bg'); if (firstPass?.result?.ip && firstPass?.result?.region) { return firstPass.result; } if (!firstPass?.result?.ip && firstPass?.abortLikeCount >= Math.min(2, probeEndpoints.length) && options?.backgroundSlowRetry !== false) { const retryTimeoutMs = Math.max(perRequestTimeoutMs + 2500, 7000); const retryEndpoints = probeEndpoints.slice(0, Math.min(2, probeEndpoints.length)); errors.push(`probe:bg:slow_retry:${retryTimeoutMs}ms`); const retryPass = await runProbePass(retryEndpoints, retryTimeoutMs, 'probe:bg:retry'); if (retryPass?.result?.ip) { return retryPass.result; } } if (!firstPass?.bestEffortResult?.ip) { appendIpProxyRuntimeErrorDiagnosticsToErrors(errors, 45000); } return firstPass?.bestEffortResult || { ip: '', region: '', source: 'background_fallback', baselineIp }; } function isPageContextProbeHost(hostname = '') { const normalized = String(hostname || '').trim().toLowerCase(); return normalized === 'chatgpt.com' || normalized.endsWith('.chatgpt.com') || normalized === 'openai.com' || normalized.endsWith('.openai.com') || normalized === 'example.com' || normalized.endsWith('.example.com') || normalized === 'ip-api.com' || normalized.endsWith('.ip-api.com') || normalized === 'httpbin.org' || normalized === 'ipwho.is' || normalized.endsWith('.ipwho.is') || normalized === 'ipinfo.io' || normalized.endsWith('.ipinfo.io') || normalized === 'ipapi.co' || normalized.endsWith('.ipapi.co'); } function isProbeErrorPageExecutionError(error) { const message = String(error?.message || error || ''); return /Frame with ID 0 is showing error page|Cannot access contents of the page/i.test(message); } function hasProbeErrorPageDiagnostics(errors = []) { if (!Array.isArray(errors) || !errors.length) { return false; } return errors.some((item) => isProbeErrorPageExecutionError(String(item || ''))); } function createProbeNavigationErrorTracker(initialTabId = null) { if (!chrome.webNavigation?.onErrorOccurred?.addListener) { return { setTabId: () => {}, appendDiagnostics: () => {}, dispose: () => {}, }; } let activeTabId = Number.isInteger(initialTabId) ? Number(initialTabId) : null; const entries = []; const listener = (details = {}) => { if (!Number.isInteger(activeTabId) || Number(details?.tabId) !== activeTabId) { return; } if (Number(details?.frameId) !== 0) { return; } const errorCode = String(details?.error || '').trim(); if (!errorCode) { return; } const url = String(details?.url || '').trim(); entries.push({ error: errorCode, host: extractProbeHostFromTabUrl(url), url, }); }; try { chrome.webNavigation.onErrorOccurred.addListener(listener); } catch { return { setTabId: () => {}, appendDiagnostics: () => {}, dispose: () => {}, }; } return { setTabId(nextTabId) { activeTabId = Number.isInteger(nextTabId) ? Number(nextTabId) : null; }, appendDiagnostics(errors, maxItems = 2) { if (!Array.isArray(errors) || !entries.length) { return; } const limit = Math.max(1, Number(maxItems) || 2); const recent = entries.slice(-limit); for (const item of recent) { const errorCode = String(item?.error || '').trim(); if (!errorCode) { continue; } const host = String(item?.host || '').trim() || 'unknown'; errors.push(`probe:page_context:navigation_error:${errorCode}@${host}`); } }, dispose() { try { chrome.webNavigation.onErrorOccurred.removeListener(listener); } catch { // ignore remove listener failures } }, }; } function extractProbeHostFromTabUrl(rawUrl = '') { try { const parsed = new URL(rawUrl); if (!['http:', 'https:'].includes(parsed.protocol)) { return ''; } return parsed.hostname || ''; } catch { return ''; } } async function pickExistingPageContextProbeTabId(options = {}) { const tabs = await queryAutomationScopedTabs({}, options); const candidates = tabs .filter((tab) => Number.isInteger(tab?.id)) .filter((tab) => isPageContextProbeHost(extractProbeHostFromTabUrl(tab?.url || ''))); if (!candidates.length) { return null; } const preferred = candidates.find((tab) => String(tab?.status || '').toLowerCase() === 'complete'); return Number((preferred || candidates[0]).id) || null; } async function waitForPageContextProbeTabReady(tabId, timeoutMs = 15000) { const timeout = Math.max(1500, Number(timeoutMs) || 15000); if (typeof waitForTabComplete === 'function') { const result = await waitForTabComplete(tabId, { timeoutMs: timeout, retryDelayMs: 300, }); return Boolean(result?.id); } const start = Date.now(); while (Date.now() - start < timeout) { try { const tab = await chrome.tabs.get(tabId); if (String(tab?.status || '').toLowerCase() === 'complete') { return true; } } catch { return false; } await new Promise((resolve) => setTimeout(resolve, 250)); } return false; } async function readExitProbeFromTabDocument(tabId) { const executionResults = await chrome.scripting.executeScript({ target: { tabId }, world: 'ISOLATED', func: () => { const preText = String(document.querySelector('pre')?.textContent || '').trim(); const bodyText = String(document.body?.innerText || document.documentElement?.innerText || '').trim(); return { url: String(location.href || ''), text: preText || bodyText, }; }, }); return executionResults?.[0]?.result || null; } function appendProbeCacheBuster(rawUrl = '', key = '_multipage_proxy_probe') { const text = String(rawUrl || '').trim(); if (!text) { return ''; } try { const parsed = new URL(text); parsed.searchParams.set(key, String(Date.now())); return parsed.toString(); } catch { const separator = text.includes('?') ? '&' : '?'; return `${text}${separator}${key}=${Date.now()}`; } } async function readTargetReachabilityFromTabDocument(tabId) { const executionResults = await chrome.scripting.executeScript({ target: { tabId }, world: 'ISOLATED', func: () => ({ href: String(location.href || ''), title: String(document.title || ''), readyState: String(document.readyState || ''), bodyLength: Number(document.body?.innerText?.length || document.documentElement?.innerText?.length || 0), }), }); return executionResults?.[0]?.result || null; } async function probeExitInfoByTabNavigation(tabId, timeoutMs = 10000, errors = []) { return probeExitInfoByTabNavigationWithEndpoints(tabId, timeoutMs, errors, []); } async function probeExitInfoByTabNavigationWithEndpoints(tabId, timeoutMs = 10000, errors = [], endpointsOverride = []) { if (!Number.isInteger(tabId)) { return null; } const configuredEndpoints = Array.isArray(endpointsOverride) ? endpointsOverride.map((item) => String(item || '').trim()).filter(Boolean) : []; const availableEndpoints = configuredEndpoints.length ? configuredEndpoints : IP_PROXY_EXIT_PROBE_ENDPOINTS; const endpoints = availableEndpoints.slice(0, 6); const perEndpointTimeoutMs = Math.max( 2000, Math.min(IP_PROXY_PAGE_CONTEXT_READY_TIMEOUT_MS, Number(timeoutMs) || 10000) ); let bestEffortResult = null; for (const endpoint of endpoints) { const endpointText = String(endpoint || '').trim(); if (!endpointText) { continue; } const probeTargetUrl = endpointText.includes('?') ? `${endpointText}&_multipage_proxy_probe=${Date.now()}` : `${endpointText}?_multipage_proxy_probe=${Date.now()}`; try { await chrome.tabs.update(tabId, { url: probeTargetUrl, active: false, }); const ready = await waitForPageContextProbeTabReady(tabId, perEndpointTimeoutMs); if (!ready) { if (Array.isArray(errors)) { errors.push(`probe:page_context:navigation:${endpointText}:not_ready`); } continue; } const documentResult = await readExitProbeFromTabDocument(tabId); const parsed = parseProxyExitProbePayload(String(documentResult?.text || ''), 'application/json'); if (parsed?.ip) { const result = { ip: String(parsed.ip || '').trim(), region: String(parsed.region || '').trim(), endpoint: String(documentResult?.url || probeTargetUrl || endpointText).trim(), source: 'page_context_navigation', }; if (result.region) { return result; } if (!bestEffortResult) { bestEffortResult = result; } if (Array.isArray(errors)) { errors.push(`probe:page_context:navigation:${endpointText}:missing_region`); } continue; } if (Array.isArray(errors)) { errors.push(`probe:page_context:navigation:${endpointText}:empty_parse`); } } catch (error) { if (Array.isArray(errors)) { errors.push(`probe:page_context:navigation:${endpointText}:${error?.message || error}`); } } } return bestEffortResult; } async function probeExitInfoViaExecuteScript(tabId, timeoutMs = 10000, endpointsOverride = []) { const perRequestTimeoutMs = Math.max( 1200, Math.min(IP_PROXY_BACKGROUND_PROBE_PER_ENDPOINT_TIMEOUT_MS, Number(timeoutMs) || 10000) ); const configuredEndpoints = Array.isArray(endpointsOverride) ? endpointsOverride.map((item) => String(item || '').trim()).filter(Boolean) : []; const availableEndpoints = configuredEndpoints.length ? configuredEndpoints : IP_PROXY_EXIT_PROBE_ENDPOINTS; const endpoints = availableEndpoints.slice(0, IP_PROXY_BACKGROUND_PROBE_MAX_ENDPOINTS); const executionResults = await chrome.scripting.executeScript({ target: { tabId }, world: 'ISOLATED', func: async (payload) => { const extractIpv4 = (value) => { const text = String(value || '').trim(); if (!text) return ''; const match = text.match(/\b(\d{1,3}(?:\.\d{1,3}){3})\b/); return match ? match[1] : ''; }; const normalizeRegionCode = (value) => { const code = String(value || '') .trim() .toUpperCase() .replace(/[^A-Z]/g, ''); return /^[A-Z]{2}$/.test(code) ? code : ''; }; const pickRegion = (parsed) => { const codeCandidates = [ parsed?.countryCode, parsed?.country_code, parsed?.countrycode, parsed?.countryAlpha2, parsed?.country_alpha2, parsed?.countryISO, parsed?.countryIso, parsed?.iso_country, parsed?.geo?.countryCode, parsed?.geo?.country_code, parsed?.location?.countryCode, parsed?.location?.country_code, ]; for (const candidate of codeCandidates) { const code = normalizeRegionCode(candidate); if (code) return code; } const fallbackCandidates = [ parsed?.regionCode, parsed?.region_code, parsed?.country, parsed?.country_name, parsed?.countryName, parsed?.regionName, parsed?.region, parsed?.city, ]; for (const candidate of fallbackCandidates) { const text = String(candidate || '').trim(); if (text) return text; } return ''; }; const parsePayload = (rawText, contentType) => { const text = String(rawText || '').trim(); if (!text) { return { ip: '', region: '' }; } const normalizedContentType = String(contentType || '').toLowerCase(); if (normalizedContentType.includes('application/json') || text.startsWith('{') || text.startsWith('[')) { try { const parsed = JSON.parse(text); const ip = extractIpv4( parsed?.ip || parsed?.query || parsed?.origin || parsed?.address || parsed?.ipAddress || parsed?.ip_address || '' ); const region = pickRegion(parsed); if (ip) { return { ip, region }; } } catch { // fall through to regex parse } } const ipMatch = extractIpv4(text); return { ip: ipMatch || '', region: '', }; }; const fetchWithTimeout = async (url, timeoutValue) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutValue) || 10000)); try { return await fetch(url, { method: 'GET', cache: 'no-store', credentials: 'omit', headers: { Accept: 'application/json, text/plain, */*', }, signal: controller.signal, }); } finally { clearTimeout(timer); } }; const endpoints = Array.isArray(payload?.endpoints) ? payload.endpoints : []; const normalizedEndpoints = []; const endpointSet = new Set(); for (const endpoint of endpoints) { const text = String(endpoint || '').trim(); if (!text || !/^https?:\/\//i.test(text) || endpointSet.has(text)) { continue; } endpointSet.add(text); normalizedEndpoints.push(text); } const timeoutValue = Math.max(1000, Number(payload?.timeoutMs) || 10000); const diagnostics = []; let bestEffortResult = null; for (const endpoint of normalizedEndpoints.slice(0, 8)) { try { const url = endpoint.includes('?') ? `${endpoint}&_t=${Date.now()}` : `${endpoint}?_t=${Date.now()}`; const response = await fetchWithTimeout(url, timeoutValue); if (!response.ok) { diagnostics.push(`${endpoint}:status:${response.status}`); continue; } const text = await response.text(); const parsed = parsePayload(text, response.headers.get('content-type')); if (parsed.ip) { const result = { ip: parsed.ip, region: parsed.region, endpoint, diagnostics, }; if (parsed.region) { return result; } if (!bestEffortResult) { bestEffortResult = result; } diagnostics.push(`${endpoint}:missing_region`); continue; } diagnostics.push(`${endpoint}:empty_parse`); } catch (error) { diagnostics.push(`${endpoint}:${error?.message || error}`); } } return bestEffortResult || { ip: '', region: '', endpoint: '', diagnostics, }; }, args: [{ endpoints, timeoutMs: perRequestTimeoutMs, }], }); return executionResults?.[0]?.result || null; } async function detectIpProxyTargetReachabilityByPageContext(options = {}) { const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS; const errors = Array.isArray(options?.errors) ? options.errors : []; const endpoints = resolveTargetReachabilityEndpoints(options); if (!endpoints.length) { return { reachable: true, skipped: true, source: 'target_page_context', endpoint: '' }; } if (!chrome.tabs?.create || !chrome.tabs?.update || !chrome.scripting?.executeScript) { errors.push('target:page_context:unavailable'); return { reachable: false, endpoint: endpoints[0], source: 'target_page_context_unavailable', error: 'target:page_context:unavailable', }; } let tabId = null; let createdTabId = null; const navigationErrorTracker = createProbeNavigationErrorTracker(null); const perEndpointTimeoutMs = Math.max(2500, Math.min(IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS, timeoutMs)); try { for (let index = 0; index < endpoints.length; index += 1) { const endpoint = String(endpoints[index] || '').trim(); if (!endpoint) { continue; } const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target'); try { if (!Number.isInteger(tabId)) { const tab = await createAutomationScopedTab({ url: targetUrl, active: false, }, options); tabId = Number(tab?.id) || null; createdTabId = tabId; navigationErrorTracker.setTabId(tabId); } else { await chrome.tabs.update(tabId, { url: targetUrl, active: false, }); } if (!Number.isInteger(tabId)) { errors.push(`target:page_context:${endpoint}:no_tab`); continue; } const ready = await waitForPageContextProbeTabReady(tabId, perEndpointTimeoutMs); if (!ready) { errors.push(`target:page_context:${endpoint}:not_ready`); continue; } const documentResult = await readTargetReachabilityFromTabDocument(tabId); const href = String(documentResult?.href || '').trim(); const host = extractProbeHostFromTabUrl(href); const expectedHost = extractProbeHostFromTabUrl(endpoint); if (host && expectedHost && (host === expectedHost || host.endsWith(`.${expectedHost}`))) { return { reachable: true, endpoint, url: href, source: 'target_page_context', }; } errors.push(`target:page_context:${endpoint}:unexpected_url:${href || 'unknown'}`); } catch (error) { errors.push(`target:page_context:${endpoint}:${error?.message || error}`); } } } finally { navigationErrorTracker.appendDiagnostics(errors, 3); navigationErrorTracker.dispose(); if (Number.isInteger(createdTabId)) { try { await chrome.tabs.remove(createdTabId); } catch { // ignore tab close failures } } } return { reachable: false, endpoint: endpoints[0], source: 'target_page_context', error: buildProbeDiagnosticsSummary(errors, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS), }; } async function detectProxyExitInfoByPageContext(options = {}) { const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000; const errors = Array.isArray(options?.errors) ? options.errors : []; const probeEndpoints = Array.isArray(options?.probeEndpoints) ? options.probeEndpoints.map((item) => String(item || '').trim()).filter(Boolean) : []; let sawProbeErrorPage = false; if (!chrome.scripting?.executeScript) { errors.push('probe:page_context:scripting_unavailable'); return { ip: '', region: '', source: 'page_context_unavailable' }; } let probeTabId = null; let createdProbeTabId = null; const probeUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}`; try { const tab = await createAutomationScopedTab({ url: probeUrl, active: false, }, options); probeTabId = Number(tab?.id) || null; createdProbeTabId = probeTabId; } catch (error) { errors.push(`probe:page_context:create_probe_tab_failed:${error?.message || error}`); probeTabId = await pickExistingPageContextProbeTabId(options); } if (!Number.isInteger(probeTabId)) { errors.push('probe:page_context:no_probe_tab'); return { ip: '', region: '', source: 'page_context_unavailable' }; } const navigationErrorTracker = createProbeNavigationErrorTracker(probeTabId); let hasSuccessfulIp = false; try { const ready = await waitForPageContextProbeTabReady( probeTabId, Math.max(2500, Math.min(IP_PROXY_PAGE_CONTEXT_READY_TIMEOUT_MS, timeoutMs + 2500)) ); if (!ready) { errors.push('probe:page_context:probe_tab_not_ready'); return { ip: '', region: '', source: 'page_context_unavailable' }; } let ip = ''; let region = ''; let endpoint = ''; let canUseDocumentProbe = false; try { const probeTab = await chrome.tabs.get(probeTabId); const probeHost = extractProbeHostFromTabUrl(probeTab?.url || ''); const expectedHost = extractProbeHostFromTabUrl(probeUrl); canUseDocumentProbe = Boolean(probeHost && expectedHost && probeHost === expectedHost); } catch { canUseDocumentProbe = false; } if (canUseDocumentProbe) { try { const documentResult = await readExitProbeFromTabDocument(probeTabId); const parsedDocument = parseProxyExitProbePayload(String(documentResult?.text || ''), 'application/json'); ip = String(parsedDocument?.ip || '').trim(); region = String(parsedDocument?.region || '').trim(); endpoint = String(documentResult?.url || '').trim(); if (ip) { let baselineIp = ''; try { const baselineUrl = `${IP_PROXY_PAGE_CONTEXT_BASELINE_URL}?_multipage_proxy_baseline=${Date.now()}`; await chrome.tabs.update(probeTabId, { url: baselineUrl, active: false, }); const baselineReady = await waitForPageContextProbeTabReady( probeTabId, Math.max(2000, Math.min(IP_PROXY_PAGE_CONTEXT_BASELINE_TIMEOUT_MS, timeoutMs + 1500)) ); if (baselineReady) { const baselineResult = await readExitProbeFromTabDocument(probeTabId); const parsedBaseline = parseProxyExitProbePayload(String(baselineResult?.text || ''), 'application/json'); baselineIp = String(parsedBaseline?.ip || '').trim(); if (!baselineIp) { errors.push('probe:page_context:baseline_empty_parse'); } } else { errors.push('probe:page_context:baseline_not_ready'); } } catch (baselineError) { errors.push(`probe:page_context:baseline_failed:${baselineError?.message || baselineError}`); } hasSuccessfulIp = true; return { ip, region, source: 'page_context', endpoint: endpoint || probeUrl, baselineIp, }; } errors.push('probe:page_context:document_empty_parse'); } catch (documentError) { if (isProbeErrorPageExecutionError(documentError)) { sawProbeErrorPage = true; } errors.push(`probe:page_context:document_failed:${documentError?.message || documentError}`); } } else { errors.push('probe:page_context:document_probe_skipped'); } const navigationProbeResult = await probeExitInfoByTabNavigationWithEndpoints( probeTabId, timeoutMs, errors, probeEndpoints ); if (navigationProbeResult?.ip) { hasSuccessfulIp = true; return { ip: String(navigationProbeResult.ip || '').trim(), region: String(navigationProbeResult.region || '').trim(), source: 'page_context', endpoint: String(navigationProbeResult.endpoint || '').trim(), }; } let scriptResult = null; try { scriptResult = await probeExitInfoViaExecuteScript(probeTabId, timeoutMs, probeEndpoints); } catch (scriptError) { if (isProbeErrorPageExecutionError(scriptError)) { sawProbeErrorPage = true; } errors.push(`probe:page_context:script_failed:${scriptError?.message || scriptError}`); if (!createdProbeTabId && isProbeErrorPageExecutionError(scriptError)) { const retryUrl = `${IP_PROXY_PAGE_CONTEXT_PROBE_URL}?_multipage_proxy_probe=${Date.now()}&retry=1`; const retryTab = await createAutomationScopedTab({ url: retryUrl, active: false, }, options); const retryTabId = Number(retryTab?.id) || 0; if (retryTabId > 0) { createdProbeTabId = retryTabId; navigationErrorTracker.setTabId(retryTabId); const retryReady = await waitForPageContextProbeTabReady( retryTabId, Math.max(2500, Math.min(IP_PROXY_PAGE_CONTEXT_READY_TIMEOUT_MS, timeoutMs + 2500)) ); if (retryReady) { scriptResult = await probeExitInfoViaExecuteScript(retryTabId, timeoutMs, probeEndpoints); } } } else { throw scriptError; } } if (!scriptResult) { return { ip: '', region: '', source: sawProbeErrorPage ? 'page_context_error_page' : 'page_context_unavailable', }; } ip = String(scriptResult?.ip || '').trim(); region = String(scriptResult?.region || '').trim(); if (ip) { hasSuccessfulIp = true; return { ip, region, source: 'page_context', endpoint: String(scriptResult?.endpoint || '').trim(), }; } if (Array.isArray(scriptResult?.diagnostics) && scriptResult.diagnostics.length) { for (const item of scriptResult.diagnostics.slice(0, 4)) { errors.push(`probe:page_context:script:${String(item)}`); } } else { errors.push('probe:page_context:script_empty'); } } catch (error) { const message = String(error?.message || error || ''); if (isProbeErrorPageExecutionError(error)) { sawProbeErrorPage = true; } if (!/probe:page_context:script_failed/i.test(message)) { errors.push(`probe:page_context:script_failed:${message}`); } } finally { if (!hasSuccessfulIp) { navigationErrorTracker.appendDiagnostics(errors, 2); } navigationErrorTracker.dispose(); if (Number.isInteger(createdProbeTabId)) { try { await chrome.tabs.remove(createdProbeTabId); } catch { // ignore tab close failures } } } return { ip: '', region: '', source: sawProbeErrorPage ? 'page_context_error_page' : 'page_context_unavailable', }; } async function detectProxyExitInfo(options = {}) { const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000; const errors = Array.isArray(options?.errors) ? options.errors : []; const preferPageContext = options?.preferPageContext !== false; const allowBackgroundFallback = options?.allowBackgroundFallback === true; const allowPageContextFallbackOnBackgroundAbort = options?.allowPageContextFallbackOnBackgroundAbort !== false; const probeEndpoints = resolveExitProbeEndpoints({ provider: options?.provider || '', username: options?.username || '', }); if (preferPageContext) { const pageResult = await detectProxyExitInfoByPageContext({ timeoutMs, errors, probeEndpoints, automationWindowId: options?.automationWindowId, windowId: options?.windowId, state: options?.state, }); if (pageResult?.ip) { return pageResult; } const shouldFallbackToBackground = allowBackgroundFallback || String(pageResult?.source || '').trim().toLowerCase() === 'page_context_error_page' || hasProbeErrorPageDiagnostics(errors); if (!shouldFallbackToBackground) { return { ip: '', region: '', source: String(pageResult?.source || 'page_context_unavailable'), }; } if (!allowBackgroundFallback) { errors.push('probe:page_context:error_page_auto_fallback'); } } const backgroundResult = await detectProxyExitInfoByBackgroundFetch({ timeoutMs, errors, probeEndpoints, }); if (!preferPageContext && allowPageContextFallbackOnBackgroundAbort && !backgroundResult?.ip && Array.isArray(errors) && errors.some((item) => /probe:bg:.*signal is aborted without reason/i.test(String(item || '')))) { errors.push('probe:bg:abort_storm_page_fallback'); const pageResult = await detectProxyExitInfoByPageContext({ timeoutMs: Math.max(5000, Math.min(9000, timeoutMs)), errors, probeEndpoints, automationWindowId: options?.automationWindowId, windowId: options?.windowId, state: options?.state, }); errors.push(`probe:bg:abort_storm_page_fallback_result:${String(pageResult?.source || 'unknown')}`); if (pageResult?.ip) { return pageResult; } } return backgroundResult; } async function pullIpProxyPoolFromApi(state = {}, options = {}) { const apiUrl = String(state?.ipProxyApiUrl || '').trim(); if (!apiUrl) { throw new Error('代理 API 不能为空。'); } let parsedUrl; try { parsedUrl = new URL(apiUrl); } catch { throw new Error('代理 API 不是有效 URL。'); } if (!['http:', 'https:'].includes(parsedUrl.protocol)) { throw new Error('代理 API 仅支持 http/https。'); } const provider = normalizeIpProxyProviderValue(state?.ipProxyService); const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : IP_PROXY_FETCH_TIMEOUT_MS; const response = await fetchWithTimeout(apiUrl, { method: 'GET', cache: 'no-store', headers: { Accept: 'application/json, text/plain, */*' }, }, timeoutMs); if (!response.ok) { throw new Error(`代理 API 请求失败(HTTP ${response.status})。`); } const rawText = await response.text(); let payload = rawText; try { payload = JSON.parse(rawText); } catch { payload = rawText; } const pool = normalizeIpProxyListFromPayload(payload, provider); if (!pool.length && typeof payload === 'string') { return normalizeProxyPoolEntries(payload, provider).slice(0, Number(options.maxItems) || 100); } const maxItems = Math.max(1, Math.min(500, Number(options.maxItems) || 100)); return pool.slice(0, maxItems); } function installIpProxyAuthListener() { if (ipProxyAuthListenerInstalled) { return; } if (!chrome.webRequest?.onAuthRequired?.addListener) { return; } chrome.webRequest.onAuthRequired.addListener( (details, callback) => { try { const auth = currentIpProxyAuthEntry; if (!auth?.username) { recordIpProxyAuthChallenge(details, false); callback(); return; } const isProxyChallenge = details?.isProxy === true; const challengerHost = String(details?.challenger?.host || '').trim().toLowerCase(); const challengerPort = Number.parseInt(String(details?.challenger?.port || ''), 10) || 0; const authHost = String(auth?.host || '').trim().toLowerCase(); const authPort = Number.parseInt(String(auth?.port || ''), 10) || 0; const challengerMatched = Boolean( challengerHost && authHost && challengerHost === authHost && (!authPort || !challengerPort || authPort === challengerPort) ); const proxyAuthStatus = Number.parseInt(String(details?.statusCode || ''), 10) === 407; const shouldProvide = isProxyChallenge || challengerMatched || proxyAuthStatus; recordIpProxyAuthChallenge(details, shouldProvide); if (!shouldProvide) { callback(); return; } callback({ authCredentials: { username: auth.username, password: String(auth.password || ''), }, }); } catch { recordIpProxyAuthChallenge(details, false); callback(); } }, { urls: [''] }, ['asyncBlocking'] ); ipProxyAuthListenerInstalled = true; } function installIpProxyErrorListener() { if (ipProxyErrorListenerInstalled) { return; } if (!chrome.proxy?.onProxyError?.addListener) { return; } chrome.proxy.onProxyError.addListener((details = {}) => { ipProxyLastRuntimeError = { error: String(details?.error || '').trim(), details: String(details?.details || '').trim(), fatal: Boolean(details?.fatal), at: Date.now(), }; }); ipProxyErrorListenerInstalled = true; } function callChromeProxySettings(method, details) { const proxySettings = chrome.proxy?.settings; if (!proxySettings || typeof proxySettings[method] !== 'function') { return Promise.reject(new Error('当前浏览器不支持扩展代理 API')); } return new Promise((resolve, reject) => { proxySettings[method](details, () => { const lastError = chrome.runtime?.lastError; if (lastError) { reject(new Error(lastError.message || String(lastError))); return; } resolve(true); }); }); } function getChromeProxySettings(details = { incognito: false }) { const proxySettings = chrome.proxy?.settings; if (!proxySettings || typeof proxySettings.get !== 'function') { return Promise.reject(new Error('当前浏览器不支持扩展代理 API')); } return new Promise((resolve, reject) => { proxySettings.get(details, (value) => { const lastError = chrome.runtime?.lastError; if (lastError) { reject(new Error(lastError.message || String(lastError))); return; } resolve(value || {}); }); }); } function validateProxyControlAfterApply(details, entry) { const level = String(details?.levelOfControl || '').trim(); if (level && level !== 'controlled_by_this_extension') { return { ok: false, message: `代理控制权不在当前扩展(levelOfControl=${level || 'unknown'})`, }; } const mode = String(details?.value?.mode || '').trim().toLowerCase(); if (mode !== 'pac_script') { return { ok: false, message: `代理模式不是 pac_script(当前为 ${mode || 'unknown'})`, }; } const pacData = String(details?.value?.pacScript?.data || ''); const endpoint = `${entry.host}:${entry.port}`; if (pacData && !pacData.includes(endpoint)) { return { ok: false, message: `PAC 未包含当前代理节点 ${endpoint},疑似被其他代理配置覆盖`, }; } return { ok: true, message: '', }; } function buildIpProxyPacScript(entry) { const normalizedProtocol = normalizeIpProxyProtocol(entry?.protocol || DEFAULT_IP_PROXY_PROTOCOL); const host = String(entry?.host || '').trim(); const port = normalizeIpProxyPort(entry?.port); if (!host || !port) { return ''; } let pacScheme = 'PROXY'; if (normalizedProtocol === 'https') { pacScheme = 'HTTPS'; } else if (normalizedProtocol === 'socks4') { pacScheme = 'SOCKS4'; } else if (normalizedProtocol === 'socks5') { pacScheme = 'SOCKS5'; } const targetPatterns = IP_PROXY_TARGET_HOST_PATTERNS.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', '); const bypassList = IP_PROXY_BYPASS_LIST.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', '); const forceDirectPatterns = (typeof IP_PROXY_FORCE_DIRECT_HOST_PATTERNS !== 'undefined' && Array.isArray(IP_PROXY_FORCE_DIRECT_HOST_PATTERNS) ? IP_PROXY_FORCE_DIRECT_HOST_PATTERNS : []) .map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`) .join(', '); const forceDirectFallback = String( typeof IP_PROXY_FORCE_DIRECT_FALLBACK !== 'undefined' && IP_PROXY_FORCE_DIRECT_FALLBACK ? IP_PROXY_FORCE_DIRECT_FALLBACK : 'DIRECT' ).replace(/"/g, '\\"'); const proxyEndpoint = `${pacScheme} ${host}:${port}`; const routeAllLiteral = (typeof IP_PROXY_ROUTE_ALL_TRAFFIC !== 'undefined' && Boolean(IP_PROXY_ROUTE_ALL_TRAFFIC)) ? 'true' : 'false'; return ` function FindProxyForURL(url, host) { if (!host) return "DIRECT"; var bypassList = [${bypassList}]; for (var i = 0; i < bypassList.length; i++) { var bypass = bypassList[i]; if (shExpMatch(host, bypass) || host === bypass) { return "DIRECT"; } } var forceDirectPatterns = [${forceDirectPatterns}]; for (var fd = 0; fd < forceDirectPatterns.length; fd++) { var directPattern = forceDirectPatterns[fd]; if (directPattern.indexOf('*.') === 0) { var directSuffix = directPattern.substring(1); var directHost = directPattern.substring(2); if (dnsDomainIs(host, directSuffix) || host === directHost) { return "${forceDirectFallback}"; } continue; } if (host === directPattern || dnsDomainIs(host, '.' + directPattern)) { return "${forceDirectFallback}"; } } var routeAllTraffic = ${routeAllLiteral}; if (routeAllTraffic) { return "${proxyEndpoint}"; } var targets = [${targetPatterns}]; var matched = false; for (var j = 0; j < targets.length; j++) { var pattern = targets[j]; if (pattern.indexOf('*.') === 0) { var suffix = pattern.substring(1); var direct = pattern.substring(2); if (dnsDomainIs(host, suffix) || host === direct) { matched = true; break; } continue; } if (host === pattern || dnsDomainIs(host, '.' + pattern)) { matched = true; break; } } if (!matched) { return "DIRECT"; } return "${proxyEndpoint}"; }`.trim(); } function buildIpProxyEntrySignature(entry = {}) { return [ normalizeIpProxyProtocol(entry?.protocol || DEFAULT_IP_PROXY_PROTOCOL), String(entry?.host || '').trim().toLowerCase(), String(normalizeIpProxyPort(entry?.port) || ''), String(entry?.username || '').trim(), String(entry?.password || ''), ].join('|'); } async function clearIpProxySettings(options = {}) { currentIpProxyAuthEntry = null; if (options?.resetAppliedSignature !== false) { lastAppliedIpProxyEntrySignature = ''; } if (options?.resetHostVariant !== false) { ipProxyAuthHostVariantToggle = false; } if (options?.resetLastAppliedAuthSnapshot === true) { lastAppliedIpProxyAuthSnapshot = { host: '', port: 0, username: '', password: '', }; } await callChromeProxySettings('clear', { scope: IP_PROXY_SETTINGS_SCOPE }); } async function clearIpProxyNetworkState() { try { if (chrome.webRequest?.handlerBehaviorChanged) { await new Promise((resolve) => { try { chrome.webRequest.handlerBehaviorChanged(() => resolve()); } catch { resolve(); } }); } if (chrome.browsingData?.remove) { await chrome.browsingData.remove( { since: 0 }, { cache: true, cacheStorage: true, serviceWorkers: true, } ); } } catch { // ignore cache clear failures; proxy apply can still continue } } async function forceProxyConnectionDrainBeforeAuthSwitch() { try { await callChromeProxySettings('set', { value: { mode: 'direct' }, scope: IP_PROXY_SETTINGS_SCOPE, }); } catch { // ignore direct mode switch failures } await new Promise((resolve) => setTimeout(resolve, 1200)); await clearIpProxyNetworkState().catch(() => {}); await new Promise((resolve) => setTimeout(resolve, 800)); } async function updateIpProxyRuntimeStatus(status = {}) { const patch = buildIpProxyRoutingStatePatch(status); await setState(patch); broadcastDataUpdate(patch); return patch; } async function applyIpProxySettingsFromState(state = {}, options = {}) { const resolvedState = state || await getState(); const enabled = Boolean(resolvedState?.ipProxyEnabled); if (!enabled) { try { await clearIpProxySettings({ resetLastAppliedAuthSnapshot: true }); } catch { // ignore clear failures when already clear } await setIpProxyLeakGuardEnabled(false); const status = { enabled: false, applied: false, reason: 'disabled', provider: normalizeIpProxyProviderValue(resolvedState?.ipProxyService), exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', error: '', }; await updateIpProxyRuntimeStatus(status); return status; } if (!chrome.proxy?.settings?.set) { await setIpProxyLeakGuardEnabled(true); const status = { enabled: true, applied: false, reason: 'proxy_api_unavailable', provider: normalizeIpProxyProviderValue(resolvedState?.ipProxyService), error: '当前浏览器不支持扩展代理 API。', exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', }; await updateIpProxyRuntimeStatus(status); return status; } const mode = normalizeIpProxyMode(resolvedState?.ipProxyMode); const provider = normalizeIpProxyProviderValue(resolvedState?.ipProxyService); if (mode === 'api' && !isApiModeProxyConfigAvailable(resolvedState)) { const clearRuntimePatch = buildIpProxyRuntimeStatePatch(mode, { pool: [], index: 0, current: null, }, provider); await setState(clearRuntimePatch); broadcastDataUpdate(clearRuntimePatch); try { await clearIpProxySettings({ resetLastAppliedAuthSnapshot: true }); } catch { // ignore clear failures } await setIpProxyLeakGuardEnabled(true); const status = { enabled: true, applied: false, reason: 'missing_proxy_entry', provider, error: 'API 模式已启用,但代理 API 为空。', exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', exitSource: '', }; await updateIpProxyRuntimeStatus(status); return status; } const entry = getIpProxyCurrentEntryFromState(resolvedState); if (!entry) { try { await clearIpProxySettings({ resetLastAppliedAuthSnapshot: true }); } catch { // ignore clear failures } await setIpProxyLeakGuardEnabled(true); const status = { enabled: true, applied: false, reason: 'missing_proxy_entry', provider: normalizeIpProxyProviderValue(resolvedState?.ipProxyService), error: mode === 'account' ? getAccountListParseFailureHint(resolvedState, provider) : '', exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', }; await updateIpProxyRuntimeStatus(status); return status; } const explicitForceAuthRebind = Boolean(options?.forceAuthRebind); const suppressAuthRebind = Boolean(options?.suppressAuthRebind); const hasAccountListConfigured = mode === 'account' && hasConfiguredAccountListEntries(resolvedState); const hasMultipleAccountEntries = mode === 'account' && hasAccountListConfigured && getAccountModeProxyPoolFromState(resolvedState, provider).length > 1; const shouldForceDrain = !suppressAuthRebind && ( explicitForceAuthRebind || shouldForceProxyConnectionDrainForEntry(entry) || ( provider === '711proxy' && hasAccountListConfigured && Boolean(String(entry?.username || '').trim()) ) ); let effectiveEntry = buildEffectiveProxyEntryForApply(entry, { forceRotateVariant: shouldForceDrain, allow711HostVariant: hasMultipleAccountEntries, }); if (shouldForceDrain) { effectiveEntry = await maybeResolveProxyHostVariantForAuthSwitch(effectiveEntry, { force: true, timeoutMs: 3500, // 仅多节点账号列表场景允许解析 IP 变体; // 单账号显式重绑仅使用 host 字面量变体,避免解析 IP 后链路不稳定。 allow711ResolvedIp: hasMultipleAccountEntries, }).catch(() => effectiveEntry); } const pacScript = buildIpProxyPacScript(effectiveEntry); if (!pacScript) { await setIpProxyLeakGuardEnabled(true); const status = { enabled: true, applied: false, reason: 'missing_proxy_entry', host: entry.host, port: entry.port, region: entry.region, hasAuth: Boolean(entry.username || entry.password), provider: normalizeIpProxyProviderValue(entry.provider || resolvedState?.ipProxyService), error: '代理配置不完整,无法生成 PAC 规则。', exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', }; await updateIpProxyRuntimeStatus(status); return status; } const entrySignature = buildIpProxyEntrySignature(effectiveEntry); const shouldResetNetworkState = Boolean( entrySignature && entrySignature !== lastAppliedIpProxyEntrySignature && !options.skipExitProbe && options.resetNetworkState !== false ); try { installIpProxyAuthListener(); installIpProxyErrorListener(); resetIpProxyRuntimeErrorDiagnostics(); if (shouldForceDrain) { if (typeof addLog === 'function') { await addLog( explicitForceAuthRebind ? '正在执行代理鉴权重绑:重置代理连接并切换主机变体,避免复用旧鉴权缓存。' : '检测到同节点切换代理账号,正在重置代理连接并切换主机变体,避免复用旧鉴权缓存。', 'info' ).catch(() => {}); if (String(effectiveEntry?.host || '').trim() !== String(entry?.host || '').trim()) { await addLog(`代理账号切换:已启用主机缓存隔离(${entry.host} -> ${effectiveEntry.host})。`, 'info').catch(() => {}); } } await setIpProxyLeakGuardEnabled(true); await forceProxyConnectionDrainBeforeAuthSwitch(); } currentIpProxyAuthEntry = effectiveEntry?.username ? { host: effectiveEntry.host, port: effectiveEntry.port, username: effectiveEntry.username, password: String(effectiveEntry.password || ''), } : null; await clearIpProxySettings({ resetAppliedSignature: false, resetHostVariant: false, }).catch(() => {}); currentIpProxyAuthEntry = effectiveEntry?.username ? { host: effectiveEntry.host, port: effectiveEntry.port, username: effectiveEntry.username, password: String(effectiveEntry.password || ''), } : null; if (shouldResetNetworkState) { await clearIpProxyNetworkState(); } await callChromeProxySettings('set', { value: { mode: 'pac_script', pacScript: { data: pacScript, // 官方文档建议启用 mandatory,避免 PAC 失效时静默回落到 DIRECT。 mandatory: true, }, }, scope: IP_PROXY_SETTINGS_SCOPE, }); const proxySettings = await getChromeProxySettings({ incognito: false }).catch(() => null); const takeoverCheck = validateProxyControlAfterApply(proxySettings || {}, effectiveEntry); if (!takeoverCheck.ok) { throw new Error(takeoverCheck.message || '代理接管校验失败。'); } await setIpProxyLeakGuardEnabled(false); lastAppliedIpProxyEntrySignature = entrySignature; lastAppliedIpProxyAuthSnapshot = { host: String(entry?.host || '').trim(), port: normalizeIpProxyPort(entry?.port), username: String(entry?.username || '').trim(), password: String(entry?.password || ''), }; } catch (error) { lastAppliedIpProxyEntrySignature = ''; await setIpProxyLeakGuardEnabled(true); const status = { enabled: true, applied: false, reason: 'apply_failed', host: entry.host, port: entry.port, region: entry.region, hasAuth: Boolean(entry.username || entry.password), provider: normalizeIpProxyProviderValue(entry.provider || resolvedState?.ipProxyService), error: error?.message || String(error || '代理设置失败'), exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', }; await updateIpProxyRuntimeStatus(status); return status; } const status = { enabled: true, applied: true, reason: 'applied', host: entry.host, port: entry.port, region: entry.region, username: String(entry.username || '').trim(), entrySource: resolveIpProxyAccountEntrySource(resolvedState, mode), hasAuth: Boolean(entry.username || entry.password), provider: normalizeIpProxyProviderValue(entry.provider || resolvedState?.ipProxyService), error: '', exitDetecting: !options.skipExitProbe, exitIp: '', exitRegion: '', exitError: '', exitSource: '', }; await updateIpProxyRuntimeStatus(status); if (shouldForceDrain) { // 同节点切换账号后给网络栈一点时间建立新隧道,避免立即探测命中旧连接。 await new Promise((resolve) => setTimeout(resolve, 900)); } if (options.skipExitProbe) { return status; } const token = ++ipProxyExitDetectionToken; const diagnostics = []; resetIpProxyAuthDiagnostics(); const exit = await detectProxyExitInfo({ timeoutMs: 10000, errors: diagnostics, provider: status?.provider || provider, username: status?.username || entry?.username || '', // 优先使用页面上下文探测,确保探测链路与真实浏览器页面一致; // 后台探测仅作为补充兜底,避免单一路径误判。 preferPageContext: true, allowBackgroundFallback: true, state, }).catch(() => ({ ip: '', region: '' })); if (!exit?.ip && Boolean(status?.hasAuth)) { appendIpProxyAuthDiagnosticsToErrors(diagnostics); } if (token !== ipProxyExitDetectionToken) { return status; } const latest = await getState(); if (!latest?.ipProxyEnabled) { return status; } const exitStatus = { ...status, exitDetecting: false, exitIp: String(exit?.ip || '').trim(), exitRegion: String(exit?.region || '').trim(), exitBaselineIp: String(exit?.baselineIp || '').trim(), exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS), exitSource: String(exit?.source || '').trim().toLowerCase(), authDiagnostics: status?.hasAuth ? getIpProxyAuthDiagnosticsSummary() : '', }; const expectedRegion = String(entry?.region || '').trim(); let normalizedExitStatus = applyExitRegionExpectation(exitStatus, expectedRegion); normalizedExitStatus = applyExitBaselineExpectation(normalizedExitStatus); if (shouldVerifyIpProxyTargetReachability(normalizedExitStatus)) { const targetDiagnostics = []; const reachability = await detectIpProxyTargetReachabilityByPageContext({ timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS, errors: targetDiagnostics, state, }).catch((error) => ({ reachable: false, endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0], source: 'target_page_context', error: error?.message || String(error || 'target reachability failed'), })); normalizedExitStatus = applyTargetReachabilityExpectation(normalizedExitStatus, { ...reachability, error: reachability?.reachable ? '' : (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)), }); } await syncIpProxyLeakGuardForStatus(normalizedExitStatus); await updateIpProxyRuntimeStatus(normalizedExitStatus); return normalizedExitStatus; } function getNextIpProxyPoolIndex(currentIndex = 0, poolLength = 0, direction = 'next') { const length = Math.max(0, Number(poolLength) || 0); if (length <= 0) return 0; const normalized = normalizeIpProxyCurrentIndex(currentIndex, 0) % length; if (String(direction || '').toLowerCase() === 'prev') { return (normalized - 1 + length) % length; } return (normalized + 1) % length; } async function refreshIpProxyPool(options = {}) { const state = options.state || await getState(); const mode = normalizeIpProxyMode(options.mode || state?.ipProxyMode); const maxItems = Math.max( 1, Math.min(500, Number(options.maxItems) || resolveIpProxyPoolTargetCountForMode(state, mode)) ); const provider = normalizeIpProxyProviderValue(state?.ipProxyService); let pool = []; if (mode === 'account') { pool = getAccountModeProxyPoolFromState(state, provider).slice(0, maxItems); if (!pool.length) { const parseFailureHint = getAccountListParseFailureHint(state, provider); if (parseFailureHint) { throw new Error(parseFailureHint); } throw new Error('账号密码模式没有可用代理,请先填写代理列表,或填写 Host/Port。'); } } else { pool = await pullIpProxyPoolFromApi(state, { maxItems, timeoutMs: options.timeoutMs, }); if (!pool.length) { throw new Error('代理列表为空,请检查 API 返回。'); } } const runtime = getIpProxyRuntimeSnapshot(state, mode, provider); const summary = buildProxyPoolSummary(pool, runtime.index); const updates = { ipProxyService: provider, ...buildIpProxyRuntimeStatePatch(mode, { pool, index: summary.index, current: summary.current, }, provider), }; await setState(updates); broadcastDataUpdate(updates); let proxyRouting = null; if (state?.ipProxyEnabled) { const applyState = { ...state, ...updates, }; const shouldRebindSingleAccountEntry = mode === 'account' && pool.length <= 1; proxyRouting = await applyIpProxySettingsFromState( applyState, shouldRebindSingleAccountEntry ? { forceAuthRebind: true, suppressAuthRebind: false, resetNetworkState: true, skipExitProbe: Boolean(options?.skipExitProbe), } : { skipExitProbe: Boolean(options?.skipExitProbe), } ); } return { mode, provider, count: summary.count, index: summary.index, current: summary.current, display: summary.display, pool, proxyRouting, }; } async function switchIpProxy(direction = 'next', options = {}) { const state = options.state || await getState(); const mode = normalizeIpProxyMode(options.mode || state?.ipProxyMode); if (mode === 'api' && !isApiModeProxyConfigAvailable(state)) { throw new Error('API 模式代理 URL 为空,请先填写代理 API 地址。'); } const maxItems = Math.max( 1, Math.min(500, Number(options.maxItems) || resolveIpProxyPoolTargetCountForMode(state, mode)) ); const provider = normalizeIpProxyProviderValue(state?.ipProxyService); const runtime = getIpProxyRuntimeSnapshot(state, mode, provider); let pool = []; if (mode === 'account') { pool = getAccountModeProxyPoolFromState(state, provider).slice(0, maxItems); } else { pool = runtime.pool.slice(0, maxItems); } if (!pool.length) { if (mode === 'api' && options.forceRefresh !== false) { return refreshIpProxyPool({ ...options, mode, state, }); } throw new Error(mode === 'account' ? '账号密码模式没有可切换的代理,请先填写代理列表或 Host/Port。' : '当前没有可切换代理,请先点击“拉取”获取 IP 列表。'); } const nextIndex = getNextIpProxyPoolIndex(runtime.index, pool.length, direction); const current = pool[nextIndex]; const updates = { ipProxyService: provider, ...buildIpProxyRuntimeStatePatch(mode, { pool, index: nextIndex, current, }, provider), }; await setState(updates); broadcastDataUpdate(updates); let proxyRouting = null; if (state?.ipProxyEnabled) { const applyState = { ...state, ...updates, }; const shouldRebindSingleAccountEntry = mode === 'account' && pool.length <= 1; proxyRouting = await applyIpProxySettingsFromState( applyState, shouldRebindSingleAccountEntry ? { forceAuthRebind: true, suppressAuthRebind: false, resetNetworkState: true, skipExitProbe: Boolean(options?.skipExitProbe), } : { skipExitProbe: Boolean(options?.skipExitProbe), } ); } const summary = buildProxyPoolSummary(pool, nextIndex); return { mode, provider, count: summary.count, index: summary.index, current: summary.current, display: summary.display, pool, proxyRouting, }; } async function changeIpProxyExit(options = {}) { const state = options.state || await getState(); if (!state?.ipProxyEnabled) { throw new Error('请先启用 IP 代理。'); } const mode = normalizeIpProxyMode(options.mode || state?.ipProxyMode); const provider = normalizeIpProxyProviderValue(state?.ipProxyService); if (mode !== 'account') { throw new Error('Change 仅支持账号密码模式。'); } if (provider !== '711proxy') { throw new Error('Change 当前仅支持 711Proxy。'); } const entry = getIpProxyCurrentEntryFromState(state); if (!entry || !entry.host || !entry.port) { throw new Error('当前没有可用代理条目,无法执行 Change。'); } const username = String(entry?.username || '').trim(); if (!has711SessionToken(username)) { throw new Error('当前账号未配置 session,无法执行 Change。请先在账号中追加 session 参数。'); } if (typeof addLog === 'function') { await addLog('正在执行 Change:保持当前 session,重绑代理链路并刷新出口。', 'info').catch(() => {}); } const proxyRouting = await applyIpProxySettingsFromState(state, { forceAuthRebind: true, suppressAuthRebind: false, resetNetworkState: true, skipExitProbe: Boolean(options?.skipExitProbe), }); const runtime = getIpProxyRuntimeSnapshot(state, mode, provider); const pool = runtime.pool.length ? runtime.pool : getAccountModeProxyPoolFromState(state, provider); const summary = buildProxyPoolSummary(pool, runtime.index); return { mode, provider, count: summary.count, index: summary.index, current: summary.current, display: summary.display, pool, proxyRouting, action: 'change', }; } async function tryRecoverApiProxyByRotation(options = {}) { const maxAttempts = Math.max(1, Math.min(12, Number(options?.maxAttempts) || 4)); let latestStatus = options?.fallbackStatus || null; let attempts = 0; let refreshedPool = false; const runSingleRotationPass = async () => { for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const state = await getState(); if (!state?.ipProxyEnabled) { break; } const mode = normalizeIpProxyMode(state?.ipProxyMode); if (mode !== 'api') { break; } const provider = normalizeIpProxyProviderValue(state?.ipProxyService); const runtime = getIpProxyRuntimeSnapshot(state, mode, provider); if (!Array.isArray(runtime?.pool) || runtime.pool.length <= 1) { break; } const switched = await switchIpProxy('next', { mode, forceRefresh: false, state, }).catch(() => null); attempts += 1; const status = switched?.proxyRouting || null; if (status) { latestStatus = status; } if (status?.applied && status?.reason !== 'connectivity_failed') { if (typeof addLog === 'function') { const nodeText = Number.isInteger(switched?.index) && Number.isInteger(switched?.count) && switched.count > 0 ? `(第 ${switched.index + 1}/${switched.count} 个节点)` : ''; const refreshText = refreshedPool ? '(已自动刷新 IP 池)' : ''; await addLog(`IP 代理自动恢复成功:API 模式已切换到可用节点${nodeText}${refreshText}。`, 'ok').catch(() => {}); } return true; } } return false; }; const firstPassRecovered = await runSingleRotationPass(); if (firstPassRecovered) { return { recovered: true, attempts, status: latestStatus }; } if (options?.allowRefresh !== false) { const latestState = await getState(); if (latestState?.ipProxyEnabled && normalizeIpProxyMode(latestState?.ipProxyMode) === 'api') { await refreshIpProxyPool({ mode: 'api', state: latestState, forceRefresh: true, }).catch(() => null); refreshedPool = true; const secondPassRecovered = await runSingleRotationPass(); if (secondPassRecovered) { return { recovered: true, attempts, status: latestStatus }; } } } if (latestStatus && attempts > 0) { const suffix = refreshedPool ? `已自动轮换 ${attempts} 个 API 节点,并刷新 IP 池后重试,仍不可用。` : `已自动轮换 ${attempts} 个 API 节点但仍不可用。`; const currentError = String(latestStatus.error || '').trim(); latestStatus = { ...latestStatus, error: currentError ? `${currentError} ${suffix}` : suffix, }; } return { recovered: false, attempts, status: latestStatus }; } async function probeIpProxyExit(options = {}) { if (ipProxyProbeInFlightPromise) { return ipProxyProbeInFlightPromise; } const probePromise = (async () => { const state = options.state || await getState(); if (!state?.ipProxyEnabled) { if (options?.detectWhenDisabled) { const diagnostics = []; const exit = await detectProxyExitInfo({ timeoutMs: Number(options?.timeoutMs) || 10000, errors: diagnostics, provider: normalizeIpProxyProviderValue(state?.ipProxyService), username: String(state?.ipProxyUsername || '').trim(), preferPageContext: true, allowBackgroundFallback: true, state, }).catch(() => ({ ip: '', region: '', endpoint: '', source: '' })); const status = { enabled: false, applied: false, reason: 'disabled_probe_only', provider: normalizeIpProxyProviderValue(state?.ipProxyService), exitDetecting: false, exitIp: String(exit?.ip || '').trim(), exitRegion: String(exit?.region || '').trim(), exitBaselineIp: String(exit?.baselineIp || '').trim(), exitEndpoint: String(exit?.endpoint || '').trim(), exitSource: String(exit?.source || '').trim().toLowerCase(), exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS), error: '', }; return { proxyRouting: status }; } const status = { enabled: false, applied: false, reason: 'disabled', provider: normalizeIpProxyProviderValue(state?.ipProxyService), exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', error: '', }; await updateIpProxyRuntimeStatus(status); return { proxyRouting: status }; } const mode = normalizeIpProxyMode(state?.ipProxyMode); const provider = normalizeIpProxyProviderValue(state?.ipProxyService); const entry = getIpProxyCurrentEntryFromState(state); if (!entry) { const status = { enabled: true, applied: false, reason: 'missing_proxy_entry', provider, error: mode === 'account' ? getAccountListParseFailureHint(state, provider) : '', exitDetecting: false, exitIp: '', exitRegion: '', exitError: '', }; await updateIpProxyRuntimeStatus(status); return { proxyRouting: status }; } installIpProxyAuthListener(); installIpProxyErrorListener(); resetIpProxyRuntimeErrorDiagnostics(); currentIpProxyAuthEntry = entry?.username ? { host: entry.host, port: entry.port, username: entry.username, password: String(entry.password || ''), } : null; const probingStatus = { enabled: true, applied: true, reason: String(state?.ipProxyAppliedReason || 'applied'), host: entry.host, port: entry.port, region: entry.region, username: String(entry.username || '').trim(), entrySource: resolveIpProxyAccountEntrySource(state, mode), hasAuth: Boolean(entry.username || entry.password), provider: normalizeIpProxyProviderValue(entry.provider || state?.ipProxyService), error: String(state?.ipProxyAppliedError || ''), exitDetecting: true, exitIp: '', exitRegion: '', exitEndpoint: '', exitError: '', exitSource: '', }; // 上一轮连通性失败会启用 fail-close 规则,手动检测前先解除,避免自阻断影响探测。 await setIpProxyLeakGuardEnabled(false); await updateIpProxyRuntimeStatus(probingStatus); const runProbeRound = async (statusSeed = probingStatus, timeoutMs = Number(options?.timeoutMs) || 10000) => { const diagnostics = []; resetIpProxyAuthDiagnostics(); const exit = await detectProxyExitInfo({ timeoutMs, errors: diagnostics, provider: statusSeed?.provider || probingStatus?.provider || provider, username: statusSeed?.username || probingStatus?.username || '', // 手动探测与自动应用保持一致:先页面上下文,再后台补充。 preferPageContext: true, allowBackgroundFallback: true, state, }).catch(() => ({ ip: '', region: '' })); if (!exit?.ip && Boolean(statusSeed?.hasAuth)) { appendIpProxyAuthDiagnosticsToErrors(diagnostics); if (typeof addLog === 'function') { await addLog(`代理出口探测失败:${buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)}`, 'warn').catch(() => {}); } } const finalStatus = { ...statusSeed, exitDetecting: false, exitIp: String(exit?.ip || '').trim(), exitRegion: String(exit?.region || '').trim(), exitBaselineIp: String(exit?.baselineIp || '').trim(), exitEndpoint: String(exit?.endpoint || '').trim(), exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS), exitSource: String(exit?.source || '').trim().toLowerCase(), authDiagnostics: statusSeed?.hasAuth ? getIpProxyAuthDiagnosticsSummary() : '', }; const expectedRegion = String(statusSeed?.region || '').trim(); let normalized = applyExitRegionExpectation(finalStatus, expectedRegion); normalized = applyExitBaselineExpectation(normalized); if (shouldVerifyIpProxyTargetReachability(normalized)) { const targetDiagnostics = []; const reachability = await detectIpProxyTargetReachabilityByPageContext({ timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS, errors: targetDiagnostics, state, }).catch((error) => ({ reachable: false, endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0], source: 'target_page_context', error: error?.message || String(error || 'target reachability failed'), })); normalized = applyTargetReachabilityExpectation(normalized, { ...reachability, error: reachability?.reachable ? '' : (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)), }); } return normalized; }; let normalizedFinalStatus = await runProbeRound(probingStatus, Number(options?.timeoutMs) || 10000); const parsedAuthSummary = parseIpProxyAuthDiagnosticsSummary(String(normalizedFinalStatus?.authDiagnostics || '')); const missingAuthChallenge = Boolean(normalizedFinalStatus?.hasAuth) && parsedAuthSummary && parsedAuthSummary.challenge <= 0 && parsedAuthSummary.provided <= 0; const shouldRetryAuthRebind = mode === 'account' && provider === '711proxy' && Boolean(String(entry?.username || '').trim()) && missingAuthChallenge && options?.authRebindRetry !== false; if (shouldRetryAuthRebind) { const maxRebindAttempts = Math.max(1, Math.min(3, Number(options?.authRebindMaxAttempts) || 2)); for (let attempt = 0; attempt < maxRebindAttempts; attempt += 1) { if (typeof addLog === 'function') { const hint = attempt === 0 ? '首次重绑复测' : `第 ${attempt + 1} 次重绑复测`; await addLog(`检测到代理链路未触发 407 挑战,正在执行${hint}。`, 'info').catch(() => {}); } const latestBeforeRebind = await getState(); const reboundStatus = await applyIpProxySettingsFromState(latestBeforeRebind, { skipExitProbe: true, resetNetworkState: true, forceAuthRebind: true, }).catch(() => null); const latestAfterRebind = await getState(); const reboundEntry = getIpProxyCurrentEntryFromState(latestAfterRebind); if (!reboundStatus?.applied || !reboundEntry?.host || !reboundEntry?.port) { break; } currentIpProxyAuthEntry = reboundEntry?.username ? { host: reboundEntry.host, port: reboundEntry.port, username: reboundEntry.username, password: String(reboundEntry.password || ''), } : null; const retryProbingStatus = { ...probingStatus, reason: String(reboundStatus?.reason || probingStatus.reason || 'applied'), host: reboundEntry.host, port: reboundEntry.port, region: String(reboundEntry.region || '').trim(), username: String(reboundEntry.username || '').trim(), entrySource: resolveIpProxyAccountEntrySource( latestAfterRebind, normalizeIpProxyMode(latestAfterRebind?.ipProxyMode) ), hasAuth: Boolean(reboundEntry.username || reboundEntry.password), provider: normalizeIpProxyProviderValue(reboundEntry.provider || latestAfterRebind?.ipProxyService), error: String(reboundStatus?.error || ''), exitDetecting: true, exitIp: '', exitRegion: '', exitEndpoint: '', exitError: '', exitSource: '', }; await updateIpProxyRuntimeStatus(retryProbingStatus); normalizedFinalStatus = await runProbeRound( retryProbingStatus, Math.max(9000, Number(options?.timeoutMs) || 10000) ); const retryAuthSummary = parseIpProxyAuthDiagnosticsSummary(String(normalizedFinalStatus?.authDiagnostics || '')); const stillMissingChallenge = Boolean(normalizedFinalStatus?.hasAuth) && retryAuthSummary && retryAuthSummary.challenge <= 0 && retryAuthSummary.provided <= 0; if (!stillMissingChallenge) { break; } } } if (normalizedFinalStatus.reason === 'connectivity_failed' && normalizeIpProxyMode(state?.ipProxyMode) === 'api') { const recovered = await tryRecoverApiProxyByRotation({ maxAttempts: options?.autoRotateMaxAttempts, fallbackStatus: normalizedFinalStatus, }); if (recovered?.status) { normalizedFinalStatus = recovered.status; } } await syncIpProxyLeakGuardForStatus(normalizedFinalStatus); await updateIpProxyRuntimeStatus(normalizedFinalStatus); return { proxyRouting: normalizedFinalStatus }; })(); ipProxyProbeInFlightPromise = probePromise; try { return await probePromise; } finally { if (ipProxyProbeInFlightPromise === probePromise) { ipProxyProbeInFlightPromise = null; } } } async function ensureIpProxySettingsAppliedFromCurrentState(options = {}) { const state = options.state || await getState(); return applyIpProxySettingsFromState(state, options); }