1398 lines
36 KiB
Markdown
1398 lines
36 KiB
Markdown
# 第三优先级:长期演进 实施计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 建立 CI/CD 自动化流水线、PyPI 发布、pre-commit 钩子、性能基准测试、gRPC API 和国际化支持,将项目从个人工具推向开源社区级产品。
|
||
|
||
**Architecture:** GitHub Actions 驱动 CI(测试 + lint + 类型检查 + 覆盖率)、pre-commit 钩子本地拦截问题、gRPC 作为高性能 API 补充 REST、i18n 支持中英双语。
|
||
|
||
**Tech Stack:** GitHub Actions, pre-commit (ruff + mypy), grpcio + grpcio-tools, gettext, sphinx, bump2version
|
||
|
||
**预估总工作量:** 约 20-25 小时(可分阶段实施)
|
||
|
||
---
|
||
|
||
## 文件结构规划
|
||
|
||
```
|
||
新增文件:
|
||
.github/workflows/ci.yml — CI 流水线
|
||
.github/workflows/publish.yml — PyPI 发布
|
||
.pre-commit-config.yaml — pre-commit 钩子配置
|
||
proto/md_vector_db.proto — gRPC 服务定义
|
||
src/server/grpc_server.py — gRPC 服务实现
|
||
src/server/grpc_client.py — gRPC 客户端示例
|
||
tests/test_grpc.py — gRPC 测试
|
||
tests/benchmarks/test_bench_embed.py — 嵌入性能基准
|
||
tests/benchmarks/test_bench_search.py — 检索性能基准
|
||
tests/benchmarks/conftest.py — 基准测试共享 fixture
|
||
docs/api_reference.md — API 参考文档
|
||
docs/architecture.md — 架构决策记录 (ADR)
|
||
CHANGELOG.md — 版本变更日志
|
||
CONTRIBUTING.md — 贡献指南
|
||
locales/zh_CN/LC_MESSAGES/messages.po — 中文翻译
|
||
locales/en/LC_MESSAGES/messages.po — 英文翻译(源)
|
||
scripts/bump_version.py — 版本号管理脚本
|
||
.bumpversion.cfg — bumpversion 配置
|
||
|
||
修改文件:
|
||
pyproject.toml — 新依赖和配置
|
||
README.md — badge、贡献指南链接
|
||
src/cli/main.py — i18n 输出
|
||
src/server/app.py — i18n 错误消息
|
||
config.yaml — 新配置项
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1: Pre-commit 钩子配置
|
||
|
||
**Files:**
|
||
- Create: `.pre-commit-config.yaml`
|
||
- Modify: `pyproject.toml`
|
||
|
||
- [ ] **Step 1: 创建 pre-commit 配置**
|
||
|
||
```yaml
|
||
# .pre-commit-config.yaml
|
||
repos:
|
||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||
rev: v0.11.0
|
||
hooks:
|
||
- id: ruff
|
||
args: [--fix]
|
||
- id: ruff-format
|
||
|
||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||
rev: v5.0.0
|
||
hooks:
|
||
- id: trailing-whitespace
|
||
- id: end-of-file-fixer
|
||
- id: check-yaml
|
||
args: [--unsafe]
|
||
- id: check-json
|
||
- id: check-toml
|
||
- id: check-added-large-files
|
||
args: ['--maxkb=500']
|
||
- id: detect-private-key
|
||
- id: debug-statements
|
||
|
||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||
rev: v1.15.0
|
||
hooks:
|
||
- id: mypy
|
||
args: [--ignore-missing-imports, --no-strict-optional]
|
||
files: ^src/
|
||
additional_dependencies:
|
||
- types-pyyaml
|
||
- types-requests
|
||
```
|
||
|
||
- [ ] **Step 2: 在 pyproject.toml 添加 ruff 格式化配置**
|
||
|
||
```toml
|
||
[tool.ruff.format]
|
||
quote-style = "double"
|
||
indent-style = "space"
|
||
skip-magic-trailing-comma = false
|
||
```
|
||
|
||
- [ ] **Step 3: 安装 pre-commit 钩子**
|
||
|
||
```bash
|
||
uv sync --extra dev
|
||
uv run pre-commit install
|
||
uv run pre-commit run --all-files
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add .pre-commit-config.yaml pyproject.toml
|
||
git commit -m "chore: 配置 pre-commit 钩子(ruff + mypy + 通用检查)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: GitHub Actions CI 流水线
|
||
|
||
**Files:**
|
||
- Create: `.github/workflows/ci.yml`
|
||
|
||
- [ ] **Step 1: 创建 CI 配置**
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yml
|
||
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
test:
|
||
name: Test (Python ${{ matrix.python-version }})
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
python-version: ["3.13"]
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Install uv
|
||
uses: astral-sh/setup-uv@v5
|
||
with:
|
||
python-version: ${{ matrix.python-version }}
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
uv sync --extra dev --extra all
|
||
|
||
- name: Run lint (ruff)
|
||
run: uv run ruff check src/ tests/
|
||
|
||
- name: Run type check (mypy)
|
||
run: uv run mypy src/ --ignore-missing-imports
|
||
|
||
- name: Run tests with coverage
|
||
run: |
|
||
uv run pytest tests/ \
|
||
--cov=src \
|
||
--cov-report=term-missing \
|
||
--cov-report=xml \
|
||
--cov-fail-under=80 \
|
||
-v
|
||
|
||
- name: Upload coverage to Codecov
|
||
uses: codecov/codecov-action@v5
|
||
with:
|
||
files: ./coverage.xml
|
||
fail_ci_if_error: false
|
||
```
|
||
|
||
- [ ] **Step 2: 在本地验证 CI 可运行**
|
||
|
||
```bash
|
||
# 模拟 CI 步骤
|
||
uv run ruff check src/ tests/
|
||
uv run mypy src/ --ignore-missing-imports
|
||
uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=80 -v
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add .github/workflows/ci.yml
|
||
git commit -m "ci: 添加 GitHub Actions 测试流水线(lint + type + coverage)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: PyPI 发布流水线
|
||
|
||
**Files:**
|
||
- Create: `.github/workflows/publish.yml`
|
||
- Modify: `pyproject.toml`
|
||
|
||
- [ ] **Step 1: 完善 pyproject.toml 发布元数据**
|
||
|
||
```toml
|
||
[project]
|
||
name = "md-vector-db"
|
||
version = "0.1.0"
|
||
description = "Markdown 文档向量数据库 — 语义检索、混合检索、REST API"
|
||
readme = "README.md"
|
||
license = {text = "MIT"}
|
||
authors = [
|
||
{name = "刘航宇", email = "3364451258@qq.com"},
|
||
]
|
||
keywords = ["vector-database", "semantic-search", "rag", "chromadb", "markdown"]
|
||
classifiers = [
|
||
"Development Status :: 4 - Beta",
|
||
"Intended Audience :: Developers",
|
||
"License :: OSI Approved :: MIT License",
|
||
"Programming Language :: Python :: 3.13",
|
||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||
"Topic :: Text Processing :: Markup",
|
||
]
|
||
|
||
[project.urls]
|
||
Homepage = "https://github.com/LHY0125/md-vector-db"
|
||
Documentation = "https://github.com/LHY0125/md-vector-db#readme"
|
||
Repository = "https://github.com/LHY0125/md-vector-db"
|
||
Issues = "https://github.com/LHY0125/md-vector-db/issues"
|
||
```
|
||
|
||
- [ ] **Step 2: 创建发布流水线**
|
||
|
||
```yaml
|
||
# .github/workflows/publish.yml
|
||
name: Publish to PyPI
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*"
|
||
|
||
jobs:
|
||
publish:
|
||
name: Build and publish
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
id-token: write # PyPI 可信发布
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Install uv
|
||
uses: astral-sh/setup-uv@v5
|
||
|
||
- name: Build package
|
||
run: uv build
|
||
|
||
- name: Publish to PyPI
|
||
uses: pypa/gh-action-pypi-publish@release/v1
|
||
with:
|
||
packages-dir: dist/
|
||
```
|
||
|
||
- [ ] **Step 3: 创建版本管理脚本**
|
||
|
||
```python
|
||
# scripts/bump_version.py
|
||
"""版本号管理 — 更新 pyproject.toml 中的 version."""
|
||
import sys
|
||
import re
|
||
from pathlib import Path
|
||
|
||
def bump(part: str) -> str:
|
||
"""递增版本号.
|
||
|
||
Args:
|
||
part: "major" | "minor" | "patch"
|
||
|
||
Returns:
|
||
新版本号字符串
|
||
"""
|
||
pyproject = Path(__file__).parent.parent / "pyproject.toml"
|
||
content = pyproject.read_text(encoding="utf-8")
|
||
match = re.search(r'version\s*=\s*"(\d+)\.(\d+)\.(\d+)"', content)
|
||
if not match:
|
||
print("错误: 未找到 version 字段", file=sys.stderr)
|
||
sys.exit(1)
|
||
major, minor, patch = int(match[1]), int(match[2]), int(match[3])
|
||
if part == "major":
|
||
major += 1; minor = 0; patch = 0
|
||
elif part == "minor":
|
||
minor += 1; patch = 0
|
||
elif part == "patch":
|
||
patch += 1
|
||
else:
|
||
print(f"错误: 未知的版本部分 '{part}',可选: major/minor/patch", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
new_version = f"{major}.{minor}.{patch}"
|
||
new_content = content.replace(match[0], f'version = "{new_version}"')
|
||
pyproject.write_text(new_content, encoding="utf-8")
|
||
print(f"版本: {match[1]}.{match[2]}.{match[3]} → {new_version}")
|
||
return new_version
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) != 2:
|
||
print("用法: python scripts/bump_version.py <major|minor|patch>")
|
||
sys.exit(1)
|
||
bump(sys.argv[1])
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add .github/workflows/publish.yml pyproject.toml scripts/bump_version.py
|
||
git commit -m "ci: 添加 PyPI 发布流水线 + 版本管理脚本"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: CHANGELOG 和 CONTRIBUTING.md
|
||
|
||
**Files:**
|
||
- Create: `CHANGELOG.md`
|
||
- Create: `CONTRIBUTING.md`
|
||
|
||
- [ ] **Step 1: 创建 CHANGELOG.md**
|
||
|
||
```markdown
|
||
# Changelog
|
||
|
||
所有值得注意的更改都将记录在此文件中。
|
||
|
||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||
|
||
## [Unreleased]
|
||
|
||
### Added
|
||
- 混合检索(BM25 + 向量联合)
|
||
- 增量入库(SHA256 文件变更追踪)
|
||
- Cross-Encoder 结果重排序
|
||
- Web 管理界面(Vue 3 SPA)
|
||
- .docx 文档支持(markitdown)
|
||
- 数据导出功能(JSON/CSV)
|
||
- Docker 部署方案 + docker-compose
|
||
- API 审计日志和请求体大小限制
|
||
|
||
### Changed
|
||
- Searcher 支持混合检索模式配置
|
||
- Embedder Protocol 修复为标准写法
|
||
- PDF/EPUB Splitter 参数名统一为 source
|
||
|
||
### Fixed
|
||
- CORS allow_credentials 与 allow_origins 冲突
|
||
- 健康检查端点免速率限制
|
||
|
||
## [0.1.0] - 2026-07-05
|
||
|
||
### Added
|
||
- Markdown 文档解析与语义检索
|
||
- 多 Provider 嵌入支持(local/OpenAI/DashScope)
|
||
- 多格式文档支持(.md/.txt/.pdf/.html/.epub)
|
||
- GPU 自动检测加速
|
||
- FastAPI HTTP API + CLI 工具
|
||
- API Key 认证和速率限制
|
||
- 路径遍历安全防护
|
||
- Obsidian 批量入库脚本
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 CONTRIBUTING.md**
|
||
|
||
```markdown
|
||
# 贡献指南
|
||
|
||
感谢你对 md-vector-db 的关注!本指南将帮助你完成第一次贡献。
|
||
|
||
## 开发环境
|
||
|
||
```bash
|
||
git clone git@github.com:LHY0125/md-vector-db.git
|
||
cd md-vector-db
|
||
uv sync --extra dev --extra all
|
||
uv run pre-commit install
|
||
```
|
||
|
||
## 开发流程
|
||
|
||
1. **Fork 仓库** → 创建功能分支 `feat/xxx`
|
||
2. **写测试**(TDD): 先在 `tests/` 下写失败测试
|
||
3. **实现功能**: 最少代码让测试通过
|
||
4. **运行全部测试**: `uv run pytest tests/ --cov=src -v`
|
||
5. **确保覆盖率 ≥ 80%**: `uv run pytest tests/ --cov=src --cov-fail-under=80`
|
||
6. **Lint 检查**: `uv run ruff check src/ tests/`
|
||
7. **类型检查**: `uv run mypy src/ --ignore-missing-imports`
|
||
8. **提交**: 遵循约定式提交格式
|
||
9. **创建 PR**: 描述清楚变更内容和测试结果
|
||
|
||
## 提交消息格式
|
||
|
||
```
|
||
<类型>: <描述>
|
||
|
||
<可选正文>
|
||
```
|
||
|
||
类型: feat, fix, refactor, docs, test, chore, perf, ci
|
||
|
||
## 代码风格
|
||
|
||
- Python 3.13+,遵循 PEP 8
|
||
- 类型注解覆盖所有 public API
|
||
- 中文注释(docstring 可以有英文摘要)
|
||
- 小文件原则(<800 行)
|
||
- 函数 <50 行
|
||
|
||
## 添加新文档格式支持
|
||
|
||
1. 在 `src/core/splitters/` 下实现 Splitter Protocol
|
||
2. 在 `src/core/splitters/registry.py` 注册扩展名映射
|
||
3. 在 `pyproject.toml` 添加可选依赖
|
||
4. 在 `tests/` 下添加测试
|
||
5. 更新 `README.md` 文档
|
||
|
||
## 运行基准测试
|
||
|
||
```bash
|
||
uv run pytest tests/benchmarks/ -v --benchmark-only
|
||
```
|
||
|
||
## 问题反馈
|
||
|
||
在 GitHub Issues 中描述:
|
||
- 你的使用场景和期望行为
|
||
- 实际发生的错误信息
|
||
- 配置文件(去除敏感信息)
|
||
- 运行环境和 Python 版本
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add CHANGELOG.md CONTRIBUTING.md
|
||
git commit -m "docs: 添加 CHANGELOG 和 CONTRIBUTING.md"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 性能基准测试
|
||
|
||
**Files:**
|
||
- Create: `tests/benchmarks/conftest.py`
|
||
- Create: `tests/benchmarks/test_bench_embed.py`
|
||
- Create: `tests/benchmarks/test_bench_search.py`
|
||
|
||
- [ ] **Step 1: 添加 benchmark 依赖**
|
||
|
||
在 `pyproject.toml` 的 `[project.optional-dependencies]` 添加:
|
||
|
||
```toml
|
||
bench = ["pytest-benchmark>=5.0"]
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 benchmark conftest**
|
||
|
||
```python
|
||
# tests/benchmarks/conftest.py
|
||
"""基准测试共享 fixture."""
|
||
import pytest
|
||
from src.core.config import EmbedConfig, ChunkConfig, SearchConfig
|
||
from src.core.db import VectorDB
|
||
from src.core.embedder import create_embedder
|
||
from src.core.ingest import DocumentIngestor
|
||
from src.core.search import Searcher
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def benchmark_db(tmp_path_factory):
|
||
"""模块级共享 ChromaDB 实例."""
|
||
persist_dir = tmp_path_factory.mktemp("bench_data")
|
||
db = VectorDB(persist_dir=str(persist_dir))
|
||
return db
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def benchmark_embedder():
|
||
"""模块级共享 LocalEmbedder(首次加载模型,后续复用)."""
|
||
config = EmbedConfig(mode="local", local_model="BAAI/bge-small-zh-v1.5")
|
||
return create_embedder(config)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def benchmark_searcher(benchmark_db, benchmark_embedder):
|
||
"""预填充数据的 Searcher."""
|
||
ingestor = DocumentIngestor(
|
||
benchmark_db, benchmark_embedder, "bench_collection",
|
||
chunk_config=ChunkConfig(max_size=1000, overlap=100),
|
||
)
|
||
# 生成 100 个测试文档
|
||
for i in range(100):
|
||
content = f"# 文档{i}\n\n" + "\n\n".join(
|
||
f"这是第{j}段内容,用于基准测试。包含一些搜索关键词如:Python, Rust, GPU, 向量数据库。"
|
||
for j in range(5)
|
||
)
|
||
ingestor.ingest_content(content, f"bench_{i}.md")
|
||
|
||
return Searcher(benchmark_db, benchmark_embedder, "bench_collection")
|
||
```
|
||
|
||
- [ ] **Step 3: 创建嵌入性能基准**
|
||
|
||
```python
|
||
# tests/benchmarks/test_bench_embed.py
|
||
"""嵌入性能基准测试."""
|
||
import pytest
|
||
|
||
|
||
def test_bench_embed_single(benchmark, benchmark_embedder):
|
||
"""单文本嵌入耗时."""
|
||
text = "这是一段测试文本,用于测量嵌入速度。"
|
||
benchmark(benchmark_embedder.embed, [text])
|
||
|
||
|
||
def test_bench_embed_batch_32(benchmark, benchmark_embedder):
|
||
"""批量 32 文本嵌入耗时."""
|
||
texts = [f"测试文本第{i}条,包含一些关键词来模拟真实文档。" for i in range(32)]
|
||
benchmark(benchmark_embedder.embed, texts)
|
||
|
||
|
||
def test_bench_embed_batch_100(benchmark, benchmark_embedder):
|
||
"""批量 100 文本嵌入耗时(GPU 优势显著)."""
|
||
texts = [
|
||
f"测试文本第{i}条。Python 是一种通用编程语言,广泛用于数据科学、Web 开发和人工智能领域。"
|
||
for i in range(100)
|
||
]
|
||
benchmark(benchmark_embedder.embed, texts)
|
||
```
|
||
|
||
- [ ] **Step 4: 创建检索性能基准**
|
||
|
||
```python
|
||
# tests/benchmarks/test_bench_search.py
|
||
"""检索性能基准测试."""
|
||
import pytest
|
||
|
||
|
||
def test_bench_search_top5(benchmark, benchmark_searcher):
|
||
"""top_k=5 检索耗时."""
|
||
benchmark(benchmark_searcher.search, "Python 向量数据库", top_k=5)
|
||
|
||
|
||
def test_bench_search_top20(benchmark, benchmark_searcher):
|
||
"""top_k=20 检索耗时."""
|
||
benchmark(benchmark_searcher.search, "Rust 编程语言", top_k=20)
|
||
|
||
|
||
def test_bench_search_cold_start(benchmark, benchmark_searcher):
|
||
"""冷启动检索(第一个查询后的 BM25 索引重建)."""
|
||
# 强制清除 BM25 缓存(如果是 hybrid 模式)
|
||
benchmark(benchmark_searcher.search, "GPU 加速 深度学习", top_k=10)
|
||
```
|
||
|
||
- [ ] **Step 5: 运行基准测试**
|
||
|
||
```bash
|
||
uv sync --extra bench
|
||
uv run pytest tests/benchmarks/ -v --benchmark-only --benchmark-columns=min,max,mean,median
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add tests/benchmarks/ pyproject.toml
|
||
git commit -m "test: 添加嵌入和检索性能基准测试"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: gRPC API(高性能替代方案)
|
||
|
||
**Files:**
|
||
- Create: `proto/md_vector_db.proto`
|
||
- Create: `src/server/grpc_server.py`
|
||
- Create: `src/server/grpc_client.py`
|
||
- Create: `tests/test_grpc.py`
|
||
- Modify: `pyproject.toml`
|
||
|
||
- [ ] **Step 1: 添加 gRPC 依赖**
|
||
|
||
在 `pyproject.toml` 的可选依赖中添加:
|
||
|
||
```toml
|
||
grpc = ["grpcio>=1.70.0", "grpcio-tools>=1.70.0"]
|
||
```
|
||
|
||
并将 `all` 更新:
|
||
|
||
```toml
|
||
all = ["md-vector-db[pdf,html,epub,docx,grpc,bench]", "requests>=2.31.0", "openai>=1.0.0"]
|
||
```
|
||
|
||
- [ ] **Step 2: 定义 Proto 服务**
|
||
|
||
```protobuf
|
||
// proto/md_vector_db.proto
|
||
syntax = "proto3";
|
||
|
||
package md_vector_db;
|
||
|
||
// 向量检索服务
|
||
service VectorSearch {
|
||
// 健康检查
|
||
rpc Health(HealthRequest) returns (HealthResponse);
|
||
|
||
// 语义检索
|
||
rpc Search(SearchRequest) returns (SearchResponse);
|
||
|
||
// 入库文档(按内容)
|
||
rpc Ingest(IngestRequest) returns (IngestResponse);
|
||
|
||
// 删除文档
|
||
rpc Delete(DeleteRequest) returns (DeleteResponse);
|
||
}
|
||
|
||
// --- 消息定义 ---
|
||
|
||
message HealthRequest {}
|
||
|
||
message HealthResponse {
|
||
string status = 1;
|
||
int32 chroma_count = 2;
|
||
int32 embed_dimension = 3;
|
||
}
|
||
|
||
message SearchRequest {
|
||
string query = 1;
|
||
int32 top_k = 2;
|
||
string collection = 3;
|
||
string source_file = 4;
|
||
}
|
||
|
||
message SearchResult {
|
||
string id = 1;
|
||
string content = 2;
|
||
string source_file = 3;
|
||
string section_title = 4;
|
||
int32 heading_level = 5;
|
||
float score = 6;
|
||
}
|
||
|
||
message SearchResponse {
|
||
repeated SearchResult results = 1;
|
||
string collection = 2;
|
||
}
|
||
|
||
message IngestRequest {
|
||
string content = 1;
|
||
string file_name = 2;
|
||
string collection = 3;
|
||
}
|
||
|
||
message IngestResponse {
|
||
string status = 1;
|
||
int32 chunks = 2;
|
||
string file = 3;
|
||
string collection = 4;
|
||
}
|
||
|
||
message DeleteRequest {
|
||
string file_name = 1;
|
||
string collection = 2;
|
||
}
|
||
|
||
message DeleteResponse {
|
||
string status = 1;
|
||
string file = 2;
|
||
string collection = 3;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 生成 gRPC stub**
|
||
|
||
```bash
|
||
uv sync --extra grpc
|
||
mkdir -p src/server/generated
|
||
python -m grpc_tools.protoc \
|
||
-I proto \
|
||
--python_out=src/server/generated \
|
||
--grpc_python_out=src/server/generated \
|
||
proto/md_vector_db.proto
|
||
```
|
||
|
||
- [ ] **Step 4: 实现 gRPC 服务**
|
||
|
||
```python
|
||
# src/server/grpc_server.py
|
||
"""gRPC 服务实现."""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import grpc
|
||
from concurrent import futures
|
||
|
||
from src.server.generated import md_vector_db_pb2 as pb2
|
||
from src.server.generated import md_vector_db_pb2_grpc as pb2_grpc
|
||
from src.server.deps import AppState
|
||
|
||
logger = logging.getLogger("md-vector-db")
|
||
|
||
|
||
class VectorSearchServicer(pb2_grpc.VectorSearchServicer):
|
||
"""gRPC 服务实现 — 委托给 AppState."""
|
||
|
||
def __init__(self, state: AppState):
|
||
self._state = state
|
||
|
||
def Health(self, request: pb2.HealthRequest, context) -> pb2.HealthResponse:
|
||
health = self._state.is_healthy()
|
||
return pb2.HealthResponse(
|
||
status=health["status"],
|
||
chroma_count=health.get("checks", {}).get("chromadb", {}).get("count", 0),
|
||
embed_dimension=health.get("checks", {}).get("embedder", {}).get("dimension", 0),
|
||
)
|
||
|
||
def Search(self, request: pb2.SearchRequest, context) -> pb2.SearchResponse:
|
||
try:
|
||
searcher = self._state.get_searcher(request.collection or None)
|
||
results = searcher.search(
|
||
request.query,
|
||
top_k=request.top_k or 10,
|
||
source_file=request.source_file or None,
|
||
)
|
||
pb_results = [
|
||
pb2.SearchResult(
|
||
id=r["id"],
|
||
content=r["content"],
|
||
source_file=r.get("source_file", ""),
|
||
section_title=r.get("section_title", ""),
|
||
heading_level=r.get("heading_level", 0),
|
||
score=r["score"],
|
||
)
|
||
for r in results
|
||
]
|
||
return pb2.SearchResponse(
|
||
results=pb_results,
|
||
collection=searcher.collection_name,
|
||
)
|
||
except Exception:
|
||
logger.exception("gRPC 检索失败")
|
||
context.set_code(grpc.StatusCode.INTERNAL)
|
||
context.set_details("服务器内部错误")
|
||
return pb2.SearchResponse()
|
||
|
||
def Ingest(self, request: pb2.IngestRequest, context) -> pb2.IngestResponse:
|
||
try:
|
||
ingestor = self._state.get_ingestor(request.collection or None)
|
||
file_name = request.file_name or "untitled.md"
|
||
count = ingestor.ingest_content(request.content, file_name)
|
||
return pb2.IngestResponse(
|
||
status="ok",
|
||
chunks=count,
|
||
file=file_name,
|
||
collection=ingestor.collection_name,
|
||
)
|
||
except Exception:
|
||
logger.exception("gRPC 入库失败")
|
||
context.set_code(grpc.StatusCode.INTERNAL)
|
||
context.set_details("服务器内部错误")
|
||
return pb2.IngestResponse()
|
||
|
||
def Delete(self, request: pb2.DeleteRequest, context) -> pb2.DeleteResponse:
|
||
try:
|
||
searcher = self._state.get_searcher(request.collection or None)
|
||
deleted = searcher.delete_by_source(request.file_name)
|
||
if not deleted:
|
||
context.set_code(grpc.StatusCode.NOT_FOUND)
|
||
context.set_details(f"文档不存在: {request.file_name}")
|
||
return pb2.DeleteResponse()
|
||
return pb2.DeleteResponse(
|
||
status="ok",
|
||
file=request.file_name,
|
||
collection=searcher.collection_name,
|
||
)
|
||
except Exception:
|
||
logger.exception("gRPC 删除失败")
|
||
context.set_code(grpc.StatusCode.INTERNAL)
|
||
context.set_details("服务器内部错误")
|
||
return pb2.DeleteResponse()
|
||
|
||
|
||
def serve_grpc(state: AppState, port: int = 50051) -> grpc.Server:
|
||
"""启动 gRPC 服务."""
|
||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
||
pb2_grpc.add_VectorSearchServicer_to_server(VectorSearchServicer(state), server)
|
||
server.add_insecure_port(f"[::]:{port}")
|
||
logger.info("gRPC 服务启动: 0.0.0.0:%d", port)
|
||
return server
|
||
```
|
||
|
||
- [ ] **Step 5: 创建 gRPC 客户端示例**
|
||
|
||
```python
|
||
# src/server/grpc_client.py
|
||
"""gRPC 客户端示例."""
|
||
from __future__ import annotations
|
||
|
||
import grpc
|
||
from src.server.generated import md_vector_db_pb2 as pb2
|
||
from src.server.generated import md_vector_db_pb2_grpc as pb2_grpc
|
||
|
||
|
||
class VectorDBClient:
|
||
"""md-vector-db gRPC 客户端."""
|
||
|
||
def __init__(self, host: str = "localhost", port: int = 50051):
|
||
self.channel = grpc.insecure_channel(f"{host}:{port}")
|
||
self.stub = pb2_grpc.VectorSearchStub(self.channel)
|
||
|
||
def health(self) -> pb2.HealthResponse:
|
||
return self.stub.Health(pb2.HealthRequest())
|
||
|
||
def search(
|
||
self, query: str, top_k: int = 10, collection: str = ""
|
||
) -> pb2.SearchResponse:
|
||
return self.stub.Search(pb2.SearchRequest(
|
||
query=query, top_k=top_k, collection=collection,
|
||
))
|
||
|
||
def ingest(
|
||
self, content: str, file_name: str = "doc.md", collection: str = ""
|
||
) -> pb2.IngestResponse:
|
||
return self.stub.Ingest(pb2.IngestRequest(
|
||
content=content, file_name=file_name, collection=collection,
|
||
))
|
||
|
||
def close(self):
|
||
self.channel.close()
|
||
|
||
|
||
# 使用示例
|
||
if __name__ == "__main__":
|
||
client = VectorDBClient()
|
||
health = client.health()
|
||
print(f"状态: {health.status}")
|
||
results = client.search("Python 向量数据库", top_k=3)
|
||
for r in results.results:
|
||
print(f"[{r.score:.4f}] {r.content[:100]}...")
|
||
```
|
||
|
||
- [ ] **Step 6: 编写 gRPC 测试**
|
||
|
||
```python
|
||
# tests/test_grpc.py
|
||
"""gRPC 服务测试."""
|
||
import pytest
|
||
import grpc
|
||
from concurrent import futures
|
||
|
||
from src.server.generated import md_vector_db_pb2 as pb2
|
||
from src.server.generated import md_vector_db_pb2_grpc as pb2_grpc
|
||
from src.server.grpc_server import VectorSearchServicer
|
||
from src.server.deps import AppState
|
||
|
||
|
||
@pytest.fixture
|
||
def grpc_server(state):
|
||
"""创建测试用 gRPC 服务."""
|
||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=1))
|
||
pb2_grpc.add_VectorSearchServicer_to_server(VectorSearchServicer(state), server)
|
||
port = server.add_insecure_port("localhost:0")
|
||
server.start()
|
||
channel = grpc.insecure_channel(f"localhost:{port}")
|
||
stub = pb2_grpc.VectorSearchStub(channel)
|
||
yield stub
|
||
channel.close()
|
||
server.stop(None)
|
||
|
||
|
||
def test_grpc_health(grpc_server):
|
||
"""gRPC 健康检查."""
|
||
resp = grpc_server.Health(pb2.HealthRequest())
|
||
assert resp.status in ("ok", "degraded")
|
||
|
||
|
||
def test_grpc_ingest_and_search(grpc_server):
|
||
"""gRPC 入库 + 检索."""
|
||
ingest_resp = grpc_server.Ingest(pb2.IngestRequest(
|
||
content="# gRPC 测试\n\n这是一段测试内容。",
|
||
file_name="grpc_test.md",
|
||
))
|
||
assert ingest_resp.status == "ok"
|
||
assert ingest_resp.chunks >= 1
|
||
|
||
search_resp = grpc_server.Search(pb2.SearchRequest(
|
||
query="测试内容",
|
||
top_k=5,
|
||
))
|
||
assert len(search_resp.results) >= 1
|
||
assert any("gRPC" in r.content or "测试" in r.content for r in search_resp.results)
|
||
```
|
||
|
||
- [ ] **Step 7: 运行 gRPC 测试**
|
||
|
||
```bash
|
||
uv run pytest tests/test_grpc.py -v
|
||
```
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```bash
|
||
git add proto/ src/server/grpc_server.py src/server/grpc_client.py tests/test_grpc.py pyproject.toml
|
||
git commit -m "feat: 添加 gRPC API(更高吞吐的替代方案)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: 国际化 (i18n) 支持
|
||
|
||
**Files:**
|
||
- Create: `src/core/i18n.py`
|
||
- Create: `locales/en/LC_MESSAGES/messages.po`
|
||
- Create: `locales/zh_CN/LC_MESSAGES/messages.po`
|
||
- Modify: `src/cli/main.py`
|
||
- Modify: `src/server/app.py`
|
||
|
||
- [ ] **Step 1: 创建 i18n 模块**
|
||
|
||
```python
|
||
# src/core/i18n.py
|
||
"""国际化支持 — 基于 gettext."""
|
||
from __future__ import annotations
|
||
|
||
import gettext
|
||
import os
|
||
from pathlib import Path
|
||
|
||
# locales 目录路径
|
||
_LOCALES_DIR = Path(__file__).parent.parent.parent / "locales"
|
||
|
||
|
||
def setup_i18n(lang: str | None = None) -> gettext.NullTranslations:
|
||
"""初始化翻译.
|
||
|
||
Args:
|
||
lang: 语言代码("zh_CN" | "en"),默认从 LANG 环境变量读取
|
||
|
||
Returns:
|
||
gettext 翻译对象
|
||
"""
|
||
if not lang:
|
||
lang = os.environ.get("LANG", "zh_CN").split(".")[0]
|
||
# 映射常见语言代码
|
||
lang_map = {"zh": "zh_CN", "zh_CN": "zh_CN", "en": "en", "en_US": "en"}
|
||
lang = lang_map.get(lang, "zh_CN")
|
||
|
||
try:
|
||
return gettext.translation(
|
||
"messages",
|
||
localedir=str(_LOCALES_DIR),
|
||
languages=[lang],
|
||
fallback=True,
|
||
)
|
||
except (FileNotFoundError, OSError):
|
||
return gettext.NullTranslations()
|
||
|
||
|
||
# 全局翻译函数(由 CLI 入口初始化)
|
||
_t = gettext.NullTranslations()
|
||
|
||
|
||
def init_translation(lang: str | None = None) -> None:
|
||
"""初始化全局翻译."""
|
||
global _t
|
||
_t = setup_i18n(lang)
|
||
|
||
|
||
def _(message: str) -> str:
|
||
"""翻译标记函数."""
|
||
return _t.gettext(message)
|
||
```
|
||
|
||
- [ ] **Step 2: 创建翻译文件**
|
||
|
||
英文(源语言):
|
||
|
||
```po
|
||
# locales/en/LC_MESSAGES/messages.po
|
||
msgid ""
|
||
msgstr ""
|
||
"Language: en\n"
|
||
"MIME-Version: 1.0\n"
|
||
"Content-Type: text/plain; charset=UTF-8\n"
|
||
|
||
msgid "未找到匹配结果。"
|
||
msgstr "No results found."
|
||
|
||
msgid "服务器内部错误"
|
||
msgstr "Internal server error"
|
||
|
||
msgid "请求过于频繁,请稍后再试"
|
||
msgstr "Too many requests, please try again later"
|
||
|
||
msgid "无效的 API Key"
|
||
msgstr "Invalid API Key"
|
||
|
||
msgid "不允许的路径"
|
||
msgstr "Path not allowed"
|
||
|
||
msgid "文件不存在: {name}"
|
||
msgstr "File not found: {name}"
|
||
|
||
msgid "文档不存在: {file_name}"
|
||
msgstr "Document not found: {file_name}"
|
||
```
|
||
|
||
中文翻译:
|
||
|
||
```po
|
||
# locales/zh_CN/LC_MESSAGES/messages.po
|
||
msgid ""
|
||
msgstr ""
|
||
"Language: zh_CN\n"
|
||
"MIME-Version: 1.0\n"
|
||
"Content-Type: text/plain; charset=UTF-8\n"
|
||
|
||
msgid "未找到匹配结果。"
|
||
msgstr "未找到匹配结果。"
|
||
|
||
msgid "服务器内部错误"
|
||
msgstr "服务器内部错误"
|
||
|
||
msgid "请求过于频繁,请稍后再试"
|
||
msgstr "请求过于频繁,请稍后再试"
|
||
|
||
msgid "无效的 API Key"
|
||
msgstr "无效的 API Key"
|
||
|
||
msgid "不允许的路径"
|
||
msgstr "不允许的路径"
|
||
|
||
msgid "文件不存在: {name}"
|
||
msgstr "文件不存在: {name}"
|
||
|
||
msgid "文档不存在: {file_name}"
|
||
msgstr "文档不存在: {file_name}"
|
||
```
|
||
|
||
- [ ] **Step 3: 编译 .mo 文件**
|
||
|
||
```bash
|
||
msgfmt locales/en/LC_MESSAGES/messages.po -o locales/en/LC_MESSAGES/messages.mo
|
||
msgfmt locales/zh_CN/LC_MESSAGES/messages.po -o locales/zh_CN/LC_MESSAGES/messages.mo
|
||
```
|
||
|
||
- [ ] **Step 4: 在 CLI 和 API 中使用 i18n**
|
||
|
||
修改 `src/cli/main.py` 顶部,在 `app` 创建前:
|
||
|
||
```python
|
||
from src.core.i18n import init_translation
|
||
|
||
init_translation()
|
||
```
|
||
|
||
修改 server 错误消息,将硬编码的中文改为 `_()`:
|
||
|
||
```python
|
||
# 示例(不修改全部,仅示意路径)
|
||
from src.core.i18n import _ as t
|
||
|
||
# 在 server/app.py 中替换:
|
||
raise HTTPException(status_code=400, detail=t("不允许的路径"))
|
||
raise HTTPException(status_code=500, detail=t("服务器内部错误"))
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/core/i18n.py locales/ src/cli/main.py src/server/app.py
|
||
git commit -m "feat: 添加国际化支持(中/英)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 架构决策记录 (ADR) 和 API 文档
|
||
|
||
**Files:**
|
||
- Create: `docs/architecture.md`
|
||
- Create: `docs/api_reference.md`
|
||
|
||
- [ ] **Step 1: 编写 API 参考文档**
|
||
|
||
```markdown
|
||
# API 参考文档
|
||
|
||
## REST API
|
||
|
||
### 基础信息
|
||
|
||
- Base URL: `http://localhost:8000/api/v1`
|
||
- 认证方式: `X-API-Key` Header(可选,取决于 `MD_VECTOR_API_KEY` 环境变量)
|
||
- 速率限制: 60s 窗口内最多 30 请求(可配置)
|
||
- Content-Type: `application/json`
|
||
|
||
### 端点
|
||
|
||
#### GET /health — 健康检查
|
||
|
||
无需认证。
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"checks": {
|
||
"chromadb": {"status": "ok", "count": 1240},
|
||
"embedder": {"status": "ok", "dimension": 512}
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误码:**
|
||
| 状态码 | 含义 |
|
||
|--------|------|
|
||
| 200 | 健康(status=ok)或部分降级(status=degraded) |
|
||
|
||
#### GET /collections — 列出集合
|
||
|
||
需 API Key 认证。
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"collections": [
|
||
{"name": "default", "count": 30},
|
||
{"name": "obsidian_blog", "count": 3611}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### POST /search — 语义检索
|
||
|
||
需 API Key 认证。
|
||
|
||
**请求:**
|
||
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||
|------|------|------|------|------|
|
||
| query | string | ✅ | 1-2000 字符 | 搜索查询 |
|
||
| top_k | int | ❌ | 1-100,默认 10 | 返回条数 |
|
||
| collection | string | ❌ | ≤128 字符,`[a-zA-Z0-9_-]+` | 目标集合 |
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"results": [
|
||
{
|
||
"id": "abc123_intro.md_0",
|
||
"content": "# 简介\n\n这是文档内容...",
|
||
"source_file": "abc123_intro.md",
|
||
"section_title": "简介",
|
||
"heading_level": 1,
|
||
"chunk_index": 0,
|
||
"score": 0.8231
|
||
}
|
||
],
|
||
"collection": "default"
|
||
}
|
||
```
|
||
|
||
#### POST /ingest — 入库文档
|
||
|
||
需 API Key 认证。
|
||
|
||
**请求:** `file_path` 或 `content` 二选一。
|
||
|
||
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||
|------|------|------|------|------|
|
||
| file_path | string | 二选一 | 安全路径 | 文件路径 |
|
||
| content | string | 二选一 | ≤500KB | 文本内容 |
|
||
| file_name | string | content 模式推荐 | 1-255 字符 | 文件名 |
|
||
| collection | string | ❌ | ≤128 字符 | 目标集合 |
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"chunks": 5,
|
||
"file": "intro.md",
|
||
"collection": "default"
|
||
}
|
||
```
|
||
|
||
#### DELETE /documents/{file_name} — 删除文档
|
||
|
||
需 API Key 认证。
|
||
|
||
**查询参数:** `collection` (可选)
|
||
|
||
**错误码:**
|
||
| 状态码 | 含义 |
|
||
|--------|------|
|
||
| 200 | 删除成功 |
|
||
| 400 | file_name 长度不合法 |
|
||
| 401 | API Key 无效 |
|
||
| 404 | 文档不存在 |
|
||
|
||
### gRPC API
|
||
|
||
详见 `proto/md_vector_db.proto`。
|
||
|
||
客户端示例:
|
||
|
||
```python
|
||
from src.server.grpc_client import VectorDBClient
|
||
|
||
client = VectorDBClient("localhost", 50051)
|
||
results = client.search("Python", top_k=5)
|
||
for r in results.results:
|
||
print(f"[{r.score:.4f}] {r.content[:100]}...")
|
||
client.close()
|
||
```
|
||
```
|
||
|
||
- [ ] **Step 2: 编写架构决策记录**
|
||
|
||
```markdown
|
||
# 架构决策记录 (ADR)
|
||
|
||
## ADR-1: 为什么选择 ChromaDB?
|
||
|
||
**日期**: 2026-07-05
|
||
**状态**: 已采纳
|
||
|
||
**背景**: 需要一个嵌入式向量数据库来持久化文档嵌入。
|
||
|
||
**候选方案**:
|
||
1. **ChromaDB** — 嵌入式,SQLite 持久化,零运维
|
||
2. **Qdrant** — 高性能,需独立服务进程
|
||
3. **FAISS** — 纯内存,无持久化
|
||
4. **Milvus** — 生产级,需 Docker + etcd + MinIO
|
||
|
||
**决策**: 选择 ChromaDB。对个人/小团队项目,零运维成本远比吞吐量重要。内置 Collection 概念正好映射我们的多知识库场景。
|
||
|
||
**代价**: 高并发下性能逊于 Qdrant/Milvus。未来若需大规模部署,可透明迁移(Embedder/Searcher 接口已隔离 ChromaDB 依赖)。
|
||
|
||
---
|
||
|
||
## ADR-2: 为什么默认使用 bge-small-zh-v1.5?
|
||
|
||
**日期**: 2026-07-05
|
||
**状态**: 已采纳
|
||
|
||
**背景**: 需要一个开箱即用的中文嵌入模型。
|
||
|
||
**候选方案**:
|
||
1. **bge-small-zh-v1.5** — 512 维,23M 参数,MTEB 中文排名靠前
|
||
2. **bge-large-zh-v1.5** — 1024 维,324M 参数,精度更高但慢 3-5×
|
||
3. **text2vec-large-chinese** — 1024 维,326M 参数
|
||
4. **m3e-base** — 768 维,110M 参数
|
||
|
||
**决策**: bge-small-zh-v1.5。RTX 4060 上 729 chunks 嵌入仅 1.8s,精度足够日常使用。需更高精度时可通过 `config.yaml` 切换到 large 模型或 OpenAI API。
|
||
|
||
---
|
||
|
||
## ADR-3: 为什么采用 Protocol 而非 ABC?
|
||
|
||
**日期**: 2026-07-05
|
||
**状态**: 已采纳
|
||
|
||
**背景**: Splitter 和 Embedder 需要可扩展的接口。
|
||
|
||
**候选方案**:
|
||
1. **typing.Protocol** — 结构化子类型(鸭子类型),无需显式继承
|
||
2. **abc.ABC** — 名义子类型,需显式继承和 @abstractmethod
|
||
3. **Callable/函数签名** — 极简,但丢失类型信息
|
||
|
||
**决策**: Protocol。允许外部模块在无需依赖本项目源码的情况下实现 Splitter/Embedder(只要结构匹配),对插件化友好。
|
||
|
||
**代价**: mypy 对 Protocol 的检查在某些场景不够严格,需补充运行时类型检查。
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add docs/api_reference.md docs/architecture.md
|
||
git commit -m "docs: 添加 API 参考文档和架构决策记录 (ADR)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: README 添加 CI/CD Badge 和项目成熟度展示
|
||
|
||
**Files:**
|
||
- Modify: `README.md`
|
||
|
||
- [ ] **Step 1: 在 README 顶部添加 badge**
|
||
|
||
```markdown
|
||
# md-vector-db
|
||
|
||
[](https://github.com/LHY0125/md-vector-db/actions/workflows/ci.yml)
|
||
[](https://codecov.io/gh/LHY0125/md-vector-db)
|
||
[](https://badge.fury.io/py/md-vector-db)
|
||
[](https://opensource.org/licenses/MIT)
|
||
```
|
||
|
||
- [ ] **Step 2: 在 README 底部添加贡献和许可章节**
|
||
|
||
```markdown
|
||
## 贡献
|
||
|
||
请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解开发流程、代码风格和提交流程。
|
||
|
||
## 变更日志
|
||
|
||
详见 [CHANGELOG.md](CHANGELOG.md)。
|
||
|
||
## 架构
|
||
|
||
详见 [架构决策记录](docs/architecture.md) 和 [API 参考](docs/api_reference.md)。
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add README.md
|
||
git commit -m "docs: README 添加 CI/Coverage/PyPI badge 和贡献链接"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: 最终验证与发布准备
|
||
|
||
- [ ] **Step 1: 全量验证**
|
||
|
||
```bash
|
||
# Lint
|
||
uv run ruff check src/ tests/
|
||
|
||
# 类型检查
|
||
uv run mypy src/ --ignore-missing-imports
|
||
|
||
# 全部测试 + 覆盖率
|
||
uv run pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=80 -v
|
||
|
||
# 基准测试
|
||
uv run pytest tests/benchmarks/ -v --benchmark-only
|
||
|
||
# Pre-commit
|
||
uv run pre-commit run --all-files
|
||
```
|
||
|
||
- [ ] **Step 2: 构建验证**
|
||
|
||
```bash
|
||
uv build
|
||
ls dist/
|
||
# 预期: md_vector_db-0.1.0-py3-none-any.whl, md_vector_db-0.1.0.tar.gz
|
||
```
|
||
|
||
- [ ] **Step 3: 本地安装测试**
|
||
|
||
```bash
|
||
uv pip install dist/md_vector_db-0.1.0-py3-none-any.whl --force-reinstall
|
||
md-vector-db --help
|
||
```
|
||
|
||
- [ ] **Step 4: 更新版本号**
|
||
|
||
```bash
|
||
uv run python scripts/bump_version.py patch
|
||
git add pyproject.toml
|
||
git commit -m "chore: bump version to 0.1.1"
|
||
|
||
# 打 tag 触发 PyPI 发布
|
||
git tag v0.1.1
|
||
git push origin main --tags
|
||
```
|
||
|
||
- [ ] **Step 5: 总结**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: 第三优先级完善 — CI/CD、PyPI、pre-commit、gRPC、i18n、ADR"
|
||
```
|
||
|
||
---
|
||
|
||
## 自审清单
|
||
|
||
1. **Spec 覆盖**: CI/CD ✅ | PyPI ✅ | pre-commit ✅ | 基准测试 ✅ | gRPC API ✅ | i18n ✅ | ADR ✅ | CHANGELOG ✅ | CONTRIBUTING ✅
|
||
2. **无占位符**: 所有代码为具体实现
|
||
3. **类型一致性**: proto 定义与 grpc_server 实现一一对应
|
||
4. **渐进式**: 第三优先级任务相互独立,可分批实施(CI/pre-commit 立即可做,gRPC/i18n 可后延)
|