新增 Tlon 渠道扩展,支持在 Yuxi 平台中集成 Tlon/Urbit 去中心化通讯平台。 包含以下功能模块: - tlon_api: Tlon API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - sse_client: SSE 客户端 - outbound: 外发消息管理 - send: 消息发送 - security: 安全校验 - auth: 认证管理 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - approval: 审批流程 - channel_mgmt: 频道管理 - channel_ops: 频道操作 - contacts: 联系人管理 - discovery: 服务发现 - doctor: 健康诊断 - expose: 服务暴露 - gallery: 图库管理 - history: 历史记录 - hooks: 钩子管理 - media: 媒体资源处理 - notebook: 笔记本功能 - settings_store: 设置存储 - setup: 初始化设置 - story: 故事功能 - targets: 目标管理 - cite_parser: 引用解析 - utils: 工具函数 - types: 类型定义
270 lines
8.6 KiB
Python
270 lines
8.6 KiB
Python
import re
|
|
|
|
|
|
def markdown_to_story(markdown_text: str) -> list[dict]:
|
|
if not markdown_text:
|
|
return [{"inline": []}]
|
|
|
|
lines = markdown_text.split("\n")
|
|
verses = []
|
|
i = 0
|
|
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
|
|
if line.startswith("```"):
|
|
code_lines = []
|
|
lang = line[3:].strip()
|
|
i += 1
|
|
while i < len(lines) and not lines[i].startswith("```"):
|
|
code_lines.append(lines[i])
|
|
i += 1
|
|
i += 1
|
|
verses.append({
|
|
"block": {
|
|
"code": {
|
|
"code": "\n".join(code_lines),
|
|
"lang": lang or None,
|
|
}
|
|
}
|
|
})
|
|
continue
|
|
|
|
if line.startswith("#"):
|
|
level = 0
|
|
for ch in line:
|
|
if ch == "#":
|
|
level += 1
|
|
else:
|
|
break
|
|
header_text = line[level:].strip()
|
|
verses.append({
|
|
"block": {
|
|
"header": {
|
|
"tag": f"h{min(level, 6)}",
|
|
"content": [header_text],
|
|
}
|
|
}
|
|
})
|
|
i += 1
|
|
continue
|
|
|
|
if line.strip() in ("---", "***", "___"):
|
|
verses.append({"block": {"rule": None}})
|
|
i += 1
|
|
continue
|
|
|
|
if line.startswith(">"):
|
|
quote_lines = []
|
|
while i < len(lines) and lines[i].startswith(">"):
|
|
quote_lines.append(lines[i][1:].strip())
|
|
i += 1
|
|
quote_text = " ".join(quote_lines)
|
|
verses.append({
|
|
"inline": [
|
|
{"blockquote": parse_inline_content(quote_text)}
|
|
]
|
|
})
|
|
continue
|
|
|
|
if line.strip() == "":
|
|
i += 1
|
|
continue
|
|
|
|
paragraph_lines = []
|
|
while i < len(lines) and lines[i].strip() != "" and not lines[i].startswith("#") and not lines[i].startswith("```") and not lines[i].startswith(">"):
|
|
paragraph_lines.append(lines[i])
|
|
i += 1
|
|
paragraph_text = " ".join(paragraph_lines)
|
|
if paragraph_text.strip():
|
|
verses.append({"inline": parse_inline_content(paragraph_text)})
|
|
|
|
if not verses:
|
|
return [{"inline": []}]
|
|
return verses
|
|
|
|
|
|
def parse_inline_content(text: str) -> list[dict]:
|
|
result = []
|
|
pos = 0
|
|
|
|
while pos < len(text):
|
|
match_info = None
|
|
earliest = len(text)
|
|
|
|
bold_match = re.match(r"\*\*(.+?)\*\*", text[pos:])
|
|
if bold_match:
|
|
if pos < earliest:
|
|
match_info = ("bold", bold_match)
|
|
earliest = pos
|
|
|
|
italic_match = re.match(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", text[pos:])
|
|
if italic_match:
|
|
if pos <= earliest:
|
|
match_info = ("italic", italic_match)
|
|
earliest = pos
|
|
|
|
strike_match = re.match(r"~~(.+?)~~", text[pos:])
|
|
if strike_match:
|
|
if pos < earliest:
|
|
match_info = ("strike", strike_match)
|
|
earliest = pos
|
|
|
|
code_match = re.match(r"`([^`]+)`", text[pos:])
|
|
if code_match:
|
|
if pos < earliest:
|
|
match_info = ("code", code_match)
|
|
earliest = pos
|
|
|
|
link_match = re.match(r"\[(.+?)\]\((.+?)\)", text[pos:])
|
|
if link_match:
|
|
if pos < earliest:
|
|
match_info = ("link", link_match)
|
|
earliest = pos
|
|
|
|
ship_match = re.match(r"~[a-z]+(-[a-z]+)*", text[pos:], re.IGNORECASE)
|
|
if ship_match:
|
|
if pos <= earliest:
|
|
match_info = ("ship", ship_match)
|
|
earliest = pos
|
|
|
|
url_match = re.match(r"https?://[^\s]+", text[pos:])
|
|
if url_match:
|
|
if pos <= earliest:
|
|
match_info = ("url", url_match)
|
|
earliest = pos
|
|
|
|
if match_info is None:
|
|
result.append(str(text[pos]))
|
|
pos += 1
|
|
else:
|
|
if earliest > pos:
|
|
result.append(text[pos:earliest])
|
|
mtype, m = match_info
|
|
if mtype == "bold":
|
|
result.append({"bold": [m.group(1)]})
|
|
elif mtype == "italic":
|
|
result.append({"italics": [m.group(1)]})
|
|
elif mtype == "strike":
|
|
result.append({"strike": [m.group(1)]})
|
|
elif mtype == "code":
|
|
result.append({"inline-code": m.group(1)})
|
|
elif mtype == "link":
|
|
result.append({"link": {"href": m.group(2), "content": m.group(1)}})
|
|
elif mtype == "ship":
|
|
result.append({"ship": m.group(0)})
|
|
elif mtype == "url":
|
|
result.append({"link": {"href": m.group(0), "content": m.group(0)}})
|
|
pos = earliest + m.end()
|
|
|
|
return result
|
|
|
|
|
|
def story_to_text(content: list[dict]) -> str:
|
|
parts = []
|
|
for verse in content:
|
|
if "inline" in verse:
|
|
parts.append(_inline_to_text(verse["inline"]))
|
|
elif "block" in verse:
|
|
block = verse["block"]
|
|
if block is None:
|
|
continue
|
|
if "header" in block:
|
|
h = block["header"]
|
|
tag = h.get("tag", "h1")
|
|
level = int(tag[1]) if tag.startswith("h") else 1
|
|
content_parts = h.get("content", [])
|
|
header_text = " ".join(
|
|
c if isinstance(c, str) else _inline_to_text([c]) if isinstance(c, dict) else str(c)
|
|
for c in content_parts
|
|
)
|
|
parts.append(f"{'#' * level} {header_text}")
|
|
elif "code" in block:
|
|
code_block = block["code"]
|
|
lang = code_block.get("lang", "")
|
|
code_text = code_block.get("code", "")
|
|
parts.append(f"```{lang}\n{code_text}\n```")
|
|
elif "rule" in block:
|
|
parts.append("---")
|
|
elif "image" in block:
|
|
img = block["image"]
|
|
alt = img.get("alt", "")
|
|
src = img.get("src", "")
|
|
parts.append(f"")
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def _inline_to_text(inlines: list) -> str:
|
|
parts = []
|
|
if not inlines:
|
|
return ""
|
|
for item in inlines:
|
|
if isinstance(item, str):
|
|
parts.append(item)
|
|
elif isinstance(item, dict):
|
|
if "bold" in item:
|
|
parts.append(f"**{''.join(str(x) for x in item['bold'])}**")
|
|
elif "italics" in item:
|
|
parts.append(f"*{''.join(str(x) for x in item['italics'])}*")
|
|
elif "strike" in item:
|
|
parts.append(f"~~{''.join(str(x) for x in item['strike'])}~~")
|
|
elif "inline-code" in item:
|
|
parts.append(f"`{item['inline-code']}`")
|
|
elif "link" in item:
|
|
link = item["link"]
|
|
parts.append(f"[{link.get('content', '')}]({link.get('href', '')})")
|
|
elif "ship" in item:
|
|
parts.append(item["ship"])
|
|
elif "blockquote" in item:
|
|
parts.append(_inline_to_text(item["blockquote"]))
|
|
else:
|
|
parts.append(str(item))
|
|
return "".join(parts)
|
|
|
|
|
|
def extract_mentions(content: list[dict]) -> list[str]:
|
|
ships = []
|
|
for verse in content:
|
|
if "inline" in verse:
|
|
for item in verse["inline"]:
|
|
if isinstance(item, dict) and "ship" in item:
|
|
ships.append(item["ship"])
|
|
return ships
|
|
|
|
|
|
def extract_image_blocks(content: list[dict]) -> list[dict]:
|
|
images = []
|
|
for verse in content:
|
|
if "block" in verse and verse["block"] and "image" in verse["block"]:
|
|
images.append(verse["block"]["image"])
|
|
return images
|
|
|
|
|
|
def build_media_story(text: str, media_url: str,
|
|
media_type: str = "image") -> list[dict]:
|
|
verses = markdown_to_story(text)
|
|
if not media_url:
|
|
return verses
|
|
|
|
if media_type in ("image", "video"):
|
|
block_type = "image" if media_type == "image" else "video"
|
|
verses.insert(0, {
|
|
"block": {
|
|
block_type: {
|
|
"src": media_url,
|
|
"alt": text[:100] if text else media_type,
|
|
"width": None,
|
|
"height": None,
|
|
}
|
|
}
|
|
})
|
|
else:
|
|
verses.insert(0, {
|
|
"block": {
|
|
"cite": {
|
|
"content": f"[{media_type}: {media_url}]",
|
|
"linked": media_url,
|
|
}
|
|
}
|
|
})
|
|
return verses |