From c61274e3a8b48af760aa1be30dfdfe77b6f80be8 Mon Sep 17 00:00:00 2001 From: theshy Date: Tue, 12 Aug 2025 22:49:47 +0800 Subject: [PATCH] first commit --- README.md | 23 ++++ background.js | 276 +++++++++++++++++++++++++++++++++++++++++++++ icons/close-16.png | Bin 0 -> 731 bytes icons/close-32.png | Bin 0 -> 1563 bytes icons/close.svg | 51 +++++++++ icons/open-16.png | Bin 0 -> 842 bytes icons/open-32.png | Bin 0 -> 1978 bytes icons/open.svg | 18 +++ manifest.json | 14 +++ popup.css | 158 ++++++++++++++++++++++++++ popup.html | 125 ++++++++++++++++++++ popup.js | 123 ++++++++++++++++++++ 12 files changed, 788 insertions(+) create mode 100644 README.md create mode 100644 background.js create mode 100644 icons/close-16.png create mode 100644 icons/close-32.png create mode 100644 icons/close.svg create mode 100644 icons/open-16.png create mode 100644 icons/open-32.png create mode 100644 icons/open.svg create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..27f3d25 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Proxy Switcher(Chrome 扩展) + +一键切换 Chrome 代理:直连 / System / HTTP / SOCKS5 / PAC,支持弹窗选择与 PAC URL 保存。 + +## 使用方法 + +1. 打开 Chrome `chrome://extensions/` +2. 右上角打开“开发者模式” +3. 点击“加载已解压的扩展程序”,选择本目录 `2025-08-12_代理切换-浏览器扩展` +4. 点击工具栏图标,在弹窗中选择代理配置并点击“应用” +5. 选择 PAC 模式时,可在输入框中填写 PAC URL 后点击“保存” +6. 选择 自定义代理,可输入协议/地址/端口与绕过列表(逗号分隔),可保存或直接“应用” + +## 权限说明 + +- `proxy`: 设置浏览器代理 +- `storage`: 保存配置与当前选择 + +## 注意 + +- 如果系统或安全策略禁用了扩展修改代理,可能无法生效 +- PAC URL 需可访问且返回有效 PAC 脚本 +- 若切换到“系统代理/自动检测”仍无效,可尝试先切到“直连”,再切回“系统/自动”,或重载扩展。代码已在切换时先清理旧规则再设置以减少残留影响。 diff --git a/background.js b/background.js new file mode 100644 index 0000000..4f16e04 --- /dev/null +++ b/background.js @@ -0,0 +1,276 @@ +const STORAGE_KEYS = { + ACTIVE_PROFILE_ID: "activeProfileId", + PROFILES: "profiles", +}; + +const DEFAULT_PROFILES = [ + { id: "direct", name: "直连(关闭代理)", type: "direct" }, + { id: "system", name: "跟随系统代理", type: "system" }, + { id: "auto", name: "自动检测(WPAD)", type: "auto_detect" }, + { + id: "local-http", + name: "HTTP 127.0.0.1:7897", + type: "fixed", + scheme: "http", + host: "127.0.0.1", + port: 7897, + bypassList: ["localhost", "127.0.0.1"], + }, + { + id: "local-socks5", + name: "SOCKS5 127.0.0.1:7897", + type: "fixed", + scheme: "socks5", + host: "127.0.0.1", + port: 7897, + bypassList: ["localhost", "127.0.0.1"], + }, + { + id: "pac", + name: "PAC(自定义 URL)", + type: "pac", + pacUrl: "", + }, + { + id: "custom", + name: "自定义代理", + type: "fixed", + scheme: "http", + host: "", + port: 0, + bypassList: ["localhost", "127.0.0.1"], + }, +]; + +// 动态图标(仅使用 PNG) +// 只支持 PNG,去掉 SVG 支持与栅格化逻辑 +const ICON_FALLBACK_PNG = { + open: { 16: "icons/open-16.png", 32: "icons/open-32.png" }, + close: { 16: "icons/close-16.png", 32: "icons/close-32.png" }, +}; + +chrome.runtime.onInstalled.addListener(async () => { + const { profiles, activeProfileId } = await chrome.storage.local.get([ + STORAGE_KEYS.PROFILES, + STORAGE_KEYS.ACTIVE_PROFILE_ID, + ]); + const ensured = ensureProfiles(profiles); + await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured }); + if (!activeProfileId) { + await chrome.storage.local.set({ + [STORAGE_KEYS.ACTIVE_PROFILE_ID]: "direct", + }); + } + await applyActiveProfile(); +}); + +chrome.runtime.onStartup.addListener(async () => { + const { profiles } = await chrome.storage.local.get(STORAGE_KEYS.PROFILES); + const ensured = ensureProfiles(profiles); + await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured }); + await applyActiveProfile(); +}); + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + (async () => { + if (message?.type === "applyProfileById") { + await chrome.storage.local.set({ + [STORAGE_KEYS.ACTIVE_PROFILE_ID]: message.profileId, + }); + await applyActiveProfile(); + sendResponse({ ok: true }); + } else if (message?.type === "updateCustomProxy") { + const { scheme, host, port, bypassList } = message; + const { profiles } = await chrome.storage.local.get( + STORAGE_KEYS.PROFILES + ); + const updated = (profiles || []).map((p) => + p.id === "custom" + ? { + ...p, + type: "fixed", + scheme: scheme || "http", + host: host || "", + port: Number(port) || 0, + bypassList: Array.isArray(bypassList) + ? bypassList + : typeof bypassList === "string" && bypassList.trim() + ? bypassList + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : p.bypassList || [], + } + : p + ); + await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: updated }); + const { activeProfileId } = await chrome.storage.local.get( + STORAGE_KEYS.ACTIVE_PROFILE_ID + ); + if (activeProfileId === "custom") { + await applyActiveProfile(); + } + sendResponse({ ok: true }); + } else if (message?.type === "updatePacUrl") { + const { profiles } = await chrome.storage.local.get( + STORAGE_KEYS.PROFILES + ); + const updated = (profiles || []).map((p) => + p.id === "pac" ? { ...p, pacUrl: message.pacUrl || "" } : p + ); + await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: updated }); + const { activeProfileId } = await chrome.storage.local.get( + STORAGE_KEYS.ACTIVE_PROFILE_ID + ); + if (activeProfileId === "pac") { + await applyActiveProfile(); + } + sendResponse({ ok: true }); + } else if (message?.type === "getState") { + const { profiles, activeProfileId } = await chrome.storage.local.get([ + STORAGE_KEYS.PROFILES, + STORAGE_KEYS.ACTIVE_PROFILE_ID, + ]); + const ensured = ensureProfiles(profiles); + if (ensured !== profiles) { + await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured }); + } + sendResponse({ ok: true, profiles: ensured, activeProfileId }); + } else if (message?.type === "resetProfiles") { + await chrome.storage.local.set({ + [STORAGE_KEYS.PROFILES]: DEFAULT_PROFILES, + [STORAGE_KEYS.ACTIVE_PROFILE_ID]: "direct", + }); + await applyActiveProfile(); + sendResponse({ ok: true }); + } + })(); + return true; +}); + +async function applyActiveProfile() { + const { profiles, activeProfileId } = await chrome.storage.local.get([ + STORAGE_KEYS.PROFILES, + STORAGE_KEYS.ACTIVE_PROFILE_ID, + ]); + const profile = + (profiles || DEFAULT_PROFILES).find((p) => p.id === activeProfileId) || + DEFAULT_PROFILES[0]; + const config = buildProxyConfig(profile); + // 为避免残留规则影响,先清理再设置 + await new Promise((resolve) => + chrome.proxy.settings.clear({ scope: "regular" }, resolve) + ); + await new Promise((resolve) => { + chrome.proxy.settings.set({ value: config, scope: "regular" }, () => { + if (chrome.runtime.lastError) { + console.warn("proxy set error:", chrome.runtime.lastError); + } + resolve(); + }); + }); + await updateBadge(profile); + await updateIcon(profile); +} + +function buildProxyConfig(profile) { + if (profile.type === "direct") { + return { mode: "direct" }; + } + if (profile.type === "system") { + return { mode: "system" }; + } + if (profile.type === "auto_detect") { + return { mode: "auto_detect" }; + } + if (profile.type === "pac") { + const url = (profile.pacUrl || "").trim(); + if (url) { + return { mode: "pac_script", pacScript: { url } }; + } + return { mode: "direct" }; + } + if (profile.type === "fixed") { + const singleProxy = { + scheme: profile.scheme || "http", + host: profile.host, + port: Number(profile.port) || 0, + }; + if (!singleProxy.host || !singleProxy.port) { + // 避免设置非法 fixed 导致联网异常 + return { mode: "direct" }; + } + const bypassList = Array.isArray(profile.bypassList) + ? profile.bypassList + : []; + return { + mode: "fixed_servers", + rules: { singleProxy, bypassList }, + }; + } + return { mode: "direct" }; +} + +async function updateBadge(profile) { + // 仅显示图标,不显示任何徽章文字 + if (chrome.action?.setBadgeText) { + await chrome.action.setBadgeText({ text: "" }); + } + if (chrome.action?.setBadgeBackgroundColor) { + await chrome.action.setBadgeBackgroundColor({ color: "#00000000" }); + } +} + +async function updateIcon(profile) { + if (!chrome.action?.setIcon) return; + const isProxyOn = profile.type !== "direct"; + const path = isProxyOn ? ICON_FALLBACK_PNG.open : ICON_FALLBACK_PNG.close; + try { + await chrome.action.setIcon({ path }); + } catch (e) { + console.warn("setIcon failed", e); + } +} + +// 已移除 SVG 栅格化与程序化绘制,专用 PNG 路径作为图标资源 + +function ensureProfiles(existing) { + const current = Array.isArray(existing) ? [...existing] : []; + const byId = new Map(current.map((p) => [p.id, p])); + const need = [ + "direct", + "system", + "auto", + "local-http", + "local-socks5", + "pac", + "custom", + ]; + let changed = false; + for (const id of need) { + if (!byId.has(id)) { + const def = DEFAULT_PROFILES.find((p) => p.id === id); + if (def) { + current.push(def); + byId.set(id, def); + changed = true; + } + } + } + // 修补关键字段 + const custom = byId.get("custom"); + if (custom) { + if (!custom.scheme) (custom.scheme = "http"), (changed = true); + if (typeof custom.port !== "number") + (custom.port = Number(custom.port) || 0), (changed = true); + if (!Array.isArray(custom.bypassList)) + (custom.bypassList = ["localhost", "127.0.0.1"]), (changed = true); + custom.type = "fixed"; + } + const auto = byId.get("auto"); + if (auto && auto.type !== "auto_detect") { + auto.type = "auto_detect"; + changed = true; + } + return changed ? current : current; +} diff --git a/icons/close-16.png b/icons/close-16.png new file mode 100644 index 0000000000000000000000000000000000000000..4c811d4631504432d1d2470418ca9ad8fd5dd6ce GIT binary patch literal 731 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 literal 0 HcmV?d00001 diff --git a/icons/close.svg b/icons/close.svg new file mode 100644 index 0000000..7c92594 --- /dev/null +++ b/icons/close.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/open-16.png b/icons/open-16.png new file mode 100644 index 0000000000000000000000000000000000000000..71861f1c7e53afefe19115ad1890025caedaaa06 GIT binary patch literal 842 zcmV-Q1GW5#P)?q)TcWNVhKX>HlmbSt*qv{q8Br3NddASkG4FyaF>s3<7DKu~Zae(;;1 z_yF+{5z+NSL{u~iBEExB$;K$rn8g^4xIXrVd-rwk9p?&)64Aps{D(6$znSw;_`(=> zo;{%xT=0Hr`c1{AHYo(e{lG4W1nbcZJG1;$x(V$?RVC= zzcQIIESn$Bw*Y2Pl%p!fF)fu!RTzeGPC&1VeZJ|XrG?#J`~95&Z?kQCktA#4dw&$Q z^;CGk!M36QgDPDAC9w*SBF)#Jze%)b5c_$7Zd z3dPd7VoQw&Q6WHzn3Sfx9wHhBrWvA}W&f1e7cH09II9MJjD8 z@Yn|wm?8kk1&H8P<{rNChEY~D215r;#fY*sDE5Y2n|=;1q_N*`Cjz;5Af2LGF<9CM zo7Qu*CwIYi(&+AMLdVMkXnwK|M!pwbRg9o-q^KX3_~pp%WB?rs4%UjH6P#?)ylFks zPjgye&y4GE^-9kQJbc}V`!5cmD|JlZXXHg$ezjx)ab@NM4Y|DelS4e(QT8P9)`6W( z1U@Wwi`XfLGxoFeRp({8mAmz`kaSt%VY;OjRkh^j|IwSA^=nsa=P!vSZ{%Tvkqc}r zpn-GqN#`GbHa*0GGm;&LqyPW_ literal 0 HcmV?d00001 diff --git a/icons/open-32.png b/icons/open-32.png new file mode 100644 index 0000000000000000000000000000000000000000..45b5d98b8cd619310d4145d5ada0ded765e5764e GIT binary patch literal 1978 zcmV;r2SxaaP)>tF@bOF9cqvt`oq+y1<|E4kLd;{|SuO1z_nh=sHlwxK`?j*oBU#Mi!qnUKfBh z>Co*uY~zxs=qeI@PvP~s0eHP$`^FUCkGA9o7EaTZVhf_TZ@NVV)2zW?P9b;H<(dH~ zr35uOK0f{>A=Fd1Cx%sNYS>8lc5!^#U_*K z6wP-~PYWSd#>U1T<{0K0adC0~9BqJW?+8Kj=;QsKj{m+ zzyIu|`n8Si&7FW%wx_> zK2?xHA#8;r+X{IEcej9cIB!kPN=?&+lN4$g^O!XUZb~Xc)1^P)y|zcu+FON1Y1`ni zq~Ye-KY#(^&a4XLCv8B2Z620nzKX)+$B<&b4JMuy&>hRGhbQ^YHwT$++75?qe; z)PxCUEe2hK|6zB+)DWzC0vzVlsN3nTSI|aKGwrv+Zc4!{$0CHK52!SOF6YoW&E1*FDUjBbaOAN|2LQhhh3nPwq7VKqEGe}>(h!k{KB zPX7hcVoT8UX(eJUnNeFx%why1z3_)SF~h!q`%94?vmEo1Hn4rgqp`&kdBB9640x(XR&Rw(Hm$U7u#4cAq90BOMjk*eyU;M zFe1iMF@>aIXl{VI;QNs4AyK?QHRpeVlU{?RBZ#N~!epYLc)>wM+35D4LC3&JUfO@b z+xItoaPCQZNz`_1$3NP?2dP5z9WN{`Af@nXr2>(%BF|gC;Pld<*?ea(sNE`sm=9gv z#|yeeDRl%;7kNC)s1c5aN&=Of8~@b?Do%$)LDJ3G0{oFK-jnx6*FzpdZ>5CKq({2Q z#E@arEvG`?oOh=0k(--3%L`iC);n9P))%z=xU9&%!cyR#6@0Fb1>UWGvgGISF!ldfQhr{5=Qd`0x~Bwy0Sr-WiE_EX4ndZd zBH}7RxI`QaZ@R_nTVLdUzoo$Qb8CV7fkd0084++wA(!%>ZY^1QjD6S+q5eqbb~4!g zwuY929C0lG4F5?r3Qj;8Y~7RB6foLY?)02o$NR-L?r1vD@`T0deyFL?{b*T%`-oA1 z5$uKmFoI2_!?q}M1~?Z%vNdYhj{gAw0RR8)+pJ3f000I_L_t&o08XenZk1u$o&W#< M07*qoM6N<$f)}06V*mgE literal 0 HcmV?d00001 diff --git a/icons/open.svg b/icons/open.svg new file mode 100644 index 0000000..654ab97 --- /dev/null +++ b/icons/open.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..679f5b8 --- /dev/null +++ b/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 3, + "name": "Proxy Switcher", + "version": "1.0.1", + "description": "一键切换 Chrome 代理", + "permissions": ["proxy", "storage"], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html", + "default_title": "Proxy Switcher" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..0bc74a5 --- /dev/null +++ b/popup.css @@ -0,0 +1,158 @@ +:root { + --bg: #ffffff; + --card: #f8fafc; + --text: #0f172a; + --muted: #64748b; + --line: #e2e8f0; + --accent: #6366f1; + --accent-2: #3b82f6; + --focus: rgba(99, 102, 241, 0.35); + --shadow: 0 6px 20px rgba(2, 6, 23, 0.06), 0 1px 3px rgba(2, 6, 23, 0.06); + --radius: 12px; + --radius-sm: 10px; + --ring: 0 0 0 3px var(--focus); +} + +* { + box-sizing: border-box; +} +html, +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); +} +body { + font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, + "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; +} + +.popup { + width: 340px; + max-width: 380px; + min-width: 320px; + padding: 14px 14px 12px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} +.title { + display: flex; + align-items: center; + gap: 8px; +} +.title h1 { + font-size: 14px; + margin: 0; + font-weight: 600; +} +.logo { + width: 18px; + height: 18px; + border-radius: 6px; + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.35); +} + +/* 移除未使用的标签页样式 */ + +.main { + display: grid; + gap: 12px; +} + +.row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.field { + display: grid; + gap: 6px; +} +.field-label { + font-size: 12px; + color: var(--muted); +} +.hint { + color: var(--muted); + font-size: 12px; +} + +select, +input[type="text"], +input[type="number"] { + padding: 6px 8px; + font-size: 12px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.text-btn { + border: 0; + background: transparent; + color: var(--accent); + font-weight: 600; + cursor: pointer; + padding: 6px 8px; + border-radius: 8px; +} +.text-btn:hover { + background: rgba(99, 102, 241, 0.1); +} +.text-btn:focus-visible { + outline: none; + box-shadow: var(--ring); +} + +/* 自定义下拉包装 */ +.select { + position: relative; + display: inline-flex; + align-items: center; + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 0; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} +.select:focus-within { + border-color: var(--accent); + box-shadow: var(--ring); +} +.select select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + border: 0; + outline: none; + padding: 6px 26px 6px 8px; + background: transparent; + font-size: 12px; +} +.select .chevron { + position: absolute; + right: 8px; + color: var(--muted); + pointer-events: none; + transition: transform 0.2s ease, color 0.2s ease; +} +.select.open .chevron { + color: var(--accent); + transform: translateY(1px) rotate(180deg); +} + +/* 移除未使用的 Quick Save 样式 */ +/* 移除未使用的按钮/状态样式 */ + +/* 移除未使用的 Toast 样式 */ + +/* 移除未使用的动画 */ diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..e4ca8ac --- /dev/null +++ b/popup.html @@ -0,0 +1,125 @@ + + + + + + Proxy Switcher + + + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..835bf49 --- /dev/null +++ b/popup.js @@ -0,0 +1,123 @@ +// ========== 代理切换逻辑 ========== +const els = { + profileSelect: document.getElementById("profileSelect"), + applyBtn: document.getElementById("applyBtn"), + pacBlock: document.getElementById("pacBlock"), + pacUrl: document.getElementById("pacUrl"), + savePac: document.getElementById("savePac"), + customBlock: document.getElementById("customBlock"), + customScheme: document.getElementById("customScheme"), + customHost: document.getElementById("customHost"), + customPort: document.getElementById("customPort"), + customBypass: document.getElementById("customBypass"), + saveCustom: document.getElementById("saveCustom"), + // resetBtn: document.getElementById("resetBtn"), +}; + +// 下拉箭头展开/收起状态同步 +function bindSelectOpenState(selectEl) { + if (!selectEl) return; + const wrapper = selectEl.closest(".select"); + if (!wrapper) return; + // 点击时认为打开 + selectEl.addEventListener("mousedown", () => wrapper.classList.add("open")); + // 获得焦点也可视为打开 + selectEl.addEventListener("focus", () => wrapper.classList.add("open")); + // 失焦或变更后关闭 + selectEl.addEventListener("blur", () => wrapper.classList.remove("open")); + selectEl.addEventListener("change", () => wrapper.classList.remove("open")); +} + +bindSelectOpenState(els.profileSelect); +bindSelectOpenState(els.customScheme); + +init(); + +async function init() { + const state = await sendMessage({ type: "getState" }); + const profiles = state.profiles || []; + const activeProfileId = state.activeProfileId || "direct"; + + // 填充下拉 + els.profileSelect.innerHTML = ""; + for (const p of profiles) { + const opt = document.createElement("option"); + opt.value = p.id; + opt.textContent = p.name; + if (p.id === activeProfileId) opt.selected = true; + els.profileSelect.appendChild(opt); + } + + toggleBlocks(); + + // 回填 PAC、自定义 + const pacProfile = profiles.find((p) => p.id === "pac"); + if (els.pacUrl) els.pacUrl.value = pacProfile?.pacUrl || ""; + + const customProfile = profiles.find((p) => p.id === "custom"); + if (customProfile) { + if (els.customScheme) + els.customScheme.value = customProfile.scheme || "http"; + if (els.customHost) els.customHost.value = customProfile.host || ""; + if (els.customPort) els.customPort.value = customProfile.port || ""; + if (els.customBypass) + els.customBypass.value = (customProfile.bypassList || []).join(", "); + } +} + +els.profileSelect?.addEventListener("change", toggleBlocks); + +function toggleBlocks() { + const mode = els.profileSelect?.value; + const isPac = mode === "pac"; + const isCustom = mode === "custom"; + if (els.pacBlock) els.pacBlock.style.display = isPac ? "block" : "none"; + if (els.customBlock) + els.customBlock.style.display = isCustom ? "block" : "none"; +} + +els.applyBtn?.addEventListener("click", async () => { + const profileId = els.profileSelect.value; + if (profileId === "custom") { + await sendMessage({ + type: "updateCustomProxy", + scheme: els.customScheme?.value, + host: els.customHost?.value.trim(), + port: Number(els.customPort?.value) || 0, + bypassList: (els.customBypass?.value || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + }); + } + await sendMessage({ type: "applyProfileById", profileId }); + window.close(); +}); + +els.savePac?.addEventListener("click", async () => { + const pacUrl = (els.pacUrl?.value || "").trim(); + await sendMessage({ type: "updatePacUrl", pacUrl }); +}); + +els.saveCustom?.addEventListener("click", async () => { + await sendMessage({ + type: "updateCustomProxy", + scheme: els.customScheme?.value, + host: els.customHost?.value.trim(), + port: Number(els.customPort?.value) || 0, + bypassList: (els.customBypass?.value || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + }); +}); + +// 重置按钮已移除,如需恢复可取消上方注释并启用以下代码 +// els.resetBtn?.addEventListener("click", async () => { +// await sendMessage({ type: "resetProfiles" }); +// await init(); +// }); + +function sendMessage(msg) { + return new Promise((resolve) => chrome.runtime.sendMessage(msg, resolve)); +}