钉钉文档技能
负责钉钉知识库和文档的所有操作,通过钉钉开放平台 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 (有下划线)可能为空。
核心操作
- 查询用户知识库列表
用户想查看有哪些知识库时:
GET https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId=<unionId>&maxResults=20&nextToken=<分页令牌> x-acs-dingtalk-access-token: <accessToken>
如有 nextToken 则继续翻页,直到无 nextToken 为止。返回字段中 workspaceId 和 rootNodeId 供后续操作使用。
- 查询知识库信息
GET https://api.dingtalk.com/v2.0/wiki/workspaces/{workspaceId}?operatorId=<unionId> x-acs-dingtalk-access-token: <accessToken>
- 查询目录结构(节点列表)
用户想看知识库里有哪些文档/文件夹时:
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 。
- 查询单个节点信息(通过 nodeId)
GET https://api.dingtalk.com/v2.0/wiki/nodes/{nodeId}?operatorId=<unionId> x-acs-dingtalk-access-token: <accessToken>
- 通过文档链接查询节点信息
用户提供了文档 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 。
- 创建文档
在指定知识库下新建文档:
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 富文本格式)。
- 删除文档
DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}?operatorId=<unionId> x-acs-dingtalk-access-token: <accessToken>
workspaceId 和 nodeId 均从创建文档的响应中获取。成功返回 200 {} 。
- 读取文档正文内容(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 ,是同一个值。
- 写入/覆盖文档正文内容
用户想修改文档内容时:
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" }
⚠️ 写入操作会覆盖原有内容,执行前请与用户确认或先读取备份。
- 文档成员管理
添加文档成员:
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