Initial commit: GovAI 政务AI平台
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user