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→入库→上传→校验)
This commit is contained in:
2026-10-11 18:03:45 +08:00
parent f7fb571bce
commit 9b0910ccb1
160 changed files with 23628 additions and 3 deletions

98
db/database.py Normal file
View File

@@ -0,0 +1,98 @@
"""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'])