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

70 lines
2.1 KiB
Python

from __future__ import annotations
import logging
import httpx
from yuxi.channel.extensions.helpscout.auth import HelpScoutAuth
from yuxi.channel.extensions.helpscout.client import HelpScoutClient
from yuxi.channel.extensions.helpscout.config import resolve_account
logger = logging.getLogger(__name__)
MAX_ATTACHMENT_SIZE_MB = 50
MAX_ATTACHMENT_SIZE_BYTES = MAX_ATTACHMENT_SIZE_MB * 1024 * 1024
async def upload_attachment(
file_content: bytes,
file_name: str,
mime_type: str,
account_id: str | None = None,
) -> dict:
if len(file_content) > MAX_ATTACHMENT_SIZE_BYTES:
raise ValueError(
f"Attachment size exceeds {MAX_ATTACHMENT_SIZE_MB}MB limit: {len(file_content) / 1024 / 1024:.1f}MB"
)
account = resolve_account(account_id or "default")
if not account.get("app_id") or not account.get("app_secret"):
raise RuntimeError("Help Scout account not configured")
auth = HelpScoutAuth(account["app_id"], account["app_secret"])
await auth.ensure_token()
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post(
"https://api.helpscout.net/v2/attachments",
files={"file": (file_name, file_content, mime_type)},
headers={"Authorization": f"Bearer {auth.access_token}"},
)
resp.raise_for_status()
result = resp.json()
logger.info(
"Help Scout attachment uploaded: id=%s name=%s",
result.get("id"),
file_name,
)
return result
async def download_attachment(
attachment_id: str,
conversation_id: int,
account_id: str | None = None,
) -> bytes:
account = resolve_account(account_id or "default")
auth = HelpScoutAuth(account["app_id"], account["app_secret"])
client = HelpScoutClient(auth)
try:
content = await client.download_attachment(attachment_id, conversation_id)
logger.debug(
"Help Scout attachment downloaded: id=%s size=%d",
attachment_id,
len(content),
)
return content
finally:
await client.close()