# Clawdbot Gateway WebSocket API 가이드

> 외부 애플리케이션에서 Clawdbot Gateway에 연결하여 AI 호출 또는 상태 조회하는 방법

## 개요

Clawdbot Gateway는 WebSocket API를 제공합니다. 이를 통해:
- **AI 호출** (`agent` 메서드) — Claude/GPT 등 모델에 질문 (토큰 소비)
- **상태 조회** (`sessions.list` 등) — 세션 정보 조회 (토큰 무소비)
- **세션 설정** (`sessions.patch`) — 모델 오버라이드 등

## 아키텍처

```
┌─────────────────┐     WebSocket      ┌─────────────────┐
│  Flask 서버     │ ◄─────────────────► │ Clawdbot Gateway│
│  (pharmacy-pos) │    Port 18789       │   (localhost)   │
└─────────────────┘                     └────────┬────────┘
                                                 │
                                        ┌────────▼────────┐
                                        │  Claude / GPT   │
                                        │   (Providers)   │
                                        └─────────────────┘
```

## 설정 파일 위치

Gateway 설정은 `~/.clawdbot/clawdbot.json`에 있음:
```json
{
  "gateway": {
    "port": 18789,
    "auth": {
      "mode": "token",
      "token": "your-gateway-token"
    }
  }
}
```

---

## 연결 프로토콜 (Python)

### 1. 기본 연결 흐름

```python
import asyncio
import json
import uuid
import websockets

async def connect_to_gateway():
    config = load_gateway_config()  # ~/.clawdbot/clawdbot.json 읽기
    url = f"ws://127.0.0.1:{config['port']}"
    token = config['token']
    
    async with websockets.connect(url) as ws:
        # 1단계: challenge 수신
        challenge = json.loads(await ws.recv())
        # {'event': 'connect.challenge', 'payload': {'nonce': '...'}}
        
        # 2단계: connect 요청
        connect_frame = {
            'type': 'req',
            'id': str(uuid.uuid4()),
            'method': 'connect',
            'params': {
                'minProtocol': 3,
                'maxProtocol': 3,
                'client': {
                    'id': 'gateway-client',      # 고정값
                    'displayName': 'My App',
                    'version': '1.0.0',
                    'platform': 'win32',
                    'mode': 'backend',           # 고정값
                    'instanceId': str(uuid.uuid4()),
                },
                'caps': [],
                'auth': {'token': token},
                'role': 'operator',
                'scopes': ['operator.admin'],    # 또는 ['operator.read']
            }
        }
        await ws.send(json.dumps(connect_frame))
        
        # 3단계: connect 응답 대기
        while True:
            msg = json.loads(await ws.recv())
            if msg.get('id') == connect_frame['id']:
                if msg.get('ok'):
                    print("연결 성공!")
                    break
                else:
                    print(f"연결 실패: {msg.get('error')}")
                    return
        
        # 이제 다른 메서드 호출 가능
        # ...
```

### 2. 주의사항: client 파라미터

⚠️ **중요**: `client.id`와 `client.mode`는 Gateway 스키마에 정의된 값만 허용됨

| 필드 | 허용되는 값 | 설명 |
|------|-------------|------|
| `client.id` | `'gateway-client'` | 백엔드 클라이언트용 |
| `client.mode` | `'backend'` | 백엔드 모드 |
| `role` | `'operator'` | 제어 클라이언트 |
| `scopes` | `['operator.admin']` 또는 `['operator.read']` | 권한 범위 |

잘못된 값 사용 시 에러:
```
invalid connect params: at /client/id: must be equal to constant
```

---

## 메서드 종류

### 토큰 소비 없는 메서드 (관리용)

| 메서드 | 용도 | 파라미터 |
|--------|------|----------|
| `sessions.list` | 세션 목록 조회 | `{limit: 10}` |
| `sessions.patch` | 세션 설정 변경 | `{key: '...', model: '...'}` |

### 토큰 소비하는 메서드 (AI 호출)

| 메서드 | 용도 | 파라미터 |
|--------|------|----------|
| `agent` | AI에게 질문 | `{message: '...', sessionId: '...'}` |

---

## 실제 구현 예제

### 예제 1: 상태 조회 (토큰 0)

```python
# services/clawdbot_client.py 참고

async def _get_gateway_status():
    """세션 목록 조회 — 토큰 소비 없음"""
    # ... (연결 코드 생략)
    
    # sessions.list 요청
    list_frame = {
        'type': 'req',
        'id': str(uuid.uuid4()),
        'method': 'sessions.list',
        'params': {'limit': 10}
    }
    await ws.send(json.dumps(list_frame))
    
    # 응답 대기
    while True:
        msg = json.loads(await ws.recv())
        if msg.get('event'):  # 이벤트는 무시
            continue
        if msg.get('id') == list_frame['id']:
            return msg.get('payload', {})
```

**응답 예시:**
```json
{
  "sessions": [
    {
      "key": "agent:main:main",
      "totalTokens": 30072,
      "contextTokens": 200000,
      "model": "claude-opus-4-5"
    }
  ],
  "defaults": {
    "model": "claude-opus-4-5",
    "contextTokens": 200000
  }
}
```

### 예제 2: AI 호출 (토큰 소비)

```python
async def ask_ai(message, session_id='my-session', model=None):
    """AI에게 질문 — 토큰 소비함"""
    # ... (연결 코드)
    
    # 모델 오버라이드 (선택)
    if model:
        patch_frame = {
            'type': 'req',
            'id': str(uuid.uuid4()),
            'method': 'sessions.patch',
            'params': {'key': session_id, 'model': model}
        }
        await ws.send(json.dumps(patch_frame))
        # 응답 대기...
    
    # agent 요청
    agent_frame = {
        'type': 'req',
        'id': str(uuid.uuid4()),
        'method': 'agent',
        'params': {
            'message': message,
            'sessionId': session_id,
            'sessionKey': session_id,
            'timeout': 60,
        }
    }
    await ws.send(json.dumps(agent_frame))
    
    # 응답 대기 (accepted → final)
    while True:
        msg = json.loads(await ws.recv())
        if msg.get('event'):
            continue
        if msg.get('id') == agent_frame['id']:
            if msg.get('payload', {}).get('status') == 'accepted':
                continue  # 아직 처리 중
            # 최종 응답
            payloads = msg.get('payload', {}).get('result', {}).get('payloads', [])
            return '\n'.join(p.get('text', '') for p in payloads)
```

### 예제 3: 모델 오버라이드

비싼 Opus 대신 저렴한 Sonnet 사용:

```python
UPSELL_MODEL = 'anthropic/claude-sonnet-4-5'

response = await ask_ai(
    message="추천 멘트 만들어줘",
    session_id='upsell-customer1',
    model=UPSELL_MODEL  # Sonnet으로 오버라이드
)
```

---

## Flask API 엔드포인트 예제

```python
# app.py

@app.route('/api/claude-status')
def api_claude_status():
    """토큰 차감 없이 상태 조회"""
    from services.clawdbot_client import get_claude_status
    
    status = get_claude_status()
    
    if not status.get('connected'):
        return jsonify({'ok': False, 'error': status.get('error')}), 503
    
    sessions = status.get('sessions', {})
    # ... 데이터 가공
    
    return jsonify({
        'ok': True,
        'context': {'used': 30000, 'max': 200000, 'percent': 15},
        'model': 'claude-opus-4-5'
    })
```

---

## 토큰 관리 전략

### 모델별 용도 분리

| 용도 | 모델 | 이유 |
|------|------|------|
| 메인 컨트롤러 | Claude Opus | 복잡한 추론, 도구 사용 |
| 단순 생성 (업셀링 등) | Claude Sonnet | 빠르고 저렴 |
| 코딩 작업 | GPT-5 Codex | 정식 지원, 안정적 |

### 세션 분리

```python
# 용도별 세션 ID 분리
ask_ai("...", session_id='upsell-고객명')      # 업셀링 전용
ask_ai("...", session_id='analysis-daily')    # 분석 전용
ask_ai("...", session_id='chat-main')         # 일반 대화
```

---

## 트러블슈팅

### 1. "invalid connect params" 에러

```
at /client/id: must be equal to constant
at /client/mode: must be equal to constant
```

**해결**: `client.id`는 `'gateway-client'`, `client.mode`는 `'backend'` 사용

### 2. Gateway 연결 실패

```python
ConnectionRefusedError: [WinError 10061]
```

**해결**: Clawdbot Gateway가 실행 중인지 확인
```bash
clawdbot gateway status
```

### 3. CLI 명령어가 hang됨

Clawdbot 내부(agent 세션)에서 `clawdbot status` 같은 CLI 호출하면 충돌.
→ WebSocket API 직접 사용할 것

---

## 파일 위치

```
pharmacy-pos-qr-system/
└── backend/
    └── services/
        └── clawdbot_client.py   # Gateway 클라이언트 구현
    └── app.py                   # Flask API (/api/claude-status)
```

---

## 참고 자료

- Clawdbot 문서: `C:\Users\청춘약국\AppData\Roaming\npm\node_modules\clawdbot\docs\`
- Gateway 프로토콜: `docs/gateway/protocol.md`
- 설정 예제: `docs/gateway/configuration-examples.md`

---

*작성: 2026-02-27 | 용림 🐉*
