完成Help Scout渠道的完整功能实现,包含API客户端、webhook处理、轮询同步、自动回复、工单标签分配、客户资料管理等核心能力,附带完整的配置Schema与插件元信息
155 lines
5.5 KiB
Python
155 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
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
|
|
from yuxi.channel.extensions.helpscout.template import text_to_html_body
|
|
from yuxi.channel.extensions.helpscout.types import OutboundResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HelpScoutOutbound:
|
|
delivery_mode = "direct"
|
|
chunker_mode = "length"
|
|
text_chunk_limit: int = 10000
|
|
|
|
async def send_text(
|
|
self,
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> OutboundResult:
|
|
account = resolve_account(account_id or "default")
|
|
if not account.get("app_id") or not account.get("app_secret") or int(account.get("mailbox_id", 0)) <= 0:
|
|
logger.error("Help Scout outbound: account not configured")
|
|
return OutboundResult(success=False, error="account_not_configured")
|
|
|
|
auto_reply_mode = account.get("auto_reply_mode", "draft")
|
|
if auto_reply_mode == "off":
|
|
logger.debug("Help Scout outbound: auto_reply_mode=off, skipping send")
|
|
return OutboundResult(success=False, error="auto_reply_disabled")
|
|
|
|
conversation_id = int(target_id.split(":")[-1])
|
|
|
|
client = self._build_client(account)
|
|
is_draft = auto_reply_mode == "draft"
|
|
html_content = text_to_html_body(content)
|
|
|
|
try:
|
|
result = await client.reply_conversation(
|
|
conversation_id=conversation_id,
|
|
text=content,
|
|
html=html_content,
|
|
draft=is_draft,
|
|
)
|
|
reply_id = result.get("id", "")
|
|
logger.info(
|
|
"Help Scout reply %s: conv=%d reply_id=%s",
|
|
"drafted" if is_draft else "sent",
|
|
conversation_id,
|
|
reply_id,
|
|
)
|
|
return OutboundResult(success=True, message_id=str(reply_id))
|
|
except Exception as e:
|
|
logger.exception("Help Scout send_text failed: conv=%d", conversation_id)
|
|
return OutboundResult(success=False, error="exception", detail=str(e))
|
|
finally:
|
|
await client.close()
|
|
|
|
async def send_draft(
|
|
self,
|
|
conversation_id: int,
|
|
content: str,
|
|
account_id: str | None = None,
|
|
) -> OutboundResult:
|
|
account = resolve_account(account_id or "default")
|
|
client = self._build_client(account)
|
|
|
|
try:
|
|
result = await client.reply_conversation(
|
|
conversation_id=conversation_id,
|
|
text=content,
|
|
draft=True,
|
|
)
|
|
return OutboundResult(success=True, message_id=str(result.get("id", "")))
|
|
except Exception as e:
|
|
logger.exception("Help Scout send_draft failed: conv=%d", conversation_id)
|
|
return OutboundResult(success=False, error="exception", detail=str(e))
|
|
finally:
|
|
await client.close()
|
|
|
|
async def add_note(
|
|
self,
|
|
conversation_id: int,
|
|
text: str,
|
|
account_id: str | None = None,
|
|
) -> OutboundResult:
|
|
account = resolve_account(account_id or "default")
|
|
client = self._build_client(account)
|
|
|
|
try:
|
|
result = await client.create_note(
|
|
conversation_id=conversation_id,
|
|
text=text,
|
|
)
|
|
return OutboundResult(success=True, message_id=str(result.get("id", "")))
|
|
except Exception as e:
|
|
logger.exception("Help Scout add_note failed: conv=%d", conversation_id)
|
|
return OutboundResult(success=False, error="exception", detail=str(e))
|
|
finally:
|
|
await client.close()
|
|
|
|
def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]:
|
|
if len(text) <= limit:
|
|
return [text]
|
|
chunks = []
|
|
while len(text) > limit:
|
|
break_pos = text.rfind("\n", 0, limit)
|
|
if break_pos == -1:
|
|
break_pos = limit
|
|
chunks.append(text[:break_pos])
|
|
text = text[break_pos:].lstrip("\n")
|
|
if text:
|
|
chunks.append(text)
|
|
return chunks
|
|
|
|
async def send_media(
|
|
self,
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
) -> OutboundResult:
|
|
account = resolve_account("default")
|
|
conversation_id = int(target_id.split(":")[-1])
|
|
client = self._build_client(account)
|
|
|
|
try:
|
|
msg = f"[{media_type.upper()}] {media_url}"
|
|
result = await client.reply_conversation(
|
|
conversation_id=conversation_id,
|
|
text=msg,
|
|
draft=True,
|
|
)
|
|
return OutboundResult(success=True, message_id=str(result.get("id", "")))
|
|
except Exception as e:
|
|
logger.exception("Help Scout send_media failed: conv=%d", conversation_id)
|
|
return OutboundResult(success=False, error="exception", detail=str(e))
|
|
finally:
|
|
await client.close()
|
|
|
|
def sanitize_text(self, text: str, payload: object) -> str:
|
|
return text
|
|
|
|
@staticmethod
|
|
def _build_client(account: dict) -> HelpScoutClient:
|
|
auth = HelpScoutAuth(account["app_id"], account["app_secret"])
|
|
return HelpScoutClient(auth)
|