ForcePilot/backend/package/yuxi/channel/extensions/yuanbao/outbound/chunk.py
Kris 5946478772 feat(channel): 添加小红书、XMPP、元宝和 Zalo 渠道扩展
新增小红书、XMPP、元宝、Zalo 四个渠道扩展。

小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window

XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor

元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils

Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
2026-05-21 12:04:05 +08:00

120 lines
3.2 KiB
Python

from __future__ import annotations
def has_unclosed_fence(text: str) -> bool:
fence_patterns = ["```", "~~~"]
for pattern in fence_patterns:
count = text.count(pattern)
if count % 2 != 0:
return True
return False
def strip_outer_fence(text: str) -> str:
for pattern in ("```", "~~~"):
if text.startswith(pattern) and text.endswith(pattern):
inner = text[len(pattern):-len(pattern)]
if "\n" in inner:
return inner
return text
def merge_block_streaming(base: str, next_chunk: str) -> str:
if not base:
return next_chunk
if not next_chunk:
return base
if has_unclosed_fence(base):
return base + "\n" + next_chunk
if base.rstrip().endswith("|") and next_chunk.lstrip().startswith("|"):
return base + "\n" + next_chunk
base_end = base.rstrip()
next_start = next_chunk.lstrip()
if base_end.endswith((".", "!", "?", ":", ";", ")", "]")) and next_start[0].isalpha():
return base + " " + next_chunk
if base_end.endswith("\n") or next_start.startswith("\n"):
return base + next_chunk
return base + "\n" + next_chunk
def ends_with_table_row(text: str) -> bool:
lines = text.rstrip().split("\n")
if not lines:
return False
last = lines[-1].strip()
return last.startswith("|") and last.endswith("|") and "---" not in last
def infer_separator(base: str, next_chunk: str) -> str:
if has_unclosed_fence(base):
return "\n"
if ends_with_table_row(base):
return "\n"
return "\n"
def chunk_atomic_aware(text: str, max_chars: int, base_chunk: str = "") -> list[str]:
if len(text) <= max_chars:
return [text]
chunks = []
sections = text.split("\n\n")
for section in sections:
if len(section) > max_chars:
if has_unclosed_fence(section):
chunks.append(section)
else:
paragraphs = section.split("\n")
current = ""
for para in paragraphs:
if len(current) + len(para) + 1 > max_chars and current:
chunks.append(current)
current = para
else:
current = current + "\n" + para if current else para
if current:
chunks.append(current)
else:
chunks.append(section)
if not chunks:
return [text[:max_chars]]
return chunks
def drain_buffer(
buffer: str,
min_chars: int,
max_chars: int,
merge_base: str = "",
) -> tuple[list[str], str]:
if not buffer:
return [], buffer
if len(buffer) < min_chars:
if has_unclosed_fence(buffer):
merged = merge_block_streaming(merge_base, buffer) if merge_base else buffer
return [merged], ""
if ends_with_table_row(buffer):
return [], buffer
return [], buffer
merged = merge_block_streaming(merge_base, buffer) if merge_base else buffer
if len(merged) <= max_chars:
return [merged], ""
chunks = chunk_atomic_aware(merged, max_chars)
if len(chunks) == 1:
return [chunks[0]], ""
return chunks[:-1], chunks[-1]