新增 Jira 渠道扩展,支持在 Yuxi 平台中集成 Jira 项目管理渠道。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息与工单管理 - streaming: 流式消息处理 - parse: Jira 内容解析 - format: 消息格式转换 - agent_tools: Agent 工具集成 - security: 安全校验 - dedupe: 消息去重 - loopbreaker: 循环响应防护 - monitor: 渠道状态监控 - status: 会话与工单状态管理 - types: 类型定义
151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
def adf_to_plain_text(adf: dict | None) -> str:
|
|
if not adf or not isinstance(adf, dict):
|
|
return ""
|
|
parts: list[str] = []
|
|
_walk_nodes(adf.get("content", []), parts)
|
|
return "".join(parts).strip()
|
|
|
|
|
|
def adf_to_markdown(adf: dict | None) -> str:
|
|
if not adf or not isinstance(adf, dict):
|
|
return ""
|
|
parts: list[str] = []
|
|
_walk_nodes_md(adf.get("content", []), parts)
|
|
return "".join(parts).strip()
|
|
|
|
|
|
def _walk_nodes(nodes: list, parts: list):
|
|
for node in nodes:
|
|
if not isinstance(node, dict):
|
|
continue
|
|
node_type = node.get("type", "")
|
|
marks = node.get("marks", [])
|
|
|
|
if node_type == "text":
|
|
text = node.get("text", "")
|
|
_process_marks(parts, text, marks)
|
|
elif node_type == "hardBreak":
|
|
parts.append("\n")
|
|
elif node_type == "mention":
|
|
parts.append(f"@{node.get('attrs', {}).get('text', 'user')}")
|
|
elif node_type == "emoji":
|
|
parts.append(node.get("attrs", {}).get("shortName", ""))
|
|
elif node_type == "paragraph":
|
|
_walk_nodes(node.get("content", []), parts)
|
|
parts.append("\n")
|
|
elif node_type == "heading":
|
|
level = node.get("attrs", {}).get("level", 1)
|
|
prefix = "#" * level + " "
|
|
parts.append(prefix)
|
|
_walk_nodes(node.get("content", []), parts)
|
|
parts.append("\n\n")
|
|
elif node_type == "codeBlock":
|
|
lang = node.get("attrs", {}).get("language", "")
|
|
parts.append(f"```{lang}\n")
|
|
_walk_nodes(node.get("content", []), parts)
|
|
parts.append("\n```\n\n")
|
|
elif node_type == "blockquote":
|
|
content_parts: list[str] = []
|
|
_walk_nodes(node.get("content", []), content_parts)
|
|
for line in "".join(content_parts).split("\n"):
|
|
if line.strip():
|
|
parts.append(f"> {line}\n")
|
|
parts.append("\n")
|
|
elif node_type == "bulletList":
|
|
_walk_list_items(node.get("content", []), parts, "- ")
|
|
elif node_type == "orderedList":
|
|
_walk_list_items(node.get("content", []), parts, "1. ")
|
|
elif node_type == "panel":
|
|
panel_type = node.get("attrs", {}).get("panelType", "info")
|
|
parts.append(f"> **[{panel_type}]** ")
|
|
_walk_nodes(node.get("content", []), parts)
|
|
parts.append("\n\n")
|
|
elif node_type == "rule":
|
|
parts.append("---\n\n")
|
|
elif node_type == "table":
|
|
_walk_table(node, parts)
|
|
parts.append("\n")
|
|
elif node_type == "media":
|
|
media_attrs = node.get("attrs", {})
|
|
display_type = media_attrs.get("type", "file")
|
|
filename = media_attrs.get("alt", "") or f"{display_type}_attachment"
|
|
if display_type == "image":
|
|
url = media_attrs.get("url", "")
|
|
parts.append(f"\n")
|
|
else:
|
|
parts.append(f"[📎 {filename}]\n")
|
|
elif node_type == "date":
|
|
timestamp = node.get("attrs", {}).get("timestamp", "")
|
|
parts.append(f"[{timestamp}]")
|
|
elif node_type == "status":
|
|
status_text = node.get("attrs", {}).get("text", "")
|
|
color = node.get("attrs", {}).get("color", "")
|
|
parts.append(f"[{status_text}]({color})")
|
|
elif "content" in node:
|
|
_walk_nodes(node["content"], parts)
|
|
|
|
|
|
def _walk_nodes_md(nodes: list, parts: list):
|
|
_walk_nodes(nodes, parts)
|
|
|
|
|
|
def _process_marks(parts: list, text: str, marks: list):
|
|
prefix = suffix = ""
|
|
links: list[str] = []
|
|
for mark in marks:
|
|
mark_type = mark.get("type", "")
|
|
if mark_type == "strong":
|
|
prefix = "**" + prefix
|
|
suffix += "**"
|
|
elif mark_type == "em":
|
|
prefix = "*" + prefix
|
|
suffix += "*"
|
|
elif mark_type == "code":
|
|
prefix = "`" + prefix
|
|
suffix += "`"
|
|
elif mark_type == "strike":
|
|
prefix = "~~" + prefix
|
|
suffix += "~~"
|
|
elif mark_type == "link":
|
|
href = mark.get("attrs", {}).get("href", "")
|
|
if href:
|
|
links.append(href)
|
|
if links:
|
|
parts.append(f"[{prefix}{text}{suffix}]({links[-1]})")
|
|
else:
|
|
parts.append(f"{prefix}{text}{suffix}")
|
|
|
|
|
|
def _walk_list_items(items: list, parts: list, prefix: str):
|
|
for item in items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
item_content = item.get("content", [])
|
|
for node in item_content:
|
|
node_type = node.get("type", "")
|
|
if node_type == "paragraph":
|
|
parts.append(prefix)
|
|
_walk_nodes([node], parts)
|
|
parts.append("\n")
|
|
elif node_type in ("bulletList", "orderedList"):
|
|
sub_prefix = " " + prefix
|
|
_walk_list_items(node.get("content", []), parts, sub_prefix)
|
|
parts.append("\n")
|
|
|
|
|
|
def _walk_table(table: dict, parts: list):
|
|
rows = table.get("content", [])
|
|
for row_idx, row in enumerate(rows):
|
|
cells = row.get("content", [])
|
|
cell_texts: list[str] = []
|
|
for cell in cells:
|
|
cell_parts: list[str] = []
|
|
_walk_nodes(cell.get("content", []), cell_parts)
|
|
cell_texts.append("".join(cell_parts).replace("\n", " ").strip())
|
|
parts.append("| " + " | ".join(cell_texts) + " |\n")
|
|
if row_idx == 0:
|
|
parts.append("| " + " | ".join(["---"] * len(cells)) + " |\n")
|