dingtalk-document
钉钉文档技能
负责钉钉知识库和文档的所有操作,通过钉钉开放平台 API 实现。
API 详情见 references/api.md。
配置管理(每次开始前必读)
配置文件路径
~/.dingtalk-skills/config(跨会话保留,所有 dingtalk-skills 共用同一文件)
本技能需要的配置
| 键 | 说明 | 来源 |
|---|---|---|
DINGTALK_APP_KEY |
钉钉应用 appKey | 开放平台 → 应用管理 → 凭证信息 |
DINGTALK_APP_SECRET |
钉钉应用 appSecret | 同上 |
DINGTALK_MY_USER_ID |
当前用户的企业员工 ID(userId) | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看(不是手机号、不是 unionId) |
DINGTALK_MY_OPERATOR_ID |
当前用户的 unionId | 首次由脚本自动通过 userId 转换获取并写入 |
启动流程(每次执行任务前)
- 读取配置:检查
~/.dingtalk-skills/config是否存在,解析已有键值 - 识别缺失项:找出上表中尚未配置的键
- 一次性收集:将所有缺失项合并为一条提问,不要逐条询问,例如:
需要以下信息才能继续(已有的无需再填):
- 钉钉应用 appKey(钉钉开放平台 → 应用管理 → 凭证信息)
- 钉钉应用 appSecret
- 你的钉钉 userId(管理后台 → 通讯录 → 成员管理 → 点击姓名查看)
- 持久化:将用户提供的值追加写入 config,后续直接读取,无需再问
- 执行任务:配置完整后开始操作
注意:
APP_KEY/APP_SECRET/OPERATOR_ID属于凭证,禁止在输出中完整打印,确认时仅显示前 4 位 +****。
认证
每次调用 API 前,用 appKey/appSecret 获取当次的 accessToken(有效期 2 小时):
POST https://api.dingtalk.com/v1.0/oauth2/accessToken
Content-Type: application/json
{
"appKey": "<应用 appKey>",
"appSecret": "<应用 appSecret>"
}
返回:{ "accessToken": "xxx", "expireIn": 7200 }
所有后续请求均需携带请求头:
x-acs-dingtalk-access-token: <accessToken>
为什么需要 operatorId(unionId)
钉钉开放平台要求所有写操作(创建文档、写入内容、管理成员等)必须代表一个真实用户身份执行,而不是以匿名应用身份操作。operatorId 就是声明"这个操作是谁做的"——它会被记录到文档的变更历史,并用于权限校验。
- 文档/知识库 API 的
operatorId参数使用 unionId(跨组织唯一),不是 userId - 配置中优先收集
userId(管理后台直接查看),系统自动转换为unionId
身份标识说明
| 标识 | 说明 | 如何获取 |
|---|---|---|
userId(= staffId) |
企业内部员工 ID,最容易获取 | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看 |
unionId |
跨企业/跨应用唯一 | 通过 userId 调用 API 转换获取 |
userId → unionId 自动转换
当配置中有 DINGTALK_MY_USER_ID 但缺少 DINGTALK_MY_OPERATOR_ID 时,自动执行转换:
# 1. 获取旧版 token
OLD_TOKEN=$(curl -s "https://oapi.dingtalk.com/gettoken?appkey=${APP_KEY}&appsecret=${APP_SECRET}" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
# 2. userId → unionId
UNION_ID=$(curl -s -X POST "https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${OLD_TOKEN}" \
-H 'Content-Type: application/json' \
-d "{\"userid\":\"${USER_ID}\"}" | grep -o '"unionid":"[^"]*"' | cut -d'"' -f4)
# 3. 写入配置文件
echo "DINGTALK_MY_OPERATOR_ID=$UNION_ID" >> ~/.dingtalk-skills/config
⚠️ 注意:返回体中
result.unionid(无下划线)有值,result.union_id(有下划线)可能为空。
核心操作
1. 查询用户知识库列表
用户想查看有哪些知识库时:
GET https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId=<unionId>&maxResults=20&nextToken=<分页令牌>
x-acs-dingtalk-access-token: <accessToken>
如有 nextToken 则继续翻页,直到无 nextToken 为止。返回字段中 workspaceId 和 rootNodeId 供后续操作使用。
2. 查询知识库信息
GET https://api.dingtalk.com/v2.0/wiki/workspaces/{workspaceId}?operatorId=<unionId>
x-acs-dingtalk-access-token: <accessToken>
3. 查询目录结构(节点列表)
用户想看知识库里有哪些文档/文件夹时:
GET https://api.dingtalk.com/v2.0/wiki/nodes?parentNodeId=<nodeId>&operatorId=<unionId>&maxResults=50
x-acs-dingtalk-access-token: <accessToken>
parentNodeId 传知识库的 rootNodeId 可列出顶层内容,传子文件夹 nodeId 可深入查看。
每个节点包含:nodeId、name、type(FILE/FOLDER)、category、workspaceId、url。
4. 查询单个节点信息(通过 nodeId)
GET https://api.dingtalk.com/v2.0/wiki/nodes/{nodeId}?operatorId=<unionId>
x-acs-dingtalk-access-token: <accessToken>
5. 通过文档链接查询节点信息
用户提供了文档 URL(如 https://alidocs.dingtalk.com/i/nodes/Xxx...)时:
POST https://api.dingtalk.com/v2.0/wiki/nodes/queryByUrl?operatorId=<unionId>
x-acs-dingtalk-access-token: <accessToken>
Content-Type: application/json
{
"url": "https://alidocs.dingtalk.com/i/nodes/<nodeId>",
"operatorId": "<unionId>"
}
返回节点信息,其中 nodeId 可作为后续内容读取的 docKey。
6. 创建文档
在指定知识库下新建文档:
POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs
x-acs-dingtalk-access-token: <accessToken>
Content-Type: application/json
{
"operatorId": "<unionId>",
"docType": "DOC",
"name": "<文档标题>"
}
返回字段:
| 字段 | 说明 |
|---|---|
nodeId |
知识库节点 ID(用于删除) |
docKey |
文档内容 Key(用于内容读写,≠ nodeId) |
workspaceId |
实际所在知识库 ID(可能与请求的不同,删除时须使用此值) |
url |
文档访问链接 |
docType固定填"DOC"(ALIDOC 富文本格式)。
10. 删除文档
DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}?operatorId=<unionId>
x-acs-dingtalk-access-token: <accessToken>
workspaceId 和 nodeId 均从创建文档的响应中获取。成功返回 200 {}。
7. 读取文档正文内容(Block 结构)
用户想看文档里写了什么内容时,使用 docKey 读取文档 Block:
GET https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/blocks?operatorId=<unionId>
x-acs-dingtalk-access-token: <accessToken>
docKey 的来源:
- 通过 wiki nodes 接口查到的节点:
docKey = nodeId(同一个值) - 通过创建文档接口新建的文档:使用响应中的
docKey字段(不是nodeId)
所需权限:Storage.File.Read
返回示例:
{
"result": {
"data": [
{ "blockType": "heading", "heading": { "level": "heading-2", "text": "快速开始" }, "index": 0, "id": "xxx" },
{ "blockType": "paragraph", "paragraph": { "text": "正文内容..." }, "index": 1, "id": "yyy" },
{ "blockType": "unknown", "index": 2, "id": "zzz" }
]
},
"success": true
}
blockType 枚举:heading、paragraph、unorderedList、orderedList、table、blockquote、unknown(代码块/图片等富文本暂未解析)。
将各 block 的文本提取后按 index 顺序拼接,即可重建文档文字内容。
docKey即通过 wiki nodes 接口获取的nodeId,是同一个值。
8. 写入/覆盖文档正文内容
用户想修改文档内容时:
POST https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/overwriteContent
x-acs-dingtalk-access-token: <accessToken>
Content-Type: application/json
{
"operatorId": "<unionId>",
"docContent": "# 新标题\n\n新的正文内容,支持 Markdown 格式。",
"contentType": "markdown"
}
⚠️ 写入操作会覆盖原有内容,执行前请与用户确认或先读取备份。
9. 文档成员管理
添加文档成员:
POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members
x-acs-dingtalk-access-token: <accessToken>
Content-Type: application/json
{
"operatorId": "<unionId>",
"members": [
{ "id": "<userId>", "roleType": "editor" }
]
}
roleType 可选:viewer(只读)、editor(可编辑)
典型场景
"读取文档 X 的内容"
- 若用户提供了 URL,调用
POST /v2.0/wiki/nodes/queryByUrl获取nodeId - 否则通过
GET /v2.0/wiki/nodes?parentNodeId=...遍历查找目标文档 - 用
nodeId作为docKey,调用GET /v1.0/doc/suites/documents/{docKey}/blocks - 将 block 文本按 index 顺序拼接后展示给用户
"把文档 X 的内容改成……"
- 先读取原内容,告知用户将被覆盖,请求确认
- 调用
POST /v1.0/doc/suites/documents/{docKey}/overwriteContent,传入新内容 - 告知写入成功
"帮我在钉钉创建一个文档"
- 询问放到哪个知识库(列出知识库或让用户说名称)
- 通过
GET /v2.0/wiki/workspaces找到对应workspaceId - 调用
POST /v1.0/doc/workspaces/{workspaceId}/docs,docType: ALIDOC - 返回文档链接给用户
"查看知识库 X 下有哪些文档"
- 通过
GET /v2.0/wiki/workspaces找到workspaceId和rootNodeId - 调用
GET /v2.0/wiki/nodes?parentNodeId={rootNodeId}&operatorId=... - 整理成目录树展示
"把用户 xxx 加到文档 Y"
- 确认文档的
nodeId和所在workspaceId - 调用
POST /v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members,指定roleType
错误处理
| HTTP 状态码 | 错误码 | 含义 | 处理方式 |
|---|---|---|---|
| 400 | MissingoperatorId |
operatorId 未传 | 补充 operatorId(unionId) |
| 400 | paramError |
参数类型错误 | operatorId 必须是 unionId,不是 userId |
| 401 | — | token 过期 | 重新获取 accessToken 后重试 |
| 403 | Forbidden.AccessDenied.AccessTokenPermissionDenied |
应用缺少权限 | 错误信息中有 requiredScopes,提示用户在开放平台开通对应权限 |
| 404 | InvalidAction.NotFound |
接口路径不存在 | 检查版本号(v1.0/v2.0)和路径是否正确 |
| 429 | — | 触发限流 | 等待 1 秒后重试 |
发生错误时,将响应体中的 code 和 message 展示给用户辅助排查。
所需应用权限
| 功能 | 权限 scope |
|---|---|
| 查询知识库/节点 | Wiki.Node.Read |
| 读取文档正文 | Storage.File.Read |
| 写入文档正文 | Storage.File.Write |
| 创建/删除文档 | Storage.File.Write |
| 查询用户 unionId(获取 operatorId) | Contact.User.Read |