ForcePilot/backend/package/yuxi/channels/adapters/wechat/streaming_fallback.py

129 lines
3.9 KiB
Python
Raw Normal View History

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))