"""数字分身 BOXIM 单聊接管 API。""" from datetime import datetime, timedelta from fastapi import APIRouter, Body, Depends, Header from sqlalchemy.orm import Session from database import get_db from models import TakeoverCursor, TakeoverReplyTask, User from responses import fail, ok from routers.authorizations import _require_authorization from routers.avatars import _require_owned_avatar router = APIRouter(tags=["分身接管"]) @router.get("/avatar/{avatar_id}/takeover/status") def get_takeover_status( avatar_id: str, authorization: str = Header(None), db: Session = Depends(get_db), ): avatar = _require_owned_avatar(db, avatar_id, authorization) permissions = (avatar.config or {}).get("authorizationPermissions", []) enabled = isinstance(permissions, list) and "takeover" in permissions user = db.query(User).filter(User.huihui_user_id == avatar.owner_id).first() cursor = db.query(TakeoverCursor).filter(TakeoverCursor.avatar_id == avatar.id).first() pending_count = ( db.query(TakeoverReplyTask) .filter( TakeoverReplyTask.avatar_id == avatar.id, TakeoverReplyTask.status.in_(("pending", "generating", "ready", "sending")), ) .count() ) if cursor and cursor.last_error: status, message = "error", cursor.last_error elif not enabled: status, message = "disabled", "主动接管未开启" elif not user or not user.huihui_token: status, message = "needs_login", "请重新登录会会生产账号以连接 BOXIM" elif ( cursor and cursor.initialized and cursor.last_polled_at and cursor.last_polled_at >= datetime.utcnow() - timedelta(seconds=15) ): status, message = "ready", "BOXIM 已连接,收到私聊消息 3 秒后自动回复" else: status, message = "connecting", "正在连接 BOXIM" return ok( { "enabled": enabled, "status": status, "message": message, "pendingCount": pending_count, "lastPolledAt": cursor.last_polled_at.isoformat() if cursor and cursor.last_polled_at else None, } ) def _has(payload: dict, camel_key: str, snake_key: str) -> bool: return camel_key in payload or snake_key in payload def _read(payload: dict, camel_key: str, snake_key: str, default=None): if camel_key in payload: return payload[camel_key] if snake_key in payload: return payload[snake_key] return default @router.put("/avatar/{avatar_id}/authorizations/takeover") def update_takeover_config( avatar_id: str, payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db), ): _require_owned_avatar(db, avatar_id, authorization) auth_id = _read(payload, "authorizationId", "authorization_id") if not auth_id: return fail("缺少 authorization_id", 400) auth = _require_authorization(db, avatar_id, str(auth_id)) enabled = bool(auth.takeover_enabled) mode = auth.takeover_mode or "immediate" delay = auth.takeover_delay_seconds or 30 if _has(payload, "takeoverEnabled", "takeover_enabled"): raw_enabled = _read(payload, "takeoverEnabled", "takeover_enabled") if not isinstance(raw_enabled, bool): return fail("takeover_enabled 必须是布尔值", 400) enabled = raw_enabled if _has(payload, "takeoverMode", "takeover_mode"): mode = _read(payload, "takeoverMode", "takeover_mode") if mode not in ("immediate", "delayed"): return fail("takeover_mode 必须是 immediate 或 delayed", 400) if _has(payload, "takeoverDelaySeconds", "takeover_delay_seconds"): delay = _read(payload, "takeoverDelaySeconds", "takeover_delay_seconds") if isinstance(delay, bool) or not isinstance(delay, int) or not 5 <= delay <= 3600: return fail("延迟时间需在 5 到 3600 秒之间", 400) if enabled and auth.target_type != "user": return fail("本期仅支持对会会用户开启单聊接管", 400) if enabled and auth.status != "active": return fail("请先启用该授权,再开启聊天接管", 400) permissions = list(auth.permissions or []) if enabled: if "chat" not in permissions and "reply" not in permissions: permissions.append("chat") if "takeover" not in permissions: permissions.append("takeover") else: permissions = [permission for permission in permissions if permission != "takeover"] auth.permissions = permissions auth.takeover_enabled = enabled auth.takeover_mode = mode auth.takeover_delay_seconds = delay db.commit() db.refresh(auth) return ok(auth.to_dict(), "接管配置已保存")