# 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 注入 `