fix: 第二轮深度调试修复 - useSpeechSynthesis守卫+NPE防护+E2E测试完善

- P0: useSpeechSynthesis.ts cancel()增加isSupported守卫
- P0: iot_provider.go 添加loader nil检查防止NPE panic
- 新增CDP E2E v4测试脚本 14项全绿通过
- 生成第二轮修复报告 docs/debug/2026-05-21-round2-fixes.md
This commit is contained in:
2026-05-22 00:10:37 +08:00
parent a058b0ab8e
commit b15e1c9541
7 changed files with 1236 additions and 8 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""调查 test-token-cyrene 的来源——检查 localStorage 和前端代码行为"""
import json, time, urllib.request
from websocket import create_connection
# 先获取真实 JWT
print("=== Step 0: 获取真实 JWT ===")
login_req = urllib.request.Request(
'http://localhost:8080/api/v1/auth/login',
data=json.dumps({"username": "yeij0942", "password": "Jiang1143218570"}).encode(),
headers={"Content-Type": "application/json"},
method='POST'
)
try:
login_resp = urllib.request.urlopen(login_req, timeout=10)
login_data = json.loads(login_resp.read())
real_token = login_data.get('data', {}).get('access_token', '')
print(f" Real token (first 20 chars): {real_token[:20]}...")
print(f" Login success: {bool(real_token)}")
except Exception as e:
print(f" Login failed: {e}")
real_token = ""
# 连接到 Chromium
print("\n=== Step 1: 创建新页面(about:blank,无旧 localStorage===")
req = urllib.request.Request('http://127.0.0.1:9225/json/new?url=about:blank', method='PUT')
resp = urllib.request.urlopen(req, timeout=10)
page = json.loads(resp.read())
ws_url = page['webSocketDebuggerUrl']
print(f" Page ID: {page.get('id')}")
print(f" URL: {page.get('url')}")
ws = create_connection(ws_url, timeout=10)
def cdp(method, params=None, msg_id=1):
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
def recv_all(timeout=3):
ws.settimeout(timeout)
msgs = []
try:
while True:
msgs.append(ws.recv())
except:
pass
return msgs
# 启用 domains
cdp("Page.enable", {}, 1)
cdp("Network.enable", {}, 2)
cdp("Runtime.enable", {}, 3)
time.sleep(0.5)
recv_all(0.5)
# === 检查 about:blank 的 localStorage ===
print("\n=== Step 2: 检查 about:blank localStorage(应为空)===")
cdp("Runtime.evaluate", {
"expression": "JSON.stringify({token: localStorage.getItem('token'), allKeys: Object.keys(localStorage)})"
}, 10)
time.sleep(0.5)
msgs = recv_all(1)
for m in msgs:
d = json.loads(m)
if d.get('id') == 10:
val = d.get('result', {}).get('result', {}).get('value', '')
print(f" about:blank localStorage: {val}")
# 如果有 token 就清除
cdp("Runtime.evaluate", {"expression": "localStorage.clear(); 'cleared'"}, 11)
time.sleep(0.5)
recv_all(0.5)
# === 导航到 5199 ===
print("\n=== Step 3: 导航到 localhost:5199 ===")
cdp("Page.navigate", {"url": "http://localhost:5199/"}, 20)
time.sleep(5)
all_msgs = recv_all(3)
# 分析网络请求
print("\n=== Step 4: 网络请求分析 ===")
for m in all_msgs:
d = json.loads(m)
method = d.get('method', '')
params = d.get('params', {})
if method == 'Network.requestWillBeSent':
req = params.get('request', {})
headers = req.get('headers', {})
auth = headers.get('Authorization', '') or headers.get('authorization', '')
url_short = req.get('url', '')[:100]
if auth:
print(f" AUTH: {req.get('method','?')} {url_short}")
print(f" Authorization: {auth[:80]}")
elif '/api/' in url_short:
print(f" NO_AUTH: {req.get('method','?')} {url_short}")
elif method == 'Runtime.consoleAPICalled':
args = params.get('args', [])
texts = [str(a.get('value', '') or a.get('description', ''))[:150] for a in args]
msg_type = params.get('type', 'log')
print(f" [CONSOLE:{msg_type}] {' '.join(texts)}")
elif method == 'Runtime.exceptionThrown':
exc = params.get('exceptionDetails', {})
print(f" [EXCEPTION] {exc.get('text', '')} at line {exc.get('lineNumber', '')}")
# === 检查 localStorage ===
print("\n=== Step 5: 检查导航后 localStorage ===")
cdp("Runtime.evaluate", {
"expression": "JSON.stringify({token: localStorage.getItem('token'), refresh_token: localStorage.getItem('refresh_token'), user_id: localStorage.getItem('user_id'), allKeys: Object.keys(localStorage)})"
}, 30)
time.sleep(0.5)
msgs = recv_all(2)
for m in msgs:
d = json.loads(m)
if d.get('id') == 30:
val = d.get('result', {}).get('result', {}).get('value', '')
print(f" localStorage: {val}")
# === 如果 token 是 test-token-cyrene,手动覆盖为真实 token 并测试 ===
print("\n=== Step 6: 注入真实 token ===")
if real_token:
cdp("Runtime.evaluate", {
"expression": f"localStorage.setItem('token', '{real_token}'); 'done'"
}, 40)
time.sleep(0.5)
recv_all(1)
# 重新加载
cdp("Page.reload", {}, 41)
time.sleep(4)
msgs = recv_all(3)
# 查看请求
print(" 重新加载后的网络请求:")
for m in msgs:
d = json.loads(m)
method = d.get('method', '')
params = d.get('params', {})
if method == 'Network.requestWillBeSent':
req = params.get('request', {})
headers = req.get('headers', {})
auth = headers.get('Authorization', '') or headers.get('authorization', '')
url_short = req.get('url', '')[:100]
if auth or '/api/' in url_short:
if auth:
print(f" AUTH: {req.get('method','?')} {url_short} -> {auth[:80]}")
else:
print(f" NO_AUTH: {req.get('method','?')} {url_short}")
elif method == 'Runtime.consoleAPICalled':
args = params.get('args', [])
texts = [str(a.get('value', '') or a.get('description', ''))[:150] for a in args]
print(f" [CONSOLE:{params.get('type','log')}] {' '.join(texts)}")
ws.close()
print("\nDone.")