Compare commits

...

4 Commits

Author SHA1 Message Date
thug0bin
3871154509 fix: PAAI OpenClaw 호출 방식 변경 (WebSocket -> CLI)
- OpenClaw 업데이트로 device identity 필수화됨
- WebSocket 대신 Node.js 직접 호출로 변경
- 특수문자/줄바꿈 문제 해결 (shell=True 제거)
- subprocess array 방식으로 안전한 인자 전달
2026-03-28 12:42:01 +09:00
thug0bin
f855fc5916 feat: OTC 라벨 프리셋 확인 API + 인쇄 조건 강화
- GET /api/otc-label-check - 바코드 배열로 프리셋 존재 여부 일괄 확인
- 인쇄 API: 프리셋 없으면 인쇄 안 함 (404 반환)
- POS 장바구니에서 인쇄 버튼 활성화 판단용
2026-03-14 00:36:57 +09:00
thug0bin
cb7450f654 feat: OTC 라벨 바로 인쇄 API 추가 (CORS 지원)
- GET /api/otc-label-print/{barcode} - 바로 인쇄
- 프리셋 있으면 해당 데이터로, 없으면 약품명만으로 인쇄
- 인쇄 횟수 자동 카운트
2026-03-14 00:18:58 +09:00
thug0bin
67fb7bf937 feat: 동물약 안내서/제품 이미지 외부 API 추가 (CORS 지원)
- GET /api/animal-drug-print/{apc|barcode} - 바로 인쇄
- GET /api/product-image/{barcode} - 제품 이미지 반환
- GET /api/product-image-info/{barcode} - 이미지 메타데이터
- 바코드→APC 자동 변환 지원
2026-03-14 00:18:04 +09:00
2 changed files with 700 additions and 29 deletions

View File

@@ -16,7 +16,7 @@ if sys.platform == 'win32':
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
from flask import Flask, request, render_template, render_template_string, jsonify, redirect, url_for, session
from flask import Flask, request, render_template, render_template_string, jsonify, redirect, url_for, session, make_response
import hashlib
import base64
import secrets
@@ -7846,6 +7846,179 @@ def api_print_otc_label():
return jsonify({'success': False, 'error': str(e)}), 500
# ═══════════════════════════════════════════════════════════════════════════════
# OTC 라벨 바로 인쇄 API (GET - URL로 바로 인쇄) - 외부 CORS 지원
# ═══════════════════════════════════════════════════════════════════════════════
@app.route('/api/otc-label-print/<barcode>')
def api_otc_label_print_by_barcode(barcode):
"""
OTC 라벨 바로 인쇄 (GET 방식)
GET /api/otc-label-print/8806436044814
- CORS 지원 (외부에서 호출 가능)
- 저장된 프리셋이 있으면 해당 데이터로 인쇄
- 프리셋이 없으면 MSSQL에서 약품명 조회 후 기본 라벨 인쇄
"""
try:
if not OTC_LABEL_AVAILABLE:
response = jsonify({'success': False, 'error': 'OTC 라벨 모듈이 로드되지 않았습니다.'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
# 1. 프리셋 조회
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT display_name, effect, dosage_instruction, usage_tip
FROM otc_label_presets
WHERE barcode = ?
""", (barcode,))
preset = cursor.fetchone()
if preset:
# 프리셋 있음 → 프리셋 데이터로 인쇄
drug_name = preset['display_name']
effect = preset['effect'] or ''
dosage_instruction = preset['dosage_instruction'] or ''
usage_tip = preset['usage_tip'] or ''
else:
# 프리셋 없음 → 인쇄 안 함
response = jsonify({
'success': False,
'error': f'바코드 {barcode}에 등록된 라벨 프리셋이 없습니다.',
'barcode': barcode,
'has_preset': False
})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 404
# 2. 인쇄 실행
success = print_otc_label(drug_name, effect, dosage_instruction, usage_tip)
if success:
# 인쇄 횟수 업데이트 (프리셋 있는 경우)
if preset:
cursor.execute("""
UPDATE otc_label_presets
SET print_count = print_count + 1, last_printed_at = CURRENT_TIMESTAMP
WHERE barcode = ?
""", (barcode,))
conn.commit()
response = jsonify({
'success': True,
'message': f'OTC 라벨 인쇄 완료: {drug_name}',
'barcode': barcode,
'drug_name': drug_name,
'has_preset': preset is not None
})
response.headers['Access-Control-Allow-Origin'] = '*'
return response
else:
response = jsonify({'success': False, 'error': '프린터 전송 실패'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
except Exception as e:
logging.error(f"OTC 라벨 GET 인쇄 오류: {e}")
import traceback
traceback.print_exc()
response = jsonify({'success': False, 'error': str(e)})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
@app.route('/api/otc-label-print/<barcode>', methods=['OPTIONS'])
def api_otc_label_print_options(barcode):
"""CORS preflight 요청 처리"""
response = make_response()
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
@app.route('/api/otc-label-check', methods=['GET', 'POST'])
def api_otc_label_check():
"""
OTC 라벨 프리셋 존재 여부 일괄 확인 API
GET /api/otc-label-check?barcodes=8806436044814,8806436058613
POST /api/otc-label-check {"barcodes": ["8806436044814", "8806436058613"]}
- CORS 지원 (외부에서 호출 가능)
- 장바구니 등에서 인쇄 버튼 활성화 여부 판단용
"""
try:
# 바코드 목록 파싱
if request.method == 'POST':
data = request.get_json() or {}
barcodes = data.get('barcodes', [])
else:
barcodes_param = request.args.get('barcodes', '')
barcodes = [b.strip() for b in barcodes_param.split(',') if b.strip()]
if not barcodes:
response = jsonify({'success': False, 'error': '바코드 목록이 필요합니다'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 400
# 프리셋 존재 여부 확인
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
placeholders = ','.join(['?' for _ in barcodes])
cursor.execute(f"""
SELECT barcode, display_name
FROM otc_label_presets
WHERE barcode IN ({placeholders})
""", barcodes)
preset_map = {row['barcode']: row['display_name'] for row in cursor.fetchall()}
# 결과 생성
results = []
printable_count = 0
for barcode in barcodes:
has_preset = barcode in preset_map
if has_preset:
printable_count += 1
results.append({
'barcode': barcode,
'has_preset': has_preset,
'display_name': preset_map.get(barcode),
'printable': has_preset
})
response = jsonify({
'success': True,
'total': len(barcodes),
'printable_count': printable_count,
'results': results
})
response.headers['Access-Control-Allow-Origin'] = '*'
return response
except Exception as e:
logging.error(f"OTC 라벨 체크 오류: {e}")
response = jsonify({'success': False, 'error': str(e)})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
@app.route('/api/otc-label-check', methods=['OPTIONS'])
def api_otc_label_check_options():
"""CORS preflight 요청 처리"""
response = make_response()
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
@app.route('/api/admin/otc-labels/search-mssql', methods=['GET'])
def api_search_mssql_drug():
"""MSSQL에서 약품 검색 (바코드 또는 이름)"""
@@ -8535,6 +8708,323 @@ def api_animal_drug_info_print():
return jsonify({'success': False, 'error': str(e)}), 500
# ═══════════════════════════════════════════════════════════════════════════════
# 동물약 정보 인쇄 API (GET - URL로 바로 인쇄) - 외부 CORS 지원
# ═══════════════════════════════════════════════════════════════════════════════
@app.route('/api/animal-drug-print/<code>')
def api_animal_drug_print_by_code(code):
"""
동물약 안내서 바로 인쇄 (GET 방식)
GET /api/animal-drug-print/0231093520106 (APC 코드)
GET /api/animal-drug-print/8809720800035 (바코드)
- CORS 지원 (외부에서 호출 가능)
- APC 또는 바코드로 조회 후 ESC/POS 프린터로 인쇄
"""
try:
import re
from html import unescape
apc = None
product_name = None
# 코드 타입 판별 (APC vs 바코드)
if code.startswith('023'):
# APC 코드
apc = code
else:
# 바코드로 가정 → APC 조회
try:
conn_str = (
'DRIVER={ODBC Driver 17 for SQL Server};'
'SERVER=192.168.0.4\\PM2014;'
'DATABASE=PM_DRUG;'
'UID=sa;'
'PWD=tmddls214!%(;'
'TrustServerCertificate=yes'
)
import pyodbc
conn = pyodbc.connect(conn_str, timeout=10)
cursor = conn.cursor()
# 바코드 → APC 조회
cursor.execute("""
SELECT TOP 1
U.CD_CD_BARCODE as apc,
G.GoodsName as product_name
FROM CD_ITEM_UNIT_MEMBER U
JOIN CD_GOODS G ON U.DRUGCODE = G.DrugCode
WHERE U.CD_CD_BARCODE = ? OR G.BARCODE = ?
""", (code, code))
row = cursor.fetchone()
conn.close()
if row and row.apc:
apc = row.apc
product_name = row.product_name
else:
response = jsonify({'success': False, 'error': f'바코드 {code}에 해당하는 APC를 찾을 수 없습니다'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 404
except Exception as e:
logging.error(f"바코드→APC 조회 오류: {e}")
response = jsonify({'success': False, 'error': f'바코드 조회 오류: {str(e)}'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
if not apc:
response = jsonify({'success': False, 'error': 'APC 코드가 필요합니다'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 400
# PostgreSQL에서 약품 정보 조회
try:
from sqlalchemy import create_engine
pg_engine = create_engine(
'postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master',
connect_args={'connect_timeout': 5}
)
with pg_engine.connect() as conn:
result = conn.execute(text("""
SELECT
a.product_name,
a.company_name,
a.main_ingredient,
a.efficacy_effect,
a.dosage_instructions,
a.precautions,
a.weight_min_kg,
a.weight_max_kg,
a.pet_size_label,
a.component_code,
g.component_name_ko,
g.dosing_interval_adult,
g.dosing_interval_high_risk,
g.dosing_interval_puppy,
g.companion_drugs
FROM apc a
LEFT JOIN component_guide g ON a.component_code = g.component_code
WHERE a.apc = :apc
LIMIT 1
"""), {'apc': apc})
row = result.fetchone()
# 포장단위 APC → 대표 APC 폴백
if not row and len(apc) == 13 and apc.startswith('023'):
item_prefix = apc[:8]
result = conn.execute(text("""
SELECT
a.product_name, a.company_name, a.main_ingredient,
a.efficacy_effect, a.dosage_instructions, a.precautions,
a.weight_min_kg, a.weight_max_kg, a.pet_size_label,
a.component_code,
g.component_name_ko, g.dosing_interval_adult,
g.dosing_interval_high_risk, g.dosing_interval_puppy,
g.companion_drugs
FROM apc a
LEFT JOIN component_guide g ON a.component_code = g.component_code
WHERE a.apc LIKE :prefix
ORDER BY LENGTH(a.apc)
LIMIT 1
"""), {'prefix': f'{item_prefix}%'})
row = result.fetchone()
if not row:
response = jsonify({'success': False, 'error': f'APC {apc} 정보를 찾을 수 없습니다'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 404
except Exception as e:
logging.error(f"PostgreSQL 조회 오류: {e}")
response = jsonify({'success': False, 'error': f'DB 조회 오류: {str(e)}'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
# HTML 태그 제거 함수
def strip_html(html_text):
if not html_text:
return ''
text = re.sub(r'</p>|<br\s*/?>|</div>', '\n', html_text)
text = re.sub(r'<[^>]+>', '', text)
text = unescape(text)
if '' in text or '' in text or ('======' in text and '------' in text):
lines = text.split('\n')
cleaned = [line.strip() for line in lines if line.strip()]
return '\n'.join(cleaned)
else:
text = re.sub(r'\s+', ' ', text).strip()
return text
# 항목별 줄바꿈 처리
def format_for_print(text):
if not text:
return ''
text = re.sub(r'\s*(가|나|다|라|마|바|사|아|자)\.\s*', r'\n\1. ', text)
text = re.sub(r'\s+(\d+)\)\s*', r'\n \1) ', text)
return text.strip()
# 텍스트 줄바꿈
def wrap_text(text, width=40):
lines = []
words = text.split()
current_line = ""
for word in words:
if len(current_line) + len(word) + 1 <= width:
current_line += (" " if current_line else "") + word
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
return lines
# 데이터 파싱
pg_product_name = row.product_name or product_name or ''
company = row.company_name or ''
ingredient = row.main_ingredient or ''
efficacy = strip_html(row.efficacy_effect)
dosage = strip_html(row.dosage_instructions)
precautions = strip_html(row.precautions)
# 80mm 프린터 = 48자 기준
LINE = "=" * 48
THIN = "-" * 48
message = f"""
{LINE}
[ 애니팜 투약지도서 ]
{LINE}
{pg_product_name}
"""
if company:
message += f"제조: {company}\n"
if ingredient and ingredient != 'NaN':
message += f"""
{THIN}
▶ 주성분
"""
for line in wrap_text(ingredient, 46):
message += f" {line}\n"
if efficacy:
message += f"""
{THIN}
▶ 효능효과
"""
formatted_efficacy = format_for_print(efficacy)
for para in formatted_efficacy.split('\n'):
for line in wrap_text(para.strip(), 44):
message += f" {line}\n"
if dosage:
message += f"""
{THIN}
▶ 용법용량
"""
has_box_table = '' in dosage or '' in dosage
has_ascii_table = '======' in dosage and '------' in dosage
if has_box_table:
for line in dosage.split('\n'):
line = line.strip()
if line:
message += f"{line}\n"
elif has_ascii_table:
for line in dosage.split('\n'):
stripped = line.strip()
if not stripped:
continue
if stripped.startswith('===') or stripped.startswith('---'):
message += f" {'' * 44}\n"
else:
message += f" {stripped}\n"
else:
formatted_dosage = format_for_print(dosage)
for para in formatted_dosage.split('\n'):
for line in wrap_text(para.strip(), 44):
message += f" {line}\n"
# 투약 주기
if row.dosing_interval_adult:
message += f"""
{THIN}
★ 투약 주기 ★
"""
message += f" 일반: {row.dosing_interval_adult}\n"
if row.dosing_interval_high_risk:
message += f" 고위험: {row.dosing_interval_high_risk}\n"
if row.dosing_interval_puppy:
message += f" 새끼: {row.dosing_interval_puppy}\n"
# 병용약 권장
if row.companion_drugs:
message += f"""
{THIN}
★ 함께 투약 권장 ★
"""
for line in wrap_text(row.companion_drugs, 44):
message += f" {line}\n"
# 주의사항
if precautions:
message += f"""
{THIN}
▶ 주의사항
"""
formatted_precautions = format_for_print(precautions)
for para in formatted_precautions.split('\n'):
for line in wrap_text(para.strip(), 44):
message += f" {line}\n"
message += f"""
{LINE}
청 춘 약 국
Tel: 033-481-5222
"""
# 네트워크 프린터로 인쇄
from pos_printer import print_text
result = print_text(message, cut=True)
if result:
response = jsonify({
'success': True,
'message': '동물약 안내서 인쇄 완료',
'apc': apc,
'product_name': pg_product_name
})
response.headers['Access-Control-Allow-Origin'] = '*'
return response
else:
response = jsonify({'success': False, 'error': '프린터 출력 실패'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
except Exception as e:
logging.error(f"동물약 GET 인쇄 API 오류: {e}")
import traceback
traceback.print_exc()
response = jsonify({'success': False, 'error': str(e)})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
@app.route('/api/animal-drug-print/<code>', methods=['OPTIONS'])
def api_animal_drug_print_options(code):
"""CORS preflight 요청 처리"""
response = make_response()
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
@app.route('/api/animal-drug-info/preview', methods=['POST'])
def api_animal_drug_info_preview():
"""동물약 정보 미리보기 (텍스트 반환)"""
@@ -10386,6 +10876,143 @@ def api_stock_forecast():
return jsonify({'success': False, 'error': str(e)}), 500
# ══════════════════════════════════════════════════════════════════════════════
# 바코드로 제품 이미지 조회 API (외부 CORS 지원)
# ══════════════════════════════════════════════════════════════════════════════
@app.route('/api/product-image/<barcode>')
def api_product_image_by_barcode(barcode):
"""
바코드로 제품 이미지 반환 API
GET /api/product-image/8806265001309
GET /api/product-image/8806265001309?type=thumbnail
- CORS 지원 (외부에서 호출 가능)
- type=thumbnail: 썸네일 반환
- 이미지가 없으면 404
"""
try:
img_type = request.args.get('type', 'full') # full 또는 thumbnail
img_db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db')
conn = sqlite3.connect(img_db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if img_type == 'thumbnail':
cursor.execute("""
SELECT thumbnail_base64, product_name
FROM product_images
WHERE barcode = ? AND thumbnail_base64 IS NOT NULL
""", (barcode,))
col_name = 'thumbnail_base64'
else:
cursor.execute("""
SELECT image_base64, product_name
FROM product_images
WHERE barcode = ? AND image_base64 IS NOT NULL
""", (barcode,))
col_name = 'image_base64'
row = cursor.fetchone()
conn.close()
if not row or not row[col_name]:
response = jsonify({'success': False, 'error': 'Image not found'})
response.status_code = 404
response.headers['Access-Control-Allow-Origin'] = '*'
return response
# base64 디코딩
import base64
image_data = base64.b64decode(row[col_name])
# 이미지 응답 생성
response = make_response(image_data)
response.headers['Content-Type'] = 'image/jpeg'
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response.headers['Cache-Control'] = 'public, max-age=86400' # 24시간 캐시
response.headers['X-Product-Name'] = row['product_name'].encode('utf-8').decode('latin-1') if row['product_name'] else ''
return response
except Exception as e:
logging.error(f"product-image API 오류: {e}")
response = jsonify({'success': False, 'error': str(e)})
response.status_code = 500
response.headers['Access-Control-Allow-Origin'] = '*'
return response
@app.route('/api/product-image/<barcode>', methods=['OPTIONS'])
def api_product_image_options(barcode):
"""CORS preflight 요청 처리"""
response = make_response()
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
@app.route('/api/product-image-info/<barcode>')
def api_product_image_info(barcode):
"""
바코드로 제품 이미지 정보 조회 (JSON)
GET /api/product-image-info/8806265001309
이미지 존재 여부, 제품명, URL 등 메타데이터 반환
"""
try:
img_db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db')
conn = sqlite3.connect(img_db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT barcode, drug_code, product_name, image_url, source, status,
CASE WHEN image_base64 IS NOT NULL THEN 1 ELSE 0 END as has_image,
CASE WHEN thumbnail_base64 IS NOT NULL THEN 1 ELSE 0 END as has_thumbnail,
created_at, updated_at
FROM product_images
WHERE barcode = ?
""", (barcode,))
row = cursor.fetchone()
conn.close()
if not row:
response = jsonify({'success': False, 'error': 'Product not found'})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 404
response = jsonify({
'success': True,
'data': {
'barcode': row['barcode'],
'drug_code': row['drug_code'],
'product_name': row['product_name'],
'image_url': f"/api/product-image/{row['barcode']}",
'thumbnail_url': f"/api/product-image/{row['barcode']}?type=thumbnail",
'original_url': row['image_url'],
'source': row['source'],
'status': row['status'],
'has_image': bool(row['has_image']),
'has_thumbnail': bool(row['has_thumbnail']),
'created_at': row['created_at'],
'updated_at': row['updated_at']
}
})
response.headers['Access-Control-Allow-Origin'] = '*'
return response
except Exception as e:
logging.error(f"product-image-info API 오류: {e}")
response = jsonify({'success': False, 'error': str(e)})
response.headers['Access-Control-Allow-Origin'] = '*'
return response, 500
if __name__ == '__main__':
import os

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