Files
proxycast/extensions/proxycast-chrome/background.js
T
coso cd95cac72f feat: release v0.74.0 - Chrome Bridge 浏览器自动化集成
🎉 重大功能
- Chrome Bridge: 完整的浏览器自动化控制系统
- 零配置自动连接:扩展自动加载和配置
- AI 原生集成:作为 MCP 工具支持自然语言控制
- 支持导航、页面读取、元素交互、表单操作等

🐛 Bug 修复
- WebSocket 路由修复 (Axum 路径参数语法)
- Chrome 扩展存储清理
- 扩展重复注入防护
- 剪贴板权限添加

🔧 代码质量
- 修复 33+ Clippy 警告
- 所有 259 个测试通过
- ESLint 无警告

📝 文档
- Chrome Bridge 使用指南
- API 技术文档
- 快速参考卡片
2026-02-28 16:05:36 +08:00

668 lines
16 KiB
JavaScript

const HEARTBEAT_INTERVAL_MS = 30000;
const RECONNECT_MIN_DELAY_MS = 1000;
const RECONNECT_MAX_DELAY_MS = 30000;
const PAGE_CAPTURE_RETRY_LIMIT = 3;
const DEFAULT_SETTINGS = {
serverUrl: "ws://127.0.0.1:8999",
bridgeKey: "",
profileKey: "default",
monitoringEnabled: true,
};
let ws = null;
let isConnected = false;
let reconnectAttempts = 0;
let reconnectTimer = null;
let heartbeatTimer = null;
let activeTabId = null;
let monitoringEnabled = true;
let latestPageInfo = null;
let lastSettings = { ...DEFAULT_SETTINGS };
function logInfo(message, payload) {
if (payload === undefined) {
console.log(`[ProxycastBridge] ${message}`);
} else {
console.log(`[ProxycastBridge] ${message}`, payload);
}
}
function logWarn(message, payload) {
if (payload === undefined) {
console.warn(`[ProxycastBridge] ${message}`);
} else {
console.warn(`[ProxycastBridge] ${message}`, payload);
}
}
function readSettings() {
return new Promise((resolve) => {
chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS), (stored) => {
resolve({ ...DEFAULT_SETTINGS, ...stored });
});
});
}
function writeSettings(partial) {
return new Promise((resolve) => {
chrome.storage.local.set(partial, () => resolve());
});
}
function buildObserverUrl(settings) {
const serverUrl = String(settings.serverUrl || "").trim();
const bridgeKey = String(settings.bridgeKey || "").trim();
if (!serverUrl || !bridgeKey) {
return null;
}
const normalized = serverUrl.replace(/\/$/, "");
const profileKey = encodeURIComponent(settings.profileKey || "default");
return `${normalized}/proxycast-chrome-observer/${encodeURIComponent(bridgeKey)}?profileKey=${profileKey}`;
}
async function connectObserver(forceReconnect = false) {
if (ws && ws.readyState === WebSocket.OPEN && !forceReconnect) {
return;
}
clearReconnectTimer();
clearHeartbeatTimer();
const settings = await readSettings();
lastSettings = settings;
monitoringEnabled = Boolean(settings.monitoringEnabled);
const url = buildObserverUrl(settings);
if (!url) {
logWarn("缺少 serverUrl 或 bridgeKey,无法建立连接");
setConnectionState(false);
broadcastStatus();
return;
}
if (forceReconnect && ws) {
try {
ws.close();
} catch (_) {}
}
logInfo(`连接 observer: ${url}`);
ws = new WebSocket(url);
ws.onopen = () => {
reconnectAttempts = 0;
setConnectionState(true);
startHeartbeat();
broadcastStatus();
triggerPageCapture("ws_open");
};
ws.onmessage = async (event) => {
try {
const payload = JSON.parse(event.data);
await handleObserverMessage(payload);
} catch (error) {
logWarn("解析消息失败", error?.message || String(error));
}
};
ws.onclose = () => {
setConnectionState(false);
clearHeartbeatTimer();
scheduleReconnect();
broadcastStatus();
};
ws.onerror = (error) => {
logWarn("WebSocket 错误", error?.message || error);
};
}
function disconnectObserver(manual = true) {
clearReconnectTimer();
clearHeartbeatTimer();
if (ws) {
try {
ws.close();
} catch (_) {}
}
if (manual) {
setConnectionState(false);
broadcastStatus();
}
}
function setConnectionState(connected) {
isConnected = connected;
chrome.action.setBadgeText({ text: connected ? "ON" : "OFF" });
chrome.action.setBadgeBackgroundColor({ color: connected ? "#16a34a" : "#dc2626" });
}
function startHeartbeat() {
clearHeartbeatTimer();
heartbeatTimer = setInterval(() => {
sendObserverMessage({ type: "heartbeat", timestamp: Date.now() });
}, HEARTBEAT_INTERVAL_MS);
}
function clearHeartbeatTimer() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
function scheduleReconnect() {
if (reconnectTimer) {
return;
}
reconnectAttempts += 1;
const delay = Math.min(
RECONNECT_MAX_DELAY_MS,
RECONNECT_MIN_DELAY_MS * Math.pow(2, reconnectAttempts - 1),
);
reconnectTimer = setTimeout(async () => {
reconnectTimer = null;
await connectObserver();
}, delay);
logInfo(`连接断开,${delay}ms 后重连(第 ${reconnectAttempts} 次)`);
}
function clearReconnectTimer() {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
}
function sendObserverMessage(payload) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return false;
}
ws.send(JSON.stringify(payload));
return true;
}
function broadcastStatus(extra) {
chrome.runtime
.sendMessage({
type: "STATUS_UPDATE",
data: {
isConnected,
monitoringEnabled,
activeTabId,
latestPageInfo,
settings: {
...lastSettings,
bridgeKey: lastSettings.bridgeKey ? "***" : "",
},
...extra,
},
})
.catch(() => {});
}
async function handleObserverMessage(payload) {
const type = payload?.type;
if (type === "heartbeat_ack" || type === "connection_ack") {
return;
}
if (type !== "command" || !payload.data) {
return;
}
await executeRemoteCommand(payload.data);
}
async function executeRemoteCommand(commandData) {
const command = String(commandData.command || "").trim();
if (!command) {
return;
}
const requestId = commandData.requestId;
const sourceClientId = commandData.sourceClientId;
const waitForPageInfo = commandData.wait_for_page_info === true;
if (command === "open_url") {
await handleOpenUrl(commandData, waitForPageInfo);
return;
}
if (command === "switch_tab") {
await handleSwitchTab(commandData, waitForPageInfo);
return;
}
const tabId = await resolveTargetTabId();
if (!tabId) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: "没有可用的活动标签页",
});
return;
}
try {
const response = await sendCommandToTab(tabId, {
type: "EXECUTE_COMMAND",
data: commandData,
});
if (response?.status === "error") {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: response.error || "命令执行失败",
});
return;
}
sendCommandResult({
requestId,
sourceClientId,
status: "success",
message: response?.message || `${command} 执行成功`,
});
if (waitForPageInfo || command === "get_page_info") {
await triggerPageCapture("command_result");
}
} catch (error) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: error?.message || String(error),
});
}
}
async function handleOpenUrl(commandData, waitForPageInfo) {
const requestId = commandData.requestId;
const sourceClientId = commandData.sourceClientId;
let targetUrl = String(commandData.url || "").trim();
if (!targetUrl) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: "open_url 缺少 url 参数",
});
return;
}
if (!/^https?:\/\//i.test(targetUrl)) {
targetUrl = `https://${targetUrl}`;
}
try {
const tab = await new Promise((resolve, reject) => {
chrome.tabs.create({ url: targetUrl, active: true }, (created) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
resolve(created);
});
});
activeTabId = tab.id;
sendCommandResult({
requestId,
sourceClientId,
status: "success",
message: `已打开 ${targetUrl}`,
});
if (waitForPageInfo) {
await waitTabLoadComplete(tab.id, 30000);
await triggerPageCapture("open_url_complete");
}
} catch (error) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: error?.message || String(error),
});
}
}
async function handleSwitchTab(commandData, waitForPageInfo) {
const requestId = commandData.requestId;
const sourceClientId = commandData.sourceClientId;
const raw = String(commandData.target || "").trim();
if (!raw) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: "switch_tab 缺少 target 参数",
});
return;
}
let targetTab = null;
const byId = Number(raw);
if (!Number.isNaN(byId) && byId > 0) {
try {
targetTab = await chrome.tabs.get(byId);
} catch (_) {
targetTab = null;
}
}
if (!targetTab) {
const tabs = await chrome.tabs.query({ currentWindow: true });
const idx = Number(raw);
if (!Number.isNaN(idx) && idx >= 0 && idx < tabs.length) {
targetTab = tabs[idx];
}
}
if (!targetTab || !targetTab.id) {
sendCommandResult({
requestId,
sourceClientId,
status: "error",
error: `未找到标签页: ${raw}`,
});
return;
}
await chrome.tabs.update(targetTab.id, { active: true });
activeTabId = targetTab.id;
sendCommandResult({
requestId,
sourceClientId,
status: "success",
message: `已切换到标签页 ${targetTab.id}`,
});
if (waitForPageInfo) {
await triggerPageCapture("switch_tab");
}
}
function sendCommandResult(data) {
sendObserverMessage({
type: "command_result",
data,
});
}
async function resolveTargetTabId() {
if (activeTabId) {
try {
const tab = await chrome.tabs.get(activeTabId);
if (tab && !tab.discarded) {
return tab.id;
}
} catch (_) {}
}
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
activeTabId = tabs[0].id;
return tabs[0].id;
}
return null;
}
async function sendCommandToTab(tabId, payload) {
try {
return await chrome.tabs.sendMessage(tabId, payload);
} catch (error) {
await injectContentScript(tabId);
return await chrome.tabs.sendMessage(tabId, payload);
}
}
async function injectContentScript(tabId) {
await chrome.scripting.executeScript({
target: { tabId },
files: ["content_script.js"],
});
}
async function triggerPageCapture(reason, retry = 0) {
if (!monitoringEnabled && reason !== "manual") {
return;
}
const tabId = await resolveTargetTabId();
if (!tabId) {
return;
}
try {
await sendCommandToTab(tabId, {
type: "REQUEST_PAGE_CAPTURE",
data: { reason },
});
} catch (error) {
if (retry < PAGE_CAPTURE_RETRY_LIMIT) {
setTimeout(() => {
triggerPageCapture(reason, retry + 1);
}, 250 * (retry + 1));
} else {
logWarn("页面抓取请求失败", error?.message || String(error));
}
}
}
function waitTabLoadComplete(tabId, timeoutMs) {
return new Promise((resolve) => {
let resolved = false;
const timer = setTimeout(() => {
if (resolved) {
return;
}
resolved = true;
chrome.tabs.onUpdated.removeListener(onUpdated);
resolve();
}, timeoutMs);
const onUpdated = (updatedTabId, changeInfo) => {
if (updatedTabId !== tabId || changeInfo.status !== "complete") {
return;
}
if (resolved) {
return;
}
resolved = true;
clearTimeout(timer);
chrome.tabs.onUpdated.removeListener(onUpdated);
resolve();
};
chrome.tabs.onUpdated.addListener(onUpdated);
});
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const type = request?.type;
if (type === "GET_STATUS") {
sendResponse({
isConnected,
monitoringEnabled,
activeTabId,
latestPageInfo,
settings: {
...lastSettings,
bridgeKey: lastSettings.bridgeKey ? "***" : "",
},
});
return true;
}
if (type === "UPDATE_SETTINGS") {
const patch = request?.data || {};
const next = {
serverUrl: typeof patch.serverUrl === "string" ? patch.serverUrl : lastSettings.serverUrl,
bridgeKey: typeof patch.bridgeKey === "string" ? patch.bridgeKey : lastSettings.bridgeKey,
profileKey: typeof patch.profileKey === "string" ? patch.profileKey : lastSettings.profileKey,
monitoringEnabled:
typeof patch.monitoringEnabled === "boolean"
? patch.monitoringEnabled
: monitoringEnabled,
};
writeSettings(next).then(async () => {
lastSettings = { ...lastSettings, ...next };
monitoringEnabled = Boolean(next.monitoringEnabled);
if (request?.data?.reconnect === true) {
await connectObserver(true);
}
broadcastStatus();
sendResponse({ success: true });
});
return true;
}
if (type === "TOGGLE_CONNECTION") {
if (isConnected) {
disconnectObserver(true);
sendResponse({ success: true, isConnected: false });
} else {
connectObserver().then(() => {
sendResponse({ success: true, isConnected: isConnected });
});
}
return true;
}
if (type === "TOGGLE_MONITORING") {
monitoringEnabled = !monitoringEnabled;
writeSettings({ monitoringEnabled }).then(() => {
if (monitoringEnabled) {
triggerPageCapture("manual");
}
broadcastStatus();
sendResponse({ success: true, monitoringEnabled });
});
return true;
}
if (type === "REQUEST_PAGE_CAPTURE") {
triggerPageCapture("manual").then(() => {
sendResponse({ success: true });
});
return true;
}
if (type === "PAGE_INFO_UPDATE") {
const senderTabId = sender?.tab?.id;
if (senderTabId && activeTabId && senderTabId !== activeTabId) {
return true;
}
const markdown = request?.data?.markdown;
if (typeof markdown !== "string" || !markdown.trim()) {
return true;
}
latestPageInfo = {
title: request?.data?.title || "",
url: request?.data?.url || "",
timestamp: Date.now(),
markdown,
};
chrome.storage.local.set({ latestPageInfo });
sendObserverMessage({
type: "pageInfoUpdate",
data: { markdown },
});
broadcastStatus({ latestPageInfo });
return true;
}
if (type === "COMMAND_RESULT") {
if (request?.data) {
sendCommandResult(request.data);
}
return true;
}
return true;
});
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
activeTabId = tabId;
await triggerPageCapture("tab_activated");
broadcastStatus();
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (tab.active) {
activeTabId = tabId;
}
if (tab.active && changeInfo.status === "complete") {
await triggerPageCapture("tab_updated");
}
});
async function loadAutoConfig() {
try {
const configUrl = chrome.runtime.getURL("auto_config.json");
logInfo(`尝试加载自动配置: ${configUrl}`);
const response = await fetch(configUrl);
logInfo(`fetch 响应状态: ${response.status}`);
if (!response.ok) {
logWarn(`自动配置文件不存在或无法访问: ${response.status}`);
return;
}
const config = await response.json();
logInfo("成功读取自动配置", config);
if (config.serverUrl && config.bridgeKey) {
logInfo("检测到自动配置,正在应用...", config);
await writeSettings({
serverUrl: config.serverUrl,
bridgeKey: config.bridgeKey,
profileKey: config.profileKey || "default",
monitoringEnabled: config.monitoringEnabled !== false,
});
logInfo("自动配置已应用");
} else {
logWarn("自动配置缺少必要字段", config);
}
} catch (error) {
// 文件不存在或解析失败时记录错误
logWarn("加载自动配置失败", error?.message || String(error));
}
}
async function init() {
await loadAutoConfig();
const settings = await readSettings();
lastSettings = settings;
monitoringEnabled = Boolean(settings.monitoringEnabled);
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
activeTabId = tabs[0]?.id || null;
if (settings.serverUrl && settings.bridgeKey) {
await connectObserver();
} else {
setConnectionState(false);
broadcastStatus();
}
chrome.storage.local.get(["latestPageInfo"], (stored) => {
if (stored.latestPageInfo) {
latestPageInfo = stored.latestPageInfo;
broadcastStatus();
}
});
}
init();