mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3230 from DaydreamCoding/feat/openai-cyber-policy-passthrough
feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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') },
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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: '邀请返利',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user