Files
Atlas/internal/api/user_handlers.go
T
renjue f920f465ed
CI / docker (push) Successful in 2m28s
实现 Atlas AI 平台一期能力并完成品牌化改造。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:22:19 +08:00

440 lines
13 KiB
Go

package api
import (
"encoding/json"
"net/http"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/rose_cat707/Atlas/internal/domain"
"github.com/rose_cat707/Atlas/internal/provider"
"github.com/rose_cat707/Atlas/internal/service/asset"
"github.com/rose_cat707/Atlas/internal/service/chat"
"github.com/rose_cat707/Atlas/internal/store"
)
func (s *Server) registerUserRoutes(r gin.IRoutes) {
r.Use(s.sessionMiddleware())
r.POST("/user/session", s.userSession)
r.GET("/user/meta", s.userMeta)
r.GET("/user/conversations", s.listConversations)
r.POST("/user/conversations", s.createConversation)
r.POST("/user/conversations/messages", s.postNewConversationMessage)
r.GET("/user/conversations/:id", s.getConversation)
r.PUT("/user/conversations/:id", s.updateConversation)
r.DELETE("/user/conversations/:id", s.deleteConversation)
r.GET("/user/conversations/:id/messages", s.listMessages)
r.POST("/user/conversations/:id/messages", s.postMessage)
r.POST("/user/generate", s.userGenerate)
r.POST("/user/upload", s.userUpload)
r.GET("/user/files/:id", s.serveFile)
r.GET("/user/generations", s.listGenerations)
r.GET("/user/generations/:id", s.getGeneration)
r.DELETE("/user/generations/:id", s.deleteGeneration)
r.GET("/user/presets", s.listPresets)
r.POST("/user/presets", s.createPreset)
r.PUT("/user/presets/:id", s.updatePreset)
r.DELETE("/user/presets/:id", s.deletePreset)
r.GET("/user/tasks/active", s.listActiveTasks)
r.GET("/user/tasks/:id/events", s.streamTaskEvents)
r.GET("/user/tasks/:id", s.getTask)
r.POST("/user/tasks/:id/cancel", s.cancelTask)
}
func (s *Server) userSession(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"session_id": sessionID(c)})
}
func (s *Server) userMeta(c *gin.Context) {
ctx := c.Request.Context()
specs := s.app.Invoke.Specs()
imageModels, _ := s.app.Models.ListImageModelsForMeta(ctx)
out := gin.H{}
for st, spec := range specs {
providers, err := s.app.Store.ListProviders(ctx, string(st), true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var provList []gin.H
var allModels any
switch st {
case domain.ServiceImageGeneration:
allModels = imageModels
default:
var entries []provider.ModelEntry
for _, p := range providers {
cfg, err := s.app.Router.Resolve(c.Request.Context(), string(st), p.ID)
if err != nil {
continue
}
models, err := s.app.Models.ListEffective(ctx, cfg)
if err != nil {
continue
}
entries = append(entries, models...)
}
allModels = entries
}
for _, p := range providers {
provList = append(provList, gin.H{"id": p.ID, "name": p.Name, "is_default": p.IsDefault})
}
caps := gin.H{}
if spec.Capabilities.Stream != nil {
caps["stream"] = true
}
if st == domain.ServiceImageGeneration {
if spec.Capabilities.Async != nil {
caps["async"] = true
}
} else {
if spec.Capabilities.Sync != nil {
caps["sync"] = true
}
if spec.Capabilities.Async != nil {
caps["async"] = true
}
}
defaultModel := ""
switch models := allModels.(type) {
case []provider.ModelDetail:
defaultModel = provider.DefaultModelDetail(models)
case []provider.ModelEntry:
defaultModel = provider.DefaultModel(models)
}
out[string(st)] = gin.H{
"capabilities": caps,
"providers": provList,
"models": allModels,
"default_model": defaultModel,
}
}
c.JSON(http.StatusOK, gin.H{"service_types": out})
}
func (s *Server) listConversations(c *gin.Context) {
items, err := s.app.Store.ListConversations(c.Request.Context(), sessionID(c))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) createConversation(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "conversation is created when sending the first message"})
}
func (s *Server) postNewConversationMessage(c *gin.Context) {
s.streamConversationMessage(c, 0)
}
func (s *Server) getConversation(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
conv, err := s.app.Store.GetConversation(c.Request.Context(), sessionID(c), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, conv)
}
func (s *Server) updateConversation(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
var in store.Conversation
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
in.ID = id
in.SessionID = sessionID(c)
if err := s.app.Store.UpdateConversation(c.Request.Context(), &in); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, in)
}
func (s *Server) deleteConversation(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := s.app.Store.DeleteConversation(c.Request.Context(), sessionID(c), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listMessages(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
items, err := s.app.Store.ListMessages(c.Request.Context(), id, 80)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) postMessage(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
s.streamConversationMessage(c, id)
}
func (s *Server) streamConversationMessage(c *gin.Context, convID int64) {
var in struct {
Content string `json:"content"`
Model string `json:"model"`
ProviderID int64 `json:"provider_id"`
}
if err := c.ShouldBindJSON(&in); err != nil || in.Content == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "content required"})
return
}
ctx := c.Request.Context()
sid := sessionID(c)
if convID == 0 {
conv := &store.Conversation{
SessionID: sid,
Title: "新对话",
Model: in.Model,
ParamsJSON: "{}",
}
id, err := s.app.Store.CreateConversation(ctx, conv)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
convID = id
} else if _, err := s.app.Store.GetConversation(ctx, sid, convID); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.Header("X-Conversation-Id", strconv.FormatInt(convID, 10))
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Status(http.StatusOK)
err := s.app.Chat.StreamMessage(ctx, chat.StreamRequest{
SessionID: sid,
ConversationID: convID,
Content: in.Content,
Model: in.Model,
ProviderID: in.ProviderID,
}, c.Writer, func() { c.Writer.Flush() })
if err != nil {
_, _ = c.Writer.Write([]byte("data: " + `{"error":"` + err.Error() + `"}` + "\n\n"))
c.Writer.Flush()
}
}
func (s *Server) userGenerate(c *gin.Context) {
var in struct {
ServiceType string `json:"service_type"`
Params json.RawMessage `json:"params"`
Model string `json:"model"`
ProviderID int64 `json:"provider_id"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
st := domain.ServiceType(in.ServiceType)
if !domain.IsImageInvokeService(st) {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported service_type"})
return
}
params := in.Params
if len(in.Params) > 0 {
var resolved map[string]any
if err := json.Unmarshal(in.Params, &resolved); err == nil {
asset.ResolvePublicFileURLs(resolved, asset.PublicBaseURL(c.Request))
if encoded, err := json.Marshal(resolved); err == nil {
params = encoded
}
}
}
result, err := s.app.Invoke.Run(c.Request.Context(), domain.InvokeRequest{
SessionID: sessionID(c),
ServiceType: st,
ProviderID: in.ProviderID,
Model: in.Model,
Params: params,
Mode: domain.ModeAsync,
})
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func (s *Server) userUpload(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
return
}
f, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer f.Close()
path, size, mimeType, err := asset.SaveUpload(s.app.DataDir, sessionID(c), file.Filename, f, 10<<20)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
exp := time.Now().Add(7 * 24 * time.Hour).UTC().Format(time.RFC3339)
rec := &store.FileRecord{
SessionID: sessionID(c),
Kind: "upload",
Mime: mimeType,
StoragePath: path,
SizeBytes: size,
ExpiresAt: &exp,
}
id, err := s.app.Store.InsertFile(c.Request.Context(), rec)
if err != nil {
os.Remove(path)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
signed := s.app.Assets.SignedURL(asset.APIBaseURL(c.Request), id, 7*24*time.Hour)
c.JSON(http.StatusOK, gin.H{"file_id": id, "signed_url": signed, "mime": mimeType})
}
func (s *Server) serveFile(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
expStr := c.Query("exp")
sig := c.Query("sig")
exp, sig, err := asset.ParseSignedQuery(expStr, sig)
if err != nil || !s.app.Assets.Verify(id, exp, sig) {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid signature"})
return
}
rec, err := s.app.Store.GetFileByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.Header("Content-Type", rec.Mime)
c.File(rec.StoragePath)
}
func (s *Server) listGenerations(c *gin.Context) {
limit, offset := parseLimitOffset(c, 20, 100)
st := c.Query("service_type")
items, total, err := s.app.Store.ListGenerations(c.Request.Context(), sessionID(c), st, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
}
func (s *Server) getGeneration(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
g, err := s.app.Store.GetGeneration(c.Request.Context(), sessionID(c), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, g)
}
func (s *Server) deleteGeneration(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := s.app.Store.DeleteGeneration(c.Request.Context(), sessionID(c), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listPresets(c *gin.Context) {
items, err := s.app.Store.ListPresets(c.Request.Context(), sessionID(c), c.Query("service_type"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) createPreset(c *gin.Context) {
var in store.Preset
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
in.SessionID = sessionID(c)
id, err := s.app.Store.CreatePreset(c.Request.Context(), &in)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
in.ID = id
c.JSON(http.StatusOK, in)
}
func (s *Server) updatePreset(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
var in store.Preset
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
in.ID = id
in.SessionID = sessionID(c)
if err := s.app.Store.UpdatePreset(c.Request.Context(), &in); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, in)
}
func (s *Server) deletePreset(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := s.app.Store.DeletePreset(c.Request.Context(), sessionID(c), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listActiveTasks(c *gin.Context) {
items, err := s.app.Store.ListActiveTasks(c.Request.Context(), sessionID(c), c.Query("service_type"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (s *Server) getTask(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if v, ok := s.app.Store.Cache.Get("task:" + strconv.FormatInt(id, 10)); ok {
c.JSON(http.StatusOK, v)
return
}
t, err := s.app.Store.GetTask(c.Request.Context(), sessionID(c), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, t)
}
func (s *Server) cancelTask(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := s.app.Invoke.CancelTask(c.Request.Context(), sessionID(c), id); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}