Change rocket.chat admin logic

This commit is contained in:
ChenKaiLiuG
2026-02-01 08:15:45 +08:00
parent 44ba70efe6
commit f105a7e748
3 changed files with 358 additions and 215 deletions

View File

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