后端 (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→入库→上传→校验)
47 lines
1.5 KiB
Python
47 lines
1.5 KiB
Python
"""docx4j HTTP 客户端 — POST 一份 docx,返回 HTML 字符串"""
|
||
import os
|
||
import uuid
|
||
import json
|
||
import urllib.request
|
||
from db.database import load_config
|
||
|
||
|
||
def _config():
|
||
return load_config()['wordparse']
|
||
|
||
|
||
def post_docx(path: str, timeout: int | None = None) -> str:
|
||
"""返回 HTML 字符串(原始,未做 MathJax/导航注入)"""
|
||
cfg = _config()
|
||
url = cfg['url']
|
||
timeout = timeout or cfg.get('timeout', 600)
|
||
|
||
boundary = '----WP' + uuid.uuid4().hex
|
||
with open(path, 'rb') as f:
|
||
data = f.read()
|
||
fn = os.path.basename(path)
|
||
body = (
|
||
f'--{boundary}\r\n'.encode()
|
||
+ f'Content-Disposition: form-data; name="file"; filename="{fn}"\r\n'.encode('utf-8')
|
||
+ b'Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n\r\n'
|
||
+ data
|
||
+ b'\r\n' + f'--{boundary}--\r\n'.encode()
|
||
)
|
||
req = urllib.request.Request(url, data=body, headers={
|
||
'Content-Type': f'multipart/form-data; boundary={boundary}',
|
||
'Content-Length': str(len(body)),
|
||
})
|
||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||
resp = json.loads(r.read().decode('utf-8'))
|
||
return resp.get('content') or ''
|
||
|
||
|
||
def health() -> bool:
|
||
cfg = _config()
|
||
try:
|
||
with urllib.request.urlopen(cfg['health_url'], timeout=5) as r:
|
||
j = json.loads(r.read().decode('utf-8'))
|
||
return j.get('status') == 'ok'
|
||
except Exception:
|
||
return False
|