feat: add dingtalk-log skill for querying work reports
- Add SKILL.md with full API documentation - Add shell script for quick queries - Update TOOLS.md with reference to new skill
This commit is contained in:
88
skills/dingtalk-document/SKILL.md
Normal file
88
skills/dingtalk-document/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: dingtalk-document
|
||||
description: 钉钉知识库和文档管理操作。当用户提到"钉钉文档"、"知识库"、"新建文档"、"查看文档目录"、"读取文档内容"、"写入文档"、"更新文档"、"文档成员"、"dingtalk doc"、"knowledge base"时使用此技能。支持:创建知识库、查询知识库列表、新建文档/文件夹、读取/写入文档正文内容、管理成员权限等全部文档类操作。
|
||||
---
|
||||
|
||||
# 钉钉文档技能
|
||||
|
||||
负责钉钉知识库和文档的所有操作。本文件为**策略指南**,仅包含决策逻辑和工作流程。完整 API 请求格式见文末「references/api.md 查阅索引」。
|
||||
|
||||
> `dt_helper.sh` 位于本 `SKILL.md` 同级目录的 `scripts/dt_helper.sh`。
|
||||
|
||||
## 核心概念
|
||||
- **知识库(Workspace)**:文档容器,有 `workspaceId` 和 `rootNodeId`
|
||||
- **节点(Node)**:文件或文件夹,`type` 为 `FILE` 或 `FOLDER`
|
||||
- **文档标识(用于 `/v1.0/doc/suites/documents/{id}`)**:可用 `docKey` 或 `dentryUuid`
|
||||
- 创建文档响应会返回:`docKey`、`dentryUuid`、`nodeId`
|
||||
- 其中 `docKey` / `dentryUuid` 可用于读写正文;`nodeId` 用于删除和文档管理类接口
|
||||
- `wiki/nodes` 返回的 `nodeId` 实际上是 `dentryUuid`,可直接用于正文读写
|
||||
- **operatorId**:所有接口必须的 unionId 参数,通过 `bash scripts/dt_helper.sh --to-unionid` 自动转换
|
||||
|
||||
## 工作流程(每次执行前)
|
||||
1. **先识别本次任务类型** → 例如:列知识库、读文档、写文档、创建文档、成员管理
|
||||
2. **按本次任务校验所需配置** → 通过 `bash scripts/dt_helper.sh --get KEY` 读取;仅校验本任务必须项
|
||||
3. **仅收集缺失配置** → 若缺少某项,**一次性询问用户**所有缺失值,用 `bash scripts/dt_helper.sh --set KEY=VALUE` 写入
|
||||
4. **获取 Token / operatorId** → 直接调用 `bash scripts/dt_helper.sh`,token 获取与缓存细节无需关心
|
||||
5. **执行操作** → 凡是包含变量替换、管道或多行逻辑的命令,写入 `/tmp/<task>.sh` 再 `bash /tmp/<task>.sh` 执行。不要把多行命令直接粘到终端里(终端工具会截断),也不要用 `<<'EOF'` 语法(heredoc 在工具中同样会被截断导致变量丢失)
|
||||
|
||||
### 按任务校验配置(必须先做)
|
||||
- **所有任务通用必需**:`DINGTALK_APP_KEY`、`DINGTALK_APP_SECRET`、`DINGTALK_MY_USER_ID`
|
||||
- **涉及任何文档/知识库 API 调用**:必须有 `DINGTALK_MY_OPERATOR_ID`(若缺失,先用 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写回)
|
||||
- **创建/读取/写入/删除/成员管理**:除上述通用项外,无额外固定配置键;`workspaceId`/`nodeId`/`docKey` 属于任务参数,运行时从用户输入或 API 响应中获取
|
||||
|
||||
> 规则:未通过“本次任务配置校验”前,不得进入 API 调用步骤。
|
||||
|
||||
> 凭证禁止在输出中完整打印,确认时仅显示前 4 位 + `****`
|
||||
|
||||
### 所需配置
|
||||
| 配置键 | 必填 | 说明 | 如何获取 |
|
||||
|---|---|---|---|
|
||||
| `DINGTALK_APP_KEY` | ✅ | 应用 AppKey | 钉钉开放平台 → 应用管理 → 凭证信息 |
|
||||
| `DINGTALK_APP_SECRET` | ✅ | 应用 AppSecret | 同上 |
|
||||
| `DINGTALK_MY_USER_ID` | ✅ | 当前用户的企业员工 ID(userId) | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看 |
|
||||
| `DINGTALK_MY_OPERATOR_ID` | ✅ | 当前用户的 unionId(operatorId) | 首次由 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写入 |
|
||||
|
||||
### 身份标识说明
|
||||
| 标识 | 说明 |
|
||||
|---|---|
|
||||
| `userId`(= `staffId`) | 企业内部员工 ID,可通过管理后台 -> 通讯录 -> 成员管理 -> 点击姓名查看 |
|
||||
| `unionId` | 跨企业/跨应用唯一标识,可通过 `bash scripts/dt_helper.sh --to-unionid <userid>` 获取 |
|
||||
|
||||
### 执行脚本模板
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
HELPER="./scripts/dt_helper.sh"
|
||||
NEW_TOKEN=$(bash "$HELPER" --token)
|
||||
OPERATOR_ID=$(bash "$HELPER" --get DINGTALK_MY_OPERATOR_ID)
|
||||
|
||||
# 在此追加具体 API 调用,例如查询知识库列表:
|
||||
WORKSPACES=$(curl -s -X GET "https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId=${OPERATOR_ID}&maxResults=20" \
|
||||
-H "x-acs-dingtalk-access-token: $NEW_TOKEN")
|
||||
echo "知识库列表: $WORKSPACES"
|
||||
```
|
||||
|
||||
> **Token 失效处理**:dt_helper 仅按时间缓存,无法感知 token 被提前吊销。若 API 返回 401(token 无效/过期),用 `--nocache` 跳过缓存强制重新获取:
|
||||
> ```bash
|
||||
> NEW_TOKEN=$(bash "$HELPER" --token --nocache)
|
||||
> ```
|
||||
|
||||
## references/api.md 查阅索引
|
||||
确定好要做什么之后,用以下命令从 `references/api.md` 中提取对应章节的完整 API 细节(请求格式、参数说明、返回值示例):
|
||||
```bash
|
||||
grep -A 30 "^## 1. 查询知识库列表" references/api.md
|
||||
grep -A 10 "^## 2. 查询知识库信息" references/api.md
|
||||
grep -A 35 "^## 3. 查询节点列表" references/api.md
|
||||
grep -A 10 "^## 4. 查询单个节点" references/api.md
|
||||
grep -A 15 "^## 5. 通过 URL 查询节点" references/api.md
|
||||
grep -A 28 "^## 6. 创建文档" references/api.md
|
||||
grep -A 10 "^## 7. 删除文档" references/api.md
|
||||
grep -A 30 "^## 8. 读取文档内容" references/api.md
|
||||
grep -A 15 "^## 9. 覆盖写入文档内容" references/api.md
|
||||
grep -A 12 "^## 10. 追加文本到段落" references/api.md
|
||||
grep -A 18 "^## 11. 添加文档成员" references/api.md
|
||||
grep -A 12 "^## 12. 更新文档成员权限" references/api.md
|
||||
grep -A 10 "^## 13. 移除文档成员" references/api.md
|
||||
grep -A 10 "^## 错误码" references/api.md
|
||||
grep -A 10 "^## 所需应用权限" references/api.md
|
||||
```
|
||||
341
skills/dingtalk-document/references/api.md
Normal file
341
skills/dingtalk-document/references/api.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# dingtalk-document API 参考
|
||||
|
||||
> 所有接口均已验证可用。
|
||||
> `NEW_TOKEN` = 新版 token(`api.dingtalk.com` 用),获取方式 `bash scripts/dt_helper.sh --token`
|
||||
> `OPERATOR_ID` = 用户 unionId,获取方式 `bash scripts/dt_helper.sh --get DINGTALK_MY_OPERATOR_ID`
|
||||
> ⚠️ **重要**:所有接口均需传 `operatorId`(unionId),缺少则返回 `MissingoperatorId` 错误。
|
||||
|
||||
---
|
||||
|
||||
## 1. 查询知识库列表
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId={OPERATOR_ID}&maxResults=20&nextToken=
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `operatorId` | string | ✅ | 用户 unionId |
|
||||
| `maxResults` | int | ❌ | 每页数量,默认 20 |
|
||||
| `nextToken` | string | ❌ | 分页令牌,首次为空 |
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"workspaceId": "QXvd5SLN2AxOQz0Z",
|
||||
"name": "团队知识库",
|
||||
"description": "...",
|
||||
"rootNodeId": "P0MALyR8kl3qpB7qTkM1xn3mW3bzYmDO",
|
||||
"type": "TEAM",
|
||||
"url": "https://alidocs.dingtalk.com/i/spaces/.../overview",
|
||||
"createTime": "2024-01-01T00:00Z",
|
||||
"modifiedTime": "2024-06-01T00:00Z"
|
||||
}
|
||||
],
|
||||
"nextToken": "..."
|
||||
}
|
||||
```
|
||||
|
||||
> 翻页:`nextToken` 非空时传入下次请求继续获取。
|
||||
|
||||
---
|
||||
|
||||
## 2. 查询知识库信息
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v2.0/wiki/workspaces/{workspaceId}?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
响应:单个 workspace 对象(结构同列表项)
|
||||
|
||||
---
|
||||
|
||||
## 3. 查询节点列表
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v2.0/wiki/nodes?parentNodeId={nodeId}&operatorId={OPERATOR_ID}&maxResults=50&nextToken=
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `parentNodeId` | string | ✅ | 父节点 ID,传知识库的 `rootNodeId` 可列出顶层内容 |
|
||||
| `operatorId` | string | ✅ | 用户 unionId |
|
||||
| `maxResults` | int | ❌ | 每页数量,默认 20 |
|
||||
| `nextToken` | string | ❌ | 分页令牌 |
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"nodeId": "LeBq413JAw31yaz1fB0BBdLGWDOnGvpb",
|
||||
"name": "使用文档.adoc",
|
||||
"type": "FILE",
|
||||
"category": "ALIDOC",
|
||||
"extension": "adoc",
|
||||
"workspaceId": "QXvd5SnBnzmZdZ0Z",
|
||||
"url": "https://alidocs.dingtalk.com/i/nodes/...",
|
||||
"createTime": "2026-03-04T16:58Z",
|
||||
"modifiedTime": "2026-03-04T17:51Z"
|
||||
}
|
||||
],
|
||||
"nextToken": "..."
|
||||
}
|
||||
```
|
||||
|
||||
> `type`:`FILE`(文档/文件)| `FOLDER`(文件夹)
|
||||
|
||||
---
|
||||
|
||||
## 4. 查询单个节点
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v2.0/wiki/nodes/{nodeId}?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
响应:`{ "node": { nodeId, name, type, category, workspaceId, url, ... } }`
|
||||
|
||||
---
|
||||
|
||||
## 5. 通过 URL 查询节点
|
||||
|
||||
```
|
||||
POST https://api.dingtalk.com/v2.0/wiki/nodes/queryByUrl?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"url": "https://alidocs.dingtalk.com/i/nodes/<nodeId>",
|
||||
"operatorId": "{OPERATOR_ID}"
|
||||
}
|
||||
```
|
||||
|
||||
响应:与 GET 单个节点相同的 node 结构。
|
||||
|
||||
---
|
||||
|
||||
## 6. 创建文档
|
||||
|
||||
```
|
||||
POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"operatorId": "{OPERATOR_ID}",
|
||||
"docType": "DOC",
|
||||
"name": "文档标题"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `operatorId` | string | ✅ | 用户 unionId |
|
||||
| `docType` | string | ✅ | 固定填 `"DOC"`(ALIDOC 富文本格式) |
|
||||
| `name` | string | ✅ | 文档标题 |
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"dentryUuid": "aaa",
|
||||
"nodeId": "xxx",
|
||||
"docKey": "yyy",
|
||||
"workspaceId": "zzz",
|
||||
"url": "https://..."
|
||||
}
|
||||
```
|
||||
|
||||
> **重要**:
|
||||
> - `docKey` / `dentryUuid`:用于 `/v1.0/doc/suites/documents/{id}` 内容读写
|
||||
> - `nodeId`:用于 `/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}` 删除/管理
|
||||
|
||||
---
|
||||
|
||||
## 7. 删除文档
|
||||
|
||||
```
|
||||
DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
> `workspaceId` 和 `nodeId` 均使用创建文档响应中的值。成功返回 `200 {}`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 读取文档内容(Block 结构)
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/blocks?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
> **docKey 的来源**:
|
||||
> - 通过 wiki nodes 接口查到的 `nodeId` 本质是 `dentryUuid`,可直接用于正文读写
|
||||
> - 通过创建文档接口新建的文档:可使用响应中的 `docKey` 或 `dentryUuid`
|
||||
> - 创建响应中的 `nodeId`(通常出现在 `/docs/<nodeId>` 链接中)不能直接用于正文读写
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"data": [
|
||||
{ "blockType": "heading", "heading": { "level": "heading-2", "text": "快速开始" }, "index": 0, "id": "xxx" },
|
||||
{ "blockType": "paragraph", "paragraph": { "text": "正文内容..." }, "index": 1, "id": "yyy" },
|
||||
{ "blockType": "table", "table": { "colSize": 2, "rowSize": 10 }, "index": 2, "id": "zzz" },
|
||||
{ "blockType": "unknown", "index": 3, "id": "aaa", "unknown": {} }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
`blockType` 枚举:`heading`、`paragraph`、`unorderedList`、`orderedList`、`table`、`blockquote`、`unknown`(代码块/图片等)
|
||||
|
||||
---
|
||||
|
||||
## 9. 覆盖写入文档内容
|
||||
|
||||
```
|
||||
POST https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/overwriteContent?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
> ⚠️ `operatorId` 必须**同时**作为 query param 和请求体字段传入,缺一会报 `Missingcontent` 错误。
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"operatorId": "{OPERATOR_ID}",
|
||||
"content": "# 新标题\n\n新的正文内容,支持 Markdown 格式。",
|
||||
"contentType": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 此操作**全量覆盖**文档内容,不可撤销。
|
||||
|
||||
---
|
||||
|
||||
## 10. 追加文本到段落
|
||||
|
||||
```
|
||||
POST https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/blocks/{blockId}/paragraph/appendText
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "operatorId": "{OPERATOR_ID}", "text": "追加的文字" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 添加文档成员
|
||||
|
||||
```
|
||||
POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"operatorId": "{OPERATOR_ID}",
|
||||
"members": [
|
||||
{ "id": "<userId>", "roleType": "viewer" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 用户 userId(**注意**:这里用 userId,不是 unionId)|
|
||||
| `roleType` | `viewer`(只读)| `editor`(可编辑)|
|
||||
|
||||
---
|
||||
|
||||
## 12. 更新文档成员权限
|
||||
|
||||
```
|
||||
PUT https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members/{memberId}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "operatorId": "{OPERATOR_ID}", "roleType": "viewer" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 移除文档成员
|
||||
|
||||
```
|
||||
DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members/{memberId}?operatorId={OPERATOR_ID}
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 搜索文档(用于找回可读写 ID)
|
||||
|
||||
```
|
||||
GET https://api.dingtalk.com/v1.0/doc/docs?operatorId={OPERATOR_ID}&workspaceId={workspaceId}&keyword={keyword}&maxResults=20&nextToken=
|
||||
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
|
||||
```
|
||||
|
||||
说明:
|
||||
- 需要权限:`Document.WorkspaceDocument.Read`
|
||||
- 返回结果中的 `docs[].nodeBO.nodeId` 即可用于 `/v1.0/doc/suites/documents/{id}` 的读写(该值是 `dentryUuid` 风格 ID)
|
||||
|
||||
示例响应(节选):
|
||||
```json
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"nodeBO": {
|
||||
"name": "测试创建.adoc",
|
||||
"nodeId": "np9zOoBVBYwB2OZBIn4y0G1vW1DK0g6l",
|
||||
"url": "https://alidocs.dingtalk.com/i/nodes/np9zOoBVBYwB2OZBIn4y0G1vW1DK0g6l"
|
||||
},
|
||||
"workspaceBO": {
|
||||
"workspaceId": "QXvd5SLN2AxOQz0Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"hasMore": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码
|
||||
|
||||
| HTTP状态码 | 错误码 | 说明 | 处理建议 |
|
||||
|---|---|---|---|
|
||||
| 400 | `MissingoperatorId` | operatorId 未传 | 补充 operatorId(unionId)|
|
||||
| 400 | `paramError` | 参数类型错误 | operatorId 必须是 unionId,不是 userId |
|
||||
| 403 | `Forbidden.AccessDenied.AccessTokenPermissionDenied` | 应用缺少权限 | 错误中有 `requiredScopes`,开通对应权限 |
|
||||
| 404 | `InvalidAction.NotFound` | 接口路径不存在 | 检查版本号(v1.0/v2.0)和路径 |
|
||||
| 429 | — | QPS 限流 | 1 秒后重试 |
|
||||
|
||||
---
|
||||
|
||||
## 所需应用权限
|
||||
|
||||
| 功能 | 权限 scope |
|
||||
|---|---|
|
||||
| 查询知识库/节点 | `Wiki.Node.Read` |
|
||||
| 读取文档正文 | `Storage.File.Read` |
|
||||
| 写入文档正文 | `Storage.File.Write` |
|
||||
| 创建/删除文档 | `Storage.File.Write` |
|
||||
| 查询用户 unionId | `Contact.User.Read` |
|
||||
381
skills/dingtalk-document/scripts/dt_helper.sh
Executable file
381
skills/dingtalk-document/scripts/dt_helper.sh
Executable file
@@ -0,0 +1,381 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# dt_helper.sh — 钉钉开放平台辅助工具
|
||||
# 路径: scripts/common/dt_helper.sh
|
||||
# 用法: bash scripts/common/dt_helper.sh <命令> [参数]
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
CONFIG="${DINGTALK_CONFIG:-$HOME/.dingtalk-skills/config}"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 帮助信息
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
show_help() {
|
||||
cat <<'EOF'
|
||||
钉钉开放平台辅助工具 (dt_helper.sh)
|
||||
用法: bash scripts/common/dt_helper.sh <命令> [参数]
|
||||
|
||||
Token 管理(两种 token 互不兼容,按域名区分):
|
||||
--token [--nocache] 获取新版 accessToken(用于 api.dingtalk.com 域名的所有接口)
|
||||
适用:待办、文档、AI 表格等 api.dingtalk.com 域名下所有版本的接口
|
||||
请求头:x-acs-dingtalk-access-token: <token>
|
||||
有缓存且未过期则直接返回,否则自动刷新并缓存
|
||||
--nocache:跳过缓存,强制重新获取(token 被提前吊销时使用)
|
||||
--token-info 查看新版 token 缓存状态(是否有效、剩余有效秒数)
|
||||
--clear-token 清除缓存的新版 token(下次 --token 时强制重新获取)
|
||||
--old-token [--nocache]
|
||||
获取旧版 access_token(用于 oapi.dingtalk.com 域名的所有接口)
|
||||
适用:群消息/工作通知/userId↔unionId 转换等 oapi.dingtalk.com 接口
|
||||
不适用:api.dingtalk.com 接口(如待办、文档、AI表格)
|
||||
⚠️ 新旧两种 token 互不兼容,混用会导致 401/403
|
||||
--nocache:跳过缓存,强制重新获取(token 被提前吊销时使用)
|
||||
|
||||
身份转换:
|
||||
--to-unionid [userId] 将 userId 转换为 unionId
|
||||
不传参数:转换配置中的 DINGTALK_MY_USER_ID(操作者自身),
|
||||
结果首次自动写入 DINGTALK_MY_OPERATOR_ID
|
||||
传入参数:动态转换指定 userId,仅返回结果,不写入配置
|
||||
--to-userid [unionId] 将 unionId 反向转换为 userId(需传入参数)
|
||||
|
||||
配置管理:
|
||||
--config 查看 ~/.dingtalk-skills/config 中的所有配置项(敏感项脱敏显示)
|
||||
--get KEY [KEY...] 获取一个或多个配置项的值(敏感项脱敏显示)
|
||||
--set KEY=VALUE 将配置项持久化写入配置文件(已存在则更新,不存在则追加,目录自动创建)
|
||||
|
||||
帮助:
|
||||
--help, -h 显示此帮助信息
|
||||
|
||||
环境变量:
|
||||
DINGTALK_CONFIG 覆盖默认配置文件路径(默认 ~/.dingtalk-skills/config)
|
||||
|
||||
配置文件:
|
||||
~/.dingtalk-skills/config key=value 格式,存储以下键:
|
||||
DINGTALK_APP_KEY 应用 Client ID(AppKey)
|
||||
DINGTALK_APP_SECRET 应用 Client Secret(AppSecret)
|
||||
DINGTALK_MY_USER_ID 企业员工 ID(userId,管理后台通讯录可查)
|
||||
DINGTALK_MY_OPERATOR_ID 操作者 unionId(由 --to-unionid 自动生成)
|
||||
DINGTALK_ACCESS_TOKEN 新版 token 缓存
|
||||
DINGTALK_TOKEN_EXPIRY 新版 token 过期时间戳(Unix 秒)
|
||||
DINGTALK_OLD_TOKEN 旧版 token 缓存
|
||||
DINGTALK_OLD_TOKEN_EXPIRY 旧版 token 过期时间戳(Unix 秒)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 工具函数
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# 从配置文件读取指定键的值
|
||||
cfg_get() {
|
||||
local key="$1"
|
||||
grep "^${key}=" "$CONFIG" 2>/dev/null | head -1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
# 写入或更新配置文件中的键值
|
||||
cfg_set() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
mkdir -p "$(dirname "$CONFIG")"
|
||||
touch "$CONFIG"
|
||||
if grep -q "^${key}=" "$CONFIG" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG"
|
||||
else
|
||||
echo "${key}=${value}" >> "$CONFIG"
|
||||
fi
|
||||
}
|
||||
|
||||
# 从配置文件删除指定键
|
||||
cfg_del() {
|
||||
local key="$1"
|
||||
sed -i "/^${key}=/d" "$CONFIG" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 确保必须的配置项存在,否则报错退出
|
||||
require_cfg() {
|
||||
local key="$1"
|
||||
local val
|
||||
val=$(cfg_get "$key")
|
||||
if [ -z "$val" ]; then
|
||||
echo "❌ 缺少配置项 ${key},请先运行: bash scripts/common/dt_helper.sh --set ${key}=<值>" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$val"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Token 管理
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_token() {
|
||||
local force="${1:-}" app_key app_secret cached expiry now resp token expire_in
|
||||
|
||||
app_key=$(require_cfg DINGTALK_APP_KEY)
|
||||
app_secret=$(require_cfg DINGTALK_APP_SECRET)
|
||||
now=$(date +%s)
|
||||
|
||||
if [ "$force" != "--nocache" ]; then
|
||||
cached=$(cfg_get DINGTALK_ACCESS_TOKEN)
|
||||
expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY)
|
||||
if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then
|
||||
echo "$cached"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 过期或无缓存,重新获取
|
||||
resp=$(curl -s -X POST "https://api.dingtalk.com/v1.0/oauth2/accessToken" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"appKey\":\"${app_key}\",\"appSecret\":\"${app_secret}\"}")
|
||||
|
||||
token=$(echo "$resp" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)
|
||||
expire_in=$(echo "$resp" | grep -o '"expireIn":[0-9]*' | cut -d: -f2)
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "❌ 获取 token 失败: $resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cfg_set DINGTALK_ACCESS_TOKEN "$token"
|
||||
cfg_set DINGTALK_TOKEN_EXPIRY "$((now + expire_in - 200))"
|
||||
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
cmd_token_info() {
|
||||
local cached expiry now remaining
|
||||
|
||||
cached=$(cfg_get DINGTALK_ACCESS_TOKEN)
|
||||
expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY)
|
||||
now=$(date +%s)
|
||||
|
||||
if [ -z "$cached" ]; then
|
||||
echo "状态: 无缓存(从未获取或已清除)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$expiry" ] || [ "$now" -ge "$expiry" ]; then
|
||||
echo "状态: 已过期"
|
||||
echo "Token: ${cached:0:20}..."
|
||||
else
|
||||
remaining=$((expiry - now))
|
||||
echo "状态: 有效"
|
||||
echo "Token: ${cached:0:20}..."
|
||||
echo "剩余: ${remaining} 秒(约 $((remaining / 60)) 分钟)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_clear_token() {
|
||||
cfg_del DINGTALK_ACCESS_TOKEN
|
||||
cfg_del DINGTALK_TOKEN_EXPIRY
|
||||
echo "✅ 新版 Token 缓存已清除"
|
||||
}
|
||||
|
||||
cmd_old_token() {
|
||||
# 旧版 access_token,用于所有 oapi.dingtalk.com 接口:
|
||||
# - 群消息、工作通知、互动卡片(dingtalk-message)
|
||||
# - userId ↔ unionId 转换
|
||||
# ⚠️ 不可用于 api.dingtalk.com 接口(待办、文档、AI表格等)
|
||||
local force="${1:-}" app_key app_secret resp token cached expiry now
|
||||
|
||||
app_key=$(require_cfg DINGTALK_APP_KEY)
|
||||
app_secret=$(require_cfg DINGTALK_APP_SECRET)
|
||||
now=$(date +%s)
|
||||
|
||||
if [ "$force" != "--nocache" ]; then
|
||||
cached=$(cfg_get DINGTALK_OLD_TOKEN)
|
||||
expiry=$(cfg_get DINGTALK_OLD_TOKEN_EXPIRY)
|
||||
if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then
|
||||
echo "$cached"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
resp=$(curl -s "https://oapi.dingtalk.com/gettoken?appkey=${app_key}&appsecret=${app_secret}")
|
||||
token=$(echo "$resp" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
expires_in=$(echo "$resp" | grep -o '"expires_in":[0-9]*' | cut -d: -f2)
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "❌ 获取旧版 token 失败: $resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cfg_set DINGTALK_OLD_TOKEN "$token"
|
||||
cfg_set DINGTALK_OLD_TOKEN_EXPIRY "$((now + expires_in - 200))"
|
||||
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 身份转换
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_to_unionid() {
|
||||
local user_id="$1"
|
||||
local is_self=false
|
||||
local old_token resp union_id
|
||||
|
||||
# 未传参 → 使用配置中的操作者自身 userId,转换结果写入配置
|
||||
if [ -z "$user_id" ]; then
|
||||
user_id=$(require_cfg DINGTALK_MY_USER_ID)
|
||||
is_self=true
|
||||
fi
|
||||
|
||||
old_token=$(cmd_old_token)
|
||||
|
||||
resp=$(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}\"}")
|
||||
|
||||
# 注意:使用无下划线的 unionid 字段(有下划线的 union_id 可能为空)
|
||||
union_id=$(echo "$resp" | grep -o '"unionid":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$union_id" ]; then
|
||||
echo "❌ userId→unionId 转换失败: $resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 仅当转换的是操作者自身时,才写入配置(动态转换他人 userId 不写入)
|
||||
if "$is_self" && [ -z "$(cfg_get DINGTALK_MY_OPERATOR_ID)" ]; then
|
||||
cfg_set DINGTALK_MY_OPERATOR_ID "$union_id"
|
||||
echo "✅ 自身 unionId 已写入配置 DINGTALK_MY_OPERATOR_ID" >&2
|
||||
fi
|
||||
|
||||
echo "$union_id"
|
||||
}
|
||||
|
||||
cmd_to_userid() {
|
||||
local union_id="$1"
|
||||
local old_token resp user_id
|
||||
|
||||
if [ -z "$union_id" ]; then
|
||||
echo "❌ 请提供 unionId 参数" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
old_token=$(cmd_old_token)
|
||||
|
||||
resp=$(curl -s -X POST \
|
||||
"https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${old_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"unionid\":\"${union_id}\"}")
|
||||
|
||||
user_id=$(echo "$resp" | grep -o '"userid":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$user_id" ]; then
|
||||
echo "❌ unionId→userId 转换失败: $resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$user_id"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 配置管理
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
cmd_config() {
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "配置文件不存在: $CONFIG"
|
||||
echo "使用 --set KEY=VALUE 写入配置项"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "配置文件: $CONFIG"
|
||||
echo "─────────────────────────────────"
|
||||
# 脱敏显示 SECRET 和 TOKEN
|
||||
while IFS= read -r line; do
|
||||
key="${line%%=*}"
|
||||
val="${line#*=}"
|
||||
case "$key" in
|
||||
DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN)
|
||||
echo "${key}=${val:0:6}***(已脱敏)"
|
||||
;;
|
||||
*)
|
||||
echo "$line"
|
||||
;;
|
||||
esac
|
||||
done < "$CONFIG"
|
||||
}
|
||||
|
||||
cmd_get() {
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "❌ 请提供至少一个键名,用法: --get KEY [KEY2 ...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
for key in "$@"; do
|
||||
val=$(cfg_get "$key")
|
||||
if [ -z "$val" ]; then
|
||||
echo "${key}=(未设置)"
|
||||
else
|
||||
case "$key" in
|
||||
DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN)
|
||||
echo "${key}=${val:0:6}***(脱敏)"
|
||||
;;
|
||||
*)
|
||||
echo "${key}=${val}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_set() {
|
||||
local kv="$1"
|
||||
if [ -z "$kv" ] || [[ "$kv" != *"="* ]]; then
|
||||
echo "❌ 格式错误,用法: --set KEY=VALUE" >&2
|
||||
exit 1
|
||||
fi
|
||||
local key="${kv%%=*}"
|
||||
local value="${kv#*=}"
|
||||
cfg_set "$key" "$value"
|
||||
echo "✅ 已设置 ${key}"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 入口:解析命令
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CMD="${1:-}"
|
||||
|
||||
case "$CMD" in
|
||||
--help|-h|"")
|
||||
show_help
|
||||
;;
|
||||
--token)
|
||||
cmd_token "${2:-}"
|
||||
;;
|
||||
--token-info)
|
||||
cmd_token_info
|
||||
;;
|
||||
--clear-token)
|
||||
cmd_clear_token
|
||||
;;
|
||||
--old-token)
|
||||
cmd_old_token "${2:-}"
|
||||
;;
|
||||
--to-unionid)
|
||||
cmd_to_unionid "${2:-}"
|
||||
;;
|
||||
--to-userid)
|
||||
cmd_to_userid "${2:-}"
|
||||
;;
|
||||
--config)
|
||||
cmd_config
|
||||
;;
|
||||
--get)
|
||||
shift
|
||||
cmd_get "$@"
|
||||
;;
|
||||
--set)
|
||||
cmd_set "${2:-}"
|
||||
;;
|
||||
*)
|
||||
echo "❌ 未知命令: $CMD" >&2
|
||||
echo "运行 --help 查看用法" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
86
skills/dingtalk-log/SKILL.md
Normal file
86
skills/dingtalk-log/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 钉钉工作日志查询
|
||||
|
||||
查询钉钉工作日志(周报/日报等)。
|
||||
|
||||
## 接口信息
|
||||
|
||||
- **接口地址**: `POST https://oapi.dingtalk.com/topapi/report/list`
|
||||
- **认证方式**: access_token(基于企业内部应用)
|
||||
- **时间单位**: ⚠️ **毫秒**(不是秒!)
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 获取 access_token:
|
||||
```bash
|
||||
curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"appKey": "dingklemniq8uqk5qbgx",
|
||||
"appSecret": "_8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8"
|
||||
}'
|
||||
```
|
||||
|
||||
2. 凭证存储在 `TOOLS.md`:
|
||||
- AgentId: 4404185308
|
||||
- Client ID (AppKey): dingklemniq8uqk5qbgx
|
||||
- Client Secret (AppSecret): _8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8
|
||||
|
||||
## 调用示例
|
||||
|
||||
```bash
|
||||
# 查询指定用户的日志(时间单位:毫秒)
|
||||
curl -s 'https://oapi.dingtalk.com/topapi/report/list?access_token={TOKEN}' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"userid": "121922510028034588",
|
||||
"offset": 0,
|
||||
"size": 10,
|
||||
"start_time": 1738329600000,
|
||||
"end_time": 1743292800000,
|
||||
"cursor": 0
|
||||
}'
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| userid | string | ✅ | 用户的钉钉 userid |
|
||||
| start_time | number | ✅ | 开始时间,**毫秒**时间戳 |
|
||||
| end_time | number | ✅ | 结束时间,**毫秒**时间戳 |
|
||||
| cursor | number | ✅ | 分页游标(首次查询传 0) |
|
||||
| size | number | ✅ | 每页数量(建议 10) |
|
||||
| offset | number | ❌ | 偏移量(兼容旧版,可不传) |
|
||||
|
||||
## 返回字段说明
|
||||
|
||||
日志内容在 `result.data_list` 数组中,每个元素的 `contents` 数组包含各字段:
|
||||
|
||||
| contents.key | 说明 |
|
||||
|-------------|------|
|
||||
| 本周完成工作 / 本日完成工作 | 完成事项 |
|
||||
| 本周工作总结 / 本日工作总结 | 详细总结 |
|
||||
| 下周工作计划 / 下日工作计划 | 后续计划 |
|
||||
| 需协调与帮助 | 协调事项 |
|
||||
| 图片 | 图片列表 |
|
||||
| 附件 | 附件列表 |
|
||||
|
||||
其他字段:
|
||||
- `report_id`: 日志ID
|
||||
- `template_name`: 日志模板名称(如"经理人周报")
|
||||
- `creator_name`: 创建人姓名
|
||||
- `dept_name`: 部门名称
|
||||
- `create_time`: 创建时间(毫秒)
|
||||
|
||||
## 分页查询
|
||||
|
||||
通过 `cursor` 分页:
|
||||
1. 首次查询 `cursor: 0`
|
||||
2. 返回 `next_cursor` 作为下次查询的游标
|
||||
3. `has_more: false` 表示最后一页
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. ⚠️ **时间单位必须是毫秒**:如 `1743292800000`(不能用 `1743292800`)
|
||||
2. ⚠️ **字段名是 `userid`(全小写)**,不是 `userId`
|
||||
3. 部分日志内容可能超长被截断,返回可能不完整
|
||||
54
skills/dingtalk-log/scripts/dt_log.sh
Normal file
54
skills/dingtalk-log/scripts/dt_log.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# 钉钉工作日志查询脚本
|
||||
|
||||
# 配置(从 TOOLS.md 读取)
|
||||
APP_KEY="dingklemniq8uqk5qbgx"
|
||||
APP_SECRET="_8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8"
|
||||
API_BASE="https://oapi.dingtalk.com"
|
||||
|
||||
# 获取 access_token
|
||||
get_token() {
|
||||
curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"appKey\":\"$APP_KEY\",\"appSecret\":\"$APP_SECRET\"}" | \
|
||||
grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4
|
||||
}
|
||||
|
||||
# 查询日志
|
||||
query_logs() {
|
||||
local userid=$1
|
||||
local start_time=$2 # 毫秒
|
||||
local end_time=$3 # 毫秒
|
||||
local size=${4:-10}
|
||||
local cursor=${5:-0}
|
||||
local token=$6
|
||||
|
||||
curl -s "$API_BASE/topapi/report/list?access_token=$token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{
|
||||
\"userid\": \"$userid\",
|
||||
\"start_time\": $start_time,
|
||||
\"end_time\": $end_time,
|
||||
\"cursor\": $cursor,
|
||||
\"size\": $size
|
||||
}"
|
||||
}
|
||||
|
||||
# 解析毫秒时间戳为日期
|
||||
ms_to_date() {
|
||||
local ms=$1
|
||||
date -d @$((ms / 1000)) "+%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
|
||||
# 帮助信息
|
||||
usage() {
|
||||
echo "钉钉工作日志查询"
|
||||
echo ""
|
||||
echo "用法: $0 <userid> <start_time_ms> <end_time_ms> [size] [cursor]"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 121922510028034588 1738329600000 1743292800000"
|
||||
echo " $0 121922510028034588 1738329600000 1743292800000 20 0"
|
||||
echo ""
|
||||
echo "时间戳获取: date +%s000000 (mac/linux)"
|
||||
}
|
||||
Reference in New Issue
Block a user