Change platform to rocket.chat
This commit is contained in:
666
api/server.py
666
api/server.py
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
API 轉接層 - 偽裝 OpenAI API,將請求轉為人工回覆隊列
|
||||
API 轉接層 - 偽裝 OpenAI API,整合 Rocket.Chat 作為管理員回覆介面
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, Request, Header
|
||||
@@ -10,6 +10,8 @@ import asyncio
|
||||
import time
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import hashlib
|
||||
import httpx
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
@@ -29,9 +31,13 @@ DB_CONFIG = {
|
||||
"password": os.getenv("DB_PASSWORD", "tobiichi_password")
|
||||
}
|
||||
|
||||
# Papercups 設定
|
||||
PAPERCUPS_URL = os.getenv("PAPERCUPS_URL", "http://papercups:4000")
|
||||
PAPERCUPS_API_TOKEN = os.getenv("PAPERCUPS_API_TOKEN", "")
|
||||
# Rocket.Chat 設定
|
||||
ROCKETCHAT_URL = os.getenv("ROCKETCHAT_URL", "http://rocketchat:3000")
|
||||
ROCKETCHAT_USER = os.getenv("ROCKETCHAT_USER", "admin")
|
||||
ROCKETCHAT_PASSWORD = os.getenv("ROCKETCHAT_PASSWORD", "admin")
|
||||
|
||||
# 全域認證狀態
|
||||
rocketchat_auth = None
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
@@ -45,14 +51,294 @@ class ChatRequest(BaseModel):
|
||||
stream: Optional[bool] = False
|
||||
|
||||
|
||||
class PapercupsWebhook(BaseModel):
|
||||
"""Papercups Webhook 資料格式"""
|
||||
event: str
|
||||
payload: Optional[dict] = None
|
||||
async def rocketchat_login():
|
||||
"""登入 Rocket.Chat 取得認證 Token"""
|
||||
global rocketchat_auth
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.post(
|
||||
f"{ROCKETCHAT_URL}/api/v1/login",
|
||||
json={
|
||||
"username": ROCKETCHAT_USER,
|
||||
"password": ROCKETCHAT_PASSWORD
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
rocketchat_auth = {
|
||||
"X-Auth-Token": data["data"]["authToken"],
|
||||
"X-User-Id": data["data"]["userId"]
|
||||
}
|
||||
print(f"✅ Rocket.Chat 登入成功")
|
||||
return rocketchat_auth
|
||||
else:
|
||||
print(f"⚠️ Rocket.Chat 登入失敗: {resp.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Rocket.Chat 登入錯誤: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_or_create_channel(user_id: str, user_name: str = None):
|
||||
"""取得或創建用戶專屬頻道"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
|
||||
if not rocketchat_auth:
|
||||
return None
|
||||
|
||||
# 頻道名稱使用用戶名,如果沒有則用 user_id
|
||||
display_name = user_name if user_name else f"User-{user_id[:8]}"
|
||||
# 清理頻道名稱(移除空格和特殊字符,Rocket.Chat 頻道名不支援)
|
||||
channel_name = display_name.replace(" ", "-").replace("@", "").lower()[:50] # 限制長度
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
# 嘗試創建頻道
|
||||
resp = await client.post(
|
||||
f"{ROCKETCHAT_URL}/api/v1/channels.create",
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"name": channel_name,
|
||||
"members": [] # 空成員列表,只有管理員可見
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
room_id = data["channel"]["_id"]
|
||||
print(f"✅ 創建頻道: {channel_name} ({display_name})")
|
||||
|
||||
# 設置頻道描述
|
||||
await client.post(
|
||||
f"{ROCKETCHAT_URL}/api/v1/channels.setDescription",
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"roomId": room_id,
|
||||
"description": f"用戶: {display_name} (ID: {user_id})"
|
||||
}
|
||||
)
|
||||
|
||||
return room_id
|
||||
|
||||
# 如果頻道已存在,取得頻道資訊
|
||||
elif resp.status_code == 400:
|
||||
resp = await client.get(
|
||||
f"{ROCKETCHAT_URL}/api/v1/channels.info",
|
||||
headers=rocketchat_auth,
|
||||
params={"roomName": channel_name}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
room_id = data["channel"]["_id"]
|
||||
print(f"✅ 使用現有頻道: {channel_name}")
|
||||
return room_id
|
||||
else:
|
||||
print(f"⚠️ 取得頻道資訊失敗: {resp.status_code}")
|
||||
return None
|
||||
else:
|
||||
print(f"⚠️ 創建頻道失敗: {resp.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Rocket.Chat 頻道操作錯誤: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_or_create_thread(room_id: str, chat_id: str):
|
||||
"""查找或創建對話的執行緒"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
|
||||
if not rocketchat_auth:
|
||||
return None
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
# 搜尋頻道中是否已有此 chat_id 的執行緒(搜尋最近50條消息)
|
||||
resp = await client.get(
|
||||
f"{ROCKETCHAT_URL}/api/v1/channels.messages",
|
||||
headers=rocketchat_auth,
|
||||
params={
|
||||
"roomId": room_id,
|
||||
"count": 50
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
messages = data.get("messages", [])
|
||||
|
||||
# 找到現有的執行緒起始訊息
|
||||
for msg in messages:
|
||||
if f"對話 {chat_id[:8]}" in msg.get("msg", ""):
|
||||
thread_id = msg["_id"]
|
||||
print(f"✅ 找到現有執行緒: {thread_id}")
|
||||
return thread_id
|
||||
|
||||
# 沒有找到,返回 None(需要創建新執行緒)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 查找執行緒錯誤: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def create_thread_message(room_id: str, chat_id: str, user_message: str, user_name: str = None):
|
||||
"""在頻道中發送訊息並創建執行緒"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
|
||||
if not rocketchat_auth:
|
||||
return None
|
||||
|
||||
display_name = user_name if user_name else "User"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
# 發送訊息(這會成為執行緒的起始訊息)
|
||||
resp = await client.post(
|
||||
f"{ROCKETCHAT_URL}/api/v1/chat.postMessage",
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"roomId": room_id,
|
||||
"text": f"**[對話 {chat_id[:8]}] - {display_name}**\n\n{user_message}"
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
message_id = data["message"]["_id"]
|
||||
print(f"✅ 創建執行緒訊息: {message_id}")
|
||||
return message_id
|
||||
else:
|
||||
print(f"⚠️ 發送訊息失敗: {resp.status_code}")
|
||||
print(f" 錯誤內容: {resp.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Rocket.Chat 發送訊息錯誤: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def add_thread_reply(room_id: str, thread_id: str, user_message: str, user_name: str = None):
|
||||
"""在現有執行緒中追加用戶訊息"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
|
||||
if not rocketchat_auth:
|
||||
return False
|
||||
|
||||
display_name = user_name if user_name else "User"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
# 在執行緒中回覆
|
||||
resp = await client.post(
|
||||
f"{ROCKETCHAT_URL}/api/v1/chat.postMessage",
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"roomId": room_id,
|
||||
"text": f"**{display_name}:**\n\n{user_message}",
|
||||
"tmid": thread_id # 指定執行緒 ID
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
print(f"✅ 追加執行緒訊息")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 追加訊息失敗: {resp.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 追加訊息錯誤: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def wait_for_thread_reply(room_id: str, thread_id: str, existing_message_count: int = 0, timeout: int = 600):
|
||||
"""輪詢等待執行緒中的管理員回覆
|
||||
|
||||
Args:
|
||||
existing_message_count: 發送訊息前執行緒中已有的訊息數量,只等待新增的回覆
|
||||
"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
|
||||
if not rocketchat_auth:
|
||||
return None
|
||||
|
||||
start_time = time.time()
|
||||
check_interval = 3 # 每 3 秒檢查一次
|
||||
|
||||
# 取得 bot 帳號的用戶名,用於過濾
|
||||
bot_username = ROCKETCHAT_USER
|
||||
|
||||
print(f"🔄 開始輪詢執行緒回覆 (thread_id: {thread_id}, 現有訊息: {existing_message_count})")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
while time.time() - start_time < timeout:
|
||||
# 取得執行緒中的所有訊息
|
||||
resp = await client.get(
|
||||
f"{ROCKETCHAT_URL}/api/v1/chat.getThreadMessages",
|
||||
headers=rocketchat_auth,
|
||||
params={
|
||||
"tmid": thread_id # Thread Message ID
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
messages = data.get("messages", [])
|
||||
|
||||
# 只看新增的訊息(比現有數量多的部分)
|
||||
if len(messages) > existing_message_count + 1: # +1 是我們剛發的訊息
|
||||
print(f" 檢查執行緒,找到 {len(messages)} 條訊息 (新增: {len(messages) - existing_message_count - 1})")
|
||||
|
||||
# 按時間排序,取最新的訊息
|
||||
sorted_messages = sorted(messages, key=lambda m: m.get("ts", ""), reverse=True)
|
||||
|
||||
# 尋找管理員的新回覆(不是 bot 發送的)
|
||||
for msg in sorted_messages:
|
||||
# 跳過原始訊息
|
||||
if msg["_id"] == thread_id:
|
||||
continue
|
||||
|
||||
# 檢查發送者
|
||||
sender = msg.get("u", {})
|
||||
sender_username = sender.get("username", "")
|
||||
|
||||
# 跳過 bot 自己發送的訊息(系統轉發的用戶訊息)
|
||||
if sender_username == bot_username:
|
||||
continue
|
||||
|
||||
# 檢查是否為真人回覆(有 u 欄位且不是 bot 標記)
|
||||
if sender and not msg.get("bot"):
|
||||
reply_text = msg.get("msg", "")
|
||||
if reply_text:
|
||||
print(f"✅ 收到管理員回覆 (from: {sender_username}): {reply_text[:50]}...")
|
||||
return reply_text
|
||||
else:
|
||||
print(f" ⚠️ API 回應異常: {resp.status_code}")
|
||||
|
||||
await asyncio.sleep(check_interval)
|
||||
|
||||
# 超時
|
||||
print(f"⚠️ 等待回覆超時 ({timeout}秒)")
|
||||
return "抱歉,目前客服繁忙,請稍後再試。"
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 檢查回覆錯誤: {e}")
|
||||
return "系統錯誤,請稍後再試。"
|
||||
|
||||
|
||||
async def get_user_name(user_id: str):
|
||||
"""從資料庫獲取用戶真實姓名"""
|
||||
"""從資料庫獲取用戶真實姓名(保留作為工具函數)"""
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
@@ -72,91 +358,12 @@ async def get_user_name(user_id: str):
|
||||
return None
|
||||
|
||||
|
||||
async def get_or_create_papercups_conversation(user_id: str, chat_id: str, user_name: str = None):
|
||||
"""獲取或創建 Papercups 對話(使用 chat_id 作為唯一標識)"""
|
||||
if not PAPERCUPS_API_TOKEN:
|
||||
print("⚠️ Papercups 未配置,跳過推送")
|
||||
return None
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
headers = {"Authorization": f"Bearer {PAPERCUPS_API_TOKEN}"}
|
||||
|
||||
# 使用 chat_id 作為唯一標識符
|
||||
identifier = chat_id if chat_id else user_id
|
||||
|
||||
# 如果沒有提供用戶名,嘗試從數據庫獲取
|
||||
if not user_name and user_id:
|
||||
user_name = await get_user_name(user_id)
|
||||
|
||||
# 生成顯示名稱
|
||||
if user_name:
|
||||
display_name = user_name
|
||||
elif user_id:
|
||||
display_name = f"User-{user_id[:8]}"
|
||||
else:
|
||||
display_name = f"Chat-{chat_id[:8]}"
|
||||
|
||||
# Papercups API: 創建或獲取對話
|
||||
conversation_url = f"{PAPERCUPS_URL}/api/conversations"
|
||||
conversation_payload = {
|
||||
"customer": {
|
||||
"external_id": identifier,
|
||||
"name": display_name
|
||||
}
|
||||
}
|
||||
|
||||
conversation_response = await client.post(conversation_url, json=conversation_payload, headers=headers)
|
||||
if conversation_response.status_code in [200, 201]:
|
||||
conversation_data = conversation_response.json()
|
||||
papercups_conv_id = conversation_data.get("id")
|
||||
print(f"✅ Papercups 對話: #{papercups_conv_id} ({display_name})")
|
||||
return papercups_conv_id
|
||||
else:
|
||||
print(f"⚠️ Papercups 對話創建失敗: {conversation_response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Papercups 錯誤: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
async def send_message_to_papercups(papercups_conv_id: str, message: str, sent_by: str = "customer"):
|
||||
"""發送訊息到 Papercups 對話"""
|
||||
if not PAPERCUPS_API_TOKEN or not papercups_conv_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
headers = {"Authorization": f"Bearer {PAPERCUPS_API_TOKEN}"}
|
||||
msg_url = f"{PAPERCUPS_URL}/api/messages"
|
||||
msg_payload = {
|
||||
"conversation_id": papercups_conv_id,
|
||||
"body": message,
|
||||
"sent_by": sent_by
|
||||
}
|
||||
|
||||
msg_response = await client.post(msg_url, json=msg_payload, headers=headers)
|
||||
if msg_response.status_code in [200, 201]:
|
||||
print(f"✅ 訊息已發送到 Papercups 對話 #{papercups_conv_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 訊息發送失敗: {msg_response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 發送訊息錯誤: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def init_db():
|
||||
"""初始化資料庫連接池和表格"""
|
||||
global db_pool
|
||||
db_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10)
|
||||
|
||||
# 建立對話隊列表格
|
||||
# 建立對話隊列表格(用於記錄和追蹤)
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS reply_queue (
|
||||
@@ -164,27 +371,23 @@ async def init_db():
|
||||
conversation_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
user_id VARCHAR(255),
|
||||
chat_id VARCHAR(255),
|
||||
user_name VARCHAR(255),
|
||||
user_message TEXT NOT NULL,
|
||||
admin_reply TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
replied_at TIMESTAMP,
|
||||
papercups_conversation_id VARCHAR(100)
|
||||
rocketchat_room_id VARCHAR(100),
|
||||
rocketchat_thread_id VARCHAR(100)
|
||||
)
|
||||
""")
|
||||
|
||||
# 添加新欄位(如果不存在)
|
||||
try:
|
||||
await conn.execute("ALTER TABLE reply_queue ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);")
|
||||
await conn.execute("ALTER TABLE reply_queue ADD COLUMN IF NOT EXISTS chat_id VARCHAR(255);")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 建立索引
|
||||
await conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_status ON reply_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_id ON reply_queue(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_id ON reply_queue(chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_id ON reply_queue(user_id);
|
||||
""")
|
||||
|
||||
|
||||
@@ -192,6 +395,9 @@ async def init_db():
|
||||
async def startup():
|
||||
await init_db()
|
||||
print("✅ 資料庫連接成功")
|
||||
|
||||
# 嘗試登入 Rocket.Chat
|
||||
await rocketchat_login()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
@@ -204,7 +410,11 @@ async def shutdown():
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路徑"""
|
||||
return {"status": "ok", "service": "TobiichiGPT API"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "TobiichiGPT API",
|
||||
"chat_backend": "Rocket.Chat"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
@@ -234,7 +444,7 @@ async def chat_completions(
|
||||
):
|
||||
"""
|
||||
模擬 OpenAI Chat Completions API
|
||||
將用戶訊息寫入資料庫,等待管理員回覆(無超時限制)
|
||||
將用戶訊息轉發到 Rocket.Chat,等待管理員回覆
|
||||
"""
|
||||
# 取得最後一則用戶訊息
|
||||
user_message = None
|
||||
@@ -249,140 +459,174 @@ async def chat_completions(
|
||||
content={"error": "No user message found"}
|
||||
)
|
||||
|
||||
# 嘗試從請求中提取用戶信息
|
||||
user_id = None
|
||||
chat_id = None
|
||||
|
||||
# 從 headers 提取信息
|
||||
# 從 Open WebUI headers 提取用戶資訊
|
||||
headers_dict = dict(http_request.headers)
|
||||
user_id = headers_dict.get("x-user-id") or headers_dict.get("user-id")
|
||||
chat_id = headers_dict.get("x-chat-id") or headers_dict.get("chat-id")
|
||||
|
||||
# 如果沒有,生成一個基於授權 token 的穩定 ID
|
||||
if not user_id and authorization:
|
||||
user_id = hashlib.md5(authorization.encode()).hexdigest()[:16]
|
||||
# 調試:輸出所有 headers
|
||||
print(f"🔍 收到的 Headers:")
|
||||
for key, value in headers_dict.items():
|
||||
if 'user' in key.lower() or 'chat' in key.lower() or 'name' in key.lower():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# 生成消息 ID(用於追蹤單條消息)
|
||||
user_id = headers_dict.get("x-openwebui-user-id")
|
||||
chat_id = headers_dict.get("x-openwebui-chat-id")
|
||||
user_name = headers_dict.get("x-openwebui-user-name")
|
||||
user_email = headers_dict.get("x-openwebui-user-email")
|
||||
|
||||
# 如果沒有從 headers 取得,使用備用方案
|
||||
if not user_id:
|
||||
user_id = headers_dict.get("x-user-id") or headers_dict.get("user-id")
|
||||
if not chat_id:
|
||||
chat_id = headers_dict.get("x-chat-id") or headers_dict.get("chat-id")
|
||||
|
||||
# 生成 message ID
|
||||
message_id = str(uuid.uuid4())
|
||||
|
||||
# 如果沒有 chat_id,使用 user_id
|
||||
# 如果還是沒有,使用 fallback
|
||||
if not user_id:
|
||||
user_id = hashlib.md5(authorization.encode() if authorization else message_id.encode()).hexdigest()[:16]
|
||||
|
||||
if not chat_id:
|
||||
chat_id = user_id if user_id else message_id
|
||||
chat_id = message_id
|
||||
|
||||
# 獲取用戶真實姓名
|
||||
user_name = await get_user_name(user_id) if user_id else None
|
||||
# 過濾 Open WebUI 的系統任務訊息(標題生成、標籤生成、後續問題生成等)
|
||||
if user_message.strip().startswith("### Task:"):
|
||||
print(f"⏭️ 跳過系統任務訊息: {user_message[:50]}...")
|
||||
# 回傳空的 JSON 回應讓 Open WebUI 處理
|
||||
return {
|
||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request_data.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "{}"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
||||
}
|
||||
|
||||
print(f"📝 收到訊息 [user:{user_name or user_id}, chat:{chat_id}]: {user_message[:50]}...")
|
||||
print(f"📝 收到訊息")
|
||||
print(f" 用戶ID: {user_id}")
|
||||
print(f" 用戶名: {user_name}")
|
||||
print(f" 對話: {chat_id[:8]}")
|
||||
print(f" 內容: {user_message[:50]}...")
|
||||
|
||||
# 獲取或創建 Papercups 對話
|
||||
papercups_conv_id = await get_or_create_papercups_conversation(user_id, chat_id, user_name)
|
||||
# 1. 取得或創建用戶頻道
|
||||
room_id = await get_or_create_channel(user_id, user_name)
|
||||
|
||||
# 寫入資料庫
|
||||
if not room_id:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Failed to create Rocket.Chat channel"}
|
||||
)
|
||||
|
||||
# 2. 查找是否已有此 chat_id 的執行緒
|
||||
thread_id = await get_or_create_thread(room_id, chat_id)
|
||||
existing_message_count = 0
|
||||
|
||||
if thread_id:
|
||||
# 已有執行緒,先取得現有訊息數量
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{ROCKETCHAT_URL}/api/v1/chat.getThreadMessages",
|
||||
headers=rocketchat_auth,
|
||||
params={"tmid": thread_id}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
existing_message_count = len(resp.json().get("messages", []))
|
||||
except Exception as e:
|
||||
print(f"⚠️ 取得現有訊息數量失敗: {e}")
|
||||
|
||||
# 追加訊息
|
||||
success = await add_thread_reply(room_id, thread_id, user_message, user_name)
|
||||
if not success:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Failed to add message to thread"}
|
||||
)
|
||||
else:
|
||||
# 沒有執行緒,創建新的
|
||||
thread_id = await create_thread_message(room_id, chat_id, user_message, user_name)
|
||||
if not thread_id:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Failed to create Rocket.Chat thread"}
|
||||
)
|
||||
|
||||
# 3. 記錄到資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO reply_queue (conversation_id, user_id, chat_id, user_message, status, papercups_conversation_id)
|
||||
VALUES ($1, $2, $3, $4, 'pending', $5)
|
||||
INSERT INTO reply_queue (
|
||||
conversation_id, user_id, chat_id, user_name, user_message,
|
||||
status, rocketchat_room_id, rocketchat_thread_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending', $6, $7)
|
||||
""",
|
||||
message_id, user_id, chat_id, user_message, papercups_conv_id
|
||||
message_id, user_id, chat_id, user_name, user_message, room_id, thread_id
|
||||
)
|
||||
|
||||
# 推送到 Papercups
|
||||
if papercups_conv_id:
|
||||
await send_message_to_papercups(papercups_conv_id, user_message, "customer")
|
||||
# 4. 等待管理員在執行緒中回覆(傳入現有訊息數量,只等待新回覆)
|
||||
admin_reply = await wait_for_thread_reply(room_id, thread_id, existing_message_count)
|
||||
|
||||
# 無限等待管理員回覆
|
||||
check_interval = 2 # 每 2 秒檢查一次
|
||||
# 5. 更新資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE reply_queue
|
||||
SET admin_reply = $1, status = 'replied', replied_at = NOW()
|
||||
WHERE conversation_id = $2
|
||||
""",
|
||||
admin_reply, message_id
|
||||
)
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(check_interval)
|
||||
|
||||
async with db_pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"SELECT admin_reply, status FROM reply_queue WHERE conversation_id = $1",
|
||||
message_id
|
||||
)
|
||||
|
||||
if row and row['status'] == 'replied' and row['admin_reply']:
|
||||
admin_reply = row['admin_reply']
|
||||
print(f"✅ 管理員已回覆 [chat:{chat_id}]")
|
||||
|
||||
# 回傳 OpenAI 格式的回應
|
||||
return {
|
||||
"id": f"chatcmpl-{message_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request_data.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": admin_reply
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": len(user_message),
|
||||
"completion_tokens": len(admin_reply),
|
||||
"total_tokens": len(user_message) + len(admin_reply)
|
||||
}
|
||||
}
|
||||
print(f"✅ 完成回覆 [chat:{chat_id[:8]}]")
|
||||
|
||||
# 6. 回傳 OpenAI 格式的回應
|
||||
return {
|
||||
"id": f"chatcmpl-{message_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request_data.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": admin_reply
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": len(user_message),
|
||||
"completion_tokens": len(admin_reply),
|
||||
"total_tokens": len(user_message) + len(admin_reply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/papercups/webhook")
|
||||
async def papercups_webhook(request: Request):
|
||||
"""接收 Papercups Webhook 回調"""
|
||||
try:
|
||||
data = await request.json()
|
||||
event = data.get("event")
|
||||
|
||||
# 只處理訊息建立事件
|
||||
if event != "message:created":
|
||||
return {"status": "ignored", "event": event}
|
||||
|
||||
payload = data.get("payload", {})
|
||||
message = payload.get("message", {})
|
||||
|
||||
# 檢查是否為管理員回覆(user type = "user")
|
||||
user = message.get("user")
|
||||
if not user:
|
||||
return {"status": "ignored", "reason": "not_agent_reply"}
|
||||
|
||||
# 取得訊息內容和對話 ID
|
||||
content = message.get("body")
|
||||
conversation_id = message.get("conversation_id")
|
||||
|
||||
if not content or not conversation_id:
|
||||
return {"status": "error", "message": "Missing content or conversation_id"}
|
||||
|
||||
# 更新資料庫中所有該對話的待處理消息(最新的一條)
|
||||
async with db_pool.acquire() as conn:
|
||||
result = await conn.execute(
|
||||
"""
|
||||
UPDATE reply_queue
|
||||
SET admin_reply = $1, status = 'replied', replied_at = NOW()
|
||||
WHERE papercups_conversation_id = $2
|
||||
AND status = 'pending'
|
||||
AND id = (
|
||||
SELECT id FROM reply_queue
|
||||
WHERE papercups_conversation_id = $2 AND status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
""",
|
||||
content, conversation_id
|
||||
)
|
||||
|
||||
print(f"✅ Papercups 回覆已處理: 對話 #{conversation_id}")
|
||||
return {"status": "success", "conversation_id": conversation_id}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Webhook 處理錯誤: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康檢查端點"""
|
||||
db_status = "ok" if db_pool else "disconnected"
|
||||
rc_status = "ok" if rocketchat_auth else "not_authenticated"
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"database": db_status,
|
||||
"rocketchat": rc_status
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user