mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-14 06:47:19 +00:00
上游对单次请求下发 error.code=cyber_policy 硬阻断时,网关在所有端点 (/v1/responses、/v1/chat/completions、/v1/messages、WebSocket)及流式/ 非流式路径下,将该结果原样透传给客户端,绝不 failover、换号或同步拦截; 命中后异步完成审计与计费: - 风控中心记录 cyber_policy 留痕并发送通知邮件,落库先于发信,SMTP 阻塞 不影响留痕 - ops 错误请求记录,状态码对齐客户端实际接收(流式 200 / 非流式 400) - 用量明细标记 request_type=cyber,按上游真实 token 计费,HTTP 与 WebSocket 计费口径统一,零 token 命中不误扣 - 会话级自动屏蔽(管理员开关,默认关):命中的会话在可配 TTL 内本地拦截 不再发往上游,仅屏蔽该会话不影响同 Key 其他会话 - 封号计数排除开关:可选让 cyber 命中不计入自动封号,命中当次不判定且 历史行在违规计数中一并排除 WebSocket 多轮连接下 cyber 标记按 turn 生命周期管理,逐轮独立检测与记录; 透传的错误响应不被兜底逻辑追加内容污染。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
2.9 KiB
Go
89 lines
2.9 KiB
Go
package service
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMarkAndGetOpsCyberPolicy(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
|
|
require.Nil(t, GetOpsCyberPolicy(c), "no mark initially")
|
|
|
|
MarkOpsCyberPolicy(c, CyberPolicyMark{
|
|
Code: "cyber_policy",
|
|
Message: "This request was flagged for cyber policy.",
|
|
Body: `{"error":{"code":"cyber_policy"}}`,
|
|
UpstreamStatus: 400,
|
|
})
|
|
|
|
got := GetOpsCyberPolicy(c)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, "cyber_policy", got.Code)
|
|
require.Equal(t, 400, got.UpstreamStatus)
|
|
}
|
|
|
|
func TestMarkOpsCyberPolicyFirstWins(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
MarkOpsCyberPolicy(c, CyberPolicyMark{Code: "cyber_policy", Message: "first"})
|
|
MarkOpsCyberPolicy(c, CyberPolicyMark{Code: "cyber_policy", Message: "second"})
|
|
require.Equal(t, "first", GetOpsCyberPolicy(c).Message, "first mark wins, later marks ignored")
|
|
}
|
|
|
|
func TestMarkOpsCyberPolicyNilContext(t *testing.T) {
|
|
MarkOpsCyberPolicy(nil, CyberPolicyMark{Code: "cyber_policy"})
|
|
require.Nil(t, GetOpsCyberPolicy(nil))
|
|
}
|
|
|
|
// TestClearOpsCyberPolicy_AllowsRemark verifies F1: after Clear, Get returns nil
|
|
// and a subsequent Mark takes effect (per-turn lifecycle in WS connections).
|
|
func TestClearOpsCyberPolicy_AllowsRemark(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
|
|
MarkOpsCyberPolicy(c, CyberPolicyMark{Message: "first", UpstreamStatus: 200})
|
|
require.NotNil(t, GetOpsCyberPolicy(c))
|
|
|
|
ClearOpsCyberPolicy(c)
|
|
require.Nil(t, GetOpsCyberPolicy(c), "mark must be invisible after Clear")
|
|
|
|
MarkOpsCyberPolicy(c, CyberPolicyMark{Message: "second", UpstreamStatus: 400})
|
|
got := GetOpsCyberPolicy(c)
|
|
require.NotNil(t, got, "re-mark after Clear must take effect")
|
|
require.Equal(t, "second", got.Message)
|
|
}
|
|
|
|
func TestDetectOpenAICyberPolicy(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
payload string
|
|
hit bool
|
|
msg string
|
|
}{
|
|
{"top-level error", `{"error":{"code":"cyber_policy","message":"flagged"}}`, true, "flagged"},
|
|
{"response-wrapped", `{"response":{"error":{"code":"cyber_policy","message":" bad "}}}`, true, "bad"},
|
|
{"case-insensitive", `{"error":{"code":"Cyber_Policy"}}`, true, ""},
|
|
{"content_policy not cyber", `{"error":{"code":"content_policy","message":"x"}}`, false, ""},
|
|
{"safety message not cyber", `{"error":{"type":"safety_error","message":"high-risk cyber activity"}}`, false, ""},
|
|
{"empty", ``, false, ""},
|
|
{"upstream_error", `{"error":{"code":"upstream_error"}}`, false, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
hit, code, msg := detectOpenAICyberPolicy([]byte(tc.payload))
|
|
require.Equal(t, tc.hit, hit)
|
|
if tc.hit {
|
|
require.Equal(t, "cyber_policy", code)
|
|
require.Equal(t, tc.msg, msg)
|
|
}
|
|
})
|
|
}
|
|
}
|