fix: SQLite 싱글톤 연결 I/O 에러 수정 + clawdbot 모델 오버라이드

- dbsetup: get_sqlite_connection()에 SELECT 1 헬스체크 추가 (죽은 연결 자동 재생성)
- pos_sales_gui: 싱글톤 SQLite conn.close() 제거 (I/O closed file 에러 원인)
- qr_token_generator: DatabaseManager() 새 생성 → 전역 db_manager 싱글톤 사용
- clawdbot_client: model 파라미터 추가, 업셀링에 claude-sonnet-4-5 지정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-02-27 01:27:47 +09:00
parent 4c3e1d08b2
commit db5f6063ec
4 changed files with 44 additions and 13 deletions

View File

@ -193,6 +193,13 @@ class DatabaseManager:
Returns: Returns:
sqlite3.Connection: SQLite 연결 객체 sqlite3.Connection: SQLite 연결 객체
""" """
# 연결이 닫혀있으면 재생성
if self.sqlite_conn is not None:
try:
self.sqlite_conn.execute("SELECT 1")
except Exception:
self.sqlite_conn = None
if self.sqlite_conn is None: if self.sqlite_conn is None:
# 파일 존재 여부 확인 # 파일 존재 여부 확인
is_new_db = not self.sqlite_db_path.exists() is_new_db = not self.sqlite_db_path.exists()

View File

@ -181,8 +181,7 @@ class SalesQueryThread(QThread):
finally: finally:
if mssql_conn: if mssql_conn:
mssql_conn.close() mssql_conn.close()
if sqlite_conn: # sqlite_conn은 싱글톤이므로 닫지 않음 (닫으면 다른 곳에서 I/O 에러 발생)
sqlite_conn.close()
class QRGeneratorThread(QThread): class QRGeneratorThread(QThread):
@ -600,9 +599,7 @@ class UserMileageDialog(QDialog):
except Exception as e: except Exception as e:
QMessageBox.critical(self, '오류', f'회원 정보 조회 실패:\n{str(e)}') QMessageBox.critical(self, '오류', f'회원 정보 조회 실패:\n{str(e)}')
finally: # conn은 싱글톤이므로 닫지 않음
if conn:
conn.close()
class POSSalesGUI(QMainWindow): class POSSalesGUI(QMainWindow):

View File

@ -45,7 +45,7 @@ def _load_gateway_config():
async def _ask_gateway(message, session_id='pharmacy-upsell', async def _ask_gateway(message, session_id='pharmacy-upsell',
system_prompt=None, timeout=60): system_prompt=None, timeout=60, model=None):
""" """
Clawdbot Gateway WebSocket API 호출 Clawdbot Gateway WebSocket API 호출
@ -112,7 +112,29 @@ async def _ask_gateway(message, session_id='pharmacy-upsell',
return None return None
break # 연결 성공 break # 연결 성공
# 4. agent 요청 # 4. 모델 오버라이드 (sessions.patch)
if model:
patch_id = str(uuid.uuid4())
patch_frame = {
'type': 'req',
'id': patch_id,
'method': 'sessions.patch',
'params': {
'key': session_id,
'model': model,
}
}
await ws.send(json.dumps(patch_frame))
# patch 응답 대기
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(msg)
if data.get('id') == patch_id:
if not data.get('ok'):
logger.warning(f"[Clawdbot] sessions.patch 실패: {data.get('error', {}).get('message', 'unknown')}")
break
# 5. agent 요청
agent_id = str(uuid.uuid4()) agent_id = str(uuid.uuid4())
agent_params = { agent_params = {
'message': message, 'message': message,
@ -174,7 +196,7 @@ async def _ask_gateway(message, session_id='pharmacy-upsell',
def ask_clawdbot(message, session_id='pharmacy-upsell', def ask_clawdbot(message, session_id='pharmacy-upsell',
system_prompt=None, timeout=60): system_prompt=None, timeout=60, model=None):
""" """
동기 래퍼: Flask에서 직접 호출 가능 동기 래퍼: Flask에서 직접 호출 가능
@ -183,6 +205,7 @@ def ask_clawdbot(message, session_id='pharmacy-upsell',
session_id: 세션 ID (대화 구분용) session_id: 세션 ID (대화 구분용)
system_prompt: 추가 시스템 프롬프트 system_prompt: 추가 시스템 프롬프트
timeout: 타임아웃 () timeout: 타임아웃 ()
model: 모델 오버라이드 (: 'anthropic/claude-sonnet-4-5')
Returns: Returns:
str: AI 응답 텍스트 (실패 None) str: AI 응답 텍스트 (실패 None)
@ -190,7 +213,7 @@ def ask_clawdbot(message, session_id='pharmacy-upsell',
try: try:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
result = loop.run_until_complete( result = loop.run_until_complete(
_ask_gateway(message, session_id, system_prompt, timeout) _ask_gateway(message, session_id, system_prompt, timeout, model=model)
) )
loop.close() loop.close()
return result return result
@ -201,6 +224,8 @@ def ask_clawdbot(message, session_id='pharmacy-upsell',
# 업셀링 전용 ────────────────────────────────────── # 업셀링 전용 ──────────────────────────────────────
UPSELL_MODEL = 'anthropic/claude-sonnet-4-5' # 업셀링은 Sonnet (빠르고 충분)
UPSELL_SYSTEM_PROMPT = """당신은 동네 약국(청춘약국)의 친절한 약사입니다. UPSELL_SYSTEM_PROMPT = """당신은 동네 약국(청춘약국)의 친절한 약사입니다.
고객의 구매 이력을 보고, 자연스럽고 따뜻한 톤으로 하나를 추천합니다. 고객의 구매 이력을 보고, 자연스럽고 따뜻한 톤으로 하나를 추천합니다.
강압적이거나 광고 같은 느낌이 아닌, 진심으로 건강을 걱정하는 약사의 말투로 작성해주세요. 강압적이거나 광고 같은 느낌이 아닌, 진심으로 건강을 걱정하는 약사의 말투로 작성해주세요.
@ -238,7 +263,8 @@ def generate_upsell(user_name, current_items, recent_products):
prompt, prompt,
session_id=f'upsell-{user_name}', session_id=f'upsell-{user_name}',
system_prompt=UPSELL_SYSTEM_PROMPT, system_prompt=UPSELL_SYSTEM_PROMPT,
timeout=30 timeout=30,
model=UPSELL_MODEL
) )
if not response_text: if not response_text:
@ -285,7 +311,8 @@ def generate_upsell_real(user_name, current_items, recent_products, available_pr
prompt, prompt,
session_id=f'upsell-real-{user_name}', session_id=f'upsell-real-{user_name}',
system_prompt=UPSELL_REAL_SYSTEM_PROMPT, system_prompt=UPSELL_REAL_SYSTEM_PROMPT,
timeout=30 timeout=30,
model=UPSELL_MODEL
) )
if not response_text: if not response_text:

View File

@ -115,8 +115,8 @@ def save_token_to_db(transaction_id, token_hash, total_amount, claimable_points,
- token_hash가 이미 존재하면 실패 (UNIQUE 제약) - token_hash가 이미 존재하면 실패 (UNIQUE 제약)
""" """
try: try:
db_manager = DatabaseManager() from db.dbsetup import db_manager as _db_manager
conn = db_manager.get_sqlite_connection() conn = _db_manager.get_sqlite_connection()
cursor = conn.cursor() cursor = conn.cursor()
# 중복 체크 (transaction_id) # 중복 체크 (transaction_id)