fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置

- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
This commit is contained in:
2026-05-21 23:10:07 +08:00
parent 8b7d4ec19a
commit a058b0ab8e
53 changed files with 5535 additions and 241 deletions
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""第16轮诊断:IoT&A 自动化规则引擎 + 控件CDP 测试脚本"""
import json, urllib.request, urllib.error, sys, time, subprocess
GATEWAY = "http://localhost:8080"
CDP = "http://127.0.0.1:9225"
def req(method, path, body=None, headers=None):
url = f"{GATEWAY}{path}"
if headers is None:
headers = {}
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
else:
data = None
try:
r = urllib.request.Request(url, data=data, headers=headers, method=method)
resp = urllib.request.urlopen(r, timeout=10)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode()
try:
return json.loads(body)
except:
return {"error": body, "status": e.code}
except Exception as e:
return {"error": str(e)}
def get_token():
result = req("POST", "/api/v1/auth/login", {"username": "yeij0942", "password": "Jiang1143218570"})
return result.get("token", "")
print("=" * 60)
print("1. JWT Token 获取")
print("=" * 60)
token = get_token()
if token:
print(f"✅ Token 获取成功: {token[:40]}...")
else:
print("❌ Token 获取失败")
sys.exit(1)
auth_headers = {"Authorization": f"Bearer {token}"}
print("\n" + "=" * 60)
print("2. Automation 规则引擎 API CRUD 测试")
print("=" * 60)
# 2a. GET Rules (should be empty)
print("\n2a. GET /api/v1/automation/rules")
result = req("GET", "/api/v1/automation/rules", headers=auth_headers)
count = result.get("count", -1)
print(f" count={count} {'' if count is not None else '⚠️'}")
# 2b. POST Create Rule
print("\n2b. POST /api/v1/automation/rules (创建规则)")
create_body = {
"name": "测试规则-关灯",
"description": "每天晚上22点关闭客厅灯",
"trigger_type": "schedule",
"trigger_config": {"time": "22:00", "days": ["mon","tue","wed","thu","fri"]},
"actions": [{"type": "set_device", "device_id": "light-livingroom", "property": "status", "value": "off"}]
}
result = req("POST", "/api/v1/automation/rules", body=create_body, headers=auth_headers)
rule_id = result.get("rule", {}).get("id", "")
print(f" success={result.get('success')} rule_id={rule_id[:16] if rule_id else 'N/A'}...")
if not result.get("success"):
print(f" ERROR: {json.dumps(result, indent=2, ensure_ascii=False)[:500]}")
# 2c. GET Rules after create
if rule_id:
print("\n2c. GET /api/v1/automation/rules")
result = req("GET", "/api/v1/automation/rules", headers=auth_headers)
name0 = result.get("rules", [{}])[0].get("name", "N/A") if result.get("rules") else "N/A"
print(f" count={result.get('count')} first_name={name0}")
# 2d. GET single
print(f"\n2d. GET /api/v1/automation/rules/{rule_id}")
result = req("GET", f"/api/v1/automation/rules/{rule_id}", headers=auth_headers)
r = result.get("rule", {})
print(f" name={r.get('name')} trigger_type={r.get('trigger_type')} enabled={r.get('enabled')}")
# 2e. PUT Update
print(f"\n2e. PUT /api/v1/automation/rules/{rule_id}")
result = req("PUT", f"/api/v1/automation/rules/{rule_id}", body={"name": "测试规则-已更新", "enabled": False}, headers=auth_headers)
r = result.get("rule", {})
print(f" success={result.get('success')} name={r.get('name')} enabled={r.get('enabled')}")
# 2f. DELETE
print(f"\n2f. DELETE /api/v1/automation/rules/{rule_id}")
result = req("DELETE", f"/api/v1/automation/rules/{rule_id}", headers=auth_headers)
print(f" success={result.get('success')}")
# 2g. Verify deleted
print("\n2g. GET /api/v1/automation/rules (验证删除)")
result = req("GET", "/api/v1/automation/rules", headers=auth_headers)
print(f" count={result.get('count')} {'' if result.get('count') == 0 else '⚠️'}")
# 2h. Unauthenticated
print("\n2h. GET /api/v1/automation/rules (未认证)")
result = req("GET", "/api/v1/automation/rules")
print(f" error={result.get('error','N/A')[:60]}")
# 2i. Manual trigger
print("\n2i. POST /api/v1/automation/rules/:id/trigger (手动触发)")
cbody = {"name": "触发测试", "trigger_type": "manual", "actions": [{"type": "notify", "title": "测试", "body": "通知"}]}
result = req("POST", "/api/v1/automation/rules", body=cbody, headers=auth_headers)
trid = result.get("rule", {}).get("id", "")
if trid:
result = req("POST", f"/api/v1/automation/rules/{trid}/trigger", headers=auth_headers)
print(f" success={result.get('success')} msg={result.get('message','')}")
req("DELETE", f"/api/v1/automation/rules/{trid}", headers=auth_headers)
else:
print(" ❌ 创建失败")
# 2j. Scenes
print("\n2j. GET /api/v1/automation/scenes")
result = req("GET", "/api/v1/automation/scenes", headers=auth_headers)
print(f" count={result.get('count')}")
print("\n" + "=" * 60)
print("3. IoT 调试服务测试")
print("=" * 60)
res = json.loads(urllib.request.urlopen("http://localhost:8083/api/v1/devices").read())
print(f"3a. 设备总数: {res.get('total', 0)}")
for d in res.get("devices", []):
print(f" {d['name']} ({d['type']}): {d.get('status','')}")
print("\n3b. Toggle light-bedroom")
r = urllib.request.Request("http://localhost:8083/api/v1/devices/light-bedroom/toggle", method="POST")
result = json.loads(urllib.request.urlopen(r).read())
dev = result.get("device", {})
print(f" action={result.get('action')} {dev.get('name')} status={dev.get('status')}")
print("\n3c. Set temperature (ac-livingroom)")
r = urllib.request.Request("http://localhost:8083/api/v1/devices/ac-livingroom/set",
data=json.dumps({"field": "temperature", "value": 24}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
result = json.loads(urllib.request.urlopen(r).read())
dev = result.get("device", {})
print(f" {dev.get('name')} temperature={dev.get('temperature')}°C")
print("\n3d. History")
result = json.loads(urllib.request.urlopen("http://localhost:8083/api/v1/devices/light-bedroom/history").read())
print(f" device_id={result.get('device_id')} history={len(result.get('history',[]))} entries")
print("\n" + "=" * 60)
print("4. CDP 前端 IoT 控件验证")
print("=" * 60)
pages = json.loads(urllib.request.urlopen(f"{CDP}/json").read())
target = None
for p in pages:
if "localhost:5199" in p.get("url", ""):
target = p
break
if target:
ws_url = target.get("webSocketDebuggerUrl", "")
print(f"4a. 目标页面: {target.get('title','')[:60]}")
print(f"4b. WebSocket: {ws_url[:80]}...")
# Use websockets to execute JS
print("\n4c. CDP Runtime.evaluate 检查 IoTStatusBar")
try:
import asyncio
try:
import websockets
except ImportError:
print(" ⚠️ websockets not installed, trying pip install...")
subprocess.run([sys.executable, "-m", "pip", "install", "websockets", "-q"], timeout=30)
import websockets
async def cdp_eval():
async with websockets.connect(ws_url, max_size=2**24) as ws:
# Runtime.enable
await ws.send(json.dumps({"id": 1, "method": "Runtime.enable"}))
await asyncio.wait_for(ws.recv(), timeout=5)
# Evaluate JS
js_code = """
(function() {
var r = {};
var allDivs = document.querySelectorAll('div');
r.totalDivs = allDivs.length;
r.iotTexts = [];
for (var i = 0; i < allDivs.length; i++) {
var t = allDivs[i].textContent || '';
if (t.indexOf('IoT') !== -1 || t.indexOf('iot') !== -1) {
r.iotTexts.push(t.substring(0, 100));
if (r.iotTexts.length >= 10) break;
}
}
r.hasRoot = !!document.getElementById('root');
r.title = document.title;
r.bodyText = (document.body ? document.body.innerText : '').substring(0, 300);
return JSON.stringify(r);
})()
"""
await ws.send(json.dumps({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": js_code, "returnByValue": True}}))
resp = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(resp)
result_val = data.get("result", {}).get("result", {}).get("value", "N/A")
return result_val
result = asyncio.new_event_loop().run_until_complete(cdp_eval())
parsed = json.loads(result) if result and result != "N/A" else {}
print(f" title={parsed.get('title','')}")
print(f" hasRoot={parsed.get('hasRoot')} totalDivs={parsed.get('totalDivs')}")
print(f" iotTexts={parsed.get('iotTexts',[])}")
print(f" bodyText(first 300): {parsed.get('bodyText','')}")
except Exception as e:
print(f" ❌ CDP 错误: {e}")
else:
print("4a. ❌ 未找到 localhost:5199 页面")
for p in pages:
print(f" {p.get('url','?')[:100]}")
print("\n" + "=" * 60)
print("5. tool-engine IoT 工具执行测试")
print("=" * 60)
# 5a. iot_query
print("\n5a. tool-engine iot_query")
r = urllib.request.Request("http://localhost:8092/api/v1/tools/execute",
data=json.dumps({"tool": "iot_query", "arguments": {}}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
try:
result = json.loads(urllib.request.urlopen(r, timeout=10).read())
print(f" output: {result.get('output','')[:200]}")
if result.get('error'):
print(f" error: {result['error'][:200]}")
except Exception as e:
print(f"{e}")
# 5b. iot_control toggle
print("\n5b. tool-engine iot_control (toggle ac-livingroom)")
r = urllib.request.Request("http://localhost:8092/api/v1/tools/execute",
data=json.dumps({"tool": "iot_control", "arguments": {"device_id": "ac-livingroom", "action": "toggle"}}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
try:
result = json.loads(urllib.request.urlopen(r, timeout=10).read())
print(f" output: {result.get('output','')[:200]}")
if result.get('error'):
print(f" error: {result['error'][:200]}")
except Exception as e:
print(f"{e}")
# 5c. iot_control set_temperature
print("\n5c. tool-engine iot_control (set_temperature 28)")
r = urllib.request.Request("http://localhost:8092/api/v1/tools/execute",
data=json.dumps({"tool": "iot_control", "arguments": {"device_id": "ac-livingroom", "action": "set_temperature", "value": 28}}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
try:
result = json.loads(urllib.request.urlopen(r, timeout=10).read())
print(f" output: {result.get('output','')[:200]}")
if result.get('error'):
print(f" error: {result['error'][:200]}")
except Exception as e:
print(f"{e}")
print("\n" + "=" * 60)
print("诊断测试完成")
print("=" * 60)