sync(server-release): 从 main 同步逻辑改动(不含 API 格式变更)
同步内容: - knowledge/manager.py: VLM max_tokens 512→2048 + reasoning_content fallback - services/feedback.py: FAQ max_tokens 200→512 - services/session.py: 消息排序增加 id DESC 次级排序 - knowledge/image_cleanup.py: 新增图片/VLM缓存孤儿文件清理模块 - knowledge/collection.py: 集合删除时清理孤儿文件 + list_collections 磁盘扫描策略 - knowledge/document.py: 文档删除时清理孤儿文件 - api/chat_routes.py: 新增 _rescue_bm25_divergence 函数(BM25-CE分歧救援) - core/intent_analyzer.py: 使用 get_intent_client 专用客户端 + 移除短追问缓存跳过逻辑 - cleanup_orphans.py: 独立孤儿文件清理脚本
This commit is contained in:
@@ -331,6 +331,21 @@ class CollectionMixin:
|
||||
except Exception as e:
|
||||
logger.warning(f"清理版本记录失败: {e}")
|
||||
|
||||
# 清理不再被引用的图片和 VLM 缓存文件
|
||||
# 注意:此时 ChromaDB collection 已删除,cleanup_image_orphans 会扫描
|
||||
# 所有剩余 collection,仅该 collection 引用的图片会被识别为孤儿
|
||||
try:
|
||||
from knowledge.image_cleanup import cleanup_image_orphans
|
||||
cleanup_result = cleanup_image_orphans(self)
|
||||
if cleanup_result['deleted_images'] or cleanup_result['deleted_caches']:
|
||||
logger.info(
|
||||
f"清理孤儿文件: {cleanup_result['deleted_images']} 图片 + "
|
||||
f"{cleanup_result['deleted_caches']} VLM缓存, "
|
||||
f"释放 {cleanup_result['freed_bytes']/1024:.1f} KB"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理孤儿文件失败: {e}")
|
||||
|
||||
if kb_name in self._metadata.get("collections", {}):
|
||||
del self._metadata["collections"][kb_name]
|
||||
self._save_metadata()
|
||||
@@ -361,8 +376,40 @@ class CollectionMixin:
|
||||
self._metadata = self._load_metadata()
|
||||
result = []
|
||||
|
||||
# 以元数据为唯一真相源,不再扫描磁盘自动补充
|
||||
# (扫描磁盘会导致其他 worker 刚创建/删除的集合被误操作)
|
||||
# 扫描 base_path 下的所有子目录作为向量库
|
||||
# 每个向量库使用独立目录:base_path/my_ky, base_path/public_kb 等
|
||||
actual_collections = []
|
||||
try:
|
||||
if os.path.exists(self.base_path):
|
||||
for item in os.listdir(self.base_path):
|
||||
item_path = os.path.join(self.base_path, item)
|
||||
if os.path.isdir(item_path) and not item.startswith('.'):
|
||||
# 检查是否包含 chroma.sqlite3(有效的向量库目录)
|
||||
if os.path.exists(os.path.join(item_path, 'chroma.sqlite3')):
|
||||
actual_collections.append(item)
|
||||
except Exception as e:
|
||||
logger.warning(f"扫描向量库目录失败: {e}")
|
||||
|
||||
# 如果扫描失败,回退到元数据中的集合列表
|
||||
if not actual_collections:
|
||||
actual_collections = list(self._metadata.get("collections", {}).keys())
|
||||
|
||||
for name in actual_collections:
|
||||
if name not in self._metadata.get("collections", {}):
|
||||
if "collections" not in self._metadata:
|
||||
self._metadata["collections"] = {}
|
||||
self._metadata["collections"][name] = {
|
||||
"display_name": name,
|
||||
"department": "",
|
||||
"description": "",
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
logger.info(f"自动补充向量库元数据: {name}")
|
||||
|
||||
self._save_metadata()
|
||||
|
||||
stale_collections = []
|
||||
|
||||
for name, info in self._metadata.get("collections", {}).items():
|
||||
try:
|
||||
collection = self.get_collection(name)
|
||||
@@ -375,18 +422,17 @@ class CollectionMixin:
|
||||
description=info.get("description", "")
|
||||
))
|
||||
except Exception as e:
|
||||
# 只跳过不修改元数据(可能是其他 worker 刚创建的集合)
|
||||
logger.warning(
|
||||
f"跳过异常向量库 '{name}': {e}"
|
||||
f"跳过异常向量库 '{name}': {e},可能是 ChromaDB 数据目录已丢失"
|
||||
)
|
||||
result.append(CollectionInfo(
|
||||
name=name,
|
||||
display_name=info.get("display_name", name),
|
||||
document_count=0,
|
||||
created_at=info.get("created_at", ""),
|
||||
department=info.get("department", ""),
|
||||
description=info.get("description", "")
|
||||
))
|
||||
stale_collections.append(name)
|
||||
|
||||
# 清理元数据中指向已失效集合的条目
|
||||
if stale_collections:
|
||||
for name in stale_collections:
|
||||
self._metadata.get("collections", {}).pop(name, None)
|
||||
logger.info(f"清理失效向量库元数据: {name}")
|
||||
self._save_metadata()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -72,6 +72,18 @@ class DocumentMixin:
|
||||
except Exception as e:
|
||||
logger.warning(f"清理版本记录失败: {e}")
|
||||
|
||||
# 清理不再被引用的图片和 VLM 缓存文件
|
||||
try:
|
||||
from knowledge.image_cleanup import cleanup_image_orphans
|
||||
cleanup_result = cleanup_image_orphans(self, collections=[kb_name])
|
||||
if cleanup_result['deleted_images'] or cleanup_result['deleted_caches']:
|
||||
logger.info(
|
||||
f"清理孤儿文件: {cleanup_result['deleted_images']} 图片 + "
|
||||
f"{cleanup_result['deleted_caches']} VLM缓存"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"清理孤儿文件失败: {e}")
|
||||
|
||||
logger.info(f"从 {kb_name} 删除文档: {filename}, 片段数: {deleted}")
|
||||
return deleted
|
||||
|
||||
|
||||
140
knowledge/image_cleanup.py
Normal file
140
knowledge/image_cleanup.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
图片/VLM缓存孤儿文件清理模块
|
||||
|
||||
提供可被 document.py / collection.py 调用的清理函数,
|
||||
也可被 cleanup_orphans.py 独立脚本使用。
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IMAGES_DIR = Path(".data/images")
|
||||
VLM_CACHE_DIR = Path(".data/cache/vlm")
|
||||
|
||||
|
||||
def compute_file_hash(file_path: str) -> str:
|
||||
"""计算文件 MD5"""
|
||||
with open(file_path, 'rb') as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
|
||||
def collect_referenced_images(manager, collections=None) -> set:
|
||||
"""
|
||||
从 ChromaDB 收集所有被引用的图片文件名。
|
||||
|
||||
Args:
|
||||
manager: KnowledgeBaseManager 实例
|
||||
collections: 限定知识库列表,None 表示全部
|
||||
|
||||
Returns:
|
||||
set of image filenames (e.g., {"185a7a75d246.png", ...})
|
||||
"""
|
||||
referenced = set()
|
||||
|
||||
if collections:
|
||||
kb_names = collections
|
||||
else:
|
||||
kb_names = [c.name if hasattr(c, 'name') else str(c)
|
||||
for c in manager.list_collections()]
|
||||
|
||||
for kb_name in kb_names:
|
||||
try:
|
||||
col = manager.get_collection(kb_name)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
results = col.get(include=['metadatas'])
|
||||
if not results['ids']:
|
||||
continue
|
||||
|
||||
for meta in results['metadatas']:
|
||||
image_path = meta.get('image_path', '')
|
||||
if image_path:
|
||||
referenced.add(os.path.basename(image_path))
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
def cleanup_image_orphans(manager, collections=None, dry_run=False) -> dict:
|
||||
"""
|
||||
清理不再被任何 ChromaDB 切片引用的图片和 VLM 缓存文件。
|
||||
|
||||
Args:
|
||||
manager: KnowledgeBaseManager 实例
|
||||
collections: 限定知识库列表,None 表示全部
|
||||
dry_run: True 时只返回孤儿列表不实际删除
|
||||
|
||||
Returns:
|
||||
{
|
||||
'orphan_images': [(filepath, filename, size_bytes)],
|
||||
'orphan_caches': [(filepath, filename, size_bytes)],
|
||||
'deleted_images': int,
|
||||
'deleted_caches': int,
|
||||
'freed_bytes': int
|
||||
}
|
||||
"""
|
||||
result = {
|
||||
'orphan_images': [],
|
||||
'orphan_caches': [],
|
||||
'deleted_images': 0,
|
||||
'deleted_caches': 0,
|
||||
'freed_bytes': 0
|
||||
}
|
||||
|
||||
# 1. 收集引用
|
||||
referenced = collect_referenced_images(manager, collections)
|
||||
|
||||
# 2. 查找孤儿图片
|
||||
if IMAGES_DIR.exists():
|
||||
for f in IMAGES_DIR.iterdir():
|
||||
if f.is_file() and f.name not in referenced:
|
||||
result['orphan_images'].append((str(f), f.name, f.stat().st_size))
|
||||
|
||||
# 3. 查找孤儿 VLM 缓存(图片已删除则缓存也应是孤儿)
|
||||
if VLM_CACHE_DIR.exists():
|
||||
referenced_hashes = set()
|
||||
for filename in referenced:
|
||||
full_path = IMAGES_DIR / filename
|
||||
if full_path.exists():
|
||||
try:
|
||||
img_hash = compute_file_hash(str(full_path))
|
||||
referenced_hashes.add(img_hash)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for f in VLM_CACHE_DIR.iterdir():
|
||||
if f.is_file() and f.suffix == '.txt':
|
||||
cache_hash = f.stem
|
||||
if cache_hash not in referenced_hashes:
|
||||
result['orphan_caches'].append((str(f), f.name, f.stat().st_size))
|
||||
|
||||
# 4. 删除
|
||||
if not dry_run:
|
||||
for filepath, filename, size in result['orphan_images']:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
result['deleted_images'] += 1
|
||||
result['freed_bytes'] += size
|
||||
except OSError as e:
|
||||
logger.warning(f"删除孤儿图片失败: {filename} - {e}")
|
||||
|
||||
for filepath, filename, size in result['orphan_caches']:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
result['deleted_caches'] += 1
|
||||
result['freed_bytes'] += size
|
||||
except OSError as e:
|
||||
logger.warning(f"删除孤儿缓存失败: {filename} - {e}")
|
||||
|
||||
if result['deleted_images'] or result['deleted_caches']:
|
||||
logger.info(
|
||||
f"清理孤儿: {result['deleted_images']} 图片 + "
|
||||
f"{result['deleted_caches']} 缓存, "
|
||||
f"释放 {result['freed_bytes']/1024:.1f} KB"
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -654,7 +654,7 @@ class KnowledgeBaseManager(
|
||||
try:
|
||||
from config import get_llm_client, DASHSCOPE_MODEL
|
||||
client = get_llm_client()
|
||||
summary = call_llm(client, prompt, DASHSCOPE_MODEL, max_tokens=512)
|
||||
summary = call_llm(client, prompt, DASHSCOPE_MODEL, max_tokens=2048)
|
||||
return summary.strip() if summary else ""
|
||||
except Exception as e:
|
||||
logger.warning(f"生成表格摘要失败: {e}")
|
||||
@@ -713,13 +713,31 @@ class KnowledgeBaseManager(
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens=512
|
||||
max_tokens=2048 # mimo-v2.5 推理模型思考链消耗 ~1000 token,需留足输出空间
|
||||
)
|
||||
|
||||
description = response.choices[0].message.content
|
||||
|
||||
# 推理模型兼容:content 为空时从 reasoning_content 提取
|
||||
if not description or not description.strip():
|
||||
reasoning = getattr(response.choices[0].message, 'reasoning_content', None)
|
||||
if reasoning and reasoning.strip():
|
||||
import re
|
||||
# 尝试从思考链中提取有用文本(去掉 <think> 标签后的内容)
|
||||
cleaned = re.sub(r'', '', reasoning, flags=re.DOTALL).strip()
|
||||
if cleaned:
|
||||
logger.info(f"VLM content为空,从reasoning_content提取描述: {image_path}")
|
||||
description = cleaned
|
||||
else:
|
||||
description = reasoning.strip()
|
||||
|
||||
if not description:
|
||||
logger.warning(f"VLM 返回空描述: {image_path}")
|
||||
return ""
|
||||
|
||||
# 缓存结果
|
||||
import hashlib
|
||||
import re as _re
|
||||
img_hash = hashlib.md5(img_path.read_bytes()).hexdigest()
|
||||
cache_dir = Path('.data/cache/vlm')
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user