7.7 KiB
name, description, agent_created
| name | description | agent_created |
|---|---|---|
| tencent-docs-mcp | 通过腾讯文档 MCP 批量操作文档空间:迁移 Markdown 知识库到空间、建目录树、 批量建文档、验收内容完整性。当用户要求「把内容传到腾讯文档」「迁移知识库到腾讯文档」 「在腾讯文档建目录/文档」「同步文档到腾讯文档空间」时使用。 也适用于排查 tdoc_call 的 502 / no_token 报错。 | true |
腾讯文档 MCP 批量操作
批量把 Markdown / 结构化内容写入腾讯文档空间,并做完整性验收。
一、🔴 最先要会排查的两个报错
ERROR:http_failed - Tunnel connection failed: 502 Bad Gateway
这个报错具有极强误导性 —— 看起来是网络/代理故障,实际 99% 是 宿主下发的 API 域名不存在。
排查三步:
# 1. 开调试,看 provider 下发的 api_base 是什么
TDOC_DEBUG=1 python tencentdocs.py tdoc_call tencent-docs <tool> '{}'
# → [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,只能建在个人根目录 |
推论:
- 无法用「查子节点」判断文件夹是否为空 → 删除前的空检查要靠全量遍历判断
- 删嵌套树不能一把梭 → 得逐个删,或删完再处理被提升的节点
- 建文档进空间必须两步:先建、再移
三、建文档进空间的正确路径
# 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 是官方为一步建好设计的。
建文件夹:
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日”(弯引号) |
| 链接 | [文字](路径) |
只剩文字,路径不显示 |
正确判据
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 个,按需取用)
python tencentdocs.py tdoc_list tencent-docs # 列全部工具
python tencentdocs.py tdoc_schema tencent-docs <tool> # 查参数(★ 调用前必做)
python tencentdocs.py tdoc_init # 检查 token
--no-proxy 可绕过代理(但通常不需要,问题在域名);--timeout 调超时。
常用分组:
manage.*— 创建/移动/搜索/权限/导入导出(create_filemove_file_to_spacefolder_listsearch_file)doc.*— Word 文档读写(create_with_markdown★insert_markdowninsert_tablefind_and_replace)create_space_node/query_space_node/query_space_list/delete_space_node— 空间树sheet.*/smartsheet.*/slide_*/flowchart.*— 表格/智能表/幻灯片/流程图get_content— 读文档纯文本(验收必备)
六、幂等与可续跑
批量任务一定要能断点续跑 —— 33 次调用中途断掉不能从头来。
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 不可逆。执行前必须:
- 用全量遍历确认该节点子树内 0 篇文档(因为查不了子节点)
- 把待删结构 dump 到文件留档
- 向用户确认
⚠️ remove_type=all 不是递归删除(见第二节),别指望一把删干净。