mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-31 21:06:18 +08:00
239 lines
9.9 KiB
Python
239 lines
9.9 KiB
Python
# HumanTakeover Console Page - 后端 API 处理
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any
|
|
|
|
from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ConsolePage(Page):
|
|
"""人工接管控制台页面后端。
|
|
|
|
通过 self.plugin 访问插件共享状态(会话、消息、接管状态)。
|
|
"""
|
|
|
|
async def handle_api(self, request: PageRequest) -> PageResponse:
|
|
plugin = self.plugin
|
|
endpoint = request.endpoint
|
|
method = request.method
|
|
body = request.body or {}
|
|
|
|
try:
|
|
# 先批量处理接管超时,保证返回数据准确
|
|
await plugin.expire_timeouts()
|
|
|
|
if endpoint == "/sessions" and method == "GET":
|
|
return PageResponse.ok({"sessions": self._build_session_list(plugin)})
|
|
|
|
if endpoint == "/messages" and method == "GET":
|
|
session_key = self._get_param(request, "session_key")
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
# 打开会话即清除未处理红点
|
|
await plugin.clear_unread(session_key)
|
|
return PageResponse.ok(self._build_messages(plugin, session_key))
|
|
|
|
if endpoint == "/clear_unread" and method == "POST":
|
|
session_key = body.get("session_key", "")
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
await plugin.clear_unread(session_key)
|
|
return PageResponse.ok({"cleared": True})
|
|
|
|
if endpoint == "/takeover" and method == "POST":
|
|
session_key = body.get("session_key", "")
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
ok = await plugin.set_takeover(session_key, True)
|
|
if not ok:
|
|
return PageResponse.fail("session not found")
|
|
return PageResponse.ok(self._takeover_state(plugin, session_key))
|
|
|
|
if endpoint == "/takeover" and method == "DELETE":
|
|
session_key = body.get("session_key", "")
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
ok = await plugin.set_takeover(session_key, False)
|
|
if not ok:
|
|
return PageResponse.fail("session not found")
|
|
return PageResponse.ok(self._takeover_state(plugin, session_key))
|
|
|
|
if endpoint == "/takeover_status" and method == "GET":
|
|
session_key = self._get_param(request, "session_key")
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
return PageResponse.ok(self._takeover_state(plugin, session_key))
|
|
|
|
if endpoint == "/reply" and method == "POST":
|
|
session_key = body.get("session_key", "")
|
|
text = (body.get("text") or "").strip()
|
|
image_base64 = body.get("image_base64") or ""
|
|
file_base64 = body.get("file_base64") or ""
|
|
file_name = body.get("file_name") or ""
|
|
if not session_key:
|
|
return PageResponse.fail("session_key is required")
|
|
if not text and not image_base64 and not file_base64:
|
|
return PageResponse.fail("text, image or file is required")
|
|
ok, err = await plugin.human_send(
|
|
session_key,
|
|
text=text or None,
|
|
image_base64=image_base64 or None,
|
|
file_base64=file_base64 or None,
|
|
file_name=file_name or None,
|
|
)
|
|
if not ok:
|
|
return PageResponse.fail(err or "send failed")
|
|
return PageResponse.ok(
|
|
{
|
|
"sent": True,
|
|
**self._takeover_state(plugin, session_key),
|
|
}
|
|
)
|
|
|
|
if endpoint == "/user_info" and method == "GET":
|
|
session_key = self._get_param(request, "session_key")
|
|
member_id = self._get_param(request, "member_id")
|
|
return PageResponse.ok(
|
|
await self._user_info(plugin, session_key, member_id)
|
|
)
|
|
|
|
if endpoint == "/group_info" and method == "GET":
|
|
session_key = self._get_param(request, "session_key")
|
|
return PageResponse.ok(await self._group_info(plugin, session_key))
|
|
|
|
if endpoint == "/clear" and method == "DELETE":
|
|
await plugin.clear_all()
|
|
return PageResponse.ok({"cleared": True})
|
|
|
|
return PageResponse.fail(f"Unknown: {method} {endpoint}")
|
|
except Exception as e: # noqa: BLE001
|
|
logger.error("HumanTakeover ConsolePage error on %s %s: %s", method, endpoint, e)
|
|
return PageResponse.fail(str(e))
|
|
|
|
# ==================== 工具 ====================
|
|
|
|
@staticmethod
|
|
def _get_param(request: PageRequest, key: str) -> str:
|
|
"""从 body 或 endpoint 查询参数中获取值。"""
|
|
body = request.body or {}
|
|
if isinstance(body, dict) and body.get(key):
|
|
return str(body.get(key))
|
|
# 兼容前端将参数放到 body 的情况
|
|
return ""
|
|
|
|
def _build_session_list(self, plugin) -> list[dict[str, Any]]:
|
|
result = []
|
|
for key, sess in plugin.sessions.items():
|
|
takeover = sess.get("takeover", {})
|
|
active = plugin.is_taken_over(key)
|
|
result.append(
|
|
{
|
|
"session_key": key,
|
|
"type": sess.get("type"),
|
|
"target_id": sess.get("target_id"),
|
|
"name": sess.get("name") or sess.get("target_id"),
|
|
"last_msg_at": sess.get("last_msg_at", 0),
|
|
"last_msg_preview": sess.get("last_msg_preview", ""),
|
|
"member_count": len(sess.get("members", {})),
|
|
"takeover_active": active,
|
|
"takeover_remaining": plugin.takeover_remaining(key),
|
|
"unread": bool(sess.get("unread")),
|
|
"triggered_word": sess.get("triggered_word", ""),
|
|
}
|
|
)
|
|
# 按最后消息时间倒序
|
|
result.sort(key=lambda x: x.get("last_msg_at", 0), reverse=True)
|
|
return result
|
|
|
|
def _build_messages(self, plugin, session_key: str) -> dict[str, Any]:
|
|
sess = plugin.sessions.get(session_key, {})
|
|
msgs = plugin.messages.get(session_key, [])
|
|
return {
|
|
"session_key": session_key,
|
|
"type": sess.get("type"),
|
|
"name": sess.get("name") or sess.get("target_id"),
|
|
"messages": msgs,
|
|
"members": sess.get("members", {}),
|
|
**self._takeover_state(plugin, session_key),
|
|
}
|
|
|
|
def _takeover_state(self, plugin, session_key: str) -> dict[str, Any]:
|
|
return {
|
|
"takeover_active": plugin.is_taken_over(session_key),
|
|
"takeover_remaining": plugin.takeover_remaining(session_key),
|
|
"takeover_timeout": plugin.get_takeover_timeout(),
|
|
}
|
|
|
|
async def _user_info(
|
|
self, plugin, session_key: str, member_id: str | None
|
|
) -> dict[str, Any]:
|
|
"""返回可获取到的用户信息卡片(含所属 bot 适配器信息)。"""
|
|
sess = plugin.sessions.get(session_key, {})
|
|
bot_uuid = sess.get("bot_uuid")
|
|
# 优先使用收消息时缓存的适配器,避免调试环境 bot 离线时反复请求报错
|
|
adapter = sess.get("adapter") or None
|
|
bot_name = None
|
|
if bot_uuid and not adapter:
|
|
try:
|
|
bot_info = await plugin.get_bot_info(bot_uuid)
|
|
adapter = bot_info.get("adapter")
|
|
bot_name = bot_info.get("name")
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("HumanTakeover get_bot_info failed: %s", e)
|
|
|
|
if sess.get("type") == "group" and member_id:
|
|
members = sess.get("members", {})
|
|
name = members.get(str(member_id), str(member_id))
|
|
return {
|
|
"type": "user",
|
|
"id": str(member_id),
|
|
"name": name,
|
|
"from_group": sess.get("name"),
|
|
"group_id": sess.get("target_id"),
|
|
"adapter": adapter,
|
|
"bot_name": bot_name,
|
|
}
|
|
# 私聊用户
|
|
return {
|
|
"type": "user",
|
|
"id": sess.get("target_id"),
|
|
"name": sess.get("name"),
|
|
"from_group": None,
|
|
"group_id": None,
|
|
"adapter": adapter,
|
|
"bot_name": bot_name,
|
|
}
|
|
|
|
async def _group_info(self, plugin, session_key: str) -> dict[str, Any]:
|
|
"""返回群聊信息卡片(含成员列表)。"""
|
|
sess = plugin.sessions.get(session_key, {})
|
|
info: dict[str, Any] = {
|
|
"type": "group",
|
|
"id": sess.get("target_id"),
|
|
"name": sess.get("name"),
|
|
"member_count": len(sess.get("members", {})),
|
|
"members": [
|
|
{"id": mid, "name": mname}
|
|
for mid, mname in sess.get("members", {}).items()
|
|
],
|
|
"bot_uuid": sess.get("bot_uuid"),
|
|
}
|
|
# 尝试补充 bot 信息
|
|
bot_uuid = sess.get("bot_uuid")
|
|
# 优先使用缓存适配器
|
|
cached_adapter = sess.get("adapter")
|
|
if cached_adapter:
|
|
info["adapter"] = cached_adapter
|
|
if bot_uuid and not cached_adapter:
|
|
try:
|
|
bot_info = await plugin.get_bot_info(bot_uuid)
|
|
info["bot_name"] = bot_info.get("name")
|
|
info["adapter"] = bot_info.get("adapter")
|
|
except Exception as e: # noqa: BLE001
|
|
logger.warning("HumanTakeover get_bot_info failed: %s", e)
|
|
return info
|