zlwu 1 سال پیش
والد
کامیت
edbcd37bde

+ 51 - 1
api/controllers/console/datasets/datasets_document.py

@@ -42,6 +42,7 @@ from core.model_runtime.errors.invoke import InvokeAuthorizationError
 from core.plugin.impl.exc import PluginDaemonClientSideError
 from core.rag.extractor.entity.datasource_type import DatasourceType
 from core.rag.extractor.entity.extract_setting import ExtractSetting
+from core.file import helpers as file_helpers
 from extensions.ext_database import db
 from fields.document_fields import (
     dataset_and_document_fields,
@@ -1010,6 +1011,55 @@ class WebsiteDocumentSyncApi(DocumentResource):
         return {"result": "success"}, 200
 
 
+class DocDownloadFileApi(DocumentResource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, dataset_id, document_id):
+        """Get upload file download URL for a document.
+
+        Returns download information and signed URL for the uploaded file
+        associated with this document.
+        """
+        dataset_id = str(dataset_id)
+        document_id = str(document_id)
+        document = self.get_document(dataset_id, document_id)
+
+        # Check if document is based on uploaded file
+        if document.data_source_type != "upload_file":
+            raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
+
+        data_source_info = document.data_source_info_dict
+        if not data_source_info or "upload_file_id" not in data_source_info:
+            raise ValueError("Upload file id not found in document data source info.")
+
+        file_id = data_source_info["upload_file_id"]
+        upload_file = (
+            db.session.query(UploadFile)
+            .where(UploadFile.tenant_id == document.tenant_id, UploadFile.id == file_id)
+            .first()
+        )
+
+        if not upload_file:
+            raise NotFound("Upload file not found.")
+
+        # Generate signed URL
+        url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
+        download_url = f"{url}&as_attachment=true"
+
+        return {
+            "id": upload_file.id,
+            "name": upload_file.name,
+            "size": upload_file.size,
+            "extension": upload_file.extension,
+            "url": url,
+            "download_url": download_url,
+            "mime_type": upload_file.mime_type,
+            "created_by": upload_file.created_by,
+            "created_at": upload_file.created_at.timestamp(),
+        }, 200
+
+
 api.add_resource(GetProcessRuleApi, "/datasets/process-rule")
 api.add_resource(DatasetDocumentListApi, "/datasets/<uuid:dataset_id>/documents")
 api.add_resource(DatasetInitApi, "/datasets/init")
@@ -1029,5 +1079,5 @@ api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:
 api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
 api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")
 api.add_resource(DocumentRenameApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/rename")
-
+api.add_resource(DocDownloadFileApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/download")
 api.add_resource(WebsiteDocumentSyncApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/website-sync")

+ 146 - 0
nginx/nginx.conf

@@ -0,0 +1,146 @@
+
+user wuzilong staff;
+worker_processes  1;
+
+#error_log  logs/error.log;
+#error_log  logs/error.log  notice;
+#error_log  logs/error.log  info;
+
+#pid        logs/nginx.pid;
+
+
+events {
+    worker_connections  1024;
+}
+
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+
+    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+    #                  '$status $body_bytes_sent "$http_referer" '
+    #                  '"$http_user_agent" "$http_x_forwarded_for"';
+
+    #access_log  logs/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    #keepalive_timeout  0;
+    keepalive_timeout  65;
+
+    #gzip  on;
+
+    server {
+        listen       18989;
+        server_name  localhost;
+
+        #charset koi8-r;
+
+        #access_log  logs/host.access.log  main;
+
+        location / {
+            root   /Users/wuzilong/work/00code/hzdyjs_xg/web/dist;
+            index  index.html index.htm;
+        }
+
+        # Dify 文件下载代理
+        location /files/ {
+            alias /Users/wuzilong/work/00code/dify/dify-1.8.1/api/storage/upload_files/;
+
+            # 启用自动索引(可选,用于浏览文件列表)
+            autoindex on;
+            autoindex_exact_size off;
+            autoindex_localtime on;
+
+            # 设置缓存
+            expires 1d;
+            add_header Cache-Control "public, immutable";
+
+            # 安全设置
+            add_header X-Content-Type-Options nosniff;
+            add_header X-Frame-Options DENY;
+            add_header X-XSS-Protection "1; mode=block";
+
+            # 支持的文件类型
+            location ~* \.(txt|pdf|doc|docx|xls|xlsx|ppt|pptx|jpg|jpeg|png|gif|bmp|svg|ico|mp4|avi|mov|mp3|wav)$ {
+                add_header Content-Disposition "inline";
+            }
+
+            # 处理跨域请求(如果需要)
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods "GET, OPTIONS";
+            add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
+        }
+
+        #error_page  404              /404.html;
+
+        # redirect server error pages to the static page /50x.html
+        #
+        error_page   500 502 503 504  /50x.html;
+        location = /50x.html {
+            root   html;
+        }
+
+        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
+        #
+        #location ~ \.php$ {
+        #    proxy_pass   http://127.0.0.1;
+        #}
+
+        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
+        #
+        #location ~ \.php$ {
+        #    root           html;
+        #    fastcgi_pass   127.0.0.1:9000;
+        #    fastcgi_index  index.php;
+        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
+        #    include        fastcgi_params;
+        #}
+
+        # deny access to .htaccess files, if Apache's document root
+        # concurs with nginx's one
+        #
+        #location ~ /\.ht {
+        #    deny  all;
+        #}
+    }
+
+
+    # another virtual host using mix of IP-, name-, and port-based configuration
+    #
+    #server {
+    #    listen       8000;
+    #    listen       somename:8080;
+    #    server_name  somename  alias  another.alias;
+
+    #    location / {
+    #        root   html;
+    #        index  index.html index.htm;
+    #    }
+    #}
+
+
+    # HTTPS server
+    #
+    #server {
+    #    listen       443 ssl;
+    #    server_name  localhost;
+
+    #    ssl_certificate      cert.pem;
+    #    ssl_certificate_key  cert.key;
+
+    #    ssl_session_cache    shared:SSL:1m;
+    #    ssl_session_timeout  5m;
+
+    #    ssl_ciphers  HIGH:!aNULL:!MD5;
+    #    ssl_prefer_server_ciphers  on;
+
+    #    location / {
+    #        root   html;
+    #        index  index.html index.htm;
+    #    }
+    #}
+    include servers/*;
+}

+ 50 - 0
web/app/components/datasets/documents/list.tsx

@@ -7,6 +7,7 @@ import { pick, uniq } from 'lodash-es'
 import {
   RiArchive2Line,
   RiDeleteBinLine,
+  RiDownloadLine,
   RiEditLine,
   RiEqualizer2Line,
   RiLoopLeftLine,
@@ -49,6 +50,7 @@ import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils
 import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
 import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
 import { noop } from 'lodash-es'
+import { get } from '@/service/base'
 
 export const useIndexStatus = () => {
   const { t } = useTranslation()
@@ -296,6 +298,54 @@ export const OperationAction: FC<{
     )}
     {embeddingAvailable && (
       <>
+        {/* 下载按钮 */}
+        <Tooltip
+          popupContent={t('datasetDocuments.list.action.download')}
+          popupClassName='text-text-secondary system-xs-medium'
+          needsDelay={false}
+        >
+          <button
+            className={cn('mr-2 cursor-pointer rounded-lg',
+              !isListScene
+                ? 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
+                : 'p-0.5 hover:bg-state-base-hover')}
+            onClick={async () => {
+              try {
+                // 第一步:使用项目的API服务获取下载信息
+                console.log('Getting download info for document:', detail.id)
+
+                const downloadInfo = await get<{ download_url?: string; url?: string; name?: string }>(
+                  `/datasets/${datasetId}/documents/${detail.id}/download`,
+                )
+                console.log('Download info:', downloadInfo)
+
+                // 第二步:使用返回的URL下载实际文件
+                const fileUrl = downloadInfo.download_url || downloadInfo.url
+                if (!fileUrl)
+                  throw new Error('未获取到有效的下载链接')
+
+                // 创建下载链接
+                const link = document.createElement('a')
+                link.href = fileUrl
+                link.download = downloadInfo.name || detail.name
+                link.target = '_blank'
+                document.body.appendChild(link)
+                link.click()
+                document.body.removeChild(link)
+
+                console.log('文件下载已开始:', downloadInfo.name || detail.name)
+              }
+              catch (error) {
+                console.error('下载文件失败:', error)
+                // 这里可以添加用户友好的错误提示
+                // notify({ type: 'error', message: `下载失败: ${error.message}` })
+              }
+            }}>
+            <RiDownloadLine className='h-4 w-4 text-components-button-secondary-text' />
+          </button>
+        </Tooltip>
+
+        {/* 分段设置按钮 */}
         <Tooltip
           popupContent={t('datasetDocuments.list.action.settings')}
           popupClassName='text-text-secondary system-xs-medium'

+ 1 - 0
web/i18n/en-US/dataset-documents.ts

@@ -22,6 +22,7 @@ const translation = {
     action: {
       uploadFile: 'Upload new file',
       settings: 'Chunking Settings',
+      download: 'Download File',
       addButton: 'Add chunk',
       add: 'Add a chunk',
       batchAdd: 'Batch add',

+ 1 - 0
web/i18n/zh-Hans/dataset-documents.ts

@@ -23,6 +23,7 @@ const translation = {
     action: {
       uploadFile: '上传新文件',
       settings: '分段设置',
+      download: '下载文件',
       addButton: '添加分段',
       add: '添加新分段',
       batchAdd: '批量添加',