Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+96
View File
@@ -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))
}
+83
View File
@@ -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")
}
+247
View File
@@ -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, "接口开发中")
}