refactor(dingtalk-ai-table): 大幅度优化ai-table skill的流程

This commit is contained in:
breath57
2026-03-16 20:08:28 +08:00
parent 75d2389070
commit 7b5d0a0b92
3 changed files with 577 additions and 429 deletions
+66 -359
View File
@@ -5,375 +5,82 @@ description: 钉钉 AI 表格(多维表格)操作。当用户提到"钉钉AI
# 钉钉 AI 表格技能
负责钉钉 AI 表格(`.able` 格式多维表格)的所有操作,通过钉钉开放平台 Notable API 实现
负责钉钉 AI 表格(`.able` 格式多维表格)的所有操作。本文件为**策略指南**,仅包含决策逻辑和工作流程。完整 API 请求格式见文末「references/api.md 查阅索引」
**核心概念:**
## 核心概念
- **AI 表格**`.able` 文件):多维表格,使用 Notable API`/v1.0/notable`**不是**普通电子表格
- **base_id**AI 表格文件的 nodeId是表格在钉钉文档系统中的唯一标识
- **工作表Sheet**AI 表格内的单张表,包含字段和记录
- **字段Field**:列定义,有名称和类型(`text``number``date`
- **记录Record**:数据行,包含各字段的值
- **base_id**AI 表格文件的 nodeId从分享链接 `https://alidocs.dingtalk.com/i/nodes/<base_id>` 提取
- **工作表Sheet**:表格内的单张表,包含字段和记录
- **字段Field**:列定义,有名称和类型(`text``number``date`
- **记录Record**:数据行,`fields` 中用**字段名称**(非 ID作键
- **operatorId**:所有接口必须的 unionId 参数,通过 `dt_helper.sh --to-unionid` 自动转换
API 详情见 `references/api.md`
## 工作流程(每次执行前)
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`
- **涉及任何 AI 表格 API 调用**:必须有 `DINGTALK_MY_OPERATOR_ID`(若缺失,先用 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写回)
- **工作表/字段/记录相关操作**:必须有 `DINGTALK_AI_TABLE_BASE_ID`(若缺失,要求用户提供 AI 表格链接并提取 `/nodes/<base_id>`
## 配置管理(每次开始前必读)
> 规则:未通过“本次任务配置校验”前,不得进入 API 调用步骤。
### 配置文件路径
> 凭证禁止在输出中完整打印,确认时仅显示前 4 位 + `****`
`~/.dingtalk-skills/config`(跨会话保留,所有 dingtalk-skills 共用同一文件)
### 本技能需要的配置说明
| 键 | 说明 | 来源 |
|---|---|---|
| `DINGTALK_APP_KEY` | 钉钉应用 appKey | 开放平台 → 应用管理 → 凭证信息 |
| `DINGTALK_APP_SECRET` | 钉钉应用 appSecret | 同上 |
| `DINGTALK_MY_USER_ID` | 当前用户的企业员工 IDuserId | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看(不是手机号、不是 unionId |
| `DINGTALK_MY_OPERATOR_ID` | 当前用户的 unionId | 首次由脚本自动通过 userId 转换获取并写入 |
| `DINGTALK_AI_TABLE_BASE_ID` | AI 表格的 nodeId | 从 AI 表格分享链接提取 |
### 启动流程(每次执行任务前)
1. **读取配置**:检查 `~/.dingtalk-skills/config` 是否存在,解析已有键值
2. **识别缺失项**:找出上表中尚未配置的键
3. **一次性收集**:将所有缺失项合并为一条提问,**不要逐条询问**,例如:
> 需要以下信息才能继续(已有的无需再填):
> - 钉钉应用 appKey钉钉开放平台 → 应用管理 → 凭证信息)
> - 钉钉应用 appSecret
> - AI 表格链接(用于提取 base_id
> - 你的钉钉 userId管理后台 → 通讯录 → 成员管理 → 点击姓名查看)
4. **持久化**:将用户提供的值追加写入 config后续直接读取无需再问
5. **执行任务**:配置完整后开始操作
> **注意**`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>" }
```
所有请求需携带:
- 请求头:`x-acs-dingtalk-access-token: <accessToken>`
- 查询参数:`operatorId=<用户 unionId>`(所有写操作及部分读操作必须)
---
## 为什么需要 base_id
钉钉文档系统中每个文件文档、表格、AI 表格)都有一个全局唯一的 **nodeId**,即 `base_id`。它的作用类似数据库主键——API 通过它定位到具体是哪个 AI 表格文件,因为账号下可能有多个 `.able` 文件。
**从链接提取 base_id**
```
https://alidocs.dingtalk.com/i/nodes/<base_id>?...
↑ 这一段就是 base_id
```
请用户提供 AI 表格的分享链接,从 `/nodes/` 后截取 ID 片段。首次获取后写入 config后续无需再问。
---
## 为什么需要 operatorIdunionId
钉钉开放平台要求所有**写操作**必须代表一个真实用户身份执行,而不是以匿名应用身份操作。`operatorId` 就是声明"这个操作是谁做的"——它会被记录到变更日志、触发对应用户的通知,并用于权限校验。
- AI 表格 API 的 `operatorId` 参数使用 **unionId**(跨组织唯一),**不是** userId
- 配置中优先收集 `userId`(管理后台直接查看),系统自动转换为 `unionId`
### 所需配置
| 配置键 | 必填 | 说明 | 如何获取 |
|---|---|---|---|
| `DINGTALK_APP_KEY` | ✅ | 应用 AppKey | 钉钉开放平台 → 应用管理 → 凭证信息 |
| `DINGTALK_APP_SECRET` | ✅ | 应用 AppSecret | 同上 |
| `DINGTALK_MY_USER_ID` | ✅ | 当前用户的企业员工 IDuserId | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看 |
| `DINGTALK_MY_OPERATOR_ID` | ✅ | 当前用户的 unionIdoperatorId | 首次由 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写入 |
| `DINGTALK_AI_TABLE_BASE_ID` | ✅ | AI 表格的 nodeId | 从 AI 表格分享链接 `/nodes/<id>` 提取 |
### 身份标识说明
| 标识 | 说明 | 如何获取 |
|---|---|---|
| `userId`= `staffId` | 企业内部员工 ID**最容易获取** | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看 |
| `unionId` | 跨企业/跨应用唯一 | 通过 userId 调用 API 转换获取 |
### userId → unionId 自动转换
当配置中有 `DINGTALK_MY_USER_ID` 但缺少 `DINGTALK_MY_OPERATOR_ID` 时,自动执行转换:
```bash
# 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/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
x-acs-dingtalk-access-token: <accessToken>
```
返回:
```json
{
"value": [
{ "id": "HAcL4SD", "name": "项目" },
{ "id": "nr2iEiW", "name": "任务" }
]
}
```
---
### 2. 查询单个工作表
```
GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
```
返回:`{ "id": "HAcL4SD", "name": "项目" }`
---
### 3. 新建工作表
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
Content-Type: application/json
{
"name": "新工作表名称",
"fields": [
{ "name": "标题", "type": "text" },
{ "name": "数量", "type": "number" }
]
}
```
`fields` 可选(不传则创建空工作表)。
返回:`{ "id": "新sheetId", "name": "新工作表名称" }`
---
### 4. 删除工作表
```
DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
```
返回:`{ "success": true }`
⚠️ 不可恢复,执行前需用户确认。
---
### 5. 列出字段
```
GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
```
返回:
```json
{
"value": [
{ "id": "6mNRNHb", "name": "标题", "type": "text" },
{ "id": "BDGLCo2", "name": "截止日期", "type": "date", "property": { "formatter": "YYYY-MM-DD" } },
{ "id": "mr8APlG", "name": "数量", "type": "number", "property": { "formatter": "INT" } }
]
}
```
---
### 6. 新建字段
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
Content-Type: application/json
{
"name": "字段名称",
"type": "number"
}
```
`type` 常用值:`text`(文本)、`number`(数字)、`date`(日期)
返回:`{ "id": "新fieldId", "name": "字段名称", "type": "number", "property": { "formatter": "INT" } }`
---
### 7. 更新字段
```
PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
Content-Type: application/json
{
"name": "新字段名称"
}
```
返回:`{ "id": "fieldId" }`(通过重新查询列表确认名称已变更)
---
### 8. 删除字段
```
DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
```
返回:`{ "success": true }`
⚠️ 删除字段会同时删除该列所有数据,执行前需用户确认。
---
### 9. 新增记录(最常用操作)
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json
{
"records": [
{ "fields": { "标题": "任务一", "数量": 3 } },
{ "fields": { "标题": "任务二", "数量": 5 } }
]
}
```
`fields` 中使用**字段名称**(非 ID作为键。
返回:`{ "value": [{ "id": "记录ID1" }, { "id": "记录ID2" }] }`
---
### 10. 查询记录列表
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/list?operatorId={operatorId}
Content-Type: application/json
{
"maxResults": 20,
"nextToken": ""
}
```
返回:
```json
{
"records": [
{
"id": "RNXU1Vm2L2",
"fields": { "标题": "任务一", "数量": 3 },
"createdTime": 1772723541439,
"createdBy": { "unionId": "xxx" },
"lastModifiedTime": 1772723541439,
"lastModifiedBy": { "unionId": "xxx" }
}
],
"hasMore": false,
"nextToken": ""
}
```
翻页:上次响应 `hasMore=true` 时,将 `nextToken` 传入下次请求。
---
### 11. 更新记录
```
PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json
{
"records": [
{ "id": "记录ID", "fields": { "标题": "新标题" } }
]
}
```
返回:`{ "value": [{ "id": "记录ID" }] }`
只传需要修改的字段,未传字段保持不变。
---
### 12. 删除记录
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/delete?operatorId={operatorId}
Content-Type: application/json
{
"recordIds": ["记录ID1", "记录ID2"]
}
```
返回:`{ "success": true }`
---
## 典型场景
### "查看 AI 表格里有哪些工作表"
1. 询问 AI 表格链接,提取 base_id
2. `GET /sheets` 获取工作表列表
3. 展示工作表名称和 ID
### "往 AI 表格的'任务'工作表添加几条记录"
1. `GET /sheets` 找到目标工作表的 sheet_id
2. `GET /fields` 了解现有字段名称和类型
3. `POST /records` 批量插入fields 中用字段名称作键
4. 告知写入成功及记录 ID
### "查询'任务'表中所有记录"
1. `GET /sheets` 找到 sheet_id
2. `POST /records/list` 翻页获取所有记录
3. 整理为表格形式展示
### "删除某条记录"
1.`POST /records/list` 定位目标记录(让用户确认)
2. `POST /records/delete` 传入 recordId
3. 告知删除成功
### "给工作表新增一个'备注'文本字段"
1. `GET /sheets` 找到 sheet_id
2. `POST /fields``{ "name": "备注", "type": "text" }`
3. 返回新字段 ID 并告知成功
---
## 字段类型参考
| type | 含义 |
| 标识 | 说明 |
|---|---|
| `text` | 纯文本 |
| `number` | 数字(含 `property.formatter``INT``PERCENT` 等) |
| `date` | 日期(含 `property.formatter`:如 `YYYY-MM-DD` |
| `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)
BASE_ID=$(bash "$HELPER" --get DINGTALK_AI_TABLE_BASE_ID)
## 错误处理
# 在此追加具体 API 调用,例如列出工作表:
SHEETS=$(curl -s -X GET "https://api.dingtalk.com/v1.0/notable/bases/${BASE_ID}/sheets?operatorId=${OPERATOR_ID}" \
-H "x-acs-dingtalk-access-token: $NEW_TOKEN")
echo "工作表列表: $SHEETS"
```
| HTTP 状态码 / code | 含义 | 处理方式 |
|---|---|---|
| 401 | token 过期 | 重新获取 accessToken |
| 403 | 权限不足 | 检查应用是否开通 Notable 相关权限 |
| `invalidRequest.document.notFound` | base_id 无效或无权访问 | 确认 AI 表格 nodeId 正确且已授权 |
| 404 | 工作表或字段不存在 | 确认 sheet_id / field_id 正确 |
| 429 | 触发限流 | 等待 1 秒后重试 |
> **Token 失效处理**dt_helper 仅按时间缓存,无法感知 token 被提前吊销。若 API 返回 401token 无效/过期),用 `--nocache` 跳过缓存强制重新获取:
> ```bash
> NEW_TOKEN=$(bash "$HELPER" --token --nocache)
> ```
## references/api.md 查阅索引
确定好要做什么之后,用以下命令从 `references/api.md` 中提取对应章节的完整 API 细节(请求格式、参数说明、返回值示例):
```bash
grep -A 20 "^## 1. 列出工作表" references/api.md
grep -A 15 "^## 2. 查询单个工作表" references/api.md
grep -A 30 "^## 3. 创建工作表" references/api.md
grep -A 15 "^## 4. 删除工作表" references/api.md
grep -A 25 "^## 5. 列出字段" references/api.md
grep -A 28 "^## 6. 创建字段" references/api.md
grep -A 15 "^## 7. 更新字段" references/api.md
grep -A 15 "^## 8. 删除字段" references/api.md
grep -A 25 "^## 9. 新增记录" references/api.md
grep -A 40 "^## 10. 查询记录列表" references/api.md
grep -A 18 "^## 11. 更新记录" references/api.md
grep -A 15 "^## 12. 删除记录" references/api.md
grep -A 10 "^## 错误码" references/api.md
grep -A 6 "^## 所需应用权限" references/api.md
```
@@ -1,26 +1,22 @@
# 钉钉 AI 表格 API 参考
# dingtalk-ai-table API 参考
> **重要:** 钉钉 AI 表格(`.able` 文件)使用 **Notable API**
> 路径前缀为 `/v1.0/notable`,与普通电子表格 API`/v1.0/doc/workbooks`)完全不同。
基础地址:`https://api.dingtalk.com/v1.0/notable`
认证:
- 请求头:`x-acs-dingtalk-access-token: <accessToken>`
- 所有接口均需查询参数:`operatorId=<用户 unionId>`
`{base_id}` = AI 表格文件的 **nodeId**(从分享链接 `/nodes/<nodeId>` 提取)
> 所有接口均已验证可用。钉钉 AI 表格(`.able` 文件)使用 **Notable API**与普通电子表格 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`
> `{base_id}` = AI 表格文件的 nodeId从分享链接 `/nodes/<nodeId>` 提取)
---
## 工作表Sheet
## 1. 列出工作表
### 查询所有工作表
```
GET /notable/bases/{base_id}/sheets?operatorId={operatorId}
GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
返回示例:
无请求体。
响应:
```json
{
"value": [
@@ -32,23 +28,31 @@ GET /notable/bases/{base_id}/sheets?operatorId={operatorId}
---
### 查询单个工作表
## 2. 查询单个工作表
```
GET /notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
返回示例:
无请求体。
响应:
```json
{ "id": "HAcL4SD", "name": "项目" }
```
---
### 创建工作表
```
POST /notable/bases/{base_id}/sheets?operatorId={operatorId}
Content-Type: application/json
## 3. 创建工作表
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{
"name": "新工作表",
"fields": [
@@ -58,28 +62,43 @@ Content-Type: application/json
}
```
`fields` 为可选,省略则创建空工作表。
返回:`{ "id": "zHTWNlh", "name": "新工作表" }`
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | ✅ | 工作表名称 |
| `fields` | array | ❌ | 初始字段定义,省略则创建空工作表 |
响应:
```json
{ "id": "zHTWNlh", "name": "新工作表" }
```
---
### 删除工作表
## 4. 删除工作表
```
DELETE /notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
返回:`{ "success": true }`
无请求体。
响应:`{ "success": true }`
> ⚠️ 不可恢复,执行前需用户确认。
---
## 字段Field
## 5. 列出字段
### 查询所有字段
```
GET /notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
返回示例:
无请求体。
响应:
```json
{
"value": [
@@ -92,19 +111,27 @@ GET /notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
---
### 创建字段
```
POST /notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
Content-Type: application/json
## 6. 创建字段
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{
"name": "字段名称",
"type": "number"
}
```
常用 `type` 值:`text``number``date`
返回示例:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | ✅ | 字段名称 |
| `type` | string | ✅ | 字段类型:`text`(文本)、`number`(数字)、`date`(日期) |
响应:
```json
{
"id": "mr8APlG",
@@ -116,34 +143,48 @@ Content-Type: application/json
---
### 更新字段
```
PUT /notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
Content-Type: application/json
## 7. 更新字段
```
PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{ "name": "新字段名称" }
```
返回`{ "id": "fieldId" }`(仅返回 id通过 GET /fields 确认名称变更)
响应`{ "id": "fieldId" }`
> 仅返回 id通过重新查询字段列表确认名称已变更。
---
### 删除字段
## 8. 删除字段
```
DELETE /notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
返回:`{ "success": true }`
无请求体。
响应:`{ "success": true }`
> ⚠️ 删除字段会同时删除该列所有数据,执行前需用户确认。
---
## 记录Record
## 9. 新增记录
### 新增记录
```
POST /notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{
"records": [
{ "fields": { "标题": "任务一", "数量": 3 } },
@@ -152,8 +193,9 @@ Content-Type: application/json
}
```
`fields` 中使用**字段名称**(非 ID作为键。
返回示例:
> `fields` 中使用**字段名称**(非 ID作为键。先用「5. 列出字段」确认现有字段名。
响应:
```json
{
"value": [
@@ -165,18 +207,27 @@ Content-Type: application/json
---
### 查询记录列表
```
POST /notable/bases/{base_id}/sheets/{sheet_id}/records/list?operatorId={operatorId}
Content-Type: application/json
## 10. 查询记录列表
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/list?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{
"maxResults": 20,
"nextToken": ""
}
```
返回示例:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `maxResults` | int | ❌ | 每页数量,默认 20 |
| `nextToken` | string | ❌ | 分页令牌,首次为空 |
响应:
```json
{
"records": [
@@ -184,9 +235,9 @@ Content-Type: application/json
"id": "RNXU1Vm2L2",
"fields": { "标题": "任务一", "数量": 3 },
"createdTime": 1772723541439,
"createdBy": { "unionId": "K1mxiiGFgkVfWYR5tNM04lAiEiE" },
"createdBy": { "unionId": "xxx" },
"lastModifiedTime": 1772723541439,
"lastModifiedBy": { "unionId": "K1mxiiGFgkVfWYR5tNM04lAiEiE" }
"lastModifiedBy": { "unionId": "xxx" }
}
],
"hasMore": false,
@@ -194,15 +245,19 @@ Content-Type: application/json
}
```
翻页:`hasMore=true` 时,将 `nextToken` 传入下次请求继续获取。
> 翻页:`hasMore=true` 时,将 `nextToken` 传入下次请求继续获取。
---
### 更新记录
```
PUT /notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json
## 11. 更新记录
```
PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{
"records": [
{ "id": "RNXU1Vm2L2", "fields": { "标题": "新标题" } }
@@ -210,20 +265,25 @@ Content-Type: application/json
}
```
只传需要修改的字段,未传字段保持不变。
返回:`{ "value": [{ "id": "RNXU1Vm2L2" }] }`
> 只传需要修改的字段,未传字段保持不变。
响应:`{ "value": [{ "id": "RNXU1Vm2L2" }] }`
---
### 删除记录
```
POST /notable/bases/{base_id}/sheets/{sheet_id}/records/delete?operatorId={operatorId}
Content-Type: application/json
## 12. 删除记录
```
POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/delete?operatorId={OPERATOR_ID}
Header: x-acs-dingtalk-access-token: {NEW_TOKEN}
```
请求体:
```json
{ "recordIds": ["RNXU1Vm2L2", "LK0kdIxCQU"] }
```
返回`{ "success": true }`
响应`{ "success": true }`
---
+381
View 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 IDAppKey
DINGTALK_APP_SECRET 应用 Client SecretAppSecret
DINGTALK_MY_USER_ID 企业员工 IDuserId管理后台通讯录可查
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