mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
feat: 钉钉机器人转发(未测试)
fix: admin初始化出错
This commit is contained in:
@@ -209,6 +209,9 @@ api/.vscode
|
|||||||
.history
|
.history
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
.claude/
|
||||||
|
.cursor/
|
||||||
|
openspec/
|
||||||
|
|
||||||
# pnpm
|
# pnpm
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:alpine as builder
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ApiGroup struct {
|
|||||||
BatchWorkflowApi
|
BatchWorkflowApi
|
||||||
AppVersionApi
|
AppVersionApi
|
||||||
ModelProviderApi
|
ModelProviderApi
|
||||||
|
ForwardProxyApi
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ package gaia
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
|
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemApi struct{}
|
type SystemApi struct{}
|
||||||
@@ -55,3 +63,182 @@ func (systemApi *SystemApi) SetDingTalk(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OkWithData("ok", c)
|
response.OkWithData("ok", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetForwardTokens 获取转发 Token 列表
|
||||||
|
// @Tags System
|
||||||
|
// @Summary 获取转发 Token 列表
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Success 200 {object} response.Response{data=[]request.ForwardToken,msg=string} "查询成功"
|
||||||
|
// @Router /gaia/system/forward-tokens [get]
|
||||||
|
func (systemApi *SystemApi) GetForwardTokens(c *gin.Context) {
|
||||||
|
integrate := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk)
|
||||||
|
|
||||||
|
var configMap request.DingTalkConfigRequest
|
||||||
|
if integrate.Config != "" {
|
||||||
|
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
|
||||||
|
response.FailWithMessage("解析配置失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回不包含 token_hash 的列表
|
||||||
|
type TokenInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := make([]TokenInfo, 0, len(configMap.ForwardConfig.Tokens))
|
||||||
|
for _, token := range configMap.ForwardConfig.Tokens {
|
||||||
|
tokens = append(tokens, TokenInfo{
|
||||||
|
ID: token.ID,
|
||||||
|
CreatedAt: token.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OkWithData(gin.H{"tokens": tokens, "count": len(tokens), "max": 20}, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateForwardToken 新增转发 Token
|
||||||
|
// @Tags System
|
||||||
|
// @Summary 新增转发 Token
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param token body string true "Token 明文"
|
||||||
|
// @Success 200 {object} response.Response{data=request.ForwardToken,msg=string} "创建成功"
|
||||||
|
// @Router /gaia/system/forward-tokens [post]
|
||||||
|
func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Token == "" {
|
||||||
|
response.FailWithMessage("Token 不能为空", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrate := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk)
|
||||||
|
var configMap request.DingTalkConfigRequest
|
||||||
|
if integrate.Config != "" {
|
||||||
|
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
|
||||||
|
response.FailWithMessage("解析配置失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数量限制
|
||||||
|
if len(configMap.ForwardConfig.Tokens) >= 20 {
|
||||||
|
response.FailWithMessage("转发 Token 最多 20 个", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一 ID 和哈希
|
||||||
|
tokenID := "tok_" + uuid.New().String()
|
||||||
|
tokenHash := fmt.Sprintf("%x", sha256.Sum256([]byte(req.Token)))
|
||||||
|
|
||||||
|
newToken := request.ForwardToken{
|
||||||
|
ID: tokenID,
|
||||||
|
TokenHash: tokenHash,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到配置
|
||||||
|
configMap.ForwardConfig.Tokens = append(configMap.ForwardConfig.Tokens, newToken)
|
||||||
|
configJSON, _ := json.Marshal(configMap)
|
||||||
|
integrate.Config = string(configJSON)
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
if err := systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil {
|
||||||
|
response.FailWithMessage("保存失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回明文 token(仅此次展示)
|
||||||
|
response.OkWithData(gin.H{
|
||||||
|
"id": tokenID,
|
||||||
|
"token": req.Token,
|
||||||
|
"created_at": newToken.CreatedAt,
|
||||||
|
}, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteForwardToken 删除转发 Token
|
||||||
|
// @Tags System
|
||||||
|
// @Summary 删除转发 Token
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @accept application/json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param id path string true "Token ID"
|
||||||
|
// @Param password body string true "当前用户密码"
|
||||||
|
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||||
|
// @Router /gaia/system/forward-tokens/:id [delete]
|
||||||
|
func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) {
|
||||||
|
tokenID := c.Param("id")
|
||||||
|
if tokenID == "" {
|
||||||
|
response.FailWithMessage("Token ID 不能为空", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证当前用户密码
|
||||||
|
userID := utils.GetUserUuid(c).String()
|
||||||
|
var user system.SysUser
|
||||||
|
if err := global.GVA_DB.Select("password").First(&user, userID).Error; err != nil {
|
||||||
|
response.FailWithMessage("查询用户失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utils.BcryptCheck(req.Password, user.Password) {
|
||||||
|
response.FailWithMessage("密码错误", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
integrate := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk)
|
||||||
|
var configMap request.DingTalkConfigRequest
|
||||||
|
if integrate.Config != "" {
|
||||||
|
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
|
||||||
|
response.FailWithMessage("解析配置失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找并删除 token
|
||||||
|
found := false
|
||||||
|
newTokens := make([]request.ForwardToken, 0, len(configMap.ForwardConfig.Tokens))
|
||||||
|
for _, token := range configMap.ForwardConfig.Tokens {
|
||||||
|
if token.ID == tokenID {
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newTokens = append(newTokens, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
response.FailWithMessage("Token 不存在", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
configMap.ForwardConfig.Tokens = newTokens
|
||||||
|
configJSON, _ := json.Marshal(configMap)
|
||||||
|
integrate.Config = string(configJSON)
|
||||||
|
|
||||||
|
if err := systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil {
|
||||||
|
response.FailWithMessage("保存失败:"+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OkWithMessage("删除成功", c)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type DBApi struct{}
|
|||||||
// @Router /init/initdb [post]
|
// @Router /init/initdb [post]
|
||||||
func (i *DBApi) InitDB(c *gin.Context) {
|
func (i *DBApi) InitDB(c *gin.Context) {
|
||||||
|
|
||||||
if !initDBService.IfInit() {
|
if initDBService.IfInit() {
|
||||||
global.GVA_LOG.Error("已存在数据库配置!")
|
global.GVA_LOG.Error("已存在数据库配置!")
|
||||||
response.FailWithMessage("已存在数据库配置", c)
|
response.FailWithMessage("已存在数据库配置", c)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ pgsql:
|
|||||||
prefix: ""
|
prefix: ""
|
||||||
port: "5432"
|
port: "5432"
|
||||||
config: sslmode=disable TimeZone=Asia/Shanghai
|
config: sslmode=disable TimeZone=Asia/Shanghai
|
||||||
db-name:
|
db-name: dify
|
||||||
username:
|
username: postgres
|
||||||
password:
|
password: difyai123456
|
||||||
path:
|
path: db_postgres
|
||||||
engine: ""
|
engine: ""
|
||||||
log-mode: error
|
log-mode: error
|
||||||
max-idle-conns: 10
|
max-idle-conns: 10
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ pgsql:
|
|||||||
db-name: dify
|
db-name: dify
|
||||||
username: postgres
|
username: postgres
|
||||||
password: difyai123456
|
password: difyai123456
|
||||||
path: 127.0.0.01
|
path: 127.0.0.1
|
||||||
engine: ""
|
engine: ""
|
||||||
log-mode: error
|
log-mode: error
|
||||||
max-idle-conns: 10
|
max-idle-conns: 10
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// overrideDBFromEnv 从环境变量覆盖数据库配置
|
||||||
|
// 优先级:环境变量 > 配置文件
|
||||||
|
func overrideDBFromEnv() {
|
||||||
|
// 数据库类型
|
||||||
|
if dbType := os.Getenv("DB_TYPE"); dbType != "" {
|
||||||
|
switch dbType {
|
||||||
|
case "mysql":
|
||||||
|
global.GVA_CONFIG.System.DbType = "mysql"
|
||||||
|
overrideMysqlFromEnv()
|
||||||
|
case "postgresql", "postgres":
|
||||||
|
global.GVA_CONFIG.System.DbType = "pgsql"
|
||||||
|
overridePgsqlFromEnv()
|
||||||
|
default:
|
||||||
|
global.GVA_CONFIG.System.DbType = "pgsql"
|
||||||
|
overridePgsqlFromEnv()
|
||||||
|
}
|
||||||
|
fmt.Printf("Database type overridden from DB_TYPE environment variable: %s\n", dbType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideMysqlFromEnv 从环境变量覆盖 MySQL 配置
|
||||||
|
func overrideMysqlFromEnv() {
|
||||||
|
cfg := &global.GVA_CONFIG.Mysql
|
||||||
|
if host := os.Getenv("DB_HOST"); host != "" {
|
||||||
|
cfg.Path = host
|
||||||
|
}
|
||||||
|
if port := os.Getenv("DB_PORT"); port != "" {
|
||||||
|
cfg.Port = port
|
||||||
|
} else {
|
||||||
|
cfg.Port = "3306"
|
||||||
|
}
|
||||||
|
if username := os.Getenv("DB_USERNAME"); username != "" {
|
||||||
|
cfg.Username = username
|
||||||
|
}
|
||||||
|
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
||||||
|
cfg.Password = password
|
||||||
|
}
|
||||||
|
if dbname := os.Getenv("DB_DATABASE"); dbname != "" {
|
||||||
|
cfg.Dbname = dbname
|
||||||
|
}
|
||||||
|
if config := os.Getenv("DB_CONFIG"); config != "" {
|
||||||
|
cfg.Config = config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overridePgsqlFromEnv 从环境变量覆盖 PostgreSQL 配置
|
||||||
|
func overridePgsqlFromEnv() {
|
||||||
|
cfg := &global.GVA_CONFIG.Pgsql
|
||||||
|
if host := os.Getenv("DB_HOST"); host != "" {
|
||||||
|
cfg.Path = host
|
||||||
|
}
|
||||||
|
if port := os.Getenv("DB_PORT"); port != "" {
|
||||||
|
cfg.Port = port
|
||||||
|
} else {
|
||||||
|
cfg.Port = "5432"
|
||||||
|
}
|
||||||
|
if username := os.Getenv("DB_USERNAME"); username != "" {
|
||||||
|
cfg.Username = username
|
||||||
|
}
|
||||||
|
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
||||||
|
cfg.Password = password
|
||||||
|
}
|
||||||
|
if dbname := os.Getenv("DB_DATABASE"); dbname != "" {
|
||||||
|
cfg.Dbname = dbname
|
||||||
|
}
|
||||||
|
if config := os.Getenv("DB_CONFIG"); config != "" {
|
||||||
|
cfg.Config = config
|
||||||
|
} else {
|
||||||
|
cfg.Config = "sslmode=disable TimeZone=Asia/Shanghai"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideRedisFromEnv 从环境变量覆盖 Redis 配置
|
||||||
|
func overrideRedisFromEnv() {
|
||||||
|
// 覆盖主 Redis 配置
|
||||||
|
if host := os.Getenv("REDIS_HOST"); host != "" {
|
||||||
|
port := os.Getenv("REDIS_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "6379"
|
||||||
|
}
|
||||||
|
global.GVA_CONFIG.Redis.Addr = host + ":" + port
|
||||||
|
}
|
||||||
|
if password := os.Getenv("REDIS_PASSWORD"); password != "" {
|
||||||
|
global.GVA_CONFIG.Redis.Password = password
|
||||||
|
}
|
||||||
|
if db := os.Getenv("REDIS_DB"); db != "" {
|
||||||
|
if dbNum, err := strconv.Atoi(db); err == nil {
|
||||||
|
global.GVA_CONFIG.Redis.DB = dbNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 Dify Redis 配置(与主 Redis 相同)
|
||||||
|
if host := os.Getenv("REDIS_HOST"); host != "" {
|
||||||
|
port := os.Getenv("REDIS_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "6379"
|
||||||
|
}
|
||||||
|
global.GVA_CONFIG.DifyRedis.Addr = host + ":" + port
|
||||||
|
}
|
||||||
|
if password := os.Getenv("REDIS_PASSWORD"); password != "" {
|
||||||
|
global.GVA_CONFIG.DifyRedis.Password = password
|
||||||
|
}
|
||||||
|
if db := os.Getenv("REDIS_DB"); db != "" {
|
||||||
|
if dbNum, err := strconv.Atoi(db); err == nil {
|
||||||
|
global.GVA_CONFIG.DifyRedis.DB = dbNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Redis configuration overridden from environment variables: %s\n", global.GVA_CONFIG.Redis.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideAllFromEnv 从环境变量覆盖所有配置
|
||||||
|
func overrideAllFromEnv() {
|
||||||
|
overrideDBFromEnv()
|
||||||
|
overrideRedisFromEnv()
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ func overrideJWTSigningKeyFromEnv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Viper //
|
// Viper //
|
||||||
// 优先级: 命令行 > 环境变量 > 默认值
|
// 优先级:命令行 > 环境变量 > 默认值
|
||||||
// Author [SliverHorn](https://github.com/SliverHorn)
|
// Author [SliverHorn](https://github.com/SliverHorn)
|
||||||
func Viper(path ...string) *viper.Viper {
|
func Viper(path ...string) *viper.Viper {
|
||||||
var config string
|
var config string
|
||||||
@@ -45,17 +45,17 @@ func Viper(path ...string) *viper.Viper {
|
|||||||
case gin.TestMode:
|
case gin.TestMode:
|
||||||
config = internal.ConfigTestFile
|
config = internal.ConfigTestFile
|
||||||
}
|
}
|
||||||
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.Mode(), config)
|
fmt.Printf("您正在使用 gin 模式的%s环境名称,config 的路径为%s\n", gin.Mode(), config)
|
||||||
} else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config
|
} else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于 config
|
||||||
config = configEnv
|
config = configEnv
|
||||||
fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config)
|
fmt.Printf("您正在使用%s环境变量,config 的路径为%s\n", internal.ConfigEnv, config)
|
||||||
}
|
}
|
||||||
} else { // 命令行参数不为空 将值赋值于config
|
} else { // 命令行参数不为空 将值赋值于 config
|
||||||
fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config)
|
fmt.Printf("您正在使用命令行的 -c 参数传递的值,config 的路径为%s\n", config)
|
||||||
}
|
}
|
||||||
} else { // 函数传递的可变参数的第一个值赋值于config
|
} else { // 函数传递的可变参数的第一个值赋值于 config
|
||||||
config = path[0]
|
config = path[0]
|
||||||
fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config)
|
fmt.Printf("您正在使用 func Viper() 传递的值,config 的路径为%s\n", config)
|
||||||
}
|
}
|
||||||
|
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
@@ -82,7 +82,11 @@ func Viper(path ...string) *viper.Viper {
|
|||||||
// Extend: Override JWT signing key from environment variable after initial load
|
// Extend: Override JWT signing key from environment variable after initial load
|
||||||
overrideJWTSigningKeyFromEnv()
|
overrideJWTSigningKeyFromEnv()
|
||||||
|
|
||||||
// root 适配性 根据root位置去找到对应迁移位置,保证root路径有效
|
// Extend: Override database and redis configuration from environment variables
|
||||||
|
// This allows admin-server to use the same configuration as docker-compose
|
||||||
|
overrideAllFromEnv()
|
||||||
|
|
||||||
|
// root 适配性 根据 root 位置去找到对应迁移位置,保证 root 路径有效
|
||||||
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
|
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ func newWithSeconds() *cron.Cron {
|
|||||||
|
|
||||||
func Corn() {
|
func Corn() {
|
||||||
var lock bool
|
var lock bool
|
||||||
|
initDBService := system.InitDBService{}
|
||||||
c := newWithSeconds()
|
c := newWithSeconds()
|
||||||
// 每分钟同步一次用户列表
|
// 每分钟同步一次用户列表
|
||||||
if _, err := c.AddFunc("0 */1 * * * *", func() {
|
if _, err := c.AddFunc("0 */1 * * * *", func() {
|
||||||
if global.GVA_DB == nil {
|
if global.GVA_DB == nil || !initDBService.IfInit() {
|
||||||
global.GVA_LOG.Info("【定时任务-每1分钟执行1次】同步用户列表任务,数据库没有初始化,暂未开始同步")
|
global.GVA_LOG.Info("【定时任务-每1分钟执行1次】同步用户列表任务,数据库没有初始化或尚未完成初始化,暂未开始同步")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func initBizRouter(routers ...*gin.RouterGroup) {
|
|||||||
gaiaRouter.InitSystemRouter(privateGroup)
|
gaiaRouter.InitSystemRouter(privateGroup)
|
||||||
gaiaRouter.InitWorkflowRouter(privateGroup)
|
gaiaRouter.InitWorkflowRouter(privateGroup)
|
||||||
gaiaRouter.InitAppVersionRouter(publicGroup, privateGroup)
|
gaiaRouter.InitAppVersionRouter(publicGroup, privateGroup)
|
||||||
gaiaRouter.InitModelProviderRouter(privateGroup) // 模型提供商路由
|
gaiaRouter.InitModelProviderRouter(privateGroup) // 模型提供商路由
|
||||||
|
gaiaRouter.InitForwardProxyRouter(publicGroup) // GPT 转发代理(免 JWT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ func CasbinHandler() gin.HandlerFunc {
|
|||||||
// 获取用户的角色
|
// 获取用户的角色
|
||||||
sub := strconv.Itoa(int(waitUse.AuthorityId))
|
sub := strconv.Itoa(int(waitUse.AuthorityId))
|
||||||
e := casbinService.Casbin() // 判断策略中是否存在
|
e := casbinService.Casbin() // 判断策略中是否存在
|
||||||
|
if e == nil {
|
||||||
|
global.GVA_LOG.Warn("Casbin enforcer is nil, skipping permission check")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
success, _ := e.Enforce(sub, obj, act)
|
success, _ := e.Enforce(sub, obj, act)
|
||||||
if !success {
|
if !success {
|
||||||
response.FailWithDetailed(gin.H{}, "权限不足", c)
|
response.FailWithDetailed(gin.H{}, "权限不足", c)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package request
|
package request
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// SystemOAuth2Error OAuth2 错误返回
|
// SystemOAuth2Error OAuth2 错误返回
|
||||||
type SystemOAuth2Error struct {
|
type SystemOAuth2Error struct {
|
||||||
Code int `json:"code" gorm:"comment:分类"` // 错误代码
|
Code int `json:"code" gorm:"comment:分类"` // 错误代码
|
||||||
@@ -11,53 +13,80 @@ type SystemOAuth2Request struct {
|
|||||||
Classify uint `json:"classify" gorm:"comment:分类"` // 分类
|
Classify uint `json:"classify" gorm:"comment:分类"` // 分类
|
||||||
Status bool `json:"status" gorm:"comment:状态"` // 状态
|
Status bool `json:"status" gorm:"comment:状态"` // 状态
|
||||||
ServerURL string `json:"server_url" gorm:"comment:服务器地址"` // OAuth2 服务器地址
|
ServerURL string `json:"server_url" gorm:"comment:服务器地址"` // OAuth2 服务器地址
|
||||||
AuthorizeURL string `json:"authorize_url" gorm:"comment:申请认证的URL"` // 申请认证的URL
|
AuthorizeURL string `json:"authorize_url" gorm:"comment:申请认证的 URL"` // 申请认证的 URL
|
||||||
TokenURL string `json:"token_url" gorm:"comment:获取Token的URL"` // 获取Token的URL
|
TokenURL string `json:"token_url" gorm:"comment:获取 Token 的 URL"` // 获取 Token 的 URL
|
||||||
UserinfoURL string `json:"userinfo_url" gorm:"comment:获取用户信息URL"` // 获取用户信息的URL
|
UserinfoURL string `json:"userinfo_url" gorm:"comment:获取用户信息 URL"` // 获取用户信息的 URL
|
||||||
LogoutURL string `json:"logout_url" gorm:"comment:退出登录回调URL"` // 退出登录回调URL
|
LogoutURL string `json:"logout_url" gorm:"comment:退出登录回调 URL"` // 退出登录回调 URL
|
||||||
DiscoveryURL string `json:"discovery_url" gorm:"comment:OIDC发现配置URL"` // OIDC 发现配置URL
|
DiscoveryURL string `json:"discovery_url" gorm:"comment:OIDC 发现配置 URL"` // OIDC 发现配置 URL
|
||||||
AppID string `json:"app_id" gorm:"comment:Client ID"` // Client ID
|
AppID string `json:"app_id" gorm:"comment:Client ID"` // Client ID
|
||||||
AppSecret string `json:"app_secret" gorm:"comment:Client Secret"` // Client Secret
|
AppSecret string `json:"app_secret" gorm:"comment:Client Secret"` // Client Secret
|
||||||
UserNameField string `json:"user_name_field" gorm:"comment:用户名字段"` // 用户名字段
|
UserNameField string `json:"user_name_field" gorm:"comment:用户名字段"` // 用户名字段
|
||||||
UserEmailField string `json:"user_email_field" gorm:"comment:邮箱字段"` // 邮箱字段
|
UserEmailField string `json:"user_email_field" gorm:"comment:邮箱字段"` // 邮箱字段
|
||||||
UserIDField string `json:"user_id_field" gorm:"comment:用户唯一标识字段"` // 用户唯一标识字段
|
UserIDField string `json:"user_id_field" gorm:"comment:用户唯一标识字段"` // 用户唯一标识字段
|
||||||
Scope string `json:"scope" gorm:"comment:授权范围scope"` // 授权范围
|
Scope string `json:"scope" gorm:"comment:授权范围 scope"` // 授权范围
|
||||||
TokenAuthMethod string `json:"token_auth_method" gorm:"comment:令牌端点认证方式"` // client_secret_post|client_secret_basic
|
TokenAuthMethod string `json:"token_auth_method" gorm:"comment:令牌端点认证方式"` // client_secret_post|client_secret_basic
|
||||||
RedirectUri string `json:"redirect_uri" gorm:"comment:测试用回调地址"` // 测试用回调地址
|
RedirectUri string `json:"redirect_uri" gorm:"comment:测试用回调地址"` // 测试用回调地址
|
||||||
Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` // 是否测试链接联通性
|
Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` // 是否测试链接联通性
|
||||||
Code string `json:"code" gorm:"default:0;comment:code代码"` // code代码
|
Code string `json:"code" gorm:"default:0;comment:code 代码"` // code 代码
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizationConfig 认证配置
|
// AuthorizationConfig 认证配置
|
||||||
type AuthorizationConfig struct {
|
type AuthorizationConfig struct {
|
||||||
Type string `json:"type"` // none | bearer | basic
|
Type string `json:"type"` // none | bearer | basic
|
||||||
Token string `json:"token"` // Bearer Token
|
Token string `json:"token"` // Bearer Token
|
||||||
Username string `json:"username"` // Basic Auth用户名
|
Username string `json:"username"` // Basic Auth 用户名
|
||||||
Password string `json:"password"` // Basic Auth密码
|
Password string `json:"password"` // Basic Auth 密码
|
||||||
}
|
}
|
||||||
|
|
||||||
// BodyData Body数据配置
|
// BodyData Body 数据配置
|
||||||
type BodyData struct {
|
type BodyData struct {
|
||||||
FormData []map[string]string `json:"form_data"` // form-data格式数据
|
FormData []map[string]string `json:"form_data"` // form-data 格式数据
|
||||||
Urlencoded []map[string]string `json:"urlencoded"` // x-www-form-urlencoded格式数据
|
Urlencoded []map[string]string `json:"urlencoded"` // x-www-form-urlencoded 格式数据
|
||||||
Raw string `json:"raw"` // raw JSON字符串
|
Raw string `json:"raw"` // raw JSON 字符串
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmailApiConfig 第三方邮箱API配置
|
// EmailApiConfig 第三方邮箱 API 配置
|
||||||
type EmailApiConfig struct {
|
type EmailApiConfig struct {
|
||||||
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
|
URL string `json:"url"` // API 地址
|
||||||
|
Method string `json:"method"` // HTTP 方法
|
||||||
|
RequestParamField string `json:"request_param_field"` // 请求参数字段名
|
||||||
|
BodyType string `json:"body_type"` // Body 类型:form-data | x-www-form-urlencoded | raw
|
||||||
|
Headers map[string]string `json:"headers"` // 请求头
|
||||||
|
Authorization AuthorizationConfig `json:"authorization"` // 认证配置
|
||||||
|
BodyData BodyData `json:"body_data"` // Body 数据
|
||||||
|
ResponseEmailField string `json:"response_email_field"` // 响应邮箱字段路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// DingIdApiConfig 第三方钉钉 ID 匹配用户 API 配置
|
||||||
|
type DingIdApiConfig struct {
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
URL string `json:"url"` // API地址
|
URL string `json:"url"` // API 地址
|
||||||
Method string `json:"method"` // HTTP方法
|
Method string `json:"method"` // HTTP 方法
|
||||||
RequestParamField string `json:"request_param_field"` // 请求参数字段名
|
RequestParamField string `json:"request_param_field"` // 请求参数字段名(如 ding_id)
|
||||||
BodyType string `json:"body_type"` // Body类型: form-data | x-www-form-urlencoded | raw
|
BodyType string `json:"body_type"` // Body 类型:form-data | x-www-form-urlencoded | raw
|
||||||
Headers map[string]string `json:"headers"` // 请求头
|
Headers map[string]string `json:"headers"` // 请求头
|
||||||
Authorization AuthorizationConfig `json:"authorization"` // 认证配置
|
Authorization AuthorizationConfig `json:"authorization"` // 认证配置
|
||||||
BodyData BodyData `json:"body_data"` // Body数据
|
BodyData BodyData `json:"body_data"` // Body 数据
|
||||||
ResponseEmailField string `json:"response_email_field"` // 响应邮箱字段路径
|
ResponseUserNamePath string `json:"response_user_name_path"` // 响应用户名 JSON 路径(如 data.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardToken 转发 Token 配置
|
||||||
|
type ForwardToken struct {
|
||||||
|
ID string `json:"id"` // 前端生成的唯一 ID(用于删除)
|
||||||
|
TokenHash string `json:"token_hash"` // SHA256(token)
|
||||||
|
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardConfig 转发集成配置
|
||||||
|
type ForwardConfig struct {
|
||||||
|
Enabled bool `json:"enabled"` // 是否启用转发
|
||||||
|
Tokens []ForwardToken `json:"tokens"` // Token 列表,最多 20 个
|
||||||
|
DingIdApi DingIdApiConfig `json:"ding_id_api"` // 第三方钉钉 ID 匹配用户 API
|
||||||
}
|
}
|
||||||
|
|
||||||
// DingTalkConfigRequest 钉钉集成配置
|
// DingTalkConfigRequest 钉钉集成配置
|
||||||
type DingTalkConfigRequest struct {
|
type DingTalkConfigRequest struct {
|
||||||
EmailApi EmailApiConfig `json:"email_api"` // 第三方邮箱API配置
|
EmailApi EmailApiConfig `json:"email_api"` // 第三方邮箱 API 配置
|
||||||
|
ForwardConfig ForwardConfig `json:"forward_config"` // 转发集成配置
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ var testApi = api.ApiGroupApp.GaiaApiGroup.TestApi
|
|||||||
var batchWorkflowApi = api.ApiGroupApp.GaiaApiGroup.BatchWorkflowApi
|
var batchWorkflowApi = api.ApiGroupApp.GaiaApiGroup.BatchWorkflowApi
|
||||||
var appVersionApi = api.ApiGroupApp.GaiaApiGroup.AppVersionApi
|
var appVersionApi = api.ApiGroupApp.GaiaApiGroup.AppVersionApi
|
||||||
var modelProviderApi = api.ApiGroupApp.GaiaApiGroup.ModelProviderApi
|
var modelProviderApi = api.ApiGroupApp.GaiaApiGroup.ModelProviderApi
|
||||||
|
var forwardProxyApi = api.ApiGroupApp.GaiaApiGroup.ForwardProxyApi
|
||||||
|
|||||||
@@ -10,16 +10,26 @@ type SystemRouter struct{}
|
|||||||
func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) {
|
func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) {
|
||||||
systemRouter := Router.Group("gaia/system")
|
systemRouter := Router.Group("gaia/system")
|
||||||
{
|
{
|
||||||
systemRouter.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置
|
systemRouter.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置
|
||||||
systemRouter.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置
|
systemRouter.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置
|
||||||
systemRouter.GET("oauth2", systemOAuth2Api.GetOAuth2Config) // 获取OAuth2配置
|
systemRouter.GET("oauth2", systemOAuth2Api.GetOAuth2Config) // 获取 OAuth2 配置
|
||||||
systemRouter.POST("oauth2", systemOAuth2Api.SetOAuth2Config) // 设置OAuth2配置
|
systemRouter.POST("oauth2", systemOAuth2Api.SetOAuth2Config) // 设置 OAuth2 配置
|
||||||
|
// 转发 Token 管理
|
||||||
|
systemRouter.GET("forward-tokens", systemApi.GetForwardTokens) // 获取转发 Token 列表
|
||||||
|
systemRouter.POST("forward-tokens", systemApi.CreateForwardToken) // 新增转发 Token
|
||||||
|
systemRouter.DELETE("forward-tokens/:id", systemApi.DeleteForwardToken) // 删除转发 Token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitForwardProxyRouter 初始化 GPT 转发代理路由(免 JWT,挂在 PublicGroup)
|
||||||
|
func (s *SystemRouter) InitForwardProxyRouter(PublicRouter *gin.RouterGroup) {
|
||||||
|
// 免 JWT 转发入口,通过 forwarding token + ding_id 鉴权
|
||||||
|
PublicRouter.Any("gaia/forward/proxy/*path", forwardProxyApi.ForwardProxy)
|
||||||
|
}
|
||||||
|
|
||||||
// InitModelProviderRouter 初始化模型提供商路由
|
// InitModelProviderRouter 初始化模型提供商路由
|
||||||
func (s *SystemRouter) InitModelProviderRouter(Router *gin.RouterGroup) {
|
func (s *SystemRouter) InitModelProviderRouter(Router *gin.RouterGroup) {
|
||||||
// 管理端API(需要JWT认证)
|
// 管理端 API(需要 JWT 认证)
|
||||||
modelProviderRouter := Router.Group("gaia/model-provider")
|
modelProviderRouter := Router.Group("gaia/model-provider")
|
||||||
{
|
{
|
||||||
modelProviderRouter.GET("list", modelProviderApi.GetProviderList) // 获取提供商配置列表
|
modelProviderRouter.GET("list", modelProviderApi.GetProviderList) // 获取提供商配置列表
|
||||||
@@ -29,10 +39,10 @@ func (s *SystemRouter) InitModelProviderRouter(Router *gin.RouterGroup) {
|
|||||||
modelProviderRouter.GET("logs", modelProviderApi.GetProxyLogs) // 获取代理日志
|
modelProviderRouter.GET("logs", modelProviderApi.GetProxyLogs) // 获取代理日志
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三方API(需要JWT认证)
|
// 第三方 API(需要 JWT 认证)
|
||||||
gaiaRouter := Router.Group("gaia")
|
gaiaRouter := Router.Group("gaia")
|
||||||
{
|
{
|
||||||
gaiaRouter.GET("models", modelProviderApi.GetModels) // 获取开启的模型列表(OpenAI格式)
|
gaiaRouter.GET("models", modelProviderApi.GetModels) // 获取开启的模型列表(OpenAI 格式)
|
||||||
gaiaRouter.Any("proxy/*path", modelProviderApi.Proxy) // 通用中转API:按路径转发(v1/chat/completions、v1/messages、v1/images/generations、v1/embeddings 等)
|
gaiaRouter.Any("proxy/*path", modelProviderApi.Proxy) // 通用中转 API:按路径转发(v1/chat/completions、v1/messages、v1/images/generations、v1/embeddings 等)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gaia
|
package gaia
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/faabiosr/cachego/file"
|
"github.com/faabiosr/cachego/file"
|
||||||
"github.com/fastwego/dingding"
|
"github.com/fastwego/dingding"
|
||||||
@@ -162,6 +164,16 @@ func (e *SystemIntegratedService) TestConnection(integrate gaia.SystemIntegratio
|
|||||||
global.GVA_LOG.Warn("第三方邮箱API配置验证失败", zap.Error(err))
|
global.GVA_LOG.Warn("第三方邮箱API配置验证失败", zap.Error(err))
|
||||||
// 不阻止保存,只记录警告
|
// 不阻止保存,只记录警告
|
||||||
}
|
}
|
||||||
|
// 验证转发集成配置
|
||||||
|
if err := e.ValidateForwardConfig(integrate); err != nil {
|
||||||
|
global.GVA_LOG.Warn("转发集成配置验证失败", zap.Error(err))
|
||||||
|
// 不阻止保存,只记录警告
|
||||||
|
}
|
||||||
|
// 验证第三方钉钉 ID 匹配 API 配置
|
||||||
|
if err := e.ValidateDingIdApiConfig(integrate); err != nil {
|
||||||
|
global.GVA_LOG.Warn("第三方钉钉 ID 匹配 API 配置验证失败", zap.Error(err))
|
||||||
|
// 不阻止保存,只记录警告
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
case gaia.SystemIntegrationOAuth2:
|
case gaia.SystemIntegrationOAuth2:
|
||||||
// 测试OAuth2连接
|
// 测试OAuth2连接
|
||||||
@@ -325,3 +337,286 @@ func (e *SystemIntegratedService) TestOAuth2Connection(integrate gaia.SystemInte
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateForwardConfig 验证转发集成配置
|
||||||
|
// @Tags System Integrated
|
||||||
|
// @Summary 验证转发集成配置
|
||||||
|
// @param: integrate gaia.SystemIntegration
|
||||||
|
// @return: error
|
||||||
|
func (e *SystemIntegratedService) ValidateForwardConfig(integrate gaia.SystemIntegration) error {
|
||||||
|
// 解析 Config 字段
|
||||||
|
if integrate.Config == "" {
|
||||||
|
return nil // 配置为空不算错误
|
||||||
|
}
|
||||||
|
|
||||||
|
var configMap request.DingTalkConfigRequest
|
||||||
|
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
|
||||||
|
return fmt.Errorf("解析配置失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否启用转发
|
||||||
|
if !configMap.ForwardConfig.Enabled {
|
||||||
|
return nil // 未启用不需要验证
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Token 数量
|
||||||
|
if len(configMap.ForwardConfig.Tokens) > 20 {
|
||||||
|
return errors.New("转发 Token 最多 20 个")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证每个 Token 的必填字段
|
||||||
|
for i, token := range configMap.ForwardConfig.Tokens {
|
||||||
|
if token.ID == "" {
|
||||||
|
return fmt.Errorf("第%d个 Token 的 ID 不能为空", i+1)
|
||||||
|
}
|
||||||
|
if token.TokenHash == "" {
|
||||||
|
return fmt.Errorf("第%d个 Token 的 TokenHash 不能为空", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("转发集成配置验证通过",
|
||||||
|
zap.Bool("enabled", configMap.ForwardConfig.Enabled),
|
||||||
|
zap.Int("token_count", len(configMap.ForwardConfig.Tokens)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDingIdApiConfig 验证第三方钉钉 ID 匹配 API 配置
|
||||||
|
// @Tags System Integrated
|
||||||
|
// @Summary 验证第三方钉钉 ID 匹配 API 配置
|
||||||
|
// @param: integrate gaia.SystemIntegration
|
||||||
|
// @return: error
|
||||||
|
func (e *SystemIntegratedService) ValidateDingIdApiConfig(integrate gaia.SystemIntegration) error {
|
||||||
|
// 解析 Config 字段
|
||||||
|
if integrate.Config == "" {
|
||||||
|
return nil // 配置为空不算错误
|
||||||
|
}
|
||||||
|
|
||||||
|
var configMap request.DingTalkConfigRequest
|
||||||
|
if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
|
||||||
|
return fmt.Errorf("解析配置失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否启用 DingIdApi
|
||||||
|
if !configMap.ForwardConfig.DingIdApi.Enabled {
|
||||||
|
return nil // 未启用不需要验证
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if configMap.ForwardConfig.DingIdApi.URL == "" {
|
||||||
|
return errors.New("钉钉 ID 匹配 API URL 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if configMap.ForwardConfig.DingIdApi.Method == "" {
|
||||||
|
configMap.ForwardConfig.DingIdApi.Method = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
if configMap.ForwardConfig.DingIdApi.RequestParamField == "" {
|
||||||
|
return errors.New("钉钉 ID 请求字段不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if configMap.ForwardConfig.DingIdApi.ResponseUserNamePath == "" {
|
||||||
|
return errors.New("响应用户名路径不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Body 类型(仅 POST/PUT/DELETE 需要)
|
||||||
|
if configMap.ForwardConfig.DingIdApi.Method != "GET" && configMap.ForwardConfig.DingIdApi.Method != "HEAD" {
|
||||||
|
bodyType := strings.ToLower(configMap.ForwardConfig.DingIdApi.BodyType)
|
||||||
|
if bodyType == "" {
|
||||||
|
configMap.ForwardConfig.DingIdApi.BodyType = "raw" // 默认 raw
|
||||||
|
} else if bodyType != "form-data" && bodyType != "x-www-form-urlencoded" && bodyType != "raw" {
|
||||||
|
return fmt.Errorf("不支持的 Body 类型:%s,支持的类型:form-data, x-www-form-urlencoded, raw", bodyType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Authorization 配置
|
||||||
|
authType := strings.ToLower(configMap.ForwardConfig.DingIdApi.Authorization.Type)
|
||||||
|
if authType != "" && authType != "none" {
|
||||||
|
if authType == "bearer" {
|
||||||
|
if configMap.ForwardConfig.DingIdApi.Authorization.Token == "" {
|
||||||
|
return errors.New("Bearer Token 不能为空")
|
||||||
|
}
|
||||||
|
} else if authType == "basic" {
|
||||||
|
if configMap.ForwardConfig.DingIdApi.Authorization.Username == "" || configMap.ForwardConfig.DingIdApi.Authorization.Password == "" {
|
||||||
|
return errors.New("Basic Auth 需要填写 Username 和 Password")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("不支持的 Authorization 类型:%s,支持的类型:none, bearer, basic", authType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("第三方钉钉 ID 匹配 API 配置验证通过",
|
||||||
|
zap.String("url", configMap.ForwardConfig.DingIdApi.URL),
|
||||||
|
zap.String("method", configMap.ForwardConfig.DingIdApi.Method),
|
||||||
|
zap.String("body_type", configMap.ForwardConfig.DingIdApi.BodyType),
|
||||||
|
zap.String("auth_type", configMap.ForwardConfig.DingIdApi.Authorization.Type))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJSONPath 按点分路径从 JSON 对象中提取字符串值,支持 "data.username" 等多层路径
|
||||||
|
func extractJSONPath(data map[string]interface{}, path string) string {
|
||||||
|
parts := strings.SplitN(path, ".", 2)
|
||||||
|
val, ok := data[parts[0]]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
if s, ok := val.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", val)
|
||||||
|
}
|
||||||
|
if nested, ok := val.(map[string]interface{}); ok {
|
||||||
|
return extractJSONPath(nested, parts[1])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// callDingIdApi 调用第三方钉钉 ID 匹配 API,返回 username
|
||||||
|
func (e *SystemIntegratedService) callDingIdApi(dingId string, config request.DingIdApiConfig) (string, error) {
|
||||||
|
method := strings.ToUpper(config.Method)
|
||||||
|
if method == "" {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if method != "GET" && method != "HEAD" {
|
||||||
|
switch strings.ToLower(config.BodyType) {
|
||||||
|
case "raw":
|
||||||
|
// 替换 Body 中的 {{ding_id}} 占位符
|
||||||
|
raw := strings.ReplaceAll(config.BodyData.Raw, "{{ding_id}}", dingId)
|
||||||
|
bodyReader = strings.NewReader(raw)
|
||||||
|
case "form-data", "x-www-form-urlencoded":
|
||||||
|
form := url.Values{}
|
||||||
|
for _, kv := range config.BodyData.Urlencoded {
|
||||||
|
for k, v := range kv {
|
||||||
|
form.Set(k, strings.ReplaceAll(v, "{{ding_id}}", dingId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.BodyType == "form-data" {
|
||||||
|
for _, kv := range config.BodyData.FormData {
|
||||||
|
for k, v := range kv {
|
||||||
|
form.Set(k, strings.ReplaceAll(v, "{{ding_id}}", dingId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyReader = strings.NewReader(form.Encode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求 URL(GET 时把 ding_id 拼入 query)
|
||||||
|
apiURL := config.URL
|
||||||
|
if method == "GET" || method == "HEAD" {
|
||||||
|
if config.RequestParamField != "" {
|
||||||
|
sep := "?"
|
||||||
|
if strings.Contains(apiURL, "?") {
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
apiURL = apiURL + sep + url.QueryEscape(config.RequestParamField) + "=" + url.QueryEscape(dingId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, apiURL, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("构建请求失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 Headers
|
||||||
|
for k, v := range config.Headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 Authorization
|
||||||
|
authType := strings.ToLower(config.Authorization.Type)
|
||||||
|
switch authType {
|
||||||
|
case "bearer":
|
||||||
|
req.Header.Set("Authorization", "Bearer "+config.Authorization.Token)
|
||||||
|
case "basic":
|
||||||
|
req.SetBasicAuth(config.Authorization.Username, config.Authorization.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("请求失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return "", fmt.Errorf("第三方 API 返回错误状态码:%d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取响应失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var respJSON map[string]interface{}
|
||||||
|
if err = json.Unmarshal(respBody, &respJSON); err != nil {
|
||||||
|
return "", fmt.Errorf("解析响应 JSON 失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
userName := extractJSONPath(respJSON, config.ResponseUserNamePath)
|
||||||
|
if userName == "" {
|
||||||
|
return "", fmt.Errorf("响应中未找到用户名(路径:%s)", config.ResponseUserNamePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveAccountByDingId 通过钉钉 ID 解析 gaia account_id
|
||||||
|
// 解析顺序:Redis 缓存 → AccountDingTalkExtend 本地表 → 第三方 DingIdApi
|
||||||
|
func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfig request.DingIdApiConfig) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
redisKey := "gaia:forward:ding:" + dingId
|
||||||
|
|
||||||
|
// 1. 查 Redis 缓存
|
||||||
|
if cached, err := global.GVA_REDIS.Get(ctx, redisKey).Result(); err == nil && cached != "" {
|
||||||
|
global.GVA_LOG.Info("ResolveAccountByDingId: Redis 命中", zap.String("ding_id", dingId), zap.String("account_id", cached))
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查本地 AccountDingTalkExtend 表
|
||||||
|
var extend gaia.AccountDingTalkExtend
|
||||||
|
if err := global.GVA_DB.Where("ding_talk = ?", dingId).First(&extend).Error; err == nil {
|
||||||
|
accountID := extend.ID.String()
|
||||||
|
global.GVA_LOG.Info("ResolveAccountByDingId: 本地表命中", zap.String("ding_id", dingId), zap.String("account_id", accountID))
|
||||||
|
global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour)
|
||||||
|
return accountID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 第三方 DingIdApi(若配置且启用)
|
||||||
|
if !apiConfig.Enabled || apiConfig.URL == "" {
|
||||||
|
return "", fmt.Errorf("未找到 ding_id=%s 对应的用户,且未配置第三方 API", dingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
userName, err := e.callDingIdApi(dingId, apiConfig)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("调用第三方 DingId API 失败:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 按 username 查 accounts 表(匹配 name 字段)
|
||||||
|
var account gaia.Account
|
||||||
|
if err = global.GVA_DB.Where("name = ?", userName).First(&account).Error; err != nil {
|
||||||
|
return "", fmt.Errorf("用户名 %s 不存在(来自第三方 API)", userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID := account.ID.String()
|
||||||
|
|
||||||
|
// 5. 写回 AccountDingTalkExtend,方便下次本地命中
|
||||||
|
global.GVA_DB.Create(&gaia.AccountDingTalkExtend{
|
||||||
|
ID: account.ID,
|
||||||
|
DingTalk: dingId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 6. 写 Redis 缓存
|
||||||
|
global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour)
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("ResolveAccountByDingId: 第三方 API 解析成功",
|
||||||
|
zap.String("ding_id", dingId),
|
||||||
|
zap.String("username", userName),
|
||||||
|
zap.String("account_id", accountID))
|
||||||
|
|
||||||
|
return accountID, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,8 +171,10 @@ func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) e
|
|||||||
|
|
||||||
func (CasbinService *CasbinService) FreshCasbin() (err error) {
|
func (CasbinService *CasbinService) FreshCasbin() (err error) {
|
||||||
e := CasbinService.Casbin()
|
e := CasbinService.Casbin()
|
||||||
err = e.LoadPolicy()
|
if e == nil {
|
||||||
return err
|
return errors.New("casbin enforcer is nil, please check database initialization")
|
||||||
|
}
|
||||||
|
return e.LoadPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
//@author: [piexlmax](https://github.com/piexlmax)
|
//@author: [piexlmax](https://github.com/piexlmax)
|
||||||
@@ -187,6 +189,10 @@ var (
|
|||||||
|
|
||||||
func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer {
|
func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
|
if global.GVA_DB == nil {
|
||||||
|
zap.L().Warn("Casbin initialization skipped: global.GVA_DB is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
a, err := gormadapter.NewAdapterByDB(global.GVA_DB)
|
a, err := gormadapter.NewAdapterByDB(global.GVA_DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err))
|
zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||||
modelSystem "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
modelSystem "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
@@ -141,6 +142,7 @@ func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
|
|||||||
global.GVA_DB = db
|
global.GVA_DB = db
|
||||||
db.Exec("DELETE FROM sys_base_menus")
|
db.Exec("DELETE FROM sys_base_menus")
|
||||||
db.Exec("DELETE FROM sys_authorities")
|
db.Exec("DELETE FROM sys_authorities")
|
||||||
|
db.Exec("DELETE FROM sys_user_authority")
|
||||||
if err = initHandler.InitTables(ctx, initializers); err != nil {
|
if err = initHandler.InitTables(ctx, initializers); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -151,6 +153,10 @@ func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) {
|
|||||||
if err = initHandler.WriteConfig(ctx); err != nil {
|
if err = initHandler.WriteConfig(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 初始化完成后刷新 Casbin 策略,避免使用旧的或空的策略
|
||||||
|
if err = CasbinServiceApp.FreshCasbin(); err != nil {
|
||||||
|
global.GVA_LOG.Warn("refresh casbin policy after InitDB failed", zap.Error(err))
|
||||||
|
}
|
||||||
initializers = initSlice{}
|
initializers = initSlice{}
|
||||||
cache = map[string]*orderedInitializer{}
|
cache = map[string]*orderedInitializer{}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/zap"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/flipped-aurora/gin-vue-admin/server/config"
|
"github.com/flipped-aurora/gin-vue-admin/server/config"
|
||||||
@@ -35,10 +36,10 @@ func (h PgsqlInitHandler) WriteConfig(ctx context.Context) error {
|
|||||||
|
|
||||||
// 改成拿dify的配置,如果不是docker运行,则从dify api的.env文件中获取jwt的加密key
|
// 改成拿dify的配置,如果不是docker运行,则从dify api的.env文件中获取jwt的加密key
|
||||||
if !global.GVA_CONFIG.System.DockerRun {
|
if !global.GVA_CONFIG.System.DockerRun {
|
||||||
var err error
|
if secretKey, err := h.GetJwtSigningKeyFormDifyApiEnv(); err != nil {
|
||||||
global.GVA_CONFIG.JWT.SigningKey, err = h.GetJwtSigningKeyFormDifyApiEnv()
|
global.GVA_LOG.Warn("failed to load JWT signing key from Dify API .env, using existing configuration", zap.Error(err))
|
||||||
if err != nil {
|
} else if secretKey != "" {
|
||||||
return err
|
global.GVA_CONFIG.JWT.SigningKey = secretKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cs := utils.StructToMap(global.GVA_CONFIG)
|
cs := utils.StructToMap(global.GVA_CONFIG)
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
|||||||
{ApiGroup: "模型管理", Method: "PATCH", Path: "/gaia/proxy/*", Description: "中转API(第三方)-PATCH"},
|
{ApiGroup: "模型管理", Method: "PATCH", Path: "/gaia/proxy/*", Description: "中转API(第三方)-PATCH"},
|
||||||
{ApiGroup: "模型管理", Method: "DELETE", Path: "/gaia/proxy/*", Description: "中转API(第三方)-DELETE"},
|
{ApiGroup: "模型管理", Method: "DELETE", Path: "/gaia/proxy/*", Description: "中转API(第三方)-DELETE"},
|
||||||
// Extend Stop: model provider
|
// Extend Stop: model provider
|
||||||
|
// 转发 Token 管理
|
||||||
|
{ApiGroup: "转发集成", Method: "GET", Path: "/gaia/system/forward-tokens", Description: "获取转发 Token 列表"},
|
||||||
|
{ApiGroup: "转发集成", Method: "POST", Path: "/gaia/system/forward-tokens", Description: "新增转发 Token"},
|
||||||
|
{ApiGroup: "转发集成", Method: "DELETE", Path: "/gaia/system/forward-tokens/:id", Description: "删除转发 Token"},
|
||||||
}
|
}
|
||||||
if err := db.Create(&entities).Error; err != nil {
|
if err := db.Create(&entities).Error; err != nil {
|
||||||
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const getSystemOAuth2 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags systrm
|
// @Tags systrm
|
||||||
// @Summary 修改OAuth2集成配置
|
// @Summary 修改 OAuth2 集成配置
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Produce application/json
|
// @Produce application/json
|
||||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
|
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
|
||||||
@@ -53,3 +53,38 @@ export const setSystemOAuth2 = (data) => {
|
|||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags systrm
|
||||||
|
// @Summary 获取转发 Token 列表
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /gaia/system/forward-tokens [get]
|
||||||
|
export const getForwardTokens = () => {
|
||||||
|
return service({
|
||||||
|
url: '/gaia/system/forward-tokens',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags systrm
|
||||||
|
// @Summary 新增转发 Token
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /gaia/system/forward-tokens [post]
|
||||||
|
export const createForwardToken = (data) => {
|
||||||
|
return service({
|
||||||
|
url: '/gaia/system/forward-tokens',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags systrm
|
||||||
|
// @Summary 删除转发 Token
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /gaia/system/forward-tokens/:id [delete]
|
||||||
|
export const deleteForwardToken = (id, password) => {
|
||||||
|
return service({
|
||||||
|
url: `/gaia/system/forward-tokens/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
data: { password },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -364,7 +364,147 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 转发集成配置卡片 -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header flex items-center justify-between">
|
||||||
|
<span class="text-lg font-medium">转发集成配置</span>
|
||||||
|
<el-tag v-if="forwardConfig.enabled" type="success" size="small">已启用</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">未启用</el-tag>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-sm mb-4">为第三方系统(如钉钉入口)提供免登录转发代理能力,通过 Token 鉴权后根据钉钉 ID 自动计费</p>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 开关 -->
|
||||||
|
<div class="card-section">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">启用转发:</span>
|
||||||
|
<el-switch
|
||||||
|
v-if="openEdit"
|
||||||
|
v-model="forwardConfig.enabled"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ forwardConfig.enabled ? '已启用' : '未启用' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- Token 列表 -->
|
||||||
|
<div class="card-section">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="section-title mb-0">
|
||||||
|
转发 Token 列表
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-gray-500 text-sm">{{ forwardTokenList.length }}/20 个</span>
|
||||||
|
<el-button
|
||||||
|
v-if="openEdit"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="forwardTokenList.length >= 20"
|
||||||
|
@click="showCreateTokenDialog = true"
|
||||||
|
>
|
||||||
|
+ 新增 Token
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="forwardTokenList" border size="small" class="w-full">
|
||||||
|
<el-table-column label="Token ID" prop="id" min-width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-mono text-xs">{{ row.id }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="created_at" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="openEdit" label="操作" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="danger" link size="small" @click="openDeleteTokenDialog(row.id)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 第三方钉钉 ID 匹配用户 API 配置 -->
|
||||||
|
<div class="card-section">
|
||||||
|
<div class="section-title">
|
||||||
|
第三方钉钉 ID 匹配用户 API
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 text-sm mb-4">当本地表中找不到钉钉 ID 对应用户时,调用此 API 通过 ding_id 获取用户名</p>
|
||||||
|
<div class="bg-gray-50 dark:bg-slate-800 p-5 border dark:border-slate-700 rounded-lg">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">启用:</span>
|
||||||
|
<el-switch v-if="openEdit" v-model="dingIdApiConfig.enabled" />
|
||||||
|
<span v-else>{{ dingIdApiConfig.enabled ? '已启用' : '未启用' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">API URL:</span>
|
||||||
|
<el-input v-if="openEdit" v-model="dingIdApiConfig.url" class="flex-1" placeholder="https://api.example.com/user/by-dingid" />
|
||||||
|
<span v-else class="info-value flex-1">{{ dingIdApiConfig.url || '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">HTTP 方法:</span>
|
||||||
|
<el-select v-if="openEdit" v-model="dingIdApiConfig.method" class="flex-1">
|
||||||
|
<el-option label="GET" value="GET" />
|
||||||
|
<el-option label="POST" value="POST" />
|
||||||
|
<el-option label="PUT" value="PUT" />
|
||||||
|
<el-option label="DELETE" value="DELETE" />
|
||||||
|
</el-select>
|
||||||
|
<span v-else class="info-value">{{ dingIdApiConfig.method || 'GET' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">请求参数字段:</span>
|
||||||
|
<el-input v-if="openEdit" v-model="dingIdApiConfig.request_param_field" class="flex-1" placeholder="ding_id" />
|
||||||
|
<span v-else class="info-value">{{ dingIdApiConfig.request_param_field || 'ding_id' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<span class="info-label">响应用户名路径:</span>
|
||||||
|
<el-input v-if="openEdit" v-model="dingIdApiConfig.response_user_name_path" class="flex-1" placeholder="data.username" />
|
||||||
|
<span v-else class="info-value">{{ dingIdApiConfig.response_user_name_path || '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 新增 Token 弹窗 -->
|
||||||
|
<el-dialog v-model="showCreateTokenDialog" title="新增转发 Token" width="480px" :close-on-click-modal="false">
|
||||||
|
<div v-if="!newTokenValue">
|
||||||
|
<p class="text-gray-600 mb-4">输入 Token 明文,系统将存储其 SHA256 哈希。Token 仅展示一次,请妥善保管。</p>
|
||||||
|
<el-input v-model="newTokenInput" placeholder="请输入 Token 明文(留空则自动生成)" clearable />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-alert type="success" title="Token 创建成功!请复制保存,此后不再显示明文。" :closable="false" class="mb-4" />
|
||||||
|
<el-input v-model="newTokenValue" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="copyToken(newTokenValue)">复制</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="!newTokenValue" @click="showCreateTokenDialog = false; newTokenInput = ''">取消</el-button>
|
||||||
|
<el-button v-if="!newTokenValue" type="primary" :loading="creatingToken" @click="handleCreateToken">确认创建</el-button>
|
||||||
|
<el-button v-if="newTokenValue" type="primary" @click="showCreateTokenDialog = false; newTokenValue = ''; newTokenInput = ''; loadForwardTokens()">完成</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 删除 Token 弹窗(需密码) -->
|
||||||
|
<el-dialog v-model="showDeleteTokenDialog" title="删除转发 Token" width="420px" :close-on-click-modal="false">
|
||||||
|
<p class="text-gray-600 mb-4">删除操作需要验证您的登录密码,请输入后确认。</p>
|
||||||
|
<el-input v-model="deleteTokenPassword" type="password" placeholder="请输入您的登录密码" show-password />
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showDeleteTokenDialog = false; deleteTokenPassword = ''; deletingTokenId = ''">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="deletingToken" @click="handleDeleteToken">确认删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -373,7 +513,7 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
import { getSystemDingTalk, setSystemDingTalk } from "@/api/gaia/system";
|
import { getSystemDingTalk, setSystemDingTalk, getForwardTokens, createForwardToken, deleteForwardToken } from "@/api/gaia/system";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'IntegratedDingTalk',
|
name: 'IntegratedDingTalk',
|
||||||
@@ -411,6 +551,109 @@ const emailApiConfig = ref({
|
|||||||
response_email_field: 'data[0].userName'
|
response_email_field: 'data[0].userName'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 转发集成配置
|
||||||
|
const forwardConfig = ref({ enabled: false, tokens: [] })
|
||||||
|
|
||||||
|
// 第三方钉钉 ID 匹配 API 配置
|
||||||
|
const dingIdApiConfig = ref({
|
||||||
|
enabled: false,
|
||||||
|
url: '',
|
||||||
|
method: 'GET',
|
||||||
|
request_param_field: 'ding_id',
|
||||||
|
body_type: 'raw',
|
||||||
|
headers: {},
|
||||||
|
authorization: { type: 'none', token: '', username: '', password: '' },
|
||||||
|
body_data: { raw: '', form_data: [], urlencoded: [] },
|
||||||
|
response_user_name_path: 'data.username',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 转发 Token 列表
|
||||||
|
const forwardTokenList = ref([])
|
||||||
|
|
||||||
|
// 新增 Token 弹窗
|
||||||
|
const showCreateTokenDialog = ref(false)
|
||||||
|
const newTokenInput = ref('')
|
||||||
|
const newTokenValue = ref('')
|
||||||
|
const creatingToken = ref(false)
|
||||||
|
|
||||||
|
// 删除 Token 弹窗
|
||||||
|
const showDeleteTokenDialog = ref(false)
|
||||||
|
const deleteTokenPassword = ref('')
|
||||||
|
const deletingTokenId = ref('')
|
||||||
|
const deletingToken = ref(false)
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
return new Date(dateStr).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载转发 Token 列表
|
||||||
|
const loadForwardTokens = async () => {
|
||||||
|
const res = await getForwardTokens()
|
||||||
|
if (res.code === 0) {
|
||||||
|
forwardTokenList.value = res.data?.tokens || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机 Token
|
||||||
|
const generateToken = () => {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let token = ''
|
||||||
|
for (let i = 0; i < 48; i++) {
|
||||||
|
token += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制 Token
|
||||||
|
const copyToken = (token) => {
|
||||||
|
navigator.clipboard.writeText(token).then(() => {
|
||||||
|
ElMessage({ type: 'success', message: 'Token 已复制到剪贴板' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Token
|
||||||
|
const handleCreateToken = async () => {
|
||||||
|
creatingToken.value = true
|
||||||
|
const token = newTokenInput.value.trim() || generateToken()
|
||||||
|
const res = await createForwardToken({ token })
|
||||||
|
creatingToken.value = false
|
||||||
|
if (res.code === 0) {
|
||||||
|
newTokenValue.value = res.data?.token || token
|
||||||
|
ElMessage({ type: 'success', message: 'Token 创建成功' })
|
||||||
|
} else {
|
||||||
|
ElMessage({ type: 'error', message: res.msg || '创建失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开删除 Token 弹窗
|
||||||
|
const openDeleteTokenDialog = (id) => {
|
||||||
|
deletingTokenId.value = id
|
||||||
|
deleteTokenPassword.value = ''
|
||||||
|
showDeleteTokenDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 Token
|
||||||
|
const handleDeleteToken = async () => {
|
||||||
|
if (!deleteTokenPassword.value) {
|
||||||
|
ElMessage({ type: 'warning', message: '请输入登录密码' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deletingToken.value = true
|
||||||
|
const res = await deleteForwardToken(deletingTokenId.value, deleteTokenPassword.value)
|
||||||
|
deletingToken.value = false
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage({ type: 'success', message: '删除成功' })
|
||||||
|
showDeleteTokenDialog.value = false
|
||||||
|
deleteTokenPassword.value = ''
|
||||||
|
deletingTokenId.value = ''
|
||||||
|
await loadForwardTokens()
|
||||||
|
} else {
|
||||||
|
ElMessage({ type: 'error', message: res.msg || '删除失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 标签页管理
|
// 标签页管理
|
||||||
const activeTab = ref('headers')
|
const activeTab = ref('headers')
|
||||||
|
|
||||||
@@ -730,11 +973,27 @@ const initForm = async() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析转发集成配置
|
||||||
|
if (configData.forward_config) {
|
||||||
|
forwardConfig.value = {
|
||||||
|
enabled: configData.forward_config.enabled || false,
|
||||||
|
tokens: configData.forward_config.tokens || [],
|
||||||
|
}
|
||||||
|
if (configData.forward_config.ding_id_api) {
|
||||||
|
dingIdApiConfig.value = {
|
||||||
|
...dingIdApiConfig.value,
|
||||||
|
...configData.forward_config.ding_id_api,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析邮箱API配置失败:', e)
|
console.error('解析邮箱API配置失败:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 加载 Token 列表
|
||||||
|
await loadForwardTokens()
|
||||||
}
|
}
|
||||||
initForm()
|
initForm()
|
||||||
|
|
||||||
@@ -792,6 +1051,11 @@ const update = async() => {
|
|||||||
...emailApiConfig.value,
|
...emailApiConfig.value,
|
||||||
headers,
|
headers,
|
||||||
body_data: bodyData
|
body_data: bodyData
|
||||||
|
},
|
||||||
|
forward_config: {
|
||||||
|
enabled: forwardConfig.value.enabled,
|
||||||
|
tokens: forwardConfig.value.tokens,
|
||||||
|
ding_id_api: { ...dingIdApiConfig.value },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.value.config = JSON.stringify(configData)
|
config.value.config = JSON.stringify(configData)
|
||||||
|
|||||||
@@ -153,4 +153,19 @@ INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES
|
|||||||
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id) VALUES (888, 42);
|
INSERT INTO sys_authority_menus (sys_authority_authority_id, sys_base_menu_id) VALUES (888, 42);
|
||||||
|
|
||||||
|
|
||||||
|
-- --------------- 9. API sys_apis (转发集成:转发 Token 管理 3 条) 2026-03-09 18:08:33 ---------------
|
||||||
|
-- 请按当前库最大 id 调整起始 id,避免冲突。例如 MAX(id)=269 则从 270 起
|
||||||
|
INSERT INTO sys_apis (id, created_at, updated_at, deleted_at, path, description, api_group, method) VALUES
|
||||||
|
(270, NOW(), NOW(), NULL, '/gaia/system/forward-tokens', '获取转发 Token 列表', '转发集成', 'GET'),
|
||||||
|
(271, NOW(), NOW(), NULL, '/gaia/system/forward-tokens', '新增转发 Token', '转发集成', 'POST'),
|
||||||
|
(272, NOW(), NOW(), NULL, '/gaia/system/forward-tokens/:id', '删除转发 Token', '转发集成', 'DELETE');
|
||||||
|
|
||||||
|
|
||||||
|
-- --------------- 10. Casbin 规则 casbin_rule (转发集成 888/8881) ---------------
|
||||||
|
INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES
|
||||||
|
('p', '888', '/gaia/system/forward-tokens', 'GET'),
|
||||||
|
('p', '888', '/gaia/system/forward-tokens', 'POST'),
|
||||||
|
('p', '888', '/gaia/system/forward-tokens/:id', 'DELETE'),
|
||||||
|
('p', '8881', '/gaia/system/forward-tokens', 'GET'),
|
||||||
|
('p', '8881', '/gaia/system/forward-tokens', 'POST'),
|
||||||
|
('p', '8881', '/gaia/system/forward-tokens/:id', 'DELETE');
|
||||||
|
|||||||
Reference in New Issue
Block a user