|
|
@@ -0,0 +1,804 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""根据 doc/文档/QiWe开放平台文档 重写 企微客户服务-功能实现说明.md"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import json
|
|
|
+import re
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+ROOT = Path(__file__).resolve().parents[1]
|
|
|
+OUT = ROOT / "doc" / "企微客户服务-功能实现说明.md"
|
|
|
+SRC = ROOT / "doc" / "企微客户服务-功能实现说明.md"
|
|
|
+QIWE_README = ROOT / "doc" / "文档" / "QiWe开放平台文档" / "README.md"
|
|
|
+DOC_BASE = "./文档/QiWe开放平台文档"
|
|
|
+
|
|
|
+COMMON_HEADERS = """| Header | 必填 | 说明 |
|
|
|
+|--------|:----:|------|
|
|
|
+| `Content-Type` | 是 | `application/json` |
|
|
|
+| `X-QIWEI-TOKEN` | 是 | 控制台申请的租户 Token,写入 `QIWEI_TOKEN` |"""
|
|
|
+
|
|
|
+DOAPI = """| 项 | 值 |
|
|
|
+|----|-----|
|
|
|
+| URL | `POST {QIWEI_BASE_URL}/api/qw/doApi`(默认 `http://manager.qiweapi.com/qiwe/api/qw/doApi`) |
|
|
|
+| Body 结构 | `{ "method": "<路径>", "params": { ... } }` |"""
|
|
|
+
|
|
|
+
|
|
|
+def compact_json(text: str) -> str:
|
|
|
+ return re.sub(r"\s+", " ", text.strip())
|
|
|
+
|
|
|
+
|
|
|
+def build_io_table(
|
|
|
+ method: str | None,
|
|
|
+ req_example: str | None,
|
|
|
+ resp_example: str | None,
|
|
|
+ req: list[tuple[str, str, str]],
|
|
|
+ resp: list[tuple[str, str, str]],
|
|
|
+ passive: bool = False,
|
|
|
+) -> str:
|
|
|
+ if passive and req_example and not req_example.strip().startswith("{"):
|
|
|
+ inp = req_example.strip()
|
|
|
+ elif passive:
|
|
|
+ inp = "无(QiWe POST → callbackUrl)"
|
|
|
+ elif req_example:
|
|
|
+ inp = compact_json(req_example)
|
|
|
+ elif method and method not in ("【占位】", "见官方页"):
|
|
|
+ m = method.split()[0]
|
|
|
+ keys = ", ".join(f'"{r[0]}": ""' for r in req[:8])
|
|
|
+ inp = compact_json(f'{{"method": "{m}", "params": {{ {keys} }}}}')
|
|
|
+ else:
|
|
|
+ inp = "—"
|
|
|
+
|
|
|
+ if resp_example:
|
|
|
+ out = compact_json(resp_example)
|
|
|
+ elif resp:
|
|
|
+ data_keys = ", ".join(f'"{r[0]}": ""' for r in resp[:8])
|
|
|
+ out = compact_json(f'{{"code": 0, "data": {{ {data_keys} }}, "msg": "成功"}}')
|
|
|
+ else:
|
|
|
+ out = "—"
|
|
|
+
|
|
|
+ # 单元格内用行内代码,避免表格嵌套代码块无法渲染
|
|
|
+ inp_cell = inp if inp == "—" else f"`{inp}`"
|
|
|
+ out_cell = out if out == "—" else f"`{out}`"
|
|
|
+ return "\n".join(
|
|
|
+ [
|
|
|
+ "| 输入 | 输出 |",
|
|
|
+ "|------|------|",
|
|
|
+ f"| {inp_cell} | {out_cell} |",
|
|
|
+ "",
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def api_block(
|
|
|
+ api_id: str,
|
|
|
+ title: str,
|
|
|
+ method: str | None,
|
|
|
+ official: str,
|
|
|
+ local_md: str | None,
|
|
|
+ desc: str,
|
|
|
+ req: list[tuple[str, str, str]],
|
|
|
+ resp: list[tuple[str, str, str]],
|
|
|
+ req_example: str | None = None,
|
|
|
+ resp_example: str | None = None,
|
|
|
+ extra: str = "",
|
|
|
+ passive: bool = False,
|
|
|
+) -> str:
|
|
|
+ local_link = f"[{Path(local_md).stem}]({DOC_BASE}/md/{local_md})" if local_md else "—"
|
|
|
+ method_line = f"`{method}`" if method else "被动推送"
|
|
|
+ off_link = (
|
|
|
+ f"[官方]({official})"
|
|
|
+ if official.startswith("http")
|
|
|
+ else "—"
|
|
|
+ )
|
|
|
+ parts = [
|
|
|
+ f'<a id="{api_id.lower()}"></a>',
|
|
|
+ "",
|
|
|
+ f"### {api_id} {title}",
|
|
|
+ "",
|
|
|
+ f"| method | 官方 | 本地 |",
|
|
|
+ f"|--------|------|------|",
|
|
|
+ f"| {method_line} | {off_link} | {local_link} |",
|
|
|
+ "",
|
|
|
+ ]
|
|
|
+ if desc.strip():
|
|
|
+ parts += [desc.strip(), ""]
|
|
|
+ parts.append(build_io_table(method, req_example, resp_example, req, resp, passive))
|
|
|
+ if extra:
|
|
|
+ parts += [extra, ""]
|
|
|
+ return "\n".join(parts)
|
|
|
+
|
|
|
+
|
|
|
+APIS: list[dict] = []
|
|
|
+
|
|
|
+def add(**kw):
|
|
|
+ APIS.append(kw)
|
|
|
+
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-01",
|
|
|
+ title="租户 Token / 快速开始",
|
|
|
+ method=None,
|
|
|
+ official="https://doc.qiweapi.com/doc-7562288",
|
|
|
+ local_md=None,
|
|
|
+ desc="> 在 [QiWe 控制台](http://manager.qiweapi.com/login) 申请 API Key;无单独 method。",
|
|
|
+ req=[],
|
|
|
+ resp=[],
|
|
|
+ req_example='Header: X-QIWEI-TOKEN',
|
|
|
+ resp_example='{"code": 0, "msg": "success"}',
|
|
|
+ passive=True,
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-02",
|
|
|
+ title="创建设备与扫码登录(四步)",
|
|
|
+ method="/client/createClient 等",
|
|
|
+ official="https://doc.qiweapi.com/api-344613850",
|
|
|
+ local_md="创建设备(步骤1).md",
|
|
|
+ desc="""人员账号纳入系统需完成登录闭环(亦可控制台在线登录直接拿 `guid`):
|
|
|
+
|
|
|
+| 步骤 | 接口文档 | method(以官方为准) |
|
|
|
+|:----:|----------|---------------------|
|
|
|
+| 1 | [创建设备](https://doc.qiweapi.com/api-344613850) | `/client/createClient` |
|
|
|
+| 2 | [二维码-获取](https://doc.qiweapi.com/api-344613856) | 见官方页 |
|
|
|
+| 3 | [二维码状态-检测](https://doc.qiweapi.com/api-344613857) | 见官方页 |
|
|
|
+| 4 | [二维码-code验证](https://doc.qiweapi.com/api-344613858) | 见官方页 |
|
|
|
+
|
|
|
+⚠️ 创建设备后 5 分钟内未登录,实例会被清理。""",
|
|
|
+ req=[
|
|
|
+ ("deviceName", "string", "设备名称(步骤1 必填)"),
|
|
|
+ ("deviceType", "integer", "0=ipad(推荐), 2=windows 等"),
|
|
|
+ ("clientVersion", "string", "客户端版本,一般可空"),
|
|
|
+ ("areaCode", "integer", "地区代理 ID,与登录地一致"),
|
|
|
+ ("proxyUrl", "string", "可选 socks5 代理"),
|
|
|
+ ("aid", "string", "可选本地 Aid 代理"),
|
|
|
+ ],
|
|
|
+ resp=[("guid", "string", "设备 ID,后续所有 params.guid")],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/client/createClient",
|
|
|
+ "params": {
|
|
|
+ "deviceName": "店长-ipad",
|
|
|
+ "deviceType": 0,
|
|
|
+ "clientVersion": "",
|
|
|
+ "areaCode": 320000,
|
|
|
+ "proxyUrl": ""
|
|
|
+ }
|
|
|
+}""",
|
|
|
+ resp_example='{"code": 0, "data": {"guid": "a3318ad6-xxxx"}, "msg": "成功"}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-03",
|
|
|
+ title="设置回调地址",
|
|
|
+ method="/client/setCallback",
|
|
|
+ official="https://doc.qiweapi.com/api-354411522",
|
|
|
+ local_md="设置回调地址.md",
|
|
|
+ desc="按 **Token** 配置回调;一个 Token 下所有账号共用。推送体含 `guid` 区分账号。",
|
|
|
+ req=[
|
|
|
+ ("callbackUrl", "string", "本方公网 URL,如 `https://{域名}/api/qiwei/webhook`"),
|
|
|
+ ("authType", "string", "如 `Authorization`"),
|
|
|
+ ("authSecret", "string", "回调鉴权密钥,可空"),
|
|
|
+ ],
|
|
|
+ resp=[],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/client/setCallback",
|
|
|
+ "params": {
|
|
|
+ "callbackUrl": "https://your.domain/api/qiwei/webhook",
|
|
|
+ "authType": "Authorization",
|
|
|
+ "authSecret": ""
|
|
|
+ }
|
|
|
+}""",
|
|
|
+ resp_example='{"code": 0, "msg": "成功"}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-04",
|
|
|
+ title="Webhook 回调结构(被动)",
|
|
|
+ method=None,
|
|
|
+ official="https://doc.qiweapi.com/doc-7331304",
|
|
|
+ local_md="回调结构说明.md",
|
|
|
+ desc="""QiWe **POST** 至 `callbackUrl`;`data[]` 含 `cmd`(15000 普通消息)、`guid`、`msgType`(13=链接)、`fromRoomId`、`msgData`。详见 [回调结构说明](%s/md/回调结构说明.md)。本项目:`cmd=15000` + `msgType=13` → 解析 `msgData.linkUrl` 发现沟通记录表。""" % DOC_BASE,
|
|
|
+ req=[],
|
|
|
+ resp=[
|
|
|
+ ("data[].cmd", "integer", "回调类型"),
|
|
|
+ ("data[].guid", "string", "账号"),
|
|
|
+ ("data[].msgType", "integer", "消息类型"),
|
|
|
+ ("data[].fromRoomId", "string", "群 ID"),
|
|
|
+ ("data[].msgData", "object", "消息内容"),
|
|
|
+ ],
|
|
|
+ passive=True,
|
|
|
+ resp_example="""{
|
|
|
+ "code": 0,
|
|
|
+ "data": [{
|
|
|
+ "cmd": 15000,
|
|
|
+ "guid": "xxx",
|
|
|
+ "msgType": 13,
|
|
|
+ "fromRoomId": "10791082xxxx",
|
|
|
+ "msgData": {
|
|
|
+ "title": "沟通记录",
|
|
|
+ "linkUrl": "https://doc.weixin.qq.com/doc/xxx?docid=YYY"
|
|
|
+ },
|
|
|
+ "timestamp": 1708324990
|
|
|
+ }],
|
|
|
+ "msg": "成功"
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-05",
|
|
|
+ title="用户状态(在线检测)",
|
|
|
+ method="/login/checkLogin",
|
|
|
+ official="https://doc.qiweapi.com/api-347221662",
|
|
|
+ local_md="用户状态.md",
|
|
|
+ desc="查询指定 `guid` 是否在线,批量任务前应调用。",
|
|
|
+ req=[("guid", "string", "设备 ID")],
|
|
|
+ resp=[
|
|
|
+ ("userOnlineStatus", "integer", "-1 需扫码;0 可免扫码;1 已扫码待确认;2 在线;4 取消;10 待验证码"),
|
|
|
+ ("userId", "string", "企微用户 ID"),
|
|
|
+ ("nickname", "string", "昵称"),
|
|
|
+ ("corpName", "string", "企业名称"),
|
|
|
+ ("lastActiveTime", "integer", "最后活跃时间"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/login/checkLogin", "params": {"guid": "{{guid}}"}}',
|
|
|
+ resp_example="""{
|
|
|
+ "code": 0,
|
|
|
+ "data": {
|
|
|
+ "userOnlineStatus": 2,
|
|
|
+ "userId": "1688852****",
|
|
|
+ "nickname": "店长A",
|
|
|
+ "corpName": "某某公司"
|
|
|
+ },
|
|
|
+ "msg": "成功"
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-06",
|
|
|
+ title="同步历史消息分页",
|
|
|
+ method="/msg/syncMsg",
|
|
|
+ official="https://doc.qiweapi.com/api-344613926",
|
|
|
+ local_md="同步历史消息分页.md",
|
|
|
+ desc="补拉历史群聊;`msgSeq` 递增分页直至 `hasMore=0`。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("msgSeq", "integer", "游标,首次 0,下次用上次返回的 seq"),
|
|
|
+ ("limit", "integer", "每页条数,如 10~100"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("hasMore", "integer", "是否还有下一页"),
|
|
|
+ ("travelSyncKey", "integer", "同步游标"),
|
|
|
+ ("syncMsgList[]", "array", "消息列表,含 fromRoomId、msgType、msgData、seq 等"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/msg/syncMsg", "params": {"guid": "{{guid}}", "msgSeq": 0, "limit": 50}}',
|
|
|
+ resp_example="""{
|
|
|
+ "code": 0,
|
|
|
+ "data": {
|
|
|
+ "hasMore": 1,
|
|
|
+ "travelSyncKey": 922174,
|
|
|
+ "syncMsgList": [{
|
|
|
+ "fromRoomId": 1023,
|
|
|
+ "msgType": 13,
|
|
|
+ "msgData": {"linkUrl": "https://doc.weixin.qq.com/..."},
|
|
|
+ "seq": 9221964,
|
|
|
+ "timestamp": 1708324990
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ "msg": "成功"
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-07",
|
|
|
+ title="群分页",
|
|
|
+ method="/room/getRoomList",
|
|
|
+ official="https://doc.qiweapi.com/api-344613881",
|
|
|
+ local_md="群分页.md",
|
|
|
+ desc="仅查**本人创建**的群;查全部群需结合 [会话分页](https://doc.qiweapi.com/api-344613938)(`sessionType=1` 为群 id)。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("nextStartIndex", "integer", "分页游标,首次 0"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("hasMore", "integer", "是否有下一页"),
|
|
|
+ ("nextStartIndex", "integer", "下次请求传入"),
|
|
|
+ ("roomCount", "integer", "本页数量"),
|
|
|
+ ("roomList[].roomId", "string", "群 ID"),
|
|
|
+ ("roomList[].roomName", "string", "群名称"),
|
|
|
+ ("roomList[].roomMemberCount", "integer", "成员数"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/room/getRoomList", "params": {"guid": "{{guid}}", "nextStartIndex": 0}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-08",
|
|
|
+ title="群详情-批量",
|
|
|
+ method="/room/batchGetRoomDetail",
|
|
|
+ official="https://doc.qiweapi.com/api-344613882",
|
|
|
+ local_md="群详情-批量.md",
|
|
|
+ desc="先 API-07 拿 `roomId`,再批量查详情;成员**显示名**需再调 API-18。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("roomIdList", "string[]", "群 ID 列表"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("roomList[].roomId", "string", "群 ID"),
|
|
|
+ ("roomList[].roomName", "string", "群名称"),
|
|
|
+ ("roomList[].roomAnnouncement", "string", "群公告(合规可匹配文档链接)"),
|
|
|
+ ("roomList[].memberList[]", "array", "成员列表 userId、joinTime 等"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/room/batchGetRoomDetail", "params": {"guid": "{{guid}}", "roomIdList": ["10802031057945400"]}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-09",
|
|
|
+ title="群成员变动查询",
|
|
|
+ method="见官方页",
|
|
|
+ official="https://doc.qiweapi.com/api-437674162",
|
|
|
+ local_md="群成员变动查询.md",
|
|
|
+ desc="按群 + 时间窗查询进退群记录(method 以 [官方页](https://doc.qiweapi.com/api-437674162) 为准)。",
|
|
|
+ req=[("guid", "string", "设备 ID"), ("roomId", "string", "群 ID"), ("startTime/endTime", "integer", "时间窗(以官方为准)")],
|
|
|
+ resp=[("memberEvents[]", "array", "进退群事件列表(字段以官方为准)")],
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-10",
|
|
|
+ title="创建群",
|
|
|
+ method="/room/createRoom",
|
|
|
+ official="https://doc.qiweapi.com/api-344613883",
|
|
|
+ local_md="创建群.md",
|
|
|
+ desc="新建外部客户群并返回 `roomId`。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("isOuterRoom", "integer", "1=外部群"),
|
|
|
+ ("memberList", "string[]", "初始成员 userId 列表"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("roomId", "string", "新群 ID"),
|
|
|
+ ("roomCreatetime", "integer", "创建时间"),
|
|
|
+ ("memberList", "string[]", "成员列表"),
|
|
|
+ ],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/room/createRoom",
|
|
|
+ "params": {
|
|
|
+ "guid": "{{guid}}",
|
|
|
+ "isOuterRoom": 1,
|
|
|
+ "memberList": ["168885****57534"]
|
|
|
+ }
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-11",
|
|
|
+ title="修改群公告",
|
|
|
+ method="/room/modifyRoomNotice",
|
|
|
+ official="https://doc.qiweapi.com/api-344613890",
|
|
|
+ local_md="修改群公告.md",
|
|
|
+ desc="将沟通记录文档链接写入群公告。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("roomId", "string", "群 ID"),
|
|
|
+ ("notice", "string", "公告正文(可含文档 URL)"),
|
|
|
+ ],
|
|
|
+ resp=[("code", "integer", "0=成功")],
|
|
|
+ req_example='{"method": "/room/modifyRoomNotice", "params": {"guid": "{{guid}}", "roomId": "108144***", "notice": "沟通记录:https://doc.weixin.qq.com/..."}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-12",
|
|
|
+ title="群消息置顶-列表",
|
|
|
+ method="/msg/roomTopMessageList",
|
|
|
+ official="https://doc.qiweapi.com/api-344613920",
|
|
|
+ local_md="群消息置顶-列表.md",
|
|
|
+ desc="⚠️ **仅群主**可置顶;用于合规检查文档是否已置顶。",
|
|
|
+ req=[("guid", "string", "设备 ID"), ("roomId", "string", "群 ID")],
|
|
|
+ resp=[
|
|
|
+ ("list[].msgUniqueIdentifier", "string", "消息唯一标识"),
|
|
|
+ ("list[].msgType", "integer", "消息类型"),
|
|
|
+ ("list[].msgData", "object", "消息体"),
|
|
|
+ ("list[].senderId", "string", "发送人"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/msg/roomTopMessageList", "params": {"guid": "{{guid}}", "roomId": "1088541******6"}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-13",
|
|
|
+ title="群消息置顶-添加",
|
|
|
+ method="/msg/roomTopMessageSet",
|
|
|
+ official="https://doc.qiweapi.com/api-344613921",
|
|
|
+ local_md="群消息置顶-添加.md",
|
|
|
+ desc="⚠️ PoC:须传原消息的 msgId、msgSenderId、msgTimestamp、msgType、msgData。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("roomId", "string", "群 ID"),
|
|
|
+ ("msgId", "string", "消息 id"),
|
|
|
+ ("msgSenderId", "string", "发送人 userId"),
|
|
|
+ ("msgTimestamp", "integer", "发送时间戳"),
|
|
|
+ ("msgType", "integer", "消息类型"),
|
|
|
+ ("msgData", "object", "如 `{ \"content\": \"...\" }`"),
|
|
|
+ ],
|
|
|
+ resp=[("code", "integer", "0=成功")],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/msg/roomTopMessageSet",
|
|
|
+ "params": {
|
|
|
+ "guid": "{{guid}}",
|
|
|
+ "roomId": "10965*****579",
|
|
|
+ "msgId": "CIGABBDd*****",
|
|
|
+ "msgSenderId": "16888****804",
|
|
|
+ "msgTimestamp": 1752224990,
|
|
|
+ "msgType": 0,
|
|
|
+ "msgData": {"content": "沟通记录表链接"}
|
|
|
+ }
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-14",
|
|
|
+ title="发送纯文本消息",
|
|
|
+ method="/msg/sendText",
|
|
|
+ official="https://doc.qiweapi.com/api-344613906",
|
|
|
+ local_md="发送纯文本消息.md",
|
|
|
+ desc="整改通知、预警、待办提醒等推送到用户或群。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("content", "string", "文本内容"),
|
|
|
+ ("toId", "string", "用户 userId 或群 roomId"),
|
|
|
+ ("isNoNeedRead", "boolean", "可选,是否无需已读"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("isSendSuccess", "integer", "是否发送成功"),
|
|
|
+ ("msgServerId", "integer", "消息服务端 ID"),
|
|
|
+ ("msgUniqueIdentifier", "string", "消息唯一标识"),
|
|
|
+ ("seq", "integer", "序号"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/msg/sendText", "params": {"guid": "{{guid}}", "content": "请更新沟通记录表", "toId": "168****768657", "isNoNeedRead": true}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-15",
|
|
|
+ title="群发消息",
|
|
|
+ method="/msg/sendGroupMsg",
|
|
|
+ official="https://doc.qiweapi.com/api-344613923",
|
|
|
+ local_md="群发消息.md",
|
|
|
+ desc="每天对每个客户/群仅可群发一次;`sendType`:0=外部联系人,1=外部群。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("sendType", "integer", "0 联系人 / 1 群"),
|
|
|
+ ("toIdList", "string[]", "接收方 ID 列表"),
|
|
|
+ ("msgList[]", "array", "消息列表,type:0 文本、13 链接、14 图片等"),
|
|
|
+ ],
|
|
|
+ resp=[("groupMsgId", "integer", "群发任务 ID,供 API-16 查询")],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/msg/sendGroupMsg",
|
|
|
+ "params": {
|
|
|
+ "guid": "{{guid}}",
|
|
|
+ "sendType": 1,
|
|
|
+ "toIdList": ["10791082****"],
|
|
|
+ "msgList": [{"type": 0, "msgData": {"content": "本周运营内容"}}]
|
|
|
+ }
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-16",
|
|
|
+ title="群发消息-状态查询",
|
|
|
+ method="/msg/sendGroupMsgStatus",
|
|
|
+ official="https://doc.qiweapi.com/api-344613924",
|
|
|
+ local_md="群发消息-状态查询.md",
|
|
|
+ desc="根据 `groupMsgId` 轮询发送进度。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("groupMsgId", "string", "群发任务 ID"),
|
|
|
+ ("endDetailId", "integer", "分页游标"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("hasSend", "boolean", "是否已发送"),
|
|
|
+ ("isEnd", "boolean", "是否结束"),
|
|
|
+ ("total", "integer", "总数"),
|
|
|
+ ("customerList[]", "array", "各接收方状态"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/msg/sendGroupMsgStatus", "params": {"guid": "{{guid}}", "groupMsgId": "115258331353230686", "endDetailId": 2}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-17",
|
|
|
+ title="外部联系人分页",
|
|
|
+ method="/contact/getWxContactList",
|
|
|
+ official="https://doc.qiweapi.com/api-344613869",
|
|
|
+ local_md="外部联系人分页.md",
|
|
|
+ desc="分页拉外部联系人;拿到 `userId` 后再调 API-18 查详情。建议落库后靠回调增量更新。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("currentSeq", "integer", "游标,首次 0"),
|
|
|
+ ("limit", "integer", "每页条数"),
|
|
|
+ ("bizType", "integer", "1=联系人变动;2=好友申请"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("hasMore", "boolean", "是否有下一页"),
|
|
|
+ ("currentSeq", "integer", "下次请求游标"),
|
|
|
+ ("contactList[].userId", "string", "用户 ID"),
|
|
|
+ ("contactList[].nickname", "string", "昵称"),
|
|
|
+ ("contactList[].remark", "string", "备注"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/contact/getWxContactList", "params": {"guid": "{{guid}}", "currentSeq": 0, "limit": 50, "bizType": 1}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-18",
|
|
|
+ title="联系人详情-批量",
|
|
|
+ method="/contact/batchGetUserinfo",
|
|
|
+ official="https://doc.qiweapi.com/api-344613868",
|
|
|
+ local_md="联系人详情-批量.md",
|
|
|
+ desc="批量查联系人详情(含群成员真实姓名场景)。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("userIdList", "string[]", "用户 ID 列表"),
|
|
|
+ ],
|
|
|
+ resp=[
|
|
|
+ ("contactList[].userId", "string", "用户 ID"),
|
|
|
+ ("contactList[].nickname", "string", "昵称"),
|
|
|
+ ("contactList[].mobile", "string", "手机号"),
|
|
|
+ ("contactList[].avatarUrl", "string", "头像"),
|
|
|
+ ],
|
|
|
+ req_example='{"method": "/contact/batchGetUserinfo", "params": {"guid": "{{guid}}", "userIdList": ["168*****5548"]}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-19",
|
|
|
+ title="客户标签-增删",
|
|
|
+ method="/label/contactEditLabel",
|
|
|
+ official="https://doc.qiweapi.com/api-344613937",
|
|
|
+ local_md="客户标签-增删.md",
|
|
|
+ desc="`opType`:1=增加,2=删除;`labelIdList`/`labelSuperIdList`/`labelOwnerList` 须一一对应。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("opType", "integer", "1 增 / 2 删"),
|
|
|
+ ("paramList[].userId", "string", "客户 userId"),
|
|
|
+ ("paramList[].labelIdList", "string[]", "标签 ID"),
|
|
|
+ ],
|
|
|
+ resp=[("data", "array", "操作结果")],
|
|
|
+ req_example="""{
|
|
|
+ "method": "/label/contactEditLabel",
|
|
|
+ "params": {
|
|
|
+ "guid": "{{guid}}",
|
|
|
+ "opType": 1,
|
|
|
+ "paramList": [{
|
|
|
+ "userId": "78813023**",
|
|
|
+ "labelIdList": ["1407374973784***"],
|
|
|
+ "labelSuperIdList": ["1407375223060***"],
|
|
|
+ "labelOwnerList": ["168885236**"]
|
|
|
+ }]
|
|
|
+ }
|
|
|
+}""",
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-20",
|
|
|
+ title="添加群成员好友",
|
|
|
+ method="/contact/addRoomContact",
|
|
|
+ official="https://doc.qiweapi.com/api-425758709",
|
|
|
+ local_md="添加群成员好友.md",
|
|
|
+ desc="⚠️ PoC:从群内发起加好友,须合规确认。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("roomId", "string", "群 ID"),
|
|
|
+ ("userId", "string", "目标成员 userId"),
|
|
|
+ ("verifyText", "string", "验证语"),
|
|
|
+ ],
|
|
|
+ resp=[("code", "integer", "0=成功")],
|
|
|
+ req_example='{"method": "/contact/addRoomContact", "params": {"guid": "{{guid}}", "roomId": "1079271***", "userId": "168885***", "verifyText": "您好"}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-21",
|
|
|
+ title="企微文件下载",
|
|
|
+ method="/cloud/wxWorkDownload",
|
|
|
+ official="https://doc.qiweapi.com/api-344613901",
|
|
|
+ local_md="企微文件下载.md",
|
|
|
+ desc="⚠️ 候选 PoC:从消息里的 fileId/fileAeskey 下载;返回临时 `cloudUrl`(7–15 天清理)。**不能替代「读在线文档正文」**。",
|
|
|
+ req=[
|
|
|
+ ("guid", "string", "设备 ID"),
|
|
|
+ ("fileId", "string", "文件 ID"),
|
|
|
+ ("fileAeskey", "string", "AES 密钥"),
|
|
|
+ ("fileSize", "integer", "文件大小"),
|
|
|
+ ("fileType", "integer", "1 大图 / 2 小图 / 4 视频 / 5 文件语音等"),
|
|
|
+ ],
|
|
|
+ resp=[("cloudUrl", "string", "临时下载地址")],
|
|
|
+ req_example='{"method": "/cloud/wxWorkDownload", "params": {"guid": "{{guid}}", "fileId": "...", "fileAeskey": "...", "fileSize": 32768, "fileType": 5}}',
|
|
|
+)
|
|
|
+
|
|
|
+add(
|
|
|
+ api_id="API-22",
|
|
|
+ title="读在线文档正文",
|
|
|
+ method="【占位】",
|
|
|
+ official="—",
|
|
|
+ local_md=None,
|
|
|
+ desc="**QiWe 开放平台当前无「按 docid 读取企微在线文档表格正文」的专用接口。** 合规读表须 PoC API-21 或等待官方能力;禁止编造 method。",
|
|
|
+ req=[],
|
|
|
+ resp=[],
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def render_section_2() -> str:
|
|
|
+ lines = [
|
|
|
+ "## 二、QiWe 官方接口速查(含入参/出参)",
|
|
|
+ "",
|
|
|
+ "> 文档来源:[QiWe 开放平台文档索引](%s/README.md)(本地最新爬取,2026)。 " % DOC_BASE,
|
|
|
+ "> 各接口仅保留 **输入 / 输出** 一张表(报文示例);公共 Header 见 §2.0。",
|
|
|
+ "",
|
|
|
+ "### 2.0 统一调用方式",
|
|
|
+ "",
|
|
|
+ COMMON_HEADERS,
|
|
|
+ "",
|
|
|
+ DOAPI,
|
|
|
+ "",
|
|
|
+ "### 2.1 接口索引",
|
|
|
+ "",
|
|
|
+ "| 编号 | 接口 | method | 官方 | 本地 md |",
|
|
|
+ "|:----:|------|--------|------|---------|",
|
|
|
+ ]
|
|
|
+ for a in APIS:
|
|
|
+ local = f"[{a['local_md']}]({DOC_BASE}/md/{a['local_md']})" if a.get("local_md") else "—"
|
|
|
+ off = a["official"]
|
|
|
+ if off.startswith("http"):
|
|
|
+ off_cell = f"[在线]({off})"
|
|
|
+ else:
|
|
|
+ off_cell = "—"
|
|
|
+ m = a.get("method") or "被动"
|
|
|
+ lines.append(f"| {a['api_id']} | {a['title']} | `{m}` | {off_cell} | {local} |")
|
|
|
+ lines.append("")
|
|
|
+ lines.append("### 2.2 各接口详细说明")
|
|
|
+ lines.append("")
|
|
|
+ for a in APIS:
|
|
|
+ lines.append(
|
|
|
+ api_block(
|
|
|
+ a["api_id"],
|
|
|
+ a["title"],
|
|
|
+ a.get("method"),
|
|
|
+ a["official"],
|
|
|
+ a.get("local_md"),
|
|
|
+ a["desc"],
|
|
|
+ a.get("req", []),
|
|
|
+ a.get("resp", []),
|
|
|
+ a.get("req_example"),
|
|
|
+ a.get("resp_example"),
|
|
|
+ a.get("extra", ""),
|
|
|
+ passive=a.get("passive", False),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
+def api_ref(ids: list[str]) -> str:
|
|
|
+ links = []
|
|
|
+ for api_id in ids:
|
|
|
+ a = next(x for x in APIS if x["api_id"] == api_id)
|
|
|
+ links.append(f"[{api_id} {a['title']}](#{api_id.lower()})")
|
|
|
+ return "、".join(links) + "(详见 §二)"
|
|
|
+
|
|
|
+
|
|
|
+def enhance_feature_section(text: str) -> str:
|
|
|
+ """在功能小节中补充接口文档链接与入参出参指引"""
|
|
|
+ text = re.sub(r"\n\*\*接口(文档与)?入参/出参[^\*]*\*\*[^\n]+\n", "\n", text)
|
|
|
+ text = re.sub(
|
|
|
+ r"(\*\*接口入参/出参表:\*\*[^\n]+\n)(?:\1)+",
|
|
|
+ r"\1",
|
|
|
+ text,
|
|
|
+ )
|
|
|
+
|
|
|
+ def repl_official(m):
|
|
|
+ api = m.group(1)
|
|
|
+ a = next((x for x in APIS if x["api_id"] == api), None)
|
|
|
+ if not a:
|
|
|
+ return m.group(0)
|
|
|
+ block = f"\n\n**接口文档:** {api_ref([api])}\n"
|
|
|
+ if a.get("method") and a["method"] not in ("【占位】", "见官方页", None):
|
|
|
+ block += f"\n**method:** `{a['method']}`\n"
|
|
|
+ if a.get("req"):
|
|
|
+ block += "\n**主要入参:** " + "、".join(f"`{r[0]}`" for r in a["req"][:6])
|
|
|
+ if len(a["req"]) > 6:
|
|
|
+ block += " …"
|
|
|
+ block += "\n"
|
|
|
+ if a.get("resp"):
|
|
|
+ block += "**主要出参:** " + "、".join(f"`{r[0]}`" for r in a["resp"][:6])
|
|
|
+ if len(a["resp"]) > 6:
|
|
|
+ block += " …"
|
|
|
+ block += "\n"
|
|
|
+ return m.group(0) + block
|
|
|
+
|
|
|
+ # 在「需实现的 QiWe 官方接口」段落后注入
|
|
|
+ text = re.sub(
|
|
|
+ r"(\*\*需实现的 QiWe 官方接口[::]\*\*[^\n]*\n)",
|
|
|
+ lambda m: m.group(1) + inject_api_refs(m.group(1)),
|
|
|
+ text,
|
|
|
+ )
|
|
|
+ text = text.replace(
|
|
|
+ "[callback-structure.md](./qiweapi-scrape/callback-structure.md)",
|
|
|
+ f"[回调结构说明]({DOC_BASE}/md/回调结构说明.md)",
|
|
|
+ )
|
|
|
+ text = text.replace("sync 页需重爬", f"[同步历史消息分页]({DOC_BASE}/md/同步历史消息分页.md)")
|
|
|
+ text = text.replace("(爬取 md 需重爬)", f"(见 [{DOC_BASE}/md/同步历史消息分页.md]({DOC_BASE}/md/同步历史消息分页.md))")
|
|
|
+ text = re.sub(
|
|
|
+ r"\| 编号 \| 接口 \| 爬取文档 \|\n\|:----:\|------\|----------\|\n(?:\| API-\d+[^\n]+\n)+",
|
|
|
+ lambda m: m.group(0).replace("爬取文档", "文档").replace("见官方链接", "见 §二"),
|
|
|
+ text,
|
|
|
+ )
|
|
|
+ text = text.replace(
|
|
|
+ "[platform-intro.md](../output/qiweapi-test/platform-intro.md)",
|
|
|
+ f"[QiWe 开放平台文档索引]({DOC_BASE}/README.md)",
|
|
|
+ )
|
|
|
+ return text
|
|
|
+
|
|
|
+
|
|
|
+def inject_api_refs(line: str) -> str:
|
|
|
+ ids = re.findall(r"API-\d+", line)
|
|
|
+ if not ids:
|
|
|
+ return ""
|
|
|
+ return f"\n**接口入参/出参表:** {api_ref(ids)}\n"
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ old = SRC.read_text(encoding="utf-8")
|
|
|
+ # 保留 §三 目录表 + §四~十四 模块正文
|
|
|
+ m = re.search(r"(## 三、功能目录总表[\s\S]*)", old)
|
|
|
+ tail = m.group(1) if m else ""
|
|
|
+ tail = enhance_feature_section(tail)
|
|
|
+
|
|
|
+ header = """# 企微客户服务 — 功能实现说明(开发用)
|
|
|
+
|
|
|
+> **文档定位:** 在 [功能清单](./企微客户服务-功能清单.md) **同一套功能条目**基础上,为开发人员补充:**每条功能的详细实现流程**、**QiWe 官方接口入参/出参**(对照 [QiWe 开放平台文档](./文档/QiWe开放平台文档/README.md))。
|
|
|
+> **表格用法:** [§三 功能目录总表](#三功能目录总表) 当**目录**,点击「详述」跳到对应功能点。
|
|
|
+> **接口速查:** [§二 QiWe 官方接口](#二qiwe-官方接口速查含入参出参) 含可点击的 method、params、响应字段。
|
|
|
+> **仅官方接口汇总:** [官方接口按模块统计](./企微客户服务-官方接口按模块统计.md)
|
|
|
+> **更新:** 2026-05-19
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、阅读说明
|
|
|
+
|
|
|
+| 标记 | 含义 |
|
|
|
+|------|------|
|
|
|
+| **QiWe 官方** | `POST {QIWEI_BASE_URL}/api/qw/doApi`,Header `X-QIWEI-TOKEN`,body `{ "method", "params" }` |
|
|
|
+| **本方** | 自研服务 / DB / 定时任务(路径为建议名) |
|
|
|
+| **【占位】** | 官方文档未提供专用接口(如读在线文档正文),**禁止编造 method** |
|
|
|
+| **本地文档** | [./文档/QiWe开放平台文档/](./文档/QiWe开放平台文档/README.md) 内 md,可离线查阅;与 [doc.qiweapi.com](https://doc.qiweapi.com/) 同步 |
|
|
|
+| **⚠️ PoC** | 须联调验证(置顶、加好友、文件下载等) |
|
|
|
+
|
|
|
+**统一请求示例:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "method": "/msg/syncMsg",
|
|
|
+ "params": {
|
|
|
+ "guid": "设备ID-来自登录",
|
|
|
+ "msgSeq": 0,
|
|
|
+ "limit": 50
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**统一响应外壳:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "data": { },
|
|
|
+ "msg": "成功"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+"""
|
|
|
+
|
|
|
+ section1_end = render_section_2()
|
|
|
+ out = header + section1_end + "\n---\n\n" + tail
|
|
|
+ out = re.sub(
|
|
|
+ r"\*\*维护:\*\*.*",
|
|
|
+ "**维护:** 官方接口变更时同步更新 [§二](#二qiwe-官方接口速查含入参出参) 与 [QiWe 开放平台文档](./文档/QiWe开放平台文档/README.md);新增功能先改 §三 目录,再补对应模块小节。",
|
|
|
+ out,
|
|
|
+ )
|
|
|
+ OUT.write_text(out, encoding="utf-8")
|
|
|
+ print("wrote", OUT, "lines", len(out.splitlines()))
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|