Initial commit: GovAI 政务AI平台
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// 批量为 knowledge_chunks 生成 embedding 向量
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/config"
|
||||
"github.com/enterprise-ai-platform/server/pkg/embedding"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, cfg.Database.URL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "数据库连接失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
client := embedding.NewClient(embedding.Config{
|
||||
APIKey: cfg.Embedding.APIKey,
|
||||
BaseURL: cfg.Embedding.BaseURL,
|
||||
Model: cfg.Embedding.Model,
|
||||
Dimensions: cfg.Embedding.Dimensions,
|
||||
})
|
||||
|
||||
if !client.IsConfigured() {
|
||||
fmt.Fprintln(os.Stderr, "EMBEDDING_API_KEY 未配置")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 查询所有没有 embedding 的 chunks
|
||||
rows, err := pool.Query(ctx,
|
||||
`SELECT id, content FROM knowledge_chunks WHERE embedding IS NULL ORDER BY created_at`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "查询失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type chunk struct {
|
||||
id string
|
||||
content string
|
||||
}
|
||||
var chunks []chunk
|
||||
for rows.Next() {
|
||||
var c chunk
|
||||
if err := rows.Scan(&c.id, &c.content); err != nil {
|
||||
continue
|
||||
}
|
||||
chunks = append(chunks, c)
|
||||
}
|
||||
|
||||
fmt.Printf("共 %d 个 chunks 需要生成 embedding\n", len(chunks))
|
||||
|
||||
success := 0
|
||||
for i, c := range chunks {
|
||||
emb, err := client.GetEmbedding(ctx, c.content)
|
||||
if err != nil {
|
||||
fmt.Printf("[%d/%d] ❌ %s: %v\n", i+1, len(chunks), c.id[:8], err)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
// 转为 pgvector 格式
|
||||
vecStr := "["
|
||||
for j, f := range emb {
|
||||
if j > 0 {
|
||||
vecStr += ","
|
||||
}
|
||||
vecStr += fmt.Sprintf("%g", f)
|
||||
}
|
||||
vecStr += "]"
|
||||
|
||||
_, err = pool.Exec(ctx,
|
||||
`UPDATE knowledge_chunks SET embedding = $2::vector WHERE id = $1`,
|
||||
c.id, vecStr)
|
||||
if err != nil {
|
||||
fmt.Printf("[%d/%d] ❌ 写入失败 %s: %v\n", i+1, len(chunks), c.id[:8], err)
|
||||
} else {
|
||||
success++
|
||||
fmt.Printf("[%d/%d] ✅ %s (dim=%d)\n", i+1, len(chunks), c.id[:8], len(emb))
|
||||
}
|
||||
|
||||
// 避免 API 限流
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Printf("\n完成!成功: %d/%d\n", success, len(chunks))
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/config"
|
||||
pkgdb "github.com/enterprise-ai-platform/server/pkg/db"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
||||
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg := config.Load()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Database
|
||||
pool, err := pkgdb.NewPool(ctx, cfg.Database.URL)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to connect to database (will retry on first request)")
|
||||
pool = nil
|
||||
} else {
|
||||
defer pool.Close()
|
||||
log.Info().Msg("Connected to PostgreSQL")
|
||||
}
|
||||
|
||||
// Redis
|
||||
opts, err := redis.ParseURL(cfg.Redis.URL)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to parse Redis URL")
|
||||
opts = &redis.Options{Addr: "localhost:6379"}
|
||||
}
|
||||
rdb := redis.NewClient(opts)
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to connect to Redis (will retry on first request)")
|
||||
} else {
|
||||
log.Info().Msg("Connected to Redis")
|
||||
}
|
||||
defer rdb.Close()
|
||||
|
||||
router := newRouter(cfg, pool, rdb)
|
||||
|
||||
addr := cfg.Server.Host + ":" + cfg.Server.Port
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 120 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Info().Str("addr", addr).Msg("Starting Aily Portal API server")
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().Err(err).Msg("Server failed to start")
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info().Msg("Shutting down server...")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Fatal().Err(err).Msg("Server forced to shutdown")
|
||||
}
|
||||
log.Info().Msg("Server exited")
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/config"
|
||||
"github.com/enterprise-ai-platform/server/internal/handler"
|
||||
mw "github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/enterprise-ai-platform/server/pkg/auth"
|
||||
"github.com/enterprise-ai-platform/server/pkg/dify"
|
||||
"github.com/enterprise-ai-platform/server/pkg/embedding"
|
||||
"github.com/enterprise-ai-platform/server/pkg/llm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func newRouter(cfg *config.Config, pool *pgxpool.Pool, rdb *redis.Client) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Global middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Timeout(15 * time.Minute))
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"http://localhost:*", "https://*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
// Services
|
||||
jwtMgr := auth.NewJWTManager(cfg.JWT.Secret, cfg.JWT.AccessExpiry, cfg.JWT.RefreshExpiry)
|
||||
difyClient := dify.NewClient(cfg.Dify.APIURL)
|
||||
|
||||
// LLM Manager — direct model calls replacing Dify for chat
|
||||
llmMgr := llm.NewManager()
|
||||
if cfg.LLM.OpenAIKey != "" {
|
||||
llmMgr.Register("openai", llm.NewOpenAIProvider(cfg.LLM.OpenAIKey, cfg.LLM.OpenAIBaseURL, cfg.LLM.OpenAIModel))
|
||||
}
|
||||
if cfg.LLM.AnthropicKey != "" {
|
||||
llmMgr.Register("anthropic", llm.NewAnthropicProvider(cfg.LLM.AnthropicKey, cfg.LLM.AnthropicBaseURL, cfg.LLM.AnthropicModel))
|
||||
}
|
||||
if cfg.LLM.Provider != "" {
|
||||
llmMgr.SetFallback(cfg.LLM.Provider)
|
||||
}
|
||||
|
||||
// Embedding client(向量化服务,支持 DashScope / OpenAI 兼容 API)
|
||||
embedClient := embedding.NewClient(embedding.Config{
|
||||
APIKey: cfg.Embedding.APIKey,
|
||||
BaseURL: cfg.Embedding.BaseURL,
|
||||
Model: cfg.Embedding.Model,
|
||||
Dimensions: cfg.Embedding.Dimensions,
|
||||
})
|
||||
|
||||
// Handlers
|
||||
authH := handler.NewAuthHandler(pool, jwtMgr)
|
||||
storeH := handler.NewStoreHandler(pool)
|
||||
chatH := handler.NewLLMChatHandler(pool, llmMgr, cfg.LLM.Provider, rdb, cfg.PPTWorker.URL, embedClient)
|
||||
favH := handler.NewFavoriteHandler(pool)
|
||||
adminH := handler.NewAdminHandler(pool)
|
||||
creatorH := handler.NewCreatorHandler(pool, difyClient)
|
||||
kbH := handler.NewKnowledgeHandler(pool, embedClient)
|
||||
docTplH := handler.NewDocTemplateHandler(pool, llmMgr, cfg.LLM.Provider)
|
||||
analysisH := handler.NewAnalysisTemplateHandler(pool, llmMgr, cfg.LLM.Provider)
|
||||
pptH := handler.NewPPTHandler(pool, rdb, cfg.PPTWorker.URL)
|
||||
platformH := handler.NewPlatformHandler(pool)
|
||||
|
||||
// Auth middleware
|
||||
requireAuth := mw.Auth(jwtMgr)
|
||||
requireAdmin := mw.RequireRole("admin")
|
||||
// Health check
|
||||
r.Get("/health", handler.HealthCheck)
|
||||
|
||||
// API v1 routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Public: auth
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/register", authH.Register)
|
||||
r.Post("/login", authH.Login)
|
||||
r.Post("/refresh", authH.Refresh)
|
||||
r.With(requireAuth).Post("/logout", authH.Logout)
|
||||
r.With(requireAuth).Get("/me", authH.Me)
|
||||
r.With(requireAuth).Put("/profile", authH.UpdateProfile)
|
||||
r.With(requireAuth).Post("/switch-org", authH.SwitchOrg)
|
||||
})
|
||||
|
||||
// Organizations (public read)
|
||||
r.Get("/organizations", authH.ListOrganizations)
|
||||
|
||||
// Store (public read, auth optional for personalization)
|
||||
r.Route("/store", func(r chi.Router) {
|
||||
r.Get("/categories", storeH.ListCategories)
|
||||
r.Get("/apps", storeH.ListApps)
|
||||
r.Get("/apps/{slug}", storeH.GetApp)
|
||||
r.Get("/featured", storeH.Featured)
|
||||
r.Get("/rankings", storeH.Rankings)
|
||||
r.With(requireAuth).Get("/recent", storeH.Recent)
|
||||
})
|
||||
|
||||
// App usage (requires auth)
|
||||
r.With(requireAuth).Route("/apps/{id}", func(r chi.Router) {
|
||||
r.With(mw.RateLimit(rdb, 30, time.Minute)).Post("/chat", chatH.Chat)
|
||||
r.Post("/completion", chatH.Completion)
|
||||
r.Post("/generate-doc", docTplH.GenerateDocument)
|
||||
r.Post("/generate-analysis", analysisH.GenerateReport)
|
||||
r.Get("/conversations", chatH.Conversations)
|
||||
r.Get("/conversations/{convId}/messages", chatH.Messages)
|
||||
r.Delete("/conversations/{convId}", chatH.DeleteConversation)
|
||||
r.Put("/conversations/{convId}/name", chatH.RenameConversation)
|
||||
r.Post("/conversations/batch-delete", chatH.BatchDeleteConversations)
|
||||
r.Post("/feedback", chatH.Feedback)
|
||||
r.Post("/favorite", favH.AddFavorite)
|
||||
r.Delete("/favorite", favH.RemoveFavorite)
|
||||
r.Post("/rating", favH.AddRating)
|
||||
r.Get("/ratings", favH.ListRatings)
|
||||
})
|
||||
|
||||
// Document templates (public read)
|
||||
r.With(requireAuth).Route("/doc-templates", func(r chi.Router) {
|
||||
r.Get("/", docTplH.ListTemplates)
|
||||
r.Get("/{templateId}", docTplH.GetTemplate)
|
||||
})
|
||||
|
||||
// Analysis report templates
|
||||
r.With(requireAuth).Route("/analysis-templates", func(r chi.Router) {
|
||||
r.Get("/", analysisH.ListTemplates)
|
||||
r.Get("/{templateId}", analysisH.GetTemplate)
|
||||
})
|
||||
|
||||
// Personal (requires auth)
|
||||
r.With(requireAuth).Route("/me", func(r chi.Router) {
|
||||
r.Get("/favorites", favH.ListFavorites)
|
||||
r.Get("/stats", favH.PersonalStats)
|
||||
})
|
||||
|
||||
// Application management (all authenticated users can manage their own apps, admins can manage all)
|
||||
r.With(requireAuth).Route("/creator", func(r chi.Router) {
|
||||
r.Get("/apps", creatorH.ListMyApps)
|
||||
r.Post("/apps", creatorH.CreateApp)
|
||||
r.Get("/apps/{id}", creatorH.GetApp)
|
||||
r.Put("/apps/{id}", creatorH.UpdateApp)
|
||||
r.Delete("/apps/{id}", creatorH.DeleteApp)
|
||||
r.Post("/apps/{id}/test", notImplemented)
|
||||
r.Post("/apps/{id}/submit-review", creatorH.SubmitReview)
|
||||
r.Post("/apps/{id}/withdraw", creatorH.WithdrawReview)
|
||||
r.Post("/apps/{id}/request-delist", creatorH.RequestDelist)
|
||||
r.Get("/templates", creatorH.ListTemplates)
|
||||
r.Post("/apps/from-template", notImplemented)
|
||||
})
|
||||
|
||||
// Knowledge base (requires auth)
|
||||
r.With(requireAuth).Route("/knowledge", func(r chi.Router) {
|
||||
r.Get("/", kbH.ListKnowledgeBases)
|
||||
r.Post("/", kbH.CreateKnowledgeBase)
|
||||
r.Post("/reindex", kbH.ReindexAll)
|
||||
r.Post("/reembed", kbH.ReembedChunks)
|
||||
r.Put("/{id}", kbH.UpdateKnowledgeBase)
|
||||
r.Delete("/{id}", kbH.DeleteKnowledgeBase)
|
||||
r.Post("/{id}/documents", kbH.UploadDocument)
|
||||
r.Get("/{id}/documents", kbH.ListDocuments)
|
||||
r.Delete("/{id}/documents/{docId}", kbH.DeleteDocument)
|
||||
})
|
||||
|
||||
// PPT 生成 (requires auth)
|
||||
r.With(requireAuth).Route("/ppt", func(r chi.Router) {
|
||||
r.Post("/tasks", pptH.CreateTask)
|
||||
r.Post("/tasks/upload", pptH.CreateTaskWithFile)
|
||||
r.Get("/tasks", pptH.ListTasks)
|
||||
r.Get("/tasks/{taskId}", pptH.GetTaskStatus)
|
||||
r.Get("/tasks/{taskId}/download", pptH.DownloadTask)
|
||||
})
|
||||
|
||||
// Admin (requires admin role)
|
||||
r.With(requireAuth, requireAdmin).With(mw.AuditLog(pool)).Route("/admin", func(r chi.Router) {
|
||||
r.Get("/apps", adminH.ListAllApps)
|
||||
r.Get("/reviews", adminH.ListPendingReviews)
|
||||
r.Post("/reviews/{id}/approve", adminH.ApproveReview)
|
||||
r.Post("/reviews/{id}/reject", adminH.RejectReview)
|
||||
r.Post("/apps/{id}/delist", adminH.DelistApp)
|
||||
r.Post("/apps/{id}/relist", adminH.RelistApp)
|
||||
r.Get("/users", adminH.ListUsers)
|
||||
r.Put("/users/{id}/role", adminH.UpdateUserRole)
|
||||
r.Put("/users/{id}/status", adminH.UpdateUserStatus)
|
||||
r.Get("/departments", notImplemented)
|
||||
r.Get("/analytics/overview", adminH.Overview)
|
||||
r.Get("/analytics/usage", adminH.UsageAnalytics)
|
||||
r.Get("/analytics/cost", notImplemented)
|
||||
r.Get("/analytics/users", notImplemented)
|
||||
r.Get("/audit-logs", adminH.ListAuditLogs)
|
||||
r.Get("/models", notImplemented)
|
||||
r.Post("/models/providers", notImplemented)
|
||||
r.Put("/quotas", notImplemented)
|
||||
})
|
||||
|
||||
// Platform (requires super_admin role) - 跨机构平台管理
|
||||
r.With(requireAuth, mw.RequireSuperAdmin).With(mw.AuditLog(pool)).Route("/platform", func(r chi.Router) {
|
||||
// 平台总览
|
||||
r.Get("/overview", platformH.Overview)
|
||||
r.Get("/org-ranking", platformH.OrgRanking)
|
||||
|
||||
// 机构管理
|
||||
r.Get("/orgs", platformH.ListOrgs)
|
||||
r.Post("/orgs", platformH.CreateOrg)
|
||||
r.Put("/orgs/{id}", platformH.UpdateOrg)
|
||||
r.Delete("/orgs/{id}", platformH.DeleteOrg)
|
||||
|
||||
// 全局用户管理
|
||||
r.Get("/users", platformH.ListAllUsers)
|
||||
r.Put("/users/{id}/role", platformH.UpdateUserRole)
|
||||
r.Put("/users/{id}/status", platformH.UpdateUserStatus)
|
||||
r.Put("/users/{id}/org", platformH.AssignUserOrg)
|
||||
|
||||
// 全局应用管理
|
||||
r.Get("/apps", platformH.ListAllApps)
|
||||
r.Put("/apps/{id}/featured", platformH.SetFeatured)
|
||||
r.Post("/apps/{id}/force-delist", platformH.ForceDelist)
|
||||
|
||||
// 全局审计日志
|
||||
r.Get("/audit-logs", platformH.ListAllAuditLogs)
|
||||
|
||||
// 模型提供商
|
||||
r.Get("/providers", platformH.ListProviders)
|
||||
r.Post("/providers", platformH.CreateProvider)
|
||||
r.Put("/providers/{id}", platformH.UpdateProvider)
|
||||
r.Delete("/providers/{id}", platformH.DeleteProvider)
|
||||
|
||||
// 全局配额
|
||||
r.Get("/quotas", platformH.ListQuotas)
|
||||
r.Post("/quotas", platformH.UpsertQuota)
|
||||
r.Delete("/quotas/{id}", platformH.DeleteQuota)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusNotImplemented, 50100, "接口开发中")
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
module github.com/enterprise-ai-platform/server
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/rs/zerolog v1.35.1
|
||||
golang.org/x/crypto v0.51.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
Redis RedisConfig
|
||||
JWT JWTConfig
|
||||
Dify DifyConfig
|
||||
LLM LLMConfig
|
||||
Embedding EmbeddingConfig
|
||||
Gateway GatewayConfig
|
||||
MinIO MinIOConfig
|
||||
PPTWorker PPTWorkerConfig
|
||||
}
|
||||
|
||||
type PPTWorkerConfig struct {
|
||||
URL string // PPT Worker 微服务地址
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
Provider string // "openai" or "anthropic"
|
||||
OpenAIKey string
|
||||
OpenAIBaseURL string
|
||||
OpenAIModel string
|
||||
AnthropicKey string
|
||||
AnthropicBaseURL string
|
||||
AnthropicModel string
|
||||
}
|
||||
|
||||
type EmbeddingConfig struct {
|
||||
APIKey string // Embedding API 密钥
|
||||
BaseURL string // Embedding API 基础 URL(OpenAI 兼容格式)
|
||||
Model string // 向量模型名称
|
||||
Dimensions int // 向量维度
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
AccessExpiry time.Duration
|
||||
RefreshExpiry time.Duration
|
||||
}
|
||||
|
||||
type DifyConfig struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type GatewayConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type MinIOConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Bucket string
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Host: getEnv("SERVER_HOST", "0.0.0.0"),
|
||||
Port: getEnv("SERVER_PORT", "8080"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
URL: getEnv("DATABASE_URL", "postgres://localhost:5432/govai_portal?sslmode=disable"),
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
URL: getEnv("REDIS_URL", "redis://localhost:6379"),
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: getEnv("JWT_SECRET", "dev-secret-change-in-production"),
|
||||
AccessExpiry: 24 * time.Hour,
|
||||
RefreshExpiry: 7 * 24 * time.Hour,
|
||||
},
|
||||
Dify: DifyConfig{
|
||||
APIURL: getEnv("DIFY_API_URL", "http://localhost:5001/v1"),
|
||||
APIKey: getEnv("DIFY_API_KEY", ""),
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
Provider: getEnv("LLM_PROVIDER", "openai"),
|
||||
OpenAIKey: getEnv("OPENAI_API_KEY", ""),
|
||||
OpenAIBaseURL: getEnv("OPENAI_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
||||
OpenAIModel: getEnv("OPENAI_MODEL", "qwen-plus"),
|
||||
AnthropicKey: getEnv("ANTHROPIC_API_KEY", ""),
|
||||
AnthropicBaseURL: getEnv("ANTHROPIC_BASE_URL", "https://api.anthropic.com"),
|
||||
AnthropicModel: getEnv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514"),
|
||||
},
|
||||
Embedding: EmbeddingConfig{
|
||||
APIKey: getEnv("EMBEDDING_API_KEY", ""),
|
||||
BaseURL: getEnv("EMBEDDING_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
||||
Model: getEnv("EMBEDDING_MODEL", "text-embedding-v3"),
|
||||
Dimensions: 1024,
|
||||
},
|
||||
Gateway: GatewayConfig{
|
||||
URL: getEnv("MODEL_GATEWAY_URL", "http://localhost:8081"),
|
||||
},
|
||||
MinIO: MinIOConfig{
|
||||
Endpoint: getEnv("MINIO_ENDPOINT", "localhost:9000"),
|
||||
AccessKey: getEnv("MINIO_ACCESS_KEY", "minioadmin"),
|
||||
SecretKey: getEnv("MINIO_SECRET_KEY", "minioadmin"),
|
||||
Bucket: getEnv("MINIO_BUCKET", "aily-files"),
|
||||
UseSSL: false,
|
||||
},
|
||||
PPTWorker: PPTWorkerConfig{
|
||||
URL: getEnv("PPT_WORKER_URL", "http://localhost:8090"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewAdminHandler(pool *pgxpool.Pool) *AdminHandler {
|
||||
return &AdminHandler{pool: pool}
|
||||
}
|
||||
|
||||
// getUserOrgID 通过当前登录用户ID查询其所属机构ID,用于多租户数据隔离
|
||||
func (h *AdminHandler) getUserOrgID(r *http.Request) (string, error) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
var orgID string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT COALESCE(org_id::text, '') FROM users WHERE id = $1`, userID).Scan(&orgID)
|
||||
return orgID, err
|
||||
}
|
||||
|
||||
// --- Overview Stats ---
|
||||
|
||||
func (h *AdminHandler) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := h.getUserOrgID(r)
|
||||
if err != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
var totalUsers, totalApps, activeUsers, todayConversations int
|
||||
var monthlyTokens int64
|
||||
var monthlyCost float64
|
||||
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE status = 'active' AND org_id = $1`, orgID).Scan(&totalUsers)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications WHERE status = 'approved' AND org_id = $1`, orgID).Scan(&totalApps)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(DISTINCT l.user_id) FROM app_usage_logs l JOIN users u ON l.user_id = u.id WHERE l.created_at >= $1 AND u.org_id = $2`, todayStart, orgID).Scan(&activeUsers)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM app_usage_logs l JOIN users u ON l.user_id = u.id WHERE l.created_at >= $1 AND u.org_id = $2`, todayStart, orgID).Scan(&todayConversations)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(l.total_tokens), 0) FROM app_usage_logs l JOIN users u ON l.user_id = u.id WHERE l.created_at >= $1 AND u.org_id = $2`, monthStart, orgID).Scan(&monthlyTokens)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(l.estimated_cost), 0) FROM app_usage_logs l JOIN users u ON l.user_id = u.id WHERE l.created_at >= $1 AND u.org_id = $2`, monthStart, orgID).Scan(&monthlyCost)
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"total_users": totalUsers,
|
||||
"total_apps": totalApps,
|
||||
"active_users": activeUsers,
|
||||
"total_conversations": todayConversations,
|
||||
"monthly_tokens": monthlyTokens,
|
||||
"monthly_cost": monthlyCost,
|
||||
})
|
||||
}
|
||||
|
||||
// --- User Management ---
|
||||
|
||||
func (h *AdminHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := h.getUserOrgID(r)
|
||||
if err != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 20
|
||||
offset := (page - 1) * pageSize
|
||||
search := q.Get("q")
|
||||
roleFilter := q.Get("role")
|
||||
statusFilter := q.Get("status")
|
||||
|
||||
query := `SELECT id, name, email, avatar_url, role, status, employee_id, last_login_at, login_count, created_at
|
||||
FROM users WHERE org_id = $1`
|
||||
args := []any{orgID}
|
||||
argIdx := 2
|
||||
|
||||
if search != "" {
|
||||
query += ` AND (name ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%' OR email ILIKE '%' || $` + strconv.Itoa(argIdx) + ` || '%')`
|
||||
args = append(args, search)
|
||||
argIdx++
|
||||
}
|
||||
if roleFilter != "" {
|
||||
query += ` AND role = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, roleFilter)
|
||||
argIdx++
|
||||
}
|
||||
if statusFilter != "" {
|
||||
query += ` AND status = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, statusFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
query += ` ORDER BY created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
args = append(args, pageSize, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询用户失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, email, role, status string
|
||||
avatarURL, employeeID *string
|
||||
lastLoginAt *time.Time
|
||||
loginCount int
|
||||
createdAt time.Time
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &email, &avatarURL, &role, &status, &employeeID, &lastLoginAt, &loginCount, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, map[string]any{
|
||||
"id": id, "name": name, "email": email,
|
||||
"avatar_url": avatarURL, "role": role, "status": status,
|
||||
"employee_id": employeeID, "last_login_at": lastLoginAt,
|
||||
"login_count": loginCount, "created_at": createdAt,
|
||||
})
|
||||
}
|
||||
if users == nil {
|
||||
users = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{"items": users, "page": page})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateUserRole(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
operatorRole := middleware.GetRole(r.Context())
|
||||
|
||||
var req struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
validRoles := map[string]bool{"user": true, "creator": true, "admin": true, "super_admin": true}
|
||||
if !validRoles[req.Role] {
|
||||
response.BadRequest(w, "无效的角色")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Role == "super_admin" && operatorRole != "super_admin" {
|
||||
response.Forbidden(w, "只有超级管理员才能设置超级管理员角色")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保只能修改本机构用户
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE users SET role = $2 WHERE id = $1 AND org_id = $3`, userID, req.Role, orgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新角色失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != "active" && req.Status != "disabled" {
|
||||
response.BadRequest(w, "无效的状态")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保只能修改本机构用户
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE users SET status = $2 WHERE id = $1 AND org_id = $3`, userID, req.Status, orgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// --- App Management ---
|
||||
|
||||
func (h *AdminHandler) ListAllApps(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := h.getUserOrgID(r)
|
||||
if err != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
offset := (page - 1) * 20
|
||||
statusFilter := q.Get("status")
|
||||
|
||||
query := `SELECT a.id, a.name, a.slug, a.description, a.icon_url,
|
||||
c.name as category_name, u.name as creator_name,
|
||||
a.dify_app_type, a.status, a.visibility, a.usage_count, a.created_at
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.org_id = $1`
|
||||
args := []any{orgID}
|
||||
argIdx := 2
|
||||
|
||||
if statusFilter != "" {
|
||||
query += ` AND a.status = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, statusFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
query += ` ORDER BY a.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
args = append(args, 20, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
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, creator *string
|
||||
appType *string
|
||||
usageCount int64
|
||||
createdAt time.Time
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &catName, &creator,
|
||||
&appType, &status, &visibility, &usageCount, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, map[string]any{
|
||||
"id": id, "name": name, "slug": slug, "description": desc,
|
||||
"icon_url": iconURL, "category_name": catName, "creator_name": creator,
|
||||
"dify_app_type": appType,
|
||||
"status": status, "visibility": visibility, "usage_count": usageCount,
|
||||
"created_at": createdAt,
|
||||
})
|
||||
}
|
||||
if apps == nil {
|
||||
apps = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{"items": apps, "page": page})
|
||||
}
|
||||
|
||||
// --- Audit Logs ---
|
||||
|
||||
func (h *AdminHandler) ListAuditLogs(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
offset := (page - 1) * 50
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT al.id, al.action, al.resource_type, al.resource_id,
|
||||
al.details, al.ip_address, al.created_at,
|
||||
u.name as user_name
|
||||
FROM audit_logs al
|
||||
LEFT JOIN users u ON al.user_id = u.id
|
||||
WHERE u.org_id = $2
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT 50 OFFSET $1`, offset, orgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询审计日志失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, action, resType string
|
||||
resID *string
|
||||
details json.RawMessage
|
||||
ipAddr *string
|
||||
createdAt time.Time
|
||||
userName *string
|
||||
)
|
||||
if err := rows.Scan(&id, &action, &resType, &resID, &details, &ipAddr, &createdAt, &userName); err != nil {
|
||||
continue
|
||||
}
|
||||
logs = append(logs, map[string]any{
|
||||
"id": id, "action": action, "resource_type": resType,
|
||||
"resource_id": resID, "details": details,
|
||||
"ip_address": ipAddr, "created_at": createdAt, "user_name": userName,
|
||||
})
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{"items": logs, "page": page})
|
||||
}
|
||||
|
||||
// --- Usage Analytics ---
|
||||
|
||||
func (h *AdminHandler) UsageAnalytics(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
days := 7
|
||||
if d, err := strconv.Atoi(r.URL.Query().Get("days")); err == nil && d > 0 && d <= 90 {
|
||||
days = d
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT DATE(l.created_at) as day, COUNT(*) as count, COALESCE(SUM(l.total_tokens), 0) as tokens
|
||||
FROM app_usage_logs l
|
||||
JOIN users u ON l.user_id = u.id
|
||||
WHERE l.created_at >= NOW() - $1::interval AND u.org_id = $2
|
||||
GROUP BY DATE(l.created_at)
|
||||
ORDER BY day`, strconv.Itoa(days)+" days", orgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询使用统计失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var dailyStats []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
day time.Time
|
||||
count int
|
||||
tokens int64
|
||||
)
|
||||
if err := rows.Scan(&day, &count, &tokens); err != nil {
|
||||
continue
|
||||
}
|
||||
dailyStats = append(dailyStats, map[string]any{
|
||||
"date": day.Format("2006-01-02"),
|
||||
"count": count,
|
||||
"total_tokens": tokens,
|
||||
})
|
||||
}
|
||||
if dailyStats == nil {
|
||||
dailyStats = []map[string]any{}
|
||||
}
|
||||
|
||||
// Top apps
|
||||
topRows, err := h.pool.Query(r.Context(), `
|
||||
SELECT a.name, COUNT(l.id) as usage_count
|
||||
FROM app_usage_logs l
|
||||
JOIN applications a ON l.app_id = a.id
|
||||
WHERE l.created_at >= NOW() - $1::interval AND a.org_id = $2
|
||||
GROUP BY a.name
|
||||
ORDER BY usage_count DESC LIMIT 10`, strconv.Itoa(days)+" days", orgID)
|
||||
if err != nil {
|
||||
response.JSON(w, http.StatusOK, map[string]any{"daily": dailyStats})
|
||||
return
|
||||
}
|
||||
defer topRows.Close()
|
||||
|
||||
var topApps []map[string]any
|
||||
for topRows.Next() {
|
||||
var name string
|
||||
var count int
|
||||
if err := topRows.Scan(&name, &count); err != nil {
|
||||
continue
|
||||
}
|
||||
topApps = append(topApps, map[string]any{"name": name, "count": count})
|
||||
}
|
||||
if topApps == nil {
|
||||
topApps = []map[string]any{}
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"daily": dailyStats,
|
||||
"top_apps": topApps,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Review Management ---
|
||||
|
||||
func (h *AdminHandler) ListPendingReviews(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT r.id, r.app_id, r.version, r.submit_comment, r.submitted_at,
|
||||
a.name as app_name, a.description as app_description, a.icon_url,
|
||||
u.name as submitter_name
|
||||
FROM app_reviews r
|
||||
JOIN applications a ON r.app_id = a.id
|
||||
JOIN users u ON r.submitter_id = u.id
|
||||
WHERE r.status = 'pending' AND a.org_id = $1
|
||||
ORDER BY r.submitted_at ASC LIMIT 50`, orgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询审核列表失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reviews []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, appID, version string
|
||||
comment *string
|
||||
submittedAt time.Time
|
||||
appName, appDesc *string
|
||||
appIcon *string
|
||||
submitterName string
|
||||
)
|
||||
if err := rows.Scan(&id, &appID, &version, &comment, &submittedAt,
|
||||
&appName, &appDesc, &appIcon, &submitterName); err != nil {
|
||||
continue
|
||||
}
|
||||
reviews = append(reviews, map[string]any{
|
||||
"id": id, "app_id": appID, "version": version,
|
||||
"submit_comment": comment, "submitted_at": submittedAt,
|
||||
"app_name": appName, "app_description": appDesc, "app_icon": appIcon,
|
||||
"submitter_name": submitterName,
|
||||
})
|
||||
}
|
||||
if reviews == nil {
|
||||
reviews = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, reviews)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ApproveReview(w http.ResponseWriter, r *http.Request) {
|
||||
reviewID := chi.URLParam(r, "id")
|
||||
reviewerID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req struct {
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
// Get app_id from review
|
||||
var appID string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT app_id FROM app_reviews WHERE id = $1 AND status = 'pending'`, reviewID).Scan(&appID)
|
||||
if err != nil {
|
||||
response.NotFound(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 = 'approved', reviewer_id = $2, review_comment = $3, reviewed_at = NOW()
|
||||
WHERE id = $1`, reviewID, reviewerID, req.Comment)
|
||||
|
||||
tx.Exec(r.Context(), `
|
||||
UPDATE applications SET status = 'approved', published_at = NOW()
|
||||
WHERE id = $1`, appID)
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
response.InternalError(w, "审核通过失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) RejectReview(w http.ResponseWriter, r *http.Request) {
|
||||
reviewID := chi.URLParam(r, "id")
|
||||
reviewerID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req struct {
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Comment == "" {
|
||||
response.BadRequest(w, "驳回必须填写原因")
|
||||
return
|
||||
}
|
||||
|
||||
var appID string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT app_id FROM app_reviews WHERE id = $1 AND status = 'pending'`, reviewID).Scan(&appID)
|
||||
if err != nil {
|
||||
response.NotFound(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 = 'rejected', reviewer_id = $2, review_comment = $3, reviewed_at = NOW()
|
||||
WHERE id = $1`, reviewID, reviewerID, req.Comment)
|
||||
|
||||
tx.Exec(r.Context(), `
|
||||
UPDATE applications SET status = 'rejected'
|
||||
WHERE id = $1`, appID)
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
response.InternalError(w, "驳回失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DelistApp(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
appID := chi.URLParam(r, "id")
|
||||
|
||||
var status string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT status FROM applications WHERE id = $1 AND org_id = $2`, appID, orgID).Scan(&status)
|
||||
if err != nil {
|
||||
response.NotFound(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 (h *AdminHandler) RelistApp(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, orgErr := h.getUserOrgID(r)
|
||||
if orgErr != nil || orgID == "" {
|
||||
response.InternalError(w, "无法确定当前机构")
|
||||
return
|
||||
}
|
||||
|
||||
appID := chi.URLParam(r, "id")
|
||||
|
||||
var status string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT status FROM applications WHERE id = $1 AND org_id = $2`, appID, orgID).Scan(&status)
|
||||
if err != nil {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
if status != "archived" {
|
||||
response.BadRequest(w, "只有已归档的应用可以重新上架")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.pool.Exec(r.Context(),
|
||||
`UPDATE applications SET status = 'approved', published_at = NOW() WHERE id = $1`, appID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "重新上架失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已重新上架"})
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/llm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type AnalysisTemplateHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
manager *llm.Manager
|
||||
provider string
|
||||
}
|
||||
|
||||
func NewAnalysisTemplateHandler(pool *pgxpool.Pool, manager *llm.Manager, provider string) *AnalysisTemplateHandler {
|
||||
return &AnalysisTemplateHandler{pool: pool, manager: manager, provider: provider}
|
||||
}
|
||||
|
||||
func (h *AnalysisTemplateHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.URL.Query().Get("org_id")
|
||||
query := `SELECT id, name, report_type, COALESCE(description,''), COALESCE(icon,''), steps, sort_order
|
||||
FROM analysis_report_templates
|
||||
WHERE is_active = true`
|
||||
var args []any
|
||||
if orgID != "" {
|
||||
query += ` AND (org_id = $1 OR org_id IS NULL)`
|
||||
args = append(args, orgID)
|
||||
}
|
||||
query += ` ORDER BY sort_order ASC`
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询模板失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var templates []map[string]any
|
||||
for rows.Next() {
|
||||
var id, name, reportType, desc, icon string
|
||||
var steps json.RawMessage
|
||||
var sortOrder int
|
||||
if err := rows.Scan(&id, &name, &reportType, &desc, &icon, &steps, &sortOrder); err != nil {
|
||||
continue
|
||||
}
|
||||
templates = append(templates, map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"report_type": reportType,
|
||||
"description": desc,
|
||||
"icon": icon,
|
||||
"steps": steps,
|
||||
"sort_order": sortOrder,
|
||||
})
|
||||
}
|
||||
if templates == nil {
|
||||
templates = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{"data": templates})
|
||||
}
|
||||
|
||||
func (h *AnalysisTemplateHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "templateId")
|
||||
|
||||
var name, reportType, desc, icon, promptTpl string
|
||||
var steps, outputSections json.RawMessage
|
||||
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
SELECT name, report_type, COALESCE(description,''), COALESCE(icon,''),
|
||||
steps, prompt_template, COALESCE(output_sections, '[]')
|
||||
FROM analysis_report_templates WHERE id = $1 AND is_active = true`, id,
|
||||
).Scan(&name, &reportType, &desc, &icon, &steps, &promptTpl, &outputSections)
|
||||
if err != nil {
|
||||
response.NotFound(w, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"report_type": reportType,
|
||||
"description": desc,
|
||||
"icon": icon,
|
||||
"steps": steps,
|
||||
"prompt_template": promptTpl,
|
||||
"output_sections": outputSections,
|
||||
})
|
||||
}
|
||||
|
||||
type generateAnalysisRequest struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
FieldData map[string]string `json:"field_data"`
|
||||
}
|
||||
|
||||
func containsAny(list []string, targets ...string) bool {
|
||||
for _, item := range list {
|
||||
for _, t := range targets {
|
||||
if item == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *AnalysisTemplateHandler) queryEconomicData(ctx context.Context, districts, timeRange string) string {
|
||||
years := []int{2025}
|
||||
switch timeRange {
|
||||
case "2024-2025年两年对比", "2024_2025":
|
||||
years = []int{2024, 2025}
|
||||
case "2023-2025年三年趋势", "2023_2025":
|
||||
years = []int{2023, 2024, 2025}
|
||||
case "2024年全年", "2024_full":
|
||||
years = []int{2024}
|
||||
}
|
||||
|
||||
districtList := strings.Split(districts, ",")
|
||||
for i := range districtList {
|
||||
districtList[i] = strings.TrimSpace(districtList[i])
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(ctx, `
|
||||
SELECT district_name, year,
|
||||
COALESCE(gdp,0), COALESCE(gdp_growth,0), COALESCE(gdp_per_capita,0),
|
||||
COALESCE(fiscal_revenue,0), COALESCE(fiscal_revenue_growth,0),
|
||||
COALESCE(fixed_investment,0), COALESCE(fixed_investment_growth,0),
|
||||
COALESCE(retail_sales,0), COALESCE(retail_sales_growth,0),
|
||||
COALESCE(industrial_output,0), COALESCE(industrial_output_growth,0),
|
||||
COALESCE(tertiary_ratio,0),
|
||||
COALESCE(import_export,0), COALESCE(import_export_growth,0),
|
||||
COALESCE(actual_fdi,0),
|
||||
COALESCE(tech_expenditure,0), COALESCE(tech_expenditure_ratio,0),
|
||||
COALESCE(population,0), COALESCE(urban_income,0), COALESCE(rural_income,0)
|
||||
FROM regional_economic_data
|
||||
WHERE year = ANY($1)
|
||||
AND (cardinality($2::text[]) = 0 OR district_name = ANY($2))
|
||||
ORDER BY year ASC, gdp DESC`, years, districtList)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n\n## 【数据库真实数据】\n\n")
|
||||
sb.WriteString("| 区县 | 年份 | GDP(亿元) | GDP增速(%) | 人均GDP(元) | 财政收入(亿元) | 财政增速(%) | 固投(亿元) | 固投增速(%) | 社零(亿元) | 社零增速(%) | 规上工业(亿元) | 工业增速(%) | 三产占比(%) | 进出口(亿元) | 进出口增速(%) | 利用外资(亿美元) | R&D投入(亿元) | R&D/GDP(%) | 人口(万) | 城镇收入(元) | 农村收入(元) |\n")
|
||||
sb.WriteString("|------|------|-----------|-----------|------------|-------------|-----------|----------|-----------|----------|-----------|-------------|-----------|-----------|-----------|------------|-------------|------------|----------|----------|-----------|----------|\n")
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var year int
|
||||
var gdp, gdpG, gdpPC, fr, frG, fi, fiG, rs, rsG, io, ioG, tr, ie, ieG, fdi, te, teR, pop, ui, ri float64
|
||||
if err := rows.Scan(&name, &year, &gdp, &gdpG, &gdpPC, &fr, &frG, &fi, &fiG, &rs, &rsG, &io, &ioG, &tr, &ie, &ieG, &fdi, &te, &teR, &pop, &ui, &ri); err != nil {
|
||||
continue
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("| %s | %d | %.1f | %.1f | %.0f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.1f | %.2f | %.1f | %.2f | %.1f | %.0f | %.0f |\n",
|
||||
name, year, gdp, gdpG, gdpPC, fr, frG, fi, fiG, rs, rsG, io, ioG, tr, ie, ieG, fdi, te, teR, pop, ui, ri))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *AnalysisTemplateHandler) GenerateReport(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req generateAnalysisRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效请求格式")
|
||||
return
|
||||
}
|
||||
if req.TemplateID == "" {
|
||||
response.BadRequest(w, "请选择报告模板")
|
||||
return
|
||||
}
|
||||
|
||||
var appModel string
|
||||
var appMaxTokens int
|
||||
var appConfigRaw json.RawMessage
|
||||
_ = h.pool.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(app_config->>'model', ''), COALESCE(max_tokens, 8192), COALESCE(app_config, '{}')
|
||||
FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&appModel, &appMaxTokens, &appConfigRaw)
|
||||
|
||||
var appCfg struct {
|
||||
Tools []string `json:"tools"`
|
||||
DataSources []string `json:"data_sources"`
|
||||
TemplateSet string `json:"template_set"`
|
||||
}
|
||||
_ = json.Unmarshal(appConfigRaw, &appCfg)
|
||||
|
||||
var promptTpl, tplName, reportType string
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
SELECT name, prompt_template, report_type
|
||||
FROM analysis_report_templates WHERE id = $1 AND is_active = true`, req.TemplateID,
|
||||
).Scan(&tplName, &promptTpl, &reportType)
|
||||
if err != nil {
|
||||
response.NotFound(w, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
prompt := promptTpl
|
||||
for k, v := range req.FieldData {
|
||||
prompt = strings.ReplaceAll(prompt, "{{"+k+"}}", v)
|
||||
}
|
||||
prompt = strings.ReplaceAll(prompt, "{{", "")
|
||||
prompt = strings.ReplaceAll(prompt, "}}", "")
|
||||
|
||||
hasDataTool := containsAny(appCfg.Tools, "economic_data_query", "data_analysis")
|
||||
hasDataSource := containsAny(appCfg.DataSources, "regional_economic_data", "statistical_yearbook")
|
||||
useEconomicData := hasDataTool || hasDataSource || reportType == "economic_comparison" || reportType == "trend_analysis"
|
||||
|
||||
if useEconomicData {
|
||||
dataTable := h.queryEconomicData(r.Context(), req.FieldData["districts"], req.FieldData["time_range"])
|
||||
if dataTable != "" {
|
||||
prompt += "\n\n⚠️ 以下是从数据库中检索到的真实经济数据,请基于这些真实数据进行分析,不要编造数据:" + dataTable
|
||||
}
|
||||
}
|
||||
|
||||
llmReq := &llm.ChatRequest{
|
||||
Model: appModel,
|
||||
Messages: []llm.Message{
|
||||
{Role: llm.RoleSystem, Content: "你是一位资深的政务数据分析和研判专家。要求:1)基于提供的真实数据库数据进行分析 2)报告必须完整输出到最后一个章节,不能中途截断 3)保持精炼,每个章节500字以内 4)使用Markdown格式排版 5)表格数据必须引用真实数据 6)报告输出完毕后,必须在最后一行单独输出「---\\n\\n**【完稿】**」作为完成标记"},
|
||||
{Role: llm.RoleUser, Content: prompt},
|
||||
},
|
||||
Temperature: 0.4,
|
||||
MaxTokens: appMaxTokens,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
body, err := h.manager.ChatStream(r.Context(), h.provider, llmReq)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50202, "模型服务不可用: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
response.InternalError(w, "Streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
convID := uuid.New().String()
|
||||
msgID := uuid.New().String()
|
||||
var fullResponse strings.Builder
|
||||
|
||||
firstEvent := map[string]string{
|
||||
"conversation_id": convID,
|
||||
"message_id": msgID,
|
||||
"template_name": tplName,
|
||||
}
|
||||
data, _ := json.Marshal(firstEvent)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
|
||||
transform := llm.TransformOpenAIStream
|
||||
if h.provider == "anthropic" {
|
||||
transform = llm.TransformAnthropicStream
|
||||
}
|
||||
|
||||
var totalTokens int
|
||||
var modelName string
|
||||
|
||||
_ = transform(body, func(event llm.StreamEvent) {
|
||||
if event.Answer != "" {
|
||||
fullResponse.WriteString(event.Answer)
|
||||
}
|
||||
if event.Usage != nil {
|
||||
totalTokens = event.Usage.TotalTokens
|
||||
modelName = event.Usage.Model
|
||||
}
|
||||
data, _ := json.Marshal(event)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
})
|
||||
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
userMsg := fmt.Sprintf("[研判报告] %s", tplName)
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
_, _ = h.pool.Exec(ctx, `
|
||||
INSERT INTO app_usage_logs (app_id, user_id, conversation_id, user_message, ai_response, total_tokens, model_name, duration_ms, client_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'web')`,
|
||||
appID, userID, convID, userMsg, fullResponse.String(), totalTokens, modelName, duration)
|
||||
_, _ = h.pool.Exec(ctx,
|
||||
`UPDATE applications SET usage_count = usage_count + 1 WHERE id = $1`, appID)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/enterprise-ai-platform/server/pkg/auth"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
jwtMgr *auth.JWTManager
|
||||
}
|
||||
|
||||
func NewAuthHandler(pool *pgxpool.Pool, jwtMgr *auth.JWTManager) *AuthHandler {
|
||||
return &AuthHandler{pool: pool, jwtMgr: jwtMgr}
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
OrgID string `json:"org_id"`
|
||||
}
|
||||
|
||||
type orgInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ShortName string `json:"short_name"`
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Role string `json:"role"`
|
||||
EmployeeID *string `json:"employee_id"`
|
||||
OrgID *string `json:"org_id"`
|
||||
Org *orgInfo `json:"org,omitempty"`
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req registerRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Email == "" || req.Password == "" {
|
||||
response.BadRequest(w, "姓名、邮箱和密码不能为空")
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
response.BadRequest(w, "密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
|
||||
var exists bool
|
||||
h.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)`, req.Email).Scan(&exists)
|
||||
if exists {
|
||||
response.Error(w, http.StatusConflict, 40901, "该邮箱已注册")
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
response.InternalError(w, "密码加密失败")
|
||||
return
|
||||
}
|
||||
|
||||
id := uuid.New()
|
||||
_, err = h.pool.Exec(r.Context(),
|
||||
`INSERT INTO users (id, name, email, password_hash, role, status) VALUES ($1, $2, $3, $4, 'user', 'active')`,
|
||||
id, req.Name, req.Email, hash)
|
||||
if err != nil {
|
||||
response.InternalError(w, "注册失败")
|
||||
return
|
||||
}
|
||||
|
||||
tokenPair, err := h.jwtMgr.GenerateTokenPair(id, req.Email, "user")
|
||||
if err != nil {
|
||||
response.InternalError(w, "生成Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "access_token",
|
||||
Value: tokenPair.AccessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(24 * time.Hour / time.Second),
|
||||
})
|
||||
|
||||
response.JSON(w, http.StatusCreated, map[string]any{
|
||||
"user": userResponse{
|
||||
ID: id.String(), Name: req.Name, Email: req.Email, Role: "user",
|
||||
},
|
||||
"access_token": tokenPair.AccessToken,
|
||||
"expires_at": tokenPair.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req loginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" || req.Password == "" {
|
||||
response.BadRequest(w, "邮箱和密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
id string
|
||||
name string
|
||||
email string
|
||||
passwordHash *string
|
||||
avatarURL *string
|
||||
role string
|
||||
employeeID *string
|
||||
status string
|
||||
orgID *string
|
||||
)
|
||||
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT id, name, email, password_hash, avatar_url, role, employee_id, status, org_id::text
|
||||
FROM users WHERE email = $1`, req.Email,
|
||||
).Scan(&id, &name, &email, &passwordHash, &avatarURL, &role, &employeeID, &status, &orgID)
|
||||
|
||||
if err != nil {
|
||||
response.Unauthorized(w, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
response.Error(w, http.StatusForbidden, 40302, "账号已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
if passwordHash == nil || !auth.CheckPassword(req.Password, *passwordHash) {
|
||||
response.Unauthorized(w, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 平台管理员不绑定机构,可登录任意机构入口
|
||||
// 普通用户/机构管理员必须属于所选机构
|
||||
if role != "super_admin" && req.OrgID != "" && orgID != nil && *orgID != req.OrgID {
|
||||
response.Unauthorized(w, "该账号不属于所选机构,请选择正确的机构")
|
||||
return
|
||||
}
|
||||
|
||||
uid, _ := uuid.Parse(id)
|
||||
tokenPair, err := h.jwtMgr.GenerateTokenPair(uid, email, role)
|
||||
if err != nil {
|
||||
response.InternalError(w, "生成Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
// Update login info
|
||||
go func() {
|
||||
_, _ = h.pool.Exec(context.Background(),
|
||||
`UPDATE users SET last_login_at = NOW(), login_count = login_count + 1 WHERE id = $1`, id)
|
||||
}()
|
||||
|
||||
// Set cookies
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "access_token",
|
||||
Value: tokenPair.AccessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(24 * time.Hour / time.Second),
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: tokenPair.RefreshToken,
|
||||
Path: "/api/v1/auth/refresh",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(7 * 24 * time.Hour / time.Second),
|
||||
})
|
||||
|
||||
usr := userResponse{
|
||||
ID: id, Name: name, Email: email,
|
||||
AvatarURL: avatarURL, Role: role, EmployeeID: employeeID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
if orgID != nil {
|
||||
usr.Org = h.loadOrgInfo(r.Context(), *orgID)
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"user": usr,
|
||||
"access_token": tokenPair.AccessToken,
|
||||
"expires_at": tokenPair.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "access_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: "",
|
||||
Path: "/api/v1/auth/refresh",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var u userResponse
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT id, name, email, avatar_url, role, employee_id, org_id::text
|
||||
FROM users WHERE id = $1`, userID,
|
||||
).Scan(&u.ID, &u.Name, &u.Email, &u.AvatarURL, &u.Role, &u.EmployeeID, &u.OrgID)
|
||||
|
||||
if err != nil {
|
||||
response.NotFound(w, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if u.OrgID != nil {
|
||||
u.Org = h.loadOrgInfo(r.Context(), *u.OrgID)
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, u)
|
||||
}
|
||||
|
||||
// ListOrganizations 返回所有可用机构列表
|
||||
func (h *AuthHandler) ListOrganizations(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.pool.Query(r.Context(),
|
||||
`SELECT id, name, slug, COALESCE(short_name,''), COALESCE(description,''), COALESCE(logo_url,'')
|
||||
FROM organizations WHERE is_active = true ORDER BY sort_order ASC`)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询机构列表失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orgs []map[string]any
|
||||
for rows.Next() {
|
||||
var id, name, slug, shortName, desc, logo string
|
||||
if rows.Scan(&id, &name, &slug, &shortName, &desc, &logo) != nil {
|
||||
continue
|
||||
}
|
||||
orgs = append(orgs, map[string]any{
|
||||
"id": id, "name": name, "slug": slug,
|
||||
"short_name": shortName, "description": desc, "logo_url": logo,
|
||||
})
|
||||
}
|
||||
if orgs == nil {
|
||||
orgs = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, orgs)
|
||||
}
|
||||
|
||||
// SwitchOrg 切换机构:管理员及以上可用,自动切换为目标机构的管理员身份
|
||||
func (h *AuthHandler) SwitchOrg(w http.ResponseWriter, r *http.Request) {
|
||||
currentRole := middleware.GetRole(r.Context())
|
||||
if currentRole != "super_admin" && currentRole != "admin" {
|
||||
response.Forbidden(w, "仅管理员可以切换机构")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
OrgID string `json:"org_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.OrgID == "" {
|
||||
response.BadRequest(w, "请选择机构")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验机构是否存在
|
||||
var exists bool
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT EXISTS(SELECT 1 FROM organizations WHERE id = $1 AND is_active = true)`, req.OrgID).Scan(&exists)
|
||||
if !exists {
|
||||
response.NotFound(w, "机构不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 查找目标机构的管理员用户(优先admin,其次super_admin,最后creator)
|
||||
var targetID uuid.UUID
|
||||
var targetEmail, targetRole, targetName string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT id, email, role, name FROM users
|
||||
WHERE org_id = $1 AND status = 'active'
|
||||
ORDER BY CASE role WHEN 'admin' THEN 1 WHEN 'super_admin' THEN 2 WHEN 'creator' THEN 3 ELSE 4 END
|
||||
LIMIT 1`, req.OrgID).Scan(&targetID, &targetEmail, &targetRole, &targetName)
|
||||
if err != nil {
|
||||
response.InternalError(w, "该机构暂无可用用户")
|
||||
return
|
||||
}
|
||||
|
||||
// 为目标用户生成新的JWT token
|
||||
tokens, err := h.jwtMgr.GenerateTokenPair(targetID, targetEmail, targetRole)
|
||||
if err != nil {
|
||||
response.InternalError(w, "生成令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
org := h.loadOrgInfo(r.Context(), req.OrgID)
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"message": "已切换",
|
||||
"org": org,
|
||||
"token": tokens.AccessToken,
|
||||
"user": map[string]any{
|
||||
"id": targetID,
|
||||
"name": targetName,
|
||||
"email": targetEmail,
|
||||
"role": targetRole,
|
||||
"org_id": req.OrgID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) loadOrgInfo(ctx context.Context, orgID string) *orgInfo {
|
||||
var o orgInfo
|
||||
err := h.pool.QueryRow(ctx,
|
||||
`SELECT id, name, slug, COALESCE(short_name,'') FROM organizations WHERE id = $1`, orgID,
|
||||
).Scan(&o.ID, &o.Name, &o.Slug, &o.ShortName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &o
|
||||
}
|
||||
|
||||
func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(),
|
||||
`UPDATE users SET name = COALESCE(NULLIF($2,''), name),
|
||||
avatar_url = COALESCE(NULLIF($3,''), avatar_url),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, userID, req.Name, req.AvatarURL)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新个人信息失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
response.Unauthorized(w, "Refresh Token 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.jwtMgr.ValidateToken(cookie.Value)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusUnauthorized, 40102, "Refresh Token 已过期")
|
||||
return
|
||||
}
|
||||
|
||||
// Get current role from DB
|
||||
var role string
|
||||
err = h.pool.QueryRow(r.Context(),
|
||||
`SELECT role FROM users WHERE id = $1 AND status = 'active'`, claims.UserID,
|
||||
).Scan(&role)
|
||||
if err != nil {
|
||||
response.Unauthorized(w, "用户不存在或已被禁用")
|
||||
return
|
||||
}
|
||||
|
||||
tokenPair, err := h.jwtMgr.GenerateTokenPair(claims.UserID, claims.Email, role)
|
||||
if err != nil {
|
||||
response.InternalError(w, "生成Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "access_token",
|
||||
Value: tokenPair.AccessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(24 * time.Hour / time.Second),
|
||||
})
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"access_token": tokenPair.AccessToken,
|
||||
"expires_at": tokenPair.ExpiresAt,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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/pgxpool"
|
||||
)
|
||||
|
||||
type ChatHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
dify *dify.Client
|
||||
}
|
||||
|
||||
func NewChatHandler(pool *pgxpool.Pool, difyClient *dify.Client) *ChatHandler {
|
||||
return &ChatHandler{pool: pool, dify: difyClient}
|
||||
}
|
||||
|
||||
type chatRequest struct {
|
||||
Message string `json:"message"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
Inputs map[string]any `json:"inputs,omitempty"`
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Chat(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req chatRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Message == "" {
|
||||
response.BadRequest(w, "消息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// Get app's Dify API key
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1 AND status = 'approved'`,
|
||||
appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在或未上架")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Call Dify streaming chat
|
||||
difyReq := &dify.ChatRequest{
|
||||
Query: req.Message,
|
||||
Inputs: req.Inputs,
|
||||
ConversationID: req.ConversationID,
|
||||
User: userID.String(),
|
||||
ResponseMode: "streaming",
|
||||
}
|
||||
|
||||
body, err := h.dify.ChatStream(r.Context(), difyAPIKey, difyReq)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "Dify 服务不可用: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
// Stream SSE to client
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
response.InternalError(w, "Streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
scanner.Buffer(make([]byte, 64*1024), 256*1024)
|
||||
|
||||
var totalTokens int
|
||||
var modelName string
|
||||
var conversationID string
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
if data == "[DONE]" {
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
break
|
||||
}
|
||||
|
||||
// Forward SSE event to client
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
|
||||
// Parse for usage tracking
|
||||
var event map[string]any
|
||||
if err := json.Unmarshal([]byte(data), &event); err == nil {
|
||||
if cid, ok := event["conversation_id"].(string); ok && cid != "" {
|
||||
conversationID = cid
|
||||
}
|
||||
if event["event"] == "message_end" {
|
||||
if meta, ok := event["metadata"].(map[string]any); ok {
|
||||
if usage, ok := meta["usage"].(map[string]any); ok {
|
||||
if t, ok := usage["total_tokens"].(float64); ok {
|
||||
totalTokens = int(t)
|
||||
}
|
||||
if m, ok := usage["model"].(string); ok {
|
||||
modelName = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record usage asynchronously
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
go func() {
|
||||
_, _ = h.pool.Exec(context.Background(), `
|
||||
INSERT INTO app_usage_logs (app_id, user_id, conversation_id, total_tokens, model_name, duration_ms, client_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'web')`,
|
||||
appID, userID, conversationID, totalTokens, modelName, duration)
|
||||
|
||||
_, _ = h.pool.Exec(context.Background(),
|
||||
`UPDATE applications SET usage_count = usage_count + 1 WHERE id = $1`, appID)
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Completion(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req chatRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Message == "" {
|
||||
response.BadRequest(w, "消息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1 AND status = 'approved'`,
|
||||
appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在或未上架")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
difyReq := &dify.ChatRequest{
|
||||
Query: req.Message,
|
||||
Inputs: req.Inputs,
|
||||
ConversationID: req.ConversationID,
|
||||
User: userID.String(),
|
||||
ResponseMode: "blocking",
|
||||
}
|
||||
|
||||
result, err := h.dify.ChatBlocking(r.Context(), difyAPIKey, difyReq)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "Dify 服务不可用: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
go func() {
|
||||
_, _ = h.pool.Exec(context.Background(), `
|
||||
INSERT INTO app_usage_logs (app_id, user_id, conversation_id, total_tokens, duration_ms, client_type)
|
||||
VALUES ($1, $2, $3, $4, $5, 'web')`,
|
||||
appID, userID, result.ConversationID, 0, duration)
|
||||
_, _ = h.pool.Exec(context.Background(),
|
||||
`UPDATE applications SET usage_count = usage_count + 1 WHERE id = $1`, appID)
|
||||
}()
|
||||
|
||||
response.JSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Feedback(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req struct {
|
||||
MessageID string `json:"message_id"`
|
||||
Rating string `json:"rating"` // "like" | "dislike" | null
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.MessageID == "" {
|
||||
response.BadRequest(w, "message_id 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
feedbackReq := &dify.FeedbackRequest{
|
||||
Rating: req.Rating,
|
||||
User: userID.String(),
|
||||
}
|
||||
if err := h.dify.SubmitFeedback(r.Context(), difyAPIKey, req.MessageID, feedbackReq); err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "提交反馈失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "反馈已提交"})
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Conversations(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.dify.ListConversations(r.Context(), difyAPIKey, userID.String(), 20, "")
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "获取对话列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *ChatHandler) Messages(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
convID := chi.URLParam(r, "convId")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.dify.ListMessages(r.Context(), difyAPIKey, userID.String(), convID, 100, "")
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "获取消息列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *ChatHandler) DeleteConversation(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
convID := chi.URLParam(r, "convId")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var difyAPIKey string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT dify_api_key FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&difyAPIKey)
|
||||
if err != nil || difyAPIKey == "" {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.dify.DeleteConversation(r.Context(), difyAPIKey, userID.String(), convID); err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50201, "删除对话失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// Suppress unused import warnings
|
||||
var _ = io.EOF
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/llm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type DocTemplateHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
manager *llm.Manager
|
||||
provider string
|
||||
}
|
||||
|
||||
func NewDocTemplateHandler(pool *pgxpool.Pool, manager *llm.Manager, provider string) *DocTemplateHandler {
|
||||
return &DocTemplateHandler{pool: pool, manager: manager, provider: provider}
|
||||
}
|
||||
|
||||
func (h *DocTemplateHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.URL.Query().Get("org_id")
|
||||
query := `SELECT id, name, doc_type, COALESCE(description,''), COALESCE(icon,''), fields, sort_order
|
||||
FROM document_templates
|
||||
WHERE is_active = true`
|
||||
var args []any
|
||||
if orgID != "" {
|
||||
query += ` AND (org_id = $1 OR org_id IS NULL)`
|
||||
args = append(args, orgID)
|
||||
}
|
||||
query += ` ORDER BY sort_order ASC`
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询模板失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var templates []map[string]any
|
||||
for rows.Next() {
|
||||
var id, name, docType, desc, icon string
|
||||
var fields json.RawMessage
|
||||
var sortOrder int
|
||||
if err := rows.Scan(&id, &name, &docType, &desc, &icon, &fields, &sortOrder); err != nil {
|
||||
continue
|
||||
}
|
||||
templates = append(templates, map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"doc_type": docType,
|
||||
"description": desc,
|
||||
"icon": icon,
|
||||
"fields": fields,
|
||||
"sort_order": sortOrder,
|
||||
})
|
||||
}
|
||||
if templates == nil {
|
||||
templates = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{"data": templates})
|
||||
}
|
||||
|
||||
func (h *DocTemplateHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "templateId")
|
||||
|
||||
var name, docType, desc, icon, formatStd, promptTpl string
|
||||
var fields json.RawMessage
|
||||
var exampleOutput *string
|
||||
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
SELECT name, doc_type, COALESCE(description,''), COALESCE(icon,''),
|
||||
format_standard, fields, prompt_template, example_output
|
||||
FROM document_templates WHERE id = $1 AND is_active = true`, id,
|
||||
).Scan(&name, &docType, &desc, &icon, &formatStd, &fields, &promptTpl, &exampleOutput)
|
||||
if err != nil {
|
||||
response.NotFound(w, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"doc_type": docType,
|
||||
"description": desc,
|
||||
"icon": icon,
|
||||
"format_standard": formatStd,
|
||||
"fields": fields,
|
||||
"prompt_template": promptTpl,
|
||||
}
|
||||
if exampleOutput != nil {
|
||||
result["example_output"] = *exampleOutput
|
||||
}
|
||||
response.JSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
type generateDocRequest struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
FieldData map[string]string `json:"field_data"`
|
||||
}
|
||||
|
||||
func (h *DocTemplateHandler) GenerateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req generateDocRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效请求格式")
|
||||
return
|
||||
}
|
||||
if req.TemplateID == "" {
|
||||
response.BadRequest(w, "请选择公文模板")
|
||||
return
|
||||
}
|
||||
|
||||
// Load app-specific model and max_tokens
|
||||
var appModel string
|
||||
var appMaxTokens int
|
||||
_ = h.pool.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(app_config->>'model', ''), COALESCE(max_tokens, 4096)
|
||||
FROM applications WHERE id = $1`, appID,
|
||||
).Scan(&appModel, &appMaxTokens)
|
||||
|
||||
var promptTpl, tplName string
|
||||
var fields json.RawMessage
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
SELECT name, fields, prompt_template
|
||||
FROM document_templates WHERE id = $1 AND is_active = true`, req.TemplateID,
|
||||
).Scan(&tplName, &fields, &promptTpl)
|
||||
if err != nil {
|
||||
response.NotFound(w, "模板不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var fieldDefs []struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
_ = json.Unmarshal(fields, &fieldDefs)
|
||||
for _, f := range fieldDefs {
|
||||
if f.Required {
|
||||
if val, ok := req.FieldData[f.Key]; !ok || strings.TrimSpace(val) == "" {
|
||||
response.BadRequest(w, fmt.Sprintf("请填写必填项:%s", f.Label))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt := promptTpl
|
||||
for k, v := range req.FieldData {
|
||||
prompt = strings.ReplaceAll(prompt, "{{"+k+"}}", v)
|
||||
}
|
||||
prompt = strings.ReplaceAll(prompt, "{{", "")
|
||||
prompt = strings.ReplaceAll(prompt, "}}", "")
|
||||
|
||||
llmReq := &llm.ChatRequest{
|
||||
Model: appModel,
|
||||
Messages: []llm.Message{
|
||||
{Role: llm.RoleSystem, Content: "你是一个专业的政务公文写作专家,精通《党政机关公文格式》国家标准(GB/T 9704)和《党政机关公文处理工作条例》。请严格按照规范格式生成公文,确保行文庄重、严谨、准确。输出完整公文内容,使用Markdown格式排版。"},
|
||||
{Role: llm.RoleUser, Content: prompt},
|
||||
},
|
||||
Temperature: 0.3,
|
||||
MaxTokens: appMaxTokens,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
body, err := h.manager.ChatStream(r.Context(), h.provider, llmReq)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadGateway, 50202, "模型服务不可用: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
response.InternalError(w, "Streaming not supported")
|
||||
return
|
||||
}
|
||||
|
||||
convID := uuid.New().String()
|
||||
msgID := uuid.New().String()
|
||||
var fullResponse strings.Builder
|
||||
|
||||
firstEvent := map[string]string{
|
||||
"conversation_id": convID,
|
||||
"message_id": msgID,
|
||||
"template_name": tplName,
|
||||
}
|
||||
data, _ := json.Marshal(firstEvent)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
|
||||
transform := llm.TransformOpenAIStream
|
||||
if h.provider == "anthropic" {
|
||||
transform = llm.TransformAnthropicStream
|
||||
}
|
||||
|
||||
var totalTokens int
|
||||
var modelName string
|
||||
|
||||
_ = transform(body, func(event llm.StreamEvent) {
|
||||
if event.Answer != "" {
|
||||
fullResponse.WriteString(event.Answer)
|
||||
}
|
||||
if event.Usage != nil {
|
||||
totalTokens = event.Usage.TotalTokens
|
||||
modelName = event.Usage.Model
|
||||
}
|
||||
data, _ := json.Marshal(event)
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
})
|
||||
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
userMsg := fmt.Sprintf("[公文生成] %s", tplName)
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
_, _ = h.pool.Exec(ctx, `
|
||||
INSERT INTO app_usage_logs (app_id, user_id, conversation_id, user_message, ai_response, total_tokens, model_name, duration_ms, client_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'web')`,
|
||||
appID, userID, convID, userMsg, fullResponse.String(), totalTokens, modelName, duration)
|
||||
_, _ = h.pool.Exec(ctx,
|
||||
`UPDATE applications SET usage_count = usage_count + 1 WHERE id = $1`, appID)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type FavoriteHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewFavoriteHandler(pool *pgxpool.Pool) *FavoriteHandler {
|
||||
return &FavoriteHandler{pool: pool}
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) AddFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
_, err := h.pool.Exec(r.Context(),
|
||||
`INSERT INTO app_favorites (user_id, app_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
||||
userID, appID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "收藏失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.Exec(r.Context(),
|
||||
`UPDATE applications SET favorite_count = favorite_count + 1 WHERE id = $1`, appID)
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]bool{"favorited": true})
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) RemoveFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
tag, err := h.pool.Exec(r.Context(),
|
||||
`DELETE FROM app_favorites WHERE user_id = $1 AND app_id = $2`,
|
||||
userID, appID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "取消收藏失败")
|
||||
return
|
||||
}
|
||||
|
||||
if tag.RowsAffected() > 0 {
|
||||
h.pool.Exec(r.Context(),
|
||||
`UPDATE applications SET favorite_count = GREATEST(favorite_count - 1, 0) WHERE id = $1`, appID)
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]bool{"favorited": false})
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) ListFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
offset := (page - 1) * 20
|
||||
|
||||
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 app_favorites f
|
||||
JOIN applications a ON f.app_id = a.id
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE f.user_id = $1
|
||||
ORDER BY f.created_at DESC
|
||||
LIMIT 20 OFFSET $2`, userID, offset)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询收藏失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
apps := scanAppList(rows)
|
||||
response.JSON(w, http.StatusOK, apps)
|
||||
}
|
||||
|
||||
// --- Rating ---
|
||||
|
||||
type ratingRequest struct {
|
||||
Score int `json:"score"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) AddRating(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req ratingRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Score < 1 || req.Score > 5 {
|
||||
response.BadRequest(w, "评分必须在1-5之间")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(), `
|
||||
INSERT INTO app_ratings (app_id, user_id, score, comment)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (app_id, user_id)
|
||||
DO UPDATE SET score = EXCLUDED.score, comment = EXCLUDED.comment`,
|
||||
appID, userID, req.Score, req.Comment)
|
||||
if err != nil {
|
||||
response.InternalError(w, "评分失败")
|
||||
return
|
||||
}
|
||||
|
||||
// Update app avg rating
|
||||
var avgRating float32
|
||||
var ratingCount int
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT COALESCE(AVG(score)::REAL, 0), COUNT(*) FROM app_ratings WHERE app_id = $1`,
|
||||
appID).Scan(&avgRating, &ratingCount)
|
||||
|
||||
h.pool.Exec(r.Context(),
|
||||
`UPDATE applications SET avg_rating = $2, rating_count = $3 WHERE id = $1`,
|
||||
appID, avgRating, ratingCount)
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"avg_rating": avgRating,
|
||||
"rating_count": ratingCount,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) ListRatings(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT r.id, r.score, r.comment, r.created_at,
|
||||
u.name as user_name, u.avatar_url
|
||||
FROM app_ratings r
|
||||
JOIN users u ON r.user_id = u.id
|
||||
WHERE r.app_id = $1
|
||||
ORDER BY r.created_at DESC LIMIT 50`, appID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询评分失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ratings []map[string]any
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var score int
|
||||
var comment *string
|
||||
var createdAt string
|
||||
var userName string
|
||||
var avatarURL *string
|
||||
if err := rows.Scan(&id, &score, &comment, &createdAt, &userName, &avatarURL); err != nil {
|
||||
continue
|
||||
}
|
||||
ratings = append(ratings, map[string]any{
|
||||
"id": id, "score": score, "comment": comment,
|
||||
"created_at": createdAt, "user_name": userName, "user_avatar": avatarURL,
|
||||
})
|
||||
}
|
||||
if ratings == nil {
|
||||
ratings = []map[string]any{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, ratings)
|
||||
}
|
||||
|
||||
func (h *FavoriteHandler) PersonalStats(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var totalConversations, totalTokens, favoriteCount int
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT COUNT(*) FROM app_usage_logs WHERE user_id = $1`, userID).Scan(&totalConversations)
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT COALESCE(SUM(total_tokens), 0) FROM app_usage_logs WHERE user_id = $1`, userID).Scan(&totalTokens)
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT COUNT(*) FROM app_favorites WHERE user_id = $1`, userID).Scan(&favoriteCount)
|
||||
|
||||
var recentApps []map[string]any
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT DISTINCT ON (l.app_id) a.id, a.name, a.icon_url, l.created_at
|
||||
FROM app_usage_logs l
|
||||
JOIN applications a ON l.app_id = a.id
|
||||
WHERE l.user_id = $1
|
||||
ORDER BY l.app_id, l.created_at DESC
|
||||
LIMIT 5`, userID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id, name string
|
||||
var icon *string
|
||||
var at string
|
||||
if rows.Scan(&id, &name, &icon, &at) == nil {
|
||||
recentApps = append(recentApps, map[string]any{
|
||||
"id": id, "name": name, "icon_url": icon, "last_used": at,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if recentApps == nil {
|
||||
recentApps = []map[string]any{}
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"total_conversations": totalConversations,
|
||||
"total_tokens": totalTokens,
|
||||
"favorite_count": favoriteCount,
|
||||
"recent_apps": recentApps,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"status": "ok",
|
||||
"service": "aily-portal-api",
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"go": runtime.Version(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/pkg/chunker"
|
||||
"github.com/enterprise-ai-platform/server/pkg/embedding"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
mw "github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func internalErr(w http.ResponseWriter, err error) {
|
||||
response.InternalError(w, err.Error())
|
||||
}
|
||||
|
||||
type KnowledgeHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
embedder *embedding.Client
|
||||
}
|
||||
|
||||
func NewKnowledgeHandler(pool *pgxpool.Pool, embedder *embedding.Client) *KnowledgeHandler {
|
||||
return &KnowledgeHandler{pool: pool, embedder: embedder}
|
||||
}
|
||||
|
||||
type knowledgeBaseRow struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
DocCount int `json:"document_count"`
|
||||
TotalChars int64 `json:"total_chars"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) ListKnowledgeBases(w http.ResponseWriter, r *http.Request) {
|
||||
userID := mw.GetUserID(r.Context())
|
||||
userRole := mw.GetRole(r.Context())
|
||||
|
||||
var query string
|
||||
var args []any
|
||||
|
||||
if userRole == "super_admin" {
|
||||
// 超级管理员查看全部知识库
|
||||
query = `SELECT id, name, COALESCE(description,''), visibility, doc_count, total_chars, status, created_at, updated_at
|
||||
FROM knowledge_bases ORDER BY updated_at DESC`
|
||||
args = []any{}
|
||||
} else {
|
||||
// 优先使用前端传入的 org_id 参数(切换机构后),否则从用户表获取
|
||||
orgFilter := r.URL.Query().Get("org_id")
|
||||
if orgFilter == "" {
|
||||
var userOrg *string
|
||||
_ = h.pool.QueryRow(r.Context(), `SELECT org_id::text FROM users WHERE id = $1`, userID).Scan(&userOrg)
|
||||
if userOrg != nil {
|
||||
orgFilter = *userOrg
|
||||
}
|
||||
}
|
||||
|
||||
query = `SELECT id, name, COALESCE(description,''), visibility, doc_count, total_chars, status, created_at, updated_at
|
||||
FROM knowledge_bases WHERE (owner_id = $1`
|
||||
args = []any{userID}
|
||||
if orgFilter != "" {
|
||||
query += ` OR org_id = $2`
|
||||
args = append(args, orgFilter)
|
||||
}
|
||||
query += `) ORDER BY updated_at DESC`
|
||||
}
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []knowledgeBaseRow
|
||||
for rows.Next() {
|
||||
var kb knowledgeBaseRow
|
||||
if err := rows.Scan(&kb.ID, &kb.Name, &kb.Description, &kb.Visibility, &kb.DocCount, &kb.TotalChars, &kb.Status, &kb.CreatedAt, &kb.UpdatedAt); err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
items = append(items, kb)
|
||||
}
|
||||
if items == nil {
|
||||
items = []knowledgeBaseRow{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) CreateKnowledgeBase(w http.ResponseWriter, r *http.Request) {
|
||||
userID := mw.GetUserID(r.Context())
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
response.BadRequest(w, "无效的请求体")
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
response.BadRequest(w, "名称不能为空")
|
||||
return
|
||||
}
|
||||
if body.Visibility == "" {
|
||||
body.Visibility = "private"
|
||||
}
|
||||
|
||||
// 获取用户所属机构
|
||||
var userOrgID *string
|
||||
_ = h.pool.QueryRow(r.Context(), `SELECT org_id::text FROM users WHERE id = $1`, userID).Scan(&userOrgID)
|
||||
|
||||
id := uuid.New()
|
||||
_, err := h.pool.Exec(r.Context(),
|
||||
`INSERT INTO knowledge_bases (id, name, description, owner_id, visibility, org_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
id, body.Name, body.Description, userID, body.Visibility, userOrgID)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusCreated, map[string]any{
|
||||
"id": id.String(),
|
||||
"name": body.Name,
|
||||
"description": body.Description,
|
||||
"visibility": body.Visibility,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) UpdateKnowledgeBase(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的ID")
|
||||
return
|
||||
}
|
||||
userID := mw.GetUserID(r.Context())
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
response.BadRequest(w, "无效的请求体")
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.pool.Exec(r.Context(),
|
||||
`UPDATE knowledge_bases SET name = COALESCE(NULLIF($1,''), name),
|
||||
description = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND owner_id = $4`,
|
||||
body.Name, body.Description, id, userID)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
response.NotFound(w, "知识库不存在")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) DeleteKnowledgeBase(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的ID")
|
||||
return
|
||||
}
|
||||
userID := mw.GetUserID(r.Context())
|
||||
|
||||
tag, err := h.pool.Exec(r.Context(),
|
||||
`DELETE FROM knowledge_bases WHERE id = $1 AND owner_id = $2`, id, userID)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
response.NotFound(w, "知识库不存在")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"})
|
||||
}
|
||||
|
||||
type documentRow struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"filename"`
|
||||
FileType string `json:"file_type"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
IndexingStatus string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) ListDocuments(w http.ResponseWriter, r *http.Request) {
|
||||
kbID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(r.Context(),
|
||||
`SELECT id, name, COALESCE(file_type,''), file_size, indexing_status, created_at
|
||||
FROM knowledge_documents WHERE kb_id = $1 ORDER BY created_at DESC`, kbID)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var docs []documentRow
|
||||
for rows.Next() {
|
||||
var d documentRow
|
||||
if err := rows.Scan(&d.ID, &d.Name, &d.FileType, &d.FileSize, &d.IndexingStatus, &d.CreatedAt); err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
docs = append(docs, d)
|
||||
}
|
||||
if docs == nil {
|
||||
docs = []documentRow{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, docs)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) UploadDocument(w http.ResponseWriter, r *http.Request) {
|
||||
kbID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
response.BadRequest(w, "文件过大或格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(w, "请上传文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var exists bool
|
||||
err = h.pool.QueryRow(r.Context(),
|
||||
`SELECT EXISTS(SELECT 1 FROM knowledge_bases WHERE id = $1)`, kbID).Scan(&exists)
|
||||
if err != nil || !exists {
|
||||
response.NotFound(w, "知识库不存在")
|
||||
return
|
||||
}
|
||||
|
||||
fileType := ""
|
||||
ext := ""
|
||||
if dot := len(header.Filename) - 1; dot > 0 {
|
||||
for i := dot; i >= 0; i-- {
|
||||
if header.Filename[i] == '.' {
|
||||
ext = header.Filename[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ext {
|
||||
case "pdf":
|
||||
fileType = "pdf"
|
||||
case "docx":
|
||||
fileType = "docx"
|
||||
case "txt":
|
||||
fileType = "txt"
|
||||
case "md":
|
||||
fileType = "md"
|
||||
case "csv":
|
||||
fileType = "csv"
|
||||
case "xlsx":
|
||||
fileType = "xlsx"
|
||||
default:
|
||||
fileType = "txt"
|
||||
}
|
||||
|
||||
// 读取文件内容(文本文件)
|
||||
var content string
|
||||
if fileType == "txt" || fileType == "md" || fileType == "csv" {
|
||||
data, err := io.ReadAll(file)
|
||||
if err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
userID := mw.GetUserID(r.Context())
|
||||
docID := uuid.New()
|
||||
|
||||
// 计算分片数
|
||||
chunkCount := 0
|
||||
if content != "" {
|
||||
chunks := chunker.ChunkText(content, chunker.DefaultOptions())
|
||||
chunkCount = len(chunks)
|
||||
}
|
||||
|
||||
_, err = h.pool.Exec(r.Context(),
|
||||
`INSERT INTO knowledge_documents (id, kb_id, name, file_type, file_size, uploader_id, indexing_status, content, char_count, chunk_count)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'processing', $7, $8, $9)`,
|
||||
docID, kbID, header.Filename, fileType, header.Size, userID, content, utf8.RuneCountInString(content), chunkCount)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = h.pool.Exec(r.Context(),
|
||||
`UPDATE knowledge_bases SET doc_count = doc_count + 1, updated_at = NOW() WHERE id = $1`, kbID)
|
||||
|
||||
// 异步执行分片和向量化
|
||||
go h.chunkAndEmbed(context.Background(), kbID, docID, content)
|
||||
|
||||
response.JSON(w, http.StatusCreated, map[string]any{
|
||||
"id": docID.String(),
|
||||
"filename": header.Filename,
|
||||
"size": header.Size,
|
||||
"chunks": chunkCount,
|
||||
"status": "processing",
|
||||
})
|
||||
}
|
||||
|
||||
// chunkAndEmbed 对文档内容执行分片和向量化(异步)
|
||||
func (h *KnowledgeHandler) chunkAndEmbed(ctx context.Context, kbID, docID uuid.UUID, content string) {
|
||||
if content == "" {
|
||||
h.pool.Exec(ctx, `UPDATE knowledge_documents SET indexing_status = 'completed' WHERE id = $1`, docID)
|
||||
return
|
||||
}
|
||||
|
||||
chunks := chunker.ChunkText(content, chunker.DefaultOptions())
|
||||
if len(chunks) == 0 {
|
||||
h.pool.Exec(ctx, `UPDATE knowledge_documents SET indexing_status = 'completed' WHERE id = $1`, docID)
|
||||
return
|
||||
}
|
||||
|
||||
embeddingAvailable := h.embedder != nil && h.embedder.IsConfigured()
|
||||
successCount := 0
|
||||
|
||||
for i, chunk := range chunks {
|
||||
chunkID := uuid.New()
|
||||
charCount := utf8.RuneCountInString(chunk)
|
||||
|
||||
if embeddingAvailable {
|
||||
emb, err := h.embedder.GetEmbedding(ctx, chunk)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("doc_id", docID.String()).Int("chunk", i).Msg("embedding failed")
|
||||
// 无 embedding 也插入 chunk
|
||||
h.pool.Exec(ctx,
|
||||
`INSERT INTO knowledge_chunks (id, kb_id, doc_id, chunk_index, content, char_count)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
chunkID, kbID, docID, i, chunk, charCount)
|
||||
} else {
|
||||
// 将 []float32 转为 pgvector 格式字符串
|
||||
vecStr := float32SliceToVectorStr(emb)
|
||||
h.pool.Exec(ctx,
|
||||
`INSERT INTO knowledge_chunks (id, kb_id, doc_id, chunk_index, content, char_count, embedding)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7::vector)`,
|
||||
chunkID, kbID, docID, i, chunk, charCount, vecStr)
|
||||
successCount++
|
||||
}
|
||||
} else {
|
||||
// 没有 embedding 服务,只存储文本分片
|
||||
h.pool.Exec(ctx,
|
||||
`INSERT INTO knowledge_chunks (id, kb_id, doc_id, chunk_index, content, char_count)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
chunkID, kbID, docID, i, chunk, charCount)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新文档状态
|
||||
h.pool.Exec(ctx, `UPDATE knowledge_documents SET indexing_status = 'completed', chunk_count = $2 WHERE id = $1`, docID, len(chunks))
|
||||
|
||||
log.Info().
|
||||
Str("doc_id", docID.String()).
|
||||
Int("chunks", len(chunks)).
|
||||
Int("embedded", successCount).
|
||||
Bool("embedding_available", embeddingAvailable).
|
||||
Msg("document chunked and embedded")
|
||||
}
|
||||
|
||||
// float32SliceToVectorStr 将 float32 切片转为 pgvector 格式字符串 "[0.1,0.2,...]"
|
||||
func float32SliceToVectorStr(v []float32) string {
|
||||
s := "["
|
||||
for i, f := range v {
|
||||
if i > 0 {
|
||||
s += ","
|
||||
}
|
||||
s += fmt.Sprintf("%g", f)
|
||||
}
|
||||
s += "]"
|
||||
return s
|
||||
}
|
||||
|
||||
// ReindexAll 对所有未分片的文档执行分片和向量化(管理端点)
|
||||
func (h *KnowledgeHandler) ReindexAll(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.pool.Query(r.Context(),
|
||||
`SELECT kd.id, kd.kb_id, kd.content FROM knowledge_documents kd
|
||||
WHERE kd.content IS NOT NULL AND kd.content != '' AND kd.chunk_count = 0`)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type docInfo struct {
|
||||
docID uuid.UUID
|
||||
kbID uuid.UUID
|
||||
content string
|
||||
}
|
||||
var docs []docInfo
|
||||
for rows.Next() {
|
||||
var d docInfo
|
||||
if err := rows.Scan(&d.docID, &d.kbID, &d.content); err != nil {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, d)
|
||||
}
|
||||
|
||||
for _, d := range docs {
|
||||
h.chunkAndEmbed(r.Context(), d.kbID, d.docID, d.content)
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"message": "重新索引完成",
|
||||
"documents": len(docs),
|
||||
})
|
||||
}
|
||||
|
||||
// ReembedChunks 为已有分片但缺失embedding的chunks补充向量化(管理端点)
|
||||
func (h *KnowledgeHandler) ReembedChunks(w http.ResponseWriter, r *http.Request) {
|
||||
if h.embedder == nil || !h.embedder.IsConfigured() {
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"message": "embedding服务未配置",
|
||||
"updated": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(r.Context(),
|
||||
`SELECT id, content FROM knowledge_chunks WHERE embedding IS NULL AND content IS NOT NULL AND content != '' LIMIT 200`)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type chunkInfo struct {
|
||||
id uuid.UUID
|
||||
content string
|
||||
}
|
||||
var chunks []chunkInfo
|
||||
for rows.Next() {
|
||||
var c chunkInfo
|
||||
if err := rows.Scan(&c.id, &c.content); err != nil {
|
||||
continue
|
||||
}
|
||||
chunks = append(chunks, c)
|
||||
}
|
||||
|
||||
updated := 0
|
||||
for _, c := range chunks {
|
||||
emb, err := h.embedder.GetEmbedding(r.Context(), c.content)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("chunk_id", c.id.String()).Msg("re-embed failed")
|
||||
continue
|
||||
}
|
||||
vecStr := float32SliceToVectorStr(emb)
|
||||
_, err = h.pool.Exec(r.Context(),
|
||||
`UPDATE knowledge_chunks SET embedding = $1::vector WHERE id = $2`, vecStr, c.id)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("chunk_id", c.id.String()).Msg("update embedding failed")
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"message": "向量补充完成",
|
||||
"total": len(chunks),
|
||||
"updated": updated,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) DeleteDocument(w http.ResponseWriter, r *http.Request) {
|
||||
kbID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的知识库ID")
|
||||
return
|
||||
}
|
||||
docID, err := uuid.Parse(chi.URLParam(r, "docId"))
|
||||
if err != nil {
|
||||
response.BadRequest(w, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.pool.Exec(r.Context(), `DELETE FROM knowledge_documents WHERE id = $1 AND kb_id = $2`, docID, kbID)
|
||||
if err != nil {
|
||||
internalErr(w, err)
|
||||
return
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
response.NotFound(w, "文档不存在")
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = h.pool.Exec(r.Context(),
|
||||
`UPDATE knowledge_bases SET doc_count = GREATEST(doc_count - 1, 0), updated_at = NOW() WHERE id = $1`, kbID)
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"})
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
package handler
|
||||
|
||||
// 平台级管理 handler — 仅 super_admin 可访问,跨机构操作,不受 org_id 限制。
|
||||
// 与 admin.go(机构级)的核心区别:
|
||||
// - admin.go 所有查询都加 WHERE org_id = $orgID 过滤
|
||||
// - platform.go 不加 org_id 过滤,可看/操作所有机构数据
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type PlatformHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewPlatformHandler(pool *pgxpool.Pool) *PlatformHandler {
|
||||
return &PlatformHandler{pool: pool}
|
||||
}
|
||||
|
||||
// ==================== 平台总览 ====================
|
||||
|
||||
// Overview 平台级数据看板:跨所有机构的聚合统计
|
||||
func (h *PlatformHandler) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
var totalOrgs, activeOrgs, totalUsers, activeUsers, totalApps, approvedApps, todayLogins, todayConvs int
|
||||
var monthlyTokens int64
|
||||
var monthlyCost float64
|
||||
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM organizations`).Scan(&totalOrgs)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM organizations WHERE is_active = true`).Scan(&activeOrgs)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users`).Scan(&totalUsers)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE status = 'active'`).Scan(&activeUsers)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications`).Scan(&totalApps)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications WHERE status = 'approved'`).Scan(&approvedApps)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE last_login_at >= $1`, todayStart).Scan(&todayLogins)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM app_usage_logs WHERE created_at >= $1`, todayStart).Scan(&todayConvs)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(total_tokens),0) FROM app_usage_logs WHERE created_at >= $1`, monthStart).Scan(&monthlyTokens)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COALESCE(SUM(estimated_cost),0) FROM app_usage_logs WHERE created_at >= $1`, monthStart).Scan(&monthlyCost)
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"total_orgs": totalOrgs,
|
||||
"active_orgs": activeOrgs,
|
||||
"total_users": totalUsers,
|
||||
"active_users": activeUsers,
|
||||
"total_apps": totalApps,
|
||||
"approved_apps": approvedApps,
|
||||
"today_logins": todayLogins,
|
||||
"today_convs": todayConvs,
|
||||
"monthly_tokens": monthlyTokens,
|
||||
"monthly_cost": monthlyCost,
|
||||
})
|
||||
}
|
||||
|
||||
// OrgRanking 各机构活跃度排行(用户数 / 应用数 / 月度对话数)
|
||||
func (h *PlatformHandler) OrgRanking(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT o.id, o.name, COALESCE(o.short_name, ''),
|
||||
COUNT(DISTINCT u.id) AS users,
|
||||
COUNT(DISTINCT a.id) FILTER (WHERE a.status='approved') AS apps,
|
||||
COALESCE(SUM(usage.cnt), 0) AS conversations,
|
||||
COALESCE(SUM(usage.tk), 0) AS tokens
|
||||
FROM organizations o
|
||||
LEFT JOIN users u ON u.org_id = o.id
|
||||
LEFT JOIN applications a ON a.org_id = o.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COUNT(*) AS cnt, COALESCE(SUM(total_tokens),0) AS tk
|
||||
FROM app_usage_logs l
|
||||
JOIN users uu ON l.user_id = uu.id
|
||||
WHERE uu.org_id = o.id AND l.created_at >= $1
|
||||
) usage ON true
|
||||
WHERE o.is_active = true
|
||||
GROUP BY o.id, o.name, o.short_name
|
||||
ORDER BY conversations DESC, users DESC
|
||||
LIMIT 50`, monthStart)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, short string
|
||||
users, apps int
|
||||
conversations, toks int64
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &short, &users, &apps, &conversations, &toks); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "name": name, "short_name": short,
|
||||
"users": users, "apps": apps,
|
||||
"conversations": conversations, "tokens": toks,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ==================== 机构管理 ====================
|
||||
|
||||
func (h *PlatformHandler) ListOrgs(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT o.id, o.name, o.slug, COALESCE(o.short_name,''), COALESCE(o.description,''),
|
||||
COALESCE(o.logo_url,''), o.sort_order, o.is_active, o.created_at,
|
||||
COALESCE((SELECT COUNT(*) FROM users WHERE org_id = o.id), 0) AS user_count,
|
||||
COALESCE((SELECT COUNT(*) FROM applications WHERE org_id = o.id), 0) AS app_count
|
||||
FROM organizations o ORDER BY o.sort_order, o.created_at`)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询机构失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, slug, short, desc, logo string
|
||||
sortOrder int
|
||||
isActive bool
|
||||
createdAt time.Time
|
||||
userCount, appCount int
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &slug, &short, &desc, &logo, &sortOrder, &isActive, &createdAt, &userCount, &appCount); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "name": name, "slug": slug, "short_name": short,
|
||||
"description": desc, "logo_url": logo, "sort_order": sortOrder,
|
||||
"is_active": isActive, "created_at": createdAt,
|
||||
"user_count": userCount, "app_count": appCount,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) CreateOrg(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ShortName string `json:"short_name"`
|
||||
Description string `json:"description"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Slug == "" {
|
||||
response.BadRequest(w, "机构名称和标识不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
INSERT INTO organizations (name, slug, short_name, description, logo_url, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id::text`,
|
||||
req.Name, req.Slug, req.ShortName, req.Description, req.LogoURL, req.SortOrder,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
response.InternalError(w, "创建机构失败:标识可能已存在")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) UpdateOrg(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
ShortName *string `json:"short_name"`
|
||||
Description *string `json:"description"`
|
||||
LogoURL *string `json:"logo_url"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(), `
|
||||
UPDATE organizations SET
|
||||
name = COALESCE($2, name),
|
||||
short_name = COALESCE($3, short_name),
|
||||
description = COALESCE($4, description),
|
||||
logo_url = COALESCE($5, logo_url),
|
||||
sort_order = COALESCE($6, sort_order),
|
||||
is_active = COALESCE($7, is_active),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, id, req.Name, req.ShortName, req.Description, req.LogoURL, req.SortOrder, req.IsActive)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) DeleteOrg(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
// 安全检查:机构下还有用户或应用时禁止物理删除,引导走停用
|
||||
var userCount, appCount int
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users WHERE org_id = $1`, id).Scan(&userCount)
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications WHERE org_id = $1`, id).Scan(&appCount)
|
||||
if userCount > 0 || appCount > 0 {
|
||||
response.BadRequest(w, "该机构尚有用户或应用,无法删除。请先停用或迁移数据")
|
||||
return
|
||||
}
|
||||
_, err := h.pool.Exec(r.Context(), `DELETE FROM organizations WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
response.InternalError(w, "删除失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 全局用户管理 ====================
|
||||
|
||||
// ListAllUsers 全局用户列表(支持分页、按机构/角色/状态过滤)
|
||||
func (h *PlatformHandler) ListAllUsers(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 20
|
||||
offset := (page - 1) * pageSize
|
||||
search := q.Get("q")
|
||||
roleFilter := q.Get("role")
|
||||
statusFilter := q.Get("status")
|
||||
orgFilter := q.Get("org_id")
|
||||
|
||||
// 构造 WHERE 子句(list 与 count 共用)
|
||||
where := ` WHERE 1=1`
|
||||
args := []any{}
|
||||
argIdx := 1
|
||||
|
||||
if search != "" {
|
||||
where += ` AND (u.name ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%' OR u.email ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%')`
|
||||
args = append(args, search)
|
||||
argIdx++
|
||||
}
|
||||
if roleFilter != "" {
|
||||
where += ` AND u.role = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, roleFilter)
|
||||
argIdx++
|
||||
}
|
||||
if statusFilter != "" {
|
||||
where += ` AND u.status = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, statusFilter)
|
||||
argIdx++
|
||||
}
|
||||
if orgFilter != "" {
|
||||
where += ` AND u.org_id = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, orgFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
// 先查总数
|
||||
var total int
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM users u`+where, args...).Scan(&total)
|
||||
|
||||
// 再分页查列表
|
||||
listQuery := `SELECT u.id, u.name, u.email, u.avatar_url, u.role, u.status, u.employee_id,
|
||||
u.last_login_at, u.login_count, u.created_at,
|
||||
COALESCE(o.id::text, ''), COALESCE(o.name, ''), COALESCE(o.short_name, '')
|
||||
FROM users u LEFT JOIN organizations o ON u.org_id = o.id` + where +
|
||||
` ORDER BY u.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
listArgs := append(args, pageSize, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), listQuery, listArgs...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询用户失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, email, role, status string
|
||||
avatarURL, employeeID *string
|
||||
lastLoginAt *time.Time
|
||||
loginCount int
|
||||
createdAt time.Time
|
||||
orgID, orgName, orgShort string
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &email, &avatarURL, &role, &status, &employeeID, &lastLoginAt, &loginCount, &createdAt, &orgID, &orgName, &orgShort); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "name": name, "email": email, "avatar_url": avatarURL,
|
||||
"role": role, "status": status, "employee_id": employeeID,
|
||||
"last_login_at": lastLoginAt, "login_count": loginCount, "created_at": createdAt,
|
||||
"org_id": orgID, "org_name": orgName, "org_short": orgShort,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// AssignUserOrg 把用户分配/迁移到指定机构
|
||||
func (h *PlatformHandler) AssignUserOrg(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
OrgID string `json:"org_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.OrgID == "" {
|
||||
response.BadRequest(w, "机构ID不能为空")
|
||||
return
|
||||
}
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE users SET org_id = $2 WHERE id = $1`, userID, req.OrgID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "迁移失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已迁移"})
|
||||
}
|
||||
|
||||
// UpdateUserRole 平台管理员可设置任意角色(包括 super_admin)
|
||||
func (h *PlatformHandler) UpdateUserRole(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
valid := map[string]bool{"user": true, "creator": true, "admin": true, "super_admin": true}
|
||||
if !valid[req.Role] {
|
||||
response.BadRequest(w, "无效的角色")
|
||||
return
|
||||
}
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE users SET role = $2 WHERE id = $1`, userID, req.Role)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新角色失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
// UpdateUserStatus 启用/禁用任意用户
|
||||
func (h *PlatformHandler) UpdateUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Status != "active" && req.Status != "disabled" {
|
||||
response.BadRequest(w, "无效的状态")
|
||||
return
|
||||
}
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE users SET status = $2 WHERE id = $1`, userID, req.Status)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新状态失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
// ==================== 全局应用管理 ====================
|
||||
|
||||
// ListAllApps 全局应用列表(支持分页、按机构/状态过滤)
|
||||
func (h *PlatformHandler) ListAllApps(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 20
|
||||
offset := (page - 1) * pageSize
|
||||
statusFilter := q.Get("status")
|
||||
orgFilter := q.Get("org_id")
|
||||
|
||||
where := ` WHERE 1=1`
|
||||
args := []any{}
|
||||
argIdx := 1
|
||||
if statusFilter != "" {
|
||||
where += ` AND a.status = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, statusFilter)
|
||||
argIdx++
|
||||
}
|
||||
if orgFilter != "" {
|
||||
where += ` AND a.org_id = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, orgFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
var total int
|
||||
h.pool.QueryRow(r.Context(), `SELECT COUNT(*) FROM applications a`+where, args...).Scan(&total)
|
||||
|
||||
listQuery := `SELECT a.id, a.name, a.slug, a.description, a.icon_url,
|
||||
a.dify_app_type, a.status, a.visibility, a.usage_count, a.is_featured, a.created_at,
|
||||
COALESCE(c.name, ''), COALESCE(u.name, ''),
|
||||
COALESCE(o.id::text, ''), COALESCE(o.name, ''), COALESCE(o.short_name, '')
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
LEFT JOIN organizations o ON a.org_id = o.id` + where +
|
||||
` ORDER BY a.usage_count DESC, a.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
listArgs := append(args, pageSize, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), listQuery, listArgs...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询应用失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, slug, status, visibility string
|
||||
desc, iconURL *string
|
||||
appType *string
|
||||
usageCount int64
|
||||
isFeatured bool
|
||||
createdAt time.Time
|
||||
catName, creatorName string
|
||||
orgID, orgName, orgShort string
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &appType, &status, &visibility,
|
||||
&usageCount, &isFeatured, &createdAt, &catName, &creatorName, &orgID, &orgName, &orgShort); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "name": name, "slug": slug, "description": desc, "icon_url": iconURL,
|
||||
"dify_app_type": appType, "status": status, "visibility": visibility,
|
||||
"usage_count": usageCount, "is_featured": isFeatured, "created_at": createdAt,
|
||||
"category_name": catName, "creator_name": creatorName,
|
||||
"org_id": orgID, "org_name": orgName, "org_short": orgShort,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// SetFeatured 平台级精选/取消精选
|
||||
func (h *PlatformHandler) SetFeatured(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
_, err := h.pool.Exec(r.Context(), `UPDATE applications SET is_featured = $2 WHERE id = $1`, appID, req.IsFeatured)
|
||||
if err != nil {
|
||||
response.InternalError(w, "操作失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
// ForceDelist 平台强制下架(不受机构限制)
|
||||
func (h *PlatformHandler) ForceDelist(w http.ResponseWriter, r *http.Request) {
|
||||
appID := chi.URLParam(r, "id")
|
||||
_, 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": "已强制下架"})
|
||||
}
|
||||
|
||||
// ==================== 全局审计日志 ====================
|
||||
|
||||
// ListAllAuditLogs 全局审计日志(支持分页、按机构/操作类型过滤)
|
||||
func (h *PlatformHandler) ListAllAuditLogs(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 20
|
||||
offset := (page - 1) * pageSize
|
||||
orgFilter := q.Get("org_id")
|
||||
actionFilter := q.Get("action")
|
||||
|
||||
where := ` WHERE 1=1`
|
||||
args := []any{}
|
||||
argIdx := 1
|
||||
if orgFilter != "" {
|
||||
where += ` AND u.org_id = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, orgFilter)
|
||||
argIdx++
|
||||
}
|
||||
if actionFilter != "" {
|
||||
where += ` AND al.action ILIKE '%'||$` + strconv.Itoa(argIdx) + `||'%'`
|
||||
args = append(args, actionFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
var total int
|
||||
h.pool.QueryRow(r.Context(),
|
||||
`SELECT COUNT(*) FROM audit_logs al LEFT JOIN users u ON al.user_id = u.id`+where,
|
||||
args...).Scan(&total)
|
||||
|
||||
listQuery := `SELECT al.id, al.action, al.resource_type, al.resource_id::text, al.details,
|
||||
al.ip_address::text, al.created_at,
|
||||
COALESCE(u.name, ''), COALESCE(u.email, ''),
|
||||
COALESCE(o.name, ''), COALESCE(o.short_name, '')
|
||||
FROM audit_logs al
|
||||
LEFT JOIN users u ON al.user_id = u.id
|
||||
LEFT JOIN organizations o ON u.org_id = o.id` + where +
|
||||
` ORDER BY al.created_at DESC LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
listArgs := append(args, pageSize, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), listQuery, listArgs...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询审计日志失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, action, resType string
|
||||
resID, ipAddr *string
|
||||
details json.RawMessage
|
||||
createdAt time.Time
|
||||
userName, userEmail, orgName, orgShort string
|
||||
)
|
||||
if err := rows.Scan(&id, &action, &resType, &resID, &details, &ipAddr, &createdAt,
|
||||
&userName, &userEmail, &orgName, &orgShort); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "action": action, "resource_type": resType, "resource_id": resID,
|
||||
"details": details, "ip_address": ipAddr, "created_at": createdAt,
|
||||
"user_name": userName, "user_email": userEmail,
|
||||
"org_name": orgName, "org_short": orgShort,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 模型提供商管理 ====================
|
||||
|
||||
func (h *PlatformHandler) ListProviders(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT id, name, base_url, models, is_active, priority, created_at, updated_at
|
||||
FROM model_providers ORDER BY priority DESC, created_at`)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, baseURL string
|
||||
models json.RawMessage
|
||||
isActive bool
|
||||
priority int
|
||||
createdAt, updatedAt time.Time
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &baseURL, &models, &isActive, &priority, &createdAt, &updatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "name": name, "base_url": baseURL, "models": models,
|
||||
"is_active": isActive, "priority": priority,
|
||||
"created_at": createdAt, "updated_at": updatedAt,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) CreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Models json.RawMessage `json:"models"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.BaseURL == "" || req.APIKey == "" {
|
||||
response.BadRequest(w, "名称、URL、API Key 不能为空")
|
||||
return
|
||||
}
|
||||
if len(req.Models) == 0 {
|
||||
req.Models = []byte(`[]`)
|
||||
}
|
||||
|
||||
var id string
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
INSERT INTO model_providers (name, base_url, api_key_encrypted, models, is_active, priority)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id::text`,
|
||||
req.Name, req.BaseURL, req.APIKey, req.Models, req.IsActive, req.Priority,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
response.InternalError(w, "创建失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) UpdateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
APIKey *string `json:"api_key"`
|
||||
Models json.RawMessage `json:"models"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
// 仅当 api_key 非空才更新(避免误清空)
|
||||
var apiKeyArg any = nil
|
||||
if req.APIKey != nil && *req.APIKey != "" {
|
||||
apiKeyArg = *req.APIKey
|
||||
}
|
||||
var modelsArg any = nil
|
||||
if len(req.Models) > 0 {
|
||||
modelsArg = req.Models
|
||||
}
|
||||
|
||||
_, err := h.pool.Exec(r.Context(), `
|
||||
UPDATE model_providers SET
|
||||
name = COALESCE($2, name),
|
||||
base_url = COALESCE($3, base_url),
|
||||
api_key_encrypted = COALESCE($4, api_key_encrypted),
|
||||
models = COALESCE($5::jsonb, models),
|
||||
is_active = COALESCE($6, is_active),
|
||||
priority = COALESCE($7, priority),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
id, req.Name, req.BaseURL, apiKeyArg, modelsArg, req.IsActive, req.Priority)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已更新"})
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) DeleteProvider(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
_, err := h.pool.Exec(r.Context(), `DELETE FROM model_providers WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
response.InternalError(w, "删除失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 全局配额管理 ====================
|
||||
|
||||
func (h *PlatformHandler) ListQuotas(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT q.id, q.target_type, q.target_id::text, q.model_name,
|
||||
q.daily_token_limit, q.monthly_token_limit, q.daily_request_limit,
|
||||
q.is_active, q.created_at,
|
||||
COALESCE(target_name.name, '')
|
||||
FROM model_quotas q
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT CASE q.target_type
|
||||
WHEN 'global' THEN '全局'
|
||||
WHEN 'department' THEN (SELECT name FROM departments WHERE id = q.target_id)
|
||||
WHEN 'user' THEN (SELECT name FROM users WHERE id = q.target_id)
|
||||
END AS name
|
||||
) target_name ON true
|
||||
ORDER BY q.target_type, q.created_at DESC LIMIT 200`)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询配额失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, targetType string
|
||||
targetID *string
|
||||
modelName *string
|
||||
dailyTokens, monthlyTokens *int64
|
||||
dailyReqs *int
|
||||
isActive bool
|
||||
createdAt time.Time
|
||||
targetName string
|
||||
)
|
||||
if err := rows.Scan(&id, &targetType, &targetID, &modelName, &dailyTokens, &monthlyTokens,
|
||||
&dailyReqs, &isActive, &createdAt, &targetName); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": id, "target_type": targetType, "target_id": targetID,
|
||||
"target_name": targetName,
|
||||
"model_name": modelName,
|
||||
"daily_token_limit": dailyTokens,
|
||||
"monthly_token_limit": monthlyTokens,
|
||||
"daily_request_limit": dailyReqs,
|
||||
"is_active": isActive,
|
||||
"created_at": createdAt,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) UpsertQuota(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ID *string `json:"id"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID *string `json:"target_id"`
|
||||
ModelName *string `json:"model_name"`
|
||||
DailyTokenLimit *int64 `json:"daily_token_limit"`
|
||||
MonthlyTokenLimit *int64 `json:"monthly_token_limit"`
|
||||
DailyRequestLimit *int `json:"daily_request_limit"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
valid := map[string]bool{"global": true, "department": true, "user": true}
|
||||
if !valid[req.TargetType] {
|
||||
response.BadRequest(w, "无效的目标类型")
|
||||
return
|
||||
}
|
||||
if req.TargetType != "global" && (req.TargetID == nil || *req.TargetID == "") {
|
||||
response.BadRequest(w, "部门/用户配额必须指定 target_id")
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID != nil && *req.ID != "" {
|
||||
_, err := h.pool.Exec(r.Context(), `
|
||||
UPDATE model_quotas SET
|
||||
target_type = $2, target_id = NULLIF($3,'')::uuid, model_name = $4,
|
||||
daily_token_limit = $5, monthly_token_limit = $6, daily_request_limit = $7,
|
||||
is_active = $8, updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
*req.ID, req.TargetType, nullableString(req.TargetID), req.ModelName,
|
||||
req.DailyTokenLimit, req.MonthlyTokenLimit, req.DailyRequestLimit, req.IsActive)
|
||||
if err != nil {
|
||||
response.InternalError(w, "更新配额失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"id": *req.ID})
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
INSERT INTO model_quotas (target_type, target_id, model_name, daily_token_limit, monthly_token_limit, daily_request_limit, is_active)
|
||||
VALUES ($1, NULLIF($2,'')::uuid, $3, $4, $5, $6, $7)
|
||||
RETURNING id::text`,
|
||||
req.TargetType, nullableString(req.TargetID), req.ModelName,
|
||||
req.DailyTokenLimit, req.MonthlyTokenLimit, req.DailyRequestLimit, req.IsActive,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
response.InternalError(w, "创建配额失败")
|
||||
return
|
||||
}
|
||||
response.InternalError(w, "创建配额失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, map[string]string{"id": id})
|
||||
}
|
||||
|
||||
func (h *PlatformHandler) DeleteQuota(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
_, err := h.pool.Exec(r.Context(), `DELETE FROM model_quotas WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
response.InternalError(w, "删除失败")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, map[string]string{"message": "已删除"})
|
||||
}
|
||||
|
||||
func nullableString(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type PPTHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
rdb *redis.Client
|
||||
workerURL string
|
||||
}
|
||||
|
||||
func NewPPTHandler(pool *pgxpool.Pool, rdb *redis.Client, workerURL string) *PPTHandler {
|
||||
return &PPTHandler{pool: pool, rdb: rdb, workerURL: workerURL}
|
||||
}
|
||||
|
||||
// ==================== 请求/响应结构 ====================
|
||||
|
||||
type createPPTRequest struct {
|
||||
Title string `json:"title"`
|
||||
SourceType string `json:"source_type"` // text / url
|
||||
SourceContent string `json:"source_content"` // 文本内容或 URL
|
||||
Config map[string]any `json:"config"`
|
||||
}
|
||||
|
||||
type pptTaskResponse struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
StatusMessage *string `json:"status_message,omitempty"`
|
||||
ErrorMessage *string `json:"error_message,omitempty"`
|
||||
OutputFile *string `json:"output_file,omitempty"`
|
||||
PageCount *int `json:"page_count,omitempty"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ==================== 接口实现 ====================
|
||||
|
||||
// CreateTask 创建 PPT 生成任务(文本/URL 输入)
|
||||
func (h *PPTHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req createPPTRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.BadRequest(w, "无效的请求格式")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title == "" {
|
||||
response.BadRequest(w, "标题不能为空")
|
||||
return
|
||||
}
|
||||
if req.SourceType == "" {
|
||||
req.SourceType = "text"
|
||||
}
|
||||
if req.SourceContent == "" {
|
||||
response.BadRequest(w, "请提供源内容")
|
||||
return
|
||||
}
|
||||
|
||||
taskID := uuid.New().String()
|
||||
configJSON, _ := json.Marshal(req.Config)
|
||||
|
||||
// 写入数据库
|
||||
_, err := h.pool.Exec(r.Context(),
|
||||
`INSERT INTO ppt_tasks (id, user_id, title, source_type, source_content, config)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
taskID, userID, req.Title, req.SourceType, req.SourceContent, configJSON,
|
||||
)
|
||||
if err != nil {
|
||||
response.InternalError(w, "创建任务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 推送到 Redis 队列
|
||||
taskMsg, _ := json.Marshal(map[string]string{"task_id": taskID})
|
||||
h.rdb.LPush(r.Context(), "ppt:tasks", taskMsg)
|
||||
|
||||
response.JSON(w, http.StatusCreated, map[string]string{
|
||||
"task_id": taskID,
|
||||
"status": "pending",
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTaskWithFile 创建带文件上传的 PPT 生成任务
|
||||
func (h *PPTHandler) CreateTaskWithFile(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
if err := r.ParseMultipartForm(50 << 20); err != nil { // 50MB 限制
|
||||
response.BadRequest(w, "文件过大或请求格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
title := r.FormValue("title")
|
||||
if title == "" {
|
||||
response.BadRequest(w, "标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
configStr := r.FormValue("config")
|
||||
var taskConfig map[string]any
|
||||
if configStr != "" {
|
||||
json.Unmarshal([]byte(configStr), &taskConfig)
|
||||
}
|
||||
if taskConfig == nil {
|
||||
taskConfig = map[string]any{}
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(w, "请上传文件")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 将文件转发到 PPT Worker
|
||||
taskID := uuid.New().String()
|
||||
configJSON, _ := json.Marshal(taskConfig)
|
||||
|
||||
// 转发文件到 Worker 服务
|
||||
err = h.forwardFileToWorker(r.Context(), taskID, userID.String(), title, string(configJSON), file, header)
|
||||
if err != nil {
|
||||
response.InternalError(w, "提交任务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusCreated, map[string]string{
|
||||
"task_id": taskID,
|
||||
"status": "pending",
|
||||
})
|
||||
}
|
||||
|
||||
// GetTaskStatus 查询任务状态
|
||||
func (h *PPTHandler) GetTaskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
taskID := chi.URLParam(r, "taskId")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
// 先从 Redis 快速查询
|
||||
key := "ppt:status:" + taskID
|
||||
cached, err := h.rdb.HGetAll(r.Context(), key).Result()
|
||||
if err == nil && len(cached) > 0 {
|
||||
progress := 0
|
||||
fmt.Sscanf(cached["progress"], "%d", &progress)
|
||||
msg := cached["message"]
|
||||
response.JSON(w, http.StatusOK, pptTaskResponse{
|
||||
TaskID: taskID,
|
||||
Status: cached["status"],
|
||||
Progress: progress,
|
||||
StatusMessage: &msg,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 回退到数据库
|
||||
var task pptTaskResponse
|
||||
var statusMsg, errMsg, outputFile *string
|
||||
var pageCount *int
|
||||
var createdAt time.Time
|
||||
|
||||
err = h.pool.QueryRow(r.Context(),
|
||||
`SELECT id, status, progress, status_message, error_message, output_file, page_count, title, created_at
|
||||
FROM ppt_tasks WHERE id = $1 AND user_id = $2`, taskID, userID,
|
||||
).Scan(&task.TaskID, &task.Status, &task.Progress, &statusMsg, &errMsg, &outputFile, &pageCount, &task.Title, &createdAt)
|
||||
if err != nil {
|
||||
response.NotFound(w, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
task.StatusMessage = statusMsg
|
||||
task.ErrorMessage = errMsg
|
||||
task.OutputFile = outputFile
|
||||
task.PageCount = pageCount
|
||||
task.CreatedAt = createdAt.Format(time.RFC3339)
|
||||
|
||||
response.JSON(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
// ListTasks 列出用户的 PPT 任务
|
||||
func (h *PPTHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
rows, err := h.pool.Query(r.Context(),
|
||||
`SELECT id, status, progress, status_message, error_message, output_file, page_count, title, created_at
|
||||
FROM ppt_tasks WHERE user_id = $1 ORDER BY created_at DESC LIMIT 50`, userID,
|
||||
)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tasks []pptTaskResponse
|
||||
for rows.Next() {
|
||||
var t pptTaskResponse
|
||||
var statusMsg, errMsg, outputFile *string
|
||||
var pageCount *int
|
||||
var createdAt time.Time
|
||||
|
||||
if err := rows.Scan(&t.TaskID, &t.Status, &t.Progress, &statusMsg, &errMsg, &outputFile, &pageCount, &t.Title, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
t.StatusMessage = statusMsg
|
||||
t.ErrorMessage = errMsg
|
||||
t.OutputFile = outputFile
|
||||
t.PageCount = pageCount
|
||||
t.CreatedAt = createdAt.Format(time.RFC3339)
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
if tasks == nil {
|
||||
tasks = []pptTaskResponse{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, tasks)
|
||||
}
|
||||
|
||||
// DownloadTask 下载生成的 PPTX 文件
|
||||
func (h *PPTHandler) DownloadTask(w http.ResponseWriter, r *http.Request) {
|
||||
taskID := chi.URLParam(r, "taskId")
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var status, title string
|
||||
var outputFile *string
|
||||
err := h.pool.QueryRow(r.Context(),
|
||||
`SELECT status, title, output_file FROM ppt_tasks WHERE id = $1 AND user_id = $2`,
|
||||
taskID, userID,
|
||||
).Scan(&status, &title, &outputFile)
|
||||
if err != nil {
|
||||
response.NotFound(w, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "completed" {
|
||||
response.BadRequest(w, "任务未完成")
|
||||
return
|
||||
}
|
||||
|
||||
// 代理下载请求到 Worker 服务
|
||||
workerResp, err := http.Get(h.workerURL + "/api/tasks/" + taskID + "/download")
|
||||
if err != nil {
|
||||
response.InternalError(w, "下载服务不可用")
|
||||
return
|
||||
}
|
||||
defer workerResp.Body.Close()
|
||||
|
||||
if workerResp.StatusCode != http.StatusOK {
|
||||
response.InternalError(w, "文件下载失败")
|
||||
return
|
||||
}
|
||||
|
||||
filename := title + ".pptx"
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
io.Copy(w, workerResp.Body)
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
func (h *PPTHandler) forwardFileToWorker(ctx context.Context, taskID, userID, title, configJSON string, file multipart.File, header *multipart.FileHeader) error {
|
||||
// 构造 multipart 请求转发到 Worker
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
writer.WriteField("task_id", taskID)
|
||||
writer.WriteField("user_id", userID)
|
||||
writer.WriteField("title", title)
|
||||
writer.WriteField("config_json", configJSON)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(header.Filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", h.workerURL+"/api/tasks/upload", &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("worker 返回错误 %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/middleware"
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type StoreHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewStoreHandler(pool *pgxpool.Pool) *StoreHandler {
|
||||
return &StoreHandler{pool: pool}
|
||||
}
|
||||
|
||||
func (h *StoreHandler) ListCategories(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.URL.Query().Get("org_id")
|
||||
query := `SELECT c.id, c.name, c.slug, c.icon, c.description, c.sort_order,
|
||||
COALESCE((SELECT COUNT(*) FROM applications a WHERE a.category_id = c.id AND a.status = 'approved'), 0) AS app_count
|
||||
FROM categories c WHERE c.status = 'active'`
|
||||
var args []any
|
||||
if orgID != "" {
|
||||
query += ` AND (c.org_id = $1 OR c.org_id IS NULL)`
|
||||
args = append(args, orgID)
|
||||
}
|
||||
query += ` ORDER BY c.sort_order ASC`
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询分类失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var cats []map[string]any
|
||||
for rows.Next() {
|
||||
var id, name, slug string
|
||||
var icon, desc *string
|
||||
var sortOrder int
|
||||
var appCount int
|
||||
if err := rows.Scan(&id, &name, &slug, &icon, &desc, &sortOrder, &appCount); err != nil {
|
||||
continue
|
||||
}
|
||||
cats = append(cats, map[string]any{
|
||||
"id": id, "name": name, "slug": slug,
|
||||
"icon": icon, "description": desc, "sort_order": sortOrder,
|
||||
"app_count": appCount,
|
||||
})
|
||||
}
|
||||
response.JSON(w, http.StatusOK, cats)
|
||||
}
|
||||
|
||||
func (h *StoreHandler) ListApps(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, _ := strconv.Atoi(q.Get("page_size"))
|
||||
if pageSize < 1 || pageSize > 50 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
search := q.Get("q")
|
||||
category := q.Get("category")
|
||||
sort := q.Get("sort")
|
||||
orgFilter := q.Get("org_id")
|
||||
if sort == "" {
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT a.id, a.name, a.slug, a.description, a.icon_url,
|
||||
c.name as category_name, c.slug as category_slug,
|
||||
u.name as creator_name,
|
||||
a.usage_count, a.favorite_count, a.avg_rating, a.rating_count,
|
||||
a.dify_app_type, a.welcome_message, a.published_at::text
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.status = 'approved' AND a.visibility = 'public'`
|
||||
|
||||
args := []any{}
|
||||
argIdx := 1
|
||||
|
||||
// 按机构过滤:显示指定机构的应用 + 无机构归属的全局应用
|
||||
if orgFilter != "" {
|
||||
query += ` AND (a.org_id = $` + strconv.Itoa(argIdx) + ` OR a.org_id IS NULL)`
|
||||
args = append(args, orgFilter)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
query += ` AND to_tsvector('simple', a.name || ' ' || COALESCE(a.description, ''))
|
||||
@@ plainto_tsquery('simple', $` + strconv.Itoa(argIdx) + `)`
|
||||
args = append(args, search)
|
||||
argIdx++
|
||||
}
|
||||
if category != "" {
|
||||
query += ` AND c.slug = $` + strconv.Itoa(argIdx)
|
||||
args = append(args, category)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
switch sort {
|
||||
case "rating":
|
||||
query += ` ORDER BY a.avg_rating DESC, a.usage_count DESC`
|
||||
case "latest":
|
||||
query += ` ORDER BY a.published_at DESC NULLS LAST`
|
||||
default:
|
||||
query += ` ORDER BY a.usage_count DESC`
|
||||
}
|
||||
|
||||
query += ` LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1)
|
||||
args = append(args, pageSize, offset)
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询应用失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, slug string
|
||||
desc, iconURL *string
|
||||
catName, catSlug *string
|
||||
creatorName *string
|
||||
usageCount int64
|
||||
favCount, ratingCt int
|
||||
avgRating float32
|
||||
difyType, welcome *string
|
||||
publishedAt *string
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &slug, &desc, &iconURL,
|
||||
&catName, &catSlug, &creatorName,
|
||||
&usageCount, &favCount, &avgRating, &ratingCt,
|
||||
&difyType, &welcome, &publishedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, map[string]any{
|
||||
"id": id, "name": name, "slug": slug,
|
||||
"description": desc, "icon_url": iconURL,
|
||||
"category_name": catName, "category_slug": catSlug,
|
||||
"creator_name": creatorName,
|
||||
"usage_count": usageCount, "favorite_count": favCount,
|
||||
"avg_rating": avgRating, "rating_count": ratingCt,
|
||||
"dify_app_type": difyType, "welcome_message": welcome,
|
||||
"published_at": publishedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if apps == nil {
|
||||
apps = []map[string]any{}
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"items": apps,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *StoreHandler) GetApp(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
|
||||
var (
|
||||
id, name, appSlug string
|
||||
desc, longDesc *string
|
||||
iconURL *string
|
||||
catName, catSlug *string
|
||||
creatorName *string
|
||||
usageCount int64
|
||||
favCount, ratingCt int
|
||||
avgRating float32
|
||||
difyType, welcome *string
|
||||
suggestedPrompts *string
|
||||
appConfig *string
|
||||
publishedAt *string
|
||||
version string
|
||||
)
|
||||
|
||||
err := h.pool.QueryRow(r.Context(), `
|
||||
SELECT a.id, a.name, a.slug, a.description, a.long_description, a.icon_url,
|
||||
c.name, c.slug, u.name,
|
||||
a.usage_count, a.favorite_count, a.avg_rating, a.rating_count,
|
||||
a.dify_app_type, a.welcome_message, a.suggested_prompts::text,
|
||||
a.app_config::text,
|
||||
a.published_at::text, a.version
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.slug = $1 AND a.status = 'approved'`, slug,
|
||||
).Scan(&id, &name, &appSlug, &desc, &longDesc, &iconURL,
|
||||
&catName, &catSlug, &creatorName,
|
||||
&usageCount, &favCount, &avgRating, &ratingCt,
|
||||
&difyType, &welcome, &suggestedPrompts,
|
||||
&appConfig,
|
||||
&publishedAt, &version)
|
||||
|
||||
if err != nil {
|
||||
response.NotFound(w, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user favorited this app
|
||||
isFavorited := false
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
if userID.String() != "00000000-0000-0000-0000-000000000000" {
|
||||
_ = h.pool.QueryRow(r.Context(),
|
||||
`SELECT EXISTS(SELECT 1 FROM app_favorites WHERE user_id = $1 AND app_id = $2)`,
|
||||
userID, id).Scan(&isFavorited)
|
||||
}
|
||||
|
||||
// Parse app_config as JSON if present
|
||||
var configData any
|
||||
if appConfig != nil {
|
||||
_ = json.Unmarshal([]byte(*appConfig), &configData)
|
||||
}
|
||||
|
||||
response.JSON(w, http.StatusOK, map[string]any{
|
||||
"id": id, "name": name, "slug": appSlug,
|
||||
"description": desc, "long_description": longDesc, "icon_url": iconURL,
|
||||
"category_name": catName, "category_slug": catSlug,
|
||||
"creator_name": creatorName,
|
||||
"usage_count": usageCount, "favorite_count": favCount,
|
||||
"avg_rating": avgRating, "rating_count": ratingCt,
|
||||
"dify_app_type": difyType, "welcome_message": welcome,
|
||||
"suggested_prompts": suggestedPrompts,
|
||||
"app_config": configData,
|
||||
"published_at": publishedAt, "version": version,
|
||||
"is_favorited": isFavorited,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *StoreHandler) Featured(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.URL.Query().Get("org_id")
|
||||
query := `
|
||||
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, a.dify_app_type
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE a.is_featured = true AND a.status = 'approved' AND a.visibility = 'public'`
|
||||
var args []any
|
||||
if orgID != "" {
|
||||
query += ` AND (a.org_id = $1 OR a.org_id IS NULL)`
|
||||
args = append(args, orgID)
|
||||
}
|
||||
query += ` ORDER BY a.usage_count DESC LIMIT 4`
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询精选应用失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
apps := scanAppList(rows)
|
||||
response.JSON(w, http.StatusOK, apps)
|
||||
}
|
||||
|
||||
func (h *StoreHandler) Rankings(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.URL.Query().Get("org_id")
|
||||
query := `
|
||||
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, a.dify_app_type
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE a.status = 'approved' AND a.visibility = 'public'`
|
||||
var args []any
|
||||
if orgID != "" {
|
||||
query += ` AND (a.org_id = $1 OR a.org_id IS NULL)`
|
||||
args = append(args, orgID)
|
||||
}
|
||||
query += ` ORDER BY a.usage_count DESC LIMIT 50`
|
||||
|
||||
rows, err := h.pool.Query(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询排行榜失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
apps := scanAppList(rows)
|
||||
response.JSON(w, http.StatusOK, apps)
|
||||
}
|
||||
|
||||
func (h *StoreHandler) Recent(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
rows, err := h.pool.Query(r.Context(), `
|
||||
SELECT DISTINCT ON (a.id) 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, a.dify_app_type
|
||||
FROM app_usage_logs l
|
||||
JOIN applications a ON l.app_id = a.id
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE l.user_id = $1
|
||||
ORDER BY a.id, l.created_at DESC
|
||||
LIMIT 10`, userID)
|
||||
if err != nil {
|
||||
response.InternalError(w, "查询最近使用失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
apps := scanAppList(rows)
|
||||
response.JSON(w, http.StatusOK, apps)
|
||||
}
|
||||
|
||||
func scanAppList(rows interface {
|
||||
Next() bool
|
||||
Scan(dest ...any) error
|
||||
}) []map[string]any {
|
||||
var apps []map[string]any
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, name, slug string
|
||||
desc, iconURL *string
|
||||
catName, catSlug *string
|
||||
usageCount int64
|
||||
avgRating float32
|
||||
ratingCt int
|
||||
difyType *string
|
||||
)
|
||||
if err := rows.Scan(&id, &name, &slug, &desc, &iconURL,
|
||||
&catName, &catSlug, &usageCount, &avgRating, &ratingCt, &difyType); err != nil {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, map[string]any{
|
||||
"id": id, "name": name, "slug": slug,
|
||||
"description": desc, "icon_url": iconURL,
|
||||
"category_name": catName, "category_slug": catSlug,
|
||||
"usage_count": usageCount, "avg_rating": avgRating, "rating_count": ratingCt,
|
||||
"dify_app_type": difyType,
|
||||
})
|
||||
}
|
||||
if apps == nil {
|
||||
apps = []map[string]any{}
|
||||
}
|
||||
return apps
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// extractIP returns a clean IP without port. Falls back to "" so the INET
|
||||
// column can take NULL via $5 when the value is empty.
|
||||
func extractIP(r *http.Request) any {
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// AuditLog records API access to the audit_logs table for important operations.
|
||||
func AuditLog(pool *pgxpool.Pool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// Only audit write operations
|
||||
if r.Method == "GET" || r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
userID := GetUserID(r.Context())
|
||||
if userID.String() == "00000000-0000-0000-0000-000000000000" {
|
||||
return
|
||||
}
|
||||
|
||||
details, _ := json.Marshal(map[string]string{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
})
|
||||
|
||||
ip := extractIP(r)
|
||||
ua := r.UserAgent()
|
||||
method := r.Method
|
||||
path := r.URL.Path
|
||||
|
||||
go func() {
|
||||
_, _ = pool.Exec(context.Background(),
|
||||
`INSERT INTO audit_logs (user_id, action, resource_type, resource_id, details, ip_address, user_agent)
|
||||
VALUES ($1, $2, $3, NULL, $4, $5, $6)`,
|
||||
userID,
|
||||
method+"."+path,
|
||||
"api",
|
||||
details,
|
||||
ip,
|
||||
ua,
|
||||
)
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/enterprise-ai-platform/server/pkg/auth"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
UserIDKey contextKey = "user_id"
|
||||
EmailKey contextKey = "email"
|
||||
RoleKey contextKey = "role"
|
||||
)
|
||||
|
||||
func GetUserID(ctx context.Context) uuid.UUID {
|
||||
v, _ := ctx.Value(UserIDKey).(uuid.UUID)
|
||||
return v
|
||||
}
|
||||
|
||||
func GetRole(ctx context.Context) string {
|
||||
v, _ := ctx.Value(RoleKey).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func GetEmail(ctx context.Context) string {
|
||||
v, _ := ctx.Value(EmailKey).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
// Auth creates a middleware that validates JWT and injects user info into context.
|
||||
func Auth(jwtMgr *auth.JWTManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenStr := extractToken(r)
|
||||
if tokenStr == "" {
|
||||
response.Unauthorized(w, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwtMgr.ValidateToken(tokenStr)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusUnauthorized, 40102, "Token 已过期或无效")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, EmailKey, claims.Email)
|
||||
ctx = context.WithValue(ctx, RoleKey, claims.Role)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func extractToken(r *http.Request) string {
|
||||
// Try Authorization header first
|
||||
bearer := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(bearer, "Bearer ") {
|
||||
return strings.TrimPrefix(bearer, "Bearer ")
|
||||
}
|
||||
|
||||
// Then try cookie
|
||||
cookie, err := r.Cookie("access_token")
|
||||
if err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RateLimit creates a per-user rate limiter using Redis sliding window.
|
||||
func RateLimit(rdb *redis.Client, maxRequests int, window time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID := GetUserID(r.Context())
|
||||
key := fmt.Sprintf("rl:%s:%s", userID.String(), r.URL.Path)
|
||||
|
||||
ctx := context.Background()
|
||||
count, err := rdb.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
rdb.Expire(ctx, key, window)
|
||||
}
|
||||
|
||||
if count > int64(maxRequests) {
|
||||
response.TooManyRequests(w, "请求过于频繁,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
)
|
||||
|
||||
var roleLevel = map[string]int{
|
||||
"user": 0,
|
||||
"creator": 1,
|
||||
"admin": 2,
|
||||
"super_admin": 3,
|
||||
}
|
||||
|
||||
// RequireRole returns middleware that checks if user has the minimum required role.
|
||||
func RequireRole(minRole string) func(http.Handler) http.Handler {
|
||||
minLevel := roleLevel[minRole]
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
role := GetRole(r.Context())
|
||||
if roleLevel[role] < minLevel {
|
||||
response.Forbidden(w, "权限不足")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireSuperAdmin restricts access to platform-level (super_admin) operations only.
|
||||
// Unlike RequireRole("admin"),super admin 不受机构(org_id)限制,可执行跨机构操作。
|
||||
func RequireSuperAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if GetRole(r.Context()) != "super_admin" {
|
||||
response.Forbidden(w, "仅平台管理员可访问")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func JSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, status int, code int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
func BadRequest(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusBadRequest, 40001, message)
|
||||
}
|
||||
|
||||
func Unauthorized(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusUnauthorized, 40101, message)
|
||||
}
|
||||
|
||||
func Forbidden(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusForbidden, 40301, message)
|
||||
}
|
||||
|
||||
func NotFound(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusNotFound, 40401, message)
|
||||
}
|
||||
|
||||
func InternalError(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusInternalServerError, 50001, message)
|
||||
}
|
||||
|
||||
func TooManyRequests(w http.ResponseWriter, message string) {
|
||||
Error(w, http.StatusTooManyRequests, 42901, message)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP TRIGGER IF EXISTS update_departments_updated_at ON departments;
|
||||
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
|
||||
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||
DROP TABLE IF EXISTS user_departments;
|
||||
DROP TABLE IF EXISTS departments;
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,79 @@
|
||||
-- 000001: 用户表与部门表
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
employee_id VARCHAR(50) UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
avatar_url TEXT,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'user'
|
||||
CHECK (role IN ('super_admin', 'admin', 'creator', 'user')),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'disabled', 'pending')),
|
||||
sso_provider VARCHAR(50),
|
||||
sso_external_id VARCHAR(255),
|
||||
last_login_at TIMESTAMPTZ,
|
||||
login_count INTEGER NOT NULL DEFAULT 0,
|
||||
preferences JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_employee_id ON users(employee_id);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
CREATE INDEX idx_users_status ON users(status);
|
||||
|
||||
-- 部门表
|
||||
CREATE TABLE departments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(50) UNIQUE NOT NULL,
|
||||
parent_id UUID REFERENCES departments(id),
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_departments_parent ON departments(parent_id);
|
||||
CREATE INDEX idx_departments_path ON departments(path);
|
||||
|
||||
-- 用户-部门关联表
|
||||
CREATE TABLE user_departments (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
dept_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'member'
|
||||
CHECK (role IN ('head', 'manager', 'member')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, dept_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_dept_user ON user_departments(user_id);
|
||||
CREATE INDEX idx_user_dept_dept ON user_departments(dept_id);
|
||||
|
||||
-- updated_at 自动更新触发器
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_departments_updated_at
|
||||
BEFORE UPDATE ON departments
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TRIGGER IF EXISTS update_applications_updated_at ON applications;
|
||||
DROP TRIGGER IF EXISTS update_categories_updated_at ON categories;
|
||||
DROP TABLE IF EXISTS applications;
|
||||
DROP TABLE IF EXISTS categories;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- 000002: 应用分类表与应用表
|
||||
|
||||
-- 应用分类表
|
||||
CREATE TABLE categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE NOT NULL,
|
||||
icon VARCHAR(50),
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
parent_id UUID REFERENCES categories(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 预置分类数据(政务场景)
|
||||
INSERT INTO categories (name, slug, icon, sort_order) VALUES
|
||||
('公文写作', 'official-writing', 'file-text', 1),
|
||||
('政策解读', 'policy-qa', 'book-open', 2),
|
||||
('政务宣传', 'gov-publicity', 'megaphone', 3),
|
||||
('数据治理', 'data-governance', 'bar-chart', 4),
|
||||
('便民服务', 'public-service', 'headphones', 5),
|
||||
('信息化工具', 'it-tools', 'code', 6),
|
||||
('组织人事', 'hr-org', 'users', 7),
|
||||
('招商引资', 'investment', 'trending-up', 8),
|
||||
('翻译外事', 'translation', 'globe', 9),
|
||||
('综合应用', 'general', 'more-horizontal', 99);
|
||||
|
||||
-- 应用表
|
||||
CREATE TABLE applications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
long_description TEXT,
|
||||
icon_url TEXT,
|
||||
category_id UUID REFERENCES categories(id),
|
||||
creator_id UUID NOT NULL REFERENCES users(id),
|
||||
dept_id UUID REFERENCES departments(id),
|
||||
|
||||
-- Dify 关联
|
||||
dify_app_id VARCHAR(255),
|
||||
dify_app_type VARCHAR(50)
|
||||
CHECK (dify_app_type IN ('chatbot', 'completion', 'workflow', 'agent')),
|
||||
dify_api_key TEXT,
|
||||
|
||||
-- 应用配置
|
||||
app_config JSONB NOT NULL DEFAULT '{}',
|
||||
welcome_message TEXT,
|
||||
suggested_prompts JSONB DEFAULT '[]',
|
||||
max_tokens INTEGER DEFAULT 4096,
|
||||
temperature REAL DEFAULT 0.7,
|
||||
|
||||
-- 状态与可见性
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'pending_review', 'approved', 'rejected', 'archived')),
|
||||
visibility VARCHAR(20) NOT NULL DEFAULT 'private'
|
||||
CHECK (visibility IN ('private', 'department', 'public')),
|
||||
is_featured BOOLEAN NOT NULL DEFAULT false,
|
||||
is_template BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- 统计(冗余计数,定期同步)
|
||||
usage_count BIGINT NOT NULL DEFAULT 0,
|
||||
favorite_count INTEGER NOT NULL DEFAULT 0,
|
||||
avg_rating REAL NOT NULL DEFAULT 0,
|
||||
rating_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- 版本
|
||||
version VARCHAR(20) NOT NULL DEFAULT '1.0.0',
|
||||
published_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_apps_creator ON applications(creator_id);
|
||||
CREATE INDEX idx_apps_category ON applications(category_id);
|
||||
CREATE INDEX idx_apps_status ON applications(status);
|
||||
CREATE INDEX idx_apps_visibility ON applications(visibility);
|
||||
CREATE INDEX idx_apps_featured ON applications(is_featured) WHERE is_featured = true;
|
||||
CREATE INDEX idx_apps_usage ON applications(usage_count DESC);
|
||||
CREATE INDEX idx_apps_name_search ON applications
|
||||
USING gin(to_tsvector('simple', name || ' ' || COALESCE(description, '')));
|
||||
|
||||
CREATE TRIGGER update_categories_updated_at
|
||||
BEFORE UPDATE ON categories
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_applications_updated_at
|
||||
BEFORE UPDATE ON applications
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TRIGGER IF EXISTS update_ratings_updated_at ON app_ratings;
|
||||
DROP TABLE IF EXISTS app_ratings;
|
||||
DROP TABLE IF EXISTS app_favorites;
|
||||
DROP TABLE IF EXISTS app_reviews;
|
||||
@@ -0,0 +1,51 @@
|
||||
-- 000003: 审核表、收藏表、评分表
|
||||
|
||||
-- 应用审核表
|
||||
CREATE TABLE app_reviews (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
app_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||
version VARCHAR(20) NOT NULL,
|
||||
submitter_id UUID NOT NULL REFERENCES users(id),
|
||||
reviewer_id UUID REFERENCES users(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn')),
|
||||
submit_comment TEXT,
|
||||
review_comment TEXT,
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reviews_app ON app_reviews(app_id);
|
||||
CREATE INDEX idx_reviews_status ON app_reviews(status);
|
||||
CREATE INDEX idx_reviews_submitter ON app_reviews(submitter_id);
|
||||
|
||||
-- 收藏表
|
||||
CREATE TABLE app_favorites (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
app_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, app_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_favorites_user ON app_favorites(user_id);
|
||||
CREATE INDEX idx_favorites_app ON app_favorites(app_id);
|
||||
|
||||
-- 评分表
|
||||
CREATE TABLE app_ratings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
app_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
score SMALLINT NOT NULL CHECK (score >= 1 AND score <= 5),
|
||||
comment TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(app_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ratings_app ON app_ratings(app_id);
|
||||
CREATE INDEX idx_ratings_user ON app_ratings(user_id);
|
||||
|
||||
CREATE TRIGGER update_ratings_updated_at
|
||||
BEFORE UPDATE ON app_ratings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS app_usage_daily;
|
||||
DROP TABLE IF EXISTS app_usage_logs;
|
||||
@@ -0,0 +1,54 @@
|
||||
-- 000004: 使用记录表
|
||||
|
||||
-- 使用明细表
|
||||
CREATE TABLE app_usage_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
app_id UUID NOT NULL REFERENCES applications(id),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
dept_id UUID REFERENCES departments(id),
|
||||
|
||||
conversation_id VARCHAR(255),
|
||||
message_count INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
total_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
model_name VARCHAR(100),
|
||||
estimated_cost DECIMAL(10, 6) NOT NULL DEFAULT 0,
|
||||
|
||||
duration_ms INTEGER,
|
||||
is_successful BOOLEAN NOT NULL DEFAULT true,
|
||||
error_message TEXT,
|
||||
client_type VARCHAR(20) DEFAULT 'web'
|
||||
CHECK (client_type IN ('web', 'wechat', 'dingtalk', 'feishu', 'api')),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_usage_app ON app_usage_logs(app_id);
|
||||
CREATE INDEX idx_usage_user ON app_usage_logs(user_id);
|
||||
CREATE INDEX idx_usage_dept ON app_usage_logs(dept_id);
|
||||
CREATE INDEX idx_usage_created ON app_usage_logs(created_at);
|
||||
CREATE INDEX idx_usage_model ON app_usage_logs(model_name);
|
||||
|
||||
-- 日汇总表(定时任务聚合)
|
||||
CREATE TABLE app_usage_daily (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
date DATE NOT NULL,
|
||||
app_id UUID NOT NULL REFERENCES applications(id),
|
||||
dept_id UUID REFERENCES departments(id),
|
||||
|
||||
total_requests BIGINT NOT NULL DEFAULT 0,
|
||||
unique_users INTEGER NOT NULL DEFAULT 0,
|
||||
total_tokens BIGINT NOT NULL DEFAULT 0,
|
||||
total_cost DECIMAL(12, 6) NOT NULL DEFAULT 0,
|
||||
avg_duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
error_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(date, app_id, dept_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_daily_date ON app_usage_daily(date);
|
||||
CREATE INDEX idx_daily_app ON app_usage_daily(app_id);
|
||||
CREATE INDEX idx_daily_dept ON app_usage_daily(dept_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
DROP TRIGGER IF EXISTS update_knowledge_documents_updated_at ON knowledge_documents;
|
||||
DROP TRIGGER IF EXISTS update_knowledge_bases_updated_at ON knowledge_bases;
|
||||
DROP TRIGGER IF EXISTS update_model_quotas_updated_at ON model_quotas;
|
||||
DROP TRIGGER IF EXISTS update_model_providers_updated_at ON model_providers;
|
||||
DROP TABLE IF EXISTS audit_logs;
|
||||
DROP TABLE IF EXISTS knowledge_documents;
|
||||
DROP TABLE IF EXISTS knowledge_bases;
|
||||
DROP TABLE IF EXISTS model_quotas;
|
||||
DROP TABLE IF EXISTS model_providers;
|
||||
@@ -0,0 +1,108 @@
|
||||
-- 000005: 模型配额、提供商、知识库、审计日志
|
||||
|
||||
-- 模型提供商表
|
||||
CREATE TABLE model_providers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
api_key_encrypted TEXT NOT NULL,
|
||||
models JSONB NOT NULL DEFAULT '[]',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_model_providers_updated_at
|
||||
BEFORE UPDATE ON model_providers
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 模型配额表
|
||||
CREATE TABLE model_quotas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
target_type VARCHAR(20) NOT NULL
|
||||
CHECK (target_type IN ('global', 'department', 'user')),
|
||||
target_id UUID,
|
||||
model_name VARCHAR(100),
|
||||
daily_token_limit BIGINT,
|
||||
monthly_token_limit BIGINT,
|
||||
daily_request_limit INTEGER,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_quotas_target ON model_quotas(target_type, target_id);
|
||||
|
||||
CREATE TRIGGER update_model_quotas_updated_at
|
||||
BEFORE UPDATE ON model_quotas
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 知识库表
|
||||
CREATE TABLE knowledge_bases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
owner_id UUID NOT NULL REFERENCES users(id),
|
||||
dept_id UUID REFERENCES departments(id),
|
||||
dify_dataset_id VARCHAR(255),
|
||||
visibility VARCHAR(20) NOT NULL DEFAULT 'private'
|
||||
CHECK (visibility IN ('private', 'department', 'public')),
|
||||
doc_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_chars BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_kb_owner ON knowledge_bases(owner_id);
|
||||
CREATE INDEX idx_kb_dept ON knowledge_bases(dept_id);
|
||||
|
||||
CREATE TRIGGER update_knowledge_bases_updated_at
|
||||
BEFORE UPDATE ON knowledge_bases
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 知识库文档表
|
||||
CREATE TABLE knowledge_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
file_url TEXT,
|
||||
file_type VARCHAR(20)
|
||||
CHECK (file_type IN ('pdf', 'docx', 'txt', 'md', 'csv', 'xlsx')),
|
||||
file_size BIGINT NOT NULL DEFAULT 0,
|
||||
char_count BIGINT NOT NULL DEFAULT 0,
|
||||
dify_doc_id VARCHAR(255),
|
||||
indexing_status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
CHECK (indexing_status IN ('pending', 'indexing', 'completed', 'failed')),
|
||||
uploader_id UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_docs_kb ON knowledge_documents(kb_id);
|
||||
CREATE INDEX idx_docs_status ON knowledge_documents(indexing_status);
|
||||
|
||||
CREATE TRIGGER update_knowledge_documents_updated_at
|
||||
BEFORE UPDATE ON knowledge_documents
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 审计日志表
|
||||
CREATE TABLE audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
action VARCHAR(50) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id UUID,
|
||||
details JSONB NOT NULL DEFAULT '{}',
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_user ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_action ON audit_logs(action);
|
||||
CREATE INDEX idx_audit_resource ON audit_logs(resource_type, resource_id);
|
||||
CREATE INDEX idx_audit_created ON audit_logs(created_at);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ppt_tasks;
|
||||
@@ -0,0 +1,64 @@
|
||||
-- 000006: PPT 生成任务表
|
||||
|
||||
CREATE TABLE ppt_tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
app_id UUID REFERENCES applications(id),
|
||||
|
||||
-- 任务输入
|
||||
title VARCHAR(200) NOT NULL,
|
||||
source_type VARCHAR(20) NOT NULL DEFAULT 'text'
|
||||
CHECK (source_type IN ('text', 'file', 'url')),
|
||||
source_content TEXT, -- 文本内容或 URL
|
||||
source_file TEXT, -- 上传文件路径
|
||||
|
||||
-- 生成配置
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
-- config 结构:
|
||||
-- {
|
||||
-- "format": "ppt169", -- 画布格式
|
||||
-- "page_count": 10, -- 目标页数
|
||||
-- "style": "general", -- 风格: general/consultant/consultant-top
|
||||
-- "color_scheme": "", -- 配色方案(可选)
|
||||
-- "language": "zh", -- 语言
|
||||
-- "with_images": true, -- 是否生成图片
|
||||
-- "image_backend": "gpt-image-2" -- 图片生成后端
|
||||
-- }
|
||||
|
||||
-- 任务状态
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN (
|
||||
'pending', -- 等待处理
|
||||
'processing', -- 处理中
|
||||
'converting', -- 转换源文件
|
||||
'designing', -- 策略师阶段
|
||||
'generating_images', -- 图片生成阶段
|
||||
'generating_svg', -- SVG 生成阶段
|
||||
'exporting', -- 导出 PPTX 阶段
|
||||
'completed', -- 完成
|
||||
'failed' -- 失败
|
||||
)),
|
||||
progress INTEGER NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100),
|
||||
status_message TEXT, -- 当前状态描述
|
||||
error_message TEXT, -- 错误信息
|
||||
|
||||
-- 输出
|
||||
output_file TEXT, -- 生成的 PPTX 文件路径
|
||||
output_preview JSONB DEFAULT '[]', -- SVG 预览图路径列表
|
||||
page_count INTEGER, -- 实际生成页数
|
||||
project_path TEXT, -- PPT Master 项目路径
|
||||
|
||||
-- 时间戳
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ppt_tasks_user ON ppt_tasks(user_id);
|
||||
CREATE INDEX idx_ppt_tasks_status ON ppt_tasks(status);
|
||||
CREATE INDEX idx_ppt_tasks_created ON ppt_tasks(created_at DESC);
|
||||
|
||||
CREATE TRIGGER update_ppt_tasks_updated_at
|
||||
BEFORE UPDATE ON ppt_tasks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE applications DROP CONSTRAINT IF EXISTS applications_dify_app_type_check;
|
||||
ALTER TABLE applications ADD CONSTRAINT applications_dify_app_type_check
|
||||
CHECK (dify_app_type IN ('chatbot', 'completion', 'workflow', 'agent'));
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 000007: 扩展 dify_app_type 支持 ppt_generator
|
||||
|
||||
ALTER TABLE applications DROP CONSTRAINT IF EXISTS applications_dify_app_type_check;
|
||||
ALTER TABLE applications ADD CONSTRAINT applications_dify_app_type_check
|
||||
CHECK (dify_app_type IN ('chatbot', 'completion', 'workflow', 'agent', 'ppt_generator'));
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS document_templates;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 公文模板表
|
||||
CREATE TABLE IF NOT EXISTS document_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
doc_type VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(50),
|
||||
format_standard TEXT NOT NULL DEFAULT '',
|
||||
fields JSONB NOT NULL DEFAULT '[]',
|
||||
prompt_template TEXT NOT NULL DEFAULT '',
|
||||
example_output TEXT,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_templates_doc_type ON document_templates(doc_type);
|
||||
CREATE INDEX idx_document_templates_active ON document_templates(is_active);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS conversation_names;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 对话重命名表
|
||||
CREATE TABLE IF NOT EXISTS conversation_names (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
app_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
conversation_id TEXT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 唯一约束:同一应用同一用户同一对话只能有一个名称
|
||||
CREATE UNIQUE INDEX idx_conversation_names_unique
|
||||
ON conversation_names(app_id, user_id, conversation_id);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE applications DROP COLUMN IF EXISTS knowledge_base_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 给应用表添加知识库关联字段
|
||||
ALTER TABLE applications ADD COLUMN IF NOT EXISTS knowledge_base_id UUID REFERENCES knowledge_bases(id);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE knowledge_documents DROP COLUMN IF EXISTS content;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 给知识库文档表添加内容字段,用于存储文档全文
|
||||
ALTER TABLE knowledge_documents ADD COLUMN IF NOT EXISTS content TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE app_usage_logs DROP COLUMN IF EXISTS user_message;
|
||||
ALTER TABLE app_usage_logs DROP COLUMN IF EXISTS ai_response;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 给使用日志表添加消息内容字段
|
||||
ALTER TABLE app_usage_logs ADD COLUMN IF NOT EXISTS user_message TEXT DEFAULT '';
|
||||
ALTER TABLE app_usage_logs ADD COLUMN IF NOT EXISTS ai_response TEXT DEFAULT '';
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE knowledge_bases DROP COLUMN IF EXISTS org_id;
|
||||
ALTER TABLE applications DROP COLUMN IF EXISTS org_id;
|
||||
DROP INDEX IF EXISTS idx_apps_org;
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS org_id;
|
||||
DROP INDEX IF EXISTS idx_users_org;
|
||||
DROP TABLE IF EXISTS organizations;
|
||||
@@ -0,0 +1,42 @@
|
||||
-- 机构/委办局表
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||
short_name VARCHAR(20),
|
||||
logo_url TEXT,
|
||||
description TEXT,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 给用户表添加机构关联
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_org ON users(org_id);
|
||||
|
||||
-- 给应用表添加机构关联(应用可属于某个机构,NULL表示全局通用应用)
|
||||
ALTER TABLE applications ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_apps_org ON applications(org_id);
|
||||
|
||||
-- 给知识库添加机构关联
|
||||
ALTER TABLE knowledge_bases ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
|
||||
-- 种子数据:常用机构
|
||||
INSERT INTO organizations (id, name, slug, short_name, description, sort_order) VALUES
|
||||
('a0000000-0000-0000-0000-000000000001', '科学技术局', 'keji', '科技局', '负责全市科技发展规划、科技项目管理、高新技术企业培育等工作', 1),
|
||||
('a0000000-0000-0000-0000-000000000002', '公安局', 'gongan', '公安局', '负责全市社会治安管理、户籍管理、交通管理、网络安全等工作', 2),
|
||||
('a0000000-0000-0000-0000-000000000003', '发展和改革局', 'fagai', '发改局', '负责全市国民经济和社会发展战略、规划、投资管理等工作', 3),
|
||||
('a0000000-0000-0000-0000-000000000004', '教育局', 'jiaoyu', '教育局', '负责全市教育事业发展规划、基础教育、职业教育等工作', 4),
|
||||
('a0000000-0000-0000-0000-000000000005', '人力资源和社会保障局', 'renshe', '人社局', '负责全市人力资源开发、就业创业、社会保障等工作', 5),
|
||||
('a0000000-0000-0000-0000-000000000006', '财政局', 'caizheng', '财政局', '负责全市财政收支管理、预算管理、政府采购等工作', 6),
|
||||
('a0000000-0000-0000-0000-000000000007', '住房和城乡建设局', 'zhujian', '住建局', '负责全市住房保障、城乡建设、房地产管理等工作', 7),
|
||||
('a0000000-0000-0000-0000-000000000008', '市场监督管理局', 'shijianju', '市监局', '负责全市市场监管、食品安全、知识产权保护等工作', 8)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 将现有用户关联到科技局
|
||||
UPDATE users SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
|
||||
-- 将现有应用关联到科技局
|
||||
UPDATE applications SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE departments DROP COLUMN IF EXISTS org_id;
|
||||
ALTER TABLE analysis_report_templates DROP COLUMN IF EXISTS org_id;
|
||||
ALTER TABLE document_templates DROP COLUMN IF EXISTS org_id;
|
||||
ALTER TABLE categories DROP COLUMN IF EXISTS org_id;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- 多租户:给更多实体添加 org_id 关联
|
||||
|
||||
-- 分类表
|
||||
ALTER TABLE categories ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_categories_org ON categories(org_id);
|
||||
|
||||
-- 公文模板表
|
||||
ALTER TABLE document_templates ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_templates_org ON document_templates(org_id);
|
||||
|
||||
-- 分析报告模板表(可能不存在,安全跳过)
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'analysis_report_templates') THEN
|
||||
ALTER TABLE analysis_report_templates ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_analysis_templates_org ON analysis_report_templates(org_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 部门表
|
||||
ALTER TABLE departments ADD COLUMN IF NOT EXISTS org_id UUID REFERENCES organizations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_org ON departments(org_id);
|
||||
|
||||
-- 将现有数据关联到科技局(默认机构)
|
||||
UPDATE categories SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
UPDATE document_templates SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'analysis_report_templates') THEN
|
||||
UPDATE analysis_report_templates SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
UPDATE departments SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 回滚:把 super_admin 恢复绑定到第一个机构(仅作占位,无法还原原始关系)
|
||||
UPDATE users SET org_id = (SELECT id FROM organizations ORDER BY sort_order LIMIT 1)
|
||||
WHERE role = 'super_admin' AND org_id IS NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 000015: 平台管理员(super_admin)不绑定机构
|
||||
-- 设计变更:super_admin 是平台级角色,可跨机构操作,因此 org_id 应为 NULL
|
||||
|
||||
-- 把所有 super_admin 用户的 org_id 置空
|
||||
UPDATE users SET org_id = NULL WHERE role = 'super_admin';
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 知识库分片与向量检索支持
|
||||
-- 需要 pgvector 扩展: CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- 知识文档分片表(每篇文档切分为多个 chunk,每个 chunk 存储 embedding 向量)
|
||||
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
doc_id UUID NOT NULL REFERENCES knowledge_documents(id) ON DELETE CASCADE,
|
||||
chunk_index INTEGER NOT NULL DEFAULT 0,
|
||||
content TEXT NOT NULL,
|
||||
char_count INTEGER NOT NULL DEFAULT 0,
|
||||
embedding vector(1024), -- 向量维度 1024(适配 text-embedding-v3)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_kb_id ON knowledge_chunks(kb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_doc_id ON knowledge_chunks(doc_id);
|
||||
|
||||
-- HNSW 向量索引(余弦距离)
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_embedding ON knowledge_chunks
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
|
||||
-- 在 knowledge_documents 表添加 chunk_count 字段
|
||||
ALTER TABLE knowledge_documents ADD COLUMN IF NOT EXISTS chunk_count INTEGER DEFAULT 0;
|
||||
@@ -0,0 +1,180 @@
|
||||
-- 政务AI应用平台 种子数据
|
||||
-- Run after applying all migrations: psql -d govai -f seed.sql
|
||||
|
||||
-- 默认管理员用户 (password: admin123)
|
||||
INSERT INTO users (id, name, email, password_hash, role, status) VALUES
|
||||
('00000000-0000-0000-0000-000000000001', '系统管理员', 'admin@govai.gov.cn',
|
||||
'$2a$10$M1zrf3BCZVQG2zmuqIyQN.rkMTeF5Q7u3f2prayJ3KLSyH.F5G8za',
|
||||
'super_admin', 'active'),
|
||||
('00000000-0000-0000-0000-000000000002', '王科长', 'wangke@govai.gov.cn',
|
||||
'$2a$10$M1zrf3BCZVQG2zmuqIyQN.rkMTeF5Q7u3f2prayJ3KLSyH.F5G8za',
|
||||
'creator', 'active'),
|
||||
('00000000-0000-0000-0000-000000000003', '李干事', 'liganshi@govai.gov.cn',
|
||||
'$2a$10$M1zrf3BCZVQG2zmuqIyQN.rkMTeF5Q7u3f2prayJ3KLSyH.F5G8za',
|
||||
'user', 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Chatbot 对话型应用(3个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
('10000000-0000-0000-0000-000000000001', '政策法规问答', 'policy-qa-bot',
|
||||
'根据政策文件库进行多轮问答,精准解答法规条款和政策要点',
|
||||
'## 功能介绍\n\n政策法规智能问答系统,基于最新政策文件库为您提供精准解答:\n\n- 法律法规条款查询与解读\n- 最新政策变动解析\n- 政策适用范围说明\n- 相关案例参考\n\n## 使用方法\n\n直接输入您的政策法规相关问题即可获得专业解答。',
|
||||
'📜',
|
||||
(SELECT id FROM categories WHERE slug = 'policy-qa' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是政策法规智能问答助手。您可以向我咨询各类法律法规和政策条款,我将为您精准解答。',
|
||||
'["最新的行政处罚法有哪些变化?","营商环境优化相关政策有哪些?","政府信息公开条例的适用范围?"]',
|
||||
'{"system_prompt":"你是一个政策法规智能问答助手,熟悉中国各级政府的法律法规和政策文件。请准确、严谨地回答用户的政策法规相关问题,必要时引用相关法条。回答应正式、专业。"}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000002', '公文写作助手', 'official-doc-writer',
|
||||
'辅助拟稿各类公文,确保格式规范、行文得体',
|
||||
'## 功能介绍\n\n公文写作智能助手,帮助您高效完成各类公文拟稿:\n\n- 通知、通报、报告拟稿\n- 请示、批复格式生成\n- 会议纪要整理\n- 公文格式规范检查\n\n## 使用方法\n\n描述您需要的公文类型和主要内容,助手将按照《党政机关公文格式》标准为您生成。',
|
||||
'📝',
|
||||
(SELECT id FROM categories WHERE slug = 'official-writing' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是公文写作助手。我可以帮您拟稿各类公文,包括通知、报告、请示、批复等,并确保格式符合《党政机关公文格式》标准。请告诉我您需要什么?',
|
||||
'["帮我起草一份工作通知","拟一份关于年度总结的报告","写一份请示文件"]',
|
||||
'{"system_prompt":"你是一个专业的公文写作助手,精通《党政机关公文格式》国家标准(GB/T 9704)。请按照规范格式帮助用户拟稿各类公文。行文应庄重、严谨、准确,符合政务公文写作规范。"}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000003', '群众来信回复', 'public-reply',
|
||||
'辅助处理群众诉求,生成专业规范的回复建议',
|
||||
'## 功能介绍\n\n群众来信回复助手,帮助高效处理各类群众诉求:\n\n- 12345热线来电回复\n- 信访来信处理建议\n- 网上投诉回复生成\n- 政策解释口径统一\n\n## 使用方法\n\n将群众诉求内容发送给我,我将帮您生成专业、规范的回复建议。',
|
||||
'💬',
|
||||
(SELECT id FROM categories WHERE slug = 'public-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是群众来信回复助手。请将群众诉求内容发送给我,我将帮您生成专业、规范的回复建议。',
|
||||
'["这封投诉信应该如何回复?","群众反映道路损坏如何答复?","帮我生成一封信访回复"]',
|
||||
'{"system_prompt":"你是一个政务信访回复助手。请根据群众诉求内容,生成专业、温和、规范的回复建议。回复应体现以人民为中心的服务理念,用词恰当、态度诚恳,引导群众通过正当渠道解决问题。"}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Completion 补全型应用(3个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, app_config) VALUES
|
||||
('10000000-0000-0000-0000-000000000004', '会议纪要生成', 'meeting-minutes',
|
||||
'输入会议记录,一键生成标准格式会议纪要',
|
||||
'## 功能介绍\n\n会议纪要智能生成器,快速整理会议记录:\n\n- 自动提取关键议题和决议\n- 标注责任人和完成时限\n- 输出标准政务会议纪要格式\n- 支持语音转写文字处理\n\n## 使用方法\n\n将会议记录或要点粘贴到输入框,点击生成即可。',
|
||||
'📋',
|
||||
(SELECT id FROM categories WHERE slug = 'official-writing' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个政务会议纪要撰写专家。请根据提供的会议内容,生成标准格式的会议纪要,包含:会议主题、时间地点、主持人、参会人员、议题讨论要点、决议事项、落实责任人和完成时限。使用Markdown格式,行文正式规范。","input_placeholder":"请粘贴会议记录、发言要点或语音转写文字...","input_label":"会议内容","output_label":"会议纪要"}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000005', '公文摘要提取', 'doc-abstract',
|
||||
'对长篇政策文件、调研报告进行智能摘要',
|
||||
'## 功能介绍\n\n公文摘要智能提取,快速掌握文件核心:\n\n- 一句话概要提炼\n- 核心要点提取(3-5条)\n- 关键数据摘录\n- 政策影响概述\n\n## 使用方法\n\n将需要摘要的文件内容粘贴到输入框,点击生成即可。',
|
||||
'📑',
|
||||
(SELECT id FROM categories WHERE slug = 'official-writing' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个政务文件分析专家。请对提供的文件进行分析,输出包含:一句话概要(50字内)、核心要点(3-5条)、关键数据摘录、政策影响分析、行动建议。使用Markdown格式,措辞严谨正式。","input_placeholder":"粘贴需要提取摘要的文件内容...","input_label":"文件内容","output_label":"核心摘要"}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000006', '翻译助手', 'gov-translator',
|
||||
'中英互译,精准处理政务专业术语和外事用语',
|
||||
'## 功能介绍\n\n政务翻译智能助手:\n\n- 中英双向翻译\n- 政务专业术语精准翻译\n- 外事用语规范表达\n- 保持原文正式语调\n\n## 使用方法\n\n输入需要翻译的文本即可获得翻译结果。',
|
||||
'🌐',
|
||||
(SELECT id FROM categories WHERE slug = 'translation' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个精通中英双向翻译的政务翻译专家。请准确翻译用户输入的文本,特别注意政务专业术语的准确性,如\"放管服改革\"→\"streamlining administration, delegating power, and improving regulation and services\"、\"一带一路\"→\"Belt and Road Initiative\"等。保持原文的正式语调。","input_placeholder":"输入需要翻译的文本...","input_label":"原文","output_label":"翻译结果"}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Workflow 工作流型应用(2个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, app_config) VALUES
|
||||
('10000000-0000-0000-0000-000000000007', '招商项目评估', 'investment-eval',
|
||||
'按步骤输入项目信息,多维度评估生成招商分析报告',
|
||||
'## 功能介绍\n\n招商项目智能评估系统,按流程引导完成项目分析:\n\n- **步骤1**:项目基本信息录入\n- **步骤2**:产业匹配度分析\n- **步骤3**:土地和环保要求\n- **步骤4**:选择报告类型\n\n系统将综合评估并生成专业分析报告。',
|
||||
'📊',
|
||||
(SELECT id FROM categories WHERE slug = 'investment' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'workflow', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个招商引资评估专家。请根据用户分步提供的项目信息,从产业匹配度、经济效益、社会效益、环保合规、风险等维度进行综合评估,生成专业的招商项目评估报告。使用Markdown格式,数据和结论要有理有据。","steps":[{"key":"project","label":"项目基本信息","description":"请输入项目名称、投资方、投资金额、所属行业","placeholder":"例如:XX科技产业园项目,投资方:XX集团,计划投资5亿元...","type":"textarea"},{"key":"match","label":"产业匹配度","description":"与本地产业规划的匹配程度","placeholder":"","type":"select","options":["高度匹配 — 属于本地重点发展产业","一般匹配 — 与本地产业有一定关联","需论证 — 属于新兴产业方向"]},{"key":"env","label":"土地和环保要求","description":"项目用地和环保相关信息","placeholder":"例如:需要工业用地200亩,年能耗约XX吨标准煤...","type":"textarea"},{"key":"format","label":"报告类型","description":"选择需要生成的报告类型","placeholder":"","type":"select","options":["初步评估报告","详细分析报告","可行性研究报告"]}]}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000008', '政策影响分析', 'policy-impact',
|
||||
'对拟出台政策进行多维度影响预评估',
|
||||
'## 功能介绍\n\n政策影响预评估系统,帮助决策者全面分析政策影响:\n\n- **步骤1**:政策内容概述\n- **步骤2**:影响群体识别\n- **步骤3**:选择评估维度\n\n系统将生成专业的政策影响分析报告。',
|
||||
'📈',
|
||||
(SELECT id FROM categories WHERE slug = 'policy-qa' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'approved', 'public', 'workflow', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个政策分析专家。请根据用户分步提供的信息,从经济、社会、环境等维度分析政策可能产生的影响,评估政策利弊,提出优化建议。报告应严谨客观、数据支撑、有建设性。使用Markdown格式。","steps":[{"key":"policy","label":"政策内容概述","description":"请描述拟出台政策的主要内容和目标","placeholder":"例如:计划出台关于优化营商环境的若干措施,主要包括简化审批流程、降低企业税费负担...","type":"textarea"},{"key":"target","label":"影响群体","description":"该政策主要影响哪些群体","placeholder":"","type":"select","options":["企业(含中小微企业)","城镇居民","农村居民","特定行业从业者","全社会"]},{"key":"dimension","label":"评估维度","description":"选择重点评估的维度","placeholder":"","type":"select","options":["经济影响评估","社会影响评估","环境影响评估","综合评估(全维度)"]}]}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Agent 智能体型应用(2个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
('10000000-0000-0000-0000-000000000009', '综合研判助手', 'analysis-agent',
|
||||
'多工具联动的智能体,可调用数据检索、趋势分析、报告生成等能力',
|
||||
'## 功能介绍\n\n综合研判智能助手,集成多种分析能力:\n\n- 🔧 **数据检索**:快速检索相关数据和指标\n- 🔧 **趋势分析**:分析经济社会发展趋势\n- 🔧 **对比分析**:横向纵向对比各类指标\n- 🔧 **报告生成**:汇总分析生成研判报告\n\nAgent 将根据您的需求自动选择合适的工具。',
|
||||
'🔍',
|
||||
(SELECT id FROM categories WHERE slug = 'data-governance' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'agent', 'app-placeholder',
|
||||
'您好!我是综合研判智能助手。我具备数据检索、趋势分析、对比分析和报告生成等能力。请描述您的分析需求。',
|
||||
'["分析近三年本地GDP增长趋势","对比各区县经济指标","生成季度经济运行分析报告"]',
|
||||
'{"system_prompt":"你是一个综合研判智能体,服务于政府部门的数据分析和决策支持。你具备以下工具能力:1.数据检索 2.趋势分析 3.对比分析 4.报告生成。在回复中,当你使用某个能力时,请用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。请给出严谨、专业的分析结论。","tools":["数据检索","趋势分析","对比分析","报告生成"]}'),
|
||||
|
||||
('10000000-0000-0000-0000-000000000010', '干部考核助手', 'hr-assessment',
|
||||
'智能化辅助干部年度考核:绩效分析、评语生成、报告汇总',
|
||||
'## 功能介绍\n\n干部考核智能助手,辅助完成干部管理工作:\n\n- 🔧 **绩效分析**:分析工作成果和完成情况\n- 🔧 **评语生成**:根据表现生成考核评语\n- 🔧 **排名建议**:综合评估提供排名参考\n- 🔧 **报告汇总**:生成科室/部门考核汇总报告\n\nAgent 将根据您的需求自动选择合适的工具。',
|
||||
'👤',
|
||||
(SELECT id FROM categories WHERE slug = 'hr-org' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'approved', 'public', 'agent', 'app-placeholder',
|
||||
'您好!我是干部考核智能助手。我可以帮您分析干部绩效、生成考核评语、提供排名建议。请告诉我您需要什么帮助?',
|
||||
'["根据以下工作成果生成干部考核评语","分析这位同志的年度绩效表现","生成科室年度考核汇总报告"]',
|
||||
'{"system_prompt":"你是一个组织人事领域的智能助手,服务于政府部门的干部管理工作。你具备以下工具能力:1.绩效分析 2.评语生成 3.排名建议 4.报告汇总。在回复中,当你使用某个能力时,请用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。评语和报告应客观公正、实事求是。","tools":["绩效分析","评语生成","排名建议","报告汇总"]}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 设置已审核通过的应用发布时间
|
||||
UPDATE applications SET published_at = NOW() WHERE status = 'approved' AND published_at IS NULL;
|
||||
|
||||
-- 添加收藏和评分数据
|
||||
INSERT INTO app_favorites (user_id, app_id) VALUES
|
||||
('00000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001'),
|
||||
('00000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000002')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO app_ratings (user_id, app_id, score, comment) VALUES
|
||||
('00000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001', 5, '政策查询非常方便'),
|
||||
('00000000-0000-0000-0000-000000000002', '10000000-0000-0000-0000-000000000004', 4, '会议纪要生成效率很高'),
|
||||
('00000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000006', 5, '翻译质量很专业')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 更新应用统计
|
||||
UPDATE applications SET avg_rating = sub.avg_score, rating_count = sub.cnt
|
||||
FROM (
|
||||
SELECT app_id, AVG(score)::numeric(3,2) as avg_score, COUNT(*) as cnt
|
||||
FROM app_ratings GROUP BY app_id
|
||||
) sub
|
||||
WHERE applications.id = sub.app_id;
|
||||
|
||||
UPDATE applications SET usage_count = 156 WHERE id = '10000000-0000-0000-0000-000000000001';
|
||||
UPDATE applications SET usage_count = 128 WHERE id = '10000000-0000-0000-0000-000000000002';
|
||||
UPDATE applications SET usage_count = 89 WHERE id = '10000000-0000-0000-0000-000000000003';
|
||||
UPDATE applications SET usage_count = 95 WHERE id = '10000000-0000-0000-0000-000000000004';
|
||||
UPDATE applications SET usage_count = 72 WHERE id = '10000000-0000-0000-0000-000000000005';
|
||||
UPDATE applications SET usage_count = 63 WHERE id = '10000000-0000-0000-0000-000000000006';
|
||||
UPDATE applications SET usage_count = 45 WHERE id = '10000000-0000-0000-0000-000000000007';
|
||||
UPDATE applications SET usage_count = 38 WHERE id = '10000000-0000-0000-0000-000000000008';
|
||||
UPDATE applications SET usage_count = 112 WHERE id = '10000000-0000-0000-0000-000000000009';
|
||||
UPDATE applications SET usage_count = 67 WHERE id = '10000000-0000-0000-0000-000000000010';
|
||||
|
||||
-- 设置精选应用
|
||||
UPDATE applications SET is_featured = true WHERE id IN (
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'10000000-0000-0000-0000-000000000002',
|
||||
'10000000-0000-0000-0000-000000000004',
|
||||
'10000000-0000-0000-0000-000000000009'
|
||||
);
|
||||
|
||||
-- 绑定基础应用和分类到科技局
|
||||
UPDATE applications SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
UPDATE categories SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE org_id IS NULL;
|
||||
|
||||
SELECT '政务AI平台种子数据插入成功!' as status;
|
||||
@@ -0,0 +1,882 @@
|
||||
-- 研判报告模板表
|
||||
CREATE TABLE IF NOT EXISTS analysis_report_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
report_type VARCHAR(50) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
icon VARCHAR(10) DEFAULT '📊',
|
||||
sort_order INT DEFAULT 0,
|
||||
-- steps: 分步引导定义,JSONB 数组
|
||||
-- 每一步: { "step": 1, "title": "xxx", "fields": [...] }
|
||||
-- field: { "key":"xxx", "label":"xxx", "type":"select|multiselect|text|textarea|daterange|number",
|
||||
-- "required": true, "options": [...], "placeholder":"xxx", "default":"xxx" }
|
||||
steps JSONB NOT NULL DEFAULT '[]',
|
||||
-- prompt_template: 最终发送给 LLM 的 prompt,用 {{key}} 做占位
|
||||
prompt_template TEXT NOT NULL DEFAULT '',
|
||||
-- output_sections: 报告输出章节定义(给前端渲染用)
|
||||
output_sections JSONB DEFAULT '[]',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- 清理旧数据
|
||||
DELETE FROM analysis_report_templates;
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 1: 区县经济指标对比分析
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'区县经济指标对比分析',
|
||||
'economic_comparison',
|
||||
'对比分析各区县经济运行核心指标,识别发展差异与增长潜力',
|
||||
'📊',
|
||||
1,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "选择分析指标",
|
||||
"description": "请选择要对比的经济指标(可多选)",
|
||||
"fields": [
|
||||
{
|
||||
"key": "indicators",
|
||||
"label": "经济指标",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "gdp", "label": "GDP总量"},
|
||||
{"value": "gdp_per_capita", "label": "人均GDP"},
|
||||
{"value": "fiscal_revenue", "label": "一般公共预算收入"},
|
||||
{"value": "fixed_investment", "label": "固定资产投资"},
|
||||
{"value": "retail_sales", "label": "社会消费品零售总额"},
|
||||
{"value": "industrial_output", "label": "规上工业增加值"},
|
||||
{"value": "tertiary_ratio", "label": "第三产业占比"},
|
||||
{"value": "import_export", "label": "进出口总额"},
|
||||
{"value": "actual_fdi", "label": "实际利用外资"},
|
||||
{"value": "tech_expenditure", "label": "R&D经费投入"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "确定分析范围",
|
||||
"description": "选择对比区县和时间段",
|
||||
"fields": [
|
||||
{
|
||||
"key": "districts",
|
||||
"label": "区县范围",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "all", "label": "全部区县"},
|
||||
{"value": "chengqu", "label": "城区组(主城区)"},
|
||||
{"value": "jiaoxian", "label": "郊县组"},
|
||||
{"value": "dongcheng", "label": "东城区"},
|
||||
{"value": "xicheng", "label": "西城区"},
|
||||
{"value": "nancheng", "label": "南城区"},
|
||||
{"value": "beicheng", "label": "北城区"},
|
||||
{"value": "gaoxin", "label": "高新区"},
|
||||
{"value": "jingkai", "label": "经开区"},
|
||||
{"value": "qingshan", "label": "青山县"},
|
||||
{"value": "heping", "label": "和平县"},
|
||||
{"value": "longhu", "label": "龙湖县"},
|
||||
{"value": "mingde", "label": "明德县"},
|
||||
{"value": "xinhua", "label": "新华区"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "time_range",
|
||||
"label": "时间范围",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "2025_full", "label": "2025年全年"},
|
||||
{"value": "2025_h1", "label": "2025年上半年"},
|
||||
{"value": "2025_q1", "label": "2025年一季度"},
|
||||
{"value": "2024_full", "label": "2024年全年"},
|
||||
{"value": "2024_2025", "label": "2024-2025年两年对比"},
|
||||
{"value": "2023_2025", "label": "2023-2025年三年趋势"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "comparison_base",
|
||||
"label": "对比基准",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "yoy", "label": "同比增长率"},
|
||||
{"value": "absolute", "label": "绝对值对比"},
|
||||
{"value": "rank", "label": "排名变化"},
|
||||
{"value": "share", "label": "占比分析"}
|
||||
],
|
||||
"default": "yoy"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "分析目标",
|
||||
"description": "明确本次研判的核心目标",
|
||||
"fields": [
|
||||
{
|
||||
"key": "analysis_goal",
|
||||
"label": "分析目标",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "growth_ranking", "label": "增长排名分析"},
|
||||
{"value": "gap_analysis", "label": "差距与短板识别"},
|
||||
{"value": "balance_eval", "label": "区域均衡性评估"},
|
||||
{"value": "potential_mining", "label": "增长潜力挖掘"},
|
||||
{"value": "policy_suggest", "label": "政策建议"},
|
||||
{"value": "risk_warning", "label": "风险预警"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "如有特殊分析需求,请在此补充(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的政务数据分析专家,需要生成一份《区县经济指标对比分析报告》。
|
||||
|
||||
**分析参数:**
|
||||
- 对比指标:{{indicators}}
|
||||
- 区县范围:{{districts}}
|
||||
- 时间范围:{{time_range}}
|
||||
- 对比基准:{{comparison_base}}
|
||||
- 分析目标:{{analysis_goal}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求(严格遵守):**
|
||||
|
||||
请按以下结构输出完整的研判报告,使用Markdown格式:
|
||||
|
||||
# 区县经济指标对比分析报告
|
||||
|
||||
## 一、报告概述
|
||||
简述分析背景、指标选择依据和数据时间范围。
|
||||
|
||||
## 二、核心指标对比
|
||||
用表格形式展示各区县的指标数据对比,包含绝对值和增长率。每个指标一个小节,配排名和简析。
|
||||
|
||||
## 三、综合研判
|
||||
### (一)增长梯队划分
|
||||
将区县分为"领跑型""追赶型""滞后型",说明依据。
|
||||
### (二)差距与短板
|
||||
指出各区县的核心短板,对比全市平均水平。
|
||||
### (三)亮点与经验
|
||||
提炼发展突出的区县经验,总结可推广做法。
|
||||
|
||||
## 四、风险提示
|
||||
识别经济运行中的潜在风险和下行压力。
|
||||
|
||||
## 五、对策建议
|
||||
针对不同梯队区县提出差异化的对策建议。
|
||||
|
||||
## 六、附录
|
||||
关键数据汇总表。
|
||||
|
||||
**注意事项:**
|
||||
1. 报告必须专业、严谨,数据分析有理有据
|
||||
2. 使用模拟但合理的数据(标注"数据为模拟示例")
|
||||
3. 表格使用Markdown格式
|
||||
4. 图表说明用文字描述替代
|
||||
5. 不要使用代码围栏包裹输出内容',
|
||||
'[
|
||||
{"id": "overview", "title": "报告概述"},
|
||||
{"id": "comparison", "title": "核心指标对比"},
|
||||
{"id": "analysis", "title": "综合研判"},
|
||||
{"id": "risks", "title": "风险提示"},
|
||||
{"id": "suggestions", "title": "对策建议"},
|
||||
{"id": "appendix", "title": "附录"}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 2: 经济运行趋势分析
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'经济运行趋势分析',
|
||||
'trend_analysis',
|
||||
'追踪核心经济指标的时间序列变化,研判发展走势与拐点信号',
|
||||
'📈',
|
||||
2,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "选择分析维度",
|
||||
"fields": [
|
||||
{
|
||||
"key": "dimension",
|
||||
"label": "分析维度",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "macro_economy", "label": "宏观经济总体"},
|
||||
{"value": "industrial", "label": "工业经济"},
|
||||
{"value": "consumption", "label": "消费市场"},
|
||||
{"value": "investment", "label": "投资领域"},
|
||||
{"value": "foreign_trade", "label": "外贸进出口"},
|
||||
{"value": "fiscal", "label": "财政收支"},
|
||||
{"value": "real_estate", "label": "房地产市场"},
|
||||
{"value": "employment", "label": "就业形势"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sub_indicators",
|
||||
"label": "细分指标",
|
||||
"type": "multiselect",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "growth_rate", "label": "增速变化"},
|
||||
{"value": "structure", "label": "结构变化"},
|
||||
{"value": "efficiency", "label": "效益指标"},
|
||||
{"value": "leading_index", "label": "先行指标"},
|
||||
{"value": "confidence", "label": "信心指数"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "时间与范围",
|
||||
"fields": [
|
||||
{
|
||||
"key": "time_span",
|
||||
"label": "分析时段",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "quarterly", "label": "近4个季度"},
|
||||
{"value": "monthly_6", "label": "近6个月"},
|
||||
{"value": "monthly_12", "label": "近12个月"},
|
||||
{"value": "yearly_3", "label": "近3年"},
|
||||
{"value": "yearly_5", "label": "近5年"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "region",
|
||||
"label": "分析区域",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "city_total", "label": "全市"},
|
||||
{"value": "urban_area", "label": "城区"},
|
||||
{"value": "suburban", "label": "郊区县"},
|
||||
{"value": "high_tech_zone", "label": "高新区"},
|
||||
{"value": "economic_zone", "label": "经开区"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "研判重点",
|
||||
"fields": [
|
||||
{
|
||||
"key": "focus_points",
|
||||
"label": "关注重点",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "inflection", "label": "拐点识别"},
|
||||
{"value": "forecast", "label": "趋势预测"},
|
||||
{"value": "benchmark", "label": "对标分析(省/全国)"},
|
||||
{"value": "seasonality", "label": "季节性波动"},
|
||||
{"value": "anomaly", "label": "异常值分析"},
|
||||
{"value": "driver", "label": "驱动因素分解"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "补充特殊分析需求(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的经济形势分析专家,需要生成一份《经济运行趋势分析报告》。
|
||||
|
||||
**分析参数:**
|
||||
- 分析维度:{{dimension}}
|
||||
- 细分指标:{{sub_indicators}}
|
||||
- 分析时段:{{time_span}}
|
||||
- 分析区域:{{region}}
|
||||
- 关注重点:{{focus_points}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求(严格遵守):**
|
||||
|
||||
# 经济运行趋势分析报告
|
||||
|
||||
## 一、总体概况
|
||||
概述当前经济运行态势和主要特征。
|
||||
|
||||
## 二、核心指标走势
|
||||
按月度/季度展示关键指标变化趋势(使用表格),分析拐点和转折。
|
||||
|
||||
## 三、趋势研判
|
||||
### (一)短期走势判断
|
||||
基于先行指标和高频数据的近期预判。
|
||||
### (二)中期趋势展望
|
||||
综合分析中期发展态势。
|
||||
### (三)驱动因素分析
|
||||
识别推动或制约经济的主要因素。
|
||||
|
||||
## 四、对标分析
|
||||
与省级、全国水平进行对比分析。
|
||||
|
||||
## 五、风险与机遇
|
||||
### (一)下行风险
|
||||
### (二)发展机遇
|
||||
|
||||
## 六、政策建议
|
||||
提出有针对性的政策建议。
|
||||
|
||||
**注意:使用模拟但合理的数据,标注"数据为模拟示例"。不要使用代码围栏包裹输出。**',
|
||||
'[]'::jsonb
|
||||
);
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 3: 产业结构分析
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'产业结构分析',
|
||||
'industry_analysis',
|
||||
'分析区域产业结构布局、优势产业链和产业升级路径',
|
||||
'🏭',
|
||||
3,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "分析类型",
|
||||
"fields": [
|
||||
{
|
||||
"key": "analysis_type",
|
||||
"label": "分析类型",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "structure_overview", "label": "产业结构总览(三次产业)"},
|
||||
{"value": "pillar_industry", "label": "支柱产业分析"},
|
||||
{"value": "emerging_industry", "label": "战略性新兴产业"},
|
||||
{"value": "chain_analysis", "label": "重点产业链分析"},
|
||||
{"value": "cluster_eval", "label": "产业集群评估"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "target_industries",
|
||||
"label": "重点产业",
|
||||
"type": "multiselect",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "electronics", "label": "电子信息"},
|
||||
{"value": "biomedical", "label": "生物医药"},
|
||||
{"value": "new_material", "label": "新材料"},
|
||||
{"value": "new_energy", "label": "新能源"},
|
||||
{"value": "intelligent_mfg", "label": "智能制造"},
|
||||
{"value": "modern_service", "label": "现代服务业"},
|
||||
{"value": "digital_economy", "label": "数字经济"},
|
||||
{"value": "green_lowcarbon", "label": "绿色低碳"},
|
||||
{"value": "cultural_tourism", "label": "文化旅游"},
|
||||
{"value": "modern_agriculture", "label": "现代农业"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "分析范围",
|
||||
"fields": [
|
||||
{
|
||||
"key": "region_scope",
|
||||
"label": "区域范围",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "city_wide", "label": "全市"},
|
||||
{"value": "specific_district", "label": "特定区县"},
|
||||
{"value": "development_zone", "label": "开发区/园区"},
|
||||
{"value": "cross_region", "label": "跨区域协同"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "data_period",
|
||||
"label": "数据时段",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "latest_year", "label": "最近一年"},
|
||||
{"value": "three_year", "label": "近三年变化"},
|
||||
{"value": "five_year", "label": "五年规划期"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "研判重点",
|
||||
"fields": [
|
||||
{
|
||||
"key": "report_focus",
|
||||
"label": "报告侧重",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "current_status", "label": "产业现状评估"},
|
||||
{"value": "competitive_advantage", "label": "竞争优势分析"},
|
||||
{"value": "weak_link", "label": "薄弱环节识别"},
|
||||
{"value": "upgrade_path", "label": "产业升级路径"},
|
||||
{"value": "investment_direction", "label": "招商引资方向"},
|
||||
{"value": "talent_demand", "label": "人才需求分析"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "补充特殊分析需求(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的产业经济分析专家,需要生成一份《产业结构分析报告》。
|
||||
|
||||
**分析参数:**
|
||||
- 分析类型:{{analysis_type}}
|
||||
- 重点产业:{{target_industries}}
|
||||
- 区域范围:{{region_scope}}
|
||||
- 数据时段:{{data_period}}
|
||||
- 报告侧重:{{report_focus}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求:**
|
||||
|
||||
# 产业结构分析报告
|
||||
|
||||
## 一、产业发展概况
|
||||
当前产业结构总体特征和发展水平概述。
|
||||
|
||||
## 二、产业结构分析
|
||||
### (一)三次产业结构
|
||||
用表格展示三次产业占比及变化趋势。
|
||||
### (二)重点产业分析
|
||||
逐个分析选定的重点产业发展状况。
|
||||
|
||||
## 三、竞争力评估
|
||||
### (一)优势产业
|
||||
### (二)短板领域
|
||||
### (三)对标先进地区
|
||||
|
||||
## 四、发展机遇与挑战
|
||||
|
||||
## 五、对策建议
|
||||
### (一)产业升级路径
|
||||
### (二)招商引资方向
|
||||
### (三)政策扶持建议
|
||||
|
||||
**注意:使用模拟但合理的数据,标注"数据为模拟示例"。不要使用代码围栏包裹输出。**',
|
||||
'[]'::jsonb
|
||||
);
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 4: 民生保障评估
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'民生保障评估',
|
||||
'livelihood_assessment',
|
||||
'评估教育、医疗、就业、社保等民生指标,分析保障水平和改善方向',
|
||||
'🏥',
|
||||
4,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "评估领域",
|
||||
"fields": [
|
||||
{
|
||||
"key": "domains",
|
||||
"label": "民生领域",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "education", "label": "教育"},
|
||||
{"value": "healthcare", "label": "医疗卫生"},
|
||||
{"value": "employment", "label": "就业创业"},
|
||||
{"value": "social_security", "label": "社会保障"},
|
||||
{"value": "housing", "label": "住房保障"},
|
||||
{"value": "elderly_care", "label": "养老服务"},
|
||||
{"value": "environment", "label": "生态环境"},
|
||||
{"value": "public_safety", "label": "公共安全"},
|
||||
{"value": "culture_sports", "label": "文化体育"},
|
||||
{"value": "transportation", "label": "交通出行"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "评估范围",
|
||||
"fields": [
|
||||
{
|
||||
"key": "eval_scope",
|
||||
"label": "评估范围",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "city_wide", "label": "全市综合"},
|
||||
{"value": "urban_rural", "label": "城乡对比"},
|
||||
{"value": "district_comparison", "label": "区县对比"},
|
||||
{"value": "year_comparison", "label": "年度对比"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "eval_year",
|
||||
"label": "评估年份",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "2025", "label": "2025年"},
|
||||
{"value": "2024", "label": "2024年"},
|
||||
{"value": "2024_2025", "label": "2024-2025年"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "分析重点",
|
||||
"fields": [
|
||||
{
|
||||
"key": "focus",
|
||||
"label": "重点关注",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "satisfaction", "label": "群众满意度"},
|
||||
{"value": "equity", "label": "公平性分析"},
|
||||
{"value": "coverage", "label": "覆盖率评估"},
|
||||
{"value": "quality", "label": "服务质量"},
|
||||
{"value": "investment", "label": "财政投入效益"},
|
||||
{"value": "benchmark", "label": "对标先进城市"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "补充特殊分析需求(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的社会治理分析专家,需要生成一份《民生保障评估报告》。
|
||||
|
||||
**分析参数:**
|
||||
- 评估领域:{{domains}}
|
||||
- 评估范围:{{eval_scope}}
|
||||
- 评估年份:{{eval_year}}
|
||||
- 重点关注:{{focus}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求:**
|
||||
|
||||
# 民生保障评估报告
|
||||
|
||||
## 一、总体评估
|
||||
民生保障整体水平概述和评分。
|
||||
|
||||
## 二、分领域评估
|
||||
按选定领域逐项分析,每项包含:关键指标表、现状评估、问题识别。
|
||||
|
||||
## 三、城乡/区域差异分析
|
||||
不同区域间的民生保障水平差异。
|
||||
|
||||
## 四、群众满意度分析
|
||||
居民对各项民生服务的满意度评价。
|
||||
|
||||
## 五、问题与不足
|
||||
突出的民生短板和亟需改善的领域。
|
||||
|
||||
## 六、改进建议
|
||||
按优先级排列的民生改善措施。
|
||||
|
||||
**注意:使用模拟但合理的数据,标注"数据为模拟示例"。不要使用代码围栏包裹输出。**',
|
||||
'[]'::jsonb
|
||||
);
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 5: 营商环境评估
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'营商环境评估',
|
||||
'business_environment',
|
||||
'评估区域营商环境各维度表现,对标先进地区识别优化方向',
|
||||
'🏢',
|
||||
5,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "评估维度",
|
||||
"fields": [
|
||||
{
|
||||
"key": "eval_dimensions",
|
||||
"label": "评估维度",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "market_entry", "label": "市场准入"},
|
||||
{"value": "approval_efficiency", "label": "审批效率"},
|
||||
{"value": "tax_environment", "label": "税费环境"},
|
||||
{"value": "financing", "label": "融资便利"},
|
||||
{"value": "legal_protection", "label": "法治保障"},
|
||||
{"value": "talent_attraction", "label": "人才吸引"},
|
||||
{"value": "infrastructure", "label": "基础设施"},
|
||||
{"value": "gov_service", "label": "政务服务"},
|
||||
{"value": "innovation_ecology", "label": "创新生态"},
|
||||
{"value": "international", "label": "国际化水平"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "对标和范围",
|
||||
"fields": [
|
||||
{
|
||||
"key": "benchmark_targets",
|
||||
"label": "对标城市",
|
||||
"type": "multiselect",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "national_best", "label": "全国标杆城市"},
|
||||
{"value": "provincial_top", "label": "省内先进城市"},
|
||||
{"value": "peer_cities", "label": "同类型城市"},
|
||||
{"value": "historical", "label": "本市历史水平"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "enterprise_type",
|
||||
"label": "重点企业类型",
|
||||
"type": "multiselect",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "private", "label": "民营企业"},
|
||||
{"value": "foreign", "label": "外资企业"},
|
||||
{"value": "sme", "label": "中小微企业"},
|
||||
{"value": "startup", "label": "初创企业"},
|
||||
{"value": "high_tech", "label": "高新技术企业"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "分析目标",
|
||||
"fields": [
|
||||
{
|
||||
"key": "report_goal",
|
||||
"label": "报告目标",
|
||||
"type": "multiselect",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "status_eval", "label": "现状评估"},
|
||||
{"value": "gap_analysis", "label": "差距分析"},
|
||||
{"value": "reform_path", "label": "改革路径"},
|
||||
{"value": "best_practice", "label": "先进经验借鉴"},
|
||||
{"value": "action_plan", "label": "行动计划"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "补充特殊分析需求(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的营商环境评估专家,需要生成一份《营商环境评估报告》。
|
||||
|
||||
**分析参数:**
|
||||
- 评估维度:{{eval_dimensions}}
|
||||
- 对标城市:{{benchmark_targets}}
|
||||
- 重点企业类型:{{enterprise_type}}
|
||||
- 报告目标:{{report_goal}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求:**
|
||||
|
||||
# 营商环境评估报告
|
||||
|
||||
## 一、总体评价
|
||||
营商环境综合得分和全国/省内排名。
|
||||
|
||||
## 二、分维度评估
|
||||
用雷达图思路逐维度打分评价(用表格+文字说明替代图表)。
|
||||
|
||||
## 三、对标分析
|
||||
与标杆城市的逐维度对比,找出差距。
|
||||
|
||||
## 四、企业反馈
|
||||
不同类型企业的痛点和诉求分析。
|
||||
|
||||
## 五、优化建议
|
||||
按短期/中期/长期分层提出改革举措。
|
||||
|
||||
## 六、行动路线图
|
||||
按季度排列的重点任务和里程碑。
|
||||
|
||||
**注意:使用模拟但合理的数据,标注"数据为模拟示例"。不要使用代码围栏包裹输出。**',
|
||||
'[]'::jsonb
|
||||
);
|
||||
|
||||
-- ================================================================
|
||||
-- 模板 6: 专题调研报告
|
||||
-- ================================================================
|
||||
INSERT INTO analysis_report_templates (name, report_type, description, icon, sort_order, steps, prompt_template, output_sections)
|
||||
VALUES (
|
||||
'专题调研报告',
|
||||
'special_research',
|
||||
'围绕特定议题开展深度调研,形成专题分析报告',
|
||||
'🔍',
|
||||
6,
|
||||
'[
|
||||
{
|
||||
"step": 1,
|
||||
"title": "调研主题",
|
||||
"fields": [
|
||||
{
|
||||
"key": "topic_category",
|
||||
"label": "主题分类",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "economy", "label": "经济发展"},
|
||||
{"value": "tech_innovation", "label": "科技创新"},
|
||||
{"value": "urban_governance", "label": "城市治理"},
|
||||
{"value": "rural_revitalization", "label": "乡村振兴"},
|
||||
{"value": "digital_transformation", "label": "数字化转型"},
|
||||
{"value": "green_development", "label": "绿色发展"},
|
||||
{"value": "social_governance", "label": "社会治理"},
|
||||
{"value": "cultural_development", "label": "文化建设"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "topic_title",
|
||||
"label": "调研题目",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "例如:数字经济赋能传统制造业转型升级的路径研究"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"title": "调研要素",
|
||||
"fields": [
|
||||
{
|
||||
"key": "background",
|
||||
"label": "调研背景",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"placeholder": "简述调研的背景和必要性"
|
||||
},
|
||||
{
|
||||
"key": "key_questions",
|
||||
"label": "核心问题",
|
||||
"type": "textarea",
|
||||
"required": true,
|
||||
"placeholder": "列出本次调研要回答的核心问题(一行一个)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"title": "报告要求",
|
||||
"fields": [
|
||||
{
|
||||
"key": "report_depth",
|
||||
"label": "报告深度",
|
||||
"type": "select",
|
||||
"required": true,
|
||||
"options": [
|
||||
{"value": "brief", "label": "简明版(2000字)"},
|
||||
{"value": "standard", "label": "标准版(5000字)"},
|
||||
{"value": "detailed", "label": "详细版(8000字以上)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "content_requirement",
|
||||
"label": "内容要求",
|
||||
"type": "multiselect",
|
||||
"required": false,
|
||||
"options": [
|
||||
{"value": "data_analysis", "label": "数据分析"},
|
||||
{"value": "case_study", "label": "案例研究"},
|
||||
{"value": "policy_review", "label": "政策梳理"},
|
||||
{"value": "expert_opinion", "label": "专家观点"},
|
||||
{"value": "international_comparison", "label": "国际比较"},
|
||||
{"value": "implementation_plan", "label": "实施方案"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "extra_requirement",
|
||||
"label": "补充说明",
|
||||
"type": "textarea",
|
||||
"required": false,
|
||||
"placeholder": "其他特殊要求(可选)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]'::jsonb,
|
||||
'你是一位资深的政策研究专家,需要生成一份专题调研报告。
|
||||
|
||||
**调研参数:**
|
||||
- 主题分类:{{topic_category}}
|
||||
- 调研题目:{{topic_title}}
|
||||
- 调研背景:{{background}}
|
||||
- 核心问题:{{key_questions}}
|
||||
- 报告深度:{{report_depth}}
|
||||
- 内容要求:{{content_requirement}}
|
||||
- 补充要求:{{extra_requirement}}
|
||||
|
||||
**报告格式要求:**
|
||||
|
||||
# {{topic_title}}
|
||||
|
||||
## 一、调研背景与目的
|
||||
阐述调研的背景、目的和意义。
|
||||
|
||||
## 二、现状分析
|
||||
深入分析当前状况,用数据说话。
|
||||
|
||||
## 三、问题与挑战
|
||||
识别存在的问题和面临的挑战。
|
||||
|
||||
## 四、经验借鉴
|
||||
国内外先进经验和典型案例分析。
|
||||
|
||||
## 五、对策建议
|
||||
系统性的解决方案和政策建议。
|
||||
|
||||
## 六、实施路径
|
||||
分阶段的实施计划和保障措施。
|
||||
|
||||
**注意:内容要专业深入,数据使用模拟但合理的数据(标注"数据为模拟示例")。不要使用代码围栏包裹输出。**',
|
||||
'[]'::jsonb
|
||||
);
|
||||
@@ -0,0 +1,358 @@
|
||||
-- 北京航空航天大学 教师AI应用中心 种子数据
|
||||
-- 默认密码 admin123(张兴娟密码 123456),可重复执行(ON CONFLICT)
|
||||
-- 执行方式:psql -d govai -f seed_beihang.sql
|
||||
|
||||
-- ========== 1. 机构 ==========
|
||||
INSERT INTO organizations (id, name, slug, short_name, description, sort_order) VALUES
|
||||
('a0000000-0000-0000-0000-000000000100', '北京航空航天大学', 'beihang', '北航',
|
||||
'双一流A类高校,航空航天特色鲜明的综合性研究型大学', 20)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 2. 分类(教育场景,绑定北航org_id) ==========
|
||||
INSERT INTO categories (name, slug, icon, description, sort_order, org_id) VALUES
|
||||
('教学辅助', 'edu-teaching', 'graduation-cap', '课程设计、教案编写、考试命题', 101, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('科研助手', 'edu-research', 'microscope', '文献综述、实验方案、数据分析', 102, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('论文写作', 'edu-paper', 'pen-tool', '论文润色、摘要生成、学术翻译', 103, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('项目申报', 'edu-grant', 'file-badge', '基金申请书、结题报告、预算编制', 104, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('学生指导', 'edu-mentoring', 'users', '论文评审、开题建议、答辩准备', 105, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('行政办公', 'edu-admin', 'file-text', '公文写作、会议纪要、通知公告', 106, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('学科建设', 'edu-discipline','building-2', '课程评估、学科规划、专业建设', 107, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('国际交流', 'edu-intl', 'globe', '学术翻译、会议通知、合作协议', 108, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('数据分析', 'edu-data', 'bar-chart-3', '教学数据、科研成果、绩效统计', 109, 'a0000000-0000-0000-0000-000000000100'),
|
||||
('智慧工具', 'edu-tools', 'cpu', '代码辅助、公式推导、技术文档', 110, 'a0000000-0000-0000-0000-000000000100')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- ========== 3. 用户 ==========
|
||||
DO $$
|
||||
DECLARE
|
||||
pwd_hash TEXT;
|
||||
BEGIN
|
||||
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
('b0000000-0000-0000-0000-000000000001', '李院长', 'liyuanzhang@buaa.edu.cn', pwd_hash, 'admin', 'active', 'a0000000-0000-0000-0000-000000000100'),
|
||||
('b0000000-0000-0000-0000-000000000002', '王教授', 'wangjiaoshou@buaa.edu.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000100'),
|
||||
('b0000000-0000-0000-0000-000000000003', '张副教授', 'zhangfujiao@buaa.edu.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000100'),
|
||||
('b0000000-0000-0000-0000-000000000004', '刘讲师', 'liujiangshi@buaa.edu.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000100'),
|
||||
('b0000000-0000-0000-0000-000000000005', '赵助教', 'zhaozhujiao@buaa.edu.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000100'),
|
||||
('b0000000-0000-0000-0000-000000000006', '张兴娟', 'zhangxingjuan@buaa.com', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000100')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END $$;
|
||||
|
||||
-- ========== 4. 应用 ==========
|
||||
|
||||
-- ---- 4.1 教学辅助(4个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 智能教案生成(补全型)
|
||||
('c0000000-0000-0000-0000-000000000101', '智能教案生成', 'beihang-lesson-plan',
|
||||
'输入课程名称和教学目标,AI 生成规范教案',
|
||||
'## 功能介绍\n\n智能教案生成器,帮助教师高效编写教学设计:\n\n- 根据OBE理念设计教学目标\n- 自动规划教学过程和时间分配\n- 生成重难点分析和教学方法建议\n- 输出标准教案格式文档\n\n## 使用方法\n\n输入课程名称、章节内容、学时安排等信息,点击生成即可。',
|
||||
'📚',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-teaching' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是北京航空航天大学的资深教学设计专家,精通OBE(成果导向教育)理念。请根据教师提供的课程信息,生成规范的教案设计,包含:\n\n1. **课程基本信息**(课程名称、授课对象、学时)\n2. **教学目标**(知识目标、能力目标、素质目标,对应毕业要求指标点)\n3. **教学重点与难点**\n4. **教学方法**(讲授法、案例教学、翻转课堂等)\n5. **教学过程设计**(导入→新知讲授→课堂互动→总结→作业布置,含时间分配)\n6. **板书/PPT设计要点**\n7. **课后反思**(留空供教师填写)\n\n使用Markdown格式,内容应体现北航\"空天报国\"的育人理念。","input_placeholder":"请输入课程名称、章节内容、授课对象、学时等信息...\n例如:\n课程:飞行力学\n章节:第三章 飞机纵向稳定性\n授课对象:航空科学与工程学院大三学生\n学时:2学时","input_label":"课程信息","output_label":"教案设计"}'),
|
||||
|
||||
-- 考试命题助手(工作流)
|
||||
('c0000000-0000-0000-0000-000000000102', '考试命题助手', 'beihang-exam-gen',
|
||||
'分步输入课程、章节、题型分布,自动生成试卷',
|
||||
'## 功能介绍\n\n考试命题智能助手,分步引导生成规范试卷:\n\n- **步骤1**:选择课程和考试类型\n- **步骤2**:设定题型分布和分值\n- **步骤3**:补充考核要求\n\n系统将自动生成包含参考答案和评分标准的试卷。',
|
||||
'📝',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-teaching' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'workflow', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是北京航空航天大学的考试命题专家。请根据教师提供的课程信息、题型分布和考核要求,生成一份完整试卷,包含:\n\n1. 试卷头(课程名称、考试时间、满分)\n2. 各题型试题(含题号、分值标注)\n3. 参考答案及评分标准\n\n试题应覆盖指定章节的核心知识点,难度分布合理(基础40%、中等40%、综合20%)。使用Markdown格式输出。","steps":[{"key":"course","label":"课程信息","description":"请输入课程名称、考试类型和涉及章节","placeholder":"例如:数据结构与算法,期末考试,涵盖第1-8章","type":"textarea"},{"key":"format","label":"题型分布","description":"选择试卷的题型组合","type":"select","options":["选择题30分+填空题20分+简答题30分+综合题20分","选择题20分+判断题10分+简答题30分+计算题40分","名词解释20分+简答题40分+论述题40分","编程题40分+算法设计题30分+分析题30分"]},{"key":"requirement","label":"补充要求","description":"特殊考核要求、难度偏好或重点章节","placeholder":"例如:重点考核第5-7章内容,增加应用型题目比例,难度适中偏上","type":"textarea"}]}'),
|
||||
|
||||
-- 课程设计咨询(对话型)
|
||||
('c0000000-0000-0000-0000-000000000103', '课程设计咨询', 'beihang-course-design',
|
||||
'围绕 OBE 理念,辅助设计课程体系和教学大纲',
|
||||
'## 功能介绍\n\n课程设计智能顾问,基于OBE理念提供全方位教学设计支持:\n\n- 课程目标与毕业要求映射\n- 教学大纲编写指导\n- 考核方案设计建议\n- 课程思政融入建议\n\n## 使用方法\n\n直接描述您的课程设计需求,AI将提供专业建议。',
|
||||
'🎓',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-teaching' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是课程设计咨询助手。我可以帮助您围绕OBE理念设计课程体系、编写教学大纲、设计考核方案。请告诉我您的需求。',
|
||||
'["如何将课程思政融入专业课教学?","帮我设计一门课程的达成度评价方案","如何设计翻转课堂的教学方案?"]',
|
||||
'{"system_prompt":"你是北京航空航天大学教务处的课程设计顾问,精通OBE(成果导向教育)理念、工程教育认证标准和课程思政要求。你的职责包括:\n\n1. 指导教师进行课程目标与毕业要求的映射设计\n2. 协助编写规范的教学大纲\n3. 设计多元化考核评价方案(形成性评价+终结性评价)\n4. 建议课程思政融入点\n5. 推荐先进教学方法(翻转课堂、PBL、案例教学等)\n\n回答应专业、具体、可操作,引用教育部相关文件和北航教学管理规定。"}'),
|
||||
|
||||
-- 教学评估报告(工作流)
|
||||
('c0000000-0000-0000-0000-000000000104', '教学评估报告', 'beihang-teaching-eval',
|
||||
'输入教学数据,生成教学质量分析报告',
|
||||
'## 功能介绍\n\n教学评估智能分析,基于数据生成质量报告:\n\n- **步骤1**:输入成绩分布数据\n- **步骤2**:输入评教和反馈信息\n- **步骤3**:选择分析维度\n\n自动生成包含问题诊断和改进建议的评估报告。',
|
||||
'📊',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-teaching' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'workflow', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是北京航空航天大学教学质量评估专家。请根据教师提供的教学数据,生成教学质量分析报告,包含:\n\n1. **数据概览**(成绩分布、合格率、优秀率统计)\n2. **成绩分析**(正态分布检验、区分度分析)\n3. **教学目标达成度分析**\n4. **学生反馈分析**\n5. **问题诊断**\n6. **改进建议**(教学方法、考核方式、课程内容调整建议)\n7. **下学期教学改进计划**\n\n使用Markdown格式,数据分析要有理有据。","steps":[{"key":"grades","label":"成绩数据","description":"请输入本学期成绩分布情况","placeholder":"例如:\n课程:数据结构\n选课人数:120人\n成绩分布:90-100分 15人,80-89分 35人,70-79分 40人,60-69分 20人,不及格 10人\n平均分:76.5","type":"textarea"},{"key":"feedback","label":"教学反馈","description":"学生评教结果和教学观察","placeholder":"例如:\n评教分数:4.3/5.0\n学生反馈:课程内容偏难,实验课时不足\n同行听课意见:教学方法较传统","type":"textarea"},{"key":"focus","label":"分析重点","description":"选择重点分析维度","type":"select","options":["综合分析报告(全维度)","成绩分布与教学目标达成度","教学方法改进建议","课程内容优化方案"]}]}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.2 科研助手(3个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 文献综述生成(补全型)
|
||||
('c0000000-0000-0000-0000-000000000105', '文献综述生成', 'beihang-lit-review',
|
||||
'输入研究主题,生成结构化文献综述框架和要点',
|
||||
'## 功能介绍\n\n学术文献综述助手:\n\n- 根据研究主题梳理文献脉络\n- 生成综述结构框架\n- 总结研究现状与不足\n- 提炼研究趋势和展望',
|
||||
'📖',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-research' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位资深学术研究者,擅长撰写高水平文献综述。请根据用户提供的研究主题,生成结构化的文献综述框架,包含:\n\n1. **研究背景与意义**\n2. **国内外研究现状**(分主题梳理,按时间线或方法论分类)\n3. **现有研究的不足与局限**\n4. **研究趋势与未来方向**\n5. **本研究的切入点建议**\n\n综述应体现学术严谨性,合理引用经典和前沿文献(可使用占位符标注需补充的具体引用),适合用于SCI/EI论文写作。","input_placeholder":"请输入您的研究主题和方向...\n例如:\n研究主题:基于深度强化学习的无人机自主避障\n研究方向:航空航天 + 人工智能\n关注重点:近5年进展、仿真与实飞差异","input_label":"研究主题","output_label":"文献综述框架"}'),
|
||||
|
||||
-- 实验方案设计(对话型)
|
||||
('c0000000-0000-0000-0000-000000000106', '实验方案设计', 'beihang-experiment',
|
||||
'围绕研究课题,辅助设计实验方案和数据采集计划',
|
||||
'## 功能介绍\n\n实验方案设计助手,帮助科研人员设计严谨的实验:\n\n- 实验变量控制设计\n- 样本量和统计方法推荐\n- 数据采集方案规划\n- 实验安全和伦理审查提醒',
|
||||
'🔬',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-research' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是实验方案设计助手。我可以帮助您设计实验方案,包括变量控制、数据采集计划和统计分析方法。请描述您的研究课题。',
|
||||
'["帮我设计一个风洞实验方案","如何设计对照实验验证算法性能?","推荐适合我研究的统计分析方法"]',
|
||||
'{"system_prompt":"你是一位经验丰富的科研实验设计专家,熟悉工科和理科各领域的实验方法学。你的职责包括:\n\n1. 协助设计实验方案(实验目的、假设、变量控制)\n2. 推荐合适的实验设计类型(单因素、正交设计、响应面法等)\n3. 计算合理的样本量和重复次数\n4. 建议数据采集方案和测量方法\n5. 推荐统计分析方法\n6. 提醒实验安全注意事项和伦理审查要求\n\n回答应严谨、具体、可操作。针对航空航天类实验(如风洞、振动、材料测试),提供专业指导。"}'),
|
||||
|
||||
-- 科研数据分析(智能体)
|
||||
('c0000000-0000-0000-0000-000000000107', '科研数据分析', 'beihang-data-analysis',
|
||||
'描述数据特征,推荐分析方法并生成代码框架',
|
||||
'## 功能介绍\n\n科研数据分析智能助手:\n\n- 根据数据描述推荐分析方法\n- 生成 Python/MATLAB 分析代码框架\n- 提供数据可视化建议\n- 辅助结果解读和论文表述',
|
||||
'📈',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-research' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'agent', 'app-placeholder',
|
||||
'您好!我是科研数据分析助手。请描述您的数据特征和分析目标,我将推荐合适的方法并帮您生成分析代码。',
|
||||
'["我有一组风洞实验数据需要分析","帮我做回归分析并画拟合图","如何检验两组实验数据的显著差异?"]',
|
||||
'{"system_prompt":"你是一位科研数据分析专家,精通统计学和数据科学。你可以使用以下工具:\n\n- Python数据分析(pandas、numpy、scipy、scikit-learn)\n- MATLAB代码生成\n- 数据可视化(matplotlib、seaborn)\n- 统计检验(t检验、ANOVA、卡方检验、非参数检验)\n\n你的职责:\n1. 根据数据描述推荐合适的分析方法\n2. 生成可直接运行的分析代码\n3. 解读分析结果的统计学意义\n4. 建议论文中的数据呈现方式(表格、图表格式)\n5. 提供结果描述的学术写法\n\n代码应包含充分的注释,便于教师和研究生理解。","tools":["Python分析","MATLAB代码","数据可视化","统计检验"]}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.3 论文写作(3个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 论文润色助手(补全型)
|
||||
('c0000000-0000-0000-0000-000000000108', '论文润色助手', 'beihang-paper-polish',
|
||||
'中英文学术论文润色、语法修正、表达优化',
|
||||
'## 功能介绍\n\n学术论文润色助手:\n\n- 语法和拼写修正\n- 学术表达优化\n- 专业术语规范化\n- 保持原意精准润色',
|
||||
'✍️',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-paper' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位资深学术论文润色专家,精通中英文学术写作规范。请对用户提交的论文文本进行专业润色:\n\n1. 修正语法、拼写和标点错误\n2. 优化学术表达(使句式更加学术化、正式化)\n3. 规范专业术语使用\n4. 改善段落逻辑和过渡\n5. 标注修改位置并说明修改原因\n\n输出格式:先给出润色后的全文,再附上修改说明列表。保持原文核心意思不变。","input_placeholder":"请粘贴需要润色的论文段落...\n支持中文和英文论文文本","input_label":"待润色文本","output_label":"润色结果"}'),
|
||||
|
||||
-- 摘要与关键词(补全型)
|
||||
('c0000000-0000-0000-0000-000000000109', '摘要与关键词生成', 'beihang-abstract-gen',
|
||||
'根据论文内容生成中英文摘要和关键词',
|
||||
'## 功能介绍\n\n论文摘要智能生成:\n\n- 中文摘要(200-300字)\n- 英文摘要(150-250 words)\n- 中英文关键词提取(3-5个)\n- 符合期刊投稿规范',
|
||||
'📄',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-paper' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位学术论文写作专家。请根据用户提供的论文内容(可以是全文、章节摘要或研究概述),生成:\n\n## 中文摘要(200-300字)\n包含:研究背景(1-2句)→ 研究目的 → 研究方法 → 主要结果 → 结论意义\n\n## English Abstract (150-250 words)\nStructure: Background → Objective → Methods → Results → Conclusions\n\n## 关键词\n- 中文关键词(3-5个,用分号分隔)\n- Keywords(3-5个,用分号分隔)\n\n摘要应简洁准确、信息完整,符合SCI/EI期刊投稿规范。","input_placeholder":"请粘贴论文全文、各章摘要、或研究概述...","input_label":"论文内容","output_label":"摘要与关键词"}'),
|
||||
|
||||
-- 学术翻译(补全型)
|
||||
('c0000000-0000-0000-0000-000000000110', '学术翻译', 'beihang-academic-trans',
|
||||
'学术论文中英互译,精准处理专业术语',
|
||||
'## 功能介绍\n\n学术论文翻译助手:\n\n- 中英文双向翻译\n- 航空航天等专业术语精准翻译\n- 保持学术论文的正式语调\n- 支持数学公式和变量保留',
|
||||
'🌐',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-intl' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位精通中英双向学术翻译的专家,尤其擅长航空航天、计算机、材料科学、数学等领域的专业翻译。请准确翻译用户提供的学术文本:\n\n1. 保持学术论文的正式语调和逻辑结构\n2. 精准翻译专业术语(如「气动弹性」→ aeroelasticity,「有限元」→ finite element)\n3. 保留数学公式和变量符号不翻译\n4. 对于专业术语首次出现时提供中英对照\n5. 保持段落结构和引用格式\n\n如果输入是中文则翻译为英文,如果输入是英文则翻译为中文。","input_placeholder":"请输入需要翻译的学术文本...","input_label":"原文","output_label":"翻译结果"}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.4 项目申报(3个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 基金申请书助手(对话型)
|
||||
('c0000000-0000-0000-0000-000000000111', '基金申请书助手', 'beihang-grant-writer',
|
||||
'辅助撰写国自然等基金申请书核心内容',
|
||||
'## 功能介绍\n\n基金申请书写作助手:\n\n- 立项依据与研究意义撰写\n- 研究内容与技术路线设计\n- 创新点提炼\n- 预期成果规划',
|
||||
'📋',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-grant' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是基金申请书写作助手。我可以帮助您撰写国自然、省部级基金等申请书的核心内容。请告诉我您的研究方向和申报基金类型。',
|
||||
'["帮我写国自然面上项目的立项依据","如何提炼创新点?","请优化我的技术路线描述"]',
|
||||
'{"system_prompt":"你是一位资深科研基金申报专家,成功指导过多个国家自然科学基金、科技部重点研发计划等项目的申报。你的职责:\n\n1. 辅助撰写立项依据(国内外研究现状→存在的科学问题→本项目的研究意义)\n2. 协助设计研究内容和技术路线\n3. 提炼研究创新点(理论创新、方法创新、应用创新)\n4. 规划预期成果(论文、专利、原型系统等)\n5. 优化申请书的学术表述和逻辑结构\n\n写作应符合国自然申请书的格式规范,语言要简洁有力、逻辑清晰、创新性突出。注意避免夸大和空泛的表述。"}'),
|
||||
|
||||
-- 项目结题报告(工作流)
|
||||
('c0000000-0000-0000-0000-000000000112', '项目结题报告', 'beihang-grant-final',
|
||||
'分步输入项目信息和成果,生成结题报告',
|
||||
'## 功能介绍\n\n项目结题报告生成器:\n\n- **步骤1**:项目基本信息\n- **步骤2**:研究成果总结\n- **步骤3**:经费使用概况\n\n自动生成规范的结题验收报告。',
|
||||
'📑',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-grant' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'workflow', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位科研项目管理专家。请根据项目信息生成规范的结题验收报告,包含:\n\n1. **项目概况**\n2. **研究工作总结**(完成的研究内容与原计划对比)\n3. **主要研究成果**(论文、专利、软件著作权、获奖等量化成果)\n4. **经费使用情况**\n5. **人才培养情况**(培养研究生、博士后等)\n6. **研究成果的理论意义和应用价值**\n7. **存在的问题与后续研究展望**\n\n报告应客观、翔实,数据准确。","steps":[{"key":"project","label":"项目基本信息","description":"请输入项目名称、类别、批准号、执行时间等","placeholder":"例如:\n项目名称:基于XX的XX研究\n项目类别:国家自然科学基金面上项目\n批准号:62XXXXXXX\n执行时间:2023.01-2025.12\n负责人:XX教授","type":"textarea"},{"key":"results","label":"研究成果","description":"请列出主要研究成果","placeholder":"例如:\n发表SCI论文5篇(其中1区3篇)\n授权发明专利2项\n培养博士研究生2名、硕士研究生4名\n获省部级科技奖1项","type":"textarea"},{"key":"budget","label":"经费使用","description":"请概述经费使用情况","placeholder":"例如:\n批准经费:60万元\n已执行:58.5万元\n设备费:15万 | 材料费:10万 | 差旅费:8万 | 劳务费:20万 | 间接费用:5.5万","type":"textarea"}]}'),
|
||||
|
||||
-- 预算编制助手(补全型)
|
||||
('c0000000-0000-0000-0000-000000000113', '预算编制助手', 'beihang-budget-plan',
|
||||
'根据项目规模辅助编制科研经费预算',
|
||||
'## 功能介绍\n\n科研经费预算编制助手:\n\n- 根据项目规模自动分配预算比例\n- 符合国自然/科技部等经费管理办法\n- 生成预算说明书\n- 提供预算合理性论证',
|
||||
'💰',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-grant' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位科研经费预算编制专家,熟悉国家自然科学基金、科技部重点研发计划等各类项目的经费管理办法。请根据项目信息生成:\n\n1. **经费预算总表**(设备费、材料费、测试化验加工费、差旅费、会议费、劳务费、专家咨询费、间接费用)\n2. **各科目预算明细**(具体用途和金额)\n3. **预算说明**(各科目预算理由和计算依据)\n4. **合理性论证**\n\n预算分配应符合最新的《国家自然科学基金资助项目资金管理办法》,间接费用比例不超过规定上限。","input_placeholder":"请输入项目信息...\n例如:\n项目类型:国自然面上项目\n申请经费:60万元\n执行期:4年\n研究内容:计算流体力学模拟+风洞实验验证\n团队:教授1人、副教授1人、博士生3人、硕士生4人","input_label":"项目信息","output_label":"预算编制方案"}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.5 学生指导(2个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 论文评审意见(补全型)
|
||||
('c0000000-0000-0000-0000-000000000114', '论文评审意见', 'beihang-paper-review',
|
||||
'输入学生论文内容,生成结构化评审意见和修改建议',
|
||||
'## 功能介绍\n\n论文评审意见生成器:\n\n- 结构化评审(摘要、引言、方法、结果、结论)\n- 逐节提出具体修改建议\n- 指出创新点和不足\n- 生成综合评价和修改优先级',
|
||||
'📝',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-mentoring' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位严谨的学术论文评审专家。请对学生提交的论文内容进行专业评审,生成结构化的评审意见:\n\n## 总体评价\n简要概括论文的研究价值、创新性和完成度(优秀/良好/合格/需大改)\n\n## 分节评审\n### 摘要\n### 引言/文献综述\n### 研究方法\n### 实验/结果\n### 讨论与结论\n\n对每一节:指出优点 → 存在问题 → 具体修改建议\n\n## 修改建议清单\n按重要性排序(必须修改 / 建议修改 / 可选改进)\n\n## 综合评审意见\n\n评审应建设性、具体、有据可依,帮助学生提高论文质量。","input_placeholder":"请粘贴学生论文全文或关键章节...","input_label":"论文内容","output_label":"评审意见"}'),
|
||||
|
||||
-- 开题报告辅助(对话型)
|
||||
('c0000000-0000-0000-0000-000000000115', '开题报告辅助', 'beihang-proposal-guide',
|
||||
'辅助指导研究生开题报告撰写和答辩准备',
|
||||
'## 功能介绍\n\n研究生开题报告指导助手:\n\n- 选题论证和可行性分析\n- 文献综述梳理建议\n- 研究方案设计指导\n- 时间进度规划\n- 答辩常见问题准备',
|
||||
'🎯',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-mentoring' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是开题报告指导助手。我可以帮助您的研究生完善选题论证、文献综述、研究方案设计。请描述研究方向或具体问题。',
|
||||
'["如何论证这个选题的研究价值?","帮我梳理开题报告的文献综述思路","设计一个合理的研究时间进度表"]',
|
||||
'{"system_prompt":"你是北京航空航天大学的研究生导师,擅长指导硕士和博士研究生撰写开题报告。你的职责:\n\n1. 协助选题论证(研究意义、国内外现状、研究问题提出)\n2. 指导文献综述撰写(梳理逻辑、找出研究空白)\n3. 帮助设计研究方案(研究目标→内容→方法→技术路线)\n4. 规划研究进度安排\n5. 准备开题答辩(预设评委可能的问题和回答策略)\n\n指导应具体、可操作,符合北航研究生培养要求。对于航空航天、计算机等北航优势学科,提供更专业的建议。"}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.6 行政办公(2个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 高校公文写作(对话型)
|
||||
('c0000000-0000-0000-0000-000000000116', '高校公文写作', 'beihang-official-doc',
|
||||
'各类高校行政公文拟稿,格式规范、行文得体',
|
||||
'## 功能介绍\n\n高校公文写作助手:\n\n- 通知、通报、请示、报告拟稿\n- 会议纪要整理\n- 学术会议通知\n- 校内规章制度草拟',
|
||||
'📄',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-admin' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是高校公文写作助手。我可以帮您拟稿各类行政公文和学术通知。请告诉我您需要什么?',
|
||||
'["起草一份学术会议通知","拟一份教学改革工作总结","写一份设备采购申请"]',
|
||||
'{"system_prompt":"你是一位高校行政公文写作专家,熟悉高等院校的公文格式和行文规范。请帮助用户撰写各类公文:\n\n- 行政通知(教学安排、工作部署、活动通知)\n- 请示报告(经费申请、设备采购、人员安排)\n- 会议纪要(学术委员会、教学工作会、党政联席会)\n- 学术通知(学术报告、答辩公告、学术会议通知)\n- 规章制度(管理办法、实施细则)\n\n公文应符合高校行文规范,语言正式得体,格式标准。署名使用「北京航空航天大学」或相应院系部门名称。"}'),
|
||||
|
||||
-- 会议纪要生成(补全型)
|
||||
('c0000000-0000-0000-0000-000000000117', '会议纪要生成', 'beihang-meeting-minutes',
|
||||
'输入会议要点,生成规范的会议纪要',
|
||||
'## 功能介绍\n\n会议纪要智能生成:\n\n- 提取关键议题和决议\n- 标注责任人和时限\n- 输出规范纪要格式',
|
||||
'📋',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-admin' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位高校行政秘书,擅长整理会议纪要。请根据提供的会议内容,生成标准格式的会议纪要,包含:\n\n1. **会议名称**\n2. **时间地点**\n3. **主持人和参会人员**\n4. **会议议题**\n5. **讨论要点和决议事项**(每项标注责任人和完成时限)\n6. **下一步工作安排**\n\n使用Markdown格式,行文正式规范。","input_placeholder":"请输入会议记录或发言要点...\n例如:\n会议:计算机学院2026年春季学期教学工作会\n时间:2026年5月20日下午2:00\n地点:新主楼A座会议室\n主持:李院长\n参会:各教研室主任、教学秘书\n\n议题1:期末考试安排...\n议题2:暑期实习计划...","input_label":"会议记录","output_label":"会议纪要"}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---- 4.7 学科建设 + PPT + 代码辅助(3个) ----
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
|
||||
-- 学科规划报告(工作流)
|
||||
('c0000000-0000-0000-0000-000000000118', '学科规划报告', 'beihang-discipline-plan',
|
||||
'分步输入学科现状和发展目标,生成学科建设规划',
|
||||
'## 功能介绍\n\n学科建设规划生成器:\n\n- **步骤1**:学科基本情况\n- **步骤2**:师资与科研现状\n- **步骤3**:发展目标与建设方向\n\n自动生成学科建设规划报告。',
|
||||
'🏛️',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-discipline' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'workflow', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位高等教育学科建设专家,熟悉教育部学科评估指标体系和双一流建设要求。请根据输入信息生成学科建设规划报告,包含:\n\n1. **学科现状分析**(SWOT分析)\n2. **建设目标**(短期1-2年、中期3-5年)\n3. **重点建设任务**(师资队伍、科研平台、人才培养、国际合作)\n4. **保障措施**\n5. **预期成效与评估指标**\n\n报告应体现北航\"空天报国\"特色,对标世界一流学科建设标准。","steps":[{"key":"basic","label":"学科基本情况","description":"请输入学科名称、层级、历史沿革等","placeholder":"例如:\n学科名称:航空宇航科学与技术\n学科层级:一级学科博士点\n第五轮评估结果:A+\n教育部重点学科:是","type":"textarea"},{"key":"status","label":"师资与科研现状","description":"请介绍当前师资队伍和科研成果","placeholder":"例如:\n专任教师:85人(教授35人、副教授30人、讲师20人)\n院士2人、杰青5人、优青8人\n近5年科研经费:3.2亿元\n近5年SCI论文:520篇","type":"textarea"},{"key":"goal","label":"发展目标","description":"选择建设方向","type":"select","options":["冲击世界一流学科前列","巩固国内一流、提升国际影响力","补短板、强特色、争突破","新兴交叉学科培育"]}]}'),
|
||||
|
||||
-- 智能PPT生成
|
||||
('c0000000-0000-0000-0000-000000000119', '学术PPT生成', 'beihang-ppt-gen',
|
||||
'输入论文或报告内容,AI 生成学术演示 PPT',
|
||||
'## 功能介绍\n\n学术演示PPT智能生成:\n\n- 自动提取论文核心内容\n- 生成结构清晰的演示文稿\n- 适合学术报告、答辩、组会',
|
||||
'📊',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-tools' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'completion', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位学术演示设计专家。请根据用户提供的论文或研究内容,生成PPT大纲和各页内容:\n\n1. **封面**(标题、作者、单位、日期)\n2. **目录**\n3. **研究背景与意义**(2-3页)\n4. **文献综述/相关工作**(2-3页)\n5. **研究方法**(3-4页)\n6. **实验结果与分析**(3-5页)\n7. **结论与展望**(1-2页)\n8. **参考文献**\n9. **致谢**\n\n每页应包含:标题、要点列表(3-5项)、备注(演讲口述内容)。单位署名:北京航空航天大学。","input_placeholder":"请粘贴论文全文或研究概要...","input_label":"论文/报告内容","output_label":"PPT 大纲与内容"}'),
|
||||
|
||||
-- 代码辅助(对话型)
|
||||
('c0000000-0000-0000-0000-000000000120', '代码辅助', 'beihang-code-helper',
|
||||
'编程问答、算法设计、代码审查',
|
||||
'## 功能介绍\n\n编程与算法辅助助手:\n\n- 编程问题解答\n- 算法设计与优化\n- 代码审查与重构建议\n- MATLAB/Python/C++ 等多语言支持',
|
||||
'💻',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-tools' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是代码辅助助手。我可以帮助您解决编程问题、设计算法、审查代码。支持 Python、MATLAB、C/C++、Java 等多种语言。',
|
||||
'["帮我优化这段MATLAB代码","用Python实现快速傅里叶变换","这段C++代码有什么问题?"]',
|
||||
'{"system_prompt":"你是一位资深编程和算法专家,精通多种编程语言和工具:\n\n- **Python**:科学计算、深度学习、数据分析\n- **MATLAB/Simulink**:数值模拟、控制系统设计\n- **C/C++**:嵌入式系统、高性能计算\n- **LaTeX**:学术论文排版\n\n你的职责:\n1. 解答编程问题,提供可运行的代码示例\n2. 设计和优化算法(附时间/空间复杂度分析)\n3. 审查代码(发现bug、提出优化建议)\n4. 辅助调试(分析错误信息、提供解决方案)\n\n代码示例应包含充分注释,适合教学和科研使用。"}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 研究生组会助手(工作流型 - 3步生成组会纪要)
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
('c0000000-0000-0000-0000-000000000121', '研究生组会助手', 'beihang-group-meeting',
|
||||
'分步输入参会人员、汇报内容、讨论要点,自动生成组会纪要和待办事项',
|
||||
E'## 功能介绍\n\n研究生组会(例会)智能助手,通过分步引导高效生成会议纪要:\n\n- **步骤1 - 会议信息**:输入日期、参会学生名单\n- **步骤2 - 学生汇报**:逐一录入每位学生的研究进展和问题\n- **步骤3 - 讨论与决议**:补充讨论要点和导师指导意见\n\n系统自动生成:\n- 📄 完整的组会纪要\n- ✅ 待办事项清单(含责任人和截止日期)\n- 📊 学生进展追踪表',
|
||||
'📋',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-mentoring' LIMIT 1),
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'approved', 'public', 'workflow', 'app-placeholder', NULL, NULL,
|
||||
'{"system_prompt":"你是一位高校导师的组会纪要生成助手。根据用户分步输入的会议信息,生成结构化的组会纪要。\n\n## 输出格式\n\n# 研究生组会纪要\n\n## 基本信息\n- 时间:{用户输入}\n- 地点:{用户输入}\n- 参会人员:{用户输入}\n\n## 学生汇报\n### [学生姓名] - [研究方向]\n- **本周进展**:\n- **遇到的问题**:\n- **导师指导意见**:\n- **下周计划**:\n\n## 讨论议题与决议\n1. ...\n\n## 行动事项(Action Items)\n| 序号 | 事项 | 责任人 | 截止日期 | 备注 |\n|------|------|--------|----------|------|\n| 1 | ... | ... | ... | ... |\n\n## 下次会议安排\n- 时间:\n- 重点关注:\n\n---\n*本纪要由AI辅助生成,请导师审核确认。*\n\n## 要求\n- 语言正式简洁\n- 重点突出\n- 行动事项必须明确责任人和截止日期\n- 关注学生研究的连续性","steps":[{"key":"meeting_info","label":"会议基本信息","description":"请填写组会日期、地点和参会学生名单","type":"textarea","placeholder":"示例:\\n日期:2026年5月23日 14:00-16:00\\n地点:新主楼B820会议室\\n参会人员:\\n- 王明(博三,飞行控制方向)\\n- 李华(博二,导航算法方向)\\n- 张敏(硕二,图像识别方向)\\n- 赵强(硕一,数据融合方向)"},{"key":"student_reports","label":"学生汇报内容","description":"请输入每位学生的本周进展、遇到的问题和下周计划","type":"textarea","placeholder":"示例:\\n【王明】\\n进展:完成了自适应控制器仿真,性能提升12%\\n问题:实物实验平台调试遇到通信延迟\\n计划:排查延迟原因,准备中期答辩\\n\\n【李华】\\n进展:改进了SLAM算法,定位精度达到厘米级\\n问题:论文初稿写作进度较慢\\n计划:完成论文第三章,提交导师审阅"},{"key":"discussion","label":"讨论要点与导师指导","description":"请补充讨论议题、导师指导意见和其他决议事项","type":"textarea","placeholder":"示例:\\n1. 王明的实验方案需要增加对比实验组\\n2. 李华论文需要11月底前完成投稿\\n3. 张敏数据集标注工作安排本科生协助\\n4. 下周组会改为周四,准备实验室检查材料"}]}')
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 5. 科研教学项目申请(workflow,4步) ==========
|
||||
INSERT INTO applications (id, slug, name, description, long_description, category_id, org_id, creator_id,
|
||||
status, visibility, dify_app_type, dify_app_id, welcome_message, suggested_prompts, app_config)
|
||||
VALUES (
|
||||
'b1000000-0000-0000-0000-000000000122',
|
||||
'beihang-project-apply',
|
||||
'科研教学项目申请',
|
||||
'分步引导撰写国自然、教改、省部级等科研教学项目申请书核心内容',
|
||||
E'## 功能介绍\n\n科研教学项目申请助手,面向高校教师的项目申报全流程辅助工具。\n\n### 支持项目类型\n- **国家自然科学基金**:面上项目、青年基金、重点项目\n- **教育部教改项目**:新工科/新文科建设、教学改革\n- **省部级科研项目**:省自然科学基金、产学研合作\n- **校级项目**:校级教改、科研启动基金\n\n### 工作流程\n1. **项目基本信息** — 选择项目类型,填写项目名称和申请人信息\n2. **研究背景与立项依据** — 输入研究领域现状和关键问题\n3. **研究方案与技术路线** — 描述研究目标、方法和创新点\n4. **预期成果与经费预算** — 规划成果产出和经费分配',
|
||||
(SELECT id FROM categories WHERE slug = 'edu-research' LIMIT 1),
|
||||
'a0000000-0000-0000-0000-000000000100',
|
||||
'b0000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'workflow', 'app-placeholder',
|
||||
'欢迎使用科研教学项目申请助手!我将分4个步骤引导您完成项目申请书的核心内容撰写。请按步骤填写信息,系统会自动生成规范的申请书内容。',
|
||||
'["帮我写一个国自然面上项目申请", "教改项目如何写立项依据?", "如何提炼项目创新点?", "省基金项目申请书怎么写?"]',
|
||||
'{"system_prompt":"你是一位资深的科研项目申报专家,熟悉国家自然科学基金、教育部教改项目、省部级科研项目等各类项目的申报要求和评审标准。根据用户分步输入的项目信息,生成结构化、规范化的项目申请书核心内容。\n\n## 输出格式\n\n# [项目类型] 项目申请书\n\n## 一、项目基本信息\n- 项目名称:\n- 项目类型:\n- 申请人:\n- 所属学科:\n- 研究期限:\n\n## 二、立项依据与研究意义\n### 2.1 研究背景\n### 2.2 国内外研究现状\n### 2.3 研究意义\n### 2.4 主要参考文献(格式建议)\n\n## 三、研究内容与方案\n### 3.1 研究目标\n### 3.2 研究内容\n### 3.3 拟解决的关键科学问题\n### 3.4 技术路线\n### 3.5 可行性分析\n\n## 四、创新点\n\n## 五、预期成果\n### 5.1 学术成果\n### 5.2 人才培养\n### 5.3 社会效益\n\n## 六、经费预算概要\n\n## 七、研究基础与工作条件\n\n---\n*本申请书由AI辅助生成,请根据具体项目要求修改完善。*\n\n## 要求\n- 学术表述规范、逻辑严谨\n- 立项依据要有充分的文献支撑\n- 创新点要明确、可验证\n- 技术路线要具体、可行\n- 经费预算要合理、符合规定","steps":[{"key":"project_info","label":"项目基本信息","description":"请选择项目类型,并填写项目名称、申请人信息和研究方向","type":"textarea","placeholder":"示例:\\n项目类型:国家自然科学基金面上项目\\n项目名称:基于深度学习的无人机自主导航关键技术研究\\n申请人:李院长,教授,博导\\n所属学科:航空宇航科学与技术\\n研究方向:飞行控制与导航\\n研究期限:2027年1月-2030年12月(4年)\\n申请经费:58万元"},{"key":"background","label":"研究背景与立项依据","description":"请描述研究领域现状、存在的关键问题和研究意义","type":"textarea","placeholder":"示例:\\n研究领域:无人机自主导航与控制\\n\\n现状:\\n- 当前无人机导航主要依赖GPS+惯导组合\\n- 深度学习在视觉导航领域取得突破\\n\\n关键问题:\\n1. 复杂动态环境下的实时感知与决策融合\\n2. 小样本条件下的迁移学习能力\\n\\n研究意义:\\n- 推动视觉导航与强化学习的交叉融合\\n- 服务于低空经济、应急救援等国家战略需求"},{"key":"methodology","label":"研究方案与技术路线","description":"请描述研究目标、研究方法、技术路线和创新点","type":"textarea","placeholder":"示例:\\n研究目标:\\n- 构建无人机自主导航系统\\n\\n研究方法:\\n1. 多模态感知融合\\n2. 层次化强化学习\\n\\n创新点:\\n1. 注意力引导的多模态特征融合网络\\n2. 课程式强化学习训练策略\\n\\n技术路线:\\n理论分析→算法设计→仿真验证→实物实验→系统集成"},{"key":"outcomes","label":"预期成果与经费预算","description":"请描述预期研究成果、人才培养计划和经费预算概况","type":"textarea","placeholder":"示例:\\n预期成果:\\n- 发表SCI论文4-6篇\\n- 申请发明专利2-3项\\n- 培养博士生2名、硕士生4名\\n\\n经费预算(共58万):\\n- 设备费:15万\\n- 差旅费:8万\\n- 劳务费:18万\\n\\n前期基础:\\n- 已发表相关SCI论文10篇"}]}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 6. 推荐应用(4个) ==========
|
||||
UPDATE applications SET is_featured = true WHERE slug IN (
|
||||
'beihang-lesson-plan',
|
||||
'beihang-lit-review',
|
||||
'beihang-grant-writer',
|
||||
'beihang-paper-polish'
|
||||
) AND org_id = 'a0000000-0000-0000-0000-000000000100';
|
||||
@@ -0,0 +1,443 @@
|
||||
-- 科技局公文模板种子数据
|
||||
-- 依据《党政机关公文处理工作条例》和《党政机关公文格式》GB/T 9704
|
||||
|
||||
DELETE FROM document_templates;
|
||||
|
||||
-- 1. 通知(最常用)
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('通知', 'notice', '用于发布规章制度、转发文件、部署工作、告知事项等', 'FileText', 1,
|
||||
'[
|
||||
{"key":"urgency","label":"紧急程度","type":"select","options":["普通","加急","特急"],"required":false,"default":"普通"},
|
||||
{"key":"secret_level","label":"密级","type":"select","options":["公开","内部","秘密"],"required":false,"default":"公开"},
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局","placeholder":"如:XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科发〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的通知"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各有关单位:"},
|
||||
{"key":"purpose","label":"通知目的/背景","type":"textarea","required":true,"placeholder":"简述发文背景、原因和目的"},
|
||||
{"key":"main_content","label":"主要内容要点","type":"textarea","required":true,"placeholder":"列出通知的核心内容要点(可多条)"},
|
||||
{"key":"requirements","label":"工作要求","type":"textarea","required":false,"placeholder":"对落实通知的具体要求"},
|
||||
{"key":"deadline","label":"截止日期","type":"text","required":false,"placeholder":"如:2026年6月30日前"},
|
||||
{"key":"contact","label":"联系人及电话","type":"text","required":false,"placeholder":"联系人:张三,电话:010-12345678"},
|
||||
{"key":"cc_orgs","label":"抄送机关","type":"text","required":false,"placeholder":"市政府办公室、市财政局"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家,精通《党政机关公文格式》国标(GB/T 9704)。请严格按以下规范生成【通知】公文:
|
||||
|
||||
## 格式规范
|
||||
- 发文机关标志居中排布,使用红色小标宋体
|
||||
- 发文字号居中排布于发文机关标志下方
|
||||
- 标题使用2号小标宋体,居中排布,回行时词意完整
|
||||
- 主送机关顶格,后加冒号
|
||||
- 正文使用3号仿宋体,首行缩进2字符
|
||||
- 结构层次依次为:一、(一)1. (1)
|
||||
- 成文日期右空四字,用阿拉伯数字
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 通知目的/背景:{{purpose}}
|
||||
- 主要内容要点:{{main_content}}
|
||||
- 工作要求:{{requirements}}
|
||||
- 截止日期:{{deadline}}
|
||||
- 联系人:{{contact}}
|
||||
- 抄送:{{cc_orgs}}
|
||||
- 成文日期:{{sign_date}}
|
||||
- 紧急程度:{{urgency}}
|
||||
- 密级:{{secret_level}}
|
||||
|
||||
请生成完整的通知公文,确保行文庄重、严谨、准确,符合政务公文写作规范。');
|
||||
|
||||
-- 2. 请示
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('请示', 'request', '向上级机关请求指示或批准事项', 'HelpCircle', 2,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科呈〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的请示"},
|
||||
{"key":"superior_org","label":"主送机关(上级)","type":"text","required":true,"placeholder":"如:市人民政府"},
|
||||
{"key":"background","label":"请示背景/原因","type":"textarea","required":true,"placeholder":"详细说明请示事项的背景和原因"},
|
||||
{"key":"request_matter","label":"请示事项","type":"textarea","required":true,"placeholder":"具体的请示内容和方案"},
|
||||
{"key":"budget","label":"经费预算","type":"text","required":false,"placeholder":"如涉及经费,请说明金额和来源"},
|
||||
{"key":"basis","label":"政策依据","type":"textarea","required":false,"placeholder":"引用相关政策文件依据"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【请示】公文:
|
||||
|
||||
## 请示公文规范
|
||||
- 一文一事,主送一个上级机关
|
||||
- 结尾用"妥否,请批示"或"以上请示,请予审批"
|
||||
- 不得同时抄送下级机关
|
||||
- 标题格式:《关于XX的请示》
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{superior_org}}
|
||||
- 背景/原因:{{background}}
|
||||
- 请示事项:{{request_matter}}
|
||||
- 经费预算:{{budget}}
|
||||
- 政策依据:{{basis}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的请示公文。');
|
||||
|
||||
-- 3. 报告
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('报告', 'report', '向上级机关汇报工作、反映情况、提出建议', 'BarChart3', 3,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科报〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX工作情况的报告"},
|
||||
{"key":"superior_org","label":"主送机关","type":"text","required":true,"placeholder":"如:市人民政府"},
|
||||
{"key":"report_type","label":"报告类型","type":"select","options":["工作报告","情况报告","答复报告","调研报告"],"required":true},
|
||||
{"key":"period","label":"汇报时间段","type":"text","required":false,"placeholder":"如:2025年度、2026年第一季度"},
|
||||
{"key":"main_work","label":"主要工作和成效","type":"textarea","required":true,"placeholder":"列出主要工作内容和取得的成效"},
|
||||
{"key":"data_highlights","label":"数据亮点","type":"textarea","required":false,"placeholder":"关键数据和指标(如有)"},
|
||||
{"key":"problems","label":"存在问题","type":"textarea","required":false,"placeholder":"当前工作中存在的困难和问题"},
|
||||
{"key":"next_plan","label":"下步工作计划","type":"textarea","required":false,"placeholder":"下一阶段工作安排"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【报告】公文:
|
||||
|
||||
## 报告公文规范
|
||||
- 报告不得夹带请示事项
|
||||
- 结尾用"特此报告"
|
||||
- 内容要实事求是,有数据支撑
|
||||
- 结构清晰:背景→主要工作→成效→问题→下步计划
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{superior_org}}
|
||||
- 报告类型:{{report_type}}
|
||||
- 汇报时间段:{{period}}
|
||||
- 主要工作和成效:{{main_work}}
|
||||
- 数据亮点:{{data_highlights}}
|
||||
- 存在问题:{{problems}}
|
||||
- 下步计划:{{next_plan}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的报告公文。');
|
||||
|
||||
-- 4. 批复
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('批复', 'reply', '答复下级机关的请示事项', 'CheckCircle', 4,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科复〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的批复"},
|
||||
{"key":"recipient_org","label":"主送机关(下级)","type":"text","required":true,"placeholder":"请示的来文单位"},
|
||||
{"key":"ref_doc","label":"来文引述","type":"text","required":true,"placeholder":"如:你单位《关于XX的请示》(X科呈〔2026〕X号)收悉"},
|
||||
{"key":"approval_result","label":"批复意见","type":"select","options":["同意","原则同意","不同意","部分同意"],"required":true},
|
||||
{"key":"approval_detail","label":"批复具体内容","type":"textarea","required":true,"placeholder":"批复的具体意见和要求"},
|
||||
{"key":"conditions","label":"附加条件","type":"textarea","required":false,"placeholder":"批复附带的条件或要求"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【批复】公文:
|
||||
|
||||
## 批复公文规范
|
||||
- 开头引述来文(文号+事由)
|
||||
- 明确表态:同意/不同意/原则同意
|
||||
- 批复意见要明确具体
|
||||
- 一文一批,主送一个下级机关
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipient_org}}
|
||||
- 来文引述:{{ref_doc}}
|
||||
- 批复意见:{{approval_result}}
|
||||
- 具体内容:{{approval_detail}}
|
||||
- 附加条件:{{conditions}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的批复公文。');
|
||||
|
||||
-- 5. 函
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('函', 'letter', '不相隶属机关之间商洽工作、询问和答复问题、请求批准和答复审批事项', 'Mail', 5,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科函〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的函"},
|
||||
{"key":"recipient_org","label":"主送机关","type":"text","required":true,"placeholder":"对方机关全称"},
|
||||
{"key":"letter_type","label":"函的类型","type":"select","options":["商洽函","询问函","答复函","请求批准函","告知函","催办函"],"required":true},
|
||||
{"key":"content","label":"函的内容","type":"textarea","required":true,"placeholder":"具体商洽、询问或答复的内容"},
|
||||
{"key":"ref_doc","label":"引述来文","type":"text","required":false,"placeholder":"如是答复函,引述对方来文"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【函】公文:
|
||||
|
||||
## 函的规范
|
||||
- 函用于不相隶属机关之间
|
||||
- 语气平和、礼貌,体现平等协商
|
||||
- 商洽函结尾:"请予函复"或"即请函复"
|
||||
- 答复函开头引述来函
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipient_org}}
|
||||
- 函的类型:{{letter_type}}
|
||||
- 内容:{{content}}
|
||||
- 引述来文:{{ref_doc}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的函公文。');
|
||||
|
||||
-- 6. 通报
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('通报', 'circular', '表彰先进、批评错误、传达重要精神或情况', 'AlertCircle', 6,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科通〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的通报"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各有关单位:"},
|
||||
{"key":"circular_type","label":"通报类型","type":"select","options":["表彰通报","批评通报","情况通报"],"required":true},
|
||||
{"key":"facts","label":"事实情况","type":"textarea","required":true,"placeholder":"通报的具体事实和经过"},
|
||||
{"key":"analysis","label":"分析评价","type":"textarea","required":false,"placeholder":"对事实的分析评价"},
|
||||
{"key":"decision","label":"处理决定/要求","type":"textarea","required":true,"placeholder":"通报的处理决定或工作要求"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【通报】公文:
|
||||
|
||||
## 通报规范
|
||||
- 表彰通报:事实→评价→表彰决定→号召学习
|
||||
- 批评通报:事实→分析原因→处理决定→要求汲取教训
|
||||
- 情况通报:说明情况→分析原因→工作要求
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 通报类型:{{circular_type}}
|
||||
- 事实情况:{{facts}}
|
||||
- 分析评价:{{analysis}}
|
||||
- 处理决定/要求:{{decision}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的通报公文。');
|
||||
|
||||
-- 7. 意见
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('意见', 'opinion', '对重要问题提出见解和处理办法', 'Lightbulb', 7,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科发〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的意见"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各有关单位:"},
|
||||
{"key":"background","label":"背景和目的","type":"textarea","required":true,"placeholder":"出台意见的背景、目的和意义"},
|
||||
{"key":"guiding_principles","label":"指导思想/基本原则","type":"textarea","required":false,"placeholder":"指导思想和基本原则"},
|
||||
{"key":"main_opinions","label":"主要意见措施","type":"textarea","required":true,"placeholder":"具体意见和措施(建议分条列出)"},
|
||||
{"key":"guarantee","label":"保障措施","type":"textarea","required":false,"placeholder":"组织保障、资金保障等"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【意见】公文:
|
||||
|
||||
## 意见规范
|
||||
- 结构:背景目的→指导思想→主要措施→保障措施
|
||||
- 措施要具体、可操作
|
||||
- 语气坚定但不武断
|
||||
|
||||
## 用户提供的信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 背景和目的:{{background}}
|
||||
- 指导思想/基本原则:{{guiding_principles}}
|
||||
- 主要意见措施:{{main_opinions}}
|
||||
- 保障措施:{{guarantee}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的意见公文。');
|
||||
|
||||
-- 8. 决定
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('决定', 'decision', '对重要事项作出决策和部署', 'Gavel', 8,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科决〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的决定"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各有关单位:"},
|
||||
{"key":"decision_type","label":"决定类型","type":"select","options":["部署性决定","表彰性决定","处分性决定","变更性决定"],"required":true},
|
||||
{"key":"background","label":"决定背景/依据","type":"textarea","required":true,"placeholder":"作出决定的背景和依据"},
|
||||
{"key":"decision_content","label":"决定事项","type":"textarea","required":true,"placeholder":"具体的决定内容"},
|
||||
{"key":"requirements","label":"执行要求","type":"textarea","required":false,"placeholder":"执行决定的具体要求"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【决定】公文:
|
||||
|
||||
## 决定规范
|
||||
- 语气庄重、权威
|
||||
- 决定事项要明确具体
|
||||
- 有执行要求和监督措施
|
||||
|
||||
## 用户信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 决定类型:{{decision_type}}
|
||||
- 背景/依据:{{background}}
|
||||
- 决定事项:{{decision_content}}
|
||||
- 执行要求:{{requirements}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的决定公文。');
|
||||
|
||||
-- 9. 会议纪要
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('会议纪要', 'meeting_minutes', '记载会议主要情况和议定事项', 'BookOpen', 9,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"纪要编号","type":"text","required":true,"placeholder":"如:X科纪〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"XX会议纪要"},
|
||||
{"key":"meeting_time","label":"会议时间","type":"text","required":true,"placeholder":"2026年X月X日"},
|
||||
{"key":"meeting_place","label":"会议地点","type":"text","required":true,"placeholder":"局X楼会议室"},
|
||||
{"key":"host","label":"主持人","type":"text","required":true,"placeholder":"局长XXX"},
|
||||
{"key":"attendees","label":"出席人员","type":"textarea","required":true,"placeholder":"列出出席人员"},
|
||||
{"key":"agenda","label":"会议议题","type":"textarea","required":true,"placeholder":"会议讨论的主要议题"},
|
||||
{"key":"decisions","label":"议定事项","type":"textarea","required":true,"placeholder":"会议决定的事项(分条列出)"},
|
||||
{"key":"tasks","label":"任务分工","type":"textarea","required":false,"placeholder":"具体任务和责任人"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按《党政机关公文格式》生成【会议纪要】公文:
|
||||
|
||||
## 会议纪要规范
|
||||
- 标题:XX会议纪要
|
||||
- 编号用"〔 〕第 号"
|
||||
- 内容包括:会议概况、议定事项、任务分工
|
||||
- 语言准确、简练
|
||||
|
||||
## 用户信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 编号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 会议时间:{{meeting_time}}
|
||||
- 会议地点:{{meeting_place}}
|
||||
- 主持人:{{host}}
|
||||
- 出席人员:{{attendees}}
|
||||
- 议题:{{agenda}}
|
||||
- 议定事项:{{decisions}}
|
||||
- 任务分工:{{tasks}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的会议纪要。');
|
||||
|
||||
-- 10. 公告
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('公告', 'announcement', '向社会公众或特定范围公布事项', 'Megaphone', 10,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于XX的公告"},
|
||||
{"key":"announcement_type","label":"公告类型","type":"select","options":["政策公告","项目申报公告","结果公示","招聘公告","其他公告"],"required":true},
|
||||
{"key":"content","label":"公告内容","type":"textarea","required":true,"placeholder":"公告的具体内容"},
|
||||
{"key":"requirements","label":"相关要求","type":"textarea","required":false,"placeholder":"对公众的要求说明"},
|
||||
{"key":"contact","label":"联系方式","type":"text","required":false,"placeholder":"咨询电话、地址等"},
|
||||
{"key":"deadline","label":"有效期限","type":"text","required":false,"placeholder":"如:本公告有效期至2026年12月31日"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是专业公文写作专家。请严格按规范生成【公告】公文:
|
||||
|
||||
## 公告规范
|
||||
- 面向社会公众,语言通俗易懂
|
||||
- 内容完整、表述准确
|
||||
- 结尾写明联系方式和有效期
|
||||
|
||||
## 用户信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 标题:{{title}}
|
||||
- 公告类型:{{announcement_type}}
|
||||
- 内容:{{content}}
|
||||
- 要求:{{requirements}}
|
||||
- 联系方式:{{contact}}
|
||||
- 有效期限:{{deadline}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的公告。');
|
||||
|
||||
-- 11. 科技局特色:项目申报通知
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('科技项目申报通知', 'project_notice', '科技项目计划申报、评审的通知文件(科技局特色公文)', 'Rocket', 11,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科发〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于组织申报2026年度XX科技计划项目的通知"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各区科技主管部门、各有关单位:"},
|
||||
{"key":"project_type","label":"项目类型","type":"select","options":["重点研发计划","科技成果转化","高新技术企业培育","科技创新平台","基础研究","科技人才"],"required":true},
|
||||
{"key":"support_scope","label":"支持范围和方向","type":"textarea","required":true,"placeholder":"列出本次支持的研究方向和领域"},
|
||||
{"key":"apply_conditions","label":"申报条件","type":"textarea","required":true,"placeholder":"申报主体资格、项目条件等"},
|
||||
{"key":"support_amount","label":"资助标准","type":"text","required":false,"placeholder":"如:单个项目资助不超过50万元"},
|
||||
{"key":"materials","label":"申报材料清单","type":"textarea","required":true,"placeholder":"列出需要提交的材料清单"},
|
||||
{"key":"timeline","label":"申报时间安排","type":"textarea","required":true,"placeholder":"网上申报时间、纸质材料提交时间等"},
|
||||
{"key":"review_process","label":"评审程序","type":"textarea","required":false,"placeholder":"形式审查→专家评审→现场考察→公示"},
|
||||
{"key":"contact","label":"联系方式","type":"text","required":true,"placeholder":"科技项目科 张三 010-12345678"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是科技局公文写作专家。请严格按规范生成【科技项目申报通知】:
|
||||
|
||||
## 特殊规范
|
||||
- 项目类型、支持范围要明确
|
||||
- 申报条件要具体、可操作
|
||||
- 时间节点要清晰
|
||||
- 材料清单要完整
|
||||
- 附件说明(如有申报书模板等)
|
||||
|
||||
## 用户信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 项目类型:{{project_type}}
|
||||
- 支持范围:{{support_scope}}
|
||||
- 申报条件:{{apply_conditions}}
|
||||
- 资助标准:{{support_amount}}
|
||||
- 材料清单:{{materials}}
|
||||
- 时间安排:{{timeline}}
|
||||
- 评审程序:{{review_process}}
|
||||
- 联系方式:{{contact}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整、专业的科技项目申报通知。');
|
||||
|
||||
-- 12. 科技局特色:科技奖励通报
|
||||
INSERT INTO document_templates (name, doc_type, description, icon, sort_order, fields, prompt_template) VALUES
|
||||
('科技奖励通报', 'tech_award', '科技进步奖、创新奖的表彰通报(科技局特色公文)', 'Award', 12,
|
||||
'[
|
||||
{"key":"issuing_org","label":"发文机关","type":"text","required":true,"default":"XX市科学技术局"},
|
||||
{"key":"doc_number","label":"发文字号","type":"text","required":true,"placeholder":"如:X科奖〔2026〕X号"},
|
||||
{"key":"title","label":"标题","type":"text","required":true,"placeholder":"关于表彰2025年度XX市科学技术奖获奖项目的通报"},
|
||||
{"key":"recipients","label":"主送机关","type":"textarea","required":true,"placeholder":"各有关单位:"},
|
||||
{"key":"award_background","label":"评审背景","type":"textarea","required":true,"placeholder":"评审工作的开展情况"},
|
||||
{"key":"award_results","label":"获奖名单","type":"textarea","required":true,"placeholder":"一等奖X项、二等奖X项,列出主要获奖项目"},
|
||||
{"key":"award_amount","label":"奖励标准","type":"text","required":false,"placeholder":"如:一等奖XX万元"},
|
||||
{"key":"requirements","label":"号召和要求","type":"textarea","required":true,"placeholder":"对全市科技工作者的号召"},
|
||||
{"key":"sign_date","label":"成文日期","type":"text","required":true,"default":"2026年5月10日"}
|
||||
]',
|
||||
'你是科技局公文写作专家。请生成【科技奖励通报】:
|
||||
|
||||
## 规范
|
||||
- 表彰通报格式
|
||||
- 先说明评审背景,再公布获奖名单
|
||||
- 最后号召学习、继续创新
|
||||
|
||||
## 用户信息
|
||||
- 发文机关:{{issuing_org}}
|
||||
- 发文字号:{{doc_number}}
|
||||
- 标题:{{title}}
|
||||
- 主送机关:{{recipients}}
|
||||
- 评审背景:{{award_background}}
|
||||
- 获奖名单:{{award_results}}
|
||||
- 奖励标准:{{award_amount}}
|
||||
- 号召和要求:{{requirements}}
|
||||
- 成文日期:{{sign_date}}
|
||||
|
||||
请生成完整的科技奖励通报。');
|
||||
@@ -0,0 +1,76 @@
|
||||
-- 区县经济指标数据表
|
||||
CREATE TABLE IF NOT EXISTS regional_economic_data (
|
||||
id SERIAL PRIMARY KEY,
|
||||
district_name VARCHAR(50) NOT NULL,
|
||||
district_code VARCHAR(20) NOT NULL,
|
||||
year INT NOT NULL,
|
||||
-- 核心经济指标
|
||||
gdp DECIMAL(14,2), -- GDP总量(亿元)
|
||||
gdp_growth DECIMAL(5,2), -- GDP同比增速(%)
|
||||
gdp_per_capita DECIMAL(12,2), -- 人均GDP(元)
|
||||
fiscal_revenue DECIMAL(12,2), -- 一般公共预算收入(亿元)
|
||||
fiscal_revenue_growth DECIMAL(5,2),
|
||||
fixed_investment DECIMAL(12,2), -- 固定资产投资(亿元)
|
||||
fixed_investment_growth DECIMAL(5,2),
|
||||
retail_sales DECIMAL(12,2), -- 社消零售总额(亿元)
|
||||
retail_sales_growth DECIMAL(5,2),
|
||||
industrial_output DECIMAL(12,2), -- 规上工业增加值(亿元)
|
||||
industrial_output_growth DECIMAL(5,2),
|
||||
tertiary_ratio DECIMAL(5,2), -- 第三产业占比(%)
|
||||
import_export DECIMAL(12,2), -- 进出口总额(亿元)
|
||||
import_export_growth DECIMAL(5,2),
|
||||
actual_fdi DECIMAL(10,2), -- 实际利用外资(亿美元)
|
||||
tech_expenditure DECIMAL(10,2), -- R&D经费投入(亿元)
|
||||
tech_expenditure_ratio DECIMAL(5,2), -- R&D/GDP(%)
|
||||
-- 补充指标
|
||||
population DECIMAL(8,2), -- 常住人口(万人)
|
||||
urban_income DECIMAL(10,2), -- 城镇居民人均可支配收入(元)
|
||||
rural_income DECIMAL(10,2), -- 农村居民人均可支配收入(元)
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(district_code, year)
|
||||
);
|
||||
|
||||
-- 清理旧数据
|
||||
DELETE FROM regional_economic_data;
|
||||
|
||||
-- ====================================================================
|
||||
-- 2024年数据
|
||||
-- ====================================================================
|
||||
INSERT INTO regional_economic_data (district_name, district_code, year,
|
||||
gdp, gdp_growth, gdp_per_capita, fiscal_revenue, fiscal_revenue_growth,
|
||||
fixed_investment, fixed_investment_growth, retail_sales, retail_sales_growth,
|
||||
industrial_output, industrial_output_growth, tertiary_ratio,
|
||||
import_export, import_export_growth, actual_fdi, tech_expenditure, tech_expenditure_ratio,
|
||||
population, urban_income, rural_income) VALUES
|
||||
('东城区', 'dongcheng', 2024, 2856.30, 5.8, 182540, 312.50, 6.2, 1285.40, 4.8, 1356.20, 5.5, 485.60, 6.1, 68.5, 856.30, 7.2, 12.80, 98.50, 3.45, 156.50, 82560, 48320),
|
||||
('西城区', 'xicheng', 2024, 2145.80, 6.1, 168920, 245.80, 5.8, 985.60, 5.2, 1125.40, 6.8, 356.20, 5.5, 72.3, 625.40, 6.5, 8.50, 72.30, 3.37, 127.00, 78450, 45680),
|
||||
('南城区', 'nancheng', 2024, 1856.50, 5.5, 152360, 198.60, 4.8, 856.30, 3.5, 985.60, 5.2, 312.50, 4.8, 65.8, 425.60, 5.8, 6.20, 52.80, 2.84, 121.90, 75680, 42560),
|
||||
('北城区', 'beicheng', 2024, 1625.40, 5.2, 145680, 178.50, 4.5, 756.80, 3.2, 856.30, 4.8, 285.60, 4.2, 63.5, 356.80, 4.5, 4.80, 42.50, 2.61, 111.60, 72340, 40250),
|
||||
('高新区', 'gaoxin', 2024, 3256.80, 8.5, 245680, 425.60, 9.2, 1856.30, 12.5, 985.60, 7.5, 856.30, 10.2, 58.2, 1856.30, 12.8, 28.50, 168.50, 5.17, 132.60, 95680, 52340),
|
||||
('经开区', 'jingkai', 2024, 2456.50, 7.8, 198560, 325.80, 8.5, 1556.80, 11.2, 856.30, 6.5, 685.60, 9.8, 52.5, 1256.80, 10.5, 18.60, 125.60, 5.11, 123.70, 88560, 48560),
|
||||
('青山县', 'qingshan', 2024, 685.60, 4.2, 82560, 62.50, 3.5, 325.60, 2.8, 356.80, 3.5, 185.60, 3.8, 45.2, 125.60, 3.2, 1.80, 12.50, 1.82, 83.00, 56780, 32560),
|
||||
('和平县', 'heping', 2024, 525.80, 3.8, 68540, 48.60, 3.2, 256.80, 2.5, 285.60, 3.2, 142.50, 3.2, 42.8, 85.60, 2.8, 1.20, 8.60, 1.64, 76.70, 52340, 30250),
|
||||
('龙湖县', 'longhu', 2024, 456.30, 3.5, 62540, 38.50, 2.8, 198.60, 2.2, 225.60, 2.8, 125.60, 2.8, 40.5, 65.80, 2.5, 0.85, 6.50, 1.42, 72.90, 48560, 28560),
|
||||
('明德县', 'mingde', 2024, 385.60, 3.2, 55680, 32.50, 2.5, 168.50, 1.8, 198.60, 2.5, 98.50, 2.5, 38.6, 45.60, 2.2, 0.65, 4.80, 1.24, 69.30, 45680, 26840),
|
||||
('新华区', 'xinhua', 2024, 1356.80, 6.5, 156780, 165.80, 6.8, 685.60, 6.5, 756.80, 6.2, 245.60, 5.8, 62.5, 385.60, 6.8, 5.60, 45.60, 3.36, 86.50, 72560, 42680);
|
||||
|
||||
-- ====================================================================
|
||||
-- 2025年数据
|
||||
-- ====================================================================
|
||||
INSERT INTO regional_economic_data (district_name, district_code, year,
|
||||
gdp, gdp_growth, gdp_per_capita, fiscal_revenue, fiscal_revenue_growth,
|
||||
fixed_investment, fixed_investment_growth, retail_sales, retail_sales_growth,
|
||||
industrial_output, industrial_output_growth, tertiary_ratio,
|
||||
import_export, import_export_growth, actual_fdi, tech_expenditure, tech_expenditure_ratio,
|
||||
population, urban_income, rural_income) VALUES
|
||||
('东城区', 'dongcheng', 2025, 3045.20, 6.6, 193560, 335.80, 7.5, 1356.80, 5.6, 1445.80, 6.6, 518.60, 6.8, 69.2, 928.50, 8.4, 14.20, 112.50, 3.69, 157.40, 87250, 51480),
|
||||
('西城区', 'xicheng', 2025, 2298.50, 7.1, 179850, 265.80, 8.1, 1065.80, 8.1, 1205.60, 7.1, 385.60, 8.3, 73.5, 685.60, 9.6, 9.80, 82.50, 3.59, 127.80, 82850, 48560),
|
||||
('南城区', 'nancheng', 2025, 1965.80, 5.9, 160580, 212.50, 7.0, 912.50, 6.6, 1052.80, 6.8, 338.50, 8.3, 66.5, 465.80, 9.4, 7.20, 60.50, 3.08, 122.40, 79850, 45280),
|
||||
('北城区', 'beicheng', 2025, 1712.50, 5.4, 152680, 189.60, 6.2, 798.50, 5.5, 905.60, 5.8, 302.50, 5.9, 64.2, 382.50, 7.2, 5.50, 48.60, 2.84, 112.20, 76450, 42850),
|
||||
('高新区', 'gaoxin', 2025, 3586.50, 10.1, 268560, 475.80, 11.8, 2125.60, 14.5, 1085.60, 10.1, 965.80, 12.8, 59.5, 2156.80, 16.2, 35.60, 198.50, 5.54, 133.50, 102560, 56280),
|
||||
('经开区', 'jingkai', 2025, 2685.80, 9.3, 215680, 362.50, 11.3, 1785.60, 14.7, 925.60, 8.1, 768.50, 12.1, 53.8, 1425.80, 13.4, 22.50, 148.50, 5.53, 124.50, 94560, 52180),
|
||||
('青山县', 'qingshan', 2025, 718.50, 4.8, 86250, 66.80, 6.9, 348.60, 7.1, 378.50, 6.1, 198.50, 7.0, 46.5, 138.50, 10.3, 2.15, 14.80, 2.06, 83.30, 59850, 34560),
|
||||
('和平县', 'heping', 2025, 548.60, 4.3, 71250, 51.80, 6.6, 272.50, 6.1, 302.50, 5.9, 152.80, 7.2, 43.5, 92.50, 8.1, 1.45, 10.20, 1.86, 77.00, 55280, 32150),
|
||||
('龙湖县', 'longhu', 2025, 476.80, 4.5, 65280, 41.20, 7.0, 215.60, 8.6, 240.80, 6.7, 135.60, 8.0, 41.8, 72.80, 10.6, 1.05, 7.80, 1.64, 73.10, 51280, 30250),
|
||||
('明德县', 'mingde', 2025, 402.80, 4.5, 58120, 34.80, 7.1, 182.50, 8.3, 212.50, 7.0, 106.50, 8.1, 39.8, 50.80, 11.4, 0.82, 5.80, 1.44, 69.30, 48250, 28560),
|
||||
('新华区', 'xinhua', 2025, 1452.50, 7.0, 166250, 179.80, 8.4, 745.80, 8.8, 812.50, 7.4, 268.50, 9.3, 63.8, 425.60, 10.4, 6.80, 52.80, 3.64, 87.40, 76850, 45280);
|
||||
@@ -0,0 +1,203 @@
|
||||
-- 发改局完整示例数据:分类 + 应用
|
||||
-- 可重复执行
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
creator_id UUID;
|
||||
fagai_org UUID := 'a0000000-0000-0000-0000-000000000003';
|
||||
cat_policy UUID;
|
||||
cat_writing UUID;
|
||||
cat_data UUID;
|
||||
cat_service UUID;
|
||||
cat_general UUID;
|
||||
cat_invest UUID;
|
||||
BEGIN
|
||||
SELECT id INTO creator_id FROM users WHERE email = 'chenchu@govai.gov.cn';
|
||||
|
||||
-- ========== 分类数据 ==========
|
||||
INSERT INTO categories (name, slug, icon, sort_order, org_id) VALUES
|
||||
('政策研究', 'fagai-policy', 'book-open', 1, fagai_org),
|
||||
('项目管理', 'fagai-project', 'folder-kanban', 2, fagai_org),
|
||||
('经济分析', 'fagai-economy', 'bar-chart', 3, fagai_org),
|
||||
('公文写作', 'fagai-writing', 'file-text', 4, fagai_org),
|
||||
('招商引资', 'fagai-invest', 'trending-up', 5, fagai_org),
|
||||
('综合应用', 'fagai-general', 'more-horizontal', 99, fagai_org)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
SELECT id INTO cat_policy FROM categories WHERE slug = 'fagai-policy';
|
||||
SELECT id INTO cat_writing FROM categories WHERE slug = 'fagai-writing';
|
||||
SELECT id INTO cat_data FROM categories WHERE slug = 'fagai-economy';
|
||||
SELECT id INTO cat_service FROM categories WHERE slug = 'fagai-project';
|
||||
SELECT id INTO cat_general FROM categories WHERE slug = 'fagai-general';
|
||||
SELECT id INTO cat_invest FROM categories WHERE slug = 'fagai-invest';
|
||||
|
||||
-- ========== 应用数据 ==========
|
||||
|
||||
-- 1. 宏观经济分析助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000001',
|
||||
'宏观经济分析助手', 'macro-economy-analyst',
|
||||
'分析区域宏观经济指标,研判经济运行态势,生成分析报告',
|
||||
E'## 功能介绍\n\n基于区域经济数据的智能分析工具,助力发改工作决策:\n\n- GDP增速趋势分析与预测\n- 产业结构变动研判\n- 固定资产投资分析\n- 消费市场运行监测\n- 进出口贸易数据解读\n\n## 使用方法\n\n输入您关注的经济指标或区域,即可获得专业分析报告。',
|
||||
'📊', cat_data, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', true, 456, 4.8, 62, '1.0',
|
||||
'{"system_prompt": "你是一位宏观经济分析专家,熟悉中国经济指标体系和区域经济发展特点。请根据用户提出的经济问题,结合最新数据进行专业分析,给出有深度的研判意见。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 5000}',
|
||||
'您好,我是宏观经济分析助手。请输入您关注的经济指标或问题,我来为您分析。',
|
||||
'["本市上半年GDP增速如何?与去年同期相比有哪些变化?", "当前固定资产投资的重点领域有哪些?增速如何?", "本地区产业结构近三年的变化趋势是什么?", "社会消费品零售总额近期表现如何?有哪些新增长点?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 项目可行性评估
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000002',
|
||||
'项目可行性评估', 'project-feasibility',
|
||||
'按步骤输入项目信息,智能生成可行性分析报告',
|
||||
E'## 功能介绍\n\n重大项目可行性研究辅助工具:\n\n- 项目基本情况梳理\n- 市场需求与前景分析\n- 投资估算与资金筹措方案\n- 社会效益与经济效益评估\n- 风险分析与防范措施\n\n## 适用场景\n\n适用于各类政府投资项目和企业重大投资项目的前期论证工作。',
|
||||
'📋', cat_service, creator_id, fagai_org,
|
||||
'workflow', 'approved', 'public', true, 289, 4.7, 38, '1.0',
|
||||
'{"system_prompt": "你是项目可行性研究专家,熟悉发改委项目审批流程和可行性研究报告编制规范。请根据用户提供的项目信息,生成专业规范的可行性分析内容。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "project_name", "label": "项目名称", "type": "text", "placeholder": "如:XX产业园基础设施建设项目", "required": true}, {"key": "project_type", "label": "项目类型", "type": "select", "options": ["基础设施", "产业发展", "社会事业", "生态环保", "科技创新"], "required": true}, {"key": "investment", "label": "投资规模", "type": "text", "placeholder": "如:总投资5亿元", "required": true}, {"key": "background", "label": "项目背景", "type": "textarea", "placeholder": "项目建设的背景、必要性、政策依据等", "required": true}, {"key": "content", "label": "建设内容", "type": "textarea", "placeholder": "主要建设内容、建设规模、建设周期等"}]}',
|
||||
'请填写项目基本信息,我将为您生成可行性分析报告。',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 产业政策解读
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000003',
|
||||
'产业政策解读', 'industry-policy-qa',
|
||||
'解读国家和地方产业政策,分析政策导向和扶持方向',
|
||||
E'## 功能介绍\n\n覆盖发改领域核心政策文件:\n\n- 国家发展规划与战略解读\n- 产业目录与准入政策\n- 区域协调发展政策\n- 价格与收费政策\n- 节能减排与绿色发展政策\n\n## 使用方法\n\n输入您关心的产业政策问题,系统将提供专业解读和政策依据。',
|
||||
'📑', cat_policy, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', true, 678, 4.9, 89, '1.0',
|
||||
'{"system_prompt": "你是发改领域的政策研究专家,熟悉国家和地方各级发展改革政策。请根据用户的问题,准确引用政策文件,并给出通俗易懂的政策解读。如涉及具体操作,请告知对应的办理部门和流程。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 4000}',
|
||||
'您好,我是产业政策解读系统。请输入您想了解的政策问题。',
|
||||
'["战略性新兴产业目前有哪些重点扶持方向?", "中小企业数字化转型有哪些国家级支持政策?", "碳达峰碳中和政策对制造业企业有哪些具体要求?", "最新的产业结构调整指导目录有哪些变化?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 4. 招商引资方案生成
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000004',
|
||||
'招商引资方案生成', 'investment-promotion',
|
||||
'根据区域优势和目标产业,生成招商引资方案和推介材料',
|
||||
E'## 功能介绍\n\n招商引资工作智能助手:\n\n- 区域投资环境分析\n- 目标产业招商方案\n- 招商推介PPT要点\n- 优惠政策梳理汇总\n- 项目洽谈话术建议\n\n## 适用场景\n\n适用于各级发改部门招商引资工作的方案策划和材料准备。',
|
||||
'💼', cat_invest, creator_id, fagai_org,
|
||||
'workflow', 'approved', 'public', false, 198, 4.6, 27, '1.0',
|
||||
'{"system_prompt": "你是招商引资策划专家,熟悉各地招商政策和产业转移趋势。请根据用户提供的区域信息和目标产业,生成专业的招商引资方案。", "model": "qwen-plus", "temperature": 0.5, "max_tokens": 5000, "app_type": "workflow", "steps": [{"key": "region", "label": "目标区域", "type": "text", "placeholder": "如:XX经济开发区", "required": true}, {"key": "industry", "label": "目标产业", "type": "select", "options": ["新能源", "新材料", "电子信息", "生物医药", "智能制造", "现代服务业"], "required": true}, {"key": "advantages", "label": "区域优势", "type": "textarea", "placeholder": "交通、人才、政策、产业基础等方面的优势", "required": true}, {"key": "requirements", "label": "特殊要求", "type": "textarea", "placeholder": "如:重点面向长三角企业招商、投资额度要求等"}]}',
|
||||
'请填写招商信息,我将为您生成招商引资方案。',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 5. 发改公文起草助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000005',
|
||||
'发改公文起草助手', 'fagai-doc-writer',
|
||||
'辅助起草发改领域各类公文、批复、通知和会议纪要',
|
||||
E'## 功能介绍\n\n发改系统公文写作专业工具:\n\n- 项目批复文件起草\n- 发展规划编制辅助\n- 工作通知与通报\n- 调研报告与建议\n- 会议纪要整理\n\n## 使用方法\n\n描述公文类型和核心内容,系统将生成规范格式的公文初稿。',
|
||||
'📝', cat_writing, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', false, 345, 4.7, 48, '1.0',
|
||||
'{"system_prompt": "你是发改系统公文写作专家,熟悉党政机关公文处理规范和发改领域专业用语。请根据用户需求,生成格式规范、表述准确、逻辑清晰的公文。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 5000}',
|
||||
'请描述您需要起草的公文类型和主要内容。',
|
||||
'["起草一份关于XX项目的立项批复", "撰写一份关于优化营商环境的工作通知", "整理本周项目推进会的会议纪要", "写一份关于本地区经济运行情况的调研报告提纲"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 6. 价格监测分析
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000006',
|
||||
'价格监测分析', 'price-monitor',
|
||||
'监测分析重要商品和服务价格变动,研判价格走势',
|
||||
E'## 功能介绍\n\n价格监测预警与分析系统:\n\n- CPI/PPI指数分析\n- 重要民生商品价格监测\n- 价格异常波动预警\n- 价格趋势预测分析\n- 价格调控政策建议\n\n## 使用方法\n\n输入您关注的商品或价格指标,获取监测分析结果。',
|
||||
'📈', cat_data, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', false, 234, 4.5, 31, '1.0',
|
||||
'{"system_prompt": "你是价格监测分析专家,熟悉价格指数编制方法和价格调控政策。请根据用户关注的价格问题,提供专业的监测数据分析和趋势研判。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 3000}',
|
||||
'请输入您关注的价格指标或商品类别。',
|
||||
'["近期猪肉价格走势如何?后期预判怎样?", "本月CPI同比变化情况及主要影响因素", "成品油价格调整机制是怎样的?", "居民用电价格的阶梯定价标准是什么?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 7. 营商环境评估
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000007',
|
||||
'营商环境评估报告', 'business-env-report',
|
||||
'按步骤输入评估数据,生成区域营商环境评估报告',
|
||||
E'## 功能介绍\n\n营商环境评估与优化工具:\n\n- 政务服务效率评估\n- 市场准入便利度分析\n- 法治环境指标评价\n- 与先进地区对标分析\n- 优化提升建议\n\n## 适用场景\n\n适用于各级政府营商环境年度评估和专项改善工作。',
|
||||
'🏢', cat_service, creator_id, fagai_org,
|
||||
'workflow', 'approved', 'public', true, 167, 4.6, 22, '1.0',
|
||||
'{"system_prompt": "你是营商环境评估专家,熟悉世界银行营商环境评价体系和中国营商环境评价指标。请根据用户提供的数据,生成专业的评估报告。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "region", "label": "评估区域", "type": "text", "placeholder": "如:XX市/XX区", "required": true}, {"key": "period", "label": "评估周期", "type": "text", "placeholder": "如:2024年度", "required": true}, {"key": "indicators", "label": "核心指标数据", "type": "textarea", "placeholder": "企业开办时间、不动产登记时间、纳税便利度等指标数据", "required": true}, {"key": "focus", "label": "重点关注", "type": "textarea", "placeholder": "如:与标杆城市对标、重点改善领域等"}]}',
|
||||
'请填写评估信息,我将为您生成营商环境评估报告。',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 8. 能耗双控分析
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000008',
|
||||
'能耗双控分析', 'energy-control',
|
||||
'分析区域能源消耗数据,评估双控目标完成情况',
|
||||
E'## 功能介绍\n\n能耗双控与碳排放分析:\n\n- 单位GDP能耗分析\n- 能源消费总量监测\n- 重点用能单位评估\n- 节能目标完成度分析\n- 碳排放核算与预测\n\n## 使用方法\n\n输入区域能源数据或问题,获取专业分析。',
|
||||
'⚡', cat_data, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', false, 145, 4.4, 19, '1.0',
|
||||
'{"system_prompt": "你是能源与节能减排领域专家,熟悉能耗双控政策、碳达峰碳中和目标和节能技术。请根据用户提出的问题,提供专业分析和政策建议。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 3000}',
|
||||
'请输入您关注的能耗或碳排放问题。',
|
||||
'["本地区单位GDP能耗目标完成情况如何?", "哪些行业是重点耗能行业?如何推进节能降耗?", "碳达峰碳中和对本地区经济发展的影响分析", "企业申请节能审查需要什么条件和材料?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 9. 项目审批进度查询
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000009',
|
||||
'项目审批指南', 'project-approval-guide',
|
||||
'查询发改委项目审批流程、所需材料和办理时限',
|
||||
E'## 功能介绍\n\n项目审批全流程指南:\n\n- 项目立项审批流程\n- 项目核准/备案区别说明\n- 所需申报材料清单\n- 审批时限与进度说明\n- 常见问题解答\n\n## 使用方法\n\n输入项目类型或审批环节,获取详细办理指南。',
|
||||
'📂', cat_service, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', false, 523, 4.6, 71, '1.0',
|
||||
'{"system_prompt": "你是发改委项目审批窗口的智能咨询员,熟悉各类项目的审批、核准、备案流程。请耐心解答用户关于项目审批的问题,提供准确的流程指引和材料清单。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 3000}',
|
||||
'请问您想了解什么类型的项目审批流程?',
|
||||
'["企业投资项目备案需要哪些材料?", "政府投资项目从立项到开工需要多长时间?", "项目可行性研究报告编制有什么要求?", "跨区域项目审批应该找哪一级发改部门?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 10. 五年规划编制助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'c0000000-0000-0000-0000-000000000010',
|
||||
'五年规划编制助手', 'five-year-plan',
|
||||
'辅助编制国民经济和社会发展五年规划及专项规划',
|
||||
E'## 功能介绍\n\n规划编制智能辅助工具:\n\n- 发展现状与成就总结\n- 面临形势与挑战分析\n- 发展目标与指标设定\n- 重点任务与工程梳理\n- 保障措施建议\n\n## 使用方法\n\n描述规划编制需求,系统将提供专业的编制建议和框架。',
|
||||
'📐', cat_policy, creator_id, fagai_org,
|
||||
'chatbot', 'approved', 'public', false, 112, 4.8, 15, '1.0',
|
||||
'{"system_prompt": "你是国民经济和社会发展规划编制专家,熟悉国家和地方各级规划的编制流程和方法论。请根据用户需求,提供专业的规划编制建议。注意要结合最新的国家战略和政策导向。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 5000}',
|
||||
'请描述您的规划编制需求,我来提供专业建议。',
|
||||
'["如何设定十五五规划的经济发展主要指标?", "区域产业发展专项规划的框架结构应该如何设计?", "五年规划编制需要收集哪些基础数据?", "如何将碳达峰碳中和目标融入地方发展规划?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
END $$;
|
||||
@@ -0,0 +1,178 @@
|
||||
-- 为发改局3个知识库 + 科技局项目申报库填充核心文档
|
||||
BEGIN;
|
||||
|
||||
-- 获取发改管理员ID 和 科技局管理员ID
|
||||
-- 发改: fg-admin@govai.gov.cn → a0000000-0000-0000-0000-000000000003
|
||||
-- 科技: liganshi@govai.gov.cn → a0000000-0000-0000-0000-000000000001
|
||||
|
||||
-- ============================
|
||||
-- 发改政策法规库
|
||||
-- ============================
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000001'::uuid, 'e0000000-0000-0000-0000-000000000001'::uuid,
|
||||
'产业结构调整指导目录(2024年本)',
|
||||
'产业结构调整指导目录(2024年本):
|
||||
一、鼓励类产业:
|
||||
1. 新一代信息技术:人工智能、量子计算、第三代半导体、6G技术研发
|
||||
2. 高端装备制造:航空航天装备、海洋工程装备、高端数控机床
|
||||
3. 新材料:碳纤维、石墨烯、超导材料
|
||||
4. 生物技术:基因治疗、细胞治疗、生物育种
|
||||
5. 新能源:光伏、风电、氢能、储能
|
||||
6. 新能源汽车:整车制造、动力电池、充换电设施
|
||||
7. 绿色环保:碳捕集利用与封存(CCUS)、污水处理、固废处理
|
||||
8. 数字经济:大数据中心、工业互联网、数字孪生
|
||||
二、限制类产业:
|
||||
1. 钢铁行业中落后生产工艺装备
|
||||
2. 电解铝行业低效产能
|
||||
3. 水泥行业过剩产能
|
||||
三、淘汰类产业:
|
||||
1. 能效不达标的工业锅炉、电机
|
||||
2. 国家明令淘汰的高污染、高耗能设备
|
||||
3. 不符合安全生产标准的矿山设备
|
||||
政策依据:《产业结构调整指导目录》由国家发展改革委制定并适时修订,是引导社会投资方向、政府管理投资项目,制定和实施财税、信贷、土地、进出口等政策的重要依据。',
|
||||
800, 0, 'pending', u.id, 'txt', 800
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000002'::uuid, 'e0000000-0000-0000-0000-000000000001'::uuid,
|
||||
'企业投资项目核准和备案管理条例',
|
||||
'《企业投资项目核准和备案管理条例》(国务院令第673号)要点:
|
||||
第二条 本条例适用于中华人民共和国境内的企业投资项目的核准和备案。
|
||||
第三条 对关系国家安全、涉及全国重大生产力布局、战略性资源开发和重大公共利益等项目,实行核准管理。
|
||||
第四条 对前条规定以外的企业投资项目,实行备案管理。
|
||||
第九条 申请核准的项目,应当提交项目申请报告:(一)项目单位的基本情况(二)项目的必要性分析(三)拟建项目情况(四)项目选址及用地情况(五)环境和生态影响分析(六)项目资源利用和能源耗用分析
|
||||
第十三条 实行备案管理的项目,企业应当在开工建设前通过在线平台将项目信息告知备案机关。
|
||||
第十四条 项目备案内容包括:企业基本情况,项目名称、建设地点、建设规模、建设内容,项目总投资额、项目符合产业政策声明。
|
||||
审批时限:核准机关应当自受理申请之日起20个工作日内作出是否予以核准的决定。',
|
||||
600, 0, 'pending', u.id, 'txt', 600
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000003'::uuid, 'e0000000-0000-0000-0000-000000000001'::uuid,
|
||||
'中华人民共和国价格法要点',
|
||||
'《中华人民共和国价格法》(1998年5月1日施行)要点:
|
||||
第三条 国家实行并逐步完善宏观经济调控下主要由市场形成价格的机制。
|
||||
第六条 商品价格和服务价格,除本法另有规定外,实行市场调节价,由经营者依照本法自主制定。
|
||||
第十八条 下列商品和服务价格,政府在必要时可以实行政府指导价或者政府定价:(一)与国民经济发展和人民生活关系重大的极少数商品价格(二)资源稀缺的少数商品价格(三)自然垄断经营的商品价格(四)重要的公用事业价格(五)重要的公益性服务价格
|
||||
第三十条 当重要商品和服务价格显著上涨或者有可能显著上涨,国务院和省、自治区、直辖市人民政府可以对部分价格采取限定差价率或者利润率、规定限价、实行提价申报制度和调价备案制度等干预措施。
|
||||
第三十三条 经营者不得有下列不正当价格行为:(一)相互串通操纵市场价格(二)以低于成本的价格倾销(三)捏造散布涨价信息哄抬价格(四)利用虚假价格手段诱骗消费者',
|
||||
700, 0, 'pending', u.id, 'txt', 700
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000004'::uuid, 'e0000000-0000-0000-0000-000000000001'::uuid,
|
||||
'固定资产投资项目节能审查办法',
|
||||
'《固定资产投资项目节能审查办法》(国家发展改革委令2023年第2号)要点:
|
||||
第二条 固定资产投资项目节能审查,是指根据节能法律法规、标准规范、政策及相关规划,对固定资产投资项目用能的科学性、合理性进行审查并形成审查意见的行为。
|
||||
第四条 年综合能源消费量1000吨标准煤以上(含)的固定资产投资项目,须由发展改革部门进行节能审查。
|
||||
第六条 建设单位应在开工建设前取得节能审查意见。
|
||||
第八条 节能审查内容:(一)项目是否符合国家和地方节能法律法规(二)项目用能分析是否客观准确(三)项目节能措施是否合理可行(四)项目的能源利用是否高效合理(五)项目能效水平是否达到同行业先进水平
|
||||
审查时限:节能审查机关应当自受理节能审查申请之日起20个工作日内,作出节能审查意见。',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000005'::uuid, 'e0000000-0000-0000-0000-000000000001'::uuid,
|
||||
'碳达峰碳中和政策要点',
|
||||
'《关于完整准确全面贯彻新发展理念做好碳达峰碳中和工作的意见》(中发〔2021〕36号)核心目标:
|
||||
到2025年:单位GDP能耗比2020年下降13.5%,单位GDP二氧化碳排放比2020年下降18%,非化石能源消费比重达到20%左右。
|
||||
到2030年:单位GDP二氧化碳排放比2005年下降65%以上,非化石能源消费比重达到25%左右,实现碳达峰。
|
||||
到2060年:实现碳中和。
|
||||
重点任务:1.推进经济社会发展全面绿色转型 2.深度调整产业结构 3.加快构建清洁低碳安全高效能源体系 4.加快推进低碳交通运输体系建设 5.提升城乡建设绿色低碳发展质量 6.加强绿色低碳重大科技攻关和推广应用 7.持续巩固提升碳汇能力 8.提高对外开放绿色低碳发展水平 9.健全法律法规标准和统计监测体系 10.完善政策机制
|
||||
保障措施:建立碳达峰碳中和"1+N"政策体系,各地区分类施策制定差异化实施方案。',
|
||||
600, 0, 'pending', u.id, 'txt', 600
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================
|
||||
-- 项目审批规范库
|
||||
-- ============================
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000010'::uuid, 'e0000000-0000-0000-0000-000000000002'::uuid,
|
||||
'项目申请报告编制要求',
|
||||
'《项目申请报告通用文本》编制规范要求:
|
||||
一、项目申请报告编写框架:第一章 申报单位及项目概况(企业基本情况、财务状况、项目名称、建设地点、建设规模、总投资)第二章 发展规划、产业政策和行业准入分析 第三章 资源开发及综合利用分析 第四章 节能方案分析 第五章 建设用地、征地拆迁及移民安置分析 第六章 环境和生态影响分析 第七章 经济影响分析 第八章 社会影响分析
|
||||
二、特别注意事项:项目申请报告应由具备相应工程咨询资质的机构编制。核准机关在20个工作日内作出决定,特殊情况可延长10个工作日。项目核准文件有效期2年,逾期未开工需重新核准。',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000011'::uuid, 'e0000000-0000-0000-0000-000000000002'::uuid,
|
||||
'备案制项目管理要求',
|
||||
'实行备案管理的企业投资项目操作要求:
|
||||
一、备案范围:除《政府核准的投资项目目录》内的项目外,企业投资建设的项目一律实行备案管理。
|
||||
二、备案方式:通过全国投资项目在线审批监管平台进行网上备案。企业应在开工建设前完成备案。
|
||||
三、备案内容:企业基本情况、项目名称、建设地点、建设规模、建设内容、项目总投资额、项目符合产业政策声明。
|
||||
四、备案流程:1.企业登录全国投资项目在线审批监管平台 2.填写项目备案信息 3.提交后即完成备案(即报即备)4.获取项目备案代码
|
||||
五、注意事项:备案机关发现已备案项目属于产业政策禁止投资建设或者实行核准管理的,应当及时告知。企业需对备案信息的真实性负责。项目建设内容发生重大变更的应重新备案。',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================
|
||||
-- 营商环境指标库
|
||||
-- ============================
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000020'::uuid, 'e0000000-0000-0000-0000-000000000003'::uuid,
|
||||
'中国营商环境评价指标体系',
|
||||
'中国营商环境评价指标体系(18个一级指标):1.开办企业 2.办理建筑许可 3.获得电力 4.登记财产 5.获得信贷 6.保护中小投资者 7.纳税 8.跨境贸易 9.执行合同 10.办理破产 11.劳动力市场监管 12.政府采购 13.招标投标 14.市场监管 15.政务服务 16.包容普惠创新 17.知识产权保护 18.法治保障
|
||||
评价方式:采取"以评促改"机制,由国家发改委组织实施,不排名、不评比,重在发现问题、推动整改。',
|
||||
400, 0, 'pending', u.id, 'txt', 400
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000021'::uuid, 'e0000000-0000-0000-0000-000000000003'::uuid,
|
||||
'优化营商环境条例核心条款',
|
||||
'《优化营商环境条例》(国务院令第722号,2020年1月1日施行)核心条款:
|
||||
第四条 国家鼓励和支持各地区、各部门结合实际情况,在法治框架内积极探索原创性、差异化的优化营商环境具体措施。
|
||||
第九条 市场主体依法享有经营自主权。对依法应当由市场主体自主决策的各类事项,任何单位和个人不得干预。
|
||||
第十三条 招标投标和政府采购应当公开透明、公平公正,依法平等对待各类所有制和不同地区的市场主体。
|
||||
第二十六条 国家持续深化"证照分离"改革,持续精简涉企经营许可事项。
|
||||
第五十七条 国家建立和完善以市场主体和社会公众满意度为导向的营商环境评价体系。
|
||||
处罚条款:对在优化营商环境工作中滥用职权、玩忽职守、徇私舞弊的,依法给予处分;构成犯罪的,依法追究刑事责任。',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'fg-admin@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============================
|
||||
-- 科技局项目申报材料模板库
|
||||
-- ============================
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000030'::uuid, '20000000-0000-0000-0000-000000000002'::uuid,
|
||||
'科技计划项目申报指南模板',
|
||||
'科技计划项目申报书编制要求:
|
||||
一、项目基本信息(项目名称不超25字、申报单位信息、项目负责人信息、起止时间、总经费及资金来源)
|
||||
二、项目研究内容(研究背景和意义、国内外现状、研究目标、研究内容和关键技术、技术路线和实施方案、创新性说明)
|
||||
三、预期成果和考核指标(论文/专利/产品/标准、量化考核指标、经济效益和社会效益预测)
|
||||
四、项目进度安排(分年度里程碑节点)
|
||||
五、经费预算(设备费、材料费、测试化验加工费、差旅费/会议费、劳务费、专家咨询费、管理费不超总预算5%)
|
||||
六、附件(营业执照、负责人身份证和职称证书、合作协议、知识产权证明)',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'liganshi@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, content, char_count, chunk_count, indexing_status, uploader_id, file_type, file_size)
|
||||
SELECT 'e1000000-0000-0000-0000-000000000031'::uuid, '20000000-0000-0000-0000-000000000002'::uuid,
|
||||
'高新技术企业认定管理办法',
|
||||
'《高新技术企业认定管理办法》(国科发火〔2016〕32号)要点:
|
||||
认定条件:1.企业注册成立一年以上 2.拥有对主要产品发挥核心支持作用的知识产权 3.技术属于《国家重点支持的高新技术领域》4.科技人员占职工总数不低于10% 5.近三年研发费用占销售收入比例:<5000万不低于5%、5000万-2亿不低于4%、>2亿不低于3% 6.高新技术产品收入占总收入不低于60%
|
||||
认定流程:1.企业申请(通过高新技术企业认定管理工作网)2.专家评审 3.认定报备 4.公示公告
|
||||
有效期:高新技术企业资格自颁发证书之日起有效期为三年
|
||||
税收优惠:高新技术企业减按15%的税率征收企业所得税(正常税率25%)',
|
||||
500, 0, 'pending', u.id, 'txt', 500
|
||||
FROM users u WHERE u.email = 'liganshi@govai.gov.cn'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 更新知识库统计
|
||||
UPDATE knowledge_bases SET doc_count = 5, total_chars = 3200 WHERE id = 'e0000000-0000-0000-0000-000000000001';
|
||||
UPDATE knowledge_bases SET doc_count = 2, total_chars = 1000 WHERE id = 'e0000000-0000-0000-0000-000000000002';
|
||||
UPDATE knowledge_bases SET doc_count = 2, total_chars = 900 WHERE id = 'e0000000-0000-0000-0000-000000000003';
|
||||
UPDATE knowledge_bases SET doc_count = 2, total_chars = 1000 WHERE id = '20000000-0000-0000-0000-000000000002';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,198 @@
|
||||
-- 公安局完整示例数据:应用中心
|
||||
-- 可重复执行
|
||||
|
||||
-- 公安局 org_id
|
||||
-- a0000000-0000-0000-0000-000000000002
|
||||
|
||||
-- 获取公安局创建者ID
|
||||
DO $$
|
||||
DECLARE
|
||||
creator_id UUID;
|
||||
gongan_org UUID := 'a0000000-0000-0000-0000-000000000002';
|
||||
cat_policy UUID;
|
||||
cat_writing UUID;
|
||||
cat_service UUID;
|
||||
cat_data UUID;
|
||||
cat_general UUID;
|
||||
cat_hr UUID;
|
||||
BEGIN
|
||||
SELECT id INTO creator_id FROM users WHERE email = 'zhangdui@govai.gov.cn';
|
||||
SELECT id INTO cat_policy FROM categories WHERE slug = 'policy-qa';
|
||||
SELECT id INTO cat_writing FROM categories WHERE slug = 'official-writing';
|
||||
SELECT id INTO cat_service FROM categories WHERE slug = 'public-service';
|
||||
SELECT id INTO cat_data FROM categories WHERE slug = 'data-governance';
|
||||
SELECT id INTO cat_general FROM categories WHERE slug = 'general';
|
||||
SELECT id INTO cat_hr FROM categories WHERE slug = 'hr-org';
|
||||
|
||||
-- ========== 应用数据 ==========
|
||||
|
||||
-- 1. 案件分析助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000001',
|
||||
'案件分析助手', 'case-analysis',
|
||||
'智能分析案件线索,提供侦查思路和法律依据参考',
|
||||
'基于AI大模型的案件分析工具,可根据案件描述智能分析:\n- 案件性质判定\n- 相关法律条文匹配\n- 侦查方向建议\n- 类似案例参考\n- 证据链梳理建议',
|
||||
'🔍', cat_data, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', true, 328, 4.7, 45, '1.0',
|
||||
'{"system_prompt": "你是一位经验丰富的刑侦分析专家,熟悉中国刑法、治安管理处罚法等法律法规。请根据用户描述的案件信息,从案件定性、法律适用、侦查方向、证据收集等角度进行专业分析。注意:你的分析仅供参考,不构成法律意见。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 4000}',
|
||||
'您好,我是案件分析助手。请描述案件基本情况,我将为您提供分析思路。',
|
||||
'["一起入室盗窃案,现场有脚印和指纹,嫌疑人可能是惯犯", "网络诈骗案,受害人通过虚假投资平台被骗50万", "交通肇事逃逸案,有行车记录仪画面但车牌模糊"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 警情报告生成器
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000002',
|
||||
'警情报告生成器', 'police-report-writer',
|
||||
'快速生成规范的警情报告、出警记录、工作汇报',
|
||||
'根据输入的警情要素,自动生成符合公安系统规范的各类文书:\n- 接处警记录\n- 案件受理报告\n- 巡逻工作日志\n- 专项行动总结\n- 治安形势分析报告',
|
||||
'📋', cat_writing, creator_id, gongan_org,
|
||||
'workflow', 'approved', 'public', true, 512, 4.8, 67, '1.0',
|
||||
'{"system_prompt": "你是公安系统公文写作专家,熟悉公安机关各类文书格式和规范用语。请根据用户提供的信息,生成规范、准确、专业的警务文书。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "report_type", "label": "报告类型", "type": "select", "options": ["接处警记录", "案件受理报告", "巡逻工作日志", "专项行动总结", "治安形势分析"], "required": true}, {"key": "time_place", "label": "时间地点", "type": "text", "placeholder": "如:2024年3月15日14时30分,XX路XX号", "required": true}, {"key": "parties", "label": "涉及人员", "type": "textarea", "placeholder": "当事人、嫌疑人、证人等信息"}, {"key": "details", "label": "事件经过", "type": "textarea", "placeholder": "详细描述事件经过、处置措施等", "required": true}, {"key": "result", "label": "处理结果", "type": "textarea", "placeholder": "处置结果、后续措施等"}]}',
|
||||
'请选择报告类型并填写相关信息,我将为您生成规范的警务文书。',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 法律法规智能问答
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000003',
|
||||
'法律法规智能问答', 'law-qa-bot',
|
||||
'快速查询刑法、治安管理处罚法等法律条文及适用解释',
|
||||
'涵盖公安执法常用法律法规:\n- 中华人民共和国刑法\n- 治安管理处罚法\n- 道路交通安全法\n- 出入境管理法\n- 反电信网络诈骗法\n- 网络安全法\n- 相关司法解释',
|
||||
'⚖️', cat_policy, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', true, 890, 4.9, 102, '1.0',
|
||||
'{"system_prompt": "你是一位精通中国法律的法律顾问,特别熟悉公安机关执法相关的法律法规。请根据用户的问题,准确引用法律条文,并给出通俗易懂的解释。如果涉及复杂的法律问题,请建议咨询专业律师。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 3000}',
|
||||
'您好,我是法律法规智能问答系统。请输入您想了解的法律问题。',
|
||||
'["醉驾的法律后果和量刑标准是什么?", "盗窃罪的立案标准是多少?各地有区别吗?", "治安调解适用于哪些情况?调解不成怎么办?", "网络造谣传谣如何定性?需要承担什么法律责任?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 4. 反诈宣传内容生成
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000004',
|
||||
'反诈宣传内容生成', 'anti-fraud-content',
|
||||
'生成反电信诈骗宣传文案、短视频脚本、社区横幅标语',
|
||||
'为反诈宣传工作提供创意内容:\n- 社区宣传单页文案\n- 朋友圈/公众号推文\n- 短视频脚本\n- 横幅标语\n- 案例警示教育材料',
|
||||
'🛡️', cat_general, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', false, 156, 4.5, 23, '1.0',
|
||||
'{"system_prompt": "你是反电信诈骗宣传专家,善于用通俗易懂、生动有趣的方式编写反诈宣传材料。请根据用户需求生成相应的宣传内容。注意内容要贴近群众生活,易于传播。", "model": "qwen-plus", "temperature": 0.7, "max_tokens": 3000}',
|
||||
'需要什么类型的反诈宣传内容?我来帮您创作。',
|
||||
'["写一段针对老年人的刷单诈骗预防宣传文案", "生成一个反杀猪盘诈骗的短视频脚本,2分钟以内", "设计10条社区反诈横幅标语,要求朗朗上口"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 5. 户籍业务办理指南
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000005',
|
||||
'户籍业务办理指南', 'huji-guide',
|
||||
'户口迁移、身份证办理、居住证等户籍业务智能咨询',
|
||||
'覆盖常见户籍业务:\n- 户口迁入迁出\n- 身份证首次办理/换领/补领\n- 居住证办理\n- 户口本补办\n- 新生儿入户\n- 集体户口管理',
|
||||
'🏠', cat_service, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', true, 1203, 4.6, 89, '1.0',
|
||||
'{"system_prompt": "你是户籍业务窗口智能客服,熟悉各类户籍业务的办理流程、所需材料和注意事项。请耐心解答群众关于户籍业务的疑问,提供准确的办理指南。如有地区差异请提醒用户确认当地政策。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 2000}',
|
||||
'您好!请问需要咨询什么户籍业务?',
|
||||
'["跨省户口迁移需要什么材料和流程?", "身份证过期了怎么换领?可以异地办理吗?", "新生儿上户口需要哪些材料?有时间限制吗?", "居住证办理条件是什么?多久能拿到?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 6. 交通事故责任判定
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000006',
|
||||
'交通事故责任判定', 'traffic-accident',
|
||||
'根据事故情况分析责任划分,提供处理流程指导',
|
||||
'交通事故处理辅助工具:\n- 事故责任初步判定\n- 处理流程指导\n- 赔偿标准参考\n- 保险理赔指引\n- 相关法条引用',
|
||||
'🚗', cat_service, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', false, 267, 4.4, 34, '1.0',
|
||||
'{"system_prompt": "你是交通事故处理专家,熟悉道路交通安全法及相关规定。请根据用户描述的事故情况,分析可能的责任划分,并提供处理建议。注意:分析仅供参考,正式责任认定以交警部门出具的事故认定书为准。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 3000}',
|
||||
'请描述交通事故的具体情况,我来帮您分析。',
|
||||
'["十字路口追尾,前车突然急刹车", "变道时与直行车辆发生刮擦", "非机动车闯红灯被机动车撞到", "停车场内倒车时碰到后方车辆"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 7. 值班排班助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000007',
|
||||
'值班排班助手', 'duty-scheduler',
|
||||
'智能生成值班表、排班方案,兼顾公平性和业务需求',
|
||||
'公安值班排班智能助手:\n- 自动生成月度值班表\n- 兼顾节假日和特殊勤务\n- 考虑人员休假和调班\n- 确保关键岗位24小时覆盖',
|
||||
'📅', cat_hr, creator_id, gongan_org,
|
||||
'workflow', 'approved', 'public', false, 98, 4.3, 12, '1.0',
|
||||
'{"system_prompt": "你是排班管理专家,请根据人员名单和排班要求,生成合理的值班排班表。注意确保:1.每人工作量基本均衡 2.关键岗位不空缺 3.符合劳动法规定 4.兼顾节假日值班补偿。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 4000, "app_type": "workflow", "steps": [{"key": "personnel", "label": "人员名单", "type": "textarea", "placeholder": "请输入参与排班的人员名单,每行一人", "required": true}, {"key": "period", "label": "排班周期", "type": "text", "placeholder": "如:2024年4月", "required": true}, {"key": "shifts", "label": "班次设置", "type": "textarea", "placeholder": "如:白班(8:00-16:00)、中班(16:00-24:00)、夜班(0:00-8:00)", "required": true}, {"key": "rules", "label": "特殊规则", "type": "textarea", "placeholder": "如:张三4月5日请假、节假日双人值班等"}]}',
|
||||
'请填写排班信息,我将为您生成合理的值班方案。',
|
||||
NULL,
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 8. 治安形势分析
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000008',
|
||||
'治安形势分析报告', 'security-analysis',
|
||||
'根据警情数据生成辖区治安形势分析报告',
|
||||
'治安态势智能研判:\n- 警情数据趋势分析\n- 重点区域/时段识别\n- 案件类型分布统计\n- 防控建议生成\n- 同比环比对比',
|
||||
'📊', cat_data, creator_id, gongan_org,
|
||||
'workflow', 'approved', 'public', true, 187, 4.6, 28, '1.0',
|
||||
'{"system_prompt": "你是治安形势分析专家。请根据提供的警情数据和时间范围,撰写专业的治安形势分析报告,包括:数据概述、趋势分析、重点问题、原因分析、对策建议等部分。报告语言要规范专业。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "area", "label": "辖区范围", "type": "text", "placeholder": "如:XX区/XX派出所辖区", "required": true}, {"key": "period", "label": "分析周期", "type": "text", "placeholder": "如:2024年第一季度", "required": true}, {"key": "data", "label": "警情数据", "type": "textarea", "placeholder": "请粘贴警情统计数据(案件类型、数量、时间分布等)", "required": true}, {"key": "focus", "label": "关注重点", "type": "textarea", "placeholder": "如:电信诈骗上升、夜间盗窃多发等"}]}',
|
||||
'请提供辖区警情数据,我将为您生成治安形势分析报告。',
|
||||
NULL,
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 9. 出入境业务咨询
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000009',
|
||||
'出入境业务咨询', 'exit-entry-service',
|
||||
'护照、签证、港澳通行证等出入境业务办理指南',
|
||||
'出入境证件办理全指南:\n- 普通护照办理/换发/补发\n- 港澳通行证及签注\n- 台湾通行证及签注\n- 外国人签证延期\n- 永久居留申请',
|
||||
'✈️', cat_service, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', false, 445, 4.5, 56, '1.0',
|
||||
'{"system_prompt": "你是出入境业务窗口智能客服,熟悉各类出入境证件的办理流程、所需材料、费用标准和办理时限。请耐心准确地解答群众咨询。如有最新政策变动,请提醒用户以窗口告知为准。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 2000}',
|
||||
'您好!请问需要咨询什么出入境业务?',
|
||||
'["首次办理护照需要什么材料?", "港澳通行证签注用完了怎么续签?", "护照快过期了,还能出国吗?什么时候换?", "外国人签证到期怎么延期?"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 10. 笔录模板助手
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||
app_config, welcome_message, suggested_prompts, published_at)
|
||||
VALUES (
|
||||
'b0000000-0000-0000-0000-000000000010',
|
||||
'笔录模板助手', 'statement-template',
|
||||
'根据案件类型生成规范的询问/讯问笔录模板',
|
||||
'辅助民警制作规范笔录:\n- 询问笔录模板(证人、被害人)\n- 讯问笔录模板(嫌疑人)\n- 辨认笔录\n- 勘验笔录\n- 关键问题提示',
|
||||
'📝', cat_writing, creator_id, gongan_org,
|
||||
'chatbot', 'approved', 'public', false, 234, 4.7, 41, '1.0',
|
||||
'{"system_prompt": "你是公安法制部门的笔录审核专家,熟悉各类笔录的规范格式和必备要素。请根据案件类型和对象,生成规范的笔录模板,并标注关键问题要点和注意事项。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 4000}',
|
||||
'请告诉我案件类型和笔录对象,我来生成规范模板。',
|
||||
'["盗窃案被害人询问笔录模板", "故意伤害嫌疑人讯问笔录要点", "交通肇事案证人询问笔录", "电信诈骗案受害人报案笔录模板"]',
|
||||
NOW()
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
END $$;
|
||||
@@ -0,0 +1,44 @@
|
||||
-- 公安局专属分类数据
|
||||
-- 为公安局机构创建独立的分类,使应用分类在公安局视角下可见
|
||||
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
gongan_org UUID := 'a0000000-0000-0000-0000-000000000002';
|
||||
BEGIN
|
||||
-- 插入公安局专属分类(复制基础分类 + 新增公安专属分类)
|
||||
INSERT INTO categories (name, slug, icon, sort_order, org_id) VALUES
|
||||
('公文写作', 'gongan-writing', 'file-text', 1, gongan_org),
|
||||
('政策解读', 'gongan-policy', 'book-open', 2, gongan_org),
|
||||
('便民服务', 'gongan-service', 'headphones', 3, gongan_org),
|
||||
('数据治理', 'gongan-data', 'bar-chart', 4, gongan_org),
|
||||
('组织人事', 'gongan-hr', 'users', 5, gongan_org),
|
||||
('综合应用', 'gongan-general', 'more-horizontal', 99, gongan_org)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- 将公安局的应用关联到新的公安分类
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-data')
|
||||
WHERE slug = 'case-analysis' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-writing')
|
||||
WHERE slug = 'police-report-writer' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-policy')
|
||||
WHERE slug = 'law-qa-bot' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-general')
|
||||
WHERE slug = 'anti-fraud-content' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-service')
|
||||
WHERE slug IN ('huji-guide', 'traffic-accident', 'exit-entry-service') AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-hr')
|
||||
WHERE slug = 'duty-scheduler' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-data')
|
||||
WHERE slug = 'security-analysis' AND org_id = gongan_org;
|
||||
|
||||
UPDATE applications SET category_id = (SELECT id FROM categories WHERE slug = 'gongan-writing')
|
||||
WHERE slug = 'statement-template' AND org_id = gongan_org;
|
||||
|
||||
END $$;
|
||||
@@ -0,0 +1,50 @@
|
||||
-- 对现有公安局知识库文档执行分片(纯文本分片,不含embedding)
|
||||
-- 使用 regexp_split_to_table 按段落分割,再合并为 ~500 字的 chunk
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
doc RECORD;
|
||||
para RECORD;
|
||||
current_chunk TEXT := '';
|
||||
chunk_idx INTEGER := 0;
|
||||
chunk_size INTEGER := 500;
|
||||
BEGIN
|
||||
FOR doc IN
|
||||
SELECT kd.id AS doc_id, kd.kb_id, kd.content, kd.name
|
||||
FROM knowledge_documents kd
|
||||
JOIN knowledge_bases kb ON kd.kb_id = kb.id
|
||||
WHERE kb.org_id = 'a0000000-0000-0000-0000-000000000002'
|
||||
AND kd.content IS NOT NULL AND kd.content != ''
|
||||
LOOP
|
||||
DELETE FROM knowledge_chunks WHERE doc_id = doc.doc_id;
|
||||
current_chunk := '';
|
||||
chunk_idx := 0;
|
||||
|
||||
-- 按段落(双换行或单换行)拆分
|
||||
FOR para IN
|
||||
SELECT unnest(regexp_split_to_array(doc.content, E'\n')) AS line
|
||||
LOOP
|
||||
IF char_length(current_chunk) + char_length(para.line) + 1 > chunk_size AND current_chunk != '' THEN
|
||||
INSERT INTO knowledge_chunks (kb_id, doc_id, chunk_index, content, char_count)
|
||||
VALUES (doc.kb_id, doc.doc_id, chunk_idx, trim(current_chunk), char_length(trim(current_chunk)));
|
||||
chunk_idx := chunk_idx + 1;
|
||||
current_chunk := '';
|
||||
END IF;
|
||||
IF current_chunk = '' THEN
|
||||
current_chunk := para.line;
|
||||
ELSE
|
||||
current_chunk := current_chunk || E'\n' || para.line;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- 最后剩余的内容
|
||||
IF char_length(trim(current_chunk)) > 0 THEN
|
||||
INSERT INTO knowledge_chunks (kb_id, doc_id, chunk_index, content, char_count)
|
||||
VALUES (doc.kb_id, doc.doc_id, chunk_idx, trim(current_chunk), char_length(trim(current_chunk)));
|
||||
chunk_idx := chunk_idx + 1;
|
||||
END IF;
|
||||
|
||||
UPDATE knowledge_documents SET chunk_count = chunk_idx WHERE id = doc.doc_id;
|
||||
RAISE NOTICE '文档 [%] 已分片: % chunks', doc.name, chunk_idx;
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -0,0 +1,458 @@
|
||||
-- 公安局知识库及文档种子数据
|
||||
-- 可重复执行
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
creator_id UUID;
|
||||
gongan_org UUID := 'a0000000-0000-0000-0000-000000000002';
|
||||
kb_law_id UUID := 'c0000000-0000-0000-0000-000000000001';
|
||||
kb_sop_id UUID := 'c0000000-0000-0000-0000-000000000002';
|
||||
BEGIN
|
||||
SELECT id INTO creator_id FROM users WHERE email = 'zhangdui@govai.gov.cn';
|
||||
|
||||
-- ========== 知识库 ==========
|
||||
|
||||
-- 1. 公安法律法规知识库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_law_id,
|
||||
'公安法律法规库',
|
||||
'收录公安机关执法常用的法律法规、司法解释、部门规章等,涵盖刑法、治安管理处罚法、道路交通安全法、反诈骗法等',
|
||||
creator_id, gongan_org, 'department', 8, 50000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 公安业务操作规程知识库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_sop_id,
|
||||
'公安业务操作规程',
|
||||
'收录接处警规范、执法办案流程、户籍业务规程、出入境业务标准等操作规范文件',
|
||||
creator_id, gongan_org, 'department', 6, 35000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 法律法规文档 ==========
|
||||
|
||||
-- 治安管理处罚法(核心条文摘录)
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000001', kb_law_id,
|
||||
'中华人民共和国治安管理处罚法', 'txt', 8000, 'completed', creator_id,
|
||||
'中华人民共和国治安管理处罚法(2012年修正)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为维护社会治安秩序,保障公共安全,保护公民、法人和其他组织的合法权益,规范和保障公安机关及其人民警察依法履行治安管理职责,制定本法。
|
||||
|
||||
第二条 扰乱公共秩序,妨害公共安全,侵犯人身权利、财产权利,妨害社会管理,具有社会危害性,依照《中华人民共和国刑法》的规定构成犯罪的,依法追究刑事责任;尚不够刑事处罚的,由公安机关依照本法给予治安管理处罚。
|
||||
|
||||
第三章 违反治安管理的行为和处罚
|
||||
|
||||
第二十三条 有下列行为之一的,处警告或者二百元以下罚款;情节较重的,处五日以上十日以下拘留,可以并处五百元以下罚款:
|
||||
(一)扰乱机关、团体、企业、事业单位秩序,致使工作、生产、营业、医疗、教学、科研不能正常进行,尚未造成严重损失的;
|
||||
(二)扰乱车站、港口、码头、机场、商场、公园、展览馆或者其他公共场所秩序的;
|
||||
(三)扰乱公共汽车、电车、火车、船舶、航空器或者其他公共交通工具上的秩序的;
|
||||
(四)非法拦截或者强登、扒乘机动车、船舶、航空器以及其他交通工具,影响交通工具正常行驶的;
|
||||
(五)破坏依法进行的选举秩序的。
|
||||
|
||||
第四十二条 有下列行为之一的,处五日以下拘留或者五百元以下罚款;情节较重的,处五日以上十日以下拘留,可以并处五百元以下罚款:
|
||||
(一)写恐吓信或者以其他方法威胁他人人身安全的;
|
||||
(二)公然侮辱他人或者捏造事实诽谤他人的;
|
||||
(三)捏造事实诬告陷害他人,企图使他人受到刑事追究或者受到治安管理处罚的;
|
||||
(四)对证人及其近亲属进行威胁、侮辱、殴打或者打击报复的;
|
||||
(五)多次发送淫秽、侮辱、恐吓或者其他信息,干扰他人正常生活的;
|
||||
(六)偷窥、偷拍、窃听、散布他人隐私的。
|
||||
|
||||
第四十三条 殴打他人的,或者故意伤害他人身体的,处五日以上十日以下拘留,并处二百元以上五百元以下罚款;情节较轻的,处五日以下拘留或者五百元以下罚款。
|
||||
|
||||
第四十九条 盗窃、诈骗、哄抢、抢夺、敲诈勒索或者故意损毁公私财物的,处五日以上十日以下拘留,可以并处五百元以下罚款;情节较重的,处十日以上十五日以下拘留,可以并处一千元以下罚款。
|
||||
|
||||
第七十二条 有下列行为之一的,处十日以上十五日以下拘留,可以并处二千元以下罚款;情节较轻的,处五日以下拘留或者五百元以下罚款:
|
||||
(一)非法持有鸦片不满二百克、海洛因或者甲基苯丙胺不满十克或者其他少量毒品的;
|
||||
(二)向他人提供毒品的;
|
||||
(三)吸食、注射毒品的;
|
||||
(四)胁迫、欺骗医务人员开具麻醉药品、精神药品的。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 刑法盗窃罪相关条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000002', kb_law_id,
|
||||
'刑法-侵犯财产罪(摘录)', 'txt', 5000, 'completed', creator_id,
|
||||
'中华人民共和国刑法 第五章 侵犯财产罪
|
||||
|
||||
第二百六十三条 【抢劫罪】以暴力、胁迫或者其他方法抢劫公私财物的,处三年以上十年以下有期徒刑,并处罚金;有下列情形之一的,处十年以上有期徒刑、无期徒刑或者死刑,并处罚金或者没收财产。
|
||||
|
||||
第二百六十四条 【盗窃罪】盗窃公私财物,数额较大的,或者多次盗窃、入户盗窃、携带凶器盗窃、扒窃的,处三年以下有期徒刑、拘役或者管制,并处或者单处罚金;数额巨大或者有其他严重情节的,处三年以上十年以下有期徒刑,并处罚金;数额特别巨大或者有其他特别严重情节的,处十年以上有期徒刑或者无期徒刑,并处罚金或者没收财产。
|
||||
|
||||
盗窃罪立案标准:
|
||||
- 数额较大:一般为1000-3000元以上(各省标准不同)
|
||||
- 数额巨大:一般为3万-10万元以上
|
||||
- 数额特别巨大:一般为30万-50万元以上
|
||||
|
||||
第二百六十六条 【诈骗罪】诈骗公私财物,数额较大的,处三年以下有期徒刑、拘役或者管制,并处或者单处罚金;数额巨大或者有其他严重情节的,处三年以上十年以下有期徒刑,并处罚金;数额特别巨大或者有其他特别严重情节的,处十年以上有期徒刑或者无期徒刑,并处罚金或者没收财产。
|
||||
|
||||
诈骗罪立案标准:
|
||||
- 数额较大:3000元至1万元以上
|
||||
- 数额巨大:3万元至10万元以上
|
||||
- 数额特别巨大:50万元以上
|
||||
|
||||
第二百七十五条 【故意毁坏财物罪】故意毁坏公私财物,数额较大或者有其他严重情节的,处三年以下有期徒刑、拘役或者罚金;数额巨大或者有其他特别严重情节的,处三年以上七年以下有期徒刑。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 道路交通安全法
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000003', kb_law_id,
|
||||
'道路交通安全法(摘录)', 'txt', 6000, 'completed', creator_id,
|
||||
'中华人民共和国道路交通安全法(2021年修正)
|
||||
|
||||
第二十二条 机动车驾驶人应当遵守道路交通安全法律、法规的规定,按照操作规范安全驾驶、文明驾驶。饮酒、服用国家管制的精神药品或者麻醉药品,或者患有妨碍安全驾驶机动车的疾病,或者过度疲劳影响安全驾驶的,不得驾驶机动车。
|
||||
|
||||
第九十一条 饮酒后驾驶机动车的,处暂扣六个月机动车驾驶证,并处一千元以上二千元以下罚款。因饮酒后驾驶机动车被处罚,再次饮酒后驾驶机动车的,处十日以下拘留,并处一千元以上二千元以下罚款,吊销机动车驾驶证。
|
||||
|
||||
醉酒驾驶机动车的,由公安机关交通管理部门约束至酒醒,吊销机动车驾驶证,依法追究刑事责任;五年内不得重新取得机动车驾驶证。
|
||||
|
||||
醉驾标准:血液中酒精含量≥80mg/100ml
|
||||
饮酒驾车标准:血液中酒精含量≥20mg/100ml且<80mg/100ml
|
||||
|
||||
第七十六条 机动车发生交通事故造成人身伤亡、财产损失的,由保险公司在机动车第三者责任强制保险责任限额范围内予以赔偿。
|
||||
|
||||
交通事故责任划分原则:
|
||||
- 全部责任:一方当事人的违法行为造成交通事故的
|
||||
- 主要责任:一方当事人的违法行为是造成交通事故的主要原因
|
||||
- 同等责任:双方当事人的违法行为共同造成交通事故
|
||||
- 次要责任:一方当事人的违法行为是造成交通事故的次要原因
|
||||
- 无责任:不是因当事人的违法行为造成的
|
||||
|
||||
第一百零一条 违反道路交通安全法律、法规的规定,发生重大交通事故,构成犯罪的,依法追究刑事责任,并由公安机关交通管理部门吊销机动车驾驶证。造成交通事故后逃逸的,由公安机关交通管理部门吊销机动车驾驶证,且终生不得重新取得机动车驾驶证。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 反电信网络诈骗法
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000004', kb_law_id,
|
||||
'反电信网络诈骗法(摘录)', 'txt', 5000, 'completed', creator_id,
|
||||
'中华人民共和国反电信网络诈骗法(2022年12月1日施行)
|
||||
|
||||
第一条 为了预防、遏制和惩治电信网络诈骗活动,加强反电信网络诈骗工作,保护公民和组织的合法权益,维护社会稳定和国家安全,制定本法。
|
||||
|
||||
第二条 本法所称电信网络诈骗,是指以非法占有为目的,利用电信网络技术手段,通过远程、非接触等方式,诈骗公私财物的行为。
|
||||
|
||||
常见电信网络诈骗类型:
|
||||
1. 刷单返利诈骗:以高额佣金为诱饵,让受害人垫付资金
|
||||
2. 虚假投资理财:虚假平台诱导投资,前期给予小额收益
|
||||
3. 冒充公检法:冒充警察、检察官等要求转账至"安全账户"
|
||||
4. 杀猪盘:通过网恋培养感情后诱导投资
|
||||
5. 冒充客服退款:以退款为由获取银行卡信息
|
||||
6. 网络贷款诈骗:以办理贷款为由收取手续费
|
||||
|
||||
第二十五条 任何单位和个人不得为他人实施电信网络诈骗活动提供下列支持或者帮助:
|
||||
(一)出售、提供个人信息;
|
||||
(二)帮助他人通过虚拟货币交易等方式洗钱;
|
||||
(三)其他为电信网络诈骗活动提供支持或者帮助的行为。
|
||||
|
||||
第三十一条 公安机关应当建立完善电信网络诈骗涉案资金即时查询、紧急止付、快速冻结、及时解冻和资金返还制度。
|
||||
|
||||
第三十八条 组织、策划、实施、参与电信网络诈骗活动或者为电信网络诈骗活动提供帮助,构成犯罪的,依法追究刑事责任。
|
||||
|
||||
预防措施提醒:
|
||||
- 不轻信陌生来电和短信
|
||||
- 不随意点击不明链接
|
||||
- 不向陌生人转账汇款
|
||||
- 不泄露个人信息和验证码
|
||||
- 遇到可疑情况拨打96110全国反诈专线'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 户籍管理规定
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000005', kb_law_id,
|
||||
'户口登记管理规定', 'txt', 5000, 'completed', creator_id,
|
||||
'户口登记管理相关规定
|
||||
|
||||
一、新生儿入户
|
||||
办理条件:新生儿出生后一个月内
|
||||
所需材料:
|
||||
1. 父母双方户口簿
|
||||
2. 父母双方身份证
|
||||
3. 出生医学证明
|
||||
4. 结婚证(非婚生子女提供亲子鉴定)
|
||||
办理时限:即时办理
|
||||
|
||||
二、户口迁移
|
||||
(一)市内迁移
|
||||
所需材料:迁移人户口簿、身份证、迁入地证明
|
||||
办理时限:即时办理
|
||||
|
||||
(二)跨省迁移
|
||||
所需材料:
|
||||
1. 迁移人户口簿、身份证
|
||||
2. 迁入地准迁证(或电子准迁证)
|
||||
3. 迁移原因证明(工作调动、购房、投靠亲属等)
|
||||
办理流程:
|
||||
- 第一步:到迁入地派出所申请准迁证
|
||||
- 第二步:到原户籍地办理迁出手续
|
||||
- 第三步:到迁入地办理落户
|
||||
办理时限:一般15-30个工作日
|
||||
|
||||
三、身份证办理
|
||||
(一)首次申领
|
||||
条件:年满16周岁公民
|
||||
材料:户口簿
|
||||
时限:30个工作日(偏远地区60日)
|
||||
|
||||
(二)换领/补领
|
||||
材料:户口簿或旧身份证
|
||||
异地办理:可在居住地任一户籍派出所办理
|
||||
时限:60个工作日
|
||||
|
||||
四、居住证办理
|
||||
条件:在居住地居住半年以上,有合法稳定就业、住所或连续就读
|
||||
材料:
|
||||
1. 身份证
|
||||
2. 居住证明(租房合同/房产证/单位证明)
|
||||
3. 近期照片
|
||||
时限:15个工作日
|
||||
有效期:1年,每年签注一次'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 出入境管理规定
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000006', kb_law_id,
|
||||
'出入境证件办理规定', 'txt', 4500, 'completed', creator_id,
|
||||
'出入境证件办理指南
|
||||
|
||||
一、普通护照
|
||||
(一)首次办理
|
||||
条件:中国公民
|
||||
材料:身份证、户口簿、照片
|
||||
费用:120元
|
||||
时限:7个工作日(加急5个工作日,加费用)
|
||||
|
||||
(二)换发
|
||||
条件:护照有效期不足一年、签证页用完、损坏
|
||||
材料:原护照、身份证、照片
|
||||
费用:120元
|
||||
|
||||
(三)补发
|
||||
条件:护照遗失或被盗
|
||||
材料:身份证、户口簿、遗失声明、照片
|
||||
费用:120元
|
||||
|
||||
二、港澳通行证
|
||||
(一)新办
|
||||
材料:身份证、户口簿、照片
|
||||
费用:60元(证件)+ 签注费
|
||||
时限:7个工作日
|
||||
|
||||
(二)签注类型
|
||||
- 个人旅游(G签):适用开放个人游城市居民
|
||||
- 团队旅游(L签):其他城市居民
|
||||
- 商务(S签):因公需要
|
||||
- 探亲(T签):探望在港澳亲属
|
||||
- 逗留(D签):就学、就业
|
||||
|
||||
(三)签注续签
|
||||
自助机办理:立等可取(部分城市)
|
||||
窗口办理:7个工作日
|
||||
|
||||
三、台湾通行证
|
||||
材料:身份证、户口簿、照片
|
||||
费用:60元(证件)+ 签注费20元
|
||||
签注类型:个人旅游、团队旅游、应邀、商务
|
||||
|
||||
四、注意事项
|
||||
- 16周岁以下需监护人陪同
|
||||
- 国家工作人员需单位意见
|
||||
- 办理前可通过"移民局"APP预约
|
||||
- 可跨省异地办理(需居住证或社保证明)'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 醉驾处理标准
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000007', kb_law_id,
|
||||
'醉驾案件办理指引', 'txt', 4000, 'completed', creator_id,
|
||||
'醉驾案件办理工作指引
|
||||
|
||||
一、法律依据
|
||||
- 《刑法》第一百三十三条之一(危险驾驶罪)
|
||||
- 《道路交通安全法》第九十一条
|
||||
- 最高人民法院、最高人民检察院、公安部《关于办理醉酒驾驶机动车刑事案件适用法律若干问题的意见》
|
||||
|
||||
二、认定标准
|
||||
- 醉酒标准:血液酒精含量(BAC)≥ 80mg/100ml
|
||||
- 饮酒标准:20mg/100ml ≤ BAC < 80mg/100ml
|
||||
|
||||
三、量刑标准
|
||||
危险驾驶罪:处拘役,并处罚金
|
||||
- BAC 80-150mg/100ml:拘役1-2个月
|
||||
- BAC 150-200mg/100ml:拘役2-3个月
|
||||
- BAC 200-300mg/100ml:拘役3-4个月
|
||||
- BAC 300mg/100ml以上:拘役4-6个月
|
||||
|
||||
从重处罚情节:
|
||||
1. 造成交通事故且负主要或全部责任
|
||||
2. 在高速公路、城市快速路上驾驶
|
||||
3. 驾驶营运客车、重型货车
|
||||
4. 严重超速、超载
|
||||
5. 逃避检查或抗拒检测
|
||||
6. 曾因酒驾受过行政处罚或刑事追究
|
||||
|
||||
四、办案流程
|
||||
1. 现场查获 → 呼气检测
|
||||
2. 呼气≥80mg → 抽血送检
|
||||
3. 血检结果确认 → 立案侦查
|
||||
4. 制作讯问笔录
|
||||
5. 扣押机动车、驾驶证
|
||||
6. 移送起诉
|
||||
|
||||
五、行政处罚
|
||||
- 饮酒驾车:暂扣驾驶证6个月 + 罚款1000-2000元
|
||||
- 再次饮酒驾车:拘留10日 + 罚款1000-2000元 + 吊销驾驶证
|
||||
- 醉酒驾车:吊销驾驶证 + 5年内不得重新取得'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 网络安全管理
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000008', kb_law_id,
|
||||
'网络安全与信息犯罪法规', 'txt', 4000, 'completed', creator_id,
|
||||
'网络安全与信息犯罪相关法规摘要
|
||||
|
||||
一、《网络安全法》要点
|
||||
第四十四条 任何个人和组织不得窃取或者以其他非法方式获取个人信息,不得非法出售或者非法向他人提供个人信息。
|
||||
第四十六条 任何个人和组织应当对其使用网络的行为负责,不得设立用于实施诈骗,传授犯罪方法,制作或者销售违禁物品、管制物品等违法犯罪活动的网站、通讯群组。
|
||||
|
||||
二、常见网络犯罪类型及量刑
|
||||
|
||||
1. 侵犯公民个人信息罪(刑法第253条之一)
|
||||
- 出售或提供行踪轨迹信息50条以上
|
||||
- 出售或提供住宿、通信、财产信息500条以上
|
||||
- 出售或提供其他个人信息5000条以上
|
||||
- 处三年以下有期徒刑,情节特别严重处三年以上七年以下
|
||||
|
||||
2. 帮助信息网络犯罪活动罪(刑法第287条之二)
|
||||
- 为犯罪提供互联网接入、服务器托管等技术支持
|
||||
- 为犯罪提供广告推广、支付结算等帮助
|
||||
- 出租出售银行卡、手机卡("两卡"犯罪)
|
||||
- 处三年以下有期徒刑或者拘役,并处罚金
|
||||
|
||||
3. 网络造谣传谣
|
||||
- 编造虚假信息在网络上散布(治安处罚:5-10日拘留)
|
||||
- 编造虚假险情、疫情等严重扰乱社会秩序(刑法第293条之一:处三年以下有期徒刑)
|
||||
- 认定标准:同一信息被转发500次以上或浏览5000次以上
|
||||
|
||||
三、管辖规定
|
||||
- 网络犯罪案件由犯罪地公安机关管辖
|
||||
- 犯罪地包括:犯罪行为发生地、结果发生地
|
||||
- 涉及多地的,由最初受理的公安机关或主要犯罪地管辖'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 操作规程文档 ==========
|
||||
|
||||
-- 接处警规范
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000009', kb_sop_id,
|
||||
'110接处警工作规范', 'txt', 5000, 'completed', creator_id,
|
||||
'110接处警工作规范
|
||||
|
||||
一、接警要求
|
||||
1. 铃响三声内接听
|
||||
2. 使用规范用语:"您好,110"
|
||||
3. 详细记录:时间、地点、事件性质、报警人信息
|
||||
4. 重大紧急警情立即上报值班领导
|
||||
|
||||
二、出警规范
|
||||
1. 接到指令后5分钟内出发
|
||||
2. 城区一般警情15分钟内到达
|
||||
3. 紧急警情就近警力先期处置
|
||||
4. 到达现场后第一时间报告情况
|
||||
|
||||
三、现场处置
|
||||
1. 优先保护人身安全
|
||||
2. 控制现场、保护证据
|
||||
3. 分离当事人、分别询问
|
||||
4. 危险警情请求支援
|
||||
|
||||
四、处置结果
|
||||
1. 当场调解:制作调解协议,双方签字
|
||||
2. 口头警告:告知违法行为,记录在案
|
||||
3. 行政处罚:依法告知权利义务,制作处罚决定
|
||||
4. 刑事立案:保护现场、固定证据、控制嫌疑人
|
||||
5. 移交处理:非公安职责的引导至相关部门
|
||||
|
||||
五、工作纪律
|
||||
1. 不得推诿、拒绝接处警
|
||||
2. 不得泄露报警人信息
|
||||
3. 不得将非紧急求助拒之门外
|
||||
4. 处警结果必须如实记录、录入系统'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 执法办案流程
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'c1000000-0000-0000-0000-000000000010', kb_sop_id,
|
||||
'治安案件办理流程', 'txt', 4500, 'completed', creator_id,
|
||||
'治安案件办理流程规范
|
||||
|
||||
一、受案
|
||||
1. 对群众报案、控告、举报、扭送、投案,当场受理
|
||||
2. 制作受案登记表
|
||||
3. 24小时内进行审查,决定是否立案
|
||||
|
||||
二、调查取证
|
||||
1. 询问被侵害人(制作询问笔录)
|
||||
2. 询问证人(分别进行,单独制作笔录)
|
||||
3. 询问违法嫌疑人(2名以上办案人员)
|
||||
4. 勘验现场、检查(必要时)
|
||||
5. 收集物证书证、视频监控等
|
||||
|
||||
三、处理决定
|
||||
1. 确有违法事实:作出治安处罚决定
|
||||
2. 情节轻微:给予警告或口头教育
|
||||
3. 适用调解:征得双方同意后进行调解
|
||||
4. 不构成违反治安管理:不予处罚
|
||||
|
||||
四、告知程序
|
||||
1. 作出处罚前:告知当事人违法事实、处罚理由和依据
|
||||
2. 告知当事人依法享有的陈述权、申辩权
|
||||
3. 听取当事人的陈述和申辩
|
||||
4. 送达处罚决定书(当面送达或邮寄)
|
||||
|
||||
五、执行
|
||||
1. 罚款:出具缴款通知书,15日内缴纳
|
||||
2. 行政拘留:24小时内送拘留所执行
|
||||
3. 拘留前通知家属(除无法通知外)
|
||||
|
||||
六、期限要求
|
||||
- 一般案件:自受案之日起30日内办结
|
||||
- 案情重大复杂:经批准可延长30日
|
||||
- 涉及鉴定:鉴定期间不计入办案期限'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 关联应用到知识库 ==========
|
||||
|
||||
-- 法律法规智能问答 → 法律法规知识库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id
|
||||
WHERE id = 'b0000000-0000-0000-0000-000000000003';
|
||||
|
||||
-- 案件分析助手 → 法律法规知识库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id
|
||||
WHERE id = 'b0000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- 交通事故责任判定 → 法律法规知识库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id
|
||||
WHERE id = 'b0000000-0000-0000-0000-000000000006';
|
||||
|
||||
-- 户籍业务办理指南 → 法律法规知识库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id
|
||||
WHERE id = 'b0000000-0000-0000-0000-000000000005';
|
||||
|
||||
-- 出入境业务咨询 → 法律法规知识库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id
|
||||
WHERE id = 'b0000000-0000-0000-0000-000000000009';
|
||||
|
||||
END $$;
|
||||
@@ -0,0 +1,86 @@
|
||||
-- 科技局模拟数据 - 政策法规知识库
|
||||
-- Run: psql -d govai_portal -f seed_keji.sql
|
||||
|
||||
-- 为管理员创建"科技局政策法规知识库"
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, visibility, doc_count, status)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000001',
|
||||
'科技局政策法规库',
|
||||
'汇集科技创新、高新技术企业认定、科技成果转化、知识产权保护等领域的核心政策法规文件,为全局工作人员提供政策查询和决策参考。',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'public',
|
||||
12,
|
||||
'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入示例文档记录
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, file_size, uploader_id, indexing_status) VALUES
|
||||
('30000000-0000-0000-0000-000000000001', '20000000-0000-0000-0000-000000000001',
|
||||
'中华人民共和国科学技术进步法(2021修订).pdf', 'pdf', 2048000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000002', '20000000-0000-0000-0000-000000000001',
|
||||
'高新技术企业认定管理办法(国科发火〔2016〕32号).pdf', 'pdf', 1536000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000003', '20000000-0000-0000-0000-000000000001',
|
||||
'国家重点研发计划管理暂行办法.pdf', 'pdf', 1024000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000004', '20000000-0000-0000-0000-000000000001',
|
||||
'科技成果转化法实施细则.docx', 'docx', 768000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000005', '20000000-0000-0000-0000-000000000001',
|
||||
'企业研发费用加计扣除政策指引(2023版).pdf', 'pdf', 1280000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000006', '20000000-0000-0000-0000-000000000001',
|
||||
'关于进一步促进科技成果转移转化的若干措施.docx', 'docx', 512000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000007', '20000000-0000-0000-0000-000000000001',
|
||||
'知识产权强国建设纲要(2021-2035年).pdf', 'pdf', 1792000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000008', '20000000-0000-0000-0000-000000000001',
|
||||
'科技型中小企业评价办法.pdf', 'pdf', 896000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000009', '20000000-0000-0000-0000-000000000001',
|
||||
'国家自然科学基金条例.pdf', 'pdf', 640000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000010', '20000000-0000-0000-0000-000000000001',
|
||||
'本市科技创新"十四五"规划.pdf', 'pdf', 3072000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000011', '20000000-0000-0000-0000-000000000001',
|
||||
'科技项目经费管理办法.docx', 'docx', 480000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000012', '20000000-0000-0000-0000-000000000001',
|
||||
'技术合同认定登记管理办法.txt', 'txt', 256000,
|
||||
'00000000-0000-0000-0000-000000000001', 'completed')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 为王科长创建"项目申报材料库"
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, visibility, doc_count, status)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000002',
|
||||
'项目申报材料模板库',
|
||||
'包含各类科技项目申报书模板、预算编制指南、评审标准等参考资料,方便各科室申报项目时参考使用。',
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'department',
|
||||
5,
|
||||
'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, file_size, uploader_id, indexing_status) VALUES
|
||||
('30000000-0000-0000-0000-000000000013', '20000000-0000-0000-0000-000000000002',
|
||||
'重点研发项目申报书模板.docx', 'docx', 384000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000014', '20000000-0000-0000-0000-000000000002',
|
||||
'科技项目预算编制指南.xlsx', 'xlsx', 256000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000015', '20000000-0000-0000-0000-000000000002',
|
||||
'高企认定申报材料清单.pdf', 'pdf', 512000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000016', '20000000-0000-0000-0000-000000000002',
|
||||
'科技成果评价报告模板.docx', 'docx', 320000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed'),
|
||||
('30000000-0000-0000-0000-000000000017', '20000000-0000-0000-0000-000000000002',
|
||||
'项目验收报告撰写规范.md', 'md', 128000,
|
||||
'00000000-0000-0000-0000-000000000002', 'completed')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
SELECT '科技局知识库模拟数据插入成功!共2个知识库,17个文档记录。' AS status;
|
||||
@@ -0,0 +1,153 @@
|
||||
-- 律师行业 AI 应用种子数据
|
||||
-- 执行: psql -d govai -f seed_legal.sql
|
||||
|
||||
-- ========== 新增分类 ==========
|
||||
INSERT INTO categories (name, slug, icon, sort_order) VALUES
|
||||
('法律服务', 'legal-service', 'scale', 10)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- ========== Chatbot 对话型(3个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config, temperature, max_tokens) VALUES
|
||||
|
||||
('20000000-0000-0000-0000-000000000001', '法律法规检索', 'legal-research',
|
||||
'多轮对话查询法律法规、司法解释、典型案例,按法条精准引用',
|
||||
'## 功能介绍\n\n法律法规智能检索系统,为律师提供精准的法律条文查询服务:\n\n- 法律法规条文精准检索与解读\n- 最高院司法解释和指导案例查询\n- 法条适用范围和关联条款分析\n- 不同法规间的条文对比\n\n## 使用方法\n\n直接输入您需要查询的法律问题或法条关键词,系统将为您精准检索相关法律法规。',
|
||||
'scale',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是法律法规智能检索助手。我可以帮您查询中国现行法律法规、司法解释和典型案例,并提供法条解读。请输入您需要检索的法律问题。',
|
||||
'["《民法典》关于合同解除的条款有哪些?","最高院关于民间借贷利率的最新司法解释","劳动合同法中经济补偿金的计算标准"]',
|
||||
'{"system_prompt":"你是一个法律法规智能检索助手,熟悉中国现行法律法规、司法解释和典型案例。\n\n## 能力\n- 精准检索法律法规条文\n- 查询司法解释和指导案例\n- 解读法条含义和适用范围\n- 对比不同法规的关联条款\n\n## 输出格式\n- 引用法条使用标准格式:《法律名》第X条\n- 涉及多个法条时按法律层级排列:宪法 > 法律 > 行政法规 > 部门规章\n- 附注法条的生效日期和最新修订版本\n\n## 限制\n- 仅提供法律法规检索和解读,不提供具体案件的法律意见\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":4000}',
|
||||
0.3, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000002', '法律咨询助手', 'legal-consult',
|
||||
'面向律师的法律问题分析,给出法律意见框架,识别法律风险',
|
||||
'## 功能介绍\n\n面向执业律师的法律问题分析助手:\n\n- 分析法律问题的核心争议焦点\n- 给出法律意见框架和分析思路\n- 识别潜在法律风险和合规问题\n- 提供类案检索方向建议\n\n## 使用方法\n\n描述您的法律问题或案件概况,助手将帮您梳理法律关系、分析争议焦点并给出意见框架。',
|
||||
'message-square-text',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是法律咨询助手。我可以帮您分析法律问题、识别法律风险、给出法律意见框架。请描述您的法律问题。',
|
||||
'["帮我分析这个合同纠纷的法律关系和争议焦点","公司股东退出有哪些法律路径?","员工工伤赔偿的法律责任如何认定?"]',
|
||||
'{"system_prompt":"你是一个资深法律咨询助手,面向执业律师提供法律问题分析和意见框架。\n\n## 能力\n- 分析法律问题的核心争议焦点\n- 给出法律意见框架和分析思路\n- 识别潜在法律风险和合规问题\n- 提供类案检索方向建议\n\n## 输出格式\n- 先明确法律关系和适用法律\n- 逐项分析争议焦点\n- 引用相关法条:《法律名》第X条\n- 给出初步法律意见框架\n- 列出需要进一步核实的事项\n\n## 限制\n- 仅提供分析框架,不替代律师的专业判断\n- 不处理涉密案件信息\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":4000}',
|
||||
0.3, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000003', '合同条款审查', 'contract-review',
|
||||
'对话式逐条审查合同,识别风险条款、遗漏条款,给出修改建议',
|
||||
'## 功能介绍\n\n专业合同条款审查工具,为律师提供高效的合同审查辅助:\n\n- 对话式逐条审查合同文本\n- 识别高风险条款和遗漏条款\n- 检查条款合法性和有效性\n- 给出具体修改建议和替代条款\n\n## 使用方法\n\n将合同全文或关键条款粘贴发送,助手将逐条审查并标注风险等级。',
|
||||
'file-search',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'chatbot', 'app-placeholder',
|
||||
'您好!我是合同条款审查助手。请将合同文本发送给我,我将逐条审查并识别风险条款、遗漏条款,给出修改建议。',
|
||||
'["请审查这份买卖合同的关键条款","这份劳动合同有哪些风险条款?","帮我审查这份合作协议的违约责任条款"]',
|
||||
'{"system_prompt":"你是一个专业的合同条款审查助手,擅长逐条审查各类合同文本。\n\n## 能力\n- 逐条审查合同条款,识别风险条款\n- 发现遗漏条款和不完善之处\n- 检查条款的合法性和有效性\n- 给出具体的修改建议和替代条款\n\n## 输出格式\n- 按条款序号逐条审查\n- 风险等级标注:【高风险】【中风险】【低风险】【合规】\n- 每条给出:原文摘录 → 风险说明 → 修改建议\n- 最后给出审查总结\n\n## 限制\n- 仅提供合同条款层面的审查,不涉及商业决策建议\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":6000}',
|
||||
0.3, 8192)
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Completion 补全型(3个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, app_config, temperature, max_tokens) VALUES
|
||||
|
||||
('20000000-0000-0000-0000-000000000004', '法律文书生成', 'legal-doc-gen',
|
||||
'输入案情要素,生成起诉状、答辩状、代理词、法律意见书',
|
||||
'## 功能介绍\n\n法律文书智能生成工具,快速生成标准格式法律文书:\n\n- 起诉状、答辩状生成\n- 代理词、法律意见书撰写\n- 严格遵循法院要求的文书格式\n- 自动组织文书结构和法条引用\n\n## 使用方法\n\n在左侧输入案情要素(文书类型、当事人信息、案件事实、诉讼请求等),点击生成即可获得标准格式文书。',
|
||||
'file-pen-line',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个专业的法律文书撰写专家,熟悉各类法律文书的标准格式和写作规范。\n\n## 能力\n- 生成起诉状、答辩状、代理词、法律意见书等文书\n- 严格遵循法院和仲裁机构要求的文书格式\n- 根据案情要素自动组织文书结构\n\n## 输出格式\n- 使用标准法律文书格式\n- 包含完整的文书要素:标题、当事人信息、事实与理由、诉讼请求、证据清单\n- 法条引用格式:《法律名》第X条第X款\n- 使用Markdown格式便于排版\n\n## 限制\n- 生成的文书为模板性质,需律师根据实际情况修改完善\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","input_label":"案情要素","output_label":"法律文书","input_placeholder":"请输入案情要素,包括:\n1. 文书类型(起诉状/答辩状/代理词/法律意见书)\n2. 当事人信息(原告/被告)\n3. 案件事实概要\n4. 诉讼请求或答辩要点\n5. 主要证据材料"}',
|
||||
0.3, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000005', '案情摘要提取', 'case-abstract',
|
||||
'粘贴判决书或案卷材料,提取案情摘要、争议焦点、裁判要旨',
|
||||
'## 功能介绍\n\n案卷材料智能分析工具,快速提取案件关键信息:\n\n- 判决书案情摘要提取\n- 争议焦点和各方观点归纳\n- 裁判要旨和法律依据分析\n- 关键启示总结\n\n## 使用方法\n\n将判决书全文或案卷材料粘贴到输入框,点击生成即可获得结构化的案情摘要。',
|
||||
'scan-text',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个法律文书分析专家,擅长从判决书、案卷材料中提取关键信息。\n\n## 能力\n- 提取案情摘要和基本事实\n- 识别争议焦点和各方观点\n- 归纳裁判要旨和法律依据\n- 分析判决理由和适用法条\n\n## 输出格式\n请按以下结构输出:\n\n### 案情摘要(200字内)\n### 当事人信息\n### 争议焦点\n### 裁判要旨\n### 适用法条\n### 判决结果\n### 关键启示\n\n## 限制\n- 客观提取,不添加主观评价\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。","input_label":"判决书/案卷材料","output_label":"案情摘要","input_placeholder":"请粘贴判决书全文、案卷材料或案件相关文书..."}',
|
||||
0.3, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000006', '合同条款生成', 'contract-clause-gen',
|
||||
'输入业务场景,生成标准合同条款(含风险提示)',
|
||||
'## 功能介绍\n\n合同条款智能生成工具,根据业务场景快速生成标准条款:\n\n- 根据业务场景自动生成合同条款\n- 每条款附风险提示和注意事项\n- 提供甲方/乙方有利版本对比\n- 确保条款合法性和可执行性\n\n## 使用方法\n\n描述业务场景(合同类型、交易内容、重点关注条款等),点击生成即可获得标准合同条款。',
|
||||
'file-plus',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'completion', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个合同条款撰写专家,擅长根据业务场景生成标准合同条款。\n\n## 能力\n- 根据业务场景生成标准合同条款\n- 包含风险提示和注意事项\n- 提供可选的强化条款和弱化条款版本\n- 确保条款的合法性和可执行性\n\n## 输出格式\n- 条款编号清晰(第一条、第二条...)\n- 每条款后附【风险提示】\n- 提供【甲方有利版本】和【乙方有利版本】对比\n- 最后给出使用建议\n\n## 限制\n- 生成的条款为通用模板,需根据具体交易调整\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","input_label":"业务场景","output_label":"合同条款","input_placeholder":"请描述业务场景,包括:\n1. 合同类型(买卖、服务、租赁、合作等)\n2. 交易双方角色\n3. 核心交易内容\n4. 需要重点关注的条款(如违约、保密、竞业等)"}',
|
||||
0.4, 8192)
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Workflow 工作流型(2个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, app_config, temperature, max_tokens) VALUES
|
||||
|
||||
('20000000-0000-0000-0000-000000000007', '案件风险评估', 'case-risk-eval',
|
||||
'分步输入案件信息,多维度评估生成案件风险评估报告',
|
||||
'## 功能介绍\n\n案件风险智能评估系统,按流程引导完成案件分析:\n\n- 多维度风险评估:胜诉可能性、证据充分性、法律适用、执行风险\n- 分步输入案件信息,结构化收集关键要素\n- 生成专业的案件风险评估报告\n- 引用相关法条和类案参考\n\n## 使用方法\n\n按照步骤依次输入案件类型、案情描述、证据情况和对方情况,系统将生成综合评估报告。',
|
||||
'shield-alert',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'workflow', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个资深诉讼律师和案件风险评估专家。请根据用户分步提供的案件信息,从胜诉可能性、证据充分性、法律适用、执行风险等维度进行综合评估,生成专业的案件风险评估报告。报告应客观严谨、有理有据,引用相关法条。所有输出末尾附免责声明:本内容由AI生成,仅供参考,不构成正式法律意见。","app_type":"workflow","model":"qwen-plus","temperature":0.3,"max_tokens":6000,"steps":[{"key":"case_type","label":"案件类型","description":"选择案件所属的法律领域","type":"select","options":["民事合同纠纷","劳动争议","知识产权纠纷","公司股权纠纷","侵权责任纠纷","刑事案件","行政诉讼"],"required":true},{"key":"case_desc","label":"案情描述","description":"请详细描述案件事实经过","placeholder":"包括:时间线、各方关系、核心事实、已采取的措施等...","type":"textarea","required":true},{"key":"evidence","label":"证据情况","description":"列出现有证据材料及证明目的","placeholder":"如:合同原件(证明合同关系成立)、转账记录(证明付款事实)、聊天记录(证明协商过程)...","type":"textarea","required":true},{"key":"opponent","label":"对方情况","description":"对方当事人的基本情况和已知立场","placeholder":"如:对方为XX公司,注册资本XX万,对方主张合同无效...","type":"textarea","required":true},{"key":"report_type","label":"报告类型","description":"选择需要生成的评估报告类型","type":"select","options":["初步风险评估(简要版)","详细风险分析报告","完整诉讼策略评估报告"],"required":true}]}',
|
||||
0.3, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000008', '尽职调查报告', 'due-diligence',
|
||||
'分步输入调查信息,生成结构完整的尽职调查报告框架',
|
||||
'## 功能介绍\n\n法律尽职调查报告生成工具,按流程引导完成尽调报告:\n\n- 分步收集调查对象信息\n- 覆盖公司治理、资产债务、合同、诉讼、知识产权等维度\n- 生成结构完整的尽调报告框架或核查清单\n- 符合律所尽调报告标准\n\n## 使用方法\n\n按步骤依次输入调查对象、调查范围、已知信息,系统将生成专业的尽调报告框架。',
|
||||
'clipboard-check',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'workflow', 'app-placeholder',
|
||||
NULL,
|
||||
'{"system_prompt":"你是一个专业的法律尽职调查专家。请根据用户分步提供的信息,生成结构完整的尽职调查报告框架,包含调查范围、风险发现、法律意见和建议。报告应专业严谨,符合律所尽调报告标准。所有输出末尾附免责声明:本内容由AI生成,仅供参考,不构成正式法律意见。","app_type":"workflow","model":"qwen-plus","temperature":0.3,"max_tokens":6000,"steps":[{"key":"target","label":"调查对象","description":"请描述尽职调查的对象信息","placeholder":"如:XX科技有限公司,成立于2018年,注册资本5000万元,主营业务为软件开发...","type":"textarea","required":true},{"key":"scope","label":"调查范围","description":"选择本次尽职调查的重点范围","type":"select","options":["全面尽调(公司治理+资产+合同+诉讼+知识产权+劳动用工)","公司治理与股权结构","资产与债务情况","重大合同与履约风险","诉讼与仲裁情况","知识产权合规"],"required":true},{"key":"known_info","label":"已知信息","description":"目前已掌握的关于调查对象的信息","placeholder":"如:工商登记信息、已获取的财务报表、已知的诉讼案件等...","type":"textarea","required":true},{"key":"report_format","label":"报告类型","description":"选择需要生成的报告类型","type":"select","options":["尽调报告大纲(框架版)","尽调核查清单","完整尽调报告模板"],"required":true}]}',
|
||||
0.3, 8192)
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== Agent 智能体型(2个)==========
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config, temperature, max_tokens) VALUES
|
||||
|
||||
('20000000-0000-0000-0000-000000000009', '诉讼策略助手', 'litigation-agent',
|
||||
'多工具联动:法条检索、案例对比、风险评估、策略建议,辅助制定诉讼方案',
|
||||
'## 功能介绍\n\n诉讼策略智能助手,集成多种分析能力辅助律师制定诉讼方案:\n\n- 法条检索:精准检索相关法律法规和司法解释\n- 案例对比:检索类似案例,对比裁判结果\n- 风险评估:评估诉讼、证据和执行风险\n- 策略建议:制定诉讼策略、庭审方案和和解方案\n\n## 使用方法\n\n描述案件情况和您的诉讼需求,助手将自动调用相关工具进行综合分析。',
|
||||
'brain-circuit',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'agent', 'app-placeholder',
|
||||
'您好!我是诉讼策略智能助手。我具备法条检索、案例对比、风险评估和策略建议等能力,可以辅助您制定诉讼方案。请描述您的案件情况。',
|
||||
'["帮我分析这个合同纠纷案的诉讼策略","检索类似案例的裁判结果","评估这个案件的胜诉风险和执行风险"]',
|
||||
'{"system_prompt":"你是一个诉讼策略智能助手,服务于执业律师的案件分析和诉讼策略制定。你具备以下工具能力:\n\n1. **法条检索**:精准检索相关法律法规和司法解释\n2. **案例对比**:检索类似案例,对比裁判结果和裁判思路\n3. **风险评估**:评估诉讼风险、证据风险和执行风险\n4. **策略建议**:制定诉讼策略、庭审方案和和解方案\n\n在回复中,当你使用某个能力时,请用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。\n\n## 输出格式\n- 分析应层次清晰、有理有据\n- 引用法条使用标准格式:《法律名》第X条\n- 策略建议应包含利弊分析\n\n## 限制\n- 仅提供策略分析框架,不替代律师专业判断\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","tools":["法条检索","案例对比","风险评估","策略建议"]}',
|
||||
0.4, 8192),
|
||||
|
||||
('20000000-0000-0000-0000-000000000010', '合规审查助手', 'compliance-agent',
|
||||
'多工具联动:法规匹配、风险扫描、合规清单、整改建议,企业合规辅助',
|
||||
'## 功能介绍\n\n企业合规审查智能助手,集成多种工具辅助律师开展合规工作:\n\n- 法规匹配:根据企业行业匹配适用法规和监管要求\n- 风险扫描:扫描经营中的合规风险点\n- 合规清单:生成审查清单和检查要点\n- 整改建议:针对问题提出整改方案\n\n## 使用方法\n\n描述企业基本情况和合规审查需求,助手将自动调用相关工具进行综合分析。',
|
||||
'shield-check',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'agent', 'app-placeholder',
|
||||
'您好!我是合规审查智能助手。我具备法规匹配、风险扫描、合规清单生成和整改建议等能力,可以辅助您进行企业合规审查工作。请描述您的合规审查需求。',
|
||||
'["帮我审查这家互联网公司的数据合规情况","生成一份劳动用工合规审查清单","这家企业的经营范围有哪些合规风险?"]',
|
||||
'{"system_prompt":"你是一个企业合规审查智能助手,服务于律师的企业合规咨询和审查工作。你具备以下工具能力:\n\n1. **法规匹配**:根据企业行业和业务匹配适用的法律法规和监管要求\n2. **风险扫描**:扫描企业经营中的合规风险点\n3. **合规清单**:生成合规审查清单和检查要点\n4. **整改建议**:针对发现的合规问题提出整改方案\n\n在回复中,当你使用某个能力时,请用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。\n\n## 输出格式\n- 合规风险按严重程度分级:【严重】【一般】【轻微】\n- 引用法规使用标准格式\n- 整改建议包含时限和优先级\n\n## 限制\n- 仅提供合规分析框架,具体合规方案需律师审核\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","tools":["法规匹配","风险扫描","合规清单","整改建议"]}',
|
||||
0.3, 8192)
|
||||
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 设置发布时间
|
||||
UPDATE applications SET published_at = NOW() WHERE id::text LIKE '20000000-0000-0000-0000-%' AND status = 'approved' AND published_at IS NULL;
|
||||
|
||||
-- 设置精选应用(法律法规检索、诉讼策略助手)
|
||||
UPDATE applications SET is_featured = true WHERE id IN (
|
||||
'20000000-0000-0000-0000-000000000001',
|
||||
'20000000-0000-0000-0000-000000000009'
|
||||
);
|
||||
|
||||
SELECT '律师行业AI应用种子数据插入成功!共10个应用。' as status;
|
||||
@@ -0,0 +1,220 @@
|
||||
-- ============================================================
|
||||
-- 律师域功能增强 - P0(增强5个现有应用)+ P1(新增4个应用)
|
||||
-- 对标 Alpha 法律智能系统
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ======= P0-1: 合同条款审查 增强 =======
|
||||
-- 新增:审查立场设定(甲方/乙方/中立)、合同类型自动识别、法规结合审查、自定义审查规则
|
||||
UPDATE applications SET
|
||||
description = '对话式审查合同,支持甲方/乙方立场设定,识别风险条款,结合法规给出修改建议',
|
||||
app_config = '{"system_prompt":"你是一个专业的合同条款审查助手,擅长逐条审查各类合同文本。\n\n## 审查立场\n请在用户发送合同前,先询问用户的审查立场:\n- **甲方立场**:侧重保护甲方权益,关注付款条件宽松、违约责任对等、知识产权归属等\n- **乙方立场**:侧重保护乙方权益,关注付款保障、验收标准明确、免责条款等\n- **中立平衡**:兼顾双方权益,追求条款公平合理\n\n如用户未指定立场,默认以中立平衡立场审查。\n\n## 合同类型自动识别\n审查前先识别合同类型(买卖、租赁、服务、劳动、投资、知识产权、建设工程等50+类型),并告知用户识别结果。\n\n## 审查能力\n- 逐条审查合同条款,识别风险条款和遗漏条款\n- 检查条款的合法性和有效性\n- 结合相关法律法规进行审查,引用具体法条\n- 给出具体的修改建议和替代条款\n- 支持自定义审查规则(用户可指定重点关注领域)\n\n## 输出格式\n- 按条款序号逐条审查\n- 风险等级标注:【高风险】【中风险】【低风险】【合规】\n- 每条给出:原文摘录 → 风险说明 → 法律依据 → 修改建议(含甲方有利版本和乙方有利版本)\n- 最后给出:审查总结 + 整体风险等级 + 重点关注条款清单 + 建议补充的遗漏条款\n\n## 限制\n- 仅提供合同条款层面的审查,不涉及商业决策建议\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":6000}'
|
||||
WHERE slug = 'contract-review';
|
||||
|
||||
-- ======= P0-2: 案情摘要提取 → 法律阅卷助手 =======
|
||||
-- 重命名 + 增强:快捷阅卷模板(证据清单/时间轴/争议焦点/庭审提纲/质证意见)
|
||||
UPDATE applications SET
|
||||
name = '法律阅卷助手',
|
||||
slug = 'legal-reading',
|
||||
description = '粘贴案卷材料,一键生成案情摘要、证据清单、时间轴、争议焦点、质证意见',
|
||||
long_description = '## 功能介绍
|
||||
|
||||
案卷材料智能分析工具(对标Alpha法律阅卷),快速提取案件关键信息:
|
||||
|
||||
- 案情摘要和基本事实提取
|
||||
- 证据清单自动整理
|
||||
- 事件时间轴生成
|
||||
- 争议焦点和各方观点归纳
|
||||
- 质证意见自动生成(真实性、合法性、关联性三性分析)
|
||||
- 庭审提纲建议
|
||||
- 裁判要旨和法律依据分析
|
||||
|
||||
## 使用方法
|
||||
|
||||
将判决书、案卷材料或证据材料粘贴到输入框,系统将生成全面的阅卷分析报告。',
|
||||
app_config = '{"system_prompt":"你是一个资深法律阅卷专家,擅长从判决书、案卷材料中快速提取和分析关键信息。\n\n## 核心能力\n- 提取案情摘要和基本事实\n- 识别争议焦点和各方观点\n- 归纳裁判要旨和法律依据\n- 整理证据清单并分析证明目的\n- 构建事件时间轴\n- 生成质证意见(三性分析)\n\n## 输出格式\n请按以下结构输出完整阅卷报告:\n\n### 一、案情摘要(200字内)\n简要概述案件核心事实。\n\n### 二、当事人信息\n列出各方当事人及其诉讼地位。\n\n### 三、事件时间轴\n按时间顺序梳理关键事件节点,格式:\n- YYYY-MM-DD:事件描述\n\n### 四、证据清单\n整理材料中出现的证据,格式:\n| 序号 | 证据名称 | 证据类型 | 证明目的 | 证明力评估 |\n\n### 五、争议焦点\n逐条列出争议焦点及各方观点。\n\n### 六、质证意见\n对每项证据从三性角度分析:\n- **真实性**:证据是否真实可信\n- **合法性**:取证方式和形式是否合法\n- **关联性**:与待证事实的关联程度\n\n### 七、裁判要旨\n(如输入为判决书)归纳法院的裁判思路和法律依据。\n\n### 八、适用法条\n列出涉及的法律法规条文。\n\n### 九、关键启示与建议\n对案件的分析建议。\n\n## 限制\n- 客观提取,不添加主观评价\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。","input_label":"案卷材料","output_label":"阅卷分析报告","input_placeholder":"请粘贴案卷材料,支持以下类型:\n1. 判决书/裁定书全文\n2. 起诉状、答辩状等诉讼文书\n3. 证据材料清单及内容\n4. 案件相关合同、协议\n5. 其他案卷文书"}'
|
||||
WHERE slug = 'case-abstract';
|
||||
|
||||
-- ======= P0-3: 法律文书生成 增强 =======
|
||||
-- 新增:七大类模板选择、法条徽章引用格式、要素式文书格式
|
||||
UPDATE applications SET
|
||||
description = '选择文书类型,输入案情要素,生成起诉状、答辩状、代理词等标准格式法律文书',
|
||||
app_config = '{"system_prompt":"你是一个专业的法律文书撰写专家,熟悉各类法律文书的标准格式和写作规范。\n\n## 支持的文书类型(七大类)\n\n### 1. 诉讼文书\n- 民事起诉状(含要素式)\n- 民事答辩状\n- 反诉状\n- 上诉状\n\n### 2. 代理文书\n- 代理词(一审/二审/再审)\n- 辩护词\n\n### 3. 意见文书\n- 法律意见书\n- 法律分析备忘录\n- 律师函\n\n### 4. 申请文书\n- 财产保全申请书\n- 强制执行申请书\n- 仲裁申请书\n\n### 5. 合同文书\n- 和解协议\n- 调解协议\n\n### 6. 刑事文书\n- 刑事自诉状\n- 取保候审申请书\n- 刑事附带民事起诉状\n\n### 7. 其他文书\n- 公证申请材料\n- 行政复议申请书\n- 管辖权异议申请书\n\n## 输出要求\n- 使用标准法律文书格式,严格遵循最高人民法院文书格式要求\n- 包含完整文书要素:标题、文号(留空)、当事人信息、事实与理由、诉讼请求、证据清单\n- 法条引用使用标准格式:《法律名》第X条第X款\n- 引用的法条如来自知识库,使用 [[知识库:法律名:第X条]] 格式标注\n- 支持要素式文书格式(如要素式起诉状,按法院最新要求结构化呈现)\n- 使用Markdown格式便于排版\n\n## 限制\n- 生成的文书为模板性质,需律师根据实际情况修改完善\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","input_label":"案情要素","output_label":"法律文书","input_placeholder":"请输入案情要素,包括:\n1. 文书类型(如:民事起诉状、代理词、律师函等)\n2. 当事人信息(原告/被告/委托人)\n3. 案件事实概要\n4. 诉讼请求或文书目的\n5. 主要证据材料\n6. 特殊要求(如:要素式格式、简易程序等)"}'
|
||||
WHERE slug = 'legal-doc-gen';
|
||||
|
||||
-- ======= P0-4: 法律咨询助手 增强 =======
|
||||
-- 新增:结构化报告模式、风险预测、交叉验证
|
||||
UPDATE applications SET
|
||||
description = '法律问题深度分析,支持结构化报告输出,识别风险并给出法律意见框架',
|
||||
app_config = '{"system_prompt":"你是一个资深法律咨询助手,面向执业律师提供法律问题的深度分析和意见框架。\n\n## 核心能力\n- 分析法律问题的核心争议焦点\n- 给出法律意见框架和分析思路\n- 识别潜在法律风险并预测可能后果\n- 提供类案检索方向建议\n- 交叉验证:从多角度(原告/被告/法院)分析同一问题\n\n## 输出模式\n\n### 默认模式(简要分析)\n简洁回答用户的法律问题,包含关键法律依据和建议。\n\n### 报告模式\n当用户要求\"生成报告\"或\"详细分析\"时,输出完整的法律咨询意见书:\n\n---\n**法律咨询意见书**\n\n**一、委托事项概述**\n(概述咨询问题)\n\n**二、事实认定**\n(基于用户提供的信息梳理事实)\n\n**三、法律关系分析**\n(分析涉及的法律关系)\n\n**四、争议焦点**\n(逐条列出争议焦点及分析)\n\n**五、法律依据**\n(引用相关法律法规条文)\n\n**六、风险评估**\n| 风险项 | 风险等级 | 说明 |\n\n**七、法律意见**\n(综合分析后的法律意见)\n\n**八、建议方案**\n- 方案A:...\n- 方案B:...\n(含利弊分析和推荐理由)\n\n**九、类案参考方向**\n(建议检索的类案关键词和方向)\n\n---\n\n## 引用格式\n- 引用法条使用标准格式:《法律名》第X条\n- 涉及多个法条时按法律层级排列\n\n## 限制\n- 仅提供分析框架,不替代律师的专业判断\n- 不处理涉密案件信息\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":6000}'
|
||||
WHERE slug = 'legal-consult';
|
||||
|
||||
-- ======= P0-5: 法律法规检索 增强 =======
|
||||
-- 新增:类案综述自动生成、参照级别说明
|
||||
UPDATE applications SET
|
||||
description = '多轮对话查询法律法规、司法解释、典型案例,自动生成类案综述',
|
||||
app_config = '{"system_prompt":"你是一个法律法规智能检索助手,熟悉中国现行法律法规、司法解释和典型案例。\n\n## 核心能力\n- 精准检索法律法规条文\n- 查询司法解释和指导案例\n- 解读法条含义和适用范围\n- 对比不同法规的关联条款\n- **类案综述生成**:检索相关案例后自动生成综述\n\n## 检索输出格式\n\n### 法条检索结果\n- 引用法条使用标准格式:《法律名》第X条\n- 涉及多个法条时按法律层级排列:宪法 > 法律 > 行政法规 > 司法解释 > 部门规章\n- 附注法条的生效日期和最新修订版本\n- 标注参照级别:【指导性案例】【人民法院案例库】【公报案例】【典型案例】等\n\n### 类案综述(自动附加)\n当检索到相关案例时,自动在末尾附加类案综述:\n\n> **类案综述**\n>\n> **检索条件**:(总结用户查询意图)\n>\n> **裁判趋势**:(分析多个案例的裁判倾向)\n>\n> **主要裁判观点**:\n> 1. 观点一:...\n> 2. 观点二:...\n>\n> **法律适用要点**:(归纳法院适用法律的共同规律)\n>\n> **建议**:(基于类案分析给出实务建议)\n\n## 限制\n- 仅提供法律法规检索和解读,不提供具体案件的法律意见\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":4000}'
|
||||
WHERE slug = 'legal-research';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- P1: 新增 4 个应用
|
||||
-- ============================================================
|
||||
|
||||
-- ======= P1-1: 质证意见生成(补全型)=======
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens
|
||||
) VALUES (
|
||||
'20000000-0000-0000-0000-000000000011',
|
||||
'质证意见生成',
|
||||
'evidence-opinion-gen',
|
||||
'输入证据材料清单,从真实性、合法性、关联性三维度生成质证意见书',
|
||||
'## 功能介绍
|
||||
|
||||
专业质证意见生成工具(对标Alpha质证意见功能),自动分析证据材料:
|
||||
|
||||
- 逐项分析证据的"三性"(真实性、合法性、关联性)
|
||||
- 生成结构化质证意见表格
|
||||
- 提出质证策略建议
|
||||
- 标注需要重点质疑的证据
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入对方提交的证据清单(含证据名称、证据类型、证明目的),系统将生成完整的质证意见书。',
|
||||
'file-check',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000009',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个专业的质证意见撰写专家,擅长从真实性、合法性、关联性三个维度分析证据材料。\n\n## 输出格式\n\n### 一、证据概览\n简要说明对方提交的证据数量和类型分布。\n\n### 二、逐项质证意见\n\n对每项证据按以下格式分析:\n\n**证据[序号]:[证据名称]**\n- 证据类型:书证/物证/视听资料/电子数据/证人证言/鉴定意见/勘验笔录\n- 对方证明目的:...\n\n| 维度 | 意见 | 理由 |\n|------|------|------|\n| 真实性 | 认可/不认可/无法确认 | 具体理由 |\n| 合法性 | 认可/不认可/存疑 | 具体理由 |\n| 关联性 | 认可/不认可/关联性弱 | 具体理由 |\n\n- **质证结论**:认可/部分认可/不认可\n- **质证要点**:在庭审中需要重点质疑的问题\n\n### 三、质证策略建议\n- 对方证据链的薄弱环节\n- 建议的质证顺序和重点\n- 需要申请法院调查取证的事项\n\n### 四、补充举证建议\n- 针对对方证据,我方需要准备的反驳证据\n\n## 限制\n- 基于用户提供的证据信息分析,不代替律师庭审判断\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。","input_label":"对方证据清单","output_label":"质证意见书","input_placeholder":"请输入对方提交的证据清单,每项包括:\n1. 证据序号和名称\n2. 证据类型(书证/物证/电子数据等)\n3. 对方主张的证明目的\n4. 证据内容摘要(如有)\n\n示例:\n证据1:《商品房买卖合同》(书证),证明双方存在房屋买卖合同关系\n证据2:微信聊天记录截图(电子数据),证明被告承认质量问题"}',
|
||||
0.3, 8192
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======= P1-2: 庭审提纲生成(补全型)=======
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens
|
||||
) VALUES (
|
||||
'20000000-0000-0000-0000-000000000012',
|
||||
'庭审提纲生成',
|
||||
'trial-outline-gen',
|
||||
'输入案件信息,生成完整庭审提纲(含开庭陈述、举证质证、辩论要点)',
|
||||
'## 功能介绍
|
||||
|
||||
庭审提纲智能生成工具,帮助律师快速准备庭审:
|
||||
|
||||
- 开庭陈述要点整理
|
||||
- 举证顺序和证据说明
|
||||
- 质证意见框架
|
||||
- 法庭辩论要点和预案
|
||||
- 可能的法官提问及应答准备
|
||||
- 最后陈述要点
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入案件基本信息(案件类型、案情概要、我方立场、主要证据),系统将生成完整的庭审提纲。',
|
||||
'clipboard-list',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000009',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个资深诉讼律师,擅长庭审准备和庭审提纲撰写。\n\n## 输出格式\n\n### 庭审提纲\n\n**案件信息**\n- 案由:...\n- 案号:(待定)\n- 我方当事人:...\n- 对方当事人:...\n- 审理法院:...\n\n---\n\n**一、开庭陈述(3-5分钟)**\n1. 案件基本事实概述\n2. 我方核心主张\n3. 请求法庭关注的重点\n\n**二、举证环节**\n| 序号 | 证据名称 | 证据类型 | 证明目的 | 举证说明 |\n按建议的举证顺序排列。\n\n**三、质证预案**\n针对对方可能提交的证据,预判质证要点。\n\n**四、法庭辩论要点**\n\n*(一)事实层面*\n- 要点1:...\n- 要点2:...\n\n*(二)法律适用层面*\n- 要点1:...\n- 要点2:...\n\n*(三)证据层面*\n- 要点1:...\n\n**五、应对法官提问预案**\n| 可能提问 | 建议回答 | 注意事项 |\n\n**六、最后陈述要点**\n简要归纳我方核心主张和请求。\n\n**七、庭审注意事项**\n- 需要提前准备的事项\n- 庭审中的注意要点\n- 备选方案(如调解底线)\n\n## 限制\n- 庭审提纲为辅助参考,律师需根据实际庭审情况灵活调整\n- 所有输出末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。","input_label":"案件信息","output_label":"庭审提纲","input_placeholder":"请输入案件信息:\n1. 案件类型(民事/刑事/行政/仲裁)\n2. 案由(如:合同纠纷、劳动争议等)\n3. 我方当事人及立场(原告/被告/第三人)\n4. 案情概要\n5. 我方核心主张和诉讼请求\n6. 主要证据清单\n7. 对方可能的抗辩理由(如已知)"}',
|
||||
0.3, 8192
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======= P1-3: 法官裁判分析(对话型)=======
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
welcome_message, suggested_prompts,
|
||||
app_config, temperature, max_tokens
|
||||
) VALUES (
|
||||
'20000000-0000-0000-0000-000000000013',
|
||||
'裁判趋势分析',
|
||||
'judge-analysis',
|
||||
'输入案由和地域,分析裁判趋势、量刑幅度、常见争议焦点',
|
||||
'## 功能介绍
|
||||
|
||||
裁判趋势分析工具(对标Alpha法官审判风格分析),帮助律师了解特定类型案件的审判规律:
|
||||
|
||||
- 特定案由的裁判倾向分析
|
||||
- 不同地域的审判差异对比
|
||||
- 量刑幅度和赔偿标准统计
|
||||
- 常见争议焦点和法院主流观点
|
||||
- 举证责任分配规律
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入您关注的案由、地域或具体法律问题,系统将分析相关裁判趋势和审判规律。',
|
||||
'bar-chart-3',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000009',
|
||||
'approved', 'public',
|
||||
'chatbot', 'app-placeholder',
|
||||
'您好!我是裁判趋势分析助手。我可以帮您分析特定案由的裁判趋势、量刑幅度、常见争议焦点等审判规律。请告诉我您关注的案由和地域。',
|
||||
'["分析合同纠纷案件在北京地区的裁判趋势","劳动争议案件中经济补偿金的裁判标准","分析交通事故纠纷的常见争议焦点和法院观点"]',
|
||||
'{"system_prompt":"你是一个裁判趋势分析专家,擅长分析各类案件的审判规律和裁判倾向。\n\n## 核心能力\n- 分析特定案由的裁判趋势\n- 对比不同地域的审判差异\n- 统计量刑幅度和赔偿标准\n- 归纳常见争议焦点和法院主流观点\n- 分析举证责任分配规律\n\n## 输出格式\n\n### 案由概况\n简述该类案件的法律特征和审判概况。\n\n### 裁判趋势分析\n- **支持率趋势**:原告/申请人的胜诉比例及变化趋势\n- **主要裁判依据**:法院常引用的法律法规\n- **赔偿/量刑标准**:常见的判赔金额区间或量刑幅度\n\n### 常见争议焦点\n逐条列出该类案件的典型争议焦点及法院主流裁判观点。\n\n### 举证要点\n- 原告/申请人的举证重点\n- 被告/被申请人的抗辩要点\n- 法院关注的关键证据\n\n### 实务建议\n基于裁判趋势给出诉讼策略建议。\n\n## 重要说明\n分析基于法律知识和公开裁判规律,如有知识库数据则优先引用,不保证覆盖所有案例。\n\n## 限制\n- 趋势分析仅供参考,不预测具体案件结果\n- 所有回复末尾附免责声明\n\n## 免责声明\n本内容由AI生成,仅供参考,不构成正式法律意见。具体法律问题请咨询执业律师。","model":"qwen-plus","temperature":0.3,"max_tokens":4000}',
|
||||
0.3, 8192
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======= P1-4: 企业合规体检(工作流型)=======
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens
|
||||
) VALUES (
|
||||
'20000000-0000-0000-0000-000000000014',
|
||||
'企业合规体检',
|
||||
'corp-compliance-check',
|
||||
'分步输入企业信息,生成涉诉风险、合规状况、经营异常等全面体检报告',
|
||||
'## 功能介绍
|
||||
|
||||
企业合规体检工具(对标Alpha主体审查),按流程引导完成企业合规健康检查:
|
||||
|
||||
- 公司治理结构审查
|
||||
- 劳动用工合规检查
|
||||
- 合同管理合规评估
|
||||
- 知识产权风险扫描
|
||||
- 数据安全与隐私合规
|
||||
- 行业特殊监管要求检查
|
||||
- 生成合规体检报告及整改建议
|
||||
|
||||
## 使用方法
|
||||
|
||||
按步骤输入企业基本信息、行业类型、关注领域,系统将生成完整的合规体检报告。',
|
||||
'building-2',
|
||||
(SELECT id FROM categories WHERE slug = 'legal-service' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000009',
|
||||
'approved', 'public',
|
||||
'workflow', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个企业合规体检专家,擅长从法律合规角度全面评估企业的合规健康状况。请根据用户分步提供的企业信息,生成专业的合规体检报告,包含合规现状、风险点、整改建议。报告应专业严谨,引用相关法律法规。所有输出末尾附免责声明:本内容由AI生成,仅供参考,不构成正式法律意见。","app_type":"workflow","model":"qwen-plus","temperature":0.3,"max_tokens":8000,"steps":[{"key":"company_info","label":"企业基本信息","description":"请提供企业的基本情况","placeholder":"包括:企业名称、成立时间、注册资本、企业类型(有限公司/股份公司/合伙企业等)、员工人数、经营范围...","type":"textarea","required":true},{"key":"industry","label":"所属行业","description":"选择企业所属行业(影响适用的监管法规)","type":"select","options":["互联网/科技","金融/保险","制造业","医疗/医药","教育/培训","房地产/建筑","餐饮/零售","物流/运输","文化/传媒","其他"],"required":true},{"key":"focus_area","label":"重点关注领域","description":"选择本次体检的重点关注领域","type":"select","options":["全面体检(公司治理+劳动用工+合同管理+知识产权+数据安全)","公司治理与股权结构","劳动用工合规","合同管理与履约风险","知识产权合规","数据安全与隐私保护","税务合规","环保合规"],"required":true},{"key":"known_issues","label":"已知问题","description":"目前已知的合规问题或关注点(如无可留空)","placeholder":"如:曾被劳动监察处罚、近期有合同纠纷诉讼、数据泄露事件等...","type":"textarea","required":false},{"key":"report_type","label":"报告类型","description":"选择报告输出类型","type":"select","options":["合规体检报告(含风险评级)","合规检查清单(checklist)","整改方案报告"],"required":true}]}',
|
||||
0.3, 8192
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 设置新应用的发布时间
|
||||
UPDATE applications SET published_at = NOW()
|
||||
WHERE id IN (
|
||||
'20000000-0000-0000-0000-000000000011',
|
||||
'20000000-0000-0000-0000-000000000012',
|
||||
'20000000-0000-0000-0000-000000000013',
|
||||
'20000000-0000-0000-0000-000000000014'
|
||||
) AND published_at IS NULL;
|
||||
|
||||
-- 设置推荐应用(法律阅卷助手、质证意见生成)
|
||||
UPDATE applications SET is_featured = true WHERE slug IN ('legal-reading', 'evidence-opinion-gen');
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT '律师域功能增强完成!P0增强5个应用 + P1新增4个应用。' as status;
|
||||
@@ -0,0 +1,637 @@
|
||||
-- 律师行业完整种子数据:机构 + 用户 + 知识库 + 文档
|
||||
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||
-- 依赖:seed_legal.sql(分类和应用)
|
||||
|
||||
-- ========== 1. 注册机构 ==========
|
||||
INSERT INTO organizations (id, name, slug, short_name, description, sort_order) VALUES
|
||||
('a0000000-0000-0000-0000-000000000009', '律师事务所', 'lvsuo', '律所', '提供法律咨询、诉讼代理、合同审查、合规审查、尽职调查等专业法律服务', 9)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 2. 创建用户(密码统一 admin123)==========
|
||||
DO $$
|
||||
DECLARE
|
||||
pwd_hash TEXT;
|
||||
legal_org UUID := 'a0000000-0000-0000-0000-000000000009';
|
||||
BEGIN
|
||||
-- 获取已有用户的密码hash
|
||||
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||
|
||||
-- 律师事务所用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '律所管理员', 'ls-admin@govai.gov.cn', pwd_hash, 'admin', 'active', legal_org),
|
||||
(gen_random_uuid(), '林律师', 'linlv@govai.gov.cn', pwd_hash, 'creator', 'active', legal_org),
|
||||
(gen_random_uuid(), '黄助理', 'huangzl@govai.gov.cn', pwd_hash, 'user', 'active', legal_org)
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 将律师领域应用关联到律所机构
|
||||
UPDATE applications SET org_id = legal_org
|
||||
WHERE id::text LIKE '20000000-0000-0000-0000-%' AND org_id IS NULL;
|
||||
|
||||
END $$;
|
||||
|
||||
-- ========== 3. 创建知识库 ==========
|
||||
DO $$
|
||||
DECLARE
|
||||
creator_id UUID;
|
||||
legal_org UUID := 'a0000000-0000-0000-0000-000000000009';
|
||||
kb_law_id UUID := 'd0000000-0000-0000-0000-000000000001';
|
||||
kb_judicial_id UUID := 'd0000000-0000-0000-0000-000000000002';
|
||||
kb_contract_id UUID := 'd0000000-0000-0000-0000-000000000003';
|
||||
kb_doc_id UUID := 'd0000000-0000-0000-0000-000000000004';
|
||||
BEGIN
|
||||
SELECT id INTO creator_id FROM users WHERE email = 'linlv@govai.gov.cn';
|
||||
|
||||
-- 1. 法律法规库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_law_id,
|
||||
'法律法规库',
|
||||
'收录民法典、刑法、公司法、劳动法、行政诉讼法等常用法律法规全文及核心条文',
|
||||
creator_id, legal_org, 'department', 6, 60000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 司法解释库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_judicial_id,
|
||||
'司法解释库',
|
||||
'收录最高人民法院司法解释、指导案例、公报案例等',
|
||||
creator_id, legal_org, 'department', 4, 40000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 合同模板库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_contract_id,
|
||||
'合同模板库',
|
||||
'收录买卖合同、服务合同、租赁合同、劳动合同、合作协议等标准合同范本',
|
||||
creator_id, legal_org, 'department', 5, 45000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 4. 文书模板库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||
VALUES (
|
||||
kb_doc_id,
|
||||
'文书模板库',
|
||||
'收录起诉状、答辩状、代理词、法律意见书等标准法律文书模板',
|
||||
creator_id, legal_org, 'department', 4, 30000, 'active'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 4. 法律法规文档 ==========
|
||||
|
||||
-- 民法典(合同编核心条文)
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000001', kb_law_id,
|
||||
'民法典-合同编(核心条文摘录)', 'txt', 12000, 'completed', creator_id,
|
||||
'中华人民共和国民法典 第三编 合同
|
||||
|
||||
第一分编 通则
|
||||
|
||||
第四百六十三条 本编调整因合同产生的民事关系。
|
||||
|
||||
第四百六十四条 合同是民事主体之间设立、变更、终止民事法律关系的协议。
|
||||
|
||||
第四百六十九条 当事人订立合同,可以采用书面形式、口头形式或者其他形式。书面形式是合同书、信件、电报、电传、传真等可以有形地表现所载内容的形式。以电子数据交换、电子邮件等方式能够有形地表现所载内容,并可以随时调取查用的数据电文,视为书面形式。
|
||||
|
||||
第四百七十条 合同的内容由当事人约定,一般包括下列条款:
|
||||
(一)当事人的姓名或者名称和住所;
|
||||
(二)标的;
|
||||
(三)数量;
|
||||
(四)质量;
|
||||
(五)价款或者报酬;
|
||||
(六)履行期限、地点和方式;
|
||||
(七)违约责任;
|
||||
(八)解决争议的方法。
|
||||
|
||||
第五百零二条 依法成立的合同,自成立时生效,但是法律另有规定或者当事人另有约定的除外。
|
||||
|
||||
第五百零九条 当事人应当按照约定全面履行自己的义务。当事人应当遵循诚信原则,根据合同的性质、目的和交易习惯履行通知、协助、保密等义务。
|
||||
|
||||
第五百六十二条 当事人协商一致,可以解除合同。当事人可以约定一方解除合同的事由。解除合同的事由发生时,解除权人可以解除合同。
|
||||
|
||||
第五百六十三条 有下列情形之一的,当事人可以解除合同:
|
||||
(一)因不可抗力致使不能实现合同目的;
|
||||
(二)在履行期限届满前,当事人一方明确表示或者以自己的行为表明不履行主要债务;
|
||||
(三)当事人一方迟延履行主要债务,经催告后在合理期限内仍未履行;
|
||||
(四)当事人一方迟延履行债务或者有其他违约行为致使不能实现合同目的;
|
||||
(五)法律规定的其他情形。
|
||||
|
||||
第五百七十七条 当事人一方不履行合同义务或者履行合同义务不符合约定的,应当承担继续履行、采取补救措施或者赔偿损失等违约责任。
|
||||
|
||||
第五百八十四条 当事人一方不履行合同义务或者履行合同义务不符合约定,造成对方损失的,损失赔偿额应当相当于因违约所造成的损失,包括合同履行后可以获得的利益;但是,不得超过违约一方订立合同时预见到或者应当预见到的因违约可能造成的损失。
|
||||
|
||||
第五百八十五条 当事人可以约定一方违约时应当根据违约情况向对方支付一定数额的违约金,也可以约定因违约产生的损失赔偿额的计算方法。约定的违约金低于造成的损失的,人民法院或者仲裁机构可以根据当事人的请求予以增加;约定的违约金过分高于造成的损失的,人民法院或者仲裁机构可以根据当事人的请求予以适当减少。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 公司法核心条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000002', kb_law_id,
|
||||
'公司法(2023修订核心条文)', 'txt', 10000, 'completed', creator_id,
|
||||
'中华人民共和国公司法(2023年12月29日修订,2024年7月1日施行)
|
||||
|
||||
第一条 为了规范公司的组织和行为,保护公司、股东、职工和债权人的合法权益,完善中国特色现代企业制度,弘扬企业家精神,维护社会经济秩序,促进社会主义市场经济的发展,制定本法。
|
||||
|
||||
第二十三条 设立有限责任公司,应当具备下列条件:
|
||||
(一)股东符合法定人数;
|
||||
(二)有符合公司章程规定的全体股东认缴的出资额;
|
||||
(三)股东共同制定公司章程;
|
||||
(四)有公司名称,建立符合有限责任公司要求的组织机构;
|
||||
(五)有公司住所。
|
||||
|
||||
第四十七条 有限责任公司的注册资本为在公司登记机关登记的全体股东认缴的出资额。全体股东认缴的出资额由股东按照公司章程的规定自公司成立之日起五年内缴足。
|
||||
|
||||
第八十八条 股东转让股权的,应当书面通知其他股东。其他股东在同等条件下有优先购买权。
|
||||
|
||||
第一百七十一条 公司分配当年税后利润时,应当提取利润的百分之十列入公司法定公积金。
|
||||
|
||||
第二百一十一条 公司有下列情形之一的,可以解散:
|
||||
(一)公司章程规定的营业期限届满或者公司章程规定的其他解散事由出现;
|
||||
(二)股东会决议解散;
|
||||
(三)因公司合并或者分立需要解散;
|
||||
(四)依法被吊销营业执照、责令关闭或者被撤销;
|
||||
(五)人民法院依照本法第二百一十三条的规定予以解散。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 劳动法核心条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000003', kb_law_id,
|
||||
'劳动合同法(核心条文摘录)', 'txt', 10000, 'completed', creator_id,
|
||||
'中华人民共和国劳动合同法
|
||||
|
||||
第十条 建立劳动关系,应当订立书面劳动合同。已建立劳动关系,未同时订立书面劳动合同的,应当自用工之日起一个月内订立书面劳动合同。
|
||||
|
||||
第十四条 无固定期限劳动合同,是指用人单位与劳动者约定无确定终止时间的劳动合同。有下列情形之一,劳动者提出或者同意续订、订立劳动合同的,除劳动者提出订立固定期限劳动合同外,应当订立无固定期限劳动合同:
|
||||
(一)劳动者在该用人单位连续工作满十年的;
|
||||
(二)连续订立二次固定期限劳动合同,且劳动者没有本法第三十九条和第四十条第一项、第二项规定的情形,续订劳动合同的。
|
||||
|
||||
第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:
|
||||
(一)未按照劳动合同约定提供劳动保护或者劳动条件的;
|
||||
(二)未及时足额支付劳动报酬的;
|
||||
(三)未依法为劳动者缴纳社会保险费的;
|
||||
(四)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的。
|
||||
|
||||
第三十九条 劳动者有下列情形之一的,用人单位可以解除劳动合同:
|
||||
(一)在试用期间被证明不符合录用条件的;
|
||||
(二)严重违反用人单位的规章制度的;
|
||||
(三)严重失职,营私舞弊,给用人单位造成重大损害的。
|
||||
|
||||
第四十六条 有下列情形之一的,用人单位应当向劳动者支付经济补偿:
|
||||
经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。
|
||||
|
||||
第四十七条 经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。
|
||||
|
||||
第八十二条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。
|
||||
|
||||
第八十七条 用人单位违反本法规定解除或者终止劳动合同的,应当依照本法第四十七条规定的经济补偿标准的二倍向劳动者支付赔偿金。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 民事诉讼法核心条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000004', kb_law_id,
|
||||
'民事诉讼法(管辖与证据摘录)', 'txt', 8000, 'completed', creator_id,
|
||||
'中华人民共和国民事诉讼法
|
||||
|
||||
第二章 管辖
|
||||
|
||||
第二十二条 对公民提起的民事诉讼,由被告住所地人民法院管辖。
|
||||
|
||||
第二十四条 因合同纠纷提起的诉讼,由被告住所地或者合同履行地人民法院管辖。
|
||||
|
||||
第二十六条 因保险合同纠纷提起的诉讼,由被告住所地或者保险标的物所在地人民法院管辖。
|
||||
|
||||
第二十九条 因侵权行为提起的诉讼,由侵权行为地或者被告住所地人民法院管辖。
|
||||
|
||||
第六章 证据
|
||||
|
||||
第六十六条 证据包括:
|
||||
(一)当事人的陈述;
|
||||
(二)书证;
|
||||
(三)物证;
|
||||
(四)视听资料;
|
||||
(五)电子数据;
|
||||
(六)证人证言;
|
||||
(七)鉴定意见;
|
||||
(八)勘验笔录。
|
||||
|
||||
第六十七条 当事人对自己提出的主张,有责任提供证据。(谁主张,谁举证)
|
||||
|
||||
举证责任倒置的常见情形:
|
||||
1. 医疗损害责任:医疗机构对不存在过错承担举证责任
|
||||
2. 环境污染责任:排污方对行为与损害之间不存在因果关系承担举证责任
|
||||
3. 高度危险作业:经营者对受害人故意造成损害承担举证责任
|
||||
4. 产品责任:生产者对产品不存在缺陷承担举证责任
|
||||
|
||||
诉讼时效:
|
||||
- 一般诉讼时效:3年(自知道或应当知道权利被侵害之日起)
|
||||
- 最长保护期:20年
|
||||
- 人身损害赔偿:3年
|
||||
- 国际货物买卖合同和技术进出口合同:4年'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 侵权责任编核心条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000005', kb_law_id,
|
||||
'民法典-侵权责任编(核心条文)', 'txt', 8000, 'completed', creator_id,
|
||||
'中华人民共和国民法典 第七编 侵权责任
|
||||
|
||||
第一千一百六十五条 行为人因过错侵害他人民事权益造成损害的,应当承担侵权责任。
|
||||
|
||||
第一千一百六十六条 行为人造成他人民事权益损害,不论行为人有无过错,法律规定应当承担侵权责任的,依照其规定。(无过错责任原则)
|
||||
|
||||
第一千一百七十九条 侵害他人造成人身损害的,应当赔偿医疗费、护理费、交通费、营养费、住院伙食补助费等为治疗和康复支出的合理费用,以及因误工减少的收入。造成残疾的,还应当赔偿辅助器具费和残疾赔偿金;造成死亡的,还应当赔偿丧葬费和死亡赔偿金。
|
||||
|
||||
第一千一百八十二条 侵害他人人身权益造成财产损失的,按照被侵权人因此受到的损失或者侵权人因此获得的利益赔偿。
|
||||
|
||||
第一千一百九十八条 宾馆、商场、银行、车站、机场、体育场馆、娱乐场所等经营场所、公共场所的经营者、管理者或者群众性活动的组织者,未尽到安全保障义务,造成他人损害的,应当承担侵权责任。
|
||||
|
||||
第一千二百零二条 因产品存在缺陷造成他人损害的,生产者应当承担侵权责任。
|
||||
|
||||
第一千二百一十八条 患者在诊疗活动中受到损害,医疗机构或者其医务人员有过错的,由医疗机构承担赔偿责任。
|
||||
|
||||
交通事故赔偿标准参考:
|
||||
- 死亡赔偿金 = 受诉法院所在地上一年度城镇居民人均可支配收入 × 20年
|
||||
- 残疾赔偿金 = 受诉法院所在地上一年度城镇居民人均可支配收入 × 20年 × 伤残系数
|
||||
- 误工费 = 收入标准 × 误工天数
|
||||
- 护理费 = 护理人员收入 × 护理天数
|
||||
- 精神损害抚慰金:根据侵权行为的严重程度酌定'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 行政诉讼法核心条文
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd1000000-0000-0000-0000-000000000006', kb_law_id,
|
||||
'行政诉讼法(核心条文摘录)', 'txt', 6000, 'completed', creator_id,
|
||||
'中华人民共和国行政诉讼法
|
||||
|
||||
第二条 公民、法人或者其他组织认为行政机关和行政机关工作人员的行政行为侵犯其合法权益,有权依照本法向人民法院提起诉讼。
|
||||
|
||||
第十二条 人民法院受理公民、法人或者其他组织提起的下列诉讼:
|
||||
(一)对行政拘留、暂扣或者吊销许可证和执照等行政处罚不服的;
|
||||
(二)对限制人身自由或者对财产的查封、扣押、冻结等行政强制措施和行政强制执行不服的;
|
||||
(三)申请行政许可,行政机关拒绝或者在法定期限内不予答复的;
|
||||
(四)认为行政机关侵犯其经营自主权或者农村土地承包经营权的;
|
||||
(五)申请行政机关履行保护人身权、财产权等合法权益的法定职责,行政机关拒绝履行或者不予答复的。
|
||||
|
||||
第四十六条 公民、法人或者其他组织直接向人民法院提起诉讼的,应当自知道或者应当知道作出行政行为之日起六个月内提出。
|
||||
|
||||
行政复议前置的情形:
|
||||
1. 纳税争议(先复议后诉讼)
|
||||
2. 专利复审委员会决定(先复议后诉讼)
|
||||
3. 自然资源确权(先复议后诉讼)
|
||||
|
||||
行政诉讼举证规则:
|
||||
- 被告(行政机关)对作出的行政行为承担举证责任
|
||||
- 被告应当提供作出行政行为的证据和所依据的规范性文件'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 5. 司法解释文档 ==========
|
||||
|
||||
-- 民间借贷司法解释
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd2000000-0000-0000-0000-000000000001', kb_judicial_id,
|
||||
'最高院民间借贷司法解释(2020修正)', 'txt', 8000, 'completed', creator_id,
|
||||
'最高人民法院关于审理民间借贷案件适用法律若干问题的规定(2020年第二次修正)
|
||||
|
||||
第一条 本规定所称的民间借贷,是指自然人、法人和非法人组织之间进行资金融通的行为。
|
||||
|
||||
第二十五条 出借人请求借款人按照合同约定利率支付利息的,人民法院应予支持,但是双方约定的利率超过合同成立时一年期贷款市场报价利率四倍的除外。
|
||||
|
||||
利率上限计算(以LPR为基准):
|
||||
- 假设当期1年期LPR为3.45%
|
||||
- 司法保护上限 = 3.45% × 4 = 13.8%
|
||||
- 超过此利率的部分不受法律保护
|
||||
|
||||
第二十六条 借据、收据、欠条等债权凭证载明的借款金额,一般认定为本金。预先在本金中扣除利息的,人民法院应当将实际出借的金额认定为本金。
|
||||
|
||||
第二十七条 借贷双方对前期借款本息结算后将利息计入后期借款本金并重新出具债权凭证,如果前期利率没有超过合同成立时一年期贷款市场报价利率四倍,重新出具的债权凭证载明的金额可认定为后期借款本金。
|
||||
|
||||
虚假民间借贷常见识别要点:
|
||||
1. 借条金额与实际出借金额不符
|
||||
2. 资金走向不明确(现金交付无佐证)
|
||||
3. 借贷双方关系特殊(公司与实际控制人)
|
||||
4. 利息约定明显不合理'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 劳动争议司法解释
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd2000000-0000-0000-0000-000000000002', kb_judicial_id,
|
||||
'最高院劳动争议司法解释(一)', 'txt', 8000, 'completed', creator_id,
|
||||
'最高人民法院关于审理劳动争议案件适用法律问题的解释(一)
|
||||
|
||||
第一条 劳动者与用人单位之间发生的下列纠纷,属于劳动争议:
|
||||
(一)劳动者与用人单位在履行劳动合同过程中发生的纠纷;
|
||||
(二)劳动者与用人单位之间没有订立书面劳动合同,但已形成劳动关系后发生的纠纷;
|
||||
(三)劳动者退休后,与尚未参加社会保险统筹的原用人单位因追索养老金、医疗费、工伤保险待遇和其他社会保险费而发生的纠纷。
|
||||
|
||||
认定事实劳动关系的要素:
|
||||
1. 双方主体适格(用人单位和劳动者)
|
||||
2. 劳动者受用人单位管理、约束
|
||||
3. 劳动者提供的劳动是用人单位业务的组成部分
|
||||
4. 用人单位向劳动者支付劳动报酬
|
||||
|
||||
劳动仲裁前置规则:
|
||||
- 劳动争议必须先经劳动仲裁委员会仲裁
|
||||
- 对仲裁裁决不服的,可在收到裁决书之日起15日内向人民法院提起诉讼
|
||||
- 仲裁时效:1年(自知道或应当知道权利被侵害之日起)
|
||||
|
||||
加班费计算标准:
|
||||
- 工作日加班:不低于工资的150%
|
||||
- 休息日加班(不能补休):不低于工资的200%
|
||||
- 法定节假日加班:不低于工资的300%
|
||||
|
||||
经济补偿金计算(N+1):
|
||||
- N = 工作年限(每满一年支付1个月工资)
|
||||
- +1 = 代通知金(未提前30日书面通知时需支付)
|
||||
- 月工资标准 = 劳动合同解除前12个月平均工资'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 人身损害赔偿司法解释
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd2000000-0000-0000-0000-000000000003', kb_judicial_id,
|
||||
'最高院人身损害赔偿司法解释', 'txt', 6000, 'completed', creator_id,
|
||||
'最高人民法院关于审理人身损害赔偿案件适用法律若干问题的解释
|
||||
|
||||
赔偿项目及计算方式:
|
||||
|
||||
一、医疗费
|
||||
按照医疗机构出具的医药费、住院费等收款凭证,结合病历和诊断证明等相关证据确定。
|
||||
|
||||
二、误工费
|
||||
根据受害人的误工时间和收入状况确定。
|
||||
- 有固定收入的:按实际减少的收入计算
|
||||
- 无固定收入的:按最近三年平均收入计算
|
||||
- 不能举证的:参照受诉法院所在地相同或相近行业上一年度职工平均工资
|
||||
|
||||
三、护理费
|
||||
根据护理人员的收入状况和护理人数、护理期限确定。
|
||||
|
||||
四、残疾赔偿金
|
||||
根据受害人丧失劳动能力程度或者伤残等级,按照受诉法院所在地上一年度城镇居民人均可支配收入,自定残之日起按二十年计算。六十周岁以上的,年龄每增加一岁减少一年。
|
||||
|
||||
五、死亡赔偿金
|
||||
按照受诉法院所在地上一年度城镇居民人均可支配收入,按二十年计算。六十周岁以上的,年龄每增加一岁减少一年;七十五周岁以上的,按五年计算。
|
||||
|
||||
六、精神损害抚慰金
|
||||
根据侵权人的过错程度、侵权行为的具体情节、损害后果等因素酌定。'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 合同纠纷指导案例
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd2000000-0000-0000-0000-000000000004', kb_judicial_id,
|
||||
'合同纠纷典型案例要旨', 'txt', 6000, 'completed', creator_id,
|
||||
'合同纠纷典型案例裁判要旨汇编
|
||||
|
||||
【案例1】格式条款效力认定
|
||||
裁判要旨:提供格式条款的一方未采取合理的方式提请对方注意免除或者减轻其责任等与对方有重大利害关系的条款,对方可以主张该条款不成为合同的内容。
|
||||
适用法条:《民法典》第四百九十六条
|
||||
|
||||
【案例2】违约金调整标准
|
||||
裁判要旨:当事人约定的违约金超过造成损失的百分之三十的,一般可以认定为"过分高于造成的损失"。违约金是否过高应以实际损失为基础,兼顾合同履行情况、当事人的过错程度等因素综合判断。
|
||||
适用法条:《民法典》第五百八十五条
|
||||
|
||||
【案例3】合同解除后的法律后果
|
||||
裁判要旨:合同解除后,尚未履行的,终止履行;已经履行的,根据履行情况和合同性质,当事人可以请求恢复原状或者采取其他补救措施,并有权请求赔偿损失。
|
||||
适用法条:《民法典》第五百六十六条
|
||||
|
||||
【案例4】预期违约的认定
|
||||
裁判要旨:在履行期限届满前,当事人一方明确表示不履行主要债务的,对方可以解除合同并请求赔偿。"明确表示"包括书面声明、口头声明以及以行为表明。
|
||||
适用法条:《民法典》第五百六十三条第二项
|
||||
|
||||
【案例5】电子合同效力
|
||||
裁判要旨:通过微信、邮件等电子通讯方式达成的合意,具备合同的基本要素(主体、标的、数量等),可以认定合同成立。
|
||||
适用法条:《民法典》第四百六十九条'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 6. 合同模板文档 ==========
|
||||
|
||||
-- 买卖合同模板
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd3000000-0000-0000-0000-000000000001', kb_contract_id,
|
||||
'买卖合同范本', 'txt', 6000, 'completed', creator_id,
|
||||
'买卖合同
|
||||
|
||||
合同编号:_____________
|
||||
签订日期:____年____月____日
|
||||
签订地点:_____________
|
||||
|
||||
甲方(出卖人):_____________ 统一社会信用代码:_____________
|
||||
法定代表人:_____________ 联系电话:_____________
|
||||
地址:_____________
|
||||
|
||||
乙方(买受人):_____________ 统一社会信用代码:_____________
|
||||
法定代表人:_____________ 联系电话:_____________
|
||||
地址:_____________
|
||||
|
||||
根据《中华人民共和国民法典》及相关法律法规的规定,甲乙双方在平等、自愿、公平、诚信的原则基础上,经协商一致,就货物买卖事宜订立本合同。
|
||||
|
||||
第一条 标的物
|
||||
品名:_____________ 规格型号:_____________
|
||||
数量:_____________ 单位:_____________
|
||||
单价:_____________元 总价:_____________元
|
||||
|
||||
第二条 质量标准
|
||||
按照(国家标准/行业标准/双方约定标准)执行。
|
||||
|
||||
第三条 交货方式及期限
|
||||
3.1 交货方式:(送货上门/自提/快递)
|
||||
3.2 交货地点:_____________
|
||||
3.3 交货期限:合同签订后____日内
|
||||
|
||||
第四条 付款方式
|
||||
4.1 付款方式:(银行转账/电汇/支票)
|
||||
4.2 付款期限:(预付____% / 货到付款 / 验收合格后____日内)
|
||||
|
||||
第五条 验收
|
||||
5.1 乙方应在收到货物后____日内进行验收
|
||||
5.2 验收不合格的,乙方应在____日内书面通知甲方
|
||||
|
||||
第六条 违约责任
|
||||
6.1 甲方逾期交货的,每逾期一日按合同总价的____‰支付违约金
|
||||
6.2 乙方逾期付款的,每逾期一日按未付金额的____‰支付违约金
|
||||
6.3 违约金上限为合同总价的____%
|
||||
|
||||
第七条 争议解决
|
||||
因本合同引起的争议,双方协商解决;协商不成的,提交____________仲裁委员会仲裁/向____________人民法院提起诉讼。
|
||||
|
||||
第八条 合同生效
|
||||
本合同一式两份,甲乙双方各执一份,自双方签字盖章之日起生效。
|
||||
|
||||
甲方(盖章): 乙方(盖章):
|
||||
法定代表人: 法定代表人:
|
||||
日期: 日期:'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 劳动合同模板
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd3000000-0000-0000-0000-000000000002', kb_contract_id,
|
||||
'劳动合同范本', 'txt', 6000, 'completed', creator_id,
|
||||
'劳动合同书
|
||||
|
||||
甲方(用人单位):_____________
|
||||
统一社会信用代码:_____________
|
||||
法定代表人:_____________
|
||||
地址:_____________
|
||||
|
||||
乙方(劳动者):_____________
|
||||
身份证号码:_____________
|
||||
住址:_____________
|
||||
联系电话:_____________
|
||||
|
||||
根据《中华人民共和国劳动合同法》及有关法律法规规定,甲乙双方经平等协商,自愿签订本劳动合同。
|
||||
|
||||
第一条 合同期限
|
||||
本合同为(固定期限/无固定期限/以完成一定工作任务为期限)劳动合同。
|
||||
合同期限自____年____月____日起至____年____月____日止。
|
||||
试用期自____年____月____日起至____年____月____日止,共____个月。
|
||||
|
||||
第二条 工作内容和工作地点
|
||||
2.1 乙方从事____________岗位工作
|
||||
2.2 工作地点:_____________
|
||||
2.3 甲方可根据生产经营需要,合理调整乙方工作岗位
|
||||
|
||||
第三条 工作时间和休息休假
|
||||
3.1 实行(标准工时制/综合计算工时制/不定时工时制)
|
||||
3.2 标准工时制:每日工作8小时,每周工作40小时
|
||||
|
||||
第四条 劳动报酬
|
||||
4.1 月工资为人民币_______元(税前)
|
||||
4.2 发薪日为每月____日
|
||||
4.3 加班工资按照法律规定标准支付
|
||||
|
||||
第五条 社会保险和福利
|
||||
甲方依法为乙方缴纳养老、医疗、失业、工伤、生育保险和住房公积金。
|
||||
|
||||
第六条 劳动保护和劳动条件
|
||||
甲方应为乙方提供符合国家规定的劳动安全卫生条件和必要的劳动防护用品。
|
||||
|
||||
第七条 保密和竞业限制
|
||||
7.1 乙方应对甲方商业秘密承担保密义务
|
||||
7.2 竞业限制期限不超过二年
|
||||
|
||||
第八条 劳动合同的解除和终止
|
||||
按照《劳动合同法》相关规定执行。
|
||||
|
||||
甲方(盖章): 乙方(签字):
|
||||
法定代表人:
|
||||
日期: 日期:'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 7. 文书模板文档 ==========
|
||||
|
||||
-- 起诉状模板
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd4000000-0000-0000-0000-000000000001', kb_doc_id,
|
||||
'民事起诉状模板', 'txt', 4000, 'completed', creator_id,
|
||||
'民事起诉状
|
||||
|
||||
原告:_____________,性别____,____年____月____日生,____族,住____________。身份证号:_____________。联系电话:_____________。
|
||||
|
||||
被告:_____________,性别____,____年____月____日生,____族,住____________。身份证号:_____________。联系电话:_____________。
|
||||
|
||||
(若被告为法人)
|
||||
被告:_____________公司,住所地:_____________。统一社会信用代码:_____________。
|
||||
法定代表人:_____________,职务:_____________。
|
||||
|
||||
诉讼请求:
|
||||
一、请求判令被告向原告支付____________元;
|
||||
二、请求判令被告承担本案诉讼费用。
|
||||
|
||||
事实与理由:
|
||||
|
||||
_____________年____月____日,原告与被告因____________事宜(签订合同/发生纠纷/造成损害),具体事实如下:
|
||||
|
||||
(详细陈述案件事实经过)
|
||||
|
||||
综上所述,被告的行为已违反《_____________》第____条之规定,侵害了原告的合法权益。原告依据《中华人民共和国民事诉讼法》第____条之规定,特向贵院提起诉讼,恳请依法判决。
|
||||
|
||||
此致
|
||||
____________人民法院
|
||||
|
||||
具状人:____________
|
||||
____年____月____日
|
||||
|
||||
附:
|
||||
1. 本诉状副本____份
|
||||
2. 证据材料____份'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 法律意见书模板
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||
VALUES (
|
||||
'd4000000-0000-0000-0000-000000000002', kb_doc_id,
|
||||
'法律意见书模板', 'txt', 4000, 'completed', creator_id,
|
||||
'法 律 意 见 书
|
||||
|
||||
致:_____________(委托人/公司名称)
|
||||
|
||||
_____________律师事务所接受贵方委托,就____________事项出具如下法律意见书。
|
||||
|
||||
一、基本情况
|
||||
(一)委托事项简述
|
||||
(二)我们审查的主要文件和资料
|
||||
|
||||
二、事实认定
|
||||
根据贵方提供的材料和我们的调查了解,本事项的基本事实如下:
|
||||
(详细陈述已查明的事实)
|
||||
|
||||
三、法律分析
|
||||
(一)适用法律
|
||||
本事项主要涉及以下法律法规:
|
||||
1.《_____________》第____条
|
||||
2.《_____________》第____条
|
||||
|
||||
(二)法律关系分析
|
||||
(详细分析法律关系、各方权利义务)
|
||||
|
||||
(三)风险提示
|
||||
1. ____________风险
|
||||
2. ____________风险
|
||||
|
||||
四、法律意见
|
||||
综合以上分析,我们的法律意见如下:
|
||||
1. _____________
|
||||
2. _____________
|
||||
3. _____________
|
||||
|
||||
五、特别说明
|
||||
1. 本法律意见书仅基于委托人提供的材料出具
|
||||
2. 如相关事实发生变化,本法律意见可能需要调整
|
||||
3. 本意见书仅供委托人内部决策参考
|
||||
|
||||
_____________律师事务所
|
||||
经办律师:_____________
|
||||
日期:____年____月____日'
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== 8. 关联知识库到应用 ==========
|
||||
|
||||
-- 法律法规检索 → 法律法规库 + 司法解释库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'legal-research';
|
||||
-- 法律咨询助手 → 法律法规库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'legal-consult';
|
||||
-- 合同条款审查 → 合同模板库
|
||||
UPDATE applications SET knowledge_base_id = kb_contract_id WHERE slug = 'contract-review';
|
||||
-- 法律文书生成 → 文书模板库
|
||||
UPDATE applications SET knowledge_base_id = kb_doc_id WHERE slug = 'legal-doc-gen';
|
||||
-- 合同条款生成 → 合同模板库
|
||||
UPDATE applications SET knowledge_base_id = kb_contract_id WHERE slug = 'contract-clause-gen';
|
||||
-- 案件风险评估 → 法律法规库
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'case-risk-eval';
|
||||
-- 诉讼策略助手 → 司法解释库
|
||||
UPDATE applications SET knowledge_base_id = kb_judicial_id WHERE slug = 'litigation-agent';
|
||||
|
||||
END $$;
|
||||
|
||||
SELECT '律师行业完整数据(机构+用户+知识库+文档+关联)插入成功!' as status;
|
||||
@@ -0,0 +1,59 @@
|
||||
-- 多机构多用户种子数据
|
||||
-- 密码统一为 admin123
|
||||
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
pwd_hash TEXT;
|
||||
BEGIN
|
||||
-- 获取已有用户的密码hash(admin123)
|
||||
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||
|
||||
-- 科技局用户(已存在的用户更新org_id)
|
||||
UPDATE users SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE email = 'admin@govai.gov.cn';
|
||||
UPDATE users SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE email = 'wangke@govai.gov.cn';
|
||||
UPDATE users SET org_id = 'a0000000-0000-0000-0000-000000000001' WHERE email = 'liganshi@govai.gov.cn';
|
||||
|
||||
-- 公安局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '张队长', 'zhangdui@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000002'),
|
||||
(gen_random_uuid(), '刘警官', 'liujing@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000002')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 发改局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '陈处长', 'chenchu@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000003'),
|
||||
(gen_random_uuid(), '赵科员', 'zhaoke@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000003')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 教育局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '孙局长', 'sunju@govai.gov.cn', pwd_hash, 'admin', 'active', 'a0000000-0000-0000-0000-000000000004'),
|
||||
(gen_random_uuid(), '周老师', 'zhoushi@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000004')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 人社局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '吴主任', 'wuzhu@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000005'),
|
||||
(gen_random_uuid(), '郑专员', 'zhengzy@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000005')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 财政局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '钱局长', 'qianju@govai.gov.cn', pwd_hash, 'admin', 'active', 'a0000000-0000-0000-0000-000000000006'),
|
||||
(gen_random_uuid(), '孟会计', 'mengkj@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000006')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 住建局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '何主任', 'hezhu@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000007'),
|
||||
(gen_random_uuid(), '冯工程师', 'fenggs@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000007')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
-- 市监局用户
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '杨科长', 'yangke@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000008'),
|
||||
(gen_random_uuid(), '许专员', 'xuzy@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000008')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
END $$;
|
||||
@@ -0,0 +1,489 @@
|
||||
-- 科技局政策法规知识库 - 真实法规文本内容
|
||||
-- Run: psql -d govai_portal -f seed_policy_content.sql
|
||||
|
||||
-- ========== 1. 科学技术进步法(核心条款摘录)==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《中华人民共和国科学技术进步法》(2021年12月24日修订)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为了全面促进科学技术进步,发挥科学技术第一生产力、创新第一动力、人才第一资源的作用,促进科技成果向现实生产力转化,推动科技创新支撑和引领经济社会发展,全面建设社会主义现代化国家,根据宪法,制定本法。
|
||||
|
||||
第二条 坚持中国共产党对科学技术事业的全面领导。国家坚持新发展理念,坚持科技创新在国家现代化建设全局中的核心地位,把科技自立自强作为国家发展的战略支撑。
|
||||
|
||||
第三条 科学技术进步工作应当面向世界科技前沿、面向经济主战场、面向国家重大需求、面向人民生命健康,为促进经济社会发展、维护国家安全和推动人类文明进步服务。
|
||||
|
||||
第七条 国家遵循科学技术活动服务国家目标与鼓励自由探索相结合的原则,超前部署重大基础研究、有重大产业应用前景的前沿技术研究和社会公益性技术研究,支持基础研究、前沿技术研究和社会公益性技术研究持续、稳定发展。
|
||||
|
||||
第二章 基础研究
|
||||
|
||||
第二十条 国家加强基础研究能力建设,尊重科学发展规律和人才成长规律,强化项目、人才、基地的系统布局,为基础研究发展提供良好的物质条件和有力的制度保障。
|
||||
|
||||
第二十二条 国家完善基础研究的学科体系布局,建立基础研究稳定支持的投入机制,提高基础研究经费在全社会研究开发经费总额中的比例。
|
||||
|
||||
第三章 应用研究与成果转化
|
||||
|
||||
第三十条 国家建立以企业为主体、市场为导向、产学研深度融合的技术创新体系,引导和扶持企业技术创新活动,发挥企业在技术创新中的主体作用。
|
||||
|
||||
第三十三条 国家鼓励和支持农业科学技术的基础研究和应用研究,传播和普及农业科学技术知识,加快农业科技成果转化和产业化,促进农业科技进步。
|
||||
|
||||
第四章 企业科技创新
|
||||
|
||||
第五十条 国家鼓励企业加大研究开发和技术创新投入,自主确立研究开发课题,开展技术创新活动。国家鼓励企业对引进技术进行消化、吸收和再创新。
|
||||
|
||||
第五十二条 国家建立以政府资金为引导、企业投入为主体、金融资本和民间投入为支撑的多元化科技创新投入体系。
|
||||
|
||||
第九章 科技人员
|
||||
|
||||
第九十三条 科学技术人员是社会主义现代化建设事业的重要人才力量。国家采取各种措施,提高科学技术人员的社会地位,培养和造就专门的科学技术人才。
|
||||
|
||||
第九十五条 各级人民政府和企业事业组织应当采取措施,完善体现知识、技术等创新要素价值的收益分配机制,优化收入结构,提高科学技术人员的工资待遇。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- ========== 2. 高新技术企业认定管理办法 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《高新技术企业认定管理办法》(国科发火〔2016〕32号)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为扶持和鼓励高新技术企业发展,根据《中华人民共和国企业所得税法》、《中华人民共和国企业所得税法实施条例》,制定本办法。
|
||||
|
||||
第二条 本办法所称的高新技术企业是指:在《国家重点支持的高新技术领域》内,持续进行研究开发与技术成果转化,形成企业核心自主知识产权,并以此为基础开展经营活动,在中国境内(不包括港、澳、台地区)注册的居民企业。
|
||||
|
||||
第三条 高新技术企业认定管理工作应遵循突出企业主体、鼓励技术创新、实施动态管理、坚持公平公正的原则。
|
||||
|
||||
第二章 组织与实施
|
||||
|
||||
第十一条 认定为高新技术企业须同时满足以下条件:
|
||||
(一)企业申请认定时须注册成立一年以上;
|
||||
(二)企业通过自主研发、受让、受赠、并购等方式,获得对其主要产品(服务)在技术上发挥核心支持作用的知识产权的所有权;
|
||||
(三)对企业主要产品(服务)发挥核心支持作用的技术属于《国家重点支持的高新技术领域》规定的范围;
|
||||
(四)企业从事研发和相关技术创新活动的科技人员占企业当年职工总数的比例不低于10%;
|
||||
(五)企业近三个会计年度(实际经营期不满三年的按实际经营时间计算)的研究开发费用总额占同期销售收入总额的比例符合如下要求:
|
||||
1. 最近一年销售收入小于5,000万元(含)的企业,比例不低于5%;
|
||||
2. 最近一年销售收入在5,000万元至2亿元(含)的企业,比例不低于4%;
|
||||
3. 最近一年销售收入在2亿元以上的企业,比例不低于3%。
|
||||
其中,企业在中国境内发生的研究开发费用总额占全部研究开发费用总额的比例不低于60%;
|
||||
(六)近一年高新技术产品(服务)收入占企业同期总收入的比例不低于60%;
|
||||
(七)企业创新能力评价应达到相应要求;
|
||||
(八)企业申请认定前一年内未发生重大安全、重大质量事故或严重环境违法行为。
|
||||
|
||||
第三章 税收优惠
|
||||
|
||||
经认定的高新技术企业,可依照《企业所得税法》及其《实施条例》、本办法和税收征管法及《实施细则》等有关规定,减按15%的税率征收企业所得税。
|
||||
|
||||
国家重点支持的高新技术领域:
|
||||
一、电子信息
|
||||
二、生物与新医药
|
||||
三、航空航天
|
||||
四、新材料
|
||||
五、高技术服务
|
||||
六、新能源与节能
|
||||
七、资源与环境
|
||||
八、先进制造与自动化
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000002';
|
||||
|
||||
-- ========== 3. 国家重点研发计划管理暂行办法 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《国家重点研发计划管理暂行办法》
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为规范国家重点研发计划(以下简称重点研发计划)的组织实施和资金管理,提高资金使用效益,根据《关于深化中央财政科技计划(专项、基金等)管理改革的方案》和国家有关法律法规,制定本办法。
|
||||
|
||||
第二条 重点研发计划由若干目标明确、边界清晰的重点专项组成,重点专项是重点研发计划组织实施的载体。
|
||||
|
||||
第三条 重点研发计划面向世界科技前沿、面向经济主战场、面向国家重大需求,重点资助事关国计民生的农业、能源资源、生态环境、健康等领域中需要长期演进的重大社会公益性研究,以及事关产业核心竞争力、整体自主创新能力和国家安全的战略性、基础性、前瞻性重大科学问题、重大共性关键技术和产品研发。
|
||||
|
||||
第二章 重点专项设立与调整
|
||||
|
||||
第六条 重点专项的设立应综合考虑以下因素:
|
||||
(一)国民经济和社会发展重大需求;
|
||||
(二)世界科技发展趋势和我国科技发展战略;
|
||||
(三)相关领域的基础和条件;
|
||||
(四)预期目标和效果。
|
||||
|
||||
第三章 项目管理
|
||||
|
||||
第十一条 项目(课题)实施期限一般不超过5年,具体按重点专项实施方案确定。
|
||||
|
||||
第十二条 项目由项目牵头单位负责组织实施,项目负责人是项目实施的第一责任人。
|
||||
|
||||
第四章 经费管理
|
||||
|
||||
第二十条 项目(课题)经费由直接费用和间接费用组成。
|
||||
直接费用包括:设备费、材料费、测试化验加工费、燃料动力费、差旅费/会议费/国际合作与交流费、出版/文献/信息传播/知识产权事务费、劳务费、专家咨询费、其他支出。
|
||||
|
||||
第二十一条 间接费用实行总额控制。间接费用按照不超过课题直接费用扣除设备购置费后的一定比例核定:
|
||||
500万元以下的部分,比例不超过20%;
|
||||
500万元至1000万元的部分,比例不超过15%;
|
||||
1000万元以上的部分,比例不超过13%。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000003';
|
||||
|
||||
-- ========== 4. 科技成果转化法实施细则 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《促进科技成果转化法》重要条款
|
||||
|
||||
第一条 为了促进科技成果转化为现实生产力,规范科技成果转化活动,加速科学技术进步,推动经济建设和社会发展,制定本法。
|
||||
|
||||
第二条 本法所称科技成果转化,是指为提高生产力水平而对科技成果所进行的后续试验、开发、应用、推广直至形成新技术、新工艺、新材料、新产品,发展新产业等活动。
|
||||
|
||||
科技成果转化的方式:
|
||||
(一)自行投资实施转化;
|
||||
(二)向他人转让该技术成果;
|
||||
(三)许可他人使用该科技成果;
|
||||
(四)以该科技成果作为合作条件,与他人共同实施转化;
|
||||
(五)以该科技成果作价投资、折算股份或者出资比例;
|
||||
(六)其他协商确定的方式。
|
||||
|
||||
重要激励措施:
|
||||
|
||||
第四十三条 国家设立的研究开发机构、高等院校转化科技成果所获得的收入全部留归本单位,在对完成、转化职务科技成果做出重要贡献的人员给予奖励和报酬后,主要用于科学技术研究开发与成果转化等相关工作。
|
||||
|
||||
第四十五条 科技成果完成单位可以规定或者与科技人员约定奖励和报酬的方式、数额和时限。单位制定相关规定,应当充分听取本单位科技人员的意见,并在本单位公开相关规定。
|
||||
|
||||
对完成、转化职务科技成果做出重要贡献的人员给予奖励和报酬的规定:
|
||||
(一)将该项职务科技成果转让、许可给他人实施的,从该项科技成果转让净收入或者许可净收入中提取不低于百分之五十的比例;
|
||||
(二)利用该项职务科技成果作价投资的,从该项科技成果形成的股份或者出资比例中提取不低于百分之五十的比例;
|
||||
(三)将该项职务科技成果自行实施或者与他人合作实施的,应当在实施转化成功投产后连续三至五年,每年从实施该项科技成果的营业利润中提取不低于百分之五的比例。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000004';
|
||||
|
||||
-- ========== 5. 研发费用加计扣除政策 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《企业研发费用加计扣除政策指引》(2023版)
|
||||
|
||||
一、政策概述
|
||||
|
||||
企业开展研发活动中实际发生的研发费用,未形成无形资产计入当期损益的,在按规定据实扣除的基础上,自2023年1月1日起,再按照实际发生额的100%在税前加计扣除;形成无形资产的,自2023年1月1日起,按照无形资产成本的200%在税前摊销。
|
||||
|
||||
二、适用对象
|
||||
|
||||
除烟草制造业、住宿和餐饮业、批发和零售业、房地产业、租赁和商务服务业、娱乐业等负面清单行业以外的其他企业。
|
||||
|
||||
三、研发费用归集范围
|
||||
|
||||
(一)人员人工费用:直接从事研发活动人员的工资薪金、基本养老保险费、基本医疗保险费、失业保险费、工伤保险费、生育保险费和住房公积金,以及外聘研发人员的劳务费用。
|
||||
|
||||
(二)直接投入费用:
|
||||
1. 研发活动直接消耗的材料、燃料和动力费用;
|
||||
2. 用于中间试验和产品试制的模具、工艺装备开发及制造费;
|
||||
3. 不构成固定资产的样品、样机及一般测试手段购置费;
|
||||
4. 用于研发活动的仪器、设备的运行维护、调整、检验、维修等费用;
|
||||
5. 用于研发活动的仪器、设备的租赁费用。
|
||||
|
||||
(三)折旧费用:用于研发活动的仪器、设备的折旧费。
|
||||
|
||||
(四)无形资产摊销费用:用于研发活动的软件、专利权、非专利技术(包括许可证、专有技术、设计和计算方法等)的摊销费用。
|
||||
|
||||
(五)新产品设计费、新工艺规程制定费、新药研制的临床试验费、勘探开发技术的现场试验费。
|
||||
|
||||
(六)其他相关费用:与研发活动直接相关的其他费用,此类费用总额不得超过可加计扣除研发费用总额的10%。
|
||||
|
||||
四、申报要求
|
||||
|
||||
企业应按照国家财务会计制度要求,对研发支出进行会计处理;同时,对享受加计扣除的研发费用按研发项目设置辅助账,准确归集核算当年可加计扣除的各项研发费用实际发生额。
|
||||
|
||||
五、注意事项
|
||||
|
||||
1. 企业同时符合高新技术企业优惠和研发费用加计扣除优惠的,可以同时享受。
|
||||
2. 委托外部研发的费用,按照费用实际发生额的80%计入委托方研发费用加计扣除。
|
||||
3. 企业共同合作开发的项目,由合作各方就自身实际承担的研发费用分别计算加计扣除。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000005';
|
||||
|
||||
-- ========== 6. 促进科技成果转移转化若干措施 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《关于进一步促进科技成果转移转化的若干措施》
|
||||
|
||||
一、总体要求
|
||||
|
||||
深入实施创新驱动发展战略,打通科技与经济结合的通道,促进大众创业、万众创新,鼓励研究开发机构、高等院校、企业等创新主体及科技人员转移转化科技成果,推进经济提质增效升级。
|
||||
|
||||
二、主要措施
|
||||
|
||||
(一)开展科技成果信息汇交与发布
|
||||
建立国家科技成果信息系统,推动财政资金支持形成的科技成果信息汇交与发布。在不涉密的前提下,相关科技成果的转化方式、转化价格等信息应当向社会公开。
|
||||
|
||||
(二)完善科技成果转化的市场化定价机制
|
||||
国家设立的研究开发机构、高等院校对其持有的科技成果,可以自主决定转让、许可或者作价投资,除涉及国家秘密、国家安全外,不需审批或者备案。
|
||||
|
||||
通过协议定价、在技术交易市场挂牌交易、拍卖等方式确定价格。
|
||||
|
||||
(三)落实科技成果转移转化的税收政策
|
||||
个人以技术成果投资入股,可选择在投资入股环节暂不纳税,待转让股权时再按规定缴纳所得税。
|
||||
|
||||
(四)加强科技成果转化人才队伍建设
|
||||
科技成果转化的业绩应当作为相关专业技术职称评审、岗位管理和考核评价的重要依据。
|
||||
|
||||
(五)加大科技成果转化的资金支持力度
|
||||
引导设立国家科技成果转化引导基金,综合运用设立创业投资子基金、贷款风险补偿和绩效奖励等方式,吸引社会资本加大对科技成果转化的投入。
|
||||
|
||||
(六)强化科技成果转移转化的知识产权保护
|
||||
加大知识产权侵权行为惩治力度,提高知识产权侵权法定赔偿上限,将故意侵犯知识产权纳入企业和个人信用记录。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000006';
|
||||
|
||||
-- ========== 7. 知识产权强国建设纲要 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《知识产权强国建设纲要(2021—2035年)》重要内容
|
||||
|
||||
一、发展目标
|
||||
|
||||
到2025年:
|
||||
- 专利密集型产业增加值占GDP比重达到13%
|
||||
- 版权产业增加值占GDP比重达到7.5%
|
||||
- 知识产权使用费年进出口总额达到3500亿元
|
||||
- 每万人口高价值发明专利拥有量达到12件
|
||||
|
||||
到2035年:
|
||||
知识产权综合竞争力跻身世界前列,中国特色、世界水平的知识产权强国基本建成。
|
||||
|
||||
二、重点任务
|
||||
|
||||
(一)建设面向社会主义现代化的知识产权制度
|
||||
1. 构建门类齐全、结构严密、内外协调的法律体系
|
||||
2. 建立公正合理、评估科学的知识产权归属制度
|
||||
3. 实行严格的知识产权保护制度
|
||||
|
||||
(二)建设支撑国际一流营商环境的知识产权保护体系
|
||||
1. 健全公正高效、管辖科学、权界清晰、系统完备的司法保护体制
|
||||
2. 健全便捷高效、严格公正、公开透明的行政保护体系
|
||||
3. 健全统一领导、衔接顺畅、快速高效的协同保护格局
|
||||
|
||||
(三)建设激励创新发展的知识产权市场运行机制
|
||||
1. 完善以企业为主体、市场为导向的高质量创造机制
|
||||
2. 建立规范有序、充满活力的市场化运营机制
|
||||
3. 构建标准引领、计量支撑、检验检测和认证认可协同的质量基础设施体系
|
||||
|
||||
(四)建设便民利民的知识产权公共服务体系
|
||||
1. 加强覆盖全面、服务规范、智能高效的公共服务供给
|
||||
2. 提升专业化、市场化、国际化服务水平
|
||||
3. 推进知识产权信息化、智能化基础设施建设
|
||||
|
||||
三、实施保障
|
||||
加强组织领导,加大资金投入,强化人才支撑,深化国际合作。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000007';
|
||||
|
||||
-- ========== 8. 科技型中小企业评价办法 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《科技型中小企业评价办法》(国科发政〔2017〕115号)
|
||||
|
||||
第一条 为加大对科技型中小企业的精准支持力度,壮大科技型中小企业群体,培育新的经济增长点,制定本办法。
|
||||
|
||||
第二条 本办法所称的科技型中小企业是指依托一定数量的科技人员从事科学技术研究开发活动,取得自主知识产权并将其转化为高新技术产品或服务,从而实现可持续发展的中小企业。
|
||||
|
||||
第六条 科技型中小企业须同时满足以下条件:
|
||||
(一)在中国境内(不包括港、澳、台地区)注册的居民企业。
|
||||
(二)职工总数不超过500人、年销售收入不超过2亿元、资产总额不超过2亿元。
|
||||
(三)企业提供的产品和服务不属于国家规定的禁止、限制和淘汰类。
|
||||
(四)企业在填报上一年及当年内未发生重大安全、重大质量事故和严重环境违法、科研严重失信行为,且企业未列入经营异常名录和严重违法失信企业名单。
|
||||
(五)企业根据科技型中小企业评价指标进行综合评价所得分值不低于60分,且科技人员指标得分不得为0分。
|
||||
|
||||
第七条 科技型中小企业评价指标具体包括以下三类:
|
||||
(一)科技人员指标(满分20分)
|
||||
(二)研发投入指标(满分50分)
|
||||
(三)科技成果指标(满分30分)
|
||||
|
||||
直通车条件(满足任一即可直接确认):
|
||||
1. 拥有经认定的省部级以上研发机构;
|
||||
2. 近五年内获得过国家级科技奖励;
|
||||
3. 近五年内主导制定过国际标准、国家标准或行业标准;
|
||||
4. 近五年内获得国家级高新技术企业认定。
|
||||
|
||||
享受优惠:
|
||||
科技型中小企业开展研发活动中实际发生的研发费用,未形成无形资产计入当期损益的,在按规定据实扣除的基础上,再按照实际发生额的100%在税前加计扣除;形成无形资产的,按照无形资产成本的200%在税前摊销。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000008';
|
||||
|
||||
-- ========== 9. 国家自然科学基金条例 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《国家自然科学基金条例》
|
||||
|
||||
第一条 为了促进自然科学基础研究发展,发挥国家自然科学基金的导向作用,规范和加强国家自然科学基金管理,制定本条例。
|
||||
|
||||
第二条 国家设立国家自然科学基金(以下简称基金),用于资助自然科学基础研究和部分应用研究。
|
||||
|
||||
第三条 基金资助应当坚持尊重科学规律,公开、公平、公正的原则,实行科学民主决策。
|
||||
|
||||
资助类别:
|
||||
|
||||
(一)面上项目:支持从事基础研究的科学技术人员在科学基金资助范围内自由选题,开展创新性的科学研究。资助期限一般为4年,资助强度根据领域不同一般为50-80万元。
|
||||
|
||||
(二)重点项目:支持从事基础研究的科学技术人员针对已有较好基础的研究方向或学科生长点开展深入、系统的创新性研究。资助期限一般为5年,资助强度一般为200-300万元。
|
||||
|
||||
(三)重大项目:支持科学技术人员瞄准国际科学前沿和国家经济、社会、科技发展及国家安全的重大需求中的重大科学问题。资助期限一般为5年,资助强度一般为800-2000万元。
|
||||
|
||||
(四)青年科学基金项目:支持青年科学技术人员在基金资助范围内自主选题,开展基础研究工作。申请人应具有博士学位或具有高级专业技术职务,年龄要求:男性未满35周岁,女性未满40周岁。
|
||||
|
||||
(五)地区科学基金项目:支持特定地区的科学技术人员开展基础研究工作,培养和扶植人才。
|
||||
|
||||
申请条件:
|
||||
1. 具有承担基础研究课题或者其他从事基础研究的经历;
|
||||
2. 具有高级专业技术职务(职称)或者具有博士学位;
|
||||
3. 有保证完成项目的时间保障。
|
||||
|
||||
评审程序:初审 → 同行评议 → 会议评审 → 批准资助
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000009';
|
||||
|
||||
-- ========== 10. 科技创新十四五规划(地方版示例)==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《科技创新"十四五"规划》(地方版要点)
|
||||
|
||||
一、发展基础
|
||||
|
||||
"十三五"期间主要成就:
|
||||
- 全社会研发经费投入强度达到2.8%
|
||||
- 高新技术企业数量增长150%
|
||||
- 技术合同成交额年均增长20%以上
|
||||
- 万人发明专利拥有量达到15件
|
||||
- 科技进步贡献率达到60%
|
||||
|
||||
二、发展目标
|
||||
|
||||
到2025年:
|
||||
1. 全社会R&D经费支出占GDP比重达到3.2%以上
|
||||
2. 规上工业企业中有研发活动的企业占比达到50%
|
||||
3. 高新技术企业数量达到1000家以上
|
||||
4. 技术合同成交额达到100亿元
|
||||
5. 万人发明专利拥有量达到25件
|
||||
6. 每万名就业人员中研发人员达到80人年
|
||||
|
||||
三、重点任务
|
||||
|
||||
(一)强化战略科技力量建设
|
||||
- 争创国家级重点实验室1-2家
|
||||
- 建设省级重点实验室10家以上
|
||||
- 培育新型研发机构20家以上
|
||||
|
||||
(二)实施关键核心技术攻关
|
||||
重点领域:
|
||||
1. 新一代信息技术(人工智能、大数据、5G应用)
|
||||
2. 高端装备制造(智能机器人、精密仪器)
|
||||
3. 新材料(碳纤维、半导体材料)
|
||||
4. 生物医药(创新药物、医疗器械)
|
||||
5. 新能源(光伏、储能、氢能)
|
||||
|
||||
(三)构建高能级创新平台体系
|
||||
- 建设科技创新中心核心区
|
||||
- 打造产业技术创新联盟
|
||||
- 建设大学科技园和科技企业孵化器
|
||||
|
||||
(四)培育壮大创新主体
|
||||
- 实施高新技术企业倍增计划
|
||||
- 建立科技型中小企业梯度培育机制
|
||||
- 支持企业建设研发机构
|
||||
|
||||
(五)促进科技成果转化
|
||||
- 建设技术转移中心
|
||||
- 完善成果转化激励机制
|
||||
- 发展科技服务业
|
||||
|
||||
四、保障措施
|
||||
|
||||
(一)加大财政科技投入
|
||||
- 确保财政科技支出年增长不低于15%
|
||||
- 设立科技创新专项资金不少于5亿元/年
|
||||
|
||||
(二)完善人才政策
|
||||
- 实施科技领军人才引进计划
|
||||
- 建立柔性引才机制
|
||||
- 提供住房、子女教育等配套保障
|
||||
|
||||
(三)优化创新生态
|
||||
- 深化科技体制改革
|
||||
- 完善知识产权保护
|
||||
- 营造创新创业氛围
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000010';
|
||||
|
||||
-- ========== 11. 科技项目经费管理办法 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《科技项目经费管理办法》
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为规范和加强科技项目经费管理,提高资金使用效益,制定本办法。
|
||||
|
||||
第二条 本办法适用于市级财政科技资金支持的各类科技计划项目(含科技重大专项)。
|
||||
|
||||
第二章 经费开支范围
|
||||
|
||||
第五条 项目经费由直接费用和间接费用组成。
|
||||
|
||||
直接费用是指在项目实施过程中发生的与之直接相关的费用,主要包括:
|
||||
|
||||
(一)设备费:购置或试制专用仪器设备,对现有仪器设备进行升级改造,以及租赁外单位仪器设备而发生的费用。
|
||||
单台套价格在200万元以上的设备,应当进行必要性和合理性论证。
|
||||
|
||||
(二)业务费:含材料/测试化验加工费、燃料动力费、出版/文献/信息传播/知识产权事务费等。
|
||||
|
||||
(三)劳务费:支付给项目组成员中没有工资性收入的在校研究生、博士后以及项目聘用的研究人员、科研辅助人员等的劳务性费用。
|
||||
|
||||
(四)专家咨询费:支付给临时聘请的咨询专家的费用。标准:高级专业技术职称人员500-1500元/人天,其他人员300-1000元/人天。
|
||||
|
||||
间接费用核定比例:
|
||||
- 直接费用扣除设备费后500万元以下的部分,不超过25%
|
||||
- 500万元至1000万元的部分,不超过20%
|
||||
- 1000万元以上的部分,不超过15%
|
||||
|
||||
第三章 经费拨付与使用
|
||||
|
||||
第十条 项目经费实行分期拨付,一般分2-3次拨付。首次拨付比例不超过70%。
|
||||
|
||||
第十一条 结余资金留归项目承担单位使用,在2年内由项目承担单位统筹安排用于科研活动的直接支出。
|
||||
|
||||
第四章 监督检查
|
||||
|
||||
第十五条 项目承担单位应当建立健全项目经费内部管理制度,加强经费核算与管理。
|
||||
|
||||
第十六条 对弄虚作假、截留、挤占、挪用项目经费等违规行为,将追缴违规资金,取消项目承担资格,并依法追究责任。
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000011';
|
||||
|
||||
-- ========== 12. 技术合同认定登记管理办法 ==========
|
||||
UPDATE knowledge_documents SET content = '
|
||||
《技术合同认定登记管理办法》
|
||||
|
||||
第一条 为规范技术合同认定登记工作,维护技术市场秩序,保障技术合同当事人的合法权益,促进技术贸易的发展,根据有关法律法规,制定本办法。
|
||||
|
||||
第二条 技术合同是指当事人就技术开发、技术转让、技术许可、技术咨询或者技术服务订立的确立相互之间权利和义务的合同。
|
||||
|
||||
技术合同类型:
|
||||
|
||||
(一)技术开发合同:当事人之间就新技术、新产品、新工艺、新品种或者新材料及其系统的研究开发所订立的合同。
|
||||
包括:委托开发合同和合作开发合同。
|
||||
|
||||
(二)技术转让合同:合法拥有技术的权利人,将现有特定的专利、专利申请、技术秘密的相关权利让与他人所订立的合同。
|
||||
|
||||
(三)技术许可合同:合法拥有技术的权利人,将现有特定的专利、技术秘密的相关权利许可他人实施、使用所订立的合同。
|
||||
包括:独占许可、排他许可、普通许可。
|
||||
|
||||
(四)技术咨询合同:当事人一方以技术知识为对方就特定技术项目提供可行性论证、技术预测、专题技术调查、分析评价报告等所订立的合同。
|
||||
|
||||
(五)技术服务合同:当事人一方以技术知识为对方解决特定技术问题所订立的合同(不包括加工承揽合同和建设工程合同)。
|
||||
|
||||
认定登记程序:
|
||||
1. 合同当事人向技术合同登记机构提出认定登记申请
|
||||
2. 提交合同文本及相关附件
|
||||
3. 技术合同登记机构在30日内完成认定
|
||||
4. 对认定为技术合同的,发放技术合同认定登记证明
|
||||
|
||||
税收优惠政策:
|
||||
- 技术开发合同和技术转让合同经认定登记后,可享受免征增值税优惠
|
||||
- 个人技术转让收入,现阶段暂免征收个人所得税
|
||||
- 居民企业技术转让所得不超过500万元的部分,免征企业所得税;超过500万元的部分,减半征收企业所得税
|
||||
|
||||
登记所需材料:
|
||||
1. 技术合同认定登记申请表
|
||||
2. 合同文本(原件或盖章复印件)
|
||||
3. 技术开发合同附技术开发计划书
|
||||
4. 与合同相关的技术背景资料
|
||||
5. 委托代理登记的,提交代理委托书
|
||||
' WHERE id = '30000000-0000-0000-0000-000000000012';
|
||||
|
||||
-- ========== 关联政策法规问答应用到知识库 ==========
|
||||
UPDATE applications
|
||||
SET knowledge_base_id = '20000000-0000-0000-0000-000000000001'
|
||||
WHERE id = '10000000-0000-0000-0000-000000000001';
|
||||
|
||||
-- 更新 search_vector(触发器会自动执行)
|
||||
-- 手动刷新一次确保所有记录都有向量
|
||||
UPDATE knowledge_documents SET search_vector = to_tsvector('simple', COALESCE(name, '') || ' ' || COALESCE(content, ''))
|
||||
WHERE kb_id = '20000000-0000-0000-0000-000000000001' AND content IS NOT NULL AND content != '';
|
||||
|
||||
SELECT '政策法规内容写入完成!共12篇法规文本,已关联到政策法规问答应用。' AS status;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- PPT 智能生成应用种子数据
|
||||
|
||||
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, status, visibility, dify_app_type, dify_api_key, welcome_message, suggested_prompts, app_config) VALUES
|
||||
('10000000-0000-0000-0000-000000000011', '智能PPT生成', 'ppt-generator',
|
||||
'上传文档或输入主题,AI 自动生成原生可编辑的 PowerPoint 演示文稿',
|
||||
'## 功能介绍\n\n智能 PPT 生成系统,基于 PPT Master 引擎:\n\n- 📄 支持 PDF、Word、网页链接、纯文本等多种输入\n- 🎨 多种风格可选:通用、咨询、顶级咨询\n- 📐 多种画布格式:16:9、4:3、竖版等\n- 🖼️ 可选 AI 生图或网络图片搜索\n- ✏️ 输出原生可编辑 PPTX(非图片)\n\n## 使用方法\n\n1. 选择输入方式(上传文件/粘贴文本/输入网址)\n2. 设置生成参数(风格、页数、格式等)\n3. 点击生成,等待 AI 完成\n4. 下载可编辑的 .pptx 文件',
|
||||
'🎯',
|
||||
(SELECT id FROM categories WHERE slug = 'general' LIMIT 1),
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'approved', 'public', 'ppt_generator', 'app-placeholder',
|
||||
'您好!我是智能 PPT 生成助手。请上传您的源文件(PDF/Word)、粘贴文本内容,或输入网页链接,我将为您生成一份专业的可编辑 PowerPoint 演示文稿。',
|
||||
'["用这份报告生成一份10页的汇报PPT","帮我做一份关于数字政府建设的PPT","把这个政策文件做成宣讲PPT"]',
|
||||
'{"system_prompt":"你是一个专业的演示文稿设计专家,擅长将各类文档和内容转化为逻辑清晰、视觉专业的PowerPoint演示文稿。","app_type":"ppt_generator","default_config":{"format":"ppt169","page_count":10,"style":"general","with_images":true,"image_backend":"gpt-image-2"}}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
UPDATE applications SET published_at = NOW() WHERE id = '10000000-0000-0000-0000-000000000011' AND published_at IS NULL;
|
||||
UPDATE applications SET is_featured = true WHERE id = '10000000-0000-0000-0000-000000000011';
|
||||
@@ -0,0 +1,359 @@
|
||||
-- ============================================================
|
||||
-- 信访局种子数据:分类 + 10个应用
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 机构
|
||||
INSERT INTO organizations (id, name, slug, short_name, description, sort_order) VALUES
|
||||
('a0000000-0000-0000-0000-000000000010', '信访局', 'xinfang', '信访办', '负责信访工作的综合协调、政策指导和督查督办', 10)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 分类
|
||||
INSERT INTO categories (id, name, slug, sort_order, org_id) VALUES
|
||||
('c0000000-0000-0000-0010-000000000001', '信访受理', 'xinfang-accept', 1, 'a0000000-0000-0000-0000-000000000010'),
|
||||
('c0000000-0000-0000-0010-000000000002', '政策答复', 'xinfang-reply', 2, 'a0000000-0000-0000-0000-000000000010'),
|
||||
('c0000000-0000-0000-0010-000000000003', '矛盾调解', 'xinfang-mediate', 3, 'a0000000-0000-0000-0000-000000000010'),
|
||||
('c0000000-0000-0000-0010-000000000004', '督查督办', 'xinfang-supervise', 4, 'a0000000-0000-0000-0000-000000000010')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 应用
|
||||
|
||||
-- 3.1 信访政策咨询(chatbot,推荐)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
welcome_message, suggested_prompts, app_config,
|
||||
temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000001',
|
||||
'信访政策咨询',
|
||||
'xinfang-policy-consult',
|
||||
'智能解答信访条例、信访流程和政策法规问题',
|
||||
'## 功能介绍
|
||||
|
||||
智能信访政策咨询助手,为信访工作人员和群众提供:
|
||||
|
||||
- 信访条例和相关法规的精准解读
|
||||
- 信访办理流程和时限规定查询
|
||||
- 信访事项受理范围和转办规则说明
|
||||
|
||||
## 使用方法
|
||||
|
||||
直接输入信访相关的政策问题,如"信访事项办理时限是多少天"、"哪些事项不属于信访受理范围"等。',
|
||||
'inbox',
|
||||
'c0000000-0000-0000-0010-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'chatbot', 'app-placeholder',
|
||||
'您好!我是信访政策咨询助手。您可以向我咨询信访工作条例、办理流程、受理范围等相关政策问题。',
|
||||
'["信访事项的受理范围包括哪些?","信访办理的法定时限是多少天?","越级信访如何处理?","网上信访的流程是什么?"]',
|
||||
'{"system_prompt":"你是一个信访政策智能问答助手,精通《信访工作条例》《信访条例》和相关法律法规。\n\n## 能力\n- 准确解读信访法规条文\n- 解答信访办理流程问题\n- 说明受理范围和转办规则\n- 解释信访人权利和义务\n\n## 输出格式\n- 使用 Markdown 格式\n- 引用法规使用标准格式:《法规名》第X条\n- 回答要准确、严谨、易懂\n\n## 限制\n- 不提供具体信访事项的处理意见\n- 不代替信访受理决定\n- 所有输出末尾附免责声明:本内容由AI生成,仅供参考,不构成正式答复。","model":"qwen-plus","temperature":0.2,"max_tokens":4000}',
|
||||
0.2, 8192, true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.2 来信来访登记(completion)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000002',
|
||||
'来信来访登记',
|
||||
'xinfang-register',
|
||||
'将群众诉求快速整理为规范的信访登记表',
|
||||
'## 功能介绍
|
||||
|
||||
自动将信访人口述或来信内容整理为规范的信访登记表,包括:
|
||||
|
||||
- 提取信访人基本信息
|
||||
- 归纳信访事项和诉求
|
||||
- 分类标注信访类型
|
||||
- 生成规范登记格式
|
||||
|
||||
## 使用方法
|
||||
|
||||
将群众来信内容或口述记录粘贴到输入框,系统将自动生成规范的信访登记表。',
|
||||
'file-text',
|
||||
'c0000000-0000-0000-0010-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个信访登记专业助手。请根据提供的来信来访内容,提取并整理为规范的信访登记表。\n\n## 输出格式\n\n### 信访登记表\n\n| 项目 | 内容 |\n|------|------|\n| 信访人 | 姓名 |\n| 联系方式 | 电话/地址 |\n| 信访方式 | 来信/来访/网上/电话 |\n| 信访类型 | 投诉/建议/举报/求助 |\n| 涉及部门 | 相关责任单位 |\n| 事项摘要 | 一句话概括 |\n| 具体诉求 | 明确列出 |\n| 事实经过 | 时间线梳理 |\n| 办理建议 | 建议转办/自办/不予受理 |\n\n注意:如信息不完整,在对应项标注【待补充】。","input_label":"来信来访内容","output_label":"信访登记表","input_placeholder":"请粘贴群众来信内容或口述记录..."}',
|
||||
0.3, 8192, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.3 信访件分类(completion)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000003',
|
||||
'信访件分类',
|
||||
'xinfang-classify',
|
||||
'自动识别信访事项类型并给出分流建议',
|
||||
'## 功能介绍
|
||||
|
||||
智能分析信访内容,自动完成:
|
||||
|
||||
- 识别信访类型(投诉、建议、举报、求助)
|
||||
- 判断信访事项所属领域
|
||||
- 评估紧急程度
|
||||
- 给出分流和转办建议
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入信访事项的描述内容,系统将自动分类并给出处理建议。',
|
||||
'tag',
|
||||
'c0000000-0000-0000-0010-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个信访件分类专家。请根据提供的信访内容,进行精确分类并给出处理建议。\n\n## 输出格式\n\n### 信访件分类结果\n\n**信访类型:** [投诉/建议/举报/求助]\n\n**所属领域:** [如:城建规划/劳动人事/社会保障/环境保护/教育卫生/农业农村/公检法司/其他]\n\n**紧急程度:** [一般/较紧急/紧急/特急]\n\n**判定依据:** 简要说明分类理由\n\n**分流建议:**\n- 主责单位:XXX\n- 协办单位:XXX(如有)\n- 办理方式:[直办/转办/交办/督办]\n- 建议时限:X个工作日\n\n**注意事项:** 特殊情况提示","input_label":"信访内容","output_label":"分类结果","input_placeholder":"请输入信访事项的详细描述..."}',
|
||||
0.2, 4096, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.4 信访答复生成(completion,推荐)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000004',
|
||||
'信访答复生成',
|
||||
'xinfang-reply-gen',
|
||||
'根据信访诉求和调查结果生成规范答复函',
|
||||
'## 功能介绍
|
||||
|
||||
根据信访人诉求和调查处理结果,自动生成规范的信访答复函:
|
||||
|
||||
- 符合信访答复规范格式
|
||||
- 引用相关政策法规依据
|
||||
- 语言规范得体
|
||||
- 包含救济途径告知
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入信访诉求内容和调查处理结果,系统将生成规范的答复函文本。',
|
||||
'message-square-reply',
|
||||
'c0000000-0000-0000-0010-000000000002',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个信访答复专家,精通信访答复函的写作规范。\n\n## 绝对红线(违反即无效)\n\n1. **禁止编造事实**:所有「调查核实情况」中的事实内容(如现场检查结果、走访记录、证据材料等)必须且只能来自用户提供的输入内容或知识库检索结果。如果用户未提供调查结果,「调查核实情况」部分只能写「(待补充:请提供调查核实的具体结果)」,绝不可凭空虚构。\n2. **禁止虚构法规条文**:只能引用知识库中存在的法规原文,不得杜撰法条内容或编号。如知识库中未检索到相关法规,应注明「(建议补充相关法规依据)」。\n\n## 第一步:管辖权判断(必须首先执行)\n\n收到信访诉求后,必须先判断该事项是否属于本信访机构的受理范围:\n\n- **属于信访受理范围**:对行政机关及其工作人员的职务行为提出的投诉、建议、意见等。\n- **不属于信访受理范围**(应告知正确途径):\n - 消费纠纷/合同纠纷 → 建议向市场监管部门(12315)投诉或向法院提起民事诉讼\n - 劳动争议 → 建议向劳动仲裁委员会申请仲裁\n - 刑事案件 → 建议向公安机关报案\n - 民事侵权 → 建议通过民事诉讼解决\n - 已进入司法程序的事项 → 依法不予受理\n\n如不属于信访受理范围,应生成**信访事项不予受理告知书**,格式如下:\n\n---\n**信访事项告知书**\n\nXXX(信访人姓名):\n\n你于X年X月X日反映的关于XXX的事项,经审查,该事项不属于信访事项受理范围。\n\n**原因说明:**\n(说明为什么不属于信访受理范围)\n\n**建议处理途径:**\n(推荐合适的机构和具体操作方式)\n\n特此告知。\n\nXXX(告知单位)\nX年X月X日\n---\n\n## 第二步:生成答复函(仅限属于信访受理范围的事项)\n\n### 答复函格式\n\n**信访事项答复意见书**\n\nXXX(信访人姓名):\n\n你(你们)于X年X月X日通过XX方式反映的关于XXX的信访事项,我单位已收悉。经调查核实,现答复如下:\n\n一、基本情况\n(仅概述用户提供的信访诉求原文,不添加任何未提供的细节)\n\n二、调查核实情况\n(严格基于用户提供的调查结果撰写。如用户未提供调查结果,写「(待补充)」)\n\n三、处理意见\n(依据知识库中的法规给出处理意见)\n\n四、法规依据\n(仅引用知识库中检索到的法律法规条文原文)\n\n如对本答复意见不服,可自收到本答复意见书之日起30日内向上一级机关提出复查申请。\n\n特此答复。\n\nXXX(答复单位)\nX年X月X日\n\n## 其他要求\n- 语言规范、客观中立\n- 态度诚恳,表述清晰\n- 区分「用户提供的事实」和「AI分析建议」,后者必须标注 [[AI建议]]","input_label":"信访诉求及处理情况","output_label":"答复函","input_placeholder":"请输入:1.信访人诉求内容 2.调查处理结果 3.相关政策依据(如有)..."}',
|
||||
0.3, 8192, true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.5 政策法规检索(chatbot)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
welcome_message, suggested_prompts, app_config,
|
||||
temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000005',
|
||||
'政策法规检索',
|
||||
'xinfang-law-search',
|
||||
'检索信访相关法律法规和政策文件',
|
||||
'## 功能介绍
|
||||
|
||||
信访领域专业法规检索工具,支持:
|
||||
|
||||
- 信访工作条例全文检索
|
||||
- 行政复议和行政诉讼法条查询
|
||||
- 信访办理规程和标准查询
|
||||
- 相关司法解释检索
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入关键词或问题即可检索相关法条,如"信访办理时限"、"行政复议条件"等。',
|
||||
'search',
|
||||
'c0000000-0000-0000-0010-000000000002',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'chatbot', 'app-placeholder',
|
||||
'您好!我是信访法规检索助手。您可以查询信访工作条例、行政复议法、行政诉讼法等相关法规内容。',
|
||||
'["信访工作条例对办理时限有什么规定?","行政复议的申请条件是什么?","信访人有哪些权利和义务?","信访事项终结的条件是什么?"]',
|
||||
'{"system_prompt":"你是一个信访领域法规检索助手,精通信访工作条例、行政复议法、行政诉讼法等相关法律法规。\n\n## 能力\n- 精确检索法律条文\n- 解释法规适用场景\n- 对比新旧法规变化\n- 说明法规之间的关联关系\n\n## 输出格式\n- 使用 Markdown 格式\n- 引用条文标注出处:《法规名》第X条\n- 重点内容加粗标注\n\n## 限制\n- 仅提供法规检索和解读\n- 不提供具体案件处理意见\n- 末尾附免责声明:本内容由AI生成,仅供参考。","model":"qwen-plus","temperature":0.2,"max_tokens":4000}',
|
||||
0.2, 8192, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.6 矛盾纠纷分析(workflow,推荐)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000006',
|
||||
'矛盾纠纷分析',
|
||||
'xinfang-dispute-analysis',
|
||||
'分步收集纠纷信息,智能输出调解方案',
|
||||
'## 功能介绍
|
||||
|
||||
通过分步引导收集纠纷信息,生成专业调解方案:
|
||||
|
||||
- 识别纠纷类型和双方诉求
|
||||
- 分析矛盾焦点和法律关系
|
||||
- 评估调解可能性
|
||||
- 生成调解方案和话术建议
|
||||
|
||||
## 使用方法
|
||||
|
||||
按步骤填写纠纷类型、当事人诉求、事实经过等信息,系统将综合分析并生成调解方案。',
|
||||
'handshake',
|
||||
'c0000000-0000-0000-0010-000000000003',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'workflow', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个矛盾纠纷调解专家,擅长分析各类基层矛盾纠纷并制定调解方案。\n\n## 能力\n- 分析矛盾焦点和双方立场\n- 找出法律依据和调解空间\n- 制定调解策略和话术\n- 预判调解难点并提供应对方案\n\n## 输出格式\n\n### 纠纷分析报告\n\n一、纠纷概述\n二、矛盾焦点分析\n三、法律关系认定\n四、调解可行性评估\n五、调解方案\n - 调解思路\n - 建议方案(甲方/乙方各退让)\n - 调解话术要点\n六、风险提示\n\n## 限制\n- 不替代正式法律意见\n- 末尾附免责声明","app_type":"workflow","model":"qwen-plus","temperature":0.3,"max_tokens":6000,"steps":[{"key":"dispute_type","label":"纠纷类型","type":"select","options":["邻里纠纷","劳资纠纷","物业纠纷","土地纠纷","婚姻家庭","医疗纠纷","消费维权","其他"],"required":true},{"key":"parties","label":"当事人诉求","type":"textarea","placeholder":"请分别描述双方当事人的诉求和立场...","required":true},{"key":"facts","label":"事实经过","type":"textarea","placeholder":"请详细描述纠纷的来龙去脉、时间线...","required":true},{"key":"attempts","label":"已尝试措施","type":"textarea","placeholder":"之前是否有过调解或协商?结果如何?","required":false}]}',
|
||||
0.3, 8192, true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.7 调解文书生成(completion)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000007',
|
||||
'调解文书生成',
|
||||
'xinfang-mediation-doc',
|
||||
'生成调解协议书、和解书等规范文书',
|
||||
'## 功能介绍
|
||||
|
||||
根据调解结果自动生成规范的调解文书:
|
||||
|
||||
- 人民调解协议书
|
||||
- 和解协议书
|
||||
- 调解记录
|
||||
- 调解工作登记表
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入调解双方信息、纠纷事项和达成的调解结果,系统将生成规范的调解文书。',
|
||||
'file-signature',
|
||||
'c0000000-0000-0000-0010-000000000003',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'completion', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个调解文书写作专家,精通人民调解协议书和相关文书的规范格式。\n\n## 输出格式\n\n人民调解协议书\n\n编号:XX人调字〔20XX〕第X号\n\n当事人甲方:XXX\n当事人乙方:XXX\n\n纠纷主要事实:\n(简述纠纷事实)\n\n经本调解委员会调解,双方当事人自愿达成如下协议:\n\n一、(调解条款)\n二、(调解条款)\n三、(调解条款)\n\n本协议自双方当事人签名(盖章)之日起生效。\n\n当事人甲方(签名):\n当事人乙方(签名):\n调解员(签名):\nXXX人民调解委员会(盖章)\n日期:\n\n## 要求\n- 严格遵循调解文书规范格式\n- 条款表述清晰、具体、可执行\n- 符合《人民调解法》相关规定","input_label":"调解情况","output_label":"调解文书","input_placeholder":"请输入:1.双方当事人信息 2.纠纷事项 3.调解结果(双方达成的协议要点)..."}',
|
||||
0.3, 8192, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.8 督查报告生成(workflow,推荐)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
app_config, temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000008',
|
||||
'督查报告生成',
|
||||
'xinfang-supervision-report',
|
||||
'分步录入督办事项信息,生成督查报告',
|
||||
'## 功能介绍
|
||||
|
||||
按步骤录入督办信息,自动生成规范的督查报告:
|
||||
|
||||
- 督办事项跟踪
|
||||
- 办理进度评价
|
||||
- 问题分析和整改建议
|
||||
- 考核评分建议
|
||||
|
||||
## 使用方法
|
||||
|
||||
按步骤填写被督查单位、督办事项、办理情况等,系统将生成结构化的督查报告。',
|
||||
'clipboard-check',
|
||||
'c0000000-0000-0000-0010-000000000004',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'workflow', 'app-placeholder',
|
||||
'{"system_prompt":"你是一个信访督查督办专家,擅长撰写督查报告和整改建议。\n\n## 输出格式\n\n### 信访督查报告\n\n一、督查概况\n- 督查时间\n- 被督查单位\n- 督查事项\n\n二、办理情况\n- 受理时间和处理时限\n- 实际办理进度\n- 是否按期办结\n\n三、问题发现\n- 存在的主要问题\n- 问题原因分析\n\n四、评价意见\n- 办理质量评价\n- 群众满意度\n- 建议评分\n\n五、整改建议\n- 具体整改要求\n- 整改时限\n- 复查安排\n\n## 要求\n- 客观公正、实事求是\n- 问题描述具体、可核查\n- 整改建议可操作、有时限","app_type":"workflow","model":"qwen-plus","temperature":0.3,"max_tokens":6000,"steps":[{"key":"target","label":"被督查单位","type":"textarea","placeholder":"请输入被督查单位名称和相关信访件编号...","required":true},{"key":"issue","label":"督办事项","type":"textarea","placeholder":"请描述督办的信访事项内容和要求...","required":true},{"key":"progress","label":"办理情况","type":"textarea","placeholder":"请描述目前的办理进度、已采取措施和办理结果...","required":true},{"key":"evaluation","label":"评价要求","type":"select","options":["常规督查","重点督办","限期整改","约谈问责"],"required":true}]}',
|
||||
0.3, 8192, true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.9 信访风险评估(agent)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
welcome_message, suggested_prompts, app_config,
|
||||
temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000009',
|
||||
'信访风险评估',
|
||||
'xinfang-risk-eval',
|
||||
'评估信访事项风险等级并给出处置建议',
|
||||
'## 功能介绍
|
||||
|
||||
综合评估信访事项风险,提供:
|
||||
|
||||
- 风险等级评定(红/橙/黄/蓝四级)
|
||||
- 群体性事件预警分析
|
||||
- 处置方案建议
|
||||
- 稳控措施建议
|
||||
|
||||
## 使用方法
|
||||
|
||||
描述信访事项的基本情况,包括涉及人数、诉求性质、已持续时间等,系统将进行风险评估。',
|
||||
'alert-circle',
|
||||
'c0000000-0000-0000-0010-000000000004',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'agent', 'app-placeholder',
|
||||
'您好!我是信访风险评估助手。我具备风险识别、等级评定、预警分析和处置建议等能力,请描述需要评估的信访事项。',
|
||||
'["有一群业主集体投诉物业问题,已持续3个月","某企业欠薪涉及50多名工人,工人多次上访","一起征地拆迁纠纷,村民计划组织集体到市政府"]',
|
||||
'{"system_prompt":"你是一个信访风险评估智能体,具备以下工具能力:\n1. 风险识别:分析信访事项中的风险因素\n2. 等级评定:按照红橙黄蓝四级评定风险\n3. 预警分析:判断是否可能升级为群体性事件\n4. 处置建议:给出具体稳控和化解措施\n\n## 风险等级标准\n- 红色(特别重大):涉及100人以上或可能引发极端事件\n- 橙色(重大):涉及50-100人或有激化趋势\n- 黄色(较大):涉及10-50人或多次重复上访\n- 蓝色(一般):个体信访,情绪稳定\n\n## 输出格式\n\n### 信访风险评估报告\n\n[工具调用: 风险识别]\n(列出风险因素)\n[工具结果: 风险识别]\n\n[工具调用: 等级评定]\n**风险等级:X色**\n[工具结果: 等级评定]\n\n[工具调用: 预警分析]\n(群体性事件可能性分析)\n[工具结果: 预警分析]\n\n[工具调用: 处置建议]\n(具体处置和稳控措施)\n[工具结果: 处置建议]\n\n## 限制\n- 评估仅供参考,不替代专业研判\n- 末尾附免责声明","tools":["风险识别","等级评定","预警分析","处置建议"]}',
|
||||
0.3, 8192, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3.10 信访数据分析(agent)
|
||||
INSERT INTO applications (
|
||||
id, name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, org_id, status, visibility,
|
||||
dify_app_type, dify_api_key,
|
||||
welcome_message, suggested_prompts, app_config,
|
||||
temperature, max_tokens, is_featured
|
||||
) VALUES (
|
||||
'50000000-0000-0000-0000-000000000010',
|
||||
'信访数据分析',
|
||||
'xinfang-data-analysis',
|
||||
'统计分析信访趋势,生成周报月报',
|
||||
'## 功能介绍
|
||||
|
||||
信访数据统计分析助手,支持:
|
||||
|
||||
- 信访量趋势分析
|
||||
- 热点领域和重点区域统计
|
||||
- 办理质效分析
|
||||
- 自动生成工作简报
|
||||
|
||||
## 使用方法
|
||||
|
||||
输入信访数据或描述分析需求(如"本月信访数据汇总"、"本季度信访趋势"等),系统将进行分析并生成报告。',
|
||||
'bar-chart-big',
|
||||
'c0000000-0000-0000-0010-000000000004',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'a0000000-0000-0000-0000-000000000010',
|
||||
'approved', 'public',
|
||||
'agent', 'app-placeholder',
|
||||
'您好!我是信访数据分析助手。我可以帮您进行信访数据的统计分析、趋势研判和报告生成。请描述您的分析需求或提供数据。',
|
||||
'["帮我分析本月信访量变化趋势","统计各类信访事项占比","分析重复信访率并找出原因","生成本周信访工作简报"]',
|
||||
'{"system_prompt":"你是一个信访数据分析智能体,具备以下工具能力:\n1. 数据统计:汇总信访数量、类型、办理情况\n2. 趋势分析:对比同期数据,发现变化趋势\n3. 热点识别:识别信访热点领域和高发区域\n4. 报告生成:生成结构化的分析报告或工作简报\n\n## 输出格式\n使用 Markdown,包含表格和分析文字:\n\n[工具调用: 数据统计]\n(统计数据表格)\n[工具结果: 数据统计]\n\n[工具调用: 趋势分析]\n(趋势分析结论)\n[工具结果: 趋势分析]\n\n[工具调用: 热点识别]\n(热点领域分析)\n[工具结果: 热点识别]\n\n[工具调用: 报告生成]\n(完整报告/简报)\n[工具结果: 报告生成]\n\n注:如用户未提供具体数据,可基于常见信访工作场景生成模拟分析框架和报告模板。","tools":["数据统计","趋势分析","热点识别","报告生成"]}',
|
||||
0.4, 8192, false
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,938 @@
|
||||
-- ============================================================
|
||||
-- 信访局完整数据:用户 + 知识库 + 文档 + 关联
|
||||
-- 依赖:seed_xinfang.sql 已执行
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
pwd_hash TEXT;
|
||||
org_id UUID := 'a0000000-0000-0000-0000-000000000010';
|
||||
creator_id UUID;
|
||||
kb_law_id UUID := 'a1000000-0000-0000-0000-000000000001';
|
||||
kb_reply_id UUID := 'a1000000-0000-0000-0000-000000000002';
|
||||
kb_mediate_id UUID := 'a1000000-0000-0000-0000-000000000003';
|
||||
kb_supervise_id UUID := 'a1000000-0000-0000-0000-000000000004';
|
||||
BEGIN
|
||||
-- ============ 用户 ============
|
||||
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||
|
||||
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||
(gen_random_uuid(), '信访管理员', 'xf-admin@govai.gov.cn', pwd_hash, 'admin', 'active', org_id),
|
||||
(gen_random_uuid(), '王主任', 'wangzr@govai.gov.cn', pwd_hash, 'creator', 'active', org_id),
|
||||
(gen_random_uuid(), '赵科员', 'zhaoky@govai.gov.cn', pwd_hash, 'user', 'active', org_id)
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
SELECT id INTO creator_id FROM users WHERE email = 'xf-admin@govai.gov.cn';
|
||||
|
||||
-- ============ 知识库 ============
|
||||
|
||||
-- 1. 信访法规库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status) VALUES
|
||||
(kb_law_id, '信访法规库', '信访工作条例、信访条例、行政复议法、行政诉讼法等', creator_id, org_id, 'department', 5, 28000, 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 2. 政策答复模板库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status) VALUES
|
||||
(kb_reply_id, '政策答复模板库', '信访答复函模板、告知书模板、回复话术规范', creator_id, org_id, 'department', 4, 16000, 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. 调解规程库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status) VALUES
|
||||
(kb_mediate_id, '调解规程库', '人民调解法、调解协议范本、矛盾排查规程', creator_id, org_id, 'department', 4, 18000, 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 4. 督查督办制度库
|
||||
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status) VALUES
|
||||
(kb_supervise_id, '督查督办制度库', '信访督查办法、限期办结制度、考核评价办法', creator_id, org_id, 'department', 3, 12000, 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ============ 文档 ============
|
||||
|
||||
-- === 信访法规库文档 ===
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content) VALUES
|
||||
('a1100000-0000-0000-0000-000000000001', kb_law_id, '信访工作条例(2022年)', 'txt', 8000, 'completed', creator_id,
|
||||
'信访工作条例
|
||||
(2022年2月25日中共中央、国务院发布,2022年5月1日起施行)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为了坚持和加强党对信访工作的全面领导,做好新时代信访工作,保持党和政府同人民群众的密切联系,制定本条例。
|
||||
|
||||
第二条 本条例所称信访,是指公民、法人或者其他组织采用书信、电子邮件、传真、电话、走访等形式,向各级机关、单位反映情况,提出建议、意见或者投诉请求,依法由有关机关、单位处理的活动。
|
||||
|
||||
第三条 信访工作是党的群众工作的重要组成部分,是党和政府了解民情、集中民智、维护民利、凝聚民心的一项重要工作。
|
||||
|
||||
第四条 信访工作应当遵循下列原则:
|
||||
(一)坚持党的全面领导;
|
||||
(二)坚持以人民为中心;
|
||||
(三)坚持落实信访工作责任;
|
||||
(四)坚持依法按政策解决问题;
|
||||
(五)坚持源头治理化解矛盾。
|
||||
|
||||
第二章 信访工作体制
|
||||
|
||||
第五条 各级党委应当加强对信访工作的全面领导,把信访工作作为重要工作内容。
|
||||
|
||||
第六条 各级机关、单位应当畅通信访渠道,做好信访工作,认真处理信访事项,倾听人民群众建议、意见和投诉请求。
|
||||
|
||||
第三章 信访事项的提出和受理
|
||||
|
||||
第十七条 公民、法人或者其他组织可以采用书信、电子邮件、传真、电话、走访等形式提出信访事项。
|
||||
|
||||
第十八条 信访人一般应当采用书信、电子邮件、网上信访等书面形式提出信访事项。
|
||||
|
||||
第十九条 信访人提出信访事项,应当客观真实,对其所提供材料内容的真实性负责,不得捏造、歪曲事实,不得诬告、陷害他人。
|
||||
|
||||
第二十条 信访人在信访过程中应当遵守法律、法规,自觉维护社会公共秩序和信访秩序。
|
||||
|
||||
第四章 信访事项的办理
|
||||
|
||||
第三十一条 对信访事项有权处理的机关、单位办理信访事项,应当听取信访人陈述事实和理由;必要时可以要求信访人、有关组织和人员说明情况。
|
||||
|
||||
第三十二条 对信访事项有权处理的机关、单位应当自收到信访事项之日起60日内办结;情况复杂的,经本机关、单位负责人批准,可以适当延长办理期限,但延长期限不得超过30日,并告知信访人延期理由。
|
||||
|
||||
第三十三条 信访事项应当自受理之日起60日内办结,情况复杂的可延长30日。
|
||||
|
||||
第五章 监督和追责
|
||||
|
||||
第四十四条 各级机关、单位应当将信访工作绩效纳入公务员考核体系。
|
||||
|
||||
第四十五条 各级机关、单位及其工作人员有下列情形之一的,由其上级机关、单位责令改正:
|
||||
(一)对收到的信访事项不按规定登记的;
|
||||
(二)对属于其职权范围的信访事项不予受理的;
|
||||
(三)未在规定期限内办结信访事项的。'),
|
||||
|
||||
('a1100000-0000-0000-0000-000000000002', kb_law_id, '行政复议法', 'txt', 6000, 'completed', creator_id,
|
||||
'中华人民共和国行政复议法
|
||||
(2023年9月1日修订,2024年1月1日起施行)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为了防止和纠正违法的或者不当的行政行为,保护公民、法人和其他组织的合法权益,监督和保障行政机关依法行使职权,发挥行政复议化解行政争议的主渠道作用,推进法治政府建设,制定本法。
|
||||
|
||||
第二条 公民、法人或者其他组织认为行政机关的行政行为侵犯其合法权益,向行政复议机关提出行政复议申请,行政复议机关办理行政复议案件,适用本法。
|
||||
|
||||
第三条 行政复议工作应当坚持中国共产党的领导,以事实为根据,以法律为准绳,遵循合法、公正、公开、高效、便民、为民的原则。
|
||||
|
||||
第二章 行政复议范围
|
||||
|
||||
第十一条 有下列情形之一的,公民、法人或者其他组织可以依照本法申请行政复议:
|
||||
(一)对行政机关作出的行政处罚决定不服;
|
||||
(二)对行政机关作出的行政强制措施、行政强制执行决定不服;
|
||||
(三)申请行政许可,行政机关拒绝或者在法定期限内不予答复,或者对行政机关作出的有关行政许可的其他决定不服;
|
||||
(四)对行政机关作出的确认自然资源的所有权或者使用权的决定不服;
|
||||
(五)对行政机关作出的征收征用决定及其补偿决定不服;
|
||||
(六)对行政机关作出的不予受理工伤认定申请的决定或者工伤认定结论不服。
|
||||
|
||||
第三章 行政复议申请
|
||||
|
||||
第二十条 公民、法人或者其他组织认为行政行为侵犯其合法权益的,可以自知道或者应当知道该行政行为之日起六十日内提出行政复议申请;但是法律规定的申请期限超过六十日的除外。
|
||||
|
||||
第二十三条 行政复议申请可以书面提出,也可以口头提出。
|
||||
|
||||
第四章 行政复议受理
|
||||
|
||||
第三十条 行政复议机关收到行政复议申请后,应当在五日内进行审查,决定是否予以受理。
|
||||
|
||||
第五章 行政复议审理
|
||||
|
||||
第四十条 行政复议机关应当自受理行政复议申请之日起六十日内作出行政复议决定;但是法律规定的行政复议期限少于六十日的除外。情况复杂,不能在规定期限内作出行政复议决定的,经行政复议机关负责人批准,可以适当延长,并告知当事人;但是延长期限最多不超过三十日。'),
|
||||
|
||||
('a1100000-0000-0000-0000-000000000003', kb_law_id, '行政诉讼法', 'txt', 5500, 'completed', creator_id,
|
||||
'中华人民共和国行政诉讼法
|
||||
(2017年6月27日修正)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为保证人民法院公正、及时审理行政案件,解决行政争议,保护公民、法人和其他组织的合法权益,监督行政机关依法行使职权,根据宪法,制定本法。
|
||||
|
||||
第二条 公民、法人或者其他组织认为行政机关和行政机关工作人员的行政行为侵犯其合法权益,有权依照本法向人民法院提起诉讼。
|
||||
|
||||
第二章 受案范围
|
||||
|
||||
第十二条 人民法院受理公民、法人或者其他组织提起的下列诉讼:
|
||||
(一)对行政拘留、暂扣或者吊销许可证和执照、责令停产停业、没收违法所得、没收非法财物、罚款、警告等行政处罚不服的;
|
||||
(二)对限制人身自由或者对财产的查封、扣押、冻结等行政强制措施和行政强制执行不服的;
|
||||
(三)申请行政许可,行政机关拒绝或者在法定期限内不予答复,或者对行政机关作出的有关行政许可的其他决定不服的;
|
||||
(四)对行政机关作出的关于确认土地、矿藏、水流、森林、山岭、草原、荒地、滩涂、海域等自然资源的所有权或者使用权的决定不服的;
|
||||
(五)对征收、征用决定及其补偿决定不服的。
|
||||
|
||||
第三章 管辖
|
||||
|
||||
第十四条 基层人民法院管辖第一审行政案件。
|
||||
第十五条 中级人民法院管辖下列第一审行政案件:对国务院部门或者县级以上地方人民政府所作的行政行为提起诉讼的案件。
|
||||
|
||||
第四章 诉讼参加人
|
||||
|
||||
第四十六条 公民、法人或者其他组织直接向人民法院提起诉讼的,应当自知道或者应当知道作出行政行为之日起六个月内提出。法律另有规定的除外。
|
||||
|
||||
第五章 起诉和受理
|
||||
|
||||
第四十九条 提起诉讼应当符合下列条件:
|
||||
(一)原告是符合本法规定的公民、法人或者其他组织;
|
||||
(二)有明确的被告;
|
||||
(三)有具体的诉讼请求和事实根据;
|
||||
(四)属于人民法院受案范围和受诉人民法院管辖。'),
|
||||
|
||||
('a1100000-0000-0000-0000-000000000004', kb_law_id, '信访事项办理规程', 'txt', 4500, 'completed', creator_id,
|
||||
'信访事项办理规程
|
||||
|
||||
一、受理环节
|
||||
|
||||
1. 来信受理
|
||||
(1)拆信登记:当日拆封、逐件登记,录入信访信息系统
|
||||
(2)初步审查:3个工作日内完成是否受理的审查
|
||||
(3)分类处理:按投诉、建议、举报、求助四类分别办理
|
||||
|
||||
2. 来访受理
|
||||
(1)引导登记:引导来访人填写来访登记表
|
||||
(2)接谈记录:详细记录来访人陈述,由来访人确认签字
|
||||
(3)当场告知:符合受理条件的当场出具受理告知书
|
||||
|
||||
3. 网上信访受理
|
||||
(1)48小时内进行受理审查
|
||||
(2)通过系统向信访人反馈受理情况
|
||||
(3)电子化全程留痕
|
||||
|
||||
二、办理环节
|
||||
|
||||
1. 办理时限
|
||||
(1)一般事项:60日内办结
|
||||
(2)复杂事项:经批准可延长30日
|
||||
(3)特殊事项:按专项规定办理
|
||||
|
||||
2. 办理方式
|
||||
(1)直办:本单位职责范围内,自行调查处理
|
||||
(2)转办:转交有权处理的机关办理,15日内转出
|
||||
(3)交办:上级机关交下级机关办理
|
||||
(4)督办:对办理不力的单位进行督促
|
||||
|
||||
3. 答复要求
|
||||
(1)书面答复:出具信访事项答复意见书
|
||||
(2)引用依据:必须引用法律法规或政策文件
|
||||
(3)告知救济:告知复查复核权利和途径
|
||||
(4)送达确认:送达信访人并取得签收
|
||||
|
||||
三、复查复核
|
||||
|
||||
1. 复查:信访人对答复不服,30日内可申请复查
|
||||
2. 复核:对复查意见不服,30日内可申请复核
|
||||
3. 终结:经过复查复核后作出的意见为最终处理意见
|
||||
|
||||
四、档案管理
|
||||
|
||||
1. 一案一档,编号归档
|
||||
2. 保管期限:一般事项5年,重大事项永久
|
||||
3. 电子档案同步保存'),
|
||||
|
||||
('a1100000-0000-0000-0000-000000000005', kb_law_id, '信访工作考核办法', 'txt', 4000, 'completed', creator_id,
|
||||
'信访工作考核办法
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为加强信访工作目标管理,提高信访工作质效,维护群众合法权益,制定本办法。
|
||||
|
||||
第二条 考核对象为各级信访工作机构及相关责任单位。
|
||||
|
||||
第三条 考核坚持客观公正、注重实效、奖惩分明的原则。
|
||||
|
||||
第二章 考核内容
|
||||
|
||||
第四条 考核内容包括:
|
||||
(一)信访工作体制机制建设(10分)
|
||||
- 信访工作领导责任制落实情况
|
||||
- 信访工作机构和队伍建设
|
||||
- 信访工作制度建设和执行
|
||||
|
||||
(二)信访事项办理质效(40分)
|
||||
- 按期办结率(目标≥95%)
|
||||
- 一次性化解率(目标≥80%)
|
||||
- 答复规范率(目标≥90%)
|
||||
- 群众满意率(目标≥85%)
|
||||
|
||||
(三)矛盾纠纷化解(25分)
|
||||
- 信访积案化解情况
|
||||
- 重复信访控制情况
|
||||
- 源头预防和排查化解
|
||||
|
||||
(四)信访秩序维护(15分)
|
||||
- 越级信访发生情况
|
||||
- 集体访、非正常访控制
|
||||
- 重大敏感时期信访稳控
|
||||
|
||||
(五)信息化建设(10分)
|
||||
- 信访信息系统使用率
|
||||
- 网上信访办理率
|
||||
- 数据报送及时准确率
|
||||
|
||||
第三章 考核方式
|
||||
|
||||
第五条 考核采用百分制,实行日常考核与年终考核相结合。
|
||||
|
||||
第六条 考核结果分为优秀(90分以上)、良好(80-89分)、合格(60-79分)、不合格(60分以下)。
|
||||
|
||||
第四章 奖惩
|
||||
|
||||
第七条 考核优秀的单位和个人,给予通报表彰。
|
||||
第八条 考核不合格的,予以通报批评,限期整改。连续两年不合格的,对主要领导进行约谈。');
|
||||
|
||||
-- === 政策答复模板库文档 ===
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content) VALUES
|
||||
('a1200000-0000-0000-0000-000000000001', kb_reply_id, '信访答复函模板', 'txt', 4000, 'completed', creator_id,
|
||||
'信访事项答复意见书(模板)
|
||||
|
||||
编号:X信复字〔20XX〕第XXX号
|
||||
|
||||
XXX(信访人姓名):
|
||||
|
||||
你(你们)于X年X月X日通过[来信/来访/网上信访]方式反映的关于XXX的信访事项(编号:XXX),我[单位名称]已依法受理。经调查核实,现答复如下:
|
||||
|
||||
一、你反映的主要问题
|
||||
|
||||
[概述信访人反映的主要诉求,客观准确,不添加评价]
|
||||
|
||||
二、调查核实情况
|
||||
|
||||
[详细描述调查过程、走访情况、收集的证据材料等]
|
||||
|
||||
1. 关于[问题一]:经核实,[调查结论]。
|
||||
2. 关于[问题二]:经核实,[调查结论]。
|
||||
|
||||
三、处理意见
|
||||
|
||||
根据《[相关法规名称]》第X条规定,[具体处理意见]。
|
||||
|
||||
(一)[处理措施一]
|
||||
(二)[处理措施二]
|
||||
(三)[处理措施三]
|
||||
|
||||
四、法律法规依据
|
||||
|
||||
1.《信访工作条例》第XX条
|
||||
2.《XXX法》第XX条
|
||||
3.《XXX规定》第XX条
|
||||
|
||||
如你对本答复意见不服,可自收到本答复意见书之日起30日内向[上一级机关名称]提出复查申请。
|
||||
|
||||
特此答复。
|
||||
|
||||
[答复单位名称](盖章)
|
||||
X年X月X日
|
||||
|
||||
---
|
||||
|
||||
附:信访不予受理告知书模板
|
||||
|
||||
编号:X信告字〔20XX〕第XXX号
|
||||
|
||||
XXX(信访人姓名):
|
||||
|
||||
你于X年X月X日提出的信访事项,经审查,因[不予受理原因],依据《信访工作条例》第XX条规定,不予受理。
|
||||
|
||||
不予受理原因:
|
||||
□ 不属于本机关职责范围
|
||||
□ 已经或依法应当通过诉讼、仲裁、行政复议等途径解决
|
||||
□ 信访事项已经办结且无新的事实和理由
|
||||
□ 其他:[具体原因]
|
||||
|
||||
建议你向[建议机关]反映/通过[建议途径]解决。
|
||||
|
||||
[单位名称](盖章)
|
||||
X年X月X日'),
|
||||
|
||||
('a1200000-0000-0000-0000-000000000002', kb_reply_id, '常见信访答复话术', 'txt', 4000, 'completed', creator_id,
|
||||
'常见信访答复话术规范
|
||||
|
||||
一、开头用语
|
||||
|
||||
1. 正式答复开头:
|
||||
"你(你们)于X年X月X日通过XX方式反映的关于XXX的事项,我单位已依法受理,经调查核实,现答复如下:"
|
||||
|
||||
2. 补充告知开头:
|
||||
"关于你X年X月X日来信反映的XXX事项,现将有关情况进一步告知如下:"
|
||||
|
||||
二、调查情况表述
|
||||
|
||||
1. 情况属实:
|
||||
"经调查核实,你反映的XXX情况属实。"
|
||||
|
||||
2. 部分属实:
|
||||
"经调查核实,你反映的情况部分属实。其中,关于XXX的问题,经核实确有其事;关于XXX的问题,与实际情况不符。"
|
||||
|
||||
3. 不属实:
|
||||
"经调查核实,你反映的XXX情况与事实不符。经查,实际情况为XXX。"
|
||||
|
||||
三、处理结论表述
|
||||
|
||||
1. 支持诉求:
|
||||
"你反映的问题,符合《XXX》第X条规定,我单位将依法予以处理。具体措施如下:……"
|
||||
|
||||
2. 部分支持:
|
||||
"关于你提出的XXX诉求,依据《XXX》第X条规定,可以予以支持;关于XXX诉求,因不符合XXX规定,暂无法满足。"
|
||||
|
||||
3. 不予支持:
|
||||
"你提出的XXX诉求,因不符合《XXX》第X条规定的条件,依法不予支持。"
|
||||
|
||||
四、结尾用语
|
||||
|
||||
1. 标准结尾:
|
||||
"如对本答复意见不服,可自收到本答复意见书之日起30日内向XXX提出复查申请。"
|
||||
|
||||
2. 表示关切:
|
||||
"我们理解你的诉求和心情,将持续关注相关问题的解决。如有新的情况或需要,请随时与我们联系。"
|
||||
|
||||
3. 涉及其他部门:
|
||||
"关于你反映的XXX问题,已转交XX部门办理,该部门将在XX日内与你联系。"
|
||||
|
||||
五、禁忌用语
|
||||
|
||||
1. 不得使用:推诿用语如"与我单位无关"、"你找错地方了"
|
||||
2. 不得使用:冷漠用语如"爱信不信"、"随便你"
|
||||
3. 不得使用:威胁用语如"再闹就报警"
|
||||
4. 不得承诺:超出职权范围的承诺'),
|
||||
|
||||
('a1200000-0000-0000-0000-000000000003', kb_reply_id, '信访答复常见问题', 'txt', 4000, 'completed', creator_id,
|
||||
'信访答复常见问题及处理
|
||||
|
||||
一、征地拆迁类
|
||||
|
||||
常见诉求:补偿标准过低、未签协议强拆、安置房未落实
|
||||
|
||||
答复要点:
|
||||
1. 引用《土地管理法》《国有土地上房屋征收与补偿条例》
|
||||
2. 说明补偿标准的制定依据和程序
|
||||
3. 如确有违法行为,告知行政复议或诉讼途径
|
||||
4. 涉及强拆的要核实是否有合法程序
|
||||
|
||||
二、劳动保障类
|
||||
|
||||
常见诉求:欠薪、工伤认定、社保缴纳
|
||||
|
||||
答复要点:
|
||||
1. 引用《劳动法》《劳动合同法》《工伤保险条例》
|
||||
2. 告知劳动仲裁申请途径和时效
|
||||
3. 欠薪问题可向劳动监察部门投诉
|
||||
4. 社保问题向社保经办机构反映
|
||||
|
||||
三、物业管理类
|
||||
|
||||
常见诉求:物业不作为、收费不合理、公共设施损坏
|
||||
|
||||
答复要点:
|
||||
1. 引用《物业管理条例》《民法典》物权编
|
||||
2. 建议通过业主大会或业委会协商
|
||||
3. 涉及违法行为可向住建部门投诉
|
||||
4. 收费争议可向价格主管部门反映
|
||||
|
||||
四、环境保护类
|
||||
|
||||
常见诉求:噪音扰民、污水排放、空气污染
|
||||
|
||||
答复要点:
|
||||
1. 引用《环境保护法》《噪声污染防治法》
|
||||
2. 告知可向生态环境部门举报
|
||||
3. 说明环境违法的处罚措施
|
||||
4. 涉及赔偿的告知诉讼途径
|
||||
|
||||
五、教育卫生类
|
||||
|
||||
常见诉求:入学划片、教育收费、医疗纠纷
|
||||
|
||||
答复要点:
|
||||
1. 教育问题引用《教育法》《义务教育法》
|
||||
2. 医疗纠纷引用《医疗纠纷预防和处理条例》
|
||||
3. 收费问题向教育/卫生主管部门反映
|
||||
4. 医疗纠纷建议调解或诉讼'),
|
||||
|
||||
('a1200000-0000-0000-0000-000000000004', kb_reply_id, '信访告知书格式汇编', 'txt', 4000, 'completed', creator_id,
|
||||
'信访告知书格式汇编
|
||||
|
||||
一、受理告知书
|
||||
|
||||
信访事项受理告知书
|
||||
编号:X信受字〔20XX〕第XXX号
|
||||
|
||||
XXX:
|
||||
你于X年X月X日提出的关于XXX的信访事项,经审查符合受理条件,我单位已予受理。
|
||||
办理期限:自受理之日起60日内办结。
|
||||
承办人:XXX 联系电话:XXX
|
||||
如有补充材料,请在XX日内提交。
|
||||
|
||||
XX单位 X年X月X日
|
||||
|
||||
二、延期办理告知书
|
||||
|
||||
信访事项延期办理告知书
|
||||
编号:X信延字〔20XX〕第XXX号
|
||||
|
||||
XXX:
|
||||
你于X年X月X日提出的信访事项(受理编号:XXX),因情况复杂,需进一步调查核实,经我单位负责人批准,延长办理期限30日。预计于X年X月X日前办结。
|
||||
延期理由:[需进一步调查取证/涉及多个部门协调/当事人外出无法联系/其他]
|
||||
|
||||
XX单位 X年X月X日
|
||||
|
||||
三、转送告知书
|
||||
|
||||
信访事项转送告知书
|
||||
编号:X信转字〔20XX〕第XXX号
|
||||
|
||||
XXX:
|
||||
你于X年X月X日提出的关于XXX的信访事项,经审查,该事项属于XX单位职责范围,已于X年X月X日转送该单位办理。
|
||||
受理单位:XXX 联系电话:XXX
|
||||
如在转送后15日内未收到受理通知,可向我单位反映。
|
||||
|
||||
XX单位 X年X月X日
|
||||
|
||||
四、终结告知书
|
||||
|
||||
信访事项处理终结告知书
|
||||
编号:X信终字〔20XX〕第XXX号
|
||||
|
||||
XXX:
|
||||
你关于XXX的信访事项,经[原办理/复查/复核],已依法作出处理意见。依据《信访工作条例》第三十六条规定,该信访事项处理终结。
|
||||
信访人对终结意见仍有异议的,可依法通过诉讼等法定途径寻求救济。
|
||||
各级信访工作机构和其他机关、单位不再受理。
|
||||
|
||||
XX单位 X年X月X日');
|
||||
|
||||
-- === 调解规程库文档 ===
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content) VALUES
|
||||
('a1300000-0000-0000-0000-000000000001', kb_mediate_id, '人民调解法', 'txt', 5000, 'completed', creator_id,
|
||||
'中华人民共和国人民调解法
|
||||
(2010年8月28日通过,2011年1月1日起施行)
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为了完善人民调解制度,规范人民调解活动,及时解决民间纠纷,维护社会和谐稳定,根据宪法,制定本法。
|
||||
|
||||
第二条 本法所称人民调解,是指人民调解委员会通过说服、疏导等方法,促使当事人在平等协商基础上自愿达成调解协议,解决民间纠纷的活动。
|
||||
|
||||
第三条 人民调解委员会调解民间纠纷,应当遵循下列原则:
|
||||
(一)在当事人自愿、平等的基础上进行调解;
|
||||
(二)不违背法律、法规和国家政策;
|
||||
(三)尊重当事人的权利,不得因调解而阻止当事人依法通过仲裁、行政、司法等途径维护自己的权利。
|
||||
|
||||
第二章 人民调解委员会
|
||||
|
||||
第七条 人民调解委员会是依法设立的调解民间纠纷的群众性组织。
|
||||
|
||||
第八条 村民委员会、居民委员会设立人民调解委员会。企业事业单位根据需要设立人民调解委员会。
|
||||
|
||||
第三章 人民调解员
|
||||
|
||||
第十四条 人民调解员应当由公道正派、热心人民调解工作,并具有一定文化水平、政策水平和法律知识的成年公民担任。
|
||||
|
||||
第四章 调解程序
|
||||
|
||||
第十七条 当事人可以向人民调解委员会申请调解;人民调解委员会也可以主动调解。
|
||||
|
||||
第二十条 人民调解员根据调解纠纷的需要,在征得当事人的同意后,可以邀请当事人的亲属、邻里、同事等参与调解,也可以邀请具有专门知识、特定经验的人员或者有关社会组织的人员参与调解。
|
||||
|
||||
第二十二条 人民调解员调解纠纷,应当做好调解笔录。
|
||||
|
||||
第五章 调解协议
|
||||
|
||||
第二十八条 经人民调解委员会调解达成调解协议的,可以制作调解协议书。
|
||||
|
||||
第二十九条 调解协议书应当载明下列事项:
|
||||
(一)当事人的基本情况;
|
||||
(二)纠纷的主要事实、争议事项以及各方当事人的责任;
|
||||
(三)当事人达成调解协议的内容,履行的方式、期限。
|
||||
|
||||
第三十一条 经人民调解委员会调解达成的调解协议,具有法律约束力,当事人应当按照约定履行。
|
||||
|
||||
第三十三条 经人民调解委员会调解达成调解协议后,双方当事人认为有必要的,可以自调解协议生效之日起三十日内共同向人民法院申请司法确认,人民法院应当及时对调解协议进行审查,依法确认调解协议的效力。'),
|
||||
|
||||
('a1300000-0000-0000-0000-000000000002', kb_mediate_id, '调解协议书范本', 'txt', 4500, 'completed', creator_id,
|
||||
'人民调解协议书范本汇编
|
||||
|
||||
一、劳资纠纷调解协议书
|
||||
|
||||
人民调解协议书
|
||||
编号:XX人调字〔20XX〕第XXX号
|
||||
|
||||
当事人甲方(用人单位):XXX公司
|
||||
法定代表人:XXX 联系电话:XXX
|
||||
当事人乙方(劳动者):XXX
|
||||
身份证号:XXX 联系电话:XXX
|
||||
|
||||
纠纷主要事实:
|
||||
乙方于X年X月X日至X年X月X日在甲方处工作,担任XX岗位。因[欠薪/解除劳动关系/工伤赔偿]产生纠纷。
|
||||
|
||||
经XX人民调解委员会调解,双方当事人自愿达成如下协议:
|
||||
|
||||
一、甲方于本协议签署之日起X日内,支付乙方[工资/经济补偿/赔偿金]共计人民币XX元整(¥XX.00元)。
|
||||
二、甲方为乙方出具[解除劳动关系证明/工作经历证明]。
|
||||
三、上述款项支付完毕后,双方就本纠纷再无争议,乙方不再就此事向任何部门主张权利。
|
||||
四、如甲方未按期履行本协议,乙方可向人民法院申请司法确认及强制执行。
|
||||
|
||||
本协议自双方当事人签名之日起生效。
|
||||
本协议一式三份,甲乙双方各执一份,调解委员会留存一份。
|
||||
|
||||
当事人甲方(盖章): 当事人乙方(签名):
|
||||
调解员(签名):
|
||||
XX人民调解委员会(盖章)
|
||||
日期:X年X月X日
|
||||
|
||||
---
|
||||
|
||||
二、邻里纠纷调解协议书
|
||||
|
||||
[格式同上,常见条款]
|
||||
一、甲方在X日内将[占用物品/违建部分]清除/恢复原状。
|
||||
二、乙方对甲方造成的[损失/影响]不再追究。
|
||||
三、双方今后应互相尊重,和睦相处,不得再因此事发生争执。
|
||||
|
||||
---
|
||||
|
||||
三、物业纠纷调解协议书
|
||||
|
||||
[格式同上,常见条款]
|
||||
一、物业公司在X日内完成[维修项目]。
|
||||
二、业主在物业完成维修后X日内,支付所欠物业费XX元。
|
||||
三、双方同意就[争议事项]按照XX标准执行。'),
|
||||
|
||||
('a1300000-0000-0000-0000-000000000003', kb_mediate_id, '矛盾排查化解工作规程', 'txt', 4500, 'completed', creator_id,
|
||||
'矛盾纠纷排查化解工作规程
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为做好矛盾纠纷排查化解工作,从源头上预防和减少信访问题,维护社会和谐稳定,制定本规程。
|
||||
|
||||
第二条 矛盾纠纷排查化解坚持"属地管理、分级负责"和"谁主管、谁负责"原则。
|
||||
|
||||
第二章 排查机制
|
||||
|
||||
第三条 建立常态化矛盾纠纷排查制度:
|
||||
(一)日常排查:基层网格员每日走访巡查
|
||||
(二)定期排查:每周一次集中排查研判
|
||||
(三)专项排查:重大节日和敏感时期开展专项排查
|
||||
(四)动态排查:对突发事件及时跟进排查
|
||||
|
||||
第四条 排查重点领域:
|
||||
(一)征地拆迁、房屋征收补偿
|
||||
(二)劳资纠纷、欠薪讨薪
|
||||
(三)环境污染、邻避效应
|
||||
(四)物业管理、社区治理
|
||||
(五)婚姻家庭、邻里关系
|
||||
(六)医患纠纷、校园安全
|
||||
(七)企业改制、群体利益
|
||||
|
||||
第三章 预警分级
|
||||
|
||||
第五条 矛盾纠纷预警分为四级:
|
||||
|
||||
一级预警(红色):
|
||||
- 涉及100人以上的群体性纠纷
|
||||
- 可能引发极端事件
|
||||
- 涉及重大政策调整引发的集体诉求
|
||||
处置要求:立即上报,24小时内制定化解方案
|
||||
|
||||
二级预警(橙色):
|
||||
- 涉及50-100人的群体性纠纷
|
||||
- 有明显激化趋势
|
||||
- 反复上访3次以上未解决
|
||||
处置要求:2日内上报,一周内启动化解
|
||||
|
||||
三级预警(黄色):
|
||||
- 涉及10-50人的纠纷
|
||||
- 可能升级但暂时可控
|
||||
- 涉及敏感领域
|
||||
处置要求:一周内上报,半月内制定方案
|
||||
|
||||
四级预警(蓝色):
|
||||
- 个体纠纷,情绪稳定
|
||||
- 诉求合理可通过正常途径解决
|
||||
处置要求:纳入日常调处
|
||||
|
||||
第四章 化解流程
|
||||
|
||||
第六条 矛盾化解基本流程:
|
||||
1. 受理登记→2. 调查核实→3. 分析研判→4. 制定方案→5. 组织调处→6. 跟踪回访
|
||||
|
||||
第七条 调处方式:
|
||||
(一)简易调处:事实清楚的简单纠纷,即时调处
|
||||
(二)一般调处:需要调查取证的,15日内完成
|
||||
(三)联合调处:涉及多部门的,组织联合调处
|
||||
(四)专家调处:复杂疑难的,邀请专家参与
|
||||
|
||||
第五章 跟踪回访
|
||||
|
||||
第八条 化解后30日内进行回访,确认:
|
||||
(一)协议是否履行
|
||||
(二)当事人是否满意
|
||||
(三)是否存在反复隐患'),
|
||||
|
||||
('a1300000-0000-0000-0000-000000000004', kb_mediate_id, '调解技巧指引', 'txt', 4000, 'completed', creator_id,
|
||||
'矛盾纠纷调解技巧指引
|
||||
|
||||
一、接待技巧
|
||||
|
||||
1. 倾听为先
|
||||
- 让当事人充分表达,不打断
|
||||
- 记录要点,适时回应
|
||||
- 表示理解和关注
|
||||
|
||||
2. 情绪疏导
|
||||
- 识别情绪状态,先稳情绪后谈事
|
||||
- 提供水、纸巾等关怀
|
||||
- 使用缓和语气:理解您的心情/换位思考
|
||||
|
||||
3. 建立信任
|
||||
- 表明中立立场
|
||||
- 说明调解的好处:快捷、免费、保密
|
||||
- 展示专业知识
|
||||
|
||||
二、调解技巧
|
||||
|
||||
1. 背对背调解
|
||||
适用:双方情绪对立、面对面易激化
|
||||
方法:分别约谈,了解底线,找交集
|
||||
|
||||
2. 面对面调解
|
||||
适用:双方理性、愿意沟通
|
||||
方法:设定规则(轮流发言、不打断)、引导聚焦核心问题
|
||||
|
||||
3. 引入第三方
|
||||
适用:当事人信任特定人士
|
||||
方法:邀请社区长者、行业专家、法律顾问参与
|
||||
|
||||
4. 法理情并用
|
||||
- 讲法律:明确权利义务边界
|
||||
- 讲道理:分析利弊得失
|
||||
- 讲情感:唤起共情和体谅
|
||||
|
||||
三、常见纠纷调解策略
|
||||
|
||||
1. 邻里纠纷
|
||||
核心:面子和尊重
|
||||
策略:各退一步、互相道歉、约定规则
|
||||
|
||||
2. 劳资纠纷
|
||||
核心:利益补偿
|
||||
策略:核算法律标准、折中协商、分期支付
|
||||
|
||||
3. 物业纠纷
|
||||
核心:服务和收费
|
||||
策略:明确服务标准、建立沟通机制、第三方评估
|
||||
|
||||
4. 家庭纠纷
|
||||
核心:感情和责任
|
||||
策略:冷静期、亲属劝解、关注弱势方
|
||||
|
||||
四、注意事项
|
||||
|
||||
1. 不强迫调解,尊重当事人意愿
|
||||
2. 不偏袒任何一方
|
||||
3. 涉及违法犯罪的及时移交司法
|
||||
4. 做好调解笔录和协议书
|
||||
5. 调解不成的引导通过法定途径解决');
|
||||
|
||||
-- === 督查督办制度库文档 ===
|
||||
|
||||
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content) VALUES
|
||||
('a1400000-0000-0000-0000-000000000001', kb_supervise_id, '信访督查办法', 'txt', 4500, 'completed', creator_id,
|
||||
'信访督查工作办法
|
||||
|
||||
第一章 总则
|
||||
|
||||
第一条 为规范信访督查工作,推动信访事项依法及时就地解决,保障信访人合法权益,制定本办法。
|
||||
|
||||
第二条 信访督查是指上级信访工作机构对下级机关、单位信访事项办理情况进行检查、督促和指导的工作。
|
||||
|
||||
第三条 督查工作遵循依法依规、客观公正、注重实效、促进整改的原则。
|
||||
|
||||
第二章 督查范围
|
||||
|
||||
第四条 下列情形应当纳入督查:
|
||||
(一)上级机关交办的重要信访事项
|
||||
(二)群众反映强烈的突出问题
|
||||
(三)超期未办结的信访事项
|
||||
(四)办理质量不高、群众不满意的事项
|
||||
(五)可能引发群体性事件的重大隐患
|
||||
(六)领导批示要求督办的事项
|
||||
|
||||
第三章 督查方式
|
||||
|
||||
第五条 督查方式包括:
|
||||
(一)书面督查:发出督查通知书,要求限期报告办理情况
|
||||
(二)实地督查:到现场检查办理落实情况
|
||||
(三)约谈督查:约谈责任单位负责人
|
||||
(四)联合督查:会同纪检监察等部门联合督查
|
||||
(五)暗访督查:不事先通知的实地调查
|
||||
|
||||
第四章 督查程序
|
||||
|
||||
第六条 督查程序:
|
||||
1. 立项审批:确定督查事项,报领导批准
|
||||
2. 下发通知:向被督查单位发出督查通知书
|
||||
3. 调查核实:通过书面审查或实地核查了解情况
|
||||
4. 形成报告:撰写督查报告,提出意见建议
|
||||
5. 反馈整改:将督查结果反馈被督查单位,限期整改
|
||||
6. 跟踪验收:对整改情况进行跟踪验收
|
||||
|
||||
第七条 督查通知书应包含:
|
||||
- 督查事项和依据
|
||||
- 需要报告的内容
|
||||
- 报告期限
|
||||
- 联系方式
|
||||
|
||||
第八条 督查报告应包含:
|
||||
- 督查时间和对象
|
||||
- 信访事项基本情况
|
||||
- 办理情况和存在问题
|
||||
- 评价意见和整改建议
|
||||
- 限期整改要求
|
||||
|
||||
第五章 督查结果运用
|
||||
|
||||
第九条 督查结果纳入年度信访工作考核。
|
||||
|
||||
第十条 对督查中发现的典型问题,进行通报:
|
||||
(一)正面典型:总结推广经验做法
|
||||
(二)反面典型:点名通报,警示教育
|
||||
|
||||
第十一条 对拒不整改或整改不力的:
|
||||
(一)第一次:书面催办
|
||||
(二)第二次:约谈负责人
|
||||
(三)第三次:提请追责问责'),
|
||||
|
||||
('a1400000-0000-0000-0000-000000000002', kb_supervise_id, '信访限期办结制度', 'txt', 3500, 'completed', creator_id,
|
||||
'信访事项限期办结制度
|
||||
|
||||
第一条 目的
|
||||
确保信访事项在法定期限内办结,杜绝久拖不决、推诿扯皮现象。
|
||||
|
||||
第二条 办理时限
|
||||
|
||||
一、受理环节时限:
|
||||
(一)来信:收到之日起15日内决定是否受理
|
||||
(二)来访:当场作出是否受理决定
|
||||
(三)网上信访:48小时内进行受理审查
|
||||
(四)转送信访:收到转送之日起15日内决定是否受理
|
||||
|
||||
二、办理环节时限:
|
||||
(一)一般事项:受理之日起60日内办结
|
||||
(二)复杂事项:经批准可延长30日(共90日)
|
||||
(三)转办事项:转出时限15日,办理时限同上
|
||||
(四)复查:收到复查申请之日起30日内作出复查意见
|
||||
(五)复核:收到复核申请之日起30日内作出复核意见
|
||||
|
||||
三、答复送达时限:
|
||||
作出处理意见之日起10日内送达信访人
|
||||
|
||||
第三条 催办机制
|
||||
|
||||
(一)临期提醒:办理期限过半时系统自动提醒
|
||||
(二)到期催办:到期未办结的,当日发出催办通知
|
||||
(三)超期督办:超期5日以上的,启动督办程序
|
||||
(四)严重超期:超期30日以上的,报请领导约谈
|
||||
|
||||
第四条 延期审批
|
||||
|
||||
申请延期须具备以下条件之一:
|
||||
(一)案情复杂,需进一步调查取证
|
||||
(二)涉及多个部门,需协调会商
|
||||
(三)当事人无法联系或外出
|
||||
(四)依法需要鉴定、评估等专业程序
|
||||
|
||||
延期审批流程:承办人申请→科室负责人审核→分管领导批准→告知信访人
|
||||
|
||||
第五条 责任追究
|
||||
|
||||
(一)无正当理由超期办结的,扣减考核分
|
||||
(二)一年内超期3次以上的,约谈承办人
|
||||
(三)因超期导致矛盾激化的,追究相关责任'),
|
||||
|
||||
('a1400000-0000-0000-0000-000000000003', kb_supervise_id, '信访工作考核评价标准', 'txt', 4000, 'completed', creator_id,
|
||||
'信访工作考核评价标准(详细版)
|
||||
|
||||
一、办理质效考核(60分)
|
||||
|
||||
1. 按期办结率(15分)
|
||||
- 95%以上:15分
|
||||
- 90%-95%:12分
|
||||
- 85%-90%:9分
|
||||
- 80%-85%:6分
|
||||
- 80%以下:0分
|
||||
|
||||
2. 一次性化解率(15分)
|
||||
- 80%以上:15分
|
||||
- 70%-80%:12分
|
||||
- 60%-70%:9分
|
||||
- 60%以下:6分
|
||||
|
||||
3. 答复规范率(15分)
|
||||
- 格式完整:5分(是否按规范格式撰写)
|
||||
- 依据充分:5分(是否引用法规政策)
|
||||
- 救济告知:3分(是否告知复查复核权利)
|
||||
- 送达规范:2分(是否规范送达并签收)
|
||||
|
||||
4. 群众满意率(15分)
|
||||
- 85%以上:15分
|
||||
- 75%-85%:12分
|
||||
- 65%-75%:9分
|
||||
- 65%以下:6分
|
||||
|
||||
满意度调查方式:
|
||||
- 电话回访(30%权重)
|
||||
- 书面评价(40%权重)
|
||||
- 网上评价(30%权重)
|
||||
|
||||
二、源头治理考核(20分)
|
||||
|
||||
1. 矛盾排查(8分)
|
||||
- 定期排查制度执行:3分
|
||||
- 重点领域专项排查:3分
|
||||
- 排查问题台账管理:2分
|
||||
|
||||
2. 源头化解(12分)
|
||||
- 初信初访化解率:6分
|
||||
- 重复信访下降率:3分
|
||||
- 信访积案清理:3分
|
||||
|
||||
三、工作规范考核(20分)
|
||||
|
||||
1. 制度建设(5分)
|
||||
- 工作制度完善:2分
|
||||
- 首接首办制度:2分
|
||||
- AB岗制度:1分
|
||||
|
||||
2. 信息化建设(5分)
|
||||
- 系统使用率95%以上:2分
|
||||
- 数据录入及时准确:2分
|
||||
- 网上信访办理率80%以上:1分
|
||||
|
||||
3. 信访秩序(5分)
|
||||
- 越级信访控制:2分
|
||||
- 集体访引导处置:2分
|
||||
- 非正常访控制:1分
|
||||
|
||||
4. 档案管理(5分)
|
||||
- 一案一档:2分
|
||||
- 录入完整:2分
|
||||
- 归档及时:1分
|
||||
|
||||
四、加分项(上限10分)
|
||||
- 化解重大积案:每件+2分
|
||||
- 工作创新被推广:+3分
|
||||
- 获上级表彰:+5分
|
||||
|
||||
五、扣分项
|
||||
- 因办理不力引发上访:每件-5分
|
||||
- 因工作失职引发群体事件:-20分
|
||||
- 弄虚作假:取消评优资格');
|
||||
|
||||
-- ============ 关联知识库到应用 ============
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'xinfang-policy-consult';
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'xinfang-law-search';
|
||||
UPDATE applications SET knowledge_base_id = kb_law_id WHERE slug = 'xinfang-risk-eval';
|
||||
UPDATE applications SET knowledge_base_id = kb_reply_id WHERE slug = 'xinfang-reply-gen';
|
||||
UPDATE applications SET knowledge_base_id = kb_mediate_id WHERE slug = 'xinfang-dispute-analysis';
|
||||
UPDATE applications SET knowledge_base_id = kb_mediate_id WHERE slug = 'xinfang-mediation-doc';
|
||||
UPDATE applications SET knowledge_base_id = kb_supervise_id WHERE slug = 'xinfang-supervision-report';
|
||||
|
||||
END $$;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTManager struct {
|
||||
secret []byte
|
||||
accessExpiry time.Duration
|
||||
refreshExpiry time.Duration
|
||||
}
|
||||
|
||||
func NewJWTManager(secret string, accessExpiry, refreshExpiry time.Duration) *JWTManager {
|
||||
return &JWTManager{
|
||||
secret: []byte(secret),
|
||||
accessExpiry: accessExpiry,
|
||||
refreshExpiry: refreshExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTManager) GenerateTokenPair(userID uuid.UUID, email, role string) (*TokenPair, error) {
|
||||
now := time.Now()
|
||||
accessExp := now.Add(m.accessExpiry)
|
||||
|
||||
accessClaims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(accessExp),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Subject: userID.String(),
|
||||
},
|
||||
}
|
||||
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
accessStr, err := accessToken.SignedString(m.secret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign access token: %w", err)
|
||||
}
|
||||
|
||||
refreshClaims := &Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshExpiry)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Subject: userID.String(),
|
||||
},
|
||||
}
|
||||
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshStr, err := refreshToken.SignedString(m.secret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign refresh token: %w", err)
|
||||
}
|
||||
|
||||
return &TokenPair{
|
||||
AccessToken: accessStr,
|
||||
RefreshToken: refreshStr,
|
||||
ExpiresAt: accessExp.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *JWTManager) ValidateToken(tokenStr string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (any, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return m.secret, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package auth
|
||||
|
||||
import "context"
|
||||
|
||||
// SSOProvider defines the interface for SSO authentication providers.
|
||||
// Implementations will be added for LDAP, OAuth2, and SAML.
|
||||
type SSOProvider interface {
|
||||
// Authenticate validates credentials and returns user information.
|
||||
Authenticate(ctx context.Context, credentials map[string]string) (*SSOUser, error)
|
||||
// Name returns the provider name (e.g., "ldap", "oauth2", "saml").
|
||||
Name() string
|
||||
}
|
||||
|
||||
type SSOUser struct {
|
||||
ExternalID string
|
||||
Name string
|
||||
Email string
|
||||
Phone string
|
||||
Department string
|
||||
AvatarURL string
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Package chunker 提供文本智能分片服务,将长文本切分为适合向量化的片段
|
||||
package chunker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Options 分片配置
|
||||
type Options struct {
|
||||
ChunkSize int // 每块最大字符数,默认 500
|
||||
Overlap int // 块间重叠字符数,默认 50
|
||||
Separators []string // 分隔符列表(按优先级)
|
||||
}
|
||||
|
||||
// DefaultOptions 默认分片配置
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
ChunkSize: 500,
|
||||
Overlap: 50,
|
||||
Separators: []string{
|
||||
"\n\n", "\n", "。", ".", "!", "!", "?", "?", ";", ";", " ",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkText 智能分片:优先按段落/句子边界切分,避免在句子中间断开
|
||||
func ChunkText(text string, opts Options) []string {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.ChunkSize <= 0 {
|
||||
opts.ChunkSize = 500
|
||||
}
|
||||
if opts.Overlap < 0 {
|
||||
opts.Overlap = 0
|
||||
}
|
||||
if opts.Separators == nil {
|
||||
opts.Separators = DefaultOptions().Separators
|
||||
}
|
||||
|
||||
runeLen := utf8.RuneCountInString(text)
|
||||
if runeLen <= opts.ChunkSize {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
// 递归分片
|
||||
chunks := recursiveSplit(text, opts.Separators, opts.ChunkSize)
|
||||
|
||||
// 添加重叠
|
||||
if opts.Overlap > 0 && len(chunks) > 1 {
|
||||
chunks = addOverlap(chunks, opts.Overlap)
|
||||
}
|
||||
|
||||
// 过滤空片段
|
||||
var result []string
|
||||
for _, c := range chunks {
|
||||
c = strings.TrimSpace(c)
|
||||
if c != "" {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// recursiveSplit 递归分片
|
||||
func recursiveSplit(text string, separators []string, chunkSize int) []string {
|
||||
if utf8.RuneCountInString(text) <= chunkSize {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
for _, sep := range separators {
|
||||
parts := strings.Split(text, sep)
|
||||
if len(parts) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
var result []string
|
||||
current := ""
|
||||
for _, part := range parts {
|
||||
candidate := current
|
||||
if candidate != "" {
|
||||
candidate += sep
|
||||
}
|
||||
candidate += part
|
||||
|
||||
if utf8.RuneCountInString(candidate) <= chunkSize {
|
||||
current = candidate
|
||||
} else {
|
||||
if current != "" {
|
||||
result = append(result, current)
|
||||
}
|
||||
if utf8.RuneCountInString(part) > chunkSize {
|
||||
// 继续用更细的分隔符拆分
|
||||
nextSeps := separators[1:]
|
||||
if len(nextSeps) == 0 {
|
||||
nextSeps = nil
|
||||
}
|
||||
result = append(result, recursiveSplit(part, nextSeps, chunkSize)...)
|
||||
current = ""
|
||||
} else {
|
||||
current = part
|
||||
}
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
result = append(result, current)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 无分隔符可用,按字符硬切
|
||||
runes := []rune(text)
|
||||
var result []string
|
||||
for i := 0; i < len(runes); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(runes) {
|
||||
end = len(runes)
|
||||
}
|
||||
result = append(result, string(runes[i:end]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// addOverlap 给分片添加重叠区域
|
||||
func addOverlap(chunks []string, overlap int) []string {
|
||||
if len(chunks) <= 1 || overlap <= 0 {
|
||||
return chunks
|
||||
}
|
||||
|
||||
result := []string{chunks[0]}
|
||||
for i := 1; i < len(chunks); i++ {
|
||||
prevRunes := []rune(chunks[i-1])
|
||||
overlapStart := len(prevRunes) - overlap
|
||||
if overlapStart < 0 {
|
||||
overlapStart = 0
|
||||
}
|
||||
prevTail := string(prevRunes[overlapStart:])
|
||||
result = append(result, prevTail+chunks[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func NewPool(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
|
||||
config, err := pgxpool.ParseConfig(databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse database url: %w", err)
|
||||
}
|
||||
|
||||
config.MaxConns = 20
|
||||
config.MinConns = 5
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("ping database: %w", err)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
-- name: CreateApplication :one
|
||||
INSERT INTO applications (
|
||||
name, slug, description, long_description, icon_url,
|
||||
category_id, creator_id, dept_id,
|
||||
dify_app_id, dify_app_type, dify_api_key,
|
||||
app_config, welcome_message, suggested_prompts,
|
||||
max_tokens, temperature, status, visibility, is_template
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetApplicationByID :one
|
||||
SELECT * FROM applications WHERE id = $1;
|
||||
|
||||
-- name: GetApplicationBySlug :one
|
||||
SELECT * FROM applications WHERE slug = $1;
|
||||
|
||||
-- name: UpdateApplication :one
|
||||
UPDATE applications
|
||||
SET name = COALESCE(sqlc.narg('name'), name),
|
||||
description = COALESCE(sqlc.narg('description'), description),
|
||||
long_description = COALESCE(sqlc.narg('long_description'), long_description),
|
||||
icon_url = COALESCE(sqlc.narg('icon_url'), icon_url),
|
||||
category_id = COALESCE(sqlc.narg('category_id'), category_id),
|
||||
app_config = COALESCE(sqlc.narg('app_config'), app_config),
|
||||
welcome_message = COALESCE(sqlc.narg('welcome_message'), welcome_message),
|
||||
suggested_prompts = COALESCE(sqlc.narg('suggested_prompts'), suggested_prompts),
|
||||
max_tokens = COALESCE(sqlc.narg('max_tokens'), max_tokens),
|
||||
temperature = COALESCE(sqlc.narg('temperature'), temperature),
|
||||
visibility = COALESCE(sqlc.narg('visibility'), visibility)
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateApplicationStatus :exec
|
||||
UPDATE applications SET status = $2 WHERE id = $1;
|
||||
|
||||
-- name: DeleteApplication :exec
|
||||
DELETE FROM applications WHERE id = $1 AND status = 'draft';
|
||||
|
||||
-- name: ListStoreApps :many
|
||||
SELECT a.*, c.name as category_name, c.slug as category_slug, u.name as creator_name
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.status = 'approved'
|
||||
AND a.visibility = 'public'
|
||||
AND (sqlc.narg('category_slug')::VARCHAR IS NULL OR c.slug = sqlc.narg('category_slug'))
|
||||
AND (sqlc.narg('search')::VARCHAR IS NULL
|
||||
OR to_tsvector('simple', a.name || ' ' || COALESCE(a.description, ''))
|
||||
@@ plainto_tsquery('simple', sqlc.narg('search')))
|
||||
ORDER BY
|
||||
CASE WHEN sqlc.narg('sort')::VARCHAR = 'popular' THEN a.usage_count END DESC,
|
||||
CASE WHEN sqlc.narg('sort')::VARCHAR = 'rating' THEN a.avg_rating END DESC,
|
||||
CASE WHEN sqlc.narg('sort')::VARCHAR IS NULL OR sqlc.narg('sort') = 'latest' THEN EXTRACT(EPOCH FROM a.published_at) END DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountStoreApps :one
|
||||
SELECT COUNT(*) FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE a.status = 'approved'
|
||||
AND a.visibility = 'public'
|
||||
AND (sqlc.narg('category_slug')::VARCHAR IS NULL OR c.slug = sqlc.narg('category_slug'))
|
||||
AND (sqlc.narg('search')::VARCHAR IS NULL
|
||||
OR to_tsvector('simple', a.name || ' ' || COALESCE(a.description, ''))
|
||||
@@ plainto_tsquery('simple', sqlc.narg('search')));
|
||||
|
||||
-- name: ListFeaturedApps :many
|
||||
SELECT a.*, c.name as category_name, u.name as creator_name
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.is_featured = true AND a.status = 'approved' AND a.visibility = 'public'
|
||||
ORDER BY a.usage_count DESC
|
||||
LIMIT $1;
|
||||
|
||||
-- name: ListTopApps :many
|
||||
SELECT a.*, c.name as category_name, u.name as creator_name
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE a.status = 'approved' AND a.visibility = 'public'
|
||||
ORDER BY a.usage_count DESC
|
||||
LIMIT $1;
|
||||
|
||||
-- name: ListCreatorApps :many
|
||||
SELECT a.*, c.name as category_name
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE a.creator_id = $1
|
||||
ORDER BY a.updated_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: ListTemplates :many
|
||||
SELECT a.*, c.name as category_name
|
||||
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;
|
||||
|
||||
-- name: IncrementUsageCount :exec
|
||||
UPDATE applications SET usage_count = usage_count + 1 WHERE id = $1;
|
||||
|
||||
-- name: UpdateFavoriteCount :exec
|
||||
UPDATE applications SET favorite_count = favorite_count + $2 WHERE id = $1;
|
||||
|
||||
-- name: UpdateAppRating :exec
|
||||
UPDATE applications
|
||||
SET avg_rating = $2, rating_count = $3
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListAllApps :many
|
||||
SELECT a.*, c.name as category_name, u.name as creator_name
|
||||
FROM applications a
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE (sqlc.narg('status')::VARCHAR IS NULL OR a.status = sqlc.narg('status'))
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountAllApps :one
|
||||
SELECT COUNT(*) FROM applications
|
||||
WHERE (sqlc.narg('status')::VARCHAR IS NULL OR status = sqlc.narg('status'));
|
||||
@@ -0,0 +1,23 @@
|
||||
-- name: CreateAuditLog :exec
|
||||
INSERT INTO audit_logs (user_id, action, resource_type, resource_id, details, ip_address, user_agent)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7);
|
||||
|
||||
-- name: ListAuditLogs :many
|
||||
SELECT al.*, u.name as user_name
|
||||
FROM audit_logs al
|
||||
LEFT JOIN users u ON al.user_id = u.id
|
||||
WHERE (sqlc.narg('user_id')::UUID IS NULL OR al.user_id = sqlc.narg('user_id'))
|
||||
AND (sqlc.narg('action')::VARCHAR IS NULL OR al.action = sqlc.narg('action'))
|
||||
AND (sqlc.narg('resource_type')::VARCHAR IS NULL OR al.resource_type = sqlc.narg('resource_type'))
|
||||
AND (sqlc.narg('start_time')::TIMESTAMPTZ IS NULL OR al.created_at >= sqlc.narg('start_time'))
|
||||
AND (sqlc.narg('end_time')::TIMESTAMPTZ IS NULL OR al.created_at <= sqlc.narg('end_time'))
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountAuditLogs :one
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE (sqlc.narg('user_id')::UUID IS NULL OR user_id = sqlc.narg('user_id'))
|
||||
AND (sqlc.narg('action')::VARCHAR IS NULL OR action = sqlc.narg('action'))
|
||||
AND (sqlc.narg('resource_type')::VARCHAR IS NULL OR resource_type = sqlc.narg('resource_type'))
|
||||
AND (sqlc.narg('start_time')::TIMESTAMPTZ IS NULL OR created_at >= sqlc.narg('start_time'))
|
||||
AND (sqlc.narg('end_time')::TIMESTAMPTZ IS NULL OR created_at <= sqlc.narg('end_time'));
|
||||
@@ -0,0 +1,10 @@
|
||||
-- name: ListCategories :many
|
||||
SELECT * FROM categories
|
||||
WHERE status = 'active'
|
||||
ORDER BY sort_order ASC;
|
||||
|
||||
-- name: GetCategoryByID :one
|
||||
SELECT * FROM categories WHERE id = $1;
|
||||
|
||||
-- name: GetCategoryBySlug :one
|
||||
SELECT * FROM categories WHERE slug = $1;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- name: AddFavorite :exec
|
||||
INSERT INTO app_favorites (user_id, app_id) VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: RemoveFavorite :exec
|
||||
DELETE FROM app_favorites WHERE user_id = $1 AND app_id = $2;
|
||||
|
||||
-- name: IsFavorited :one
|
||||
SELECT EXISTS(SELECT 1 FROM app_favorites WHERE user_id = $1 AND app_id = $2);
|
||||
|
||||
-- name: ListUserFavorites :many
|
||||
SELECT a.*, c.name as category_name
|
||||
FROM app_favorites f
|
||||
JOIN applications a ON f.app_id = a.id
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE f.user_id = $1
|
||||
ORDER BY f.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: UpsertRating :one
|
||||
INSERT INTO app_ratings (app_id, user_id, score, comment)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (app_id, user_id)
|
||||
DO UPDATE SET score = EXCLUDED.score, comment = EXCLUDED.comment
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetAppAvgRating :one
|
||||
SELECT COALESCE(AVG(score)::REAL, 0) as avg_rating, COUNT(*) as rating_count
|
||||
FROM app_ratings WHERE app_id = $1;
|
||||
|
||||
-- name: ListAppRatings :many
|
||||
SELECT r.*, u.name as user_name, u.avatar_url as user_avatar
|
||||
FROM app_ratings r
|
||||
JOIN users u ON r.user_id = u.id
|
||||
WHERE r.app_id = $1
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
@@ -0,0 +1,40 @@
|
||||
-- name: CreateReview :one
|
||||
INSERT INTO app_reviews (app_id, version, submitter_id, submit_comment)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetReviewByID :one
|
||||
SELECT * FROM app_reviews WHERE id = $1;
|
||||
|
||||
-- name: ListPendingReviews :many
|
||||
SELECT r.*, a.name as app_name, a.description as app_description,
|
||||
a.icon_url as app_icon, u.name as submitter_name
|
||||
FROM app_reviews r
|
||||
JOIN applications a ON r.app_id = a.id
|
||||
JOIN users u ON r.submitter_id = u.id
|
||||
WHERE r.status = 'pending'
|
||||
ORDER BY r.submitted_at ASC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountPendingReviews :one
|
||||
SELECT COUNT(*) FROM app_reviews WHERE status = 'pending';
|
||||
|
||||
-- name: ApproveReview :exec
|
||||
UPDATE app_reviews
|
||||
SET status = 'approved', reviewer_id = $2, review_comment = $3, reviewed_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: RejectReview :exec
|
||||
UPDATE app_reviews
|
||||
SET status = 'rejected', reviewer_id = $2, review_comment = $3, reviewed_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: WithdrawReview :exec
|
||||
UPDATE app_reviews SET status = 'withdrawn' WHERE id = $1 AND status = 'pending';
|
||||
|
||||
-- name: ListAppReviews :many
|
||||
SELECT r.*, u.name as reviewer_name
|
||||
FROM app_reviews r
|
||||
LEFT JOIN users u ON r.reviewer_id = u.id
|
||||
WHERE r.app_id = $1
|
||||
ORDER BY r.created_at DESC;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- name: CreateUsageLog :one
|
||||
INSERT INTO app_usage_logs (
|
||||
app_id, user_id, dept_id, conversation_id, message_count,
|
||||
prompt_tokens, completion_tokens, total_tokens, model_name,
|
||||
estimated_cost, duration_ms, is_successful, error_message, client_type
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetRecentUsedApps :many
|
||||
SELECT DISTINCT ON (a.id) a.*, c.name as category_name, l.created_at as last_used_at
|
||||
FROM app_usage_logs l
|
||||
JOIN applications a ON l.app_id = a.id
|
||||
LEFT JOIN categories c ON a.category_id = c.id
|
||||
WHERE l.user_id = $1
|
||||
ORDER BY a.id, l.created_at DESC
|
||||
LIMIT $2;
|
||||
|
||||
-- name: GetUserStats :one
|
||||
SELECT
|
||||
COUNT(*) as total_conversations,
|
||||
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
||||
COALESCE(SUM(estimated_cost), 0) as total_cost
|
||||
FROM app_usage_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2;
|
||||
|
||||
-- name: GetOverviewStats :one
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM users WHERE status = 'active') as total_users,
|
||||
(SELECT COUNT(*) FROM applications WHERE status = 'approved') as total_apps,
|
||||
(SELECT COUNT(DISTINCT user_id) FROM app_usage_logs WHERE created_at >= $1) as active_users,
|
||||
(SELECT COUNT(*) FROM app_usage_logs WHERE created_at >= $1) as total_conversations,
|
||||
(SELECT COALESCE(SUM(total_tokens), 0) FROM app_usage_logs WHERE created_at >= $2) as monthly_tokens,
|
||||
(SELECT COALESCE(SUM(estimated_cost), 0) FROM app_usage_logs WHERE created_at >= $2) as monthly_cost;
|
||||
@@ -0,0 +1,52 @@
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = $1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users WHERE email = $1;
|
||||
|
||||
-- name: GetUserByEmployeeID :one
|
||||
SELECT * FROM users WHERE employee_id = $1;
|
||||
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (name, email, password_hash, phone, avatar_url, role, status, sso_provider, sso_external_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserProfile :one
|
||||
UPDATE users
|
||||
SET name = COALESCE(sqlc.narg('name'), name),
|
||||
phone = COALESCE(sqlc.narg('phone'), phone),
|
||||
avatar_url = COALESCE(sqlc.narg('avatar_url'), avatar_url)
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserRole :exec
|
||||
UPDATE users SET role = $2 WHERE id = $1;
|
||||
|
||||
-- name: UpdateUserStatus :exec
|
||||
UPDATE users SET status = $2 WHERE id = $1;
|
||||
|
||||
-- name: UpdateUserLogin :exec
|
||||
UPDATE users
|
||||
SET last_login_at = NOW(), login_count = login_count + 1
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListUsers :many
|
||||
SELECT * FROM users
|
||||
WHERE (sqlc.narg('role')::VARCHAR IS NULL OR role = sqlc.narg('role'))
|
||||
AND (sqlc.narg('status')::VARCHAR IS NULL OR status = sqlc.narg('status'))
|
||||
AND (sqlc.narg('search')::VARCHAR IS NULL
|
||||
OR name ILIKE '%' || sqlc.narg('search') || '%'
|
||||
OR email ILIKE '%' || sqlc.narg('search') || '%'
|
||||
OR employee_id ILIKE '%' || sqlc.narg('search') || '%')
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountUsers :one
|
||||
SELECT COUNT(*) FROM users
|
||||
WHERE (sqlc.narg('role')::VARCHAR IS NULL OR role = sqlc.narg('role'))
|
||||
AND (sqlc.narg('status')::VARCHAR IS NULL OR status = sqlc.narg('status'))
|
||||
AND (sqlc.narg('search')::VARCHAR IS NULL
|
||||
OR name ILIKE '%' || sqlc.narg('search') || '%'
|
||||
OR email ILIKE '%' || sqlc.narg('search') || '%'
|
||||
OR employee_id ILIKE '%' || sqlc.narg('search') || '%');
|
||||
@@ -0,0 +1,74 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path, apiKey string, body any) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
defer resp.Body.Close()
|
||||
var apiErr APIError
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
||||
return nil, fmt.Errorf("dify API error (status %d)", resp.StatusCode)
|
||||
}
|
||||
apiErr.Status = resp.StatusCode
|
||||
return nil, &apiErr
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method, path, apiKey string, body any, result any) error {
|
||||
resp, err := c.do(ctx, method, path, apiKey, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChatStream sends a chat message and returns a reader for SSE events.
|
||||
// Caller is responsible for closing the returned io.ReadCloser.
|
||||
func (c *Client) ChatStream(ctx context.Context, apiKey string, req *ChatRequest) (io.ReadCloser, error) {
|
||||
req.ResponseMode = "streaming"
|
||||
|
||||
resp, err := c.do(ctx, "POST", "/chat-messages", apiKey, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// ChatBlocking sends a chat message and waits for the complete response.
|
||||
func (c *Client) ChatBlocking(ctx context.Context, apiKey string, req *ChatRequest) (*ChatStreamEvent, error) {
|
||||
req.ResponseMode = "blocking"
|
||||
|
||||
var result ChatStreamEvent
|
||||
if err := c.doJSON(ctx, "POST", "/chat-messages", apiKey, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ParseSSEStream parses a Dify SSE stream and calls handler for each event.
|
||||
func ParseSSEStream(reader io.Reader, handler func(event ChatStreamEvent) error) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 64*1024), 256*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
if data == "[DONE]" {
|
||||
break
|
||||
}
|
||||
|
||||
var event ChatStreamEvent
|
||||
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := handler(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// ListConversations returns the user's conversation list for an app.
|
||||
func (c *Client) ListConversations(ctx context.Context, apiKey, user string, limit int, firstID string) (*ConversationListResponse, error) {
|
||||
path := fmt.Sprintf("/conversations?user=%s&limit=%d", user, limit)
|
||||
if firstID != "" {
|
||||
path += "&first_id=" + firstID
|
||||
}
|
||||
|
||||
var result ConversationListResponse
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ListMessages returns messages in a conversation.
|
||||
func (c *Client) ListMessages(ctx context.Context, apiKey, user, conversationID string, limit int, firstID string) (*MessageListResponse, error) {
|
||||
path := fmt.Sprintf("/messages?user=%s&conversation_id=%s&limit=%d", user, conversationID, limit)
|
||||
if firstID != "" {
|
||||
path += "&first_id=" + firstID
|
||||
}
|
||||
|
||||
var result MessageListResponse
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteConversation deletes a conversation.
|
||||
func (c *Client) DeleteConversation(ctx context.Context, apiKey, user, conversationID string) error {
|
||||
body := map[string]string{"user": user}
|
||||
return c.doJSON(ctx, "DELETE", "/conversations/"+conversationID, apiKey, body, nil)
|
||||
}
|
||||
|
||||
// SubmitFeedback submits feedback for a message.
|
||||
func (c *Client) SubmitFeedback(ctx context.Context, apiKey, messageID string, req *FeedbackRequest) error {
|
||||
return c.doJSON(ctx, "POST", "/messages/"+messageID+"/feedbacks", apiKey, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CreateDataset creates a new knowledge base (dataset) in Dify.
|
||||
func (c *Client) CreateDataset(ctx context.Context, apiKey string, req *DatasetCreateRequest) (*Dataset, error) {
|
||||
var result Dataset
|
||||
if err := c.doJSON(ctx, "POST", "/datasets", apiKey, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteDataset deletes a knowledge base.
|
||||
func (c *Client) DeleteDataset(ctx context.Context, apiKey, datasetID string) error {
|
||||
return c.doJSON(ctx, "DELETE", "/datasets/"+datasetID, apiKey, nil, nil)
|
||||
}
|
||||
|
||||
// UploadDocument uploads a file to a dataset for indexing.
|
||||
func (c *Client) UploadDocument(ctx context.Context, apiKey, datasetID string, filename string, fileReader io.Reader) (*DocumentIndexingStatus, error) {
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create form file: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(part, fileReader); err != nil {
|
||||
return nil, fmt.Errorf("copy file: %w", err)
|
||||
}
|
||||
|
||||
// indexing mode
|
||||
if err := writer.WriteField("indexing_technique", "high_quality"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writer.WriteField("process_rule", `{"mode": "automatic"}`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
path := fmt.Sprintf("/datasets/%s/document/create_by_file", datasetID)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+path, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("upload failed (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Dify returns document info in response
|
||||
var result struct {
|
||||
Document DocumentIndexingStatus `json:"document"`
|
||||
}
|
||||
if err := decodeJSON(resp.Body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result.Document, nil
|
||||
}
|
||||
|
||||
// DeleteDocument deletes a document from a dataset.
|
||||
func (c *Client) DeleteDocument(ctx context.Context, apiKey, datasetID, documentID string) error {
|
||||
path := fmt.Sprintf("/datasets/%s/documents/%s", datasetID, documentID)
|
||||
return c.doJSON(ctx, "DELETE", path, apiKey, nil, nil)
|
||||
}
|
||||
|
||||
// GetDocumentIndexingStatus checks the indexing status of a document.
|
||||
func (c *Client) GetDocumentIndexingStatus(ctx context.Context, apiKey, datasetID, batch string) (*DocumentIndexingStatus, error) {
|
||||
path := fmt.Sprintf("/datasets/%s/documents/%s/indexing-status", datasetID, batch)
|
||||
var result DocumentIndexingStatus
|
||||
if err := c.doJSON(ctx, "GET", path, apiKey, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, v any) error {
|
||||
return json.NewDecoder(r).Decode(v)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package dify
|
||||
|
||||
import "time"
|
||||
|
||||
// --- Request types ---
|
||||
|
||||
type ChatRequest struct {
|
||||
Query string `json:"query"`
|
||||
Inputs map[string]any `json:"inputs,omitempty"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
User string `json:"user"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
}
|
||||
|
||||
type CompletionRequest struct {
|
||||
Inputs map[string]any `json:"inputs"`
|
||||
User string `json:"user"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
}
|
||||
|
||||
type FeedbackRequest struct {
|
||||
Rating string `json:"rating"` // "like", "dislike", null
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// --- Response types ---
|
||||
|
||||
type ChatStreamEvent struct {
|
||||
Event string `json:"event"`
|
||||
TaskID string `json:"task_id"`
|
||||
MessageID string `json:"message_id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
Answer string `json:"answer"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type MessageEndMetadata struct {
|
||||
Usage TokenUsage `json:"usage"`
|
||||
}
|
||||
|
||||
type TokenUsage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Inputs any `json:"inputs"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ConversationListResponse struct {
|
||||
Data []Conversation `json:"data"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
Query string `json:"query"`
|
||||
Answer string `json:"answer"`
|
||||
Feedback *Feedback `json:"feedback"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Inputs map[string]any `json:"inputs"`
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
Rating string `json:"rating"`
|
||||
}
|
||||
|
||||
type MessageListResponse struct {
|
||||
Data []Message `json:"data"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// --- Dataset/Knowledge types ---
|
||||
|
||||
type DatasetCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Dataset struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
DocCount int `json:"document_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type DocumentIndexingStatus struct {
|
||||
ID string `json:"id"`
|
||||
IndexingStatus string `json:"indexing_status"`
|
||||
ProcessingStart string `json:"processing_started_at"`
|
||||
CompletedAt string `json:"completed_at"`
|
||||
}
|
||||
|
||||
// --- Error ---
|
||||
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Package embedding 提供文本向量化服务,支持 OpenAI 兼容的 Embedding API
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config embedding 服务配置
|
||||
type Config struct {
|
||||
APIKey string // API 密钥
|
||||
BaseURL string // API 基础 URL(OpenAI 兼容格式)
|
||||
Model string // 模型名称
|
||||
Dimensions int // 向量维度
|
||||
}
|
||||
|
||||
// Client embedding 客户端
|
||||
type Client struct {
|
||||
cfg Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient 创建 embedding 客户端
|
||||
func NewClient(cfg Config) *Client {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
}
|
||||
if cfg.Model == "" {
|
||||
cfg.Model = "text-embedding-v3"
|
||||
}
|
||||
if cfg.Dimensions == 0 {
|
||||
cfg.Dimensions = 1024
|
||||
}
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// embeddingRequest OpenAI 兼容的 embedding 请求
|
||||
type embeddingRequest struct {
|
||||
Input interface{} `json:"input"`
|
||||
Model string `json:"model"`
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
}
|
||||
|
||||
// embeddingResponse OpenAI 兼容的 embedding 响应
|
||||
type embeddingResponse struct {
|
||||
Data []struct {
|
||||
Embedding []float32 `json:"embedding"`
|
||||
Index int `json:"index"`
|
||||
} `json:"data"`
|
||||
Usage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
// GetEmbedding 获取单条文本的向量嵌入
|
||||
func (c *Client) GetEmbedding(ctx context.Context, text string) ([]float32, error) {
|
||||
if c.cfg.APIKey == "" {
|
||||
return nil, fmt.Errorf("embedding API key not configured")
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("empty text")
|
||||
}
|
||||
// 截断过长文本
|
||||
if len([]rune(text)) > 8000 {
|
||||
text = string([]rune(text)[:8000])
|
||||
}
|
||||
|
||||
req := embeddingRequest{
|
||||
Input: text,
|
||||
Model: c.cfg.Model,
|
||||
Dimensions: c.cfg.Dimensions,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := strings.TrimRight(c.cfg.BaseURL, "/") + "/embeddings"
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("embedding request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("embedding error (status %d): %s", resp.StatusCode, string(errBody))
|
||||
}
|
||||
|
||||
var result embeddingResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Data) == 0 {
|
||||
return nil, fmt.Errorf("no embedding data returned")
|
||||
}
|
||||
return result.Data[0].Embedding, nil
|
||||
}
|
||||
|
||||
// GetEmbeddingBatch 批量获取文本向量嵌入
|
||||
func (c *Client) GetEmbeddingBatch(ctx context.Context, texts []string) ([][]float32, error) {
|
||||
results := make([][]float32, len(texts))
|
||||
for i, text := range texts {
|
||||
emb, err := c.GetEmbedding(ctx, text)
|
||||
if err != nil {
|
||||
results[i] = nil
|
||||
continue
|
||||
}
|
||||
results[i] = emb
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// IsConfigured 检查 embedding 服务是否已配置
|
||||
func (c *Client) IsConfigured() bool {
|
||||
return c.cfg.APIKey != ""
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AnthropicProvider struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
model string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewAnthropicProvider(apiKey, baseURL, defaultModel string) *AnthropicProvider {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.anthropic.com"
|
||||
}
|
||||
if defaultModel == "" {
|
||||
defaultModel = "claude-sonnet-4-20250514"
|
||||
}
|
||||
return &AnthropicProvider{
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
model: defaultModel,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) Name() string { return "anthropic" }
|
||||
|
||||
type anthropicRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []anthropicMessage `json:"messages"`
|
||||
System string `json:"system,omitempty"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
type anthropicMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type anthropicResponse struct {
|
||||
ID string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = p.model
|
||||
}
|
||||
|
||||
system, msgs := extractSystemMessage(req.Messages)
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens == 0 {
|
||||
maxTokens = 4096
|
||||
}
|
||||
|
||||
aReq := anthropicRequest{
|
||||
Model: model,
|
||||
Messages: toAnthropicMessages(msgs),
|
||||
System: system,
|
||||
MaxTokens: maxTokens,
|
||||
Temperature: req.Temperature,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(aReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/v1/messages", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("x-api-key", p.apiKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("anthropic request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("anthropic error (status %d): %s", resp.StatusCode, string(errBody))
|
||||
}
|
||||
|
||||
var aResp anthropicResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&aResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := ""
|
||||
for _, c := range aResp.Content {
|
||||
if c.Type == "text" {
|
||||
content += c.Text
|
||||
}
|
||||
}
|
||||
|
||||
return &ChatResponse{
|
||||
Content: content,
|
||||
Model: aResp.Model,
|
||||
PromptTokens: aResp.Usage.InputTokens,
|
||||
CompletionTokens: aResp.Usage.OutputTokens,
|
||||
TotalTokens: aResp.Usage.InputTokens + aResp.Usage.OutputTokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnthropicProvider) ChatStream(ctx context.Context, req *ChatRequest) (io.ReadCloser, error) {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = p.model
|
||||
}
|
||||
|
||||
system, msgs := extractSystemMessage(req.Messages)
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens == 0 {
|
||||
maxTokens = 4096
|
||||
}
|
||||
|
||||
aReq := anthropicRequest{
|
||||
Model: model,
|
||||
Messages: toAnthropicMessages(msgs),
|
||||
System: system,
|
||||
MaxTokens: maxTokens,
|
||||
Temperature: req.Temperature,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(aReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/v1/messages", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("x-api-key", p.apiKey)
|
||||
httpReq.Header.Set("anthropic-version", "2023-06-01")
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("anthropic stream request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("anthropic error (status %d): %s", resp.StatusCode, string(errBody))
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func extractSystemMessage(msgs []Message) (string, []Message) {
|
||||
system := ""
|
||||
var filtered []Message
|
||||
for _, m := range msgs {
|
||||
if m.Role == RoleSystem {
|
||||
system = m.Content
|
||||
} else {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
return system, filtered
|
||||
}
|
||||
|
||||
func toAnthropicMessages(msgs []Message) []anthropicMessage {
|
||||
out := make([]anthropicMessage, len(msgs))
|
||||
for i, m := range msgs {
|
||||
out[i] = anthropicMessage{Role: string(m.Role), Content: m.Content}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Manager manages multiple LLM providers and routes requests.
|
||||
type Manager struct {
|
||||
providers map[string]Provider
|
||||
fallback string
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
providers: make(map[string]Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Register(name string, provider Provider) {
|
||||
m.providers[name] = provider
|
||||
if m.fallback == "" {
|
||||
m.fallback = name
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetFallback(name string) {
|
||||
m.fallback = name
|
||||
}
|
||||
|
||||
func (m *Manager) GetProvider(name string) (Provider, error) {
|
||||
if p, ok := m.providers[name]; ok {
|
||||
return p, nil
|
||||
}
|
||||
if p, ok := m.providers[m.fallback]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no provider found: %s", name)
|
||||
}
|
||||
|
||||
// Chat performs a blocking chat completion using the specified provider.
|
||||
func (m *Manager) Chat(ctx context.Context, providerName string, req *ChatRequest) (*ChatResponse, error) {
|
||||
provider, err := m.GetProvider(providerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provider.ChatCompletion(ctx, req)
|
||||
}
|
||||
|
||||
// ChatStream performs a streaming chat and returns the raw SSE body.
|
||||
func (m *Manager) ChatStream(ctx context.Context, providerName string, req *ChatRequest) (io.ReadCloser, error) {
|
||||
provider, err := m.GetProvider(providerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Stream = true
|
||||
return provider.ChatStream(ctx, req)
|
||||
}
|
||||
|
||||
// StreamEvent represents a normalized SSE event for the frontend.
|
||||
type StreamEvent struct {
|
||||
Event string `json:"event"`
|
||||
Answer string `json:"answer,omitempty"`
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// TransformOpenAIStream reads an OpenAI SSE stream and writes normalized events to the writer.
|
||||
func TransformOpenAIStream(reader io.Reader, write func(event StreamEvent)) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 64*1024), 256*1024)
|
||||
|
||||
var totalContent string
|
||||
var model string
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
if data == "[DONE]" {
|
||||
write(StreamEvent{
|
||||
Event: "message_end",
|
||||
Usage: &Usage{
|
||||
TotalTokens: estimateTokens(totalContent),
|
||||
Model: model,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
var chunk struct {
|
||||
ID string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
model = chunk.Model
|
||||
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
|
||||
content := chunk.Choices[0].Delta.Content
|
||||
totalContent += content
|
||||
write(StreamEvent{
|
||||
Event: "message",
|
||||
Answer: content,
|
||||
MessageID: chunk.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// TransformAnthropicStream reads an Anthropic SSE stream and writes normalized events.
|
||||
func TransformAnthropicStream(reader io.Reader, write func(event StreamEvent)) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(make([]byte, 64*1024), 256*1024)
|
||||
|
||||
var model string
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
if strings.HasPrefix(line, "event: ") {
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
|
||||
var event map[string]any
|
||||
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
eventType, _ := event["type"].(string)
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
if msg, ok := event["message"].(map[string]any); ok {
|
||||
if m, ok := msg["model"].(string); ok {
|
||||
model = m
|
||||
}
|
||||
}
|
||||
case "content_block_delta":
|
||||
if delta, ok := event["delta"].(map[string]any); ok {
|
||||
if text, ok := delta["text"].(string); ok {
|
||||
write(StreamEvent{
|
||||
Event: "message",
|
||||
Answer: text,
|
||||
})
|
||||
}
|
||||
}
|
||||
case "message_delta":
|
||||
if usage, ok := event["usage"].(map[string]any); ok {
|
||||
outputTokens := int(getFloat(usage, "output_tokens"))
|
||||
write(StreamEvent{
|
||||
Event: "message_end",
|
||||
Usage: &Usage{
|
||||
CompletionTokens: outputTokens,
|
||||
TotalTokens: outputTokens,
|
||||
Model: model,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func getFloat(m map[string]any, key string) float64 {
|
||||
if v, ok := m[key].(float64); ok {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func estimateTokens(text string) int {
|
||||
return len(text) / 4
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type OpenAIProvider struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
model string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewOpenAIProvider(apiKey, baseURL, defaultModel string) *OpenAIProvider {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
}
|
||||
if defaultModel == "" {
|
||||
defaultModel = "gpt-4o-mini"
|
||||
}
|
||||
return &OpenAIProvider{
|
||||
apiKey: apiKey,
|
||||
baseURL: baseURL,
|
||||
model: defaultModel,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) Name() string { return "openai" }
|
||||
|
||||
type openAIRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []openAIMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
type openAIMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type openAIResponse struct {
|
||||
ID string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = p.model
|
||||
}
|
||||
|
||||
oaiReq := openAIRequest{
|
||||
Model: model,
|
||||
Messages: toOpenAIMessages(req.Messages),
|
||||
Temperature: req.Temperature,
|
||||
MaxTokens: req.MaxTokens,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(oaiReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openai request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("openai error (status %d): %s", resp.StatusCode, string(errBody))
|
||||
}
|
||||
|
||||
var oaiResp openAIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&oaiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := ""
|
||||
if len(oaiResp.Choices) > 0 {
|
||||
content = oaiResp.Choices[0].Message.Content
|
||||
}
|
||||
|
||||
return &ChatResponse{
|
||||
Content: content,
|
||||
Model: oaiResp.Model,
|
||||
PromptTokens: oaiResp.Usage.PromptTokens,
|
||||
CompletionTokens: oaiResp.Usage.CompletionTokens,
|
||||
TotalTokens: oaiResp.Usage.TotalTokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) ChatStream(ctx context.Context, req *ChatRequest) (io.ReadCloser, error) {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = p.model
|
||||
}
|
||||
|
||||
oaiReq := openAIRequest{
|
||||
Model: model,
|
||||
Messages: toOpenAIMessages(req.Messages),
|
||||
Temperature: req.Temperature,
|
||||
MaxTokens: req.MaxTokens,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(oaiReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openai stream request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("openai error (status %d): %s", resp.StatusCode, string(errBody))
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func toOpenAIMessages(msgs []Message) []openAIMessage {
|
||||
out := make([]openAIMessage, len(msgs))
|
||||
for i, m := range msgs {
|
||||
out[i] = openAIMessage{Role: string(m.Role), Content: m.Content}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleSystem Role = "system"
|
||||
RoleUser Role = "user"
|
||||
RoleAssistant Role = "assistant"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role Role `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Content string `json:"content"`
|
||||
Model string `json:"model"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
}
|
||||
|
||||
type StreamDelta struct {
|
||||
Content string `json:"content"`
|
||||
FinishReason string `json:"finish_reason,omitempty"`
|
||||
}
|
||||
|
||||
// Provider abstracts an LLM API (OpenAI, Anthropic, etc.)
|
||||
type Provider interface {
|
||||
ChatCompletion(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
|
||||
ChatStream(ctx context.Context, req *ChatRequest) (io.ReadCloser, error)
|
||||
Name() string
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
Type string `json:"type"`
|
||||
APIKey string `json:"api_key"`
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "pkg/db/queries/"
|
||||
schema: "migrations/"
|
||||
gen:
|
||||
go:
|
||||
package: "db"
|
||||
out: "pkg/db/generated"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
||||
emit_empty_slices: true
|
||||
emit_pointers_for_null_types: true
|
||||
overrides:
|
||||
- db_type: "uuid"
|
||||
go_type: "github.com/google/uuid.UUID"
|
||||
- db_type: "timestamptz"
|
||||
go_type: "time.Time"
|
||||
- db_type: "jsonb"
|
||||
go_type: "json.RawMessage"
|
||||
import: "encoding/json"
|
||||
- db_type: "inet"
|
||||
go_type: "string"
|
||||
Reference in New Issue
Block a user