Change rocket.chat admin logic
This commit is contained in:
260
api/server.py
260
api/server.py
@@ -82,18 +82,21 @@ async def rocketchat_login():
|
||||
return None
|
||||
|
||||
|
||||
async def get_or_create_channel(user_id: str, user_name: str = None):
|
||||
"""取得或創建用戶專屬頻道"""
|
||||
async def get_or_create_chat_channel(chat_id: str, user_name: str = None):
|
||||
"""為每個對話創建專屬頻道
|
||||
|
||||
新架構:每個 chat_id 對應一個 Channel,不使用 Thread
|
||||
"""
|
||||
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] # 限制長度
|
||||
# 頻道名稱:用戶名-對話ID前8位
|
||||
display_name = user_name if user_name else "user"
|
||||
clean_name = display_name.replace(" ", "-").replace("@", "").lower()
|
||||
channel_name = f"{clean_name}-{chat_id[:8]}"[:50] # 限制長度
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
@@ -103,14 +106,14 @@ async def get_or_create_channel(user_id: str, user_name: str = None):
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"name": channel_name,
|
||||
"members": [] # 空成員列表,只有管理員可見
|
||||
"members": []
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
room_id = data["channel"]["_id"]
|
||||
print(f"✅ 創建頻道: {channel_name} ({display_name})")
|
||||
print(f"✅ 創建對話頻道: {channel_name}")
|
||||
|
||||
# 設置頻道描述
|
||||
await client.post(
|
||||
@@ -118,7 +121,7 @@ async def get_or_create_channel(user_id: str, user_name: str = None):
|
||||
headers=rocketchat_auth,
|
||||
json={
|
||||
"roomId": room_id,
|
||||
"description": f"用戶: {display_name} (ID: {user_id})"
|
||||
"description": f"用戶: {display_name} | 對話: {chat_id[:8]}"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -149,122 +152,50 @@ async def get_or_create_channel(user_id: str, user_name: str = None):
|
||||
return None
|
||||
|
||||
|
||||
async def get_or_create_thread(room_id: str, chat_id: str):
|
||||
"""查找或創建對話的執行緒"""
|
||||
async def send_user_message(room_id: str, user_message: str, user_name: str = None):
|
||||
"""發送用戶訊息到頻道(不使用 Thread)"""
|
||||
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"
|
||||
display_name = user_name if user_name else "用戶"
|
||||
|
||||
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}"
|
||||
"text": f"**💬 {display_name}:**\n{user_message}"
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
message_id = data["message"]["_id"]
|
||||
print(f"✅ 創建執行緒訊息: {message_id}")
|
||||
return message_id
|
||||
message_ts = data["message"]["ts"]
|
||||
print(f"✅ 發送用戶訊息: {message_id}")
|
||||
return {"message_id": message_id, "ts": message_ts}
|
||||
else:
|
||||
print(f"⚠️ 發送訊息失敗: {resp.status_code}")
|
||||
print(f" 錯誤內容: {resp.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Rocket.Chat 發送訊息錯誤: {e}")
|
||||
print(f"❌ 發送訊息錯誤: {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):
|
||||
"""輪詢等待執行緒中的管理員回覆
|
||||
async def wait_for_admin_reply(room_id: str, after_ts: str, exclude_msg_id: str = None, timeout: int = 600):
|
||||
"""等待管理員在頻道中的回覆
|
||||
|
||||
Args:
|
||||
existing_message_count: 發送訊息前執行緒中已有的訊息數量,只等待新增的回覆
|
||||
room_id: 頻道 ID
|
||||
after_ts: 用戶訊息的時間戳 (ISO 格式)
|
||||
exclude_msg_id: 要排除的用戶訊息 ID (避免自己讀到自己)
|
||||
timeout: 超時秒數
|
||||
"""
|
||||
if not rocketchat_auth:
|
||||
await rocketchat_login()
|
||||
@@ -273,19 +204,23 @@ async def wait_for_thread_reply(room_id: str, thread_id: str, existing_message_c
|
||||
return None
|
||||
|
||||
start_time = time.time()
|
||||
check_interval = 3 # 每 3 秒檢查一次
|
||||
check_interval = 2 # 每 2 秒檢查一次
|
||||
|
||||
print(f"🔄 開始輪詢執行緒回覆 (thread_id: {thread_id}, 現有訊息: {existing_message_count})")
|
||||
# 取得機器人用戶 ID
|
||||
bot_user_id = rocketchat_auth.get("X-User-Id")
|
||||
|
||||
print(f"🔄 等待回覆 (room: {room_id[:8]}..., after: {after_ts}, exclude: {exclude_msg_id})")
|
||||
|
||||
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",
|
||||
f"{ROCKETCHAT_URL}/api/v1/channels.messages",
|
||||
headers=rocketchat_auth,
|
||||
params={
|
||||
"tmid": thread_id # Thread Message ID
|
||||
"roomId": room_id,
|
||||
"count": 20 # 稍微增加數量以防萬一
|
||||
}
|
||||
)
|
||||
|
||||
@@ -293,33 +228,64 @@ async def wait_for_thread_reply(room_id: str, thread_id: str, existing_message_c
|
||||
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})")
|
||||
found_new_reply = False
|
||||
|
||||
for msg in messages:
|
||||
msg_id = msg.get("_id")
|
||||
msg_ts = msg.get("ts", "")
|
||||
sender_id = msg.get("u", {}).get("_id", "")
|
||||
sender_name = msg.get("u", {}).get("username", "?")
|
||||
reply_text = msg.get("msg", "")
|
||||
msg_type = msg.get("t")
|
||||
|
||||
# 按時間排序,取最新的訊息
|
||||
sorted_messages = sorted(messages, key=lambda m: m.get("ts", ""), reverse=True)
|
||||
# 1. 基本過濾
|
||||
if msg_type: # 忽略系統訊息
|
||||
continue
|
||||
|
||||
# 尋找管理員的新回覆
|
||||
for msg in sorted_messages:
|
||||
# 跳過原始訊息
|
||||
if msg["_id"] == thread_id:
|
||||
continue
|
||||
if msg_id == exclude_msg_id: # 忽略剛發送的用戶訊息
|
||||
continue
|
||||
|
||||
# 檢查是否為真人回覆(有 u 欄位且不是 bot)
|
||||
if "u" in msg and not msg.get("bot"):
|
||||
reply_text = msg.get("msg", "")
|
||||
# 跳過用戶自己的訊息(檢查是否包含對話標記)
|
||||
if reply_text and not reply_text.startswith("[對話"):
|
||||
print(f"✅ 收到管理員回覆: {reply_text[:50]}...")
|
||||
return reply_text
|
||||
# 2. 時間戳檢查 (放寬條件: 只要 >= 就考慮,因為可能有精度差)
|
||||
# 但為了避免讀到舊訊息,我們仍需確保它"看起來"是新的
|
||||
if msg_ts < after_ts:
|
||||
continue
|
||||
|
||||
# 3. 機器人自我發送過濾
|
||||
if sender_id == bot_user_id:
|
||||
# 這是最關鍵的部分: 如何區分 "API轉發的用戶訊息" 與 "Admin用同一帳號的回覆"
|
||||
|
||||
# A. 如果這就是我們要排除的 ID (前面已處理)
|
||||
|
||||
# B. 檢查內容格式
|
||||
# 格式為: "**💬 {display_name}:**\n{user_message}"
|
||||
# 我們檢查幾個特徵: 開頭是 **💬, 包含 :**
|
||||
is_forwarded_msg = False
|
||||
if reply_text.strip().startswith("**💬") and ":**" in reply_text:
|
||||
is_forwarded_msg = True
|
||||
|
||||
if is_forwarded_msg:
|
||||
# 這是轉發的訊息,跳過
|
||||
continue
|
||||
|
||||
# 如果不是轉發格式,那我們假設這是 Admin 的手動回覆
|
||||
pass
|
||||
|
||||
# 4. 找到有效回覆
|
||||
if reply_text:
|
||||
print(f"✅ 收到管理員回覆 (from {sender_name}, id: {msg_id}): {reply_text[:50]}...")
|
||||
return reply_text
|
||||
|
||||
# 沒找到
|
||||
elapsed = int(time.time() - start_time)
|
||||
if elapsed % 10 == 0:
|
||||
print(f"🔄 等待中... ({elapsed}s)")
|
||||
|
||||
else:
|
||||
print(f" ⚠️ API 回應異常: {resp.status_code}")
|
||||
print(f"⚠️ API 錯誤: {resp.status_code}")
|
||||
|
||||
await asyncio.sleep(check_interval)
|
||||
|
||||
# 超時
|
||||
print(f"⚠️ 等待回覆超時 ({timeout}秒)")
|
||||
print(f"⚠️ 等待回覆超時")
|
||||
return "抱歉,目前客服繁忙,請稍後再試。"
|
||||
|
||||
except Exception as e:
|
||||
@@ -502,13 +468,12 @@ async def chat_completions(
|
||||
}
|
||||
|
||||
print(f"📝 收到訊息")
|
||||
print(f" 用戶ID: {user_id}")
|
||||
print(f" 用戶名: {user_name}")
|
||||
print(f" 用戶: {user_name} ({user_id[:8]}...)")
|
||||
print(f" 對話: {chat_id[:8]}")
|
||||
print(f" 內容: {user_message[:50]}...")
|
||||
|
||||
# 1. 取得或創建用戶頻道
|
||||
room_id = await get_or_create_channel(user_id, user_name)
|
||||
# 1. 為此對話取得或創建專屬頻道(每個 chat_id = 一個 Channel)
|
||||
room_id = await get_or_create_chat_channel(chat_id, user_name)
|
||||
|
||||
if not room_id:
|
||||
return JSONResponse(
|
||||
@@ -516,39 +481,16 @@ async def chat_completions(
|
||||
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
|
||||
# 2. 發送用戶訊息到頻道
|
||||
msg_result = await send_user_message(room_id, user_message, user_name)
|
||||
|
||||
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"}
|
||||
)
|
||||
if not msg_result:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Failed to send message to Rocket.Chat"}
|
||||
)
|
||||
|
||||
user_message_ts = msg_result["ts"] # 用時間戳比較,更可靠
|
||||
|
||||
# 3. 記錄到資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
@@ -560,11 +502,11 @@ async def chat_completions(
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending', $6, $7)
|
||||
""",
|
||||
message_id, user_id, chat_id, user_name, user_message, room_id, thread_id
|
||||
message_id, user_id, chat_id, user_name, user_message, room_id, msg_result["message_id"]
|
||||
)
|
||||
|
||||
# 4. 等待管理員在執行緒中回覆(傳入現有訊息數量,只等待新回覆)
|
||||
admin_reply = await wait_for_thread_reply(room_id, thread_id, existing_message_count)
|
||||
# 4. 等待管理員在頻道中回覆(使用時間戳比較)
|
||||
admin_reply = await wait_for_admin_reply(room_id, user_message_ts, exclude_msg_id=msg_result["message_id"])
|
||||
|
||||
# 5. 更新資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
|
||||
Reference in New Issue
Block a user