From 38711545095e8938ba30ba60a0e310cd1cb58e9b Mon Sep 17 00:00:00 2001 From: thug0bin Date: Sat, 28 Mar 2026 12:42:01 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20PAAI=20OpenClaw=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20(WebSocket=20->=20CLI?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OpenClaw 업데이트로 device identity 필수화됨 - WebSocket 대신 Node.js 직접 호출로 변경 - 특수문자/줄바꿈 문제 해결 (shell=True 제거) - subprocess array 방식으로 안전한 인자 전달 --- backend/services/clawdbot_client.py | 100 ++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/backend/services/clawdbot_client.py b/backend/services/clawdbot_client.py index 5b56883..5c94f72 100644 --- a/backend/services/clawdbot_client.py +++ b/backend/services/clawdbot_client.py @@ -25,23 +25,29 @@ import websockets logger = logging.getLogger(__name__) -# Gateway 설정 (clawdbot.json에서 읽기) +# Gateway 설정 (openclaw.json 또는 clawdbot.json에서 읽기) +OPENCLAW_CONFIG_PATH = Path.home() / '.openclaw' / 'openclaw.json' CLAWDBOT_CONFIG_PATH = Path.home() / '.clawdbot' / 'clawdbot.json' def _load_gateway_config(): - """clawdbot.json에서 Gateway 설정 로드""" - try: - with open(CLAWDBOT_CONFIG_PATH, 'r', encoding='utf-8') as f: - config = json.load(f) - gw = config.get('gateway', {}) - return { - 'port': gw.get('port', 18789), - 'token': gw.get('auth', {}).get('token', ''), - } - except Exception as e: - logger.warning(f"[Clawdbot] 설정 파일 로드 실패: {e}") - return {'port': 18789, 'token': ''} + """OpenClaw/Clawdbot Gateway 설정 로드""" + for config_path in [OPENCLAW_CONFIG_PATH, CLAWDBOT_CONFIG_PATH]: + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + gw = config.get('gateway', {}) + token = gw.get('auth', {}).get('token', '') + if token: + logger.info(f"[Gateway] 설정 로드: {config_path.name}") + return { + 'port': gw.get('port', 18789), + 'token': token, + } + except Exception: + continue + logger.warning("[Gateway] 설정 파일 로드 실패") + return {'port': 18789, 'token': ''} async def _ask_gateway(message, session_id='pharmacy-upsell', @@ -85,10 +91,10 @@ async def _ask_gateway(message, session_id='pharmacy-upsell', 'maxProtocol': 3, 'client': { 'id': 'gateway-client', - 'displayName': 'Pharmacy Upsell', + 'displayName': 'Pharmacy PAAI', 'version': '1.0.0', 'platform': 'win32', - 'mode': 'backend', + 'mode': 'cli', 'instanceId': str(uuid.uuid4()), }, 'caps': [], @@ -96,7 +102,7 @@ async def _ask_gateway(message, session_id='pharmacy-upsell', 'token': token, }, 'role': 'operator', - 'scopes': ['operator.admin'], + 'scopes': ['operator.admin', 'operator.write', 'operator.read'], } } await ws.send(json.dumps(connect_frame)) @@ -198,27 +204,65 @@ async def _ask_gateway(message, session_id='pharmacy-upsell', def ask_clawdbot(message, session_id='pharmacy-upsell', system_prompt=None, timeout=60, model=None): """ - 동기 래퍼: Flask에서 직접 호출 가능 - + OpenClaw CLI를 통한 AI 호출 (WebSocket 대신) + Args: message: 사용자 메시지 session_id: 세션 ID (대화 구분용) - system_prompt: 추가 시스템 프롬프트 + system_prompt: 추가 시스템 프롬프트 (현재 미사용) timeout: 타임아웃 (초) - model: 모델 오버라이드 (예: 'anthropic/claude-sonnet-4-5') + model: 모델 오버라이드 (현재 미사용 - CLI가 기본 모델 사용) Returns: str: AI 응답 텍스트 (실패 시 None) """ + import subprocess + import os + from pathlib import Path + try: - loop = asyncio.new_event_loop() - result = loop.run_until_complete( - _ask_gateway(message, session_id, system_prompt, timeout, model=model) + # Node.js로 OpenClaw 직접 호출 (shell 없이, 특수문자 안전) + node_path = r'C:\Program Files\nodejs\node.exe' + openclaw_path = str(Path.home() / 'AppData/Roaming/npm/node_modules/openclaw/openclaw.mjs') + + cmd = [node_path, openclaw_path, 'agent', '-m', message, '--session-id', session_id, '--json'] + logger.info(f"[OpenClaw] session={session_id}, msg_len={len(message)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout + 30, + encoding='utf-8', + env={**os.environ, 'PYTHONIOENCODING': 'utf-8'} ) - loop.close() - return result + + if result.returncode != 0: + logger.warning(f"[OpenClaw] CLI 에러: {result.stderr}") + # 에러가 있어도 stdout에 결과가 있을 수 있음 + if not result.stdout: + return None + + # JSON 파싱 + data = json.loads(result.stdout) + if data.get('status') == 'ok': + payloads = data.get('result', {}).get('payloads', []) + if payloads: + text = payloads[0].get('text', '') + logger.info(f"[OpenClaw] 응답 수신: {len(text)}자") + return text + + logger.warning(f"[OpenClaw] 응답 없음: {data}") + return None + + except subprocess.TimeoutExpired: + logger.warning(f"[OpenClaw] 타임아웃 ({timeout}초)") + return None + except json.JSONDecodeError as e: + logger.warning(f"[OpenClaw] JSON 파싱 실패: {e}") + return None except Exception as e: - logger.warning(f"[Clawdbot] 호출 실패: {e}") + logger.warning(f"[OpenClaw] 호출 실패: {e}") return None @@ -356,13 +400,13 @@ async def _get_gateway_status(): 'displayName': 'Pharmacy Status', 'version': '1.0.0', 'platform': 'win32', - 'mode': 'backend', + 'mode': 'cli', 'instanceId': str(uuid.uuid4()), }, 'caps': [], 'auth': {'token': token}, 'role': 'operator', - 'scopes': ['operator.read'], + 'scopes': ['operator.admin', 'operator.write', 'operator.read'], } } await ws.send(json.dumps(connect_frame))