@@ -139,17 +139,31 @@ def _next_step(self) -> int:
139139
140140 # ── BaseController 实现 ──────────────────────
141141
142+ def _run_cli (self , cmd_name : str , args : list , timeout : int = 15 ) -> bool :
143+ """执行CLI命令,返回是否成功。"""
144+ try :
145+ r = subprocess .run (
146+ args , capture_output = True , timeout = timeout ,
147+ encoding = "utf-8" , errors = "replace" ,
148+ )
149+ logger .info ("cli {} 完成 (rc={})" , cmd_name , r .returncode )
150+ return True
151+ except Exception as e :
152+ logger .warning ("cli {} 失败: {}" , cmd_name , e )
153+ return False
154+
142155 async def launch (self ) -> None :
143- """启动小程序自动化连接(长连接模式)。
144-
145- 完整流程(参考盲测v4/v5和test_connect.js跑通的经验):
146- 1. cli close 关闭开发者工具中的项目(防止端口占用)
147- 2. cli open 重新打开项目
148- 3. cli auto --auto-port 9420 启动自动化,指定固定端口
149- 4. 等待WebSocket端口就绪(TCP探测)
150- 5. 启动桥接服务器(HTTP端口9421)
151- 6. 等HTTP服务器就绪 → POST connect命令主动触发连接
152- 7. 桥接服务器中导航全部使用evaluate(wx.xxx())原生API(SDK方法会超时!)
156+ """启动小程序自动化连接(长连接模式 v8.3)。
157+
158+ 流程(参考test_connect.js和官方CLI文档):
159+ 阶段1: cli auto 开启自动化(若项目未打开则先open再auto)
160+ 阶段2: TCP探测WS端口就绪(最多20秒)
161+ 阶段3: 启动桥接HTTP服务器
162+ 阶段4: 等HTTP就绪
163+ 阶段5: POST connect触发automator连接(重试8次x5秒=40秒)
164+
165+ 重要:官方文档要求先在设置→安全设置中开启服务端口!
166+ 导航全部使用evaluate(wx.xxx())原生API(SDK方法会超时!)
153167 """
154168 import socket
155169 import urllib .request
@@ -162,45 +176,52 @@ async def launch(self) -> None:
162176 project_path = self ._config .project_path
163177
164178 if not cli_path :
165- raise RuntimeError ("未找到微信开发者工具cli,请安装微信开发者工具并确保cli.bat在默认路径" )
166-
167- # ═══ 阶段1: 强制重启小程序 ═══
168- logger .info ("═══ 阶段1: 强制重启小程序 ═══" )
169- logger .info ("cli: {} | 项目: {}" , cli_path , project_path )
179+ raise RuntimeError (
180+ "未找到微信开发者工具cli,请安装微信开发者工具"
181+ )
170182
171- for cmd_name , cmd_args in [
172- ("close" , [cli_path , "close" , "--project" , project_path ]),
173- ("open" , [cli_path , "open" , "--project" , project_path ]),
174- ("auto" , [cli_path , "auto" , "--project" , project_path , "--auto-port" , str (ws_port )]),
175- ]:
176- try :
177- subprocess .run (cmd_args , capture_output = True , timeout = 15 ,
178- encoding = "utf-8" , errors = "replace" )
179- logger .info ("cli {} 完成" , cmd_name )
180- except Exception as e :
181- logger .warning ("cli {} 失败: {}" , cmd_name , e )
183+ # ═══ 阶段1: 启动自动化 ═══
184+ logger .info ("═══ 阶段1: 启动自动化 ═══" )
185+ logger .info ("cli: {} | 项目: {} | 端口: {}" , cli_path , project_path , ws_port )
186+
187+ # 先尝试直接 cli auto(如果项目已打开,这步就够了)
188+ auto_ok = self ._run_cli ("auto" , [
189+ cli_path , "auto" , "--project" , project_path ,
190+ "--auto-port" , str (ws_port ),
191+ ])
192+ await asyncio .sleep (3 )
193+
194+ # 检查端口是否已通
195+ port_ok = self ._check_tcp_port (ws_port )
196+
197+ if not port_ok :
198+ # 项目可能没打开,先open再auto
199+ logger .info ("端口未通,尝试 cli open + cli auto ..." )
200+ self ._run_cli ("open" , [cli_path , "open" , "--project" , project_path ])
201+ # open后需要等模拟器启动(关键!模拟器需要时间加载)
202+ logger .info ("等待模拟器启动(10秒)..." )
203+ await asyncio .sleep (10 )
204+ self ._run_cli ("auto" , [
205+ cli_path , "auto" , "--project" , project_path ,
206+ "--auto-port" , str (ws_port ),
207+ ])
182208 await asyncio .sleep (3 )
183209
184- # ═══ 阶段2: 等待WebSocket端口就绪 (TCP探测,最多等15秒 ) ═══
210+ # ═══ 阶段2: 等待WS端口就绪 (TCP探测,最多20秒 ) ═══
185211 logger .info ("═══ 阶段2: 等待WS端口 {} 就绪 ═══" , ws_port )
186212 port_ready = False
187- for i in range (15 ):
188- try :
189- s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
190- s .settimeout (1 )
191- s .connect (("127.0.0.1" , ws_port ))
192- s .close ()
213+ for i in range (20 ):
214+ if self ._check_tcp_port (ws_port ):
193215 port_ready = True
194216 logger .info ("WS端口 {} 已就绪(第{}秒)" , ws_port , i + 1 )
195217 break
196- except Exception :
197- s .close ()
198- await asyncio .sleep (1 )
218+ await asyncio .sleep (1 )
199219
200220 if not port_ready :
201221 raise RuntimeError (
202- f"WebSocket端口 { ws_port } 未就绪(等了15秒)。"
203- f"请检查微信开发者工具是否已打开项目并开启了服务端口。"
222+ f"WebSocket端口 { ws_port } 未就绪(等了20秒)。\n "
223+ f"请确认:1) 微信开发者工具已打开项目 "
224+ f"2) 设置→安全设置→已开启服务端口"
204225 )
205226
206227 # ═══ 阶段3: 启动桥接服务器 ═══
@@ -227,9 +248,9 @@ async def launch(self) -> None:
227248 try :
228249 req = urllib .request .Request (self ._http_base )
229250 with urllib .request .urlopen (req , timeout = 2 ) as resp :
230- json .loads (resp .read ()) # 只要能响应就行,不管connected状态
251+ json .loads (resp .read ())
231252 http_ready = True
232- logger .info ("HTTP服务器已就绪(第 {:.1f}秒)" , (i + 1 ) * 0.5 )
253+ logger .info ("HTTP服务器已就绪({:.1f}秒)" , (i + 1 ) * 0.5 )
233254 break
234255 except Exception :
235256 continue
@@ -238,35 +259,54 @@ async def launch(self) -> None:
238259 self ._kill_bridge ()
239260 raise RuntimeError ("桥接服务器HTTP端口9421未响应" )
240261
241- # ═══ 阶段5: POST connect命令,主动触发连接(重试5次×3秒) ═══
242- logger .info ("═══ 阶段5: 发送connect命令 ═══" )
243- for i in range (5 ):
262+ # ═══ 阶段5: POST connect(重试8次x5秒=40秒) ═══
263+ logger .info ("═══ 阶段5: 发送connect命令(最多重试8次) ═══" )
264+ last_err = ""
265+ for i in range (8 ):
244266 try :
245- payload = json .dumps ({"action" : "connect" , "params" : {}}).encode ("utf-8" )
267+ payload = json .dumps ({"action" : "connect" , "params" : {}}).encode ()
246268 req = urllib .request .Request (
247269 self ._http_base , data = payload ,
248270 headers = {"Content-Type" : "application/json" }, method = "POST" ,
249271 )
250- with urllib .request .urlopen (req , timeout = 10 ) as resp :
272+ with urllib .request .urlopen (req , timeout = 15 ) as resp :
251273 data = json .loads (resp .read ())
252274 if data .get ("success" ):
253275 self ._connected = True
254276 self ._device .is_connected = True
255277 self ._device .extra = {"project" : project_path , "port" : ws_port }
256- logger .info ("小程序自动化已连接 | 端口: {} | 第{}次尝试 " , ws_port , i + 1 )
278+ logger .info ("小程序自动化已连接 | 端口:{} | 第{}次 " , ws_port , i + 1 )
257279 return
258- else :
259- logger .warning ("connect返回失败 : {}(第{}次)" , data . get ( "error" , "" ) , i + 1 )
280+ last_err = data . get ( "error" , "未知错误" )
281+ logger .warning ("connect失败 : {}(第{}次)" , last_err , i + 1 )
260282 except Exception as e :
261- logger .warning ("connect请求异常: {}(第{}次)" , e , i + 1 )
262- await asyncio .sleep (3 )
283+ last_err = str (e )
284+ logger .warning ("connect异常: {}(第{}次)" , last_err , i + 1 )
285+ await asyncio .sleep (5 )
263286
264287 self ._kill_bridge ()
265288 raise RuntimeError (
266- f"桥接服务器connect命令5次均失败。WS端口 { ws_port } 已确认打开, "
267- f"但automator连接失败。请确认开发者工具已打开项目 。"
289+ f"automator连接8次均失败(最后错误: { last_err } )。 \n "
290+ f"WS端口 { ws_port } 已通但连接失败。请确认模拟器已完全启动 。"
268291 )
269292
293+ @staticmethod
294+ def _check_tcp_port (port : int , host : str = "127.0.0.1" ) -> bool :
295+ """检查TCP端口是否可连接。"""
296+ import socket
297+ try :
298+ s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
299+ s .settimeout (1 )
300+ s .connect ((host , port ))
301+ s .close ()
302+ return True
303+ except Exception :
304+ try :
305+ s .close ()
306+ except Exception :
307+ pass
308+ return False
309+
270310 def _kill_bridge (self ) -> None :
271311 """清理桥接服务器进程。"""
272312 if self ._bridge_proc and self ._bridge_proc .poll () is None :
0 commit comments