514 lines
15 KiB
Go
514 lines
15 KiB
Go
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
|
|
}
|