ForcePilot/backend/package/yuxi/channels/adapters/line/reply_chunks.py
Kris a4ec94ef9d feat(line): 实现完整的 LINE 聊天适配器功能
新增 LINE 官方账号对接的全套功能,包括:
1. 基础的 Bot 探测、会话解析、消息格式化能力
2. 富媒体消息模板、快速回复、卡片指令支持
3. Webhook 签名验证、重放防护、多账户路由管理
4. 消息发送、回复、分块传输、用户绑定管理
5. 交互式配置向导与诊断工具
2026-05-12 00:45:33 +08:00

107 lines
3.5 KiB
Python

from __future__ import annotations
import asyncio
from typing import Any
from yuxi.channels.models import DeliveryResult
_MAX_CHUNK_COUNT = 5
_LINE_MAX_CHARS = 5000
class ReplyChunker:
"""LINE 回复分块策略:基于 5 条消息上限实现智能分块。"""
def __init__(self, adapter):
self._adapter = adapter
self._accumulated: str = ""
self._sent_count: int = 0
def reset(self) -> None:
self._accumulated = ""
self._sent_count = 0
async def add_chunk(self, chat_id: str, msg_id: str, chunk: str, finished: bool) -> DeliveryResult:
self._accumulated += chunk
if finished or len(self._accumulated) >= _LINE_MAX_CHARS or self._sent_count >= _MAX_CHUNK_COUNT:
return await self._flush(chat_id)
return DeliveryResult(success=True)
async def _flush(self, chat_id: str) -> DeliveryResult:
if not self._accumulated:
return DeliveryResult(success=True)
if self._sent_count >= _MAX_CHUNK_COUNT:
self._accumulated = ""
return DeliveryResult(success=True)
result = await self._adapter.send_stream_chunk(
chat_id,
f"chunk_{self._sent_count}",
self._accumulated,
finished=self._sent_count >= _MAX_CHUNK_COUNT - 1,
)
self._accumulated = ""
self._sent_count += 1
return result
async def reply_with_chunks(
adapter,
chat_id: str,
content: str,
reply_token: str | None = None,
loading_seconds: int = 0,
) -> DeliveryResult:
"""主分块回复入口:如果内容超过 5000 字符或需要多条消息,则分块发送。"""
from yuxi.channels.models import ChannelIdentity, ChannelResponse
if loading_seconds > 0:
await adapter.send_loading_animation(chat_id, loading_seconds)
await asyncio.sleep(min(loading_seconds, 2))
if len(content) <= _LINE_MAX_CHARS:
identity = ChannelIdentity(
channel_id=adapter.channel_id,
channel_type=adapter.channel_type,
channel_user_id=chat_id,
channel_chat_id=chat_id,
)
metadata: dict[str, Any] = {}
if reply_token:
metadata["reply_token"] = reply_token
response = ChannelResponse(identity=identity, content=content, metadata=metadata)
return await adapter.send(response)
last_result = DeliveryResult(success=True)
chunk_idx = 0
remaining = content
while remaining and chunk_idx < _MAX_CHUNK_COUNT:
chunk_end = min(len(remaining), _LINE_MAX_CHARS)
chunk = remaining[:chunk_end]
remaining = remaining[chunk_end:]
finished = not remaining or chunk_idx >= _MAX_CHUNK_COUNT - 1
msg_content = chunk
if chunk_idx == 0 and reply_token:
from yuxi.channels.models import ChannelIdentity, ChannelResponse
identity = ChannelIdentity(
channel_id=adapter.channel_id,
channel_type=adapter.channel_type,
channel_user_id=chat_id,
channel_chat_id=chat_id,
)
response = ChannelResponse(identity=identity, content=msg_content, metadata={"reply_token": reply_token})
last_result = await adapter.send(response)
else:
last_result = await adapter.send_stream_chunk(chat_id, f"chunk_{chunk_idx}", msg_content, finished)
chunk_idx += 1
if chunk_idx < _MAX_CHUNK_COUNT and remaining:
await asyncio.sleep(0.5)
return last_result