ForcePilot/backend/package/yuxi/channel/extensions/helpscout/auth.py
Kris 432d5db970 feat(helpscout): 新增Help Scout客服工单渠道插件
完成Help Scout渠道的完整功能实现,包含API客户端、webhook处理、轮询同步、自动回复、工单标签分配、客户资料管理等核心能力,附带完整的配置Schema与插件元信息
2026-05-21 10:49:56 +08:00

75 lines
2.2 KiB
Python

from __future__ import annotations
import logging
import time
from dataclasses import dataclass
import httpx
logger = logging.getLogger(__name__)
TOKEN_URL = "https://api.helpscout.net/v2/oauth2/token"
REFRESH_BUFFER_SECONDS = 300
@dataclass
class HelpScoutToken:
access_token: str
token_type: str = "bearer"
expires_at: float = 0.0
class HelpScoutAuth:
def __init__(self, app_id: str, app_secret: str):
self._app_id = app_id
self._app_secret = app_secret
self._token: HelpScoutToken | None = None
@property
def access_token(self) -> str | None:
if self._token:
return self._token.access_token
return None
@property
def auth_header(self) -> dict:
return {"Authorization": f"Bearer {self.access_token or ''}"}
@property
def is_token_valid(self) -> bool:
return self._token is not None and time.time() < self._token.expires_at - REFRESH_BUFFER_SECONDS
async def ensure_token(self) -> str:
if not self.is_token_valid:
await self._fetch_token()
return self._token.access_token
async def force_refresh(self) -> str:
logger.info("Help Scout: forcing token refresh due to 401")
await self._fetch_token()
return self._token.access_token
async def _fetch_token(self) -> None:
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_id": self._app_id,
"client_secret": self._app_secret,
},
)
resp.raise_for_status()
data = resp.json()
expires_in = data.get("expires_in", 172800)
self._token = HelpScoutToken(
access_token=data["access_token"],
token_type=data.get("token_type", "bearer"),
expires_at=time.time() + expires_in,
)
logger.info(
"Help Scout token obtained: expires_in=%ds, expires_at=%s",
expires_in,
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._token.expires_at)),
)