mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 08:22:34 +00:00
Implements #2646: the risk-control content audit can now send OpenAI Moderations requests through a proxy from IP Management - Proxy Servers. Backend: - ContentModerationConfig gains proxy_id (nil = direct, unchanged default) - update semantics: null keeps, 0 clears, >0 selects (validated to exist) - moderation calls build the client via the shared httpclient pool; proxy resolution failure surfaces as a moderation error and never silently falls back to direct connection - proxy_id -> URL resolution cached 60s (single-entry, invalidated on config save) so the pre-block hot path does not hit the DB per request - test-key endpoint accepts proxy_id too (null = saved config's proxy, 0 = force direct), so input-key/saved-key tests exercise the same path - proxy usage/inactivity logged (content_moderation.proxy_enabled / proxy_not_active) without leaking credentials Frontend: - ProxySelector in the risk-control basic settings tab, proxy list loaded non-blockingly; save and test payloads carry proxy_id; zh/en i18n
301 lines
7.8 KiB
TypeScript
301 lines
7.8 KiB
TypeScript
import { apiClient } from '../client'
|
|
|
|
export type ModerationMode = 'off' | 'observe' | 'pre_block'
|
|
export type KeywordBlockingMode = 'keyword_only' | 'keyword_and_api' | 'api_only'
|
|
export type ContentModerationModelFilterType = 'all' | 'include' | 'exclude'
|
|
|
|
export interface ContentModerationModelFilter {
|
|
type: ContentModerationModelFilterType
|
|
models: string[]
|
|
}
|
|
|
|
export interface ContentModerationConfig {
|
|
enabled: boolean
|
|
mode: ModerationMode
|
|
base_url: string
|
|
model: string
|
|
proxy_id: number | null
|
|
api_key_configured: boolean
|
|
api_key_masked: string
|
|
api_key_count: number
|
|
api_key_masks: string[]
|
|
api_key_statuses: ContentModerationAPIKeyStatus[]
|
|
timeout_ms: number
|
|
sample_rate: number
|
|
all_groups: boolean
|
|
group_ids: number[]
|
|
record_non_hits: boolean
|
|
thresholds: Record<string, number>
|
|
worker_count: number
|
|
queue_size: number
|
|
block_status: number
|
|
block_message: string
|
|
email_on_hit: boolean
|
|
auto_ban_enabled: boolean
|
|
ban_threshold: number
|
|
violation_window_hours: number
|
|
retry_count: number
|
|
hit_retention_days: number
|
|
non_hit_retention_days: number
|
|
pre_hash_check_enabled: boolean
|
|
blocked_keywords: string[]
|
|
keyword_blocking_mode: KeywordBlockingMode
|
|
model_filter: ContentModerationModelFilter
|
|
cyber_policy_exclude_from_ban_count: boolean
|
|
}
|
|
|
|
export type ContentModerationAPIKeyStatusValue = 'unknown' | 'ok' | 'error' | 'frozen'
|
|
|
|
export interface ContentModerationAPIKeyStatus {
|
|
index: number
|
|
key_hash: string
|
|
masked: string
|
|
status: ContentModerationAPIKeyStatusValue
|
|
failure_count: number
|
|
success_count: number
|
|
last_error: string
|
|
last_checked_at?: string
|
|
frozen_until?: string
|
|
last_latency_ms: number
|
|
last_http_status: number
|
|
last_tested: boolean
|
|
configured: boolean
|
|
}
|
|
|
|
export interface TestContentModerationAPIKeysPayload {
|
|
api_keys?: string[]
|
|
base_url?: string
|
|
model?: string
|
|
timeout_ms?: number
|
|
// null/undefined 沿用已保存配置的代理;0 强制直连;>0 指定代理
|
|
proxy_id?: number
|
|
prompt?: string
|
|
images?: string[]
|
|
}
|
|
|
|
export interface TestContentModerationAPIKeysResponse {
|
|
items: ContentModerationAPIKeyStatus[]
|
|
audit_result?: ContentModerationTestAuditResult
|
|
image_count: number
|
|
}
|
|
|
|
export interface ContentModerationTestAuditResult {
|
|
flagged: boolean
|
|
highest_category: string
|
|
highest_score: number
|
|
composite_score: number
|
|
category_scores: Record<string, number>
|
|
thresholds: Record<string, number>
|
|
}
|
|
|
|
export interface UpdateContentModerationConfig {
|
|
enabled?: boolean
|
|
mode?: ModerationMode
|
|
base_url?: string
|
|
model?: string
|
|
// undefined 不修改;0 清除(直连);>0 指定代理
|
|
proxy_id?: number
|
|
api_key?: string
|
|
api_keys?: string[]
|
|
api_keys_mode?: 'append' | 'replace'
|
|
delete_api_key_hashes?: string[]
|
|
clear_api_key?: boolean
|
|
timeout_ms?: number
|
|
sample_rate?: number
|
|
all_groups?: boolean
|
|
group_ids?: number[]
|
|
record_non_hits?: boolean
|
|
thresholds?: Record<string, number>
|
|
worker_count?: number
|
|
queue_size?: number
|
|
block_status?: number
|
|
block_message?: string
|
|
email_on_hit?: boolean
|
|
auto_ban_enabled?: boolean
|
|
ban_threshold?: number
|
|
violation_window_hours?: number
|
|
retry_count?: number
|
|
hit_retention_days?: number
|
|
non_hit_retention_days?: number
|
|
pre_hash_check_enabled?: boolean
|
|
blocked_keywords?: string[]
|
|
keyword_blocking_mode?: KeywordBlockingMode
|
|
model_filter?: ContentModerationModelFilter
|
|
cyber_policy_exclude_from_ban_count?: boolean
|
|
}
|
|
|
|
export interface ContentModerationRuntimeStatus {
|
|
enabled: boolean
|
|
risk_control_enabled: boolean
|
|
mode: ModerationMode
|
|
worker_count: number
|
|
max_workers: number
|
|
active_workers: number
|
|
idle_workers: number
|
|
queue_size: number
|
|
queue_length: number
|
|
queue_usage_percent: number
|
|
enqueued: number
|
|
dropped: number
|
|
processed: number
|
|
errors: number
|
|
pre_block_active: number
|
|
pre_block_checked: number
|
|
pre_block_allowed: number
|
|
pre_block_blocked: number
|
|
pre_block_errors: number
|
|
pre_block_avg_latency_ms: number
|
|
pre_block_api_key_active: number
|
|
pre_block_api_key_available_count: number
|
|
pre_block_api_key_total_calls: number
|
|
pre_block_api_key_loads: ContentModerationAPIKeyLoad[]
|
|
api_key_statuses: ContentModerationAPIKeyStatus[]
|
|
flagged_hash_count: number
|
|
last_cleanup_at?: string
|
|
last_cleanup_deleted_hit: number
|
|
last_cleanup_deleted_non_hit: number
|
|
}
|
|
|
|
export interface ContentModerationAPIKeyLoad {
|
|
index: number
|
|
key_hash: string
|
|
masked: string
|
|
status: ContentModerationAPIKeyStatusValue
|
|
active: number
|
|
total: number
|
|
success: number
|
|
errors: number
|
|
avg_latency_ms: number
|
|
last_latency_ms: number
|
|
last_http_status: number
|
|
}
|
|
|
|
export interface ContentModerationLog {
|
|
id: number
|
|
request_id: string
|
|
user_id: number | null
|
|
user_email: string
|
|
api_key_id: number | null
|
|
api_key_name: string
|
|
group_id: number | null
|
|
group_name: string
|
|
endpoint: string
|
|
provider: string
|
|
model: string
|
|
mode: string
|
|
action: string
|
|
flagged: boolean
|
|
highest_category: string
|
|
highest_score: number
|
|
matched_keyword: string
|
|
category_scores: Record<string, number>
|
|
threshold_snapshot: Record<string, number>
|
|
input_excerpt: string
|
|
upstream_latency_ms: number | null
|
|
error: string
|
|
violation_count: number
|
|
auto_banned: boolean
|
|
email_sent: boolean
|
|
user_status: string
|
|
queue_delay_ms: number | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface ListContentModerationLogsParams {
|
|
page?: number
|
|
page_size?: number
|
|
result?: string
|
|
group_id?: number
|
|
endpoint?: string
|
|
search?: string
|
|
from?: string
|
|
to?: string
|
|
}
|
|
|
|
export interface ContentModerationLogsResponse {
|
|
items: ContentModerationLog[]
|
|
total: number
|
|
page: number
|
|
page_size: number
|
|
pages: number
|
|
}
|
|
|
|
export interface ContentModerationUnbanUserResponse {
|
|
user_id: number
|
|
status: string
|
|
}
|
|
|
|
export interface DeleteFlaggedHashResponse {
|
|
input_hash: string
|
|
deleted: boolean
|
|
}
|
|
|
|
export interface ClearFlaggedHashesResponse {
|
|
deleted: number
|
|
}
|
|
|
|
export async function getConfig(): Promise<ContentModerationConfig> {
|
|
const { data } = await apiClient.get<ContentModerationConfig>('/admin/risk-control/config')
|
|
return data
|
|
}
|
|
|
|
export async function updateConfig(
|
|
payload: UpdateContentModerationConfig
|
|
): Promise<ContentModerationConfig> {
|
|
const { data } = await apiClient.put<ContentModerationConfig>('/admin/risk-control/config', payload)
|
|
return data
|
|
}
|
|
|
|
export async function getStatus(): Promise<ContentModerationRuntimeStatus> {
|
|
const { data } = await apiClient.get<ContentModerationRuntimeStatus>('/admin/risk-control/status')
|
|
return data
|
|
}
|
|
|
|
export async function testAPIKeys(
|
|
payload: TestContentModerationAPIKeysPayload = {}
|
|
): Promise<TestContentModerationAPIKeysResponse> {
|
|
const { data } = await apiClient.post<TestContentModerationAPIKeysResponse>('/admin/risk-control/api-keys/test', payload)
|
|
return data
|
|
}
|
|
|
|
export async function listLogs(
|
|
params: ListContentModerationLogsParams = {}
|
|
): Promise<ContentModerationLogsResponse> {
|
|
const { data } = await apiClient.get<ContentModerationLogsResponse>('/admin/risk-control/logs', {
|
|
params,
|
|
})
|
|
return data
|
|
}
|
|
|
|
export async function unbanUser(userID: number): Promise<ContentModerationUnbanUserResponse> {
|
|
const { data } = await apiClient.post<ContentModerationUnbanUserResponse>(
|
|
`/admin/risk-control/users/${userID}/unban`
|
|
)
|
|
return data
|
|
}
|
|
|
|
export async function deleteFlaggedHash(inputHash: string): Promise<DeleteFlaggedHashResponse> {
|
|
const { data } = await apiClient.delete<DeleteFlaggedHashResponse>('/admin/risk-control/hashes', {
|
|
data: { input_hash: inputHash },
|
|
})
|
|
return data
|
|
}
|
|
|
|
export async function clearFlaggedHashes(): Promise<ClearFlaggedHashesResponse> {
|
|
const { data } = await apiClient.delete<ClearFlaggedHashesResponse>('/admin/risk-control/hashes/all')
|
|
return data
|
|
}
|
|
|
|
export const riskControlAPI = {
|
|
getConfig,
|
|
updateConfig,
|
|
getStatus,
|
|
testAPIKeys,
|
|
listLogs,
|
|
unbanUser,
|
|
deleteFlaggedHash,
|
|
clearFlaggedHashes,
|
|
}
|
|
|
|
export default riskControlAPI
|