feat: Clawdbot Gateway 모니터링 페이지 + API 클라이언트

- /admin/ai-gw: 토큰 사용량/비용 실시간 모니터링 대시보드
- clawdbot_client.py: Gateway HTTP API 클라이언트 (세션 상태, 사용량 조회)
- 세션별 토큰/비용 통계, 모델별 breakdown
- API 문서 추가 (docs/clawdbot-gateway-api.md)
This commit is contained in:
thug0bin
2026-02-27 12:22:05 +09:00
parent ccb0067a1c
commit 2a090c9704
3 changed files with 1017 additions and 0 deletions

View File

@@ -321,6 +321,122 @@ def generate_upsell_real(user_name, current_items, recent_products, available_pr
return _parse_upsell_response(response_text)
# ===== Claude 상태 조회 =====
async def _get_gateway_status():
"""
Clawdbot Gateway에서 세션 목록 조회
토큰 차감 없음 (AI 호출 아님)
"""
config = _load_gateway_config()
url = f"ws://127.0.0.1:{config['port']}"
token = config['token']
try:
async with websockets.connect(url, max_size=25 * 1024 * 1024,
close_timeout=5) as ws:
# 1. connect.challenge 대기
challenge_msg = await asyncio.wait_for(ws.recv(), timeout=10)
challenge = json.loads(challenge_msg)
nonce = None
if challenge.get('event') == 'connect.challenge':
nonce = challenge.get('payload', {}).get('nonce')
# 2. connect 요청
connect_id = str(uuid.uuid4())
connect_frame = {
'type': 'req',
'id': connect_id,
'method': 'connect',
'params': {
'minProtocol': 3,
'maxProtocol': 3,
'client': {
'id': 'gateway-client',
'displayName': 'Pharmacy Status',
'version': '1.0.0',
'platform': 'win32',
'mode': 'backend',
'instanceId': str(uuid.uuid4()),
},
'caps': [],
'auth': {'token': token},
'role': 'operator',
'scopes': ['operator.read'],
}
}
await ws.send(json.dumps(connect_frame))
# 3. connect 응답 대기
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(msg)
if data.get('id') == connect_id:
if not data.get('ok'):
error = data.get('error', {}).get('message', 'connect failed')
logger.warning(f"[Clawdbot] connect 실패: {error}")
return {'error': error, 'connected': False}
break
# 4. sessions.list 요청
list_id = str(uuid.uuid4())
list_frame = {
'type': 'req',
'id': list_id,
'method': 'sessions.list',
'params': {
'limit': 10
}
}
await ws.send(json.dumps(list_frame))
# 5. 응답 대기
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(msg)
# 이벤트 무시
if data.get('event'):
continue
if data.get('id') == list_id:
if data.get('ok'):
return {
'connected': True,
'sessions': data.get('payload', {})
}
else:
error = data.get('error', {}).get('message', 'unknown')
return {'error': error, 'connected': True}
except asyncio.TimeoutError:
logger.warning("[Clawdbot] Gateway 타임아웃")
return {'error': 'timeout', 'connected': False}
except (ConnectionRefusedError, OSError) as e:
logger.warning(f"[Clawdbot] Gateway 연결 실패: {e}")
return {'error': str(e), 'connected': False}
except Exception as e:
logger.warning(f"[Clawdbot] 상태 조회 실패: {e}")
return {'error': str(e), 'connected': False}
def get_claude_status():
"""
동기 래퍼: Claude 상태 조회
Returns:
dict: 상태 정보
"""
try:
loop = asyncio.new_event_loop()
result = loop.run_until_complete(_get_gateway_status())
loop.close()
return result
except Exception as e:
logger.warning(f"[Clawdbot] 상태 조회 실패: {e}")
return {'error': str(e), 'connected': False}
def _parse_upsell_response(text):
"""AI 응답에서 JSON 추출"""
import re