diff --git a/src/stores/aiChat.ts b/src/stores/aiChat.ts index 195b4fd0..67f0709f 100644 --- a/src/stores/aiChat.ts +++ b/src/stores/aiChat.ts @@ -17,6 +17,7 @@ import { useSkillStore } from '@/stores/skill' import { useLLMStore } from '@/stores/llm' import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '@electron/shared/types' import { useAgentStreamService } from '@/services/ai-stream/service' +import { buildSerializablePreprocessConfig, shouldEnsureDesensitizeRulesBeforeSerialize } from './aiPreprocessConfig' // 工具调用记录 export interface ToolCallRecord { @@ -811,7 +812,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => { ? { platformId: state.ownerInfo.platformId, displayName: state.ownerInfo.displayName } : undefined, mentionedMembers: currentMentionedMembers.length > 0 ? currentMentionedMembers : undefined, - preprocessConfig: buildSerializablePreprocessConfig(), + preprocessConfig: await buildReadySerializablePreprocessConfig(), searchContextBefore: aiGlobalSettings.value.searchContextBefore, searchContextAfter: aiGlobalSettings.value.searchContextAfter, } @@ -1096,30 +1097,13 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => { } } - function buildSerializablePreprocessConfig() { + async function buildReadySerializablePreprocessConfig() { const preprocessConfig = settingsStore.aiPreprocessConfig - const hasPreprocess = - preprocessConfig.dataCleaning || - preprocessConfig.mergeConsecutive || - preprocessConfig.blacklistKeywords.length > 0 || - preprocessConfig.denoise || - preprocessConfig.desensitize || - preprocessConfig.anonymizeNames - - if (!hasPreprocess) return undefined - return { - dataCleaning: preprocessConfig.dataCleaning, - mergeConsecutive: preprocessConfig.mergeConsecutive, - mergeWindowSeconds: preprocessConfig.mergeWindowSeconds, - blacklistKeywords: [...preprocessConfig.blacklistKeywords], - denoise: preprocessConfig.denoise, - desensitize: preprocessConfig.desensitize, - desensitizeRules: preprocessConfig.desensitizeRules.map((rule) => ({ - ...rule, - locales: [...rule.locales], - })), - anonymizeNames: preprocessConfig.anonymizeNames, + if (shouldEnsureDesensitizeRulesBeforeSerialize(preprocessConfig)) { + await settingsStore.ensureDesensitizeRules() } + + return buildSerializablePreprocessConfig(settingsStore.aiPreprocessConfig) } function normalizeMentionLookupText(value: string): string { @@ -1360,7 +1344,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => { ? { platformId: state.ownerInfo.platformId, displayName: state.ownerInfo.displayName } : undefined, mentionedMembers: currentMentionedMembers.length > 0 ? currentMentionedMembers : undefined, - preprocessConfig: buildSerializablePreprocessConfig(), + preprocessConfig: await buildReadySerializablePreprocessConfig(), searchContextBefore: aiGlobalSettings.value.searchContextBefore, searchContextAfter: aiGlobalSettings.value.searchContextAfter, } diff --git a/src/stores/aiPreprocessConfig.test.ts b/src/stores/aiPreprocessConfig.test.ts new file mode 100644 index 00000000..eaab5eff --- /dev/null +++ b/src/stores/aiPreprocessConfig.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { shouldEnsureDesensitizeRulesBeforeSerialize } from './aiPreprocessConfig' + +describe('AI preprocess config serialization guards', () => { + it('requires built-in rules before serializing an enabled empty desensitize config', () => { + assert.equal( + shouldEnsureDesensitizeRulesBeforeSerialize({ + desensitize: true, + desensitizeRules: [], + }), + true + ) + }) + + it('requires built-in rules before serializing enabled custom-only desensitize rules', () => { + assert.equal( + shouldEnsureDesensitizeRulesBeforeSerialize({ + desensitize: true, + desensitizeRules: [ + { + id: 'custom_staff_id', + label: 'Staff ID', + pattern: 'EMP-\\d+', + replacement: '[Staff ID]', + enabled: true, + builtin: false, + locales: [], + }, + ], + }), + true + ) + }) + + it('does not require built-in rules when enabled rules already include a built-in rule', () => { + assert.equal( + shouldEnsureDesensitizeRulesBeforeSerialize({ + desensitize: true, + desensitizeRules: [ + { + id: 'email', + label: 'desensitize.rules.email', + pattern: '[^@\\s]+@[^@\\s]+', + replacement: '[Email]', + enabled: true, + builtin: true, + locales: [], + }, + ], + }), + false + ) + }) + + it('does not require built-in rules when masking is disabled', () => { + assert.equal( + shouldEnsureDesensitizeRulesBeforeSerialize({ + desensitize: false, + desensitizeRules: [], + }), + false + ) + }) +}) diff --git a/src/stores/aiPreprocessConfig.ts b/src/stores/aiPreprocessConfig.ts new file mode 100644 index 00000000..b21121dc --- /dev/null +++ b/src/stores/aiPreprocessConfig.ts @@ -0,0 +1,32 @@ +import type { PreprocessConfig } from '@electron/preload/index' + +export function shouldEnsureDesensitizeRulesBeforeSerialize( + config: Pick +): boolean { + return config.desensitize && !config.desensitizeRules.some((rule) => rule.builtin) +} + +export function buildSerializablePreprocessConfig(config: PreprocessConfig) { + const hasPreprocess = + config.dataCleaning || + config.mergeConsecutive || + config.blacklistKeywords.length > 0 || + config.denoise || + config.desensitize || + config.anonymizeNames + + if (!hasPreprocess) return undefined + return { + dataCleaning: config.dataCleaning, + mergeConsecutive: config.mergeConsecutive, + mergeWindowSeconds: config.mergeWindowSeconds, + blacklistKeywords: [...config.blacklistKeywords], + denoise: config.denoise, + desensitize: config.desensitize, + desensitizeRules: config.desensitizeRules.map((rule) => ({ + ...rule, + locales: [...rule.locales], + })), + anonymizeNames: config.anonymizeNames, + } +}