feat(moderation): add Tencent TMS as fallback for AI content moderation
Some checks failed
Build Backend / build (push) Failing after 45s
Build Backend / build-docker (push) Has been skipped

Add Tencent Cloud Text Moderation System (TMS) as a fallback moderation
service when OpenAI is unavailable or returns an error. The service
now checks OpenAI first, and falls back to Tencent TMS if it fails,
providing better reliability for content moderation.

Features:
- New Tencent TMS configuration in config.yaml with environment variable overrides
- New tencent package in internal/pkg/tencent/ with TMS client implementation
- PostAIService now uses dual moderation with OpenAI primary and Tencent fallback
- Strict mode configuration passed to PostAIService for consistent behavior

BREAKING CHANGE: NewPostAIService now requires tencentClient and strictMode
parameters - update wire configuration accordingly.
This commit is contained in:
lafay
2026-04-21 22:31:30 +08:00
parent 90495385cd
commit 38d628c696
10 changed files with 546 additions and 48 deletions

115
.kilo/package-lock.json generated Normal file
View File

@@ -0,0 +1,115 @@
{
"name": ".kilo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.10"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.10",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.10.tgz",
"integrity": "sha512-VJPhJC+E5WWu7XgEJzrVOxKJlwJ+OATwxEzgjqEPj8KN5N38YxUPBY/rzUTjv90x7nkzyk1rFGfCVqXdA/Koug==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.10",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.10",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.10.tgz",
"integrity": "sha512-H6jGXYAhN/yjOGX3MRZ0OxyEAuRGY3VOwDbLTh4O6ljpgutFHaLvomDZ82qNVy7gl7AjJgi3SAQAt9UQpeGl/w==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -54,7 +54,8 @@ func InitializeApp() (*App, error) {
userProfileAuditRepository := repository.NewUserProfileAuditRepository(db)
manager := wire.ProvideHookManager()
openaiClient := wire.ProvideOpenAIClient(config)
postAIService := wire.ProvidePostAIService(openaiClient)
tencentClient := wire.ProvideTencentClient(config)
postAIService := wire.ProvidePostAIService(openaiClient, tencentClient, config)
commentRepository := repository.NewCommentRepository(db)
sensitiveWordRepository := repository.NewSensitiveWordRepository(db)
sensitiveService := wire.ProvideSensitiveService(sensitiveWordRepository, client, config)

View File

@@ -176,6 +176,19 @@ openai:
request_timeout: 30
strict_moderation: false
# 腾讯云文本内容安全配置(作为 OpenAI 备用)
# 当 OpenAI 不可用或审核失败时使用腾讯TMS进行文本审核
# 环境变量:
# APP_TENCENT_TMS_ENABLED, APP_TENCENT_TMS_SECRET_ID, APP_TENCENT_TMS_SECRET_KEY
# APP_TENCENT_TMS_REGION, APP_TENCENT_TMS_BIZ_TYPE, APP_TENCENT_TMS_TIMEOUT
tencent_tms:
enabled: false
secret_id: ""
secret_key: ""
region: "ap-guangzhou"
biz_type: "" # 可选,使用控制台配置的自定义策略
timeout: 30
# SMTP发信配置gomail.v2
# 环境变量:
# APP_EMAIL_ENABLED, APP_EMAIL_HOST, APP_EMAIL_PORT

View File

@@ -23,6 +23,7 @@ type Config struct {
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Upload UploadConfig `mapstructure:"upload"`
OpenAI OpenAIConfig `mapstructure:"openai"`
TencentTMS TencentTMSConfig `mapstructure:"tencent_tms"`
Email EmailConfig `mapstructure:"email"`
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
GRPC GRPCConfig `mapstructure:"grpc"`
@@ -111,6 +112,12 @@ func Load(configPath string) (*Config, error) {
viper.SetDefault("openai.moderation_max_images_per_request", 1)
viper.SetDefault("openai.request_timeout", 30)
viper.SetDefault("openai.strict_moderation", false)
viper.SetDefault("tencent_tms.enabled", false)
viper.SetDefault("tencent_tms.secret_id", "")
viper.SetDefault("tencent_tms.secret_key", "")
viper.SetDefault("tencent_tms.region", "ap-guangzhou")
viper.SetDefault("tencent_tms.biz_type", "")
viper.SetDefault("tencent_tms.timeout", 30)
viper.SetDefault("email.enabled", false)
viper.SetDefault("email.host", "")
viper.SetDefault("email.port", 587)
@@ -221,6 +228,13 @@ func Load(configPath string) (*Config, error) {
cfg.OpenAI.RequestTimeout, _ = strconv.Atoi(getEnvOrDefault("APP_OPENAI_REQUEST_TIMEOUT", fmt.Sprintf("%d", cfg.OpenAI.RequestTimeout)))
cfg.OpenAI.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_ENABLED", fmt.Sprintf("%t", cfg.OpenAI.Enabled)))
cfg.OpenAI.StrictModeration, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_STRICT_MODERATION", fmt.Sprintf("%t", cfg.OpenAI.StrictModeration)))
// TencentTMS 环境变量覆盖
cfg.TencentTMS.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_TENCENT_TMS_ENABLED", fmt.Sprintf("%t", cfg.TencentTMS.Enabled)))
cfg.TencentTMS.SecretID = getEnvOrDefault("APP_TENCENT_TMS_SECRET_ID", cfg.TencentTMS.SecretID)
cfg.TencentTMS.SecretKey = getEnvOrDefault("APP_TENCENT_TMS_SECRET_KEY", cfg.TencentTMS.SecretKey)
cfg.TencentTMS.Region = getEnvOrDefault("APP_TENCENT_TMS_REGION", cfg.TencentTMS.Region)
cfg.TencentTMS.BizType = getEnvOrDefault("APP_TENCENT_TMS_BIZ_TYPE", cfg.TencentTMS.BizType)
cfg.TencentTMS.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_TENCENT_TMS_TIMEOUT", fmt.Sprintf("%d", cfg.TencentTMS.Timeout)))
cfg.Email.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_ENABLED", fmt.Sprintf("%t", cfg.Email.Enabled)))
cfg.Email.Host = getEnvOrDefault("APP_EMAIL_HOST", cfg.Email.Host)
cfg.Email.Port, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_PORT", fmt.Sprintf("%d", cfg.Email.Port)))

View File

@@ -24,3 +24,13 @@ type OpenAIConfig struct {
RequestTimeout int `mapstructure:"request_timeout"`
StrictModeration bool `mapstructure:"strict_moderation"`
}
// TencentTMSConfig 腾讯云文本内容安全配置(作为 OpenAI 审核的备用)
type TencentTMSConfig struct {
Enabled bool `mapstructure:"enabled"`
SecretID string `mapstructure:"secret_id"`
SecretKey string `mapstructure:"secret_key"`
Region string `mapstructure:"region"`
BizType string `mapstructure:"biz_type"`
Timeout int `mapstructure:"timeout"`
}

View File

@@ -0,0 +1,193 @@
package tencent
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"go.uber.org/zap"
)
const (
host = "tms.tencentcloudapi.com"
service = "tms"
version = "2020-12-29"
action = "TextModeration"
algorithm = "TC3-HMAC-SHA256"
)
type Client interface {
IsEnabled() bool
TextModeration(ctx context.Context, content string) (*ModerationResponse, error)
}
type clientImpl struct {
cfg Config
httpClient *http.Client
}
func NewClient(cfg Config) Client {
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 30
}
return &clientImpl{
cfg: cfg,
httpClient: &http.Client{
Timeout: time.Duration(timeout) * time.Second,
},
}
}
func (c *clientImpl) IsEnabled() bool {
return c.cfg.Enabled && c.cfg.SecretID != "" && c.cfg.SecretKey != ""
}
func (c *clientImpl) TextModeration(ctx context.Context, content string) (*ModerationResponse, error) {
if !c.IsEnabled() {
return nil, fmt.Errorf("tencent tms client is not enabled")
}
if strings.TrimSpace(content) == "" {
return &ModerationResponse{Suggestion: SuggestionPass}, nil
}
encodedContent := base64.StdEncoding.EncodeToString([]byte(content))
reqBody := textModerationRequest{
Content: encodedContent,
BizType: c.cfg.BizType,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
timestamp := time.Now().Unix()
authorization, err := c.sign(string(payload), timestamp)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+host, strings.NewReader(string(payload)))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", authorization)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Host", host)
req.Header.Set("X-TC-Action", action)
req.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("X-TC-Version", version)
req.Header.Set("X-TC-Region", c.cfg.Region)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request tencent tms: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("tencent tms error status=%d body=%s", resp.StatusCode, string(body))
}
var apiResp tencentAPIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if apiResp.Response.Error != nil {
return nil, fmt.Errorf("tencent tms api error: code=%s message=%s",
apiResp.Response.Error.Code,
apiResp.Response.Error.Message)
}
keywords := make([]string, 0, len(apiResp.Response.Keywords))
for _, kw := range apiResp.Response.Keywords {
keywords = append(keywords, kw.Word)
}
result := &ModerationResponse{
Suggestion: ModerationSuggestion(apiResp.Response.Suggestion),
Label: apiResp.Response.Label,
RiskLevel: apiResp.Response.RiskLevel,
Keywords: keywords,
RequestID: apiResp.Response.RequestID,
}
zap.L().Debug("Tencent TMS moderation result",
zap.String("suggestion", string(result.Suggestion)),
zap.String("label", result.Label),
zap.String("risk_level", result.RiskLevel),
zap.Strings("keywords", result.Keywords),
zap.String("request_id", result.RequestID),
)
return result, nil
}
func (c *clientImpl) sign(payload string, timestamp int64) (string, error) {
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
canonicalHeaders := "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n"
signedHeaders := "content-type;host"
hashedPayload := sha256hex(payload)
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
"POST",
"/",
"",
canonicalHeaders,
signedHeaders,
hashedPayload,
)
credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, service)
hashedCanonicalRequest := sha256hex(canonicalRequest)
string2sign := fmt.Sprintf("%s\n%d\n%s\n%s",
algorithm,
timestamp,
credentialScope,
hashedCanonicalRequest,
)
secretDate := hmacsha256(date, []byte("TC3"+c.cfg.SecretKey))
secretService := hmacsha256(service, secretDate)
secretSigning := hmacsha256("tc3_request", secretService)
signature := hex.EncodeToString(hmacsha256(string2sign, secretSigning))
authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
algorithm,
c.cfg.SecretID,
credentialScope,
signedHeaders,
signature,
)
return authorization, nil
}
func sha256hex(s string) string {
b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:])
}
func hmacsha256(s string, key []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write([]byte(s))
return h.Sum(nil)
}

View File

@@ -0,0 +1,58 @@
package tencent
type Config struct {
Enabled bool
SecretID string
SecretKey string
Region string
BizType string
Timeout int
}
type ModerationSuggestion string
const (
SuggestionPass ModerationSuggestion = "Pass"
SuggestionReview ModerationSuggestion = "Review"
SuggestionBlock ModerationSuggestion = "Block"
)
type ModerationResponse struct {
Suggestion ModerationSuggestion
Label string
RiskLevel string
Keywords []string
RequestID string
}
type textModerationRequest struct {
Content string `json:"Content"`
BizType string `json:"BizType,omitempty"`
}
type tencentAPIResponse struct {
Response struct {
Suggestion string `json:"Suggestion"`
Label string `json:"Label"`
RiskLevel string `json:"RiskLevel"`
Keywords []struct {
Word string `json:"Word"`
} `json:"Keywords,omitempty"`
RequestID string `json:"RequestId"`
Error *struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"Error,omitempty"`
} `json:"Response"`
}
func ConfigFromAppConfig(cfg *Config) Config {
return Config{
Enabled: cfg.Enabled,
SecretID: cfg.SecretID,
SecretKey: cfg.SecretKey,
Region: cfg.Region,
BizType: cfg.BizType,
Timeout: cfg.Timeout,
}
}

View File

@@ -2,9 +2,11 @@ package service
import (
"context"
"fmt"
"strings"
"carrot_bbs/internal/pkg/openai"
"carrot_bbs/internal/pkg/tencent"
"go.uber.org/zap"
)
@@ -171,32 +173,49 @@ func (e *BioModerationReviewError) IsReview() bool {
type PostAIService struct {
openAIClient openai.Client
tencentClient tencent.Client
strictMode bool
}
func NewPostAIService(openAIClient openai.Client) *PostAIService {
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) *PostAIService {
return &PostAIService{
openAIClient: openAIClient,
tencentClient: tencentClient,
strictMode: strictMode,
}
}
func (s *PostAIService) IsEnabled() bool {
if s == nil {
return false
}
return (s.openAIClient != nil && s.openAIClient.IsEnabled()) ||
(s.tencentClient != nil && s.tencentClient.IsEnabled())
}
func (s *PostAIService) isOpenAIEnabled() bool {
return s != nil && s.openAIClient != nil && s.openAIClient.IsEnabled()
}
func (s *PostAIService) isTencentEnabled() bool {
return s != nil && s.tencentClient != nil && s.tencentClient.IsEnabled()
}
func (s *PostAIService) ModeratePost(ctx context.Context, title, content string, images []string) error {
if !s.IsEnabled() {
return nil
}
if s.isOpenAIEnabled() {
resp, err := s.openAIClient.ModeratePost(ctx, title, content, images)
if err != nil {
if s.openAIClient.Config().StrictModeration {
return err
}
zap.L().Warn("AI moderation failed, fallback allow",
zap.L().Warn("AI post moderation failed, trying Tencent fallback",
zap.Error(err),
)
return nil
return s.moderateTextWithTencent(ctx, title+" "+content,
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
)
}
switch resp.Result {
@@ -209,20 +228,27 @@ func (s *PostAIService) ModeratePost(ctx context.Context, title, content string,
}
}
return s.moderateTextWithTencent(ctx, title+" "+content,
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
)
}
func (s *PostAIService) ModerateComment(ctx context.Context, content string, images []string) error {
if !s.IsEnabled() {
return nil
}
if s.isOpenAIEnabled() {
resp, err := s.openAIClient.ModerateComment(ctx, content, images)
if err != nil {
if s.openAIClient.Config().StrictModeration {
return err
}
zap.L().Warn("AI comment moderation failed, fallback allow",
zap.L().Warn("AI comment moderation failed, trying Tencent fallback",
zap.Error(err),
)
return nil
return s.moderateTextWithTencent(ctx, content,
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
)
}
switch resp.Result {
@@ -235,14 +261,20 @@ func (s *PostAIService) ModerateComment(ctx context.Context, content string, ima
}
}
return s.moderateTextWithTencent(ctx, content,
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
)
}
func (s *PostAIService) ModerateImage(ctx context.Context, imageURL string) error {
if !s.IsEnabled() {
if !s.isOpenAIEnabled() {
return nil
}
resp, err := s.openAIClient.ModerateImage(ctx, imageURL)
if err != nil {
if s.openAIClient.Config().StrictModeration {
if s.strictMode {
return err
}
zap.L().Warn("AI image moderation failed, fallback allow",
@@ -266,15 +298,16 @@ func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
return nil
}
if s.isOpenAIEnabled() {
resp, err := s.openAIClient.ModerateBio(ctx, bio)
if err != nil {
if s.openAIClient.Config().StrictModeration {
return err
}
zap.L().Warn("AI bio moderation failed, fallback allow",
zap.L().Warn("AI bio moderation failed, trying Tencent fallback",
zap.Error(err),
)
return nil
return s.moderateTextWithTencent(ctx, bio,
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
)
}
switch resp.Result {
@@ -286,3 +319,49 @@ func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
return nil
}
}
return s.moderateTextWithTencent(ctx, bio,
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
)
}
func (s *PostAIService) moderateTextWithTencent(
ctx context.Context,
text string,
rejectFactory func(string) error,
reviewFactory func(string) error,
) error {
if !s.isTencentEnabled() {
if s.strictMode {
return fmt.Errorf("no moderation service available")
}
zap.L().Warn("No moderation service available, fallback allow")
return nil
}
resp, err := s.tencentClient.TextModeration(ctx, text)
if err != nil {
if s.strictMode {
return err
}
zap.L().Warn("Tencent TMS moderation failed, fallback allow",
zap.Error(err),
)
return nil
}
reason := resp.Label
if len(resp.Keywords) > 0 {
reason = resp.Label + ": " + strings.Join(resp.Keywords, ", ")
}
switch resp.Suggestion {
case tencent.SuggestionBlock:
return rejectFactory(reason)
case tencent.SuggestionReview:
return reviewFactory(reason)
default:
return nil
}
}

View File

@@ -12,6 +12,7 @@ import (
"carrot_bbs/internal/pkg/openai"
"carrot_bbs/internal/pkg/redis"
"carrot_bbs/internal/pkg/s3"
"carrot_bbs/internal/pkg/tencent"
"carrot_bbs/internal/pkg/ws"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
@@ -49,6 +50,7 @@ var InfrastructureSet = wire.NewSet(
// 外部服务客户端
ProvideOpenAIClient,
ProvideTencentClient,
ProvideEmailClient,
ProvideS3Client,
@@ -169,6 +171,18 @@ func ProvideOpenAIClient(cfg *config.Config) openai.Client {
return openai.NewClient(openai.ConfigFromAppConfig(&cfg.OpenAI))
}
// ProvideTencentClient 提供腾讯云TMS客户端
func ProvideTencentClient(cfg *config.Config) tencent.Client {
return tencent.NewClient(tencent.Config{
Enabled: cfg.TencentTMS.Enabled,
SecretID: cfg.TencentTMS.SecretID,
SecretKey: cfg.TencentTMS.SecretKey,
Region: cfg.TencentTMS.Region,
BizType: cfg.TencentTMS.BizType,
Timeout: cfg.TencentTMS.Timeout,
})
}
// ProvideEmailClient 提供邮件客户端
func ProvideEmailClient(cfg *config.Config) email.Client {
return email.NewClient(email.ConfigFromAppConfig(&cfg.Email))

View File

@@ -11,6 +11,7 @@ import (
"carrot_bbs/internal/pkg/openai"
"carrot_bbs/internal/pkg/redis"
"carrot_bbs/internal/pkg/s3"
"carrot_bbs/internal/pkg/tencent"
"carrot_bbs/internal/pkg/ws"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
@@ -87,8 +88,8 @@ func ProvideEmailService(client email.Client) service.EmailService {
}
// ProvidePostAIService 提供 AI 服务
func ProvidePostAIService(client openai.Client) *service.PostAIService {
return service.NewPostAIService(client)
func ProvidePostAIService(openAIClient openai.Client, tencentClient tencent.Client, cfg *config.Config) *service.PostAIService {
return service.NewPostAIService(openAIClient, tencentClient, cfg.OpenAI.StrictModeration)
}
// ProvidePushService 提供推送服务