Merge pull request #3230 from DaydreamCoding/feat/openai-cyber-policy-passthrough

feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
This commit is contained in:
Wesley Liddick
2026-06-16 16:55:51 +08:00
committed by GitHub
56 changed files with 3036 additions and 184 deletions
+2
View File
@@ -40,6 +40,7 @@ export interface ContentModerationConfig {
blocked_keywords: string[]
keyword_blocking_mode: KeywordBlockingMode
model_filter: ContentModerationModelFilter
cyber_policy_exclude_from_ban_count: boolean
}
export type ContentModerationAPIKeyStatusValue = 'unknown' | 'ok' | 'error' | 'frozen'
@@ -115,6 +116,7 @@ export interface UpdateContentModerationConfig {
blocked_keywords?: string[]
keyword_blocking_mode?: KeywordBlockingMode
model_filter?: ContentModerationModelFilter
cyber_policy_exclude_from_ban_count?: boolean
}
export interface ContentModerationRuntimeStatus {
+10
View File
@@ -569,6 +569,11 @@ export interface SystemSettings {
// Payment configuration
payment_enabled: boolean;
risk_control_enabled: boolean;
// Cyber session block
cyber_session_block_enabled: boolean;
cyber_session_block_ttl_seconds: number;
payment_min_amount: number;
payment_max_amount: number;
payment_daily_limit: number;
@@ -806,6 +811,11 @@ export interface UpdateSettingsRequest {
// Payment configuration
payment_enabled?: boolean;
risk_control_enabled?: boolean;
// Cyber session block
cyber_session_block_enabled?: boolean;
cyber_session_block_ttl_seconds?: number;
payment_min_amount?: number;
payment_max_amount?: number;
payment_daily_limit?: number;
@@ -233,7 +233,8 @@ const requestTypeOptions = ref<SelectOption[]>([
{ value: null, label: t('admin.usage.allTypes') },
{ value: 'ws_v2', label: t('usage.ws') },
{ value: 'stream', label: t('usage.stream') },
{ value: 'sync', label: t('usage.sync') }
{ value: 'sync', label: t('usage.sync') },
{ value: 'cyber', label: t('usage.cyber') }
])
const billingTypeOptions = ref<SelectOption[]>([
@@ -475,6 +475,7 @@ const tokenTooltipData = ref<AdminUsageLog | null>(null)
const getRequestTypeLabel = (row: AdminUsageLog): string => {
const requestType = resolveUsageRequestType(row)
if (requestType === 'cyber') return t('usage.cyber')
if (requestType === 'ws_v2') return t('usage.ws')
if (requestType === 'stream') return t('usage.stream')
if (requestType === 'sync') return t('usage.sync')
@@ -483,6 +484,7 @@ const getRequestTypeLabel = (row: AdminUsageLog): string => {
const getRequestTypeBadgeClass = (row: AdminUsageLog): string => {
const requestType = resolveUsageRequestType(row)
if (requestType === 'cyber') return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
if (requestType === 'ws_v2') return 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200'
if (requestType === 'stream') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
if (requestType === 'sync') return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
@@ -123,7 +123,7 @@ const localModel = ref<string | null>('')
const localCategory = ref<string>('')
const localApiKeyId = ref<number | null>(null)
const categoryCodes = ['auth', 'rate_limit', 'quota', 'invalid_request', 'service_unavailable', 'upstream', 'internal']
const categoryCodes = ['auth', 'rate_limit', 'quota', 'invalid_request', 'service_unavailable', 'upstream', 'internal', 'cyber']
const categoryOptions = computed(() => [
{ value: '', label: t('usage.errors.allCategories') },
+9 -1
View File
@@ -923,6 +923,7 @@ export default {
ws: 'WS',
stream: 'Stream',
sync: 'Sync',
cyber: 'Cyber',
unknown: 'Unknown',
in: 'In',
out: 'Out',
@@ -980,7 +981,7 @@ export default {
categories: {
auth: 'Auth failed', rate_limit: 'Rate limited', quota: 'Balance/Subscription',
invalid_request: 'Invalid request', service_unavailable: 'Service unavailable',
upstream: 'Upstream error', internal: 'Platform error', other: 'Other',
upstream: 'Upstream error', internal: 'Platform error', other: 'Other', cyber: 'Cyber policy',
},
detail: {
title: 'Error Request Detail',
@@ -2627,6 +2628,9 @@ export default {
emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.',
autoBan: 'Auto Ban User',
autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.',
cyberPolicyExcludeBan: 'Exclude Cyber Policy Hits from Ban Count',
cyberPolicyExcludeBanHint: 'When enabled, cyber_policy hits no longer count toward auto-ban violations: no ban judgment on the hit itself, and history rows are excluded from the rolling count. Logs and notice emails are unaffected.',
violationNotCounted: 'Not counted',
banThreshold: 'Ban Threshold',
violationWindowHours: 'Count Window (hours)',
hitRetentionDays: 'Hit Record Retention (days)',
@@ -2771,6 +2775,7 @@ export default {
action: {
block: 'Blocked',
keywordBlock: 'Keyword Blocked',
cyberPolicy: 'Cyber policy',
error: 'Error',
},
},
@@ -5449,6 +5454,9 @@ export default {
configureLink: 'Configure content moderation in Risk Control',
enabled: 'Enable Risk Control',
enabledHint: 'When off, the admin sidebar entry is hidden and gateway moderation is skipped.',
cyberSessionBlock: 'Cyber session auto-block',
cyberSessionBlockHint: 'When enabled, sessions hit by upstream cyber_policy are blocked locally for the TTL and no longer forwarded. Only the offending session is blocked; other sessions on the same key are unaffected.',
cyberSessionBlockTTL: 'Block TTL (seconds)',
},
affiliate: {
title: 'Affiliate (Invite Rebate)',
+9 -1
View File
@@ -927,6 +927,7 @@ export default {
ws: 'WS',
stream: '流式',
sync: '同步',
cyber: '安全策略',
unknown: '未知',
in: '输入',
out: '输出',
@@ -984,7 +985,7 @@ export default {
categories: {
auth: '认证失败', rate_limit: '限流', quota: '余额/订阅',
invalid_request: '参数错误', service_unavailable: '服务暂时不可用',
upstream: '上游错误', internal: '平台错误', other: '其他',
upstream: '上游错误', internal: '平台错误', other: '其他', cyber: '安全策略',
},
detail: {
title: '错误请求详情',
@@ -2704,6 +2705,9 @@ export default {
emailOnHitHint: '开启后每次达到阈值都会向用户发送风控提醒邮件;自动封禁通知始终发送。',
autoBan: '自动封禁用户',
autoBanHint: '命中次数达到阈值后将禁用用户账号、刷新认证缓存并发送封禁通知邮件。',
cyberPolicyExcludeBan: 'cyber_policy 不计入封号次数',
cyberPolicyExcludeBanHint: '开启后,cyber_policy 拦截不再计入自动封号的违规次数:当次不判定封号,历史累计亦排除。风控日志与通知邮件照常。',
violationNotCounted: '未计入封号',
banThreshold: '封禁触发次数',
violationWindowHours: '累计窗口(小时)',
hitRetentionDays: '命中记录保留(天)',
@@ -2848,6 +2852,7 @@ export default {
action: {
block: '拦截',
keywordBlock: '关键词拦截',
cyberPolicy: '网络安全策略',
error: '异常',
},
},
@@ -5609,6 +5614,9 @@ export default {
configureLink: '前往 风控中心 配置内容审计',
enabled: '启用风控中心',
enabledHint: '关闭后管理员侧边栏入口隐藏,网关内容审计不会执行。',
cyberSessionBlock: 'cyber 会话自动屏蔽',
cyberSessionBlockHint: '开启后,被上游网络安全策略(cyber_policy)拦截的会话将在 TTL 内被本地屏蔽,不再发往上游。仅屏蔽该会话,不影响同 Key 其他会话。',
cyberSessionBlockTTL: '屏蔽时长(秒)',
},
affiliate: {
title: '邀请返利',
+1 -1
View File
@@ -1203,7 +1203,7 @@ export interface CodexSessionImportResult {
// ==================== Usage & Redeem Types ====================
export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription' | 'invitation'
export type UsageRequestType = 'unknown' | 'sync' | 'stream' | 'ws_v2'
export type UsageRequestType = 'unknown' | 'sync' | 'stream' | 'ws_v2' | 'cyber'
export type ImageSizeSource = 'output' | 'input' | 'default' | 'legacy'
export type ImageSizeBreakdown = Record<string, number>
+3 -2
View File
@@ -6,7 +6,7 @@ export interface UsageRequestTypeLike {
openai_ws_mode?: boolean | null
}
const VALID_REQUEST_TYPES = new Set<UsageRequestType>(['unknown', 'sync', 'stream', 'ws_v2'])
const VALID_REQUEST_TYPES = new Set<UsageRequestType>(['unknown', 'sync', 'stream', 'ws_v2', 'cyber'])
export const isUsageRequestType = (value: unknown): value is UsageRequestType => {
return typeof value === 'string' && VALID_REQUEST_TYPES.has(value as UsageRequestType)
@@ -23,7 +23,8 @@ export const resolveUsageRequestType = (value: UsageRequestTypeLike): UsageReque
}
export const requestTypeToLegacyStream = (requestType?: UsageRequestType | null): boolean | null | undefined => {
if (!requestType || requestType === 'unknown') {
// cyber 与 stream 正交(cyber 可发生在 stream 或非 stream 请求),不映射到 legacy stream 维度。
if (!requestType || requestType === 'unknown' || requestType === 'cyber') {
return null
}
if (requestType === 'sync') {
+13 -1
View File
@@ -881,6 +881,13 @@
</div>
<Toggle v-model="configForm.auto_ban_enabled" />
</div>
<div class="flex items-center justify-between rounded-lg border border-gray-100 p-4 dark:border-dark-700 lg:col-span-2">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.riskControl.cyberPolicyExcludeBan') }}</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.riskControl.cyberPolicyExcludeBanHint') }}</p>
</div>
<Toggle v-model="configForm.cyber_policy_exclude_from_ban_count" />
</div>
<div>
<label class="input-label">{{ t('admin.riskControl.banThreshold') }}</label>
<input v-model.number="configForm.ban_threshold" type="number" min="1" max="1000" class="input" />
@@ -1226,6 +1233,7 @@ const configForm = reactive({
block_message: '内容审计命中风险规则,请调整输入后重试',
email_on_hit: true,
auto_ban_enabled: true,
cyber_policy_exclude_from_ban_count: false,
ban_threshold: 10,
violation_window_hours: 720,
hit_retention_days: 180,
@@ -1702,6 +1710,7 @@ function applyConfig(config: ContentModerationConfig) {
configForm.block_message = config.block_message || '内容审计命中风险规则,请调整输入后重试'
configForm.email_on_hit = config.email_on_hit ?? true
configForm.auto_ban_enabled = config.auto_ban_enabled ?? true
configForm.cyber_policy_exclude_from_ban_count = config.cyber_policy_exclude_from_ban_count ?? false
configForm.ban_threshold = config.ban_threshold || 10
configForm.violation_window_hours = config.violation_window_hours || 720
configForm.hit_retention_days = config.hit_retention_days || 180
@@ -1782,6 +1791,7 @@ async function saveConfig() {
block_message: configForm.block_message || '内容审计命中风险规则,请调整输入后重试',
email_on_hit: configForm.email_on_hit,
auto_ban_enabled: configForm.auto_ban_enabled,
cyber_policy_exclude_from_ban_count: configForm.cyber_policy_exclude_from_ban_count,
ban_threshold: Number(configForm.ban_threshold) || 10,
violation_window_hours: Number(configForm.violation_window_hours) || 720,
hit_retention_days: Number(configForm.hit_retention_days) || 180,
@@ -2097,6 +2107,7 @@ function modeDescription(mode: ModerationMode): string {
}
function resultLabel(row: ContentModerationLog): string {
if (row.action === 'cyber_policy') return t('admin.riskControl.action.cyberPolicy')
if (row.action === 'keyword_block') return t('admin.riskControl.action.keywordBlock')
if (row.action === 'block') return t('admin.riskControl.action.block')
if (row.action === 'error' || row.error) return t('admin.riskControl.action.error')
@@ -2105,7 +2116,7 @@ function resultLabel(row: ContentModerationLog): string {
}
function resultBadgeClass(row: ContentModerationLog): string {
if (row.action === 'block' || row.action === 'keyword_block') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
if (row.action === 'block' || row.action === 'keyword_block' || row.action === 'cyber_policy') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
if (row.action === 'error' || row.error) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
if (row.flagged) return 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300'
return 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
@@ -2303,6 +2314,7 @@ function parseBlockedKeywords(value: string): string[] {
function violationCountText(row: ContentModerationLog): string {
if (!row.flagged) return '-'
if (row.violation_count === 0) return t('admin.riskControl.violationNotCounted')
return t('admin.riskControl.violationCount', { count: row.violation_count || 1 })
}
+30
View File
@@ -5528,6 +5528,31 @@
</div>
<Toggle v-model="form.risk_control_enabled" />
</div>
<div class="flex items-center justify-between">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.features.riskControl.cyberSessionBlock') }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.features.riskControl.cyberSessionBlockHint') }}
</p>
</div>
<Toggle v-model="form.cyber_session_block_enabled" />
</div>
<div v-if="form.cyber_session_block_enabled">
<label class="input-label">
{{ t('admin.settings.features.riskControl.cyberSessionBlockTTL') }}
<span class="text-red-500">*</span>
</label>
<input
v-model.number="form.cyber_session_block_ttl_seconds"
type="number"
min="1"
class="input"
/>
</div>
</div>
</div>
@@ -7648,6 +7673,8 @@ const form = reactive<SettingsForm>({
hide_ccs_import_button: false,
payment_enabled: false,
risk_control_enabled: false,
cyber_session_block_enabled: false,
cyber_session_block_ttl_seconds: 3600,
payment_min_amount: 1,
payment_max_amount: 10000,
payment_daily_limit: 50000,
@@ -8943,6 +8970,9 @@ async function saveSettings() {
// Payment configuration
payment_enabled: form.payment_enabled,
risk_control_enabled: form.risk_control_enabled,
cyber_session_block_enabled: form.cyber_session_block_enabled,
cyber_session_block_ttl_seconds:
Number(form.cyber_session_block_ttl_seconds) || 3600,
payment_min_amount: Number(form.payment_min_amount) || 0,
payment_max_amount: Number(form.payment_max_amount) || 0,
payment_daily_limit: Number(form.payment_daily_limit) || 0,
+1
View File
@@ -490,6 +490,7 @@ const cancelExport = () => exportAbortController?.abort()
const openCleanupDialog = () => { cleanupDialogVisible.value = true }
const getRequestTypeLabel = (log: AdminUsageLog): string => {
const requestType = resolveUsageRequestType(log)
if (requestType === 'cyber') return t('usage.cyber')
if (requestType === 'ws_v2') return t('usage.ws')
if (requestType === 'stream') return t('usage.stream')
if (requestType === 'sync') return t('usage.sync')
+3
View File
@@ -773,6 +773,7 @@ const formatUserAgent = (ua: string): string => {
const getRequestTypeLabel = (log: UsageLog): string => {
const requestType = resolveUsageRequestType(log)
if (requestType === 'cyber') return t('usage.cyber')
if (requestType === 'ws_v2') return t('usage.ws')
if (requestType === 'stream') return t('usage.stream')
if (requestType === 'sync') return t('usage.sync')
@@ -781,6 +782,7 @@ const getRequestTypeLabel = (log: UsageLog): string => {
const getRequestTypeBadgeClass = (log: UsageLog): string => {
const requestType = resolveUsageRequestType(log)
if (requestType === 'cyber') return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
if (requestType === 'ws_v2') return 'bg-violet-100 text-violet-800 dark:bg-violet-900 dark:text-violet-200'
if (requestType === 'stream') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
if (requestType === 'sync') return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
@@ -790,6 +792,7 @@ const getRequestTypeBadgeClass = (log: UsageLog): string => {
const getRequestTypeExportText = (log: UsageLog): string => {
const requestType = resolveUsageRequestType(log)
if (requestType === 'cyber') return 'Cyber'
if (requestType === 'ws_v2') return 'WS'
if (requestType === 'stream') return 'Stream'
if (requestType === 'sync') return 'Sync'