新增 LINE 官方账号对接的全套功能,包括: 1. 基础的 Bot 探测、会话解析、消息格式化能力 2. 富媒体消息模板、快速回复、卡片指令支持 3. Webhook 签名验证、重放防护、多账户路由管理 4. 消息发送、回复、分块传输、用户绑定管理 5. 交互式配置向导与诊断工具
107 lines
3.5 KiB
Python
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
|