ForcePilot/backend/package/yuxi/channel/extensions/kuaishou/api.py
Kris f6df6e2c95 feat(channel): 添加快手渠道扩展
新增快手(Kuaishou)渠道扩展,支持在 Yuxi 平台中集成快手客服渠道。

包含以下功能模块:
- api: 快手 API 客户端封装
- accounts: 账户管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- media: 媒体资源处理
- types: 类型定义
2026-05-21 11:09:37 +08:00

132 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 授权 URLauthorization_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()