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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user