新增 Jira 渠道扩展,支持在 Yuxi 平台中集成 Jira 项目管理渠道。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息与工单管理 - streaming: 流式消息处理 - parse: Jira 内容解析 - format: 消息格式转换 - agent_tools: Agent 工具集成 - security: 安全校验 - dedupe: 消息去重 - loopbreaker: 循环响应防护 - monitor: 渠道状态监控 - status: 会话与工单状态管理 - types: 类型定义
243 lines
7.3 KiB
Python
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,
|
|
},
|
|
}
|
|
],
|
|
}
|