Files
rag/api/kb_routes.py
lacerate551 183a57e7f1 refactor(api): 统一响应格式迁移 + 异步任务系统 + 状态码体系完善
- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式
- 修复 sync_routes.py error_response 参数错误(P0)
- 新增异步任务系统:task_registry + task_routes
- 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040)
- 修正 task_routes/exam_pkg 中语义不匹配的状态码
- 更新 curl 测试手册、后端对接规范文档
- 添加缓存性能报告和 Redis 迁移计划
2026-06-05 22:56:00 +08:00

908 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
多向量库管理 API
本模块提供向量库的 CRUD 操作和文档同步功能,支持:
- 创建、查询、修改、删除向量库
- 文档向量化同步
- 文档版本管理(废止/恢复)
- 知识库路由测试
路由列表:
GET /collections : 获取向量库列表
POST /collections : 创建向量库
PUT /collections/<kb_name> : 修改向量库信息
DELETE /collections/<kb_name> : 删除向量库
GET /collections/<kb_name>/documents : 获取向量库文档列表
GET /collections/<kb_name>/chunks : 获取向量库切片列表
POST /documents/sync : 触发文档同步
POST /kb/route : 测试知识库路由(调试)
文档版本管理:
POST /collections/<kb_name>/documents/<path:filename>/deprecate : 废止文档
POST /collections/<kb_name>/documents/<path:filename>/restore : 恢复文档
GET /collections/<kb_name>/documents/<path:filename>/versions : 版本历史
架构说明:
- 权限验证由后端网关完成RAG 服务不做权限判断
- 每个部门可拥有独立的向量库(多租户架构)
- 向量库元数据存储在 kb_metadata.json
Example:
curl -X GET http://localhost:5001/collections \\
-H "Authorization: Bearer mock-token-admin"
"""
import os
from typing import Tuple, Optional, Any
from flask import Blueprint, request, jsonify, current_app
import logging
from core.status_codes import (
SUCCESS, CREATED, DELETE_SUCCESS, UPDATE_SUCCESS, SYNC_SUCCESS,
BAD_REQUEST, NOT_FOUND, COLLECTION_NOT_FOUND, NO_COLLECTION, TASK_CONFLICT,
INTERNAL_ERROR, SERVICE_UNAVAILABLE, SYNC_ERROR, REINDEX_ERROR
)
from api.response_utils import success_response, error_response
logger = logging.getLogger(__name__)
from auth.gateway import require_gateway_auth
kb_bp = Blueprint('kb', __name__)
# 延迟初始化缓存
_kb_manager = None
_kb_router = None
_kb_checked = False
_has_multi_kb = False
def _check_multi_kb() -> bool:
"""
检查多向量库模块是否可用
延迟导入知识库管理器和路由器,避免循环依赖。
结果缓存在模块级变量中,后续调用直接返回缓存。
Returns:
多向量库可用返回 True否则返回 False
"""
global _kb_checked, _has_multi_kb, _kb_manager, _kb_router
if not _kb_checked:
try:
from knowledge.manager import get_kb_manager
from knowledge.router import get_kb_router
_kb_manager = get_kb_manager()
_kb_router = get_kb_router()
_has_multi_kb = True
except ImportError as e:
logger.warning(f"多向量库模块导入失败: {e}")
_has_multi_kb = False
_kb_checked = True
return _has_multi_kb
def _require_multi_kb() -> Tuple[Optional[Any], Optional[Any], Optional[Tuple]]:
"""
检查多向量库是否可用,返回管理器和路由器
Returns:
元组 (kb_manager, kb_router, error_response)
- 成功时 error_response 为 None
- 失败时 kb_manager 和 kb_router 为 None
Example:
>>> kb_manager, kb_router, err = _require_multi_kb()
>>> if err:
... return err
"""
if not _check_multi_kb():
return None, None, (jsonify({"error": "多向量库模块未启用"}), 503)
return _kb_manager, _kb_router, None
@kb_bp.route('/collections', methods=['GET'])
@require_gateway_auth
def list_collections() -> Tuple[Any, int]:
"""
获取向量库列表
返回当前用户可访问的所有向量库信息,包括:
- 向量库名称和显示名称
- 文档数量
- 所属部门
- 创建时间
- 描述信息
Returns:
JSON 响应,包含 collections 列表和 total 总数
Example:
GET /collections
Response: {"collections": [...], "total": 3}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
user = request.current_user
# 获取所有向量库(权限由后端网关管理)
all_collections = kb_manager.list_collections()
result = []
for coll in all_collections:
result.append({
"name": coll.name,
"display_name": coll.display_name,
"document_count": coll.document_count,
"department": coll.department,
"created_at": coll.created_at,
"description": coll.description
})
return success_response(data={"collections": result, "total": len(result)})
@kb_bp.route('/collections', methods=['POST'])
@require_gateway_auth
def create_collection() -> Tuple[Any, int]:
"""
创建新向量库
请求体:
{
"name": "向量库标识(英文)",
"display_name": "显示名称",
"department": "所属部门",
"description": "描述信息"
}
注意:
- 名称只能包含字母、数字、下划线和连字符
- 名称不能为空
Returns:
成功: {"success": true, "message": "...", "name": "..."} (201)
失败: {"error": "..."} (400)
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
data = request.json or {}
name = data.get('name', '').strip()
display_name = data.get('display_name', '')
department = data.get('department', '')
description = data.get('description', '')
if not name:
return error_response("INVALID_NAME", BAD_REQUEST, "向量库名称不能为空", http_status=400)
# 验证名称格式ChromaDB 限制)
if not name.replace('_', '').replace('-', '').isalnum():
return error_response("INVALID_NAME_FORMAT", BAD_REQUEST, "向量库名称只能包含字母、数字、下划线和连字符", http_status=400)
success, message = kb_manager.create_collection(
name, display_name, department, description
)
if success:
return success_response(data={"name": name}, status_code=CREATED, message=message, http_status=201)
return error_response("CREATE_FAILED", BAD_REQUEST, message, http_status=400)
@kb_bp.route('/collections/<kb_name>', methods=['PUT'])
@require_gateway_auth
def update_collection(kb_name: str) -> Tuple[Any, int]:
"""
修改向量库信息
Args:
kb_name: 向量库名称
请求体:
{
"display_name": "新显示名称",
"description": "新描述"
}
Returns:
成功: {"success": true, "message": "向量库信息已更新"}
失败: {"error": "..."} (404/500)
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
data = request.json or {}
display_name = data.get('display_name')
description = data.get('description')
# 检查向量库是否存在
collections = kb_manager.list_collections()
if not any(c.name == kb_name for c in collections):
return error_response("COLLECTION_NOT_FOUND", COLLECTION_NOT_FOUND, f"向量库 '{kb_name}' 不存在", http_status=404)
# 更新元数据
success = kb_manager.update_collection_metadata(
kb_name,
display_name=display_name,
description=description
)
if success:
return success_response(data=None, status_code=UPDATE_SUCCESS, message="向量库信息已更新")
return error_response("UPDATE_FAILED", INTERNAL_ERROR, "更新失败", http_status=500)
@kb_bp.route('/collections/<kb_name>', methods=['DELETE'])
@require_gateway_auth
def delete_collection(kb_name: str) -> Tuple[Any, int]:
"""
删除向量库
Args:
kb_name: 向量库名称
查询参数:
delete_documents: 是否删除文档源文件(默认 false
Returns:
成功: {"success": true, "message": "...", "deleted_documents": bool}
失败: {"error": "..."} (400)
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
# 获取参数
delete_documents = request.args.get('delete_documents', 'false').lower() == 'true'
success, message = kb_manager.delete_collection(kb_name, delete_documents)
if success:
return success_response(data={"deleted_documents": delete_documents}, status_code=DELETE_SUCCESS, message=message)
return error_response("DELETE_FAILED", BAD_REQUEST, message, http_status=400)
@kb_bp.route('/collections/<kb_name>/documents', methods=['GET'])
@require_gateway_auth
def list_collection_documents(kb_name: str) -> Tuple[Any, int]:
"""
获取向量库中的文档列表
Args:
kb_name: 向量库名称
Returns:
{"collection": "...", "documents": [...], "total": N}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
documents = kb_manager.list_documents(kb_name)
return success_response(data={"collection": kb_name, "documents": documents, "total": len(documents)})
@kb_bp.route('/collections/<kb_name>/chunks', methods=['GET'])
@require_gateway_auth
def list_collection_chunks(kb_name: str) -> Tuple[Any, int]:
"""
获取向量库中的切片列表
Args:
kb_name: 向量库名称
查询参数:
document_id: 过滤指定文档的切片(可选)
limit: 返回数量限制(默认 100
offset: 偏移量(默认 0
Returns:
{"collection": "...", "chunks": [...], "total": N}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
# 可选过滤参数
document_id = request.args.get('document_id')
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
chunks = kb_manager.list_chunks(kb_name, document_id=document_id, limit=limit, offset=offset)
return success_response(data={"collection": kb_name, "chunks": chunks, "total": len(chunks)})
@kb_bp.route('/documents/sync', methods=['POST'])
@require_gateway_auth
def sync_documents() -> Tuple[Any, int]:
"""
触发文档向量化同步(异步任务)
立即返回 task_id后台线程执行同步。
请求体:
{
"collection": "向量库名称" // 可选,不传则同步所有
}
Returns:
{"success": true, "data": {"task_id": "xxx", "message": "..."}}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
from config import DOCUMENTS_PATH
data = request.json or {}
target_collection = data.get('collection')
# 确定要同步的向量库
if target_collection:
collections_to_sync = [target_collection]
else:
all_collections = kb_manager.list_collections()
collections_to_sync = [c.name for c in all_collections]
if not collections_to_sync:
return error_response("NO_COLLECTION", NO_COLLECTION, "没有可同步的向量库", http_status=400)
sync_service = current_app.config.get('SYNC_SERVICE')
if not sync_service:
return error_response("SERVICE_UNAVAILABLE", SERVICE_UNAVAILABLE, "同步服务不可用", http_status=503,
results=[{"collection": c, "status": "warning", "message": "同步服务不可用"} for c in collections_to_sync],
synced_count=0
)
from core.task_registry import get_registry
registry = get_registry()
# 检查是否有正在运行的同步任务
running = registry.list_tasks(status='running', task_type='sync', limit=1)
if running:
return error_response("TASK_RUNNING", TASK_CONFLICT, f"同步任务正在执行中 (task_id: {running[0].id})", http_status=409)
desc = f"文档同步: {target_collection or '所有向量库'}"
task = registry.create_task('sync', desc)
def _do_sync(task, sync_svc):
processed = [0]
def on_change(change):
processed[0] += 1
registry.update_progress(
task.id, current=processed[0],
stage='处理文件',
message=f"已处理: {change.document_name if hasattr(change, 'document_name') else change.document_id}"
)
old_callback = sync_svc.on_change_callback
sync_svc.on_change_callback = on_change
try:
registry.update_progress(task.id, stage='扫描文档', message='正在检测变更...')
sync_result = sync_svc.sync_now()
return {
'collection': target_collection or 'all',
'status': 'success',
'message': f"同步完成: 处理 {sync_result.documents_processed} 个文档",
'details': {
'added': sync_result.documents_added,
'modified': sync_result.documents_modified,
'deleted': sync_result.documents_deleted,
'errors': sync_result.errors,
}
}
finally:
sync_svc.on_change_callback = old_callback
registry.start_task(task.id, _do_sync, sync_service)
return success_response(
data={"task_id": task.id, "message": f"同步任务已启动,通过 GET /tasks/{task.id} 查询进度"},
status_code=SYNC_SUCCESS,
message="同步任务已启动"
)
@kb_bp.route('/debug/scan', methods=['GET'])
@require_gateway_auth
def debug_scan() -> Tuple[Any, int]:
"""
调试:查看同步服务扫描的文件
返回文档目录的文件列表和同步服务状态,
用于排查文档同步问题。
Returns:
{
"documents_path": "...",
"exists": bool,
"files": [...],
"sync_service_path": "...",
"scanned_count": N
}
"""
from config import DOCUMENTS_PATH
import os
result = {
"documents_path": DOCUMENTS_PATH,
"exists": os.path.exists(DOCUMENTS_PATH),
"files": []
}
if os.path.exists(DOCUMENTS_PATH):
for root, dirs, files in os.walk(DOCUMENTS_PATH):
for f in files:
fp = os.path.join(root, f)
rel = os.path.relpath(fp, DOCUMENTS_PATH)
result["files"].append({
"rel_path": rel,
"size": os.path.getsize(fp),
"ext": os.path.splitext(f)[1].lower()
})
# 也检查同步服务的路径
sync_service = current_app.config.get('SYNC_SERVICE')
if sync_service:
result["sync_service_path"] = sync_service.documents_path
result["sync_service_path_exists"] = os.path.exists(sync_service.documents_path)
# 直接调用 scan_documents
try:
scanned = sync_service.scan_documents()
result["scanned_count"] = len(scanned)
result["scanned_ids"] = list(scanned.keys())[:10]
except Exception as e:
logger.error(f"扫描异常: {e}")
result["scan_error"] = "扫描失败"
return jsonify(result)
@kb_bp.route('/collections/<kb_name>/reindex', methods=['POST'])
@require_gateway_auth
def reindex_collection(kb_name: str) -> Tuple[Any, int]:
"""
强制重新向量化指定集合的所有文档(异步任务)
清除该集合的文档哈希记录,触发完整重新索引。
立即返回 task_id后台线程执行重建。
Args:
kb_name: 向量库名称
Returns:
{"success": true, "data": {"task_id": "xxx", "message": "..."}}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
from config import DOCUMENTS_PATH
# 清除该集合的文档哈希记录(同步完成,很快)
try:
from data.db import get_connection
with get_connection("knowledge") as conn:
cursor = conn.cursor()
escaped_kb = kb_name.replace('%', '\\%').replace('_', '\\_')
cursor.execute("DELETE FROM document_hashes WHERE document_id LIKE ? ESCAPE '\\' OR document_id LIKE ? ESCAPE '\\'",
(f"{escaped_kb}/%", f"{escaped_kb}\\%"))
deleted = cursor.rowcount
logger.info(f"已清除 {deleted} 条哈希记录: {kb_name}")
except Exception as e:
logger.warning(f"清除哈希记录失败: {e}")
sync_service = current_app.config.get('SYNC_SERVICE')
if not sync_service:
return error_response("SERVICE_UNAVAILABLE", SERVICE_UNAVAILABLE, "同步服务不可用", http_status=503)
from core.task_registry import get_registry
registry = get_registry()
# 检查是否有正在运行的重建任务
running = registry.list_tasks(status='running', task_type='reindex', limit=1)
if running:
return error_response("TASK_RUNNING", TASK_CONFLICT, f"重建任务正在执行中 (task_id: {running[0].id})", http_status=409)
task = registry.create_task('reindex', f'重建索引: {kb_name}')
def _do_reindex(task, sync_svc, kb):
"""后台执行重建索引"""
processed = [0]
def on_change(change):
processed[0] += 1
registry.update_progress(
task.id,
current=processed[0],
stage='重新索引',
message=f"已处理: {change.document_name if hasattr(change, 'document_name') else change.document_id}"
)
old_callback = sync_svc.on_change_callback
sync_svc.on_change_callback = on_change
try:
registry.update_progress(task.id, stage='扫描文档', message='正在检测变更...')
result = sync_svc.sync_now()
return {
'message': f"重新索引完成: 处理 {result.documents_processed} 个文档",
'documents_processed': result.documents_processed,
'documents_added': result.documents_added,
'errors': result.errors,
}
finally:
sync_svc.on_change_callback = old_callback
registry.start_task(task.id, _do_reindex, sync_service, kb_name)
return success_response(
data={"task_id": task.id, "message": f"重建索引任务已启动: {kb_name},通过 GET /tasks/{task.id} 查询进度"},
status_code=SYNC_SUCCESS,
message="重建索引任务已启动"
)
@kb_bp.route('/kb/route', methods=['POST'])
@require_gateway_auth
def test_routing() -> Tuple[Any, int]:
"""
测试知识库路由(调试用)
分析查询意图,返回目标向量库列表和意图分析结果。
请求体:
{"query": "用户查询"}
Returns:
{
"query": "...",
"user_role": "...",
"user_department": "...",
"target_collections": [...],
"intent": {
"is_general": bool,
"department": "...",
"confidence": float,
"keywords": [...],
"reason": "..."
}
}
"""
_, kb_router, err = _require_multi_kb()
if err:
return err
from knowledge.router import route_query
user = request.current_user
data = request.json or {}
query = data.get('query', '')
if not query:
return error_response("MISSING_PARAMS", BAD_REQUEST, "请提供查询内容", http_status=400)
# 获取路由结果
target_kbs = route_query(
query,
user.get("role", "user"),
user.get("department", "")
)
# 获取意图分析
intent = kb_router.analyze_intent(query)
return success_response(data={
"query": query,
"user_role": user.get("role"),
"user_department": user.get("department", ""),
"target_collections": target_kbs,
"intent": {
"is_general": intent.is_general,
"department": intent.department,
"confidence": intent.confidence,
"keywords": intent.keywords,
"reason": intent.reason
}
})
# ==================== 文档版本管理 API ====================
@kb_bp.route('/collections/<kb_name>/documents/<path:filename>/deprecate', methods=['POST'])
@require_gateway_auth
def deprecate_document(kb_name: str, filename: str) -> Tuple[Any, int]:
"""
废止文档(软删除)
将文档标记为已废止,相关切片在检索时被过滤。
文档数据保留,可通过 restore_document 恢复。
Args:
kb_name: 向量库名称
filename: 文档文件名
请求体:
{"reason": "废止原因"}
Returns:
{
"success": true,
"deprecated_chunks": N,
"document_id": "...",
"collection": "...",
"deprecated_date": "ISO 8601 时间"
}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
user = request.current_user
data = request.json or {}
reason = data.get('reason', '文档已废止')
try:
result = kb_manager.deprecate_document(
kb_name,
filename,
reason,
deprecated_by=user.get('user_id', 'unknown')
)
return success_response(data=result)
except Exception as e:
logger.error(f"操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@kb_bp.route('/collections/<kb_name>/documents/<path:filename>/restore', methods=['POST'])
@require_gateway_auth
def restore_document(kb_name: str, filename: str) -> Tuple[Any, int]:
"""
恢复已废止的文档
将已废止的文档恢复为有效状态,相关切片重新参与检索。
Args:
kb_name: 向量库名称
filename: 文档文件名
Returns:
{
"success": true,
"restored_chunks": N,
"document_id": "...",
"collection": "..."
}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
try:
result = kb_manager.restore_document(kb_name, filename)
return success_response(data=result)
except Exception as e:
logger.error(f"操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@kb_bp.route('/collections/<kb_name>/documents/<path:filename>/versions', methods=['GET'])
@require_gateway_auth
def get_document_versions(kb_name: str, filename: str) -> Tuple[Any, int]:
"""
获取文档版本历史
返回文档的所有版本记录,包括状态变更、时间戳等信息。
Args:
kb_name: 向量库名称
filename: 文档文件名
查询参数:
limit: 返回数量限制(默认 10
Returns:
{
"success": true,
"document_id": "...",
"collection": "...",
"versions": [
{
"version": "v2",
"status": "active",
"created_at": "ISO 8601",
"chunk_count": N
},
...
],
"total": N
}
"""
_, _, err = _require_multi_kb()
if err:
return err
limit = request.args.get('limit', 10, type=int)
try:
from knowledge.document_versions import get_version_query
version_query = get_version_query()
versions = version_query.get_document_history(kb_name, filename, limit)
versions_data = [v.to_dict() for v in versions]
return success_response(data={
"document_id": filename,
"collection": kb_name,
"versions": versions_data,
"total": len(versions_data)
})
except Exception as e:
logger.error(f"操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@kb_bp.route('/collections/<kb_name>/update-image-descriptions', methods=['POST'])
@require_gateway_auth
def update_image_descriptions(kb_name: str) -> Tuple[Any, int]:
"""
更新图片切片的轻量级描述
重新提取已入库文档中图片切片的图号/表号信息,
生成新的轻量级描述,提高检索准确度。
适用场景:
- 文档入库时未提取图号
- 图号提取规则更新后需要重新处理
Args:
kb_name: 向量库名称
Returns:
{
"success": true,
"image_count": N,
"updated_count": M
}
"""
kb_manager, _, err = _require_multi_kb()
if err:
return err
try:
result = kb_manager.update_image_descriptions(kb_name)
return success_response(data=result)
except Exception as e:
logger.error(f"操作异常: {e}")
return error_response("OPERATION_FAILED", INTERNAL_ERROR, "操作失败,请稍后重试", http_status=500)
@kb_bp.route('/collections/sync-vlm-cache', methods=['POST'])
@require_gateway_auth
def sync_vlm_cache() -> Tuple[Any, int]:
"""
同步 VLM 缓存到向量库元数据
将 .data/cache/vlm/ 中已生成的图片描述同步到向量库的 vlm_desc 字段。
同步后,检索时可直接从向量库获取 VLM 描述,无需读取文件系统。
请求参数:
- collection: 可选,指定同步的集合名,不传则同步所有集合
- dry_run: 可选,设为 true 只预览不执行
Returns:
{
"success": true,
"total_cache_files": N,
"synced_count": M,
"skipped_count": K,
"details": [...]
}
"""
from pathlib import Path
import hashlib
import json as json_module
kb_manager, _, err = _require_multi_kb()
if err:
return err
data = request.get_json() or {}
collection_filter = data.get('collection')
dry_run = data.get('dry_run', False)
vlm_cache_dir = Path(".data/cache/vlm")
images_dir = Path(".data/images")
if not vlm_cache_dir.exists():
return error_response("VLM_CACHE_NOT_FOUND", BAD_REQUEST, "VLM 缓存目录不存在", http_status=400)
# 获取所有 VLM 缓存文件
cache_files = list(vlm_cache_dir.glob("*.txt"))
if not cache_files:
return success_response(data={"total_cache_files": 0, "synced_count": 0}, message="无 VLM 缓存文件")
# 构建图片 MD5 → 文件名 的映射
image_hash_map = {}
if images_dir.exists():
for img_file in images_dir.glob("*.png"):
img_hash = hashlib.md5(img_file.read_bytes()).hexdigest()
image_hash_map[img_hash] = img_file.name
# 结果统计
synced_count = 0
skipped_count = 0
details = []
# 获取所有集合
collections = kb_manager.list_collections()
if collection_filter:
collections = [c for c in collections if c.name == collection_filter]
for cache_file in cache_files:
cache_hash = cache_file.stem # 文件名即 MD5
# 查找对应的图片文件名
image_filename = image_hash_map.get(cache_hash)
if not image_filename:
skipped_count += 1
details.append({"cache": cache_file.name, "status": "skipped", "reason": "图片文件不存在"})
continue
# 读取 VLM 描述
vlm_desc = cache_file.read_text(encoding='utf-8')
# 在所有集合中查找该图片切片
found = False
for coll in collections:
collection = kb_manager.get_collection(coll.name)
if not collection:
continue
# 查找 image_path 匹配的切片
result = collection.get(where={"image_path": image_filename})
if result['ids']:
found = True
if not dry_run:
# 更新元数据
updated_metadatas = []
for meta in result['metadatas']:
meta['vlm_desc'] = vlm_desc
meta['has_vlm_desc'] = True
updated_metadatas.append(meta)
collection.update(
ids=result['ids'],
metadatas=updated_metadatas
)
synced_count += len(result['ids'])
details.append({
"cache": cache_file.name,
"image": image_filename,
"collection": coll.name,
"count": len(result['ids']),
"status": "synced" if not dry_run else "preview"
})
if not found:
skipped_count += 1
details.append({"cache": cache_file.name, "image": image_filename, "status": "skipped", "reason": "向量库中未找到对应切片"})
return success_response(data={
"total_cache_files": len(cache_files),
"synced_count": synced_count,
"skipped_count": skipped_count,
"dry_run": dry_run,
"details": details[:20] # 只返回前 20 条详情
})