package handler import ( "encoding/json" "fmt" "net/http" "strings" "time" "github.com/enterprise-ai-platform/server/internal/middleware" "github.com/enterprise-ai-platform/server/internal/response" "github.com/enterprise-ai-platform/server/pkg/dify" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) type CreatorHandler struct { pool *pgxpool.Pool dify *dify.Client } func NewCreatorHandler(pool *pgxpool.Pool, difyClient *dify.Client) *CreatorHandler { return &CreatorHandler{pool: pool, dify: difyClient} } type createAppRequest struct { Name string `json:"name"` Description string `json:"description"` LongDescription string `json:"long_description"` CategoryID string `json:"category_id"` Visibility string `json:"visibility"` AppType string `json:"app_type"` SystemPrompt string `json:"system_prompt"` WelcomeMessage string `json:"welcome_message"` SuggestedPrompts []string `json:"suggested_prompts"` Model string `json:"model"` Temperature float32 `json:"temperature"` MaxTokens int `json:"max_tokens"` KnowledgeBaseIDs []string `json:"knowledge_base_ids"` // Agent-type config Tools []string `json:"tools"` DataSources []string `json:"data_sources"` TemplateSet string `json:"template_set"` // Completion-type config InputLabel string `json:"input_label"` OutputLabel string `json:"output_label"` InputPlaceholder string `json:"input_placeholder"` FormatTemplates map[string]any `json:"format_templates"` } func buildAppConfig(req *createAppRequest) json.RawMessage { cfg := map[string]any{ "system_prompt": req.SystemPrompt, "model": req.Model, } if len(req.Tools) > 0 { cfg["tools"] = req.Tools } if len(req.DataSources) > 0 { cfg["data_sources"] = req.DataSources } if req.TemplateSet != "" { cfg["template_set"] = req.TemplateSet } if req.InputLabel != "" { cfg["input_label"] = req.InputLabel } if req.OutputLabel != "" { cfg["output_label"] = req.OutputLabel } if req.InputPlaceholder != "" { cfg["input_placeholder"] = req.InputPlaceholder } if len(req.FormatTemplates) > 0 { cfg["format_templates"] = req.FormatTemplates } b, _ := json.Marshal(cfg) return b } func (h *CreatorHandler) ListMyApps(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) role := middleware.GetRole(r.Context()) isAdmin := role == "admin" || role == "super_admin" var rows pgx.Rows var err error if isAdmin { // 管理员查看本机构所有应用(通过用户表获取org_id) rows, err = h.pool.Query(r.Context(), ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, a.dify_app_type, a.status, a.visibility, a.usage_count, a.updated_at FROM applications a LEFT JOIN categories c ON a.category_id = c.id WHERE a.org_id = (SELECT org_id FROM users WHERE id = $1) ORDER BY a.updated_at DESC`, userID) } else { rows, err = h.pool.Query(r.Context(), ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, a.dify_app_type, a.status, a.visibility, a.usage_count, a.updated_at FROM applications a LEFT JOIN categories c ON a.category_id = c.id WHERE a.creator_id = $1 ORDER BY a.updated_at DESC`, userID) } if err != nil { response.InternalError(w, "查询应用失败") return } defer rows.Close() var apps []map[string]any for rows.Next() { var ( id, name, slug, status, visibility string desc, iconURL, catName, appType *string usageCount int64 updatedAt time.Time ) if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &catName, &appType, &status, &visibility, &usageCount, &updatedAt); err != nil { continue } apps = append(apps, map[string]any{ "id": id, "name": name, "slug": slug, "description": desc, "icon_url": iconURL, "category_name": catName, "dify_app_type": appType, "status": status, "visibility": visibility, "usage_count": usageCount, "updated_at": updatedAt, }) } if apps == nil { apps = []map[string]any{} } response.JSON(w, http.StatusOK, apps) } func (h *CreatorHandler) GetApp(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) role := middleware.GetRole(r.Context()) isAdmin := role == "admin" || role == "super_admin" var ( id, name, slug, status, visibility, version string desc, longDesc, iconURL, catID, difyType *string welcomeMsg *string kbID *string appConfig json.RawMessage suggestedPrompts json.RawMessage maxTokens int temperature float32 usageCount int64 createdAt, updatedAt time.Time ) var query string var args []any if isAdmin { query = `SELECT a.id, a.name, a.slug, a.description, a.long_description, a.icon_url, a.category_id, a.dify_app_type, a.app_config, a.welcome_message, a.suggested_prompts, a.max_tokens, a.temperature, a.status, a.visibility, a.is_featured, a.usage_count, a.version, a.knowledge_base_id, a.created_at, a.updated_at FROM applications a WHERE a.id = $1` args = []any{appID} } else { query = `SELECT a.id, a.name, a.slug, a.description, a.long_description, a.icon_url, a.category_id, a.dify_app_type, a.app_config, a.welcome_message, a.suggested_prompts, a.max_tokens, a.temperature, a.status, a.visibility, a.is_featured, a.usage_count, a.version, a.knowledge_base_id, a.created_at, a.updated_at FROM applications a WHERE a.id = $1 AND a.creator_id = $2` args = []any{appID, userID} } err := h.pool.QueryRow(r.Context(), query, args...).Scan( &id, &name, &slug, &desc, &longDesc, &iconURL, &catID, &difyType, &appConfig, &welcomeMsg, &suggestedPrompts, &maxTokens, &temperature, &status, &visibility, new(bool), &usageCount, &version, &kbID, &createdAt, &updatedAt) if err != nil { response.NotFound(w, "应用不存在或无权访问") return } result := map[string]any{ "id": id, "name": name, "slug": slug, "description": desc, "long_description": longDesc, "icon_url": iconURL, "category_id": catID, "dify_app_type": difyType, "app_config": appConfig, "welcome_message": welcomeMsg, "suggested_prompts": suggestedPrompts, "max_tokens": maxTokens, "temperature": temperature, "status": status, "visibility": visibility, "usage_count": usageCount, "version": version, "knowledge_base_id": kbID, "created_at": createdAt, "updated_at": updatedAt, } response.JSON(w, http.StatusOK, result) } func (h *CreatorHandler) CreateApp(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) var req createAppRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } if req.Name == "" { response.BadRequest(w, "应用名称不能为空") return } if req.Visibility == "" { req.Visibility = "private" } if req.AppType == "" { req.AppType = "chatbot" } if req.Temperature == 0 { req.Temperature = 0.7 } if req.MaxTokens == 0 { req.MaxTokens = 4096 } slug := generateSlug(req.Name) suggestedPromptsJSON, _ := json.Marshal(req.SuggestedPrompts) appConfig := buildAppConfig(&req) difyAppID := "" difyAPIKey := "" var appID string err := h.pool.QueryRow(r.Context(), ` INSERT INTO applications ( name, slug, description, long_description, category_id, creator_id, dify_app_id, dify_app_type, dify_api_key, app_config, welcome_message, suggested_prompts, max_tokens, temperature, status, visibility ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, 'draft', $15) RETURNING id`, req.Name, slug, req.Description, req.LongDescription, nilIfEmpty(req.CategoryID), userID, nilIfEmpty(difyAppID), req.AppType, nilIfEmpty(difyAPIKey), appConfig, req.WelcomeMessage, string(suggestedPromptsJSON), req.MaxTokens, req.Temperature, req.Visibility, ).Scan(&appID) if err != nil { if strings.Contains(err.Error(), "duplicate key") { response.Error(w, http.StatusConflict, 40901, "应用名称已存在") return } response.InternalError(w, "创建应用失败: "+err.Error()) return } response.JSON(w, http.StatusCreated, map[string]any{ "id": appID, "name": req.Name, "slug": slug, "status": "draft", }) } func (h *CreatorHandler) UpdateApp(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) role := middleware.GetRole(r.Context()) isAdmin := role == "admin" || role == "super_admin" var status string var creatorID string err := h.pool.QueryRow(r.Context(), `SELECT status, creator_id FROM applications WHERE id = $1`, appID).Scan(&status, &creatorID) if err != nil { response.NotFound(w, "应用不存在") return } if !isAdmin && creatorID != userID.String() { response.Forbidden(w, "只能修改自己创建的应用") return } var req struct { createAppRequest KnowledgeBaseID string `json:"knowledge_base_id"` AppType string `json:"app_type"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.BadRequest(w, "无效的请求格式") return } suggestedPromptsJSON, _ := json.Marshal(req.SuggestedPrompts) appConfig := buildAppConfig(&req.createAppRequest) newStatus := status if status == "draft" || status == "rejected" { newStatus = "draft" } _, err = h.pool.Exec(r.Context(), ` UPDATE applications SET name = COALESCE(NULLIF($2, ''), name), description = COALESCE(NULLIF($3, ''), description), long_description = $4, category_id = COALESCE($5::UUID, category_id), app_config = $6, welcome_message = $7, suggested_prompts = $8, max_tokens = $9, temperature = $10, visibility = COALESCE(NULLIF($11, ''), visibility), knowledge_base_id = $12::UUID, dify_app_type = COALESCE(NULLIF($13, ''), dify_app_type), status = $14 WHERE id = $1`, appID, req.Name, req.Description, req.LongDescription, nilIfEmpty(req.CategoryID), appConfig, req.WelcomeMessage, string(suggestedPromptsJSON), req.MaxTokens, req.Temperature, req.Visibility, nilIfEmpty(req.KnowledgeBaseID), req.AppType, newStatus, ) if err != nil { response.InternalError(w, "更新应用失败: "+err.Error()) return } response.JSON(w, http.StatusOK, map[string]string{"message": "更新成功"}) } func (h *CreatorHandler) DeleteApp(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) role := middleware.GetRole(r.Context()) isAdmin := role == "admin" || role == "super_admin" var tag pgconn.CommandTag var err error if isAdmin { tag, err = h.pool.Exec(r.Context(), `DELETE FROM applications WHERE id = $1`, appID) } else { tag, err = h.pool.Exec(r.Context(), `DELETE FROM applications WHERE id = $1 AND creator_id = $2 AND status = 'draft'`, appID, userID) } if err != nil { response.InternalError(w, "删除失败") return } if tag.RowsAffected() == 0 { if isAdmin { response.NotFound(w, "应用不存在") } else { response.BadRequest(w, "只能删除草稿状态的应用") } return } response.JSON(w, http.StatusOK, nil) } func (h *CreatorHandler) SubmitReview(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) var status, creatorID, version string err := h.pool.QueryRow(r.Context(), `SELECT status, creator_id, version FROM applications WHERE id = $1`, appID, ).Scan(&status, &creatorID, &version) if err != nil { response.NotFound(w, "应用不存在") return } if creatorID != userID.String() { response.Forbidden(w, "只能提交自己创建的应用") return } if status != "draft" && status != "rejected" { response.BadRequest(w, "只有草稿或被驳回的应用可以提交审核") return } var req struct { Comment string `json:"comment"` } json.NewDecoder(r.Body).Decode(&req) tx, err := h.pool.Begin(r.Context()) if err != nil { response.InternalError(w, "事务开始失败") return } defer tx.Rollback(r.Context()) tx.Exec(r.Context(), ` INSERT INTO app_reviews (app_id, version, submitter_id, submit_comment) VALUES ($1, $2, $3, $4)`, appID, version, userID, req.Comment) tx.Exec(r.Context(), ` UPDATE applications SET status = 'pending_review' WHERE id = $1`, appID) if err := tx.Commit(r.Context()); err != nil { response.InternalError(w, "提交审核失败") return } response.JSON(w, http.StatusOK, nil) } func (h *CreatorHandler) WithdrawReview(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) var creatorID string h.pool.QueryRow(r.Context(), `SELECT creator_id FROM applications WHERE id = $1`, appID).Scan(&creatorID) if creatorID != userID.String() { response.Forbidden(w, "只能撤回自己的审核") return } tx, err := h.pool.Begin(r.Context()) if err != nil { response.InternalError(w, "事务开始失败") return } defer tx.Rollback(r.Context()) tx.Exec(r.Context(), ` UPDATE app_reviews SET status = 'withdrawn' WHERE app_id = $1 AND status = 'pending'`, appID) tx.Exec(r.Context(), ` UPDATE applications SET status = 'draft' WHERE id = $1 AND status = 'pending_review'`, appID) tx.Commit(r.Context()) response.JSON(w, http.StatusOK, nil) } func (h *CreatorHandler) ListTemplates(w http.ResponseWriter, r *http.Request) { rows, err := h.pool.Query(r.Context(), ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, c.slug as category_slug, a.usage_count, a.avg_rating, a.rating_count FROM applications a LEFT JOIN categories c ON a.category_id = c.id WHERE a.is_template = true AND a.status = 'approved' ORDER BY a.usage_count DESC`) if err != nil { response.InternalError(w, "查询模板失败") return } defer rows.Close() apps := scanAppList(rows) response.JSON(w, http.StatusOK, apps) } func generateSlug(name string) string { slug := strings.ToLower(strings.TrimSpace(name)) slug = strings.ReplaceAll(slug, " ", "-") return fmt.Sprintf("%s-%d", slug, time.Now().UnixMilli()%10000) } func (h *CreatorHandler) RequestDelist(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) var status, creatorID string err := h.pool.QueryRow(r.Context(), `SELECT status, creator_id FROM applications WHERE id = $1`, appID).Scan(&status, &creatorID) if err != nil { response.NotFound(w, "应用不存在") return } if creatorID != userID.String() { response.Forbidden(w, "只能操作自己创建的应用") return } if status != "approved" { response.BadRequest(w, "只有已上架的应用可以申请下架") return } _, err = h.pool.Exec(r.Context(), `UPDATE applications SET status = 'archived' WHERE id = $1`, appID) if err != nil { response.InternalError(w, "申请下架失败") return } response.JSON(w, http.StatusOK, map[string]string{"message": "已下架"}) } func nilIfEmpty(s string) *string { if s == "" { return nil } return &s }