Files

195 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <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**,只能建在个人根目录 |
**推论**:
- 无法用「查子节点」判断文件夹是否为空 → 删除前的空检查要靠**全量遍历**判断
- 删嵌套树不能一把梭 → 得逐个删,或删完再处理被提升的节点
- 建文档进空间必须两步:**先建、再移**
---
## 三、建文档进空间的正确路径
```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 <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 次调用中途断掉不能从头来。
```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` 不是递归删除(见第二节),别指望一把删干净。