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