277 lines
8.2 KiB
JavaScript
277 lines
8.2 KiB
JavaScript
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;
|
||
}
|