537 lines
24 KiB
Markdown
537 lines
24 KiB
Markdown
# 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. `<meta name="robots" content="noindex,nofollow,noarchive">`
|
||
3. `<meta name="referrer" content="no-referrer">`
|
||
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 防存在性探测
|
||
- `<meta name="referrer" content="no-referrer">` 防文档页跳出时 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 <repo> /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 注入 `<script>` | 中 | Jinja2 渲染 AI 页时 sanitize(bleach 或 Markdown 库 safe 模式);管理端 Vue 默认 escape |
|
||
| R8 | **配额并发超额**:SQLite 写锁串行化已天然防竞态,但应用层"先查后写"仍有窗口 | 低 | 原子 SQL `UPDATE ... WHERE storage_used + :n <= quota` |
|
||
|
||
---
|
||
|
||
## 12. 建议修改 / 补充
|
||
|
||
1. **【建议】Token 加密存储**(Fernet)以满足"后台显示完整链接"。需求 §16 说"只在生成时提供",但 §33 需要"复制 AI 链接"按钮 → 后台需能取回原文。两处表述矛盾,Fernet 方案同时满足。
|
||
2. **【建议】入口页每篇文档附一句话摘要**:显著提高 AI 不点进去也能判断相关性的概率;代价是页面变大,用 50 条/页分页对冲。
|
||
3. **【建议】公共页加 `<meta name="referrer" content="no-referrer">`**:需求未提,防文档页跳出时 Referer 泄漏 token。
|
||
4. **【建议】文档 Token 也用 Fernet 加密存储**:与 KB Token 同方案,便于构建文档 URL。
|
||
5. **【建议】Session 用内存 dict + TTL 而非 SQLite 表**:MVP 更简单,重启丢失可接受;未来切 Redis 时无缝。
|
||
6. **【明确化】需求 §24 指定 PyMuPDF**:MVP 阶段接受,但必须在代码中标记 `# TODO: 商品化前替换为 pypdfium2(AGPL 风险)`。
|
||
7. **【明确化】配额扣减必须用原子 SQL**,不能"先查 storage_used 再写"(即使 SQLite 串行写也尽量用原子语句,养成习惯以备 PostgreSQL 切换)。
|
||
|
||
---
|
||
|
||
## 13. 开发阶段确认
|
||
|
||
按需求 §56,共 17 个 Phase。技术审查通过后从 Phase 1(项目初始化)开始,每 Phase 完成必须验证后再进入下一 Phase。
|