- P1 vstore: chunks.py 切块(600字/8段/重叠1/上限200,表格原位聚合,outline 不参与)+ChunkIndex 三文件原子写+无降级档;embed_batch_chunked 批量16x并发4; vault add/delete/rebuild_chunks 钩子(chunk 失败不阻断 doc 级);search-chunks/ rebuild-chunks 端点+health chunk_count;rebuild CLI - P2 BFF: privateqa.ask_private(同doc≤3去重/12k截断/40s超时/带引用短答); POST /api/qa/private 三态;schemas PrivateDocBlock 入 union;client.search_chunks; llm timeout 参数;main.py 注册 - P3 前端: _firePrivate 并行检索+竞态防护(pending/flush/防重入/跨轮归属); AssistantMessage private_doc 块;i18n/private.js;PrivateLibraryPane 挂 DocPane 空态;removeLibrary(204 兜底) 自测: vstore pytest 26/27(1 失败为 T1 骨架笔误待修)+74 回归;前端 vitest 373/373;npm run build 0 error;sdk 222 passed(3 failed 为基线预存);红线 diff 空。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.5 KiB
Python
90 lines
3.5 KiB
Python
"""settings.py —— vstore 环境变量驱动配置(dataclass,不引 pydantic)。
|
||
|
||
环境变量键:
|
||
- VSTORE_KEY:Fernet key,必填(缺失抛异常拒绝启动)
|
||
- VSTORE_ROOT:数据根目录(默认 data/vstore)
|
||
- VSTORE_VAULT_PATH / VSTORE_INDEX_PATH:可覆盖(本地路径或 http(s):// URL)
|
||
- VSTORE_EMBED_URL:ollama 嵌入服务(默认 http://127.0.0.1:11434)
|
||
- VSTORE_EMBED_MODEL:嵌入模型(默认 bge-m3:567m)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
|
||
|
||
# ---- 生成参数(2026-09-04 手工测试:上传不再设大小/数量/字符门槛)----
|
||
# EMBED_BODY_CAP 仅是嵌入分块预算(向量化截断),不是上传限制;
|
||
# 原文件全文始终加密入库,不受影响。
|
||
MAX_SECTIONS = 12 # 模版大纲裁剪上限(节数)
|
||
MAX_OUTLINE_CHUNKS = 3 # 大纲嵌入切块上限
|
||
EMBED_BODY_CAP = 200_000 # 正文嵌入分块预算(字符)
|
||
|
||
# ---- 检索参数 ----
|
||
EMBED_DIM = 1024 # bge-m3:567m 输出维度
|
||
DEFAULT_TOP_K = 5
|
||
MAX_TOP_K = 20
|
||
OUTLINE_EMBED_MAX_CHARS = 7500 # 大纲超过此长度按节切块(≤MAX_OUTLINE_CHUNKS)
|
||
FALLBACK_LATEST_N = 8 # 三档降级最后一档:最新 N 条
|
||
|
||
# ---- 嵌入服务 ----
|
||
DEFAULT_EMBED_URL = "http://127.0.0.1:11434"
|
||
DEFAULT_EMBED_MODEL = "bge-m3:567m"
|
||
EMBED_CONCURRENCY = 8 # embed_batch 并发上限
|
||
EMBED_TIMEOUT = 60.0
|
||
|
||
|
||
@dataclass
|
||
class VstorePaths:
|
||
"""vstore 数据路径(vault/index 可分别指向本地目录或远程 http(s):// 根)。"""
|
||
|
||
root: str = field(default_factory=lambda: os.environ.get("VSTORE_ROOT", "data/vstore"))
|
||
vault_path: str = field(default_factory=lambda: os.environ.get("VSTORE_VAULT_PATH", ""))
|
||
index_path: str = field(default_factory=lambda: os.environ.get("VSTORE_INDEX_PATH", ""))
|
||
chunk_path: str = field(default_factory=lambda: os.environ.get("VSTORE_CHUNK_PATH", ""))
|
||
embed_url: str = field(default_factory=lambda: os.environ.get("VSTORE_EMBED_URL", DEFAULT_EMBED_URL))
|
||
embed_model: str = field(default_factory=lambda: os.environ.get("VSTORE_EMBED_MODEL", DEFAULT_EMBED_MODEL))
|
||
|
||
def __post_init__(self) -> None:
|
||
# vault/index 未显式指定时落在 root 下
|
||
if not self.vault_path:
|
||
self.vault_path = str(Path(self.root) / "vault")
|
||
if not self.index_path:
|
||
self.index_path = str(Path(self.root) / "index")
|
||
if not self.chunk_path:
|
||
self.chunk_path = str(Path(self.root) / "chunks")
|
||
|
||
|
||
def get_fernet_key() -> bytes:
|
||
"""VSTORE_KEY(Fernet key,必填)。缺失抛异常(阻止启动)。
|
||
|
||
生成方式:
|
||
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||
"""
|
||
raw = os.environ.get("VSTORE_KEY", "")
|
||
if not raw:
|
||
raise RuntimeError(
|
||
"缺少环境变量 VSTORE_KEY(Fernet key,必填)。生成:"
|
||
'python -c "from cryptography.fernet import Fernet; '
|
||
'print(Fernet.generate_key().decode())"'
|
||
)
|
||
return raw.encode("utf-8")
|
||
|
||
|
||
_paths: VstorePaths | None = None
|
||
|
||
|
||
def get_paths() -> VstorePaths:
|
||
"""取全局路径配置(首次调用构造)。"""
|
||
global _paths
|
||
if _paths is None:
|
||
_paths = VstorePaths()
|
||
return _paths
|
||
|
||
|
||
def set_paths(paths: VstorePaths) -> None:
|
||
"""注入配置(测试/启动用)。"""
|
||
global _paths
|
||
_paths = paths
|