feat: add cross-browser builds and refresh project documentation

- add Chrome/Chromium Manifest V3 build with PAC proxy routing
- keep Firefox Manifest V2 and Android compatibility
- add Chromium API adapter and service worker
- add Chrome build script and Makefile target
- update README and add AGENT.md
- document installation, signing, permissions, themes, and platform
  limitations
This commit is contained in:
wt committed 2026-10-09 16:17:33 +07:00
1 parent 11508e6740
commit d0c5fc5679
12 files changed
+256 -14

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
// Chromium implementation. Chrome's proxy API uses a PAC script rather than
// Firefox's per-request proxy.onRequest listener.
let config = configManager.getConfig();
async function init() {
await configManager.load();
config = configManager.getConfig();
await applyProxyConfig();
configManager.addListener(() => {
config = configManager.getConfig();
applyProxyConfig().catch(error => console.error('Failed to apply proxy config:', error));
});
}
function escapePacString(value) {
return JSON.stringify(String(value));
}
function buildPacScript() {
const rules = {};
Object.entries(config.domains || {}).forEach(([domain, entry]) => {
if (!entry || !entry.enabled || !entry.host || !entry.port) return;
const scheme = entry.type === 'socks' ? 'SOCKS5' : 'PROXY';
rules[domain] = `${scheme} ${entry.host}:${parseInt(entry.port, 10)}`;
});
return `function FindProxyForURL(url, host) {
var rules = ${JSON.stringify(rules)};
return Object.prototype.hasOwnProperty.call(rules, host) ? rules[host] : "DIRECT";
}`;
}
async function applyProxyConfig() {
const hasRules = Object.values(config.domains || {}).some(entry =>
entry && entry.enabled && entry.host && entry.port
);
return chrome.proxy.settings.set({
value: hasRules
? { mode: 'pac_script', pacScript: { data: buildPacScript(), mandatory: true } }
: { mode: 'direct' },
scope: 'regular'
});
}
function getDomain(url) {
try { return new URL(url).hostname.replace(/^www\./, ''); } catch (_) { return null; }
}
function sendConfigResponse(sendResponse, action) {
if (action === 'getConfig') sendResponse({ config: configManager.getConfig() });
else if (action === 'getProfiles') sendResponse({ profiles: configManager.getProfiles() });
else return false;
return true;
}
chrome.webRequest.onAuthRequired.addListener(
(details, sendResponse) => {
if (!details.isProxy) return sendResponse({});
const entry = config.domains && config.domains[getDomain(details.url)];
if (!entry || !entry.enabled || !entry.username || !entry.password) return sendResponse({});
sendResponse({ authCredentials: { username: entry.username, password: entry.password } });
},
{ urls: ['<all_urls>'] },
['blocking']
);
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const direct = sendConfigResponse(sendResponse, message.action);
if (direct) return true;
const asyncActions = {
updateDomain: () => configManager.updateDomain(message.domain, message.domainConfig),
removeDomain: () => configManager.removeDomain(message.domain),
updateProfile: () => configManager.updateProfile(message.profileName, message.profileConfig),
removeProfile: () => configManager.removeProfile(message.profileName),
updateSettings: () => configManager.updateSettings(message.settings || {}),
reloadConfig: () => configManager.reload(),
clearAllDomains: () => { configManager.config.domains = {}; return configManager.save(); },
clearAllProfiles: () => { configManager.config.profiles = {}; return configManager.save(); },
clearEverything: () => { configManager.config.domains = {}; configManager.config.profiles = {}; return configManager.save(); }
};
if (message.action === 'getProfile') {
sendResponse({ profile: configManager.getProfileConfig(message.profileName) });
return false;
}
if (message.action === 'toggleProxy') {
const entry = configManager.getDomainConfig(message.domain);
if (!entry) { sendResponse({ success: false, error: 'Domain not configured' }); return false; }
entry.enabled = message.enabled;
asyncActions.updateDomain = () => configManager.updateDomain(message.domain, entry);
}
const operation = asyncActions[message.action];
if (!operation) return false;
operation().then(() => sendResponse({ success: true })).catch(error =>
sendResponse({ success: false, error: error.message })
);
return true;
});
init().catch(error => console.error('Failed to initialize Chrome proxy support:', error));