ForcePilot/backend/package/yuxi/channels/adapters/urbit/story.py
Kris 49ecd949e8 feat(urbit): 实现完整的Urbit聊天适配器模块
新增了从错误定义、客户端实现到功能完整的Urbit适配器全套代码,包括认证、消息处理、目录查询、邀请管理、媒体处理、速率限制等核心功能模块
2026-05-12 00:50:27 +08:00

317 lines
11 KiB
Python

from __future__ import annotations
import re
from typing import Any
_HEADER_RE = re.compile(r"^(#{1,4})\s+(.+)$", re.MULTILINE)
_BOLD_RE = re.compile(r"\*\*(.+?)\*\*")
_ITALIC_RE = re.compile(r"\*(.+?)\*")
_STRIKE_RE = re.compile(r"~~(.+?)~~")
_INLINE_CODE_RE = re.compile(r"`([^`]+)`")
_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
_CODE_BLOCK_RE = re.compile(r"```(\w*)\n?(.*?)```", re.DOTALL)
_BLOCKQUOTE_RE = re.compile(r"^>\s?(.+)$", re.MULTILINE)
_UNORDERED_LIST_RE = re.compile(r"^[-*+]\s+(.+)$", re.MULTILINE)
_ORDERED_LIST_RE = re.compile(r"^\d+\.\s+(.+)$", re.MULTILINE)
_SHIP_RE = re.compile(r"(?<!\w)(~[a-z]{3,}(-[a-z]{3,})*)(?!\w)", re.IGNORECASE)
_RULE_RE = re.compile(r"^[-*_]{3,}\s*$", re.MULTILINE)
def markdown_to_story(content: str) -> list[dict[str, Any]]:
if not content:
return [{"text": ""}]
if (
"```" in content
or "**" in content
or "#" in content
or ">" in content
or "[" in content
or "~" in content
or _RULE_RE.search(content)
):
return _parse_markdown_story(content)
lines = content.split("\n")
if len(lines) == 1:
return [{"text": content}]
story: list[dict[str, Any]] = []
for line in lines:
if not line.strip():
story.append({"break": None})
else:
story.append({"text": line})
return story
def _parse_markdown_story(content: str) -> list[dict[str, Any]]:
story: list[dict[str, Any]] = []
pos = 0
while pos < len(content):
if content[pos:].startswith("```"):
end = content.find("```", pos + 3)
if end == -1:
story.append({"text": content[pos:]})
break
block_text = content[pos + 3 : end]
lang = ""
if "\n" in block_text:
first_line, rest = block_text.split("\n", 1)
if first_line.strip() and not first_line.strip().startswith(" "):
lang = first_line.strip()
block_text = rest
story.append({"code": {"code": block_text.strip(), "lang": lang}})
pos = end + 3
while pos < len(content) and content[pos] == "\n":
pos += 1
continue
if content[pos:].startswith(("# ", "## ", "### ", "#### ")):
end = content.find("\n", pos)
if end == -1:
end = len(content)
line = content[pos:end]
level = 0
for ch in line:
if ch == "#":
level += 1
else:
break
tag = f"h{level}" if level <= 4 else "h4"
story.append({"header": {"content": line[level:].strip(), "tag": tag}})
pos = end + 1
continue
if content[pos:].startswith("> "):
end = content.find("\n", pos)
if end == -1:
end = len(content)
quote_text = content[pos + 2 : end]
story.append({"blockquote": _parse_inline(quote_text)})
pos = end + 1
continue
m = re.match(r"^[-*+]\s+", content[pos:])
if m:
items: list[str] = []
while pos < len(content):
m2 = re.match(r"^[-*+]\s+(.+)$", content[pos:], re.MULTILINE)
if not m2:
break
items.append(m2.group(1))
end = content.find("\n", pos + m2.end())
if end == -1:
pos = len(content)
else:
pos = end + 1
if items:
story.append({"listing": {"type": "unordered", "items": items, "contents": []}})
continue
m = re.match(r"^\d+\.\s+", content[pos:])
if m:
items: list[str] = []
while pos < len(content):
m2 = re.match(r"^\d+\.\s+(.+)$", content[pos:], re.MULTILINE)
if not m2:
break
items.append(m2.group(1))
end = content.find("\n", pos + m2.end())
if end == -1:
pos = len(content)
else:
pos = end + 1
if items:
story.append({"listing": {"type": "ordered", "items": items, "contents": []}})
continue
m = _RULE_RE.match(content[pos:])
if m:
end = content.find("\n", pos)
if end == -1:
end = len(content)
story.append({"block": {"rule": None}})
pos = end + 1
continue
end = content.find("\n", pos)
if end == -1:
end = len(content)
line = content[pos:end]
if line.strip():
story.append(_parse_inline(line))
pos = end + 1
return story
def _parse_inline(text: str) -> dict[str, Any]:
remaining = text
inlines: list[dict[str, Any]] = []
while remaining:
has_match = False
earliest = len(remaining)
match_type = None
match_data = None
for m in _BOLD_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "bold"
match_data = m
has_match = True
for m in _ITALIC_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "italic"
match_data = m
has_match = True
for m in _STRIKE_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "strike"
match_data = m
has_match = True
for m in _INLINE_CODE_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "code"
match_data = m
has_match = True
for m in _LINK_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "link"
match_data = m
has_match = True
for m in _SHIP_RE.finditer(remaining):
if m.start() < earliest:
earliest = m.start()
match_type = "ship_ref"
match_data = m
has_match = True
if not has_match:
if remaining.strip():
inlines.append({"text": remaining})
break
if earliest > 0:
inlines.append({"text": remaining[:earliest]})
if match_type == "bold":
inlines.append({"bold": [_parse_inline(match_data.group(1))]})
elif match_type == "italic":
inlines.append({"italics": [_parse_inline(match_data.group(1))]})
elif match_type == "strike":
inlines.append({"strike": [_parse_inline(match_data.group(1))]})
elif match_type == "code":
inlines.append({"inline-code": match_data.group(1)})
elif match_type == "link":
inlines.append({"link": {"href": match_data.group(2), "content": match_data.group(1)}})
elif match_type == "ship_ref":
inlines.append({"ship": match_data.group(1)})
remaining = remaining[match_data.end() :]
if len(inlines) == 1:
return inlines[0]
has_only_text = all("text" in item for item in inlines)
if has_only_text:
return {"text": "".join(item["text"] for item in inlines)}
return {"text": "".join(_inline_to_text(item) for item in inlines)}
def _inline_to_text(item: dict[str, Any]) -> str:
if "text" in item:
return item["text"]
if "bold" in item:
inner = item["bold"]
if isinstance(inner, list):
return "".join(_inline_to_text(i) for i in inner)
return str(inner)
if "italics" in item:
inner = item["italics"]
if isinstance(inner, list):
return "".join(_inline_to_text(i) for i in inner)
return str(inner)
if "strike" in item:
inner = item["strike"]
if isinstance(inner, list):
return "".join(_inline_to_text(i) for i in inner)
return str(inner)
if "inline-code" in item:
return str(item["inline-code"])
if "link" in item:
link = item["link"]
return link.get("content", link.get("href", "")) if isinstance(link, dict) else str(link)
if "ship" in item:
return item["ship"]
return ""
def story_to_plain_text(story: list[dict[str, Any]]) -> str:
parts: list[str] = []
for item in story:
if "text" in item:
parts.append(item["text"])
elif "bold" in item:
parts.append("".join(_inline_to_text(i) for i in item["bold"]))
elif "italics" in item:
parts.append("".join(_inline_to_text(i) for i in item["italics"]))
elif "strike" in item:
parts.append("".join(_inline_to_text(i) for i in item["strike"]))
elif "inline-code" in item:
parts.append(item["inline-code"])
elif "blockquote" in item:
parts.append(_story_to_text_recursive(item["blockquote"]))
elif "header" in item:
parts.append(item["header"].get("content", ""))
elif "code" in item:
code_data = item["code"]
if isinstance(code_data, dict):
parts.append(code_data.get("code", ""))
elif "link" in item:
link = item["link"]
if isinstance(link, dict):
parts.append(link.get("content", link.get("href", "")))
elif "break" in item:
parts.append("\n")
elif "ship" in item:
parts.append(item["ship"])
elif "listing" in item:
listing = item["listing"]
if isinstance(listing, dict):
for list_item in listing.get("items", []):
parts.append(f"- {list_item}\n")
return "".join(parts)
def _story_to_text_recursive(items: list[dict[str, Any]]) -> str:
parts: list[str] = []
for item in items:
if "text" in item:
parts.append(item["text"])
elif "bold" in item:
parts.append("".join(_inline_to_text(i) for i in item["bold"]))
elif "italics" in item:
parts.append("".join(_inline_to_text(i) for i in item["italics"]))
elif "inline-code" in item:
parts.append(item["inline-code"])
elif "link" in item:
link = item["link"]
parts.append(link.get("content", "") if isinstance(link, dict) else str(link))
elif "break" in item:
parts.append("\n")
return "".join(parts)