ForcePilot/backend/package/yuxi/channels/adapters/qqbot/token.py
Kris ef5483dc1a refactor(qqbot): 重构QQ机器人适配器代码,优化多项功能与结构
主要变更:
1. 修复速率限流器使用setdefault替代重复创建令牌桶
2. 重构交互注册表匹配逻辑,优化精确匹配查找
3. 重构去重缓存逻辑,移到适配器实例方法
4. 重构发送URL解析,增加合法性校验并拆分公共方法
5. 优化流式消息处理逻辑,简化flush_controller调用
6. 重构群聊类型判断代码,简化语法
7. 修复重连管理器对None类型关闭分类的处理
8. 新增消息缓存、线程模拟器、发送初始化模块
9. 重构凭证备份与会话存储逻辑,支持环境变量指定状态目录
10. 新增配置提示与向导二维码绑定功能
11. 优化媒体上传逻辑,增加重试机制与缓存
12. 新增审批键盘模板构建函数
13. 重构消息格式处理,修正媒体发送字段与长度限制
14. 修复令牌过期时间计算,使用time.time替代monotonic
15. 新增群组激活缓冲区与用户追踪器增强功能
16. 修复换行符问题,统一文件结尾格式
2026-05-13 16:13:48 +08:00

131 lines
4.6 KiB
Python

from __future__ import annotations
import asyncio
import time
import aiohttp
from yuxi.channels.exceptions import ChannelAuthenticationError
from yuxi.utils.logging_config import logger
class QQBotTokenManager:
def __init__(
self,
app_id: str,
app_secret: str,
sandbox: bool = False,
http_client: aiohttp.ClientSession | None = None,
):
self.app_id = app_id
self.app_secret = app_secret
self.sandbox = sandbox
self._http_client = http_client
self._access_token: str | None = None
self._expires_at: float | None = None
self._token_lock = asyncio.Lock()
self._refresh_in_progress: asyncio.Event | None = None
self._refresh_task: asyncio.Task | None = None
self._refresh_interval: float = 60.0
@property
def api_base(self) -> str:
if self.sandbox:
return "https://sandbox.api.sgroup.qq.com"
return "https://api.sgroup.qq.com"
@property
def access_token(self) -> str | None:
return self._access_token
@property
def expires_at(self) -> float | None:
return self._expires_at
async def get_token(self) -> str:
async with self._token_lock:
if self._is_expired():
await self._do_refresh()
return self._access_token
async def force_refresh(self) -> str:
async with self._token_lock:
await self._do_refresh()
return self._access_token
async def _do_refresh(self) -> None:
if self._refresh_in_progress is not None:
await self._refresh_in_progress.wait()
return
self._refresh_in_progress = asyncio.Event()
try:
await self._refresh()
self._refresh_in_progress.set()
except Exception:
self._refresh_in_progress.set()
raise
finally:
self._refresh_in_progress = None
async def _refresh(self) -> None:
client = self._http_client or aiohttp.ClientSession()
try:
async with client.post(
f"{self.api_base}/oauth2/token",
json={
"app_id": self.app_id,
"app_secret": self.app_secret,
},
) as resp:
if resp.status != 200:
raise ChannelAuthenticationError(f"Token refresh failed: HTTP {resp.status}")
data = await resp.json()
self._access_token = data["access_token"]
expires_in = data.get("expires_in", 7200)
self._expires_at = time.time() + expires_in
logger.info(f"[QQBot] Token refreshed, expires in {expires_in}s (app_id={self.app_id[:6]}...)")
finally:
if not self._http_client:
await client.close()
def start_background_refresh(self) -> None:
if self._refresh_task is not None and not self._refresh_task.done():
return
self._refresh_task = asyncio.create_task(self._background_refresh_loop())
logger.debug(f"[QQBot] Background token refresh started (interval={self._refresh_interval}s)")
def stop_background_refresh(self) -> None:
if self._refresh_task and not self._refresh_task.done():
self._refresh_task.cancel()
self._refresh_task = None
logger.debug("[QQBot] Background token refresh stopped")
async def _background_refresh_loop(self) -> None:
while True:
try:
sleep_duration = self._refresh_interval
if self._expires_at is not None:
remaining = self._expires_at - time.time() - 300
sleep_duration = max(60.0, min(self._refresh_interval, max(remaining, 0.0)))
await asyncio.sleep(sleep_duration)
async with self._token_lock:
if not self._is_expired():
continue
try:
await self._do_refresh()
except Exception as e:
logger.warning(f"[QQBot] Background token refresh failed (will retry): {e}")
except asyncio.CancelledError:
logger.debug("[QQBot] Background token refresh cancelled")
break
except Exception as e:
logger.error(f"[QQBot] Background token refresh loop error: {e}")
await asyncio.sleep(5)
def _is_expired(self) -> bool:
if self._access_token is None or self._expires_at is None:
return True
return time.time() > self._expires_at - 300