Files
book-mcp/docs/book-mcp-design.md
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

15 KiB
Raw Blame History

教辅处理 MCP 服务 — 方案设计

作者: 助手(Quentin 的 AI 助手) 日期: 2026-10-11 状态: 待评审


1. 业务目标

Agent 上传一个教辅 zip → MCP 自动解压 + 批量转 HTML + 上传对象存储 + 入库 → 后续页面可查、可看 HTML。

核心工作流

┌─ Agent 端 ───────────────────────────────────────────┐
│                                                       │
│  1) Agent: upload_zip(local_zip) → rustfs 返回 key    │
│  2) Agent: mcp.create_book(name, zip_path=key)         │
│       ↓                                               │
│     返回 {book_id, status="pending"}                   │
│                                                       │
│  3) Agent: 本地解压 zip                               │
│  4) Agent: 循环每 docx                                │
│     ├─ mcp.add_document(book_id, filename, path)     │
│     │    ↓ 返回 doc_id                                │
│     ├─ wordparse.docx_to_html(docx) → 本地 html       │
│     ├─ mcp.upload_to_rustfs(html, book/doc_id/html)  │
│     ├─ Agent 本地解析 html → structure.json           │
│     ├─ mcp.upload_to_rustfs(json, book/doc_id/json)  │
│     ├─ mcp.mark_html_ready(doc_id, html_url, stats)  │
│     └─ mcp.mark_json_ready(doc_id, json_url, ver)    │
│                                                       │
│  5) Agent: mcp.update_book_status(book_id, "done")    │
│                                                       │
└─ 后续页面 ───────────────────────────────────────────┘
│  - HTTP API 查 book / doc / file(复用 mcp 的查询类)  │
│  - 列表页:分页查 book + 每本完成进度条                 │
│  - 详情页:doc 列表 + 跳 html / json 在线预览          │
└──────────────────────────────────────────────────────┘

2. 数据模型

2.1 ER 关系

┌────────────┐ 1      N ┌────────────────┐ 1      N ┌──────────────┐
│  book      │─────────│  book_document │─────────│ document_file│
│  教辅      │          │  文档          │          │ 文件(多形态) │
└────────────┘          └────────────────┘          └──────────────┘

2.2 实体字段

book 教辅(zip / 套书)

字段 类型 说明
id BIGINT PK 自增
name VARCHAR(200) 教辅名("2027版.曲一线3•2物理.广西版.一轮复习-突破训练")
subject VARCHAR(50) 学科(物理/数学/语文/...)
grade VARCHAR(50) 年级(高三/高一/...)
version VARCHAR(50) 版本(人教版/广西版/2027版/...)
type VARCHAR(50) 类型(突破训练/同步训练/真题/...)
zip_path VARCHAR(500) 原始 zip 在对象存储的路径(如 book/2027-物理-突破训练.zip)
zip_sha256 VARCHAR(64) UNIQUE 原始 zip 哈希(幂等去重)
zip_size BIGINT 原始 zip 字节数
status ENUM pending / processing / done / failed / partial
total_count INT zip 内 docx 总数
success_count INT 成功转 HTML 数
failed_count INT 失败数
error_msg TEXT 失败原因
created_at DATETIME 创建时间
updated_at DATETIME 处理完成时间
created_by VARCHAR(100) 哪个 Agent / 哪个用户传
is_deleted TINYINT 软删标记(0/1)

唯一索引: uk_name_version (name, version, is_deleted) 防同名教辅重复创建

book_document 文档(zip 里的每个 docx 一条)

字段 类型 说明
id BIGINT PK 自增
book_id BIGINT FK→book.id 属于哪本教辅
doc_id VARCHAR(64) UNIQUE 本服务的核心 ID(算法见下)
original_filename VARCHAR(200) 原始文件名
original_path VARCHAR(500) zip 内的相对路径
title VARCHAR(200) 解析出的标题(如能从 docx 提取)
doc_type ENUM 课时作业 / 周末提升 / 阶段质量评估 / 综合评估 / unknown
order_index INT 在教辅中的顺序(按文件名排序)
status ENUM pending / html_ready / json_ready / failed
error_msg TEXT 失败原因
created_at DATETIME
updated_at DATETIME

doc_id 生成算法(保证全局唯一 + 路径稳定):

doc_id = sha1(f"{book_id}|{original_path}").hexdigest()[:16]
# 例: 5b3a2f1e9c0d4b7a
  • 同本书同路径永远同一 doc_id
  • 重复传同一 zip 不会重复入库(幂等)
  • 不带敏感信息(文件名脱敏)

索引: uk_doc_id, idx_book_id (book_id, order_index)

document_file 文件(HTML / JSON / 原 docx 多产物)

字段 类型 说明
id BIGINT PK 自增
doc_id VARCHAR(64) FK→book_document.doc_id 关联到文档
file_type ENUM docx / html / json / preview_png / error_log
object_key VARCHAR(500) 对象存储 key
object_bucket VARCHAR(50) 桶名
size BIGINT 字节数
checksum VARCHAR(64) sha256
url VARCHAR(500) 公开 URL(如果是公开桶)
meta JSON 附加信息:公式数、图片数、字符数等
created_at DATETIME

索引: uk_doc_type (doc_id, file_type), idx_bucket_key (object_bucket, object_key)

一个 doc 对应 3-5 条 file 记录: 原 docx + html + json + (可选) 错误日志


3. 对象存储路径结构

<bucket>/
└── book/
    └── <book_id_or_slug>/              # book-{id}-{slug}
        └── docs/
            └── <doc_id>/                # sha1 短 hash
                ├── source.docx          # 原 docx(备份用)
                ├── output.html          # 转的 HTML(含 MathJax)
                ├── structure.json       # Agent 后续结构化输出
                ├── preview.png          # 可选:首页截图
                └── error.log            # 仅失败时有

优点:

  • 同 doc_id 所有产物都在一个目录
  • 删教辅 → 删整个 <book_id>/ 前缀
  • 列表时一次 list <book_id>/docs/ 拿到全部

4. MCP 工具集

4.1 写入类

# 工具 签名 返回
1 create_book (name, zip_path, subject?, grade?, version?, type?, created_by?) {book_id, status, total_count}
2 add_document (book_id, original_filename, original_path, zip_size?) {doc_id, order_index, status}
3 mark_html_ready (doc_id, html_url, html_size, stats: {formulas, images, chars}) {ok, file_id}
4 mark_json_ready (doc_id, json_url, json_size, schema_version) {ok, file_id}
5 mark_failed (doc_id, error_msg, stage: "html" | "json" | "upload") {ok, status}
6 update_book_status (book_id, status, error_msg?) {ok}
15 upload_to_rustfs (local_path, key, bucket?) {url, key, size, checksum}

4.2 查询类

# 工具 签名 返回
7 get_book (book_id) {book + documents[] + files[]}
8 list_books (status?, page?, page_size?, keyword?) {books[], total, page, page_size}
9 get_document (doc_id) {document + files[]}
10 list_documents (book_id, status?, page?, page_size?) {documents[], total}
11 get_document_files (doc_id) {files[]}
12 get_document_url (doc_id, file_type: "html"|"json"|"docx") {url, expires_in}

4.3 辅助类

# 工具 签名 返回
13 delete_book (book_id, hard?: bool) {ok, deleted_files}
14 retry_failed (book_id) {retried_count, ok, failed_count}

4.4 工具数量

  • v1 最小可用: 8 个(create_book / add_document / mark_html_ready / mark_json_ready / mark_failed / update_book_status / get_book / list_books)
  • v2 完整: 加 get_document / list_documents / get_document_files / get_document_url
  • v3 兜底: 加 delete_book / retry_failed / upload_to_rustfs

5. 技术栈选型

层 选型 理由
MCP 框架 fastmcp (Python 3.13) 跟 wordparse_server.py 一致,<10 行起 server
HTTP 框架 不另起 — fastmcp 自动暴露 HTTP 省一个服务
数据库 MySQL / PolarDB 跟 jby 同栈;SQLAlchemy 2.0 + alembic
对象存储 RustFS fs.code-lab.cn(你已有) 同 wordparse
Word→HTML 复用 wordparse_server.py 的 docx_to_html 不重写,自动清洗已集成
HTML→JSON Agent 调 LLM 自己做 复杂结构化逻辑,放业务侧
配置 YAML 跟 wordparse 一致

6. 项目目录

book-mcp/
├── config.yaml              # db / rustfs / 默认桶
├── book_mcp_server.py       # MCP 入口(fastmcp)
├── db/
│   ├── models.py            # SQLAlchemy ORM
│   ├── migrations/          # alembic
│   └── schema.sql           # 一份纯 SQL(给 DBA / 手动建表用)
├── services/
│   ├── book_service.py      # book CRUD
│   ├── document_service.py  # doc CRUD
│   ├── file_service.py      # file CRUD
│   └── rustfs_service.py    # 上传 / 删
├── tests/
└── README.md

7. 关键设计决策

问题 决策 理由
用 book_id 还是 name 查重? name + version 唯一索引 同名教辅重复传不创建新 book
同一 zip 多次上传? 幂等(按 zip sha256 去重 book) 防呆
docx 处理失败怎么办? book.status = "partial",不阻塞整本 容错,UI 标黄提醒
html 公开还是私有? 公开(读教辅是低频操作) 简化 URL 生成
json 谁生成? Agent 调 LLM 强业务相关,不放 MCP
MCP 一次返回多少文档? list_documents 分页 50/页 防大书 OOM
数据库有没有? 独立新库 book_mcp 避免污染 jby schema
删除能级? 软删(is_deleted 字段) 防误删
后续页面怎么查? 直接调 MCP 的查询类工具(fastmcp 自动暴露 HTTP) 不写新 API

8. v1 范围(最小可用)

做这 8 个工具就够用了:

  1. create_book — 必做
  2. add_document — 必做
  3. mark_html_ready — 必做
  4. mark_json_ready — 必做
  5. mark_failed — 必做
  6. update_book_status — 必做
  7. get_book — 必做
  8. list_books — 必做

v2 再加: get_document / list_documents / get_document_files / get_document_url / delete_book / retry_failed

v1 不做:

  • 鉴权(内网/白名单)
  • 全文搜索(用对象存储 + ES 后续加)
  • 文件版本管理(覆盖式)
  • 异步任务队列(同步够用,每本 ~3-5 分钟)

9. 风险 & 备选

风险 应对
上传 1000+ docx 的大教辅会卡 Agent add_document 一次传 1 个,避免事务过大;Agent 用并发池
docx 转 HTML 全失败(脏文件) 用之前加的 _clean_docx_xml 自动清洗
对象存储 URL 变(key 改了) 数据库记 object_key,URL 重新生成
MCP server 自己挂了 launchd 保活 + 自动重启(参考 paop gateway)

10. 待确认事项

待定 选项
数据库 A. 复用 jby PolarDB(questions-bank 库加表)
B. 起新库 book_mcp
C. 用 SQLite(开发期够用)
MCP 名字 book-mcp / exam-book-mcp / teach-assist-mcp
桶 复用 scan 还是新开 book 桶?
写入控制 要不要签名 URL(防外链)?还是公开?
MCP 端口 8080 / 8100 / 复用 8089

附录 A: 数据库 schema 草稿

CREATE TABLE book (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    subject VARCHAR(50),
    grade VARCHAR(50),
    version VARCHAR(50),
    type VARCHAR(50),
    zip_path VARCHAR(500) NOT NULL,
    zip_sha256 VARCHAR(64) NOT NULL,
    zip_size BIGINT NOT NULL DEFAULT 0,
    status ENUM('pending','processing','done','failed','partial') NOT NULL DEFAULT 'pending',
    total_count INT NOT NULL DEFAULT 0,
    success_count INT NOT NULL DEFAULT 0,
    failed_count INT NOT NULL DEFAULT 0,
    error_msg TEXT,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by VARCHAR(100),
    is_deleted TINYINT NOT NULL DEFAULT 0,
    UNIQUE KEY uk_zip_sha (zip_sha256),
    UNIQUE KEY uk_name_version (name, version, is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE book_document (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    book_id BIGINT NOT NULL,
    doc_id VARCHAR(64) NOT NULL,
    original_filename VARCHAR(200) NOT NULL,
    original_path VARCHAR(500) NOT NULL,
    title VARCHAR(200),
    doc_type ENUM('课时作业','周末提升','阶段质量评估','综合评估','unknown') NOT NULL DEFAULT 'unknown',
    order_index INT NOT NULL DEFAULT 0,
    status ENUM('pending','html_ready','json_ready','failed') NOT NULL DEFAULT 'pending',
    error_msg TEXT,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_doc_id (doc_id),
    INDEX idx_book_order (book_id, order_index),
    CONSTRAINT fk_book_doc FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE document_file (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    doc_id VARCHAR(64) NOT NULL,
    file_type ENUM('docx','html','json','preview_png','error_log') NOT NULL,
    object_key VARCHAR(500) NOT NULL,
    object_bucket VARCHAR(50) NOT NULL,
    size BIGINT NOT NULL DEFAULT 0,
    checksum VARCHAR(64),
    url VARCHAR(500),
    meta JSON,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_doc_type (doc_id, file_type),
    INDEX idx_bucket_key (object_bucket, object_key),
    CONSTRAINT fk_file_doc FOREIGN KEY (doc_id) REFERENCES book_document(doc_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

附录 B: 状态机

book.status 状态机:
  pending → processing → done        (全部成功)
                    └──→ partial    (有失败)
                    └──→ failed     (完全失败,异常中断)

book_document.status 状态机:
  pending → html_ready → json_ready  (正常完成)
         └──→ failed                 (任何一步失败,可重试)

END — 等 Quentin 确认待定项后开干。