完成文本审查服务开发

This commit is contained in:
GeekMaster
2025-08-31 18:21:09 +08:00
parent 0585edd895
commit 9a4239290b
27 changed files with 896 additions and 164 deletions

View File

@@ -148,14 +148,24 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
logger.Error("load payment config error: ", err)
}
// 加载文本审查配置
var moderationConfig types.ModerationConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyModeration).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &moderationConfig)
if err != nil {
logger.Error("load moderation config error: ", err)
}
return &types.SystemConfig{
Base: baseConfig,
License: license,
SMS: smsConfig,
OSS: ossConfig,
SMTP: smtpConfig,
Payment: paymentConfig,
Captcha: captchaConfig,
WxLogin: wxLoginConfig,
Base: baseConfig,
License: license,
SMS: smsConfig,
OSS: ossConfig,
SMTP: smtpConfig,
Payment: paymentConfig,
Captcha: captchaConfig,
WxLogin: wxLoginConfig,
Moderation: moderationConfig,
}
}

View File

@@ -100,29 +100,31 @@ type BaseConfig struct {
}
type SystemConfig struct {
Base BaseConfig
Payment PaymentConfig
OSS OSSConfig
SMS SMSConfig
SMTP SmtpConfig
Captcha CaptchaConfig
WxLogin WxLoginConfig
Jimeng JimengConfig
License License
Base BaseConfig
Payment PaymentConfig
OSS OSSConfig
SMS SMSConfig
SMTP SmtpConfig
Captcha CaptchaConfig
WxLogin WxLoginConfig
Jimeng JimengConfig
License License
Moderation ModerationConfig
}
// 配置键名常量
const (
ConfigKeySystem = "system"
ConfigKeyNotice = "notice"
ConfigKeyAgreement = "agreement"
ConfigKeyPrivacy = "privacy"
ConfigKeyMarkMap = "mark_map"
ConfigKeyCaptcha = "captcha"
ConfigKeyWxLogin = "wx_login"
ConfigKeyLicense = "license"
ConfigKeySms = "sms"
ConfigKeySmtp = "smtp"
ConfigKeyOss = "oss"
ConfigKeyPayment = "payment"
ConfigKeySystem = "system"
ConfigKeyNotice = "notice"
ConfigKeyAgreement = "agreement"
ConfigKeyPrivacy = "privacy"
ConfigKeyMarkMap = "mark_map"
ConfigKeyCaptcha = "captcha"
ConfigKeyWxLogin = "wx_login"
ConfigKeyLicense = "license"
ConfigKeySms = "sms"
ConfigKeySmtp = "smtp"
ConfigKeyOss = "oss"
ConfigKeyPayment = "payment"
ConfigKeyModeration = "moderation"
)

View File

@@ -0,0 +1,55 @@
package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 文本审查
type ModerationConfig struct {
Enable bool `json:"enable"` // 是否启用文本审查
Active string `json:"active"`
GuidePrompt string `json:"guide_prompt"` // 模型引导提示词
Gitee ModerationGiteeConfig `json:"gitee"`
Baidu ModerationBaiduConfig `json:"baidu"`
Tencent ModerationTencentConfig `json:"tencent"`
}
const (
ModerationGitee = "gitee"
ModerationBaidu = "baidu"
ModerationTencent = "tencent"
)
// GiteeAI 文本审查配置
type ModerationGiteeConfig struct {
ApiKey string `json:"api_key"`
Model string `json:"model"` // 文本审核模型
}
// 百度文本审查配置
type ModerationBaiduConfig struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
}
// 腾讯云文本审查配置
type ModerationTencentConfig struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
}
type ModerationResult struct {
Flagged bool `json:"flagged"`
Categories map[string]bool `json:"categories"`
CategoryScores map[string]float64 `json:"category_scores"`
}
var ModerationCategories = map[string]string{
"politic": "内容涉及人物、事件或敏感的政治观点",
"porn": "明确的色情内容",
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
}

View File

@@ -8,11 +8,13 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/payment"
"geekai/service/sms"
@@ -26,21 +28,17 @@ import (
type ConfigHandler struct {
handler.BaseHandler
licenseService *service.LicenseService
sysConfig *types.SystemConfig
alipayService *payment.AlipayService
wxpayService *payment.WxPayService
epayService *payment.EPayService
smsAliyun *sms.AliYunSmsService
smsBao *sms.BaoSmsService
smsManager *sms.SmsManager
localOss *oss.LocalStorage
qiniuOss *oss.QiNiuOss
aliyunOss *oss.AliYunOss
minioOss *oss.MiniOss
smtpService *service.SmtpService
captchaService *service.CaptchaService
wxLoginService *service.WxLoginService
licenseService *service.LicenseService
sysConfig *types.SystemConfig
alipayService *payment.AlipayService
wxpayService *payment.WxPayService
epayService *payment.EPayService
smsManager *sms.SmsManager
uploaderManager *oss.UploaderManager
smtpService *service.SmtpService
captchaService *service.CaptchaService
wxLoginService *service.WxLoginService
moderationManager *moderation.ServiceManager
}
func NewConfigHandler(
@@ -51,34 +49,26 @@ func NewConfigHandler(
alipayService *payment.AlipayService,
wxpayService *payment.WxPayService,
epayService *payment.EPayService,
smsAliyun *sms.AliYunSmsService,
smsBao *sms.BaoSmsService,
smsManager *sms.SmsManager,
localOss *oss.LocalStorage,
qiniuOss *oss.QiNiuOss,
aliyunOss *oss.AliYunOss,
minioOss *oss.MiniOss,
uploaderManager *oss.UploaderManager,
smtpService *service.SmtpService,
captchaService *service.CaptchaService,
wxLoginService *service.WxLoginService,
moderationManager *moderation.ServiceManager,
) *ConfigHandler {
return &ConfigHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
licenseService: licenseService,
sysConfig: sysConfig,
alipayService: alipayService,
wxpayService: wxpayService,
epayService: epayService,
smsAliyun: smsAliyun,
smsBao: smsBao,
smsManager: smsManager,
localOss: localOss,
qiniuOss: qiniuOss,
aliyunOss: aliyunOss,
minioOss: minioOss,
smtpService: smtpService,
captchaService: captchaService,
wxLoginService: wxLoginService,
BaseHandler: handler.BaseHandler{App: app, DB: db},
licenseService: licenseService,
sysConfig: sysConfig,
alipayService: alipayService,
wxpayService: wxpayService,
epayService: epayService,
smsManager: smsManager,
uploaderManager: uploaderManager,
moderationManager: moderationManager,
smtpService: smtpService,
captchaService: captchaService,
wxLoginService: wxLoginService,
}
}
@@ -101,6 +91,8 @@ func (h *ConfigHandler) RegisterRoutes() {
rg.POST("update/sms", h.UpdateSms)
rg.POST("update/oss", h.UpdateOss)
rg.POST("update/smtp", h.UpdateStmp)
rg.POST("update/moderation", h.UpdateModeration)
rg.POST("moderation/test", h.TestModeration)
rg.GET("get", h.Get)
rg.POST("license/active", h.Active)
rg.GET("license/get", h.GetLicense)
@@ -280,14 +272,7 @@ func (h *ConfigHandler) UpdatePayment(c *gin.Context) {
return
}
var config model.Config
oldData := types.PaymentConfig{}
err := h.DB.Where("name", types.ConfigKeyPayment).First(&config).Error
if err == nil {
utils.JsonDecode(config.Value, &oldData)
}
err = h.Update(types.ConfigKeyPayment, data)
err := h.Update(types.ConfigKeyPayment, data)
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -324,32 +309,14 @@ func (h *ConfigHandler) UpdateSms(c *gin.Context) {
return
}
var config model.Config
oldData := types.SMSConfig{}
err := h.DB.Where("name", types.ConfigKeySms).First(&config).Error
if err == nil {
utils.JsonDecode(config.Value, &oldData)
}
err = h.Update(types.ConfigKeySms, data)
err := h.Update(types.ConfigKeySms, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新服务配置
switch data.Active {
case sms.Ali:
err = h.smsAliyun.UpdateConfig(&data.Ali)
if err != nil {
resp.ERROR(c, err.Error())
return
}
case sms.Bao:
h.smsBao.UpdateConfig(&data.Bao)
}
h.smsManager.SetActive(data.Active)
h.smsManager.UpdateConfig(data)
resp.SUCCESS(c, data)
}
@@ -362,39 +329,14 @@ func (h *ConfigHandler) UpdateOss(c *gin.Context) {
return
}
var config model.Config
oldData := types.OSSConfig{}
err := h.DB.Where("name", types.ConfigKeyOss).First(&config).Error
if err == nil {
utils.JsonDecode(config.Value, &oldData)
}
err = h.Update("oss", data)
err := h.Update(types.ConfigKeyOss, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新服务配置
switch data.Active {
case oss.Local:
h.localOss.UpdateConfig(&data.Local)
case oss.QiNiu:
h.qiniuOss.UpdateConfig(&data.QiNiu)
case oss.AliYun:
err := h.aliyunOss.UpdateConfig(&data.AliYun)
if err != nil {
resp.ERROR(c, err.Error())
return
}
case oss.Minio:
err := h.minioOss.UpdateConfig(&data.Minio)
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
h.uploaderManager.UpdateConfig(data)
h.sysConfig.OSS = data
resp.SUCCESS(c, data)
@@ -408,24 +350,14 @@ func (h *ConfigHandler) UpdateStmp(c *gin.Context) {
return
}
var config model.Config
oldData := types.SmtpConfig{}
err := h.DB.Where("name", types.ConfigKeySmtp).First(&config).Error
if err == nil {
utils.JsonDecode(config.Value, &oldData)
}
err = h.Update(types.ConfigKeySmtp, data)
err := h.Update(types.ConfigKeySmtp, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 配置发生改变时更新服务配置
if !data.Equal(&oldData) {
h.smtpService.UpdateConfig(&data)
}
// 更新服务配置
h.smtpService.UpdateConfig(&data)
h.sysConfig.SMTP = data
resp.SUCCESS(c, data)
}
@@ -519,4 +451,89 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
resp.SUCCESS(c, license)
}
//
// UpdateModeration 更新文本审查配置
func (h *ConfigHandler) UpdateModeration(c *gin.Context) {
var data types.ModerationConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyModeration, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
h.moderationManager.UpdateConfig(data)
h.sysConfig.Moderation = data
resp.SUCCESS(c, data)
}
// 测试结果类型,用于前端显示
type ModerationTestResult struct {
IsAbnormal bool `json:"isAbnormal"`
Details []ModerationTestDetail `json:"details"`
}
type ModerationTestDetail struct {
Category string `json:"category"`
Description string `json:"description"`
Confidence string `json:"confidence"`
IsCategory bool `json:"isCategory"`
}
// TestModeration 测试文本审查服务
func (h *ConfigHandler) TestModeration(c *gin.Context) {
var data struct {
Text string `json:"text"`
Service string `json:"service"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Text == "" {
resp.ERROR(c, "测试文本不能为空")
return
}
// 检查是否启用了文本审查
if !h.sysConfig.Moderation.Enable {
resp.ERROR(c, "文本审查服务未启用")
return
}
// 获取当前激活的审核服务
service := h.moderationManager.GetService()
// 执行文本审核
result, err := service.Moderate(data.Text)
if err != nil {
resp.ERROR(c, "审核服务调用失败: "+err.Error())
return
}
// 转换为前端需要的格式
testResult := ModerationTestResult{
IsAbnormal: result.Flagged,
Details: make([]ModerationTestDetail, 0),
}
// 构建详细信息
for category, description := range types.ModerationCategories {
score := result.CategoryScores[category]
isCategory := result.Categories[category]
testResult.Details = append(testResult.Details, ModerationTestDetail{
Category: category,
Description: description,
Confidence: fmt.Sprintf("%.2f", score),
IsCategory: isCategory,
})
}
resp.SUCCESS(c, testResult)
}

View File

@@ -19,6 +19,7 @@ import (
"geekai/service/dalle"
"geekai/service/jimeng"
"geekai/service/mj"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/payment"
"geekai/service/sd"
@@ -241,6 +242,12 @@ func main() {
// 用户服务
fx.Provide(service.NewUserService),
// 文本审查服务
fx.Provide(moderation.NewGiteeAIModeration),
fx.Provide(moderation.NewBaiduAIModeration),
fx.Provide(moderation.NewTencentAIModeration),
fx.Provide(moderation.NewServiceManager),
// 注册路由
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
h.RegisterRoutes()

View File

@@ -0,0 +1,33 @@
package moderation
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"geekai/core/types"
)
type BaiduAIModeration struct {
config types.ModerationBaiduConfig
}
func NewBaiduAIModeration(sysConfig *types.SystemConfig) *BaiduAIModeration {
return &BaiduAIModeration{
config: sysConfig.Moderation.Baidu,
}
}
func (s *BaiduAIModeration) UpdateConfig(config types.ModerationBaiduConfig) {
s.config = config
}
func (s *BaiduAIModeration) Moderate(text string) (types.ModerationResult, error) {
return types.ModerationResult{}, errors.New("not implemented")
}
var _ Service = (*BaiduAIModeration)(nil)

View File

@@ -0,0 +1,58 @@
package moderation
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"geekai/core/types"
"github.com/imroc/req/v3"
)
type GiteeAIModeration struct {
config types.ModerationGiteeConfig
apiURL string
}
func NewGiteeAIModeration(sysConfig *types.SystemConfig) *GiteeAIModeration {
return &GiteeAIModeration{
config: sysConfig.Moderation.Gitee,
apiURL: "https://ai.gitee.com/v1/moderations",
}
}
func (s *GiteeAIModeration) UpdateConfig(config types.ModerationGiteeConfig) {
s.config = config
}
type GiteeAIModerationResult struct {
ID string `json:"id"`
Model string `json:"model"`
Results []types.ModerationResult `json:"results"`
}
func (s *GiteeAIModeration) Moderate(text string) (types.ModerationResult, error) {
body := map[string]any{
"input": text,
"model": s.config.Model,
}
var res GiteeAIModerationResult
r, err := req.C().R().SetHeader("Authorization", "Bearer "+s.config.ApiKey).SetBody(body).SetSuccessResult(&res).Post(s.apiURL)
if err != nil {
return types.ModerationResult{}, err
}
if r.IsErrorState() {
return types.ModerationResult{}, errors.New(r.String())
}
return res.Results[0], nil
}
var _ Service = (*GiteeAIModeration)(nil)

View File

@@ -0,0 +1,58 @@
package moderation
import (
"geekai/core/types"
logger2 "geekai/logger"
)
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
var logger = logger2.GetLogger()
type Service interface {
Moderate(text string) (types.ModerationResult, error)
}
type ServiceManager struct {
gitee *GiteeAIModeration
baidu *BaiduAIModeration
tencent *TencentAIModeration
active string
}
func NewServiceManager(gitee *GiteeAIModeration, baidu *BaiduAIModeration, tencent *TencentAIModeration) *ServiceManager {
return &ServiceManager{
gitee: gitee,
baidu: baidu,
tencent: tencent,
}
}
func (s *ServiceManager) GetService() Service {
switch s.active {
case types.ModerationBaidu:
return s.baidu
case types.ModerationTencent:
return s.tencent
default:
return s.gitee
}
}
func (s *ServiceManager) UpdateConfig(config types.ModerationConfig) {
switch config.Active {
case types.ModerationGitee:
s.gitee.UpdateConfig(config.Gitee)
case types.ModerationBaidu:
s.baidu.UpdateConfig(config.Baidu)
case types.ModerationTencent:
s.tencent.UpdateConfig(config.Tencent)
}
s.active = config.Active
}

View File

@@ -0,0 +1,33 @@
package moderation
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"geekai/core/types"
)
type TencentAIModeration struct {
config types.ModerationTencentConfig
}
func NewTencentAIModeration(sysConfig *types.SystemConfig) *TencentAIModeration {
return &TencentAIModeration{
config: sysConfig.Moderation.Tencent,
}
}
func (s *TencentAIModeration) UpdateConfig(config types.ModerationTencentConfig) {
s.config = config
}
func (s *TencentAIModeration) Moderate(text string) (types.ModerationResult, error) {
return types.ModerationResult{}, errors.New("not implemented")
}
var _ Service = (*TencentAIModeration)(nil)

View File

@@ -23,7 +23,7 @@ import (
)
type AliYunOss struct {
config *types.AliYunOssConfig
config types.AliYunOssConfig
bucket *oss.Bucket
proxyURL string
}
@@ -33,7 +33,7 @@ func NewAliYunOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*A
proxyURL: appConfig.ProxyURL,
}
if sysConfig.OSS.Active == AliYun {
err := s.UpdateConfig(&sysConfig.OSS.AliYun)
err := s.UpdateConfig(sysConfig.OSS.AliYun)
if err != nil {
logger.Errorf("阿里云OSS初始化失败: %v", err)
}
@@ -42,7 +42,7 @@ func NewAliYunOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*A
}
func (s *AliYunOss) UpdateConfig(config *types.AliYunOssConfig) error {
func (s *AliYunOss) UpdateConfig(config types.AliYunOssConfig) error {
client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
if err != nil {
return err

View File

@@ -21,18 +21,18 @@ import (
)
type LocalStorage struct {
config *types.LocalStorageConfig
config types.LocalStorageConfig
proxyURL string
}
func NewLocalStorage(sysConfig *types.SystemConfig, appConfig *types.AppConfig) *LocalStorage {
return &LocalStorage{
config: &sysConfig.OSS.Local,
config: sysConfig.OSS.Local,
proxyURL: appConfig.ProxyURL,
}
}
func (s *LocalStorage) UpdateConfig(config *types.LocalStorageConfig) {
func (s *LocalStorage) UpdateConfig(config types.LocalStorageConfig) {
s.config = config
}

View File

@@ -24,7 +24,7 @@ import (
)
type MiniOss struct {
config *types.MiniOssConfig
config types.MiniOssConfig
client *minio.Client
proxyURL string
}
@@ -33,7 +33,7 @@ func NewMiniOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*Min
s := &MiniOss{proxyURL: appConfig.ProxyURL}
if sysConfig.OSS.Active == Minio {
err := s.UpdateConfig(&sysConfig.OSS.Minio)
err := s.UpdateConfig(sysConfig.OSS.Minio)
if err != nil {
logger.Errorf("MinioOSS初始化失败: %v", err)
}
@@ -41,7 +41,7 @@ func NewMiniOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*Min
return s, nil
}
func (s *MiniOss) UpdateConfig(config *types.MiniOssConfig) error {
func (s *MiniOss) UpdateConfig(config types.MiniOssConfig) error {
minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
Secure: config.UseSSL,

View File

@@ -25,7 +25,7 @@ import (
)
type QiNiuOss struct {
config *types.QiNiuOssConfig
config types.QiNiuOssConfig
mac *qbox.Mac
putPolicy storage.PutPolicy
uploader *storage.FormUploader
@@ -38,12 +38,12 @@ func NewQiNiuOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) *QiN
proxyURL: appConfig.ProxyURL,
}
if sysConfig.OSS.Active == QiNiu {
s.UpdateConfig(&sysConfig.OSS.QiNiu)
s.UpdateConfig(sysConfig.OSS.QiNiu)
}
return s
}
func (s *QiNiuOss) UpdateConfig(config *types.QiNiuOssConfig) {
func (s *QiNiuOss) UpdateConfig(config types.QiNiuOssConfig) {
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
if !ok {
zone = storage.ZoneHuanan

View File

@@ -21,7 +21,7 @@ type UploaderManager struct {
aliyun *AliYunOss
mini *MiniOss
qiniu *QiNiuOss
config *types.OSSConfig
active string
}
func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QiNiuOss) (*UploaderManager, error) {
@@ -31,7 +31,7 @@ func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliy
sysConfig.OSS.Active = strings.ToLower(sysConfig.OSS.Active)
return &UploaderManager{
config: &sysConfig.OSS,
active: sysConfig.OSS.Active,
local: local,
aliyun: aliyun,
mini: mini,
@@ -40,7 +40,7 @@ func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliy
}
func (m *UploaderManager) GetUploadHandler() Uploader {
switch m.config.Active {
switch m.active {
case Local:
return m.local
case AliYun:
@@ -52,3 +52,17 @@ func (m *UploaderManager) GetUploadHandler() Uploader {
}
return m.local
}
func (m *UploaderManager) UpdateConfig(config types.OSSConfig) {
switch config.Active {
case Local:
m.local.UpdateConfig(config.Local)
case AliYun:
m.aliyun.UpdateConfig(config.AliYun)
case Minio:
m.mini.UpdateConfig(config.Minio)
case QiNiu:
m.qiniu.UpdateConfig(config.QiNiu)
}
m.active = config.Active
}

View File

@@ -15,14 +15,14 @@ import (
)
type AliYunSmsService struct {
config *types.SmsConfigAli
config types.SmsConfigAli
client *dysmsapi.Client
domain string
zoneId string
}
func NewAliYunSmsService(sysConfig *types.SystemConfig) (*AliYunSmsService, error) {
config := &sysConfig.SMS.Ali
config := sysConfig.SMS.Ali
domain := "dysmsapi.aliyuncs.com"
zoneId := "cn-hangzhou"
@@ -40,7 +40,7 @@ func NewAliYunSmsService(sysConfig *types.SystemConfig) (*AliYunSmsService, erro
return &s, nil
}
func (s *AliYunSmsService) UpdateConfig(config *types.SmsConfigAli) error {
func (s *AliYunSmsService) UpdateConfig(config types.SmsConfigAli) error {
client, err := dysmsapi.NewClientWithAccessKey(
s.zoneId,
config.AccessKey,

View File

@@ -19,18 +19,18 @@ import (
)
type BaoSmsService struct {
config *types.SmsConfigBao
config types.SmsConfigBao
domain string
}
func NewBaoSmsService(sysConfig *types.SystemConfig) *BaoSmsService {
return &BaoSmsService{
config: &sysConfig.SMS.Bao,
config: sysConfig.SMS.Bao,
domain: "api.smsbao.com",
}
}
func (s *BaoSmsService) UpdateConfig(config *types.SmsConfigBao) {
func (s *BaoSmsService) UpdateConfig(config types.SmsConfigBao) {
s.config = config
}

View File

@@ -30,12 +30,25 @@ func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao
}
func (m *SmsManager) GetService() Service {
if m.active == Ali {
switch m.active {
case Ali:
return m.aliyun
case Bao:
return m.bao
}
return m.bao
return nil
}
func (m *SmsManager) SetActive(active string) {
m.active = active
}
func (m *SmsManager) UpdateConfig(config types.SMSConfig) {
switch config.Active {
case Ali:
m.aliyun.UpdateConfig(config.Ali)
case Bao:
m.bao.UpdateConfig(config.Bao)
}
m.active = config.Active
}

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1754626711656') format('woff2'),
url('iconfont.woff?t=1754626711656') format('woff'),
url('iconfont.ttf?t=1754626711656') format('truetype');
src: url('iconfont.woff2?t=1756631578371') format('woff2'),
url('iconfont.woff?t=1756631578371') format('woff'),
url('iconfont.ttf?t=1756631578371') format('truetype');
}
.iconfont {
@@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-tencent:before {
content: "\e655";
}
.icon-baidu:before {
content: "\e656";
}
.icon-moderation:before {
content: "\e6c6";
}
.icon-back-bold:before {
content: "\e654";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,27 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "3547761",
"name": "tencent",
"font_class": "tencent",
"unicode": "e655",
"unicode_decimal": 58965
},
{
"icon_id": "26267540",
"name": "baidu",
"font_class": "baidu",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "31975090",
"name": "Content Moderation",
"font_class": "moderation",
"unicode": "e6c6",
"unicode_decimal": 59078
},
{
"icon_id": "27025075",
"name": "返回",

Binary file not shown.

View File

@@ -36,7 +36,14 @@
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ finalTokens }}</span>
<span class="bar-item">
<el-tooltip class="box-item" effect="dark" content="复制" placement="bottom">
<i
class="iconfont icon-copy cursor-pointer"
@click="copyContent(data.content.text)"
></i>
</el-tooltip>
</span>
</div>
</div>
</div>
@@ -81,7 +88,14 @@
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
<span class="bar-item">
<el-tooltip class="box-item" effect="dark" content="复制" placement="bottom">
<i
class="iconfont icon-copy cursor-pointer"
@click="copyContent(data.content.text)"
></i>
</el-tooltip>
</span>
</div>
</div>
</div>
@@ -90,6 +104,7 @@
<script setup>
import { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
import { showMessageSuccess } from '@/utils/dialog'
import { dateFormat, isImage, processPrompt } from '@/utils/libs'
import { Clock } from '@element-plus/icons-vue'
import hl from 'highlight.js'
@@ -167,6 +182,10 @@ const processFiles = () => {
const isExternalImg = (link, files) => {
return isImage(link) && !files.find((file) => file.url === link)
}
const copyContent = (text) => {
navigator.clipboard.writeText(text)
showMessageSuccess('复制成功')
}
</script>
<style lang="scss">

View File

@@ -180,6 +180,18 @@ const items = [
],
},
{
icon: 'moderation',
index: '/admin/config/moderation',
title: '文本审查',
subs: [
{
icon: 'config',
index: '/admin/config/moderation',
title: '审查配置',
},
],
},
{
icon: 'role',
index: '/admin/manger',

View File

@@ -209,6 +209,12 @@ const routes = [
meta: { title: '插件配置' },
component: () => import('@/views/admin/settings/PluginConfig.vue'),
},
{
path: '/admin/config/moderation',
name: 'admin-config-moderation',
meta: { title: '文本审查配置' },
component: () => import('@/views/admin/settings/ModerationConfig.vue'),
},
{
path: '/admin/config/markmap',
name: 'admin-config-markmap',

View File

@@ -0,0 +1,362 @@
<template>
<div class="settings container p-5">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane name="gitee">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-gitee"></i>
<span class="ml-2">模力方舟</span>
</div>
</template>
<Alert type="info">
如果你不知道怎么获取这些配置信息请参考文档
<a href="https://ai.gitee.com/docs/organization/access-token" target="_blank"
>模力方舟访问令牌配置</a
>
</Alert>
<el-form :model="configs.gitee" label-position="top">
<el-form-item label="API密钥">
<el-input v-model="configs.gitee.api_key" placeholder="请输入API密钥" />
</el-form-item>
<el-form-item label="模型">
<el-select v-model="configs.gitee.model" placeholder="请选择模型">
<el-option v-for="v in models" :value="v.value" :label="v.label" :key="v.value">
{{ v.label }}
</el-option>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane name="baidu">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-baidu"></i>
<span class="ml-2">百度</span>
</div>
</template>
<Alert type="warning"> 百度文本审查服务暂未实现 </Alert>
<el-form :model="configs.baidu" label-position="top">
<el-form-item label="AccessKey">
<el-input v-model="configs.baidu.access_key" placeholder="请输入AccessKey" />
</el-form-item>
<el-form-item label="SecretKey">
<el-input v-model="configs.baidu.secret_key" placeholder="请输入SecretKey" />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane name="tencent">
<template #label>
<div class="flex items-center">
<i class="iconfont icon-tencent"></i>
<span class="ml-2">腾讯云</span>
</div>
</template>
<Alert type="warning"> 腾讯云文本审查服务暂未实现 </Alert>
<el-form :model="configs.baidu" label-position="top">
<el-form-item label="AccessKey">
<el-input v-model="configs.baidu.access_key" placeholder="请输入AccessKey" />
</el-form-item>
<el-form-item label="SecretKey">
<el-input v-model="configs.baidu.secret_key" placeholder="请输入SecretKey" />
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<el-form :model="configs" label-position="top" class="py-5">
<el-form-item label="启用文本审查">
<el-switch v-model="configs.enable" />
</el-form-item>
<el-form-item>
<template #label>
<span class="mr-2">大模型引导提示词</span>
<el-tooltip
effect="dark"
content="大模型引导提示词,用于引导大模型进行文本审查<br/>如果为空,则不使用大模型引导提示词"
placement="right"
raw-content
>
<i class="iconfont icon-info"></i>
</el-tooltip>
</template>
<el-input
v-model="configs.guide_prompt"
type="textarea"
:rows="3"
placeholder="请输入大模型引导提示词"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-2">选择审查服务</span>
<el-tooltip
effect="dark"
content="只有当文本审查启用时,选择审查服务才会生效"
placement="right"
>
<i class="iconfont icon-info"></i>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="configs.active" size="large">
<el-radio value="gitee" border>模力方舟</el-radio>
<el-radio value="baidu" border>百度</el-radio>
<el-radio value="tencent" border>腾讯云</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div class="flex justify-left">
<el-button type="primary" @click="saveModerationConfig" :loading="loading"
>提交保存</el-button
>
</div>
<div class="mt-7">
<el-card shadow="never" class="mb-6">
<el-form :model="testForm" label-position="top">
<el-form-item label="测试文本">
<el-input
v-model="testForm.text"
type="textarea"
:rows="4"
placeholder="请输入要测试的文本内容"
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item>
<el-button
type="success"
@click="testModeration"
:loading="testLoading"
:disabled="!testForm.text.trim()"
>
提交测试
</el-button>
</el-form-item>
</el-form>
<!-- 测试结果显示 -->
<div v-if="testResult" class="test-result">
<div class="result-header mb-4">
<div class="flex items-center py-2">
<span class="text-base font-semibold mr-2">检测结果:</span>
<el-tag :type="testResult.isAbnormal ? 'danger' : 'success'" size="large">
<i
class="iconfont"
:class="testResult.isAbnormal ? 'icon-error' : 'icon-success'"
></i>
<span class="text-sm ml-2">{{ testResult.isAbnormal ? '异常' : '正常' }}</span>
</el-tag>
</div>
<p class="text-sm text-gray-500 mt-2">检测结果仅供参考</p>
</div>
<el-table :data="testResult.details" border stripe class="result-table">
<el-table-column prop="category" label="类别" width="120">
<template #default="{ row }">
<span class="font-medium">{{ row.category }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="300">
<template #default="{ row }">
<span class="text-gray-700">{{ row.description }}</span>
</template>
</el-table-column>
<el-table-column prop="confidence" label="置信度" width="100" align="center">
<template #default="{ row }">
<span class="font-mono">{{ row.confidence }}</span>
</template>
</el-table-column>
<el-table-column prop="isCategory" label="是否为该类别" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.isCategory ? 'danger' : 'success'" size="small">
{{ row.isCategory ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import Alert from '@/components/ui/Alert.vue'
import { showMessageError } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { onMounted, ref, watch } from 'vue'
const loading = ref(false)
const activeTab = ref('gitee')
const configs = ref({
enable: false,
active: 'gitee',
guide_prompt:
'请拒绝输出任何有关色情,暴力相关内容,禁止输出跟中国政治相关的内容,比如政治敏感事件,国家领导人敏感信息等相关的内容。任何时刻都必须牢记这一原则。',
gitee: {
api_key: '',
model: 'Security-semantic-filtering',
},
baidu: {
access_key: '',
secret_key: '',
},
tencent: {
access_key: '',
secret_key: '',
},
})
// 测试相关数据
const testLoading = ref(false)
const testForm = ref({
text: '',
})
const testResult = ref(null)
const models = ref([
{
label: '违规文本检测模型:限时免费',
value: 'Security-semantic-filtering',
},
{
label: '文本审核模型0.0002元/条',
value: 'moark-text-moderation',
},
])
onMounted(async () => {
try {
const res = await httpGet('/api/admin/config/get?key=moderation')
configs.value = Object.assign(configs.value, res.data)
} catch (e) {
// 使用默认值
showMessageError('加载文本审查配置失败: ' + e.message)
}
})
// 监听tab切换清空测试结果
watch(activeTab, (newTab) => {
if (newTab !== 'test') {
testResult.value = null
testForm.value.text = ''
}
})
const saveModerationConfig = async () => {
loading.value = true
try {
await httpPost('/api/admin/config/update/moderation', configs.value)
ElMessage.success('保存成功')
} catch (e) {
ElMessage.error('保存失败:' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
// 测试文本审核服务
const testModeration = async () => {
if (!testForm.value.text.trim()) {
ElMessage.warning('请输入测试文本')
return
}
// 检查是否启用了文本审查
if (!configs.value.enable) {
ElMessage.warning('请先启用文本审查服务')
return
}
testLoading.value = true
try {
const res = await httpPost('/api/admin/config/moderation/test', {
text: testForm.value.text.trim(),
service: configs.value.active,
})
// 处理测试结果
testResult.value = {
isAbnormal: res.data.isAbnormal || false,
details: res.data.details || [],
}
ElMessage.success('测试完成')
// 清空输入框,提升用户体验
testForm.value.text = ''
} catch (e) {
ElMessage.error('测试失败:' + (e.message || '未知错误'))
// 清空之前的结果
testResult.value = null
} finally {
testLoading.value = false
}
}
</script>
<style lang="scss">
.settings {
a {
color: #409eff;
&:hover {
text-decoration: underline;
}
}
.el-form-item__label {
font-weight: 700;
}
}
// 测试相关样式
.test-result {
.result-header {
.status-badge {
display: inline-block;
margin-left: 12px;
.status-tag {
font-size: 14px;
padding: 8px 16px;
.iconfont {
margin-right: 6px;
}
}
}
}
.result-table {
.el-table__header {
background-color: #f5f7fa;
th {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
}
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
</style>