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:
94
services/rustfs_service.py
Normal file
94
services/rustfs_service.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""RustFS 对象存储封装(基于 boto3)"""
|
||||
import os
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
import boto3
|
||||
import botocore
|
||||
|
||||
from db.database import load_config
|
||||
|
||||
|
||||
def _s3():
|
||||
cfg = load_config()['rustfs']
|
||||
return boto3.client(
|
||||
's3',
|
||||
endpoint_url=cfg['endpoint'],
|
||||
aws_access_key_id=cfg['access_key'],
|
||||
aws_secret_access_key=cfg['secret_key'],
|
||||
region_name='us-east-1',
|
||||
config=botocore.config.Config(
|
||||
signature_version='s3v4',
|
||||
s3={'addressing_style': 'path'},
|
||||
),
|
||||
verify=cfg.get('verify_ssl', False),
|
||||
)
|
||||
|
||||
|
||||
def _content_type(path: str) -> str:
|
||||
p = path.lower()
|
||||
if p.endswith('.html'):
|
||||
return 'text/html; charset=utf-8'
|
||||
if p.endswith('.js'):
|
||||
return 'application/javascript'
|
||||
if p.endswith('.json'):
|
||||
return 'application/json'
|
||||
if p.endswith('.docx'):
|
||||
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
if p.endswith('.png'):
|
||||
return 'image/png'
|
||||
if p.endswith('.log'):
|
||||
return 'text/plain; charset=utf-8'
|
||||
if p.endswith('.zip'):
|
||||
return 'application/zip'
|
||||
return 'application/octet-stream'
|
||||
|
||||
|
||||
def upload(local_path: str, key: str, bucket: str | None = None) -> dict:
|
||||
"""上传文件 → 返回 {url, key, bucket, size, checksum}"""
|
||||
cfg = load_config()['rustfs']
|
||||
bucket = bucket or cfg['default_bucket']
|
||||
|
||||
size = os.path.getsize(local_path)
|
||||
# sha256
|
||||
h = hashlib.sha256()
|
||||
with open(local_path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(65536), b''):
|
||||
h.update(chunk)
|
||||
checksum = h.hexdigest()
|
||||
|
||||
_s3().upload_file(
|
||||
local_path, bucket, key,
|
||||
ExtraArgs={'ContentType': _content_type(local_path)},
|
||||
)
|
||||
url = f"{cfg['public_base']}/{bucket}/" + urllib.parse.quote(key, safe='/')
|
||||
return {
|
||||
'url': url,
|
||||
'key': key,
|
||||
'bucket': bucket,
|
||||
'size': size,
|
||||
'checksum': checksum,
|
||||
}
|
||||
|
||||
|
||||
def delete_prefix(prefix: str, bucket: str | None = None) -> int:
|
||||
"""批量删一个前缀下的所有对象,返回删除条数"""
|
||||
cfg = load_config()['rustfs']
|
||||
bucket = bucket or cfg['default_bucket']
|
||||
s3 = _s3()
|
||||
n = 0
|
||||
paginator = s3.get_paginator('list_objects_v2')
|
||||
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
||||
keys = [{'Key': o['Key']} for o in page.get('Contents', [])]
|
||||
if keys:
|
||||
s3.delete_objects(Bucket=bucket, Delete={'Objects': keys})
|
||||
n += len(keys)
|
||||
return n
|
||||
|
||||
|
||||
def build_key(book_id: int, doc_id: str, file_type: str) -> str:
|
||||
"""根据 book_id / doc_id / file_type 生成对象存储 key"""
|
||||
cfg = load_config()['storage']
|
||||
file_naming = cfg['file_naming']
|
||||
filename = file_naming.get(file_type, f'{file_type}.bin')
|
||||
return f"{cfg['book_prefix']}/{book_id}/{cfg['docs_subdir']}/{doc_id}/{filename}"
|
||||
Reference in New Issue
Block a user