diff --git a/digital-avatar-app/backend/routers/knowledge.py b/digital-avatar-app/backend/routers/knowledge.py index 77b664e..58a9eec 100644 --- a/digital-avatar-app/backend/routers/knowledge.py +++ b/digital-avatar-app/backend/routers/knowledge.py @@ -32,6 +32,14 @@ class EnabledIn(BaseModel): enabled: bool = True +def _doc_payload(doc: KnowledgeDoc) -> dict: + payload = doc.to_dict() + stored_name = os.path.basename(doc.file_url or "") + stored_path = os.path.join(UPLOAD_DIR, doc.avatar_id, stored_name) + payload["filePresent"] = bool(stored_name and os.path.isfile(stored_path)) + return payload + + def _resolve_user(authorization: str | None, db: Session): if not authorization: return None @@ -61,7 +69,7 @@ def list_docs(avatar_id: str, authorization: str = Header(None), db: Session = D .order_by(KnowledgeDoc.created_at.desc()) .all() ) - return ok([d.to_dict() for d in docs]) + return ok([_doc_payload(d) for d in docs]) @router.post("/avatar/{avatar_id}/knowledge/docs") @@ -121,7 +129,7 @@ async def upload_doc(avatar_id: str, file: UploadFile = File(...), authorization db.commit() db.refresh(doc) - return ok(doc.to_dict()) + return ok(_doc_payload(doc)) @router.delete("/avatar/{avatar_id}/knowledge/docs/{doc_id}") diff --git a/digital-avatar-app/backend/tests/test_knowledge_storage.py b/digital-avatar-app/backend/tests/test_knowledge_storage.py new file mode 100644 index 0000000..89c6f3d --- /dev/null +++ b/digital-avatar-app/backend/tests/test_knowledge_storage.py @@ -0,0 +1,23 @@ +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +from routers.knowledge import _doc_payload + + +def test_doc_payload_reports_whether_the_persisted_file_exists(tmp_path: Path): + avatar_id = "avatar-1" + stored_name = "knowledge.md" + doc = SimpleNamespace( + avatar_id=avatar_id, + file_url=f"/api/files/{avatar_id}/{stored_name}", + to_dict=lambda: {"id": "doc-1", "fileUrl": f"/api/files/{avatar_id}/{stored_name}"}, + ) + stored_dir = tmp_path / avatar_id + stored_dir.mkdir() + stored_file = stored_dir / stored_name + + with patch("routers.knowledge.UPLOAD_DIR", str(tmp_path)): + assert _doc_payload(doc)["filePresent"] is False + stored_file.write_text("knowledge", encoding="utf-8") + assert _doc_payload(doc)["filePresent"] is True diff --git a/digital-avatar-app/src/api/index.ts b/digital-avatar-app/src/api/index.ts index fe43335..929be23 100644 --- a/digital-avatar-app/src/api/index.ts +++ b/digital-avatar-app/src/api/index.ts @@ -202,6 +202,7 @@ export interface KnowledgeDoc { fileSize: number fileUrl: string status: string + filePresent?: boolean vectorized?: boolean embeddingModel?: string chunkCount?: number diff --git a/digital-avatar-app/src/views/KnowledgeManage.vue b/digital-avatar-app/src/views/KnowledgeManage.vue index 574673c..ac7d5ee 100644 --- a/digital-avatar-app/src/views/KnowledgeManage.vue +++ b/digital-avatar-app/src/views/KnowledgeManage.vue @@ -37,10 +37,10 @@
{{ doc.filename }} - {{ doc.vectorized ? '已入库' : '处理中' }} + {{ doc.filePresent === false ? '文件缺失' : (doc.vectorized ? '已入库' : '处理中') }}

{{ doc.fileType.toUpperCase() }} · {{ formatSize(doc.fileSize) }} · {{ formatDate(doc.createdAt) }}

-

{{ doc.vectorized ? `已切分 ${doc.chunkCount || 0} 段,可用于对话` : '正在解析并建立知识索引' }}

+

{{ doc.filePresent === false ? '原文件不可用,请删除后重新上传' : (doc.vectorized ? `已切分 ${doc.chunkCount || 0} 段,可用于对话` : '正在解析并建立知识索引') }}

@@ -268,6 +268,7 @@ onMounted(async () => { .card-title-row { display: flex; align-items: center; gap: 8px; min-width: 0; } .card-title-row strong { min-width: 0; flex: 1; overflow: hidden; color: #27201C; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } .status-pill { flex: 0 0 auto; display: inline-flex; padding: 4px 7px; border-radius: 999px; color: #15803D; background: #ECFDF3; font-size: 10px; white-space: nowrap; }.status-pill.pending { color: #B45309; background: #FFFBEB; } +.status-pill.missing { color: #B91C1C; background: #FEF2F2; } .card-meta, .card-detail { margin: 5px 0 0; color: #9398AE; font-size: 11px; line-height: 1.4; }.card-detail { color: #8B6B58; } .card-delete { flex: 0 0 auto; align-self: center; border: 0; color: #EF4444; background: #FEF2F2; border-radius: 8px; padding: 7px 9px; font-size: 12px; cursor: pointer; } .card-empty { padding: 42px 16px; border: 1px dashed #F1D9C3; border-radius: 16px; color: #9398AE; background: #fff; font-size: 14px; text-align: center; }