export_live_data.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. #!/usr/bin/env python3
  2. """
  3. 导出 QiWe 真实联调数据(给交付 / 上级查看)
  4. 不跑本地假数据用例,只请求真实企微接口,生成:
  5. - live_data_report.md 人类可读
  6. - live_data_snapshot.json 原始摘要(可贴进报告)
  7. 用法(先 pnpm dev):
  8. cd backend/tests_python
  9. python export_live_data.py
  10. """
  11. from __future__ import annotations
  12. import json
  13. import sys
  14. from datetime import datetime, timezone
  15. from pathlib import Path
  16. import requests
  17. from client import ApiClient
  18. from load_env import load_backend_env
  19. ROOT = Path(__file__).resolve().parent
  20. PC_BASE = "http://localhost:3101/api"
  21. OUT_MD = ROOT / "live_data_report.md"
  22. OUT_JSON = ROOT / "live_data_snapshot.json"
  23. def _session_room_ids(data: dict) -> list[str]:
  24. ids: list[str] = []
  25. for key in ("collectList", "shieldList", "topList", "markList"):
  26. for item in data.get(key) or []:
  27. if item.get("sessionType") == 1 and item.get("sessionId"):
  28. ids.append(str(item["sessionId"]))
  29. return ids
  30. def _parse_room_ids(env: dict[str, str]) -> list[str]:
  31. raw = env.get("QIWEI_ROOM_IDS", "").strip()
  32. return [x.strip() for x in raw.split(",") if x.strip()]
  33. def main() -> int:
  34. env = load_backend_env()
  35. guid = env.get("QIWEI_GUID", "").strip()
  36. token_ok = bool(env.get("QIWEI_TOKEN"))
  37. room_ids = _parse_room_ids(env)
  38. if not token_ok or not guid:
  39. print("请在 backend/.env 配置 QIWEI_TOKEN 与 QIWEI_GUID")
  40. return 1
  41. try:
  42. requests.get(f"{PC_BASE}/health", timeout=3).raise_for_status()
  43. except requests.RequestException:
  44. print("PC 服务未启动,请先: cd backend && pnpm dev")
  45. return 1
  46. api = ApiClient(PC_BASE)
  47. snapshot: dict = {
  48. "exportedAt": datetime.now(timezone.utc).isoformat(),
  49. "guid": guid,
  50. "roomIdsConfigured": room_ids,
  51. "sections": {},
  52. }
  53. lines = [
  54. "# QiWe 真实联调数据导出",
  55. "",
  56. f"- 导出时间(UTC):{snapshot['exportedAt']}",
  57. f"- GUID:`{guid}`",
  58. f"- 配置群数:{len(room_ids)}",
  59. "",
  60. "> 由 `python export_live_data.py` 生成,数据来自真实 QiWe API。",
  61. "",
  62. ]
  63. def section(title: str, key: str, fn):
  64. print(f" 拉取: {title}...")
  65. try:
  66. data = fn()
  67. snapshot["sections"][key] = data
  68. lines.append(f"## {title}")
  69. lines.append("")
  70. lines.append("```json")
  71. lines.append(json.dumps(data, ensure_ascii=False, indent=2))
  72. lines.append("```")
  73. lines.append("")
  74. return data
  75. except Exception as e:
  76. err = {"error": str(e)}
  77. snapshot["sections"][key] = err
  78. lines.append(f"## {title}")
  79. lines.append("")
  80. lines.append(f"**失败:** {e}")
  81. lines.append("")
  82. return None
  83. # 1. 在线状态
  84. staff = section(
  85. "1. 员工在线状态",
  86. "staffStatus",
  87. lambda: api.assert_success(api.get(f"/qiwei/staff/{guid}/status")),
  88. )
  89. if staff:
  90. lines.append(
  91. f"**摘要:** 昵称/账号 `{staff.get('nickname') or staff.get('userId')}`,"
  92. f"在线状态 `{staff.get('userOnlineStatus')}`"
  93. )
  94. lines.append("")
  95. # 2. 会话列表(真实群 id 来源)
  96. session_data = section(
  97. "2. 会话列表 getSessionList",
  98. "sessionList",
  99. lambda: api.assert_success(
  100. api.post(
  101. "/qiwei/proxy",
  102. json={"method": "/session/getSessionList", "params": {"guid": guid}},
  103. )
  104. ),
  105. )
  106. session_rooms = _session_room_ids(session_data or {})
  107. if session_data:
  108. lines.append(f"- 会话中群聊(sessionType=1)共 **{len(session_rooms)}** 个")
  109. lines.append("")
  110. # 3. getRoomList(可能为空)
  111. room_list = section(
  112. "3. 群分页 getRoomList(可能为空)",
  113. "getRoomList",
  114. lambda: api.assert_success(
  115. api.post(
  116. "/qiwei/proxy",
  117. json={
  118. "method": "/room/getRoomList",
  119. "params": {"guid": guid, "nextStartIndex": 0},
  120. },
  121. )
  122. ),
  123. )
  124. # 4. 按 QIWEI_ROOM_IDS 同步群
  125. ids_to_sync = room_ids or session_rooms[:5]
  126. if ids_to_sync:
  127. synced = section(
  128. "4. 按 roomIdList 同步群(真实群名/人数)",
  129. "roomsSyncByIds",
  130. lambda: api.assert_success(
  131. api.post(
  132. "/rooms/sync",
  133. json={"guid": guid, "roomIdList": ids_to_sync},
  134. )
  135. ),
  136. )
  137. if synced and synced.get("rooms"):
  138. lines.append("### 群摘要")
  139. lines.append("")
  140. lines.append("| roomId | 群名 | 人数 |")
  141. lines.append("|--------|------|------|")
  142. for r in synced["rooms"]:
  143. lines.append(
  144. f"| {r.get('roomId')} | {r.get('roomName', '')} | {r.get('memberCount', '')} |"
  145. )
  146. lines.append("")
  147. else:
  148. snapshot["sections"]["roomsSyncByIds"] = {
  149. "skipped": "未配置 QIWEI_ROOM_IDS 且会话列表无群"
  150. }
  151. # 5. 每个群的详情与健康度
  152. for i, rid in enumerate(ids_to_sync or [], 1):
  153. detail = section(
  154. f"5.{i} 群详情 GET /rooms/{rid}",
  155. f"roomDetail_{rid}",
  156. lambda r=rid: api.assert_success(api.get(f"/rooms/{r}")),
  157. )
  158. section(
  159. f"5.{i}b 群健康度",
  160. f"roomHealth_{rid}",
  161. lambda r=rid: api.assert_success(api.get(f"/rooms/{r}/health")),
  162. )
  163. # 6. 同步消息(真实消息条数)
  164. msgs = section(
  165. "6. 同步消息 syncMsg(最近若干条)",
  166. "syncMessages",
  167. lambda: api.assert_success(
  168. api.post("/qiwei/sync", json={"guid": guid, "msgSeq": 0, "limit": 10})
  169. ),
  170. )
  171. if msgs:
  172. n = len(msgs.get("messages") or [])
  173. lines.append(f"- 本批拉取消息数:**{n}**,hasMore={msgs.get('hasMore')}")
  174. lines.append("")
  175. # 7. 外部联系人
  176. contacts = section(
  177. "7. 外部联系人同步",
  178. "contactsSync",
  179. lambda: api.assert_success(
  180. api.post(
  181. "/contacts/sync",
  182. json={"guid": guid, "currentSeq": 0, "limit": 10},
  183. )
  184. ),
  185. )
  186. if contacts:
  187. lines.append(f"- 联系人数:**{len(contacts.get('contacts') or [])}**")
  188. lines.append("")
  189. if ids_to_sync:
  190. import json as _json
  191. import time as _time
  192. import uuid as _uuid
  193. fix = Path(__file__).parent / "fixtures" / "qiwei_webhook_sample.json"
  194. wh = _json.loads(fix.read_text(encoding="utf-8"))
  195. rid = ids_to_sync[0]
  196. docid = f"DOC_EXPORT_{_uuid.uuid4().hex[:8]}"
  197. ts = int(_time.time())
  198. for i, item in enumerate(wh.get("data", [])):
  199. item["guid"] = guid
  200. item["fromRoomId"] = rid
  201. item["timestamp"] = ts + i
  202. if item.get("msgType") == 13:
  203. item["msgData"]["linkUrl"] = (
  204. f"https://doc.weixin.qq.com/txdoc/excel?docid={docid}"
  205. )
  206. section("8. Webhook 登记群文档", "webhookLive", lambda: api.assert_success(
  207. api.post("/qiwei/webhook", json=wh)
  208. ))
  209. section(
  210. "8b. 群文档台账",
  211. "roomDocLive",
  212. lambda: api.assert_success(api.get("/qiwei/room-docs", params={"roomId": rid})),
  213. )
  214. msgs = section(
  215. "8c. 已存消息列表",
  216. "messagesLive",
  217. lambda: api.assert_success(
  218. api.get("/qiwei/messages", params={"roomId": rid, "limit": 10})
  219. ),
  220. )
  221. if msgs is not None:
  222. lines.append(f"- 消息条数:**{len(msgs)}**")
  223. lines.append("")
  224. if env.get("QIWEI_LIVE_NOTIFY", "").strip() == "1" and ids_to_sync:
  225. room_name = ""
  226. rooms_data = snapshot["sections"].get("roomsSyncByIds") or {}
  227. if rooms_data.get("rooms"):
  228. room_name = rooms_data["rooms"][0].get("roomName", "")
  229. tpl = section(
  230. "9. 整改通知模板",
  231. "notifyTemplate",
  232. lambda: api.assert_success(
  233. api.get(
  234. "/notifications/template",
  235. params={"issueType": "missing_doc", "roomName": room_name or "联调群"},
  236. )
  237. ),
  238. )
  239. if tpl and ids_to_sync:
  240. section(
  241. "10. 真实发送通知 sendText(发到第一个配置群)",
  242. "notifySendLive",
  243. lambda: api.assert_success(
  244. api.post(
  245. "/notifications/send",
  246. json={
  247. "type": "compliance",
  248. "receiverId": 1,
  249. "title": tpl["title"],
  250. "content": tpl["content"] + " [export_live_data]",
  251. "refId": 1,
  252. "guid": guid,
  253. "toId": ids_to_sync[0],
  254. },
  255. ),
  256. status=201,
  257. ),
  258. )
  259. OUT_JSON.write_text(
  260. json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8"
  261. )
  262. OUT_MD.write_text("\n".join(lines), encoding="utf-8")
  263. print()
  264. print("真实联调数据已导出:")
  265. print(f" {OUT_MD}")
  266. print(f" {OUT_JSON}")
  267. return 0
  268. if __name__ == "__main__":
  269. raise SystemExit(main())