(更新: 2026.04.08)

OpenClawで複数Googleアカウントを自然言語で管理する仕組みを作った

OpenClawで複数Googleアカウントを自然言語で管理する仕組みを作った

はじめに

「明日14時にA社とMTG、1時間」

こう言うだけでカレンダーに自動登録される——そんな仕組みを作りました。

複数のGoogleアカウント(仕事用・副業用など)を使い分けている人にとって、どのアカウントに登録するかを毎回選ぶのは面倒ですよね。そこで、自然言語から自動で判定して、適切なアカウントのカレンダーに登録するシステムを開発しました。

開発の背景

私は以下の2つのGoogleアカウントを日常的に使っています:

  1. クロスリンクアカウント(メイン業務)
  2. SEO記事作成、ブログ運用
  3. 顧客との打ち合わせ
  4. ラーニング事業部の定例会

  5. プログラミングスクールアカウント(副業)

  6. 体験会セールス
  7. シフト管理
  8. リーダーMTG

これらを別々に管理するのは非効率だったため、自然言語で投げたタスクを自動で適切なアカウントに振り分けるシステムを構築しました。

システム構成

アーキテクチャ

ユーザー(自然言語入力)
    ↓
OpenClaw(AIアシスタント)
    ↓
自然言語パーサー(日時・タイトル抽出)
    ↓
アカウント判定ロジック(キーワードマッチング)
    ↓
Google Calendar API(イベント作成)
    ↓
通知(Chatwork + iMessage)

技術スタック

  • Python 3.11
  • OpenClaw – 個人用AIアシスタント
  • Google Calendar API v3
  • 自然言語パーサー(dateparser + 独自実装)
  • Chatwork API – 通知用

実装のポイント

1. 自然言語パーサー

「明日14時にMTG、1時間」のような自然言語を解析してイベント情報を抽出します。

import re
from datetime import datetime, timedelta
from typing import Optional, Tuple
import dateparser

def parse_natural_language(text: str) -> dict:
    """
    自然言語からイベント情報を抽出

    例:
        "明日14時にA社とMTG、1時間"
        → {
            "summary": "A社とMTG",
            "start": "2026-03-21T14:00:00+09:00",
            "end": "2026-03-21T15:00:00+09:00"
        }
    """
    result = {
        "summary": None,
        "start": None,
        "end": None,
        "duration_minutes": None
    }

    # 1. 日時を抽出
    # "明日14時"、"来週水曜10時"、"3/25 9:30" などに対応
    start_time = extract_datetime(text)
    if start_time:
        result["start"] = start_time.isoformat()
    else:
        # デフォルトは明日9時
        tomorrow = datetime.now() + timedelta(days=1)
        result["start"] = tomorrow.replace(hour=9, minute=0, second=0).isoformat()

    # 2. 期間を抽出
    # "1時間"、"30分"、"2h" などに対応
    duration = extract_duration(text)
    if duration:
        result["duration_minutes"] = duration
        end_time = datetime.fromisoformat(result["start"]) + timedelta(minutes=duration)
        result["end"] = end_time.isoformat()
    else:
        # デフォルトは1時間
        result["duration_minutes"] = 60
        end_time = datetime.fromisoformat(result["start"]) + timedelta(hours=1)
        result["end"] = end_time.isoformat()

    # 3. タイトルを抽出
    # 日時・期間を除いた部分がタイトル
    summary = extract_summary(text, start_time, duration)
    result["summary"] = summary or "予定"

    return result

def extract_datetime(text: str) -> Optional[datetime]:
    """日時を抽出"""
    # 日本語の相対表現をサポート
    settings = {
        'TIMEZONE': 'Asia/Tokyo',
        'RETURN_AS_TIMEZONE_AWARE': True,
        'PREFER_DATES_FROM': 'future',  # 過去の日付を避ける
        'RELATIVE_BASE': datetime.now()
    }

    # "明日14時"、"来週水曜10時" などをパース
    parsed = dateparser.parse(text, settings=settings, languages=['ja'])

    if parsed:
        return parsed

    # パターンマッチング(backup)
    patterns = [
        r'(\d{1,2})/(\d{1,2})\s+(\d{1,2}):(\d{2})',  # 3/25 14:00
        r'(\d{1,2})月(\d{1,2})日\s+(\d{1,2})時',     # 3月25日 14時
        r'(\d{1,2})時',                               # 14時
    ]

    for pattern in patterns:
        match = re.search(pattern, text)
        if match:
            # ... (詳細は省略)
            return parsed_datetime

    return None

def extract_duration(text: str) -> Optional[int]:
    """期間を分単位で抽出"""
    patterns = [
        (r'(\d+)時間', lambda m: int(m.group(1)) * 60),
        (r'(\d+)分', lambda m: int(m.group(1))),
        (r'(\d+)h', lambda m: int(m.group(1)) * 60),
        (r'(\d+)m', lambda m: int(m.group(1))),
    ]

    for pattern, converter in patterns:
        match = re.search(pattern, text)
        if match:
            return converter(match)

    return None

def extract_summary(text: str, start_time: datetime, duration: int) -> str:
    """タイトルを抽出(日時・期間を除去)"""
    # 日時パターンを削除
    cleaned = re.sub(r'明日|今日|来週|再来週', '', text)
    cleaned = re.sub(r'\d{1,2}/\d{1,2}', '', cleaned)
    cleaned = re.sub(r'\d{1,2}:\d{2}', '', cleaned)
    cleaned = re.sub(r'\d{1,2}時\d{0,2}分?', '', cleaned)

    # 期間パターンを削除
    cleaned = re.sub(r'\d+時間', '', cleaned)
    cleaned = re.sub(r'\d+分', '', cleaned)
    cleaned = re.sub(r'、|,', '', cleaned)

    # 前後の空白を削除
    cleaned = cleaned.strip()

    return cleaned

ポイント
dateparser で「明日」「来週水曜」などの相対表現をサポート
– 正規表現でバックアップパターンを用意
– タイトルは日時・期間を除去した残りの部分

2. アカウント自動判定

キーワードマッチングで適切なアカウントを判定します。

from typing import Literal

AccountType = Literal['crosslink', 'programming_school']

def determine_account(text: str) -> AccountType:
    """
    テキストからアカウントを判定

    例:
        "A社とMTG" → 'crosslink'
        "体験会セールス" → 'programming_school'
    """
    # クロスリンクのキーワード
    crosslink_keywords = [
        # 顧客名
        'a社', 'b社', 'プロシーズ', 'サンヴァーテックス',
        # 業務
        'seo', 'ブログ', '記事', 'ラーニング', '朝会',
        '求人', '営業', '提案', '商談',
    ]

    # プログラミングスクールのキーワード
    school_keywords = [
        # 業務
        '体験会', 'ココグラム', 'シフト', '座席表',
        'スクール', '研修',
        # 地域
        '名古屋', '関西', '東海', '大阪', '門真', '一宮',
        # 人名
        'リーダー', 'センス社長', '久保社長',
    ]

    text_lower = text.lower()

    # キーワードマッチング
    crosslink_score = sum(1 for kw in crosslink_keywords if kw in text_lower)
    school_score = sum(1 for kw in school_keywords if kw in text_lower)

    if crosslink_score > school_score:
        return 'crosslink'
    elif school_score > crosslink_score:
        return 'programming_school'
    else:
        # 同点の場合はデフォルト
        return 'crosslink'

def get_account_email(account: AccountType) -> str:
    """アカウント名からメールアドレスを取得"""
    mapping = {
        'crosslink': 'work@example.com',
        'programming_school': 'school@example.com'
    }
    return mapping[account]

ポイント
– キーワードのスコアリングで判定
– 顧客名・業務名・地域名を網羅
– 判定が曖昧な場合はデフォルトアカウント

3. OpenClaw統合

OpenClawから呼び出せるように、ヘルパー関数を実装します。

def add_event_from_text(text: str, confirm: bool = True) -> dict:
    """
    自然言語からイベントを作成(OpenClaw用インターフェース)

    Args:
        text: 自然言語入力(例: "明日14時にMTG、1時間")
        confirm: Trueなら確認、Falseなら即登録

    Returns:
        {
            "event_id": "...",
            "summary": "MTG",
            "start": "2026-03-21T14:00:00+09:00",
            "end": "2026-03-21T15:00:00+09:00",
            "account": "crosslink",
            "html_link": "https://calendar.google.com/..."
        }
    """
    # 1. 自然言語パース
    event_info = parse_natural_language(text)

    # 2. アカウント判定
    account = determine_account(text)
    account_email = get_account_email(account)

    # 3. 確認フロー
    if confirm:
        # OpenClawに確認メッセージを返す
        confirmation = {
            "needs_confirmation": True,
            "message": f"""
以下の予定を登録しますか?

📝 タイトル: {event_info['summary']}
📅 日時: {format_datetime(event_info['start'])}
⏱️ 期間: {event_info['duration_minutes']}分
🏢 アカウント: {get_account_name(account)}

よろしければ「OK」と返信してください。
            """,
            "pending_data": {
                "event_info": event_info,
                "account": account
            }
        }
        return confirmation

    # 4. カレンダーに登録
    service = authenticate(account)

    event_body = {
        'summary': event_info['summary'],
        'start': {
            'dateTime': event_info['start'],
            'timeZone': 'Asia/Tokyo',
        },
        'end': {
            'dateTime': event_info['end'],
            'timeZone': 'Asia/Tokyo',
        },
    }

    event = service.events().insert(
        calendarId='primary',
        body=event_body
    ).execute()

    # 5. 通知
    notify_event_created(event, account)

    return {
        "event_id": event['id'],
        "summary": event['summary'],
        "start": event['start']['dateTime'],
        "end": event['end']['dateTime'],
        "account": account,
        "html_link": event.get('htmlLink')
    }

4. 通知機能

Chatwork + iMessage で通知を送信します。

import requests
from typing import Optional

def notify_event_created(event: dict, account: str):
    """イベント作成をChatwork + iMessageに通知"""
    account_name = get_account_name(account)

    message = f"""
✅ カレンダーに登録しました!

📝 {event['summary']}
📅 {format_datetime(event['start']['dateTime'])}
🏢 アカウント: {account_name}
🔗 {event.get('htmlLink', '')}
    """.strip()

    # Chatwork通知
    send_chatwork(message)

    # iMessage通知(macOSのみ)
    send_imessage(message)

def send_chatwork(message: str):
    """Chatwork APIで通知"""
    api_token = os.getenv('CHATWORK_API_TOKEN')
    room_id = os.getenv('CHATWORK_ROOM_ID')

    if not api_token or not room_id:
        return

    url = f'https://api.chatwork.com/v2/rooms/{room_id}/messages'
    headers = {'X-ChatWorkToken': api_token}
    data = {'body': message}

    try:
        response = requests.post(url, headers=headers, data=data)
        response.raise_for_status()
    except Exception as e:
        print(f"Chatwork notification failed: {e}")

def send_imessage(message: str):
    """iMessageで通知(macOSのみ)"""
    recipient = os.getenv('IMESSAGE_RECIPIENT')

    if not recipient:
        return

    # AppleScriptでiMessage送信
    script = f'''
    tell application "Messages"
        set targetService to 1st service whose service type = iMessage
        set targetBuddy to buddy "{recipient}" of targetService
        send "{message}" to targetBuddy
    end tell
    '''

    try:
        subprocess.run(['osascript', '-e', script], check=True)
    except Exception as e:
        print(f"iMessage notification failed: {e}")

実際の使用例

例1: シンプルな予定

👤 「明日14時にMTG、1時間」

🤖 以下の予定を登録しますか?

   📝 タイトル: MTG
   📅 日時: 2026-03-21 14:00
   ⏱️ 期間: 60分
   🏢 アカウント: クロスリンク

   よろしければ「OK」と返信してください。

👤 「OK」

🤖 ✅ カレンダーに登録しました!

例2: キーワードで自動判定

👤 「来週水曜10時に体験会セールス、2時間」

🤖 以下の予定を登録しますか?

   📝 タイトル: 体験会セールス
   📅 日時: 2026-03-26 10:00
   ⏱️ 期間: 120分
   🏢 アカウント: プログラミングスクール ← 自動判定

   よろしければ「OK」と返信してください。

例3: 明示的にアカウント指定

👤 「明日9:30に新ラーニング朝会、1時間、クロスリンク」

🤖 ✅ カレンダーに登録しました! ← 即登録(確認スキップ)

追加機能

1. 定期予定の作成

def create_recurring_event(text: str, recurrence_rule: str):
    """
    定期予定を作成

    Args:
        text: "毎週水曜10時にチームMTG、1時間"
        recurrence_rule: "RRULE:FREQ=WEEKLY;BYDAY=WE"
    """
    event_info = parse_natural_language(text)
    account = determine_account(text)
    service = authenticate(account)

    event_body = {
        'summary': event_info['summary'],
        'start': {
            'dateTime': event_info['start'],
            'timeZone': 'Asia/Tokyo',
        },
        'end': {
            'dateTime': event_info['end'],
            'timeZone': 'Asia/Tokyo',
        },
        'recurrence': [recurrence_rule],  # 定期設定
    }

    event = service.events().insert(
        calendarId='primary',
        body=event_body
    ).execute()

    return event

2. 予定のテンプレート

def create_from_template(template_name: str, start_time: str):
    """
    テンプレートから予定を作成

    例: "定例MTG" テンプレート
    → タイトル、期間、参加者が自動設定
    """
    templates = {
        "定例MTG": {
            "summary": "週次定例MTG",
            "duration_minutes": 60,
            "account": "crosslink",
            "location": "オンライン",
            "description": "議題: TBD"
        },
        "体験会": {
            "summary": "プログラミング体験会",
            "duration_minutes": 120,
            "account": "programming_school",
            "location": "ココグラム名古屋校",
        }
    }

    template = templates.get(template_name)
    if not template:
        raise ValueError(f"Unknown template: {template_name}")

    # ... (イベント作成)

セキュリティ対策

1. OAuth 2.0 トークンの管理

# トークンファイルのパーミッション制限
chmod 600 token_*.json

# .gitignoreで除外
echo "token_*.json" >> .gitignore
echo "credentials.json" >> .gitignore

2. 環境変数での管理

# .env ファイル
CHATWORK_API_TOKEN=your_token_here
CHATWORK_ROOM_ID=your_room_id_here
IMESSAGE_RECIPIENT=受信者の名前

まとめ

自然言語カレンダー登録システムで得られた効果:

入力が圧倒的に楽になった
→ 「明日14時にMTG」だけでOK

アカウント切り替えが不要に
→ キーワードで自動判定

確認フローで誤登録を防止
→ 明示的指定なら即登録も可能

通知で見逃しを防ぐ
→ Chatwork + iMessage で確実に届く

今後の改善予定:
– 音声入力対応(Siri shortcuts)
– 会議室の自動予約
– 参加者の自動招待
– スケジュール最適化提案

開発期間: 約2週間
コード行数: 約1,500行
使用API: Google Calendar API v3, Chatwork API


自然言語処理とカレンダー連携は、日常業務の効率化に直結します。ぜひ試してみてください!