Files
proxy-switcher/background.js
2025-08-12 22:49:47 +08:00

277 lines
8.2 KiB
JavaScript
Raw 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.

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;
}