first commit

This commit is contained in:
theshy
2025-08-12 22:49:47 +08:00
commit c61274e3a8
12 changed files with 788 additions and 0 deletions

276
background.js Normal file
View File

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