ForcePilot/backend/package/yuxi/channel/extensions/jira/parse.py
Kris c5d8e3550e feat(channel): 添加 Jira 渠道扩展
新增 Jira 渠道扩展,支持在 Yuxi 平台中集成 Jira 项目管理渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- outbound: 外发消息与工单管理
- streaming: 流式消息处理
- parse: Jira 内容解析
- format: 消息格式转换
- agent_tools: Agent 工具集成
- security: 安全校验
- dedupe: 消息去重
- loopbreaker: 循环响应防护
- monitor: 渠道状态监控
- status: 会话与工单状态管理
- types: 类型定义
2026-05-21 11:10:54 +08:00

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"![{filename}]({url})\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")