| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- #!/usr/bin/env python3
- """
- 导出 QiWe 真实联调数据(给交付 / 上级查看)
- 不跑本地假数据用例,只请求真实企微接口,生成:
- - live_data_report.md 人类可读
- - live_data_snapshot.json 原始摘要(可贴进报告)
- 用法(先 pnpm dev):
- cd backend/tests_python
- python export_live_data.py
- """
- from __future__ import annotations
- import json
- import sys
- from datetime import datetime, timezone
- from pathlib import Path
- import requests
- from client import ApiClient
- from load_env import load_backend_env
- ROOT = Path(__file__).resolve().parent
- PC_BASE = "http://localhost:3101/api"
- OUT_MD = ROOT / "live_data_report.md"
- OUT_JSON = ROOT / "live_data_snapshot.json"
- def _session_room_ids(data: dict) -> list[str]:
- ids: list[str] = []
- for key in ("collectList", "shieldList", "topList", "markList"):
- for item in data.get(key) or []:
- if item.get("sessionType") == 1 and item.get("sessionId"):
- ids.append(str(item["sessionId"]))
- return ids
- def _parse_room_ids(env: dict[str, str]) -> list[str]:
- raw = env.get("QIWEI_ROOM_IDS", "").strip()
- return [x.strip() for x in raw.split(",") if x.strip()]
- def main() -> int:
- env = load_backend_env()
- guid = env.get("QIWEI_GUID", "").strip()
- token_ok = bool(env.get("QIWEI_TOKEN"))
- room_ids = _parse_room_ids(env)
- if not token_ok or not guid:
- print("请在 backend/.env 配置 QIWEI_TOKEN 与 QIWEI_GUID")
- return 1
- try:
- requests.get(f"{PC_BASE}/health", timeout=3).raise_for_status()
- except requests.RequestException:
- print("PC 服务未启动,请先: cd backend && pnpm dev")
- return 1
- api = ApiClient(PC_BASE)
- snapshot: dict = {
- "exportedAt": datetime.now(timezone.utc).isoformat(),
- "guid": guid,
- "roomIdsConfigured": room_ids,
- "sections": {},
- }
- lines = [
- "# QiWe 真实联调数据导出",
- "",
- f"- 导出时间(UTC):{snapshot['exportedAt']}",
- f"- GUID:`{guid}`",
- f"- 配置群数:{len(room_ids)}",
- "",
- "> 由 `python export_live_data.py` 生成,数据来自真实 QiWe API。",
- "",
- ]
- def section(title: str, key: str, fn):
- print(f" 拉取: {title}...")
- try:
- data = fn()
- snapshot["sections"][key] = data
- lines.append(f"## {title}")
- lines.append("")
- lines.append("```json")
- lines.append(json.dumps(data, ensure_ascii=False, indent=2))
- lines.append("```")
- lines.append("")
- return data
- except Exception as e:
- err = {"error": str(e)}
- snapshot["sections"][key] = err
- lines.append(f"## {title}")
- lines.append("")
- lines.append(f"**失败:** {e}")
- lines.append("")
- return None
- # 1. 在线状态
- staff = section(
- "1. 员工在线状态",
- "staffStatus",
- lambda: api.assert_success(api.get(f"/qiwei/staff/{guid}/status")),
- )
- if staff:
- lines.append(
- f"**摘要:** 昵称/账号 `{staff.get('nickname') or staff.get('userId')}`,"
- f"在线状态 `{staff.get('userOnlineStatus')}`"
- )
- lines.append("")
- # 2. 会话列表(真实群 id 来源)
- session_data = section(
- "2. 会话列表 getSessionList",
- "sessionList",
- lambda: api.assert_success(
- api.post(
- "/qiwei/proxy",
- json={"method": "/session/getSessionList", "params": {"guid": guid}},
- )
- ),
- )
- session_rooms = _session_room_ids(session_data or {})
- if session_data:
- lines.append(f"- 会话中群聊(sessionType=1)共 **{len(session_rooms)}** 个")
- lines.append("")
- # 3. getRoomList(可能为空)
- room_list = section(
- "3. 群分页 getRoomList(可能为空)",
- "getRoomList",
- lambda: api.assert_success(
- api.post(
- "/qiwei/proxy",
- json={
- "method": "/room/getRoomList",
- "params": {"guid": guid, "nextStartIndex": 0},
- },
- )
- ),
- )
- # 4. 按 QIWEI_ROOM_IDS 同步群
- ids_to_sync = room_ids or session_rooms[:5]
- if ids_to_sync:
- synced = section(
- "4. 按 roomIdList 同步群(真实群名/人数)",
- "roomsSyncByIds",
- lambda: api.assert_success(
- api.post(
- "/rooms/sync",
- json={"guid": guid, "roomIdList": ids_to_sync},
- )
- ),
- )
- if synced and synced.get("rooms"):
- lines.append("### 群摘要")
- lines.append("")
- lines.append("| roomId | 群名 | 人数 |")
- lines.append("|--------|------|------|")
- for r in synced["rooms"]:
- lines.append(
- f"| {r.get('roomId')} | {r.get('roomName', '')} | {r.get('memberCount', '')} |"
- )
- lines.append("")
- else:
- snapshot["sections"]["roomsSyncByIds"] = {
- "skipped": "未配置 QIWEI_ROOM_IDS 且会话列表无群"
- }
- # 5. 每个群的详情与健康度
- for i, rid in enumerate(ids_to_sync or [], 1):
- detail = section(
- f"5.{i} 群详情 GET /rooms/{rid}",
- f"roomDetail_{rid}",
- lambda r=rid: api.assert_success(api.get(f"/rooms/{r}")),
- )
- section(
- f"5.{i}b 群健康度",
- f"roomHealth_{rid}",
- lambda r=rid: api.assert_success(api.get(f"/rooms/{r}/health")),
- )
- # 6. 同步消息(真实消息条数)
- msgs = section(
- "6. 同步消息 syncMsg(最近若干条)",
- "syncMessages",
- lambda: api.assert_success(
- api.post("/qiwei/sync", json={"guid": guid, "msgSeq": 0, "limit": 10})
- ),
- )
- if msgs:
- n = len(msgs.get("messages") or [])
- lines.append(f"- 本批拉取消息数:**{n}**,hasMore={msgs.get('hasMore')}")
- lines.append("")
- # 7. 外部联系人
- contacts = section(
- "7. 外部联系人同步",
- "contactsSync",
- lambda: api.assert_success(
- api.post(
- "/contacts/sync",
- json={"guid": guid, "currentSeq": 0, "limit": 10},
- )
- ),
- )
- if contacts:
- lines.append(f"- 联系人数:**{len(contacts.get('contacts') or [])}**")
- lines.append("")
- if ids_to_sync:
- import json as _json
- import time as _time
- import uuid as _uuid
- fix = Path(__file__).parent / "fixtures" / "qiwei_webhook_sample.json"
- wh = _json.loads(fix.read_text(encoding="utf-8"))
- rid = ids_to_sync[0]
- docid = f"DOC_EXPORT_{_uuid.uuid4().hex[:8]}"
- ts = int(_time.time())
- for i, item in enumerate(wh.get("data", [])):
- item["guid"] = guid
- item["fromRoomId"] = rid
- item["timestamp"] = ts + i
- if item.get("msgType") == 13:
- item["msgData"]["linkUrl"] = (
- f"https://doc.weixin.qq.com/txdoc/excel?docid={docid}"
- )
- section("8. Webhook 登记群文档", "webhookLive", lambda: api.assert_success(
- api.post("/qiwei/webhook", json=wh)
- ))
- section(
- "8b. 群文档台账",
- "roomDocLive",
- lambda: api.assert_success(api.get("/qiwei/room-docs", params={"roomId": rid})),
- )
- msgs = section(
- "8c. 已存消息列表",
- "messagesLive",
- lambda: api.assert_success(
- api.get("/qiwei/messages", params={"roomId": rid, "limit": 10})
- ),
- )
- if msgs is not None:
- lines.append(f"- 消息条数:**{len(msgs)}**")
- lines.append("")
- if env.get("QIWEI_LIVE_NOTIFY", "").strip() == "1" and ids_to_sync:
- room_name = ""
- rooms_data = snapshot["sections"].get("roomsSyncByIds") or {}
- if rooms_data.get("rooms"):
- room_name = rooms_data["rooms"][0].get("roomName", "")
- tpl = section(
- "9. 整改通知模板",
- "notifyTemplate",
- lambda: api.assert_success(
- api.get(
- "/notifications/template",
- params={"issueType": "missing_doc", "roomName": room_name or "联调群"},
- )
- ),
- )
- if tpl and ids_to_sync:
- section(
- "10. 真实发送通知 sendText(发到第一个配置群)",
- "notifySendLive",
- lambda: api.assert_success(
- api.post(
- "/notifications/send",
- json={
- "type": "compliance",
- "receiverId": 1,
- "title": tpl["title"],
- "content": tpl["content"] + " [export_live_data]",
- "refId": 1,
- "guid": guid,
- "toId": ids_to_sync[0],
- },
- ),
- status=201,
- ),
- )
- OUT_JSON.write_text(
- json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8"
- )
- OUT_MD.write_text("\n".join(lines), encoding="utf-8")
- print()
- print("真实联调数据已导出:")
- print(f" {OUT_MD}")
- print(f" {OUT_JSON}")
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
|