fix: PAAI OpenClaw 호출 방식 변경 (WebSocket -> CLI)

- OpenClaw 업데이트로 device identity 필수화됨
- WebSocket 대신 Node.js 직접 호출로 변경
- 특수문자/줄바꿈 문제 해결 (shell=True 제거)
- subprocess array 방식으로 안전한 인자 전달
This commit is contained in:
thug0bin
2026-03-28 12:42:01 +09:00
parent f855fc5916
commit 3871154509

View File

@@ -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))