
Dingtalk Docs
- 179 installs
- 46 repo stars
- Updated March 17, 2026
- aliramw/dingtalk-docs
Integrate DingTalk Docs APIs to create, sync, search, and permission enterprise documents from agents or internal workflow automations.
About
dingtalk-docs skill guides integration with DingTalk Docs for programmatic document creation, updates, search, and permission management—useful for enterprise agents automating knowledge capture inside DingTalk workspaces.
- DingTalk Docs API usage
- Enterprise auth and tokens
- Document create and sync flows
- Permission and space management
- Webhook or polling update patterns
Dingtalk Docs by the numbers
- 179 all-time installs (skills.sh)
- Ranked #267 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aliramw/dingtalk-docs --skill dingtalk-docsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 179 |
|---|---|
| repo stars | ★ 46 |
| Last updated | March 17, 2026 |
| Repository | aliramw/dingtalk-docs ↗ |
What it does
Integrate DingTalk Docs APIs to create, sync, search, and permission enterprise documents from agents or internal workflow automations.
Files
钉钉云文档 Skill
⚠️ 版本兼容提醒
本 Skill v1.0 需要新版钉钉文档 MCP URL(mcpId=9629)。
如果你看到的工具名是 list_accessible_documents、write_content_to_document 等旧名称,说明配置的是旧版 MCP URL,需要重新获取:
1. 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL 2. 重新配置:mcporter config add dingtalk-docs --url "<新版URL>"
严格禁止
1. 禁止编造 nodeId / blockId — 必须从工具返回值中提取,编造 ID 会操作到错误文档或块 2. 覆盖前必须确认 — update_document(mode="overwrite") 会清空全部内容,不确定时先问用户 3. 禁止删除前不确认 blockId — delete_document_block 不可恢复,必须先用 list_document_blocks 确认 4. 仅 ALIDOC 支持 Markdown 读写 — 表格/PPT/PDF 不支持 get_document_content 和 update_document 5. `get_document_content` 需要下载权限 — 仅有查看权限时无法获取内容,且不支持跨组织文档 6. `heading.level` 必须传整数 — insert_document_block 插入标题时,level 必须传 1 而非 "1",传字符串会导致后端报错
工具列表
核心工具(8个)
| 工具 | 用途 | 必填参数 |
|---|---|---|
search_documents | 搜索有权限的文档 | 无(keyword 选填) |
create_document | 创建在线文档(可含初始 Markdown 内容) | name |
create_file | 创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹) | name, type |
get_document_content | 获取文档 Markdown 内容 | nodeId |
update_document | 更新文档内容(覆盖或追加) | nodeId, markdown |
get_document_info | 获取文档元信息 | nodeId |
create_folder | 创建文件夹 | name |
list_nodes | 遍历文件夹/知识库子节点 | 无(folderId 选填) |
Block 精细编辑工具(4个,按需使用)
| 工具 | 用途 | 必填参数 |
|---|---|---|
list_document_blocks | 查询块列表(获取 blockId) | nodeId |
insert_document_block | 在指定位置插入块元素 | nodeId, element |
update_document_block | 更新块元素(仅支持 paragraph) | nodeId, blockId, element |
delete_document_block | 删除块元素(不可恢复) | nodeId, blockId |
意图判断
创建在线文档("新建文档/帮我建个文档/写个文档"):
- 直接
create_document(name, markdown?)— 不传 folderId 默认到根目录 - 指定文件夹 →
create_document(name, folderId=<文件夹nodeId>)
创建其他类型文件("新建表格/脑图/白板/演示/多维表/文件夹"):
create_file(name, type)— type 枚举:adoc/axls/appt/adraw/amind/able/folder- 指定文件夹 →
create_file(name, type, folderId=<文件夹nodeId>) - 指定知识库 →
create_file(name, type, workspaceId=<知识库ID>)(folderId 优先级高于 workspaceId) create_documentvscreate_file:前者专为在线文档设计且支持写入初始 Markdown,后者支持 7 种文件类型但不支持初始内容
搜索文档("找文档/查一下/有没有某个文档"):
search_documents(keyword=关键词)
读取文档内容("读文档/看看内容/这个文档写了什么"):
get_document_content(nodeId)— nodeId 支持 URL 或 ID 自动识别- 若返回 UNSUPPORTED_CONTENT_TYPE → 告知用户该文档类型不支持 Markdown 读取
更新文档内容("写入/更新/编辑/往文档里加点东西"):
- 替换全部 →
update_document(nodeId, markdown, mode="overwrite")(⚠️ 会清空,先确认) - 追加内容 →
update_document(nodeId, markdown, mode="append") - 不确定 → 先问用户是覆盖还是追加
创建文件夹("建文件夹/新建目录"):
create_folder(name, folderId?)— 不传 folderId 默认到根目录
遍历文件夹("列出文件夹/看看里面有什么"):
list_nodes(folderId?)— 支持分页(pageSize, nextPageToken)
精细编辑块元素("修改第几段/在某段后面插入/删除某个块/在标题后加内容"):
第一步:必须先 list_document_blocks(nodeId) 获取 blockId、index 和 blockType,禁止猜测或编造。
第二步,根据意图选择操作:
- 插入新块 →
insert_document_block(nodeId, element, referenceBlockId?, where?) - 不传位置参数 → 插入到文档末尾
where="after"/where="before"配合referenceBlockId控制插入位置- 修改已有块 →
update_document_block(nodeId, blockId, element)(⚠️ 仅支持 paragraph 类型) - 删除块 →
delete_document_block(nodeId, blockId)(不可恢复,操作前务必向用户确认) - 批量删除时从后向前按 index 倒序删除,避免 index 位移
⚠️ 高频易错点:
paragraph属性对象不可省略,内容为空时须传"paragraph": {}heading.level必须传整数(1而非"1"),传字符串会导致后端报错- 列表块的
list字段必填,不可省略 - 多级有序列表同组须保持相同
listId,否则展示错误
element 常用类型速查(完整结构见 dingtalk_document_struct.md):
// 段落(paragraph)— paragraph 对象不可省略,空段落传 {}
{ "blockType": "paragraph", "paragraph": {}, "children": [{ "text": "普通文字" }] }
// 标题(heading)— level 传整数 1~6
{ "blockType": "heading", "heading": { "level": 1 }, "children": [{ "text": "一级标题" }] }
// 引用(blockquote)
{ "blockType": "blockquote", "blockquote": {}, "children": [{ "text": "引用内容" }] }
// 无序列表(unorderedList)— list 字段必填
{
"blockType": "unorderedList",
"unorderedList": {
"list": { "level": 0, "listStyleType": "disc", "listStyle": { "format": "disc", "text": "%1", "align": "left" } }
},
"children": [{ "text": "列表项" }]
}
// 有序列表(orderedList)— list 字段必填,同组多级列表须保持相同 listId
{
"blockType": "orderedList",
"orderedList": {
"list": { "listId": "list-001", "level": 0, "listStyleType": "decimal", "listStyle": { "format": "decimal", "text": "%1.", "align": "left" } }
},
"children": [{ "text": "列表项" }]
}
// 表格(table)— cells 为二维字符串数组
{ "blockType": "table", "table": { "rolSize": 2, "colSize": 3, "cells": [["A", "B", "C"], ["1", "2", "3"]] } }children 行内元素(InlineElement)常用写法:
{ "text": "普通文字" }
{ "text": "加粗", "bold": true }
{ "text": "斜体", "italic": true }
{ "text": "代码", "fonts": "monospace" }
{ "elementType": "link", "properties": { "href": "https://..." }, "children": [{ "text": "链接文字" }] }
{ "elementType": "sticker", "properties": { "code": "灯泡" } }核心工作流
创建文档并写入内容(一步完成):
create_document(name="标题", markdown="# 标题\n\n内容") → 提取 nodeId搜索并读取:
search_documents(keyword) → 提取 nodeId
get_document_content(nodeId) → 获取 markdown 内容遍历文件夹并操作文档:
list_nodes(folderId?) → 提取 nodes[].nodeId
get_document_info(nodeId) → 确认 contentType=ALIDOC
get_document_content(nodeId) → 读取内容Block 精细编辑:
list_document_blocks(nodeId) → 提取 blockId
insert_document_block(nodeId, referenceBlockId, where, element)错误处理
1. PERMISSION_DENIED — 提示用户确认对该文档有操作权限 2. UNSUPPORTED_CONTENT_TYPE — 该文档类型(表格/PPT等)不支持 Markdown 读写 3. BLOCK_NOT_FOUND — blockId 不存在,先用 list_document_blocks 重新获取 4. UNSUPPORTED_BLOCK_TYPE — update_document_block 当前仅支持 paragraph 类型 5. CROSS_ORG_NOT_ALLOWED — 跨组织操作被禁止 6. Invalid credentials — 提示用户重新配置凭证,检查 MCP URL 是否为新版
遇到错误时展示 logId 给用户,便于向钉钉官方反馈排查。
详细参考(按需读取)
- references/api-reference.md — 12 个工具完整参数 Schema + 返回值(含 Block 工具 9-12)
- dingtalk_document_struct.md — Block 元素完整数据结构(BlockElement / InlineElement)
- references/error-codes.md — 错误码说明 + 调试流程
# Dependencies
node_modules/
# Credentials
*.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Build
dist/
build/
# Test coverage
coverage/
# Temporary files
tmp/
temp/
# Local personal config (not for git)
.local/
# Test fixtures(含敏感节点 ID,不提交)
tests/fixtures/test_data.json
# Test reports(运行时生成,不提交)
tests/e2e_report.md
Changelog
[0.4.0] - 2026-03-17
新增
- ✅ 新增
create_file工具文档(references/api-reference.md第 12 节):支持 7 种文件类型(adoc/axls/appt/adraw/amind/able/folder),含完整入参/出参/调用示例/错误码 - ✅ 新增
scripts/create_file.py:命令行创建文件脚本,支持 7 种类型和位置参数 - ✅ 新增
scripts/block_ops.py:Block 精细编辑脚本,支持 list/insert/update/delete 四个子命令 - ✅ 新增
references/block-api.md分栏(columns)结构示例(B.4 节)
文档更新
- ✅
SKILL.md:工具列表 7→8 个,新增create_file;意图判断新增"创建其他类型文件"场景;Block 意图判断重构(两步流程 + 高频易错点 + element 常用类型速查 + InlineElement 速查) - ✅
SKILL.md:修复严格禁止第 6 条错误描述(heading.level必须传整数,不是字符串) - ✅
SKILL.md:frontmatter description 更新,移除"不要在用户需要操作多维表时触发"(create_file现已支持多维表) - ✅
README.md:功能特性列表新增create_file;工具列表新增create_file;目录结构补充mcporter_utils.py/create_file.py/block_ops.py;api-reference.md 注释更新为 12 个工具 - ✅
references/block-api.md:所有调用示例从点号写法改为空格写法;insert_document_block返回值补充blockId;heading示例修正(level 传整数,text 移到 children);blockquote示例修正(属性对象不可省略);新增高频易错点警告块 - ✅
package.json:description 同步create_file、Block 工具,移除多维表排除描述 - ✅
scripts/mcporter_utils.py:工具名对照注释扩展至 12 个工具 - ✅
testcases.json:新增 6 个create_file测试用例(044~049),总用例数 43→49
修复
- ✅ 修复
SKILL.md中heading.level描述错误(之前误写为"必须传字符串",实际 schema 类型为 number) - ✅ 修复
references/block-api.md中blockquote示例(属性对象不可省略,须传{})
---
[0.3.4] - 2026-03-07
修复
- ✅ SKILL.md frontmatter 补充
mcporter二进制依赖声明 - ✅ SKILL.md frontmatter 补充
DINGTALK_MCP_DOCS_URL凭证声明和primaryEnv - ✅ README.md 明确补充环境变量配置方式与凭证敏感性说明
- ✅ README.md / SKILL.md 新增本地文件脚本行为说明,明确 import / export 会处理工作区内文件
- ✅ 统一 registry metadata、package.json、README、SKILL 的依赖与凭证口径
[0.3.3] - 2026-03-07
文档一致性修复
- ✅ README.md 的读取示例补充灰度提示,避免用户误以为所有实例都能直接调用读取接口
- ✅ references/api-reference.md 补充
get_document_content_by_url灰度说明 - ✅ references/api-reference.md 的“搜索并读取”工作流改为条件可用说明
- ✅ references/error-codes.md 新增“方法列表只显示 5 个工具”时的处理规则
[0.3.2] - 2026-03-07
文档更新
- ✅ README.md 新增已知限制说明,明确
get_document_content_by_url当前仍处于灰度发布 - ✅ README.md 补充 5 个稳定工具 + 1 个灰度工具的可用性说明
- ✅ SKILL.md 新增灰度发布说明,要求缺少读取接口时按官方未放量处理
- ✅ SKILL.md 调整读文档意图与错误处理,避免把缺少接口误判为本地配置或权限问题
[0.3.1] - 2026-03-05
修复 (open-spec 合规审计)
SKILL.md 三项审计修复:
- ✅ "重要约束"改为"严格禁止",使用 Authority 原则措辞("禁止"/"必须")
- ✅ 核心工作流写入步骤添加 HARD-GATE 标注 + "提取什么"标注
- ✅ 意图判断新增"关键区分"说明,明确区分在线表格/多维表/文档
package.json description 同步:
- ✅ description 与 SKILL.md frontmatter 保持一致,补充口语同义词和排除场景
---
[0.3.0] - 2026-03-05
重构
SKILL.md 按 dingtalk-neulink 技能包规范全面重构:
- ✅ 满足 7 区块规范(frontmatter / 严格禁止 / 方法总览 / 意图决策树 / 核心工作流 / 上下文传递 / 错误处理)
- ✅ 主文件控制在 117 行(规范要求 ≤ 150 行)
- ✅ 严格禁止 7 条,含产品专属禁令(类型混淆、URL 格式、覆盖写入确认等)
- ✅ 意图决策树覆盖 6 种用户意图,使用中文自然语言 + 口语化表达
- ✅ 上下文传递链完整无断裂
- ✅ 参数格式区块含正确/错误 JSON 对比示例
package.json 规范化:
- ✅ description 符合 D-01~D-08 规则(动词短语开头、含触发场景、含排除场景、含口语同义词、无内部术语)
- ✅ 版本号升级至 0.3.0
- ✅ keywords 移除 "mcp",新增 "钉钉文档"、"在线文档"
references/api-reference.md 增强:
- ✅ 每个方法描述包含四要素(一句话用途 + 使用场景 + 区分说明 + 调用示例)
- ✅ 参数来源标注(如 parentDentryUuid 来自 get_my_docs_root_dentry_uuid)
- ✅ 类型陷阱高亮(accessType 字符串、updateType 数字、docUrl 完整 URL)
references/error-codes.md 精简:
- ✅ 错误码速查表精简为一行一错误,含具体修复动作
- ✅ 新增参数类型速查表(正确/错误/后果)
- ✅ 调试流程精简为 6 步
README.md 同步更新:
- ✅ 使用示例改为
--argsJSON 格式 - ✅ 新增目录结构说明
- ✅ 新增注意事项(参数类型陷阱)
---
[0.2.1] - 2026-03-04
修复
- 🐛 测试套件同步适配 v0.2.0 JSON 传参(旧测试用位置参数调用 run_mcporter,与代码不一致)
- ✅ 新增 parse_response / 函数签名一致性 / 内容常量等测试用例(10→18 个)
- 🐛 修复 macOS symlink 导致路径比较失败(/var → /private/var)
- 📝 更新 TEST_REPORT.md
[0.2.0] - 2026-03-04
改动
传参方式统一为 JSON:
- ✅ SKILL.md 所有示例改用
--args '{"key": "value"}'格式 - ✅
create_doc.py—run_mcporter()改为--argsJSON 传参 - ✅
import_docs.py— 同上 - ✅
export_docs.py— 同上 - ✅ 三个脚本新增
parse_response()统一处理嵌套result返回结构
Bug 修复:
- 🐛 修复脚本无法正确提取
dentryUuid和pcUrl(API 返回嵌套在result字段内) - 🐛
export_docs.pyUUID 正则从固定 32 位改为[a-zA-Z0-9]+,兼容不同长度 ID
---
[0.1.1] - 2026-03-02
新增功能
脚本工具:
- ✅
create_doc.py- 创建文档并写入内容 - ✅
import_docs.py- 从本地文件导入文档(支持 .md/.txt/.markdown) - ✅
export_docs.py- 导出文档到本地
测试套件:
- ✅
test_security.py- 10 个安全功能单元测试 - ✅
TEST_REPORT.md- 完整测试报告
API 方法(6 个)
| 方法 | 说明 |
|---|---|
list_accessible_documents(keyword?) | 搜索文档 |
get_my_docs_root_dentry_uuid() | 获取根目录 ID |
create_doc_under_node(name, parentDentryUuid) | 创建文档 |
create_dentry_under_node(name, accessType, parentDentryUuid) | 创建节点(11 种类型) |
write_content_to_document(content, updateType, targetDentryUuid) | 写入内容 |
get_document_content_by_url(docUrl) | 获取文档内容 |
安全特性
- ✅ 路径沙箱保护
- ✅ 文件扩展名白名单
- ✅ 文件大小限制(10MB)
- ✅ 内容长度限制(50K 字符)
- ✅ URL 格式验证
- ✅ 命令超时保护
---
[0.1.0] - 2026-03-02
初始发布
- ✅ 6 个真实可用的 API 方法
- ✅ 完整 SKILL.md 文档
- ✅ README.md 使用指南
---
已知限制
- ⚠️ 创建文档可能返回错误码
52600007(企业账号限制) - ⚠️ 仅支持当前用户有权限访问的文档
Data Structures
An online document is a tree composed of block elements. Different types of BlockElement support different nestable child element types. For example, a callout block can nest any BlockElement, while a paragraph block can only nest InlineElement.
A single block element has the following structure:
{
"blockType": enum(BlockType), // Block element type, see BlockType enum below
/**
* The following are all supported Block types, such as Heading, Paragraph.
* Each block type has its own specific property object.
*/
"blockquote": Object(Callout),
"callout": Object(Callout),
"columns": Object(Columns),
"heading": Object(Heading),
"paragraph": Object(Paragraph),
"orderedList": Object(OrderedList),
"unorderedList": Object(UnorderedList),
"table": Object(Table),
// Each Block type has its own supported BlockChildren, see each Block type's `children` description
"children": BlockChildren[],
}BlockElement Reference
An online document is a tree structure composed of block elements (BlockElement). Different block types support different child element types:
calloutandcolumns:childrenmust be a BlockElement arrayparagraph,heading,orderedList,unorderedList,blockquote:childrenmust be an InlineElement arraytable:childrenis not supported
---
BlockType Enum
| Value | Description |
|---|---|
paragraph | Paragraph block |
heading | Heading block |
blockquote | Blockquote block |
callout | Callout (highlight) block |
columns | Columns (multi-column layout) block |
orderedList | Ordered list block |
unorderedList | Unordered list block |
table | Table block |
tableRow | Table row block (child of table) |
tableCell | Table cell block (child of table) |
Unsupported element types are returned as Undefined.---
1. Paragraph
{
"blockType": "paragraph",
"paragraph": {
"text": "Paragraph text content",
"indent": { "left": 32 },
"folded": false
},
"children": [InlineElement]
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | No | Text content of the paragraph |
indent | object(Indent) | No | Indentation; left must be a positive integer |
folded | boolean | No | Whether to fold blocks with a larger indent value than this paragraph |
children: InlineElement array- The
paragraphobject must not be omitted; pass{}when content is empty
---
2. Heading
{
"blockType": "heading",
"heading": {
"level": 1,
"text": "Heading Level 1"
},
"children": [InlineElement]
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | No | Heading text content |
level | integer(enum) | No | Heading level, 1–6; 1 = H1 |
children: InlineElement array
---
3. Blockquote
{
"blockType": "blockquote",
"blockquote": {
"text": "Quoted content",
"indent": { "left": 32 }
},
"children": [InlineElement]
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | No | Text inside the blockquote |
indent | object(Indent) | No | Indentation value |
children: InlineElement array
---
4. Callout
{
"blockType": "callout",
"callout": {
"sticker": "灯泡",
"showstk": true,
"color": "#333333",
"border": "#FFD700",
"bgcolor": "#FFF9C4"
},
"children": [BlockElement]
}| Field | Type | Required | Description |
|---|---|---|---|
sticker | string(enum Emoji) | No | Emoji code, see Emoji enum |
showstk | boolean | No | Whether to display the emoji |
color | string | No | Text color |
border | string | No | Border color |
bgcolor | string | No | Background color |
children: BlockElement array (InlineElement is not allowed)
---
5. Columns
{
"blockType": "columns",
"columns": {
"size": 2,
"noFill": false
},
"children": [BlockElement]
}| Field | Type | Required | Description |
|---|---|---|---|
size | number | No | Number of columns |
noFill | boolean | No | Whether to disable automatic background fill |
children: BlockElement array (InlineElement is not allowed)
---
6. Ordered List
{
"blockType": "orderedList",
"orderedList": {
"list": {
"listId": "list-001",
"level": 0,
"listStyleType": "decimal",
"listStyle": {
"format": "decimal",
"text": "%1.",
"align": "left"
},
"symbolStyle": {
"bold": false
}
},
"indent": { "left": 32 }
},
"children": [InlineElement]
}| Field | Type | Required | Description |
|---|---|---|---|
list | object(ListObject) | Yes | Ordered list properties, see ListObject |
indent | object(Indent) | No | Indentation; left must be a positive integer |
children: InlineElement array
---
7. Unordered List
{
"blockType": "unorderedList",
"unorderedList": {
"list": {
"listId": "list-002",
"level": 0,
"listStyleType": "disc",
"listStyle": {
"format": "disc",
"text": "%1",
"align": "left"
},
"symbolStyle": {
"bold": false
}
},
"indent": { "left": 32 }
},
"children": [InlineElement]
}| Field | Type | Required | Description |
|---|---|---|---|
list | object(ListObject) | Yes | Unordered list properties, see ListObject |
indent | object(Indent) | No | Indentation; left must be a positive integer |
children: InlineElement array
---
8. Table
{
"blockType": "table",
"table": {
"rolSize": 3,
"colSize": 4,
"cells": [
["Row1-Col1", "Row1-Col2", "Row1-Col3", "Row1-Col4"],
["Row2-Col1", "Row2-Col2", "Row2-Col3", "Row2-Col4"],
["Row3-Col1", "Row3-Col2", "Row3-Col3", "Row3-Col4"]
]
}
}| Field | Type | Required | Description |
|---|---|---|---|
rolSize | number | Yes | Number of rows |
colSize | number | Yes | Number of columns |
cells | string[][] | No | Cell text content as a 2D string array |
children: Not supported
---
InlineElement Reference
Inline elements are used in the children of text-based block elements (paragraph, heading, list, etc.).
InlineType Enum
| Value | Description |
|---|---|
text | Text |
sticker | Emoji |
image | Image |
link | Hyperlink |
---
Text
{
"text": "Bold red text",
"sz": 16,
"color": "red",
"highlight": "yellow",
"bold": true,
"italic": false,
"stike": false,
"underline": false,
"fonts": "monospace"
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Text content |
sz | number | No | Font size (px) |
color | string | No | Text color |
highlight | string | No | Highlight background color |
bold | boolean | No | Bold |
italic | boolean | No | Italic |
stike | boolean | No | Strikethrough |
underline | boolean | No | Underline |
fonts | string(enum Fonts) | No | Font family, see Fonts enum |
Fonts Enum
| Value | Description |
|---|---|
monospace | Monospace |
STSong | STSong |
Microsoft YaHei | Microsoft YaHei |
FangSong_GB2312 | FangSong GB2312 |
Helvetica | Helvetica |
Helvetica Neue | Helvetica Neue |
Consolas | Consolas |
宋体 | SimSun |
Impact | Impact |
sanrif | Sanrif |
Calibri | Calibri |
---
Emoji (sticker)
{
"elementType": "sticker",
"properties": {
"code": "大笑"
}
}| Field | Type | Required | Description |
|---|---|---|---|
code | string(enum Emoji) | Yes | Emoji code, see Emoji enum |
---
Image
{
"elementType": "image",
"properties": {
"src": "https://example.com/image.jpg"
}
}| Field | Type | Required | Description |
|---|---|---|---|
src | string | No | Image URL |
---
Link
{
"elementType": "link",
"properties": {
"href": "https://example.com"
},
"children": [
{ "text": "Click to open link" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
href | string | No | Link URL |
children: Text array — required when inserting a link; must contain at least one text node with a non-emptytextfield.
---
Common Data Structures
Indent
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
left | number | No | Indentation value; must be a positive integer, otherwise an error is returned | 32 |
---
ListObject
| Field | Type | Required | Description |
|---|---|---|---|
listId | string | No | List ID. For multi-level ordered lists, all items in the same group must share the same listId; otherwise the display will be incorrect |
level | number | Yes | List nesting level (starts from 0) |
listStyleType | string | Yes | List style type |
listStyle | object(ListStyle) | Yes | List style details, see ListStyle |
symbolStyle | object(SymbolStyle) | No | Bullet/number symbol style, see SymbolStyle |
---
ListStyle
| Field | Type | Required | Description |
|---|---|---|---|
format | string | Yes | Bullet format |
text | string | Yes | Text template |
align | string | Yes | Alignment: left, center, or right |
---
SymbolStyle
| Field | Type | Required | Description |
|---|---|---|---|
sz | number | No | Symbol font size |
shd | string | No | Symbol background color |
fonts | string | No | Symbol font family |
color | string | No | Symbol font color |
bold | boolean | No | Bold |
strike | boolean | No | Strikethrough |
italic | boolean | No | Italic |
---
Emoji Enum (Complete List)
The following values can be used in callout.sticker and inline sticker elements (sticker.properties.code).
优先级: 1 | 优先级: 2 | 优先级: 3 | 优先级: 4 |
优先级: 5 | 优先级: 6 | 优先级: 7 | 进度: 未开始 |
进度: 20% | 进度: 40% | 进度: 50% | 进度: 70% |
进度: 90% | 进度: 已完成 | 微笑 | 憨笑 |
大笑 | 加油 | 色 | 偷笑 |
跳舞 | 捂脸哭 | 笑哭 | 流泪 |
疑问 | 傻笑 | 流鼻血 | 狗子 |
赞 | OK | 抱拳 | 向上 |
向下 | 向左 | 向右 | 资料 |
本子 | 笔记本 | 折线图 | 柱状图 |
羽毛笔 | 钢笔 | 警告 | 问号 |
禁止 | 禁行 | 锁 | 气泡 |
沙漏 | 公文包 | 火箭 | 火 |
奖牌 | 灯泡 | 钉子 | 旗子 |
茶 | 休假 | 气球 | 锦鲤 |
咖啡 | 奶茶 | 调色板 | 感谢 |
打招呼 | 666 | 握手 | 胜利 |
一点点 | 鼓掌 | 送花花 | 比心 |
加一 | 100分 | KPI | 对勾 |
爱心 | 可爱 | 发呆 | 老板 |
害羞 | 闭嘴 | 睡 | 大哭 |
尴尬 | 调皮 | 惊讶 | 流汗 |
广播 | 自信 | 你强 | 怒吼 |
惊愕 | 快哭了 | 无聊 | 吐 |
算账 | 晕 | 摸摸 | 飞吻 |
鄙视 | 嘘 | 思考 | 亲亲 |
感冒 | 对不起 | 再见 | 投降 |
哼 | 欠扁 | 坏笑 | 拜托 |
可怜 | 舒服 | 爱意 | 财迷 |
迷惑 | PK | 委屈 | 灵感 |
天使 | 鬼脸 | 凄凉 | 郁闷 |
吃瓜 | 嘿嘿 | 抠鼻 | 呲牙 |
彩虹 | 耶 | 捂眼睛 | 推眼镜 |
暗中观察 | 开心 | 惊喜 | 回头 |
发怒 | 忍者 | 衰 | 脑暴 |
冷笑 | 黑眼圈 | 恭喜 | 费解 |
收到 | 炸弹 | 白眼 | 一团乱麻 |
无奈 | 敲打 | 专注 | 忙疯了 |
鞠躬 | 摊手 | 抱抱 | 举手 |
跪了 | 猫咪 | 二哈 | 三多 |
承让 | 撒花 | 邮件 | 文档 |
演示 | 表格 | 生日快乐 | 心碎 |
红包 | 嘴唇 | 鲜花 | 残花 |
干杯 | 出差 | 时间 | 福 |
月饼 | 礼物 | 幼苗 | 烟花 |
灯笼 | 爆竹 | 鸡腿 | 高铁 |
三连 | OKR | Done |
数据结构
一篇在线文档是由若干个块元素组成的树,不同类型的 BlockElment 内部可嵌套的元素的类型是不同,例如 高亮块 里可以嵌套任意的BlockElement,但是 段落块 下只能其嵌套 InlineElement 。
1 个块元素的数据结构如下:
{
"blockType": enum(BlockType), // 块元素类型,详见文档底部的 `BlockType` 枚举类型
/**
* 以下为所有支持的 Block 类型,如 Heading、Paragraph。
* 不同类型的 Block 有自己的 property。
*/
"blockquote": Object(Callout),
"callout": Object(Callout),
"columns": Object(Columns),
"heading": Object(Heading),
"paragraph": Object(Paragraph),
"orderedList": Object(OrderedList)
"unorderedList": Object(UnorderedList),
"table": Object(Table),
// 每1种 Block 都有自己能支持的 BlockChildren,详见每个 Block 类型里的 `children` 的描述
"children": BlockChildren[],
}块元素数据结构说明(BlockElement Reference)
一篇在线文档由若干块元素(BlockElement) 组成的树状结构构成。不同类型的块元素内部可嵌套的子元素类型不同:
callout、columns的children只能是 BlockElement 数组paragraph、heading、orderedList、unorderedList、blockquote的children只能是 InlineElement 数组table暂不支持指定children
---
BlockType 枚举
| 枚举值 | 描述 |
|---|---|
paragraph | 段落块 |
heading | 标题块 |
blockquote | 引用块 |
callout | 高亮块 |
columns | 分栏块 |
orderedList | 有序列表块 |
unorderedList | 无序列表块 |
table | 表格块 |
tableRow | 表格行块(表格块的子块) |
tableCell | 表格单元格块(表格块的子块) |
暂未支持的元素类型统一返回 Undefined。---
一、段落(paragraph)
{
"blockType": "paragraph",
"paragraph": {
"text": "段落文字内容",
"indent": { "left": 32 },
"folded": false
},
"children": [InlineElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
text | string | 否 | 段落的文本内容 |
indent | object(Indent) | 否 | 缩进值,left 必须是大于 0 的整数 |
folded | boolean | 否 | 是否折叠段落(折叠 indent 值比当前段落大的块元素) |
children:InlineElement 数组paragraph对象不可省略,内容为空时须传{}
---
二、标题(heading)
{
"blockType": "heading",
"heading": {
"level": 1,
"text": "一级标题"
},
"children": [InlineElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
text | string | 否 | 标题的文本内容 |
level | integer(enum) | 否 | 标题级别,取值 1~6,1 表示一级标题 |
children:InlineElement 数组
---
三、引用(blockquote)
{
"blockType": "blockquote",
"blockquote": {
"text": "引用内容",
"indent": { "left": 32 }
},
"children": [InlineElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
text | string | 否 | 引用里的文字 |
indent | object(Indent) | 否 | 具体的缩进值 |
children:InlineElement 数组
---
四、高亮块(callout)
{
"blockType": "callout",
"callout": {
"sticker": "灯泡",
"showstk": true,
"color": "#333333",
"border": "#FFD700",
"bgcolor": "#FFF9C4"
},
"children": [BlockElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
sticker | string(enum Emoji) | 否 | 表情编码,详见 Emoji 枚举值 |
showstk | boolean | 否 | 是否显示表情 |
color | string | 否 | 字色 |
border | string | 否 | 边框颜色 |
bgcolor | string | 否 | 背景色 |
children:BlockElement 数组(不能是 InlineElement)
---
五、分栏(columns)
{
"blockType": "columns",
"columns": {
"size": 2,
"noFill": false
},
"children": [BlockElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
size | number | 否 | 分栏数量 |
noFill | boolean | 否 | 是否自动填充背景色 |
children:BlockElement 数组(不能是 InlineElement)
---
六、有序列表(orderedList)
{
"blockType": "orderedList",
"orderedList": {
"list": {
"listId": "list-001",
"level": 0,
"listStyleType": "decimal",
"listStyle": {
"format": "decimal",
"text": "%1.",
"align": "left"
},
"symbolStyle": {
"bold": false
}
},
"indent": { "left": 32 }
},
"children": [InlineElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
list | object(ListObject) | 是 | 有序列表的具体属性,详见 ListObject 说明 |
indent | object(Indent) | 否 | 缩进值,left 必须是大于 0 的整数 |
children:InlineElement 数组
---
七、无序列表(unorderedList)
{
"blockType": "unorderedList",
"unorderedList": {
"list": {
"listId": "list-002",
"level": 0,
"listStyleType": "disc",
"listStyle": {
"format": "disc",
"text": "%1",
"align": "left"
},
"symbolStyle": {
"bold": false
}
},
"indent": { "left": 32 }
},
"children": [InlineElement]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
list | object(ListObject) | 是 | 无序列表的具体属性,详见 ListObject 说明 |
indent | object(Indent) | 否 | 缩进值,left 必须是大于 0 的整数 |
children:InlineElement 数组
---
八、表格(table)
{
"blockType": "table",
"table": {
"rolSize": 3,
"colSize": 4,
"cells": [
["Row1-Col1", "Row1-Col2", "Row1-Col3", "Row1-Col4"],
["Row2-Col1", "Row2-Col2", "Row2-Col3", "Row2-Col4"],
["Row3-Col1", "Row3-Col2", "Row3-Col3", "Row3-Col4"]
]
}
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
rolSize | number | 是 | 行数 |
colSize | number | 是 | 列数 |
cells | string[][] | 否 | 单元格文本内容,二维 String 数组 |
children:暂不支持指定 children
---
InlineElement 数据结构说明
行内元素用于文本类块元素(段落、标题、列表等)的 children 中。
InlineType 枚举
| 枚举值 | 描述 |
|---|---|
text | 文本 |
sticker | 表情 |
image | 图片 |
link | 链接 |
---
文本(text)
{
"text": "加粗红色文字",
"sz": 16,
"color": "red",
"highlight": "yellow",
"bold": true,
"italic": false,
"stike": false,
"underline": false,
"fonts": "monospace"
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
text | string | 是 | 文本内容 |
sz | number | 否 | 字号(默认单位 px) |
color | string | 否 | 文字颜色 |
highlight | string | 否 | 高亮背景色 |
bold | boolean | 否 | 是否加粗 |
italic | boolean | 否 | 是否斜体 |
stike | boolean | 否 | 是否中划线 |
underline | boolean | 否 | 是否下划线 |
fonts | string(enum Fonts) | 否 | 字体,详见 Fonts 枚举 |
Fonts 枚举值
| 枚举值 | 描述 |
|---|---|
monospace | 等宽字体 |
STSong | 华文宋体 |
Microsoft YaHei | 微软雅黑 |
FangSong_GB2312 | 仿宋 GB2312 |
Helvetica | Helvetica 字体 |
Helvetica Neue | Neue Helvetica 字体 |
Consolas | Consolas 字体 |
宋体 | 宋体 |
Impact | Impact 字体 |
sanrif | sanrif 字体 |
Calibri | Calibri 字体 |
---
表情(sticker)
{
"elementType": "sticker",
"properties": {
"code": "大笑"
}
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
code | string(enum Emoji) | 是 | 表情编码,详见 Emoji 枚举值 |
---
图片(image)
{
"elementType": "image",
"properties": {
"src": "https://example.com/image.jpg"
}
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
src | string | 否 | 图片资源地址 |
---
链接(link)
{
"elementType": "link",
"properties": {
"href": "https://example.com"
},
"children": [
{ "text": "点击跳转链接" }
]
}| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
href | string | 否 | 链接地址 |
children:Text 数组,插入链接时必须指定children,且至少包含 1 个text不为空的文本节点。
---
通用数据结构
Indent(缩进)
| 字段名 | 类型 | 必填 | 含义 | 示例 |
|---|---|---|---|---|
left | number | 否 | 具体缩进值,必须是大于 0 的整数,否则报错 | 32 |
---
ListObject(列表对象)
| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
listId | string | 否 | 当前列表的 ID;若插入多级有序列表,需确保同组多级列表的 listId 一致,否则展示错误 |
level | number | 是 | 列表的层级(从 0 开始) |
listStyleType | string | 是 | 列表样式类型 |
listStyle | object(ListStyle) | 是 | 列表的具体样式,详见 ListStyle 说明 |
symbolStyle | object(SymbolStyle) | 否 | 列表符的样式,详见 SymbolStyle 说明 |
---
ListStyle(列表样式)
| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
format | string | 是 | 项目符号格式 |
text | string | 是 | 文本 |
align | string | 是 | 对齐方式,如 left、center、right |
---
SymbolStyle(列表符样式)
| 字段名 | 类型 | 必填 | 含义 |
|---|---|---|---|
sz | number | 否 | 项目符号字体大小 |
shd | string | 否 | 项目符号背景色 |
fonts | string | 否 | 项目符号字体格式 |
color | string | 否 | 项目符号字体颜色 |
bold | boolean | 否 | 是否加粗 |
strike | boolean | 否 | 是否展示删除线 |
italic | boolean | 否 | 是否展示斜体 |
---
Emoji 枚举值(表情编码完整列表)
以下枚举值可用于 callout.sticker 和行内表情元素 sticker.properties.code。
优先级: 1 | 优先级: 2 | 优先级: 3 | 优先级: 4 |
优先级: 5 | 优先级: 6 | 优先级: 7 | 进度: 未开始 |
进度: 20% | 进度: 40% | 进度: 50% | 进度: 70% |
进度: 90% | 进度: 已完成 | 微笑 | 憨笑 |
大笑 | 加油 | 色 | 偷笑 |
跳舞 | 捂脸哭 | 笑哭 | 流泪 |
疑问 | 傻笑 | 流鼻血 | 狗子 |
赞 | OK | 抱拳 | 向上 |
向下 | 向左 | 向右 | 资料 |
本子 | 笔记本 | 折线图 | 柱状图 |
羽毛笔 | 钢笔 | 警告 | 问号 |
禁止 | 禁行 | 锁 | 气泡 |
沙漏 | 公文包 | 火箭 | 火 |
奖牌 | 灯泡 | 钉子 | 旗子 |
茶 | 休假 | 气球 | 锦鲤 |
咖啡 | 奶茶 | 调色板 | 感谢 |
打招呼 | 666 | 握手 | 胜利 |
一点点 | 鼓掌 | 送花花 | 比心 |
加一 | 100分 | KPI | 对勾 |
爱心 | 可爱 | 发呆 | 老板 |
害羞 | 闭嘴 | 睡 | 大哭 |
尴尬 | 调皮 | 惊讶 | 流汗 |
广播 | 自信 | 你强 | 怒吼 |
惊愕 | 快哭了 | 无聊 | 吐 |
算账 | 晕 | 摸摸 | 飞吻 |
鄙视 | 嘘 | 思考 | 亲亲 |
感冒 | 对不起 | 再见 | 投降 |
哼 | 欠扁 | 坏笑 | 拜托 |
可怜 | 舒服 | 爱意 | 财迷 |
迷惑 | PK | 委屈 | 灵感 |
天使 | 鬼脸 | 凄凉 | 郁闷 |
吃瓜 | 嘿嘿 | 抠鼻 | 呲牙 |
彩虹 | 耶 | 捂眼睛 | 推眼镜 |
暗中观察 | 开心 | 惊喜 | 回头 |
发怒 | 忍者 | 衰 | 脑暴 |
冷笑 | 黑眼圈 | 恭喜 | 费解 |
收到 | 炸弹 | 白眼 | 一团乱麻 |
无奈 | 敲打 | 专注 | 忙疯了 |
鞠躬 | 摊手 | 抱抱 | 举手 |
跪了 | 猫咪 | 二哈 | 三多 |
承让 | 撒花 | 邮件 | 文档 |
演示 | 表格 | 生日快乐 | 心碎 |
红包 | 嘴唇 | 鲜花 | 残花 |
干杯 | 出差 | 时间 | 福 |
月饼 | 礼物 | 幼苗 | 烟花 |
灯笼 | 爆竹 | 鸡腿 | 高铁 |
三连 | OKR | Done |
---
{
"name": "dingtalk-docs",
"version": "0.3.4",
"description": "管理钉钉云文档中的文档、文件夹和内容。当用户想要创建文档、创建表格/脑图/白板/演示/多维表等文件、搜索文档、读取或写入文档内容、创建文件夹整理文档、精确编辑文档块元素时使用。也适用于用户提到云文档、在线文档、钉钉文档、钉文档等关键词的场景。不要在用户需要管理日程、发消息或处理审批流时触发。",
"keywords": [
"dingtalk",
"docs",
"document",
"云文档",
"钉钉文档",
"在线文档",
"openclaw",
"skill"
],
"author": "Marila@Dingtalk",
"contributors": [
"Marila@Dingtalk"
],
"license": "MIT",
"homepage": "https://github.com/aliramw/dingtalk-docs",
"repository": {
"type": "git",
"url": "https://github.com/aliramw/dingtalk-docs.git"
},
"bugs": {
"url": "https://github.com/aliramw/dingtalk-docs/issues"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"mcporter": ">=0.7.0"
},
"clawhub": {
"requiresBinaries": [
"mcporter"
],
"credentials": [
{
"name": "DINGTALK_MCP_DOCS_URL",
"description": "新版钉钉文档 MCP 的 StreamableHttp 地址(含访问令牌)。请从 https://mcp.dingtalk.com/#/detail?mcpId=9629 获取新版 URL,旧版 URL 不兼容本 Skill v1.0。",
"docs": "https://mcp.dingtalk.com/#/detail?mcpId=9629",
"storageMethod": "mcporter config (recommended) or environment variable"
}
],
"notes": "v1.0 需要新版钉钉文档 MCP URL(mcpId=9629),旧版 URL 的工具名不兼容。"
},
"scripts": {
"test": "python3 tests/test_security.py -v"
}
}
钉钉文档操作技能 (dingtalk-docs) v1.0
管理钉钉云文档中的文档、文件夹和内容。支持文档搜索、创建、内容读写、文件夹遍历和块元素精细编辑。
⚠️ 版本兼容提醒
本 Skill v1.0 需要新版钉钉文档 MCP URL。
如果你之前配置的是旧版 URL(工具名为 list_accessible_documents、write_content_to_document 等),需要重新配置:
1. 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL 2. 重新配置:mcporter config add dingtalk-docs --url "<新版URL>"
功能特性
- ✅ 文档搜索 — 搜索有权限访问的文档(
search_documents) - ✅ 文档创建 — 支持同时写入初始内容,默认创建到根目录(
create_document) - ✅ 内容读取 — 获取文档 Markdown 内容(
get_document_content) - ✅ 内容更新 — 覆盖或追加模式(
update_document) - ✅ 文档元信息 — 获取文档类型、创建时间等(
get_document_info) - ✅ 文件夹管理 — 创建文件夹(
create_folder) - ✅ 文件夹遍历 — 列出子节点,支持分页和递归(
list_nodes) - ✅ Block 精细编辑 — 插入/更新/删除文档块元素(
insert/update/delete_document_block) - ✅ 文件创建 — 创建在线文档、表格、演示、白板、脑图、多维表、文件夹(
create_file)
快速开始
1. 安装技能
clawhub install dingtalk-docs2. 安装依赖
npm install -g mcporter3. 配置凭证
访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL:
mcporter config add dingtalk-docs --url "<你的新版URL>"也可以使用环境变量:
export DINGTALK_MCP_DOCS_URL="<你的新版URL>"这个 URL 含访问令牌,属于敏感凭证。推荐优先用 mcporter config 保存,避免泄露到 shell 历史。4. 使用示例
# 创建文档(带初始内容,一步完成)
mcporter call dingtalk-docs create_document --args '{"name": "项目计划", "markdown": "# 项目计划\n\n## 目标"}'
# 搜索文档
mcporter call dingtalk-docs search_documents --args '{"keyword": "项目"}'
# 获取文档内容(支持 URL 或 nodeId)
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "https://alidocs.dingtalk.com/i/nodes/xxx"}'
# 追加内容到文档
mcporter call dingtalk-docs update_document --args '{"nodeId": "doc_nodeId", "markdown": "\n\n## 新章节", "mode": "append"}'
# 列出文件夹内容
mcporter call dingtalk-docs list_nodes --args '{"folderId": "folder_nodeId"}'
# 创建文件夹
mcporter call dingtalk-docs create_folder --args '{"name": "2026 项目"}'工具列表
核心工具
| 工具 | 说明 | 必填参数 |
|---|---|---|
search_documents | 搜索文档 | 无(keyword 选填) |
create_document | 创建在线文档(可含初始 Markdown 内容) | name |
create_file | 创建文件(在线文档/表格/演示/白板/脑图/多维表/文件夹) | name, type |
get_document_content | 获取文档 Markdown 内容 | nodeId |
update_document | 更新文档内容(覆盖或追加) | nodeId, markdown |
get_document_info | 获取文档元信息 | nodeId |
create_folder | 创建文件夹 | name |
list_nodes | 遍历文件夹/知识库子节点 | 无(folderId 选填) |
Block 精细编辑工具
| 工具 | 说明 | 必填参数 |
|---|---|---|
list_document_blocks | 查询文档块列表(获取 blockId) | nodeId |
insert_document_block | 在指定位置插入块元素 | nodeId, element |
update_document_block | 更新指定块元素(仅支持 paragraph) | nodeId, blockId, element |
delete_document_block | 删除指定块元素(不可恢复) | nodeId, blockId |
完整参数说明请查看 references/api-reference.md
Block 元素数据结构请查看:
- 中文版:dingtalk_document_struct.md
- English:dingtalk_document_struct_en.md
注意事项
- nodeId 支持 URL 或 ID 自动识别,无需手动拼接 URL
- `update_document(mode="overwrite")` 会清空全部内容,操作前请确认
- `delete_document_block` 不可恢复,删除前先用
list_document_blocks确认 blockId - 仅支持 contentType=ALIDOC 的文档读写内容,表格/PPT/PDF 不支持 Markdown 读写
- `get_document_content` 需要对目标文档有「下载」权限,仅有查看权限时无法获取内容
- `get_document_content` 不支持跨组织文档,跨组织文档会返回
forbidden.accessDenied错误 - 凭证 URL 包含访问令牌,请妥善保管
目录结构
dingtalk-docs/
├── SKILL.md # AI 技能入口(≤150 行)
├── package.json # 元数据
├── README.md # 人类可读说明
├── CHANGELOG.md # 变更日志
├── references/
│ ├── api-reference.md # 12 个工具完整参数 Schema
│ ├── block-api.md # Block 工具完整 Schema + 块元素数据结构
│ └── error-codes.md # 错误码说明 + 调试流程
├── scripts/
│ ├── mcporter_utils.py # mcporter 公共工具函数
│ ├── create_doc.py # 创建在线文档脚本
│ ├── create_file.py # 创建文件脚本(支持7种类型)
│ ├── block_ops.py # Block 精细编辑脚本
│ ├── import_docs.py # 导入文档脚本
│ └── export_docs.py # 导出文档脚本
└── tests/
├── test_security.py # 安全功能测试
├── testcases.json # 评测用例
└── TEST_REPORT.md # 测试报告开发
# 克隆仓库
git clone https://github.com/aliramw/dingtalk-docs.git
# 运行测试
python3 tests/test_security.py -v许可证
MIT License
作者
Marila@Dingtalk
# References directory placeholder
钉钉云文档 API 参考(v1.0)
新版 MCP 工具完整参数 Schema、返回值格式和调用示例。
需要新版 MCP URL:钉钉文档 MCP 广场
公共说明
nodeId 格式
所有接受 nodeId / folderId 的参数均支持两种格式,系统自动识别:
- 文档 URL:
https://alidocs.dingtalk.com/i/nodes/{dentryUuid} - 文档 ID:32 位字母数字字符串(dentryUuid)
公共返回字段
每个工具的返回值都包含以下公共字段:
| 字段 | 类型 | 说明 |
|---|---|---|
success | boolean | 是否成功 |
logId | string | 请求追踪 ID,遇到问题时提供给钉钉官方排查 |
errorCode | string | 错误码(仅失败时) |
errorMessage | string | 错误描述(仅失败时) |
---
1. search_documents — 搜索文档
根据关键词搜索当前用户有权限访问的文档列表。不传 keyword 时返回最近访问的文档(最多 10 条)。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
keyword | string | 否 | 搜索关键词,匹配文档标题和内容。不传则返回最近访问的文档列表 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
documents | array | 文档列表(最多 10 条) |
documents[].nodeId | string | 节点 ID,可直接用于其他工具的 nodeId 参数 |
documents[].name | string | 文档标题(已剔除文件后缀) |
documents[].nodeType | string | 节点结构类型:folder 或 file |
documents[].contentType | string | 内容类型(nodeType=file 时):ALIDOC/DOCUMENT/IMAGE/VIDEO/AUDIO/ARCHIVE/OTHER |
documents[].extension | string | 文件后缀(不含点号,如 adoc、xlsx、pdf) |
documents[].docUrl | string | 文档访问链接 |
documents[].lastEditTime | integer | 最后编辑时间(毫秒时间戳) |
documents[].updateTime | integer | 最后变更时间(毫秒时间戳) |
调用示例:
# 搜索包含"项目"的文档
mcporter call dingtalk-docs search_documents --args '{"keyword": "项目"}'
# 返回最近访问的文档
mcporter call dingtalk-docs search_documents---
2. get_document_content — 获取文档内容
获取钉钉文档的内容,以 Markdown 格式返回。仅支持 contentType=ALIDOC 的在线文档,表格/PPT/PDF 等不支持。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
title | string | 文档标题 |
markdown | string | 文档内容(Markdown 格式) |
nodeId | string | 文档节点 ID |
docUrl | string | 文档访问链接 |
调用示例:
# 通过 nodeId 获取
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1"}'
# 通过 URL 获取(自动识别)
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "https://alidocs.dingtalk.com/i/nodes/DnRL6jAJMNX9kAgycoLy2vOo8yMoPYe1"}'---
3. create_document — 创建文档
创建一篇新的钉钉在线文档,支持同时写入初始内容。不传 folderId 时默认创建到用户"我的文档"根目录。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 新文档的标题 |
folderId | string | 否 | 目标文件夹节点 ID,支持 URL 或 ID。不传则创建到根目录(或 workspaceId 的根目录) |
workspaceId | string | 否 | 目标知识库 ID。同时传了 folderId 时以 folderId 为准 |
markdown | string | 否 | 文档初始内容(Markdown 格式)。不传则创建空文档 |
入参优先级: folderId > workspaceId > 默认(我的文档根目录)
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeId | string | 新文档的节点 ID |
name | string | 文档名称 |
folderId | string | 实际创建位置的父文件夹节点 ID |
docUrl | string | 文档访问链接 |
createTime | integer | 创建时间(毫秒时间戳) |
调用示例:
# 创建空文档到根目录
mcporter call dingtalk-docs create_document --args '{"name": "项目计划"}'
# 创建带初始内容的文档(一步完成)
mcporter call dingtalk-docs create_document --args '{"name": "项目计划", "markdown": "# 项目计划\n\n## 目标\n完成 Q1 目标"}'
# 在指定文件夹下创建
mcporter call dingtalk-docs create_document --args '{"name": "子文档", "folderId": "folder_nodeId"}'
# 在知识库根目录下创建
mcporter call dingtalk-docs create_document --args '{"name": "知识库文档", "workspaceId": "workspace_id"}'---
4. update_document — 更新文档内容
更新钉钉文档的内容,支持覆盖和追加两种模式。
⚠️ overwrite 模式会清空文档全部内容(包括图片、评论等),操作前请先确认用户意图,建议先用 get_document_content 备份。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 目标文档标识,支持 URL 或 ID 自动识别 |
markdown | string | 是 | 要写入的 Markdown 内容 |
mode | string | 否 | 更新模式,默认 overwrite。可选:overwrite(覆盖全文)、append(追加到末尾) |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeId | string | 文档节点 ID |
mode | string | 使用的更新模式 |
suggestion | string | 修复建议(仅失败时) |
调用示例:
# 覆盖写入(⚠️ 会清空原内容)
mcporter call dingtalk-docs update_document --args '{"nodeId": "doc_nodeId", "markdown": "# 新内容\n\n全量替换", "mode": "overwrite"}'
# 追加内容(安全,不影响现有内容)
mcporter call dingtalk-docs update_document --args '{"nodeId": "doc_nodeId", "markdown": "\n\n## 新章节\n追加的内容", "mode": "append"}'---
5. get_document_info — 获取文档元信息
获取文档的元信息(标题、类型、创建时间等),不返回文档正文内容。适合在读取内容前先确认文档类型(contentType=ALIDOC 才支持 Markdown 读写)。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeId | string | 节点 ID |
workspaceId | string | 所属知识库 ID |
name | string | 文档标题(已剔除文件后缀) |
docUrl | string | 文档访问链接 |
nodeType | string | 节点类型:folder 或 file |
contentType | string | 内容类型(nodeType=file 时):ALIDOC/DOCUMENT/IMAGE/VIDEO/AUDIO/ARCHIVE/OTHER |
extension | string | 文件后缀(不含点号) |
folderId | string | 父文件夹节点 ID |
createTime | integer | 创建时间(毫秒时间戳) |
updateTime | integer | 最后变更时间(毫秒时间戳) |
调用示例:
mcporter call dingtalk-docs get_document_info --args '{"nodeId": "doc_nodeId"}'---
6. create_folder — 创建文件夹
在指定位置创建一个新的文件夹。不传 folderId 时默认创建到用户"我的文档"根目录。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 新文件夹的名称 |
folderId | string | 否 | 父文件夹节点 ID,支持 URL 或 ID。不传则创建到根目录 |
workspaceId | string | 否 | 目标知识库 ID。同时传了 folderId 时以 folderId 为准 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeId | string | 新文件夹的节点 ID,可作为后续创建操作的 folderId |
name | string | 文件夹名称 |
folderId | string | 父文件夹节点 ID |
docUrl | string | 文件夹访问链接 |
createTime | integer | 创建时间(毫秒时间戳) |
调用示例:
# 在根目录创建文件夹
mcporter call dingtalk-docs create_folder --args '{"name": "2026 项目"}'
# 在指定文件夹下创建子文件夹
mcporter call dingtalk-docs create_folder --args '{"name": "子文件夹", "folderId": "parent_folder_nodeId"}'---
7. list_nodes — 遍历文件列表
列出指定文件夹或知识库下的直接子节点列表,支持分页。返回结果基于当前用户的可访问权限过滤。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
folderId | string | 否 | 要遍历的文件夹节点 ID,支持 URL 或 ID |
workspaceId | string | 否 | 知识库 ID。同时传了 folderId 时以 folderId 为准 |
pageSize | integer | 否 | 每页数量,默认 50,最大 50 |
pageToken | string | 否 | 分页游标,从上一次返回的 nextPageToken 获取 |
入参优先级: folderId > workspaceId > 默认(我的文档根目录)
| 场景 | folderId | workspaceId | 遍历位置 |
|---|---|---|---|
| 指定文件夹 | ✅ 传入 | 可传可不传 | folderId 对应的文件夹 |
| 知识库根目录 | ❌ 不传 | ✅ 传入 | workspaceId 对应的知识库根目录 |
| 我的文档 | ❌ 不传 | ❌ 不传 | 用户"我的文档"根目录 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodes | array | 子节点列表 |
nodes[].nodeId | string | 节点 ID,可直接用于其他工具的 nodeId/folderId 参数 |
nodes[].workspaceId | string | 所属知识库 ID |
nodes[].name | string | 节点名称(已剔除文件后缀) |
nodes[].nodeType | string | 节点类型:folder(目录)或 file(文件) |
nodes[].contentType | string | 内容类型(nodeType=file 时):ALIDOC/DOCUMENT/IMAGE/VIDEO/AUDIO/ARCHIVE/OTHER |
nodes[].extension | string | 文件后缀(不含点号) |
nodes[].docUrl | string | 访问链接 |
nodes[].createTime | integer | 创建时间(毫秒时间戳) |
nodes[].updateTime | integer | 最后变更时间(毫秒时间戳) |
nodes[].hasChildren | boolean | 是否存在子节点(主要对 folder 有意义) |
hasMore | boolean | 是否还有更多节点 |
nextPageToken | string | 下一页游标(仅 hasMore=true 时返回) |
nodeType 与 contentType 关系:
| nodeType | contentType | 说明 |
|---|---|---|
folder | —(不返回) | 文件夹,可递归遍历(hasChildren=true 时) |
file | ALIDOC | 钉钉在线文档,可用 get_document_content 获取内容 |
file | DOCUMENT | 本地文档(docx、xlsx、pptx、pdf 等) |
file | IMAGE | 图片 |
file | VIDEO | 视频 |
file | AUDIO | 音频 |
file | ARCHIVE | 压缩包 |
file | OTHER | 其他文件 |
调用示例:
# 列出根目录
mcporter call dingtalk-docs list_nodes
# 列出指定文件夹
mcporter call dingtalk-docs list_nodes --args '{"folderId": "folder_nodeId"}'
# 分页获取
mcporter call dingtalk-docs list_nodes --args '{"folderId": "folder_nodeId", "pageSize": 10, "pageToken": "next_page_token"}'---
完整工作流示例
创建文档并写入内容(一步完成)
# 直接创建带内容的文档,无需先获取根目录 ID
mcporter call dingtalk-docs create_document --args '{"name": "项目计划", "markdown": "# 项目计划\n\n## 目标\n完成 Q1 目标"}'搜索并读取文档
# 1. 搜索文档,获取 nodeId
mcporter call dingtalk-docs search_documents --args '{"keyword": "项目"}'
# 2. 获取文档内容(直接用 nodeId,无需拼接 URL)
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "<step1返回的nodeId>"}'遍历文件夹并读取 ALIDOC 文档
# 1. 列出文件夹内容
mcporter call dingtalk-docs list_nodes --args '{"folderId": "folder_nodeId"}'
# 2. 对 contentType=ALIDOC 的节点读取内容
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "<nodes[].nodeId>"}'创建文件夹并在其中创建文档
# 1. 创建文件夹
mcporter call dingtalk-docs create_folder --args '{"name": "2026 项目"}'
# 返回 nodeId: "folder_abc123"
# 2. 在文件夹中创建文档
mcporter call dingtalk-docs create_document --args '{"name": "Q1 计划", "folderId": "folder_abc123"}'追加内容到已有文档
# 1. 搜索文档
mcporter call dingtalk-docs search_documents --args '{"keyword": "周报"}'
# 2. 追加内容(不影响现有内容)
mcporter call dingtalk-docs update_document --args '{"nodeId": "<nodeId>", "markdown": "\n\n## 2026-W11\n本周完成了...", "mode": "append"}'覆盖更新前先备份
# 1. 先读取现有内容作为备份
mcporter call dingtalk-docs get_document_content --args '{"nodeId": "doc_nodeId"}'
# 2. 确认用户意图后再覆盖
mcporter call dingtalk-docs update_document --args '{"nodeId": "doc_nodeId", "markdown": "# 全新内容", "mode": "overwrite"}'Block 精细编辑工作流
# 1. 查询文档块列表,获取 blockId
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "doc_nodeId"}'
# 2. 在指定块之后插入一个段落
mcporter call dingtalk-docs insert_document_block --args '{"nodeId": "doc_nodeId", "element": {"blockType": "paragraph", "paragraph": {"text": "新段落内容"}}, "referenceBlockId": "block_id", "where": "after"}'
# 3. 更新某个段落块的内容
mcporter call dingtalk-docs update_document_block --args '{"nodeId": "doc_nodeId", "blockId": "block_id", "element": {"paragraph": {"elements": [{"textRun": {"content": "更新后的内容"}}]}}}'
# 4. 删除指定块(不可恢复)
mcporter call dingtalk-docs delete_document_block --args '{"nodeId": "doc_nodeId", "blockId": "block_id"}'---
Block 精细编辑工具
Block 工具用于对文档进行块级精细操作。块元素的完整数据结构请参考 dingtalk_document_struct.md。
公共说明
- blockId:块元素的唯一标识,通过
list_document_blocks获取 - element:块元素数据对象,必须包含
blockType字段及对应类型的属性对象 - 所有写操作(insert/update/delete)均需要对文档有编辑权限
---
8. list_document_blocks — 查询文档块列表
查询指定文档下的一级块元素列表(根节点下第一级 BlockElement),支持按起始/终止位置范围及块类型过滤。返回每个块的 blockId、index 和完整数据结构。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
startIndex | integer | 否 | 起始位置(≥0),从第 startIndex 个块开始查询,不传则从头开始 |
endIndex | integer | 否 | 终止位置(≥0),查询到第 endIndex 个块为止(含),不传则查询到末尾 |
blockType | string | 否 | 按块类型过滤,不传返回所有类型。枚举值见下表 |
blockType 枚举值:
| 值 | 说明 |
|---|---|
paragraph | 段落块 |
heading | 标题块 |
blockquote | 引用块 |
callout | 高亮块 |
columns | 分栏块 |
orderedList | 有序列表块 |
unorderedList | 无序列表块 |
table | 表格块 |
tableRow | 表格行块 |
tableCell | 表格单元格块 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
blocks | array | 块元素列表 |
blocks[].blockId | string | 块元素唯一标识,可用于 insert/update/delete 操作 |
blocks[].index | integer | 块在文档根节点中的位置(从 0 开始) |
blocks[].blockType | string | 块类型 |
blocks[].element | object | 块的完整数据结构 |
调用示例:
# 查询所有块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "doc_nodeId"}'
# 查询第 0-5 个块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "doc_nodeId", "startIndex": 0, "endIndex": 5}'
# 只查询 heading 类型的块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "doc_nodeId", "blockType": "heading"}'---
9. insert_document_block — 插入块元素
在指定文档的根目录中插入 1 个块元素,可指定插入位置和方向(之前/之后)。两者均不传时默认插入到文档末尾。目前仅支持插入到根目录(一级节点)。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
element | object | 是 | 块元素数据,必须包含 blockType 字段及对应类型的属性对象 |
referenceBlockId | string | 否 | 参考块的 blockId,新块将插入到该块的前面或后面 |
index | integer | 否 | 插入位置(≥0),与 referenceBlockId 二选一,同时传时以 referenceBlockId 为准 |
where | string | 否 | 插入方向,默认 after。可选:before(之前)、after(之后) |
⚠️ 注意:heading.level参数必须传字符串类型(如"1"),传整数会导致后端报错。
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
blockId | string | 新插入块的 blockId |
blockType | string | 新插入块的类型 |
index | integer | 新块在文档中的位置 |
message | string | 操作结果描述 |
调用示例:
# 插入段落到文档末尾
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "doc_nodeId",
"element": {
"blockType": "paragraph",
"paragraph": {"text": "新段落内容"},
"children": [{"text": "新段落内容"}]
}
}'
# 在指定块之后插入标题(注意 level 必须是字符串)
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "doc_nodeId",
"element": {
"blockType": "heading",
"heading": {"level": "2", "text": "二级标题"}
},
"referenceBlockId": "block_id",
"where": "after"
}'
# 在指定块之前插入引用块
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "doc_nodeId",
"element": {
"blockType": "blockquote",
"blockquote": {"indent": {"left": 32}},
"children": [{"text": "引用内容"}]
},
"referenceBlockId": "block_id",
"where": "before"
}'
# 插入表格(使用 dingtalk_document_struct.md 中的 table 结构)
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "doc_nodeId",
"element": {
"blockType": "table",
"table": {
"rolSize": 2,
"colSize": 3,
"cells": [["表头1", "表头2", "表头3"], ["数据1", "数据2", "数据3"]]
}
}
}'
# 插入高亮块(callout)
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "doc_nodeId",
"element": {
"blockType": "callout",
"callout": {"sticker": "灯泡", "showstk": true, "bgcolor": "#FFF9C4", "border": "#FFD700"},
"children": [{"blockType": "paragraph", "paragraph": {"text": "高亮内容"}}]
}
}'---
10. update_document_block — 更新块元素
更新指定文档中某一个块元素的属性。本操作为 PATCH(局部更新) 语义,只修改 element 中传入的字段,未传入的字段保持原值不变。
⚠️ 目前仅支持更新 `paragraph`(段落)类型的块,其他类型会返回 `UNSUPPORTED_BLOCK_TYPE` 错误。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
blockId | string | 是 | 要更新的块的 blockId,通过 list_document_blocks 获取 |
element | object | 是 | 要更新的块属性,PATCH 语义,只更新传入的字段 |
⚠️ 注意:indent参数类型为对象(如{"indentFirstLine": {"unit": "pt", "value": 24}}),传入整数会报错。
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
blockId | string | 被更新块的 blockId |
message | string | 操作结果描述 |
调用示例:
# 更新段落文字内容
mcporter call dingtalk-docs update_document_block --args '{
"nodeId": "doc_nodeId",
"blockId": "block_id",
"element": {
"paragraph": {
"elements": [{"textRun": {"content": "更新后的文字内容"}}]
}
}
}'
# 仅更新折叠状态(不影响文字)
mcporter call dingtalk-docs update_document_block --args '{
"nodeId": "doc_nodeId",
"blockId": "block_id",
"element": {
"paragraph": {"collapsed": true}
}
}'
# 同时更新文字和缩进
mcporter call dingtalk-docs update_document_block --args '{
"nodeId": "doc_nodeId",
"blockId": "block_id",
"element": {
"paragraph": {
"elements": [{"textRun": {"content": "更新后的文字"}}],
"indent": {"indentFirstLine": {"unit": "pt", "value": 24}}
}
}
}'---
11. delete_document_block — 删除块元素
删除指定文档中的某一个块元素。此操作不可恢复,删除前请先用 list_document_blocks 确认 blockId。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID 自动识别 |
blockId | string | 是 | 要删除的块的 blockId,通过 list_document_blocks 获取 |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
blockId | string | 被删除块的 blockId |
message | string | 操作结果描述 |
调用示例:
# 删除指定块
mcporter call dingtalk-docs delete_document_block --args '{"nodeId": "doc_nodeId", "blockId": "block_id"}'批量删除建议:批量删除多个块时,建议从后向前按 index 倒序删除,避免删除后 index 位移导致定位错误。
---
Block 工具错误码汇总
| 错误码 | 说明 |
|---|---|
ARGUMENT_ILLEGAL | 参数非法:nodeId/blockId 为空、文档不存在、无访问权限、跨组织文档 |
BLOCK_NOT_FOUND | 指定的 blockId 在文档中不存在 |
UNSUPPORTED_BLOCK_TYPE | 不支持的块类型(update_document_block 目前仅支持 paragraph) |
invalidRequest.inputArgs.invalid | 输入参数校验失败(如 startIndex > endIndex、blockType 枚举值非法) |
---
12. create_file — 创建文件
在指定位置创建一个新文件,支持钉钉在线文档、表格、演示、白板、脑图、多维表和文件夹七种类型。
入参:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 新文件的名称 |
type | string | 是 | 文件类型,枚举值见下表 |
folderId | string | 否 | 目标文件夹节点 ID,支持 URL 或 ID。不传时:有 workspaceId 则创建在知识库根目录,否则创建在"我的文档"根目录 |
workspaceId | string | 否 | 目标知识库 ID,支持知识库 ID 或知识库 URL。同时传了 folderId 时以 folderId 为准 |
入参优先级: folderId > workspaceId > 默认(我的文档根目录)
`type` 枚举值:
| type 值 | 兼容数字值 | 含义 | 返回 contentType |
|---|---|---|---|
adoc | "0" | 钉钉在线文档 | ALIDOC |
axls | "1" | 钉钉表格 | WORKBOOK |
appt | "2" | 钉钉演示(PPT) | PPT |
adraw | "3" | 钉钉白板 | WBD |
amind | "6" | 钉钉脑图 | MIND |
able | "7" | 钉钉多维表 | NOTABLE |
folder | "13" | 文件夹 | FOLDER |
出参:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeId | string | 新建节点的 ID(dentryUuid) |
name | string | 节点名称 |
folderId | string | 实际创建位置的父文件夹节点 ID |
nodeType | string | 节点结构类型:folder 或 file |
contentType | string | 内容类型(见 type 枚举表) |
extension | string | 文件后缀(不含点号,如 adoc、axls) |
docUrl | string | 节点访问链接 |
createTime | integer | 创建时间(毫秒时间戳) |
lastEditTime | integer | 最后编辑时间(毫秒时间戳) |
message | string | 操作结果描述 |
调用示例:
# 创建钉钉在线文档到"我的文档"根目录
mcporter call dingtalk-docs create_file --args '{"name": "2026 Q2 计划", "type": "adoc"}'
# 在指定文件夹下创建表格
mcporter call dingtalk-docs create_file --args '{"name": "数据统计", "type": "axls", "folderId": "folder_nodeId"}'
# 在知识库根目录下创建多维表
mcporter call dingtalk-docs create_file --args '{"name": "项目看板", "type": "able", "workspaceId": "workspace_id"}'
# 在指定文件夹下创建子文件夹
mcporter call dingtalk-docs create_file --args '{"name": "2026 项目", "type": "folder", "folderId": "folder_nodeId"}'
# 创建脑图
mcporter call dingtalk-docs create_file --args '{"name": "架构设计", "type": "amind"}'
# 创建白板
mcporter call dingtalk-docs create_file --args '{"name": "头脑风暴", "type": "adraw"}'
# 创建演示文稿
mcporter call dingtalk-docs create_file --args '{"name": "季度汇报", "type": "appt"}'错误码:
| 错误码 | 说明 |
|---|---|
invalidRequest.inputArgs.invalid | 参数非法:name 为空、type 不在枚举范围、folderId 格式不合法(非 32 位字母数字字符串或 URL) |
ARGUMENT_ILLEGAL | 目标位置不存在或无写入权限 |
钉钉云文档 Block API 参考 (v1.0)
Block 精细编辑工具的完整参数 Schema 和块元素数据结构。
适用于需要精确控制文档结构的场景(在指定位置插入、修改、删除块元素)。
常规的文档内容读写请使用update_document/get_document_content,无需使用 Block API。
---
Block 工具概览
| 工具 | 用途 | 必填参数 |
|---|---|---|
list_document_blocks | 查询文档一级块列表,获取 blockId | nodeId |
insert_document_block | 在指定位置插入一个块元素 | nodeId, element |
update_document_block | 更新指定块元素(PATCH 语义,仅支持 paragraph) | nodeId, blockId, element |
delete_document_block | 删除指定块元素(不可恢复) | nodeId, blockId |
重要限制:
- 所有 Block 工具仅操作一级块元素(根节点直接子节点),不支持嵌套层级
update_document_block当前仅支持paragraph类型delete_document_block不可恢复,删除含子块的块(callout/columns/table)时子块一并删除
---
1. list_document_blocks — 查询块列表
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID |
startIndex | integer | 否 | 起始位置(≥0),不传则从头开始 |
endIndex | integer | 否 | 终止位置(≥0,含),不传则到末尾 |
blockType | string | 否 | 按块类型过滤,见 BlockType 枚举 |
返回值:
{
"success": true,
"blocks": [
{
"blockId": "mmpzx96vb8asm77jil",
"index": 0,
"blockType": "heading",
"element": { "blockType": "heading", "heading": { "level": 1, "text": "文档标题" } }
},
{
"blockId": "mmpzx96ve10038tufl",
"index": 1,
"blockType": "paragraph",
"element": { "blockType": "paragraph", "paragraph": { "text": "段落内容" } }
}
]
}关键字段:
blocks[].blockId— 块唯一标识,用于update_document_block/delete_document_block的 blockIdblocks[].index— 块在文档中的位置(从 0 开始),用于insert_document_block的 index 参数
调用示例:
# 查询全部块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "docNodeId"}'
# 查询第 1~3 个块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "docNodeId", "startIndex": 1, "endIndex": 3}'
# 只查询段落块
mcporter call dingtalk-docs list_document_blocks --args '{"nodeId": "docNodeId", "blockType": "paragraph"}'---
2. insert_document_block — 插入块元素
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID |
element | object | 是 | 待插入的块元素,必须包含 blockType 及对应属性对象 |
referenceBlockId | string | 否 | 参照块 ID(由 list_document_blocks 获取),优先级高于 index |
index | integer | 否 | 参照位置索引(从 0 开始)。两者均不传则追加到末尾 |
where | string | 否 | 插入方向:before(之前)或 after(之后,默认) |
返回值:
{ "success": true, "blockId": "新块的blockId" }调用示例:
# 在文档末尾插入段落
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "docNodeId",
"element": {"blockType": "paragraph", "paragraph": {}, "children": [{"text": "新段落"}]}
}'
# 在指定块之后插入二级标题(level 传整数)
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "docNodeId",
"referenceBlockId": "mmpzx96vb8asm77jil",
"where": "after",
"element": {"blockType": "heading", "heading": {"level": 2}, "children": [{"text": "新章节"}]}
}'
# 在指定块之前插入无序列表
mcporter call dingtalk-docs insert_document_block --args '{
"nodeId": "docNodeId",
"referenceBlockId": "mmpzx96vb8asm77jil",
"where": "before",
"element": {"blockType": "unorderedList", "unorderedList": {"list": {"level": 0, "listStyleType": "disc", "listStyle": {"format": "disc", "text": "%1", "align": "left"}}}, "children": [{"text": "列表项"}]}
}'---
3. update_document_block — 更新块元素
PATCH 语义:只修改传入的字段,未传入的字段保持原值不变。当前仅支持 paragraph 类型。
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID |
blockId | string | 是 | 目标块 ID(由 list_document_blocks 获取) |
element | object | 是 | 更新内容,必须包含 blockType 及对应属性(当前仅支持 paragraph) |
返回值:
{ "success": true }调用示例:
# 修改段落文字(不影响其他属性)
mcporter call dingtalk-docs update_document_block --args '{
"nodeId": "docNodeId",
"blockId": "mmpzx96ve10038tufl",
"element": {"blockType": "paragraph", "paragraph": {}, "children": [{"text": "更新后的内容"}]}
}'
# 设置段落折叠(不影响文字)
mcporter call dingtalk-docs update_document_block --args '{
"nodeId": "docNodeId",
"blockId": "mmpzx96ve10038tufl",
"element": {"blockType": "paragraph", "paragraph": {"folded": true}}
}'---
4. delete_document_block — 删除块元素
⚠️ 删除不可恢复。删除含子块的块(callout/columns/table)时,所有子块一并删除。
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nodeId | string | 是 | 文档标识,支持 URL 或 ID |
blockId | string | 是 | 目标块 ID(由 list_document_blocks 获取) |
返回值:
{ "success": true }调用示例:
# 删除指定块(先确认 blockId)
mcporter call dingtalk-docs delete_document_block --args '{"nodeId": "docNodeId", "blockId": "mmpzx97482905xaj5dy"}'批量删除建议: 按 index 从后向前删除,避免索引位移导致误删。
---
附录 A:BlockType 枚举
| 枚举值 | 描述 | 支持 insert | 支持 update |
|---|---|---|---|
paragraph | 段落 | ✅ | ✅ |
heading | 标题(level 1-6) | ✅ | ❌ |
blockquote | 引用块 | ✅ | ❌ |
callout | 高亮块 | ✅ | ❌ |
columns | 分栏块 | ✅ | ❌ |
orderedList | 有序列表 | ✅ | ❌ |
unorderedList | 无序列表 | ✅ | ❌ |
table | 表格 | ✅ | ❌ |
sheet | 电子表格 | ✅ | ❌ |
attachment | 附件 | ✅ | ❌ |
slot | 插槽 | ✅ | ❌ |
---
附录 B:块元素数据结构(insert 时使用)
children 规则
paragraph、heading、orderedList、unorderedList、blockquote的children→ InlineElement 数组callout、columns的children→ BlockElement 数组table暂不支持指定children
⚠️ 高频易错点:
paragraph属性对象不可省略,内容为空时须传"paragraph": {}(blockquote同理)heading.level必须传整数(1而非"1"),传字符串会导致后端报错- 列表块的
list字段必填,不可省略 - 多级有序列表同组须保持相同
listId,否则展示错误
B.1 段落(paragraph)
{
"blockType": "paragraph",
"paragraph": { "text": "段落文字", "indent": { "left": 32 }, "folded": false },
"children": [{ "text": "加粗文字", "bold": true }, { "text": "普通文字" }]
}B.2 标题(heading)
{
"blockType": "heading",
"heading": { "level": 1 },
"children": [{ "text": "一级标题" }]
}level 取值 1~6(整数),1 表示一级标题。⚠️ 必须传整数,传字符串 `"1"` 会报错。
B.3 引用(blockquote)
{
"blockType": "blockquote",
"blockquote": {},
"children": [{ "text": "引用内容" }]
}⚠️ `blockquote` 属性对象不可省略,内容为空时须传 `{}`。
B.4 分栏(columns)
{
"blockType": "columns",
"columns": { "size": 2, "noFill": false },
"children": [
{ "blockType": "paragraph", "paragraph": {}, "children": [{ "text": "左栏内容" }] },
{ "blockType": "paragraph", "paragraph": {}, "children": [{ "text": "右栏内容" }] }
]
}size 为分栏数量,children 为 BlockElement 数组(不能是 InlineElement)。
B.5 高亮块(callout)
{
"blockType": "callout",
"callout": { "sticker": "灯泡", "showstk": true, "bgcolor": "#FFF9C4", "border": "#FFD700" },
"children": [
{ "blockType": "paragraph", "paragraph": {}, "children": [{ "text": "高亮块内容" }] }
]
}sticker 可选值见附录 D(Emoji 枚举)。
B.6 有序列表(orderedList)
{
"blockType": "orderedList",
"orderedList": {
"list": { "listId": "list-001", "level": 0, "listStyleType": "decimal",
"listStyle": { "format": "decimal", "text": "%1.", "align": "left" } }
},
"children": [{ "text": "列表项内容" }]
}同组多级列表需保持 listId 一致,level 从 0 开始。
B.7 无序列表(unorderedList)
{
"blockType": "unorderedList",
"unorderedList": {
"list": { "listId": "list-002", "level": 0, "listStyleType": "disc",
"listStyle": { "format": "disc", "text": "%1", "align": "left" } }
},
"children": [{ "text": "列表项内容" }]
}B.8 表格(table)
{
"blockType": "table",
"table": {
"rolSize": 3,
"colSize": 4,
"cells": [
["Row1-Col1", "Row1-Col2", "Row1-Col3", "Row1-Col4"],
["Row2-Col1", "Row2-Col2", "Row2-Col3", "Row2-Col4"],
["Row3-Col1", "Row3-Col2", "Row3-Col3", "Row3-Col4"]
]
}
}rolSize(行数)和 colSize(列数)必填,cells 为二维字符串数组。
---
附录 C:InlineElement 数据结构
行内元素用于文本类块元素的 children 中。
C.1 文本(text)
{ "text": "加粗红色文字", "bold": true, "color": "red", "highlight": "yellow",
"italic": false, "underline": false, "stike": false, "sz": 16, "fonts": "monospace" }| 字段 | 类型 | 说明 |
|---|---|---|
text | string | 文本内容(必填) |
bold | boolean | 加粗 |
italic | boolean | 斜体 |
underline | boolean | 下划线 |
stike | boolean | 中划线 |
color | string | 文字颜色 |
highlight | string | 高亮背景色 |
sz | number | 字号(px) |
fonts | string | 字体:monospace/Microsoft YaHei/Helvetica 等 |
C.2 链接(link)
{
"elementType": "link",
"properties": { "href": "https://example.com" },
"children": [{ "text": "点击跳转" }]
}插入链接时 children 必须包含至少 1 个 text 不为空的文本节点。
C.3 图片(image)
{ "elementType": "image", "properties": { "src": "https://example.com/image.jpg" } }---
附录 D:常用 Emoji 枚举(callout sticker)
灯泡 警告 对勾 禁止 火箭 火 赞 100分 KPI OKR Done 优先级: 1 优先级: 2 优先级: 3 进度: 未开始 进度: 已完成 大笑 流泪 思考 疑问 惊讶 加油 鼓掌 撒花 握手
完整枚举值区分全角/半角及大小写,请严格按照上述格式填写。
钉钉云文档错误码参考 (v1.0)
SKILL.md 的按需加载参考文件。遇到错误时查阅此文档。
>
每次 API 调用都会返回 `logId`(请求追踪 ID),遇到问题时请保存该值提供给技术支持。
错误码速查表
| 错误信息 / 错误码 | 原因 | 修复动作 |
|---|---|---|
Invalid credentials / 401 | 凭证配置错误、令牌过期,或配置的是旧版 MCP URL | 访问 钉钉文档 MCP 广场 获取新版 StreamableHttp URL,执行 mcporter config add dingtalk-docs --url "<新版URL>" |
Permission denied / PERMISSION_DENIED | 当前用户无权限操作该文档/文件夹 | 确认文档分享权限,检查是否只读,联系文档所有者授权 |
Document not found / NODE_NOT_FOUND | nodeId 无效或文档已删除 | 确认 nodeId 来自返回值(禁止编造),用 search_documents 重新搜索 |
UNSUPPORTED_BLOCK_TYPE | update_document_block 传入了非 paragraph 类型的块 | 当前仅支持更新 paragraph 类型,其他类型请用 update_document(mode="overwrite") 替代 |
BLOCK_NOT_FOUND | blockId 不存在或不属于该文档的一级块 | 先调 list_document_blocks 重新获取最新 blockId |
UNSUPPORTED_CONTENT_TYPE | 对表格/PPT/PDF 等非 ALIDOC 文档调用内容读写 | 先用 get_document_info 确认 contentType=ALIDOC,非 ALIDOC 文档不支持 Markdown 读写 |
CROSS_ORG_NOT_ALLOWED | 文档属于其他组织 | 不支持跨组织操作,确认文档属于当前用户所在组织 |
Timeout | 网络超时 | 检查网络连接,稍后重试;提供 logId 给技术支持 |
Invalid parameter | 参数格式错误 | 见下方参数说明 |
看到旧工具名(如 list_accessible_documents、write_content_to_document) | 配置的是旧版 MCP URL | 升级到新版 MCP URL(同 Invalid credentials 修复步骤) |
新版参数说明
新版 API 参数体系已简化,不再有旧版的类型陷阱:
| 参数 | 新版格式 | 旧版对比 |
|---|---|---|
nodeId | 字符串,支持 URL 或 ID 自动识别 | 旧版需手动拼完整 URL |
mode | 字符串枚举:"overwrite" 或 "append" | 旧版 updateType=0/1(数字,易传错) |
folderId | 字符串,支持 URL 或 ID | 旧版 parentDentryUuid(仅支持 ID) |
调试流程
1. 保存 logId → 每次 API 返回都包含,遇到问题时提供给技术支持
2. 检查错误码 → 对照上方速查表
3. 确认 nodeId 来源 → 必须从 API 返回值中提取,禁止编造
4. 确认文档类型 → get_document_info 确认 contentType=ALIDOC 才能读写内容
5. 检查权限 → 确认用户对文档/文件夹有操作权限
6. 检查 MCP URL 版本 → 新版工具名以 search_documents / create_document 等为准
7. 重试 → 排除服务端临时故障日志位置
~/.mcporter/logs/ # mcporter 日志
~/.openclaw/logs/ # 技能执行日志#!/usr/bin/env python3
"""
钉钉文档技能统一测试入口(run_tests.py)
用法:
python3 run_tests.py # 运行全部测试(单元 + 端到端)
python3 run_tests.py --unit # 仅运行单元测试(test_security.py)
python3 run_tests.py --e2e # 仅运行端到端测试(test_e2e.py)
python3 run_tests.py --no-report # 不生成 Markdown 报告
前置条件(端到端测试):
复制 tests/fixtures/test_data.example.json 为 tests/fixtures/test_data.json
并填入真实节点 ID,然后确保 mcporter 已配置 dingtalk-docs server。
报告输出:
tests/e2e_report.md — 端到端测试报告(gitignore,不提交)
"""
import argparse
import io
import json
import subprocess
import sys
import unittest
from datetime import datetime
from pathlib import Path
TESTS_DIR = Path(__file__).parent / "tests"
E2E_REPORT_PATH = TESTS_DIR / "e2e_report.md"
TEST_DATA_PATH = TESTS_DIR / "fixtures" / "test_data.json"
TEST_DATA_EXAMPLE_PATH = TESTS_DIR / "fixtures" / "test_data.example.json"
def check_prerequisites() -> list[str]:
"""检查前置条件,返回警告信息列表。"""
warnings = []
if not TEST_DATA_PATH.exists():
warnings.append(
f"⚠️ 端到端测试数据文件不存在:{TEST_DATA_PATH}\n"
f" 请复制 {TEST_DATA_EXAMPLE_PATH.name} 为 test_data.json 并填入真实节点 ID。\n"
f" 端到端测试用例将全部跳过。"
)
return warnings
def run_unit_tests(verbosity: int = 2) -> unittest.TestResult:
"""运行单元测试(test_security.py)。"""
print("\n" + "=" * 60)
print("📋 单元测试(test_security.py)")
print("=" * 60)
sys.path.insert(0, str(TESTS_DIR.parent / "scripts"))
loader = unittest.TestLoader()
suite = loader.discover(str(TESTS_DIR), pattern="test_security.py")
runner = unittest.TextTestRunner(verbosity=verbosity)
return runner.run(suite)
def run_e2e_tests(verbosity: int = 2, generate_report: bool = True) -> unittest.TestResult:
"""运行端到端测试(test_e2e.py),可选生成 Markdown 报告。"""
print("\n" + "=" * 60)
print("🌐 端到端集成测试(test_e2e.py)")
print("=" * 60)
# 动态导入 test_e2e 模块
import importlib.util
spec = importlib.util.spec_from_file_location("test_e2e", TESTS_DIR / "test_e2e.py")
test_e2e_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_e2e_module)
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(test_e2e_module)
if generate_report:
# 捕获输出并生成 Markdown 报告
terminal_buffer = io.StringIO()
runner = unittest.TextTestRunner(stream=terminal_buffer, verbosity=verbosity)
result = runner.run(suite)
print(terminal_buffer.getvalue())
_write_e2e_report(result, test_e2e_module)
else:
runner = unittest.TextTestRunner(verbosity=verbosity)
result = runner.run(suite)
return result
def _write_e2e_report(result: unittest.TestResult, module) -> None:
"""将端到端测试结果写入 Markdown 报告文件。"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
total = result.testsRun
failures = len(result.failures)
errors = len(result.errors)
skipped = len(result.skipped)
passed = total - failures - errors - skipped
status_icon = "✅" if result.wasSuccessful() else "❌"
lines = [
"# 钉钉文档 MCP 端到端测试报告",
"",
"## 概览",
"",
"| 项目 | 值 |",
"|------|-----|",
f"| 运行时间 | {now} |",
f"| 总用例数 | {total} |",
f"| 通过 | {passed} |",
f"| 失败 | {failures} |",
f"| 错误 | {errors} |",
f"| 跳过 | {skipped} |",
f"| 整体结果 | {status_icon} {'全部通过' if result.wasSuccessful() else '存在失败'} |",
"",
"---",
"",
]
if result.failures:
lines += ["## 失败用例", ""]
for test, traceback_text in result.failures:
lines += [f"### ❌ {test}", "", "```", traceback_text.strip(), "```", ""]
if result.errors:
lines += ["## 错误用例", ""]
for test, traceback_text in result.errors:
lines += [f"### 💥 {test}", "", "```", traceback_text.strip(), "```", ""]
if result.skipped:
lines += ["## 跳过用例", ""]
for test, reason in result.skipped:
lines += [f"- ⏭️ `{test}` — {reason}"]
lines.append("")
# 全量用例列表(按测试类分组)
lines += ["## 全量用例结果", ""]
failed_ids = {str(t) for t, _ in result.failures}
error_ids = {str(t) for t, _ in result.errors}
skipped_ids = {str(t) for t, _ in result.skipped}
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(module)
class_groups: dict = {}
for test_suite in suite:
for case in test_suite:
class_name = type(case).__name__
if class_name not in class_groups:
class_groups[class_name] = []
class_groups[class_name].append(case)
for class_name, cases in class_groups.items():
lines += [f"### {class_name}", "", "| 用例 | 结果 |", "|------|------|"]
for case in cases:
case_id = str(case)
method_doc = getattr(case, case._testMethodName).__doc__ or case._testMethodName
method_doc = method_doc.strip().split("\n")[0]
if case_id in failed_ids:
icon = "❌ 失败"
elif case_id in error_ids:
icon = "💥 错误"
elif case_id in skipped_ids:
icon = "⏭️ 跳过"
else:
icon = "✅ 通过"
lines.append(f"| {method_doc} | {icon} |")
lines.append("")
E2E_REPORT_PATH.write_text("\n".join(lines), encoding="utf-8")
print(f"\n📄 端到端测试报告已写入:{E2E_REPORT_PATH}")
def main() -> int:
parser = argparse.ArgumentParser(description="钉钉文档技能统一测试入口")
parser.add_argument("--unit", action="store_true", help="仅运行单元测试")
parser.add_argument("--e2e", action="store_true", help="仅运行端到端测试")
parser.add_argument("--no-report", action="store_true", help="不生成 Markdown 报告")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
args = parser.parse_args()
verbosity = 2 if args.verbose else 1
run_unit = not args.e2e
run_e2e = not args.unit
generate_report = not args.no_report
print(f"🚀 钉钉文档技能测试 — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 检查前置条件
for warning in check_prerequisites():
print(warning)
all_passed = True
if run_unit:
unit_result = run_unit_tests(verbosity=verbosity)
if not unit_result.wasSuccessful():
all_passed = False
if run_e2e:
e2e_result = run_e2e_tests(verbosity=verbosity, generate_report=generate_report)
if not e2e_result.wasSuccessful():
all_passed = False
print("\n" + "=" * 60)
if all_passed:
print("✅ 全部测试通过")
else:
print("❌ 存在失败用例,请查看上方输出")
print("=" * 60)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())
# Scripts directory placeholder
#!/usr/bin/env python3
"""
钉钉文档 Block 精细编辑工具
支持对钉钉在线文档(ALIDOC)的块元素进行查询、插入、更新、删除操作。
用法:
python block_ops.py list <node_id>
python block_ops.py insert <node_id> <block_type> [--text TEXT] [--level LEVEL]
[--after BLOCK_ID | --before BLOCK_ID]
python block_ops.py update <node_id> <block_id> <text>
python block_ops.py delete <node_id> <block_id>
子命令说明:
list 列出文档所有块元素(含 blockId、index、blockType)
insert 在文档中插入新块,不指定位置时追加到末尾
update 更新指定块的文本内容(仅支持 paragraph 类型)
delete 删除指定块(不可恢复,操作前请确认)
参数:
node_id 文档节点 ID(dentryUuid)或文档 URL
block_id 块元素 ID(blockId),从 list 子命令结果中获取
block_type 块类型,支持:paragraph / heading / blockquote /
unorderedList / orderedList / table
text 块的文本内容
level 标题级别(1~6),仅 heading 类型有效,默认 1
示例:
# 查看文档所有块
python block_ops.py list YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA
# 在文档末尾追加一个段落
python block_ops.py insert YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA paragraph --text "新增段落内容"
# 在指定块后面插入一个二级标题
python block_ops.py insert YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA heading --text "二级标题" --level 2 --after mmpzx96vb8asm77jil
# 在指定块前面插入一个引用
python block_ops.py insert YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA blockquote --text "引用内容" --before mmpzx96vb8asm77jil
# 更新段落内容
python block_ops.py update YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA mmpzx96ve10038tufl "更新后的段落文字"
# 删除指定块
python block_ops.py delete YndMj49yWjnbjwX0uDoKKMA9W3pmz5aA mmpzx96ve10038tufl
"""
import argparse
import json
import sys
from mcporter_utils import parse_response, run_mcporter
SERVER_NAME = "dingtalk-docs"
SUPPORTED_INSERT_TYPES = ["paragraph", "heading", "blockquote", "unorderedList", "orderedList", "table"]
# ──────────────────────────────────────────────
# element 构造函数
# ──────────────────────────────────────────────
def build_paragraph_element(text: str) -> dict:
return {
"blockType": "paragraph",
"paragraph": {},
"children": [{"text": text}],
}
def build_heading_element(text: str, level: int) -> dict:
if not 1 <= level <= 6:
raise ValueError(f"heading level 必须在 1~6 之间,当前值:{level}")
return {
"blockType": "heading",
"heading": {"level": level},
"children": [{"text": text}],
}
def build_blockquote_element(text: str) -> dict:
return {
"blockType": "blockquote",
"blockquote": {},
"children": [{"text": text}],
}
def build_unordered_list_element(text: str) -> dict:
return {
"blockType": "unorderedList",
"unorderedList": {
"list": {
"level": 0,
"listStyleType": "disc",
"listStyle": {"format": "disc", "text": "%1", "align": "left"},
}
},
"children": [{"text": text}],
}
def build_ordered_list_element(text: str) -> dict:
return {
"blockType": "orderedList",
"orderedList": {
"list": {
"listId": "list-001",
"level": 0,
"listStyleType": "decimal",
"listStyle": {"format": "decimal", "text": "%1.", "align": "left"},
}
},
"children": [{"text": text}],
}
def build_table_element(rows: int = 2, cols: int = 3) -> dict:
cells = [[""] * cols for _ in range(rows)]
return {
"blockType": "table",
"table": {"rolSize": rows, "colSize": cols, "cells": cells},
}
def build_element(block_type: str, text: str, level: int) -> dict:
builders = {
"paragraph": lambda: build_paragraph_element(text),
"heading": lambda: build_heading_element(text, level),
"blockquote": lambda: build_blockquote_element(text),
"unorderedList": lambda: build_unordered_list_element(text),
"orderedList": lambda: build_ordered_list_element(text),
"table": lambda: build_table_element(),
}
if block_type not in builders:
raise ValueError(f"不支持的块类型:{block_type},支持:{', '.join(SUPPORTED_INSERT_TYPES)}")
return builders[block_type]()
# ──────────────────────────────────────────────
# 子命令实现
# ──────────────────────────────────────────────
def cmd_list(node_id: str):
"""列出文档所有块元素。"""
print(f"📋 查询文档块列表:{node_id}")
print("-" * 60)
success, output = run_mcporter(SERVER_NAME, "list_document_blocks", {"nodeId": node_id})
if not success:
print(f"❌ 查询失败:{output}")
sys.exit(1)
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
sys.exit(1)
blocks = result.get("blocks", [])
if not blocks:
print("(文档暂无块元素)")
return
print(f"共 {len(blocks)} 个块:\n")
for block in blocks:
index = block.get("index", "?")
block_id = block.get("blockId", "")
block_type = block.get("blockType", "")
# 尝试提取文本预览
text_preview = _extract_text_preview(block)
print(f" [{index:>3}] {block_type:<16} {block_id} {text_preview}")
def _extract_text_preview(block: dict, max_length: int = 30) -> str:
"""从块元素中提取文本预览。"""
children = block.get("children", [])
texts = []
for child in children:
if isinstance(child, dict) and "text" in child:
texts.append(child["text"])
preview = "".join(texts)
if len(preview) > max_length:
preview = preview[:max_length] + "…"
return f'"{preview}"' if preview else ""
def cmd_insert(node_id: str, block_type: str, text: str, level: int,
after_block_id: str, before_block_id: str):
"""在文档中插入新块。"""
try:
element = build_element(block_type, text, level)
except ValueError as error:
print(f"❌ {error}")
sys.exit(1)
args = {"nodeId": node_id, "element": element}
position_hint = "文档末尾"
if after_block_id:
args["referenceBlockId"] = after_block_id
args["where"] = "after"
position_hint = f"块 {after_block_id} 之后"
elif before_block_id:
args["referenceBlockId"] = before_block_id
args["where"] = "before"
position_hint = f"块 {before_block_id} 之前"
print(f"➕ 插入 {block_type} 块到 {position_hint}")
print(f" 文档:{node_id}")
if text:
print(f" 内容:{text[:50]}{'…' if len(text) > 50 else ''}")
print("-" * 60)
success, output = run_mcporter(SERVER_NAME, "insert_document_block", args)
if not success:
print(f"❌ 插入失败:{output}")
sys.exit(1)
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
sys.exit(1)
new_block_id = result.get("blockId", "")
print(f"✅ 插入成功")
print(f" 新块 ID:{new_block_id}")
def cmd_update(node_id: str, block_id: str, text: str):
"""更新指定块的文本内容(仅支持 paragraph)。"""
element = build_paragraph_element(text)
args = {"nodeId": node_id, "blockId": block_id, "element": element}
print(f"✏️ 更新块:{block_id}")
print(f" 文档:{node_id}")
print(f" 新内容:{text[:50]}{'…' if len(text) > 50 else ''}")
print("-" * 60)
success, output = run_mcporter(SERVER_NAME, "update_document_block", args)
if not success:
print(f"❌ 更新失败:{output}")
sys.exit(1)
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
sys.exit(1)
print(f"✅ 更新成功")
def cmd_delete(node_id: str, block_id: str):
"""删除指定块(不可恢复)。"""
print(f"🗑️ 删除块:{block_id}")
print(f" 文档:{node_id}")
print(" ⚠️ 此操作不可恢复!")
print("-" * 60)
confirm = input("确认删除?输入 yes 继续:").strip().lower()
if confirm != "yes":
print("已取消删除。")
return
args = {"nodeId": node_id, "blockId": block_id}
success, output = run_mcporter(SERVER_NAME, "delete_document_block", args)
if not success:
print(f"❌ 删除失败:{output}")
sys.exit(1)
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
sys.exit(1)
print(f"✅ 删除成功")
# ──────────────────────────────────────────────
# CLI 入口
# ──────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="钉钉文档 Block 精细编辑工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
# list 子命令
list_parser = subparsers.add_parser("list", help="列出文档所有块元素")
list_parser.add_argument("node_id", help="文档节点 ID 或 URL")
# insert 子命令
insert_parser = subparsers.add_parser("insert", help="插入新块元素")
insert_parser.add_argument("node_id", help="文档节点 ID 或 URL")
insert_parser.add_argument(
"block_type",
choices=SUPPORTED_INSERT_TYPES,
help="块类型",
)
insert_parser.add_argument("--text", default="", help="块的文本内容")
insert_parser.add_argument("--level", type=int, default=1, help="标题级别(1~6,仅 heading 有效)")
position_group = insert_parser.add_mutually_exclusive_group()
position_group.add_argument("--after", metavar="BLOCK_ID", help="插入到指定块之后")
position_group.add_argument("--before", metavar="BLOCK_ID", help="插入到指定块之前")
# update 子命令
update_parser = subparsers.add_parser("update", help="更新块文本内容(仅支持 paragraph)")
update_parser.add_argument("node_id", help="文档节点 ID 或 URL")
update_parser.add_argument("block_id", help="块 ID(从 list 结果中获取)")
update_parser.add_argument("text", help="新的文本内容")
# delete 子命令
delete_parser = subparsers.add_parser("delete", help="删除块元素(不可恢复)")
delete_parser.add_argument("node_id", help="文档节点 ID 或 URL")
delete_parser.add_argument("block_id", help="块 ID(从 list 结果中获取)")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
if args.command == "list":
cmd_list(args.node_id)
elif args.command == "insert":
cmd_insert(
node_id=args.node_id,
block_type=args.block_type,
text=args.text,
level=args.level,
after_block_id=args.after or "",
before_block_id=args.before or "",
)
elif args.command == "update":
cmd_update(args.node_id, args.block_id, args.text)
elif args.command == "delete":
cmd_delete(args.node_id, args.block_id)
if __name__ == "__main__":
main()
#!/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 mcporter_utils import create_document_with_content
# ============== 安全常量 ==============
MAX_CONTENT_LENGTH = 50000 # 最大内容长度(字符)
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)
# 处理转义字符(命令行传入的 \n 转为真实换行)
if content:
content = content.replace('\\n', '\n').replace('\\t', '\t')
if content and len(content) > MAX_CONTENT_LENGTH:
print(f"⚠️ 内容过长({len(content)} 字符),截断到 {MAX_CONTENT_LENGTH} 字符")
content = content[:MAX_CONTENT_LENGTH]
print(f"📝 创建文档:{title}")
print("-" * 50)
# 新版 create_document 支持直接传 markdown,一步完成创建+写入
# 不传 folderId 时默认创建到"我的文档"根目录,无需提前获取根目录 ID
result = create_document_with_content(name=title, markdown=content or None)
if not result:
sys.exit(1)
node_id = result.get('nodeId', '')
doc_url = result.get('docUrl') or f"https://alidocs.dingtalk.com/i/nodes/{node_id}"
print(f"✅ 文档创建成功:{title}")
print(f" 文档 ID: {node_id}")
print("-" * 50)
print(f"\n文档链接:{doc_url}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
在钉钉文档中创建指定类型的文件
用法:
python create_file.py <name> <type> [folder_id]
参数:
name: 文件名称(必填)
type: 文件类型(必填),支持以下值:
adoc — 钉钉在线文档
axls — 钉钉表格
appt — 钉钉演示(PPT)
adraw — 钉钉白板
amind — 钉钉脑图
able — 钉钉多维表
folder — 文件夹
folder_id: 可选,目标文件夹节点 ID(dentryUuid)或文件夹 URL。
不传则创建到用户"我的文档"根目录。
示例:
python create_file.py "数据统计" axls
python create_file.py "架构设计" amind dpYLaezmVNd76KezTmDMX9bz8rMqPxX6
python create_file.py "2026 项目" folder
"""
import json
import sys
from mcporter_utils import run_mcporter
SUPPORTED_TYPES = {
"adoc": "钉钉在线文档",
"axls": "钉钉表格",
"appt": "钉钉演示(PPT)",
"adraw": "钉钉白板",
"amind": "钉钉脑图",
"able": "钉钉多维表",
"folder": "文件夹",
}
TYPE_EMOJI = {
"adoc": "📄",
"axls": "📊",
"appt": "📽️",
"adraw": "🎨",
"amind": "🧠",
"able": "🗂️",
"folder": "📁",
}
def create_file(name: str, file_type: str, folder_id: str = None) -> dict:
"""调用 create_file 工具创建文件,返回结果 dict,失败时返回 None。"""
args = {"name": name, "type": file_type}
if folder_id:
args["folderId"] = folder_id
result = run_mcporter("create_file", args)
return result
def main():
if len(sys.argv) < 3:
print(__doc__)
print("错误:缺少必要参数 name 或 type")
sys.exit(1)
name = sys.argv[1].strip()
file_type = sys.argv[2].strip().lower()
folder_id = sys.argv[3].strip() if len(sys.argv) > 3 else None
if not name:
print("错误:文件名称不能为空")
sys.exit(1)
if file_type not in SUPPORTED_TYPES:
supported_list = "、".join(f"{k}({v})" for k, v in SUPPORTED_TYPES.items())
print(f"错误:不支持的文件类型 \"{file_type}\"")
print(f"支持的类型:{supported_list}")
sys.exit(1)
emoji = TYPE_EMOJI.get(file_type, "📄")
type_label = SUPPORTED_TYPES[file_type]
location_hint = f"文件夹 {folder_id}" if folder_id else "我的文档根目录"
print(f"{emoji} 创建{type_label}:{name}")
print(f" 目标位置:{location_hint}")
print("-" * 50)
result = create_file(name=name, file_type=file_type, folder_id=folder_id)
if not result:
sys.exit(1)
node_id = result.get("nodeId", "")
doc_url = result.get("docUrl") or f"https://alidocs.dingtalk.com/i/nodes/{node_id}"
content_type = result.get("contentType", "")
print(f"✅ 创建成功:{name}")
print(f" 类型:{type_label}(contentType={content_type})")
print(f" 文件 ID:{node_id}")
print("-" * 50)
print(f"\n访问链接:{doc_url}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
导出钉钉文档到本地文件
用法:
python export_docs.py <node_id> [output.md]
参数:
node_id: 文档标识,支持两种格式:
- 文档 URL:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}
- 文档 ID(dentryUuid):32 位字母数字字符串
output.md: 可选,输出文件路径(默认:<doc_id>.md)
示例:
python export_docs.py https://alidocs.dingtalk.com/i/nodes/abc123
python export_docs.py abc123def456ghi789jkl012mno345pq
python export_docs.py https://alidocs.dingtalk.com/i/nodes/abc123 output.md
"""
import os
import re
import sys
from pathlib import Path
from mcporter_utils import get_document_content, resolve_safe_path
# ============== 安全常量 ==============
MAX_CONTENT_LENGTH = 100000 # 最大内容长度(字符)
ALLOWED_ROOT = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
# 支持 URL 或纯 ID 两种格式
DOC_URL_PATTERN = re.compile(
r'^https://alidocs\.dingtalk\.com/i/nodes/([a-zA-Z0-9]+)$',
re.IGNORECASE
)
DOC_ID_PATTERN = re.compile(r'^[a-zA-Z0-9]{16,}$')
def extract_doc_id(node_id: str) -> str:
"""
从 node_id 提取文档 ID(用于生成默认文件名)。
支持 URL 格式和纯 ID 格式。
"""
url_match = DOC_URL_PATTERN.match(node_id.strip())
if url_match:
return url_match.group(1)
if DOC_ID_PATTERN.match(node_id.strip()):
return node_id.strip()
return None
def save_content(content: str, path: Path) -> bool:
"""保存内容到文件"""
try:
with open(path, 'w', encoding='utf-8') as file:
file.write(content)
return True
except Exception as error:
print(f"❌ 保存文件失败:{error}")
return False
def main():
"""主函数"""
if len(sys.argv) < 2:
print(__doc__)
print("错误:缺少文档标识参数")
sys.exit(1)
node_id = sys.argv[1].strip()
output_path = sys.argv[2] if len(sys.argv) > 2 else None
# 提取文档 ID(用于生成默认文件名)
doc_id = extract_doc_id(node_id)
if not doc_id:
print("❌ 无效的文档标识格式")
print("支持格式:")
print(" URL:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}")
print(" ID:32 位字母数字字符串")
sys.exit(1)
# 确定输出文件路径
if not output_path:
output_path = f"{doc_id}.md"
# 解析并验证输出路径(防止目录遍历)
try:
safe_output = resolve_safe_path(output_path)
except ValueError as error:
print(f"❌ {error}")
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" 文档标识:{node_id}")
print(f" 目标文件:{safe_output}")
print("-" * 50)
# 步骤 1:获取文档内容(新版 API,nodeId 支持 URL 或 ID 自动识别)
print("步骤 1: 获取文档内容...")
content = get_document_content(node_id)
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]
# 步骤 2:保存文件
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()
#!/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 mcporter_utils import create_document_with_content, 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 read_local_file(path: Path) -> str:
"""读取本地文件内容,自动处理编码"""
if not validate_file_size(path):
sys.exit(1)
try:
with open(path, 'r', encoding='utf-8') as file:
return file.read()
except UnicodeDecodeError:
with open(path, 'r', encoding='gbk') as file:
return file.read()
except Exception as error:
print(f"❌ 读取文件失败:{error}")
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 error:
print(f"❌ {error}")
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)
# 步骤 1:读取文件内容
print("步骤 1: 读取文件内容...")
content = read_local_file(safe_path)
print(f" 内容长度:{len(content)} 字符")
if len(content) > MAX_CONTENT_LENGTH:
print(f"⚠️ 内容过长,截断到 {MAX_CONTENT_LENGTH} 字符")
content = content[:MAX_CONTENT_LENGTH]
# 步骤 2:创建文档并写入内容(新版 API 一步完成,无需先获取根目录 ID)
print("\n步骤 2: 创建文档并写入内容...")
result = create_document_with_content(name=title, markdown=content)
if not result:
sys.exit(1)
node_id = result.get('nodeId', '')
doc_url = result.get('docUrl') or f"https://alidocs.dingtalk.com/i/nodes/{node_id}"
print("-" * 50)
print("✅ 导入完成!")
print(f"\n文档链接:{doc_url}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
mcporter 公共工具函数
提供 mcporter 命令执行、响应解析、路径安全校验等通用功能,
供 create_doc.py、import_docs.py、export_docs.py 共用。
新版 MCP 工具名对照(v1.0,共 12 个):
create_document — 创建在线文档(支持直接传 markdown 初始内容,不传 folderId 默认到根目录)
create_file — 创建文件(adoc/axls/appt/adraw/amind/able/folder 七种类型)
update_document — 更新文档内容(mode: overwrite/append,默认 overwrite)
get_document_content — 获取文档内容(nodeId 支持 URL 或 ID 自动识别,需下载权限)
search_documents — 搜索文档(不传 keyword 返回最近访问列表)
create_folder — 创建文件夹(支持 folderId/workspaceId)
list_nodes — 遍历文件夹(支持 pageToken 分页)
get_document_info — 获取文档元信息
list_document_blocks — 查询文档块列表
insert_document_block — 插入块元素(heading.level 必须传整数)
update_document_block — 更新块元素(仅支持 paragraph)
delete_document_block — 删除块元素(不可恢复)
"""
import json
import os
import subprocess
from pathlib import Path
from typing import Optional, Tuple
def run_mcporter(server_name: str, tool_name: str, args: dict = None, timeout: int = 60) -> Tuple[bool, str]:
"""
执行 mcporter 命令(使用 --args JSON 传参)
Args:
server_name: MCP 服务名称,如 dingtalk-docs
tool_name: 工具名称,如 create_document
args: 参数字典,传入 --args JSON
timeout: 超时时间(秒)
Returns:
(success, output) 元组
"""
command = ['mcporter', 'call', server_name, tool_name, '--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 create_document_with_content(name: str, markdown: str = None, folder_id: str = None) -> Optional[dict]:
"""
创建文档(新版 API)。
新版 create_document 支持直接传 markdown 初始内容,一步完成创建+写入。
不传 folder_id 时默认创建到用户"我的文档"根目录,无需提前获取根目录 ID。
Args:
name: 文档标题
markdown: 文档初始内容(Markdown 格式),不传则创建空文档
folder_id: 目标文件夹节点 ID(支持 URL 或 ID),不传则创建到根目录
Returns:
包含 nodeId、docUrl 等字段的结果字典,失败返回 None
"""
args: dict = {'name': name}
if markdown:
args['markdown'] = markdown
if folder_id:
args['folderId'] = folder_id
success, output = run_mcporter('dingtalk-docs', 'create_document', args)
if not success:
print(f"❌ 创建文档失败:{output}")
return None
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
return None
return result
def get_document_content(node_id: str) -> Optional[str]:
"""
获取文档内容(新版 API)。
node_id 支持两种格式,系统自动识别:
- 文档 URL:https://alidocs.dingtalk.com/i/nodes/{dentryUuid}
- 文档 ID(dentryUuid):32 位字母数字字符串
Args:
node_id: 文档标识(URL 或 ID)
Returns:
文档 Markdown 内容字符串,失败返回 None
"""
success, output = run_mcporter('dingtalk-docs', 'get_document_content', {'nodeId': node_id})
if not success:
print(f"❌ 获取文档内容失败:{output}")
return None
result = parse_response(output)
if result is None:
print(f"❌ 解析响应失败:{output}")
return None
return result.get('markdown', '')
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 环境变量"
)
# Tests directory placeholder
{
"_comment": "复制此文件为 test_data.json,填入真实节点 ID 后即可运行端到端测试。test_data.json 已被 gitignore,不会提交到仓库。",
"docs": {
"writable_overwrite": "<有写权限的文档 nodeId,用于覆盖写测试>",
"writable_append": "<有写权限的文档 nodeId,用于追加写测试>",
"readable": "<只读文档 nodeId,含图片>",
"spreadsheet": "<表格文档 nodeId,不支持 Markdown>",
"no_permission": "<无权限文档 nodeId>",
"cross_org": "<跨组织文档 nodeId>",
"block_ops": "<Block 操作专用文档 nodeId>",
"empty": "<近似空文档 nodeId>"
},
"folders": {
"writable": "<有写权限的文件夹 nodeId>",
"readonly": "<只读文件夹 nodeId>",
"cross_org": "<跨组织文件夹 nodeId>",
"empty": "<空文件夹 nodeId>",
"paginated": "<分页测试文件夹 nodeId,至少 6 个子节点>"
},
"workspace_id": "<知识库 ID>",
"blocks": {
"heading_0": "<Block 文档中 index=0 的 heading 块 ID>",
"paragraph_1": "<Block 文档中 index=1 的 paragraph 块 ID>",
"blockquote_9": "<Block 文档中 index=9 的 blockquote 块 ID>",
"table_10": "<Block 文档中 index=10 的 table 块 ID>",
"paragraph_11": "<Block 文档中 index=11 的 paragraph 块 ID>"
}
}