后端 (book_mcp_server.py, 335 行, 15 个 MCP 工具):
- create_book / add_document / mark_html_ready / mark_json_ready / mark_failed
- update_book_status / soft_delete_book / retry_failed
- get_book / list_books / get_document / list_documents / get_document_url
- upload_to_rustfs / health
- 走 JSON-RPC + stateless_http + json_response(前端可直接调)
- launchd 守护 / 6 秒拉起(macOS)
- systemd 守护脚本(Linux/codelab)
存储:
- MongoDB (mongo:8.0 容器) - book/book_document/document_file 三表
- RustFS 对象存储(fs.code-lab.cn/scan/)- book/<id>/docs/<doc_id>/{docx,html,json}
- 幂等设计: zip_sha256 唯一 + (book_id+original_path) 唯一
前端 (Vue 3 + TS + Arco Design):
- 列表页 /book/list - 搜索 + 状态过滤 + 进度
- 详情页 /book/:id - 文档折叠 + 文件下载
- MCP 客户端 src/api/mcp.ts (JSON-RPC over fetch)
部署:
- deploy/install.sh (codelab 一键装 backend + systemd)
- deploy/install-frontend.sh (vite dev / 生产构建)
- docs/deploy.md (运维手册)
测试:
- tests/test_basic.py (sanity 7 项)
- tests/test_e2e.py (完整 e2e: docx→HTML→入库→上传→校验)
71 lines
3.0 KiB
Python
71 lines
3.0 KiB
Python
"""HTML 美化:注入 MathJax / 样式 / 导航 / 统计"""
|
||
import re
|
||
import os
|
||
import shutil
|
||
import urllib.request
|
||
|
||
# --- 常量 ---
|
||
NAV = ('<div class="nav" style="position:sticky;top:0;background:#f8fafc;border-bottom:1px solid #e2e8f0;'
|
||
'padding:8px 0;margin:0 0 16px;font-size:14px"><a href="index.html" '
|
||
'style="color:#2563eb;text-decoration:none">← 返回索引</a></div>')
|
||
|
||
STYLE = ('<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||
'<style>body{max-width:900px;margin:0 auto;padding:24px 18px;font-family:-apple-system,'
|
||
'"PingFang SC","Microsoft YaHei",serif;font-size:16px;line-height:1.9;color:#1a1a1a;background:#fff}'
|
||
'img{max-width:100%;height:auto}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px 6px}'
|
||
'mjx-container{overflow-x:auto;overflow-y:hidden;max-width:100%}</style>')
|
||
|
||
MATHJAX_TAG = ('<script>window.MathJax={tex:{inlineMath:[["\\\\(","\\\\)"]],displayMath:[["\\\\[","\\\\]"]]},'
|
||
'svg:{fontCache:"local"},options:{enableMenu:false}};</script>'
|
||
'<script src="tex-svg.js"></script>')
|
||
|
||
ASSETS_DIR = os.path.expanduser('~/.hermes/mcp/assets')
|
||
MATHJAX_FILE = os.path.join(ASSETS_DIR, 'tex-svg.js')
|
||
MATHJAX_MIRROR = 'https://registry.npmmirror.com/mathjax/3.2.2/files/es5/tex-svg.js'
|
||
|
||
|
||
def _fix_math(m):
|
||
inner = (m.group(1).replace(' ', ' ').replace('&', '&')
|
||
.replace('<', '<').replace('>', '>').replace(' ', ' '))
|
||
return r'\(' + re.sub(r'[ \t]{2,}', ' ', inner).strip() + r'\)'
|
||
|
||
|
||
def clean_html(content: str, mathjax: bool = True) -> str:
|
||
"""原始 HTML → 加 MathJax + 导航 + 样式"""
|
||
c = re.sub(r'^<\?xml[^>]*\?>\s*', '', content)
|
||
c = re.sub(r'<!DOCTYPE[^>]*>', '<!DOCTYPE html>', c, count=1)
|
||
c = re.sub(r'\\\((.*?)\\\)', _fix_math, c, flags=re.S)
|
||
inject = STYLE + (MATHJAX_TAG if mathjax else '')
|
||
c = c.replace('</head>', inject + '</head>', 1) if '</head>' in c else inject + c
|
||
m = re.search(r'<body[^>]*>', c, re.I)
|
||
c = (c[:m.end()] + NAV + c[m.end():]) if m else (NAV + c)
|
||
return c
|
||
|
||
|
||
def stats(html: str) -> dict:
|
||
"""HTML 统计:字符 / 公式 / 图片 / 未包裹 LaTeX"""
|
||
stripped = re.sub(r'\\\(.*?\\\)', '', html, flags=re.S)
|
||
return {
|
||
'chars': len(html),
|
||
'formulas': len(re.findall(r'\\\(', html)),
|
||
'images': html.count('<img'),
|
||
'unwrapped_latex': len(re.findall(
|
||
r'\\(?:dfrac|frac|sqrt|left|begin\{array)', stripped)),
|
||
}
|
||
|
||
|
||
def ensure_mathjax() -> str:
|
||
"""下载/复用 MathJax 离线文件 → 返回路径"""
|
||
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||
if not os.path.exists(MATHJAX_FILE) or os.path.getsize(MATHJAX_FILE) < 500_000:
|
||
urllib.request.urlretrieve(MATHJAX_MIRROR, MATHJAX_FILE)
|
||
return MATHJAX_FILE
|
||
|
||
|
||
def copy_mathjax_to(out_dir: str) -> str:
|
||
"""把 MathJax 复制到 out_dir,返回目标路径"""
|
||
src = ensure_mathjax()
|
||
dst = os.path.join(out_dir, 'tex-svg.js')
|
||
shutil.copy(src, dst)
|
||
return dst
|