Redesign sensitive info protection settings

This commit is contained in:
fawney19
2026-05-14 11:14:20 +08:00
parent 91955ad1e0
commit 509bd30252
71 changed files with 3254 additions and 884 deletions

View File

@@ -120,6 +120,7 @@ export interface UserExport {
rate_limit?: number | null // null = 跟随系统默认0 = 不限制
rate_limit_mode?: 'inherit' | 'system' | 'custom'
model_capability_settings?: Record<string, Record<string, boolean>>
feature_settings?: Record<string, unknown> | null
group_ids?: string[]
group_names?: string[]
unlimited?: boolean
@@ -140,6 +141,7 @@ export interface UserApiKeyExport {
rate_limit?: number | null // legacy/null 兼容1.3+ standalone null = 跟随系统默认
concurrent_limit?: number | null
force_capabilities?: Record<string, boolean>
feature_settings?: Record<string, unknown> | null
is_active: boolean
expires_at?: string | null
auto_delete_on_expiry?: boolean
@@ -453,6 +455,7 @@ export interface AdminApiKey {
allowed_providers?: string[] | null // 允许的提供商列表
allowed_api_formats?: string[] | null // 允许的 API 格式列表
allowed_models?: string[] | null // 允许的模型列表
feature_settings?: Record<string, unknown> | null
auto_delete_on_expiry?: boolean // 过期后是否自动删除
last_used_at?: string
expires_at?: string
@@ -472,6 +475,7 @@ export interface CreateStandaloneApiKeyRequest {
initial_balance_usd: number | null // 初始余额null = 无限制
unlimited_balance?: boolean | null // 编辑时仅切换额度模式,不调整余额数值
auto_delete_on_expiry?: boolean // 过期后是否自动删除
feature_settings?: Record<string, unknown> | null
}
export interface AdminApiKeysResponse {

View File

@@ -4,6 +4,7 @@ import type { TieredPricingConfig } from './endpoints/types'
import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
import type { UserSession } from '@/types/session'
import type { FeatureSettingsMap } from '@/utils/featureSettings'
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
@@ -22,6 +23,7 @@ export interface Profile {
auth_source: 'local' | 'ldap' | 'oauth'
has_password: boolean
preferences?: UserPreferences
feature_settings?: FeatureSettingsMap | null
}
export interface UserPreferences {
@@ -169,6 +171,7 @@ export interface ApiKey {
concurrent_limit?: number | null
allowed_providers?: ProviderConfig[]
force_capabilities?: Record<string, boolean> | null // 强制能力配置
feature_settings?: FeatureSettingsMap | null
}
export type InstallTargetCli = 'claude_code' | 'codex_cli' | 'gemini_cli'
@@ -205,6 +208,7 @@ export const meApi = {
async updateProfile(data: {
email?: string
username?: string
feature_settings?: FeatureSettingsMap | null
}): Promise<{ message: string }> {
const response = await apiClient.put('/api/users/me', data)
return response.data
@@ -244,7 +248,7 @@ export const meApi = {
return response.data
},
async createApiKey(data: { name: string; rate_limit?: number | null; concurrent_limit?: number | null }): Promise<ApiKey> {
async createApiKey(data: { name: string; rate_limit?: number | null; concurrent_limit?: number | null; feature_settings?: FeatureSettingsMap | null }): Promise<ApiKey> {
const response = await apiClient.post<ApiKey>('/api/users/me/api-keys', data)
return response.data
},
@@ -277,7 +281,7 @@ export const meApi = {
async updateApiKey(
keyId: string,
data: { name?: string; rate_limit?: number | null; concurrent_limit?: number | null }
data: { name?: string; rate_limit?: number | null; concurrent_limit?: number | null; feature_settings?: FeatureSettingsMap | null | undefined }
): Promise<ApiKey & { message: string }> {
const response = await apiClient.put<ApiKey & { message: string }>(
`/api/users/me/api-keys/${keyId}`,

View File

@@ -23,84 +23,127 @@ 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 ChatPiiRedactionRuleFeatures {
validator?: string | null
[key: string]: unknown
}
export interface ChatPiiRedactionRule {
id: string
name: string
pattern: string
enabled: boolean
system?: boolean
features?: ChatPiiRedactionRuleFeatures | null
}
export interface ChatPiiRedactionConfig {
enabled: boolean
provider_scope: ChatPiiRedactionProviderScope
entities: ChatPiiRedactionEntity[]
rules: ChatPiiRedactionRule[]
cache_ttl_seconds: ChatPiiRedactionTtlSeconds
inject_model_instruction: boolean
placeholder_prefix: string
}
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',
export const CHAT_PII_REDACTION_DEFAULT_RULES: ChatPiiRedactionRule[] = [
{ id: 'email', name: '邮箱', pattern: '(?i)[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\\.[A-Z]{2,63}', enabled: true, features: { validator: 'email' }, system: true },
{ id: 'cn_phone', name: '手机号', pattern: '(?:\\+?86[- ]?)?(?:1[3-9]\\d[- ]?\\d{4}[- ]?\\d{4}|0\\d{2,3}[- ]\\d{7,8}(?:-\\d{1,6})?)', enabled: true, features: { validator: 'cn_phone' }, system: true },
{ id: 'global_phone', name: '国际号码', pattern: '\\+[1-9]\\d(?:[ -]?\\d){6,13}\\d', enabled: true, features: { validator: 'global_phone' }, system: true },
{ id: 'cn_id', name: '身份证号', pattern: '(?i)\\b\\d{17}[\\dX]\\b', enabled: true, features: { validator: 'cn_id' }, system: true },
{ id: 'payment_card', name: '银行卡号', pattern: '\\b(?:\\d[ -]?){12,18}\\d\\b', enabled: true, features: { validator: 'payment_card' }, system: true },
{ id: 'ipv4', name: 'IPv4', pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b', enabled: true, features: { validator: 'ipv4' }, system: true },
{ id: 'ipv6', name: 'IPv6', pattern: '\\b(?:[0-9A-Fa-f]{1,4}:){2,7}[0-9A-Fa-f:.]{1,39}\\b', enabled: true, features: { validator: 'ipv6' }, system: true },
{ id: 'api_key', name: 'API Key', pattern: '\\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|(?:gh[pousr]_[A-Za-z0-9_]{30,}|github_pat_[A-Za-z0-9_]{30,})|xox[baprs]-[A-Za-z0-9-]{20,}|(?:AKIA|ASIA)[0-9A-Z]{16}|[A-Za-z0-9_-]{32,})\\b', enabled: true, features: { validator: 'api_key' }, system: true },
{ id: 'access_token', name: 'Access Token', pattern: "(?i)\\baccess[_-]?token\\s*[:=]\\s*[\"']?[A-Za-z0-9._~+/=-]{20,}", enabled: true, features: { validator: 'access_token' }, system: true },
{ id: 'secret_key', name: 'Secret Key', pattern: "(?i)\\bsecret[_-]?key\\s*[:=]\\s*[\"']?[A-Za-z0-9._~+/=-]{20,}", enabled: true, features: { validator: 'secret_key' }, system: true },
{ id: 'bearer_token', name: 'Bearer Token', pattern: '(?i)\\bBearer\\s+[A-Za-z0-9._~+/=-]{20,}', enabled: true, features: { validator: 'bearer_token' }, system: true },
{ id: 'jwt', name: 'JWT', pattern: '\\b[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b', enabled: true, features: { validator: 'jwt' }, system: true },
]
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',
rules: 'module.chat_pii_redaction.rules',
cache_ttl_seconds: 'module.chat_pii_redaction.cache_ttl_seconds',
inject_model_instruction: 'module.chat_pii_redaction.inject_model_instruction',
placeholder_prefix: 'module.chat_pii_redaction.placeholder_prefix',
} as const
const CHAT_PII_REDACTION_DEFAULT_CONFIG: ChatPiiRedactionConfig = {
enabled: false,
provider_scope: 'selected_providers',
entities: [...CHAT_PII_REDACTION_ENTITIES],
rules: CHAT_PII_REDACTION_DEFAULT_RULES.map(rule => ({ ...rule })),
cache_ttl_seconds: 300,
inject_model_instruction: true,
placeholder_prefix: 'AETHER',
}
function isChatPiiRedactionEntity(value: unknown): value is ChatPiiRedactionEntity {
return typeof value === 'string' && CHAT_PII_REDACTION_ENTITIES.includes(value as ChatPiiRedactionEntity)
function cloneDefaultChatPiiRedactionRules(): ChatPiiRedactionRule[] {
return CHAT_PII_REDACTION_DEFAULT_RULES.map(rule => ({ ...rule }))
}
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)
function normalizeChatPiiRedactionRule(value: unknown, index: number): ChatPiiRedactionRule | null {
if (!value || typeof value !== 'object') return null
const item = value as Record<string, unknown>
const id = typeof item.id === 'string' && item.id.trim()
? item.id.trim()
: `custom_${index + 1}`
const name = typeof item.name === 'string' && item.name.trim()
? item.name.trim()
: id
const pattern = typeof item.pattern === 'string' ? item.pattern : ''
if (!pattern.trim()) return null
const rawFeatures = item.features && typeof item.features === 'object' && !Array.isArray(item.features)
? { ...(item.features as Record<string, unknown>) }
: {}
const legacyValidator = typeof item.kind === 'string' && item.kind.trim()
? item.kind.trim()
: null
const validator = typeof rawFeatures.validator === 'string' && rawFeatures.validator.trim()
? rawFeatures.validator.trim()
: legacyValidator
if (validator) {
rawFeatures.validator = validator
} else {
delete rawFeatures.validator
}
const features = Object.keys(rawFeatures).length > 0 ? rawFeatures : null
return {
id,
name,
pattern,
enabled: item.enabled !== false,
system: item.system === true,
features,
}
return CHAT_PII_REDACTION_ENTITIES.filter((item) => unique.has(item))
}
function normalizeChatPiiRedactionConfig(values: Record<keyof ChatPiiRedactionConfig, unknown>): ChatPiiRedactionConfig {
function normalizeChatPiiRedactionRules(value: unknown): ChatPiiRedactionRule[] {
if (!Array.isArray(value)) return cloneDefaultChatPiiRedactionRules()
return value
.map((item, index) => normalizeChatPiiRedactionRule(item, index))
.filter((item): item is ChatPiiRedactionRule => item !== null)
}
function normalizeChatPiiRedactionConfig(values: {
enabled: unknown
rules: unknown
cache_ttl_seconds: unknown
placeholder_prefix: unknown
}): ChatPiiRedactionConfig {
return {
enabled: values.enabled === true,
provider_scope: values.provider_scope === 'all_providers' ? 'all_providers' : 'selected_providers',
entities: normalizeChatPiiRedactionEntities(values.entities),
rules: normalizeChatPiiRedactionRules(values.rules),
cache_ttl_seconds: values.cache_ttl_seconds === 3600 ? 3600 : 300,
inject_model_instruction: values.inject_model_instruction !== false,
placeholder_prefix: normalizePlaceholderPrefix(values.placeholder_prefix),
}
}
function normalizePlaceholderPrefix(value: unknown): string {
if (typeof value !== 'string') return CHAT_PII_REDACTION_DEFAULT_CONFIG.placeholder_prefix
const normalized = value.trim().toUpperCase()
return /^[A-Z0-9_]{1,32}$/.test(normalized)
? normalized
: CHAT_PII_REDACTION_DEFAULT_CONFIG.placeholder_prefix
}
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
@@ -147,38 +190,34 @@ export const modulesApi = {
},
async getChatPiiRedactionConfig(): Promise<ChatPiiRedactionConfig> {
const [enabled, providerScope, entities, cacheTtlSeconds, injectModelInstruction] = await Promise.all([
const [enabled, rules, cacheTtlSeconds, placeholderPrefix] = 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.rules),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.inject_model_instruction),
getSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.placeholder_prefix),
])
return normalizeChatPiiRedactionConfig({
enabled,
provider_scope: providerScope,
entities,
rules,
cache_ttl_seconds: cacheTtlSeconds,
inject_model_instruction: injectModelInstruction,
placeholder_prefix: placeholderPrefix,
})
},
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, '敏感信息替换保护模型提示说明'),
const [enabled, rules, cacheTtlSeconds, placeholderPrefix] = await Promise.all([
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.enabled, config.enabled, '敏感信息保护总开关'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.rules, config.rules, '敏感信息保护替换规则'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.cache_ttl_seconds, config.cache_ttl_seconds, '敏感信息保护缓存 TTL'),
updateSystemConfigValue(CHAT_PII_REDACTION_CONFIG_KEYS.placeholder_prefix, config.placeholder_prefix, '敏感信息保护占位符前缀'),
])
return normalizeChatPiiRedactionConfig({
enabled,
provider_scope: providerScope,
entities,
rules,
cache_ttl_seconds: cacheTtlSeconds,
inject_model_instruction: injectModelInstruction,
placeholder_prefix: placeholderPrefix,
})
},

View File

@@ -5,6 +5,7 @@ import type { UserSession as SessionRecord } from '@/types/session'
export type UserRole = 'admin' | 'user'
export type ListPolicyMode = 'inherit' | 'unrestricted' | 'specific' | 'deny_all'
export type RateLimitPolicyMode = 'inherit' | 'system' | 'custom'
export type FeatureSettings = Record<string, unknown>
export interface UserGroupSummary {
id: string
@@ -35,6 +36,7 @@ export interface User {
role: UserRole
is_active: boolean
unlimited: boolean
feature_settings?: FeatureSettings | null
allowed_providers: string[] | null // 允许使用的提供商 ID 列表
allowed_providers_mode?: ListPolicyMode
allowed_api_formats: string[] | null // 允许使用的 API 格式列表
@@ -60,6 +62,7 @@ export interface CreateUserRequest {
initial_gift_usd?: number | null
unlimited?: boolean
group_ids?: string[]
feature_settings?: FeatureSettings | null
}
export interface UpdateUserRequest {
@@ -69,6 +72,7 @@ export interface UpdateUserRequest {
unlimited?: boolean
password?: string
group_ids?: string[]
feature_settings?: FeatureSettings | null
}
export interface UserBatchSelectionFilters {
@@ -213,6 +217,7 @@ export interface ApiKey {
is_active: boolean
is_locked: boolean // 管理员锁定标志
is_standalone: boolean // 是否为独立余额Key
feature_settings?: FeatureSettings | null
rate_limit?: number | null // 普通Key: 0 = 不限制,历史 null 视为跟随系统默认
concurrent_limit?: number | null // 普通Key: 0 = 不限制并发,历史 null 兼容
total_requests?: number // 总请求数
@@ -223,6 +228,7 @@ export interface UpsertUserApiKeyRequest {
name?: string
rate_limit?: number | null
concurrent_limit?: number | null
feature_settings?: FeatureSettings | null
}
export type UserSession = SessionRecord