feat: 更新数据库和后端服务
- 新增chat_sessions和chat_groups数据库表 - 更新skill_handler和model相关接口 - 修改main.go注册新路由 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -155,7 +157,7 @@ func (h *SkillHandler) Create(c *gin.Context) {
|
||||
// 处理文件上传
|
||||
file, err := c.FormFile("file")
|
||||
if err == nil {
|
||||
// 读取文件内容,解析 YAML front matter 获取 name
|
||||
// 重新打开文件以便多次读取
|
||||
fileContent, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file: " + err.Error()})
|
||||
@@ -163,51 +165,172 @@ func (h *SkillHandler) Create(c *gin.Context) {
|
||||
}
|
||||
defer fileContent.Close()
|
||||
|
||||
content := make([]byte, 1024*1024) // 最多读取 1MB
|
||||
n, _ := fileContent.Read(content)
|
||||
contentStr := string(content[:n])
|
||||
// 检测是否是 zip 文件
|
||||
isZip := strings.HasSuffix(strings.ToLower(file.Filename), ".zip")
|
||||
|
||||
// 解析 name
|
||||
parsedName, parsedDesc := parseSkillMeta(contentStr)
|
||||
log.Printf("[SkillHandler] Original skill_name from form: %s, parsed name from file: %s", originalSkillName, parsedName)
|
||||
var contentStr string
|
||||
|
||||
// 优先使用文件解析出的 name
|
||||
if parsedName != "" {
|
||||
skillName = parsedName
|
||||
} else if skillName == "" {
|
||||
// 如果解析不到且表单也没传,用文件名
|
||||
skillName = filepath.Base(file.Filename)
|
||||
}
|
||||
if isZip {
|
||||
// 解压 zip 文件
|
||||
log.Printf("[SkillHandler] Processing ZIP file: %s", file.Filename)
|
||||
|
||||
if parsedDesc != "" {
|
||||
skillDesc = parsedDesc
|
||||
}
|
||||
// 读取整个 zip 内容
|
||||
zipData, err := io.ReadAll(fileContent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read zip file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 清理 skillName,只保留纯文件名,去除所有路径
|
||||
skillName = filepath.Base(skillName)
|
||||
// 去除 .md 后缀
|
||||
skillName = strings.TrimSuffix(skillName, ".md")
|
||||
// 去除空格
|
||||
skillName = strings.TrimSpace(skillName)
|
||||
// 如果包含路径分隔符,取最后一部分
|
||||
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
|
||||
skillName = skillName[idx+1:]
|
||||
}
|
||||
// 创建临时 zip reader
|
||||
zipReader, err := zip.NewReader(strings.NewReader(string(zipData)), int64(len(zipData)))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse zip file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[SkillHandler] Final skill name: %s", skillName)
|
||||
// 先创建技能目录(使用传入的 skill_name 或从 zip 文件名推断)
|
||||
if skillName == "" {
|
||||
skillName = strings.TrimSuffix(filepath.Base(file.Filename), ".zip")
|
||||
}
|
||||
skillName = strings.TrimSpace(skillName)
|
||||
|
||||
// 创建技能目录
|
||||
skillPath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
|
||||
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
|
||||
return
|
||||
}
|
||||
skillPath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
|
||||
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 保存文件(使用之前读取的内容)
|
||||
skillFilePath := filepath.Join(skillPath, "SKILL.md")
|
||||
if err := os.WriteFile(skillFilePath, []byte(contentStr), 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()})
|
||||
return
|
||||
// 解压所有文件
|
||||
for _, zipFile := range zipReader.File {
|
||||
fileName := zipFile.Name
|
||||
// 跳过目录
|
||||
if strings.HasSuffix(fileName, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 确保文件路径安全,去除任何绝对路径或路径遍历
|
||||
fileName = strings.TrimPrefix(fileName, "/")
|
||||
if strings.Contains(fileName, "..") {
|
||||
continue
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(skillPath, fileName)
|
||||
|
||||
// 创建父目录
|
||||
parentDir := filepath.Dir(targetPath)
|
||||
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
||||
log.Printf("[SkillHandler] Warning: failed to create directory %s: %v", parentDir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取并解压文件
|
||||
zipFileContent, err := zipFile.Open()
|
||||
if err != nil {
|
||||
log.Printf("[SkillHandler] Warning: failed to open %s in zip: %v", fileName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(zipFileContent)
|
||||
zipFileContent.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[SkillHandler] Warning: failed to read %s from zip: %v", fileName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.WriteFile(targetPath, content, 0644); err != nil {
|
||||
log.Printf("[SkillHandler] Warning: failed to write %s: %v", targetPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[SkillHandler] Extracted: %s", targetPath)
|
||||
|
||||
// 如果是 SKILL.md,解析元数据
|
||||
if strings.HasSuffix(strings.ToLower(fileName), "skill.md") {
|
||||
contentStr = string(content)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到 SKILL.md,尝试找其他 .md 文件
|
||||
if contentStr == "" {
|
||||
files, _ := filepath.Glob(filepath.Join(skillPath, "*.md"))
|
||||
if len(files) > 0 {
|
||||
content, err := os.ReadFile(files[0])
|
||||
if err == nil {
|
||||
contentStr = string(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析元数据
|
||||
if contentStr != "" {
|
||||
parsedName, parsedDesc := parseSkillMeta(contentStr)
|
||||
if parsedName != "" {
|
||||
// 如果从 zip 中解析到 skill name,可能需要重命名目录
|
||||
if parsedName != skillName {
|
||||
newSkillPath := filepath.Join(projectRoot, "core", "agents", "skills", skillDir, parsedName)
|
||||
if err := os.Rename(skillPath, newSkillPath); err != nil {
|
||||
log.Printf("[SkillHandler] Warning: failed to rename skill directory: %v", err)
|
||||
} else {
|
||||
skillPath = newSkillPath
|
||||
}
|
||||
skillName = parsedName
|
||||
}
|
||||
}
|
||||
if parsedDesc != "" {
|
||||
skillDesc = parsedDesc
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[SkillHandler] ZIP imported successfully: %s", skillName)
|
||||
} else {
|
||||
// 普通 md 文件处理(原有逻辑)
|
||||
content := make([]byte, 1024*1024) // 最多读取 1MB
|
||||
n, _ := fileContent.Read(content)
|
||||
contentStr = string(content[:n])
|
||||
|
||||
// 解析 name
|
||||
parsedName, parsedDesc := parseSkillMeta(contentStr)
|
||||
log.Printf("[SkillHandler] Original skill_name from form: %s, parsed name from file: %s", originalSkillName, parsedName)
|
||||
|
||||
// 优先使用文件解析出的 name
|
||||
if parsedName != "" {
|
||||
skillName = parsedName
|
||||
} else if skillName == "" {
|
||||
// 如果解析不到且表单也没传,用文件名
|
||||
skillName = filepath.Base(file.Filename)
|
||||
}
|
||||
|
||||
if parsedDesc != "" {
|
||||
skillDesc = parsedDesc
|
||||
}
|
||||
|
||||
// 清理 skillName,只保留纯文件名,去除所有路径
|
||||
skillName = filepath.Base(skillName)
|
||||
// 去除 .md 后缀
|
||||
skillName = strings.TrimSuffix(skillName, ".md")
|
||||
// 去除空格
|
||||
skillName = strings.TrimSpace(skillName)
|
||||
// 如果包含路径分隔符,取最后一部分
|
||||
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
|
||||
skillName = skillName[idx+1:]
|
||||
}
|
||||
|
||||
log.Printf("[SkillHandler] Final skill name: %s", skillName)
|
||||
|
||||
// 创建技能目录
|
||||
skillPath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
|
||||
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 保存文件(使用之前读取的内容)
|
||||
skillFilePath := filepath.Join(skillPath, "SKILL.md")
|
||||
if err := os.WriteFile(skillFilePath, []byte(contentStr), 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有上传文件但提供了 name 和 desc,创建默认文件
|
||||
|
||||
@@ -12,7 +12,7 @@ type ModelInfo struct {
|
||||
APIKey string `json:"api_key" gorm:"type:text"` // API 密钥
|
||||
BaseURL string `json:"base_url" gorm:"type:varchar(500)"` // 基础 URL
|
||||
APIEndpoint string `json:"api_endpoint" gorm:"type:varchar(500)"` // API 端点路径
|
||||
Status string `json:"status" gorm:"type:varchar(20);default:active"` // active/inactive
|
||||
Status int `json:"status" gorm:"type:tinyint;default:0"` // 1:active, 0:inactive
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -39,7 +39,7 @@ type CreateModelRequest struct {
|
||||
APIKey string `json:"api_key" binding:"required"`
|
||||
BaseURL string `json:"base_url" binding:"required"`
|
||||
APIEndpoint string `json:"api_endpoint"`
|
||||
Status string `json:"status"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateModelRequest 更新模型请求
|
||||
@@ -51,7 +51,7 @@ type UpdateModelRequest struct {
|
||||
APIKey string `json:"api_key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIEndpoint string `json:"api_endpoint"`
|
||||
Status string `json:"status"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// TestModelRequest 测试模型连接请求
|
||||
|
||||
@@ -41,10 +41,10 @@ func (s *ModelService) Create(req model.CreateModelRequest) (*model.ModelInfo, e
|
||||
return nil, fmt.Errorf("model with name '%s' already exists", req.Name)
|
||||
}
|
||||
|
||||
// 如果没有提供状态,默认设置为 inactive
|
||||
// 如果没有提供状态,默认设置为 inactive (0)
|
||||
status := req.Status
|
||||
if status == "" {
|
||||
status = "inactive"
|
||||
if status == 0 {
|
||||
status = 0 // inactive
|
||||
}
|
||||
|
||||
info := &model.ModelInfo{
|
||||
@@ -96,7 +96,9 @@ func (s *ModelService) Update(id string, req model.UpdateModelRequest) (*model.M
|
||||
if req.APIEndpoint != "" {
|
||||
fields["api_endpoint"] = req.APIEndpoint
|
||||
}
|
||||
if req.Status != "" {
|
||||
// Status为int类型,0表示inactive,1表示active
|
||||
// 只在明确传入Status值时才更新(Status > 0 表示传入了值)
|
||||
if req.Status > 0 {
|
||||
fields["status"] = req.Status
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user