Files

1667 lines
63 KiB
JavaScript
Raw Permalink Blame History

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