generate_impl_doc.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. # -*- coding: utf-8 -*-
  2. """根据 doc/文档/QiWe开放平台文档 重写 企微客户服务-功能实现说明.md"""
  3. from __future__ import annotations
  4. import json
  5. import re
  6. from pathlib import Path
  7. ROOT = Path(__file__).resolve().parents[1]
  8. OUT = ROOT / "doc" / "企微客户服务-功能实现说明.md"
  9. SRC = ROOT / "doc" / "企微客户服务-功能实现说明.md"
  10. QIWE_README = ROOT / "doc" / "文档" / "QiWe开放平台文档" / "README.md"
  11. DOC_BASE = "./文档/QiWe开放平台文档"
  12. COMMON_HEADERS = """| Header | 必填 | 说明 |
  13. |--------|:----:|------|
  14. | `Content-Type` | 是 | `application/json` |
  15. | `X-QIWEI-TOKEN` | 是 | 控制台申请的租户 Token,写入 `QIWEI_TOKEN` |"""
  16. DOAPI = """| 项 | 值 |
  17. |----|-----|
  18. | URL | `POST {QIWEI_BASE_URL}/api/qw/doApi`(默认 `http://manager.qiweapi.com/qiwe/api/qw/doApi`) |
  19. | Body 结构 | `{ "method": "<路径>", "params": { ... } }` |"""
  20. def compact_json(text: str) -> str:
  21. return re.sub(r"\s+", " ", text.strip())
  22. def build_io_table(
  23. method: str | None,
  24. req_example: str | None,
  25. resp_example: str | None,
  26. req: list[tuple[str, str, str]],
  27. resp: list[tuple[str, str, str]],
  28. passive: bool = False,
  29. ) -> str:
  30. if passive and req_example and not req_example.strip().startswith("{"):
  31. inp = req_example.strip()
  32. elif passive:
  33. inp = "无(QiWe POST → callbackUrl)"
  34. elif req_example:
  35. inp = compact_json(req_example)
  36. elif method and method not in ("【占位】", "见官方页"):
  37. m = method.split()[0]
  38. keys = ", ".join(f'"{r[0]}": ""' for r in req[:8])
  39. inp = compact_json(f'{{"method": "{m}", "params": {{ {keys} }}}}')
  40. else:
  41. inp = "—"
  42. if resp_example:
  43. out = compact_json(resp_example)
  44. elif resp:
  45. data_keys = ", ".join(f'"{r[0]}": ""' for r in resp[:8])
  46. out = compact_json(f'{{"code": 0, "data": {{ {data_keys} }}, "msg": "成功"}}')
  47. else:
  48. out = "—"
  49. # 单元格内用行内代码,避免表格嵌套代码块无法渲染
  50. inp_cell = inp if inp == "—" else f"`{inp}`"
  51. out_cell = out if out == "—" else f"`{out}`"
  52. return "\n".join(
  53. [
  54. "| 输入 | 输出 |",
  55. "|------|------|",
  56. f"| {inp_cell} | {out_cell} |",
  57. "",
  58. ]
  59. )
  60. def api_block(
  61. api_id: str,
  62. title: str,
  63. method: str | None,
  64. official: str,
  65. local_md: str | None,
  66. desc: str,
  67. req: list[tuple[str, str, str]],
  68. resp: list[tuple[str, str, str]],
  69. req_example: str | None = None,
  70. resp_example: str | None = None,
  71. extra: str = "",
  72. passive: bool = False,
  73. ) -> str:
  74. local_link = f"[{Path(local_md).stem}]({DOC_BASE}/md/{local_md})" if local_md else "—"
  75. method_line = f"`{method}`" if method else "被动推送"
  76. off_link = (
  77. f"[官方]({official})"
  78. if official.startswith("http")
  79. else "—"
  80. )
  81. parts = [
  82. f'<a id="{api_id.lower()}"></a>',
  83. "",
  84. f"### {api_id} {title}",
  85. "",
  86. f"| method | 官方 | 本地 |",
  87. f"|--------|------|------|",
  88. f"| {method_line} | {off_link} | {local_link} |",
  89. "",
  90. ]
  91. if desc.strip():
  92. parts += [desc.strip(), ""]
  93. parts.append(build_io_table(method, req_example, resp_example, req, resp, passive))
  94. if extra:
  95. parts += [extra, ""]
  96. return "\n".join(parts)
  97. APIS: list[dict] = []
  98. def add(**kw):
  99. APIS.append(kw)
  100. add(
  101. api_id="API-01",
  102. title="租户 Token / 快速开始",
  103. method=None,
  104. official="https://doc.qiweapi.com/doc-7562288",
  105. local_md=None,
  106. desc="> 在 [QiWe 控制台](http://manager.qiweapi.com/login) 申请 API Key;无单独 method。",
  107. req=[],
  108. resp=[],
  109. req_example='Header: X-QIWEI-TOKEN',
  110. resp_example='{"code": 0, "msg": "success"}',
  111. passive=True,
  112. )
  113. add(
  114. api_id="API-02",
  115. title="创建设备与扫码登录(四步)",
  116. method="/client/createClient 等",
  117. official="https://doc.qiweapi.com/api-344613850",
  118. local_md="创建设备(步骤1).md",
  119. desc="""人员账号纳入系统需完成登录闭环(亦可控制台在线登录直接拿 `guid`):
  120. | 步骤 | 接口文档 | method(以官方为准) |
  121. |:----:|----------|---------------------|
  122. | 1 | [创建设备](https://doc.qiweapi.com/api-344613850) | `/client/createClient` |
  123. | 2 | [二维码-获取](https://doc.qiweapi.com/api-344613856) | 见官方页 |
  124. | 3 | [二维码状态-检测](https://doc.qiweapi.com/api-344613857) | 见官方页 |
  125. | 4 | [二维码-code验证](https://doc.qiweapi.com/api-344613858) | 见官方页 |
  126. ⚠️ 创建设备后 5 分钟内未登录,实例会被清理。""",
  127. req=[
  128. ("deviceName", "string", "设备名称(步骤1 必填)"),
  129. ("deviceType", "integer", "0=ipad(推荐), 2=windows 等"),
  130. ("clientVersion", "string", "客户端版本,一般可空"),
  131. ("areaCode", "integer", "地区代理 ID,与登录地一致"),
  132. ("proxyUrl", "string", "可选 socks5 代理"),
  133. ("aid", "string", "可选本地 Aid 代理"),
  134. ],
  135. resp=[("guid", "string", "设备 ID,后续所有 params.guid")],
  136. req_example="""{
  137. "method": "/client/createClient",
  138. "params": {
  139. "deviceName": "店长-ipad",
  140. "deviceType": 0,
  141. "clientVersion": "",
  142. "areaCode": 320000,
  143. "proxyUrl": ""
  144. }
  145. }""",
  146. resp_example='{"code": 0, "data": {"guid": "a3318ad6-xxxx"}, "msg": "成功"}',
  147. )
  148. add(
  149. api_id="API-03",
  150. title="设置回调地址",
  151. method="/client/setCallback",
  152. official="https://doc.qiweapi.com/api-354411522",
  153. local_md="设置回调地址.md",
  154. desc="按 **Token** 配置回调;一个 Token 下所有账号共用。推送体含 `guid` 区分账号。",
  155. req=[
  156. ("callbackUrl", "string", "本方公网 URL,如 `https://{域名}/api/qiwei/webhook`"),
  157. ("authType", "string", "如 `Authorization`"),
  158. ("authSecret", "string", "回调鉴权密钥,可空"),
  159. ],
  160. resp=[],
  161. req_example="""{
  162. "method": "/client/setCallback",
  163. "params": {
  164. "callbackUrl": "https://your.domain/api/qiwei/webhook",
  165. "authType": "Authorization",
  166. "authSecret": ""
  167. }
  168. }""",
  169. resp_example='{"code": 0, "msg": "成功"}',
  170. )
  171. add(
  172. api_id="API-04",
  173. title="Webhook 回调结构(被动)",
  174. method=None,
  175. official="https://doc.qiweapi.com/doc-7331304",
  176. local_md="回调结构说明.md",
  177. desc="""QiWe **POST** 至 `callbackUrl`;`data[]` 含 `cmd`(15000 普通消息)、`guid`、`msgType`(13=链接)、`fromRoomId`、`msgData`。详见 [回调结构说明](%s/md/回调结构说明.md)。本项目:`cmd=15000` + `msgType=13` → 解析 `msgData.linkUrl` 发现沟通记录表。""" % DOC_BASE,
  178. req=[],
  179. resp=[
  180. ("data[].cmd", "integer", "回调类型"),
  181. ("data[].guid", "string", "账号"),
  182. ("data[].msgType", "integer", "消息类型"),
  183. ("data[].fromRoomId", "string", "群 ID"),
  184. ("data[].msgData", "object", "消息内容"),
  185. ],
  186. passive=True,
  187. resp_example="""{
  188. "code": 0,
  189. "data": [{
  190. "cmd": 15000,
  191. "guid": "xxx",
  192. "msgType": 13,
  193. "fromRoomId": "10791082xxxx",
  194. "msgData": {
  195. "title": "沟通记录",
  196. "linkUrl": "https://doc.weixin.qq.com/doc/xxx?docid=YYY"
  197. },
  198. "timestamp": 1708324990
  199. }],
  200. "msg": "成功"
  201. }""",
  202. )
  203. add(
  204. api_id="API-05",
  205. title="用户状态(在线检测)",
  206. method="/login/checkLogin",
  207. official="https://doc.qiweapi.com/api-347221662",
  208. local_md="用户状态.md",
  209. desc="查询指定 `guid` 是否在线,批量任务前应调用。",
  210. req=[("guid", "string", "设备 ID")],
  211. resp=[
  212. ("userOnlineStatus", "integer", "-1 需扫码;0 可免扫码;1 已扫码待确认;2 在线;4 取消;10 待验证码"),
  213. ("userId", "string", "企微用户 ID"),
  214. ("nickname", "string", "昵称"),
  215. ("corpName", "string", "企业名称"),
  216. ("lastActiveTime", "integer", "最后活跃时间"),
  217. ],
  218. req_example='{"method": "/login/checkLogin", "params": {"guid": "{{guid}}"}}',
  219. resp_example="""{
  220. "code": 0,
  221. "data": {
  222. "userOnlineStatus": 2,
  223. "userId": "1688852****",
  224. "nickname": "店长A",
  225. "corpName": "某某公司"
  226. },
  227. "msg": "成功"
  228. }""",
  229. )
  230. add(
  231. api_id="API-06",
  232. title="同步历史消息分页",
  233. method="/msg/syncMsg",
  234. official="https://doc.qiweapi.com/api-344613926",
  235. local_md="同步历史消息分页.md",
  236. desc="补拉历史群聊;`msgSeq` 递增分页直至 `hasMore=0`。",
  237. req=[
  238. ("guid", "string", "设备 ID"),
  239. ("msgSeq", "integer", "游标,首次 0,下次用上次返回的 seq"),
  240. ("limit", "integer", "每页条数,如 10~100"),
  241. ],
  242. resp=[
  243. ("hasMore", "integer", "是否还有下一页"),
  244. ("travelSyncKey", "integer", "同步游标"),
  245. ("syncMsgList[]", "array", "消息列表,含 fromRoomId、msgType、msgData、seq 等"),
  246. ],
  247. req_example='{"method": "/msg/syncMsg", "params": {"guid": "{{guid}}", "msgSeq": 0, "limit": 50}}',
  248. resp_example="""{
  249. "code": 0,
  250. "data": {
  251. "hasMore": 1,
  252. "travelSyncKey": 922174,
  253. "syncMsgList": [{
  254. "fromRoomId": 1023,
  255. "msgType": 13,
  256. "msgData": {"linkUrl": "https://doc.weixin.qq.com/..."},
  257. "seq": 9221964,
  258. "timestamp": 1708324990
  259. }]
  260. },
  261. "msg": "成功"
  262. }""",
  263. )
  264. add(
  265. api_id="API-07",
  266. title="群分页",
  267. method="/room/getRoomList",
  268. official="https://doc.qiweapi.com/api-344613881",
  269. local_md="群分页.md",
  270. desc="仅查**本人创建**的群;查全部群需结合 [会话分页](https://doc.qiweapi.com/api-344613938)(`sessionType=1` 为群 id)。",
  271. req=[
  272. ("guid", "string", "设备 ID"),
  273. ("nextStartIndex", "integer", "分页游标,首次 0"),
  274. ],
  275. resp=[
  276. ("hasMore", "integer", "是否有下一页"),
  277. ("nextStartIndex", "integer", "下次请求传入"),
  278. ("roomCount", "integer", "本页数量"),
  279. ("roomList[].roomId", "string", "群 ID"),
  280. ("roomList[].roomName", "string", "群名称"),
  281. ("roomList[].roomMemberCount", "integer", "成员数"),
  282. ],
  283. req_example='{"method": "/room/getRoomList", "params": {"guid": "{{guid}}", "nextStartIndex": 0}}',
  284. )
  285. add(
  286. api_id="API-08",
  287. title="群详情-批量",
  288. method="/room/batchGetRoomDetail",
  289. official="https://doc.qiweapi.com/api-344613882",
  290. local_md="群详情-批量.md",
  291. desc="先 API-07 拿 `roomId`,再批量查详情;成员**显示名**需再调 API-18。",
  292. req=[
  293. ("guid", "string", "设备 ID"),
  294. ("roomIdList", "string[]", "群 ID 列表"),
  295. ],
  296. resp=[
  297. ("roomList[].roomId", "string", "群 ID"),
  298. ("roomList[].roomName", "string", "群名称"),
  299. ("roomList[].roomAnnouncement", "string", "群公告(合规可匹配文档链接)"),
  300. ("roomList[].memberList[]", "array", "成员列表 userId、joinTime 等"),
  301. ],
  302. req_example='{"method": "/room/batchGetRoomDetail", "params": {"guid": "{{guid}}", "roomIdList": ["10802031057945400"]}}',
  303. )
  304. add(
  305. api_id="API-09",
  306. title="群成员变动查询",
  307. method="见官方页",
  308. official="https://doc.qiweapi.com/api-437674162",
  309. local_md="群成员变动查询.md",
  310. desc="按群 + 时间窗查询进退群记录(method 以 [官方页](https://doc.qiweapi.com/api-437674162) 为准)。",
  311. req=[("guid", "string", "设备 ID"), ("roomId", "string", "群 ID"), ("startTime/endTime", "integer", "时间窗(以官方为准)")],
  312. resp=[("memberEvents[]", "array", "进退群事件列表(字段以官方为准)")],
  313. )
  314. add(
  315. api_id="API-10",
  316. title="创建群",
  317. method="/room/createRoom",
  318. official="https://doc.qiweapi.com/api-344613883",
  319. local_md="创建群.md",
  320. desc="新建外部客户群并返回 `roomId`。",
  321. req=[
  322. ("guid", "string", "设备 ID"),
  323. ("isOuterRoom", "integer", "1=外部群"),
  324. ("memberList", "string[]", "初始成员 userId 列表"),
  325. ],
  326. resp=[
  327. ("roomId", "string", "新群 ID"),
  328. ("roomCreatetime", "integer", "创建时间"),
  329. ("memberList", "string[]", "成员列表"),
  330. ],
  331. req_example="""{
  332. "method": "/room/createRoom",
  333. "params": {
  334. "guid": "{{guid}}",
  335. "isOuterRoom": 1,
  336. "memberList": ["168885****57534"]
  337. }
  338. }""",
  339. )
  340. add(
  341. api_id="API-11",
  342. title="修改群公告",
  343. method="/room/modifyRoomNotice",
  344. official="https://doc.qiweapi.com/api-344613890",
  345. local_md="修改群公告.md",
  346. desc="将沟通记录文档链接写入群公告。",
  347. req=[
  348. ("guid", "string", "设备 ID"),
  349. ("roomId", "string", "群 ID"),
  350. ("notice", "string", "公告正文(可含文档 URL)"),
  351. ],
  352. resp=[("code", "integer", "0=成功")],
  353. req_example='{"method": "/room/modifyRoomNotice", "params": {"guid": "{{guid}}", "roomId": "108144***", "notice": "沟通记录:https://doc.weixin.qq.com/..."}}',
  354. )
  355. add(
  356. api_id="API-12",
  357. title="群消息置顶-列表",
  358. method="/msg/roomTopMessageList",
  359. official="https://doc.qiweapi.com/api-344613920",
  360. local_md="群消息置顶-列表.md",
  361. desc="⚠️ **仅群主**可置顶;用于合规检查文档是否已置顶。",
  362. req=[("guid", "string", "设备 ID"), ("roomId", "string", "群 ID")],
  363. resp=[
  364. ("list[].msgUniqueIdentifier", "string", "消息唯一标识"),
  365. ("list[].msgType", "integer", "消息类型"),
  366. ("list[].msgData", "object", "消息体"),
  367. ("list[].senderId", "string", "发送人"),
  368. ],
  369. req_example='{"method": "/msg/roomTopMessageList", "params": {"guid": "{{guid}}", "roomId": "1088541******6"}}',
  370. )
  371. add(
  372. api_id="API-13",
  373. title="群消息置顶-添加",
  374. method="/msg/roomTopMessageSet",
  375. official="https://doc.qiweapi.com/api-344613921",
  376. local_md="群消息置顶-添加.md",
  377. desc="⚠️ PoC:须传原消息的 msgId、msgSenderId、msgTimestamp、msgType、msgData。",
  378. req=[
  379. ("guid", "string", "设备 ID"),
  380. ("roomId", "string", "群 ID"),
  381. ("msgId", "string", "消息 id"),
  382. ("msgSenderId", "string", "发送人 userId"),
  383. ("msgTimestamp", "integer", "发送时间戳"),
  384. ("msgType", "integer", "消息类型"),
  385. ("msgData", "object", "如 `{ \"content\": \"...\" }`"),
  386. ],
  387. resp=[("code", "integer", "0=成功")],
  388. req_example="""{
  389. "method": "/msg/roomTopMessageSet",
  390. "params": {
  391. "guid": "{{guid}}",
  392. "roomId": "10965*****579",
  393. "msgId": "CIGABBDd*****",
  394. "msgSenderId": "16888****804",
  395. "msgTimestamp": 1752224990,
  396. "msgType": 0,
  397. "msgData": {"content": "沟通记录表链接"}
  398. }
  399. }""",
  400. )
  401. add(
  402. api_id="API-14",
  403. title="发送纯文本消息",
  404. method="/msg/sendText",
  405. official="https://doc.qiweapi.com/api-344613906",
  406. local_md="发送纯文本消息.md",
  407. desc="整改通知、预警、待办提醒等推送到用户或群。",
  408. req=[
  409. ("guid", "string", "设备 ID"),
  410. ("content", "string", "文本内容"),
  411. ("toId", "string", "用户 userId 或群 roomId"),
  412. ("isNoNeedRead", "boolean", "可选,是否无需已读"),
  413. ],
  414. resp=[
  415. ("isSendSuccess", "integer", "是否发送成功"),
  416. ("msgServerId", "integer", "消息服务端 ID"),
  417. ("msgUniqueIdentifier", "string", "消息唯一标识"),
  418. ("seq", "integer", "序号"),
  419. ],
  420. req_example='{"method": "/msg/sendText", "params": {"guid": "{{guid}}", "content": "请更新沟通记录表", "toId": "168****768657", "isNoNeedRead": true}}',
  421. )
  422. add(
  423. api_id="API-15",
  424. title="群发消息",
  425. method="/msg/sendGroupMsg",
  426. official="https://doc.qiweapi.com/api-344613923",
  427. local_md="群发消息.md",
  428. desc="每天对每个客户/群仅可群发一次;`sendType`:0=外部联系人,1=外部群。",
  429. req=[
  430. ("guid", "string", "设备 ID"),
  431. ("sendType", "integer", "0 联系人 / 1 群"),
  432. ("toIdList", "string[]", "接收方 ID 列表"),
  433. ("msgList[]", "array", "消息列表,type:0 文本、13 链接、14 图片等"),
  434. ],
  435. resp=[("groupMsgId", "integer", "群发任务 ID,供 API-16 查询")],
  436. req_example="""{
  437. "method": "/msg/sendGroupMsg",
  438. "params": {
  439. "guid": "{{guid}}",
  440. "sendType": 1,
  441. "toIdList": ["10791082****"],
  442. "msgList": [{"type": 0, "msgData": {"content": "本周运营内容"}}]
  443. }
  444. }""",
  445. )
  446. add(
  447. api_id="API-16",
  448. title="群发消息-状态查询",
  449. method="/msg/sendGroupMsgStatus",
  450. official="https://doc.qiweapi.com/api-344613924",
  451. local_md="群发消息-状态查询.md",
  452. desc="根据 `groupMsgId` 轮询发送进度。",
  453. req=[
  454. ("guid", "string", "设备 ID"),
  455. ("groupMsgId", "string", "群发任务 ID"),
  456. ("endDetailId", "integer", "分页游标"),
  457. ],
  458. resp=[
  459. ("hasSend", "boolean", "是否已发送"),
  460. ("isEnd", "boolean", "是否结束"),
  461. ("total", "integer", "总数"),
  462. ("customerList[]", "array", "各接收方状态"),
  463. ],
  464. req_example='{"method": "/msg/sendGroupMsgStatus", "params": {"guid": "{{guid}}", "groupMsgId": "115258331353230686", "endDetailId": 2}}',
  465. )
  466. add(
  467. api_id="API-17",
  468. title="外部联系人分页",
  469. method="/contact/getWxContactList",
  470. official="https://doc.qiweapi.com/api-344613869",
  471. local_md="外部联系人分页.md",
  472. desc="分页拉外部联系人;拿到 `userId` 后再调 API-18 查详情。建议落库后靠回调增量更新。",
  473. req=[
  474. ("guid", "string", "设备 ID"),
  475. ("currentSeq", "integer", "游标,首次 0"),
  476. ("limit", "integer", "每页条数"),
  477. ("bizType", "integer", "1=联系人变动;2=好友申请"),
  478. ],
  479. resp=[
  480. ("hasMore", "boolean", "是否有下一页"),
  481. ("currentSeq", "integer", "下次请求游标"),
  482. ("contactList[].userId", "string", "用户 ID"),
  483. ("contactList[].nickname", "string", "昵称"),
  484. ("contactList[].remark", "string", "备注"),
  485. ],
  486. req_example='{"method": "/contact/getWxContactList", "params": {"guid": "{{guid}}", "currentSeq": 0, "limit": 50, "bizType": 1}}',
  487. )
  488. add(
  489. api_id="API-18",
  490. title="联系人详情-批量",
  491. method="/contact/batchGetUserinfo",
  492. official="https://doc.qiweapi.com/api-344613868",
  493. local_md="联系人详情-批量.md",
  494. desc="批量查联系人详情(含群成员真实姓名场景)。",
  495. req=[
  496. ("guid", "string", "设备 ID"),
  497. ("userIdList", "string[]", "用户 ID 列表"),
  498. ],
  499. resp=[
  500. ("contactList[].userId", "string", "用户 ID"),
  501. ("contactList[].nickname", "string", "昵称"),
  502. ("contactList[].mobile", "string", "手机号"),
  503. ("contactList[].avatarUrl", "string", "头像"),
  504. ],
  505. req_example='{"method": "/contact/batchGetUserinfo", "params": {"guid": "{{guid}}", "userIdList": ["168*****5548"]}}',
  506. )
  507. add(
  508. api_id="API-19",
  509. title="客户标签-增删",
  510. method="/label/contactEditLabel",
  511. official="https://doc.qiweapi.com/api-344613937",
  512. local_md="客户标签-增删.md",
  513. desc="`opType`:1=增加,2=删除;`labelIdList`/`labelSuperIdList`/`labelOwnerList` 须一一对应。",
  514. req=[
  515. ("guid", "string", "设备 ID"),
  516. ("opType", "integer", "1 增 / 2 删"),
  517. ("paramList[].userId", "string", "客户 userId"),
  518. ("paramList[].labelIdList", "string[]", "标签 ID"),
  519. ],
  520. resp=[("data", "array", "操作结果")],
  521. req_example="""{
  522. "method": "/label/contactEditLabel",
  523. "params": {
  524. "guid": "{{guid}}",
  525. "opType": 1,
  526. "paramList": [{
  527. "userId": "78813023**",
  528. "labelIdList": ["1407374973784***"],
  529. "labelSuperIdList": ["1407375223060***"],
  530. "labelOwnerList": ["168885236**"]
  531. }]
  532. }
  533. }""",
  534. )
  535. add(
  536. api_id="API-20",
  537. title="添加群成员好友",
  538. method="/contact/addRoomContact",
  539. official="https://doc.qiweapi.com/api-425758709",
  540. local_md="添加群成员好友.md",
  541. desc="⚠️ PoC:从群内发起加好友,须合规确认。",
  542. req=[
  543. ("guid", "string", "设备 ID"),
  544. ("roomId", "string", "群 ID"),
  545. ("userId", "string", "目标成员 userId"),
  546. ("verifyText", "string", "验证语"),
  547. ],
  548. resp=[("code", "integer", "0=成功")],
  549. req_example='{"method": "/contact/addRoomContact", "params": {"guid": "{{guid}}", "roomId": "1079271***", "userId": "168885***", "verifyText": "您好"}}',
  550. )
  551. add(
  552. api_id="API-21",
  553. title="企微文件下载",
  554. method="/cloud/wxWorkDownload",
  555. official="https://doc.qiweapi.com/api-344613901",
  556. local_md="企微文件下载.md",
  557. desc="⚠️ 候选 PoC:从消息里的 fileId/fileAeskey 下载;返回临时 `cloudUrl`(7–15 天清理)。**不能替代「读在线文档正文」**。",
  558. req=[
  559. ("guid", "string", "设备 ID"),
  560. ("fileId", "string", "文件 ID"),
  561. ("fileAeskey", "string", "AES 密钥"),
  562. ("fileSize", "integer", "文件大小"),
  563. ("fileType", "integer", "1 大图 / 2 小图 / 4 视频 / 5 文件语音等"),
  564. ],
  565. resp=[("cloudUrl", "string", "临时下载地址")],
  566. req_example='{"method": "/cloud/wxWorkDownload", "params": {"guid": "{{guid}}", "fileId": "...", "fileAeskey": "...", "fileSize": 32768, "fileType": 5}}',
  567. )
  568. add(
  569. api_id="API-22",
  570. title="读在线文档正文",
  571. method="【占位】",
  572. official="—",
  573. local_md=None,
  574. desc="**QiWe 开放平台当前无「按 docid 读取企微在线文档表格正文」的专用接口。** 合规读表须 PoC API-21 或等待官方能力;禁止编造 method。",
  575. req=[],
  576. resp=[],
  577. )
  578. def render_section_2() -> str:
  579. lines = [
  580. "## 二、QiWe 官方接口速查(含入参/出参)",
  581. "",
  582. "> 文档来源:[QiWe 开放平台文档索引](%s/README.md)(本地最新爬取,2026)。 " % DOC_BASE,
  583. "> 各接口仅保留 **输入 / 输出** 一张表(报文示例);公共 Header 见 §2.0。",
  584. "",
  585. "### 2.0 统一调用方式",
  586. "",
  587. COMMON_HEADERS,
  588. "",
  589. DOAPI,
  590. "",
  591. "### 2.1 接口索引",
  592. "",
  593. "| 编号 | 接口 | method | 官方 | 本地 md |",
  594. "|:----:|------|--------|------|---------|",
  595. ]
  596. for a in APIS:
  597. local = f"[{a['local_md']}]({DOC_BASE}/md/{a['local_md']})" if a.get("local_md") else "—"
  598. off = a["official"]
  599. if off.startswith("http"):
  600. off_cell = f"[在线]({off})"
  601. else:
  602. off_cell = "—"
  603. m = a.get("method") or "被动"
  604. lines.append(f"| {a['api_id']} | {a['title']} | `{m}` | {off_cell} | {local} |")
  605. lines.append("")
  606. lines.append("### 2.2 各接口详细说明")
  607. lines.append("")
  608. for a in APIS:
  609. lines.append(
  610. api_block(
  611. a["api_id"],
  612. a["title"],
  613. a.get("method"),
  614. a["official"],
  615. a.get("local_md"),
  616. a["desc"],
  617. a.get("req", []),
  618. a.get("resp", []),
  619. a.get("req_example"),
  620. a.get("resp_example"),
  621. a.get("extra", ""),
  622. passive=a.get("passive", False),
  623. )
  624. )
  625. return "\n".join(lines)
  626. def api_ref(ids: list[str]) -> str:
  627. links = []
  628. for api_id in ids:
  629. a = next(x for x in APIS if x["api_id"] == api_id)
  630. links.append(f"[{api_id} {a['title']}](#{api_id.lower()})")
  631. return "、".join(links) + "(详见 §二)"
  632. def enhance_feature_section(text: str) -> str:
  633. """在功能小节中补充接口文档链接与入参出参指引"""
  634. text = re.sub(r"\n\*\*接口(文档与)?入参/出参[^\*]*\*\*[^\n]+\n", "\n", text)
  635. text = re.sub(
  636. r"(\*\*接口入参/出参表:\*\*[^\n]+\n)(?:\1)+",
  637. r"\1",
  638. text,
  639. )
  640. def repl_official(m):
  641. api = m.group(1)
  642. a = next((x for x in APIS if x["api_id"] == api), None)
  643. if not a:
  644. return m.group(0)
  645. block = f"\n\n**接口文档:** {api_ref([api])}\n"
  646. if a.get("method") and a["method"] not in ("【占位】", "见官方页", None):
  647. block += f"\n**method:** `{a['method']}`\n"
  648. if a.get("req"):
  649. block += "\n**主要入参:** " + "、".join(f"`{r[0]}`" for r in a["req"][:6])
  650. if len(a["req"]) > 6:
  651. block += " …"
  652. block += "\n"
  653. if a.get("resp"):
  654. block += "**主要出参:** " + "、".join(f"`{r[0]}`" for r in a["resp"][:6])
  655. if len(a["resp"]) > 6:
  656. block += " …"
  657. block += "\n"
  658. return m.group(0) + block
  659. # 在「需实现的 QiWe 官方接口」段落后注入
  660. text = re.sub(
  661. r"(\*\*需实现的 QiWe 官方接口[::]\*\*[^\n]*\n)",
  662. lambda m: m.group(1) + inject_api_refs(m.group(1)),
  663. text,
  664. )
  665. text = text.replace(
  666. "[callback-structure.md](./qiweapi-scrape/callback-structure.md)",
  667. f"[回调结构说明]({DOC_BASE}/md/回调结构说明.md)",
  668. )
  669. text = text.replace("sync 页需重爬", f"[同步历史消息分页]({DOC_BASE}/md/同步历史消息分页.md)")
  670. text = text.replace("(爬取 md 需重爬)", f"(见 [{DOC_BASE}/md/同步历史消息分页.md]({DOC_BASE}/md/同步历史消息分页.md))")
  671. text = re.sub(
  672. r"\| 编号 \| 接口 \| 爬取文档 \|\n\|:----:\|------\|----------\|\n(?:\| API-\d+[^\n]+\n)+",
  673. lambda m: m.group(0).replace("爬取文档", "文档").replace("见官方链接", "见 §二"),
  674. text,
  675. )
  676. text = text.replace(
  677. "[platform-intro.md](../output/qiweapi-test/platform-intro.md)",
  678. f"[QiWe 开放平台文档索引]({DOC_BASE}/README.md)",
  679. )
  680. return text
  681. def inject_api_refs(line: str) -> str:
  682. ids = re.findall(r"API-\d+", line)
  683. if not ids:
  684. return ""
  685. return f"\n**接口入参/出参表:** {api_ref(ids)}\n"
  686. def main():
  687. old = SRC.read_text(encoding="utf-8")
  688. # 保留 §三 目录表 + §四~十四 模块正文
  689. m = re.search(r"(## 三、功能目录总表[\s\S]*)", old)
  690. tail = m.group(1) if m else ""
  691. tail = enhance_feature_section(tail)
  692. header = """# 企微客户服务 — 功能实现说明(开发用)
  693. > **文档定位:** 在 [功能清单](./企微客户服务-功能清单.md) **同一套功能条目**基础上,为开发人员补充:**每条功能的详细实现流程**、**QiWe 官方接口入参/出参**(对照 [QiWe 开放平台文档](./文档/QiWe开放平台文档/README.md))。
  694. > **表格用法:** [§三 功能目录总表](#三功能目录总表) 当**目录**,点击「详述」跳到对应功能点。
  695. > **接口速查:** [§二 QiWe 官方接口](#二qiwe-官方接口速查含入参出参) 含可点击的 method、params、响应字段。
  696. > **仅官方接口汇总:** [官方接口按模块统计](./企微客户服务-官方接口按模块统计.md)
  697. > **更新:** 2026-05-19
  698. ---
  699. ## 一、阅读说明
  700. | 标记 | 含义 |
  701. |------|------|
  702. | **QiWe 官方** | `POST {QIWEI_BASE_URL}/api/qw/doApi`,Header `X-QIWEI-TOKEN`,body `{ "method", "params" }` |
  703. | **本方** | 自研服务 / DB / 定时任务(路径为建议名) |
  704. | **【占位】** | 官方文档未提供专用接口(如读在线文档正文),**禁止编造 method** |
  705. | **本地文档** | [./文档/QiWe开放平台文档/](./文档/QiWe开放平台文档/README.md) 内 md,可离线查阅;与 [doc.qiweapi.com](https://doc.qiweapi.com/) 同步 |
  706. | **⚠️ PoC** | 须联调验证(置顶、加好友、文件下载等) |
  707. **统一请求示例:**
  708. ```json
  709. {
  710. "method": "/msg/syncMsg",
  711. "params": {
  712. "guid": "设备ID-来自登录",
  713. "msgSeq": 0,
  714. "limit": 50
  715. }
  716. }
  717. ```
  718. **统一响应外壳:**
  719. ```json
  720. {
  721. "code": 0,
  722. "data": { },
  723. "msg": "成功"
  724. }
  725. ```
  726. ---
  727. """
  728. section1_end = render_section_2()
  729. out = header + section1_end + "\n---\n\n" + tail
  730. out = re.sub(
  731. r"\*\*维护:\*\*.*",
  732. "**维护:** 官方接口变更时同步更新 [§二](#二qiwe-官方接口速查含入参出参) 与 [QiWe 开放平台文档](./文档/QiWe开放平台文档/README.md);新增功能先改 §三 目录,再补对应模块小节。",
  733. out,
  734. )
  735. OUT.write_text(out, encoding="utf-8")
  736. print("wrote", OUT, "lines", len(out.splitlines()))
  737. if __name__ == "__main__":
  738. main()