--- name: tencent-docs-mcp description: > 通过腾讯文档 MCP 批量操作文档空间:迁移 Markdown 知识库到空间、建目录树、 批量建文档、验收内容完整性。当用户要求「把内容传到腾讯文档」「迁移知识库到腾讯文档」 「在腾讯文档建目录/文档」「同步文档到腾讯文档空间」时使用。 也适用于排查 tdoc_call 的 502 / no_token 报错。 agent_created: true --- # 腾讯文档 MCP 批量操作 批量把 Markdown / 结构化内容写入腾讯文档空间,并做完整性验收。 ## 一、🔴 最先要会排查的两个报错 ### `ERROR:http_failed - Tunnel connection failed: 502 Bad Gateway` **这个报错具有极强误导性** —— 看起来是网络/代理故障,实际 99% 是 **宿主下发的 API 域名不存在**。 排查三步: ```bash # 1. 开调试,看 provider 下发的 api_base 是什么 TDOC_DEBUG=1 python tencentdocs.py tdoc_call tencent-docs '{}' # → [TencentDocsSkillCredential] token provider result personal=available api_base=https://xxx # 2. 验证该域名是否存在 nslookup <上面那个域名> # → NXDOMAIN / Non-existent domain ⇒ 就是它的问题 # 3. 显式覆盖成正确域名 TDOC_API_BASE_URL=https://docs.qq.com python tencentdocs.py ... ``` 个人版正确域名 = **`https://docs.qq.com`**。 ⚠️ **不要**因为看到 502 就去 unset 代理 —— 那会变成 `getaddrinfo failed` (本机往往无法直连外网,代理是必需的)。**问题在域名,不在代理。** ### `ERROR:no_token - 未检测到腾讯文档登录票据` 连接器没授权。让用户在连接器管理里授权「腾讯文档」即可。 `provider personal=not_connected enterprise=connector_disabled` 就是没连。 --- ## 二、🔴 三个与文档描述不符的 API 行为 | 接口 | 文档说 | 实测 | |---|---|---| | `query_space_node` | 传 `parent_node_id` 查子节点 | **该参数完全被忽略**,总返回整个空间的扁平节点列表 | | `delete_space_node` | `remove_type=all` 递归删除子节点 | **只删当前节点,子节点被提升到根目录** | | `doc.create_with_markdown` | — | **不接收 space_id / parent_id**,只能建在个人根目录 | **推论**: - 无法用「查子节点」判断文件夹是否为空 → 删除前的空检查要靠**全量遍历**判断 - 删嵌套树不能一把梭 → 得逐个删,或删完再处理被提升的节点 - 建文档进空间必须两步:**先建、再移** --- ## 三、建文档进空间的正确路径 ```python # 1) 建文档 + 写内容(原子操作) r = call("tencent-docs", "doc.create_with_markdown", { "title": title, "base64_markdown": base64.b64encode(md.encode()).decode(), }) file_id = r["data"]["file_id"] # 2) 移入目标文件夹 call("tencent-docs", "manage.move_file_to_space", { "file_id": file_id, "space_id": SPACE, "target_parent_id": folder_id, }) ``` **为什么不用 `insert_markdown`**:那条路要先 `create_space_node(node_type=wiki_tdoc)` 建一个**空文档节点**,再写内容。中途失败就留下**空壳文档**, 而且两步之间用户点进去会看到空白页。`create_with_markdown` 是官方为一步建好设计的。 建文件夹: ```python call("tencent-docs", "create_space_node", { "space_id": SPACE, "title": name, "node_type": "wiki_folder", "wiki_folder_node": {"title": name}, }) ``` `node_type` 还支持 `wiki_tdoc`(在线文档)、`link`。 **耗时参考**:33 篇文档 ≈ **1 分 28 秒**。不必并发,串行足够。 --- ## 四、🔴 验收判据:用「句子覆盖率」,别用片段指纹 这一步反复翻车过两次,方法论比代码重要。 ### 为什么片段指纹不行 **坑 1:`get_content` 返回纯文本,表格结构信息全丢** 表格被拆成「单元格文本逐行拼接」,**没有 `|` 分隔符**。 拿源 md 的表格行(`预算明细表 | | | | |`)去比对必然失败。 **坑 2:三类格式差异会造成假的「内容缺失」** | 差异 | 源 Markdown | 腾讯文档渲染后 | |---|---|---| | 列表编号 | `1. 必须带日期:…` | `必须带日期:…`(自动编号,**不返回前缀**)| | 引号 | `"9月19日"`(直角) | `”9月19日”`(弯引号)| | 链接 | `[文字](路径)` | 只剩`文字`,路径不显示 | ### 正确判据 ```python def norm(s): """激进归一化:只留实义字符""" s = re.sub(r"[\s\u3000\u00a0]+", "", s) s = re.sub(r"[|*`>#_~\[\]()()【】「」『』{}]", "", s) s = re.sub(r"[\"'‘’“”‚‛„‟‹›«»]", "", s) # 各类引号(含弯引号!) s = re.sub(r"[,,。.::;;!!??、-—–\-·…]", "", s) s = re.sub(r"^\d+", "", s) # 列表编号残留 return s def sentences(md): body = re.sub(r"^---\n.*?\n---\n+", "", md, flags=re.S) # 去 frontmatter body = re.sub(r"```[\s\S]*?```", "", body) # 去代码块 body = re.sub(r"^#{1,6}\s+", "", body, flags=re.M) # 标题标记 body = re.sub(r"^\s*\d+\.\s+", "", body, flags=re.M) # 有序列表编号 body = re.sub(r"^\s*[-*+]\s+", "", body, flags=re.M) # 无序列表符号 body = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", body) # 链接→只留文字 body = re.sub(r"\*\*|__|`|~~", "", body) body = re.sub(r"^\s*\|", "", body, flags=re.M) body = re.sub(r"\|\s*$", "", body, flags=re.M) body = re.sub(r"\|\s*\|", " ", body) parts = re.split(r"[。!?;\n]", body) return [p.strip() for p in parts if len(norm(p)) >= 8] # 短句无判别力,丢掉 # 覆盖率 ≥ 90% 判通过 miss = [s for s in sentences(src) if norm(s) not in norm(got)] cov = 1 - len(miss) / len(sentences(src)) ``` 实测结果:33/33 通过,多数 100%,最低 91% (91% 那几篇差异来自表格跨行边界,**不是内容丢失**)。 --- ## 五、工具清单(224 个,按需取用) ```bash python tencentdocs.py tdoc_list tencent-docs # 列全部工具 python tencentdocs.py tdoc_schema tencent-docs # 查参数(★ 调用前必做) python tencentdocs.py tdoc_init # 检查 token ``` `--no-proxy` 可绕过代理(但通常不需要,问题在域名);`--timeout` 调超时。 常用分组: - `manage.*` — 创建/移动/搜索/权限/导入导出(`create_file` `move_file_to_space` `folder_list` `search_file`) - `doc.*` — Word 文档读写(`create_with_markdown` ★ `insert_markdown` `insert_table` `find_and_replace`) - `create_space_node` / `query_space_node` / `query_space_list` / `delete_space_node` — 空间树 - `sheet.*` / `smartsheet.*` / `slide_*` / `flowchart.*` — 表格/智能表/幻灯片/流程图 - `get_content` — 读文档纯文本(验收必备) --- ## 六、幂等与可续跑 批量任务一定要能断点续跑 —— 33 次调用中途断掉不能从头来。 ```python STATE = Path("_migrate_state.json") # {"folders": {名: id}, "docs": {key: {file_id, ...}}} if stem in st["docs"]: print("跳过(已完成)") continue # ... 完成后立刻写回 st["docs"][stem] = {"file_id": fid, "title": title, "section": sec} save_state(st) # 每篇写完就落盘,不要等全部结束 ``` 文件夹同样做「已存在则复用」: 先查根目录同名节点,有就复用,没有才建 —— 避免重跑产生一堆重复文件夹。 ⚠️ 因为 `parent_node_id` 不生效,只能靠**全量列表 + 标题匹配**来找。 --- ## 七、危险操作 `delete_space_node` 不可逆。执行前必须: 1. 用**全量遍历**确认该节点子树内 **0 篇文档**(因为查不了子节点) 2. 把待删结构 dump 到文件留档 3. 向用户确认 ⚠️ `remove_type=all` 不是递归删除(见第二节),别指望一把删干净。