新增 Slack 渠道扩展,支持在 Yuxi 平台中集成 Slack 团队协作平台。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - monitor: 渠道状态监控 - status: 会话状态管理 - actions: 交互动作处理 - interactive: 交互式消息 - commands: 斜杠指令 - threading: 线程管理 - mentions: @提及 - constants: 常量定义 - types: 类型定义
203 lines
5.5 KiB
Python
203 lines
5.5 KiB
Python
import re
|
|
|
|
from yuxi.channel.extensions.slack.constants import (
|
|
SLACK_AGENT_PROMPT_RULES,
|
|
SLACK_MENTION_PATTERN,
|
|
SLACK_TEXT_LIMIT,
|
|
)
|
|
|
|
_LINK_PATTERN = re.compile(r"\[([^\]]*)\]\(([^)]*)\)")
|
|
_BOLD_PATTERN = re.compile(r"\*\*(.+?)\*\*")
|
|
_ITALIC_PATTERN = re.compile(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)")
|
|
_STRIKETHROUGH_PATTERN = re.compile(r"~~(.+?)~~")
|
|
_HEADING_PATTERN = re.compile(r"^#{1,6}\s+(.+)$", re.MULTILINE)
|
|
|
|
_FORMAT_TOKEN_PATTERN = re.compile(
|
|
r"""
|
|
\*[^*]+\* |
|
|
_[^_]+_ |
|
|
~[^~]+~ |
|
|
`[^`]+` |
|
|
<[^>]+>
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def _find_safe_split_position(text: str, pos: int) -> int:
|
|
for match in _FORMAT_TOKEN_PATTERN.finditer(text):
|
|
start, end = match.start(), match.end()
|
|
if start < pos < end:
|
|
before_len = pos - start
|
|
after_len = end - pos
|
|
return start if before_len <= after_len else end
|
|
return pos
|
|
|
|
|
|
def markdown_to_slack(text: str) -> str:
|
|
text = _LINK_PATTERN.sub(r"<\2|\1>", text)
|
|
text = _BOLD_PATTERN.sub(r"*\1*", text)
|
|
text = _ITALIC_PATTERN.sub(r"_\1_", text)
|
|
text = _STRIKETHROUGH_PATTERN.sub(r"~\1~", text)
|
|
text = _HEADING_PATTERN.sub(r"*\1*", text)
|
|
lines = text.split("\n")
|
|
result: list[str] = []
|
|
table_buffer: list[str] = []
|
|
in_table = False
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith("|") and stripped.endswith("|"):
|
|
if not in_table:
|
|
in_table = True
|
|
table_buffer = []
|
|
table_buffer.append(stripped)
|
|
continue
|
|
else:
|
|
if in_table:
|
|
in_table = False
|
|
result.extend(_convert_table_rows(table_buffer))
|
|
table_buffer = []
|
|
result.append(line)
|
|
|
|
if in_table and table_buffer:
|
|
result.extend(_convert_table_rows(table_buffer))
|
|
|
|
return "\n".join(result)
|
|
|
|
|
|
def _convert_table_rows(rows: list[str]) -> list[str]:
|
|
if len(rows) < 2:
|
|
return rows
|
|
|
|
def _parse_cells(row: str) -> list[str]:
|
|
return [cell.strip() for cell in row.strip("|").split("|")]
|
|
|
|
header = _parse_cells(rows[0])
|
|
separator = _parse_cells(rows[1])
|
|
body_rows = rows[2:]
|
|
is_sep = all(re.fullmatch(r"[-:]{3,}", cell) or re.fullmatch(r":[-:]+:", cell) for cell in separator)
|
|
|
|
if not is_sep:
|
|
all_rows = [_parse_cells(r) for r in rows]
|
|
else:
|
|
all_rows = [_parse_cells(r) for r in body_rows]
|
|
|
|
if not header:
|
|
return [", ".join(cells) for cells in all_rows]
|
|
|
|
bullet_lines: list[str] = []
|
|
for cells in all_rows:
|
|
pairs = [f"{header[i]}: {cells[i]}" if i < len(cells) else f"{header[i]}: " for i in range(len(header))]
|
|
bullet_lines.append("• " + ", ".join(pairs))
|
|
|
|
return bullet_lines
|
|
|
|
|
|
def chunk_text(text: str, limit: int = SLACK_TEXT_LIMIT) -> list[str]:
|
|
if not text:
|
|
return []
|
|
|
|
if len(text) <= limit:
|
|
return [text]
|
|
|
|
chunks: list[str] = []
|
|
paragraphs = text.split("\n\n")
|
|
|
|
for para in paragraphs:
|
|
if not para:
|
|
continue
|
|
if len(para) <= limit:
|
|
chunks.append(para)
|
|
else:
|
|
chunks.extend(_chunk_single_paragraph(para, limit))
|
|
|
|
merged: list[str] = []
|
|
for chunk in chunks:
|
|
if not merged:
|
|
merged.append(chunk)
|
|
continue
|
|
last = merged[-1]
|
|
candidate = last + "\n\n" + chunk
|
|
if len(candidate) <= limit:
|
|
merged[-1] = candidate
|
|
else:
|
|
merged.append(chunk)
|
|
|
|
return merged
|
|
|
|
|
|
def _chunk_single_paragraph(text: str, limit: int) -> list[str]:
|
|
sentences = re.split(r"(?<=[.?!])\s+", text)
|
|
chunks: list[str] = []
|
|
current = ""
|
|
|
|
for sentence in sentences:
|
|
if not sentence:
|
|
continue
|
|
if len(sentence) > limit:
|
|
if current:
|
|
chunks.append(current.rstrip())
|
|
current = ""
|
|
chunks.extend(_chunk_by_words(sentence, limit))
|
|
else:
|
|
candidate = (current + " " + sentence).strip() if current else sentence
|
|
if len(candidate) <= limit:
|
|
current = candidate
|
|
else:
|
|
chunks.append(current.rstrip())
|
|
current = sentence
|
|
|
|
if current:
|
|
chunks.append(current.rstrip())
|
|
|
|
return chunks
|
|
|
|
|
|
def _chunk_by_words(text: str, limit: int) -> list[str]:
|
|
words = text.split(" ")
|
|
chunks: list[str] = []
|
|
current = ""
|
|
|
|
for word in words:
|
|
if not word:
|
|
continue
|
|
if not current:
|
|
current = word
|
|
continue
|
|
candidate = current + " " + word
|
|
if len(candidate) <= limit:
|
|
current = candidate
|
|
else:
|
|
chunks.append(current)
|
|
current = word
|
|
|
|
if current:
|
|
chunks.append(current)
|
|
|
|
safe_chunks: list[str] = []
|
|
for chunk in chunks:
|
|
if len(chunk) <= limit:
|
|
safe_chunks.append(chunk)
|
|
else:
|
|
split_pos = _find_safe_split_position(chunk, limit)
|
|
if split_pos == 0 or split_pos == limit:
|
|
safe_chunks.append(chunk)
|
|
else:
|
|
safe_chunks.append(chunk[:split_pos])
|
|
safe_chunks.append(chunk[split_pos:])
|
|
|
|
return safe_chunks
|
|
|
|
|
|
def strip_mentions(text: str) -> str:
|
|
return re.sub(SLACK_MENTION_PATTERN, "", text).strip()
|
|
|
|
|
|
def extract_mentions(text: str) -> list[str]:
|
|
return re.findall(SLACK_MENTION_PATTERN, text)
|
|
|
|
|
|
def get_agent_prompt_rules() -> list[str]:
|
|
return SLACK_AGENT_PROMPT_RULES
|