You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

258 lines
9.2 KiB

#!/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.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.json',
'timezone': 'Asia/Shanghai'
}
result = handle_calendar_command(cmd, args, config)
print(result)
else:
print("Usage: python google_calendar.py <command> [args]")
print("Commands: today, tomorrow, week, status, add, help")