#!/usr/bin/env python3 """ fam-edge 发版前稳定性检查 - 依次调用线上接口做基本断言,输出 pass/fail 清单。 不是 CI(项目没有 CI 基础设施),是给人在部署后手动跑的检查脚本。 用法: python3 smoke_test.py --base-url http://127.0.0.1:5000 --token \ [--video-id 173 --ts "2026-08-21 16:11:25"] [--label 人物A] [--skip-chat] video-id/ts/label 是可选的:不传就跳过 /api/oracle/frame 和 /api/oracle/avatar 检查 (这两个接口需要库里真实存在的数据,不同部署环境的数据不一样,没法硬编码)。 """ import argparse import sys import time import requests PASS = "PASS" FAIL = "FAIL" SKIP = "SKIP" class Report: def __init__(self): self.rows = [] def add(self, name, status, detail=""): self.rows.append((name, status, detail)) mark = {"PASS": "✓", "FAIL": "✗", "SKIP": "-"}[status] print(f"[{mark}] {name}: {status}" + (f" ({detail})" if detail else "")) def ok(self): return all(r[1] != FAIL for r in self.rows) def check_health(report, base_url): try: t0 = time.time() resp = requests.get(f"{base_url}/health", timeout=10) dt = time.time() - t0 if resp.status_code != 200: report.add("GET /health", FAIL, f"HTTP {resp.status_code}") return data = resp.json() if data.get("status") != "ok": report.add("GET /health", FAIL, f"status={data.get('status')}") return report.add("GET /health", PASS, f"{dt:.2f}s, processed_videos={data.get('processed_videos')}") except Exception as e: report.add("GET /health", FAIL, str(e)) def check_sync(report, base_url, token): try: t0 = time.time() resp = requests.get(f"{base_url}/api/oracle/sync", params={"since": "", "token": token}, timeout=30) dt = time.time() - t0 if resp.status_code != 200: report.add("GET /api/oracle/sync", FAIL, f"HTTP {resp.status_code}: {resp.text[:150]}") return data = resp.json() missing = [k for k in ("videos", "events", "people", "server_time") if k not in data] if missing: report.add("GET /api/oracle/sync", FAIL, f"缺字段: {missing}") return report.add("GET /api/oracle/sync", PASS, f"{dt:.2f}s, videos={len(data['videos'])} events={len(data['events'])}") except Exception as e: report.add("GET /api/oracle/sync", FAIL, str(e)) def check_activity(report, base_url, token): try: t0 = time.time() resp = requests.get(f"{base_url}/api/oracle/activity", params={"token": token}, timeout=15) dt = time.time() - t0 if resp.status_code != 200: report.add("GET /api/oracle/activity", FAIL, f"HTTP {resp.status_code}") return data = resp.json() if "queue" not in data or "db" not in data: report.add("GET /api/oracle/activity", FAIL, "缺 queue/db 字段") return report.add("GET /api/oracle/activity", PASS, f"{dt:.2f}s, queue={data.get('queue')}") except Exception as e: report.add("GET /api/oracle/activity", FAIL, str(e)) def check_frame(report, base_url, token, video_id, ts): if not video_id or not ts: report.add("GET /api/oracle/frame", SKIP, "未提供 --video-id/--ts") return try: t0 = time.time() resp = requests.get(f"{base_url}/api/oracle/frame", params={"video_id": video_id, "ts": ts, "token": token, "w": 440}, timeout=60) dt = time.time() - t0 if resp.status_code != 200: report.add("GET /api/oracle/frame", FAIL, f"HTTP {resp.status_code}") return if resp.headers.get("content-type") != "image/jpeg" or len(resp.content) < 100: report.add("GET /api/oracle/frame", FAIL, "返回内容不是合法 JPEG") return report.add("GET /api/oracle/frame", PASS, f"{dt:.2f}s, {len(resp.content)} bytes") except Exception as e: report.add("GET /api/oracle/frame", FAIL, str(e)) def check_avatar(report, base_url, token, label): if not label: report.add("GET /api/oracle/avatar", SKIP, "未提供 --label") return try: t0 = time.time() resp = requests.get(f"{base_url}/api/oracle/avatar", params={"label": label, "token": token, "w": 160}, timeout=60) dt = time.time() - t0 if resp.status_code != 200: report.add("GET /api/oracle/avatar", FAIL, f"HTTP {resp.status_code}") return if resp.headers.get("content-type") != "image/jpeg" or len(resp.content) < 100: report.add("GET /api/oracle/avatar", FAIL, "返回内容不是合法 JPEG") return report.add("GET /api/oracle/avatar", PASS, f"{dt:.2f}s, {len(resp.content)} bytes") except Exception as e: report.add("GET /api/oracle/avatar", FAIL, str(e)) def check_chat(report, base_url): try: t0 = time.time() resp = requests.post(f"{base_url}/api/edge/chat/ask", json={"prompt": "只回复两个字:在线", "max_tokens": 30}, timeout=120) dt = time.time() - t0 if resp.status_code != 200: report.add("POST /api/edge/chat/ask", FAIL, f"HTTP {resp.status_code}: {resp.text[:150]}") return data = resp.json() if not data.get("answer"): report.add("POST /api/edge/chat/ask", FAIL, "answer 为空") return report.add("POST /api/edge/chat/ask", PASS, f"{dt:.2f}s, provider={data.get('provider')}") except Exception as e: report.add("POST /api/edge/chat/ask", FAIL, str(e)) def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument("--base-url", default="http://127.0.0.1:5000") p.add_argument("--token", required=True, help="ORACLE_SYNC_TOKEN") p.add_argument("--video-id", type=int, default=None) p.add_argument("--ts", default=None, help="例如 '2026-08-21 16:11:25'") p.add_argument("--label", default=None, help="例如 人物A") p.add_argument("--skip-chat", action="store_true", help="跳过问答检查(耗时最长且可能扣配额)") args = p.parse_args() report = Report() check_health(report, args.base_url) check_sync(report, args.base_url, args.token) check_activity(report, args.base_url, args.token) check_frame(report, args.base_url, args.token, args.video_id, args.ts) check_avatar(report, args.base_url, args.token, args.label) if not args.skip_chat: check_chat(report, args.base_url) else: report.add("POST /api/edge/chat/ask", SKIP, "--skip-chat") print() n_pass = sum(1 for r in report.rows if r[1] == PASS) n_fail = sum(1 for r in report.rows if r[1] == FAIL) n_skip = sum(1 for r in report.rows if r[1] == SKIP) print(f"结果: {n_pass} PASS / {n_fail} FAIL / {n_skip} SKIP") sys.exit(0 if report.ok() else 1) if __name__ == "__main__": main()