feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs

This commit is contained in:
Neo
2026-03-20 09:02:10 +08:00
parent 3d2e5f8165
commit beb88d5bea
388 changed files with 6436 additions and 25458 deletions

View File

@@ -0,0 +1,225 @@
"""
RNS1.4 CHAT 模块端到端测试脚本
用法: python scripts/ops/test_chat_e2e.py
前置条件:
- 后端服务已启动 (uvicorn app.main:app)
- .env 中配置了 TEST_USER_TOKEN 和 APP_DB_DSN
- test_zqyy_app 数据库可访问
环境变量:
BACKEND_URL — 后端地址,默认 http://localhost:8000
TEST_USER_TOKEN — 测试用户 JWT token
APP_DB_DSN — 业务数据库连接串(指向 test_zqyy_app
"""
# CHANGE 2026-03-20 | RNS1.4 T13.1: CHAT 端到端测试脚本
import json
import os
import sys
import time
from pathlib import Path
# 加载根 .env
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parents[2] / ".env")
BACKEND_URL = os.environ.get("BACKEND_URL", "http://localhost:8000").rstrip("/")
TOKEN = os.environ.get("TEST_USER_TOKEN", "")
DB_DSN = os.environ.get("APP_DB_DSN", "")
if not TOKEN:
print("❌ 缺少 TEST_USER_TOKEN 环境变量")
sys.exit(1)
if not DB_DSN:
print("❌ 缺少 APP_DB_DSN 环境变量")
sys.exit(1)
import httpx
import psycopg2
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
results: list[dict] = []
def record(name: str, passed: bool, detail: str = ""):
status = "✅ PASS" if passed else "❌ FAIL"
print(f" {status}{name}" + (f" ({detail})" if detail else ""))
results.append({"test": name, "passed": passed, "detail": detail})
def main():
print(f"\n🔗 后端: {BACKEND_URL}")
print(f"🗄️ 数据库: {DB_DSN[:40]}...\n")
chat_id = None
# ── CHAT-1: 对话历史列表 ──
print("── CHAT-1: GET /api/xcx/chat/history ──")
try:
r = httpx.get(f"{BACKEND_URL}/api/xcx/chat/history", headers=HEADERS, timeout=10)
record("CHAT-1 状态码 200", r.status_code == 200, f"got {r.status_code}")
if r.status_code == 200:
data = r.json()
record("CHAT-1 返回 items 数组", isinstance(data.get("items"), list))
except Exception as e:
record("CHAT-1 请求", False, str(e))
# ── 创建/获取对话(通过 general 入口)──
print("\n── CHAT-2b: GET /api/xcx/chat/messages?contextType=general ──")
try:
r = httpx.get(
f"{BACKEND_URL}/api/xcx/chat/messages",
params={"contextType": "general", "contextId": ""},
headers=HEADERS, timeout=10,
)
record("CHAT-2b 状态码 200", r.status_code == 200, f"got {r.status_code}")
if r.status_code == 200:
data = r.json()
chat_id = data.get("chatId")
record("CHAT-2b 返回 chatId", chat_id is not None, f"chatId={chat_id}")
except Exception as e:
record("CHAT-2b 请求", False, str(e))
if not chat_id:
print("\n⚠️ 无法获取 chatId跳过后续测试")
_print_summary()
return
# ── CHAT-2a: 通过 chatId 查询消息 ──
print(f"\n── CHAT-2a: GET /api/xcx/chat/{chat_id}/messages ──")
try:
r = httpx.get(
f"{BACKEND_URL}/api/xcx/chat/{chat_id}/messages",
headers=HEADERS, timeout=10,
)
record("CHAT-2a 状态码 200", r.status_code == 200, f"got {r.status_code}")
except Exception as e:
record("CHAT-2a 请求", False, str(e))
# ── CHAT-3: 发送消息(同步) ──
print(f"\n── CHAT-3: POST /api/xcx/chat/{chat_id}/messages ──")
test_content = "你好,这是一条端到端测试消息,请简短回复。"
ai_reply_id = None
try:
r = httpx.post(
f"{BACKEND_URL}/api/xcx/chat/{chat_id}/messages",
json={"content": test_content},
headers=HEADERS, timeout=30,
)
record("CHAT-3 状态码 200", r.status_code == 200, f"got {r.status_code}")
if r.status_code == 200:
data = r.json()
user_msg = data.get("userMessage", {})
ai_msg = data.get("aiReply", {})
record("CHAT-3 用户消息已返回", bool(user_msg.get("id")))
record("CHAT-3 AI 回复已返回", bool(ai_msg.get("id")))
record("CHAT-3 AI 回复非空", bool(ai_msg.get("content")))
ai_reply_id = ai_msg.get("id")
if ai_msg.get("content"):
print(f" AI 回复: {ai_msg['content'][:80]}...")
except Exception as e:
record("CHAT-3 请求", False, str(e))
# ── CHAT-4: SSE 流式 ──
print(f"\n── CHAT-4: POST /api/xcx/chat/stream (SSE) ──")
sse_content = "请用一句话介绍台球运动。"
sse_tokens: list[str] = []
sse_done = False
sse_message_id = None
try:
with httpx.stream(
"POST",
f"{BACKEND_URL}/api/xcx/chat/stream",
json={"chatId": int(chat_id), "content": sse_content},
headers=HEADERS,
timeout=60,
) as resp:
record("CHAT-4 状态码 200", resp.status_code == 200, f"got {resp.status_code}")
current_event = ""
for line in resp.iter_lines():
if line.startswith("event:"):
current_event = line[6:].strip()
elif line.startswith("data:"):
raw = line[5:].strip()
try:
d = json.loads(raw)
except json.JSONDecodeError:
continue
if current_event == "message" and "token" in d:
sse_tokens.append(d["token"])
elif current_event == "done":
sse_done = True
sse_message_id = d.get("messageId")
elif current_event == "error":
record("CHAT-4 收到 error 事件", False, d.get("message", ""))
full_reply = "".join(sse_tokens)
record("CHAT-4 收到 token 事件", len(sse_tokens) > 0, f"{len(sse_tokens)} tokens")
record("CHAT-4 收到 done 事件", sse_done)
record("CHAT-4 回复语义通顺", len(full_reply) > 5, f"len={len(full_reply)}")
if full_reply:
print(f" SSE 回复: {full_reply[:80]}...")
except Exception as e:
record("CHAT-4 请求", False, str(e))
# ── 数据库验证 ──
print("\n── 数据库持久化验证 ──")
try:
conn = psycopg2.connect(DB_DSN)
cur = conn.cursor()
# 验证 ai_messages 包含用户消息和 AI 回复
cur.execute(
"SELECT id, role, tokens_used FROM biz.ai_messages "
"WHERE conversation_id = %s ORDER BY created_at DESC LIMIT 4",
(int(chat_id),),
)
rows = cur.fetchall()
roles = [r[1] for r in rows]
record("DB: ai_messages 有 user 消息", "user" in roles)
record("DB: ai_messages 有 assistant 回复", "assistant" in roles)
# 验证 tokens_used
assistant_rows = [r for r in rows if r[1] == "assistant"]
if assistant_rows:
tokens = assistant_rows[0][2]
record("DB: tokens_used 已记录", tokens is not None and tokens > 0,
f"tokens_used={tokens}")
else:
record("DB: tokens_used 已记录", False, "无 assistant 行")
# 验证 ai_conversations 元数据更新
cur.execute(
"SELECT last_message, last_message_at FROM biz.ai_conversations WHERE id = %s",
(int(chat_id),),
)
conv = cur.fetchone()
if conv:
record("DB: last_message 已更新", bool(conv[0]))
record("DB: last_message_at 已更新", conv[1] is not None)
else:
record("DB: ai_conversations 记录存在", False)
cur.close()
conn.close()
except Exception as e:
record("DB 验证", False, str(e))
_print_summary()
def _print_summary():
print("\n" + "=" * 50)
passed = sum(1 for r in results if r["passed"])
failed = sum(1 for r in results if not r["passed"])
print(f"总计: {len(results)} 项 | ✅ {passed} 通过 | ❌ {failed} 失败")
# 输出 JSON 报告
report_path = Path(__file__).parent / "chat_e2e_report.json"
report_path.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"📄 报告已保存: {report_path}")
if __name__ == "__main__":
main()