feat(gateway): add reversible chat pii redaction

This commit is contained in:
Kayphoon
2026-05-13 18:25:13 +08:00
parent 3c2497f019
commit 2958041dc7
43 changed files with 7770 additions and 126 deletions

View File

@@ -4,10 +4,14 @@ import type {
ClaudeCodeAdvancedConfig,
FailoverRulesConfig,
PoolAdvancedConfig,
ProviderConfig,
ProviderWithEndpointsSummary,
ProxyConfig,
} from './types'
import { normalizePoolAdvancedConfig as normalizePoolAdvanced } from './types'
import {
normalizeChatPiiRedactionProviderConfig as normalizeChatPiiRedactionProvider,
normalizePoolAdvancedConfig as normalizePoolAdvanced,
} from './types'
interface ProviderRequestOptions {
timeout?: number
@@ -42,6 +46,7 @@ function normalizeProviderSummary(
): ProviderWithEndpointsSummary {
return {
...provider,
chat_pii_redaction: normalizeChatPiiRedactionProvider(provider.chat_pii_redaction),
pool_advanced: normalizePoolAdvanced(provider.pool_advanced),
}
}
@@ -107,6 +112,7 @@ export async function updateProvider(
claude_code_advanced: ClaudeCodeAdvancedConfig | null
pool_advanced: PoolAdvancedConfig | null
failover_rules: FailoverRulesConfig | null
config: ProviderConfig | null
}>,
requestOptions?: ProviderRequestOptions,
): Promise<ProviderWithEndpointsSummary> {
@@ -138,6 +144,7 @@ export async function createProvider(
claude_code_advanced?: ClaudeCodeAdvancedConfig | null
pool_advanced?: PoolAdvancedConfig | null
failover_rules?: FailoverRulesConfig | null
config?: ProviderConfig | null
}
): Promise<{ id: string; name: string; message?: string }> {
const response = await client.post('/api/admin/providers/', data)

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { normalizePoolAdvancedConfig } from '@/api/endpoints/types'
import { normalizeChatPiiRedactionProviderConfig, normalizePoolAdvancedConfig } from '@/api/endpoints/types'
describe('normalizePoolAdvancedConfig', () => {
it('keeps object payloads, including empty objects', () => {
@@ -19,3 +19,17 @@ describe('normalizePoolAdvancedConfig', () => {
expect(normalizePoolAdvancedConfig(['lru'])).toBeNull()
})
})
describe('normalizeChatPiiRedactionProviderConfig', () => {
it('defaults unsupported payloads to disabled', () => {
expect(normalizeChatPiiRedactionProviderConfig(null)).toEqual({ enabled: false })
expect(normalizeChatPiiRedactionProviderConfig({})).toEqual({ enabled: false })
expect(normalizeChatPiiRedactionProviderConfig({ enabled: 'yes' })).toEqual({ enabled: false })
})
it('passes through enabled state only', () => {
expect(normalizeChatPiiRedactionProviderConfig({ enabled: true })).toEqual({ enabled: true })
expect(normalizeChatPiiRedactionProviderConfig({ enabled: false, entities: ['email'] })).toEqual({ enabled: false })
})
})

View File

@@ -176,6 +176,18 @@ export interface FormatAcceptanceConfig {
reject_formats?: string[] // 黑名单:拒绝哪些格式(优先级高于白名单)
}
export interface ChatPiiRedactionProviderConfig {
enabled: boolean
}
export interface ProviderConfig {
chat_pii_redaction?: ChatPiiRedactionProviderConfig
pool_advanced?: PoolAdvancedConfig
failover_rules?: FailoverRulesConfig
claude_code_advanced?: ClaudeCodeAdvancedConfig
[key: string]: unknown
}
export interface ProviderEndpoint {
id: string
provider_id: string
@@ -579,6 +591,13 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
export function normalizeChatPiiRedactionProviderConfig(value: unknown): ChatPiiRedactionProviderConfig {
if (!isPlainObject(value) || typeof value.enabled !== 'boolean') {
return { enabled: false }
}
return { enabled: value.enabled }
}
export function normalizePoolAdvancedConfig(value: unknown): PoolAdvancedConfig | null {
if (value == null || value === false) return null
if (value === true) return {}
@@ -631,6 +650,7 @@ export interface ProviderWithEndpointsSummary {
api_formats: string[]
endpoint_health_details: EndpointHealthDetail[]
claude_code_advanced?: ClaudeCodeAdvancedConfig | null
chat_pii_redaction?: ChatPiiRedactionProviderConfig | null
pool_advanced?: PoolAdvancedConfig | null
failover_rules?: FailoverRulesConfig | null
ops_configured: boolean // 是否配置了扩展操作(余额监控等)

View File

@@ -23,6 +23,97 @@ export interface AuthModuleInfo {
active: boolean
}
export type ChatPiiRedactionProviderScope = 'all_providers' | 'selected_providers'
export type ChatPiiRedactionTtlSeconds = 300 | 3600
export type ChatPiiRedactionEntity =
| 'email'
| 'cn_phone'
| 'global_phone'
| 'cn_id'
| 'payment_card'
| 'ipv4'
| 'ipv6'
| 'api_key'
| 'access_token'
| 'secret_key'
| 'bearer_token'
| 'jwt'
export interface ChatPiiRedactionConfig {
enabled: boolean
provider_scope: ChatPiiRedactionProviderScope
entities: ChatPiiRedactionEntity[]
cache_ttl_seconds: ChatPiiRedactionTtlSeconds
inject_model_instruction: boolean
}
const CHAT_PII_REDACTION_ENTITIES: ChatPiiRedactionEntity[] = [
'email',
'cn_phone',
'global_phone',
'cn_id',
'payment_card',
'ipv4',
'ipv6',
'api_key',
'access_token',
'secret_key',
'bearer_token',
'jwt',
]
const CHAT_PII_REDACTION_CONFIG_KEYS = {
enabled: 'module.chat_pii_redaction.enabled',
provider_scope: 'module.chat_pii_redaction.provider_scope',
entities: 'module.chat_pii_redaction.entities',
cache_ttl_seconds: 'module.chat_pii_redaction.cache_ttl_seconds',
inject_model_instruction: 'module.chat_pii_redaction.inject_model_instruction',
} as const
const CHAT_PII_REDACTION_DEFAULT_CONFIG: ChatPiiRedactionConfig = {
enabled: false,
provider_scope: 'selected_providers',
entities: [...CHAT_PII_REDACTION_ENTITIES],
cache_ttl_seconds: 300,
inject_model_instruction: true,
}
function isChatPiiRedactionEntity(value: unknown): value is ChatPiiRedactionEntity {
return typeof value === 'string' && CHAT_PII_REDACTION_ENTITIES.includes(value as ChatPiiRedactionEntity)
}
function normalizeChatPiiRedactionEntities(value: unknown): ChatPiiRedactionEntity[] {
if (!Array.isArray(value)) return [...CHAT_PII_REDACTION_DEFAULT_CONFIG.entities]
const unique = new Set<ChatPiiRedactionEntity>()
for (const item of value) {
if (isChatPiiRedactionEntity(item)) unique.add(item)
}
return CHAT_PII_REDACTION_ENTITIES.filter((item) => unique.has(item))
}
function normalizeChatPiiRedactionConfig(values: Record<keyof ChatPiiRedactionConfig, unknown>): ChatPiiRedactionConfig {
return {
enabled: values.enabled === true,
provider_scope: values.provider_scope === 'all_providers' ? 'all_providers' : 'selected_providers',
entities: normalizeChatPiiRedactionEntities(values.entities),
cache_ttl_seconds: values.cache_ttl_seconds === 3600 ? 3600 : 300,
inject_model_instruction: values.inject_model_instruction !== false,
}
}
async function getSystemConfigValue(key: string): Promise<unknown> {
const response = await apiClient.get<{ key: string; value: unknown }>(`/api/admin/system/configs/${key}`)
return response.data.value
}
async function updateSystemConfigValue(key: string, value: unknown, description: string) {
const response = await apiClient.put<{ key: string; value: unknown; description?: string }>(
`/api/admin/system/configs/${key}`,
{ value, description },
)
return response.data.value
}
export const modulesApi = {
/**
* 获取所有模块状态(管理员)
@@ -55,6 +146,42 @@ export const modulesApi = {
return response.data
},
async getChatPiiRedactionConfig(): Promise<ChatPiiRedactionConfig> {
const [enabled, providerScope, entities, cacheTtlSeconds, injectModelInstruction] = await Promise.all([
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.enabled),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.provider_scope),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.entities),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.inject_model_instruction),
])
return normalizeChatPiiRedactionConfig({
enabled,
provider_scope: providerScope,
entities,
cache_ttl_seconds: cacheTtlSeconds,
inject_model_instruction: injectModelInstruction,
})
},
async updateChatPiiRedactionConfig(config: ChatPiiRedactionConfig): Promise<ChatPiiRedactionConfig> {
const [enabled, providerScope, entities, cacheTtlSeconds, injectModelInstruction] = await Promise.all([
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.enabled, config.enabled, '敏感信息替换保护总开关'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.provider_scope, config.provider_scope, '敏感信息替换保护启用范围'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.entities, config.entities, '敏感信息替换保护检测类型'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds, config.cache_ttl_seconds, '敏感信息替换保护缓存 TTL'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.inject_model_instruction, config.inject_model_instruction, '敏感信息替换保护模型提示说明'),
])
return normalizeChatPiiRedactionConfig({
enabled,
provider_scope: providerScope,
entities,
cache_ttl_seconds: cacheTtlSeconds,
inject_model_instruction: injectModelInstruction,
})
},
/**
* 获取认证模块状态(公开接口,供登录页使用)
*/