Files
book-mcp/services/wordparse/render.py
Quentin 9b0910ccb1 Initial commit: book-mcp 教辅处理 MCP server + Arco Vue 管理端
后端 (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→入库→上传→校验)
2026-10-11 18:03:57 +08:00

71 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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('&nbsp;', ' ').replace('&amp;', '&')
.replace('&lt;', '<').replace('&gt;', '>').replace('&#160;', ' '))
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