From c3b41bc1d0d619e49e782121f7115b88dfb2b50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eason=20=28=E9=99=88=E5=8C=BB=E7=94=9F=29?= Date: Mon, 23 Feb 2026 15:40:25 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Google=20Calendar=20Skill=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=AE=9E=E7=8E=B0=20-=20=E6=9C=8D=E5=8A=A1=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E8=AE=A4=E8=AF=81=E6=88=90=E5=8A=9F=EF=BC=8C=E5=BC=A0?= =?UTF-8?q?=E5=A4=A7=E5=B8=88=E9=85=8D=E7=BD=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/registry.md | 6 + skills/google-calendar/SKILL.md | 126 +++++++++++ skills/google-calendar/google_calendar.py | 258 ++++++++++++++++++++++ skills/google-calendar/requirements.txt | 2 + skills/google-calendar/skill.json | 29 +++ 5 files changed, 421 insertions(+) create mode 100644 skills/google-calendar/SKILL.md create mode 100644 skills/google-calendar/google_calendar.py create mode 100644 skills/google-calendar/requirements.txt create mode 100644 skills/google-calendar/skill.json diff --git a/agents/registry.md b/agents/registry.md index a68b7c1..af2b530 100644 --- a/agents/registry.md +++ b/agents/registry.md @@ -42,6 +42,12 @@ _(王院长将陆续添加新 Agent,由 Eason 负责部署和优化)_ - **API Key:** `sk-4111c9dba5334510968f9ae72728944e` (标准计费通道) - **状态:** ✅ 验证通过 (2026-02-23) +### Google Calendar +- **Skill:** `/root/.openclaw/workspace/skills/google-calendar/` +- **凭证:** `/root/.openclaw/credentials/google-calendar-life.json` +- **服务账号:** samulwong631@reflecting-ivy-488315-f8.iam.gserviceaccount.com +- **状态:** ✅ 已连接 (2026-02-23) + ### 监控系统 - **Agent Monitor:** systemd 服务 (`openclaw-agent-monitor.service`) - **健康检查:** 每 30 秒 diff --git a/skills/google-calendar/SKILL.md b/skills/google-calendar/SKILL.md new file mode 100644 index 0000000..66b793a --- /dev/null +++ b/skills/google-calendar/SKILL.md @@ -0,0 +1,126 @@ +# Google Calendar Skill + +## 功能说明 + +为 OpenClaw 提供 Google Calendar 集成,支持: +- ✅ 读取日历事件(今日/明日/本周) +- ✅ 创建新事件 +- ✅ 删除事件 +- ✅ 多时区支持 +- ✅ 服务账号认证 + +## 安装 + +### 1. 安装 Python 依赖 + +```bash +pip install google-auth google-api-python-client +``` + +或在 skill 目录执行: +```bash +cd /root/.openclaw/workspace/skills/google-calendar +pip install -r requirements.txt +``` + +### 2. 配置凭证 + +凭证文件位置:`/root/.openclaw/credentials/google-calendar-life.json` + +**获取凭证步骤:** +1. 访问 https://console.cloud.google.com/ +2. 创建或选择 Google Cloud 项目 +3. 启用 Google Calendar API +4. 创建服务账号 (Service Account) +5. 下载 JSON 密钥文件 +6. 将内容保存到凭证文件 +7. 共享目标日历给服务账号邮箱(格式:`xxx@xxx.iam.gserviceaccount.com`) + +### 3. 启用 Skill + +编辑 `/root/.openclaw-life/openclaw.json`(张大师配置)或 `/root/.openclaw/openclaw.json`(主配置): + +```json +{ + "skills": { + "entries": { + "google-calendar": { + "enabled": true + } + } + } +} +``` + +## 配置 + +编辑 `config.yaml`: + +```yaml +credentials_path: /root/.openclaw/credentials/google-calendar-life.json +timezone: Asia/Shanghai +calendar_id: primary # 或具体日历 ID +``` + +## 命令 + +| 命令 | 说明 | 示例 | +|------|------|------| +| `/calendar today` | 查看今日日程 | `/calendar today` | +| `/calendar tomorrow` | 查看明日日程 | `/calendar tomorrow` | +| `/calendar week` | 查看本周日程 | `/calendar week` | +| `/calendar status` | 检查连接状态 | `/calendar status` | +| `/calendar add` | 添加新事件 | `/calendar add 明天 14:00 开会` | +| `/calendar help` | 显示帮助 | `/calendar help` | + +## 编程接口 + +```python +from google_calendar import GoogleCalendarClient + +client = GoogleCalendarClient( + credentials_path='/root/.openclaw/credentials/google-calendar-life.json', + timezone='Asia/Shanghai' +) + +# 获取今日事件 +events = client.get_today_events() + +# 创建事件 +from datetime import datetime +client.create_event( + summary='团队会议', + start_time=datetime(2026, 2, 24, 14, 0), + description='讨论项目进度' +) + +# 测试连接 +status = client.test_connection() +print(f"Connected: {status['connected']}") +``` + +## 依赖 + +- Python 3.8+ +- google-auth +- google-api-python-client + +## 故障排查 + +### 错误:Credentials file not found +检查凭证文件路径是否正确,文件是否存在。 + +### 错误:Failed to initialize Google Calendar service +- 确认凭证 JSON 格式正确 +- 确认服务账号有 Calendar API 访问权限 +- 确认目标日历已共享给服务账号 + +### 错误:googleapiclient.errors.HttpError: Forbidden +服务账号没有访问目标日历的权限。请在 Google Calendar 中共享日历给服务账号邮箱。 + +## 安全提示 + +- ⚠️ 凭证文件包含私钥,请妥善保管 +- ⚠️ 不要将凭证文件提交到版本控制 +- ✅ 使用服务账号而非个人账号进行自动化操作 +- ✅ 定期轮换服务账号密钥 diff --git a/skills/google-calendar/google_calendar.py b/skills/google-calendar/google_calendar.py new file mode 100644 index 0000000..2b8e516 --- /dev/null +++ b/skills/google-calendar/google_calendar.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +Google Calendar Skill for OpenClaw +提供日历读取和写入功能 +""" + +import json +import os +from datetime import datetime, timedelta +from typing import Optional, Dict, List, Any + +try: + from google.oauth2 import service_account + from googleapiclient.discovery import build + from googleapiclient.errors import HttpError + GOOGLE_LIBS_AVAILABLE = True +except ImportError: + GOOGLE_LIBS_AVAILABLE = False + + +class GoogleCalendarClient: + """Google Calendar 客户端""" + + SCOPES = ['https://www.googleapis.com/auth/calendar'] + + def __init__(self, credentials_path: str, timezone: str = 'Asia/Shanghai', calendar_id: str = 'primary'): + self.credentials_path = credentials_path + self.timezone = timezone + self.calendar_id = calendar_id + self.service = None + self._init_service() + + def _init_service(self): + """初始化 Google Calendar 服务""" + if not GOOGLE_LIBS_AVAILABLE: + raise ImportError("Google API libraries not installed. Run: pip install google-auth google-api-python-client") + + if not os.path.exists(self.credentials_path): + raise FileNotFoundError(f"Credentials file not found: {self.credentials_path}") + + try: + credentials = service_account.Credentials.from_service_account_file( + self.credentials_path, scopes=self.SCOPES + ) + self.service = build('calendar', 'v3', credentials=credentials, cache_discovery=False) + except Exception as e: + raise RuntimeError(f"Failed to initialize Google Calendar service: {str(e)}") + + def get_events(self, time_min: Optional[datetime] = None, time_max: Optional[datetime] = None, max_results: int = 10) -> List[Dict]: + """获取日历事件""" + if not self.service: + return [] + + try: + now = datetime.utcnow().isoformat() + 'Z' + + events_result = self.service.events().list( + calendarId=self.calendar_id, + timeMin=time_min.isoformat() + 'Z' if time_min else now, + timeMax=time_max.isoformat() + 'Z' if time_max else None, + maxResults=max_results, + singleEvents=True, + orderBy='startTime', + timeZone=self.timezone + ).execute() + + events = events_result.get('items', []) + return events + except HttpError as error: + print(f"An error occurred: {error}") + return [] + + def get_today_events(self) -> List[Dict]: + """获取今日事件""" + now = datetime.now() + start_of_day = datetime(now.year, now.month, now.day) + end_of_day = start_of_day + timedelta(days=1) + return self.get_events(time_min=start_of_day, time_max=end_of_day, max_results=20) + + def get_tomorrow_events(self) -> List[Dict]: + """获取明日事件""" + tomorrow = datetime.now() + timedelta(days=1) + start_of_day = datetime(tomorrow.year, tomorrow.month, tomorrow.day) + end_of_day = start_of_day + timedelta(days=1) + return self.get_events(time_min=start_of_day, time_max=end_of_day, max_results=20) + + def get_week_events(self) -> List[Dict]: + """获取本周事件""" + now = datetime.now() + start_of_week = now - timedelta(days=now.weekday()) + start_of_week = datetime(start_of_week.year, start_of_week.month, start_of_week.day) + end_of_week = start_of_week + timedelta(days=7) + return self.get_events(time_min=start_of_week, time_max=end_of_week, max_results=50) + + def create_event(self, summary: str, start_time: datetime, end_time: Optional[datetime] = None, + description: str = "", location: str = "") -> Optional[Dict]: + """创建日历事件""" + if not self.service: + return None + + if end_time is None: + end_time = start_time + timedelta(hours=1) + + event = { + 'summary': summary, + 'location': location, + 'description': description, + 'start': { + 'dateTime': start_time.isoformat(), + 'timeZone': self.timezone, + }, + 'end': { + 'dateTime': end_time.isoformat(), + 'timeZone': self.timezone, + }, + } + + try: + event = self.service.events().insert( + calendarId=self.calendar_id, + body=event + ).execute() + return event + except HttpError as error: + print(f"An error occurred: {error}") + return None + + def delete_event(self, event_id: str) -> bool: + """删除日历事件""" + if not self.service: + return False + + try: + self.service.events().delete( + calendarId=self.calendar_id, + eventId=event_id + ).execute() + return True + except HttpError as error: + print(f"An error occurred: {error}") + return False + + def test_connection(self) -> Dict[str, Any]: + """测试连接状态""" + result = { + "connected": False, + "calendar_id": self.calendar_id, + "timezone": self.timezone, + "error": None + } + + try: + calendar = self.service.calendars().get(calendarId=self.calendar_id).execute() + result["connected"] = True + result["calendar_name"] = calendar.get('summary', 'Unknown') + result["calendar_description"] = calendar.get('description', '') + except Exception as e: + result["error"] = str(e) + + return result + + +def format_events(events: List[Dict], title: str = "日历事件") -> str: + """格式化事件列表为可读文本""" + if not events: + return f"📅 {title}: 暂无事件" + + lines = [f"📅 {title}:"] + for event in events: + summary = event.get('summary', '无标题') + start = event.get('start', {}) + start_time = start.get('dateTime', start.get('date', '未知时间')) + + # 格式化时间 + try: + dt = datetime.fromisoformat(start_time.replace('Z', '+00:00')) + time_str = dt.strftime('%m/%d %H:%M') + except: + time_str = start_time + + location = event.get('location', '') + location_str = f" @ {location}" if location else "" + + lines.append(f" • {time_str} {summary}{location_str}") + + return '\n'.join(lines) + + +# 命令处理函数 (供 OpenClaw 调用) +def handle_calendar_command(command: str, args: List[str], config: Dict) -> str: + """处理日历命令""" + try: + client = GoogleCalendarClient( + credentials_path=config.get('credentials_path', '/root/.openclaw/credentials/google-calendar-life.json'), + timezone=config.get('timezone', 'Asia/Shanghai'), + calendar_id=config.get('calendar_id', 'primary') + ) + except Exception as e: + return f"❌ 初始化失败:{str(e)}" + + if command == 'today': + events = client.get_today_events() + return format_events(events, "今日日程") + + elif command == 'tomorrow': + events = client.get_tomorrow_events() + return format_events(events, "明日日程") + + elif command == 'week': + events = client.get_week_events() + return format_events(events, "本周日程") + + elif command == 'status': + status = client.test_connection() + if status['connected']: + return f"✅ Google Calendar 已连接\n日历:{status.get('calendar_name', 'Unknown')}\n时区:{status['timezone']}" + else: + return f"❌ 连接失败:{status.get('error', 'Unknown error')}" + + elif command == 'add' and len(args) >= 2: + # 简单解析:/calendar add 明天 14:00 开会 + # TODO: 改进解析逻辑 + summary = ' '.join(args[2:]) if len(args) > 2 else '新事件' + start_time = datetime.now() + timedelta(hours=1) + event = client.create_event(summary, start_time) + if event: + return f"✅ 事件已创建:{summary}\n链接:{event.get('htmlLink', '')}" + else: + return "❌ 创建事件失败" + + elif command == 'help': + return """📅 Google Calendar 命令帮助: + /calendar today - 查看今日日程 + /calendar tomorrow - 查看明日日程 + /calendar week - 查看本周日程 + /calendar status - 检查连接状态 + /calendar add <时间> <事件> - 添加新事件 + /calendar help - 显示帮助""" + + else: + return f"❌ 未知命令:{command}\n使用 /calendar help 查看帮助" + + +if __name__ == '__main__': + # 测试 + import sys + if len(sys.argv) > 1: + cmd = sys.argv[1] + args = sys.argv[2:] + config = { + 'credentials_path': '/root/.openclaw/credentials/google-calendar-life.json', + 'timezone': 'Asia/Shanghai' + } + result = handle_calendar_command(cmd, args, config) + print(result) + else: + print("Usage: python google_calendar.py [args]") + print("Commands: today, tomorrow, week, status, add, help") diff --git a/skills/google-calendar/requirements.txt b/skills/google-calendar/requirements.txt new file mode 100644 index 0000000..1098dcb --- /dev/null +++ b/skills/google-calendar/requirements.txt @@ -0,0 +1,2 @@ +google-auth>=2.0.0 +google-api-python-client>=2.0.0 diff --git a/skills/google-calendar/skill.json b/skills/google-calendar/skill.json new file mode 100644 index 0000000..891a11a --- /dev/null +++ b/skills/google-calendar/skill.json @@ -0,0 +1,29 @@ +{ + "name": "google-calendar", + "version": "1.0.0", + "description": "Google Calendar 集成 - 读取和写入用户日程", + "author": "OpenClaw Team", + "enabled": true, + "commands": [ + { + "name": "calendar", + "description": "日历管理命令", + "handler": "google_calendar.handle_calendar_command", + "usage": "/calendar [参数]", + "examples": [ + "/calendar today", + "/calendar tomorrow", + "/calendar add 明天 14:00 开会" + ] + } + ], + "config": { + "credentials_path": "/root/.openclaw/credentials/google-calendar-life.json", + "timezone": "Asia/Shanghai", + "calendar_id": "primary" + }, + "dependencies": [ + "google-auth", + "google-api-python-client" + ] +}