package transport import ( "context" "errors" "io" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/geo-platform/tenant-api/internal/bootstrap" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/tenant/app" ) type KolManageHandler struct { profileSvc *app.KolProfileService pkgSvc *app.KolPackageService promptSvc *app.KolPromptService assistSvc *app.KolAssistService assets *AssetHandler } type kolAssistStreamEvent struct { Mode string `json:"mode,omitempty"` Status string `json:"status"` Delta string `json:"delta,omitempty"` Content string `json:"content,omitempty"` Schema *app.KolSchemaJSON `json:"schema,omitempty"` Error string `json:"error,omitempty"` Model string `json:"model,omitempty"` Done bool `json:"done"` } func NewKolManageHandler(a *bootstrap.App, assets *AssetHandler) *KolManageHandler { profileSvc := app.NewKolProfileService(a.KolProfiles).WithObjectStorage(a.ObjectStorage) return &KolManageHandler{ profileSvc: profileSvc, pkgSvc: app.NewKolPackageService(profileSvc, a.KolPackages, a.KolPrompts, a.KolSubscriptions).WithCache(a.Cache), promptSvc: app.NewKolPromptService(a.DB, profileSvc, a.KolPackages, a.KolPrompts, a.KolSubscriptions, a.KolPromptAsset).WithCache(a.Cache), assistSvc: app.NewKolAssistService(a.DB, profileSvc, a.KolAssists, a.LLM, a.RabbitMQ, a.Config.LLM, a.Logger).WithCache(a.Cache), assets: assets, } } func (h *KolManageHandler) GetProfile(c *gin.Context) { actor, ok := actorFromRequest(c) if !ok { return } profile, err := h.profileSvc.RequireActiveKol(c.Request.Context(), actor) if err != nil { response.Error(c, err) return } response.Success(c, app.NewKolProfileResponse(profile)) } type updateKolProfileRequest struct { DisplayName string `json:"display_name"` Bio *string `json:"bio"` AvatarURL *string `json:"avatar_url"` } func (h *KolManageHandler) UpdateProfile(c *gin.Context) { actor, ok := actorFromRequest(c) if !ok { return } var req updateKolProfileRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } profile, err := h.profileSvc.UpdateProfile(c.Request.Context(), actor, app.UpdateKolProfileServiceInput{ DisplayName: req.DisplayName, Bio: req.Bio, AvatarURL: req.AvatarURL, }) if err != nil { response.Error(c, err) return } response.Success(c, app.NewKolProfileResponse(profile)) } func (h *KolManageHandler) UploadAvatar(c *gin.Context) { actor, ok := actorFromRequest(c) if !ok { return } fileHeader, err := c.FormFile("file") if err != nil { response.Error(c, response.ErrBadRequest(40004, "kol_avatar_invalid", "avatar file is required")) return } f, err := fileHeader.Open() if err != nil { response.Error(c, response.ErrBadRequest(40004, "kol_avatar_invalid", "avatar file cannot be opened")) return } defer f.Close() content, err := io.ReadAll(f) if err != nil { response.Error(c, response.ErrBadRequest(40004, "kol_avatar_invalid", "avatar file cannot be read")) return } result, err := h.profileSvc.UploadAvatar(c.Request.Context(), actor, fileHeader.Filename, content) if err != nil { response.Error(c, err) return } result.URL = h.assets.BuildArticleImageURL(result.ObjectKey) response.SuccessWithStatus(c, http.StatusCreated, result) } func (h *KolManageHandler) ListPackages(c *gin.Context) { actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.List(c.Request.Context(), actor) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) CreatePackage(c *gin.Context) { var req app.CreateKolPackageInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.Create(c.Request.Context(), actor, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) UpdatePackage(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } var req app.UpdateKolPackageInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.Update(c.Request.Context(), actor, id, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) DeletePackage(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } if err := h.pkgSvc.Delete(c.Request.Context(), actor, id); err != nil { response.Error(c, err) return } response.Success(c, nil) } func (h *KolManageHandler) PublishPackage(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.Publish(c.Request.Context(), actor, id) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) ArchivePackage(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.Archive(c.Request.Context(), actor, id) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) SelfSubscribePackage(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.SelfSubscribe(c.Request.Context(), actor, id) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) CancelSelfSubscription(c *gin.Context) { id, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.pkgSvc.CancelSelfSubscription(c.Request.Context(), actor, id) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) ListPrompts(c *gin.Context) { packageID, ok := parseInt64Param(c, "id", "package id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.ListByPackage(c.Request.Context(), actor, packageID) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) CreatePrompt(c *gin.Context) { packageID, ok := parseInt64Param(c, "id", "package id") if !ok { return } var req app.CreatePromptInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } req.PackageID = packageID actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.CreatePrompt(c.Request.Context(), actor, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) GetPromptLatest(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.GetForOwner(c.Request.Context(), actor, promptID) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) UpdatePromptMetadata(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } var req app.UpdateKolPromptInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.UpdateMetadata(c.Request.Context(), actor, promptID, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) DeletePrompt(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } if err := h.promptSvc.Delete(c.Request.Context(), actor, promptID); err != nil { response.Error(c, err) return } response.Success(c, nil) } func (h *KolManageHandler) SavePrompt(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } var req app.PublishPromptInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.Save(c.Request.Context(), actor, promptID, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) PublishPrompt(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } var req app.PublishPromptInput if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.Publish(c.Request.Context(), actor, promptID, req) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) ActivatePrompt(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.Activate(c.Request.Context(), actor, promptID) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) ArchivePrompt(c *gin.Context) { promptID, ok := parseInt64Param(c, "id", "prompt id") if !ok { return } actor, ok := actorFromRequest(c) if !ok { return } data, err := h.promptSvc.Archive(c.Request.Context(), actor, promptID) if err != nil { response.Error(c, err) return } response.Success(c, data) } func (h *KolManageHandler) AssistSubmit(c *gin.Context) { var req app.AssistRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } taskID, err := h.assistSvc.Submit(c.Request.Context(), actor, req) if err != nil { response.Error(c, err) return } response.Success(c, app.KolAssistSubmitResponse{TaskID: taskID}) } func (h *KolManageHandler) AssistStream(c *gin.Context) { var req app.AssistRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error())) return } actor, ok := actorFromRequest(c) if !ok { return } c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") flusher, ok := c.Writer.(http.Flusher) if !ok { response.Error(c, response.ErrInternal(50011, "stream_unsupported", "response writer does not support streaming")) return } if err := writeSSE(c, "start", kolAssistStreamEvent{ Mode: req.Mode, Status: "generating", Done: false, }); err != nil { return } flusher.Flush() var streamed strings.Builder var streamWriteErr error result, model, err := h.assistSvc.Run(c.Request.Context(), actor, req, func(delta string) { if delta == "" || streamWriteErr != nil { return } streamed.WriteString(delta) if writeErr := writeSSE(c, "delta", kolAssistStreamEvent{ Mode: req.Mode, Status: "generating", Delta: delta, Content: streamed.String(), Done: false, }); writeErr != nil { streamWriteErr = writeErr return } flusher.Flush() }) if streamWriteErr != nil { return } if err != nil { if errors.Is(c.Request.Context().Err(), context.Canceled) || errors.Is(c.Request.Context().Err(), context.DeadlineExceeded) { return } appErr := response.Normalize(err) _ = writeSSE(c, "error", kolAssistStreamEvent{ Mode: req.Mode, Status: "failed", Content: strings.TrimSpace(streamed.String()), Error: resolveKolAssistStreamErrorMessage(appErr), Done: true, }) flusher.Flush() return } if err := writeSSE(c, "completed", kolAssistStreamEvent{ Mode: req.Mode, Status: "completed", Content: result.Content, Schema: &result.Schema, Model: model, Done: true, }); err != nil { return } flusher.Flush() } func (h *KolManageHandler) AssistGet(c *gin.Context) { taskID := c.Param("id") if taskID == "" { response.Error(c, response.ErrBadRequest(40001, "invalid_id", "task id must not be empty")) return } actor, ok := actorFromRequest(c) if !ok { return } task, err := h.assistSvc.Get(c.Request.Context(), actor, taskID) if err != nil { response.Error(c, err) return } data, err := app.NewKolAssistTaskResponse(task) if err != nil { response.Error(c, err) return } response.Success(c, data) } func actorFromRequest(c *gin.Context) (auth.Actor, bool) { actor, ok := auth.ActorFromCtx(c.Request.Context()) if !ok { response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated")) return auth.Actor{}, false } return actor, true } func parseInt64Param(c *gin.Context, key, label string) (int64, bool) { id, err := strconv.ParseInt(c.Param(key), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40001, "invalid_id", label+" must be a number")) return 0, false } return id, true } func resolveKolAssistStreamErrorMessage(appErr *response.AppError) string { if appErr == nil { return "AI 处理失败,请稍后重试" } if detail := strings.TrimSpace(appErr.Detail); detail != "" { return detail } if message := strings.TrimSpace(appErr.Message); message != "" { return message } return "AI 处理失败,请稍后重试" }