fix: CLI 路径检查改用 is_safe_cli_path(允许绝对路径),更新文档
This commit is contained in:
@@ -4,14 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 项目概述
|
||||
|
||||
Markdown 文档向量数据库 — 将 .md 文件分块 → 嵌入 → 存入 ChromaDB,通过 FastAPI HTTP 或 CLI 提供语义检索。
|
||||
文档向量数据库 — 将 .md/.txt/.pdf/.html/.epub 文件分块 → 嵌入 → 存入 ChromaDB,通过 FastAPI HTTP 或 CLI 提供语义检索。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
```Shell
|
||||
# --- 安装与测试 ---
|
||||
uv sync # 安装依赖(lockfile 已锁定 CUDA torch)
|
||||
uv run pytest tests/ -v # 全部测试 (115+ 个)
|
||||
uv run pytest tests/ -v # 全部测试 (120 个)
|
||||
uv run pytest tests/test_api.py -v # 单个测试模块
|
||||
uv run pytest tests/ -v -k "test_search" # 按名称过滤
|
||||
|
||||
@@ -36,6 +36,7 @@ uv run python scripts/ingest_obsidian.py
|
||||
│ ├── db.py # VectorDB: 线程安全的 ChromaDB 封装
|
||||
│ ├── embedder.py # 策略模式: LocalEmbedder / OpenAIEmbedder / DashscopeEmbedder
|
||||
│ ├── ingest.py # DocumentIngestor: 按扩展名自动选择 Splitter
|
||||
│ ├── security.py # 路径遍历防护 (is_safe_path + is_path_within_workspace)
|
||||
│ ├── search.py # Searcher: 语义检索 + 源文件管理
|
||||
│ └── splitters/ # 文档分块器包
|
||||
│ ├── base.py # Splitter(Protocol) + BaseTextSplitter(ABC)
|
||||
@@ -43,6 +44,7 @@ uv run python scripts/ingest_obsidian.py
|
||||
│ ├── text.py # TextSplitter: 纯文本段落切分
|
||||
│ ├── pdf.py # PDFSplitter: pymupdf 提取文字
|
||||
│ ├── html.py # HTMLSplitter: bs4 去标签
|
||||
│ ├── epub.py # EPUBSplitter: ebooklib 提取章节
|
||||
│ └── registry.py # 扩展名 → Splitter 自动选择
|
||||
|
||||
server/ # FastAPI HTTP 层
|
||||
@@ -55,7 +57,7 @@ cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
|
||||
**数据流**: 文件 → `get_splitter(path)` 自动选择 → `Splitter.split()` → `batch_embed()` → `ChromaDB collection.add()` → `Searcher.search()`
|
||||
|
||||
**支持的格式**: `.md` / `.txt` / `.pdf` / `.html` — 安装可选依赖: `uv sync --extra all`
|
||||
**支持的格式**: `.md` / `.txt` / `.pdf` / `.html` / `.epub` — 安装可选依赖: `uv sync --extra all`
|
||||
|
||||
**依赖方向**: `config` ← `db` ← `embedder` ← `ingest`/`search` ← `server`/`cli`
|
||||
|
||||
@@ -90,7 +92,7 @@ cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
|
||||
- **API Key**: 环境变量 `MD_VECTOR_API_KEY` → `verify_api_key` 依赖注入到 ingest/search/delete 端点;未设置则跳过
|
||||
- **速率限制**: `RateLimiter` 中间件,默认 60s 窗口内最多 30 请求
|
||||
- **路径遍历防护**: `_is_safe_path()` 拒绝绝对路径和 `..` 穿越
|
||||
- **路径遍历防护**: `is_path_within_workspace()` 拒绝绝对路径、`..` 穿越和目录外访问
|
||||
- **错误信息**: 500 返回通用消息,详细错误记入 `logger.exception`
|
||||
|
||||
### 配置系统 (config.py)
|
||||
@@ -119,7 +121,7 @@ cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
## 已入库知识库
|
||||
|
||||
| 集合名 | 来源 | 文件数 | chunks | 说明 |
|
||||
|--------|------|--------|--------|------|
|
||||
| ----------------- | -------------------------------------------------------------------------------- | ------ | ------ | ------------------------------ |
|
||||
| `novel_taohou` | `D:\Code\doing_exercises\exercise\Novel\我有太后罩着,你们有什么\原有章节剧情` | 220 | 670 | 小说章节(GPU bge-small-v1.5) |
|
||||
| `obsidian_blog` | `D:\Code\Obsidian` | 51 | 3,611 | 博客笔记(GPU bge-small-v1.5) |
|
||||
| `default` | 测试文件 | 2 | ~30 | test-guide.md + stdin-doc.md |
|
||||
@@ -127,6 +129,7 @@ cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
搜索时务必用 `-C` 指定集合,否则只会搜到 default 中的测试数据。
|
||||
|
||||
**小说搜索示例**:
|
||||
|
||||
```bash
|
||||
uv run md-vector-db search "张莽和孙太后的关系" -k 3 -C novel_taohou
|
||||
uv run md-vector-db search "抄家事件" -k 5 -C novel_taohou
|
||||
@@ -149,6 +152,7 @@ index-strategy = "unsafe-best-match" # 允许跨源查找
|
||||
**不要删除 `[tool.uv]` 配置**,否则 `uv sync` 会重新解析为 CPU 版 torch。
|
||||
|
||||
### MarkdownSplitter 边界情况
|
||||
|
||||
`_split_single_paragraph` 中,当段落分隔符(。!?等)距 chunk 起点 < overlap(100) 时,
|
||||
`start` 会回退为负数,Python `str.rfind` 的负索引会绕回文本末尾,造成死循环。
|
||||
此 bug 已被修复(`start = max(start + 1, next_start)`),但给超长段落测试时需留意类似问题。
|
||||
|
||||
@@ -4,14 +4,14 @@ Markdown 文档向量数据库 — 将 Markdown 文件自动分块、嵌入、
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **文档入库**: 支持单文件、目录批量导入 Markdown 文档,自动按标题+段落智能分块
|
||||
- **文档入库**: 支持单文件、目录批量导入多种格式文档,自动按标题+段落智能分块
|
||||
- **语义检索**: 自然语言查询,返回最相关的文档片段及来源定位(文件名、章节标题)
|
||||
- **多 Provider**: 本地模型 + 云端 API(OpenAI、阿里云 DashScope、硅基流动等 OpenAI 兼容服务)
|
||||
- **GPU 加速**: 本地模型自动检测 CUDA(RTX 4060 实测:729 chunks 嵌入仅 1.8s)
|
||||
- **多集合**: 支持多项目数据隔离,不同知识库存入不同 ChromaDB collection
|
||||
- **HTTP API**: FastAPI 提供 RESTful 接口,附带 Swagger 文档
|
||||
- **安全**: 可选 API Key 认证、速率限制、路径遍历防护
|
||||
- **多格式文档**: 支持 `.md` / `.txt` / `.pdf` / `.html`,按扩展名自动选择分块器,可通过 `Splitter` Protocol 扩展
|
||||
- **多格式文档**: 支持 `.md` / `.txt` / `.pdf` / `.html` / `.epub`,按扩展名自动选择分块器,可通过 `Splitter` Protocol 扩展
|
||||
- **去重**: 同一文件重复入库自动覆盖旧版本(基于路径 SHA256 哈希)
|
||||
|
||||
## 快速开始
|
||||
@@ -52,7 +52,7 @@ embed:
|
||||
# 单文件
|
||||
uv run md-vector-db ingest docs/intro.md
|
||||
|
||||
# 批量导入目录(递归扫描所有 .md 文件)
|
||||
# 批量导入目录(递归扫描所有支持的文档格式)
|
||||
uv run md-vector-db ingest-dir ./md_docs/
|
||||
|
||||
# 指定集合(多项目数据隔离)
|
||||
@@ -151,8 +151,8 @@ for r in resp.json()["results"]:
|
||||
|
||||
| 命令 | 说明 |
|
||||
| --------------------------- | -------------------------------------------------------- |
|
||||
| `ingest <文件路径>` | 入库单个 .md 文件,支持`-C` 指定集合 |
|
||||
| `ingest-dir <目录路径>` | 递归入库目录下所有 .md 文件 |
|
||||
| `ingest <文件路径>` | 入库单个文件(自动识别格式),支持`-C` 指定集合 |
|
||||
| `ingest-dir <目录路径>` | 递归入库目录下所有支持的文档格式 |
|
||||
| `search <查询> -k <数量>` | 语义检索,`-k` 默认 10、最大 100,`--json` JSON 输出 |
|
||||
| `stats` | 显示 chunks 总数、源文件列表 |
|
||||
| `serve -p <端口>` | 启动 HTTP 服务(默认 8000) |
|
||||
@@ -224,6 +224,7 @@ md-vector-db/
|
||||
│ │ ├── db.py # ChromaDB 封装(线程安全)
|
||||
│ │ ├── embedder.py # 嵌入器(Local/OpenAI/Dashscope)
|
||||
│ │ ├── ingest.py # 混合分块 + 入库
|
||||
│ │ ├── security.py # 路径遍历防护
|
||||
│ │ ├── search.py # 语义检索
|
||||
│ │ └── splitters/ # 文档分块器包
|
||||
│ │ ├── base.py # Splitter(Protocol) + BaseTextSplitter(ABC)
|
||||
@@ -244,13 +245,13 @@ md-vector-db/
|
||||
├── scripts/
|
||||
│ ├── serve.py # 快速启动脚本
|
||||
│ └── ingest_obsidian.py # 批量入库 Obsidian 知识库
|
||||
└── tests/ # 测试(115+ 个)
|
||||
└── tests/ # 测试(120 个)
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
uv run pytest tests/ -v # 全部测试 (115+ 个)
|
||||
uv run pytest tests/ -v # 全部测试 (120 个)
|
||||
uv run pytest tests/test_embedder.py -v # 嵌入器测试
|
||||
uv run pytest tests/ -v -k "search" # 按名称过滤
|
||||
uv run pytest tests/ -v --cov=src --cov-report=term-missing # 覆盖率
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ if sys.stdout.encoding != "utf-8":
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.core.config import DEFAULT_CONFIG_PATH
|
||||
from src.core.security import is_path_within_workspace
|
||||
from src.core.security import is_safe_cli_path
|
||||
from src.server.deps import get_state, get_default_collection
|
||||
|
||||
app = typer.Typer(
|
||||
@@ -82,7 +82,7 @@ def ingest(
|
||||
if file_paths:
|
||||
total = 0
|
||||
for fp in file_paths:
|
||||
if not is_path_within_workspace(fp):
|
||||
if not is_safe_cli_path(fp):
|
||||
typer.echo(f"[SKIP] 不安全的路径: {fp}", err=True)
|
||||
continue
|
||||
# 支持通配符 (shell 展开或 Python glob)
|
||||
@@ -90,7 +90,7 @@ def ingest(
|
||||
if "*" in fp or "?" in fp:
|
||||
matches = _glob.glob(fp, recursive=True)
|
||||
for m in matches:
|
||||
if not is_path_within_workspace(m):
|
||||
if not is_safe_cli_path(m):
|
||||
typer.echo(f"[SKIP] 不安全的路径: {m}", err=True)
|
||||
continue
|
||||
c = ingestor.ingest_file(m)
|
||||
@@ -117,7 +117,7 @@ def ingest_dir(
|
||||
collection: CollectionOpt = None,
|
||||
):
|
||||
_init_config(config)
|
||||
if not is_path_within_workspace(dir_path):
|
||||
if not is_safe_cli_path(dir_path):
|
||||
typer.echo(f"错误: 不安全的路径 — {dir_path}", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
state = get_state()
|
||||
|
||||
@@ -46,3 +46,15 @@ def is_path_within_workspace(path_str: str) -> bool:
|
||||
except ValueError:
|
||||
return False
|
||||
return common == cwd
|
||||
|
||||
|
||||
def is_safe_cli_path(path_str: str) -> bool:
|
||||
"""CLI 路径安全检查 — 仅拒绝 .. 穿越组件,允许绝对路径和任意目录。
|
||||
|
||||
CLI 是本地工具,用户有权限访问系统中任意路径。
|
||||
与 is_path_within_workspace(API 用,绑定当前目录)相比更宽松。
|
||||
"""
|
||||
parts = path_str.replace("\\", "/").split("/")
|
||||
if ".." in parts:
|
||||
return False
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user