初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
频道模块 - API数据完整性回归验证(改进规范版)
|
||||
覆盖:字段类型验证、空数据处理、错误码验证
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
BASE = "http://localhost:18080"
|
||||
API = f"{BASE}/app/api/channels"
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
results_log = []
|
||||
|
||||
def login(username="admin", password="admin123"):
|
||||
r = requests.post(f"{BASE}/app/api/auth/login", json={"username": username, "password": password})
|
||||
if r.status_code == 200:
|
||||
return r.json().get("data", {}).get("access_token", "")
|
||||
return ""
|
||||
|
||||
def hdr(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
def check_field(obj, field, expect_type, path=""):
|
||||
full = f"{path}.{field}" if path else field
|
||||
if field not in obj:
|
||||
return False, f"缺少字段 '{full}'"
|
||||
val = obj[field]
|
||||
if expect_type == "int" and not (isinstance(val, int) and not isinstance(val, bool)):
|
||||
return False, f"'{full}' 期望int, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "float_or_int" and not (isinstance(val, (int, float)) and not isinstance(val, bool)):
|
||||
return False, f"'{full}' 期望number, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "str_or_none" and not (val is None or isinstance(val, str)):
|
||||
return False, f"'{full}' 期望str|None, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "bool" and not isinstance(val, bool):
|
||||
return False, f"'{full}' 期望bool, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "dict_or_none" and not (val is None or isinstance(val, dict)):
|
||||
return False, f"'{full}' 期望dict|None, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "list" and not isinstance(val, list):
|
||||
return False, f"'{full}' 期望list, 实际{type(val).__name__}={val}"
|
||||
if expect_type == "str" and not isinstance(val, str):
|
||||
return False, f"'{full}' 期望str, 实际{type(val).__name__}={val}"
|
||||
return True, ""
|
||||
|
||||
def record(name, method, path, ok, detail):
|
||||
global passed, failed
|
||||
s = "PASS" if ok else "FAIL"
|
||||
print(f"[{s}] {name}")
|
||||
print(f" {method} {path} | {detail}")
|
||||
results_log.append({"name": name, "method": method, "path": path, "passed": ok, "detail": detail})
|
||||
if ok: passed += 1
|
||||
else: failed += 1
|
||||
|
||||
# ===== 各测试用例 =====
|
||||
|
||||
def t1_platform_configs():
|
||||
"""平台凭证配置 - 字段完整性"""
|
||||
tok = login()
|
||||
r = requests.get(f"{API}/platforms/config", headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t1", "GET", "/platforms/config", False, f"HTTP {r.status_code}")
|
||||
|
||||
data = r.json()
|
||||
if data.get("code") != 0:
|
||||
return record("t1", "GET", "/platforms/config", False, f"code={data.get('code')}")
|
||||
|
||||
platforms = data.get("data", {})
|
||||
expected = ["feishu", "wechat", "dingtalk", "whatsapp", "telegram", "slack"]
|
||||
ok = True
|
||||
details = []
|
||||
for p in expected:
|
||||
if p not in platforms:
|
||||
ok = False; details.append(f"缺少平台 {p}")
|
||||
continue
|
||||
cfg = platforms[p]
|
||||
for f in ["name", "description", "fields", "doc_url"]:
|
||||
f_ok, f_d = check_field(cfg, f, "str" if f != "fields" else "list", p)
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
for i, field in enumerate(cfg.get("fields", [])):
|
||||
for ff in ["key", "label", "type", "placeholder"]:
|
||||
f_ok, f_d = check_field(field, ff, "str", f"{p}.fields[{i}]")
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
detail = "6个平台配置完整" if ok else "; ".join(details)
|
||||
record("t1", "GET", "/platforms/config", ok, detail)
|
||||
|
||||
def t2_list_empty():
|
||||
"""空频道列表场景"""
|
||||
tok = login()
|
||||
# 先清空所有频道
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
if r0.status_code == 200:
|
||||
for ch in r0.json().get("data", []):
|
||||
requests.delete(f"{API}/{ch['id']}", headers=hdr(tok))
|
||||
|
||||
r = requests.get(API + "/", headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t2", "GET", "/", False, f"HTTP {r.status_code}")
|
||||
|
||||
items = r.json().get("data", [])
|
||||
if not isinstance(items, list):
|
||||
record("t2", "GET", "/", False, f"data不是list: {type(items)}")
|
||||
elif len(items) != 0:
|
||||
record("t2", "GET", "/", False, f"清理后仍有{len(items)}条记录")
|
||||
else:
|
||||
record("t2", "GET", "/", True, "空列表返回 [] 正确")
|
||||
|
||||
def t3_create_all_platforms():
|
||||
"""创建6个平台频道"""
|
||||
tok = login()
|
||||
platforms = [
|
||||
("feishu", {"app_id": "t1", "app_secret": "s1"}),
|
||||
("wechat", {"app_id": "w1", "app_secret": "s2", "token": "t1"}),
|
||||
("dingtalk", {"app_key": "d1", "app_secret": "s3"}),
|
||||
("whatsapp", {"phone_number_id": "123", "access_token": "a1"}),
|
||||
("telegram", {"bot_token": "b1"}),
|
||||
("slack", {"bot_token": "b2", "signing_secret": "s4"}),
|
||||
]
|
||||
created = []
|
||||
ok = True
|
||||
details = []
|
||||
for p_name, cred in platforms:
|
||||
r = requests.post(API + "/", json={
|
||||
"name": f"测试-{p_name}",
|
||||
"platform": p_name,
|
||||
"description": f"{p_name}频道",
|
||||
"credential": cred
|
||||
}, headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
ok = False; details.append(f"{p_name}: HTTP {r.status_code}")
|
||||
continue
|
||||
data = r.json()
|
||||
if data.get("code") != 0:
|
||||
ok = False; details.append(f"{p_name}: code={data.get('code')}")
|
||||
continue
|
||||
ch = data.get("data", {})
|
||||
# 字段检查
|
||||
for f in ["id", "name", "platform", "status", "is_active", "credential_masked"]:
|
||||
f_ok, f_d = check_field(ch, f, "str" if f != "is_active" else "bool", p_name)
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
if ch.get("platform") != p_name:
|
||||
ok = False; details.append(f"{p_name}: platform不匹配={ch.get('platform')}")
|
||||
created.append(ch["id"])
|
||||
|
||||
detail = f"{len(created)}/6创建成功" + ("; "+"; ".join(details[-3:]) if details else "")
|
||||
record("t3", "POST", "/", ok, detail)
|
||||
return created
|
||||
|
||||
def t4_channel_detail():
|
||||
"""频道详情字段完整性"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
if not chs:
|
||||
return record("t4", "GET", "/{id}", False, "无可用频道")
|
||||
|
||||
ch_id = chs[0]["id"]
|
||||
r = requests.get(f"{API}/{ch_id}", headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t4", "GET", f"/{ch_id}", False, f"HTTP {r.status_code}")
|
||||
|
||||
ch = r.json().get("data", {})
|
||||
ok = True; details = []
|
||||
checks = [
|
||||
("id","str"),("name","str"),("platform","str"),
|
||||
("status","str"),("is_active","bool"),
|
||||
("description","str_or_none"),("webhook_url","str_or_none"),
|
||||
("role","str_or_none"),("member_count","int"),("message_count","int"),
|
||||
("credential_masked","dict_or_none")
|
||||
]
|
||||
for f, t in checks:
|
||||
f_ok, f_d = check_field(ch, f, t)
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
detail = "所有字段类型正确" if ok else "; ".join(details)
|
||||
record("t4", "GET", f"/{ch_id}", ok, detail)
|
||||
|
||||
def t5_update_settings():
|
||||
"""更新频道AI配置"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
if not chs:
|
||||
return record("t5", "PUT", "/{id}", False, "无可用频道")
|
||||
|
||||
ch_id = chs[0]["id"]
|
||||
r = requests.put(f"{API}/{ch_id}", json={
|
||||
"name": "已更新名称",
|
||||
"description": "已更新描述",
|
||||
"settings": {
|
||||
"agent_id": "agent-001",
|
||||
"knowledge_base_id": "kb-001",
|
||||
"skill_ids": ["skill-1"],
|
||||
"welcome_message": "欢迎"
|
||||
}
|
||||
}, headers=hdr(tok))
|
||||
|
||||
if r.status_code != 200:
|
||||
return record("t5", "PUT", f"/{ch_id}", False, f"HTTP {r.status_code}")
|
||||
data = r.json()
|
||||
ch = data.get("data", {})
|
||||
ok = True; details = []
|
||||
if ch.get("name") != "已更新名称":
|
||||
ok = False; details.append(f"name未更新={ch.get('name')}")
|
||||
if ch.get("default_agent_id") != "agent-001":
|
||||
details.append(f"agent_id同步={ch.get('default_agent_id')}(期望agent-001)")
|
||||
if ch.get("knowledge_base_id") != "kb-001":
|
||||
details.append(f"kb_id同步={ch.get('knowledge_base_id')}(期望kb-001)")
|
||||
detail = "更新成功" if ok else "; ".join(details)
|
||||
record("t5", "PUT", f"/{ch_id}", ok, detail)
|
||||
|
||||
def t6_connect_disconnect():
|
||||
"""连接/断开频道"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
if not chs:
|
||||
return record("t6", "POST", "/{id}/connect", False, "无可用频道")
|
||||
|
||||
ch_id = chs[0]["id"]
|
||||
# 连接
|
||||
r = requests.post(f"{API}/{ch_id}/connect", headers=hdr(tok))
|
||||
if r.status_code not in (200, 400):
|
||||
return record("t6", "POST", f"/{ch_id}/connect", False, f"HTTP {r.status_code}")
|
||||
|
||||
data = r.json()
|
||||
ok = True; details = []
|
||||
if r.json().get("code") == 0:
|
||||
conn = data.get("data", {})
|
||||
for f in ["connected", "platform", "status", "message"]:
|
||||
if f not in conn:
|
||||
ok = False; details.append(f"缺少字段{f}")
|
||||
details.append("连接成功")
|
||||
|
||||
# 断开
|
||||
r2 = requests.post(f"{API}/{ch_id}/disconnect", headers=hdr(tok))
|
||||
if r2.status_code == 200 and r2.json().get("code") == 0:
|
||||
details.append("断开成功")
|
||||
else:
|
||||
ok = False; details.append(f"断开失败: HTTP {r2.status_code}")
|
||||
|
||||
# 重新连接
|
||||
r3 = requests.post(f"{API}/{ch_id}/connect", headers=hdr(tok))
|
||||
if r3.status_code == 200 and r3.json().get("code") == 0:
|
||||
details.append("重新连接成功")
|
||||
else:
|
||||
details.append(f"重新连接: HTTP {r3.status_code}")
|
||||
else:
|
||||
details.append(f"连接失败: code={data.get('code')}, msg={data.get('message')}")
|
||||
|
||||
detail = "; ".join(details)
|
||||
record("t6", "POST", f"/{ch_id}/connect|disconnect", ok, detail)
|
||||
|
||||
def t7_webhook_message():
|
||||
"""Webhook消息收发"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
connected = [c for c in chs if c.get("status") == "connected"]
|
||||
if not connected:
|
||||
return record("t7", "POST", "/{id}/webhook", False, "无已连接频道")
|
||||
|
||||
ch_id = connected[0]["id"]
|
||||
r = requests.post(f"{API}/{ch_id}/webhook", json={
|
||||
"from_user": "wx_user_001",
|
||||
"content": "你好测试消息",
|
||||
"message_type": "text"
|
||||
}, headers=hdr(tok))
|
||||
|
||||
if r.status_code not in (200, 400):
|
||||
return record("t7", "POST", f"/{ch_id}/webhook", False, f"HTTP {r.status_code}")
|
||||
|
||||
data = r.json()
|
||||
if data.get("code") != 0:
|
||||
return record("t7", "POST", f"/{ch_id}/webhook", False, f"code={data.get('code')}, msg={data.get('message')}")
|
||||
|
||||
wd = data.get("data", {})
|
||||
ok = True; details = []
|
||||
# received检查
|
||||
if "received" in wd:
|
||||
rc = wd["received"]
|
||||
for f in ["id", "from_user", "content", "message_type", "created_at"]:
|
||||
if f not in rc: ok = False; details.append(f"received缺{f}")
|
||||
else:
|
||||
ok = False; details.append("缺received")
|
||||
# reply检查
|
||||
if "reply" in wd:
|
||||
rp = wd["reply"]
|
||||
for f in ["id", "content", "role", "created_at"]:
|
||||
if f not in rp: ok = False; details.append(f"reply缺{f}")
|
||||
if rp.get("role") != "assistant":
|
||||
ok = False; details.append(f"role={rp.get('role')}")
|
||||
else:
|
||||
ok = False; details.append("缺reply")
|
||||
|
||||
detail = "received+reply字段完整" if ok else "; ".join(details)
|
||||
record("t7", "POST", f"/{ch_id}/webhook", ok, detail)
|
||||
|
||||
def t8_messages_list():
|
||||
"""消息列表字段完整性"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
connected = [c for c in chs if c.get("status") == "connected"]
|
||||
if not connected:
|
||||
return record("t8", "GET", "/{id}/messages", False, "无已连接频道")
|
||||
|
||||
ch_id = connected[0]["id"]
|
||||
r = requests.get(f"{API}/{ch_id}/messages", params={"limit": 10}, headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t8", "GET", f"/{ch_id}/messages", False, f"HTTP {r.status_code}")
|
||||
|
||||
msgs = r.json().get("data", [])
|
||||
if not isinstance(msgs, list):
|
||||
return record("t8", "GET", f"/{ch_id}/messages", False, f"data不是list: {type(msgs)}")
|
||||
|
||||
ok = True; details = [f"共{len(msgs)}条"]
|
||||
for i, m in enumerate(msgs):
|
||||
for f in ["id", "channel_id", "user_id", "content", "role"]:
|
||||
f_ok, f_d = check_field(m, f, "str", f"msgs[{i}]")
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
detail = "; ".join(details)
|
||||
record("t8", "GET", f"/{ch_id}/messages", ok, detail)
|
||||
|
||||
def t9_send_message():
|
||||
"""发送消息字段完整性"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
connected = [c for c in chs if c.get("status") == "connected"]
|
||||
if not connected:
|
||||
return record("t9", "POST", "/{id}/messages", False, "无已连接频道")
|
||||
|
||||
ch_id = connected[0]["id"]
|
||||
r = requests.post(f"{API}/{ch_id}/messages", json={"content": "测试消息"}, headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t9", "POST", f"/{ch_id}/messages", False, f"HTTP {r.status_code}, body={r.text[:100]}")
|
||||
|
||||
data = r.json()
|
||||
if data.get("code") != 0:
|
||||
return record("t9", "POST", f"/{ch_id}/messages", False, f"code={data.get('code')}")
|
||||
|
||||
wd = data.get("data", {})
|
||||
ok = True; details = []
|
||||
for section in ["message", "reply", "webhook_response"]:
|
||||
if section not in wd:
|
||||
ok = False; details.append(f"缺{section}")
|
||||
elif section == "message":
|
||||
for f in ["id", "channel_id", "user_id", "content"]:
|
||||
if f not in wd[section]: ok = False; details.append(f"message缺{f}")
|
||||
elif section == "reply":
|
||||
for f in ["id", "content", "role"]:
|
||||
if f not in wd[section]: ok = False; details.append(f"reply缺{f}")
|
||||
elif section == "webhook_response":
|
||||
for f in ["status", "platform", "message"]:
|
||||
if f not in wd[section]: ok = False; details.append(f"webhook_response缺{f}")
|
||||
detail = "message/reply/webhook_response完整" if ok else "; ".join(details)
|
||||
record("t9", "POST", f"/{ch_id}/messages", ok, detail)
|
||||
|
||||
def t10_members():
|
||||
"""频道成员管理"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
if not chs:
|
||||
return record("t10", "GET", "/{id}/members", False, "无可用频道")
|
||||
|
||||
ch_id = chs[0]["id"]
|
||||
r = requests.get(f"{API}/{ch_id}/members", headers=hdr(tok))
|
||||
if r.status_code != 200:
|
||||
return record("t10", "GET", f"/{ch_id}/members", False, f"HTTP {r.status_code}")
|
||||
|
||||
members = r.json().get("data", [])
|
||||
ok = True; details = [f"共{len(members)}名成员"]
|
||||
for i, m in enumerate(members):
|
||||
for f in ["id", "user_id", "username", "role"]:
|
||||
f_ok, f_d = check_field(m, f, "str", f"members[{i}]")
|
||||
if not f_ok: ok = False; details.append(f_d)
|
||||
detail = "; ".join(details)
|
||||
record("t10", "GET", f"/{ch_id}/members", ok, detail)
|
||||
|
||||
def t11_delete_and_cleanup():
|
||||
"""删除频道"""
|
||||
tok = login()
|
||||
r0 = requests.get(API + "/", headers=hdr(tok))
|
||||
chs = r0.json().get("data", [])
|
||||
if not chs:
|
||||
return record("t11", "DELETE", "/{id}", True, "无频道需删除,跳过")
|
||||
|
||||
ch_id = chs[0]["id"]
|
||||
r = requests.delete(f"{API}/{ch_id}", headers=hdr(tok))
|
||||
ok = r.status_code == 200 and r.json().get("code") == 0
|
||||
detail = "删除成功" if ok else f"HTTP {r.status_code}"
|
||||
record("t11", "DELETE", f"/{ch_id}", ok, detail)
|
||||
|
||||
# ===== 主函数 =====
|
||||
|
||||
def main():
|
||||
global passed, failed
|
||||
print("=" * 70)
|
||||
print(f"频道模块 API数据完整性回归 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 70)
|
||||
|
||||
t1_platform_configs()
|
||||
t2_list_empty()
|
||||
created = t3_create_all_platforms()
|
||||
t4_channel_detail()
|
||||
t5_update_settings()
|
||||
t6_connect_disconnect()
|
||||
t7_webhook_message()
|
||||
t8_messages_list()
|
||||
t9_send_message()
|
||||
t10_members()
|
||||
t11_delete_and_cleanup()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
total = passed + failed
|
||||
rate = passed / total * 100 if total > 0 else 0
|
||||
print(f"汇总: {passed}/{total} PASS, {failed} FAIL, {rate:.1f}%")
|
||||
print("=" * 70)
|
||||
|
||||
# JSON report
|
||||
report = {
|
||||
"test_time": datetime.now().isoformat(),
|
||||
"module": "频道模块-API数据完整性",
|
||||
"total": total, "passed": passed, "failed": failed,
|
||||
"pass_rate": f"{rate:.1f}%",
|
||||
"results": results_log
|
||||
}
|
||||
print("\n=== JSON_REPORT ===")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print("=== END_JSON_REPORT ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user