11// bro bridge —— 后台 service worker
22// ---------------------------------------------------------------------------
3- // 只做两件事:
4- // 1) 特权抓取:借 host_permissions,代表前端读取搜索引擎等站点的响应内容(不受 CORS 限制);
5- // 2) 站点代理:在本机后台标签页中托管 Kimi / DeepSeek,并通过 content script 驱动其登录与对话。
3+ // 职责:
4+ // 1) 搜索引擎抓取:先用 Service Worker fetch(最轻量,零痕迹),若返回验证页或解析不到
5+ // 结果则降级到 Offscreen Document(依然完全静默,无任何标签/窗口)。
6+ // 全程对用户不可见,没有任何跳转或弹窗。
7+ // 2) Kimi / DeepSeek 会话:在本机后台标签页托管(登录态不能放在 Offscreen),通过
8+ // content script 驱动。这部分会创建标签但不抢占焦点,与搜索抓取链路解耦。
69// 全部运行在浏览器内部,不依赖 Node / npm / Chromium 等外部环境。
710// ---------------------------------------------------------------------------
811
@@ -11,75 +14,118 @@ const SITES = {
1114 deepseek : { url : 'https://chat.deepseek.com/' } ,
1215} ;
1316
14- const siteTabs = new Map ( ) ; // site -> tabId
17+ const siteTabs = new Map ( ) ; // site -> tabId(仅 Kimi / DeepSeek 使用)
1518const sleep = ( ms ) => new Promise ( ( r ) => setTimeout ( r , ms ) ) ;
1619
17- /* ------------------------------ 特权抓取 ------------------------------ */
20+ /* ---------------------------- Offscreen 文档管理 ---------------------------- */
21+ const OFFSCREEN_PATH = 'offscreen.html' ;
22+ const OFFSCREEN_CHANNEL = 'bro-offscreen-v1' ;
23+ const offscreenReady = new Promise ( ( resolve ) => {
24+ const listener = ( msg ) => {
25+ if ( msg && msg . channel === OFFSCREEN_CHANNEL && msg . dir === 'ready' ) {
26+ chrome . runtime . onMessage . removeListener ( listener ) ;
27+ resolve ( ) ;
28+ }
29+ } ;
30+ chrome . runtime . onMessage . addListener ( listener ) ;
31+ } ) ;
32+
33+ // 确保 Offscreen 文档存在(完全无界面、不出现任何标签)
34+ async function ensureOffscreen ( ) {
35+ try {
36+ // Chrome 116+ 支持;老版本抛错时回退到 fetch
37+ const has = await chrome . offscreen . hasDocument ( ) ;
38+ if ( has ) return ;
39+ } catch ( _ ) { /* 不支持 offscreen,走纯 fetch 路径 */ }
40+
41+ try {
42+ await chrome . offscreen . createDocument ( {
43+ url : OFFSCREEN_PATH ,
44+ reasons : [ 'DOM_PARSER' ] ,
45+ justification : '为 bro 搜索引擎后台静默抓取提供 DOM 环境,避免裸 fetch 被站点防爬拦截。' ,
46+ } ) ;
47+ await offscreenReady ;
48+ } catch ( err ) {
49+ throw new Error ( `无法创建 Offscreen 文档:${ err ?. message || err } ` ) ;
50+ }
51+ }
52+
53+ // 通过 Service Worker → Offscreen 发请求并拿回渲染后的 HTML
54+ function callOffscreen ( url , timeout = 25000 ) {
55+ const id = Date . now ( ) + '-' + Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) ;
56+ return new Promise ( ( resolve , reject ) => {
57+ const timer = setTimeout ( ( ) => {
58+ chrome . runtime . onMessage . removeListener ( onMsg ) ;
59+ reject ( new Error ( 'Offscreen 文档未响应' ) ) ;
60+ } , timeout ) ;
61+ function onMsg ( msg ) {
62+ if ( ! msg || msg . channel !== OFFSCREEN_CHANNEL || msg . dir !== 'res' || msg . id !== id ) return ;
63+ clearTimeout ( timer ) ;
64+ chrome . runtime . onMessage . removeListener ( onMsg ) ;
65+ if ( msg . ok ) resolve ( msg . data ) ;
66+ else reject ( new Error ( msg . error || 'Offscreen 返回错误' ) ) ;
67+ }
68+ chrome . runtime . onMessage . addListener ( onMsg ) ;
69+ chrome . runtime . sendMessage ( {
70+ channel : OFFSCREEN_CHANNEL , dir : 'req' , id, method : 'fetch' ,
71+ params : { url, timeout : 20000 , settle : 600 } ,
72+ } ) ;
73+ } ) ;
74+ }
75+
76+ /* ------------------------------ 搜索引擎抓取 ------------------------------ */
1877async function fetchText ( url ) {
78+ // Service Worker fetch:最轻量,扩展身份(带完整 Cookie + UA + host_permissions)
1979 const res = await fetch ( url , {
2080 method : 'GET' ,
21- credentials : 'include' , // 带上本机浏览器已有的 Cookie,提高结果页可用性
81+ credentials : 'include' ,
2282 redirect : 'follow' ,
2383 cache : 'no-store' ,
24- headers : { 'Accept-Language' : 'zh-CN,zh;q=0.9,en;q=0.8' } ,
84+ headers : {
85+ 'Accept-Language' : 'zh-CN,zh;q=0.9,en;q=0.8' ,
86+ 'User-Agent' : navigator . userAgent ,
87+ } ,
2588 } ) ;
2689 const text = await res . text ( ) ;
2790 if ( ! res . ok ) throw new Error ( `目标站点返回 ${ res . status } ` ) ;
2891 return { status : res . status , finalUrl : res . url , text } ;
2992}
3093
31- // 等待标签页加载完成(status === 'complete'),带超时
32- function waitForTabComplete ( tabId , timeout = 25000 ) {
33- return new Promise ( ( resolve , reject ) => {
34- let settled = false ;
35- const finish = ( err ) => {
36- if ( settled ) return ;
37- settled = true ;
38- chrome . tabs . onUpdated . removeListener ( onUpdated ) ;
39- clearTimeout ( timer ) ;
40- err ? reject ( err ) : resolve ( ) ;
41- } ;
42- const onUpdated = ( id , info ) => { if ( id === tabId && info . status === 'complete' ) finish ( ) ; } ;
43- chrome . tabs . onUpdated . addListener ( onUpdated ) ;
44- const timer = setTimeout ( ( ) => finish ( new Error ( '页面加载超时' ) ) , timeout ) ;
45- // 可能监听前就已加载完成
46- chrome . tabs . get ( tabId ) . then ( ( t ) => { if ( t && t . status === 'complete' ) finish ( ) ; } ) . catch ( ( ) => { } ) ;
47- } ) ;
94+ // 判断 HTML 是否为搜索引擎返回的安全验证页(而非真正的搜索结果页)
95+ function looksLikeCaptcha ( html ) {
96+ const h = html || '' ;
97+ return / w a p p a s s \. b a i d u \. c o m | 百 度 安 全 验 证 | 请 输 入 验 证 码 | 滑 动 验 证 | 人 机 验 证 | u n u s u a l t r a f f i c | s e c u r i t y c h e c k | c a p t c h a / i. test ( h ) ;
4898}
4999
50- // 后台标签页真实加载目标页,读取渲染后的 DOM。
51- // 搜索引擎的防爬校验(如百度安全验证、必应 JS 校验)会拦截裸 fetch;
52- // 用真实标签页加载等价于用户在浏览器里打开,可带回完整请求头、Cookie 与脚本渲染结果。
53- async function fetchViaTab ( url , { timeout = 25000 , settle = 700 } = { } ) {
54- const tab = await chrome . tabs . create ( { url, active : false } ) ;
100+ // 后台静默抓取:先 fetch(最轻量),若拿到验证页或空结果再降级 Offscreen。
101+ // 全程对用户不可见——没有跳转、没有标签闪动、没有任何窗口。
102+ async function fetchPageSilent ( url ) {
103+ // 第一优先级:Service Worker fetch(零痕迹)
55104 try {
56- await waitForTabComplete ( tab . id , timeout ) ;
57- await sleep ( settle ) ; // 等待结果脚本渲染进 DOM
58- const injected = await chrome . scripting . executeScript ( {
59- target : { tabId : tab . id } ,
60- func : ( ) => document . documentElement . outerHTML ,
61- } ) ;
62- const html = injected && injected [ 0 ] ? injected [ 0 ] . result : '' ;
63- if ( ! html ) throw new Error ( '未能读取页面内容' ) ;
64- return { status : 200 , finalUrl : url , text : html } ;
65- } finally {
66- chrome . tabs . remove ( tab . id ) . catch ( ( ) => { } ) ;
105+ const res = await fetchText ( url ) ;
106+ if ( ! looksLikeCaptcha ( res . text ) ) return { ...res , via : 'sw-fetch' } ;
107+ // fetch 拿到了验证页,降级到 Offscreen
108+ } catch ( _ ) { /* fetch 失败直接走 Offscreen */ }
109+
110+ // 第二优先级:Offscreen Document(同样零痕迹)
111+ try {
112+ await ensureOffscreen ( ) ;
113+ const res = await callOffscreen ( url ) ;
114+ return { ...res , via : 'offscreen' } ;
115+ } catch ( err ) {
116+ throw new Error ( `静默抓取失败:${ err ?. message || err } ` ) ;
67117 }
68118}
69119
70- /* ------------------------------ 站点代理 ------------------------------ */
120+ /* ------------------------------ 站点代理(Kimi / DeepSeek) ------------------------------ */
71121async function ensureTab ( site ) {
72122 const cfg = SITES [ site ] ;
73123 if ( ! cfg ) throw new Error ( `未知站点:${ site } ` ) ;
74124
75125 const existing = siteTabs . get ( site ) ;
76126 if ( existing != null ) {
77- try {
78- await chrome . tabs . get ( existing ) ;
79- return existing ;
80- } catch {
81- siteTabs . delete ( site ) ;
82- }
127+ try { await chrome . tabs . get ( existing ) ; return existing ; }
128+ catch { siteTabs . delete ( site ) ; }
83129 }
84130
85131 const tab = await chrome . tabs . create ( { url : cfg . url , active : false } ) ;
@@ -91,12 +137,8 @@ async function ensureTab(site) {
91137async function sendToTab ( tabId , message , { retries = 25 , delay = 800 } = { } ) {
92138 let lastErr ;
93139 for ( let i = 0 ; i < retries ; i ++ ) {
94- try {
95- return await chrome . tabs . sendMessage ( tabId , message ) ;
96- } catch ( err ) {
97- lastErr = err ;
98- await sleep ( delay ) ;
99- }
140+ try { return await chrome . tabs . sendMessage ( tabId , message ) ; }
141+ catch ( err ) { lastErr = err ; await sleep ( delay ) ; }
100142 }
101143 throw new Error ( `无法与站点页面通信:${ lastErr ?. message || lastErr } ` ) ;
102144}
@@ -109,41 +151,18 @@ async function aiCall(site, cmd, payload = {}) {
109151/* --------------------------- 消息分发入口 --------------------------- */
110152async function handle ( method , params = { } ) {
111153 switch ( method ) {
112- case 'ping' :
113- return { pong : true } ;
114- case 'fetch' :
115- return fetchText ( params . url ) ;
116-
117- // 搜索引擎抓取主通道:后台标签页真实加载,规避防爬校验与 JS 渲染问题
118- case 'fetchPage' :
119- try {
120- return await fetchViaTab ( params . url ) ;
121- } catch ( err ) {
122- // 标签页方式失败时回退到网络抓取,尽量给出结果
123- const r = await fetchText ( params . url ) ;
124- return { ...r , fallback : String ( err ?. message || err ) } ;
125- }
126-
127- case 'ai.open' :
128- return aiCall ( params . site , 'state' ) ;
129- case 'ai.state' :
130- return aiCall ( params . site , 'state' ) ;
131- case 'ai.login' :
132- return aiCall ( params . site , 'login' , {
133- username : params . username ,
134- password : params . password ,
135- captcha : params . captcha ,
136- } ) ;
137- case 'ai.chat' :
138- return aiCall ( params . site , 'chat' , { text : params . text , images : params . images || [ ] } ) ;
139- case 'ai.newChat' :
140- return aiCall ( params . site , 'newChat' ) ;
154+ case 'ping' : return { pong : true } ;
155+ case 'fetch' : return fetchText ( params . url ) ;
156+ case 'fetchPage' : return fetchPageSilent ( params . url ) ; // 搜索引擎:全程静默
157+
158+ case 'ai.open' : return aiCall ( params . site , 'state' ) ;
159+ case 'ai.state' : return aiCall ( params . site , 'state' ) ;
160+ case 'ai.login' : return aiCall ( params . site , 'login' , { username : params . username , password : params . password , captcha : params . captcha } ) ;
161+ case 'ai.chat' : return aiCall ( params . site , 'chat' , { text : params . text , images : params . images || [ ] } ) ;
162+ case 'ai.newChat' : return aiCall ( params . site , 'newChat' ) ;
141163 case 'ai.reset' : {
142164 const tabId = siteTabs . get ( params . site ) ;
143- if ( tabId != null ) {
144- await chrome . tabs . remove ( tabId ) . catch ( ( ) => { } ) ;
145- siteTabs . delete ( params . site ) ;
146- }
165+ if ( tabId != null ) { await chrome . tabs . remove ( tabId ) . catch ( ( ) => { } ) ; siteTabs . delete ( params . site ) ; }
147166 return { ok : true } ;
148167 }
149168 default :
@@ -159,9 +178,8 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
159178 return true ; // 异步响应
160179} ) ;
161180
162- // 用户手动关闭托管标签页时同步清理映射
163181chrome . tabs . onRemoved . addListener ( ( tabId ) => {
164182 for ( const [ site , id ] of siteTabs . entries ( ) ) {
165183 if ( id === tabId ) siteTabs . delete ( site ) ;
166184 }
167- } ) ;
185+ } ) ;
0 commit comments