dingtalk-docs
管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、搜索文档、读取或写入文档内容、创建文件夹整理文档时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要操作多维表、管理日程、发消息或处理审批流时触发。
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-dingtalk-docs
Repository
Skill path: skills/aliramw/dingtalk-docs
管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、搜索文档、读取或写入文档内容、创建文件夹整理文档时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要操作多维表、管理日程、发消息或处理审批流时触发。
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install dingtalk-docs into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding dingtalk-docs to shared team environments
- Use dingtalk-docs for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: dingtalk-docs
description: 管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、搜索文档、读取或写入文档内容、创建文件夹整理文档时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要操作多维表、管理日程、发消息或处理审批流时触发。
version: 0.3.4
metadata:
openclaw:
requires:
bins:
- mcporter
env:
- DINGTALK_MCP_DOCS_URL
primaryEnv: DINGTALK_MCP_DOCS_URL
homepage: https://github.com/aliramw/dingtalk-docs
---
# 钉钉云文档 Skill
## Overview
用户可能要求你创建、搜索、读取或编辑钉钉云文档。操作之间存在严格依赖关系:必须先获取 ID 才能执行后续操作。
## 严格禁止
1. **禁止编造 ID** -- dentryUuid 必须从返回值中提取,编造 ID 会操作到错误文档或报错
2. **创建前必须先获取根目录 ID** -- 必须先调 get_my_docs_root_dentry_uuid 拿到 rootDentryUuid
3. **禁止混淆两个创建方法** -- create_doc_under_node 只能创建文档,create_dentry_under_node 支持文件夹/表格/PPT 等多种类型
4. **写入前必须确认 updateType** -- 0=覆盖(清空后写入),1=续写(追加到末尾),搞反会丢数据,不确定时必须先问用户
5. **禁止只传 ID 读内容** -- 必须拼成完整 URL `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}`
6. **禁止在用户说"表格"时默认创建文档** -- 可能要在线表格(accessType="1")或多维表(accessType="7"),不确定必须先问
7. **禁止传错参数类型** -- accessType 必须是字符串,updateType 必须是数字,类型传错会导致静默失败
## 可用方法列表
| 方法 | 用途 | 必填参数 | 可用性 |
|------|------|---------|--------|
| `get_my_docs_root_dentry_uuid` | 获取"我的文档"根目录 ID | 无 | 稳定可用 |
| `list_accessible_documents` | 搜索有权限的文档 | 无 (keyword 选填) | 稳定可用 |
| `create_doc_under_node` | 创建在线文档 | name, parentDentryUuid | 稳定可用 |
| `create_dentry_under_node` | 创建节点 (文档/表格/文件夹等) | name, accessType, parentDentryUuid | 稳定可用 |
| `write_content_to_document` | 写入文档内容 (覆盖或续写) | content, updateType, targetDentryUuid | 稳定可用 |
| `get_document_content_by_url` | 通过 URL 获取文档 Markdown 内容 | docUrl | **灰度中,部分实例不可见** |
## 灰度发布说明(重要)
根据 GitHub issue #1 下维护者的明确回复:`get_document_content_by_url` **目前在灰度中,全量还需要一点时间**。
因此你必须按下面规则处理:
1. **如果 MCP 客户端里只看到 5 个工具,不要先判断为配置错误**
2. **如果缺少 `get_document_content_by_url`,不要先判断为权限缺失**
3. 通过钉钉 MCP 广场拿到的 URL,当前很可能因为**服务端未放量**而看不到该方法
4. 在该方法未放开前,Skill 应把“读文档内容”视为**条件可用能力**,不是所有环境都保证存在
5. 向用户说明时要直接说清:**这是官方灰度状态,不是本地接入姿势问题**
## 意图判断
用户说"创建文档/新建文档/写个文档/帮我建个文档":
- 创建文档 → 先 get_my_docs_root_dentry_uuid,再 create_doc_under_node
- 创建到指定文件夹 → 用文件夹的 dentryUuid 作为 parentDentryUuid
用户说"建文件夹/新建目录/整理一下文档":
- 创建文件夹 → create_dentry_under_node(accessType="13")
用户说"创建表格/建个PPT/做个脑图":
- 非文档类型 → create_dentry_under_node,accessType: 表格="1",PPT="2",脑图="6",多维表="7"
- 用户说"表格"但不确定类型 → 先问是在线表格还是多维表
关键区分: 在线表格(accessType="1") vs 多维表(accessType="7") vs 文档(用 create_doc_under_node)
用户说"搜索/找文档/查一下/有没有某个文档":
- 搜索 → list_accessible_documents(keyword=关键词)
用户说"读文档/看看内容/打开文档/这个文档写了什么":
- **先确认当前 MCP 服务是否真的暴露了 `get_document_content_by_url`**
- 有 URL 且该方法可用 → 直接 get_document_content_by_url
- 有文档名且该方法可用 → 先 list_accessible_documents 搜索,拿到 dentryUuid,拼 URL 再读
- 如果当前实例缺少 `get_document_content_by_url` → 明确告诉用户:**该读取能力目前仍在官方灰度中,你的实例暂未放开**,不要把原因归咎于用户配置
用户说"写入/更新内容/编辑文档/往文档里加点东西":
- 全新内容或替换 → write_content_to_document(updateType=0) 覆盖
- 追加内容 → write_content_to_document(updateType=1) 续写
- 不确定 → 问用户是覆盖还是追加
## 核心工作流
创建文档并写入:
1. get_my_docs_root_dentry_uuid() → 提取 rootDentryUuid
2. create_doc_under_node(name, parentDentryUuid=rootDentryUuid) → 提取 dentryUuid
3. (HARD-GATE: 必须确认 updateType) write_content_to_document(content, updateType=0, targetDentryUuid=dentryUuid) → 提取写入结果
4. get_document_content_by_url(docUrl="https://alidocs.dingtalk.com/i/nodes/{dentryUuid}") → 验证
搜索并读取(仅当 `get_document_content_by_url` 已放量可用时):
1. list_accessible_documents(keyword="关键词") → 提取 docs[].dentryUuid
2. get_document_content_by_url(docUrl="https://alidocs.dingtalk.com/i/nodes/{dentryUuid}")
如果当前实例没有 `get_document_content_by_url`:
- 停在搜索结果这一步
- 明确提示用户该能力仍处于官方灰度阶段
- 不要伪造“读取成功”或编造替代读接口
创建文件夹并整理:
1. get_my_docs_root_dentry_uuid() → 提取 rootDentryUuid
2. create_dentry_under_node(name, accessType="13", parentDentryUuid=rootDentryUuid) → 提取 dentryUuid
3. create_doc_under_node(name, parentDentryUuid=文件夹dentryUuid)
## 上下文传递规则
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| get_my_docs_root_dentry_uuid | rootDentryUuid | create_doc_under_node / create_dentry_under_node 的 parentDentryUuid |
| create_doc_under_node | dentryUuid | write_content_to_document 的 targetDentryUuid,拼 URL 读内容 |
| create_dentry_under_node | dentryUuid | 作为子节点的 parentDentryUuid |
| list_accessible_documents | docs[].dentryUuid | 拼成 `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` 用于读取 |
## CRITICAL: 参数格式
```jsonc
// [正确] docUrl 必须是完整 URL
{"docUrl": "https://alidocs.dingtalk.com/i/nodes/DnRL6jAJ..."}
// [错误] 只传 ID → 报错
{"docUrl": "DnRL6jAJ..."}
// [正确] accessType 是字符串
{"name": "报表", "accessType": "1", "parentDentryUuid": "xxx"}
// [错误] accessType 传数字 → 静默失败
{"name": "报表", "accessType": 1, "parentDentryUuid": "xxx"}
// [正确] updateType 是数字
{"content": "...", "updateType": 0, "targetDentryUuid": "xxx"}
// [错误] updateType 传字符串 → 静默失败
{"content": "...", "updateType": "0", "targetDentryUuid": "xxx"}
```
## 本地文件脚本说明
`scripts/` 目录中的辅助脚本会处理本地文件输入 / 输出:
- `import_docs.py` 会读取工作区内的 `.md` / `.txt` / `.markdown` 文件并导入到钉钉文档
- `export_docs.py` 会将钉钉文档内容导出为工作区内的本地 Markdown 文件
- `create_doc.py` 会调用 `mcporter` 创建文档并写入内容
这些脚本都受以下规则约束:
- 仅允许访问工作区内路径
- 使用 `resolve_safe_path()` 防止目录遍历
- 限制文件大小和扩展名
- 仅通过 `mcporter` 调用 MCP 服务,不直接发起网络请求
## 错误处理
1. 遇到错误: 展示错误信息给用户,不要自行猜测解决方案
2. "Invalid credentials": 提示用户重新配置凭证
3. "Permission denied": 提示用户确认对该文档有操作权限
4. "Document not found": 用 list_accessible_documents 重新搜索确认文档是否存在
5. 如果方法列表里根本没有 `get_document_content_by_url`:按**官方灰度未放量**处理,不要误报为本地配置错误
6. 错误码 52600007: 可能是企业账号限制或父节点 ID 无效,确认 parentDentryUuid 来源
## 详细参考 (按需读取)
- [references/api-reference.md](./references/api-reference.md) -- 完整参数 Schema + 返回值 + 节点类型枚举
- [references/error-codes.md](./references/error-codes.md) -- 错误码说明 + 调试流程
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/api-reference.md
```markdown
# 钉钉云文档参数参考
> 完整的参数 Schema、返回值格式和调用示例。SKILL.md 的按需加载参考文件。
## 1. list_accessible_documents
搜索当前用户有权限访问的文档列表。当用户想找某个文档、查看文档列表时使用。与 get_document_content_by_url 不同:本方法搜索文档元信息,后者读取文档正文内容。
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| keyword | string | 否 | 搜索关键词,为空时返回所有有权限的文档 |
**返回值:**
```json
{
"docs": [
{
"dentryUuid": "DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1",
"title": "项目计划",
"type": "document",
"updateTime": "2026-03-01T10:00:00Z"
}
]
}
```
**关键字段:**
- `dentryUuid` — 文档唯一 ID,用于拼接 `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` 读取内容
- `title` — 文档标题,用于展示给用户确认
**调用示例:**
```bash
# 搜索包含"项目"的文档
mcporter call dingtalk-docs.list_accessible_documents --args '{"keyword": "项目"}'
# 列出所有有权限的文档
mcporter call dingtalk-docs.list_accessible_documents
```
---
## 2. get_my_docs_root_dentry_uuid
获取当前用户"我的文档"空间的根目录节点 ID。当用户想创建文档或文件夹时,必须先调用此方法获取根目录 ID 作为父节点。与 list_accessible_documents 不同:本方法获取根节点 ID,后者搜索文档。
**参数:** 无
**返回值:**
```json
{
"rootDentryUuid": "DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1"
}
```
**关键字段:**
- `rootDentryUuid` — 根目录 ID,用作 create_doc_under_node 或 create_dentry_under_node 的 parentDentryUuid
**调用示例:**
```bash
mcporter call dingtalk-docs.get_my_docs_root_dentry_uuid
```
---
## 3. create_doc_under_node
在指定父节点下创建一篇新的在线文档。当用户只需要创建文档时使用。与 create_dentry_under_node 不同:本方法只能创建文档,后者支持文件夹、表格、PPT 等多种类型。
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| name | string | 是 | 文档名称 |
| parentDentryUuid | string | 是 | 父节点 ID(来源:get_my_docs_root_dentry_uuid 的 rootDentryUuid 或文件夹的 dentryUuid) |
**返回值:**
```json
{
"dentryUuid": "abc123def456",
"title": "项目计划",
"createTime": "2026-03-01T10:00:00Z",
"url": "https://alidocs.dingtalk.com/i/nodes/abc123def456"
}
```
**关键字段:**
- `dentryUuid` — 新文档 ID,用于 write_content_to_document 的 targetDentryUuid,或拼接 URL 读取内容
- `url` — 文档访问链接
**调用示例:**
```bash
mcporter call dingtalk-docs.create_doc_under_node --args '{"name": "我的新文档", "parentDentryUuid": "ROOT_ID"}'
```
---
## 4. create_dentry_under_node
在指定节点下创建新节点,支持多种类型。当用户需要创建文件夹、表格、PPT、脑图等非文档类型时使用。与 create_doc_under_node 不同:本方法支持所有节点类型,后者只能创建文档。
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| name | string | 是 | 节点名称 |
| accessType | string | 是 | 节点类型(**必须是字符串**,见下方枚举表) |
| parentDentryUuid | string | 是 | 父节点 ID(来源:get_my_docs_root_dentry_uuid 的 rootDentryUuid 或文件夹的 dentryUuid) |
**accessType 节点类型枚举:**
| 值 | 类型 | 说明 |
|----|------|------|
| "0" | 文档 | 在线文档(等同于 create_doc_under_node) |
| "1" | 表格 | 在线表格 |
| "2" | PPT | 在线演示文稿 |
| "3" | 白板 | 在线白板 |
| "6" | 脑图 | 思维导图 |
| "7" | 多维表 | AI 多维表 |
| "9" | 视频 | 视频文件 |
| "10" | 图片 | 图片文件 |
| "13" | 文件夹 | 文件夹(用于整理文档) |
| "14" | PDF | PDF 文件 |
| "99" | 其他文件 | 其他类型文件 |
**⚠️ accessType 必须是字符串类型,传数字会静默失败。**
**返回值:**
```json
{
"dentryUuid": "folder789xyz",
"title": "项目资料",
"createTime": "2026-03-01T10:00:00Z"
}
```
**关键字段:**
- `dentryUuid` — 新节点 ID,文件夹可作为后续创建操作的 parentDentryUuid
**调用示例:**
```bash
# 创建文件夹
mcporter call dingtalk-docs.create_dentry_under_node --args '{"name": "项目资料", "accessType": "13", "parentDentryUuid": "ROOT_ID"}'
# 创建表格
mcporter call dingtalk-docs.create_dentry_under_node --args '{"name": "数据报表", "accessType": "1", "parentDentryUuid": "ROOT_ID"}'
# 创建脑图
mcporter call dingtalk-docs.create_dentry_under_node --args '{"name": "需求分析", "accessType": "6", "parentDentryUuid": "ROOT_ID"}'
```
---
## 5. write_content_to_document
将文本内容写入目标文档,支持覆盖或续写模式。当用户想写入、更新或追加文档内容时使用。与 get_document_content_by_url 不同:本方法写入内容,后者读取内容。
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| content | string | 是 | 要写入的内容(支持 Markdown 格式) |
| updateType | number | 是 | 0=覆盖写入(**清空原内容**),1=续写(追加到末尾)。**必须是数字**,传字符串会静默失败 |
| targetDentryUuid | string | 是 | 目标文档 ID(来源:create_doc_under_node 的 dentryUuid 或搜索结果的 dentryUuid) |
**⚠️ 覆盖写入(updateType=0)会清空文档原有内容,操作前应确认用户意图。不确定时先问用户。**
**返回值:**
```json
{
"success": true
}
```
**调用示例:**
```bash
# 覆盖写入
mcporter call dingtalk-docs.write_content_to_document --args '{"content": "# 项目计划\n\n## 目标\n完成 Q1 目标", "updateType": 0, "targetDentryUuid": "doc_xxx"}'
# 续写(追加内容)
mcporter call dingtalk-docs.write_content_to_document --args '{"content": "\n\n## 更新日志\n- 2026-03-02: 初始版本", "updateType": 1, "targetDentryUuid": "doc_xxx"}'
```
---
## 6. get_document_content_by_url
根据文档 URL 获取文档内容,返回 Markdown 格式。当用户想查看、读取文档内容时使用。与 list_accessible_documents 不同:本方法读取文档正文,后者搜索文档元信息。
**灰度说明:** 该方法目前仍处于官方灰度发布阶段。部分通过钉钉 MCP 广场接入的实例,方法列表里可能看不到它,只会显示另外 5 个稳定工具。遇到这种情况,优先按**服务端未放量**处理,不要先判断为本地配置错误或权限问题。
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| docUrl | string | 是 | 文档完整 URL,格式:`https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` |
**⚠️ 必须传完整 URL,不能只传 dentryUuid。需要先通过其他方法获取 dentryUuid,再拼接成完整 URL。**
**返回值:**
```json
{
"content": "# 文档标题\n\n正文内容...",
"format": "markdown"
}
```
**关键字段:**
- `content` — 文档的 Markdown 格式内容
**调用示例:**
```bash
mcporter call dingtalk-docs.get_document_content_by_url --args '{"docUrl": "https://alidocs.dingtalk.com/i/nodes/DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1"}'
```
---
## 完整工作流示例
### 创建文档并写入内容
```bash
# 1. 获取根目录 ID
ROOT_ID=$(mcporter call dingtalk-docs.get_my_docs_root_dentry_uuid --output json | jq -r '.rootDentryUuid')
# 2. 创建新文档
DOC_ID=$(mcporter call dingtalk-docs.create_doc_under_node --args "{\"name\": \"项目计划\", \"parentDentryUuid\": \"$ROOT_ID\"}" --output json | jq -r '.dentryUuid')
# 3. 写入内容
mcporter call dingtalk-docs.write_content_to_document --args "{\"content\": \"# 项目计划\\n\\n## 目标\\n完成 Q1 目标\", \"updateType\": 0, \"targetDentryUuid\": \"$DOC_ID\"}"
# 4. 验证内容
mcporter call dingtalk-docs.get_document_content_by_url --args "{\"docUrl\": \"https://alidocs.dingtalk.com/i/nodes/$DOC_ID\"}"
```
### 搜索并读取文档(仅当 `get_document_content_by_url` 已放量可用时)
```bash
# 1. 搜索文档
mcporter call dingtalk-docs.list_accessible_documents --args '{"keyword": "项目"}'
# 2. 获取文档内容(假设搜索到 dentryUuid=abc123)
mcporter call dingtalk-docs.get_document_content_by_url --args '{"docUrl": "https://alidocs.dingtalk.com/i/nodes/abc123"}'
```
如果当前实例的方法列表里没有 `get_document_content_by_url`,说明该能力暂未对你的实例放量;此时只能先停在搜索结果,不要误判成本地配置问题。
### 创建文件夹并整理文档
```bash
# 1. 获取根目录
ROOT_ID=$(mcporter call dingtalk-docs.get_my_docs_root_dentry_uuid --output json | jq -r '.rootDentryUuid')
# 2. 创建文件夹
FOLDER_ID=$(mcporter call dingtalk-docs.create_dentry_under_node --args "{\"name\": \"2026 项目\", \"accessType\": \"13\", \"parentDentryUuid\": \"$ROOT_ID\"}" --output json | jq -r '.dentryUuid')
# 3. 在文件夹中创建文档
mcporter call dingtalk-docs.create_doc_under_node --args "{\"name\": \"Q1 计划\", \"parentDentryUuid\": \"$FOLDER_ID\"}"
```
```
### references/error-codes.md
```markdown
# 钉钉云文档错误码参考
> SKILL.md 的按需加载参考文件。遇到错误时查阅此文档。
## 错误码速查表
| 错误信息 | 原因 | 修复动作 |
|----------|------|----------|
| Invalid credentials | 凭证配置错误或令牌过期 | 访问 https://mcp.dingtalk.com 重新获取 URL,执行 `mcporter config add dingtalk-docs --url "<新URL>"` |
| Permission denied | 当前用户无权限操作该文档 | 确认文档分享权限,检查是否被锁定或只读,联系文档所有者授权 |
| Document not found | dentryUuid 无效或文档已删除 | 确认 ID 来自返回值(禁止编造),用 list_accessible_documents 重新搜索 |
| 52600007 | 企业账号限制或父节点 ID 无效 | 确认企业钉钉账号,确认 parentDentryUuid 来自 get_my_docs_root_dentry_uuid |
| Timeout | 网络超时 | 检查网络连接,稍后重试 |
| Invalid parameter | 参数类型或格式错误 | 见下方参数类型速查 |
| 方法列表里没有 `get_document_content_by_url` / 只显示 5 个工具 | 官方灰度未放量 | 按服务端未放量处理,不要优先判断为本地配置错误或权限问题 |
## 参数类型速查
| 参数 | 正确 | 错误 | 后果 |
|------|------|------|------|
| accessType | `"13"`(字符串) | `13`(数字) | 静默失败 |
| updateType | `0`(数字) | `"0"`(字符串) | 静默失败 |
| docUrl | `https://alidocs.dingtalk.com/i/nodes/{id}`(完整 URL) | `DnRL6jAJ...`(只传 ID) | 报错 |
## 调试流程
```
1. 检查错误信息 → 对照上方速查表
2. 确认参数格式 → accessType 是字符串,updateType 是数字,docUrl 是完整 URL
3. 确认 ID 来源 → 所有 dentryUuid 必须从返回值中提取
4. 检查权限 → 确认用户对文档有操作权限
5. 检查配置 → 确认凭证 URL 配置正确
6. 如果方法列表里缺少 `get_document_content_by_url` → 按官方灰度未放量处理
7. 重试 → 排除服务端临时故障
```
## 日志位置
```bash
~/.mcporter/logs/ # mcporter 日志
~/.openclaw/logs/ # 技能执行日志
```
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# 钉钉文档操作技能 (dingtalk-docs)
管理钉钉云文档中的文档、文件夹和内容。支持文档搜索、创建、内容读写和文件夹整理。
## 功能特性
- ✅ 文档搜索 — 搜索有权限访问的文档
- ✅ 文档创建 — 在指定节点下创建新文档
- ✅ 多类型节点创建 — 支持文档/表格/PPT/文件夹等 11 种类型
- ✅ 内容写入 — 覆盖写入或续写模式(支持 Markdown)
- ✅ 内容读取 — 通过 URL 获取文档 Markdown 内容(当前为灰度能力)
- ✅ 根目录获取 — 获取"我的文档"根节点 ID
## 当前已知限制
### `get_document_content_by_url` 仍在灰度发布
根据 GitHub issue #1 下维护者的回复:`get_document_content_by_url` **目前还在灰度中,全量发布还需要一点时间**。
这意味着:
- 通过 **钉钉 MCP 广场** 获取的 Streamable HTTP URL,接入后**可能只会看到 5 个工具**
- 缺失 `get_document_content_by_url` **不代表你配置错了,也不一定是权限问题**
- 当前更可能是**官方服务端尚未对你的实例放量**
目前常见的 5 个可见工具是:
- `list_accessible_documents`
- `get_my_docs_root_dentry_uuid`
- `create_doc_under_node`
- `create_dentry_under_node`
- `write_content_to_document`
如果你在 `mcporter list` 或其他 MCP 客户端里看不到 `get_document_content_by_url`,先按**服务端灰度未放开**处理,不要先怀疑本地配置。
## 快速开始
### 1. 安装技能
```bash
clawhub install dingtalk-docs
```
### 2. 安装依赖
```bash
npm install -g mcporter
```
### 3. 配置凭证
访问 [钉钉 MCP 广场](https://mcp.dingtalk.com) 找到 **钉钉文档** 服务,获取 Streamable HTTP URL:
```bash
mcporter config add dingtalk-docs --url "<你的_URL>"
```
也可以使用环境变量:
```bash
export DINGTALK_MCP_DOCS_URL="<你的_URL>"
```
> 这个 URL 含访问令牌,属于敏感凭证。推荐优先用 `mcporter config` 保存,避免泄露到 shell 历史。
### 4. 使用示例
```bash
# 获取根目录 ID
mcporter call dingtalk-docs.get_my_docs_root_dentry_uuid
# 创建文档
mcporter call dingtalk-docs.create_doc_under_node --args '{"name": "我的文档", "parentDentryUuid": "ROOT_ID"}'
# 搜索文档
mcporter call dingtalk-docs.list_accessible_documents --args '{"keyword": "项目"}'
# 写入内容到文档(覆盖模式)
mcporter call dingtalk-docs.write_content_to_document --args '{"content": "# 标题\n\n内容", "updateType": 0, "targetDentryUuid": "doc_xxx"}'
# 获取文档内容(仅当你的实例已放量该灰度接口时可用)
mcporter call dingtalk-docs.get_document_content_by_url --args '{"docUrl": "https://alidocs.dingtalk.com/i/nodes/doc_xxx"}'
```
## 方法列表
| 方法 | 说明 | 必填参数 | 状态 |
|------|------|---------|------|
| `get_my_docs_root_dentry_uuid` | 获取根目录 ID | 无 | 稳定可用 |
| `list_accessible_documents` | 搜索文档 | 无(keyword 选填) | 稳定可用 |
| `create_doc_under_node` | 创建文档 | name, parentDentryUuid | 稳定可用 |
| `create_dentry_under_node` | 创建节点(多类型) | name, accessType, parentDentryUuid | 稳定可用 |
| `write_content_to_document` | 写入内容 | content, updateType, targetDentryUuid | 稳定可用 |
| `get_document_content_by_url` | 获取文档内容 | docUrl | **灰度中,部分实例不可见** |
完整参数说明请查看 [references/api-reference.md](references/api-reference.md)
## 注意事项
- **accessType 必须是字符串**(如 `"13"`),传数字会静默失败
- **updateType 必须是数字**(如 `0`),传字符串会静默失败
- **docUrl 必须是完整 URL**(`https://alidocs.dingtalk.com/i/nodes/{dentryUuid}`),不能只传 ID
- 凭证 URL 包含访问令牌,请妥善保管
- 仅能操作当前用户有权限访问的文档
## 本地文件脚本说明
`scripts/` 目录中的辅助脚本会读写本地文件:
- `import_docs.py`:读取工作区内的本地 Markdown / TXT 文件并导入到钉钉文档
- `export_docs.py`:将钉钉文档内容导出到工作区内的本地 Markdown 文件
- `create_doc.py`:读取输入内容并创建文档
这些脚本已做安全限制:
- 仅允许访问工作区内路径
- 使用 `resolve_safe_path()` 防止目录遍历
- 限制文件大小和扩展名
- 仅通过 `mcporter` 调用 MCP 服务,不直接发起网络请求
## 目录结构
```
dingtalk-docs/
├── SKILL.md # AI 技能入口(≤150 行)
├── package.json # 元数据
├── README.md # 人类可读说明
├── CHANGELOG.md # 变更日志
├── references/
│ ├── api-reference.md # 完整参数 Schema + 返回值
│ └── error-codes.md # 错误码说明 + 调试流程
├── scripts/
│ ├── create_doc.py # 创建文档脚本
│ ├── import_docs.py # 导入文档脚本
│ └── export_docs.py # 导出文档脚本
└── tests/
├── test_security.py # 安全功能测试
└── TEST_REPORT.md # 测试报告
```
## 开发
```bash
# 克隆仓库
git clone https://github.com/aliramw/dingtalk-docs.git
# 运行测试
python3 tests/test_security.py -v
```
## 许可证
MIT License
## 作者
Marila@Dingtalk
```
### _meta.json
```json
{
"owner": "aliramw",
"slug": "dingtalk-docs",
"displayName": "dingtalk-docs",
"latest": {
"version": "0.3.4",
"publishedAt": 1772839202876,
"commit": "https://github.com/openclaw/skills/commit/d7567fd23c87afb2ef1cd1b8a0e92f7e8362a839"
},
"history": [
{
"version": "0.3.1",
"publishedAt": 1772711078552,
"commit": "https://github.com/openclaw/skills/commit/b941daf4a9ef26b9f37237658ec76d5d1650f613"
},
{
"version": "0.2.1",
"publishedAt": 1772612784308,
"commit": "https://github.com/openclaw/skills/commit/4c91d3adf70a0b743c4fb41f54647c7222fdc6cf"
}
]
}
```
### scripts/create_doc.py
```python
#!/usr/bin/env python3
"""
在钉钉文档中创建新文档并写入内容
用法:
python create_doc.py <title> [content]
参数:
title: 文档标题
content: 可选,文档内容(支持 Markdown 格式,默认空内容)
示例:
python create_doc.py "项目计划" "# 项目计划\n\n## 目标\n完成 Q1 目标"
python create_doc.py "会议纪要"
"""
import sys
from typing import Optional
from mcporter_utils import run_mcporter, parse_response, get_root_dentry_uuid
# ============== 安全常量 ==============
MAX_CONTENT_LENGTH = 50000 # 最大内容长度(字符)
# ============== 业务函数 ==============
def create_doc(title: str, parent_uuid: str) -> Optional[str]:
"""
创建文档
Args:
title: 文档标题
parent_uuid: 父节点 ID
Returns:
新文档的 dentryUuid,失败返回 None
"""
success, output = run_mcporter('dingtalk-docs.create_doc_under_node', {
'name': title,
'parentDentryUuid': parent_uuid
})
if not success:
print(f"❌ 创建文档失败:{output}")
return None
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
return None
dentry_uuid = result.get('dentryUuid')
url = result.get('pcUrl') or result.get('url', 'N/A')
print(f"✅ 文档创建成功:{title}")
print(f" 文档 ID: {dentry_uuid}")
print(f" 访问链接:{url}")
return dentry_uuid
def write_content(doc_uuid: str, content: str, update_type: int = 0) -> bool:
"""
写入文档内容
Args:
doc_uuid: 文档 ID
content: 内容(Markdown 格式)
update_type: 0=覆盖,1=续写
Returns:
成功返回 True
"""
if len(content) > MAX_CONTENT_LENGTH:
print(f"⚠️ 内容过长({len(content)} 字符),截断到 {MAX_CONTENT_LENGTH} 字符")
content = content[:MAX_CONTENT_LENGTH]
success, output = run_mcporter('dingtalk-docs.write_content_to_document', {
'content': content,
'updateType': update_type,
'targetDentryUuid': doc_uuid
})
if not success:
print(f"❌ 写入内容失败:{output}")
return False
print(f"✅ 内容写入成功(模式:{'覆盖' if update_type == 0 else '续写'})")
return True
def main():
"""主函数"""
if len(sys.argv) < 2:
print(__doc__)
print("错误:缺少文档标题参数")
sys.exit(1)
title = sys.argv[1].strip()
content = sys.argv[2] if len(sys.argv) > 2 else ""
if not title:
print("错误:文档标题不能为空")
sys.exit(1)
print(f"📝 开始创建文档:{title}")
print("-" * 50)
# 1. 获取根目录 ID
print("步骤 1: 获取根目录 ID...")
root_uuid = get_root_dentry_uuid()
if not root_uuid:
sys.exit(1)
print(f" 根目录 ID: {root_uuid}")
# 2. 创建文档
print("\n步骤 2: 创建文档...")
doc_uuid = create_doc(title, root_uuid)
if not doc_uuid:
sys.exit(1)
# 3. 写入内容(如果有)
if content:
print("\n步骤 3: 写入内容...")
# 处理转义字符
content = content.replace('\\n', '\n').replace('\\t', '\t')
if not write_content(doc_uuid, content):
sys.exit(1)
print("-" * 50)
print("✅ 完成!")
print(f"\n文档链接:https://alidocs.dingtalk.com/i/nodes/{doc_uuid}")
if __name__ == '__main__':
main()
```
### scripts/export_docs.py
```python
#!/usr/bin/env python3
"""
导出钉钉文档到本地文件
用法:
python export_docs.py <doc_url> [output.md]
参数:
doc_url: 钉钉文档 URL(格式:https://alidocs.dingtalk.com/i/nodes/{dentryUuid})
output.md: 可选,输出文件路径(默认:<doc_id>.md)
示例:
python export_docs.py https://alidocs.dingtalk.com/i/nodes/abc123
python export_docs.py https://alidocs.dingtalk.com/i/nodes/abc123 output.md
"""
import os
import re
import sys
from pathlib import Path
from typing import Optional
from mcporter_utils import run_mcporter, parse_response, resolve_safe_path
# ============== 安全常量 ==============
MAX_CONTENT_LENGTH = 100000 # 最大内容长度
ALLOWED_ROOT = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
DOC_URL_PATTERN = re.compile(
r'^https://alidocs\.dingtalk\.com/i/nodes/([a-zA-Z0-9]+)$',
re.IGNORECASE
)
# ============== 安全函数 ==============
def extract_doc_uuid(url: str) -> Optional[str]:
"""从 URL 提取文档 ID"""
match = DOC_URL_PATTERN.match(url.strip())
if match:
return match.group(1)
return None
def get_document_content(doc_url: str) -> Optional[str]:
"""获取文档内容"""
success, output = run_mcporter('dingtalk-docs.get_document_content_by_url', {
'docUrl': doc_url
})
if not success:
print(f"❌ 获取文档内容失败:{output}")
return None
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
return None
return result.get('content', '')
def save_content(content: str, path: Path) -> bool:
"""保存内容到文件"""
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"❌ 保存文件失败:{e}")
return False
def main():
"""主函数"""
if len(sys.argv) < 2:
print(__doc__)
print("错误:缺少文档 URL 参数")
sys.exit(1)
doc_url = sys.argv[1].strip()
output_path = sys.argv[2] if len(sys.argv) > 2 else None
# 提取文档 ID
doc_uuid = extract_doc_uuid(doc_url)
if not doc_uuid:
print("❌ 无效的文档 URL 格式")
print("正确格式:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}")
sys.exit(1)
# 确定输出文件路径
if not output_path:
output_path = f"{doc_uuid}.md"
# 解析并验证输出路径
try:
safe_output = resolve_safe_path(output_path)
except ValueError as e:
print(f"❌ {e}")
sys.exit(1)
# 确保输出文件在允许的目录内
safe_output = safe_output.resolve()
if not str(safe_output).startswith(ALLOWED_ROOT):
safe_output = Path(ALLOWED_ROOT) / safe_output.name
print(f"📥 导出文档")
print(f" 源 URL: {doc_url}")
print(f" 目标文件:{safe_output}")
print("-" * 50)
# 获取文档内容
print("步骤 1: 获取文档内容...")
content = get_document_content(doc_url)
if content is None:
sys.exit(1)
print(f" 内容长度:{len(content)} 字符")
if len(content) > MAX_CONTENT_LENGTH:
print(f"⚠️ 内容过长,截断到 {MAX_CONTENT_LENGTH} 字符")
content = content[:MAX_CONTENT_LENGTH]
# 保存文件
print("\n步骤 2: 保存文件...")
if not save_content(content, safe_output):
sys.exit(1)
print("-" * 50)
print("✅ 导出完成!")
print(f"\n文件路径:{safe_output}")
if __name__ == '__main__':
main()
```
### scripts/import_docs.py
```python
#!/usr/bin/env python3
"""
从本地文件导入文档到钉钉文档
用法:
python import_docs.py <file.md> [title]
参数:
file.md: Markdown 文件路径
title: 可选,文档标题(默认使用文件名)
示例:
python import_docs.py README.md
python import_docs.py notes.md "项目笔记"
"""
import sys
from pathlib import Path
from typing import Optional
from mcporter_utils import run_mcporter, parse_response, get_root_dentry_uuid, resolve_safe_path
# ============== 安全常量 ==============
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
MAX_CONTENT_LENGTH = 50000 # 最大内容长度
ALLOWED_EXTENSIONS = ['.md', '.txt', '.markdown']
# ============== 安全函数 ==============
def validate_file_extension(filename: str) -> bool:
"""验证文件扩展名"""
ext = Path(filename).suffix.lower()
return ext in ALLOWED_EXTENSIONS
def validate_file_size(path: Path) -> bool:
"""验证文件大小"""
size = path.stat().st_size
if size > MAX_FILE_SIZE:
print(f"❌ 文件过大:{size / 1024 / 1024:.2f}MB(最大 {MAX_FILE_SIZE / 1024 / 1024}MB)")
return False
return True
def create_doc(title: str, parent_uuid: str) -> Optional[str]:
"""创建文档"""
success, output = run_mcporter('dingtalk-docs.create_doc_under_node', {
'name': title,
'parentDentryUuid': parent_uuid
})
if not success:
print(f"❌ 创建文档失败:{output}")
return None
result = parse_response(output)
if result is None:
return None
return result.get('dentryUuid')
def write_content(doc_uuid: str, content: str) -> bool:
"""写入内容"""
success, output = run_mcporter('dingtalk-docs.write_content_to_document', {
'content': content,
'updateType': 0,
'targetDentryUuid': doc_uuid
})
if not success:
print(f"❌ 写入内容失败:{output}")
return False
return True
def read_file(path: Path) -> str:
"""读取文件内容"""
try:
# 检查文件大小
if not validate_file_size(path):
sys.exit(1)
with open(path, 'r', encoding='utf-8') as f:
return f.read()
except UnicodeDecodeError:
# 尝试其他编码
with open(path, 'r', encoding='gbk') as f:
return f.read()
except Exception as e:
print(f"❌ 读取文件失败:{e}")
sys.exit(1)
def main():
"""主函数"""
if len(sys.argv) < 2:
print(__doc__)
print("错误:缺少文件参数")
sys.exit(1)
file_path = sys.argv[1]
title = sys.argv[2].strip() if len(sys.argv) > 2 else None
# 验证文件扩展名
if not validate_file_extension(file_path):
print(f"❌ 不支持的文件类型:{Path(file_path).suffix}")
print(f"支持的类型:{', '.join(ALLOWED_EXTENSIONS)}")
sys.exit(1)
# 解析并验证路径
try:
safe_path = resolve_safe_path(file_path)
except ValueError as e:
print(f"❌ {e}")
sys.exit(1)
if not safe_path.exists():
print(f"❌ 文件不存在:{safe_path}")
sys.exit(1)
# 使用文件名作为标题(如果没有提供)
if not title:
title = safe_path.stem
print(f"📝 导入文档:{title}")
print(f" 源文件:{safe_path}")
print("-" * 50)
# 读取文件内容
print("步骤 1: 读取文件内容...")
content = read_file(safe_path)
print(f" 内容长度:{len(content)} 字符")
if len(content) > MAX_CONTENT_LENGTH:
print(f"⚠️ 内容过长,截断到 {MAX_CONTENT_LENGTH} 字符")
content = content[:MAX_CONTENT_LENGTH]
# 获取根目录 ID
print("\n步骤 2: 获取根目录 ID...")
root_uuid = get_root_dentry_uuid()
if not root_uuid:
sys.exit(1)
# 创建文档
print("\n步骤 3: 创建文档...")
doc_uuid = create_doc(title, root_uuid)
if not doc_uuid:
sys.exit(1)
# 写入内容
print("\n步骤 4: 写入内容...")
if not write_content(doc_uuid, content):
sys.exit(1)
print("-" * 50)
print("✅ 导入完成!")
print(f"\n文档链接:https://alidocs.dingtalk.com/i/nodes/{doc_uuid}")
if __name__ == '__main__':
main()
```
### scripts/mcporter_utils.py
```python
#!/usr/bin/env python3
"""
mcporter 公共工具函数
提供 mcporter 命令执行、响应解析、路径安全校验等通用功能,
供 create_doc.py、import_docs.py、export_docs.py 共用。
"""
import json
import os
import subprocess
from pathlib import Path
from typing import Optional, Tuple
def run_mcporter(tool: str, args: dict = None, timeout: int = 60) -> Tuple[bool, str]:
"""
执行 mcporter 命令(使用 --args JSON 传参)
Args:
tool: 工具名称,如 dingtalk-docs.get_my_docs_root_dentry_uuid
args: 参数字典,传入 --args JSON
timeout: 超时时间(秒)
Returns:
(success, output) 元组
"""
command = ['mcporter', 'call', tool, '--output', 'json']
if args:
command.extend(['--args', json.dumps(args, ensure_ascii=False)])
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout
)
if result.returncode == 0:
return True, result.stdout
else:
return False, result.stderr
except subprocess.TimeoutExpired:
return False, f"命令执行超时({timeout}秒)"
except Exception as error:
return False, str(error)
def parse_response(output: str) -> Optional[dict]:
"""解析 mcporter 响应,自动处理嵌套 result 结构"""
try:
data = json.loads(output)
if isinstance(data, dict) and 'result' in data:
return data['result']
return data
except json.JSONDecodeError:
return None
def get_root_dentry_uuid() -> Optional[str]:
"""获取"我的文档"根目录 ID"""
success, output = run_mcporter('dingtalk-docs.get_my_docs_root_dentry_uuid')
if not success:
print(f"❌ 获取根目录 ID 失败:{output}")
return None
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
return None
return result.get('rootDentryUuid')
def resolve_safe_path(path: str) -> Path:
"""解析路径并限制在工作目录内,防止路径遍历攻击"""
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
if Path(path).is_absolute():
target_path = Path(path).resolve()
else:
target_path = (Path.cwd() / path).resolve()
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量"
)
```