新增 QQ Bot 渠道扩展,支持在 Yuxi 平台中集成 QQ 机器人渠道。 包含以下功能模块: - api_client: QQ API 客户端封装 - api_routes: API 路由管理 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - websocket: WebSocket 实时连接 - credentials: 凭证管理 - token: Token 管理 - outbound: 外发消息管理 - outbound_media: 媒体外发 - streaming: 流式消息处理 - streaming_media: 媒体流处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - pipeline: 消息管道 - pipeline_stages: 管道阶段 - commands: 指令处理 - commands_builtin: 内置指令 - interaction: 交互处理 - approval: 审批流程 - ark: ARK 消息 - audio: 音频处理 - media: 媒体资源 - media_chunked: 分块媒体 - media_tags: 媒体标签 - message_queue: 消息队列 - delivery: 消息送达确认 - reconnect: 重连机制 - typing_keepalive: 输入状态保活 - group_activation: 群激活 - group_gating: 群门控 - group_history: 群历史 - known_users: 已知用户 - ref_index: 引用索引 - tools: Agent 工具集成 - types: 类型定义
169 lines
4.6 KiB
Python
169 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
QQ_MARKDOWN_CHUNK_LIMIT = 5000
|
|
|
|
QQ_MARKDOWN_SUPPORTED = {
|
|
"**", "__",
|
|
"*", "_",
|
|
"`",
|
|
"```",
|
|
"~~",
|
|
"[",
|
|
}
|
|
|
|
|
|
def chunk_markdown_text(text: str, limit: int = QQ_MARKDOWN_CHUNK_LIMIT) -> list[str]:
|
|
if len(text) <= limit:
|
|
return [text]
|
|
|
|
chunks = []
|
|
paragraphs = text.split("\n\n")
|
|
current = ""
|
|
|
|
for para in paragraphs:
|
|
if len(current) + len(para) + 2 <= limit:
|
|
current = f"{current}\n\n{para}" if current else para
|
|
else:
|
|
if current:
|
|
chunks.append(current)
|
|
if len(para) > limit:
|
|
sub_chunks = _split_long_paragraph(para, limit)
|
|
chunks.extend(sub_chunks[:-1])
|
|
current = sub_chunks[-1]
|
|
else:
|
|
current = para
|
|
|
|
if current:
|
|
chunks.append(current)
|
|
|
|
return chunks
|
|
|
|
|
|
def _split_long_paragraph(text: str, limit: int) -> list[str]:
|
|
result = []
|
|
sentences = re.split(r"(?<=[。!?.!?])\s*", text)
|
|
current = ""
|
|
|
|
for sent in sentences:
|
|
if not sent.strip():
|
|
continue
|
|
if len(current) + len(sent) <= limit:
|
|
current += sent
|
|
else:
|
|
if current:
|
|
result.append(current)
|
|
if len(sent) > limit:
|
|
for i in range(0, len(sent), limit):
|
|
result.append(sent[i : i + limit])
|
|
current = ""
|
|
else:
|
|
current = sent
|
|
|
|
if current:
|
|
result.append(current)
|
|
|
|
return result or [text[:limit]]
|
|
|
|
|
|
def md_to_qq_markdown(text: str) -> str:
|
|
placeholder_map: dict[str, str] = {}
|
|
text = _extract_code_blocks(text, placeholder_map)
|
|
text = _handle_headers(text)
|
|
text = _handle_tables(text)
|
|
text = _handle_images(text)
|
|
text = _restore_code_blocks(text, placeholder_map)
|
|
return text
|
|
|
|
|
|
def _extract_code_blocks(text: str, placeholder_map: dict[str, str]) -> str:
|
|
pattern = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
|
|
parts: list[str] = []
|
|
idx = 0
|
|
last_end = 0
|
|
for m in pattern.finditer(text):
|
|
idx += 1
|
|
lang = m.group(1)
|
|
code = m.group(2)
|
|
placeholder = f"\n__QBCB{idx}__\n"
|
|
placeholder_map[placeholder] = f"\n```{lang}\n{code}```\n"
|
|
parts.append(text[last_end : m.start()])
|
|
parts.append(placeholder)
|
|
last_end = m.end()
|
|
parts.append(text[last_end:])
|
|
return "".join(parts)
|
|
|
|
|
|
def _restore_code_blocks(text: str, placeholder_map: dict[str, str]) -> str:
|
|
for placeholder, code_block in placeholder_map.items():
|
|
text = text.replace(placeholder, code_block)
|
|
return text
|
|
|
|
|
|
def _handle_headers(text: str) -> str:
|
|
result = []
|
|
for line in text.split("\n"):
|
|
header_match = re.match(r"^(#{1,6})\s+(.+)", line)
|
|
if header_match:
|
|
result.append(f"**{header_match.group(2)}**")
|
|
else:
|
|
result.append(line)
|
|
return "\n".join(result)
|
|
|
|
|
|
def _handle_tables(text: str) -> str:
|
|
lines = text.split("\n")
|
|
if not any("|" in line and line.strip().startswith("|") for line in lines):
|
|
return text
|
|
|
|
result = []
|
|
i = 0
|
|
while i < len(lines):
|
|
if "|" in lines[i] and lines[i].strip().startswith("|"):
|
|
table_lines = []
|
|
while i < len(lines) and "|" in lines[i] and lines[i].strip().startswith("|"):
|
|
table_lines.append(lines[i])
|
|
i += 1
|
|
result.append(_table_to_ascii(table_lines))
|
|
else:
|
|
result.append(lines[i])
|
|
i += 1
|
|
return "\n".join(result)
|
|
|
|
|
|
def _table_to_ascii(table_lines: list[str]) -> str:
|
|
rows = []
|
|
for line in table_lines:
|
|
if re.match(r"^\|[\s\-:|]+\|$", line):
|
|
continue
|
|
cells = [cell.strip() for cell in line.strip("|").split("|")]
|
|
rows.append(cells)
|
|
|
|
if not rows:
|
|
return ""
|
|
|
|
col_widths = [max(len(row[col]) for row in rows) for col in range(len(rows[0]))]
|
|
|
|
result = []
|
|
for row in rows:
|
|
padded = [cell.ljust(col_widths[i]) for i, cell in enumerate(row)]
|
|
result.append("| " + " | ".join(padded) + " |")
|
|
|
|
return "\n".join(result)
|
|
|
|
|
|
def _handle_images(text: str) -> str:
|
|
return re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", r'<qqimg>\2</qqimg>', text)
|
|
|
|
|
|
def strip_qqbot_mentions(text: str, bot_openid: str | None = None) -> str:
|
|
if bot_openid:
|
|
text = text.replace(f"<@!{bot_openid}>", "").replace(f"<@{bot_openid}>", "")
|
|
text = re.sub(r"<@!\d+>", "", text)
|
|
text = re.sub(r"<@\d+>", "", text)
|
|
return text.strip() |