refactor(wechat adapter): 重构微信适配器相关代码
1. 删除废弃的 timeline_adapter.py 文件 2. 新增历史消息拉取器基类实现 WeChatHistoryFetcher 3. 新增流式消息分片发送和打字指示器支持逻辑
This commit is contained in:
parent
a1d9ba9683
commit
f551745ec2
@ -0,0 +1,128 @@
|
|||||||
|
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))
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from yuxi.channels.history_injector import HistoryFetcher
|
||||||
|
from yuxi.channels.models import FetchOptions, HistoricalMessage
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class WeChatHistoryFetcher(HistoryFetcher):
|
||||||
|
def __init__(self, mode: str, client: Any = None):
|
||||||
|
self._mode = mode
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
async def fetch_thread_history(self, thread_id: str, options: FetchOptions) -> list[HistoricalMessage]:
|
||||||
|
if self._mode == "wecom":
|
||||||
|
return await self._fetch_wecom_history(thread_id, options)
|
||||||
|
if self._mode == "mp":
|
||||||
|
return await self._fetch_mp_history(thread_id, options)
|
||||||
|
if self._mode == "personal":
|
||||||
|
return await self._fetch_personal_history(thread_id, options)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def fetch_parent_message(self, thread_id: str) -> HistoricalMessage | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fetch_wecom_history(self, thread_id: str, options: FetchOptions) -> list[HistoricalMessage]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def _fetch_mp_history(self, thread_id: str, options: FetchOptions) -> list[HistoricalMessage]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def _fetch_personal_history(self, thread_id: str, options: FetchOptions) -> list[HistoricalMessage]:
|
||||||
|
return []
|
||||||
@ -1,42 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
from yuxi.utils.logging_config import logger
|
|
||||||
|
|
||||||
from .mp.client import MPClient
|
|
||||||
from .wecom.client import WeComClient
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_timeline_wecom(
|
|
||||||
client: WeComClient, http_client: httpx.AsyncClient, chat_id: str, limit: int = 50
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
messages: list[dict[str, Any]] = []
|
|
||||||
try:
|
|
||||||
await client.get_access_token()
|
|
||||||
return messages
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[WeChat/Timeline] resolve_timeline_wecom failed: {e}")
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_timeline_mp(
|
|
||||||
client: MPClient, http_client: httpx.AsyncClient, open_id: str, limit: int = 50
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
messages: list[dict[str, Any]] = []
|
|
||||||
try:
|
|
||||||
await client.get_access_token()
|
|
||||||
return messages
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[WeChat/Timeline] resolve_timeline_mp failed: {e}")
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
async def resolve_timeline_bridge(bridge_client, chat_id: str, limit: int = 50) -> list[dict[str, Any]]:
|
|
||||||
try:
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[WeChat/Timeline] resolve_timeline_bridge failed: {e}")
|
|
||||||
return []
|
|
||||||
Loading…
Reference in New Issue
Block a user