diff --git a/src/cli/main.py b/src/cli/main.py index a1a3e60..3f39f07 100644 --- a/src/cli/main.py +++ b/src/cli/main.py @@ -2,15 +2,15 @@ from pathlib import Path import sys import io +from typing import Annotated import typer import uvicorn -# Windows 终端默认用 GBK, 无法输出 emoji, 强制 UTF-8 +# Windows 终端 GBK → UTF-8 if sys.stdout.encoding != "utf-8": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") -# 确保 src 在路径中 sys.path.insert(0, str(Path(__file__).parent.parent)) from src.core.config import load_config, DEFAULT_CONFIG_PATH @@ -19,8 +19,15 @@ from src.core.embedder import create_embedder from src.core.ingest import DocumentIngestor from src.core.search import Searcher - -app = typer.Typer(name="md-vector-db", help="Markdown 文档向量数据库管理工具") +app = typer.Typer( + name="md-vector-db", + help="Markdown 文档向量数据库 — 入库、嵌入、语义检索", + epilog="示例:\n" + " md-vector-db ingest docs/readme.md\n" + " md-vector-db ingest-dir ./md_docs/\n" + " md-vector-db search \"如何配置\" --top-k 5\n" + " md-vector-db serve --port 8000", +) def _get_components(config_path: str = DEFAULT_CONFIG_PATH): @@ -33,18 +40,35 @@ def _get_components(config_path: str = DEFAULT_CONFIG_PATH): return cfg, searcher, ingestor -@app.command() -def ingest(file_path: str): - """入库单个 Markdown 文件.""" - _, _, ingestor = _get_components() +# -- 共享选项 -- +ConfigOption = Annotated[ + str, + typer.Option( + "--config", "-c", + help="配置文件路径 (默认: config.yaml)", + show_default="config.yaml", + ), +] + +# --- 命令 --- + + +@app.command(help="入库单个 Markdown 文件.") +def ingest( + file_path: Annotated[str, typer.Argument(help="Markdown 文件路径")], + config: ConfigOption = DEFAULT_CONFIG_PATH, +): + _, _, ingestor = _get_components(config) count = ingestor.ingest_file(file_path) typer.echo(f"✅ 已入库: {file_path} ({count} 个 chunks)") -@app.command() -def ingest_dir(dir_path: str): - """入库目录下所有 Markdown 文件.""" - _, _, ingestor = _get_components() +@app.command(help="批量入库目录下所有 .md 文件 (递归).") +def ingest_dir( + dir_path: Annotated[str, typer.Argument(help="包含 Markdown 文件的目录路径")], + config: ConfigOption = DEFAULT_CONFIG_PATH, +): + _, _, ingestor = _get_components(config) results = ingestor.ingest_directory(dir_path) total = sum(results.values()) for name, count in results.items(): @@ -52,10 +76,13 @@ def ingest_dir(dir_path: str): typer.echo(f"✅ 共入库 {len(results)} 个文件, {total} 个 chunks") -@app.command() -def search(query: str, top_k: int = 10): - """语义检索.""" - _, searcher, _ = _get_components() +@app.command(help="语义检索已入库的文档.") +def search( + query: Annotated[str, typer.Argument(help="搜索关键词或自然语言查询")], + top_k: Annotated[int, typer.Option("--top-k", "-k", help="返回结果数量 (1-100)")] = 10, + config: ConfigOption = DEFAULT_CONFIG_PATH, +): + _, searcher, _ = _get_components(config) results = searcher.search(query, top_k=top_k) if not results: typer.echo("未找到匹配结果。") @@ -69,18 +96,22 @@ def search(query: str, top_k: int = 10): typer.echo(preview) -@app.command() -def serve(port: int = 8000): - """启动 HTTP 服务.""" +@app.command(help="启动 HTTP API 服务.") +def serve( + port: Annotated[int, typer.Option("--port", "-p", help="监听端口")] = 8000, + config: ConfigOption = DEFAULT_CONFIG_PATH, +): + # serve 模式下 config 仅用于校验路径存在,实际由 server 模块自行加载 typer.echo(f"🚀 启动服务: http://localhost:{port}") typer.echo(f"📖 API 文档: http://localhost:{port}/docs") uvicorn.run("src.server.app:app", host="0.0.0.0", port=port, reload=False) -@app.command() -def stats(): - """查看统计信息.""" - _, searcher, _ = _get_components() +@app.command(help="查看向量库统计信息 (collection、chunks 数量、源文件列表).") +def stats( + config: ConfigOption = DEFAULT_CONFIG_PATH, +): + _, searcher, _ = _get_components(config) info = searcher.get_collection_info() sources = searcher.list_sources() typer.echo(f"📊 Collection: {info['name']}")