mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
Redesign sensitive info protection settings
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user