feat: 飞书加急提醒技能
- SKILL.md: remind-me 技能定义 - scripts/feishu_urgent.py: 飞书加急消息发送脚本 - user_id 通过环境变量或传参获取,不硬编码 - 支持自然语言设置定时提醒,到点飞书弹窗
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
# remind-me — 飞书加急提醒技能
|
||||||
|
|
||||||
|
一句话设置定时提醒,到点飞书加急弹窗。配合 OpenClaw 使用。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add NightStar/remind-me -g --agent openclaw
|
||||||
|
```
|
||||||
|
|
||||||
|
或手动复制 SKILL.md 到 skill 目录。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- 飞书应用需开通 `im:message.urgent:app_send` 权限
|
||||||
|
- `feishu_urgent.py` 脚本放在 `scripts/` 目录下
|
||||||
|
- 目标用户 open_id 需通过环境变量或传参提供
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```
|
||||||
|
"今晚8点提醒我买洗发膏"
|
||||||
|
"明天早上9点叫我开会"
|
||||||
|
"30分钟后提醒我抢东西"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
用户说一句话 → 创建 cron 任务 → 到点触发独立 session → 执行 `feishu_urgent.py` → 飞书弹窗
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
- `SKILL.md` — 技能定义文件(OpenClaw 加载)
|
||||||
|
- `scripts/feishu_urgent.py` — 飞书加急消息发送脚本
|
||||||
|
|
||||||
|
## 脚本用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 传参方式
|
||||||
|
python3 scripts/feishu_urgent.py "消息内容" <user_id>
|
||||||
|
|
||||||
|
# 环境变量方式
|
||||||
|
FEISHU_USER_ID=<user_id> python3 scripts/feishu_urgent.py "消息内容"
|
||||||
|
```
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Remind Me — 飞书加急提醒
|
||||||
|
|
||||||
|
设置定时提醒,到点自动通过飞书加急消息弹窗通知用户。支持"X点提醒我XXX"等自然语言。
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户说出类似以下话语时触发:
|
||||||
|
- "X点提醒我XXX"
|
||||||
|
- "X分钟后提醒我XXX"
|
||||||
|
- "今晚8点提醒我XXX"
|
||||||
|
- "明天早上9点叫我XXX"
|
||||||
|
|
||||||
|
## 流程
|
||||||
|
|
||||||
|
### 1. 解析时间和内容
|
||||||
|
|
||||||
|
从用户话语中提取:
|
||||||
|
- **时间**:转换为 ISO-8601 UTC 时间戳(注意时区:用户在东八区 Asia/Shanghai,需减8小时转UTC)
|
||||||
|
- **内容**:提醒的具体事项
|
||||||
|
- 如果时间表述模糊(如"明天早上"),确认具体时间
|
||||||
|
|
||||||
|
### 2. 创建 cron 任务
|
||||||
|
|
||||||
|
使用 `cron(action="add")` 创建 `agentTurn` 类型的定时任务。
|
||||||
|
|
||||||
|
`payload.message` 只需一行脚本调用:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /root/.openclaw/workspace && FEISHU_USER_ID=<从飞书消息上下文获取的open_id> python3 scripts/feishu_urgent.py "⏰ {提醒内容的友好表述}"
|
||||||
|
```
|
||||||
|
|
||||||
|
完整 cron 参数:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "remind-me-{简短描述}",
|
||||||
|
"schedule": { "kind": "at", "at": "{ISO-8601 UTC时间}" },
|
||||||
|
"payload": {
|
||||||
|
"kind": "agentTurn",
|
||||||
|
"message": "cd /root/.openclaw/workspace && FEISHU_USER_ID=<用户open_id> python3 scripts/feishu_urgent.py \"⏰ {提醒内容,带上emoji,醒目但不啰嗦,10-30字}\"",
|
||||||
|
"timeoutSeconds": 30
|
||||||
|
},
|
||||||
|
"sessionTarget": "isolated",
|
||||||
|
"deleteAfterRun": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键字段:
|
||||||
|
- `schedule.kind: "at"` — 一次性定时
|
||||||
|
- `schedule.at` — UTC 时间,北京时间20:00 = `2026-07-29T12:00:00.000Z`
|
||||||
|
- `sessionTarget: "isolated"` — 独立 session
|
||||||
|
- `deleteAfterRun: true` — 提醒后自动清理
|
||||||
|
- `user_id` 从当前消息的 sender.id 获取,通过环境变量传入脚本
|
||||||
|
|
||||||
|
### 3. 确认
|
||||||
|
|
||||||
|
创建成功后告诉用户:
|
||||||
|
- 提醒时间(北京时间)
|
||||||
|
- 提醒内容
|
||||||
|
- "到点会飞书加急弹窗哦~"
|
||||||
|
|
||||||
|
### 4. 修改/取消
|
||||||
|
|
||||||
|
用户说"取消提醒" → `cron(action="remove")` 删除最近创建的 remind-me-* 任务
|
||||||
|
|
||||||
|
## 脚本依赖
|
||||||
|
|
||||||
|
`scripts/feishu_urgent.py` 已封装飞书加急全流程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/feishu_urgent.py "消息内容" <user_id>
|
||||||
|
# 或通过环境变量
|
||||||
|
FEISHU_USER_ID=<user_id> python3 scripts/feishu_urgent.py "消息内容"
|
||||||
|
```
|
||||||
|
|
||||||
|
内部自动完成:读配置 → 获取 token → 发消息 → 加急弹窗
|
||||||
|
|
||||||
|
## 时区换算
|
||||||
|
|
||||||
|
用户说的都是北京时间(Asia/Shanghai)。转为 UTC:
|
||||||
|
- 北京时间 - 8 小时 = UTC
|
||||||
|
- 例:今晚8点 = `2026-07-29T20:00:00+08:00` = `2026-07-29T12:00:00.000Z`
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""飞书加急消息发送脚本
|
||||||
|
用法:
|
||||||
|
python3 scripts/feishu_urgent.py "提醒内容" <user_id>
|
||||||
|
|
||||||
|
参数:
|
||||||
|
content 提醒消息内容(必填)
|
||||||
|
user_id 目标用户 open_id(必填,支持环境变量 FEISHU_USER_ID)
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
FEISHU_USER_ID 默认目标用户 open_id
|
||||||
|
FEISHU_CONFIG_PATH 飞书配置文件路径(默认 ~/.openclaw/openclaw.json)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python3 scripts/feishu_urgent.py "🧴 主人!该买洗发膏了!" ou_xxxxxx
|
||||||
|
FEISHU_USER_ID=ou_xxxxxx python3 scripts/feishu_urgent.py "开会了"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
CONFIG_PATH = os.environ.get("FEISHU_CONFIG_PATH",
|
||||||
|
os.path.expanduser("~/.openclaw/openclaw.json"))
|
||||||
|
DEFAULT_USER_ID = os.environ.get("FEISHU_USER_ID", "")
|
||||||
|
BASE_URL = "https://open.feishu.cn/open-apis"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
fs = cfg["channels"]["feishu"]
|
||||||
|
return fs["appId"], fs["appSecret"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(app_id, app_secret):
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{BASE_URL}/auth/v3/tenant_access_token/internal",
|
||||||
|
data=json.dumps({"app_id": app_id, "app_secret": app_secret}).encode(),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
if data.get("code") != 0:
|
||||||
|
raise RuntimeError(f"获取 token 失败: {data}")
|
||||||
|
return data["tenant_access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(token, user_id, content):
|
||||||
|
body = {
|
||||||
|
"receive_id": user_id,
|
||||||
|
"msg_type": "text",
|
||||||
|
"content": json.dumps({"text": content}),
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{BASE_URL}/im/v1/messages?receive_id_type=open_id",
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
if data.get("code") != 0:
|
||||||
|
raise RuntimeError(f"发送消息失败: {data}")
|
||||||
|
return data["data"]["message_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def make_urgent(token, message_id, user_id):
|
||||||
|
body = {"urgent_type": "app", "user_id_list": [user_id]}
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{BASE_URL}/im/v1/messages/{message_id}/urgent_app?user_id_type=open_id",
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="PATCH",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
if data.get("code") != 0:
|
||||||
|
raise RuntimeError(f"加急失败: {data}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("用法: python3 feishu_urgent.py <消息内容> <user_id>", file=sys.stderr)
|
||||||
|
print("或设置环境变量 FEISHU_USER_ID 作为默认 user_id", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
content = sys.argv[1]
|
||||||
|
user_id = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_USER_ID
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
print("❌ 必须提供 user_id:传参或设置 FEISHU_USER_ID 环境变量", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_id, app_secret = load_config()
|
||||||
|
print(f"[1/4] 已读取飞书配置", file=sys.stderr)
|
||||||
|
|
||||||
|
token = get_token(app_id, app_secret)
|
||||||
|
print(f"[2/4] 已获取 token", file=sys.stderr)
|
||||||
|
|
||||||
|
msg_id = send_message(token, user_id, content)
|
||||||
|
print(f"[3/4] 消息已发送", file=sys.stderr)
|
||||||
|
|
||||||
|
result = make_urgent(token, msg_id, user_id)
|
||||||
|
invalid = result.get("data", {}).get("invalid_user_id_list", [])
|
||||||
|
if invalid:
|
||||||
|
print(f"[4/4] ⚠️ 加急部分失败,无效用户", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"[4/4] ✅ 加急成功!用户应已收到弹窗", file=sys.stderr)
|
||||||
|
|
||||||
|
print(json.dumps({"ok": True, "message_id": msg_id, "invalid_users": invalid}))
|
||||||
|
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"\n❌ {e}", file=sys.stderr)
|
||||||
|
print(json.dumps({"ok": False, "error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user