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
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Round 12 Part 2: WebSocket 测试 + 子服务直调"""
import json, urllib.request, urllib.error, socket, struct, base64, os, hashlib, time
TOKEN = open("/tmp/cyrene_test_token.txt").read().strip()
SID = open("/tmp/cyrene_test_sid.txt").read().strip()
print(f"TOKEN={TOKEN[:20]}... SID={SID}")
# ====== Step 1: memory-service 正确端点测试 ======
print("\n=== Step 1: memory-service 正确端点 ===")
# 1a: GET /api/v1/memories?user_id=admin
r = urllib.request.Request("http://localhost:8091/api/v1/memories?user_id=admin")
try:
resp = urllib.request.urlopen(r, timeout=5)
print(f" GET /memories: {resp.status} {resp.read().decode()[:200]}")
except Exception as e:
print(f" GET /memories: FAIL {e}")
# 1b: POST /api/v1/memories/query
r = urllib.request.Request("http://localhost:8091/api/v1/memories/query",
data=json.dumps({"user_id":"admin","query_text":"test","limit":5}).encode())
r.add_header("Content-Type", "application/json")
try:
resp = urllib.request.urlopen(r, timeout=5)
print(f" POST /memories/query: {resp.status} {resp.read().decode()[:200]}")
except Exception as e:
print(f" POST /memories/query: FAIL {e}")
# 1c: 通过 gateway 代理 memory
print("\n --- Gateway proxy to memory ---")
headers = {"Authorization": f"Bearer {TOKEN}"}
# GET /api/v1/memory/search?q=test
r = urllib.request.Request("http://localhost:8080/api/v1/memory/search?q=test")
r.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(r, timeout=10)
print(f" GW memory/search: {resp.status} {resp.read().decode()[:200]}")
except urllib.error.HTTPError as e:
print(f" GW memory/search: {e.code} {e.read().decode()[:200]}")
except Exception as e:
print(f" GW memory/search: FAIL {e}")
# ====== Step 2: tool-engine 正确端点测试 ======
print("\n=== Step 2: tool-engine 正确端点 ===")
# 2a: GET /api/v1/tools
r = urllib.request.Request("http://localhost:8092/api/v1/tools")
try:
resp = urllib.request.urlopen(r, timeout=5)
body = resp.read().decode()
tools_data = json.loads(body)
tool_names = [t.get("name","?") for t in tools_data.get("tools",[])]
print(f" GET /tools: {resp.status} total={tools_data.get('total')} names={tool_names}")
except Exception as e:
print(f" GET /tools: FAIL {e}")
# 2b: POST /api/v1/tools/calculator/execute
r = urllib.request.Request("http://localhost:8092/api/v1/tools/calculator/execute",
data=json.dumps({"arguments":{"expression":"2+3"}}).encode())
r.add_header("Content-Type", "application/json")
try:
resp = urllib.request.urlopen(r, timeout=5)
print(f" POST calc/execute: {resp.status} {resp.read().decode()[:200]}")
except Exception as e:
print(f" POST calc/execute: FAIL {e}")
# ====== Step 3: WebSocket 测试 (手动构造) ======
print("\n=== Step 3: WebSocket 连接测试 ===")
WS_KEY = base64.b64encode(os.urandom(16)).decode()
def ws_handshake():
"""Try WebSocket upgrade to /ws/chat?token=..."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect(("127.0.0.1", 8080))
request = (
f"GET /ws/chat?token={TOKEN}&session_id={SID} HTTP/1.1\r\n"
f"Host: localhost:8080\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {WS_KEY}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
sock.send(request.encode())
response = b""
while b"\r\n\r\n" not in response:
response += sock.recv(4096)
headers = response.decode()
status_line = headers.split("\r\n")[0]
print(f" WS handshake: {status_line}")
if "101" in status_line:
print(f" ✅ WebSocket 升级成功!")
# Send a simple chat message
msg = json.dumps({"type":"message","content":"Hello Cyrene!","mode":"text"})
import struct as st
frame = bytearray()
frame.append(0x81) # FIN + text opcode
frame.append(0x80 | len(msg)) # MASK + length
mask_key = os.urandom(4)
frame.extend(mask_key)
masked = bytes([msg[i] ^ mask_key[i%4] for i in range(len(msg))])
frame.extend(masked)
sock.send(bytes(frame))
print(f" Sent: {msg}")
# Read response
time.sleep(3)
sock.settimeout(5)
try:
resp_data = sock.recv(4096)
print(f" Received {len(resp_data)} bytes: {resp_data[:500]}")
except socket.timeout:
print(f" ⚠️ No response within 3s - backend may be processing")
else:
print(f" ❌ WebSocket handshake failed")
print(f" Full response:\n{headers[:500]}")
except Exception as e:
print(f" WS connect FAIL: {e}")
finally:
sock.close()
ws_handshake()
# ====== Step 4: ai-core 直接调用的完整 SSE 响应 ======
print("\n=== Step 4: ai-core 完整 SSE 响应 ===")
ai_body = {"user_id":"admin","session_id":SID,"message":"用一句话介绍你自己","mode":"text"}
r = urllib.request.Request("http://localhost:8081/api/v1/chat",
data=json.dumps(ai_body).encode(), method="POST")
r.add_header("Content-Type", "application/json")
r.add_header("Accept", "text/event-stream")
try:
resp = urllib.request.urlopen(r, timeout=60)
full = []
for line in resp:
line = line.decode().strip()
if line.startswith("data:"):
full.append(line)
print(f" Lines received: {len(full)}")
print(f" Last 3 lines: {full[-3:]}")
except Exception as e:
print(f" FAIL: {e}")
# ====== Step 5: Gateway 通过 memory_handler 代理测试 ======
print("\n=== Step 5: Gateway memory proxy ===")
for path, qs in [("/api/v1/memory/search","q=hello"), ("/api/v1/memory","")]:
url = f"http://localhost:8080{path}"
if qs:
url += f"?{qs}"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {TOKEN}")
try:
resp = urllib.request.urlopen(req, timeout=10)
print(f" GET {path}: {resp.status} {resp.read().decode()[:200]}")
except urllib.error.HTTPError as e:
body = e.read().decode()[:200]
print(f" GET {path}: {e.code} {body}")
except Exception as e:
print(f" GET {path}: FAIL {e}")
print("\n[DONE]")