Add files via upload

This commit is contained in:
公明
2026-07-22 16:23:57 +08:00
committed by GitHub
parent 3f2984b7c6
commit 3763da8773
3 changed files with 650 additions and 18 deletions
+611 -16
View File
@@ -1,13 +1,17 @@
package handler
import (
"archive/zip"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
@@ -21,8 +25,15 @@ import (
)
const (
chatUploadsRootDirName = "chat_uploads"
maxChatUploadEditBytes = 2 * 1024 * 1024 // 文本编辑上限
chatUploadsRootDirName = "chat_uploads"
reductionRootDirName = "tmp/reduction"
artifactsRootDirName = "data/conversation_artifacts"
reductionVirtualPrefix = "__reduction__/"
artifactVirtualPrefix = "__conversation_artifact__/"
chatUploadSourceUpload = "upload"
chatUploadSourceReduction = "reduction"
chatUploadSourceConversation = "conversation_artifact"
maxChatUploadEditBytes = 2 * 1024 * 1024 // 文本编辑上限
)
// ChatUploadsHandler 对话中上传附件(chat_uploads 目录)的管理 API
@@ -66,6 +77,64 @@ func (h *ChatUploadsHandler) pathAllowed(c *gin.Context, relativePath string) bo
return h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", parts[1])
}
func (h *ChatUploadsHandler) reductionPathAllowed(c *gin.Context, scope, id string) bool {
session, ok := security.CurrentSession(c)
if !ok || h.db == nil {
return false
}
if session.Scope == database.RBACScopeAll {
return true
}
id = strings.TrimSpace(id)
switch scope {
case "conversations":
if id == "" || id == "default" {
return false
}
return h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", id)
case "projects":
if id == "" {
return false
}
return h.db.UserCanAccessResource(session.UserID, session.Scope, "project", id)
default:
return false
}
}
func (h *ChatUploadsHandler) reductionVirtualPathAllowed(c *gin.Context, relativePath string) bool {
rel := strings.TrimPrefix(strings.TrimSpace(relativePath), reductionVirtualPrefix)
parts := strings.Split(filepath.ToSlash(rel), "/")
if len(parts) < 2 {
return false
}
return h.reductionPathAllowed(c, parts[0], parts[1])
}
func (h *ChatUploadsHandler) conversationArtifactPathAllowed(c *gin.Context, conversationID string) bool {
session, ok := security.CurrentSession(c)
if !ok || h.db == nil {
return false
}
if session.Scope == database.RBACScopeAll {
return true
}
conversationID = strings.TrimSpace(conversationID)
if conversationID == "" || conversationID == "default" {
return false
}
return h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", conversationID)
}
func (h *ChatUploadsHandler) conversationArtifactVirtualPathAllowed(c *gin.Context, relativePath string) bool {
rel := strings.TrimPrefix(strings.TrimSpace(relativePath), artifactVirtualPrefix)
parts := strings.Split(filepath.ToSlash(rel), "/")
if len(parts) < 1 {
return false
}
return h.conversationArtifactPathAllowed(c, parts[0])
}
func (h *ChatUploadsHandler) absRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
@@ -74,6 +143,46 @@ func (h *ChatUploadsHandler) absRoot() (string, error) {
return filepath.Abs(filepath.Join(cwd, chatUploadsRootDirName))
}
func (h *ChatUploadsHandler) absReductionRoot() (string, error) {
if h.db != nil {
if base := strings.TrimSpace(h.db.EinoReductionBaseDir()); base != "" {
if filepath.IsAbs(base) {
return filepath.Abs(base)
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Abs(filepath.Join(cwd, base))
}
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Abs(filepath.Join(cwd, reductionRootDirName))
}
func (h *ChatUploadsHandler) absConversationArtifactsRoot() (string, error) {
if h.db != nil {
if base := strings.TrimSpace(h.db.ConversationArtifactsBaseDir()); base != "" {
if filepath.IsAbs(base) {
return filepath.Abs(base)
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Abs(filepath.Join(cwd, base))
}
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Abs(filepath.Join(cwd, artifactsRootDirName))
}
// resolveUnderChatUploads 校验 relativePath(使用 / 分隔)对应文件必须在 chat_uploads 根下
func (h *ChatUploadsHandler) resolveUnderChatUploads(relativePath string) (abs string, err error) {
root, err := h.absRoot()
@@ -105,30 +214,44 @@ type ChatUploadFileItem struct {
RelativePath string `json:"relativePath"`
AbsolutePath string `json:"absolutePath"` // 服务器上的绝对路径,便于在对话中引用(与附件落盘路径一致)
Name string `json:"name"`
Source string `json:"source,omitempty"`
Size int64 `json:"size"`
ModifiedUnix int64 `json:"modifiedUnix"`
Date string `json:"date"`
ConversationID string `json:"conversationId"`
ProjectID string `json:"projectId,omitempty"`
// SubPath 为日期、会话目录之下的子路径(不含文件名),如 date/conv/a/b/file 则为 "a/b";无嵌套则为 ""。
SubPath string `json:"subPath"`
}
// List GET /api/chat-uploads
func (h *ChatUploadsHandler) List(c *gin.Context) {
conversationFilter := strings.TrimSpace(c.Query("conversation"))
func (h *ChatUploadsHandler) conversationProjectID(conversationID string, cache map[string]string) string {
conversationID = strings.TrimSpace(conversationID)
if conversationID == "" || conversationID == "_manual" || conversationID == "_new" || h.db == nil {
return ""
}
if v, ok := cache[conversationID]; ok {
return v
}
projectID, err := h.db.GetConversationProjectID(conversationID)
if err != nil {
projectID = ""
}
cache[conversationID] = projectID
return projectID
}
func (h *ChatUploadsHandler) collectFiles(c *gin.Context, conversationFilter, projectFilter string) ([]ChatUploadFileItem, []string, error) {
root, err := h.absRoot()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
return nil, nil, err
}
// 保证根目录存在,否则「按文件夹」浏览时无法 mkdir,且首次列表为空时界面无路径工具栏
if err := os.MkdirAll(root, 0755); err != nil {
h.logger.Warn("创建 chat_uploads 根目录失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
return nil, nil, err
}
var files []ChatUploadFileItem
var folders []string
projectCache := make(map[string]string)
err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
@@ -157,6 +280,7 @@ func (h *ChatUploadsHandler) List(c *gin.Context) {
if len(parts) >= 3 {
convID = parts[1]
}
projectID := h.conversationProjectID(convID, projectCache)
var subPath string
if len(parts) >= 4 {
subPath = strings.Join(parts[2:len(parts)-1], "/")
@@ -164,29 +288,32 @@ func (h *ChatUploadsHandler) List(c *gin.Context) {
if conversationFilter != "" && convID != conversationFilter {
return nil
}
if projectFilter != "" && projectID != projectFilter {
return nil
}
absPath, _ := filepath.Abs(path)
files = append(files, ChatUploadFileItem{
RelativePath: relSlash,
AbsolutePath: absPath,
Name: d.Name(),
Source: chatUploadSourceUpload,
Size: info.Size(),
ModifiedUnix: info.ModTime().Unix(),
Date: dateStr,
ConversationID: convID,
ProjectID: projectID,
SubPath: subPath,
})
return nil
})
if err != nil {
h.logger.Warn("列举对话附件失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
return nil, nil, err
}
if conversationFilter != "" {
if conversationFilter != "" || projectFilter != "" {
filteredFolders := make([]string, 0, len(folders))
for _, rel := range folders {
parts := strings.Split(rel, "/")
if len(parts) >= 2 && parts[1] == conversationFilter {
if len(parts) >= 2 && (conversationFilter == "" || parts[1] == conversationFilter) && (projectFilter == "" || h.conversationProjectID(parts[1], projectCache) == projectFilter) {
filteredFolders = append(filteredFolders, rel)
continue
}
@@ -221,12 +348,480 @@ func (h *ChatUploadsHandler) List(c *gin.Context) {
sort.Slice(files, func(i, j int) bool {
return files[i].ModifiedUnix > files[j].ModifiedUnix
})
c.JSON(http.StatusOK, gin.H{"files": files, "folders": folders})
reductionFiles, err := h.collectReductionFiles(c, conversationFilter, projectFilter)
if err != nil {
h.logger.Warn("列举 reduction 产物失败", zap.Error(err))
} else if len(reductionFiles) > 0 {
files = append(files, reductionFiles...)
}
artifactFiles, err := h.collectConversationArtifactFiles(c, conversationFilter, projectFilter)
if err != nil {
h.logger.Warn("列举 conversation_artifacts 产物失败", zap.Error(err))
} else if len(artifactFiles) > 0 {
files = append(files, artifactFiles...)
}
sort.Slice(files, func(i, j int) bool {
return files[i].ModifiedUnix > files[j].ModifiedUnix
})
return files, folders, nil
}
func (h *ChatUploadsHandler) collectReductionFiles(c *gin.Context, conversationFilter, projectFilter string) ([]ChatUploadFileItem, error) {
root, err := h.absReductionRoot()
if err != nil {
return nil, err
}
if st, err := os.Stat(root); err != nil || !st.IsDir() {
return nil, nil
}
projectCache := make(map[string]string)
files := make([]ChatUploadFileItem, 0)
for _, scope := range []string{"conversations", "projects"} {
scopeRoot := filepath.Join(root, scope)
_ = filepath.WalkDir(scopeRoot, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil || d == nil || d.IsDir() {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return nil
}
relSlash := filepath.ToSlash(rel)
parts := strings.Split(relSlash, "/")
if len(parts) < 3 {
return nil
}
ownerID := parts[1]
if !h.reductionPathAllowed(c, scope, ownerID) {
return nil
}
var convID, projectID string
if scope == "conversations" {
convID = ownerID
projectID = h.conversationProjectID(convID, projectCache)
} else {
projectID = ownerID
}
if conversationFilter != "" && convID != conversationFilter {
return nil
}
if projectFilter != "" && projectID != projectFilter {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
name := d.Name()
if filepath.Ext(name) == "" {
name += ".txt"
}
abs, _ := filepath.Abs(path)
files = append(files, ChatUploadFileItem{
RelativePath: reductionVirtualPrefix + relSlash,
AbsolutePath: abs,
Name: name,
Source: chatUploadSourceReduction,
Size: info.Size(),
ModifiedUnix: info.ModTime().Unix(),
Date: info.ModTime().Format("2006-01-02"),
ConversationID: convID,
ProjectID: projectID,
SubPath: strings.Join(parts[2:len(parts)-1], "/"),
})
return nil
})
}
return files, nil
}
func (h *ChatUploadsHandler) collectConversationArtifactFiles(c *gin.Context, conversationFilter, projectFilter string) ([]ChatUploadFileItem, error) {
root, err := h.absConversationArtifactsRoot()
if err != nil {
return nil, err
}
if st, err := os.Stat(root); err != nil || !st.IsDir() {
return nil, nil
}
projectCache := make(map[string]string)
files := make([]ChatUploadFileItem, 0)
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil || d == nil || d.IsDir() {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return nil
}
relSlash := filepath.ToSlash(rel)
parts := strings.Split(relSlash, "/")
if len(parts) < 2 {
return nil
}
convID := parts[0]
if !h.conversationArtifactPathAllowed(c, convID) {
return nil
}
projectID := h.conversationProjectID(convID, projectCache)
if conversationFilter != "" && convID != conversationFilter {
return nil
}
if projectFilter != "" && projectID != projectFilter {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
name := d.Name()
if filepath.Ext(name) == "" {
name += ".txt"
}
abs, _ := filepath.Abs(path)
files = append(files, ChatUploadFileItem{
RelativePath: artifactVirtualPrefix + relSlash,
AbsolutePath: abs,
Name: name,
Source: chatUploadSourceConversation,
Size: info.Size(),
ModifiedUnix: info.ModTime().Unix(),
Date: info.ModTime().Format("2006-01-02"),
ConversationID: convID,
ProjectID: projectID,
SubPath: strings.Join(parts[1:len(parts)-1], "/"),
})
return nil
})
return files, nil
}
func (h *ChatUploadsHandler) resolveReductionVirtualPath(relativePath string) (string, error) {
rel := strings.TrimPrefix(strings.TrimSpace(relativePath), reductionVirtualPrefix)
rel = filepath.Clean(filepath.FromSlash(rel))
if rel == "." || strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("invalid path")
}
root, err := h.absReductionRoot()
if err != nil {
return "", err
}
full, err := filepath.Abs(filepath.Join(root, rel))
if err != nil {
return "", err
}
rootAbs, _ := filepath.Abs(root)
if full != rootAbs && !strings.HasPrefix(full, rootAbs+string(filepath.Separator)) {
return "", fmt.Errorf("path escapes reduction root")
}
return full, nil
}
func (h *ChatUploadsHandler) resolveConversationArtifactVirtualPath(relativePath string) (string, error) {
rel := strings.TrimPrefix(strings.TrimSpace(relativePath), artifactVirtualPrefix)
rel = filepath.Clean(filepath.FromSlash(rel))
if rel == "." || strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("invalid path")
}
root, err := h.absConversationArtifactsRoot()
if err != nil {
return "", err
}
full, err := filepath.Abs(filepath.Join(root, rel))
if err != nil {
return "", err
}
rootAbs, _ := filepath.Abs(root)
if full != rootAbs && !strings.HasPrefix(full, rootAbs+string(filepath.Separator)) {
return "", fmt.Errorf("path escapes conversation artifacts root")
}
return full, nil
}
func chatUploadItemIsInternal(item ChatUploadFileItem) bool {
return item.Source == chatUploadSourceReduction ||
item.Source == chatUploadSourceConversation ||
strings.HasPrefix(item.RelativePath, reductionVirtualPrefix) ||
strings.HasPrefix(item.RelativePath, artifactVirtualPrefix)
}
func (h *ChatUploadsHandler) resolveListedFilePath(item ChatUploadFileItem) (string, error) {
switch {
case item.Source == chatUploadSourceReduction || strings.HasPrefix(item.RelativePath, reductionVirtualPrefix):
return h.resolveReductionVirtualPath(item.RelativePath)
case item.Source == chatUploadSourceConversation || strings.HasPrefix(item.RelativePath, artifactVirtualPrefix):
return h.resolveConversationArtifactVirtualPath(item.RelativePath)
default:
return h.resolveUnderChatUploads(item.RelativePath)
}
}
func chatUploadSourceMatches(item ChatUploadFileItem, sourceFilter string) bool {
sourceFilter = strings.TrimSpace(sourceFilter)
if sourceFilter == "" || sourceFilter == "all" {
return true
}
source := strings.TrimSpace(item.Source)
if source == "" {
source = chatUploadSourceUpload
}
return source == sourceFilter
}
func chatUploadSearchMatches(item ChatUploadFileItem, search string) bool {
search = strings.ToLower(strings.TrimSpace(search))
if search == "" {
return true
}
values := []string{
item.RelativePath,
item.Name,
item.Source,
item.Date,
item.ConversationID,
item.ProjectID,
item.SubPath,
}
for _, value := range values {
if strings.Contains(strings.ToLower(value), search) {
return true
}
}
return false
}
func filterChatUploadItems(files []ChatUploadFileItem, sourceFilter, search string) []ChatUploadFileItem {
if strings.TrimSpace(sourceFilter) == "" && strings.TrimSpace(search) == "" {
return files
}
out := make([]ChatUploadFileItem, 0, len(files))
for _, item := range files {
if chatUploadSourceMatches(item, sourceFilter) && chatUploadSearchMatches(item, search) {
out = append(out, item)
}
}
return out
}
func parsePositiveIntQuery(c *gin.Context, key string, def, max int) int {
raw := strings.TrimSpace(c.Query(key))
if raw == "" {
return def
}
if strings.EqualFold(raw, "all") {
return 0
}
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return def
}
if max > 0 && n > max {
return max
}
return n
}
func paginateChatUploadItems(files []ChatUploadFileItem, page, pageSize int) []ChatUploadFileItem {
if pageSize <= 0 {
return files
}
if page <= 0 {
page = 1
}
start := (page - 1) * pageSize
if start >= len(files) {
return []ChatUploadFileItem{}
}
end := start + pageSize
if end > len(files) {
end = len(files)
}
return files[start:end]
}
// List GET /api/chat-uploads
func (h *ChatUploadsHandler) List(c *gin.Context) {
conversationFilter := strings.TrimSpace(c.Query("conversation"))
projectFilter := strings.TrimSpace(c.Query("project"))
sourceFilter := strings.TrimSpace(c.Query("source"))
search := strings.TrimSpace(c.Query("search"))
page := parsePositiveIntQuery(c, "page", 1, 0)
pageSize := parsePositiveIntQuery(c, "pageSize", 20, 200)
files, folders, err := h.collectFiles(c, conversationFilter, projectFilter)
if err != nil {
h.logger.Warn("列举对话附件失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
files = filterChatUploadItems(files, sourceFilter, search)
total := len(files)
paged := paginateChatUploadItems(files, page, pageSize)
c.JSON(http.StatusOK, gin.H{
"files": paged,
"folders": folders,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// Export GET /api/chat-uploads/export?conversation=...&project=...
func (h *ChatUploadsHandler) Export(c *gin.Context) {
conversationFilter := strings.TrimSpace(c.Query("conversation"))
projectFilter := strings.TrimSpace(c.Query("project"))
sourceFilter := strings.TrimSpace(c.Query("source"))
search := strings.TrimSpace(c.Query("search"))
files, _, err := h.collectFiles(c, conversationFilter, projectFilter)
if err != nil {
h.logger.Warn("导出对话附件失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
files = filterChatUploadItems(files, sourceFilter, search)
if len(files) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "no files to export"})
return
}
nameParts := []string{"chat-files"}
if projectFilter != "" {
nameParts = append(nameParts, "project-"+projectFilter)
}
if conversationFilter != "" {
nameParts = append(nameParts, "conversation-"+conversationFilter)
}
nameParts = append(nameParts, time.Now().Format("20060102-150405"))
filename := strings.Join(nameParts, "-") + ".zip"
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
zw := zip.NewWriter(c.Writer)
defer zw.Close()
manifest := gin.H{
"exportedAt": time.Now().UTC().Format(time.RFC3339),
"conversationId": conversationFilter,
"projectId": projectFilter,
"source": sourceFilter,
"search": search,
"fileCount": len(files),
"files": files,
"layout": "chat uploads are stored under conversations/<conversationId>/; internal outputs are stored under internal/<source>/",
"sourceDirectory": []string{chatUploadsRootDirName, reductionRootDirName, artifactsRootDirName},
}
manifestBytes, _ := json.MarshalIndent(manifest, "", " ")
mw, err := zw.Create("manifest.json")
if err != nil {
h.logger.Warn("写入附件导出清单失败", zap.Error(err))
return
}
_, _ = mw.Write(manifestBytes)
used := make(map[string]int)
for _, item := range files {
abs, err := h.resolveListedFilePath(item)
if err != nil {
continue
}
st, err := os.Stat(abs)
if err != nil || st.IsDir() {
continue
}
var zipName string
if item.Source == chatUploadSourceReduction || strings.HasPrefix(item.RelativePath, reductionVirtualPrefix) {
rel := strings.TrimPrefix(item.RelativePath, reductionVirtualPrefix)
zipName = filepath.ToSlash(filepath.Join("internal", "reduction", rel))
if filepath.Ext(zipName) == "" {
zipName += ".txt"
}
} else if item.Source == chatUploadSourceConversation || strings.HasPrefix(item.RelativePath, artifactVirtualPrefix) {
rel := strings.TrimPrefix(item.RelativePath, artifactVirtualPrefix)
zipName = filepath.ToSlash(filepath.Join("internal", "conversation_artifacts", rel))
if filepath.Ext(zipName) == "" {
zipName += ".txt"
}
} else {
conv := strings.TrimSpace(item.ConversationID)
if conv == "" || conv == "_manual" || conv == "_new" {
conv = "manual"
}
zipName = filepath.ToSlash(filepath.Join("conversations", conv, strings.TrimSpace(item.SubPath), item.Name))
}
if used[zipName] > 0 {
ext := filepath.Ext(zipName)
zipName = strings.TrimSuffix(zipName, ext) + fmt.Sprintf("-%d", used[zipName]+1) + ext
}
used[zipName]++
fw, err := zw.Create(zipName)
if err != nil {
h.logger.Warn("创建附件导出项失败", zap.String("path", item.RelativePath), zap.Error(err))
continue
}
src, err := os.Open(abs)
if err != nil {
continue
}
_, copyErr := io.Copy(fw, src)
_ = src.Close()
if copyErr != nil {
h.logger.Warn("复制附件导出项失败", zap.String("path", item.RelativePath), zap.Error(copyErr))
return
}
}
if h.audit != nil {
h.audit.RecordOK(c, "file", "export", "导出对话附件", "chat_upload", filename, map[string]interface{}{
"conversation_id": conversationFilter,
"project_id": projectFilter,
"file_count": len(files),
})
}
}
// Download GET /api/chat-uploads/download?path=...
func (h *ChatUploadsHandler) Download(c *gin.Context) {
p := c.Query("path")
if strings.HasPrefix(strings.TrimSpace(p), reductionVirtualPrefix) {
if !h.reductionVirtualPathAllowed(c, p) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
return
}
abs, err := h.resolveReductionVirtualPath(p)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
st, err := os.Stat(abs)
if err != nil || st.IsDir() {
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
return
}
name := filepath.Base(abs)
if filepath.Ext(name) == "" {
name += ".txt"
}
c.FileAttachment(abs, name)
return
}
if strings.HasPrefix(strings.TrimSpace(p), artifactVirtualPrefix) {
if !h.conversationArtifactVirtualPathAllowed(c, p) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
return
}
abs, err := h.resolveConversationArtifactVirtualPath(p)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
st, err := os.Stat(abs)
if err != nil || st.IsDir() {
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
return
}
name := filepath.Base(abs)
if filepath.Ext(name) == "" {
name += ".txt"
}
c.FileAttachment(abs, name)
return
}
if !h.pathAllowed(c, p) {
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
return
+38 -1
View File
@@ -5798,10 +5798,15 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"get": map[string]interface{}{
"tags": []string{"对话附件"},
"summary": "列出附件",
"description": "获取对话附件文件列表,可按对话ID过滤。",
"description": "获取对话文件列表,包含手动上传附件、工具输出和会话产物,可按会话、项目、来源、文件名搜索和分页过滤。",
"operationId": "listChatUploads",
"parameters": []map[string]interface{}{
{"name": "conversation", "in": "query", "required": false, "description": "按对话ID过滤", "schema": map[string]interface{}{"type": "string"}},
{"name": "project", "in": "query", "required": false, "description": "按项目ID过滤", "schema": map[string]interface{}{"type": "string"}},
{"name": "source", "in": "query", "required": false, "description": "按来源过滤:upload/reduction/conversation_artifact/all", "schema": map[string]interface{}{"type": "string", "enum": []string{"all", "upload", "reduction", "conversation_artifact"}}},
{"name": "search", "in": "query", "required": false, "description": "按文件名或子路径搜索", "schema": map[string]interface{}{"type": "string"}},
{"name": "page", "in": "query", "required": false, "description": "页码,从1开始", "schema": map[string]interface{}{"type": "integer", "default": 1}},
{"name": "pageSize", "in": "query", "required": false, "description": "每页数量,传 all 返回全部", "schema": map[string]interface{}{"oneOf": []map[string]interface{}{{"type": "integer"}, {"type": "string", "enum": []string{"all"}}}}},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
@@ -5823,11 +5828,18 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
"modifiedUnix": map[string]interface{}{"type": "integer"},
"date": map[string]interface{}{"type": "string"},
"conversationId": map[string]interface{}{"type": "string"},
"projectId": map[string]interface{}{"type": "string"},
"subPath": map[string]interface{}{"type": "string"},
"source": map[string]interface{}{"type": "string", "description": "upload/reduction/conversation_artifact"},
},
},
},
"folders": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}},
"total": map[string]interface{}{"type": "integer"},
"page": map[string]interface{}{"type": "integer"},
"pageSize": map[string]interface{}{
"type": "integer",
},
},
},
},
@@ -5902,6 +5914,31 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
},
},
"/api/chat-uploads/export": map[string]interface{}{
"get": map[string]interface{}{
"tags": []string{"对话附件"},
"summary": "导出附件",
"description": "按当前过滤条件导出对话文件 ZIP,包含 manifest.json。",
"operationId": "exportChatUploads",
"parameters": []map[string]interface{}{
{"name": "conversation", "in": "query", "required": false, "description": "按对话ID过滤", "schema": map[string]interface{}{"type": "string"}},
{"name": "project", "in": "query", "required": false, "description": "按项目ID过滤", "schema": map[string]interface{}{"type": "string"}},
{"name": "source", "in": "query", "required": false, "description": "按来源过滤:upload/reduction/conversation_artifact/all", "schema": map[string]interface{}{"type": "string", "enum": []string{"all", "upload", "reduction", "conversation_artifact"}}},
{"name": "search", "in": "query", "required": false, "description": "按文件名或子路径搜索", "schema": map[string]interface{}{"type": "string"}},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "ZIP文件下载",
"content": map[string]interface{}{
"application/zip": map[string]interface{}{
"schema": map[string]interface{}{"type": "string", "format": "binary"},
},
},
},
"401": map[string]interface{}{"description": "未授权"},
},
},
},
"/api/chat-uploads/download": map[string]interface{}{
"get": map[string]interface{}{
"tags": []string{"对话附件"},
+1 -1
View File
@@ -61,7 +61,7 @@ var apiDocI18nSummaryToKey = map[string]string{
"获取连接状态": "getWebshellConnectionState", "保存连接状态": "saveWebshellConnectionState",
"获取AI对话历史": "getWebshellAIHistory", "列出AI对话": "listWebshellAIConversations",
"执行WebShell命令": "webshellExec", "WebShell文件操作": "webshellFileOp",
"列出附件": "listChatUploads", "上传附件": "uploadChatFile", "删除附件": "deleteChatUpload",
"列出附件": "listChatUploads", "导出附件": "exportChatUploads", "上传附件": "uploadChatFile", "删除附件": "deleteChatUpload",
"下载附件": "downloadChatUpload", "获取附件文本内容": "getChatUploadContent",
"写入附件文本内容": "putChatUploadContent", "创建附件目录": "mkdirChatUpload", "重命名附件": "renameChatUpload",
"企业微信回调验证": "wecomCallbackVerify", "企业微信消息回调": "wecomCallbackMessage",