Files
sub2api/backend/internal/securityaudit/coordinator.go
T

140 lines
4.1 KiB
Go
Raw Normal View History

package securityaudit
import (
"context"
"errors"
"net/http"
"sync"
)
type LegacyEngine interface {
Check(ctx context.Context, req Request) (*LegacyDecision, error)
}
type PromptEngine interface {
EffectiveMode() Mode
Enqueue(ctx context.Context, req Request) error
Evaluate(ctx context.Context, req Request) (*PromptDecision, error)
}
type Coordinator struct {
legacy LegacyEngine
prompt PromptEngine
}
func NewCoordinator(legacy LegacyEngine, prompt PromptEngine) *Coordinator {
return &Coordinator{legacy: legacy, prompt: prompt}
}
func (c *Coordinator) Check(ctx context.Context, req Request) Decision {
if c == nil {
return allowDecision(nil, nil)
}
mode := ModeOff
if c.prompt != nil {
mode = c.prompt.EffectiveMode()
}
switch mode {
case ModeAsync:
// Enqueue is deliberately best-effort. The implementation owns a bounded
// context and copies request memory before it can outlive the Handler.
_ = c.prompt.Enqueue(ctx, req.Clone())
legacy, _ := c.checkLegacy(ctx, req)
return prioritize(legacy, nil)
case ModeBlocking:
return c.checkBlocking(ctx, req)
default:
legacy, _ := c.checkLegacy(ctx, req)
return prioritize(legacy, nil)
}
}
func (c *Coordinator) checkBlocking(ctx context.Context, req Request) Decision {
var wg sync.WaitGroup
wg.Add(2)
var legacy *LegacyDecision
var prompt *PromptDecision
go func() {
defer wg.Done()
legacy, _ = c.checkLegacy(ctx, req)
}()
go func() {
defer wg.Done()
if c.prompt == nil {
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
return
}
result, err := c.prompt.Evaluate(ctx, req.Clone())
if err != nil {
var guardErr *GuardError
if errors.As(err, &guardErr) && guardErr.Code == ErrorCodeInvalidResponse {
prompt = unavailablePromptDecision(ErrorCodeInvalidResponse)
return
}
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
return
}
if result == nil {
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
return
}
prompt = result
}()
wg.Wait()
return prioritize(legacy, prompt)
}
func (c *Coordinator) checkLegacy(ctx context.Context, req Request) (*LegacyDecision, error) {
if c.legacy == nil {
return nil, nil
}
return c.legacy.Check(ctx, req)
}
func prioritize(legacy *LegacyDecision, prompt *PromptDecision) Decision {
if legacy != nil && legacy.Blocked {
status := legacy.StatusCode
if status < 400 || status > 599 {
status = http.StatusForbidden
}
code := legacy.ErrorCode
if code == "" {
code = "content_policy_violation"
}
return Decision{
Kind: DecisionBlock, HTTPStatus: status, ErrorCode: code, ClientMessage: legacy.Message,
Legacy: legacy, Prompt: prompt, AllowNextStage: false,
}
}
if prompt == nil {
return allowDecision(legacy, nil)
}
switch prompt.Kind {
case DecisionBlock:
return Decision{Kind: DecisionBlock, HTTPStatus: http.StatusForbidden, ErrorCode: ErrorCodeBlocked,
ClientMessage: "提示词安全审计拒绝了该请求,请调整输入后重试", Legacy: legacy, Prompt: prompt}
case DecisionInvalid:
return Decision{Kind: DecisionInvalid, HTTPStatus: http.StatusServiceUnavailable, ErrorCode: ErrorCodeInvalidResponse,
ClientMessage: "提示词安全审计暂时不可用,请稍后重试", Legacy: legacy, Prompt: prompt}
case DecisionUnavailable:
return Decision{Kind: DecisionUnavailable, HTTPStatus: http.StatusServiceUnavailable, ErrorCode: ErrorCodeUnavailable,
ClientMessage: "提示词安全审计暂时不可用,请稍后重试", Legacy: legacy, Prompt: prompt}
case DecisionFlag:
return Decision{Kind: DecisionFlag, HTTPStatus: http.StatusOK, Legacy: legacy, Prompt: prompt, AllowNextStage: true}
default:
return allowDecision(legacy, prompt)
}
}
func allowDecision(legacy *LegacyDecision, prompt *PromptDecision) Decision {
return Decision{Kind: DecisionAllow, HTTPStatus: http.StatusOK, Legacy: legacy, Prompt: prompt, AllowNextStage: true}
}
func unavailablePromptDecision(code string) *PromptDecision {
kind := DecisionUnavailable
if code == ErrorCodeInvalidResponse {
kind = DecisionInvalid
}
return &PromptDecision{Kind: kind, ErrorCode: code, AllowNextStage: false}
}