Initial commit with translated description
This commit is contained in:
63
SKILL.md
Normal file
63
SKILL.md
Normal file
@@ -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 <ISO> --to <ISO> --max <N>]
|
||||
google-calendar add --title <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.
|
||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7ez06mtd5e4r78d14pa6gp41808bw8",
|
||||
"slug": "google-calendar",
|
||||
"version": "0.1.0",
|
||||
"publishedAt": 1769879360981
|
||||
}
|
||||
145
scripts/google_calendar.py
Normal file
145
scripts/google_calendar.py
Normal file
@@ -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)
|
||||
51
scripts/refresh_token.py
Normal file
51
scripts/refresh_token.py
Normal file
@@ -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()
|
||||
5
scripts/venv/pyvenv.cfg
Normal file
5
scripts/venv/pyvenv.cfg
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user