mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
Merge remote-tracking branch 'origin/pr/451' into aether-rust-pioneer
This commit is contained in:
@@ -268,6 +268,34 @@
|
||||
@update:model-value="(v: boolean) => form.pool_mode_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between gap-4 p-3 border rounded-lg bg-muted/50"
|
||||
:class="redactionModuleScope === 'all_providers' ? 'border-primary/30 bg-primary/5' : ''"
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">敏感信息替换保护</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
{{ redactionHelperText }}
|
||||
</p>
|
||||
<p
|
||||
v-if="!redactionModuleEnabled"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
模块总开关未开启,保存此供应商不会立即生效。
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ redactionSwitchLabel }}
|
||||
</span>
|
||||
<Switch
|
||||
:model-value="redactionSwitchValue"
|
||||
:disabled="redactionModuleScope === 'all_providers' || redactionModuleLoading"
|
||||
@update:model-value="(v: boolean) => form.chat_pii_redaction_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -313,9 +341,12 @@ import {
|
||||
updateProvider,
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { modulesApi, type ChatPiiRedactionProviderScope } from '@/api/modules'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { dateTimeLocalToRfc3339, formatDateTimeLocalInput } from '@/utils/date'
|
||||
import { getProviderRedactionConfig, withProviderRedactionConfig } from '@/features/providers/utils/providerRedactionPayload'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -331,6 +362,9 @@ const emit = defineEmits<{
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const loading = ref(false)
|
||||
const redactionModuleLoading = ref(false)
|
||||
const redactionModuleEnabled = ref(false)
|
||||
const redactionModuleScope = ref<ChatPiiRedactionProviderScope>('selected_providers')
|
||||
|
||||
// 内部状态
|
||||
const internalOpen = computed(() => props.modelValue)
|
||||
@@ -368,8 +402,43 @@ const form = ref({
|
||||
request_timeout: undefined as number | undefined,
|
||||
// 号池模式
|
||||
pool_mode_enabled: false,
|
||||
chat_pii_redaction_enabled: false,
|
||||
})
|
||||
|
||||
const redactionSwitchValue = computed(() => {
|
||||
if (redactionModuleScope.value === 'all_providers') {
|
||||
return redactionModuleEnabled.value
|
||||
}
|
||||
return form.value.chat_pii_redaction_enabled
|
||||
})
|
||||
|
||||
const redactionSwitchLabel = computed(() => {
|
||||
if (redactionModuleScope.value === 'all_providers') {
|
||||
return redactionModuleEnabled.value ? '继承开启' : '未生效'
|
||||
}
|
||||
return form.value.chat_pii_redaction_enabled ? '已开启' : '未开启'
|
||||
})
|
||||
|
||||
const redactionHelperText = computed(() => {
|
||||
if (redactionModuleScope.value === 'all_providers') {
|
||||
return '模块管理已设置为“全部供应商”,此供应商会自动执行替换保护。替换类型由模块管理中的“替换类型配置”决定。'
|
||||
}
|
||||
return '仅当“开启敏感信息替换保护”和此供应商开关都开启时生效。替换类型在模块管理的“替换类型配置”中统一选择,适用于所有已开启该功能的供应商。供应商只会看到占位符,客户端响应会自动还原。'
|
||||
})
|
||||
|
||||
async function loadRedactionModuleState() {
|
||||
redactionModuleLoading.value = true
|
||||
try {
|
||||
const config = await modulesApi.getChatPiiRedactionConfig()
|
||||
redactionModuleEnabled.value = config.enabled
|
||||
redactionModuleScope.value = config.provider_scope
|
||||
} catch (err) {
|
||||
log.warn('加载敏感信息替换保护模块配置失败', err)
|
||||
} finally {
|
||||
redactionModuleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
@@ -394,6 +463,7 @@ function resetForm() {
|
||||
request_timeout: undefined,
|
||||
// 号池模式
|
||||
pool_mode_enabled: false,
|
||||
chat_pii_redaction_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +494,7 @@ function loadProviderData() {
|
||||
request_timeout: props.provider.request_timeout ?? undefined,
|
||||
// 号池模式
|
||||
pool_mode_enabled: poolAdvanced !== null,
|
||||
chat_pii_redaction_enabled: getProviderRedactionConfig(props.provider).enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +508,12 @@ const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
|
||||
resetForm,
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) {
|
||||
loadRedactionModuleState()
|
||||
}
|
||||
})
|
||||
|
||||
// 新建模式下切换 provider_type 时不自动开启号池模式
|
||||
watch(() => form.value.provider_type, () => {
|
||||
if (!isEditMode.value) {
|
||||
@@ -471,7 +548,7 @@ const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
||||
const basePayload = {
|
||||
const basePayload = withProviderRedactionConfig({
|
||||
name: form.value.name,
|
||||
provider_type: form.value.provider_type,
|
||||
description: form.value.description || undefined,
|
||||
@@ -491,7 +568,7 @@ const handleSubmit = async () => {
|
||||
pool_advanced: form.value.pool_mode_enabled
|
||||
? (currentPoolAdvanced ?? {})
|
||||
: null,
|
||||
}
|
||||
}, form.value.chat_pii_redaction_enabled)
|
||||
|
||||
if (isEditMode.value && props.provider) {
|
||||
// 更新提供商
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints/types'
|
||||
import {
|
||||
buildProviderRedactionConfig,
|
||||
getProviderRedactionConfig,
|
||||
withProviderRedactionConfig,
|
||||
} from '../providerRedactionPayload'
|
||||
|
||||
function makeProvider(overrides: Partial<ProviderWithEndpointsSummary> = {}): ProviderWithEndpointsSummary {
|
||||
return {
|
||||
id: 'provider-1',
|
||||
name: 'Provider One',
|
||||
provider_priority: 1,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: true,
|
||||
is_active: true,
|
||||
total_endpoints: 0,
|
||||
active_endpoints: 0,
|
||||
total_keys: 0,
|
||||
active_keys: 0,
|
||||
total_models: 0,
|
||||
active_models: 0,
|
||||
global_model_ids: [],
|
||||
avg_health_score: 0,
|
||||
unhealthy_endpoints: 0,
|
||||
api_formats: [],
|
||||
endpoint_health_details: [],
|
||||
ops_configured: false,
|
||||
created_at: '2026-05-02T00:00:00Z',
|
||||
updated_at: '2026-05-02T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('provider redaction payload helpers', () => {
|
||||
it('defaults provider redaction to disabled', () => {
|
||||
expect(getProviderRedactionConfig()).toEqual({ enabled: false })
|
||||
expect(getProviderRedactionConfig(makeProvider())).toEqual({ enabled: false })
|
||||
})
|
||||
|
||||
it('loads existing provider redaction config', () => {
|
||||
const provider = makeProvider({ chat_pii_redaction: { enabled: true } })
|
||||
|
||||
expect(getProviderRedactionConfig(provider)).toEqual({ enabled: true })
|
||||
})
|
||||
|
||||
it('builds create and update payloads with provider-level enabled only', () => {
|
||||
expect(buildProviderRedactionConfig(true)).toEqual({
|
||||
chat_pii_redaction: { enabled: true },
|
||||
})
|
||||
|
||||
expect(
|
||||
withProviderRedactionConfig(
|
||||
{
|
||||
name: 'Provider One',
|
||||
config: { pool_advanced: { global_priority: 10 } },
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toEqual({
|
||||
name: 'Provider One',
|
||||
config: {
|
||||
pool_advanced: { global_priority: 10 },
|
||||
chat_pii_redaction: { enabled: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('does not include provider-level entity or ttl config', () => {
|
||||
const payload = withProviderRedactionConfig({ name: 'Provider One' }, true)
|
||||
|
||||
expect(payload.config.chat_pii_redaction).toEqual({ enabled: true })
|
||||
expect(payload.config.chat_pii_redaction).not.toHaveProperty('entities')
|
||||
expect(payload.config.chat_pii_redaction).not.toHaveProperty('cache_ttl_seconds')
|
||||
expect(payload.config.chat_pii_redaction).not.toHaveProperty('inject_model_instruction')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ProviderConfig, ProviderWithEndpointsSummary } from '@/api/endpoints/types'
|
||||
import { normalizeChatPiiRedactionProviderConfig } from '@/api/endpoints/types'
|
||||
|
||||
export const DEFAULT_PROVIDER_REDACTION_CONFIG = Object.freeze({ enabled: false })
|
||||
|
||||
type ProviderConfigWithRedaction = ProviderConfig & Required<Pick<ProviderConfig, 'chat_pii_redaction'>>
|
||||
|
||||
type ProviderRedactionPayload<TPayload extends object> = Omit<TPayload, 'config'> & {
|
||||
config: ProviderConfigWithRedaction
|
||||
}
|
||||
|
||||
export function getProviderRedactionConfig(provider?: ProviderWithEndpointsSummary | null) {
|
||||
return normalizeChatPiiRedactionProviderConfig(provider?.chat_pii_redaction)
|
||||
}
|
||||
|
||||
export function buildProviderRedactionConfig(enabled: boolean): ProviderConfigWithRedaction {
|
||||
return {
|
||||
chat_pii_redaction: { enabled },
|
||||
}
|
||||
}
|
||||
|
||||
export function withProviderRedactionConfig<TPayload extends object>(
|
||||
payload: TPayload & { config?: ProviderConfig | null },
|
||||
enabled: boolean,
|
||||
): ProviderRedactionPayload<TPayload> {
|
||||
const { config, ...payloadWithoutConfig } = payload
|
||||
|
||||
return {
|
||||
...payloadWithoutConfig,
|
||||
config: {
|
||||
...(config ?? {}),
|
||||
...buildProviderRedactionConfig(enabled),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user