Back to skills
SkillHub ClubWrite Technical DocsFull StackTech Writer

dingtalk-docs

管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、搜索文档、读取或写入文档内容、创建文件夹整理文档时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要操作多维表、管理日程、发消息或处理审批流时触发。

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,087
Hot score
99
Updated
March 20, 2026
Overall rating
C5.3
Composite score
5.3
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-dingtalk-docs

Repository

openclaw/skills

Skill path: skills/aliramw/dingtalk-docs

管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、搜索文档、读取或写入文档内容、创建文件夹整理文档时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要操作多维表、管理日程、发消息或处理审批流时触发。

Open repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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 环境变量"
        )

```