diff --git a/electron/services/config.ts b/electron/services/config.ts index 8b4fad1..b3af210 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -70,6 +70,8 @@ interface ConfigSchema { aiInsightApiModel: string aiInsightSilenceDays: number aiInsightAllowContext: boolean + aiInsightWhitelistEnabled: boolean + aiInsightWhitelist: string[] } // 需要 safeStorage 加密的字段(普通模式) @@ -149,7 +151,9 @@ export class ConfigService { aiInsightApiKey: '', aiInsightApiModel: 'gpt-4o-mini', aiInsightSilenceDays: 3, - aiInsightAllowContext: false + aiInsightAllowContext: false, + aiInsightWhitelistEnabled: false, + aiInsightWhitelist: [] } const storeOptions: any = { @@ -697,7 +701,7 @@ export class ConfigService { // === 工具方法 === /** - * 获取当前 wxid 对应的图片密钥,优先从 wxidConfigs 中取,找不到则回退到全局配置 + * 获取当前 wxid 对应的图片密钥,优先从 wxidConfigs 中取,找不到则回退到全局��置 */ getImageKeysForCurrentWxid(): { xorKey: unknown; aesKey: string } { const wxid = this.get('myWxid') diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 97c7eb1..9414d9a 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -267,6 +267,18 @@ class InsightService { return this.config.get('aiInsightEnabled') === true } + /** + * 判断某个会话是否允许触发见解。 + * 若白名单未启用,则所有私聊会话均允许; + * 若白名单已启用,则只有在白名单中的会话才允许。 + */ + private isSessionAllowed(sessionId: string): boolean { + const whitelistEnabled = this.config.get('aiInsightWhitelistEnabled') as boolean + if (!whitelistEnabled) return true + const whitelist = (this.config.get('aiInsightWhitelist') as string[]) || [] + return whitelist.includes(sessionId) + } + private resetIfNewDay(): void { const todayStart = getStartOfDay() if (todayStart > this.todayDate) { @@ -332,6 +344,7 @@ class InsightService { const sessionId = session.username?.trim() || '' if (!sessionId || sessionId.endsWith('@chatroom')) continue // 跳过群聊 if (sessionId.toLowerCase().includes('placeholder')) continue + if (!this.isSessionAllowed(sessionId)) continue // 白名单过滤 // lastTimestamp 单位是秒,需要转换为毫秒 const lastTimestamp = (session.lastTimestamp || 0) * 1000 @@ -378,7 +391,7 @@ class InsightService { const candidates = sessions .filter((s) => { const id = s.username?.trim() || '' - return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') + return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) }) .slice(0, 5) diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index a3c0aed..c6acd2b 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -225,6 +225,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [isTestingInsight, setIsTestingInsight] = useState(false) const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null) const [showInsightApiKey, setShowInsightApiKey] = useState(false) + const [aiInsightWhitelistEnabled, setAiInsightWhitelistEnabled] = useState(false) + const [aiInsightWhitelist, setAiInsightWhitelist] = useState>(new Set()) + const [insightWhitelistSearch, setInsightWhitelistSearch] = useState('') const [isWayland, setIsWayland] = useState(false) useEffect(() => { @@ -458,12 +461,16 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiInsightApiModel = await configService.getAiInsightApiModel() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() + const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() setAiInsightEnabled(savedAiInsightEnabled) setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl) setAiInsightApiKey(savedAiInsightApiKey) setAiInsightApiModel(savedAiInsightApiModel) setAiInsightSilenceDays(savedAiInsightSilenceDays) setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) + setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) } catch (e: any) { console.error('加载配置失败:', e) @@ -1434,7 +1441,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- + 选择聊天中引用消息与正文的上下顺序,下方预览会同步展示布局差异。
{[ @@ -2676,6 +2683,194 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+ {/* 对话白名单 */} + {(() => { + const sortedSessions = [...chatSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) + const keyword = insightWhitelistSearch.trim().toLowerCase() + const filteredSessions = sortedSessions.filter((s) => { + const id = s.username?.trim() || '' + if (!id || id.endsWith('@chatroom') || id.toLowerCase().includes('placeholder')) return false + if (!keyword) return true + return ( + String(s.displayName || '').toLowerCase().includes(keyword) || + id.toLowerCase().includes(keyword) + ) + }) + const filteredIds = filteredSessions.map((s) => s.username) + const selectedCount = aiInsightWhitelist.size + const selectedInFilteredCount = filteredIds.filter((id) => aiInsightWhitelist.has(id)).length + const allFilteredSelected = filteredIds.length > 0 && selectedInFilteredCount === filteredIds.length + + const toggleSession = (id: string) => { + setAiInsightWhitelist((prev) => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + const saveWhitelist = async (next: Set) => { + await configService.setAiInsightWhitelist(Array.from(next)) + } + + const selectAllFiltered = () => { + setAiInsightWhitelist((prev) => { + const next = new Set(prev) + for (const id of filteredIds) next.add(id) + void saveWhitelist(next) + return next + }) + } + + const clearSelection = () => { + const next = new Set() + setAiInsightWhitelist(next) + void saveWhitelist(next) + } + + return ( +
+
+
+

对话白名单

+

+ 开启后,AI 见解仅对勾选的私聊对话生效,未勾选的对话将被完全忽略。关闭时对所有私聊均生效。 +

+
+
+
+ 私聊总数 + {filteredIds.length + (keyword ? 0 : 0)} +
+
+ 已选中 + {selectedCount} +
+
+
+ +
+ + {aiInsightWhitelistEnabled ? '白名单已启用(仅对勾选对话生效)' : '白名单未启用(对所有私聊生效)'} + + +
+ +
+
+
+ + setInsightWhitelistSearch(e.target.value)} + /> +
+
+
+ + +
+
+
+ +
+
+ 已选 {selectedCount} 个对话 + 筛选命中 {selectedInFilteredCount} / {filteredIds.length} +
+
+
+ +
+ {filteredSessions.length === 0 ? ( +
+ {insightWhitelistSearch ? '没有匹配的对话' : '暂无私聊对话'} +
+ ) : ( + <> +
+ 对话({filteredSessions.length}) + 状态 +
+ {filteredSessions.map((session) => { + const isSelected = aiInsightWhitelist.has(session.username) + return ( +
+ +
+ +