ForcePilot/backend/package/yuxi/channel/extensions/line/gateway.py
Kris 7fd658af57 feat(channel): 添加 LINE 渠道扩展
新增 LINE 渠道扩展,支持在 Yuxi 平台中集成 LINE 即时通讯渠道。

包含以下功能模块:
- bot: LINE Bot 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- token_manager: Token 管理
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- flex_templates: Flex 模板消息
- card_command: 卡片指令处理
- template_messages: 模板消息
- rich_menu: 富菜单管理
- actions: 动作处理
- directives: 指令处理
- delivery: 消息送达确认
- loading: 加载动画
- media: 媒体资源处理
- types: 类型定义
2026-05-21 11:16:16 +08:00

110 lines
3.8 KiB
Python

from __future__ import annotations
import asyncio
import logging
from yuxi.channel.extensions.line.bot import LineBotClient
from yuxi.channel.extensions.line.webhook import LineWebhookHandler, set_webhook_config
logger = logging.getLogger(__name__)
class LineGatewayAdapter:
def __init__(self):
self._webhook = LineWebhookHandler()
self._running = False
async def start(self, ctx) -> object:
account_id = getattr(ctx, "account_id", "default")
config = getattr(ctx, "config", {}) or {}
account = self._resolve_account(ctx)
token = account.get("channel_access_token", "")
secret = account.get("channel_secret", "")
if not token or not secret:
logger.warning("LINE gateway start: token or secret not configured for account %s", account_id)
self._running = True
queue = getattr(ctx, "queue", asyncio.Queue())
return queue
bot = LineBotClient(channel_access_token=token)
profile = await bot.get_bot_info()
if profile:
logger.info(
"LINE bot connected: display_name=%s user_id=%s",
profile.display_name,
profile.user_id,
)
else:
logger.warning("LINE bot probe failed for account %s", account_id)
self._webhook.start()
self._running = True
set_webhook_config(config)
queue = getattr(ctx, "queue", asyncio.Queue())
return queue
@staticmethod
def _resolve_account(ctx) -> dict:
account_id = getattr(ctx, "account_id", "default")
config = getattr(ctx, "config", {}) or {}
line_cfg = config.get("channels", {}).get("line", {})
if not isinstance(line_cfg, dict):
line_cfg = {}
accounts = line_cfg.get("accounts", {})
if not isinstance(accounts, dict):
accounts = {}
account_raw = accounts.get(account_id, {})
if not isinstance(account_raw, dict):
account_raw = {}
token = (
account_raw.get("channel_access_token")
or line_cfg.get("channel_access_token", "")
)
secret = (
account_raw.get("channel_secret")
or line_cfg.get("channel_secret", "")
)
return {
"account_id": account_id,
"channel_access_token": token,
"channel_secret": secret,
"name": account_raw.get("name", account_id),
"dm_policy": account_raw.get("dm_policy", line_cfg.get("dm_policy", "pairing")),
"group_policy": account_raw.get("group_policy", line_cfg.get("group_policy", "allowlist")),
"allow_from": account_raw.get("allow_from", line_cfg.get("allow_from", [])),
"group_allow_from": account_raw.get("group_allow_from", line_cfg.get("group_allow_from", [])),
"webhook_path": account_raw.get("webhook_path", line_cfg.get("webhook_path", "/line/webhook")),
"text_chunk_limit": account_raw.get("text_chunk_limit", line_cfg.get("text_chunk_limit", 5000)),
"media_max_mb": account_raw.get("media_max_mb", line_cfg.get("media_max_mb", 10)),
}
async def stop(self, ctx) -> None:
self._running = False
self._webhook.stop()
logger.info("LINE gateway stopped")
async def probe_bot(self, account: dict) -> dict | None:
token = account.get("channel_access_token", "")
if not token:
return None
bot = LineBotClient(channel_access_token=token)
profile = await bot.get_bot_info()
if profile:
return {
"user_id": profile.user_id,
"display_name": profile.display_name,
"picture_url": profile.picture_url,
"chat_mode": profile.chat_mode,
}
return None