From 89f2f9a0450ed029dcc20e7cdfb37841ca5583fc Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 24 Apr 2026 14:18:25 +0800 Subject: [PATCH] feat(citation): support downloading cited documents with allow_download toggle Added `allow_download` flag to citation config and `download_url` field to citation output. Implemented `/citations/{document_id}/download` endpoint to serve original files when enabled. Removed unused `files` field and `HttpRequestDataProcessing` model from HTTP request node config. --- api/app/controllers/app_controller.py | 43 +++++++++++++++++++ .../workflow/nodes/http_request/config.py | 12 ------ api/app/schemas/app_schema.py | 4 +- api/app/services/draft_run_service.py | 16 +++++-- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index eda5e76a..41422bd4 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -1298,3 +1298,46 @@ async def import_app( data={"app": app_schema.App.model_validate(result_app), "warnings": warnings}, msg="应用导入成功" + (",但部分资源需手动配置" if warnings else "") ) + + +@router.get("/citations/{document_id}/download", summary="下载引用文档原始文件") +async def download_citation_file( + document_id: uuid.UUID = Path(..., description="引用文档ID"), + db: Session = Depends(get_db), +): + """ + 下载引用文档的原始文件。 + 仅当应用功能特性 citation.allow_download=true 时,前端才会展示此下载链接。 + 路由本身不做权限校验,由业务层通过 allow_download 开关控制入口。 + """ + import os + from fastapi import HTTPException, status as http_status + from fastapi.responses import FileResponse + from app.core.config import settings + from app.models.document_model import Document + from app.models.file_model import File as FileModel + + doc = db.query(Document).filter(Document.id == document_id).first() + if not doc: + raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail="文档不存在") + + file_record = db.query(FileModel).filter(FileModel.id == doc.file_id).first() + if not file_record: + raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail="原始文件不存在") + + file_path = os.path.join( + settings.FILE_PATH, + str(file_record.kb_id), + str(file_record.parent_id), + f"{file_record.id}{file_record.file_ext}" + ) + if not os.path.exists(file_path): + raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail="文件未找到") + + encoded_name = quote(doc.file_name) + return FileResponse( + path=file_path, + filename=doc.file_name, + media_type="application/octet-stream", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}"} + ) diff --git a/api/app/core/workflow/nodes/http_request/config.py b/api/app/core/workflow/nodes/http_request/config.py index a6d16912..72474436 100644 --- a/api/app/core/workflow/nodes/http_request/config.py +++ b/api/app/core/workflow/nodes/http_request/config.py @@ -132,11 +132,6 @@ class HttpErrorDefaultTemplate(BaseModel): description="Default HTTP headers returned on error", ) - files: list = Field( - default_factory=list, - description="Default files list returned on error", - ) - output: str = Field( default="SUCCESS", description="HTTP response body", @@ -251,13 +246,6 @@ class HttpRequestNodeConfig(BaseNodeConfig): } -class HttpRequestDataProcessing(BaseModel): - request: str = Field( - default="", - description="Raw HTTP request format for debugging", - ) - - class HttpRequestNodeOutput(BaseModel): body: str = Field( ..., diff --git a/api/app/schemas/app_schema.py b/api/app/schemas/app_schema.py index f64d2ac4..11c27b56 100644 --- a/api/app/schemas/app_schema.py +++ b/api/app/schemas/app_schema.py @@ -200,6 +200,7 @@ class TextToSpeechConfig(BaseModel): class CitationConfig(BaseModel): """引用和归属配置""" enabled: bool = Field(default=False) + allow_download: bool = Field(default=False, description="是否允许下载引用文档") class Citation(BaseModel): @@ -207,6 +208,7 @@ class Citation(BaseModel): file_name: str knowledge_id: str score: float + download_url: Optional[str] = Field(default=None, description="引用文档下载链接(allow_download 开启时返回)") class WebSearchConfig(BaseModel): @@ -657,7 +659,7 @@ class DraftRunResponse(BaseModel): usage: Optional[Dict[str, Any]] = Field(default=None, description="Token 使用情况") elapsed_time: Optional[float] = Field(default=None, description="耗时(秒)") suggested_questions: List[str] = Field(default_factory=list, description="下一步建议问题") - citations: List[CitationSource] = Field(default_factory=list, description="引用来源") + citations: List[Dict[str, Any]] = Field(default_factory=list, description="引用来源") audio_url: Optional[str] = Field(default=None, description="TTS 语音URL") def model_dump(self, **kwargs): diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index f21dd8da..2869326f 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -475,11 +475,19 @@ class AgentRunService: features_config: Dict[str, Any], citations: List[Citation] ) -> List[Any]: - """根据 citation 开关决定是否返回引用来源""" + """根据 citation 开关决定是否返回引用来源,并根据 allow_download 附加下载链接""" citation_cfg = features_config.get("citation", {}) - if isinstance(citation_cfg, dict) and citation_cfg.get("enabled"): - return [cit.model_dump() for cit in citations] - return [] + if not (isinstance(citation_cfg, dict) and citation_cfg.get("enabled")): + return [] + allow_download = citation_cfg.get("allow_download", False) + result = [] + for cit in citations: + item = cit.model_dump() if hasattr(cit, "model_dump") else dict(cit) + if allow_download and item.get("document_id"): + from app.core.config import settings + item["download_url"] = f"{settings.FILE_LOCAL_SERVER_URL}/apps/citations/{item['document_id']}/download" + result.append(item) + return result async def run( self,