mirror of
https://github.com/breath57/dingtalk-skills.git
synced 2026-09-14 20:51:31 +08:00
refactor(dingtalk-ai-table): 大幅度优化ai-table skill的流程
This commit is contained in:
@@ -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` | 当前用户的企业员工 ID(userId) | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看(不是手机号、不是 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,后续无需再问。
|
||||
|
||||
---
|
||||
|
||||
## 为什么需要 operatorId(unionId)
|
||||
|
||||
钉钉开放平台要求所有**写操作**必须代表一个真实用户身份执行,而不是以匿名应用身份操作。`operatorId` 就是声明"这个操作是谁做的"——它会被记录到变更日志、触发对应用户的通知,并用于权限校验。
|
||||
|
||||
- AI 表格 API 的 `operatorId` 参数使用 **unionId**(跨组织唯一),**不是** userId
|
||||
- 配置中优先收集 `userId`(管理后台直接查看),系统自动转换为 `unionId`
|
||||
### 所需配置
|
||||
| 配置键 | 必填 | 说明 | 如何获取 |
|
||||
|---|---|---|---|
|
||||
| `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` 自动转换并写入 |
|
||||
| `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 返回 401(token 无效/过期),用 `--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
@@ -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
|
||||
Reference in New Issue
Block a user