ForcePilot/backend/package/yuxi/channel/extensions/jira/format.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

243 lines
7.3 KiB
Python

from __future__ import annotations
import re
def markdown_to_adf(markdown: str, *, is_final: bool = False) -> dict:
lines = markdown.split("\n")
blocks: list[dict] = []
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
if line.startswith("```"):
code_lines: list[str] = []
lang = _extract_lang(line)
i += 1
while i < len(lines) and not lines[i].startswith("```"):
code_lines.append(lines[i])
i += 1
i += 1
blocks.append(_adf_code_block("\n".join(code_lines), lang))
continue
img_match = re.match(r"!\[([^\]]*)\]\(([^)]+)\)", line.strip())
if img_match:
blocks.append(_adf_media(img_match.group(2), img_match.group(1) or "image"))
i += 1
continue
if line.startswith("> "):
quote_lines: list[str] = []
while i < len(lines) and lines[i].startswith("> "):
quote_lines.append(lines[i][2:])
i += 1
blocks.append(_adf_blockquote("\n".join(quote_lines)))
continue
if line.startswith("#"):
level = len(line) - len(line.lstrip("#"))
heading_text = line.lstrip("#").strip()
blocks.append(_adf_heading(heading_text, level))
i += 1
continue
if line.strip().startswith("|") and line.strip().endswith("|"):
table_rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
cells = [c.strip() for c in lines[i].strip().strip("|").split("|")]
table_rows.append(cells)
i += 1
if len(table_rows) >= 2:
header = table_rows[0]
data_start = 2 if _is_table_separator(table_rows[1]) else 1
blocks.append(_adf_table(header, table_rows[data_start:]))
continue
if line.startswith("- ") or line.startswith("* "):
list_items: list[str] = []
while i < len(lines) and (lines[i].startswith("- ") or lines[i].startswith("* ")):
list_items.append(lines[i][2:].strip())
i += 1
blocks.append(_adf_bullet_list(list_items))
continue
if re.match(r"^\d+\. ", line):
list_items: list[str] = []
while i < len(lines) and re.match(r"^\d+\. ", lines[i]):
list_items.append(re.sub(r"^\d+\. ", "", lines[i]).strip())
i += 1
blocks.append(_adf_ordered_list(list_items))
continue
if line.strip() == "---":
blocks.append({"type": "rule"})
i += 1
continue
blocks.append(_adf_paragraph(line))
i += 1
return {"type": "doc", "version": 1, "content": blocks}
def _extract_lang(line: str) -> str:
return line[3:].strip()
def _is_table_separator(cells: list[str]) -> bool:
return all(re.match(r"^:?-{3,}:?$", c) for c in cells)
def _adf_table(header: list[str], rows: list[list[str]]) -> dict:
table_rows = [
{
"type": "tableRow",
"content": [
{
"type": "tableHeader",
"content": [_adf_simple_paragraph(cell)],
}
for cell in header
],
}
]
for row in rows:
table_rows.append(
{
"type": "tableRow",
"content": [
{
"type": "tableCell",
"content": [_adf_simple_paragraph(cell)],
}
for cell in row
],
}
)
return {
"type": "table",
"attrs": {"isNumberColumnEnabled": False, "layout": "default"},
"content": table_rows,
}
def _adf_simple_paragraph(text: str) -> dict:
return {"type": "paragraph", "content": _parse_inline(text)}
def _parse_inline(text: str) -> list[dict]:
result: list[dict] = []
pattern = re.compile(
r"(\*\*(.+?)\*\*|"
r"~~(.+?)~~|"
r"`(.+?)`|"
r"\*(.+?)\*|"
r"\[([^\]]+)\]\(([^)]+)\)|"
r"@([\w.-]+))"
)
last_end = 0
for match in pattern.finditer(text):
if match.start() > last_end:
result.append({"type": "text", "text": text[last_end : match.start()]})
if match.group(2):
result.append({"type": "text", "text": match.group(2), "marks": [{"type": "strong"}]})
elif match.group(3):
result.append({"type": "text", "text": match.group(3), "marks": [{"type": "strike"}]})
elif match.group(4):
result.append({"type": "text", "text": match.group(4), "marks": [{"type": "code"}]})
elif match.group(5):
result.append({"type": "text", "text": match.group(5), "marks": [{"type": "em"}]})
elif match.group(6):
result.append(
{
"type": "text",
"text": match.group(6),
"marks": [{"type": "link", "attrs": {"href": match.group(7)}}],
}
)
elif match.group(8):
result.append(
{
"type": "mention",
"attrs": {
"id": match.group(8),
"text": match.group(8),
"accessLevel": "",
},
}
)
last_end = match.end()
if last_end < len(text):
result.append({"type": "text", "text": text[last_end:]})
if not result:
result.append({"type": "text", "text": text})
return result
def _adf_paragraph(text: str) -> dict:
return {"type": "paragraph", "content": _parse_inline(text)}
def _adf_heading(text: str, level: int) -> dict:
return {
"type": "heading",
"attrs": {"level": min(level, 6)},
"content": _parse_inline(text),
}
def _adf_code_block(code: str, language: str = "") -> dict:
attrs = {}
if language:
attrs["language"] = language
return {
"type": "codeBlock",
"attrs": attrs,
"content": [{"type": "text", "text": code}],
}
def _adf_blockquote(text: str) -> dict:
return {
"type": "blockquote",
"content": [_adf_paragraph(text)],
}
def _adf_bullet_list(items: list[str]) -> dict:
return {
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [_adf_paragraph(item)],
}
for item in items
],
}
def _adf_ordered_list(items: list[str]) -> dict:
return {
"type": "orderedList",
"content": [
{
"type": "listItem",
"content": [_adf_paragraph(item)],
}
for item in items
],
}
def _adf_media(url: str, alt: str = "") -> dict:
return {
"type": "mediaSingle",
"attrs": {"layout": "center"},
"content": [
{
"type": "media",
"attrs": {
"type": "external",
"url": url,
"alt": alt,
},
}
],
}