新增快手(Kuaishou)渠道扩展,支持在 Yuxi 平台中集成快手客服渠道。 包含以下功能模块: - api: 快手 API 客户端封装 - accounts: 账户管理 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - media: 媒体资源处理 - types: 类型定义
132 lines
4.9 KiB
Python
132 lines
4.9 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import time
|
||
from typing import Any
|
||
from urllib.parse import urlencode
|
||
|
||
import httpx
|
||
|
||
from .types import TokenInfo
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
KUWAISHOU_OPEN_API_BASE = "https://open.kuaishou.com"
|
||
|
||
# 快手官方 access_token 有效期 48h (172800s)
|
||
DEFAULT_EXPIRES_IN = 172800
|
||
# refresh_token 有效期 180 天 (15552000s)
|
||
DEFAULT_REFRESH_EXPIRES_IN = 15552000
|
||
|
||
|
||
class KuaishouAPIError(Exception):
|
||
def __init__(self, message: str, code: int = -1, raw: dict | None = None):
|
||
super().__init__(message)
|
||
self.code = code
|
||
self.raw = raw or {}
|
||
|
||
|
||
class KuaishouAPIClient:
|
||
def __init__(self, app_id: str, app_secret: str, timeout: float = 15.0):
|
||
self._app_id = app_id
|
||
self._app_secret = app_secret
|
||
self._http = httpx.AsyncClient(
|
||
base_url=KUWAISHOU_OPEN_API_BASE,
|
||
timeout=timeout,
|
||
)
|
||
self._token_info: TokenInfo | None = None
|
||
self._token_lock = asyncio.Lock()
|
||
|
||
async def close(self):
|
||
await self._http.aclose()
|
||
|
||
@property
|
||
def access_token(self) -> str | None:
|
||
if self._token_info and time.time() < self._token_info.expires_at - 300:
|
||
return self._token_info.access_token
|
||
return None
|
||
|
||
def get_authorization_url(self, redirect_uri: str, scope: str = "user_info") -> str:
|
||
"""生成快手 OAuth 授权 URL(authorization_code 模式)。"""
|
||
params = {
|
||
"app_id": self._app_id,
|
||
"redirect_uri": redirect_uri,
|
||
"scope": scope,
|
||
"response_type": "code",
|
||
}
|
||
return f"{KUWAISHOU_OPEN_API_BASE}/oauth2/authorize?{urlencode(params)}"
|
||
|
||
async def fetch_access_token(self, code: str, redirect_uri: str) -> TokenInfo:
|
||
"""使用 authorization_code 换取 access_token。
|
||
|
||
快手官方格式:GET /oauth2/access_token?grant_type=authorization_code
|
||
&code={code}&redirect_uri={redirect_uri}&app_id={app_id}&app_secret={app_secret}
|
||
"""
|
||
async with self._token_lock:
|
||
params = {
|
||
"grant_type": "authorization_code",
|
||
"code": code,
|
||
"redirect_uri": redirect_uri,
|
||
"app_id": self._app_id,
|
||
"app_secret": self._app_secret,
|
||
}
|
||
resp = await self._http.get("/oauth2/access_token", params=params)
|
||
data = resp.json()
|
||
if data.get("result") != 1:
|
||
raise KuaishouAPIError(
|
||
f"获取 access_token 失败: {data}",
|
||
code=data.get("error_code", -1),
|
||
raw=data,
|
||
)
|
||
self._token_info = TokenInfo(
|
||
access_token=data["access_token"],
|
||
expires_at=time.time() + data.get("expires_in", DEFAULT_EXPIRES_IN),
|
||
refresh_token=data.get("refresh_token", ""),
|
||
refresh_expires_at=time.time() + data.get("refresh_expires_in", DEFAULT_REFRESH_EXPIRES_IN),
|
||
)
|
||
return self._token_info
|
||
|
||
async def refresh_access_token(self) -> TokenInfo:
|
||
if not self._token_info or not self._token_info.refresh_token:
|
||
raise KuaishouAPIError("无 refresh_token,无法刷新")
|
||
|
||
async with self._token_lock:
|
||
params = {
|
||
"grant_type": "refresh_token",
|
||
"refresh_token": self._token_info.refresh_token,
|
||
"app_id": self._app_id,
|
||
"app_secret": self._app_secret,
|
||
}
|
||
resp = await self._http.get("/oauth2/refresh_token", params=params)
|
||
data = resp.json()
|
||
if data.get("result") != 1:
|
||
raise KuaishouAPIError(
|
||
f"刷新 access_token 失败: {data}",
|
||
code=data.get("error_code", -1),
|
||
raw=data,
|
||
)
|
||
self._token_info = TokenInfo(
|
||
access_token=data["access_token"],
|
||
expires_at=time.time() + data.get("expires_in", DEFAULT_EXPIRES_IN),
|
||
refresh_token=data.get("refresh_token", ""),
|
||
refresh_expires_at=time.time() + data.get("refresh_expires_in", DEFAULT_REFRESH_EXPIRES_IN),
|
||
)
|
||
return self._token_info
|
||
|
||
async def ensure_token(self) -> str:
|
||
if token := self.access_token:
|
||
return token
|
||
if self._token_info and self._token_info.refresh_token:
|
||
info = await self.refresh_access_token()
|
||
return info.access_token
|
||
raise KuaishouAPIError("未获取 access_token,请先完成 OAuth 授权")
|
||
|
||
async def post(self, path: str, json: dict | None = None, **kwargs) -> dict[str, Any]:
|
||
resp = await self._http.post(path, json=json, **kwargs)
|
||
return resp.json()
|
||
|
||
async def get(self, path: str, **kwargs) -> dict[str, Any]:
|
||
resp = await self._http.get(path, **kwargs)
|
||
return resp.json()
|