# AI Knowledge Link — 技术审查报告(MVP v1)
> 审查范围:需求文档全文(57 节)。
> 核心原则:**MVP 保持简单,6 个扩展接口预留但不实现,SQLite + 本地文件 + 同步解析**。
> 结论:架构成立,核心需求可实现。发现 1 个必须修改的设计、若干建议补充。
---
## 1. 最终架构
### 1.1 架构总览
```
开发环境(Windows) 生产环境(Ubuntu Docker)
───────────────── ─────────────────────
Vue3 dev server ─┐ nginx :80
├─► FastAPI :8000 ├── /api/** → backend:8000
uvicorn --reload ─┘ ├── /k/** → backend:8000
└── / → frontend:80
FastAPI Docker Compose:
├── /api/** 管理端(需登录) ├── frontend (Vue build → nginx serve)
├── /k/** 公共AI页(SSR, 零JS) ├── backend (uvicorn)
└── SQLite + 本地文件系统 └── nginx
/data/ /data/ (宿主机 volume)
├── app.db ├── app.db
└── users/{uid}/kbs/{kb}/... └── users/{uid}/kbs/{kb}/...
├── original/ ├── original/
└── markdown/ └── markdown/
```
### 1.2 分层
```
api/ → 路由层:参数校验 + 鉴权 + 调 Service + 组装响应
services/ → 业务逻辑:配额、Token、编排
repositories/→ 数据访问:隔离 SQLAlchemy 细节
processors/ → 文档解析:MarkItDown → PyMuPDF/python-docx fallback
storage/ → 存储抽象:LocalStorageService(未来 MinIO/S3)
retrieval/ → 检索抽象:KeywordRetriever(未来 Vector/Hybrid)
public/ → AI 公共页面渲染:Jinja2 SSR + md/txt/json 生成器
```
### 1.3 关键技术决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 数据库 | **SQLite** + SQLAlchemy + Alembic | MVP;DATABASE_URL 可切换 PostgreSQL(§7 扩展接口) |
| 文件存储 | **本地文件系统** + StorageService 抽象 | MVP;不引入 MinIO(§8 扩展接口) |
| 文档解析 | **同步** + DocumentProcessor 抽象 | MVP 单用户低并发;未来 Celery(§9 扩展接口) |
| 检索 | **SQLite FTS5** + Retriever 抽象 | 中文 FTS5 支持 better than LIKE;未来 Qdrant(§10 扩展接口) |
| 鉴权 | **服务端 Session**(签发 opaque token 存 SQLite,Cookie 传递) | 同源 SPA;比 JWT 简单且可即时吊销;SQLite 即 session store |
| 密码哈希 | **Argon2id**(argon2-cffi) | 需求指定 |
| 限流 | **内存 TokenBucket**(单进程够用) + RateLimiter 抽象 | 无 Redis 依赖;未来 RedisRateLimiter(§39) |
| 公共页渲染 | **Jinja2 SSR**,零 JS 零外链 | AI 兼容性第一原则 |
| Token 存储 | **SHA-256 hash**(DB)+ **Fernet 加密原文**(可解密,SECRET_KEY 派生) | 满足"后台显示完整链接"需求;DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY) |
| 后缀路由 | 显式注册 `/k/{token}.md` / `.txt` / `.json` **先于** `/k/{token}` | FastAPI path param 默认匹配 `[^/]+`,`{token}` 会吞掉 `.md`(§19) |
| Python 版本 | 3.12+ 编写;本机 3.13.9 运行 | FastAPI / SQLAlchemy 2 / Pydantic v2 均已支持 |
| PyMuPDF 许可 | **MVP 阶段可接受**(AGPL-3.0);商品化前必须替换为 pypdfium2 | 需求 §24 明确指定 PyMuPDF;AGPL 对**未分发**的 SaaS 后端(仅服务端运行)在多数法域下合规,但商品化闭源分发时需替换。MVP 阶段标记 TODO 并在 requirements 中注释 |
---
## 2. 数据库 ER 设计
### 2.1 实体关系
```
plans 1 ──── N users 1 ──── N knowledge_bases 1 ──── N documents 1 ──── 1 document_categories
│ │
│ └ 1 ──── N access_logs
└ 1 ──── N (配额归集)
```
### 2.2 表定义
**plans**
| 列 | 类型 | 说明 |
|---|---|---|
| id | integer PK | |
| code | text unique | `free` / `basic` / `pro` |
| name | text | 套餐名 |
| storage_quota | integer | 字节;free = 104857600 (100MB) |
| max_file_size | integer | 字节;free = 20971520 (20MB) |
| is_active | boolean | |
**users**
| 列 | 类型 | 说明 |
|---|---|---|
| id | text PK (UUID4 hex) | |
| username | text unique not null | |
| email | text unique not null | |
| password_hash | text not null | Argon2id |
| status | text | `active` / `disabled` |
| plan_id | integer FK → plans | 默认 free |
| storage_used | integer default 0 | 字节计数器 |
| created_at / updated_at | text (ISO8601) | |
**knowledge_bases**
| 列 | 类型 | 说明 |
|---|---|---|
| id | text PK (UUID4 hex) | |
| user_id | text FK → users not null | 隔离边界 |
| name | text not null | |
| description | text | |
| enabled | boolean default true | 链接开关 |
| token_hash | text unique not null | SHA-256(secret_token) hex |
| token_encrypted | text | Fernet 加密原文(可解密供后台显示) |
| token_hint | text | token 末 8 位明文,供后台识别 |
| created_at / updated_at | text | |
> SQLite 不支持 citext / timestamptz / uuid 原生类型,统一用 text + 应用层校验。
> 迁移到 PostgreSQL 时,Alembic 迁移负责类型映射,业务层无感。
**document_categories**
| 列 | 类型 |
|---|---|
| id text PK, knowledge_base_id text FK, name text not null, sort_order integer, created_at text |
**documents**
| 列 | 类型 | 说明 |
|---|---|---|
| id | text PK (UUID4 hex) | |
| knowledge_base_id | text FK | |
| user_id | text FK | 冗余存,便于隔离校验 + 配额统计 |
| category_id | text FK nullable | |
| original_filename | text | 仅展示用,不用于物理路径 |
| storage_path | text | 相对于 data/ 的物理路径,不外泄 |
| markdown_path | text | 相对于 data/ 的 Markdown 文件路径 |
| file_size | integer | 字节 |
| mime_type | text | |
| file_ext | text | `.docx` / `.pdf` |
| sha256 | text | 去重与完整性 |
| doc_token_hash | text unique | 单文档公共 URL 凭证 |
| doc_token_encrypted | text | Fernet 加密 |
| doc_token_hint | text | |
| title | text | 用户可改,默认从 Markdown H1 提取 |
| description | text | 用户可改,默认自动生成摘要 |
| keywords | text | 逗号分隔,jieba 提取 + 用户可改 |
| content_summary | text | 抽取式摘要 ~200 字 |
| status | text | PENDING / PROCESSING / READY / FAILED / DELETED |
| error_code | text | 安全错误码(如 `SCANNED_PDF_NO_TEXT_LAYER`) |
| created_at / updated_at | text | |
**access_logs**
| 列 | 类型 |
|---|---|
| id integer PK AUTOINCREMENT, knowledge_base_id text FK, document_id text nullable FK, path text, accessed_at text, user_agent text, request_type text |
> SQLite FTS5 虚拟表(documents_content)单独建,与 documents 通过 id 关联。
### 2.3 索引
- `knowledge_bases(token_hash)` unique
- `documents(doc_token_hash)` unique
- `documents(knowledge_base_id, status)`
- `documents(user_id, status)`
- `users(username)` / `users(email)` unique
---
## 3. API 设计
### 3.1 管理端(/api,需登录)
```
POST /api/auth/register {username, email, password} → 201
POST /api/auth/login {username|email, password} → Set-Cookie + 200
POST /api/auth/logout 清除 Cookie
GET /api/me 用户信息 + 套餐
PATCH /api/me 改密码
GET /api/knowledge-bases 分页列表
POST /api/knowledge-bases {name, description} → 201(返回完整 AI URL,仅此一次)
GET /api/knowledge-bases/{id} 详情
PUT /api/knowledge-bases/{id} 改名/改描述
DELETE /api/knowledge-bases/{id} 软删 + 异步清理文件
POST /api/knowledge-bases/{id}/regenerate-token 旧链立即失效
POST /api/knowledge-bases/{id}/enable
POST /api/knowledge-bases/{id}/disable
GET /api/knowledge-bases/{id}/documents 文档列表
POST /api/knowledge-bases/{id}/documents/upload multipart 上传
GET /api/documents/{id} 详情 + markdown 预览
PUT /api/documents/{id} 改 title/description/keywords/category
DELETE /api/documents/{id} 软删 + 清理文件
POST /api/documents/{id}/reprocess 重新解析
GET /api/knowledge-bases/{id}/categories 分类 CRUD
GET /api/storage {storage_used, storage_quota}
```
统一错误体:`{"code": "STORAGE_QUOTA_EXCEEDED", "message": "存储空间不足", "detail": null}`
### 3.2 公共 AI 端(/k,无登录,限流)
```
GET /k/{token} HTML 入口页(Jinja2 SSR)
GET /k/{token}.md Markdown 入口
GET /k/{token}.txt 纯文本入口
GET /k/{token}.json JSON 目录(白名单字段)
GET /k/{token}/search?q=&page= HTML(默认)/ .json
GET /k/{token}/doc/{doc_token} 文档 HTML 页
GET /k/{token}/doc/{doc_token}.md 原始 Markdown
GET /k/{token}/doc/{doc_token}.txt 纯文本
```
公共端硬性规则:
1. token 不存在 / enabled=false / 软删 → **一律 404**
2. ``
3. ``
4. JSON 输出白名单:`name, description, documents[{title, file_type, description, summary, keywords, updated_at, url}]`
5. 限流:内存 TokenBucket,单 IP 30/min,单 token 60/min
---
## 4. 项目目录
```
amb_rag/
├── backend/
│ ├── app/
│ │ ├── main.py # app 工厂、路由挂载、异常处理、lifespan
│ │ ├── api/ # 管理端路由: auth.py, knowledge_bases.py, documents.py, me.py, deps.py
│ │ ├── public/ # 公共AI路由: routes.py, render.py, serializers.py
│ │ ├── core/ # config.py, security.py, errors.py, session.py, rate_limit.py, logging.py
│ │ ├── models/ # SQLAlchemy 2.0 (Mapped/mapped_column)
│ │ ├── schemas/ # Pydantic v2 request/response
│ │ ├── services/ # auth_service.py, kb_service.py, doc_service.py, kb_public_service.py, storage_service.py
│ │ ├── repositories/ # user_repo.py, kb_repo.py, doc_repo.py, access_log_repo.py
│ │ ├── processors/ # base.py(DocumentProcessor), local_processor.py, parsers/markitdown_parser.py, pdf_parser.py, docx_parser.py
│ │ ├── retrieval/ # base.py(Retriever), keyword_retriever.py
│ │ ├── storage/ # base.py(StorageService), local_storage.py
│ │ └── templates/ # kb_index.html.j2, doc_page.html.j2
│ ├── alembic/ # env.py + versions/
│ ├── tests/ # conftest.py + 各模块测试
│ ├── requirements.txt
│ ├── pyproject.toml # ruff + mypy + pytest 配置
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── api/ # axios 实例 + 各资源 client
│ │ ├── views/ # Login, Register, Dashboard, KnowledgeBases, KbDetail, Settings
│ │ ├── components/
│ │ ├── layouts/
│ │ ├── stores/ # Pinia stores
│ │ ├── router/
│ │ └── types/
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ └── Dockerfile
├── nginx/
│ └── nginx.conf
├── data/ # .gitkeep;运行时生成
│ └── .gitkeep
├── docs/
│ └── technical-review.md # 本文件
├── docker-compose.yml
├── .env.example
├── .gitignore
├── CLAUDE.md
└── README.md
```
---
## 5. 六个未来扩展接口设计
### 5.1 SQLite → PostgreSQL
| 层 | 影响 |
|---|---|
| `core/config.py` | `DATABASE_URL` 从 `sqlite:///./data/app.db` 切为 `postgresql+asyncpg://...`;引擎换 async |
| `core/db.py` | 同步引擎 → 异步引擎 + async_sessionmaker;get_session 改 async generator |
| `repositories/` | 现有同步调用改 `await`;SQLAlchemy 2.x 统一风格使改动最小 |
| Alembic | 新增迁移处理类型映射(text→varchar/citext/uuid, integer→bigint) |
| 业务层 | **零改动**(Repository 已隔离) |
**第一版必须遵守的规则**:
- 不用 PRAGMA 做业务逻辑(只用 WAL 模式设置)
- 不用 SQLite 专属函数(`group_concat` → SQLAlchemy `func` 通用)
- UUID 存 text 而非 blob(跨 DB 兼容)
- 时间存 ISO8601 text 而非 SQLite datetime 函数
- 布尔存 integer 0/1(SQLAlchemy Boolean 在 SQLite 自动映射)
### 5.2 本地文件 → MinIO/S3
```python
# storage/base.py
class StorageService(Protocol):
async def save(self, key: str, data: bytes | IO) -> str: ...
async def read(self, key: str) -> bytes: ...
async def delete(self, key: str) -> None: ...
async def exists(self, key: str) -> bool: ...
async def get_size(self, key: str) -> int: ...
```
- `LocalStorageService`:key 即相对路径,拼 `data/` 前缀;save → 写文件,read → 读文件。
- `MinioStorageService`:key 即对象 key;save → minio.put_object,read → minio.get_object。
- 业务层通过 `core/config.py` 中的 `STORAGE_BACKEND` 选择实现,注入 Service。
- **第一版**:所有 `open()` / `Path()` 操作只在 `local_storage.py` 中出现。
### 5.3 同步解析 → 异步任务
```python
# processors/base.py
class DocumentProcessor(Protocol):
async def process(self, document_id: str) -> None: ...
```
- `LocalDocumentProcessor`:同步调用 parsers,在同一进程中完成。
- `CeleryDocumentProcessor`:提交 Celery task,轮询状态。
- **第一版**:上传 endpoint 中调 `await processor.process(doc_id)`(同步包在 `run_in_executor` 里避免阻塞事件循环)。
- 文档状态机(PENDING→PROCESSING→READY/FAILED)在两个实现中完全相同。
### 5.4 关键词搜索 → RAG
```python
# retrieval/base.py
class Retriever(Protocol):
async def search(self, query: str, knowledge_base_id: str, limit: int = 10) -> list[SearchResult]: ...
```
- `KeywordRetriever`:SQLite FTS5 `MATCH` 查询 + `snippet()` 高亮。
- `VectorRetriever`:Embedding + Qdrant search。
- `HybridRetriever`:关键词 + 向量加权融合。
- **第一版**:FTS5 在 Alembic 迁移中建虚拟表 `documents_fts`;写入时同步更新 FTS。
### 5.5 个人 → 团队/企业
- **第一版**:`knowledge_bases.user_id` 是 owner;所有查询 `WHERE user_id = :me`。
- **扩展**:新增 `organizations` 表 + `org_members`(role);`knowledge_bases.owner_type` + `owner_id`(多态外键);Repository 查询加 `owner_scope` 参数。
- **关键**:第一版不把 `user_id` 硬编码成无法泛化的东西(如拼接进 URL 路径)。
### 5.6 网页访问 → API/MCP
- `public/` 路由调用 `KnowledgeBasePublicService` 获取数据 → 由 Renderer(HTML/MD/TXT/JSON)格式化输出。
- **扩展**:新增 `/api/v1/public/...` 路由,鉴权用 API Key,**复用同一个 Service**。
- MCP Server 是独立进程,调 Repository 层。
- **关键**:公共数据获取逻辑只在 Service 中写一次,Renderer 各格式只做序列化。
---
## 6. 安全模型
### 6.1 Secret Token
```
生成:token = secrets.token_urlsafe(16) # 22 字符, 128bit 熵
存储:knowledge_bases.token_hash = SHA-256(token).hexdigest()
knowledge_bases.token_encrypted = Fernet(SECRET_KEY 派生).encrypt(token)
knowledge_bases.token_hint = token[-8:]
校验:请求 token → 正则校验 → SHA-256 → 单条索引查询 → enabled/软删检查 → 404 或放行
轮换:regenerate → 新 token,旧 hash 失效,旧缓存失效
```
安全属性:
- 128bit 熵 + 限流 → 暴力枚举不可行
- DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY)
- 统一 404 防存在性探测
- `` 防文档页跳出时 Referer 泄漏 token
### 6.2 IDOR 防护
所有管理端 API 在 Service 层强制校验 `resource.user_id == current_user.id`;不匹配 → 404(不暴露存在性)。
### 6.3 文件上传安全
- 扩展名白名单(`.docx`, `.pdf`)
- MIME 嗅探(filetype 库,前 8KB)
- 随机物理文件名(`secrets.token_urlsafe(8)_safe_original_name`)
- 路径穿越清洗(取 basename,剔除 `..` / `\` / 控制字符)
- 文件大小前置校验(Nginx `client_max_body_size` + 应用层双重保险)
### 6.4 XSS 防护
- AI 公共页:Markdown → HTML 走 Jinja2 `{{ content | safe }}`,但 Markdown 本身是服务端生成(非用户直接输入 HTML),风险可控;用户手改 Markdown 时需 sanitize(bleach 或 markdown 配置 `sanitize=True`)。
- 管理端:Vue SPA,API 返回 JSON,Element Plus 组件默认 escape。
### 6.5 Session 安全
- Cookie: `HttpOnly`, `SameSite=Lax`, 生产环境 `Secure`
- Session token: `secrets.token_urlsafe(32)`, 存 SQLite `sessions` 表(或内存 dict — MVP 足够)
- 过期: 7 天不活动自动清除
---
## 7. 文档处理流程
```
[HTTP 上传](同步返回)
鉴权 → KB 归属 → MIME 嗅探 → 扩展名白名单 → 大小校验
→ 配额原子扣减(UPDATE users SET storage_used = storage_used + :size WHERE id=:uid AND storage_used + :size <= storage_quota)
失败 → STORAGE_QUOTA_EXCEEDED / FILE_TOO_LARGE
→ 流式 SHA256(边读边算,内存峰值 = 一个 chunk)
→ StorageService.save() 保存原始文件(随机文件名)
→ Document(PENDING) 入库
→ DocumentProcessor.process(doc_id) 同步解析:
┌ .pdf → markitdown → 失败/空 → PyMuPDF fallback → 仍空 → SCANNED_PDF_NO_TEXT_LAYER
├ .docx → markitdown → 失败 → python-docx fallback
└ 其他 → Phase 2+ 扩展
→ Markdown 清洗(压空行、规整标题层级、截超长行)
→ 提取 H1 → title(未填时)
→ 抽取前 ~200 字纯文本 → content_summary
→ jieba.analyse.extract_tags top-10 → keywords
→ StorageService.save() 保存 Markdown 文件
→ 更新 FTS 索引
→ Document(READY)
任何异常 → Document(FAILED, error_code),用户只看友好文案
→ 返回 201 + Document 信息
```
> **同步解析的注意事项**:大 PDF 解析可能耗时数秒,会阻塞当前请求。MVP 阶段可接受(单用户低并发),但必须:
> 1. 设置请求级超时(uvicorn `--timeout-keep-alive` 不够,需应用层 middleware 或 nginx `proxy_read_timeout`)
> 2. 前端上传组件显示 loading + 预估时间
> 3. 未来切 Celery 时,上传立即返回 202,前端轮询状态
---
## 8. AI 访问流程
```
用户 → 在 AI 对话中粘贴 https://example.com/k/7fA92xKpQ8m... + 描述需求
AI 抓取器 → GET /k/7fA92xKpQ8m...(无 Cookie/JS)
服务端:限流 → SHA256(token) → 查 KB → 未命中 404
→ 渲染入口页:
· KB 名称/描述
· 文档列表(每条:标题、类型、描述、一句话摘要、关键词、更新时间、完整文档 URL)
· 分页(≤50 条/页)
· 页脚双语机器可读说明
AI → 根据用户描述匹配文档 → GET /k/{token}/doc/{doc_token}.md(优先 .md)
或 .html;需精确定位 → /k/{token}/search?q=
AI → 汇总回答用户
```
**兜底**:若 AI 不跟随链接,用户可直接粘贴文档级 URL。README 必须说明。
---
## 9. Windows 开发流程
```bash
# 后端
cd backend
python -m venv .venv
# Git Bash:
source .venv/Scripts/activate
# 或 CMD:
.venv\Scripts\activate
pip install -r requirements.txt
# 初始化数据库
alembic upgrade head
# 启动
uvicorn app.main:app --reload --port 8000
# 访问
# http://localhost:8000/api/health → 健康检查
# http://localhost:8000/api/docs → Swagger(开发期)
# 前端
cd frontend
npm install
npm run dev
# http://localhost:5173
# Vite proxy: /api → localhost:8000, /k → localhost:8000
```
**关键**:`data/` 目录在首次运行时由 backend 自动创建(`os.makedirs`);`.env` 仅生产需要(开发期 config 用默认值)。
---
## 10. Docker 生产流程
```yaml
# docker-compose.yml(Ubuntu 服务器)
services:
frontend: # multi-stage: node build → nginx:alpine serve dist
backend: # python:3.12-slim → uvicorn
nginx: # 反代 /api + /k → backend:8000;/ → frontend:80
volumes:
- ./data:/app/data # SQLite + 用户文件,宿主机持久化
```
```bash
# 部署
git clone /opt/ai-knowledge-link
cd /opt/ai-knowledge-link
cp .env.example .env # 修改全部密钥
mkdir -p data
docker compose up -d --build
# 更新
git pull
docker compose up -d --build # 数据在 ./data 不受影响
# 备份
sqlite3 data/app.db ".backup data/backup/app_$(date +%Y%m%d).db"
tar czf data/backup/files_$(date +%Y%m%d).tar.gz data/users/
```
---
## 11. 风险清单
| # | 风险 | 等级 | 缓解 |
|---|---|---|---|
| R1 | **同步解析阻塞请求**:大 PDF 解析耗时数秒到十几秒 | 中 | MVP 可接受;前端显示 loading;设请求超时 60s;未来切 Celery |
| R2 | **SQLite 并发写锁**:多用户同时上传时 WAL 模式允许并发读但写仍串行 | 中 | `PRAGMA journal_mode=WAL`;写操作短事务;未来切 PostgreSQL |
| R3 | **PyMuPDF AGPL-3.0**:商品化闭源分发时许可风险 | 中 | MVP 阶段可接受(仅服务端运行,不分发);requirements 中注释标记;商品化前换 pypdfium2 |
| R4 | **SQLite FTS5 中文分词**:FTS5 的 `unicode61` tokenizer 对中文按字分词(非词),召回率低于 jieba | 中 | 第一版仍用 FTS5(比 LIKE 好很多);关键词字段用 jieba 分词写入辅助;Phase 9 验收时若不满意可切换到 jieba 预分词 + LIKE 方案 |
| R5 | **FastAPI 后缀路由被 path param 吞掉** | 低 | 显式后缀路由注册在无后缀路由之前;token 用正则 `^[A-Za-z0-9_-]{22}$` |
| R6 | **内存限流单进程局限** | 低 | MVP 单 uvicorn worker 够用;未来多 worker/容器需 Redis 限流 |
| R7 | **Markdown XSS**:用户手改 Markdown 注入 `