Skip to content

Commit 6d5f2b7

Browse files
committed
重写小程序launch()五阶段流程:cli重启TCP探测WS端口启桥接HTTP等HTTP就绪POST connect主动触发连接
1 parent 4a4c67f commit 6d5f2b7

3 files changed

Lines changed: 202 additions & 68 deletions

File tree

src/controller/miniprogram.py

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,18 @@ def _next_step(self) -> int:
142142
async def launch(self) -> None:
143143
"""启动小程序自动化连接(长连接模式)。
144144
145-
完整流程(参考盲测v4/v5跑通的经验):
145+
完整流程(参考盲测v4/v5和test_connect.js跑通的经验):
146146
1. cli close 关闭开发者工具中的项目(防止端口占用)
147147
2. cli open 重新打开项目
148148
3. cli auto --auto-port 9420 启动自动化,指定固定端口
149-
4. 启动桥接服务器连接该端口
150-
5. 桥接服务器中导航全部使用evaluate(wx.xxx())原生API(SDK方法会超时!)
149+
4. 等待WebSocket端口就绪(TCP探测)
150+
5. 启动桥接服务器(HTTP端口9421)
151+
6. 等HTTP服务器就绪 → POST connect命令主动触发连接
152+
7. 桥接服务器中导航全部使用evaluate(wx.xxx())原生API(SDK方法会超时!)
151153
"""
154+
import socket
155+
import urllib.request
156+
152157
if not self._config.project_path:
153158
raise RuntimeError("请指定小程序项目路径 (project_path)")
154159

@@ -159,48 +164,50 @@ async def launch(self) -> None:
159164
if not cli_path:
160165
raise RuntimeError("未找到微信开发者工具cli,请安装微信开发者工具并确保cli.bat在默认路径")
161166

162-
# ── 强制重启小程序(关键!不重启可能端口不通或状态不干净) ──
163-
logger.info("强制重启小程序 | cli: {} | 项目: {}", cli_path, project_path)
164-
165-
# 步骤1: cli close 关闭项目
166-
try:
167-
subprocess.run(
168-
[cli_path, "close", "--project", project_path],
169-
capture_output=True, timeout=15, encoding="utf-8", errors="replace",
170-
)
171-
logger.info("cli close 完成")
172-
except Exception as e:
173-
logger.warning("cli close 失败(可忽略): {}", e)
174-
await asyncio.sleep(2)
167+
# ═══ 阶段1: 强制重启小程序 ═══
168+
logger.info("═══ 阶段1: 强制重启小程序 ═══")
169+
logger.info("cli: {} | 项目: {}", cli_path, project_path)
175170

176-
# 步骤2: cli open 打开项目
177-
try:
178-
subprocess.run(
179-
[cli_path, "open", "--project", project_path],
180-
capture_output=True, timeout=15, encoding="utf-8", errors="replace",
181-
)
182-
logger.info("cli open 完成")
183-
except Exception as e:
184-
logger.warning("cli open 失败: {}", e)
185-
await asyncio.sleep(2)
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)
182+
await asyncio.sleep(3)
183+
184+
# ═══ 阶段2: 等待WebSocket端口就绪(TCP探测,最多等15秒) ═══
185+
logger.info("═══ 阶段2: 等待WS端口 {} 就绪 ═══", ws_port)
186+
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()
193+
port_ready = True
194+
logger.info("WS端口 {} 已就绪(第{}秒)", ws_port, i + 1)
195+
break
196+
except Exception:
197+
s.close()
198+
await asyncio.sleep(1)
186199

187-
# 步骤3: cli auto 启动自动化,指定端口
188-
try:
189-
subprocess.run(
190-
[cli_path, "auto", "--project", project_path, "--auto-port", str(ws_port)],
191-
capture_output=True, timeout=15, encoding="utf-8", errors="replace",
200+
if not port_ready:
201+
raise RuntimeError(
202+
f"WebSocket端口 {ws_port} 未就绪(等了15秒)。"
203+
f"请检查微信开发者工具是否已打开项目并开启了服务端口。"
192204
)
193-
logger.info("cli auto --auto-port {} 完成", ws_port)
194-
except Exception as e:
195-
logger.warning("cli auto 失败: {}", e)
196-
# cli auto后开发者工具需要较长时间准备WebSocket端口(首次可能弹信任确认框)
197-
await asyncio.sleep(5)
198205

199-
# ── 启动桥接服务器连接 ──
206+
# ═══ 阶段3: 启动桥接服务器 ═══
200207
self._http_port = 9421
201208
self._http_base = f"http://127.0.0.1:{self._http_port}"
202209

203-
logger.info("启动桥接服务器 | WS端口: {} | HTTP端口: {}", ws_port, self._http_port)
210+
logger.info("═══ 阶段3: 启动桥接服务器 ═══ WS:{} HTTP:{}", ws_port, self._http_port)
204211

205212
bridge_server = Path(__file__).parent / "miniprogram_bridge_server.js"
206213
if not bridge_server.exists():
@@ -212,31 +219,63 @@ async def launch(self) -> None:
212219
stderr=subprocess.PIPE,
213220
)
214221

215-
# 等待服务器启动并连接(最多等30秒)
216-
# 桥接服务器内部也有重试逻辑(6次×3秒),所以Python端要等够时间
217-
import urllib.request
218-
for i in range(60):
222+
# ═══ 阶段4: 等HTTP服务器就绪(最多5秒) ═══
223+
logger.info("═══ 阶段4: 等待HTTP服务器就绪 ═══")
224+
http_ready = False
225+
for i in range(10):
219226
await asyncio.sleep(0.5)
220227
try:
221228
req = urllib.request.Request(self._http_base)
222229
with urllib.request.urlopen(req, timeout=2) as resp:
230+
json.loads(resp.read()) # 只要能响应就行,不管connected状态
231+
http_ready = True
232+
logger.info("HTTP服务器已就绪(第{:.1f}秒)", (i + 1) * 0.5)
233+
break
234+
except Exception:
235+
continue
236+
237+
if not http_ready:
238+
self._kill_bridge()
239+
raise RuntimeError("桥接服务器HTTP端口9421未响应")
240+
241+
# ═══ 阶段5: POST connect命令,主动触发连接(重试5次×3秒) ═══
242+
logger.info("═══ 阶段5: 发送connect命令 ═══")
243+
for i in range(5):
244+
try:
245+
payload = json.dumps({"action": "connect", "params": {}}).encode("utf-8")
246+
req = urllib.request.Request(
247+
self._http_base, data=payload,
248+
headers={"Content-Type": "application/json"}, method="POST",
249+
)
250+
with urllib.request.urlopen(req, timeout=10) as resp:
223251
data = json.loads(resp.read())
224-
if data.get("connected"):
252+
if data.get("success"):
225253
self._connected = True
226254
self._device.is_connected = True
227255
self._device.extra = {"project": project_path, "port": ws_port}
228-
logger.info("小程序自动化已连接(长连接模式) | 端口: {}", ws_port)
256+
logger.info("小程序自动化已连接 | 端口: {} | 第{}次尝试", ws_port, i + 1)
229257
return
230-
except Exception:
231-
continue
232-
233-
# 如果循环结束还没连接成功
234-
if self._bridge_proc.poll() is not None:
235-
stdout = self._bridge_proc.stdout.read().decode("utf-8", errors="replace") if self._bridge_proc.stdout else ""
236-
stderr = self._bridge_proc.stderr.read().decode("utf-8", errors="replace") if self._bridge_proc.stderr else ""
237-
raise RuntimeError(f"桥接服务器启动失败:\n{stdout}\n{stderr}")
258+
else:
259+
logger.warning("connect返回失败: {}(第{}次)", data.get("error", ""), i + 1)
260+
except Exception as e:
261+
logger.warning("connect请求异常: {}(第{}次)", e, i + 1)
262+
await asyncio.sleep(3)
263+
264+
self._kill_bridge()
265+
raise RuntimeError(
266+
f"桥接服务器connect命令5次均失败。WS端口{ws_port}已确认打开,"
267+
f"但automator连接失败。请确认开发者工具已打开项目。"
268+
)
238269

239-
raise RuntimeError("桥接服务器启动超时(30秒),请确认微信开发者工具已安装并开启服务端口")
270+
def _kill_bridge(self) -> None:
271+
"""清理桥接服务器进程。"""
272+
if self._bridge_proc and self._bridge_proc.poll() is None:
273+
self._bridge_proc.terminate()
274+
try:
275+
self._bridge_proc.wait(timeout=3)
276+
except Exception:
277+
self._bridge_proc.kill()
278+
self._bridge_proc = None
240279

241280
async def close(self) -> None:
242281
"""关闭小程序自动化连接。"""

src/controller/miniprogram_bridge_server.js

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -221,23 +221,13 @@ const server = http.createServer(async (req, res) => {
221221
});
222222
});
223223

224-
server.listen(HTTP_PORT, '127.0.0.1', async () => {
224+
server.listen(HTTP_PORT, '127.0.0.1', () => {
225225
console.log('========================================');
226-
console.log(' 小程序自动化桥接服务器 v8.2');
226+
console.log(' 小程序自动化桥接服务器 v8.3');
227227
console.log(` HTTP 端口: ${HTTP_PORT}`);
228228
console.log(` WebSocket 端口: ${WS_PORT}`);
229229
console.log('========================================');
230-
231-
// 启动时自动连接(重试最多6次,每次等3秒,共18秒)
232-
// cli auto执行后开发者工具需要时间准备WebSocket端口
233-
for (let retry = 0; retry < 6; retry++) {
234-
const ok = await ensureConnected();
235-
if (ok) {
236-
console.log(`[OK] 连接成功(第${retry + 1}次尝试)`);
237-
return;
238-
}
239-
console.log(`[RETRY] 连接失败,等待3秒后重试 ${retry + 1}/6...`);
240-
await new Promise(r => setTimeout(r, 3000));
241-
}
242-
console.error('[FAIL] 6次重试均失败,等待手动连接或请求时重试');
230+
console.log('[READY] HTTP服务器已启动,等待connect命令...');
231+
// 不在启动时自动连接!由Python端通过POST connect命令主动触发
232+
// 这样HTTP服务器能立即响应Python的健康检查
243233
});

test_connect.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* 诊断脚本:测试微信开发者工具自动化连接
3+
*/
4+
const { execSync } = require('child_process');
5+
const automator = require('miniprogram-automator');
6+
7+
const CLI = 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat';
8+
const PROJECT = 'D:\\projects\\TestPilotAI\\miniprogram-demo';
9+
const WS_PORT = 9420;
10+
11+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
12+
13+
async function main() {
14+
// 步骤1: close
15+
console.log('[1] cli close...');
16+
try { execSync(`"${CLI}" close --project "${PROJECT}"`, { timeout: 15000, stdio: 'pipe' }); } catch(e) {}
17+
console.log('[1] done');
18+
await sleep(3000);
19+
20+
// 步骤2: open
21+
console.log('[2] cli open...');
22+
try {
23+
const r = execSync(`"${CLI}" open --project "${PROJECT}"`, { timeout: 15000, encoding: 'utf8', stdio: 'pipe' });
24+
console.log('[2] output:', r.trim());
25+
} catch(e) {
26+
console.log('[2] stderr:', (e.stderr || '').toString().substring(0, 300));
27+
}
28+
await sleep(3000);
29+
30+
// 步骤3: auto --auto-port
31+
console.log('[3] cli auto --auto-port ' + WS_PORT + '...');
32+
try {
33+
const r = execSync(`"${CLI}" auto --project "${PROJECT}" --auto-port ${WS_PORT}`, { timeout: 15000, encoding: 'utf8', stdio: 'pipe' });
34+
console.log('[3] output:', r.trim());
35+
} catch(e) {
36+
console.log('[3] stderr:', (e.stderr || '').toString().substring(0, 300));
37+
}
38+
await sleep(3000);
39+
40+
// 步骤4: 检查端口
41+
console.log('[4] 检查端口 ' + WS_PORT + '...');
42+
const net = require('net');
43+
const portOpen = await new Promise(resolve => {
44+
const s = net.createConnection(WS_PORT, '127.0.0.1');
45+
s.setTimeout(2000);
46+
s.on('connect', () => { s.destroy(); resolve(true); });
47+
s.on('error', () => resolve(false));
48+
s.on('timeout', () => { s.destroy(); resolve(false); });
49+
});
50+
console.log('[4] 端口 ' + WS_PORT + (portOpen ? ' 已打开' : ' 未打开'));
51+
52+
// 步骤5: 尝试连接automator
53+
console.log('[5] 尝试连接 ws://localhost:' + WS_PORT + '...');
54+
for (let i = 0; i < 5; i++) {
55+
try {
56+
const mp = await Promise.race([
57+
automator.connect({ wsEndpoint: `ws://localhost:${WS_PORT}` }),
58+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout 5s')), 5000))
59+
]);
60+
const page = await mp.currentPage();
61+
console.log('[5] 连接成功! 页面:', page.path);
62+
await mp.disconnect();
63+
return;
64+
} catch(e) {
65+
console.log(`[5] 第${i+1}次失败: ${e.message}`);
66+
await sleep(3000);
67+
}
68+
}
69+
70+
// 步骤6: 如果9420不行,扫描其他端口
71+
console.log('[6] 扫描其他端口...');
72+
try {
73+
const r = execSync('netstat -ano | findstr LISTENING | findstr 127.0.0.1', { encoding: 'utf8', timeout: 5000 });
74+
const ports = [];
75+
r.split('\n').forEach(line => {
76+
const m = line.match(/127\.0\.0\.1:(\d+)/);
77+
if (m) {
78+
const p = parseInt(m[1]);
79+
if (p > 9000 && p < 65535 && p !== 9421 && p !== 8900) ports.push(p);
80+
}
81+
});
82+
const unique = [...new Set(ports)].sort((a,b) => a-b);
83+
console.log('[6] 候选端口:', unique.join(', '));
84+
85+
for (const port of unique.slice(0, 10)) {
86+
try {
87+
const mp = await Promise.race([
88+
automator.connect({ wsEndpoint: `ws://localhost:${port}` }),
89+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 3000))
90+
]);
91+
const page = await mp.currentPage();
92+
console.log(`[6] 端口 ${port} 连接成功! 页面: ${page.path}`);
93+
await mp.disconnect();
94+
return;
95+
} catch(e) {
96+
// skip
97+
}
98+
}
99+
console.log('[6] 所有候选端口均失败');
100+
} catch(e) {
101+
console.log('[6] 扫描失败:', e.message);
102+
}
103+
}
104+
105+
main().catch(e => console.error('Fatal:', e.message));

0 commit comments

Comments
 (0)