feat(risk-control): add content moderation audit

This commit is contained in:
shaw
2026-05-07 09:01:48 +08:00
parent a1106e8167
commit fff4a300c6
54 changed files with 6840 additions and 34 deletions
+205
View File
@@ -382,6 +382,7 @@ export default {
channelPricing: 'Channel Pricing',
channelMonitor: 'Channel Monitor',
channelStatus: 'Channel Status',
riskControl: 'Risk Control',
},
// Auth
@@ -410,6 +411,9 @@ export default {
passwordRequired: 'Password is required',
passwordMinLength: 'Password must be at least 6 characters',
loginFailed: 'Login failed. Please check your credentials and try again.',
errors: {
USER_NOT_ACTIVE: 'Account has been disabled.',
},
registrationFailed: 'Registration failed. Please try again.',
emailSuffixNotAllowed: 'This email domain is not allowed for registration.',
emailSuffixNotAllowedWithAllowed:
@@ -2305,6 +2309,200 @@ export default {
}
},
riskControl: {
title: 'Risk Control',
description: 'Configure content moderation and review audit records',
loadFailed: 'Failed to load risk control',
saveFailed: 'Failed to save content moderation config',
logsFailed: 'Failed to load audit records',
saved: 'Content moderation config saved',
refresh: 'Refresh',
config: 'Content Moderation Config',
configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.',
openSettings: 'Moderation Settings',
settingsTitle: 'Content Moderation Settings',
refreshStatus: 'Refresh Status',
records: 'Audit Records',
recordsHint: 'Shows hits, blocks, errors, and sampled records.',
saveConfig: 'Save Moderation Config',
statusFailed: 'Failed to load runtime status',
enabled: 'Enable Content Moderation',
enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.',
mode: 'Global Mode',
modePreBlock: 'Pre-Block',
modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.',
modeObserve: 'Observe Only',
modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.',
modeOff: 'Off',
modeOffDesc: 'Content moderation is disabled and no audit records are written.',
baseUrl: 'OpenAI Base URL',
model: 'Model',
apiKey: 'OpenAI API Key',
apiKeys: 'OpenAI API Keys',
apiKeyCount: '{count} keys',
apiKeyPlaceholder: 'Enter API Key',
apiKeysPlaceholder: 'One API Key per line',
apiKeysPlaceholderKeep: 'Leave empty to keep stored keys; enter values to replace them',
apiKeysHint: '{count} keys are currently stored. Values entered here replace stored keys; leave empty to keep them.',
apiKeyPlaceholderKeep: 'Leave empty to keep current key',
apiKeyWillClear: 'Configured key will be cleared on save',
apiKeyConfigured: 'Configured',
apiKeyTemporary: 'Pending',
inputApiKeyCount: '{count} keys in input',
storedApiKeyCount: '{count} stored keys',
testInputApiKeys: 'Test input keys',
testStoredApiKeys: 'Test stored keys',
testContentWithStoredApiKey: 'Test content with stored key',
testingApiKeys: 'Testing',
apiKeyTestNoInput: 'Enter OpenAI API Keys to test first',
apiKeyTestDone: 'Key test completed for {count} keys',
apiKeyTestFailed: 'Failed to test OpenAI API Keys',
apiKeyHealth: 'Key Availability',
apiKeyFreezeRule: 'Three consecutive failures freeze a key for 1 minute; moderation rotation skips frozen keys.',
apiKeyRows: '{count} keys',
apiKeyHealthEmpty: 'No key status yet',
apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.',
apiKeyStatusOk: 'Available',
apiKeyStatusError: 'Error',
apiKeyStatusFrozen: 'Frozen',
apiKeyStatusUnknown: 'Untested',
apiKeyFailureCount: '{count} failures',
apiKeyLatency: '{ms} ms',
apiKeyHTTPStatus: 'HTTP {status}',
apiKeyFrozenUntil: 'Frozen until {time}',
apiKeyLastChecked: 'Checked at {time}',
apiKeyNotTested: 'Not tested',
auditTestInput: 'Audit Test Input',
auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.',
auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.',
auditTestImages: 'Test Images',
auditTestImagesHint: 'Upload, drag, or paste images. Up to 4 images, 8MB each.',
addAuditTestImage: 'Add image',
clearAuditTest: 'Clear test',
auditTestImageLimit: 'You can add up to {count} test images',
auditTestImageTooLarge: 'Each test image must be 8MB or smaller',
auditTestImageReadFailed: 'Failed to read test image',
auditTestResult: 'Audit Test Result',
auditTestHighest: 'Top category {category}, score {score}',
auditTestComposite: 'Composite score',
auditTestFlagged: 'Threshold hit',
auditTestPassed: 'Pass',
notConfigured: 'Not configured',
clearApiKey: 'Clear stored key',
keepApiKey: 'Keep stored key',
timeoutMs: 'HTTP Timeout (ms)',
retryCount: 'Retry Count',
sampleRate: 'Sample Rate',
recordNonHits: 'Record Non-Hits',
recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.',
preHashCheck: 'Enable Pre-Hash Check',
preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.',
flaggedHashCount: 'Current hash collection size: {count}',
flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.',
flaggedHashPlaceholder: 'Paste full 64-character input hash',
deleteFlaggedHash: 'Delete hash',
clearFlaggedHashes: 'Clear all',
clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.',
flaggedHashDeleted: 'Risk hash deleted',
flaggedHashNotFound: 'Risk hash not found',
flaggedHashDeleteFailed: 'Failed to delete risk hash',
flaggedHashesCleared: 'Cleared {count} risk hashes',
flaggedHashesClearFailed: 'Failed to clear risk hashes',
workerCount: 'Worker Count',
queueSize: 'Async Queue Size',
blockStatus: 'Block HTTP Status',
blockMessage: 'Custom Block Message',
emailOnHit: 'Email on Hit',
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.',
banThreshold: 'Ban Threshold',
violationWindowHours: 'Count Window (hours)',
hitRetentionDays: 'Hit Record Retention (days)',
nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)',
violationCount: '{count} hits',
emailSent: 'Email sent',
emailNotSent: 'No email',
autoBanned: 'Banned',
unbanUser: 'Unban',
unbanSuccess: 'User has been unbanned',
unbanFailed: 'Failed to unban user',
inputDetailTitle: 'Input Summary Detail',
inputDetailContent: 'Full Content',
queueDelay: 'Queued {ms} ms',
allGroups: 'All Groups',
allGroupsHint: 'Auditing all groups',
selectedGroupsHint: 'Auditing selected groups',
groupScope: 'Audit Groups',
groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.',
selectedGroups: 'Selected Groups',
searchGroups: 'Search group name or platform',
noGroups: 'No groups available',
emptyLogs: 'No audit records',
workerStatus: 'Worker Runtime',
workerStatusHint: 'Queue and worker pool status for asynchronous observation tasks.',
workerPool: 'Worker Pool',
workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total',
queueUsage: 'Queue Usage',
activeWorkers: 'Processing',
idleWorkers: 'Idle Ready',
workerActive: 'Processing an asynchronous audit task',
workerIdle: 'Started, idle and ready',
workerDisabled: 'Risk control or content audit is disabled',
processed: 'Processed',
droppedErrors: 'Dropped / Errors',
autoRefresh: 'Auto refresh every 15s',
lastCleanup: 'Last cleanup: {time}',
cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits',
riskSwitchOff: 'System switch off',
tabs: {
basic: 'Basic',
scope: 'Scope',
runtime: 'Runtime',
response: 'Hit Notice',
retention: 'Retention',
},
overview: {
status: 'Status',
enabled: 'Enabled',
disabled: 'Disabled',
apiKey: 'API Key',
groupScope: 'Scope',
logs: 'Audit Records',
currentFilter: 'Current filter',
},
filters: {
search: 'Search user/key/summary',
from: 'From',
to: 'To',
allGroups: 'All Groups',
allEndpoints: 'All Endpoints',
},
table: {
time: 'Time',
group: 'Group',
user: 'User',
apiKey: 'API Key',
endpoint: 'Endpoint',
result: 'Result',
highest: 'Highest',
actionMeta: 'Action',
latency: 'Latency',
input: 'Input Summary',
},
result: {
all: 'All Results',
hit: 'Hit',
blocked: 'Blocked',
pass: 'Pass',
error: 'Error',
},
action: {
block: 'Blocked',
error: 'Error',
},
},
// Channel Monitor
channelMonitor: {
title: 'Channel Monitor',
@@ -4862,6 +5060,13 @@ export default {
enabled: 'Enable Available Channels',
enabledHint: 'When off, the sidebar entry is hidden and the endpoint returns an empty list.',
},
riskControl: {
title: 'Risk Control',
description: 'Enable the content moderation menu and gateway audit entry point. Disabled by 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.',
},
affiliate: {
title: 'Affiliate (Invite Rebate)',
description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitees recharges. Disabled by default.',
+205
View File
@@ -382,6 +382,7 @@ export default {
channelPricing: '渠道定价',
channelMonitor: '渠道监控',
channelStatus: '渠道状态',
riskControl: '风控中心',
},
// Auth
@@ -410,6 +411,9 @@ export default {
passwordRequired: '请输入密码',
passwordMinLength: '密码至少需要 6 个字符',
loginFailed: '登录失败,请检查您的凭据后重试。',
errors: {
USER_NOT_ACTIVE: '账号已被禁用',
},
registrationFailed: '注册失败,请重试。',
emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。',
emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}',
@@ -2382,6 +2386,200 @@ export default {
}
},
riskControl: {
title: '风控中心',
description: '配置内容审计策略并查看审核记录',
loadFailed: '加载风控中心失败',
saveFailed: '保存内容审计配置失败',
logsFailed: '加载审核记录失败',
saved: '内容审计配置已保存',
refresh: '刷新',
config: '内容审计配置',
configHint: '调用 OpenAI Moderations 进行请求内容评分,命中阈值后按模式处理。',
openSettings: '内容审计设置',
settingsTitle: '内容审计设置',
refreshStatus: '刷新状态',
records: '审核记录',
recordsHint: '展示命中、拦截、异常和已采样记录。',
saveConfig: '保存内容审计配置',
statusFailed: '加载运行状态失败',
enabled: '开启内容审计',
enabledHint: '关闭后即使风控中心菜单启用,也不会审核网关请求。',
mode: '全局模式',
modePreBlock: '前置拦截',
modePreBlockDesc: '每次请求先同步审核最新用户输入,命中后立即拒绝请求。',
modeObserve: '仅观察',
modeObserveDesc: '请求直接放行,最新用户输入进入异步审核队列;命中后只记录、通知和按规则累计。',
modeOff: '关闭',
modeOffDesc: '不执行内容审计,也不会写入审核记录。',
baseUrl: 'OpenAI Base URL',
model: '模型名',
apiKey: 'OpenAI API Key',
apiKeys: 'OpenAI API Keys',
apiKeyCount: '{count} 个 Key',
apiKeyPlaceholder: '请输入 API Key',
apiKeysPlaceholder: '每行一个 API Key',
apiKeysPlaceholderKeep: '留空保持已保存的 Key;填写后将替换为这些 Key',
apiKeysHint: '当前已保存 {count} 个 Key;填写文本框会替换已保存 Key,留空则保持不变。',
apiKeyPlaceholderKeep: '留空保持不变',
apiKeyWillClear: '保存后清除已配置 Key',
apiKeyConfigured: '已配置',
apiKeyTemporary: '待保存',
inputApiKeyCount: '输入区 {count} 个 Key',
storedApiKeyCount: '已保存 {count} 个 Key',
testInputApiKeys: '测试输入区 Key',
testStoredApiKeys: '测试已保存 Key',
testContentWithStoredApiKey: '用已保存 Key 试跑内容',
testingApiKeys: '测试中',
apiKeyTestNoInput: '请先输入需要测试的 OpenAI API Key',
apiKeyTestDone: 'Key 测试完成,共 {count} 个',
apiKeyTestFailed: '测试 OpenAI API Key 失败',
apiKeyHealth: 'Key 可用状态',
apiKeyFreezeRule: '连续 3 次失败会冻结 1 分钟,审计轮询会自动跳过。',
apiKeyRows: '{count} 个',
apiKeyHealthEmpty: '暂无 Key 状态',
apiKeyHealthEmptyHint: '保存 Key 或测试输入区 Key 后会显示可用性。',
apiKeyStatusOk: '可用',
apiKeyStatusError: '异常',
apiKeyStatusFrozen: '冻结',
apiKeyStatusUnknown: '未测试',
apiKeyFailureCount: '失败 {count} 次',
apiKeyLatency: '{ms} ms',
apiKeyHTTPStatus: 'HTTP {status}',
apiKeyFrozenUntil: '冻结至 {time}',
apiKeyLastChecked: '检查于 {time}',
apiKeyNotTested: '尚未测试',
auditTestInput: '审计试跑输入',
auditTestInputHint: '可填写提示词并上传或粘贴图片;图片以 base64 发送,不会保存文件。',
auditTestPromptPlaceholder: '输入要测试的用户提示词;留空时仅测试 Key 可用性。',
auditTestImages: '测试图片',
auditTestImagesHint: '支持上传、拖拽或粘贴图片,最多 4 张,每张不超过 8MB。',
addAuditTestImage: '添加图片',
clearAuditTest: '清空试跑',
auditTestImageLimit: '最多只能添加 {count} 张测试图片',
auditTestImageTooLarge: '单张测试图片不能超过 8MB',
auditTestImageReadFailed: '读取测试图片失败',
auditTestResult: '审计试跑结果',
auditTestHighest: '最高分类 {category},分数 {score}',
auditTestComposite: '综合评分',
auditTestFlagged: '命中阈值',
auditTestPassed: '未命中',
notConfigured: '未配置',
clearApiKey: '清除已保存 Key',
keepApiKey: '保留已保存 Key',
timeoutMs: 'HTTP 超时 (ms)',
retryCount: '失败重试次数',
sampleRate: '采样率',
recordNonHits: '记录未命中输入',
recordNonHitsHint: '开启后会记录抽样但未命中的请求摘要,摘要会先脱敏再入库。',
preHashCheck: '启用前置哈希比对',
preHashCheckHint: '异步审核命中过的输入哈希会被前置拦截;该拦截不发送邮件,也不累计封禁次数。',
flaggedHashCount: '当前哈希集合数量:{count} 个',
flaggedHashHint: '哈希永久保存在 Redis 集合中;可粘贴完整 64 位哈希删除误拦截项,或一键清空全部风险哈希。',
flaggedHashPlaceholder: '粘贴完整 64 位输入哈希',
deleteFlaggedHash: '删除指定哈希',
clearFlaggedHashes: '一键清空',
clearFlaggedHashesConfirm: '确定要清空全部风险输入哈希吗?此操作不会删除审核记录,但会取消所有历史哈希拦截。',
flaggedHashDeleted: '风险哈希已删除',
flaggedHashNotFound: '该风险哈希不存在',
flaggedHashDeleteFailed: '删除风险哈希失败',
flaggedHashesCleared: '已清空 {count} 个风险哈希',
flaggedHashesClearFailed: '清空风险哈希失败',
workerCount: 'Worker 数',
queueSize: '异步队列大小',
blockStatus: '拦截 HTTP 状态码',
blockMessage: '自定义拦截提示',
emailOnHit: '命中后发送邮件',
emailOnHitHint: '开启后每次达到阈值都会向用户发送风控提醒邮件;自动封禁通知始终发送。',
autoBan: '自动封禁用户',
autoBanHint: '命中次数达到阈值后将禁用用户账号、刷新认证缓存并发送封禁通知邮件。',
banThreshold: '封禁触发次数',
violationWindowHours: '累计窗口(小时)',
hitRetentionDays: '命中记录保留(天)',
nonHitRetentionDays: '未命中记录保留(天,最多 3 天)',
violationCount: '{count} 次',
emailSent: '已发邮件',
emailNotSent: '未发邮件',
autoBanned: '已封禁',
unbanUser: '解封',
unbanSuccess: '用户已解封',
unbanFailed: '解封用户失败',
inputDetailTitle: '输入摘要详情',
inputDetailContent: '完整内容',
queueDelay: '排队 {ms} ms',
allGroups: '全部分组',
allGroupsHint: '当前审计全部分组',
selectedGroupsHint: '当前审计指定分组',
groupScope: '审计分组',
groupScopeHint: '开启右侧开关表示全部分组,关闭后选择指定分组。',
selectedGroups: '指定分组',
searchGroups: '搜索分组名称或平台',
noGroups: '暂无可用分组',
emptyLogs: '暂无审核记录',
workerStatus: 'Worker 运行状态',
workerStatusHint: '异步观察任务的队列和 worker 池状态。',
workerPool: 'Worker 池',
workerPoolMeta: '{active} 个处理中,{idle} 个空闲可用,共 {total} 个',
queueUsage: '队列占用',
activeWorkers: '处理中',
idleWorkers: '空闲可用',
workerActive: '正在处理异步审计任务',
workerIdle: '已启动,当前空闲可用',
workerDisabled: '风控或内容审计未启用',
processed: '已处理',
droppedErrors: '丢弃/异常',
autoRefresh: '每 15 秒自动刷新',
lastCleanup: '上次清理:{time}',
cleanupStats: '上次清理删除命中 {hit} 条,未命中 {nonHit} 条',
riskSwitchOff: '系统开关关闭',
tabs: {
basic: '基础',
scope: '审计范围',
runtime: '运行队列',
response: '命中通知',
retention: '日志保留',
},
overview: {
status: '运行状态',
enabled: '已启用',
disabled: '未启用',
apiKey: 'API Key',
groupScope: '审计范围',
logs: '审核记录',
currentFilter: '当前筛选结果',
},
filters: {
search: '按用户/Key/摘要搜索',
from: '开始时间',
to: '结束时间',
allGroups: '全部分组',
allEndpoints: '全部端点',
},
table: {
time: '时间',
group: '分组',
user: '用户',
apiKey: 'API Key',
endpoint: '端点',
result: '结果',
highest: '最高分',
actionMeta: '处置',
latency: '上游耗时',
input: '输入摘要',
},
result: {
all: '全部结果',
hit: '命中',
blocked: '已拦截',
pass: '未命中',
error: '异常',
},
action: {
block: '拦截',
error: '异常',
},
},
// Channel Monitor
channelMonitor: {
title: '渠道监控',
@@ -5025,6 +5223,13 @@ export default {
enabled: '启用可用渠道',
enabledHint: '关闭后用户端侧边栏入口隐藏,接口返回空数组。',
},
riskControl: {
title: '风控中心',
description: '启用内容审计菜单和全端点请求审核入口。默认关闭。',
configureLink: '前往 风控中心 配置内容审计',
enabled: '启用风控中心',
enabledHint: '关闭后管理员侧边栏入口隐藏,网关内容审计不会执行。',
},
affiliate: {
title: '邀请返利',
description: '老用户邀请新用户注册,新用户充值后老用户按比例获得返利额度。默认关闭。',