© 2026 方可乐 版权所有
方可乐AI Growth
首页关于我成长圈面试咨询
探索成长圈
内容专栏/公开文章/OpenClaw
OpenClaw

龙虾如何在飞书中创建飞书日程?

新建空白文章,请补充这篇内容要解决的问题和目标读者。

发布于 2026/04/0624 分钟阅读

参考SOP和skill

# 飞书日历创建 SOP

> 适用场景:用户要求创建飞书日程并邀请自己参会
> 执行账号:todo agent

---

## 前置准备

1. 读取 `skills/feishu-calendar-openapi/SKILL.md`,了解:
- `ACCOUNT_ID` 必须与当前 agent ID 一致
- 日程创建命令:`create-event`
- 参会者规则:必须将用户的 open_id 传入,否则用户看不到日程

2. 确认用户信息(从会话 metadata 获取 sender_id)

---

## 操作流程

### Step 1:解析时间

用户说"明天早上10点",换算为北京时间格式 `YYYY-MM-DD HH:MM:SS`。

### Step 2:调用日历创建脚本

```bash
ACCOUNT_ID=<当前agent> bash skills/feishu-calendar-openapi/scripts/calendar.sh create-event \
"<日历名>" \
"<日程标题>" \
"<开始时间 YYYY-MM-DD HH:MM:SS>" \
"<结束时间 YYYY-MM-DD HH:MM:SS>" \
"" \
"<用户open_id>"
```

参数说明:
| 参数 | 说明 |
|------|------|
| calendar | 日历名称(脚本自动匹配 owner/writer 角色的日历) |
| title | 日程标题 |
| start | 开始时间(北京时间) |
| end | 结束时间(北京时间) |
| description | 描述,可选 |
| attendees | 用户的 open_id,必填 |

### Step 3:验证返回

**关键检查点**:`参会人数` ≥ 1,否则参会者未被成功添加。

成功示例:
```
=== 日程创建成功 ===
参会人数: 1
```

---

## 常见错误排查

| 错误现象 | 可能原因 | 解决方式 |
|---------|---------|---------|
| `open_id cross app` 报错 | `ACCOUNT_ID` 与实际 agent 不符 | 用 `openclaw accounts list` 确认 |
| 参会人数为 0 | 应用缺少 `contact:user.id:readonly` 权限 | 飞书开放平台补充权限 |
| 时间差 8 小时 | 周期性任务未加 `--tz "Asia/Shanghai"` | 创建 cron 时必须指定 tz |

---

## SOP 复核清单

- [ ] 读取了 feishu-calendar-openapi SKILL.md
- [ ] `ACCOUNT_ID` 设置为当前 agent
- [ ] 时间转换为北京时间格式 `YYYY-MM-DD HH:MM:SS`
- [ ] 参会者 open_id 传入(从会话 metadata 获取 sender_id)
- [ ] 创建后检查 `参会人数 ≥ 1`
- [ ] 向用户确认日程创建成功

---

---
name: feishu-calendar-openapi
description: 通过 Feishu Calendar OpenAPI 创建/查询日程。优先用 scripts/calendar.sh;失败时返回真实错误,不编造。
---

# Feishu Calendar OpenAPI

使用 `scripts/calendar.sh` 调用飞书 Calendar v4 API。

## 重要:ACCOUNT_ID 必须与当前 agent 一致

`ACCOUNT_ID` 决定使用哪个飞书应用的凭证。**必须设置为当前 agent 的 ID**,例如:
- todo agent → `ACCOUNT_ID=todo`
- coding agent → `ACCOUNT_ID=coding`
- ops agent → `ACCOUNT_ID=ops`

**不要硬编码为某个固定值。**

## 命令

```bash
# 列出所有日历
ACCOUNT_ID=<当前agent的id> bash skills/feishu-calendar-openapi/scripts/calendar.sh list-calendars

# 列出指定日历的日程(支持日历名或 calendar_id)
ACCOUNT_ID=<当前agent的id> bash skills/feishu-calendar-openapi/scripts/calendar.sh list-events <calendar_name_or_id>

# 获取单个日程详情
ACCOUNT_ID=<当前agent的id> bash skills/feishu-calendar-openapi/scripts/calendar.sh get-event <calendar> <event_id>

# 创建日程
ACCOUNT_ID=<当前agent的id> bash skills/feishu-calendar-openapi/scripts/calendar.sh create-event <calendar> "<title>" "<start>" "<end>" "[description]" "[attendees]"

# 删除日程
ACCOUNT_ID=<当前agent的id> bash skills/feishu-calendar-openapi/scripts/calendar.sh delete-event <calendar> <event_id>
```

## 参数说明

### create-event
- `<calendar>`: 日历名(如"待办虾"、"编程虾")或 calendar_id(如 `feishu.cn_xxxxxxxx@group.calendar.feishu.cn`)
- `<title>`: 日程标题
- `<start>`: 开始时间,格式 `YYYY-MM-DD HH:MM:SS`(默认 Asia/Shanghai 时区)
- `<end>`: 结束时间,格式 `YYYY-MM-DD HH:MM:SS`
- `[description]`: 描述(可选)
- `[attendees]`: 逗号分隔的 open_id 列表(可选),如 `ou_xxx1,ou_xxx2`

## 输出说明

- **创建成功**: 输出纯文本,包含 calendar_id、event_id、标题、时间、参会人数
- **参会人未写入**: 如果指定了参会者但返回人数为 0,会显示 WARNING 并附上接口返回码和返回体
- **加载失败提示**: 如果飞书客户端显示"加载失败",可能只是链接预览渲染失败,不代表创建失败

## ⚠️ 参会者规则(必须遵守)

**创建日程时必须把用户加为参会者。** 不传 attendees 参数会导致日程只创建在 agent 的日历上,用户看不到也收不到通知。

用户的 open_id(从会话 metadata sender_id 获取,示例中以 `ou_xxx` 代替)

正确示例:
```bash
# 正确:传入用户 open_id
ACCOUNT_ID=<当前agent> bash skills/feishu-calendar-openapi/scripts/calendar.sh create-event \
"<日历名>" \
"<日程标题>" \
"<开始时间 YYYY-MM-DD HH:MM:SS>" \
"<结束时间 YYYY-MM-DD HH:MM:SS>" \
"" \
"<用户open_id(从sender_id获取)>"
```

错误示例(缺少参会者):
```bash
# ❌ 这样用户收不到日程邀请
ACCOUNT_ID=<当前agent> bash skills/feishu-calendar-openapi/scripts/calendar.sh create-event \
"<日历名>" "<日程标题>" "<开始时间>" "<结束时间>" ""
```

## 注意事项

1. **ACCOUNT_ID**: 必须与当前 agent ID 一致,不同 agent 的飞书应用凭证不同
2. **参会者**: 创建日程时必须传入用户的 open_id 作为 attendees 参数(从会话 metadata sender_id 获取)
3. **日历名匹配**: 支持传入日历名,脚本会自动匹配(优先 owner/writer 角色的日历)
4. **时区**: 所有时间统一使用 Asia/Shanghai 时区
5. **参会者权限**: 添加参会者需要应用在飞书开放平台开通 `contact:user.id:readonly` 权限
6. **Token 获取**: 脚本自动从 openclaw.json 读取对应 ACCOUNT_ID 的 appId/appSecret

## 权限要求

飞书开放平台需开通:
- `calendar:calendar` - 创建/编辑日程
- `contact:contact:user` - 读取联系人信息
- `contact:user.id:readonly` - 读取用户 ID(添加参会者必需)

--

#!/usr/bin/env bash
set -euo pipefail

CFG="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
ACCOUNT_ID="${ACCOUNT_ID:-default}"

get_cfg() {
jq -r --arg a "$ACCOUNT_ID" "$1 // empty" "$CFG"
}

APP_ID="${FEISHU_APP_ID:-$(get_cfg '.channels.feishu.accounts[$a].appId')}"
APP_SECRET="${FEISHU_APP_SECRET:-$(get_cfg '.channels.feishu.accounts[$a].appSecret')}"

if [ -z "$APP_ID" ] || [ -z "$APP_SECRET" ]; then
echo "ERROR: missing appId/appSecret for account=$ACCOUNT_ID"
exit 1
fi

BASE="https://open.feishu.cn/open-apis"

# 获取 Token(带超时,稳定解析)
get_token() {
local resp
resp=$(curl -fsS --max-time 10 -X POST "$BASE/auth/v3/tenant_access_token/internal" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg id "$APP_ID" --arg secret "$APP_SECRET" '{app_id:$id,app_secret:$secret}')" 2>&1) || {
echo "ERROR: failed to call token API: $resp"
return 1
}

local token
token=$(echo "$resp" | jq -er '.tenant_access_token // empty' 2>&1) || {
echo "ERROR: failed to parse token from response: $resp"
return 1
}

if [ -z "$token" ]; then
echo "ERROR: got empty token, response: $resp"
return 1
fi

echo "$token"
}

TOKEN="$(get_token)" || exit 1

# 获取日历列表
list_calendars_api() {
curl -fsS --max-time 15 -H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars?page_size=100"
}

# 根据日历名或 ID 查找日历(优先 owner/writer)
find_calendar() {
local input="$1"

# 如果包含 @group.calendar.feishu.cn 或 @primary.calendar.feishu.cn,当作 calendar_id 直接使用
if [[ "$input" == *"@group.calendar.feishu.cn"* ]] || [[ "$input" == *"@primary.calendar.feishu.cn"* ]]; then
echo "$input"
return 0
fi

# 否则当作日历名,从列表匹配
local resp
resp=$(list_calendars_api) || {
echo "ERROR: failed to list calendars"
return 1
}

# 优先匹配 owner 或 writer 角色的日历
local cal_id
cal_id=$(echo "$resp" | jq -r --arg name "$input" '
.data.calendar_list
| map(select(.summary == $name and (.role == "owner" or .role == "writer")))
| .[0].calendar_id // empty' 2>&1) || {
echo "ERROR: failed to parse calendar list"
return 1
}

if [ -n "$cal_id" ]; then
echo "$cal_id"
return 0
fi

# 没找到 owner/writer,尝试匹配任意角色
cal_id=$(echo "$resp" | jq -r --arg name "$input" '
.data.calendar_list
| map(select(.summary == $name))
| .[0].calendar_id // empty' 2>&1) || {
echo "ERROR: failed to parse calendar list"
return 1
}

if [ -n "$cal_id" ]; then
echo "$cal_id"
return 0
fi

echo "ERROR: calendar not found: $input"
return 1
}

# 校验 calendar_id 是否有效
validate_calendar_id() {
local cal_id="$1"
local resp
resp=$(curl -fsS --max-time 10 -H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars/$cal_id" 2>&1) || {
echo "ERROR: failed to validate calendar_id"
return 1
}

local code
code=$(echo "$resp" | jq -r '.code // 0' 2>&1)

if [ "$code" = "191001" ]; then
echo "INVALID"
return 1
fi

echo "VALID"
return 0
}

# 添加参会者到事件(单独 API 调用)
add_attendees() {
local cal_id="$1"
local event_id="$2"
local attendees="$3" # 逗号分隔的 open_id 列表

# 解析逗号分隔的 open_id 列表
IFS=',' read -ra ATTENDEE_ARRAY <<< "$attendees"
attendees_json="["
first=true
for att in "${ATTENDEE_ARRAY[@]}"; do
att=$(echo "$att" | xargs) # 去除空格
if [ -n "$att" ]; then
if [ "$first" = true ]; then
first=false
else
attendees_json+=","
fi
# 关键:需要 type: "user" 字段
attendees_json+="{\"type\":\"user\",\"user_id\":\"$att\",\"is_optional\":false}"
fi
done
attendees_json+="]"

# 关键:需要 need_notification: true
local payload
payload=$(jq -n --argjson att "$attendees_json" '{attendees:$att,need_notification:true}')

local resp
# 关键:URL 需要 ?user_id_type=open_id 查询参数
resp=$(curl -fsS --max-time 15 -X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
"$BASE/calendar/v4/calendars/$cal_id/events/$event_id/attendees?user_id_type=open_id" \
-d "$payload" 2>&1)

echo "$resp"
}

# 列出事件的参会者
list_attendees() {
local cal_id="$1"
local event_id="$2"

curl -fsS --max-time 10 -H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars/$cal_id/events/$event_id/attendees"
}

cmd="${1:-}"

case "$cmd" in
list-calendars)
list_calendars_api | jq .
;;

list-events)
cal="${2:?calendar_id or calendar_name required}"
cal_id=$(find_calendar "$cal") || exit 1
curl -fsS --max-time 15 -H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars/$cal_id/events" | jq .
;;

get-event)
cal="${2:?calendar_id or calendar_name required}"
event_id="${3:?event_id required}"
cal_id=$(find_calendar "$cal") || exit 1
curl -fsS --max-time 10 -H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars/$cal_id/events/$event_id" | jq .
;;

create-event)
cal="${2:?calendar_id or calendar_name required}"
title="${3:?title required}"
start="${4:?start required}"
end="${5:?end required}"
desc="${6:-}"
attendees="${7:-}" # 逗号分隔的 open_id 列表

# 查找/校验 calendar_id
cal_id=$(find_calendar "$cal") || exit 1

# 验证 calendar_id
validation=$(validate_calendar_id "$cal_id") || {
if [ "$validation" = "INVALID" ]; then
echo "WARNING: invalid calendar_id, retrying lookup..."
cal_id=$(find_calendar "$cal") || exit 1
fi
}

# 转换时间为时间戳
s="$(date -d "$start" +%s)" || {
echo "ERROR: invalid start time format: $start"
exit 1
}
e="$(date -d "$end" +%s)" || {
echo "ERROR: invalid end time format: $end"
exit 1
}

# 构建 payload(创建时不传 attendees)
payload=$(jq -n \
--arg t "$title" \
--arg d "$desc" \
--arg s "$s" \
--arg e "$e" \
'{
summary: $t,
description: $d,
start_time: {timestamp: $s, timezone: "Asia/Shanghai"},
end_time: {timestamp: $e, timezone: "Asia/Shanghai"}
}')

# 创建日程
resp=$(curl -fsS --max-time 15 -X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
"$BASE/calendar/v4/calendars/$cal_id/events" \
-d "$payload" 2>&1)

# 解析响应
code=$(echo "$resp" | jq -r '.code // 0' 2>&1)

if [ "$code" != "0" ]; then
echo "ERROR: create event failed"
echo "Code: $code"
echo "Response: $resp"
exit 1
fi

event_id=$(echo "$resp" | jq -r '.data.event.event_id // empty' 2>&1)

# 如果有参会者,调用单独 API 添加
attendee_count=0
add_attendee_result=""
if [ -n "$attendees" ]; then
add_attendee_result=$(add_attendees "$cal_id" "$event_id" "$attendees")
add_code=$(echo "$add_attendee_result" | jq -r '.code // 0' 2>&1)

if [ "$add_code" = "0" ]; then
attendee_count=$(echo "$add_attendee_result" | jq -r '.data.attendees | length // 0' 2>&1)
fi
fi

echo "=== 日程创建成功 ==="
echo "calendar_id: $cal_id"
echo "event_id: $event_id"
echo "标题: $title"
echo "时间: $start - $end (Asia/Shanghai)"
echo "参会人数: ${attendee_count:-0}"

# 如果有参会者但添加失败,输出详细错误
if [ -n "$attendees" ] && [ "${attendee_count:-0}" = "0" ]; then
echo ""
echo "WARNING: 事件创建成功但参会人没写进去"
echo "添加参会者接口返回码: ${add_code:-N/A}"
echo "添加参会者接口返回体: ${add_attendee_result:-N/A}"
echo ""
echo "可能原因:应用缺少'contact:user.id:readonly'权限"
fi
;;

add-attendees)
cal="${2:?calendar_id or calendar_name required}"
event_id="${3:?event_id required}"
attendees="${4:?attendees required}" # 逗号分隔的 open_id 列表
cal_id=$(find_calendar "$cal") || exit 1

resp=$(add_attendees "$cal_id" "$event_id" "$attendees")
code=$(echo "$resp" | jq -r '.code // 0' 2>&1)

if [ "$code" = "0" ]; then
count=$(echo "$resp" | jq -r '.data.attendees | length // 0' 2>&1)
echo "=== 参会者添加成功 ==="
echo "event_id: $event_id"
echo "添加人数: $count"
echo "$resp" | jq '.data.attendees[] | {user_id, display_name, rsvp_status}'
else
echo "ERROR: add attendees failed"
echo "Code: $code"
echo "Response: $resp"
exit 1
fi
;;

list-attendees)
cal="${2:?calendar_id or calendar_name required}"
event_id="${3:?event_id required}"
cal_id=$(find_calendar "$cal") || exit 1
list_attendees "$cal_id" "$event_id" | jq .
;;

delete-event)
cal="${2:?calendar_id or calendar_name required}"
event_id="${3:?event_id required}"
cal_id=$(find_calendar "$cal") || exit 1
curl -fsS --max-time 10 -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$BASE/calendar/v4/calendars/$cal_id/events/$event_id" | jq .
;;

*)
echo "usage: $0 <command> [args...]"
echo ""
echo "commands:"
echo " list-calendars 列出所有日历"
echo " list-events <calendar> 列出日程(支持日历名或 calendar_id)"
echo " get-event <calendar> <event_id> 获取单个日程详情"
echo " create-event <cal> <title> <start> <end> [desc] [attendees]"
echo " 创建日程"
echo " - attendees: 逗号分隔的 open_id 列表(如:ou_xxx1,ou_xxx2)"
echo " - 时间格式:YYYY-MM-DD HH:MM:SS"
echo " add-attendees <calendar> <event_id> <attendees> 添加参会者到已有日程"
echo " list-attendees <calendar> <event_id> 列出日程的参会者"
echo " delete-event <calendar> <event_id> 删除日程"
exit 1
;;
esac