后端 (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→入库→上传→校验)
98 lines
3.2 KiB
Python
98 lines
3.2 KiB
Python
"""MongoDB 连接 + collection 管理
|
||
|
||
3 个 collection:
|
||
book — 教辅
|
||
book_document — 教辅文档
|
||
document_file — 文档文件
|
||
|
||
设计思路:
|
||
- _id 用业务 ID(doc_id 16 字符 sha1 / book_id 自增字符串 / file_id 自增字符串)
|
||
- book_id 用 INT 自增(mongo 中用 find_one_and_update + $inc 实现)
|
||
- 不强制 schema(mongo 灵活),但用 helper 函数做字段约束
|
||
- 索引在 init_db() 时一次性建好
|
||
"""
|
||
import os
|
||
import yaml
|
||
from pymongo import MongoClient, ASCENDING, DESCENDING, ReturnDocument
|
||
from pymongo.database import Database
|
||
|
||
_CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.yaml')
|
||
|
||
_client: MongoClient | None = None
|
||
|
||
|
||
def load_config() -> dict:
|
||
with open(_CONFIG_PATH) as f:
|
||
return yaml.safe_load(f)
|
||
|
||
|
||
def get_client() -> MongoClient:
|
||
global _client
|
||
if _client is None:
|
||
cfg = load_config()['database']
|
||
_client = MongoClient(cfg['url'], serverSelectionTimeoutMS=5000)
|
||
return _client
|
||
|
||
|
||
def get_db() -> Database:
|
||
cfg = load_config()['database']
|
||
return get_client()[cfg['db_name']]
|
||
|
||
|
||
def get_collection(name: str):
|
||
return get_db()[name]
|
||
|
||
|
||
# ---- collection 名常量 ----
|
||
COL_BOOK = 'book'
|
||
COL_DOC = 'book_document'
|
||
COL_FILE = 'document_file'
|
||
|
||
# ---- counters collection(book_id / file_id 自增用)----
|
||
COL_COUNTER = 'counters'
|
||
|
||
|
||
# ---- 索引初始化 ----
|
||
def init_db():
|
||
"""建所有索引(幂等)"""
|
||
db = get_db()
|
||
|
||
# book: zip_sha256 唯一 + (name, version, is_deleted) 唯一
|
||
db[COL_BOOK].create_index([('zip_sha256', ASCENDING)], unique=True, name='uk_book_zip_sha')
|
||
db[COL_BOOK].create_index([('name', ASCENDING), ('version', ASCENDING), ('is_deleted', ASCENDING)],
|
||
unique=True, name='uk_book_name_version')
|
||
db[COL_BOOK].create_index([('status', ASCENDING)], name='idx_book_status')
|
||
db[COL_BOOK].create_index([('created_at', DESCENDING)], name='idx_book_created')
|
||
|
||
# book_document: _id 已唯一(doc_id 即 _id);额外索引仅用于查询加速
|
||
db[COL_DOC].create_index([('book_id', ASCENDING), ('order_index', ASCENDING)], name='idx_book_order')
|
||
db[COL_DOC].create_index([('status', ASCENDING)], name='idx_doc_status')
|
||
|
||
# document_file: (doc_id, file_type) 唯一 + (bucket, key)
|
||
db[COL_FILE].create_index([('doc_id', ASCENDING), ('file_type', ASCENDING)],
|
||
unique=True, name='uk_doc_type')
|
||
db[COL_FILE].create_index([('object_bucket', ASCENDING), ('object_key', ASCENDING)],
|
||
name='idx_bucket_key')
|
||
|
||
print(f"✓ MongoDB 已就绪(db: {db.name}, collections: {[COL_BOOK, COL_DOC, COL_FILE]})")
|
||
print(f" ✓ 8 个索引创建完成")
|
||
|
||
|
||
def health() -> bool:
|
||
try:
|
||
get_client().admin.command('ping')
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def next_id(name: str) -> int:
|
||
"""原子自增 ID(mongo 没有 AUTO_INCREMENT,用 counters collection 模拟)"""
|
||
db = get_db()
|
||
r = db[COL_COUNTER].find_one_and_update(
|
||
{'_id': name},
|
||
{'$inc': {'seq': 1}},
|
||
upsert=True,
|
||
return_document=ReturnDocument.AFTER,
|
||
)
|
||
return int(r['seq']) |