mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(gateway): add reversible chat pii redaction
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 // 是否配置了扩展操作(余额监控等)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取认证模块状态(公开接口,供登录页使用)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user