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
+85
View File
@@ -450,6 +450,57 @@
{{ editingApiKey ? '留空表示保持当前值,填 0 表示不限并发' : '留空表示不限并发,填 0 也表示不限并发' }}
</p>
</div>
<div class="rounded-lg border border-border/60 bg-muted/30 p-4">
<div class="flex items-center justify-between gap-4">
<div>
<Label class="text-sm font-semibold">敏感信息保护</Label>
<p class="mt-1 text-xs text-muted-foreground">
{{ keyRedactionMode === 'inherit' ? '默认跟随账户设置' : '管理员开启功能后生效' }}
</p>
</div>
<div class="flex items-center gap-2">
<Button
size="sm"
:variant="keyRedactionMode === 'inherit' ? 'default' : 'outline'"
@click="keyRedactionMode = 'inherit'"
>
跟随账户
</Button>
<Button
size="sm"
:variant="keyRedactionMode === 'custom' ? 'default' : 'outline'"
@click="keyRedactionMode = 'custom'"
>
单独配置
</Button>
</div>
</div>
<div
v-if="keyRedactionMode === 'custom'"
class="mt-4 flex items-center justify-between gap-4 border-t border-border/50 pt-4"
>
<div>
<Label class="text-sm font-medium">启用保护</Label>
<p class="mt-1 text-xs text-muted-foreground">
只影响此 API Key
</p>
</div>
<Switch v-model="newKeyRedactionEnabled" />
</div>
<div
v-if="keyRedactionMode === 'custom' && newKeyRedactionEnabled"
class="mt-4 flex items-center justify-between gap-4 border-t border-border/50 pt-4"
>
<div>
<Label class="text-sm font-medium">占位符说明</Label>
<p class="mt-1 text-xs text-muted-foreground">
向模型说明占位符含义
</p>
</div>
<Switch v-model="newKeyRedactionInjectNotice" />
</div>
</div>
</div>
<template #footer>
@@ -667,6 +718,7 @@ import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
import Badge from '@/components/ui/badge.vue'
import Switch from '@/components/ui/switch.vue'
import { Dialog, Pagination } from '@/components/ui'
import { LoadingState, AlertDialog, EmptyState } from '@/components/common'
import {
@@ -685,6 +737,11 @@ import { parseApiError } from '@/utils/errorParser'
import { formatRateLimitSimple } from '@/utils/format'
import { parseNumberInput } from '@/utils/form'
import { getErrorStatus } from '@/types/api-error'
import {
hasChatPiiRedactionFeatureSettings,
mergeChatPiiRedactionFeatureSettings,
readChatPiiRedactionFeatureSettings,
} from '@/utils/featureSettings'
const { success, error: showError } = useToast()
@@ -722,6 +779,9 @@ const showInstallDialog = ref(false)
const newKeyName = ref('')
const newKeyRateLimit = ref<number | undefined>(undefined)
const newKeyConcurrentLimit = ref<number | undefined>(undefined)
const keyRedactionMode = ref<'inherit' | 'custom'>('inherit')
const newKeyRedactionEnabled = ref(false)
const newKeyRedactionInjectNotice = ref(true)
const newKeyValue = ref('')
const keyToDelete = ref<ApiKey | null>(null)
const editingApiKey = ref<ApiKey | null>(null)
@@ -801,10 +861,15 @@ function resetInstallCopiedState() {
}
function openEditApiKeyDialog(apiKey: ApiKey) {
const hasRedactionFeature = hasChatPiiRedactionFeatureSettings(apiKey.feature_settings)
const redactionFeature = readChatPiiRedactionFeatureSettings(apiKey.feature_settings)
editingApiKey.value = apiKey
newKeyName.value = apiKey.name || ''
newKeyRateLimit.value = apiKey.rate_limit ?? undefined
newKeyConcurrentLimit.value = apiKey.concurrent_limit ?? undefined
keyRedactionMode.value = hasRedactionFeature ? 'custom' : 'inherit'
newKeyRedactionEnabled.value = redactionFeature.enabled
newKeyRedactionInjectNotice.value = redactionFeature.inject_model_instruction
showCreateDialog.value = true
}
@@ -813,6 +878,9 @@ function openCreateApiKeyDialog() {
newKeyName.value = ''
newKeyRateLimit.value = undefined
newKeyConcurrentLimit.value = undefined
keyRedactionMode.value = 'inherit'
newKeyRedactionEnabled.value = false
newKeyRedactionInjectNotice.value = true
showCreateDialog.value = true
}
@@ -889,6 +957,9 @@ function closeApiKeyDialog() {
newKeyName.value = ''
newKeyRateLimit.value = undefined
newKeyConcurrentLimit.value = undefined
keyRedactionMode.value = 'inherit'
newKeyRedactionEnabled.value = false
newKeyRedactionInjectNotice.value = true
}
async function saveApiKey() {
@@ -905,6 +976,12 @@ async function saveApiKey() {
name: newKeyName.value,
rate_limit: newKeyRateLimit.value ?? 0,
concurrent_limit: newKeyConcurrentLimit.value,
feature_settings: keyRedactionMode.value === 'custom'
? mergeChatPiiRedactionFeatureSettings(editingApiKey.value.feature_settings, {
enabled: newKeyRedactionEnabled.value,
inject_model_instruction: newKeyRedactionInjectNotice.value,
})
: null,
})
success('API 密钥更新成功')
} else {
@@ -912,6 +989,14 @@ async function saveApiKey() {
name: newKeyName.value,
rate_limit: newKeyRateLimit.value ?? 0,
concurrent_limit: newKeyConcurrentLimit.value,
...(keyRedactionMode.value === 'custom'
? {
feature_settings: mergeChatPiiRedactionFeatureSettings(null, {
enabled: newKeyRedactionEnabled.value,
inject_model_instruction: newKeyRedactionInjectNotice.value,
}),
}
: {}),
})
newKeyValue.value = newKey.key || ''
if (isCreatingFirstApiKey) {
+92
View File
@@ -81,6 +81,49 @@
</form>
</Card>
<Card class="p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-medium text-foreground">
敏感信息保护
</h3>
<p class="text-sm text-muted-foreground mt-1">
管理员开启功能后,可默认应用到你的账户和 API Key
</p>
</div>
<Button
variant="outline"
:disabled="savingFeatureSettings || !hasFeatureSettingsChanges"
@click="updateFeatureSettings"
>
{{ savingFeatureSettings ? '保存中...' : '保存' }}
</Button>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between gap-4 rounded-lg border border-border/60 bg-muted/30 px-4 py-3">
<div>
<Label class="text-sm font-medium">默认启用</Label>
<p class="mt-1 text-xs text-muted-foreground">
未单独配置的 API Key 会跟随此设置
</p>
</div>
<Switch v-model="featureSettingsForm.chatPiiRedactionEnabled" />
</div>
<div class="flex items-center justify-between gap-4 rounded-lg border border-border/60 bg-muted/30 px-4 py-3">
<div>
<Label class="text-sm font-medium">占位符说明</Label>
<p class="mt-1 text-xs text-muted-foreground">
向模型说明占位符含义
</p>
</div>
<Switch
v-model="featureSettingsForm.chatPiiRedactionInjectNotice"
:disabled="!featureSettingsForm.chatPiiRedactionEnabled"
/>
</div>
</div>
</Card>
<!-- 密码设置(LDAP 用户不显示) -->
<Card
v-if="profile?.auth_source !== 'ldap'"
@@ -626,6 +669,10 @@ import { formatCurrency } from '@/utils/format'
import { getApiUrl } from '@/utils/url'
import { log } from '@/utils/logger'
import { getErrorMessage, getErrorStatus } from '@/types/api-error'
import {
mergeChatPiiRedactionFeatureSettings,
readChatPiiRedactionFeatureSettings,
} from '@/utils/featureSettings'
const authStore = useAuthStore()
const route = useRoute()
@@ -660,7 +707,13 @@ const preferencesForm = ref({
}
})
const featureSettingsForm = ref({
chatPiiRedactionEnabled: false,
chatPiiRedactionInjectNotice: true,
})
const savingProfile = ref(false)
const savingFeatureSettings = ref(false)
const changingPassword = ref(false)
const sessionsLoading = ref(false)
const sessionActionLoading = ref<string | null>(null)
@@ -679,6 +732,7 @@ const emailConfigured = ref(false) // 系统是否配置了邮箱服务
// 原始值,用于检测是否有修改
const originalProfileForm = ref({ email: '', username: '' })
const originalPreferencesForm = ref({ avatar_url: '', bio: '' })
const originalFeatureSettingsForm = ref({ ...featureSettingsForm.value })
// 检测基本信息是否有修改
const hasProfileChanges = computed(() => {
@@ -690,6 +744,13 @@ const hasProfileChanges = computed(() => {
)
})
const hasFeatureSettingsChanges = computed(() => {
return (
featureSettingsForm.value.chatPiiRedactionEnabled !== originalFeatureSettingsForm.value.chatPiiRedactionEnabled ||
featureSettingsForm.value.chatPiiRedactionInjectNotice !== originalFeatureSettingsForm.value.chatPiiRedactionInjectNotice
)
})
const passwordPolicyHint = computed(() => getPasswordPolicyHint(passwordPolicyLevel.value))
const passwordError = computed(() =>
validatePasswordByPolicy(passwordForm.value.new_password, passwordPolicyLevel.value)
@@ -752,14 +813,45 @@ async function loadProfile() {
email: profile.value.email || '',
username: profile.value.username
}
const redactionFeature = readChatPiiRedactionFeatureSettings(profile.value.feature_settings)
featureSettingsForm.value = {
chatPiiRedactionEnabled: redactionFeature.enabled,
chatPiiRedactionInjectNotice: redactionFeature.inject_model_instruction,
}
// 保存原始值
originalProfileForm.value = { ...profileForm.value }
originalFeatureSettingsForm.value = { ...featureSettingsForm.value }
} catch (error) {
log.error('加载个人信息失败:', error)
showError('加载个人信息失败')
}
}
async function updateFeatureSettings() {
savingFeatureSettings.value = true
try {
await meApi.updateProfile({
feature_settings: mergeChatPiiRedactionFeatureSettings(profile.value?.feature_settings, {
enabled: featureSettingsForm.value.chatPiiRedactionEnabled,
inject_model_instruction: featureSettingsForm.value.chatPiiRedactionInjectNotice,
}),
})
if (profile.value) {
profile.value.feature_settings = mergeChatPiiRedactionFeatureSettings(profile.value.feature_settings, {
enabled: featureSettingsForm.value.chatPiiRedactionEnabled,
inject_model_instruction: featureSettingsForm.value.chatPiiRedactionInjectNotice,
})
}
originalFeatureSettingsForm.value = { ...featureSettingsForm.value }
success('敏感信息保护设置已保存')
} catch (err) {
log.error('更新敏感信息保护设置失败:', err)
showError(getErrorMessage(err), '更新敏感信息保护设置失败')
} finally {
savingFeatureSettings.value = false
}
}
async function loadSessions() {
sessionsLoading.value = true
try {