# -*- 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'', "", 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()