
Dingtalk Calendar
- 241 installs
- 97 repo stars
- Updated June 26, 2026
- breath57/dingtalk-skills
Create, read, and update DingTalk calendar events from agent workflows for enterprise teams that schedule meetings and reminders inside DingTalk.
About
The dingtalk-calendar skill helps developers integrate DingTalk’s enterprise calendar into automated workflows: listing events, creating meetings, updating times, and aligning reminders with DingTalk’s API expectations. It is aimed at teams using DingTalk for internal scheduling who want agents or backend jobs to manage calendars without manual UI steps or guessing payload formats.
- DingTalk calendar event create and update flows
- Enterprise scheduling inside agent automations
- Handles calendar-specific API fields and constraints
- Useful for China-based team coordination
- Pairs with other DingTalk skills in the repo
Dingtalk Calendar by the numbers
- 241 all-time installs (skills.sh)
- Ranked #537 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/breath57/dingtalk-skills --skill dingtalk-calendarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 241 |
|---|---|
| repo stars | ★ 97 |
| Last updated | June 26, 2026 |
| Repository | breath57/dingtalk-skills ↗ |
What it does
Create, read, and update DingTalk calendar events from agent workflows for enterprise teams that schedule meetings and reminders inside DingTalk.
Files
钉钉日程技能
负责钉钉日历(Calendar)API 的操作。本文件为策略指南;完整请求格式见 references/api.md。
dt_helper.sh位于本SKILL.md同级目录的scripts/dt_helper.sh。
核心概念
- 路径中的 `userId`:日程 API 路径
/v1.0/calendar/users/{userId}/...中的{userId}为 unionId(与待办、文档一致),不是 staffId。 - 主日历:个人默认日历的
calendarId固定使用字符串 `primary`(小写)。创建/查询/列表/更新/删除均针对.../calendars/primary/events...。 - 时间格式:
start/end、闲忙的startTime/endTime、列表的timeMin/timeMax须使用 UTC ISO8601 且含毫秒,例如2026-03-24T07:02:48.000Z。省略毫秒易触发ParsedISO8601TimestampError。 - 修改日程:HTTP 方法为 PUT(与「部分更新」语义对应的路径相同),请求体需包含日程
id及要改的字段(如summary)。 - 视频会议:创建日程时在请求体中加
"onlineMeetingInfo":{"type":"dingtalk"},响应含onlineMeetingInfo.url等。 - 会议室:先通过 会议室忙闲 接口按
roomIds+ 时间窗查询;再在已有日程上 添加会议室(需企业内会议室roomId,管理后台或开放平台可查)。集成测试可用环境变量TEST_MEETING_ROOM_IDS(逗号分隔)。 - 签到/签退:日程创建后,可 GET 签到/签退链接 分发参会人;组织者/参与者 POST 签到;详情用 GET signin / signOut 列表接口(见 api.md)。是否与线下会议、审批流联动以钉钉侧能力为准。
场景路由(先分类再调 API)
| 用户意图 | 优先接口方向 |
|---|---|
| 订会议室、查会议室有没有空 | POST .../meetingRooms/schedules/query |
| 给已有日程加会议室 | POST .../events/{eventId}/meetingRooms |
| 要签到码、签退链接 | GET .../signInLinks、GET .../signOutLinks |
| 每周重复、每天重复 | 创建日程时带 recurrence(见 api.md) |
| 只看人忙闲(不针对会议室) | POST .../querySchedule |
工作流程(每次执行前)
1. 识别任务 → 按上表归类后,再选具体 API(见 references/api.md)。 2. 校验配置 → bash scripts/dt_helper.sh --get 读取 DINGTALK_APP_KEY、DINGTALK_APP_SECRET、DINGTALK_MY_USER_ID、DINGTALK_MY_OPERATOR_ID(缺 unionId 时 --to-unionid)。 3. 收集缺失项 → 一次性询问并 --set 写入 ~/.dingtalk-skills/config。 4. 获取新版 Token → NEW_TOKEN=$(bash scripts/dt_helper.sh --token),请求头 x-acs-dingtalk-access-token。 5. 执行 API → 多行逻辑写入 /tmp/<task>.sh 再执行;禁止 heredoc。
按任务校验配置
- 通用必需:
DINGTALK_APP_KEY、DINGTALK_APP_SECRET、DINGTALK_MY_USER_ID;调用前需 unionId(DINGTALK_MY_OPERATOR_ID或通过--to-unionid生成)。
未通过校验前不得调用 API。凭证展示仅前 4 位 + ****。所需配置
| 配置键 | 必填 | 说明 |
|---|---|---|
DINGTALK_APP_KEY | ✅ | Client ID(AppKey) |
DINGTALK_APP_SECRET | ✅ | Client Secret |
DINGTALK_MY_USER_ID | ✅ | 当前用户 userId(管理后台通讯录) |
DINGTALK_MY_OPERATOR_ID | ✅ | 当前用户 unionId(--to-unionid 可写入) |
身份标识说明
| 标识 | 说明 |
|---|---|
userId | 企业员工 ID,管理后台可见 |
unionId | 日程路径参数与 body 中的用户标识均使用 unionId |
userId → unionId:使用旧版 access_token 调 POST https://oapi.dingtalk.com/topapi/v2/user/get(见 references/api.md),取 result.unionid(无下划线)。
执行脚本模板
#!/bin/bash
set -e
HELPER="./scripts/dt_helper.sh"
NEW_TOKEN=$(bash "$HELPER" --token)
UNION_ID=$(bash "$HELPER" --get DINGTALK_MY_OPERATOR_ID)
CAL_ID="primary"
curl -s -X POST "https://api.dingtalk.com/v1.0/calendar/users/${UNION_ID}/calendars/${CAL_ID}/events" \
-H "x-acs-dingtalk-access-token: $NEW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"summary":"周会","start":{"dateTime":"2026-03-25T02:00:00.000Z","timeZone":"UTC"},"end":{"dateTime":"2026-03-25T03:00:00.000Z","timeZone":"UTC"}}'Token 异常时:bash "$HELPER" --token --nocachereferences/api.md 查阅索引
grep -A 35 "^## 1. 创建日程" references/api.md
grep -A 25 "^## 2. 查询单个日程" references/api.md
grep -A 30 "^## 3. 查询日程列表" references/api.md
grep -A 25 "^## 4. 更新日程" references/api.md
grep -A 15 "^## 5. 删除日程" references/api.md
grep -A 28 "^## 6. 查询闲忙" references/api.md
grep -A 22 "^## 7. 视频会议" references/api.md
grep -A 28 "^## 8. 查询会议室忙闲" references/api.md
grep -A 25 "^## 9. 添加与移除会议室" references/api.md
grep -A 18 "^## 10. 签到与签退链接" references/api.md
grep -A 22 "^## 11. 签到与签退详情列表" references/api.md
grep -A 18 "^## 12. 签到与签退操作" references/api.md
grep -A 35 "^## 13. 循环日程" references/api.md
grep -A 15 "^## 14. 订阅日历" references/api.md
grep -A 15 "^## 错误码" references/api.md
grep -A 18 "^## 所需应用权限" references/api.md钉钉日程(Calendar)API 参考
基础 URL:https://api.dingtalk.com公共请求头:x-acs-dingtalk-access-token: <新版 accessToken>、Content-Type: application/json
路径中的 {unionId} 为操作者或目标用户的 unionId(不是 staffId)。主日历 ID 使用 `primary`。---
身份标识与 userId → unionId
日程接口路径 .../users/{unionId}/... 使用 unionId。若仅有 userId,使用旧版 token:
GET https://oapi.dingtalk.com/gettoken?appkey=<AppKey>&appsecret=<AppSecret>
POST https://oapi.dingtalk.com/topapi/v2/user/get?access_token=<旧版token>
Body: {"userid":"<userId>"}
→ result.unionid(无下划线字段)---
1. 创建日程
POST /v1.0/calendar/users/{unionId}/calendars/primary/events
请求体
{
"summary": "项目周会",
"description": "讨论排期",
"start": {
"dateTime": "2026-03-25T02:00:00.000Z",
"timeZone": "UTC"
},
"end": {
"dateTime": "2026-03-25T03:00:00.000Z",
"timeZone": "UTC"
}
}| 字段 | 必填 | 说明 |
|---|---|---|
summary | ✅ | 标题 |
start / end | ✅ | dateTime 为 UTC ISO8601 含毫秒;timeZone 如 UTC |
响应(节选)
{
"id": "xxxxxxxx",
"summary": "项目周会",
"start": { "dateTime": "2026-03-25T02:00:00Z", "timeZone": "UTC" },
"end": { "dateTime": "2026-03-25T03:00:00Z", "timeZone": "UTC" },
"requestId": "..."
}---
2. 查询单个日程
GET /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}
可选 Query:maxAttendees
响应
返回完整事件对象,含 id、summary、start、end、organizer 等。
---
3. 查询日程列表
GET /v1.0/calendar/users/{unionId}/calendars/primary/events
Query 参数
| 参数 | 说明 |
|---|---|
timeMin | 范围开始(UTC ISO8601,建议含毫秒) |
timeMax | 范围结束 |
maxResults | 每页条数,如 50 |
nextToken | 分页 |
响应(节选)
{
"events": [
{
"id": "...",
"summary": "...",
"start": { "dateTime": "...", "timeZone": "UTC" },
"end": { "dateTime": "...", "timeZone": "UTC" }
}
],
"nextToken": null
}---
4. 更新日程
PUT /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}
(SDK 中方法名为 PatchEvent,HTTP 为 PUT。)
请求体
{
"id": "<与路径中 eventId 相同>",
"summary": "项目周会(已改)"
}可携带 start/end/description 等字段做修改。
响应
返回更新后的完整事件对象。
---
5. 删除日程
DELETE /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}
可选 Query:pushNotification(boolean)
响应
{ "requestId": "..." }---
6. 查询闲忙
POST /v1.0/calendar/users/{unionId}/querySchedule
请求体
{
"startTime": "2026-03-24T00:00:00.000Z",
"endTime": "2026-03-25T00:00:00.000Z",
"userIds": ["<unionId1>", "<unionId2>"]
}| 字段 | 说明 |
|---|---|
startTime / endTime | UTC ISO8601 含毫秒 |
userIds | 要查询闲忙的用户的 unionId 列表 |
---
7. 视频会议(钉钉会议)
创建日程时在请求体中增加:
"onlineMeetingInfo": { "type": "dingtalk" }成功时响应含 onlineMeetingInfo.url、conferenceId、extraInfo(如网页入会链接、会议号)等。
---
8. 查询会议室忙闲
POST /v1.0/calendar/users/{unionId}/meetingRooms/schedules/query
请求体
{
"startTime": "2026-03-24T00:00:00.000Z",
"endTime": "2026-03-25T00:00:00.000Z",
"roomIds": ["<会议室 roomId>", "<roomId2>"]
}| 字段 | 说明 |
|---|---|
startTime / endTime | UTC ISO8601,建议含毫秒(与闲忙接口一致) |
roomIds | 会议室 ID 列表(企业内会议室资源 ID,非 unionId) |
返回各会议室在时间窗内的占用情况(结构以实际响应为准)。
---
9. 添加与移除会议室
添加 — POST /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/meetingRooms
{
"meetingRoomsToAdd": [{ "roomId": "<会议室ID>" }]
}批量移除 — POST /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/meetingRooms/batchRemove
{
"meetingRoomsToRemove": [{ "roomId": "<会议室ID>" }]
}---
10. 签到与签退链接
GET /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signInLinks GET /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signOutLinks
响应(节选)
{ "signInLink": "https://..." }{ "signOutLink": "https://..." }---
11. 签到与签退详情列表
GET /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signin GET /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signOut
可选 Query:maxResults、nextToken、type(以开放平台说明为准)。
---
12. 签到与签退操作
POST /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signin — 签到(无 body) POST /v1.0/calendar/users/{unionId}/calendars/primary/events/{eventId}/signOut — 签退(无 body)
成功时响应可含 checkInTime 等字段。重复签到、未开放签到等场景可能返回业务错误码,需按返回提示处理。
---
13. 循环日程
创建日程时在请求体中增加 recurrence,包含 pattern(重复规则)与 range(结束条件)。以下为示例,具体 `type` 取值以钉钉开放平台文档为准:
{
"summary": "每日站会",
"start": { "dateTime": "2026-03-24T01:00:00.000Z", "timeZone": "UTC" },
"end": { "dateTime": "2026-03-24T01:15:00.000Z", "timeZone": "UTC" },
"recurrence": {
"pattern": { "type": "daily", "interval": 1 },
"range": { "type": "endDate", "endDate": "2026-04-24T00:00:00.000Z" }
}
}循环系列删除、修改单实例等可使用 listEventsInstances 等接口(SDK 中另有方法)。
---
14. 订阅日历
创建/删除/更新订阅日历、订阅公共日历等接口路径在 .../subscribedCalendars...,需开放平台开通 Calendar.Calendar.Write(及对应读权限)。本技能主流程以主日历 primary 为主;订阅能力按需查阅 SDK create_subscribed_calendar 等。
---
错误码
| HTTP / 业务 | 说明 | 处理建议 |
|---|---|---|
400 InvalidParameter.ParsedISO8601TimestampError | 时间字符串格式不符 | 使用 yyyy-MM-ddTHH:mm:ss.000Z |
| 401 | Token 无效或过期 | 重新获取 accessToken,必要时 --nocache |
403 AccessDenied | 缺少权限 | 开放平台为应用开通日历/日程相关权限 |
---
所需应用权限
在钉钉开放平台 → 应用管理 → 权限管理中开通与 日历(Calendar) 相关的读/写能力。常见对应关系(以控制台实际名称为准):
| 能力 | 典型权限标识 |
|---|---|
| 日程增删改查、会议室绑定、签到签退等 | Calendar.Event.Read / Calendar.Event.Write |
| 用户闲忙(querySchedule) | Calendar.EventSchedule.Read |
| 会议室忙闲查询 | 一般含在日程读能力或单独会议室相关说明中 |
| 订阅日历、公共日历 | Calendar.Calendar.Write(及 Calendar.Calendar.Read) |
未开通时接口返回 403,响应体中的 requiredScopes / message 会提示需申请的权限标识。
#!/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