Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载环境变量
|
||||
_ = godotenv.Load("../../.env")
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "postgres://freedak:@localhost:5432/govai_portal?sslmode=disable"
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(context.Background(), dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("无法连接数据库: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
fmt.Println("=== 数据库中的模型提供商配置 ===")
|
||||
|
||||
rows, err := pool.Query(context.Background(), `
|
||||
SELECT id, name, base_url,
|
||||
SUBSTRING(api_key_encrypted, 1, 20) as api_key_prefix,
|
||||
models, is_active, priority, config
|
||||
FROM model_providers
|
||||
ORDER BY priority DESC, created_at
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var id, name, baseURL, apiKeyPrefix string
|
||||
var modelsJSON, configJSON []byte
|
||||
var isActive bool
|
||||
var priority int
|
||||
|
||||
err := rows.Scan(&id, &name, &baseURL, &apiKeyPrefix, &modelsJSON, &isActive, &priority, &configJSON)
|
||||
if err != nil {
|
||||
log.Printf("扫描行失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
count++
|
||||
fmt.Printf("【提供商 %d】\n", count)
|
||||
fmt.Printf("ID: %s\n", id)
|
||||
fmt.Printf("名称: %s\n", name)
|
||||
fmt.Printf("Base URL: %s\n", baseURL)
|
||||
fmt.Printf("API Key: %s...\n", apiKeyPrefix)
|
||||
fmt.Printf("激活状态: %v\n", isActive)
|
||||
fmt.Printf("优先级: %d\n", priority)
|
||||
|
||||
// 解析 models
|
||||
var models []map[string]interface{}
|
||||
if err := json.Unmarshal(modelsJSON, &models); err == nil {
|
||||
fmt.Printf("可用模型: ")
|
||||
for i, m := range models {
|
||||
if i > 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("%v", m["id"])
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// 解析 config
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal(configJSON, &config); err == nil {
|
||||
fmt.Printf("Provider 类型: %v\n", config["provider"])
|
||||
fmt.Printf("默认模型: %v\n", config["default_model"])
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
fmt.Println("❌ 数据库中没有任何模型提供商配置")
|
||||
fmt.Println("提示: 运行种子数据脚本来初始化配置")
|
||||
} else {
|
||||
fmt.Printf("✅ 共找到 %d 个提供商配置\n", count)
|
||||
}
|
||||
|
||||
// 查询应用配置中的模型使用情况
|
||||
fmt.Println("\n=== 应用配置中的模型使用情况 ===")
|
||||
|
||||
appRows, err := pool.Query(context.Background(), `
|
||||
SELECT name,
|
||||
COALESCE(app_config->>'model', '未配置') as model,
|
||||
status
|
||||
FROM applications
|
||||
WHERE status = 'approved'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
`)
|
||||
if err != nil {
|
||||
log.Printf("查询应用失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer appRows.Close()
|
||||
|
||||
appCount := 0
|
||||
modelStats := make(map[string]int)
|
||||
|
||||
for appRows.Next() {
|
||||
var name, model, status string
|
||||
if err := appRows.Scan(&name, &model, &status); err != nil {
|
||||
continue
|
||||
}
|
||||
appCount++
|
||||
modelStats[model]++
|
||||
fmt.Printf("应用: %-30s | 模型: %-15s | 状态: %s\n", name, model, status)
|
||||
}
|
||||
|
||||
if appCount > 0 {
|
||||
fmt.Printf("\n✅ 共查询到 %d 个已批准的应用\n\n", appCount)
|
||||
fmt.Println("模型使用统计:")
|
||||
for model, count := range modelStats {
|
||||
fmt.Printf(" - %-15s: %d 个应用\n", model, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载环境变量
|
||||
_ = godotenv.Load("../../.env")
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "postgres://freedak:@localhost:5432/govai_portal?sslmode=disable"
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(context.Background(), dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("无法连接数据库: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
fmt.Println("=== 清空应用硬编码模型配置 ===")
|
||||
fmt.Println("目标:让所有应用自动使用优先级最高的 Provider 的默认模型")
|
||||
|
||||
// 1. 查看当前状态
|
||||
fmt.Println("【步骤1】查看当前应用模型配置...")
|
||||
var totalApps, configuredApps int
|
||||
err = pool.QueryRow(context.Background(), `
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN app_config->>'model' IS NOT NULL AND app_config->>'model' != '' THEN 1 END) as configured
|
||||
FROM applications
|
||||
WHERE status = 'approved'
|
||||
`).Scan(&totalApps, &configuredApps)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" - 已批准应用总数: %d\n", totalApps)
|
||||
fmt.Printf(" - 配置了模型的应用: %d\n", configuredApps)
|
||||
fmt.Printf(" - 未配置模型的应用: %d\n\n", totalApps-configuredApps)
|
||||
|
||||
// 2. 清空所有应用的模型配置
|
||||
fmt.Println("【步骤2】清空所有应用的模型配置...")
|
||||
result, err := pool.Exec(context.Background(), `
|
||||
UPDATE applications
|
||||
SET app_config = app_config - 'model'
|
||||
WHERE status = 'approved'
|
||||
AND app_config ? 'model'
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
rowsAffected := result.RowsAffected()
|
||||
fmt.Printf("✅ 已清空 %d 个应用的模型配置\n\n", rowsAffected)
|
||||
|
||||
// 3. 验证结果
|
||||
fmt.Println("【步骤3】验证结果...")
|
||||
var remainingConfigured int
|
||||
err = pool.QueryRow(context.Background(), `
|
||||
SELECT COUNT(*)
|
||||
FROM applications
|
||||
WHERE status = 'approved'
|
||||
AND app_config->>'model' IS NOT NULL
|
||||
AND app_config->>'model' != ''
|
||||
`).Scan(&remainingConfigured)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("验证查询失败: %v", err)
|
||||
}
|
||||
|
||||
if remainingConfigured == 0 {
|
||||
fmt.Println("✅ 所有应用的模型配置已清空")
|
||||
} else {
|
||||
fmt.Printf("⚠️ 还有 %d 个应用仍有模型配置\n", remainingConfigured)
|
||||
}
|
||||
|
||||
// 4. 显示当前优先级最高的 Provider
|
||||
fmt.Println("\n【步骤4】当前优先级最高的 Provider...")
|
||||
var providerName, defaultModel string
|
||||
var priority int
|
||||
err = pool.QueryRow(context.Background(), `
|
||||
SELECT name, COALESCE(config->>'default_model', '未配置'), priority
|
||||
FROM model_providers
|
||||
WHERE is_active = true
|
||||
ORDER BY priority DESC, created_at
|
||||
LIMIT 1
|
||||
`).Scan(&providerName, &defaultModel, &priority)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("查询 Provider 失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" Provider: %s\n", providerName)
|
||||
fmt.Printf(" 默认模型: %s\n", defaultModel)
|
||||
fmt.Printf(" 优先级: %d\n", priority)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== 配置清理完成 ===")
|
||||
fmt.Println("\n说明:")
|
||||
fmt.Println(" - 现在所有应用将自动使用优先级最高的 Provider 的默认模型")
|
||||
fmt.Println(" - 当前会使用: " + providerName + " / " + defaultModel)
|
||||
fmt.Println(" - 如需特殊应用使用指定模型,可在应用配置中单独设置")
|
||||
}
|
||||
@@ -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,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载环境变量
|
||||
_ = godotenv.Load("../../.env")
|
||||
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
dbURL = "postgres://freedak:@localhost:5432/govai_portal?sslmode=disable"
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(context.Background(), dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("无法连接数据库: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
fmt.Println("=== 开始修复数据库配置 ===")
|
||||
|
||||
// 1. 修复本地模型配置
|
||||
fmt.Println("【任务1】修复本地模型配置...")
|
||||
result, err := pool.Exec(context.Background(), `
|
||||
UPDATE model_providers
|
||||
SET config = '{
|
||||
"provider": "openai",
|
||||
"default_model": "qwen2.5-7b-instruct",
|
||||
"supports_streaming": true,
|
||||
"supports_function_calling": false
|
||||
}'::jsonb,
|
||||
models = '[
|
||||
{"id": "qwen2.5-7b-instruct", "name": "Qwen2.5-7B-Instruct", "type": "chat"}
|
||||
]'::jsonb
|
||||
WHERE name = '本地LLM (Qwen2.5-7B)'
|
||||
`)
|
||||
if err != nil {
|
||||
log.Printf("❌ 修复本地模型失败: %v", err)
|
||||
} else {
|
||||
rowsAffected := result.RowsAffected()
|
||||
if rowsAffected > 0 {
|
||||
fmt.Printf("✅ 成功修复本地模型配置 (影响 %d 行)\n\n", rowsAffected)
|
||||
} else {
|
||||
fmt.Println("⚠️ 未找到本地模型记录")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 为未配置模型的应用设置默认模型
|
||||
fmt.Println("【任务2】为未配置模型的应用设置默认模型...")
|
||||
result, err = pool.Exec(context.Background(), `
|
||||
UPDATE applications
|
||||
SET app_config = jsonb_set(
|
||||
COALESCE(app_config, '{}'::jsonb),
|
||||
'{model}',
|
||||
'"qwen2.5-7b-instruct"'
|
||||
)
|
||||
WHERE (app_config->>'model' IS NULL OR app_config->>'model' = '')
|
||||
AND status = 'approved'
|
||||
`)
|
||||
if err != nil {
|
||||
log.Printf("❌ 更新应用配置失败: %v", err)
|
||||
} else {
|
||||
rowsAffected := result.RowsAffected()
|
||||
if rowsAffected > 0 {
|
||||
fmt.Printf("✅ 成功为 %d 个应用设置默认模型\n\n", rowsAffected)
|
||||
} else {
|
||||
fmt.Println("ℹ️ 所有应用已配置模型")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 验证修复结果
|
||||
fmt.Println("【验证】检查修复结果...")
|
||||
|
||||
var localConfig, localModels string
|
||||
err = pool.QueryRow(context.Background(), `
|
||||
SELECT
|
||||
COALESCE(config::text, 'null') as config,
|
||||
COALESCE(models::text, 'null') as models
|
||||
FROM model_providers
|
||||
WHERE name = '本地LLM (Qwen2.5-7B)'
|
||||
`).Scan(&localConfig, &localModels)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("查询验证失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf("本地模型 config: %s\n", localConfig)
|
||||
fmt.Printf("本地模型 models: %s\n\n", localModels)
|
||||
}
|
||||
|
||||
var unconfiguredCount int
|
||||
err = pool.QueryRow(context.Background(), `
|
||||
SELECT COUNT(*)
|
||||
FROM applications
|
||||
WHERE (app_config->>'model' IS NULL OR app_config->>'model' = '')
|
||||
AND status = 'approved'
|
||||
`).Scan(&unconfiguredCount)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("查询未配置应用数量失败: %v", err)
|
||||
} else {
|
||||
fmt.Printf("未配置模型的应用数量: %d\n", unconfiguredCount)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== 修复完成 ===")
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 连接数据库
|
||||
db, err := sql.Open("postgres", "postgres://freedak:@localhost:5432/govai_portal?sslmode=disable")
|
||||
if err != nil {
|
||||
log.Fatal("连接数据库失败:", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 生成密码哈希
|
||||
password := "admin123"
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatal("生成密码哈希失败:", err)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
result, err := db.Exec(`
|
||||
UPDATE users
|
||||
SET password_hash = $1, updated_at = NOW()
|
||||
WHERE email = $2
|
||||
`, string(hash), "admin@govai.gov.cn")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("更新密码失败:", err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
fmt.Printf("✅ 已重置 admin@govai.gov.cn 的密码为: %s (影响%d行)\n", password, rows)
|
||||
|
||||
// 验证
|
||||
var email, role string
|
||||
err = db.QueryRow("SELECT email, role FROM users WHERE email = $1", "admin@govai.gov.cn").Scan(&email, &role)
|
||||
if err != nil {
|
||||
log.Fatal("查询用户失败:", err)
|
||||
}
|
||||
fmt.Printf("用户: %s, 角色: %s\n", email, role)
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load("../../.env")
|
||||
|
||||
databaseURL := os.Getenv("DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
log.Fatal("DATABASE_URL 环境变量未设置")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
log.Fatalf("连接数据库失败: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// 插入阿里云百炼(通义千问)配置
|
||||
apiKey := os.Getenv("QWEN_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Println("⚠️ QWEN_API_KEY 未设置,跳过初始化千问")
|
||||
return
|
||||
}
|
||||
|
||||
sql := `
|
||||
INSERT INTO model_providers (
|
||||
name,
|
||||
base_url,
|
||||
api_key_encrypted,
|
||||
models,
|
||||
is_active,
|
||||
priority,
|
||||
config
|
||||
) VALUES (
|
||||
'阿里云百炼 (通义千问)',
|
||||
'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
$1,
|
||||
'[
|
||||
{"id": "qwen-plus", "name": "通义千问-Plus", "type": "chat"},
|
||||
{"id": "qwen-turbo", "name": "通义千问-Turbo", "type": "chat"},
|
||||
{"id": "qwen-max", "name": "通义千问-Max", "type": "chat"},
|
||||
{"id": "qwen-long", "name": "通义千问-Long", "type": "chat"}
|
||||
]'::jsonb,
|
||||
true,
|
||||
100,
|
||||
'{
|
||||
"provider": "openai",
|
||||
"default_model": "qwen-plus",
|
||||
"supports_function_calling": true,
|
||||
"supports_streaming": true
|
||||
}'::jsonb
|
||||
)
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
_, err = pool.Exec(ctx, sql, apiKey)
|
||||
if err != nil {
|
||||
log.Fatalf("插入数据失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ 成功初始化模型提供商数据:阿里云百炼 (通义千问)")
|
||||
}
|
||||
@@ -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,268 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/config"
|
||||
"github.com/enterprise-ai-platform/server/internal/handler"
|
||||
"github.com/enterprise-ai-platform/server/internal/metrics"
|
||||
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"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
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()
|
||||
llmMgr.SetPool(pool)
|
||||
|
||||
// 优先从数据库加载 providers(如果失败,回退到环境变量配置)
|
||||
if err := llmMgr.LoadProvidersFromDB(context.Background()); err != nil {
|
||||
// 回退:使用环境变量配置
|
||||
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")
|
||||
// Prometheus metrics middleware
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.HTTPRequestsTotal.WithLabelValues(r.Method, r.URL.Path, "200").Inc()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
// Health check and metrics endpoints
|
||||
healthH := handler.NewHealthHandler(pool, rdb)
|
||||
r.Get("/health", healthH.HealthCheck)
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
// 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.Post("/suggestions", chatH.GetSuggestions)
|
||||
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.Post("/providers/{id}/test", platformH.TestProvider)
|
||||
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, "接口开发中")
|
||||
}
|
||||
Reference in New Issue
Block a user