ForcePilot/backend/package/yuxi/channel/extensions/qqbot/format.py
Kris 2ab65f153f feat(channel): 添加 QQ Bot 渠道扩展
新增 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: 类型定义
2026-05-21 11:35:12 +08:00

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