Files
PlantAI-rag-new/vstore/api.py
T
jerryandClaude Fable 5 5db3ead73e feat(docgen): doc_writer 能力全链——vstore 独立服务 + BFF docgen 路由 + 前端文档工作台
- vstore 独立安装包(8094):解析/嵌入/加密 vault/faiss 检索,存储路径支持本地 file 与 http(s):// URL,启动握手校验,集群化预留(embed 独立模块)
- BFF docgen 路由:上传入库/generate/编辑/导出/下载/主库管理,意图护栏(与前端同词典),vstore 失败兜底,quota 507 与 TTL 惰性清理
- 前端:自然触发意图路由、拖拽上传 chips、能力徽标、右侧文档 TAB(WYSIWYG 编辑/prompt 修改/PDF+DOCX 导出下载)、右栏拖宽+手柄仿左栏
- 批量导入工具 docgen_import.py(并发 8、导入即加密+删源明文)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:13:37 +08:00

150 lines
4.8 KiB
Python

"""api.py —— vstore FastAPI 服务(8094)。
端点(docs/team/brief_dev1_p1.md):
- POST /api/vstore/ingest multipart: file + name/category/description/tags/as_template
- GET /api/vstore/search ?q&top_k&category&as_template
- GET /api/vstore/documents/{id} 明文全文 + outline + meta(仅生成时取)
- GET /api/vstore/documents/{id}/style
- DELETE /api/vstore/documents/{id}
- POST /api/vstore/rebuild-index
- GET /api/vstore/health
"""
from __future__ import annotations
import logging
from typing import Any
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from . import vault as _vault
from .backend import BackendError, handshake, parse_backend
from .settings import (
DEFAULT_TOP_K,
MAX_TOP_K,
get_paths,
)
from .vault import VaultError
logger = logging.getLogger(__name__)
app = FastAPI(title="PlantAI vstore", version="0.1.0")
def _error(code: str, message: str, status: int, degraded: bool = False) -> JSONResponse:
"""错误信封(与 BFF 现有格式一致:{ok, error:{code, message, degraded}})。"""
return JSONResponse(
status_code=status,
content={"ok": False, "error": {"code": code, "message": message, "degraded": degraded}},
)
# ---- 启动钩子:解析后端 + 握手校验(失败抛异常拒绝启动)----
def _init_service() -> None:
paths = get_paths()
backend = parse_backend(paths.vault_path, paths.index_path)
try:
handshake(backend)
except BackendError as exc:
logger.error("vstore: 启动握手失败,拒绝启动:%s", exc)
raise
_vault.set_backend(backend)
_vault.init_vault()
@app.on_event("startup")
def _startup() -> None:
_init_service()
# ---- 端点 ----
@app.get("/api/vstore/health")
async def health() -> dict[str, Any]:
"""健康检查:后端 ok + 库存量。"""
vault = _vault.get_vault()
count = vault.index.count() if hasattr(vault, "index") else len(vault.list_rows())
return {"ok": True, "count": count, "backend": "local"}
@app.post("/api/vstore/ingest")
async def ingest(
file: UploadFile = File(...),
name: str = Form(""),
category: str = Form(""),
description: str = Form(""),
tags: str = Form(""),
as_template: bool = Form(False),
) -> dict[str, Any]:
"""multipart 入库。文件解析/嵌入/加密落盘/索引由 vault.add 完成。"""
content = await file.read()
try:
item = _vault.get_vault().add(
content,
filename=file.filename or "document",
meta={"name": name, "category": category, "description": description, "tags": tags},
as_template=bool(as_template),
)
except VaultError as exc:
return _error("VALIDATION", str(exc), 422)
return {"ok": True, "item": item}
@app.get("/api/vstore/search")
async def search(
q: str = "",
top_k: int = DEFAULT_TOP_K,
category: str = "",
as_template: bool | None = None,
) -> dict[str, Any]:
"""三档降级检索。"""
try:
result = _vault.get_vault().search(
q,
top_k=max(1, min(top_k, MAX_TOP_K)),
category=category or None,
as_template=as_template,
)
return {"ok": True, **result}
except VaultError as exc:
return _error("INTERNAL", str(exc), 500, degraded=True)
@app.get("/api/vstore/documents/{doc_id}")
async def get_document(doc_id: str) -> dict[str, Any]:
"""明文全文 + outline + meta(仅生成时取)。"""
doc = _vault.get_vault().get_document_json(doc_id)
if doc is None:
raise HTTPException(status_code=404, detail="document not found")
return {"ok": True, "document": doc}
@app.get("/api/vstore/documents/{doc_id}/style")
async def get_style(doc_id: str) -> dict[str, Any]:
"""docx 样式提取(导出对齐用)。"""
style = _vault.get_vault().get_style(doc_id)
if style is None:
raise HTTPException(status_code=404, detail="document not found")
return {"ok": True, "style_hints": style}
@app.delete("/api/vstore/documents/{doc_id}")
async def delete_document(doc_id: str) -> dict[str, Any]:
"""删除:索引行 + 密文 secure_delete。"""
removed = _vault.get_vault().delete(doc_id)
return {"ok": True, "removed": removed}
@app.post("/api/vstore/rebuild-index")
async def rebuild_index() -> dict[str, Any]:
"""重建索引:全部密文重新解密 → 嵌入 → 原子切换。
明文只在内存流转;任何一条失败(密钥不符/嵌入失败)整库拒绝切换。
"""
vault = _vault.get_vault()
rebuilt = vault.rebuild_index()
if rebuilt["status"] == "error":
return _error("INTERNAL", rebuilt["message"], 500, degraded=True)
return {"ok": True, "count": rebuilt["count"]}