fix: 导出文档时把 mermaid mindmap 转成 flowchart LR,外部阅读器清晰可读
- 老版 mermaid 渲染 mindmap 是根居中放射状、粗彩线,外部阅读器里很难读 - 导出接口现把 ```mermaid mindmap 块转换为 flowchart LR 树状图,网页样式不受影响 - 支持 root(( )) 关键字、形状符号、::icon() 与行尾注释的清理 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+79
-1
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -368,7 +369,9 @@ async def export_conversation(conv_id: str, fmt: str = "markdown", db: AsyncSess
|
||||
lines = [f"# {conv.title or '对话'}", f"书籍: {book.title if book else '未知'}\n"]
|
||||
for m in msgs:
|
||||
role_label = "🧑 用户" if m.role == "user" else "🤖 AI"
|
||||
lines.append(f"## {role_label}\n\n{m.content}\n")
|
||||
# 把 mermaid mindmap 转成 flowchart LR:老版阅读器渲染 mindmap 是放射状粗线,很难读
|
||||
content = _transform_mindmaps(m.content)
|
||||
lines.append(f"## {role_label}\n\n{content}\n")
|
||||
if m.citations:
|
||||
try:
|
||||
citations = json.loads(m.citations)
|
||||
@@ -379,6 +382,81 @@ async def export_conversation(conv_id: str, fmt: str = "markdown", db: AsyncSess
|
||||
return {"content": "\n".join(lines), "format": fmt, "title": conv.title}
|
||||
|
||||
|
||||
_FENCE_RE = re.compile(r"```(mermaid)?\s*\n(.*?)\n```", re.DOTALL)
|
||||
|
||||
|
||||
def _transform_mindmaps(text: str) -> str:
|
||||
"""把 markdown 里的 ```mermaid mindmap 块转换成 flowchart LR(清晰左→右树状)。"""
|
||||
def _repl(m: re.Match) -> str:
|
||||
body = m.group(2)
|
||||
if body.lstrip().startswith("mindmap"):
|
||||
return f"```mermaid\n{_mindmap_to_flowchart(body)}\n```"
|
||||
return m.group(0)
|
||||
|
||||
return _FENCE_RE.sub(_repl, text)
|
||||
|
||||
|
||||
def _mindmap_to_flowchart(body: str) -> str:
|
||||
"""mindmap(缩进式)→ flowchart LR(节点 + 边)。
|
||||
|
||||
mindmap:
|
||||
root((主题))
|
||||
一级分支
|
||||
二级分支
|
||||
→ flowchart LR:
|
||||
n1["主题"]
|
||||
n1 --> n2["一级分支"]
|
||||
n2 --> n3["二级分支"]
|
||||
"""
|
||||
lines = []
|
||||
for raw in body.splitlines():
|
||||
s = raw.strip()
|
||||
if not s or s == "mindmap":
|
||||
continue
|
||||
lines.append(raw)
|
||||
if not lines:
|
||||
return "flowchart LR\n n1[空思维导图]"
|
||||
|
||||
out = ["flowchart LR"]
|
||||
stack: list[tuple[int, str]] = [] # (缩进长度, 节点 id)
|
||||
seq = 0
|
||||
|
||||
def _new_id() -> str:
|
||||
nonlocal seq
|
||||
seq += 1
|
||||
return f"n{seq}"
|
||||
|
||||
for raw in lines:
|
||||
m = re.match(r"[ \t]*", raw)
|
||||
indent_len = len(m.group(0).replace("\t", " "))
|
||||
text = raw[m.end():].strip()
|
||||
# 去掉 ::icon(...) 与行尾 # 注释
|
||||
text = re.sub(r"::icon\([^)]*\)", "", text)
|
||||
text = re.split(r"\s+#\s*", text)[0].strip()
|
||||
# 去掉 mindmap 的 root 关键字:root((主题)) 里的 root 只是根标记,标签在形状符号内
|
||||
if text.startswith("root") and len(text) > 4:
|
||||
rest = text[4:].lstrip()
|
||||
if rest and rest[0] in "([{":
|
||||
text = rest
|
||||
# 去掉 mindmap 形状符号包裹:(()) / [[ ]] / [ ] / ( ) / { }
|
||||
for open_, close in (("((", "))"), ("[[", "]]"), ("[", "]"), ("(", ")"), ("{", "}")):
|
||||
if text.startswith(open_) and text.endswith(close):
|
||||
text = text[len(open_):-len(close)].strip()
|
||||
break
|
||||
label = text.strip() or _new_id()
|
||||
|
||||
while stack and stack[-1][0] >= indent_len:
|
||||
stack.pop()
|
||||
nid = _new_id()
|
||||
safe = label.replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ")
|
||||
out.append(f' {nid}["{safe}"]')
|
||||
if stack:
|
||||
out.append(f" {stack[-1][1]} --> {nid}")
|
||||
stack.append((indent_len, nid))
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _msg_to_out(m: Message) -> MessageOut:
|
||||
citations = None
|
||||
if m.citations:
|
||||
|
||||
Reference in New Issue
Block a user