Compare commits
4 Commits
7a2f6dd30a
...
b8a683ce67
| Author | SHA1 | Date | |
|---|---|---|---|
| b8a683ce67 | |||
| 00eb23cf54 | |||
| 4017c0e1df | |||
| bb04c4afd0 |
@@ -98,7 +98,7 @@ func main() {
|
||||
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
|
||||
var uploadHandler *handler.UploadHandler
|
||||
if uploadService != nil {
|
||||
uploadHandler = handler.NewUploadHandler(uploadService)
|
||||
uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo)
|
||||
}
|
||||
|
||||
// 7. 设置路由
|
||||
@@ -216,6 +216,8 @@ func main() {
|
||||
// 上传路由
|
||||
r.POST("/api/file_upload", uploadHandler.Upload)
|
||||
r.DELETE("/api/file_upload/:filename", uploadHandler.Delete)
|
||||
// 文件代理路由(解决 MinIO 内网和 HTTPS 问题)
|
||||
r.GET("/api/file_proxy", uploadHandler.ProxyFile)
|
||||
}
|
||||
|
||||
// 8. 启动服务
|
||||
|
||||
@@ -147,6 +147,10 @@ func (h *KnowledgeHandler) UploadDocument(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if doc == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Upload failed"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"x-agents/server/internal/repository"
|
||||
"x-agents/server/internal/service"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
uploadService *service.UploadService
|
||||
uploadService *service.UploadService
|
||||
knowledgeRepo *repository.KnowledgeRepository
|
||||
}
|
||||
|
||||
func NewUploadHandler(uploadService *service.UploadService) *UploadHandler {
|
||||
return &UploadHandler{uploadService: uploadService}
|
||||
func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repository.KnowledgeRepository) *UploadHandler {
|
||||
return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
@@ -58,3 +63,71 @@ func (h *UploadHandler) Delete(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
||||
}
|
||||
|
||||
// ProxyFile 代理文件访问(解决 MinIO 内网地址和 HTTPS 问题)
|
||||
func (h *UploadHandler) ProxyFile(c *gin.Context) {
|
||||
fileKey := c.Query("key")
|
||||
kbID := c.Query("kb_id")
|
||||
|
||||
if fileKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File key is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var content io.Reader
|
||||
var contentType string
|
||||
|
||||
// 如果提供了知识库 ID,使用知识库的存储配置
|
||||
if kbID != "" && h.knowledgeRepo != nil {
|
||||
kb, err := h.knowledgeRepo.FindByID(kbID)
|
||||
if err == nil && kb.StorageConfig.Type == "minio" {
|
||||
reader, ct, err := h.uploadService.GetFileContentWithConfig(fileKey, kb.StorageConfig)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
content = reader
|
||||
contentType = ct
|
||||
defer reader.Close()
|
||||
} else {
|
||||
// 使用全局配置
|
||||
reader, ct, err := h.uploadService.GetFileContent(fileKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
content = reader
|
||||
contentType = ct
|
||||
defer reader.Close()
|
||||
}
|
||||
} else {
|
||||
// 使用全局配置
|
||||
reader, ct, err := h.uploadService.GetFileContent(fileKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
content = reader
|
||||
contentType = ct
|
||||
defer reader.Close()
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
// 如果 Content-Type 是 application/octet-stream,根据文件扩展名推断
|
||||
if contentType == "application/octet-stream" {
|
||||
ext := filepath.Ext(fileKey)
|
||||
if detected := mime.TypeByExtension(ext); detected != "" {
|
||||
contentType = detected
|
||||
}
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
c.Header("Content-Type", contentType)
|
||||
} else {
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
}
|
||||
c.Header("Content-Disposition", "inline")
|
||||
|
||||
// 直接流式输出文件内容
|
||||
c.DataFromReader(http.StatusOK, -1, contentType, content, nil)
|
||||
}
|
||||
|
||||
@@ -32,19 +32,47 @@ func (p ParsingConfig) Value() (driver.Value, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// StorageConfig 存储配置
|
||||
type StorageConfig struct {
|
||||
Type string `json:"type"` // local / minio / s3
|
||||
Endpoint string `json:"endpoint"` // MinIO/S3 endpoint
|
||||
Bucket string `json:"bucket"` // MinIO/S3 bucket
|
||||
AccessKeyID string `json:"access_key_id"` // MinIO/S3 access key
|
||||
SecretAccessKey string `json:"secret_access_key"` // MinIO/S3 secret key
|
||||
UseSSL bool `json:"use_ssl"` // MinIO/S3 use SSL
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口
|
||||
func (s *StorageConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(bytes, s)
|
||||
}
|
||||
|
||||
// Value 实现 driver.Valuer 接口
|
||||
func (s StorageConfig) Value() (driver.Value, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// KnowledgeBase 知识库
|
||||
type KnowledgeBase struct {
|
||||
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
|
||||
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
LLMModelID string `json:"llm_model_id" gorm:"type:varchar(36);not null"`
|
||||
EmbeddingModelID string `json:"embedding_model_id" gorm:"type:varchar(36);not null"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config" gorm:"type:json"`
|
||||
Status string `json:"status" gorm:"type:varchar(20);default:active"` // active / inactive
|
||||
DocumentCount int `json:"document_count" gorm:"default:0"`
|
||||
ChunkCount int `json:"chunk_count" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
|
||||
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
LLMModelID string `json:"llm_model_id" gorm:"type:varchar(36);not null"`
|
||||
EmbeddingModelID string `json:"embedding_model_id" gorm:"type:varchar(36);not null"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config" gorm:"type:json"`
|
||||
StorageConfig StorageConfig `json:"storage_config" gorm:"type:json"` // 存储配置
|
||||
Status string `json:"status" gorm:"type:varchar(20);default:active"` // active / inactive
|
||||
DocumentCount int `json:"document_count" gorm:"default:0"`
|
||||
ChunkCount int `json:"chunk_count" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (KnowledgeBase) TableName() string {
|
||||
@@ -72,21 +100,23 @@ func (KnowledgeDocument) TableName() string {
|
||||
|
||||
// CreateKnowledgeRequest 创建知识库请求
|
||||
type CreateKnowledgeRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
LLMModelID string `json:"llm_model_id" binding:"required"`
|
||||
EmbeddingModelID string `json:"embedding_model_id" binding:"required"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
LLMModelID string `json:"llm_model_id" binding:"required"`
|
||||
EmbeddingModelID string `json:"embedding_model_id" binding:"required"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config" binding:"required"`
|
||||
StorageConfig StorageConfig `json:"storage_config"` // 存储配置,不传则使用全局配置
|
||||
}
|
||||
|
||||
// UpdateKnowledgeRequest 更新知识库请求
|
||||
type UpdateKnowledgeRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
LLMModelID string `json:"llm_model_id"`
|
||||
EmbeddingModelID string `json:"embedding_model_id"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config"`
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
LLMModelID string `json:"llm_model_id"`
|
||||
EmbeddingModelID string `json:"embedding_model_id"`
|
||||
ParsingConfig ParsingConfig `json:"parsing_config"`
|
||||
StorageConfig StorageConfig `json:"storage_config"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// KnowledgeListResponse 知识库列表响应
|
||||
|
||||
@@ -3,8 +3,11 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -12,6 +15,14 @@ import (
|
||||
"x-agents/server/internal/repository"
|
||||
)
|
||||
|
||||
// debugLog 专用调试日志
|
||||
var knowledgeDebugLog *log.Logger
|
||||
|
||||
func init() {
|
||||
debugFile, _ := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
knowledgeDebugLog = log.New(debugFile, "", log.Ldate|log.Ltime)
|
||||
}
|
||||
|
||||
type KnowledgeService struct {
|
||||
repo *repository.KnowledgeRepository
|
||||
modelRepo *repository.ModelRepository
|
||||
@@ -46,6 +57,7 @@ func (s *KnowledgeService) Create(req model.CreateKnowledgeRequest) (*model.Know
|
||||
LLMModelID: req.LLMModelID,
|
||||
EmbeddingModelID: req.EmbeddingModelID,
|
||||
ParsingConfig: req.ParsingConfig,
|
||||
StorageConfig: req.StorageConfig,
|
||||
Status: "active",
|
||||
DocumentCount: 0,
|
||||
ChunkCount: 0,
|
||||
@@ -104,6 +116,9 @@ func (s *KnowledgeService) Update(id string, req model.UpdateKnowledgeRequest) e
|
||||
if req.ParsingConfig.Engine != "" {
|
||||
updates["parsing_config"] = req.ParsingConfig
|
||||
}
|
||||
if req.StorageConfig.Type != "" {
|
||||
updates["storage_config"] = req.StorageConfig
|
||||
}
|
||||
if req.Status != "" {
|
||||
updates["status"] = req.Status
|
||||
}
|
||||
@@ -122,29 +137,72 @@ func (s *KnowledgeService) Delete(id string) error {
|
||||
|
||||
// ListDocuments 获取知识库下的文档列表
|
||||
func (s *KnowledgeService) ListDocuments(kbID string, status string) ([]model.KnowledgeDocument, error) {
|
||||
return s.repo.FindDocumentsByKBID(kbID, status)
|
||||
docs, err := s.repo.FindDocumentsByKBID(kbID, status)
|
||||
if err != nil {
|
||||
knowledgeDebugLog.Printf("[Knowledge ListDocuments] 错误: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
knowledgeDebugLog.Printf("[Knowledge ListDocuments] kbID=%s, count=%d", kbID, len(docs))
|
||||
for i, doc := range docs {
|
||||
knowledgeDebugLog.Printf("[Knowledge ListDocuments] doc[%d]: id=%s, name=%s, file_url=%q", i, doc.ID, doc.Name, doc.FileURL)
|
||||
|
||||
// 如果是 MinIO 内网地址,转换为代理 URL
|
||||
if doc.FileURL != "" && (strings.Contains(doc.FileURL, "10.10.10.189") || strings.Contains(doc.FileURL, ":9768")) {
|
||||
// 从 URL 中提取 fileKey(包含扩展名)
|
||||
parts := strings.Split(doc.FileURL, "/")
|
||||
if len(parts) > 0 {
|
||||
fileName := parts[len(parts)-1]
|
||||
fileKey := strings.Split(fileName, "?")[0]
|
||||
doc.FileURL = "/api/file_proxy?kb_id=" + kbID + "&key=" + fileKey
|
||||
}
|
||||
}
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// UploadDocument 上传文档到知识库
|
||||
func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeader) (*model.KnowledgeDocument, string, error) {
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 开始上传文件: kbID=%s, filename=%s, size=%d", kbID, file.Filename, file.Size)
|
||||
|
||||
// 验证知识库存在
|
||||
kb, err := s.repo.FindByID(kbID)
|
||||
if err != nil {
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 知识库不存在, kbID=%s, err=%v", kbID, err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
result, err := s.uploadService.Upload(file)
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 知识库配置: kbID=%s, storage_config.Type=%q, storage_config=%+v",
|
||||
kbID, kb.StorageConfig.Type, kb.StorageConfig)
|
||||
|
||||
// 上传文件(根据知识库的 storage_config 选择存储方式)
|
||||
var result *UploadResponse
|
||||
if kb.StorageConfig.Type != "" {
|
||||
// 使用知识库的存储配置
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 使用知识库存储配置: type=%s, endpoint=%s, bucket=%s",
|
||||
kb.StorageConfig.Type, kb.StorageConfig.Endpoint, kb.StorageConfig.Bucket)
|
||||
result, err = s.uploadService.UploadWithConfig(file, kb.StorageConfig)
|
||||
} else {
|
||||
// 使用全局配置
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 使用全局存储配置")
|
||||
result, err = s.uploadService.Upload(file)
|
||||
}
|
||||
if err != nil {
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 上传失败, err=%v", err)
|
||||
return nil, "", err
|
||||
}
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 上传结果: success=%v, url=%s, message=%s",
|
||||
result.Success, result.URL, result.Message)
|
||||
if !result.Success {
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 上传返回失败, message=%s", result.Message)
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
ext := getFileExt(file.Filename)
|
||||
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 准备创建文档记录: result.URL=%q, fileKey=%s, ext=%s",
|
||||
result.URL, result.FileKey, ext)
|
||||
|
||||
// 创建文档记录
|
||||
doc := &model.KnowledgeDocument{
|
||||
ID: uuid.New().String(),
|
||||
@@ -157,6 +215,8 @@ func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeade
|
||||
UploadedAt: time.Now(),
|
||||
}
|
||||
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 文档创建完成: doc.FileURL=%q", doc.FileURL)
|
||||
|
||||
if err := s.repo.CreateDocument(doc); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -235,9 +295,23 @@ func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取知识库配置
|
||||
kb, err := s.repo.FindByID(kbID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if doc.FileKey != "" {
|
||||
s.uploadService.DeleteFile(doc.FileKey)
|
||||
knowledgeDebugLog.Printf("[Knowledge DeleteDocument] 删除文件: kbID=%s, docID=%s, fileKey=%s, storageType=%s",
|
||||
kbID, docID, doc.FileKey, kb.StorageConfig.Type)
|
||||
if kb.StorageConfig.Type != "" {
|
||||
// 使用知识库的存储配置删除
|
||||
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig)
|
||||
} else {
|
||||
// 使用全局配置删除
|
||||
s.uploadService.DeleteFile(doc.FileKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文档记录
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -15,8 +17,18 @@ import (
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/google/uuid"
|
||||
"x-agents/server/internal/config"
|
||||
"x-agents/server/internal/model"
|
||||
)
|
||||
|
||||
// debugLog 专用调试日志
|
||||
var debugLog *log.Logger
|
||||
|
||||
func init() {
|
||||
// 创建调试日志文件
|
||||
debugFile, _ := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
debugLog = log.New(debugFile, "", log.Ldate|log.Ltime)
|
||||
}
|
||||
|
||||
type UploadService struct {
|
||||
cfg *config.Config
|
||||
minioClient *minio.Client
|
||||
@@ -64,7 +76,7 @@ func NewUploadService(cfg *config.Config) (*UploadService, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
// Upload 上传文件(使用全局配置)
|
||||
func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, error) {
|
||||
// 打开文件
|
||||
f, err := file.Open()
|
||||
@@ -85,10 +97,106 @@ func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, err
|
||||
return s.uploadToLocal(f, filename, fileKey)
|
||||
}
|
||||
|
||||
// UploadWithConfig 上传文件(使用指定配置)
|
||||
func (s *UploadService) UploadWithConfig(file *multipart.FileHeader, storageConfig model.StorageConfig) (*UploadResponse, error) {
|
||||
// 打开文件
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return &UploadResponse{Success: false, Message: err.Error()}, nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
|
||||
fileKey := strings.TrimSuffix(filename, ext)
|
||||
|
||||
// 根据配置选择存储方式
|
||||
if storageConfig.Type == "minio" {
|
||||
return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig)
|
||||
}
|
||||
// 默认使用本地存储
|
||||
return s.uploadToLocal(f, filename, fileKey)
|
||||
}
|
||||
|
||||
// uploadToMinIOWithConfig 使用指定配置上传到 MinIO
|
||||
func (s *UploadService) uploadToMinIOWithConfig(f multipart.File, filename, fileKey string, size int64, cfg model.StorageConfig) (*UploadResponse, error) {
|
||||
debugLog.Printf("[MinIO Upload] 开始上传: endpoint=%s, bucket=%s, filename=%s, size=%d",
|
||||
cfg.Endpoint, cfg.Bucket, filename, size)
|
||||
debugLog.Printf("[MinIO Upload] MinIO配置详情: endpoint=%s, useSSL=%v, accessKey=%s",
|
||||
cfg.Endpoint, cfg.UseSSL, cfg.AccessKeyID)
|
||||
|
||||
// 创建 MinIO 客户端
|
||||
client, err := minio.New(cfg.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
|
||||
Secure: cfg.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
debugLog.Printf("[MinIO Upload] 错误: 创建MinIO客户端失败, err=%v", err)
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create MinIO client: %v", err)}, nil
|
||||
}
|
||||
debugLog.Printf("[MinIO Upload] MinIO客户端创建成功")
|
||||
|
||||
// 检查 bucket 是否存在,不存在则创建
|
||||
exists, err := client.BucketExists(context.Background(), cfg.Bucket)
|
||||
if err != nil {
|
||||
debugLog.Printf("[MinIO Upload] 错误: 检查bucket失败, err=%v", err)
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to check bucket: %v", err)}, nil
|
||||
}
|
||||
debugLog.Printf("[MinIO Upload] Bucket存在检查: exists=%v", exists)
|
||||
|
||||
if !exists {
|
||||
debugLog.Printf("[MinIO Upload] 正在创建Bucket: %s", cfg.Bucket)
|
||||
if err := client.MakeBucket(context.Background(), cfg.Bucket, minio.MakeBucketOptions{}); err != nil {
|
||||
debugLog.Printf("[MinIO Upload] 错误: 创建bucket失败, err=%v", err)
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create bucket: %v", err)}, nil
|
||||
}
|
||||
debugLog.Printf("[MinIO Upload] Bucket创建成功")
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
debugLog.Printf("[MinIO Upload] 正在上传文件到MinIO...")
|
||||
// 根据文件扩展名获取正确的 Content-Type
|
||||
contentType := mime.TypeByExtension(filepath.Ext(filename))
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
_, err = client.PutObject(context.Background(), cfg.Bucket, filename, f, size, minio.PutObjectOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
debugLog.Printf("[MinIO Upload] 错误: 上传文件失败, err=%v", err)
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
|
||||
}
|
||||
debugLog.Printf("[MinIO Upload] 文件上传成功")
|
||||
|
||||
// 生成预签名 URL(有效期 24 小时)
|
||||
debugLog.Printf("[MinIO Upload] 正在生成预签名URL...")
|
||||
presignedURL, err := client.PresignedGetObject(context.Background(), cfg.Bucket, filename, 24*time.Hour, nil)
|
||||
if err != nil {
|
||||
debugLog.Printf("[MinIO Upload] 错误: 生成预签名URL失败, err=%v", err)
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to generate URL: %v", err)}, nil
|
||||
}
|
||||
debugLog.Printf("[MinIO Upload] 预签名URL生成成功: %s", presignedURL.String())
|
||||
|
||||
return &UploadResponse{
|
||||
Success: true,
|
||||
URL: presignedURL.String(),
|
||||
FileKey: fileKey,
|
||||
Message: "Upload successful",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// uploadToMinIO 上传到 MinIO
|
||||
func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string, size int64) (*UploadResponse, error) {
|
||||
// 根据文件扩展名获取正确的 Content-Type
|
||||
contentType := mime.TypeByExtension(filepath.Ext(filename))
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
_, err := s.minioClient.PutObject(context.Background(), s.cfg.MinIOBucket, filename, f, size, minio.PutObjectOptions{
|
||||
ContentType: "application/octet-stream",
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil
|
||||
@@ -150,6 +258,27 @@ func (s *UploadService) DeleteFile(filename string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// DeleteFileWithConfig 使用指定配置删除文件
|
||||
func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig model.StorageConfig) error {
|
||||
if storageConfig.Type == "minio" {
|
||||
// 创建 MinIO 客户端
|
||||
client, err := minio.New(storageConfig.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""),
|
||||
Secure: storageConfig.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
debugLog.Printf("[MinIO Delete] 创建客户端失败: %v", err)
|
||||
return err
|
||||
}
|
||||
debugLog.Printf("[MinIO Delete] 删除文件: bucket=%s, filename=%s", storageConfig.Bucket, filename)
|
||||
return client.RemoveObject(context.Background(), storageConfig.Bucket, filename, minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
||||
// 本地删除
|
||||
path := filepath.Join(s.cfg.UploadLocalPath, filename)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件 URL(用于已存在的文件)
|
||||
func (s *UploadService) GetFileURL(filename string) (string, error) {
|
||||
if s.cfg.UploadMode == "minio" {
|
||||
@@ -164,3 +293,60 @@ func (s *UploadService) GetFileURL(filename string) (string, error) {
|
||||
escaped := url.PathEscape(filename)
|
||||
return fmt.Sprintf("%s/files/%s", s.cfg.ServerBaseURL, escaped), nil
|
||||
}
|
||||
|
||||
// GetFileContent 获取文件内容(用于代理访问)
|
||||
func (s *UploadService) GetFileContent(fileKey string) (io.ReadCloser, string, error) {
|
||||
if s.cfg.UploadMode == "minio" {
|
||||
object, err := s.minioClient.GetObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// 获取 Content-Type
|
||||
stat, err := s.minioClient.StatObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return object, stat.ContentType, nil
|
||||
}
|
||||
|
||||
// 本地文件
|
||||
path := filepath.Join(s.cfg.UploadLocalPath, fileKey)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return f, "application/octet-stream", nil
|
||||
}
|
||||
|
||||
// GetFileContentWithConfig 使用指定配置获取文件内容
|
||||
func (s *UploadService) GetFileContentWithConfig(fileKey string, storageConfig model.StorageConfig) (io.ReadCloser, string, error) {
|
||||
if storageConfig.Type == "minio" {
|
||||
// 创建 MinIO 客户端
|
||||
client, err := minio.New(storageConfig.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""),
|
||||
Secure: storageConfig.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
object, err := client.GetObject(context.Background(), storageConfig.Bucket, fileKey, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
// 获取 Content-Type
|
||||
stat, err := client.StatObject(context.Background(), storageConfig.Bucket, fileKey, minio.StatObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return object, stat.ContentType, nil
|
||||
}
|
||||
|
||||
// 本地文件
|
||||
path := filepath.Join(s.cfg.UploadLocalPath, fileKey)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return f, "application/octet-stream", nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,6 +28,13 @@ Content-Type: application/json
|
||||
| - docling_url | String | 条件 | Docling URL(engine=docling 时必填) |
|
||||
| - enable_pdf | Boolean | 否 | 是否启用 PDF 解析 |
|
||||
| - pandoc | Boolean | 否 | 是否启用 Pandoc |
|
||||
| storage_config | Object | 否 | 存储配置,不传则使用全局配置 |
|
||||
| - type | String | 否 | 存储模式:local / minio |
|
||||
| - endpoint | String | 否 | MinIO endpoint |
|
||||
| - bucket | String | 否 | MinIO bucket |
|
||||
| - access_key | String | 否 | MinIO access key |
|
||||
| - secret_key | String | 否 | MinIO secret key |
|
||||
| - use_ssl | Boolean | 否 | MinIO 是否使用 SSL |
|
||||
|
||||
**响应**
|
||||
|
||||
@@ -149,6 +156,8 @@ GET /api/knowledge/:id/documents
|
||||
"id": "doc_001",
|
||||
"knowledge_base_id": "kb_001",
|
||||
"name": "产品手册_v2.0.pdf",
|
||||
"file_key": "abc123.pdf",
|
||||
"file_url": "http://localhost:8082/files/abc123.pdf",
|
||||
"file_size": 2516582,
|
||||
"status": "parsed",
|
||||
"chunk_count": 156,
|
||||
|
||||
@@ -29,9 +29,16 @@ Content-Type: application/json
|
||||
| - docling_url | String | 条件必填 | Docling 服务 URL(engine=docling 时必填) |
|
||||
| - enable_pdf | Boolean | 否 | 是否启用 PDF 解析(默认 true) |
|
||||
| - pandoc | Boolean | 否 | 是否启用 Pandoc(默认 true) |
|
||||
| storage_config | Object | 否 | 存储配置(默认 local) |
|
||||
| - type | String | 是 | 存储类型:local / minio / s3 |
|
||||
| - endpoint | String | 否 | MinIO Endpoint(如 minio:9000) |
|
||||
| - access_key_id | String | 否 | MinIO Access Key ID |
|
||||
| - secret_access_key | String | 否 | MinIO Secret Access Key |
|
||||
| - bucket | String | 否 | MinIO Bucket 名称 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
本地存储:
|
||||
```json
|
||||
{
|
||||
"name": "产品文档知识库",
|
||||
@@ -42,12 +49,14 @@ Content-Type: application/json
|
||||
"engine": "markitdown",
|
||||
"enable_pdf": true,
|
||||
"pandoc": true
|
||||
},
|
||||
"storage_config": {
|
||||
"type": "local"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或使用 Docling:
|
||||
|
||||
使用 Docling + MinIO:
|
||||
```json
|
||||
{
|
||||
"name": "产品文档知识库",
|
||||
@@ -59,6 +68,13 @@ Content-Type: application/json
|
||||
"docling_url": "http://localhost:8501",
|
||||
"enable_pdf": true,
|
||||
"pandoc": true
|
||||
},
|
||||
"storage_config": {
|
||||
"type": "minio",
|
||||
"endpoint": "localhost:9000",
|
||||
"access_key_id": "minioadmin",
|
||||
"secret_access_key": "minioadmin",
|
||||
"bucket": "x-agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -204,7 +220,10 @@ GET /api/knowledge/:id/documents
|
||||
"data": [
|
||||
{
|
||||
"id": "doc_001",
|
||||
"knowledge_base_id": "kb_001",
|
||||
"name": "产品手册_v2.0.pdf",
|
||||
"file_key": "abc123.pdf",
|
||||
"file_url": "http://localhost:8082/files/abc123.pdf",
|
||||
"file_size": 2516582,
|
||||
"status": "parsed",
|
||||
"chunk_count": 156,
|
||||
@@ -334,6 +353,7 @@ GET /api/knowledge/:id/documents/:doc_id/preview
|
||||
| llm_model_id | String | LLM 模型 ID |
|
||||
| embedding_model_id | String | Embedding 模型 ID |
|
||||
| parsing_config | JSON | 解析配置 |
|
||||
| storage_config | JSON | 存储配置(包含 type, endpoint, access_key_id, secret_access_key, bucket) |
|
||||
| status | String | 状态:active / inactive |
|
||||
| document_count | Integer | 文档数量 |
|
||||
| chunk_count | Integer | 切片数量 |
|
||||
@@ -348,6 +368,7 @@ GET /api/knowledge/:id/documents/:doc_id/preview
|
||||
| knowledge_base_id | String | 知识库 ID |
|
||||
| name | String | 文档名称 |
|
||||
| file_key | String | 文件存储 key |
|
||||
| file_url | String | 文件访问 URL(本地路径或 MinIO 预签名 URL) |
|
||||
| file_size | BigInteger | 文件大小 |
|
||||
| status | String | 状态:parsing / parsed / failed |
|
||||
| chunk_count | Integer | 切片数量 |
|
||||
|
||||
@@ -4,12 +4,26 @@
|
||||
|
||||
### 2026-03-08
|
||||
|
||||
- [ ] **知识库(Knowledge Base)API** - 后端待实现
|
||||
- [x] **知识库(Knowledge Base)API** - 后端已完成 ✔
|
||||
- 创建知识库、获取列表、获取详情、删除
|
||||
- 上传文档、删除文档、重新解析
|
||||
- 获取文档预览内容
|
||||
- 详细需求:[knowledge-base-api.md](./knowledge-base-api.md)
|
||||
|
||||
- [x] **编辑时正确处理 sub_tables** - 后端已完成 ✔
|
||||
- 问题:取消选中 1 个表后保存,再次进入仍显示 2 个表
|
||||
- 详细需求:[sub-tables-edit.md](./sub-tables-edit.md)
|
||||
|
||||
- [x] **知识库存储配置 (MinIO/S3)** - 后端已完成 ✔
|
||||
- 前端已完成:添加 storage_config 参数传递
|
||||
- 后端已完成:KnowledgeBase 模型添加 storage_config 字段
|
||||
- 上传文件时使用知识库的 storage_config,而非全局配置
|
||||
- 详细需求:[knowledge-base-api.md](./knowledge-base-api.md)
|
||||
|
||||
- [x] **文档列表返回 file_url** - 后端已完成 ✔
|
||||
- 问题:重新进入知识库后 PDF 无法预览
|
||||
- 已确认:API 返回的 file_url 字段有值
|
||||
|
||||
---
|
||||
|
||||
### 2026-03-07
|
||||
@@ -26,10 +40,6 @@
|
||||
- 问题:用户从 2 个子表修改为 1 个后,Tables 列没有更新
|
||||
- 详细需求:[table-count-update-edit.md](./table-count-update-edit.md)
|
||||
|
||||
- [ ] **编辑时正确处理 sub_tables** - 后端待实现
|
||||
- 问题:取消选中 1 个表后保存,再次进入仍显示 2 个表
|
||||
- 详细需求:[sub-tables-edit.md](./sub-tables-edit.md)
|
||||
|
||||
---
|
||||
|
||||
> 需求完成后请完成者打 ✔
|
||||
|
||||
@@ -48,7 +48,18 @@ const step3Valid = computed(() => {
|
||||
}
|
||||
return true
|
||||
})
|
||||
const step4Valid = computed(() => true) // Storage - 暂时默认通过
|
||||
const step4Valid = computed(() => {
|
||||
// Local 存储不需要额外配置
|
||||
if (storageConfig.value.type === 'local') {
|
||||
return true
|
||||
}
|
||||
// MinIO 存储需要填写所有字段
|
||||
if (storageConfig.value.type === 'minio') {
|
||||
return !!(storageConfig.value.endpoint && storageConfig.value.accessKeyId && storageConfig.value.secretAccessKey && storageConfig.value.bucket)
|
||||
}
|
||||
// S3 暂时默认通过
|
||||
return true
|
||||
})
|
||||
|
||||
// 获取当前步骤是否有效
|
||||
const isCurrentStepValid = computed(() => {
|
||||
@@ -123,11 +134,29 @@ const knowledgeDocuments = ref<any[]>([]) // 知识库文档列表
|
||||
const loadingDocuments = ref(false)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const uploading = ref(false)
|
||||
const previewUrl = ref('') // 文档预览URL
|
||||
const previewUrl = ref('') // 文档预览URL (blob URL)
|
||||
const previewDownloadUrl = ref('') // 原始下载链接
|
||||
const loadingPreview = ref(false)
|
||||
const previewPage = ref(1) // 当前页码
|
||||
const previewTotalPages = ref(1) // 总页数
|
||||
|
||||
// 使用代理接口加载PDF
|
||||
const loadPdfWithProxy = async (doc: any): Promise<string> => {
|
||||
if (!selectedKnowledge.value || !doc.file_key) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
const { getFileProxyUrl } = await import('./knowledge/useKnowledge')
|
||||
const proxyUrl = getFileProxyUrl(selectedKnowledge.value.id, doc.file_key)
|
||||
console.log('Using proxy URL for PDF:', proxyUrl)
|
||||
return proxyUrl
|
||||
} catch (error) {
|
||||
console.error('Failed to get proxy URL:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const newKbForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -151,6 +180,10 @@ const parsingConfig = ref({
|
||||
// Storage 配置
|
||||
const storageConfig = ref({
|
||||
type: 'local',
|
||||
endpoint: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
bucket: '',
|
||||
})
|
||||
|
||||
const openCreateDialog = () => {
|
||||
@@ -197,6 +230,13 @@ const createKnowledgeBase = async () => {
|
||||
docling_url: parsingConfig.value.engine === 'docling' ? parsingConfig.value.doclingUrl : undefined,
|
||||
enable_pdf: parsingConfig.value.enablePdf,
|
||||
pandoc: parsingConfig.value.pandoc,
|
||||
},
|
||||
storage_config: {
|
||||
type: storageConfig.value.type,
|
||||
endpoint: storageConfig.value.type === 'minio' ? storageConfig.value.endpoint : undefined,
|
||||
access_key_id: storageConfig.value.type === 'minio' ? storageConfig.value.accessKeyId : undefined,
|
||||
secret_access_key: storageConfig.value.type === 'minio' ? storageConfig.value.secretAccessKey : undefined,
|
||||
bucket: storageConfig.value.type === 'minio' ? storageConfig.value.bucket : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -297,17 +337,16 @@ const enterKnowledge = async (kb: any) => {
|
||||
selectedKnowledge.value = kb
|
||||
selectedFile.value = null
|
||||
previewUrl.value = ''
|
||||
previewDownloadUrl.value = ''
|
||||
|
||||
// 获取文档列表
|
||||
loadingDocuments.value = true
|
||||
try {
|
||||
const docs = await fetchKnowledgeDocuments(kb.id, fileFilter.value)
|
||||
console.log('Fetched documents:', docs)
|
||||
knowledgeDocuments.value = docs
|
||||
|
||||
// 自动选中第一个文档
|
||||
if (docs && docs.length > 0) {
|
||||
console.log('First doc:', docs[0])
|
||||
await selectDocument(docs[0])
|
||||
}
|
||||
} finally {
|
||||
@@ -336,15 +375,27 @@ const selectDocument = async (doc: any) => {
|
||||
selectedFile.value = doc.id
|
||||
selectedDocument.value = doc
|
||||
previewUrl.value = ''
|
||||
previewDownloadUrl.value = ''
|
||||
previewPage.value = 1
|
||||
previewTotalPages.value = 1
|
||||
|
||||
// 尝试从多个字段获取文件URL
|
||||
// 优先使用代理接口加载PDF
|
||||
if (doc.file_key && selectedKnowledge.value) {
|
||||
previewUrl.value = await loadPdfWithProxy(doc)
|
||||
if (previewUrl.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果代理失败,尝试使用file_url
|
||||
const fileUrl = doc.file_url || doc.fileUrl || doc.url || doc.FileURL
|
||||
if (fileUrl) {
|
||||
previewUrl.value = fileUrl
|
||||
} else if (selectedKnowledge.value && doc.status === 'parsed') {
|
||||
// 获取文档预览
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有file_url,调用预览API获取
|
||||
if (selectedKnowledge.value) {
|
||||
loadingPreview.value = true
|
||||
try {
|
||||
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||
@@ -402,29 +453,22 @@ const handleFileSelect = async (event: Event) => {
|
||||
if (result.success) {
|
||||
ElMessage.success('File uploaded successfully')
|
||||
|
||||
// 后端返回 result.url 在顶层,result.document 里有 file_url
|
||||
const fileUrl = result.url || result.document?.file_url
|
||||
// 添加到文档列表
|
||||
const newDoc = result.document || {
|
||||
id: result.id,
|
||||
name: file.name,
|
||||
file_size: file.size,
|
||||
status: 'parsing',
|
||||
chunk_count: 0,
|
||||
uploaded_at: new Date().toISOString(),
|
||||
file_url: fileUrl
|
||||
}
|
||||
// 如果返回了file_url,添加到列表开头
|
||||
if (fileUrl) {
|
||||
previewUrl.value = fileUrl
|
||||
// 设置选中的文档信息
|
||||
// 刷新文档列表以获取最新数据(包括 file_key)
|
||||
await changeFileFilter(fileFilter.value)
|
||||
|
||||
// 获取刚上传的文档
|
||||
const uploadedDoc = knowledgeDocuments.value.find(d => d.id === result.id)
|
||||
if (uploadedDoc) {
|
||||
// 选中新上传的文档
|
||||
selectedFile.value = result.id
|
||||
selectedDocument.value = newDoc
|
||||
// 添加到文档列表
|
||||
knowledgeDocuments.value = [newDoc, ...knowledgeDocuments.value]
|
||||
} else {
|
||||
// 刷新文档列表
|
||||
await changeFileFilter(fileFilter.value)
|
||||
selectedDocument.value = uploadedDoc
|
||||
|
||||
// 使用代理接口加载PDF
|
||||
if (uploadedDoc.file_key) {
|
||||
previewUrl.value = await loadPdfWithProxy(uploadedDoc)
|
||||
} else if (uploadedDoc.file_url) {
|
||||
previewUrl.value = uploadedDoc.file_url
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to upload file')
|
||||
@@ -453,6 +497,7 @@ const deleteDocument = async (docId: string) => {
|
||||
selectedFile.value = null
|
||||
selectedDocument.value = null
|
||||
previewUrl.value = ''
|
||||
previewDownloadUrl.value = ''
|
||||
}
|
||||
// 刷新文档列表
|
||||
await changeFileFilter(fileFilter.value)
|
||||
@@ -739,6 +784,22 @@ const deleteDocument = async (docId: string) => {
|
||||
<el-option label="S3" value="s3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- MinIO 配置 -->
|
||||
<template v-if="storageConfig.type === 'minio'">
|
||||
<el-form-item label="Endpoint">
|
||||
<el-input v-model="storageConfig.endpoint" placeholder="http://localhost:9000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Access Key ID">
|
||||
<el-input v-model="storageConfig.accessKeyId" placeholder="Enter Access Key ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Secret Access Key">
|
||||
<el-input v-model="storageConfig.secretAccessKey" type="password" placeholder="Enter Secret Access Key" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket">
|
||||
<el-input v-model="storageConfig.bucket" placeholder="Enter Bucket name" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -925,14 +986,21 @@ const deleteDocument = async (docId: string) => {
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
<span>Loading preview...</span>
|
||||
</div>
|
||||
<!-- 有预览URL时显示PDF -->
|
||||
<embed
|
||||
<!-- 有blob预览URL时显示PDF (使用iframe) -->
|
||||
<iframe
|
||||
v-else-if="previewUrl"
|
||||
type="application/pdf"
|
||||
:src="previewUrl"
|
||||
class="pdf-embed"
|
||||
/>
|
||||
<!-- 无预览时显示提示 -->
|
||||
<!-- 无预览但有下载链接时显示下载按钮 -->
|
||||
<div v-else-if="previewDownloadUrl" class="preview-no-file">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span>Cannot preview PDF directly</span>
|
||||
<a :href="previewDownloadUrl" target="_blank" class="download-link">
|
||||
<i class="fa-solid fa-download"></i> Download PDF
|
||||
</a>
|
||||
</div>
|
||||
<!-- 无预览也无下载链接时显示提示 -->
|
||||
<div v-else class="preview-no-file">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span>Document preview not available</span>
|
||||
|
||||
@@ -77,6 +77,13 @@ export const createKnowledgeBase = async (params: {
|
||||
enable_pdf?: boolean
|
||||
pandoc?: boolean
|
||||
}
|
||||
storage_config?: {
|
||||
type: string
|
||||
endpoint?: string
|
||||
access_key_id?: string
|
||||
secret_access_key?: string
|
||||
bucket?: string
|
||||
}
|
||||
}): Promise<{ success: boolean; id?: string; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/create`, {
|
||||
@@ -198,3 +205,9 @@ export const getDocumentPreview = async (kbId: string, docId: string, page: numb
|
||||
return { success: false, message: 'Failed to get document preview' }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件代理URL (用于预览PDF)
|
||||
export const getFileProxyUrl = (kbId: string, key: string): string => {
|
||||
const encodedKey = encodeURIComponent(key)
|
||||
return `${API_BASE}/api/file_proxy?kb_id=${kbId}&key=${encodedKey}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user