commit 73ed801397ce1db0c4f2f7216b52e8800ef2030b Author: zlei9 Date: Sun Mar 29 08:20:27 2026 +0800 Initial commit with translated description diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..33adedf --- /dev/null +++ b/SKILL.md @@ -0,0 +1,63 @@ +--- +name: google-calendar +description: "通过Google Calendar API与Google日历交互——列出即将发生的事件、创建新事件、更新或删除事件。当您需要从OpenClaw以编程方式访问日历时使用此技能。" +--- + +# Google Calendar Skill + +## Overview +This skill provides a thin wrapper around the Google Calendar REST API. It lets you: +- **list** upcoming events (optionally filtered by time range or query) +- **add** a new event with title, start/end time, description, location, and attendees +- **update** an existing event by its ID +- **delete** an event by its ID + +The skill is implemented in Python (`scripts/google_calendar.py`). It expects the following environment variables to be set (you can store them securely with `openclaw secret set`): +``` +GOOGLE_CLIENT_ID=… +GOOGLE_CLIENT_SECRET=… +GOOGLE_REFRESH_TOKEN=… # obtained after OAuth consent +GOOGLE_CALENDAR_ID=primary # or the ID of a specific calendar +``` +The first time you run the skill you may need to perform an OAuth flow to obtain a refresh token – see the **Setup** section below. + +## Commands +``` +google-calendar list [--from --to --max ] +google-calendar add --title [--start <ISO> --end <ISO>] + [--desc <description> --location <loc> --attendees <email1,email2>] +google-calendar update --event-id <id> [--title <title> ... other fields] +google-calendar delete --event-id <id> +``` +All commands return a JSON payload printed to stdout. Errors are printed to stderr and cause a non‑zero exit code. + +## Setup +1. **Create a Google Cloud project** and enable the *Google Calendar API*. +2. **Create OAuth credentials** (type *Desktop app*). Note the `client_id` and `client_secret`. +3. Run the helper script to obtain a refresh token: + ```bash + GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… python3 -m google_calendar.auth + ``` + It will open a browser (or print a URL you can open elsewhere) and ask you to grant access. After you approve, copy the `refresh_token` it prints. +4. Store the credentials securely: + ```bash + openclaw secret set GOOGLE_CLIENT_ID <value> + openclaw secret set GOOGLE_CLIENT_SECRET <value> + openclaw secret set GOOGLE_REFRESH_TOKEN <value> + openclaw secret set GOOGLE_CALENDAR_ID primary # optional + ``` +5. Install the required Python packages (once): + ```bash + pip install --user google-auth google-auth-oauthlib google-api-python-client + ``` + +## How it works (brief) +The script loads the credentials from the environment, refreshes the access token using the refresh token, builds a `service = build('calendar', 'v3', credentials=creds)`, and then calls the appropriate API method. + +## References +- Google Calendar API reference: https://developers.google.com/calendar/api/v3/reference +- OAuth 2.0 for installed apps: https://developers.google.com/identity/protocols/oauth2/native-app + +--- + +**Note:** This skill does not require a GUI; it works entirely via HTTP calls, so it is suitable for headless servers. diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..1d4c266 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7ez06mtd5e4r78d14pa6gp41808bw8", + "slug": "google-calendar", + "version": "0.1.0", + "publishedAt": 1769879360981 +} \ No newline at end of file diff --git a/scripts/google_calendar.py b/scripts/google_calendar.py new file mode 100644 index 0000000..305a6e9 --- /dev/null +++ b/scripts/google_calendar.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +import os, sys, json, urllib.request, urllib.parse, argparse + +BASE_URL = 'https://www.googleapis.com/calendar/v3' + +def get_access_token(): + token = os.getenv('GOOGLE_ACCESS_TOKEN') + if not token: + sys.stderr.write('Error: GOOGLE_ACCESS_TOKEN env var not set\n') + sys.exit(1) + return token + +def get_calendar_ids(): + # Support multiple IDs via env var (comma‑separated) or single ID fallback + ids = os.getenv('GOOGLE_CALENDAR_IDS') + if ids: + return [c.strip() for c in ids.split(',') if c.strip()] + # fallback to single ID for backward compatibility + single = os.getenv('GOOGLE_CALENDAR_ID') + return [single] if single else [] + +def request(method, url, data=None): + req = urllib.request.Request(url, data=data, method=method) + req.add_header('Authorization', f'Bearer {get_access_token()}') + req.add_header('Accept', 'application/json') + if data: + req.add_header('Content-Type', 'application/json') + try: + with urllib.request.urlopen(req) as resp: + return json.load(resp) + except urllib.error.HTTPError as e: + sys.stderr.write(f'HTTP error {e.code}: {e.read().decode()}\n') + sys.exit(1) + +def list_events(args): + parser = argparse.ArgumentParser() + parser.add_argument('--from', dest='time_min', help='ISO start time') + parser.add_argument('--to', dest='time_max', help='ISO end time') + parser.add_argument('--max', dest='max_results', type=int, default=10) + parsed = parser.parse_args(args) + results = {} + for cal_id in get_calendar_ids(): + params = { + 'maxResults': parsed.max_results, + 'singleEvents': 'true', + 'orderBy': 'startTime', + } + if parsed.time_min: + params['timeMin'] = parsed.time_min + if parsed.time_max: + params['timeMax'] = parsed.time_max + url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events?{urllib.parse.urlencode(params)}" + resp = request('GET', url) + results[cal_id] = resp.get('items', []) + # Output a combined JSON mapping calendar ID -> list of events + print(json.dumps(results, indent=2)) + +# The other commands (add, update, delete) remain single‑calendar for simplicity +def add_event(args): + parser = argparse.ArgumentParser() + parser.add_argument('--title', required=True) + parser.add_argument('--start', required=True, help='ISO datetime') + parser.add_argument('--end', required=True, help='ISO datetime') + parser.add_argument('--desc', default='') + parser.add_argument('--location', default='') + parser.add_argument('--attendees', default='') + parsed = parser.parse_args(args) + cal_id = get_calendar_ids()[0] if get_calendar_ids() else None + if not cal_id: + sys.stderr.write('No calendar ID configured\n') + sys.exit(1) + event = { + 'summary': parsed.title, + 'start': {'dateTime': parsed.start}, + 'end': {'dateTime': parsed.end}, + 'description': parsed.desc, + 'location': parsed.location, + } + if parsed.attendees: + event['attendees'] = [{'email': e.strip()} for e in parsed.attendees.split(',') if e.strip()] + url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events" + data = json.dumps(event).encode() + resp = request('POST', url, data=data) + print(json.dumps(resp, indent=2)) + +def update_event(args): + parser = argparse.ArgumentParser() + parser.add_argument('--event-id', required=True) + parser.add_argument('--title') + parser.add_argument('--start') + parser.add_argument('--end') + parser.add_argument('--desc') + parser.add_argument('--location') + parser.add_argument('--attendees') + parsed = parser.parse_args(args) + cal_id = get_calendar_ids()[0] if get_calendar_ids() else None + if not cal_id: + sys.stderr.write('No calendar ID configured\n') + sys.exit(1) + get_url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events/{urllib.parse.quote(parsed.event_id)}" + event = request('GET', get_url) + if parsed.title: + event['summary'] = parsed.title + if parsed.start: + event.setdefault('start', {})['dateTime'] = parsed.start + if parsed.end: + event.setdefault('end', {})['dateTime'] = parsed.end + if parsed.desc is not None: + event['description'] = parsed.desc + if parsed.location is not None: + event['location'] = parsed.location + if parsed.attendees: + event['attendees'] = [{'email': e.strip()} for e in parsed.attendees.split(',') if e.strip()] + resp = request('PUT', get_url, data=json.dumps(event).encode()) + print(json.dumps(resp, indent=2)) + +def delete_event(args): + parser = argparse.ArgumentParser() + parser.add_argument('--event-id', required=True) + parsed = parser.parse_args(args) + cal_id = get_calendar_ids()[0] if get_calendar_ids() else None + if not cal_id: + sys.stderr.write('No calendar ID configured\n') + sys.exit(1) + url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events/{urllib.parse.quote(parsed.event_id)}" + resp = request('DELETE', url) + print(json.dumps(resp, indent=2)) + +if __name__ == '__main__': + if len(sys.argv) < 2: + sys.stderr.write('Usage: google_calendar.py <command> [options]\n') + sys.exit(1) + cmd = sys.argv[1] + args = sys.argv[2:] + if cmd == 'list': + list_events(args) + elif cmd == 'add': + add_event(args) + elif cmd == 'update': + update_event(args) + elif cmd == 'delete': + delete_event(args) + else: + sys.stderr.write(f'Unknown command: {cmd}\n') + sys.exit(1) diff --git a/scripts/refresh_token.py b/scripts/refresh_token.py new file mode 100644 index 0000000..c0bf088 --- /dev/null +++ b/scripts/refresh_token.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import os, sys, json, urllib.request, urllib.parse + +def refresh(): + client_id = os.getenv('GOOGLE_CLIENT_ID') + client_secret = os.getenv('GOOGLE_CLIENT_SECRET') + refresh_token = os.getenv('GOOGLE_REFRESH_TOKEN') + if not all([client_id, client_secret, refresh_token]): + sys.stderr.write('Missing one of GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN\n') + sys.exit(1) + data = urllib.parse.urlencode({ + 'client_id': client_id, + 'client_secret': client_secret, + 'refresh_token': refresh_token, + 'grant_type': 'refresh_token', + }).encode() + req = urllib.request.Request('https://oauth2.googleapis.com/token', data=data, method='POST') + req.add_header('Content-Type', 'application/x-www-form-urlencoded') + try: + with urllib.request.urlopen(req) as resp: + resp_data = json.load(resp) + except urllib.error.HTTPError as e: + sys.stderr.write(f'HTTP error {e.code}: {e.read().decode()}\n') + sys.exit(1) + access_token = resp_data.get('access_token') + if not access_token: + sys.stderr.write('No access_token in response\n') + sys.exit(1) + # Update the secrets.env file + env_path = os.path.expanduser('~/.config/google-calendar/secrets.env') + # Read existing lines, replace or add GOOGLE_ACCESS_TOKEN + lines = [] + if os.path.exists(env_path): + with open(env_path, 'r') as f: + lines = f.readlines() + new_lines = [] + token_set = False + for line in lines: + if line.startswith('export GOOGLE_ACCESS_TOKEN='): + new_lines.append(f'export GOOGLE_ACCESS_TOKEN={access_token}\n') + token_set = True + else: + new_lines.append(line) + if not token_set: + new_lines.append(f'export GOOGLE_ACCESS_TOKEN={access_token}\n') + with open(env_path, 'w') as f: + f.writelines(new_lines) + print(json.dumps(resp_data, indent=2)) + +if __name__ == '__main__': + refresh() diff --git a/scripts/venv/pyvenv.cfg b/scripts/venv/pyvenv.cfg new file mode 100644 index 0000000..8b600dc --- /dev/null +++ b/scripts/venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/adrian/.npm-global/lib/node_modules/openclaw/skills/google-calendar/scripts/venv