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.
194 lines
4.8 KiB
Go
194 lines
4.8 KiB
Go
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)
|
|
}
|