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:
root
2026-03-29 14:15:03 +08:00
parent f8d9936d56
commit 1a9fdc7274
8 changed files with 1177 additions and 0 deletions

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