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:
373
docs/book-mcp-design.md
Normal file
373
docs/book-mcp-design.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# 教辅处理 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` 生成算法**(保证全局唯一 + 路径稳定):
|
||||
|
||||
```python
|
||||
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` 库加表)<br>B. 起新库 `book_mcp`<br>C. 用 SQLite(开发期够用)|
|
||||
| **MCP 名字** | `book-mcp` / `exam-book-mcp` / `teach-assist-mcp` |
|
||||
| **桶** | 复用 `scan` 还是新开 `book` 桶? |
|
||||
| **写入控制** | 要不要签名 URL(防外链)?还是公开? |
|
||||
| **MCP 端口** | 8080 / 8100 / 复用 8089 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A: 数据库 schema 草稿
|
||||
|
||||
```sql
|
||||
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 确认待定项后开干。
|
||||
213
docs/deploy.md
Normal file
213
docs/deploy.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# book-mcp 部署文档
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
Agent (任意 Agent 客户端)
|
||||
↓ HTTP/JSON-RPC
|
||||
book-mcp (8100) ──┬─→ MongoDB (27018/book_mcp) ← docker container
|
||||
└─→ RustFS (fs.code-lab.cn/scan/) ← 对象存储
|
||||
└─→ wordparse (8089) ← docker container docx4j
|
||||
```
|
||||
|
||||
## 组件
|
||||
|
||||
| 组件 | 位置 | 端口 | 启动方式 |
|
||||
|---|---|---|---|
|
||||
| **book-mcp** | `~/WorkSpace/book-mcp/book_mcp_server.py` | 8100 | launchd `ai.book-mcp.server` |
|
||||
| **MongoDB** | docker container `book-mcp-mongo` | 27018 | `docker start book-mcp-mongo` |
|
||||
| **RustFS** | `fs.code-lab.cn` | 443 | 远程 |
|
||||
| **wordparse (docx4j)** | docker container `wordparse-test` | 8089 | `docker start wordparse-test` |
|
||||
|
||||
## 安装(一次性)
|
||||
|
||||
### 1. MongoDB 容器
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name book-mcp-mongo \
|
||||
-p 27018:27017 \
|
||||
-e MONGO_INITDB_ROOT_USERNAME=book_admin \
|
||||
-e MONGO_INITDB_ROOT_PASSWORD=book_mongo_2026 \
|
||||
-e MONGO_INITDB_DATABASE=book_mcp \
|
||||
-v book-mcp-mongo-data:/data/db \
|
||||
--restart unless-stopped \
|
||||
mongo:8.0
|
||||
```
|
||||
|
||||
### 2. Python venv(一次性)
|
||||
|
||||
```bash
|
||||
python3.13 -m venv /tmp/wordparse_venv
|
||||
/tmp/wordparse_venv/bin/pip install -r ~/WorkSpace/book-mcp/requirements.txt
|
||||
```
|
||||
|
||||
依赖:`fastmcp boto3 pymongo pyyaml`
|
||||
|
||||
### 3. MongoDB 索引
|
||||
|
||||
```bash
|
||||
/tmp/wordparse_venv/bin/python ~/WorkSpace/book-mcp/book_mcp_server.py --init-db
|
||||
```
|
||||
|
||||
输出:
|
||||
```
|
||||
✓ MongoDB 已就绪(db: book_mcp, collections: [...])
|
||||
✓ 8 个索引创建完成
|
||||
```
|
||||
|
||||
### 4. LaunchAgent
|
||||
|
||||
```bash
|
||||
# 装(已装好:~/Library/LaunchAgents/ai.book-mcp.server.plist)
|
||||
launchctl load -w ~/Library/LaunchAgents/ai.book-mcp.server.plist
|
||||
```
|
||||
|
||||
## 日常运维
|
||||
|
||||
### 状态查询
|
||||
|
||||
```bash
|
||||
# launchd 状态
|
||||
launchctl print gui/501/ai.book-mcp.server | grep -E "state|pid"
|
||||
|
||||
# 端口
|
||||
lsof -i :8100
|
||||
|
||||
# 健康
|
||||
curl http://127.0.0.1:8100/mcp
|
||||
# fastmcp 在 /mcp 走 HTTP
|
||||
|
||||
# 实时日志
|
||||
tail -f ~/.hermes/services/book-mcp/logs/server.log
|
||||
```
|
||||
|
||||
### 重启
|
||||
|
||||
```bash
|
||||
# 软重启(SIGTERM,launchd 30s ThrottleInterval)
|
||||
kill $(lsof -ti :8100)
|
||||
|
||||
# launchd 自动拉起
|
||||
sleep 5
|
||||
ps aux | grep book_mcp_server | grep -v grep
|
||||
```
|
||||
|
||||
### 调试
|
||||
|
||||
```bash
|
||||
# 1. 跑 e2e 测试
|
||||
/tmp/wordparse_venv/bin/python ~/WorkSpace/book-mcp/tests/test_e2e.py
|
||||
|
||||
# 2. 直接调 MCP 工具(fastmcp client)
|
||||
/tmp/wordparse_venv/bin/python << 'PYEOF'
|
||||
import asyncio
|
||||
from fastmcp.client import Client
|
||||
async def main():
|
||||
async with Client("http://127.0.0.1:8100/mcp") as c:
|
||||
r = await c.call_tool('health', {})
|
||||
print(r.data)
|
||||
asyncio.run(main())
|
||||
PYEOF
|
||||
|
||||
# 3. mongo 直连
|
||||
/tmp/wordparse_venv/bin/python << 'PYEOF'
|
||||
from db import get_collection, COL_BOOK
|
||||
print(f"books count: {get_collection(COL_BOOK).count_documents({})}")
|
||||
PYEOF
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
`~/WorkSpace/book-mcp/config.yaml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8100 # MCP server 端口
|
||||
database:
|
||||
url: mongodb://book_admin:book_mongo_2026@127.0.0.1:27018/book_mcp?authSource=admin
|
||||
db_name: book_mcp
|
||||
rustfs:
|
||||
endpoint: https://fs.code-lab.cn
|
||||
default_bucket: scan
|
||||
wordparse:
|
||||
url: http://127.0.0.1:8089/api/v3/server_office_file/file/parse
|
||||
timeout: 600
|
||||
```
|
||||
|
||||
## MCP 工具列表(15 个)
|
||||
|
||||
| # | 工具 | 类别 |
|
||||
|---|---|---|
|
||||
| 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 | 查询 |
|
||||
| 9 | get_document | 查询 |
|
||||
| 10 | list_documents | 查询 |
|
||||
| 11 | get_document_url | 查询 |
|
||||
| 12 | upload_to_rustfs | 辅助 |
|
||||
| 13 | soft_delete_book | 辅助 |
|
||||
| 14 | retry_failed | 辅助 |
|
||||
| 15 | health | 诊断 |
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
zip 上传 RustFS
|
||||
↓ create_book(zip_sha256)
|
||||
→ book.id
|
||||
↓ 循环每 docx
|
||||
→ add_document(filename, path)
|
||||
→ doc.doc_id = sha1(book_id|path)[:16]
|
||||
→ wordparse.convert_one (cleaner + docx4j + MathJax)
|
||||
→ RustFS upload → mark_html_ready(url)
|
||||
→ Agent 解析 HTML → structure.json
|
||||
→ RustFS upload → mark_json_ready(url)
|
||||
→ update_book_status("done")
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 原因 | 解决 |
|
||||
|---|---|---|
|
||||
| `mongo` health=False | 容器没起 | `docker start book-mcp-mongo` |
|
||||
| `wordparse` health=False | docx4j 容器没起 | `docker start wordparse-test` |
|
||||
| 8100 端口未 listen | 进程挂了 | `kill $(lsof -ti :8100)` 走 launchd 自动拉起 |
|
||||
| `add_document` 报 NoneType | 数据库索引问题(已修复) | 重跑 `--init-db` |
|
||||
| 上传 403 Forbidden | 桶不存在 | 检查 `default_bucket` 是否在 RustFS 实际存在 |
|
||||
| e2e 测试报 `uk_name_version` | 同名教辅已存在 | 改 name 或 version 加时间戳 |
|
||||
|
||||
## 备份
|
||||
|
||||
- **MongoDB 数据**:`book-mcp-mongo-data` docker volume(在 docker 里)
|
||||
- **RustFS 文件**:`https://fs.code-lab.cn/scan/book/<book_id>/...`
|
||||
- **代码**:`~/WorkSpace/book-mcp/`
|
||||
|
||||
## 升级路径
|
||||
|
||||
```bash
|
||||
# 1. 拉新代码
|
||||
cd ~/WorkSpace/book-mcp
|
||||
git pull # 假设推到 git
|
||||
|
||||
# 2. 升级依赖
|
||||
/tmp/wordparse_venv/bin/pip install -r requirements.txt --upgrade
|
||||
|
||||
# 3. 重启 launchd
|
||||
kill $(lsof -ti :8100)
|
||||
sleep 5
|
||||
ps aux | grep book_mcp_server | grep -v grep
|
||||
```
|
||||
|
||||
## 相关链接
|
||||
|
||||
- 设计文档:`docs/book-mcp-design.md`
|
||||
- Schema:`db/schema_doc.md`
|
||||
- wordparse 工具:`services/wordparse/`
|
||||
- 启动日志:`~/.hermes/services/book-mcp/logs/server.log`
|
||||
- 错误日志:`~/.hermes/services/book-mcp/logs/server.error.log`
|
||||
Reference in New Issue
Block a user