commit 8917d0a102573017edbe22a09d5a894177c77705 Author: NightStar Date: Wed Jul 29 19:19:24 2026 +0800 feat: 飞书加急提醒技能 - SKILL.md: remind-me 技能定义 - scripts/feishu_urgent.py: 飞书加急消息发送脚本 - user_id 通过环境变量或传参获取,不硬编码 - 支持自然语言设置定时提醒,到点飞书弹窗 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b41c63 --- /dev/null +++ b/README.md @@ -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 "消息内容" + +# 环境变量方式 +FEISHU_USER_ID= python3 scripts/feishu_urgent.py "消息内容" +``` diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..8d3e946 --- /dev/null +++ b/SKILL.md @@ -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 "消息内容" +# 或通过环境变量 +FEISHU_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` diff --git a/scripts/feishu_urgent.py b/scripts/feishu_urgent.py new file mode 100644 index 0000000..95dc6b4 --- /dev/null +++ b/scripts/feishu_urgent.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""飞书加急消息发送脚本 +用法: + python3 scripts/feishu_urgent.py "提醒内容" + +参数: + 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 <消息内容> ", 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()