Files

7.7 KiB
Raw Permalink Blame History

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_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 次调用中途断掉不能从头来。

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 不是递归删除(见第二节),别指望一把删干净。