From 30b0c00589b7280c27e5a0223ac7671582fde23e Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Mon, 27 Apr 2026 14:07:48 +0800 Subject: [PATCH] test: document IP proxy integration and harden sidepanel restore --- sidepanel/sidepanel.js | 96 +++++++++++++----- tests/background-ip-proxy-core.test.js | 135 +++++++++++++++++++++++++ 项目完整链路说明.md | 43 +++++++- 项目文件结构说明.md | 23 +++-- 4 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 tests/background-ip-proxy-core.test.js diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index c2afd8d..5d66c65 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -2746,6 +2746,44 @@ function applySettingsState(state) { if (typeof syncStepDefinitionsForMode === 'function') { syncStepDefinitionsForMode(Boolean(state?.plusModeEnabled)); } + const fallbackIpProxyService = '711proxy'; + const fallbackIpProxyMode = 'account'; + const fallbackIpProxyProtocol = 'http'; + const resolveIpProxyService = (value) => (typeof normalizeIpProxyService === 'function' + ? normalizeIpProxyService(value) + : String(value || fallbackIpProxyService).trim().toLowerCase() || fallbackIpProxyService); + const resolveIpProxyMode = (value) => { + if (typeof normalizeIpProxyModeForCurrentRelease === 'function') { + return normalizeIpProxyModeForCurrentRelease(value); + } + if (typeof normalizeIpProxyMode === 'function') { + return normalizeIpProxyMode(value); + } + const normalized = String(value || fallbackIpProxyMode).trim().toLowerCase(); + return normalized || fallbackIpProxyMode; + }; + const resolveIpProxyProtocol = (value) => (typeof normalizeIpProxyProtocol === 'function' + ? normalizeIpProxyProtocol(value) + : String(value || fallbackIpProxyProtocol).trim().toLowerCase() || fallbackIpProxyProtocol); + const resolveIpProxyPort = (value) => { + if (typeof normalizeIpProxyPort === 'function') { + return normalizeIpProxyPort(value); + } + const numeric = Number.parseInt(String(value || '').trim(), 10); + return Number.isInteger(numeric) && numeric > 0 && numeric <= 65535 ? numeric : 0; + }; + const resolveIpProxyAccountList = (value) => (typeof normalizeIpProxyAccountList === 'function' + ? normalizeIpProxyAccountList(value || '') + : String(value || '').replace(/\r/g, '').trim()); + const resolveIpProxySessionPrefix = (value) => (typeof normalizeIpProxyAccountSessionPrefix === 'function' + ? normalizeIpProxyAccountSessionPrefix(value || '') + : String(value || '').trim()); + const resolveIpProxyAccountLifeMinutes = (value) => (typeof normalizeIpProxyAccountLifeMinutes === 'function' + ? normalizeIpProxyAccountLifeMinutes(value || '') + : String(value || '').trim()); + const resolveIpProxyPoolTargetCount = (value) => (typeof normalizeIpProxyPoolTargetCount === 'function' + ? normalizeIpProxyPoolTargetCount(value || '', 20) + : String(value || '20').trim() || '20'); syncLatestState(state); syncAutoRunState(state); renderStepStatuses(latestState); @@ -2770,7 +2808,7 @@ function applySettingsState(state) { inputSub2ApiPassword.value = state?.sub2apiPassword || ''; inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; inputSub2ApiDefaultProxy.value = state?.sub2apiDefaultProxyName || ''; - const normalizedIpProxyService = normalizeIpProxyService(state?.ipProxyService); + const normalizedIpProxyService = resolveIpProxyService(state?.ipProxyService); const normalizedIpProxyServiceProfiles = typeof normalizeIpProxyServiceProfiles === 'function' ? normalizeIpProxyServiceProfiles(state?.ipProxyServiceProfiles || {}, state || {}) : (state?.ipProxyServiceProfiles || {}); @@ -2781,60 +2819,62 @@ function applySettingsState(state) { ipProxyServiceProfiles: normalizedIpProxyServiceProfiles, }) : { - mode: typeof normalizeIpProxyModeForCurrentRelease === 'function' - ? normalizeIpProxyModeForCurrentRelease(state?.ipProxyMode) - : normalizeIpProxyMode(state?.ipProxyMode), + mode: resolveIpProxyMode(state?.ipProxyMode), apiUrl: String(state?.ipProxyApiUrl || '').trim(), - accountList: normalizeIpProxyAccountList(state?.ipProxyAccountList || ''), - accountSessionPrefix: normalizeIpProxyAccountSessionPrefix(state?.ipProxyAccountSessionPrefix || ''), - accountLifeMinutes: normalizeIpProxyAccountLifeMinutes(state?.ipProxyAccountLifeMinutes || ''), - poolTargetCount: normalizeIpProxyPoolTargetCount(state?.ipProxyPoolTargetCount || '', 20), + accountList: resolveIpProxyAccountList(state?.ipProxyAccountList || ''), + accountSessionPrefix: resolveIpProxySessionPrefix(state?.ipProxyAccountSessionPrefix || ''), + accountLifeMinutes: resolveIpProxyAccountLifeMinutes(state?.ipProxyAccountLifeMinutes || ''), + poolTargetCount: resolveIpProxyPoolTargetCount(state?.ipProxyPoolTargetCount || ''), host: String(state?.ipProxyHost || '').trim(), - port: String(normalizeIpProxyPort(state?.ipProxyPort || '') || ''), - protocol: normalizeIpProxyProtocol(state?.ipProxyProtocol), + port: String(resolveIpProxyPort(state?.ipProxyPort || '') || ''), + protocol: resolveIpProxyProtocol(state?.ipProxyProtocol), username: String(state?.ipProxyUsername || '').trim(), password: String(state?.ipProxyPassword || ''), region: String(state?.ipProxyRegion || '').trim(), }; - if (selectIpProxyService) { + if (typeof selectIpProxyService !== 'undefined' && selectIpProxyService) { selectIpProxyService.value = normalizedIpProxyService; } - if (inputIpProxyApiUrl) { + if (typeof inputIpProxyApiUrl !== 'undefined' && inputIpProxyApiUrl) { inputIpProxyApiUrl.value = String(activeIpProxyProfile.apiUrl || '').trim(); } - if (inputIpProxyAccountList) { + if (typeof inputIpProxyAccountList !== 'undefined' && inputIpProxyAccountList) { inputIpProxyAccountList.value = activeIpProxyProfile.accountList; } - if (inputIpProxyAccountSessionPrefix) { + if (typeof inputIpProxyAccountSessionPrefix !== 'undefined' && inputIpProxyAccountSessionPrefix) { inputIpProxyAccountSessionPrefix.value = activeIpProxyProfile.accountSessionPrefix; } - if (inputIpProxyAccountLifeMinutes) { + if (typeof inputIpProxyAccountLifeMinutes !== 'undefined' && inputIpProxyAccountLifeMinutes) { inputIpProxyAccountLifeMinutes.value = activeIpProxyProfile.accountLifeMinutes; } - if (inputIpProxyPoolTargetCount) { + if (typeof inputIpProxyPoolTargetCount !== 'undefined' && inputIpProxyPoolTargetCount) { inputIpProxyPoolTargetCount.value = activeIpProxyProfile.poolTargetCount; } - if (inputIpProxyHost) { + if (typeof inputIpProxyHost !== 'undefined' && inputIpProxyHost) { inputIpProxyHost.value = activeIpProxyProfile.host; } - if (inputIpProxyPort) { - const normalizedPort = normalizeIpProxyPort(activeIpProxyProfile.port || ''); + if (typeof inputIpProxyPort !== 'undefined' && inputIpProxyPort) { + const normalizedPort = resolveIpProxyPort(activeIpProxyProfile.port || ''); inputIpProxyPort.value = normalizedPort > 0 ? String(normalizedPort) : ''; } - if (selectIpProxyProtocol) { - selectIpProxyProtocol.value = normalizeIpProxyProtocol(activeIpProxyProfile.protocol); + if (typeof selectIpProxyProtocol !== 'undefined' && selectIpProxyProtocol) { + selectIpProxyProtocol.value = resolveIpProxyProtocol(activeIpProxyProfile.protocol); } - if (inputIpProxyUsername) { + if (typeof inputIpProxyUsername !== 'undefined' && inputIpProxyUsername) { inputIpProxyUsername.value = activeIpProxyProfile.username; } - if (inputIpProxyPassword) { + if (typeof inputIpProxyPassword !== 'undefined' && inputIpProxyPassword) { inputIpProxyPassword.value = activeIpProxyProfile.password; } - if (inputIpProxyRegion) { + if (typeof inputIpProxyRegion !== 'undefined' && inputIpProxyRegion) { inputIpProxyRegion.value = activeIpProxyProfile.region; } - setIpProxyMode(activeIpProxyProfile.mode); - setIpProxyEnabled(Boolean(state?.ipProxyEnabled)); + if (typeof setIpProxyMode === 'function') { + setIpProxyMode(activeIpProxyProfile.mode); + } + if (typeof setIpProxyEnabled === 'function') { + setIpProxyEnabled(Boolean(state?.ipProxyEnabled)); + } syncLatestState({ ipProxyService: normalizedIpProxyService, ipProxyServiceProfiles: normalizedIpProxyServiceProfiles, @@ -2842,7 +2882,9 @@ function applySettingsState(state) { ? buildIpProxyStatePatchFromServiceProfile(normalizedIpProxyService, activeIpProxyProfile) : {}), }); - updateIpProxyUI(latestState); + if (typeof updateIpProxyUI === 'function') { + updateIpProxyUI(latestState); + } inputCodex2ApiUrl.value = state?.codex2apiUrl || ''; inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || ''; const restoredMailProvider = isCustomMailProvider(state?.mailProvider) diff --git a/tests/background-ip-proxy-core.test.js b/tests/background-ip-proxy-core.test.js new file mode 100644 index 0000000..b43627b --- /dev/null +++ b/tests/background-ip-proxy-core.test.js @@ -0,0 +1,135 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +function loadIpProxyCore({ accountListEnabled = true } = {}) { + const providerSource = fs.readFileSync('background/ip-proxy-provider-711proxy.js', 'utf8'); + const coreSource = fs.readFileSync('background/ip-proxy-core.js', 'utf8'); + return new Function(` +const self = {}; +const chrome = {}; +const DEFAULT_IP_PROXY_SERVICE = '711proxy'; +const IP_PROXY_SERVICE_VALUES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy']; +const IP_PROXY_ENABLED_SERVICE_VALUES = ['711proxy']; +const DEFAULT_IP_PROXY_MODE = 'account'; +const IP_PROXY_MODE_VALUES = ['api', 'account']; +const DEFAULT_IP_PROXY_PROTOCOL = 'http'; +const IP_PROXY_PROTOCOL_VALUES = ['http', 'https', 'socks4', 'socks5']; +const IP_PROXY_FETCH_TIMEOUT_MS = 20000; +const IP_PROXY_SETTINGS_SCOPE = 'regular'; +const IP_PROXY_BYPASS_LIST = ['', 'localhost', '127.0.0.1']; +const IP_PROXY_ROUTE_ALL_TRAFFIC = true; +const IP_PROXY_ACCOUNT_LIST_ENABLED = ${accountListEnabled ? 'true' : 'false'}; +const IP_PROXY_TARGET_HOST_PATTERNS = [ + 'openai.com', + '*.openai.com', + 'chatgpt.com', + '*.chatgpt.com', +]; +${providerSource} +const transformIpProxyAccountEntryByProvider = self.transformIpProxyAccountEntryByProvider; +${coreSource} +return { + buildIpProxyPacScript, + getAccountModeProxyPoolFromState, + normalizeIpProxyAccountList, + normalizeProxyPoolEntries, + parseIpProxyLine, + resolveIpProxyAutoSwitchThreshold, +}; +`)(); +} + +test('IP proxy parser ignores disabled lines and normalizes proxy entries', () => { + const api = loadIpProxyCore(); + + assert.equal( + api.normalizeIpProxyAccountList([ + '# disabled', + ' // disabled', + '; disabled', + 'global.rotgb.711proxy.com:10000:user:pass', + '', + ].join('\n')), + 'global.rotgb.711proxy.com:10000:user:pass' + ); + + const pool = api.normalizeProxyPoolEntries([ + 'http://global.rotgb.711proxy.com:10000:user:pa:ss', + 'http://global.rotgb.711proxy.com:10000:user:pa:ss', + { host: 'us.proxy.example', port: '8080', username: 'u2', password: 'p2' }, + ]); + + assert.equal(pool.length, 2); + assert.deepStrictEqual(pool[0], { + host: 'global.rotgb.711proxy.com', + port: 10000, + username: 'user', + password: 'pa:ss', + protocol: 'http', + region: '', + provider: '711proxy', + }); + assert.equal(pool[1].host, 'us.proxy.example'); + assert.equal(pool[1].port, 8080); +}); + +test('711 fixed-account mode applies region and sticky session parameters', () => { + const api = loadIpProxyCore(); + const pool = api.getAccountModeProxyPoolFromState({ + ipProxyService: '711proxy', + ipProxyMode: 'account', + ipProxyHost: 'global.rotgb.711proxy.com', + ipProxyPort: '10000', + ipProxyProtocol: 'http', + ipProxyUsername: 'USER047152-zone-custom', + ipProxyPassword: 'secret', + ipProxyRegion: 'US', + ipProxyAccountSessionPrefix: 'sticky_001', + ipProxyAccountLifeMinutes: '30', + }); + + assert.equal(pool.length, 1); + assert.equal(pool[0].host, 'global.rotgb.711proxy.com'); + assert.equal(pool[0].port, 10000); + assert.equal(pool[0].region, 'US'); + assert.match(pool[0].username, /region-US/); + assert.match(pool[0].username, /session-sticky_001/); + assert.match(pool[0].username, /sessTime-30/); +}); + +test('IP proxy PAC keeps local traffic direct and routes target traffic through proxy', () => { + const api = loadIpProxyCore(); + const pac = api.buildIpProxyPacScript({ + host: 'global.rotgb.711proxy.com', + port: 10000, + protocol: 'http', + }); + + assert.match(pac, /FindProxyForURL/); + assert.match(pac, /localhost/); + assert.match(pac, /PROXY global\.rotgb\.711proxy\.com:10000/); + assert.match(pac, /chatgpt\.com/); + assert.match(pac, /openai\.com/); +}); + +test('sidepanel loads IP proxy scripts before sidepanel bootstrap', () => { + const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8'); + const providerIndex = html.indexOf(''); + const panelIndex = html.indexOf(''); + const sidepanelIndex = html.indexOf(''); + + assert.notEqual(providerIndex, -1); + assert.notEqual(panelIndex, -1); + assert.notEqual(sidepanelIndex, -1); + assert.ok(providerIndex < panelIndex); + assert.ok(panelIndex < sidepanelIndex); +}); + +test('IP proxy auto-switch threshold is clamped to the supported range', () => { + const api = loadIpProxyCore(); + + assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '0' }), 1); + assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '25' }), 25); + assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '9999' }), 500); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 371bc0d..77acb92 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -145,6 +145,7 @@ - localhost 回调地址 - 自动运行轮次信息 - 当前自动运行 session 标识 `autoRunSessionId` +- IP 代理运行态:`ipProxyApiPool / ipProxyAccountPool / ipProxyCurrent / ipProxyApplied*`,用于记录当前代理池、当前代理节点、PAC 接管结果、出口检测结果和诊断信息 - 标签注册表 - 最近打开的来源地址 - LuckMail 当前运行时选择 @@ -160,6 +161,7 @@ - CPA / SUB2API 配置 - Codex2API 配置 +- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值 - Plus 模式开关 `plusModeEnabled` - PayPal 登录配置 `paypalEmail / paypalPassword` - 邮箱 provider 配置 @@ -195,6 +197,8 @@ - `ICLOUD_LOGIN_REQUIRED` - `ICLOUD_ALIASES_CHANGED` +IP 代理模块在同步、切换、Change、出口探测和自动运行成功阈值切换后,会通过 `DATA_UPDATED` 广播 `ipProxyApplied*` 与当前代理池字段,sidepanel 只根据广播更新状态卡,不在 UI 层重新推断后台代理结果。 + ### 4.4 贡献模式链路 贡献模式的目标不是新增一个 provider,而是在 sidepanel 内开启一套“贡献账号”专用 UI,并把公开贡献服务并进原有 10 步主链: @@ -764,6 +768,42 @@ Hide My Email 获取与管理链路: - iCloud 登录态提示必须区分“真实未登录”和“已登录但请求上下文波动”;不能把所有 401 / 403 / 421 都直接提示为未登录。 - iCloud 别名缓存只用于短暂失败回退,不应改变最终的已用 / 保留状态来源;状态仍以 `manualAliasUsage`、`preservedAliases` 与最新线上列表合并后的结果为准。 +### 7.7 IP Proxy + +组成: + +- [background/ip-proxy-core.js](c:/Users/projectf/Downloads/codex注册扩展/background/ip-proxy-core.js) +- [background/ip-proxy-provider-711proxy.js](c:/Users/projectf/Downloads/codex注册扩展/background/ip-proxy-provider-711proxy.js) +- [sidepanel/ip-proxy-panel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/ip-proxy-panel.js) +- [sidepanel/ip-proxy-provider-711proxy.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/ip-proxy-provider-711proxy.js) +- [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md) + +职责边界: + +- `background/ip-proxy-core.js` 负责代理条目解析、运行态代理池、PAC 应用与清除、`webRequest.onAuthRequired` 代理鉴权回填、出口探测和失败时的目标站点 fail-close 规则。 +- `background/ip-proxy-provider-711proxy.js` 只承接 711Proxy 的账号串参数规则,负责把 `region / session / sessTime` 叠加到最终用户名。 +- `background/message-router.js` 只暴露 `REFRESH_IP_PROXY_POOL / SWITCH_IP_PROXY / CHANGE_IP_PROXY_EXIT / PROBE_IP_PROXY_EXIT` 消息,不在路由层复制代理解析逻辑。 +- `sidepanel/ip-proxy-panel.js` 负责 UI 状态渲染、表单快照、按钮动作和 711 参数输入联动;`sidepanel/sidepanel.js` 只保留配置保存、事件绑定和广播接线。 + +基础链路: + +1. 用户在 sidepanel 打开 `IP代理`,选择服务和账号模式。 +2. 首版只开放 `711proxy + account` 主路径;API 模式和账号列表入口保留但默认禁用。 +3. 用户填写固定账号的 Host / Port / Protocol / Username / Password,可选填写地区、session 和时长。 +4. 点击 `同步` 后,sidepanel 发送 `REFRESH_IP_PROXY_POOL`,后台解析当前账号为代理池并调用 `chrome.proxy.settings.set` 写入 PAC。 +5. 如果代理需要账号密码,后台通过 `webRequest.onAuthRequired` 对代理 407 challenge 回填用户名和密码。 +6. PAC 成功应用后,后台关闭 fail-close 规则;如果配置缺失、应用失败或出口探测失败,则开启只覆盖 OpenAI / ChatGPT 目标域的阻断规则,避免误以为已经走代理。 +7. `检测出口` 会优先用页面上下文探测公网 IP,再用后台请求兜底,并把出口 IP、地区、探测来源和诊断摘要写回运行态。 +8. `下一条` 根据当前代理池 index 切换节点;`Change` 在 711 session 场景下强制重绑代理链路,用于刷新出口。 +9. 自动运行成功轮数命中 `ipProxyPoolTargetCount` 时,后台会尝试自动切到下一条代理;只有代理候选数量大于 1 时才执行切换。 + +维护约定: + +- 代理解析、PAC、鉴权和出口检测必须留在后台模块,不应继续堆进 `background.js`。 +- UI 层不能凭输入值直接判断“代理已接管”,只能展示后台返回的 `ipProxyApplied*` 状态。 +- 新增代理服务商时,应优先新增 provider 规则模块,并让共享解析/运行态继续走 `background/ip-proxy-core.js`。 +- 修改代理字段、权限或链路时,需要同步更新 [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md)、当前完整链路说明和结构说明。 + ## 8. 自动运行完整链路 文件: @@ -794,7 +834,8 @@ Hide My Email 获取与管理链路: - 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态) 8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId` 9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起” -10. 所有轮次结束后输出汇总,并清空当前 session 标识 +10. 如果启用 IP 代理,后台会在每轮成功后按 `ipProxyPoolTargetCount` 检查是否需要切换出口;候选代理不足时只记录日志并跳过切换 +11. 所有轮次结束后输出汇总,并清空当前 session 标识 ## 9. 新增功能时最容易漏掉的地方 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 41ea51a..db94a0d 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -29,7 +29,7 @@ - `mail-provider-utils.js`:网页邮箱 provider 配置纯工具,负责 `163 / 163 VIP / 126 / QQ / Inbucket / Hotmail` 的基础归一化与页面入口配置,并统一承接 iCloud 转发收码目标邮箱 provider 的归一化、选项列表和收码入口配置。 - `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。 - `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案;当前还统一承接 `mail2925Mode` 的归一化与“2925 仅在 provide 模式下参与别名生成”的共享判定。 -- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。 +- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集;当前额外声明 `proxy / webRequest / webRequestAuthProvider`,用于 IP 代理 PAC 接管和代理鉴权回填。 - `microsoft-email.js`:Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。 - `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。 - `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。 @@ -46,15 +46,17 @@ - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。 - `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。 - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。 +- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。 +- `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。 -- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。 +- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。 - `background/phone-verification-flow.js`:手机号验证共享流程模块,负责在 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回步骤 7 重新拿号。 - `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 -- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 +- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 ## `background/steps/` @@ -102,6 +104,7 @@ ## `docs/` - `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。 +- `docs/ip-proxy-module.md`:IP Proxy 模块说明,记录模块定位、后台/侧栏代码结构、当前功能范围、711Proxy 参数联动、操作方式、发布策略与已知限制。 - `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。 - `docs/images/十轮自动.png`:README 中展示的自动运行效果图。 - `docs/images/微信.png`:README 中展示的微信收款码图片。 @@ -130,8 +133,10 @@ - `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。 - `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。 - `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。 -- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。 - - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡 +- `sidepanel/ip-proxy-panel.js`:侧边栏 IP 代理面板逻辑,负责代理配置显隐、服务/模式切换、账号串参数同步、状态卡渲染,以及同步、下一条、Change、检测出口、检查 IP 等按钮行为。 +- `sidepanel/ip-proxy-provider-711proxy.js`:侧边栏 711Proxy 输入辅助逻辑,负责从 host、username、region 输入中推断和归一化国家/地区参数。 +- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行、统一的收起态列表高度样式,以及 IP 代理配置区、状态卡和操作按钮样式。 +- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡 献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内 展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“接码”开关与“验证码重发”拆成同一行展示;只有开启接码后,下面的 HeroSMS 平 台、国家与 API Key 行才会显示;页面继续加载 `managed-alias-utils.js` 与 `mail-provider-utils.js`,并把旧的“邮箱前缀”字段语义改为“别 @@ -139,9 +144,8 @@ receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时 额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收 码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密 - 钥配置行;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添 - 加”按钮和共享表单容器。 - - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装 + 钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。 +- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装 配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启 动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接 到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显 @@ -152,7 +156,7 @@ 据配置;Step 8 的自定义邮箱确认弹窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表 会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只 要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑; - Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站 + Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站 公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示。 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Ultra` / 历史 `Pro` / legacy `v` 版本族排序、缓存读取与版本展示。 @@ -176,6 +180,7 @@ - `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。 - `tests/background-icloud-login-help.test.js`:测试 iCloud 登录判定、页面上下文回退、别名缓存回退和保留异常恢复的关键源码约束。 - `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析,并覆盖转发收码模式复用共享 provider 配置。 +- `tests/background-ip-proxy-core.test.js`:测试 IP 代理核心解析与接入约束,覆盖注释行过滤、代理条目归一化、711 固定账号参数叠加、PAC 生成、侧栏脚本加载顺序和自动切换阈值钳制。 - `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。 - `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。 - `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。