ForcePilot/backend/package/yuxi/channel/extensions/tlon/story.py
Kris 0babb1ca8e feat(channel): 添加 Tlon 渠道扩展
新增 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: 类型定义
2026-05-21 11:55:25 +08:00

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"![{alt}]({src})")
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