update: 2026-03-28 20:59
This commit is contained in:
9
infogenie-backend-go/.dockerignore
Normal file
9
infogenie-backend-go/.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.env
|
||||
.env.*
|
||||
!.env.production.example
|
||||
docker-compose.yml
|
||||
**/tmp/
|
||||
**/*_test.go
|
||||
30
infogenie-backend-go/.env.development
Normal file
30
infogenie-backend-go/.env.development
Normal file
@@ -0,0 +1,30 @@
|
||||
# InfoGenie Go Backend - 开发环境配置
|
||||
APP_ENV=development
|
||||
APP_PORT=5002
|
||||
|
||||
# MySQL 测试数据库
|
||||
DB_HOST=10.1.1.100
|
||||
DB_PORT=3306
|
||||
DB_NAME=infogenie-test
|
||||
DB_USER=infogenie-test
|
||||
DB_PASSWORD=infogenie-test
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8
|
||||
JWT_EXPIRE_DAYS=7
|
||||
|
||||
# 邮件服务
|
||||
MAIL_HOST=smtp.qiye.aliyun.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=notice@smyhub.com
|
||||
MAIL_PASSWORD=tyh@19900420
|
||||
|
||||
# AI 配置文件路径
|
||||
AI_CONFIG_PATH=ai_config.json
|
||||
|
||||
# 萌芽账户认证中心
|
||||
AUTH_CENTER_API_URL=https://auth.api.shumengya.top
|
||||
AUTH_CENTER_ADMIN_TOKEN=
|
||||
|
||||
# 站点前台配置(与前端管理员口令一致,用于保存 60s 功能展示开关)
|
||||
INFOGENIE_SITE_ADMIN_TOKEN=shumengya520
|
||||
3
infogenie-backend-go/.gitignore
vendored
Normal file
3
infogenie-backend-go/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# 含数据库密码、SMTP 等,勿提交
|
||||
.env.production
|
||||
.env.local
|
||||
20
infogenie-backend-go/Dockerfile
Normal file
20
infogenie-backend-go/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# 多阶段构建:生产镜像仅含二进制与 ai_config.json,敏感配置通过运行时环境变量或 env_file 注入(勿将 .env.production 打入镜像)
|
||||
FROM golang:1.24-alpine AS builder
|
||||
WORKDIR /src
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
ENV GOTOOLCHAIN=auto
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/server ./cmd/server
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/server .
|
||||
COPY ai_config.json ./
|
||||
EXPOSE 5002
|
||||
ENV APP_ENV=production
|
||||
ENV APP_PORT=5002
|
||||
CMD ["./server"]
|
||||
0
infogenie-backend-go/README.md
Normal file
0
infogenie-backend-go/README.md
Normal file
13
infogenie-backend-go/ai_config.json
Normal file
13
infogenie-backend-go/ai_config.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"deepseek": {
|
||||
"api_key": "sk-832f8e5250464de08a31523c7fd712",
|
||||
"api_base": "https://api.deepseek.com",
|
||||
"model": ["deepseek-chat","deepseek-reasoner"]
|
||||
},
|
||||
|
||||
"kimi": {
|
||||
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja",
|
||||
"api_base": "https://api.moonshot.cn",
|
||||
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
|
||||
}
|
||||
}
|
||||
43
infogenie-backend-go/cmd/server/main.go
Normal file
43
infogenie-backend-go/cmd/server/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/router"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("配置加载失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("InfoGenie Go Backend 启动中... [环境: %s, DB: %s:%s/%s]", cfg.Env, cfg.DB.Host, cfg.DB.Port, cfg.DB.Name)
|
||||
|
||||
if err := database.Init(cfg.DB); err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
|
||||
if err := database.AutoMigrate(); err != nil {
|
||||
log.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
log.Println("数据库表自动迁移完成")
|
||||
|
||||
if cfg.Env == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
router.Setup(r)
|
||||
|
||||
addr := fmt.Sprintf("0.0.0.0:%s", cfg.Port)
|
||||
log.Printf("🚀 InfoGenie 后端服务已启动: http://localhost:%s", cfg.Port)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("服务启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
BIN
infogenie-backend-go/cmd/server/server.exe
Normal file
BIN
infogenie-backend-go/cmd/server/server.exe
Normal file
Binary file not shown.
233
infogenie-backend-go/config/config.go
Normal file
233
infogenie-backend-go/config/config.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Env string
|
||||
Port string
|
||||
|
||||
DB DBConfig
|
||||
Mail MailConfig
|
||||
AI AIConfig
|
||||
AuthCenter AuthCenterConfig
|
||||
// SiteAdminToken 与前端管理员口令一致,用于更新站点展示配置(如 60s 功能开关);为空则禁止写入
|
||||
SiteAdminToken string
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
Name string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (d DBConfig) DSN() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s",
|
||||
d.User, d.Password, d.Host, d.Port, d.Name)
|
||||
}
|
||||
|
||||
type MailConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type AuthCenterConfig struct {
|
||||
APIURL string
|
||||
AdminToken string
|
||||
}
|
||||
|
||||
type AIProviderConfig struct {
|
||||
APIKey string `json:"api_key"`
|
||||
APIBase string `json:"api_base"`
|
||||
Models []string `json:"model"`
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
Providers map[string]AIProviderConfig
|
||||
}
|
||||
|
||||
var Cfg *AppConfig
|
||||
|
||||
const (
|
||||
envDevelopment = "development"
|
||||
envProduction = "production"
|
||||
|
||||
defaultDevDBHost = "10.1.1.100"
|
||||
defaultDevDBPort = "3306"
|
||||
defaultDevDBName = "infogenie-test"
|
||||
defaultDevDBUser = "infogenie-test"
|
||||
defaultDevDBPassword = "infogenie-test"
|
||||
)
|
||||
|
||||
func Load() (*AppConfig, error) {
|
||||
env := normalizeEnv(os.Getenv("APP_ENV"))
|
||||
if env != envDevelopment && env != envProduction {
|
||||
return nil, fmt.Errorf("不支持的APP_ENV: %s", env)
|
||||
}
|
||||
|
||||
if err := loadEnvFile(env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mailPort, _ := strconv.Atoi(getEnv("MAIL_PORT", "465"))
|
||||
|
||||
dbHost, err := getEnvByEnvironment(env, "DB_HOST", defaultDevDBHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbPort, err := getEnvByEnvironment(env, "DB_PORT", defaultDevDBPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbName, err := getEnvByEnvironment(env, "DB_NAME", defaultDevDBName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbUser, err := getEnvByEnvironment(env, "DB_USER", defaultDevDBUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbPassword, err := getEnvByEnvironment(env, "DB_PASSWORD", defaultDevDBPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &AppConfig{
|
||||
Env: env,
|
||||
Port: getEnv("APP_PORT", "5002"),
|
||||
DB: DBConfig{
|
||||
Host: dbHost,
|
||||
Port: dbPort,
|
||||
Name: dbName,
|
||||
User: dbUser,
|
||||
Password: dbPassword,
|
||||
},
|
||||
Mail: MailConfig{
|
||||
Host: getEnv("MAIL_HOST", "smtp.qq.com"),
|
||||
Port: mailPort,
|
||||
Username: getEnv("MAIL_USERNAME", ""),
|
||||
Password: getEnv("MAIL_PASSWORD", ""),
|
||||
},
|
||||
AuthCenter: AuthCenterConfig{
|
||||
APIURL: getEnv("AUTH_CENTER_API_URL", "https://auth.api.shumengya.top"),
|
||||
AdminToken: getEnv("AUTH_CENTER_ADMIN_TOKEN", ""),
|
||||
},
|
||||
SiteAdminToken: getEnv("INFOGENIE_SITE_ADMIN_TOKEN", ""),
|
||||
}
|
||||
|
||||
if err := validateDBConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// AI配置现在完全从数据库读取,不再加载ai_config.json文件
|
||||
cfg.AI = AIConfig{Providers: make(map[string]AIProviderConfig)}
|
||||
|
||||
Cfg = cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadEnvFile(env string) error {
|
||||
envFile := fmt.Sprintf(".env.%s", env)
|
||||
if _, err := os.Stat(envFile); err == nil {
|
||||
return godotenv.Load(envFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDBConfig(cfg *AppConfig) error {
|
||||
switch cfg.Env {
|
||||
case envDevelopment:
|
||||
if !isDevelopmentDBTarget(cfg.DB) {
|
||||
return fmt.Errorf("开发环境必须使用测试数据库: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||||
}
|
||||
if looksProductionLike(cfg.DB) {
|
||||
return fmt.Errorf("开发环境检测到生产数据库配置: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||||
}
|
||||
case envProduction:
|
||||
missing := make([]string, 0, 4)
|
||||
if strings.TrimSpace(cfg.DB.Host) == "" {
|
||||
missing = append(missing, "DB_HOST")
|
||||
}
|
||||
if strings.TrimSpace(cfg.DB.Port) == "" {
|
||||
missing = append(missing, "DB_PORT")
|
||||
}
|
||||
if strings.TrimSpace(cfg.DB.Name) == "" {
|
||||
missing = append(missing, "DB_NAME")
|
||||
}
|
||||
if strings.TrimSpace(cfg.DB.User) == "" {
|
||||
missing = append(missing, "DB_USER")
|
||||
}
|
||||
if strings.TrimSpace(cfg.DB.Password) == "" {
|
||||
missing = append(missing, "DB_PASSWORD")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("生产环境缺少必需数据库配置: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
if isDevelopmentDBTarget(cfg.DB) {
|
||||
return fmt.Errorf("生产环境数据库配置看起来像开发/测试环境: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的APP_ENV: %s", cfg.Env)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDevelopmentDBTarget(db DBConfig) bool {
|
||||
host := strings.ToLower(strings.TrimSpace(db.Host))
|
||||
name := strings.ToLower(strings.TrimSpace(db.Name))
|
||||
|
||||
if host == "localhost" || strings.HasPrefix(host, "127.") || strings.HasPrefix(host, "10.1.1.") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(host, "dev") || strings.Contains(host, "test") || strings.Contains(host, "local") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(name, "test") || strings.Contains(name, "dev") || strings.Contains(name, "local") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func looksProductionLike(db DBConfig) bool {
|
||||
host := strings.ToLower(strings.TrimSpace(db.Host))
|
||||
name := strings.ToLower(strings.TrimSpace(db.Name))
|
||||
return strings.Contains(host, "bigmengya") || strings.Contains(host, "shumengya.top") || strings.Contains(name, "prod") || strings.Contains(name, "production")
|
||||
}
|
||||
|
||||
func normalizeEnv(raw string) string {
|
||||
env := strings.ToLower(strings.TrimSpace(raw))
|
||||
if env == "" {
|
||||
return envDevelopment
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvByEnvironment(env, key, devFallback string) (string, error) {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
if env == envProduction {
|
||||
return "", fmt.Errorf("生产环境缺少必需配置: %s", key)
|
||||
}
|
||||
|
||||
return devFallback, nil
|
||||
}
|
||||
16
infogenie-backend-go/docker-compose.yml
Normal file
16
infogenie-backend-go/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# 生产:宿主机 12364 -> 容器内 5002;容器名 infogenie-backend-go
|
||||
# 使用:在同级目录准备 .env.production(数据库等),然后 docker compose up -d --build
|
||||
services:
|
||||
infogenie-backend-go:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: infogenie-backend-go:latest
|
||||
container_name: infogenie-backend-go
|
||||
ports:
|
||||
- "12364:5002"
|
||||
env_file:
|
||||
- .env.production
|
||||
environment:
|
||||
APP_ENV: production
|
||||
restart: unless-stopped
|
||||
45
infogenie-backend-go/go.mod
Normal file
45
infogenie-backend-go/go.mod
Normal file
@@ -0,0 +1,45 @@
|
||||
module infogenie-backend
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/cors v1.7.6 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.12.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gorm.io/driver/mysql v1.6.0 // indirect
|
||||
gorm.io/gorm v1.31.1 // indirect
|
||||
)
|
||||
96
infogenie-backend-go/go.sum
Normal file
96
infogenie-backend-go/go.sum
Normal file
@@ -0,0 +1,96 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
57
infogenie-backend-go/internal/database/mysql.go
Normal file
57
infogenie-backend-go/internal/database/mysql.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Init(cfg config.DBConfig) error {
|
||||
logLevel := logger.Warn
|
||||
if config.Cfg.Env == "development" {
|
||||
logLevel = logger.Info
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(cfg.DSN()), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logLevel),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接MySQL失败: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取底层DB连接失败: %w", err)
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return fmt.Errorf("Ping数据库失败: %w", err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
log.Printf("MySQL连接成功 [env=%s]: %s:%s/%s", config.Cfg.Env, cfg.Host, cfg.Port, cfg.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func AutoMigrate() error {
|
||||
return DB.AutoMigrate(
|
||||
&model.AIConfig{},
|
||||
&model.Site60sDisabled{},
|
||||
&model.SiteAIRuntime{},
|
||||
&model.Site60sUpstream{},
|
||||
&model.SiteAIModelDisabled{},
|
||||
)
|
||||
}
|
||||
109
infogenie-backend-go/internal/handler/ai_runtime.go
Normal file
109
infogenie-backend-go/internal/handler/ai_runtime.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
type AIRuntimeHandler struct{}
|
||||
|
||||
func NewAIRuntimeHandler() *AIRuntimeHandler { return &AIRuntimeHandler{} }
|
||||
|
||||
func maskAPIKey(k string) (set bool, hint string) {
|
||||
k = strings.TrimSpace(k)
|
||||
if k == "" {
|
||||
return false, ""
|
||||
}
|
||||
if len(k) <= 4 {
|
||||
return true, "****"
|
||||
}
|
||||
return true, "****" + k[len(k)-4:]
|
||||
}
|
||||
|
||||
// GetAIRuntime 管理员读取当前 AI 上游配置(密钥脱敏)
|
||||
func (h *AIRuntimeHandler) GetAIRuntime(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "admin_not_configured"})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
|
||||
var row model.SiteAIRuntime
|
||||
_ = database.DB.First(&row, 1).Error
|
||||
keySet, keyHint := maskAPIKey(row.APIKey)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"api_base": strings.TrimSpace(row.APIBase),
|
||||
"api_key_set": keySet,
|
||||
"api_key_hint": keyHint,
|
||||
"default_model": strings.TrimSpace(row.DefaultModel),
|
||||
"default_provider": strings.TrimSpace(row.DefaultProv),
|
||||
})
|
||||
}
|
||||
|
||||
type putAIRuntimeBody struct {
|
||||
APIBase string `json:"api_base"`
|
||||
APIKey string `json:"api_key"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
DefaultProvider string `json:"default_provider"`
|
||||
}
|
||||
|
||||
// PutAIRuntime 管理员写入;api_key 留空或不传则保留原密钥
|
||||
func (h *AIRuntimeHandler) PutAIRuntime(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "admin_not_configured"})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
|
||||
var body putAIRuntimeBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
|
||||
var row model.SiteAIRuntime
|
||||
err := database.DB.First(&row, 1).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
row = model.SiteAIRuntime{ID: 1}
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
row.APIBase = strings.TrimSpace(body.APIBase)
|
||||
if strings.TrimSpace(body.DefaultModel) != "" {
|
||||
row.DefaultModel = strings.TrimSpace(body.DefaultModel)
|
||||
}
|
||||
if strings.TrimSpace(body.DefaultProvider) != "" {
|
||||
row.DefaultProv = strings.TrimSpace(body.DefaultProvider)
|
||||
} else if row.DefaultProv == "" {
|
||||
row.DefaultProv = "deepseek"
|
||||
}
|
||||
|
||||
newKey := strings.TrimSpace(body.APIKey)
|
||||
if newKey != "" && !strings.Contains(newKey, "****") {
|
||||
row.APIKey = newKey
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&row).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
625
infogenie-backend-go/internal/handler/aimodel.go
Normal file
625
infogenie-backend-go/internal/handler/aimodel.go
Normal file
@@ -0,0 +1,625 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/service"
|
||||
)
|
||||
|
||||
type AIModelHandler struct{}
|
||||
|
||||
func NewAIModelHandler() *AIModelHandler { return &AIModelHandler{} }
|
||||
|
||||
// 输入长度限制
|
||||
const (
|
||||
maxInputLen = 5000
|
||||
maxChatMsgCount = 20
|
||||
)
|
||||
|
||||
// 允许用户选择的模型白名单
|
||||
var allowedModels = map[string]map[string]bool{
|
||||
"deepseek": {
|
||||
"deepseek-chat": true,
|
||||
"deepseek-reasoner": true,
|
||||
},
|
||||
"kimi": {
|
||||
"kimi-k2-0905-preview": true,
|
||||
"kimi-k2-0711-preview": true,
|
||||
},
|
||||
}
|
||||
|
||||
func safeAIError(err error) string {
|
||||
log.Printf("AI调用失败: %v", err)
|
||||
return "AI 服务暂时不可用,请稍后重试"
|
||||
}
|
||||
|
||||
func validateTextLen(text string, label string) (string, error) {
|
||||
t := strings.TrimSpace(text)
|
||||
if t == "" {
|
||||
return "", fmt.Errorf("%s不能为空", label)
|
||||
}
|
||||
if len(t) > maxInputLen {
|
||||
return "", fmt.Errorf("%s超出长度限制(最大 %d 字符)", label, maxInputLen)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/chat
|
||||
func (h *AIModelHandler) Chat(c *gin.Context) {
|
||||
var req struct {
|
||||
Messages []service.ChatMessage `json:"messages"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求数据为空"})
|
||||
return
|
||||
}
|
||||
if req.Provider == "" {
|
||||
req.Provider = "deepseek"
|
||||
}
|
||||
if req.Model == "" {
|
||||
req.Model = "deepseek-chat"
|
||||
}
|
||||
|
||||
// 模型白名单校验
|
||||
if models, ok := allowedModels[req.Provider]; !ok || !models[req.Model] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的模型"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Messages) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "消息内容不能为空"})
|
||||
return
|
||||
}
|
||||
if len(req.Messages) > maxChatMsgCount {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("消息数量不能超过 %d 条", maxChatMsgCount)})
|
||||
return
|
||||
}
|
||||
// 校验每条消息的长度
|
||||
for _, m := range req.Messages {
|
||||
if len(m.Content) > maxInputLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("单条消息长度不能超过 %d 字符", maxInputLen)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
content, err := service.CallAI(req.Provider, req.Model, req.Messages)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"content": content,
|
||||
"provider": req.Provider,
|
||||
"model": req.Model,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/name-analysis
|
||||
func (h *AIModelHandler) NameAnalysis(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "姓名不能为空"})
|
||||
return
|
||||
}
|
||||
name, err := validateTextLen(req.Name, "姓名")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位专业的姓名学专家和语言学家,请对输入的姓名进行全面分析。请直接输出分析结果,不要包含任何思考过程或<think>标签。
|
||||
|
||||
姓名:%s
|
||||
|
||||
请按照以下格式严格输出分析结果:
|
||||
|
||||
【稀有度评分】
|
||||
评分:X%%
|
||||
评价:[对稀有度的详细说明,包括姓氏和名字的常见程度分析]
|
||||
|
||||
【音韵评价】
|
||||
评分:X%%
|
||||
评价:[对音韵美感的分析,包括声调搭配、读音流畅度、音律和谐度等]
|
||||
|
||||
【含义解读】
|
||||
[详细分析姓名的寓意内涵,包括:
|
||||
1. 姓氏的历史渊源和文化背景
|
||||
2. 名字各字的含义和象征
|
||||
3. 整体姓名的寓意组合
|
||||
4. 可能体现的父母期望或文化内涵
|
||||
5. 与传统文化、诗词典故的关联等]
|
||||
|
||||
要求:
|
||||
1. 评分必须是1-100的整数百分比,要有明显区分度,避免雷同
|
||||
2. 分析要专业、客观、有依据,评分要根据实际情况有所差异
|
||||
3. 含义解读要详细深入,至少150字
|
||||
4. 严格按照上述格式输出,不要添加思考过程、<think>标签或其他内容
|
||||
5. 如果是生僻字或罕见姓名,要特别说明
|
||||
6. 直接输出最终结果,不要显示推理过程`, name)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"analysis": content,
|
||||
"name": name,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/variable-naming
|
||||
func (h *AIModelHandler) VariableNaming(c *gin.Context) {
|
||||
var req struct {
|
||||
Description string `json:"description"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "变量描述不能为空"})
|
||||
return
|
||||
}
|
||||
desc, err := validateTextLen(req.Description, "变量描述")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
lang := req.Language
|
||||
if lang == "" {
|
||||
lang = "javascript"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一个专业的变量命名助手。请根据以下描述为变量生成合适的名称:
|
||||
|
||||
描述:%s
|
||||
|
||||
请为每种命名规范生成3个变量名建议:
|
||||
1. camelCase (驼峰命名法)
|
||||
2. PascalCase (帕斯卡命名法)
|
||||
3. snake_case (下划线命名法)
|
||||
4. kebab-case (短横线命名法)
|
||||
5. CONSTANT_CASE (常量命名法)
|
||||
|
||||
请按JSON格式返回:
|
||||
{"suggestions":{"camelCase":[{"name":"变量名","description":"说明"}],"PascalCase":[{"name":"变量名","description":"说明"}],"snake_case":[{"name":"变量名","description":"说明"}],"kebab-case":[{"name":"变量名","description":"说明"}],"CONSTANT_CASE":[{"name":"变量名","description":"说明"}]}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。`, desc)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"suggestions": extractOrRaw(content),
|
||||
"description": desc,
|
||||
"language": lang,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/poetry
|
||||
func (h *AIModelHandler) Poetry(c *gin.Context) {
|
||||
var req struct {
|
||||
Theme string `json:"theme"`
|
||||
Style string `json:"style"`
|
||||
Mood string `json:"mood"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "诗歌主题不能为空"})
|
||||
return
|
||||
}
|
||||
theme, err := validateTextLen(req.Theme, "诗歌主题")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
style := req.Style
|
||||
if style == "" {
|
||||
style = "现代诗"
|
||||
}
|
||||
mood := req.Mood
|
||||
if mood == "" {
|
||||
mood = "自由发挥"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位才华横溢的诗人,请根据以下要求创作一首诗歌。
|
||||
|
||||
主题:%s
|
||||
风格:%s
|
||||
情感基调:%s
|
||||
|
||||
创作要求:
|
||||
1. 紧扣主题,情感真挚
|
||||
2. 语言优美,意境深远
|
||||
3. 符合指定的诗歌风格
|
||||
4. 长度适中,朗朗上口
|
||||
5. 如果是古体诗,注意平仄和韵律
|
||||
|
||||
请直接输出诗歌作品,不需要额外的解释或分析。`, theme, style, mood)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"poem": content,
|
||||
"theme": theme,
|
||||
"style": style,
|
||||
"mood": mood,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/translation
|
||||
func (h *AIModelHandler) Translation(c *gin.Context) {
|
||||
var req struct {
|
||||
SourceText string `json:"source_text"`
|
||||
TargetLanguage string `json:"target_language"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "翻译内容不能为空"})
|
||||
return
|
||||
}
|
||||
text, err := validateTextLen(req.SourceText, "翻译内容")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
targetLang := req.TargetLanguage
|
||||
if targetLang == "" {
|
||||
targetLang = "zh-CN"
|
||||
}
|
||||
|
||||
langMap := map[string]string{
|
||||
"zh-CN": "中文(简体)", "zh-TW": "中文(繁体)", "en": "英语",
|
||||
"ja": "日语", "ko": "韩语", "fr": "法语", "de": "德语",
|
||||
"es": "西班牙语", "it": "意大利语", "pt": "葡萄牙语",
|
||||
"ru": "俄语", "ar": "阿拉伯语", "hi": "印地语",
|
||||
"th": "泰语", "vi": "越南语",
|
||||
}
|
||||
langName := langMap[targetLang]
|
||||
if langName == "" {
|
||||
langName = targetLang
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位专业的翻译专家,精通多种语言的翻译工作。请将以下文本翻译成%s。
|
||||
|
||||
原文:%s
|
||||
|
||||
翻译要求:
|
||||
1. 【信】- 忠实原文,准确传达原意
|
||||
2. 【达】- 译文通顺流畅,符合目标语言的表达习惯
|
||||
3. 【雅】- 用词优美得体,风格与原文相符
|
||||
|
||||
请按以下JSON格式返回翻译结果:
|
||||
{"detected_language":"检测到的源语言","target_language":"%s","translation":"翻译结果","alternative_translations":["备选翻译1","备选翻译2"],"explanation":"翻译说明","pronunciation":"发音指导"}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。`, langName, text, langName)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"translation_result": content,
|
||||
"source_text": text,
|
||||
"target_language": targetLang,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/classical_conversion
|
||||
func (h *AIModelHandler) ClassicalConversion(c *gin.Context) {
|
||||
var req struct {
|
||||
ModernText string `json:"modern_text"`
|
||||
Style string `json:"style"`
|
||||
ArticleType string `json:"article_type"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "现代文内容不能为空"})
|
||||
return
|
||||
}
|
||||
text, err := validateTextLen(req.ModernText, "现代文内容")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
style := req.Style
|
||||
if style == "" {
|
||||
style = "古雅"
|
||||
}
|
||||
artType := req.ArticleType
|
||||
if artType == "" {
|
||||
artType = "散文"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位精通古代文言文的文学大师,擅长将现代文转换为优美的文言文。请将以下现代文转换为文言文。
|
||||
|
||||
现代文:%s
|
||||
风格:%s
|
||||
文体:%s
|
||||
|
||||
请按以下JSON格式返回转换结果:
|
||||
{"classical_text":"转换后的文言文","translation_notes":"转换说明","style_analysis":"风格分析","difficulty_level":"难度等级","key_phrases":[{"modern":"现代词汇","classical":"文言文词汇","explanation":"转换说明"}],"cultural_elements":"文化内涵说明"}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。`, text, style, artType)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"conversion_result": content,
|
||||
"modern_text": text,
|
||||
"style": style,
|
||||
"article_type": artType,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/expression-maker
|
||||
func (h *AIModelHandler) ExpressionMaker(c *gin.Context) {
|
||||
var req struct {
|
||||
Text string `json:"text"`
|
||||
Style string `json:"style"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "文字内容不能为空"})
|
||||
return
|
||||
}
|
||||
text, err := validateTextLen(req.Text, "文字内容")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
style := req.Style
|
||||
if style == "" {
|
||||
style = "mixed"
|
||||
}
|
||||
|
||||
styleMap := map[string]string{
|
||||
"mixed": "混合使用Emoji表情和颜文字", "emoji": "仅使用Emoji表情符号",
|
||||
"kaomoji": "仅使用颜文字", "cute": "使用可爱风格的表情符号",
|
||||
"cool": "使用酷炫风格的表情符号",
|
||||
}
|
||||
styleDesc := styleMap[style]
|
||||
if styleDesc == "" {
|
||||
styleDesc = styleMap["mixed"]
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一个专业的表情符号专家。请根据以下文字内容生成相应的表情符号:
|
||||
|
||||
文字内容:%s
|
||||
表情风格:%s
|
||||
|
||||
请按JSON格式返回:
|
||||
{"expressions":{"emoji":[{"symbol":"😊","description":"场景说明","intensity":"中等","usage":"使用建议"}],"kaomoji":[{"symbol":"(^_^)","description":"场景说明","intensity":"轻微","usage":"使用建议"}],"combination":[{"symbol":"🎉✨","description":"场景说明","intensity":"强烈","usage":"使用建议"}]},"summary":{"emotion_analysis":"情感分析","recommended_usage":"推荐使用场景","style_notes":"风格特点"}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。`, text, styleDesc)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"expressions": extractOrRaw(content),
|
||||
"text": text,
|
||||
"style": style,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/linux-command
|
||||
func (h *AIModelHandler) LinuxCommand(c *gin.Context) {
|
||||
var req struct {
|
||||
TaskDescription string `json:"task_description"`
|
||||
DifficultyLevel string `json:"difficulty_level"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "任务描述不能为空"})
|
||||
return
|
||||
}
|
||||
desc, err := validateTextLen(req.TaskDescription, "任务描述")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
level := req.DifficultyLevel
|
||||
if level == "" {
|
||||
level = "beginner"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位Linux系统专家,请根据用户的任务描述生成相应的Linux命令。
|
||||
|
||||
任务描述:%s
|
||||
用户水平:%s
|
||||
|
||||
请按JSON格式返回:
|
||||
{"commands":[{"command":"具体命令","description":"说明","safety_level":"safe","explanation":"解释","example_output":"示例输出","alternatives":["替代命令"]}],"safety_warnings":["安全提示"],"prerequisites":["前置条件"],"related_concepts":["相关概念"]}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。`, desc, level)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"command_result": content,
|
||||
"task_description": desc,
|
||||
"difficulty_level": level,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/markdown_formatting
|
||||
func (h *AIModelHandler) MarkdownFormatting(c *gin.Context) {
|
||||
var req struct {
|
||||
ArticleText string `json:"article_text"`
|
||||
EmojiStyle string `json:"emoji_style"`
|
||||
MarkdownOption string `json:"markdown_option"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "文章内容不能为空"})
|
||||
return
|
||||
}
|
||||
text, err := validateTextLen(req.ArticleText, "文章内容")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
emojiStyle := req.EmojiStyle
|
||||
if emojiStyle == "" {
|
||||
emojiStyle = "balanced"
|
||||
}
|
||||
mdOption := req.MarkdownOption
|
||||
if mdOption == "" {
|
||||
mdOption = "standard"
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位专业的文档排版助手。请将用户提供的全文按"标准Markdown格式"进行排版,并在不改变任何原文内容的前提下进行结构化呈现。严格遵守以下规则:
|
||||
|
||||
1) 保留所有原始内容,严禁改写、删减或添加新内容。
|
||||
2) 使用合理的Markdown结构(标题、分节、段落、列表、引用、表格如有必要、代码块仅当原文包含)。
|
||||
3) 智能添加适量Emoji以增强可读性(%s),在标题、关键句、列表项等处点缀;避免过度使用,保持专业。
|
||||
4) 保持语言与语气不变,只优化排版和表现形式。
|
||||
5) 输出"纯Markdown文本",不要包含任何JSON、HTML、XML、解释文字、或代码块围栏标记。
|
||||
|
||||
如果原文本较长,可在开头自动生成简洁的"目录"以便阅读。
|
||||
|
||||
原文如下:
|
||||
%s`, emojiStyle, text)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"formatted_markdown": content,
|
||||
"source_text": text,
|
||||
"emoji_style": emojiStyle,
|
||||
"markdown_option": mdOption,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/aimodelapp/kinship-calculator
|
||||
func (h *AIModelHandler) KinshipCalculator(c *gin.Context) {
|
||||
var req struct {
|
||||
RelationChain string `json:"relation_chain"`
|
||||
Dialects []string `json:"dialects"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "亲属关系链不能为空"})
|
||||
return
|
||||
}
|
||||
chain, err := validateTextLen(req.RelationChain, "亲属关系链")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
dialects := req.Dialects
|
||||
if len(dialects) == 0 {
|
||||
dialects = []string{"粤语", "闽南语", "上海话", "四川话", "东北话", "客家话"}
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`你是一位中国亲属称呼专家。请解析下面的亲属关系链,给出最终的亲属称呼。
|
||||
|
||||
请遵循:
|
||||
1) 以中国大陆通行的标准普通话称呼为准。
|
||||
2) 同时给出若干方言的对应称呼:%s。
|
||||
3) 如存在地区差异或性别歧义,请在notes中说明。
|
||||
4) 不要展示推理过程;只输出JSON。
|
||||
|
||||
严格按以下JSON结构输出:
|
||||
{"mandarin_title":"标准普通话称呼","dialect_titles":{"粤语":{"title":"称呼","romanization":"粤拼","notes":"说明"}},"notes":"总体说明"}
|
||||
|
||||
关系链:%s`, strings.Join(dialects, "、"), chain)
|
||||
|
||||
messages := []service.ChatMessage{{Role: "user", Content: prompt}}
|
||||
content, err := service.CallDeepSeek(messages, "", 3)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": safeAIError(err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"kinship_result": content,
|
||||
"relation_chain": chain,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/aimodelapp/models — 仅返回允许的模型列表
|
||||
func (h *AIModelHandler) GetModels(c *gin.Context) {
|
||||
models := make(map[string][]string)
|
||||
for provider, modelSet := range allowedModels {
|
||||
for m := range modelSet {
|
||||
models[provider] = append(models[provider], m)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"models": models,
|
||||
"default_provider": "deepseek",
|
||||
"default_model": "deepseek-chat",
|
||||
})
|
||||
}
|
||||
|
||||
func extractOrRaw(content string) interface{} {
|
||||
return content
|
||||
}
|
||||
|
||||
// Ping 用于健康检查(从 config 引用原始配置可选)
|
||||
func Ping(c *gin.Context) {
|
||||
_ = config.Cfg
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "running",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
30
infogenie-backend-go/internal/handler/auth.go
Normal file
30
infogenie-backend-go/internal/handler/auth.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct{}
|
||||
|
||||
func NewAuthHandler() *AuthHandler { return &AuthHandler{} }
|
||||
|
||||
func (h *AuthHandler) Check(c *gin.Context) {
|
||||
account, exists := c.Get("account")
|
||||
if !exists || account == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "logged_in": false})
|
||||
return
|
||||
}
|
||||
username, _ := c.Get("username")
|
||||
email, _ := c.Get("email")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"logged_in": true,
|
||||
"user": gin.H{
|
||||
"account": account,
|
||||
"username": username,
|
||||
"email": email,
|
||||
},
|
||||
})
|
||||
}
|
||||
265
infogenie-backend-go/internal/handler/siteconfig.go
Normal file
265
infogenie-backend-go/internal/handler/siteconfig.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
type SiteConfigHandler struct{}
|
||||
|
||||
func NewSiteConfigHandler() *SiteConfigHandler { return &SiteConfigHandler{} }
|
||||
|
||||
func siteAdminTokenOK(headerToken string) bool {
|
||||
if config.Cfg == nil {
|
||||
return false
|
||||
}
|
||||
expected := strings.TrimSpace(config.Cfg.SiteAdminToken)
|
||||
if expected == "" {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(headerToken), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
// Get60sDisabled 公开:返回当前隐藏的 60s 功能 id 列表(与前端 item.id 对应)
|
||||
func (h *SiteConfigHandler) Get60sDisabled(c *gin.Context) {
|
||||
var rows []model.Site60sDisabled
|
||||
if err := database.DB.Order("feature_id").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.FeatureID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"disabled": ids})
|
||||
}
|
||||
|
||||
type put60sDisabledBody struct {
|
||||
Disabled []string `json:"disabled"`
|
||||
}
|
||||
|
||||
// Put60sDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致(建议与前端管理员口令相同)
|
||||
func (h *SiteConfigHandler) Put60sDisabled(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "admin_not_configured",
|
||||
"message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"})
|
||||
return
|
||||
}
|
||||
|
||||
var body put60sDisabledBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
if len(body.Disabled) > 512 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
clean := make([]string, 0, len(body.Disabled))
|
||||
for _, raw := range body.Disabled {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" || len(id) > 96 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.Site60sDisabled{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
for _, id := range clean {
|
||||
if err := tx.Create(&model.Site60sDisabled{FeatureID: id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)})
|
||||
}
|
||||
|
||||
// —— 60s API 上游节点(仅管理员可切换)——
|
||||
|
||||
type sixtySrcInfo struct {
|
||||
Base string
|
||||
Label string
|
||||
}
|
||||
|
||||
var sixtyUpstreamRegistry = map[string]sixtySrcInfo{
|
||||
"self": {Base: "https://60s.api.shumengya.top", Label: "萌芽节点"},
|
||||
"official": {Base: "https://60s.viki.moe", Label: "官方节点"},
|
||||
}
|
||||
|
||||
func resolve60sUpstream(sourceID string) (id string, info sixtySrcInfo) {
|
||||
id = strings.TrimSpace(sourceID)
|
||||
if id == "" {
|
||||
id = "self"
|
||||
}
|
||||
var ok bool
|
||||
info, ok = sixtyUpstreamRegistry[id]
|
||||
if !ok {
|
||||
id = "self"
|
||||
info = sixtyUpstreamRegistry["self"]
|
||||
}
|
||||
return id, info
|
||||
}
|
||||
|
||||
// Get60sSource 公开:当前站点使用的 60s 上游 base_url(供静态页 iframe 传参)
|
||||
func (h *SiteConfigHandler) Get60sSource(c *gin.Context) {
|
||||
var row model.Site60sUpstream
|
||||
_ = database.DB.First(&row, 1).Error
|
||||
sid, info := resolve60sUpstream(row.SourceID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"source_id": sid,
|
||||
"base_url": info.Base,
|
||||
"label": info.Label,
|
||||
})
|
||||
}
|
||||
|
||||
type put60sSourceBody struct {
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
|
||||
// Put60sSource 管理员切换节点:source_id 为 self | official
|
||||
func (h *SiteConfigHandler) Put60sSource(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "admin_not_configured"})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
var body put60sSourceBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
sid := strings.TrimSpace(body.SourceID)
|
||||
if _, ok := sixtyUpstreamRegistry[sid]; !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_source_id"})
|
||||
return
|
||||
}
|
||||
var row model.Site60sUpstream
|
||||
err := database.DB.First(&row, 1).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
row = model.Site60sUpstream{ID: 1}
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
row.SourceID = sid
|
||||
if err := database.DB.Save(&row).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
_, info := resolve60sUpstream(sid)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "source_id": sid, "base_url": info.Base, "label": info.Label})
|
||||
}
|
||||
|
||||
// —— AI 应用可见性控制(仅管理员可配置)——
|
||||
|
||||
// GetAIModelDisabled 公开:返回当前隐藏的 AI 应用 id 列表(与前端 StaticPageConfig 中 AI_MODEL_APPS 的索引对应)
|
||||
func (h *SiteConfigHandler) GetAIModelDisabled(c *gin.Context) {
|
||||
var rows []model.SiteAIModelDisabled
|
||||
if err := database.DB.Order("app_id").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.AppID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"disabled": ids})
|
||||
}
|
||||
|
||||
type putAIModelDisabledBody struct {
|
||||
Disabled []string `json:"disabled"`
|
||||
}
|
||||
|
||||
// PutAIModelDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致
|
||||
func (h *SiteConfigHandler) PutAIModelDisabled(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "admin_not_configured",
|
||||
"message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"})
|
||||
return
|
||||
}
|
||||
|
||||
var body putAIModelDisabledBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
if len(body.Disabled) > 64 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
clean := make([]string, 0, len(body.Disabled))
|
||||
for _, raw := range body.Disabled {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" || len(id) > 96 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.SiteAIModelDisabled{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
for _, id := range clean {
|
||||
if err := tx.Create(&model.SiteAIModelDisabled{AppID: id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)})
|
||||
}
|
||||
36
infogenie-backend-go/internal/handler/user.go
Normal file
36
infogenie-backend-go/internal/handler/user.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/internal/middleware"
|
||||
)
|
||||
|
||||
type UserHandler struct{}
|
||||
|
||||
func NewUserHandler() *UserHandler { return &UserHandler{} }
|
||||
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
authUser, exists := c.Get("auth_user")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未认证"})
|
||||
return
|
||||
}
|
||||
user := authUser.(*middleware.AuthCenterUser)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"account": user.Account,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.AvatarURL,
|
||||
"level": user.Level,
|
||||
"sprout_coins": user.SproutCoins,
|
||||
"checkin_days": user.CheckInDays,
|
||||
"checkin_streak": user.CheckInStreak,
|
||||
},
|
||||
})
|
||||
}
|
||||
127
infogenie-backend-go/internal/middleware/auth.go
Normal file
127
infogenie-backend-go/internal/middleware/auth.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/config"
|
||||
)
|
||||
|
||||
type AuthCenterUser struct {
|
||||
Account string `json:"account"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins float64 `json:"sproutCoins"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
WebsiteURL string `json:"websiteUrl"`
|
||||
Bio string `json:"bio"`
|
||||
CheckInDays int `json:"checkInDays"`
|
||||
CheckInStreak int `json:"checkInStreak"`
|
||||
}
|
||||
|
||||
type VerifyResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
User *AuthCenterUser `json:"user,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
BanReason string `json:"banReason,omitempty"`
|
||||
}
|
||||
|
||||
func verifyTokenWithAuthCenter(tokenStr string) (*VerifyResponse, error) {
|
||||
body, _ := json.Marshal(map[string]string{"token": tokenStr})
|
||||
|
||||
url := config.Cfg.AuthCenter.APIURL + "/api/auth/verify"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Auth-Client", "infogenie")
|
||||
req.Header.Set("X-Auth-Client-Name", "万象口袋")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求认证中心失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == 401 {
|
||||
return &VerifyResponse{Valid: false, Error: "invalid token"}, nil
|
||||
}
|
||||
|
||||
var result VerifyResponse
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析认证响应失败: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func extractToken(c *gin.Context) string {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
return auth[7:]
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func JWTAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenStr := extractToken(c)
|
||||
if tokenStr == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "缺少认证token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
result, err := verifyTokenWithAuthCenter(tokenStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "认证服务暂时不可用"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Valid || result.User == nil {
|
||||
msg := "Token无效或已过期"
|
||||
if result.BanReason != "" {
|
||||
msg = "账户已被封禁"
|
||||
}
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": msg})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("account", result.User.Account)
|
||||
c.Set("username", result.User.Username)
|
||||
c.Set("email", result.User.Email)
|
||||
c.Set("sprout_coins", result.User.SproutCoins)
|
||||
c.Set("auth_user", result.User)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func OptionalJWTAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tokenStr := extractToken(c)
|
||||
if tokenStr != "" {
|
||||
result, err := verifyTokenWithAuthCenter(tokenStr)
|
||||
if err == nil && result.Valid && result.User != nil {
|
||||
c.Set("account", result.User.Account)
|
||||
c.Set("username", result.User.Username)
|
||||
c.Set("email", result.User.Email)
|
||||
c.Set("sprout_coins", result.User.SproutCoins)
|
||||
c.Set("auth_user", result.User)
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
23
infogenie-backend-go/internal/middleware/cors.go
Normal file
23
infogenie-backend-go/internal/middleware/cors.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 宽松策略:放行任意 Origin(由 gin-contrib/cors 回显请求 Origin),便于前后端分离域名部署。
|
||||
// 如需收紧,可改为仅白名单或仅允许 https://infogenie.shumengya.top 等。
|
||||
func CORS() gin.HandlerFunc {
|
||||
return cors.New(cors.Config{
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "Accept", "X-Site-Admin-Token"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
})
|
||||
}
|
||||
18
infogenie-backend-go/internal/model/ai_config.go
Normal file
18
infogenie-backend-go/internal/model/ai_config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// AI配置表,用于存储各种AI提供商的配置
|
||||
type AIConfig struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Provider string `gorm:"type:varchar(50);not null;uniqueIndex" json:"provider"` // deepseek, kimi, etc.
|
||||
APIKey string `gorm:"type:varchar(255);not null" json:"api_key"`
|
||||
APIBase string `gorm:"type:varchar(255);not null" json:"api_base"`
|
||||
DefaultModel string `gorm:"type:varchar(100)" json:"default_model"`
|
||||
Models string `gorm:"type:text" json:"models"` // JSON格式的模型列表
|
||||
IsEnabled bool `gorm:"default:true" json:"is_enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (AIConfig) TableName() string { return "ai_configs" }
|
||||
1
infogenie-backend-go/internal/model/models.go
Normal file
1
infogenie-backend-go/internal/model/models.go
Normal file
@@ -0,0 +1 @@
|
||||
package model
|
||||
8
infogenie-backend-go/internal/model/site_60s_disabled.go
Normal file
8
infogenie-backend-go/internal/model/site_60s_disabled.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
// Site60sDisabled 记录在 60s API 列表中隐藏的功能项(仅存 feature_id,与前端 Api60sConfig 中 item.id 一致)
|
||||
type Site60sDisabled struct {
|
||||
FeatureID string `gorm:"primaryKey;type:varchar(96);not null" json:"feature_id"`
|
||||
}
|
||||
|
||||
func (Site60sDisabled) TableName() string { return "site_60s_disabled" }
|
||||
9
infogenie-backend-go/internal/model/site_60s_upstream.go
Normal file
9
infogenie-backend-go/internal/model/site_60s_upstream.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
// Site60sUpstream 单例行(id=1):60s API 上游节点,仅管理员可改
|
||||
type Site60sUpstream struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
SourceID string `gorm:"type:varchar(32);not null;default:self" json:"source_id"` // self | official
|
||||
}
|
||||
|
||||
func (Site60sUpstream) TableName() string { return "site_60s_upstream" }
|
||||
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
// SiteAIModelDisabled 记录在 AI 应用列表中隐藏的功能项(仅存 app_id,与前端 StaticPageConfig 中 AI_MODEL_APPS 的索引或唯一标识对应)
|
||||
type SiteAIModelDisabled struct {
|
||||
AppID string `gorm:"primaryKey;type:varchar(96);not null" json:"app_id"`
|
||||
}
|
||||
|
||||
func (SiteAIModelDisabled) TableName() string { return "site_ai_model_disabled" }
|
||||
15
infogenie-backend-go/internal/model/site_ai_runtime.go
Normal file
15
infogenie-backend-go/internal/model/site_ai_runtime.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// SiteAIRuntime 单例行(id=1):管理员配置的 AI 上游地址与密钥,优先于 ai_config.json
|
||||
type SiteAIRuntime struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
APIBase string `gorm:"type:varchar(512)" json:"api_base"`
|
||||
APIKey string `gorm:"type:varchar(2048)" json:"-"`
|
||||
DefaultModel string `gorm:"type:varchar(120)" json:"default_model"`
|
||||
DefaultProv string `gorm:"type:varchar(64)" json:"default_provider"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (SiteAIRuntime) TableName() string { return "site_ai_runtime" }
|
||||
92
infogenie-backend-go/internal/router/router.go
Normal file
92
infogenie-backend-go/internal/router/router.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/handler"
|
||||
"infogenie-backend/internal/middleware"
|
||||
)
|
||||
|
||||
func Setup(r *gin.Engine) {
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
authH := handler.NewAuthHandler()
|
||||
userH := handler.NewUserHandler()
|
||||
aiH := handler.NewAIModelHandler()
|
||||
siteH := handler.NewSiteConfigHandler()
|
||||
aiRtH := handler.NewAIRuntimeHandler()
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "万象口袋 后端 API 服务运行中",
|
||||
"description": "提供AI模型应用接口,用户认证由萌芽账户认证中心提供",
|
||||
"version": "3.2.0-go",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"endpoints": gin.H{
|
||||
"auth": "/api/auth (via 萌芽认证中心)",
|
||||
"user": "/api/user",
|
||||
"aimodelapp": "/api/aimodelapp",
|
||||
"site": "/api/site",
|
||||
"admin_site": "/api/admin/site/*",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// 健康检查:实际检测数据库连接
|
||||
r.GET("/api/health", func(c *gin.Context) {
|
||||
dbStatus := "connected"
|
||||
if database.DB != nil {
|
||||
sqlDB, err := database.DB.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
dbStatus = "disconnected"
|
||||
}
|
||||
} else {
|
||||
dbStatus = "not_initialized"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "running",
|
||||
"database": dbStatus,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
})
|
||||
|
||||
auth := r.Group("/api/auth")
|
||||
{
|
||||
auth.GET("/check", middleware.OptionalJWTAuth(), authH.Check)
|
||||
}
|
||||
|
||||
user := r.Group("/api/user", middleware.JWTAuth())
|
||||
{
|
||||
user.GET("/profile", userH.GetProfile)
|
||||
}
|
||||
|
||||
// 站点公开配置(无需登录)
|
||||
r.GET("/api/site/60s-disabled", siteH.Get60sDisabled)
|
||||
r.GET("/api/site/60s-source", siteH.Get60sSource)
|
||||
r.GET("/api/site/ai-model-disabled", siteH.GetAIModelDisabled)
|
||||
r.PUT("/api/admin/site/60s-disabled", siteH.Put60sDisabled)
|
||||
r.PUT("/api/admin/site/60s-source", siteH.Put60sSource)
|
||||
r.PUT("/api/admin/site/ai-model-disabled", siteH.PutAIModelDisabled)
|
||||
r.GET("/api/admin/site/ai-runtime", aiRtH.GetAIRuntime)
|
||||
r.PUT("/api/admin/site/ai-runtime", aiRtH.PutAIRuntime)
|
||||
|
||||
ai := r.Group("/api/aimodelapp")
|
||||
{
|
||||
ai.POST("/chat", middleware.JWTAuth(), aiH.Chat)
|
||||
ai.POST("/name-analysis", middleware.JWTAuth(), aiH.NameAnalysis)
|
||||
ai.POST("/variable-naming", middleware.JWTAuth(), aiH.VariableNaming)
|
||||
ai.POST("/poetry", middleware.JWTAuth(), aiH.Poetry)
|
||||
ai.POST("/translation", middleware.JWTAuth(), aiH.Translation)
|
||||
ai.POST("/classical_conversion", middleware.JWTAuth(), aiH.ClassicalConversion)
|
||||
ai.POST("/expression-maker", middleware.JWTAuth(), aiH.ExpressionMaker)
|
||||
ai.POST("/linux-command", middleware.JWTAuth(), aiH.LinuxCommand)
|
||||
ai.POST("/markdown_formatting", middleware.JWTAuth(), aiH.MarkdownFormatting)
|
||||
ai.POST("/kinship-calculator", middleware.JWTAuth(), aiH.KinshipCalculator)
|
||||
// models 端点添加认证保护
|
||||
ai.GET("/models", middleware.JWTAuth(), aiH.GetModels)
|
||||
}
|
||||
}
|
||||
213
infogenie-backend-go/internal/service/ai.go
Normal file
213
infogenie-backend-go/internal/service/ai.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type chatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []ChatMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
}
|
||||
|
||||
type chatResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
// loadAIConfig 从数据库读取AI配置
|
||||
func loadAIConfig(provider string) (apiKey, apiBase, defaultModel string, models []string, ok bool) {
|
||||
if database.DB == nil {
|
||||
return "", "", "", nil, false
|
||||
}
|
||||
|
||||
var config model.AIConfig
|
||||
if err := database.DB.Where("provider = ? AND is_enabled = ?", provider, true).First(&config).Error; err != nil {
|
||||
return "", "", "", nil, false
|
||||
}
|
||||
|
||||
// 解析models JSON
|
||||
var modelList []string
|
||||
if config.Models != "" {
|
||||
if err := json.Unmarshal([]byte(config.Models), &modelList); err != nil {
|
||||
// 如果解析失败,返回空的模型列表
|
||||
modelList = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
return config.APIKey, config.APIBase, config.DefaultModel, modelList, true
|
||||
}
|
||||
|
||||
// loadRuntimeDeepSeek 读取管理员在后台配置的 DeepSeek 兼容接口(OpenAI 格式),优先于 ai_config.json
|
||||
func loadRuntimeDeepSeek() (apiBase, apiKey, defModel string, ok bool) {
|
||||
if database.DB == nil {
|
||||
return "", "", "", false
|
||||
}
|
||||
var row model.SiteAIRuntime
|
||||
if err := database.DB.First(&row, 1).Error; err != nil {
|
||||
return "", "", "", false
|
||||
}
|
||||
base := strings.TrimSpace(row.APIBase)
|
||||
key := strings.TrimSpace(row.APIKey)
|
||||
dm := strings.TrimSpace(row.DefaultModel)
|
||||
if base != "" && key != "" {
|
||||
return base, key, dm, true
|
||||
}
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
func CallDeepSeek(messages []ChatMessage, model string, maxRetries int) (string, error) {
|
||||
// 首先尝试从SiteAIRuntime读取配置(向后兼容)
|
||||
if base, key, defModel, ok := loadRuntimeDeepSeek(); ok {
|
||||
if model == "" {
|
||||
model = defModel
|
||||
}
|
||||
if model == "" {
|
||||
model = "deepseek-chat"
|
||||
}
|
||||
url := strings.TrimSuffix(base, "/") + "/chat/completions"
|
||||
return callOpenAICompatible(url, key, model, messages, maxRetries, 90*time.Second)
|
||||
}
|
||||
|
||||
// 从新的AI配置表读取
|
||||
if apiKey, apiBase, defaultModel, models, ok := loadAIConfig("deepseek"); ok {
|
||||
if model == "" {
|
||||
model = defaultModel
|
||||
}
|
||||
if model == "" {
|
||||
model = "deepseek-chat"
|
||||
}
|
||||
// 验证模型是否在允许列表中
|
||||
if len(models) > 0 {
|
||||
allowed := false
|
||||
for _, m := range models {
|
||||
if m == model {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
model = models[0] // 使用第一个允许的模型
|
||||
}
|
||||
}
|
||||
url := strings.TrimSuffix(apiBase, "/") + "/chat/completions"
|
||||
return callOpenAICompatible(url, apiKey, model, messages, maxRetries, 90*time.Second)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("DeepSeek配置未设置,请在管理员后台配置API Key和Base URL")
|
||||
}
|
||||
|
||||
func CallKimi(messages []ChatMessage, model string) (string, error) {
|
||||
// 从新的AI配置表读取
|
||||
if apiKey, apiBase, defaultModel, models, ok := loadAIConfig("kimi"); ok {
|
||||
if model == "" {
|
||||
model = defaultModel
|
||||
}
|
||||
if model == "" {
|
||||
model = "kimi-k2-0905-preview"
|
||||
}
|
||||
// 验证模型是否在允许列表中
|
||||
if len(models) > 0 {
|
||||
allowed := false
|
||||
for _, m := range models {
|
||||
if m == model {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
model = models[0] // 使用第一个允许的模型
|
||||
}
|
||||
}
|
||||
url := strings.TrimSuffix(apiBase, "/") + "/v1/chat/completions"
|
||||
return callOpenAICompatible(url, apiKey, model, messages, 1, 30*time.Second)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Kimi配置未设置,请在管理员后台配置API Key和Base URL")
|
||||
}
|
||||
|
||||
func callOpenAICompatible(url, apiKey, model string, messages []ChatMessage, maxRetries int, timeout time.Duration) (string, error) {
|
||||
reqBody := chatRequest{
|
||||
Model: model,
|
||||
Messages: messages,
|
||||
Temperature: 0.7,
|
||||
MaxTokens: 2000,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化请求失败: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if attempt < maxRetries-1 {
|
||||
backoff := time.Duration(math.Pow(2, float64(attempt))) * time.Second
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("API调用异常(已重试%d次): %w", maxRetries, err)
|
||||
}
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
var result chatResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return "", fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
if len(result.Choices) == 0 {
|
||||
return "", fmt.Errorf("AI未返回有效内容")
|
||||
}
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
lastErr = fmt.Errorf("API调用失败: %d - %s", resp.StatusCode, string(respBody))
|
||||
if attempt < maxRetries-1 {
|
||||
backoff := time.Duration(math.Pow(2, float64(attempt))) * time.Second
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
}
|
||||
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
func CallAI(provider, model string, messages []ChatMessage) (string, error) {
|
||||
switch provider {
|
||||
case "deepseek":
|
||||
return CallDeepSeek(messages, model, 3)
|
||||
case "kimi":
|
||||
return CallKimi(messages, model)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的AI提供商: %s,目前支持的提供商: deepseek, kimi", provider)
|
||||
}
|
||||
}
|
||||
65
infogenie-backend-go/internal/service/email.go
Normal file
65
infogenie-backend-go/internal/service/email.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package service
|
||||
|
||||
// 邮件服务模块
|
||||
// SSO 重构后,验证码发送/验证已由认证中心处理
|
||||
// 此文件仅保留通用工具函数,如有需要可用于未来的邮件通知功能
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
|
||||
"infogenie-backend/config"
|
||||
)
|
||||
|
||||
// SendNotificationEmail 发送通知邮件(预留,供未来使用)
|
||||
func SendNotificationEmail(to, subject, htmlBody string) error {
|
||||
return sendSMTPMail(to, subject, htmlBody)
|
||||
}
|
||||
|
||||
func sendSMTPMail(to, subject, htmlBody string) error {
|
||||
cfg := config.Cfg.Mail
|
||||
from := cfg.Username
|
||||
|
||||
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n",
|
||||
from, to, subject)
|
||||
msg := headers + htmlBody
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS连接失败: %w", err)
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SMTP客户端创建失败: %w", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP认证失败: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte(msg)); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user