@@ -5,7 +5,8 @@ import * as Router from 'koa-router';
55/**
66 * websocket不可用的环境,留一个http轮询的接入接口
77 */
8- const connections : Record < string , { socket : WebSocket , stream : PassThrough , expiry : number , messages : Data [ ] } > = { } ;
8+ type ConnectionEntry = { socket : WebSocket , stream : PassThrough , expiry : number , messages : Data [ ] } ;
9+ const connections : Record < string , ConnectionEntry > = { } ;
910const cleanerInterval = 10000 ; // 清理间隔10s
1011let cleanerIntervalId : NodeJS . Timer = null ;
1112
@@ -28,7 +29,9 @@ const initCleaner = () => {
2829 }
2930} ;
3031
31- export function listenHttpSocket ( router : Router ) {
32+ // ─── SDK 端降级接口(SSE + POST 轮询)─────────────────────────────────────────
33+
34+ function listenTargetHttpSocket ( router : Router ) {
3235 router . get ( '/target/:id' , async ctx => {
3336 // 使用SSE来推送服务端数据
3437 ctx . request . socket . setTimeout ( 0 ) ;
@@ -39,41 +42,42 @@ export function listenHttpSocket(router: Router) {
3942 'Cache-Control' : 'no-cache' ,
4043 'Connection' : 'keep-alive' ,
4144 } ) ;
42- const id = ctx . params . id ;
43- if ( connections [ id ] ) {
44- ctx . body = connections [ id ] . stream = new PassThrough ( ) ;
45- const { stream, messages } = connections [ id ] ;
46- connections [ id ] . expiry = Date . now ( ) + cleanerInterval ;
47- connections [ id ] . messages = [ ] ;
45+ const key = ctx . params . id ;
46+ if ( connections [ key ] ) {
47+ ctx . body = connections [ key ] . stream = new PassThrough ( ) ;
48+ const { stream, messages } = connections [ key ] ;
49+ connections [ key ] . expiry = Date . now ( ) + cleanerInterval ;
50+ connections [ key ] . messages = [ ] ;
4851 stream . write ( `data: ${ JSON . stringify ( messages ) } \n\n` ) ;
4952 stream . addListener ( 'close' , ( ) => {
50- // 清空SSE流,以便定期清理
51- if ( connections [ id ] ) {
52- connections [ id ] . expiry = Date . now ( ) + cleanerInterval ;
53- connections [ id ] . stream = null ;
53+ if ( connections [ key ] ) {
54+ connections [ key ] . stream = null ;
55+ // SSE 断开说明页面已关闭,立即关闭内部 WS,触发 channel 清理
56+ connections [ key ] . socket . close ( ) ;
57+ delete connections [ key ] ;
5458 }
5559 } ) ;
5660 }
5761 } ) ;
62+
5863 router . post ( '/target/:id' , async ctx => {
59- const id = ctx . params . id ;
64+ const { id } = ctx . params ;
6065 const data = ctx . request . body as string [ ] ;
6166 // 初始化连接,记得干掉之前的连接
6267 if ( data [ 0 ] === 'connect' ) {
6368 if ( connections [ id ] ) {
6469 connections [ id ] . socket . terminate ( ) ;
6570 await new Promise ( resolve => {
66- // 延迟一下,防止收到上个页面的调试消息
67- connections [ id ] . socket . onclose = ( ) => setTimeout ( resolve , 1000 ) ;
71+ connections [ id ] . socket . once ( 'close' , ( ) => setTimeout ( resolve , 1000 ) ) ;
6872 } ) ;
6973 }
7074 connections [ id ] = {
71- socket : new WebSocket ( `ws://0.0.0.0:${ ctx . socket . localPort } ${ ctx . url } ` ) ,
75+ socket : new WebSocket ( `ws://0.0.0.0:${ ctx . socket . localPort } /target/ ${ id } ` ) ,
7276 stream : null ,
7377 expiry : 0 ,
7478 messages : [ ] ,
7579 } ;
76- connections [ id ] . socket . onmessage = ( { data } ) => {
80+ connections [ id ] . socket . on ( 'message' , ( data ) => {
7781 if ( connections [ id ] ) {
7882 connections [ id ] . messages . push ( data ) ;
7983 if ( connections [ id ] . stream ) { // 如果支持SSE,直接推送
@@ -83,16 +87,95 @@ export function listenHttpSocket(router: Router) {
8387 stream . write ( `data: ${ JSON . stringify ( messages ) } \n\n` ) ;
8488 }
8589 }
86- } ;
90+ } ) ;
8791 initCleaner ( ) ;
88- await new Promise ( resolve => connections [ id ] . socket . onopen = resolve ) ;
92+ await new Promise ( resolve => connections [ id ] . socket . once ( 'open' , resolve ) ) ;
8993 }
90- // 处理消息
94+ // 处理消息(connect 为内部指令,不转发)
9195 const { socket, messages } = connections [ id ] ;
9296 connections [ id ] . expiry = Date . now ( ) + cleanerInterval * 3 ;
9397 connections [ id ] . messages = [ ] ;
94- data . forEach ( ( message ) => socket . send ( message ) ) ;
98+ data . filter ( msg => msg !== 'connect' ) . forEach ( ( message ) => socket . send ( message ) ) ;
9599 // 返回缓存的消息
96100 ctx . body = JSON . stringify ( messages ) ;
97101 } ) ;
102+ }
103+
104+ // ─── 控制端降级接口(请求-响应模式)──────────────────────────────────────────
105+
106+ type PendingRequest = { resolve : ( value : any ) => void , timer : NodeJS . Timeout } ;
107+ type DevtoolEntry = { socket : WebSocket , cmdId : number , pending : Map < number , PendingRequest > } ;
108+ const devtoolConnections : Record < string , DevtoolEntry > = { } ;
109+
110+ function listenDevtoolHttpSocket ( router : Router ) {
111+ // POST /devtool/:id — 建立连接或发送 CDP 命令,等待响应后返回
112+ router . post ( '/devtool/:id' , async ctx => {
113+ const { id } = ctx . params ;
114+ const { method, params, timeout = 30000 } = ctx . request . body as {
115+ method : string , params ?: Record < string , any > , timeout ?: number ,
116+ } ;
117+
118+ // 初始化到 devtool WS 的连接
119+ if ( ! devtoolConnections [ id ] ) {
120+ const socket = new WebSocket ( `ws://0.0.0.0:${ ctx . socket . localPort } /devtool?targetId=${ id } ` ) ;
121+ const entry : DevtoolEntry = { socket, cmdId : 0 , pending : new Map ( ) } ;
122+ devtoolConnections [ id ] = entry ;
123+
124+ socket . on ( 'message' , ( data ) => {
125+ try {
126+ const msg = JSON . parse ( data . toString ( ) ) ;
127+ const req = entry . pending . get ( msg . id ) ;
128+ if ( req ) {
129+ clearTimeout ( req . timer ) ;
130+ entry . pending . delete ( msg . id ) ;
131+ req . resolve ( msg ) ;
132+ }
133+ } catch { /* 忽略非 JSON 消息 */ }
134+ } ) ;
135+
136+ socket . on ( 'close' , ( ) => {
137+ // 连接断开时,清理所有 pending 请求
138+ entry . pending . forEach ( ( { resolve, timer } ) => {
139+ clearTimeout ( timer ) ;
140+ resolve ( { error : 'connection closed' } ) ;
141+ } ) ;
142+ entry . pending . clear ( ) ;
143+ delete devtoolConnections [ id ] ;
144+ } ) ;
145+
146+ await new Promise < void > ( ( resolve , reject ) => {
147+ socket . once ( 'open' , ( ) => resolve ( ) ) ;
148+ socket . once ( 'error' , ( e ) => reject ( e ) ) ;
149+ } ) ;
150+ }
151+
152+ if ( ! method ) {
153+ ctx . status = 400 ;
154+ ctx . body = { error : 'Missing method' } ;
155+ return ;
156+ }
157+
158+ const entry = devtoolConnections [ id ] ;
159+ const cmdId = ++ entry . cmdId ;
160+
161+ try {
162+ const result = await new Promise < any > ( ( resolve , reject ) => {
163+ const timer = setTimeout ( ( ) => {
164+ entry . pending . delete ( cmdId ) ;
165+ reject ( new Error ( 'timeout' ) ) ;
166+ } , timeout ) ;
167+ entry . pending . set ( cmdId , { resolve, timer } ) ;
168+ entry . socket . send ( JSON . stringify ( { id : cmdId , method, params : params || { } } ) ) ;
169+ } ) ;
170+ ctx . body = { success : true , ...result } ;
171+ } catch ( err ) {
172+ ctx . status = 504 ;
173+ ctx . body = { success : false , error : err . message } ;
174+ }
175+ } ) ;
176+ }
177+
178+ export function listenHttpSocket ( router : Router ) {
179+ listenTargetHttpSocket ( router ) ;
180+ listenDevtoolHttpSocket ( router ) ;
98181}
0 commit comments