Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+513
View File
@@ -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
}