第一步
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
# AI Knowledge Link — 产品需求文档(MVP v1)
|
||||
|
||||
> 状态标注说明:✅ 已完成 | 🔄 部分完成 | ⬜ 未开始(对应 Phase N) | 📌 持续约束(贯穿全程)
|
||||
>
|
||||
> **版本说明**:本文档为当前有效需求(MVP 版)。早前一版需求(PostgreSQL + Redis + Celery + MinIO 全功能栈)已被本 MVP 版**取代**,核心差异:SQLite 替代 PostgreSQL、本地文件系统替代 MinIO、同步解析替代 Celery、内存限流替代 Redis、FTS5 替代 jieba+tsvector。原版中的商业许可审查结论仍有效(PDF 解析商品化前需将 PyMuPDF 替换为 pypdfium2,见 `technical-review.md` R3)。
|
||||
>
|
||||
> 关联文档:[技术审查报告](technical-review.md) | [CLAUDE.md](../CLAUDE.md)
|
||||
|
||||
## Phase 进度总览(对应 §56 开发顺序)
|
||||
|
||||
| Phase | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| 1 | 项目初始化 | ✅ 完成 |
|
||||
| 2 | 数据库和 Alembic | ⬜ |
|
||||
| 3 | 用户注册登录 | ⬜ |
|
||||
| 4 | 知识库 CRUD | ⬜ |
|
||||
| 5 | 本地 StorageService | ✅ 完成(提前实现于 Phase 1) |
|
||||
| 6 | 文档上传 | ⬜ |
|
||||
| 7 | 文档解析和 Markdown | ⬜ |
|
||||
| 8 | 文档管理 | ⬜ |
|
||||
| 9 | Secret URL | ⬜ |
|
||||
| 10 | AI 公共页面 | ⬜ |
|
||||
| 11 | Markdown/TXT/JSON 输出 | ⬜ |
|
||||
| 12 | 搜索 | ⬜ |
|
||||
| 13 | 安全 | ⬜ |
|
||||
| 14 | 前端完善 | ⬜ |
|
||||
| 15 | 测试 | ⬜ |
|
||||
| 16 | Docker | 🔄 compose/Dockerfile 已写(Phase 1),待 Docker 环境验证 |
|
||||
| 17 | Ubuntu 部署文档 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 一、产品核心目标 ⬜(产品定位,随各 Phase 逐步实现)
|
||||
|
||||
这个产品不是普通网盘。
|
||||
|
||||
核心功能是:
|
||||
|
||||
用户上传自己的 Word、PDF 等文档 → 系统自动解析文档 → 转换成 Markdown → 建立个人知识库 → 系统生成一个 AI 专属访问链接 → 用户复制这个链接 → 发送给第三方 AI → AI 访问知识库 → AI 查看知识库中的文档列表 → 根据用户的描述选择一个或多个相关文档 → 访问具体文档内容 → 利用这些资料回答用户问题
|
||||
|
||||
例如:用户上传 公司介绍.docx、产品说明.pdf、技术架构.pdf、项目需求.docx,系统生成 `https://example.com/k/xxxxxxxx`。用户把这个链接发送给 AI:"请访问这个知识库,阅读产品A相关的技术架构和产品说明,然后告诉我产品A有哪些技术风险。" AI 访问 /k/xxxxxxxx,看到文档列表,然后根据用户要求访问相关文档。
|
||||
|
||||
## 二、第一阶段必须保持简单 📌 持续约束(Phase 1 已遵循)
|
||||
|
||||
这是一个 MVP。第一版禁止过度工程化。
|
||||
|
||||
第一版只使用:
|
||||
|
||||
- 前端:Vue 3、TypeScript、Vite、Element Plus、Pinia、Axios
|
||||
- 后端:Python 3.12+、FastAPI、SQLAlchemy、Pydantic、Alembic
|
||||
- 数据库:SQLite
|
||||
- 文件存储:服务器本地文件系统
|
||||
- 文档解析:MarkItDown、PyMuPDF、python-docx
|
||||
- 部署:Docker Compose,可以使用 Nginx
|
||||
|
||||
第一版禁止强制依赖:PostgreSQL、Redis、Celery、MinIO、Qdrant、Elasticsearch、Kafka。不要因为未来可能商品化而提前加入这些组件。
|
||||
|
||||
## 三、开发环境 ✅ 完成(Phase 1 验证通过)
|
||||
|
||||
本地开发环境:Windows。本地没有 Docker。因此 Windows 开发必须可以直接运行。
|
||||
|
||||
- 前端:`npm install` → `npm run dev` ✅
|
||||
- 后端:Python venv → `pip install` → `uvicorn` ✅
|
||||
- 本地开发不要求 Docker ✅
|
||||
- Docker 主要用于 Ubuntu 云服务器
|
||||
|
||||
生产环境:Ubuntu、Docker、Docker Compose。
|
||||
|
||||
## 四、第一版系统架构 ⬜(架构已定,实现随 Phase 推进)
|
||||
|
||||
采用:
|
||||
|
||||
- Windows 开发:Vue 3 + FastAPI + SQLite + 本地文件
|
||||
- 生产:Ubuntu + Docker(frontend / backend / nginx)
|
||||
|
||||
数据目录:
|
||||
|
||||
```
|
||||
/data
|
||||
├── app.db
|
||||
└── users/{user_id}/knowledge_bases/{knowledge_base_id}/
|
||||
├── original/
|
||||
└── markdown/
|
||||
```
|
||||
|
||||
SQLite 保存元数据。文件系统保存:Word、PDF、Markdown。
|
||||
|
||||
## 五、不要把文件塞进 SQLite 📌 持续约束(存储层设计已按此实现)
|
||||
|
||||
禁止把原始 PDF、Word 二进制内容存进 SQLite。SQLite 只保存:用户信息、知识库信息、文档元数据、文件路径、Markdown 路径、文档状态、Token hash、时间、分类、描述、关键词、搜索相关数据。原始文件与 Markdown 均存服务器文件系统。
|
||||
|
||||
## 六、数据库访问必须抽象 ⬜ Phase 2(Phase 1 已建 db.py 引擎层)
|
||||
|
||||
不能在业务代码中到处直接调用 sqlite3。必须使用 SQLAlchemy,并通过 Repository / Service 分层(UserRepository、KnowledgeBaseRepository、DocumentRepository)。业务逻辑不能依赖 SQLite 具体实现。数据库配置通过 `DATABASE_URL`(如 `sqlite:///./data/app.db`),未来可以切换 PostgreSQL 而尽量不修改业务层。
|
||||
|
||||
## 七、未来扩展接口 1:SQLite → PostgreSQL 📌 持续约束(设计规则已在 db.py 落地)
|
||||
|
||||
第一版使用 SQLite,但设计 DatabaseRepository、SQLAlchemy models、Alembic migrations。不要使用 SQLite 特有 SQL 作为核心业务逻辑;避免 PRAGMA(WAL 设置除外)、SQLite 专属函数、SQLite 专属数据类型。业务代码尽可能使用标准 SQLAlchemy。未来切换只需更换连接配置并处理迁移。
|
||||
|
||||
## 八、未来扩展接口 2:本地文件 → MinIO/S3 ✅ 完成(Phase 1 提前实现)
|
||||
|
||||
已实现 `StorageService` 协议(`storage/base.py`:save/read/delete/exists/get_size)与 `LocalStorageService`(`storage/local_storage.py`)。业务代码不能直接 `open("/data/xxx")`,所有文件读写必须经过 StorageService。未来可增加 MinioStorageService / S3StorageService。
|
||||
|
||||
## 九、未来扩展接口 3:同步解析 → 异步任务 ⬜ Phase 7(DocumentProcessor 协议)
|
||||
|
||||
第一版:上传文件 → FastAPI → 同步解析 → 保存 Markdown → 返回结果。必须抽象 `DocumentProcessor`(如 `process_document()`):第一版 `LocalDocumentProcessor`,未来可增加 `CeleryDocumentProcessor`。业务代码不要把文档解析逻辑写进 API endpoint。
|
||||
|
||||
## 十、未来扩展接口 4:关键词搜索 → RAG ⬜ Phase 12(Retriever 协议)
|
||||
|
||||
第一版不使用向量数据库。实现 `Retriever` 统一接口(`search(query, knowledge_base_id)`),第一版 `KeywordRetriever`(SQLite FTS 或简单关键词搜索)。未来可加 VectorRetriever / HybridRetriever / QdrantRetriever、Embedding、Reranker。第一版不安装这些依赖。
|
||||
|
||||
## 十一、未来扩展接口 5:个人用户 → 团队/企业 📌 持续约束(数据库设计时遵守)
|
||||
|
||||
第一版:一个用户拥有多个知识库(User → KnowledgeBase → Documents)。但数据库设计必须避免以后无法扩展。未来:Organization → Members → KnowledgeBases → Documents。第一版不实现团队,但不要把 user_id 硬编码成未来无法扩展的权限结构。
|
||||
|
||||
## 十二、未来扩展接口 6:AI 网页访问 → API/MCP 📌 持续约束(Phase 10 落地)
|
||||
|
||||
第一版:AI 通过 Secret URL 访问(GET /k/{token}),这是产品核心功能。未来可增加 API Key、REST API、MCP Server、OpenAPI、AI Agent 接口。第一版不实现 MCP,但公共知识库服务层必须独立:HTML、Markdown、TXT、JSON 都调用同一个 `KnowledgeBasePublicService`(Controller → Service → Repository → Database),不要每种格式各写一套查询逻辑。
|
||||
|
||||
## 十三、用户系统 ⬜ Phase 3
|
||||
|
||||
实现注册、登录、退出登录。
|
||||
|
||||
User:id、username、email、password_hash、status、storage_quota、storage_used、created_at、updated_at。密码必须安全哈希,优先 Argon2id;禁止明文密码、MD5、SHA1。
|
||||
|
||||
## 十四、存储限制 🔄 部分完成(config.py 已有配额配置;Plan 模型 Phase 2 落地)
|
||||
|
||||
默认免费用户 100MB,单文件 20MB。不要硬编码,放到配置或 Plan 模型。第一版 Free Plan(100MB / 20MB-per-file),未来 Basic 1GB、Pro 5GB、Enterprise 自定义。第一版不实现支付。
|
||||
|
||||
## 十五、知识库 ⬜ Phase 4
|
||||
|
||||
KnowledgeBase:id、user_id、name、description、enabled、secret_token_hash、created_at、updated_at。一个用户可以创建多个知识库(公司知识库、项目A、项目B、个人AI资料)。
|
||||
|
||||
## 十六、AI 专属链接 ⬜ Phase 9
|
||||
|
||||
每个知识库生成一个 Secret Token(`https://example.com/k/7fA92xKpQ8m...`)。必须使用密码学安全随机数(`secrets.token_urlsafe()` 或同等方案);禁止自增 ID、时间戳、用户名、UUID 短截断。数据库保存 token_hash 而不是明文 Token。用户复制的完整 URL 只在生成时提供给用户。
|
||||
|
||||
## 十七、AI 链接安全模型 ⬜ Phase 9/13
|
||||
|
||||
公共 AI 访问不需要登录(第三方 AI 无法使用用户后台登录状态),Secret URL 本身就是访问凭证,相当于 Bearer Credential——任何获得 URL 的人理论上都可以读取知识库,必须在产品中明确这一点。提供启用/禁用链接、重新生成链接;重新生成后旧链接立即失效。
|
||||
|
||||
## 十八、AI 知识库首页 ⬜ Phase 10
|
||||
|
||||
`GET /k/{token}` 必须返回服务端生成的 HTML,不能依赖 Vue / JavaScript(第三方 AI 可能不执行 JS),页面必须首屏直接包含核心信息。页面结构:title=知识库名称、`<meta name="robots" content="noindex,nofollow,noarchive">`、知识库名称、知识库描述、Documents 列表(每篇含标题、描述、类型、更新时间、URL)。
|
||||
|
||||
## 十九、多格式输出 ⬜ Phase 11
|
||||
|
||||
- `GET /k/{token}` → HTML
|
||||
- `GET /k/{token}.md` → Markdown
|
||||
- `GET /k/{token}.txt` → 纯文本
|
||||
- `GET /k/{token}.json` → JSON
|
||||
|
||||
JSON 只能返回必要的信息,禁止:用户密码、token hash、内部数据库 ID、服务器路径、MinIO 地址、内部配置。
|
||||
|
||||
## 二十、单文档访问 ⬜ Phase 10/11
|
||||
|
||||
- `GET /k/{token}/doc/{document_token}` → HTML
|
||||
- `GET /k/{token}/doc/{document_token}.md` → Markdown
|
||||
- `GET /k/{token}/doc/{document_token}.txt` → TXT
|
||||
|
||||
文档页面:标题、描述、文件类型、关键词、更新时间、Markdown 正文。默认不直接提供原始文件下载。
|
||||
|
||||
## 二十一、文档 Token ⬜ Phase 9
|
||||
|
||||
不要使用 /doc/1 这种容易猜测的 URL。每个文档生成随机 document_token,数据库保存 hash。例如 `/k/knowledge-token/doc/document-token`,两个 Token 都必须随机。
|
||||
|
||||
## 二十二、文档上传 ⬜ Phase 6
|
||||
|
||||
支持 .docx、.pdf;暂不支持 .doc、.xls、.xlsx、.ppt、.pptx,但代码架构预留扩展。
|
||||
|
||||
上传流程:POST /api/documents/upload → 检查登录 → 检查知识库权限 → 检查文件大小 → 检查扩展名 → 检查 MIME → 计算 SHA256 → 检查存储额度 → 保存原始文件 → 解析 → 转换 Markdown → 提取标题 → 生成摘要 → 提取关键词 → 保存 Markdown → 更新 Document → 返回结果。
|
||||
|
||||
## 二十三、文件目录 📌 持续约束(key 生成已实现于 storage/object_keys.py)
|
||||
|
||||
推荐布局:
|
||||
|
||||
```
|
||||
data/
|
||||
├── app.db
|
||||
└── users/{user_id}/knowledge_bases/{knowledge_base_id}/
|
||||
├── original/ (random-name.docx / random-name.pdf)
|
||||
└── markdown/ (random-name.md)
|
||||
```
|
||||
|
||||
不要使用用户原始文件名直接作为物理存储路径(如 `../../evil.pdf` 必须安全处理)。
|
||||
|
||||
## 二十四、文档解析 ⬜ Phase 7
|
||||
|
||||
优先 MarkItDown;PDF 用 PyMuPDF 作为辅助/fallback;Word 用 python-docx 作为 fallback。Markdown 需尽可能保留:标题、段落、列表、表格、粗体、斜体、代码块、引用。解析失败时 Document.status = FAILED,不向用户显示 Traceback / 服务器路径 / Python 异常堆栈,只显示友好错误("文档解析失败,请检查文件是否损坏或格式是否受支持。"),服务器日志记录详细错误。
|
||||
|
||||
## 二十五、扫描 PDF ⬜ Phase 7
|
||||
|
||||
第一版不强制 OCR。如果 PDF 没有文本层,提示"该 PDF 可能是扫描件,当前版本暂不支持 OCR。"未来预留 OCRProcessor(PaddleOCR 等),第一版不加入 OCR。
|
||||
|
||||
## 二十六、文档状态 ⬜ Phase 2(状态模型)/ Phase 7(流转)
|
||||
|
||||
PENDING、PROCESSING、READY、FAILED、DELETED。虽然第一版同步处理,但状态模型必须保留,未来异步任务可以直接使用。
|
||||
|
||||
## 二十七、知识库搜索 ⬜ Phase 12
|
||||
|
||||
`GET /k/{token}/search?q=关键词` 返回:文档、标题、匹配摘要、URL。第一版用 SQLite FTS 或简单关键词搜索,实现 Retriever 接口(第一版 KeywordRetriever)。不引入 Qdrant、Embedding。
|
||||
|
||||
## 二十八、AI 选择多个文档 ⬜ Phase 10(产品最重要功能之一)
|
||||
|
||||
知识库首页不能把所有文档正文全部输出。首页应提供:文档标题、文档描述、关键词、文档类型、文档 URL。例如"产品A技术架构(关键词:产品A、架构、服务器、数据库、Redis)→ /k/xxxx/doc/yyyy"。这样 AI 可以根据用户描述选择文档——用户说"请阅读产品A的架构和数据库设计",AI 应能发现并访问"产品A技术架构"和"产品A数据库设计"两个文档。
|
||||
|
||||
## 二十九、AI 兼容性 ⬜ Phase 10/11(README 声明 Phase 17)
|
||||
|
||||
不能假设第三方 AI 一定支持 JavaScript、Cookie、登录、API 调用、自动跟随所有链接、搜索接口。AI 页面必须:服务端渲染、HTML 标准、正文直接返回、链接使用标准 `<a href>`、不依赖 JS、不使用复杂 SPA、内容结构清晰。同时提供 HTML / Markdown / TXT / JSON,尽可能提高 DeepSeek、豆包、Kimi、ChatGPT 及其他支持网页访问的 AI 的兼容性。README 必须明确:第三方 AI 是否支持访问外部 URL、跟随链接和读取页面,由第三方 AI 自身能力决定,本系统不能保证所有 AI 都一定会访问后续文档。
|
||||
|
||||
## 三十、前端 🔄 骨架完成(Phase 1);完整 UI ⬜ Phase 14
|
||||
|
||||
Vue 3、TypeScript、Vite、Element Plus、Pinia。UI 要求:现代、简洁、SaaS 风格、中文、不要花哨、不要大量动画。主要页面:/login、/register、/dashboard、/knowledge-bases、/knowledge-bases/:id、/settings。
|
||||
|
||||
## 三十一、登录页面 🔄 占位页已有(Phase 1);功能 ⬜ Phase 3/14
|
||||
|
||||
登录:用户名/邮箱、密码、登录。注册:用户名、邮箱、密码、确认密码。基本校验。
|
||||
|
||||
## 三十二、Dashboard 🔄 占位页已有(Phase 1);功能 ⬜ Phase 14
|
||||
|
||||
显示:知识库数量、文档数量、已使用空间/总空间(如 32MB / 100MB)、最近知识库、最近上传、解析失败文件。
|
||||
|
||||
## 三十三、知识库管理 ⬜ Phase 4(接口)/ Phase 14(UI)
|
||||
|
||||
显示:知识库名称、描述、文档数量、空间、创建时间、AI 链接。操作:管理、复制 AI 链接、预览、启用/禁用、重新生成链接、删除。
|
||||
|
||||
## 三十四、文档管理 ⬜ Phase 6/8(接口)/ Phase 14(UI)
|
||||
|
||||
支持拖拽上传、多文件上传。显示:文件名、类型、大小、状态、更新时间。操作:预览、重新解析、删除。
|
||||
|
||||
## 三十五、文档编辑 ⬜ Phase 8
|
||||
|
||||
用户可以修改:标题、描述、关键词、分类。Markdown 正文默认由系统生成。第一版可提供 Markdown 预览;是否允许直接编辑 Markdown 不是最高优先级,时间有限可暂时只读。
|
||||
|
||||
## 三十六、权限 ⬜ Phase 13(编码时全程遵守)
|
||||
|
||||
必须防止 IDOR:用户A访问 /api/knowledge-bases/10,不能通过修改 10 → 11 读取用户B知识库。所有后台接口必须验证:当前登录用户 → 资源 owner → 允许操作。不能只验证资源 ID 存在。
|
||||
|
||||
## 三十七、公共 AI 链接权限 ⬜ Phase 9
|
||||
|
||||
公共 AI URL 不要求登录,但必须验证 Secret Token。enabled = false 返回 404。重新生成 Token 后旧 Token 立即失效。
|
||||
|
||||
## 三十八、安全 ⬜ Phase 13(编码时全程遵守)
|
||||
|
||||
考虑:SQL 注入、XSS、CSRF、路径穿越、恶意文件、超大文件、Zip Bomb、暴力破解、Token 猜测、IDOR、资源耗尽。上传文件:限制大小、限制扩展名、限制 MIME、随机物理文件名。Markdown 输出必须防存储型 XSS;用户输入的标题、描述、关键词不能直接插入 HTML;Markdown 渲染需要安全处理。
|
||||
|
||||
## 三十九、限流 ✅ RateLimiter 抽象完成(Phase 1);接入公共 URL ⬜ Phase 13
|
||||
|
||||
第一版不需要 Redis,可以使用内存限流(如单 IP 每分钟一定次数),保护公共 AI URL。注意内存限流只适合单实例 MVP,代码中抽象 RateLimiter,未来可实现 RedisRateLimiter。(已实现 `core/rate_limit.py`:内存 TokenBucket + 配置化阈值。)
|
||||
|
||||
## 四十、访问日志 ⬜ Phase 2(模型)/ Phase 10(记录)
|
||||
|
||||
第一版可简单记录:knowledge_base_id、document_id、timestamp、user_agent、请求类型。不要默认长期保存完整 IP。后台可显示访问次数、最近访问时间。不要声称可以准确判断访问者是不是 AI,使用"外部访问"而不是"AI 访问"。
|
||||
|
||||
## 四十一、数据库模型 ⬜ Phase 2
|
||||
|
||||
至少:User、Plan、KnowledgeBase、Document、DocumentCategory、AccessLog。
|
||||
|
||||
Document:id、knowledge_base_id、user_id、document_token_hash、original_filename、storage_path、markdown_path、file_size、mime_type、sha256、title、description、keywords、category_id、status、created_at、updated_at。
|
||||
|
||||
KnowledgeBase:id、user_id、name、description、secret_token_hash、enabled、created_at、updated_at。
|
||||
|
||||
## 四十二、数据库迁移 ⬜ Phase 2
|
||||
|
||||
使用 Alembic,即使 SQLite 也必须使用迁移。不要手工修改生产数据库。README 提供:初始化迁移、升级迁移、回滚迁移。
|
||||
|
||||
## 四十三、API 设计 🔄 分 Phase 实现
|
||||
|
||||
认证:POST /api/auth/register、POST /api/auth/login、POST /api/auth/logout、GET /api/me
|
||||
知识库:GET/POST /api/knowledge-bases、GET/PUT/DELETE /api/knowledge-bases/{id}
|
||||
知识库链接:POST /api/knowledge-bases/{id}/regenerate-token、/enable、/disable
|
||||
文档:GET /api/knowledge-bases/{id}/documents、POST /api/knowledge-bases/{id}/documents、GET/PUT/DELETE /api/documents/{id}、POST /api/documents/{id}/reprocess
|
||||
存储:GET /api/storage
|
||||
公共:GET /k/{token}(+.md/.txt/.json)、GET /k/{token}/search?q=、GET /k/{token}/doc/{document_token}(+.md/.txt)
|
||||
|
||||
## 四十四、统一 Service 层 🔄 分 Phase 实现(Phase 1 已落 StorageService)
|
||||
|
||||
不要把业务逻辑全部写进 FastAPI 路由。至少:AuthService、UserService、KnowledgeBaseService、DocumentService、DocumentProcessor、StorageService、KnowledgeBasePublicService、Retriever、RateLimiter。
|
||||
|
||||
## 四十五、公共 AI 访问统一 Service ⬜ Phase 10
|
||||
|
||||
HTML / Markdown / TXT / JSON 各 Renderer 都必须:KnowledgeBasePublicService → 返回知识库数据 → 各格式 Renderer。所有格式必须使用同一套数据来源。
|
||||
|
||||
## 四十六、Docker 🔄 compose/Dockerfile 已写(Phase 1);验证 ⬜ Phase 16
|
||||
|
||||
虽然 Windows 开发不需要 Docker,但项目必须提供生产 Docker 配置。docker-compose.yml:frontend、backend、nginx。SQLite 和文件:宿主机 ./data 挂载到 /app/data。重要:数据库和用户文件不能写进 Docker 镜像;删除容器以后数据仍然存在。
|
||||
|
||||
## 四十七、生产目录 ⬜ Phase 17
|
||||
|
||||
服务器 /opt/ai-knowledge-link/:docker-compose.yml、.env、data/(app.db + users/)、nginx/、project/(或合理目录)。data 必须独立于代码。
|
||||
|
||||
## 四十八、备份 ⬜ Phase 17
|
||||
|
||||
第一版使用 SQLite,必须提供备份脚本:app.db + users/。建议每日备份,保留最近 7 天、最近 4 周。备份不能影响正在运行的服务。README 说明如何备份、如何恢复。
|
||||
|
||||
## 四十九、Git ✅ 完成(Phase 1)
|
||||
|
||||
已提供 .gitignore(禁止提交:.env、data/、*.db、上传文件、虚拟环境、node_modules)与 .env.example。
|
||||
|
||||
## 五十、Windows 开发 ✅ 完成(Phase 1,README 已含完整步骤;实测通过)
|
||||
|
||||
README 必须详细说明:安装 Python、创建 venv(`python -m venv .venv`、Windows 激活 `.venv\Scripts\activate`)、安装依赖(`pip install -r requirements.txt`)、启动 FastAPI(`uvicorn app.main:app --reload`);安装 Node、`npm install`、`npm run dev`。
|
||||
|
||||
## 五十一、生产部署 ⬜ Phase 17
|
||||
|
||||
README 必须详细说明:Ubuntu 安装 Docker、克隆 Git 仓库、配置 .env、创建 data 目录、启动(`docker compose up -d --build`)、查看(`docker compose ps`)、日志(`docker compose logs -f`)、更新(`git pull && docker compose up -d --build`)。数据不能因为更新丢失。
|
||||
|
||||
## 五十二、测试 🔄 冒烟测试 3 条通过(Phase 1);完整覆盖 ⬜ Phase 15
|
||||
|
||||
必须编写测试,至少覆盖:注册、登录、错误密码、创建知识库、删除知识库、用户隔离、上传 DOCX、上传 PDF、文件大小限制、存储空间限制、文档解析、Markdown 生成、Token 生成、Token 禁用、Token 重新生成、旧 Token 失效、公共知识库 HTML、公共 Markdown、公共 TXT、公共 JSON、单文档访问、文档 Token、搜索、XSS 防护、路径穿越、IDOR。
|
||||
|
||||
## 五十三、项目目录 ✅ 完成(Phase 1)
|
||||
|
||||
```
|
||||
project/
|
||||
├── backend/(app/{api,core,models,schemas,repositories,services,processors,storage,retrieval,public}、alembic/、tests/、requirements.txt、Dockerfile)
|
||||
├── frontend/(src/{api,components,layouts,views,stores,router,types}、package.json、Dockerfile)
|
||||
├── nginx/nginx.conf
|
||||
├── data/.gitkeep
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 五十四、代码原则 📌 持续约束(Phase 1 已遵循)
|
||||
|
||||
不要:一个 main.py 写完所有东西;所有数据库查询写在路由;业务代码直接 open 文件;硬编码路径;硬编码 100MB;硬编码 Secret Token;把密码明文保存;把用户 A/B 数据混在一起;使用 Vue 渲染 AI 公共页面。
|
||||
|
||||
## 五十五、产品第一阶段验收标准 ⬜ 最终验收(Phase 15 后)
|
||||
|
||||
1. 用户注册。2. 用户登录。3. 创建"公司知识库"。4. 上传 公司介绍.docx、产品说明.pdf、技术架构.pdf。5. 系统自动转换 Markdown。6. 后台显示 3 个文档。7. 用户复制 AI 专属链接。8. 退出登录。9. 未登录状态打开链接仍能访问知识库。10. 页面显示知识库名称、描述、文档列表、文档描述、文档 URL。11. 访问其中一个文档能看到 Markdown 正文。12. 访问 .md 得到 Markdown。13. 访问 .txt 得到纯文本。14. 访问 .json 得到机器可读数据。15. /search?q=产品A 能找到相关文档。16. 重新生成 AI 链接后旧链接立即失效。17. 禁用 AI 链接后公共访问失败。18. 用户A无法访问用户B后台数据。19. 用户A无法通过修改 ID 读取用户B资源。20. Docker 部署后删除容器并重新创建,用户数据不能丢失。
|
||||
|
||||
## 五十六、第一阶段开发顺序 ✅ Phase 1 完成,按序推进中
|
||||
|
||||
Phase 1 项目初始化 → Phase 2 数据库和 Alembic → Phase 3 用户注册登录 → Phase 4 知识库 CRUD → Phase 5 本地 StorageService → Phase 6 文档上传 → Phase 7 文档解析和 Markdown → Phase 8 文档管理 → Phase 9 Secret URL → Phase 10 AI 公共页面 → Phase 11 Markdown/TXT/JSON 输出 → Phase 12 搜索 → Phase 13 安全 → Phase 14 前端完善 → Phase 15 测试 → Phase 16 Docker → Phase 17 Ubuntu 部署文档。
|
||||
|
||||
每完成一个 Phase:1. 检查代码 2. 运行测试 3. 修复错误 4. 确认没有破坏之前功能 5. 再进入下一阶段。
|
||||
|
||||
## 五十七、当前任务 ✅ 完成(技术审查报告见 technical-review.md)
|
||||
|
||||
第一步不要立即创建大量业务代码,先对整个需求进行技术审查,输出:最终架构、数据库 ER 结构、API 结构、项目目录、六个未来扩展接口如何设计、安全模型、文档处理流程、AI 访问流程、Windows 开发流程、Docker 生产流程、发现的风险、建议修改的地方。确认后从 Phase 1 开始,不要跳过架构审查。
|
||||
@@ -0,0 +1,536 @@
|
||||
# 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。
|
||||
Reference in New Issue
Block a user