ForcePilot/backend/package/yuxi/channels/adapters/wechat/streaming_fallback.py
Kris f551745ec2 refactor(wechat adapter): 重构微信适配器相关代码
1. 删除废弃的 timeline_adapter.py 文件
2. 新增历史消息拉取器基类实现 WeChatHistoryFetcher
3. 新增流式消息分片发送和打字指示器支持逻辑
2026-05-13 16:17:06 +08:00

129 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import re
from typing import TYPE_CHECKING
from yuxi.channels.models import DeliveryResult
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from .adapter import WeChatAdapter
_PARAGRAPH_SPLITTER = re.compile(r"\n{2,}")
_SENTENCE_BOUNDARY = re.compile(r"[。!?.!?\n]")
class ParagraphChunker:
def __init__(self, chunk_size: int = 2000, min_chunk_size: int = 100):
self.chunk_size = chunk_size
self.min_chunk_size = min_chunk_size
self._buffer: list[str] = []
def feed(self, text: str) -> list[str]:
self._buffer.append(text)
accumulated = "".join(self._buffer)
results: list[str] = []
if "\n\n" in accumulated:
paragraphs = _PARAGRAPH_SPLITTER.split(accumulated)
if len(paragraphs) > 1:
for para in paragraphs[:-1]:
stripped = para.strip()
if len(stripped) >= self.min_chunk_size:
results.append(stripped)
self._buffer = [paragraphs[-1]]
return results
def flush(self) -> list[str]:
if not self._buffer:
return []
accumulated = "".join(self._buffer)
self._buffer = []
if not accumulated.strip():
return []
if len(accumulated) <= self.chunk_size:
return [accumulated.strip()]
results = []
remaining = accumulated
while len(remaining) > self.chunk_size:
split_at = self._find_split_point(remaining, self.chunk_size)
results.append(remaining[:split_at].strip())
remaining = remaining[split_at:].strip()
if remaining.strip():
results.append(remaining.strip())
return results
def _find_split_point(self, text: str, limit: int) -> int:
text_slice = text[:limit]
for sep in ("\n\n", "\n", "", "", "", ". ", "! ", "? "):
idx = text_slice.rfind(sep)
if idx > limit // 2:
return idx + len(sep)
return limit
class ProgressPrefixFormatter:
def __init__(self, template: str = "({current}/{total})", enabled: bool = True):
self._template = template
self.enabled = enabled
def format(self, current: int, total: int) -> str:
if not self.enabled:
return ""
return self._template.format(current=current, total=total)
async def send_stream_chunked(
adapter: WeChatAdapter,
chat_id: str,
text: str,
finished: bool = False,
) -> DeliveryResult:
from yuxi.channels.models import ChannelIdentity, ChannelResponse
identity = ChannelIdentity(
channel_id=adapter.channel_id,
channel_type=adapter.channel_type,
channel_chat_id=chat_id,
channel_user_id="",
)
response = ChannelResponse(identity=identity, content=text)
try:
return await adapter.send(response)
except Exception as e:
logger.error(f"[WeChat] Chunked send failed for {chat_id}: {e}")
return DeliveryResult(success=False, error=str(e))
async def send_with_typing(
adapter: WeChatAdapter,
chat_id: str,
text: str,
typing_enabled: bool = True,
is_wecom: bool = False,
) -> DeliveryResult:
if typing_enabled and is_wecom:
try:
await adapter.heartbeat_adapter.send_typing_indicator(adapter.send, chat_id)
except Exception:
pass
from yuxi.channels.models import ChannelIdentity, ChannelResponse
identity = ChannelIdentity(
channel_id=adapter.channel_id,
channel_type=adapter.channel_type,
channel_chat_id=chat_id,
channel_user_id="",
)
response = ChannelResponse(identity=identity, content=text)
try:
return await adapter.send(response)
except Exception as e:
return DeliveryResult(success=False, error=str(e))