diff --git a/electron/main.ts b/electron/main.ts index 3aa813c..6c5c447 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1404,6 +1404,10 @@ function registerIpcHandlers() { return insightService.getTodayStats() }) + ipcMain.handle('insight:triggerTest', async () => { + return insightService.triggerTest() + }) + ipcMain.handle('config:clear', async () => { if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) { const result = setSystemLaunchAtStartup(false) diff --git a/electron/preload.ts b/electron/preload.ts index 79c7d0f..068a4b7 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -447,6 +447,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // AI 见解 insight: { testConnection: () => ipcRenderer.invoke('insight:testConnection'), - getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats') + getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats'), + triggerTest: () => ipcRenderer.invoke('insight:triggerTest') } }) diff --git a/electron/services/config.ts b/electron/services/config.ts index b3af210..1709a9a 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -72,6 +72,12 @@ interface ConfigSchema { aiInsightAllowContext: boolean aiInsightWhitelistEnabled: boolean aiInsightWhitelist: string[] + /** 活跃分析冷却时间(分钟),0 表示无冷却 */ + aiInsightCooldownMinutes: number + /** 沉默联系人扫描间隔(小时) */ + aiInsightScanIntervalHours: number + /** 发送上下文时的最大消息条数 */ + aiInsightContextCount: number } // 需要 safeStorage 加密的字段(普通模式) @@ -153,7 +159,10 @@ export class ConfigService { aiInsightSilenceDays: 3, aiInsightAllowContext: false, aiInsightWhitelistEnabled: false, - aiInsightWhitelist: [] + aiInsightWhitelist: [], + aiInsightCooldownMinutes: 120, + aiInsightScanIntervalHours: 4, + aiInsightContextCount: 40 } const storeOptions: any = { diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 9414d9a..8e3d0c7 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -15,7 +15,10 @@ import https from 'https' import http from 'http' +import fs from 'fs' +import path from 'path' import { URL } from 'url' +import { app } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' import { showNotification } from '../windows/notificationWindow' @@ -25,18 +28,12 @@ import { showNotification } from '../windows/notificationWindow' /** DB 变更防抖延迟(毫秒) */ const DB_CHANGE_DEBOUNCE_MS = 500 -/** 沉默扫描间隔(毫秒),4 小时 */ -const SILENCE_SCAN_INTERVAL_MS = 4 * 60 * 60 * 1000 - /** 首次沉默扫描延迟(毫秒),避免启动期间抢占资源 */ const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000 /** 单次 API 请求超时(毫秒) */ const API_TIMEOUT_MS = 45_000 -/** 最大上下文消息数(授权发送真实上下文时) */ -const MAX_CONTEXT_MESSAGES = 40 - /** 沉默天数阈值默认值 */ const DEFAULT_SILENCE_DAYS = 3 @@ -47,6 +44,35 @@ interface TodayTriggerRecord { timestamps: number[] } +// ─── 桌面日志 ───────────────────────────────────────────────────────────────── + +/** + * 将日志同时输出到 console 和桌面上的 weflow-insight.log 文件。 + * 文件名带当天日期,每天自动换一个新文件,旧文件保留。 + */ +function insightLog(level: 'INFO' | 'WARN' | 'ERROR', message: string): void { + const now = new Date() + const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-') + const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false }) + const line = `[${dateStr} ${timeStr}] [${level}] ${message}\n` + + // 同步到 console + if (level === 'ERROR' || level === 'WARN') { + console.warn(`[InsightService] ${message}`) + } else { + console.log(`[InsightService] ${message}`) + } + + // 写入桌面日志文件 + try { + const desktopPath = app.getPath('desktop') + const logFile = path.join(desktopPath, `weflow-insight-${dateStr}.log`) + fs.appendFileSync(logFile, line, 'utf-8') + } catch { + // 写文件失败静默处理,不影响主流程 + } +} + // ─── 工具函数 ───────────────────────────────────────────────────────────────── /** @@ -171,6 +197,18 @@ class InsightService { private todayTriggers: Map = new Map() private todayDate = getStartOfDay() + /** + * 活跃分析冷却记录:sessionId -> 上次分析时间戳(毫秒) + * 同一会话 2 小时内不重复触发活跃分析,防止 DB 频繁变更时爆量调用 API。 + */ + private lastActivityAnalysis: Map = new Map() + + /** + * 跟踪每个会话上次见到的最新消息时间戳,用于判断是否有真正的新消息。 + * sessionId -> lastMessageTimestamp(秒,与微信 DB 保持一致) + */ + private lastSeenTimestamp: Map = new Map() + private started = false constructor() { @@ -182,7 +220,7 @@ class InsightService { start(): void { if (this.started) return this.started = true - console.log('[InsightService] 已启动') + insightLog('INFO', '已启动') this.scheduleSilenceScan() } @@ -200,7 +238,7 @@ class InsightService { clearTimeout(this.silenceInitialDelayTimer) this.silenceInitialDelayTimer = null } - console.log('[InsightService] 已停止') + insightLog('INFO', '已停止') } /** @@ -221,7 +259,7 @@ class InsightService { } /** - * 测试 API 连接,返回 { success, message }。 + * 测��� API 连接,返回 { success, message }。 * 供设置页"测试连接"按钮调用。 */ async testConnection(): Promise<{ success: boolean; message: string }> { @@ -247,6 +285,48 @@ class InsightService { } } + /** + * 强制立即对最近一个私聊会话触发一次见解(忽略冷却,用于测试)。 + * 返回触发结果描述,供设置页展示。 + */ + async triggerTest(): Promise<{ success: boolean; message: string }> { + insightLog('INFO', '手动触发测试见解...') + const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string + const apiKey = this.config.get('aiInsightApiKey') as string + if (!apiBaseUrl || !apiKey) { + return { success: false, message: '请先填写 API 地址和 Key' } + } + try { + const connectResult = await chatService.connect() + if (!connectResult.success) { + return { success: false, message: '数据库连接失败,请先在"数据库连接"页完成配置' } + } + const sessionsResult = await chatService.getSessions() + if (!sessionsResult.success || !sessionsResult.sessions || sessionsResult.sessions.length === 0) { + return { success: false, message: '未找到任何会话,请确认数据库已正确连接' } + } + // 找第一个允许的私聊 + const session = (sessionsResult.sessions as ChatSession[]).find((s) => { + const id = s.username?.trim() || '' + return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) + }) + if (!session) { + return { success: false, message: '未找到任何私聊会话(若已启用白名单,请检查是否有勾选的私聊)' } + } + const sessionId = session.username?.trim() || '' + const displayName = session.displayName || sessionId + insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`) + await this.generateInsightForSession({ + sessionId, + displayName, + triggerReason: 'activity' + }) + return { success: true, message: `已向「${displayName}」发送测试见解,请查看右下角弹窗` } + } catch (e) { + return { success: false, message: `测试失败:${(e as Error).message}` } + } + } + /** 获取今日触发统计(供设置页展示) */ getTodayStats(): { sessionId: string; count: number; times: string[] }[] { this.resetIfNewDay() @@ -313,56 +393,82 @@ class InsightService { // ── 沉默联系人扫描 ────────────────────────────────────────────────────────── private scheduleSilenceScan(): void { - // 延迟 3 分钟后首次扫描,之后每 4 小时一次 this.silenceInitialDelayTimer = setTimeout(() => { void this.runSilenceScan() - this.silenceScanTimer = setInterval(() => { - void this.runSilenceScan() - }, SILENCE_SCAN_INTERVAL_MS) + // 每次扫描完毕后重新读取间隔配置,允许用户动态调整不需要重启 + const scheduleNext = () => { + const intervalHours = (this.config.get('aiInsightScanIntervalHours') as number) || 4 + const intervalMs = Math.max(0.1, intervalHours) * 60 * 60 * 1000 + insightLog('INFO', `下次沉默扫描将在 ${intervalHours} 小时后执行`) + this.silenceScanTimer = setTimeout(() => { + void this.runSilenceScan().then(scheduleNext) + }, intervalMs) + } + scheduleNext() }, SILENCE_SCAN_INITIAL_DELAY_MS) } private async runSilenceScan(): Promise { - if (!this.isEnabled()) return - if (this.processing) return + if (!this.isEnabled()) { + insightLog('INFO', '沉默扫描:AI 见解未启用,跳过') + return + } + if (this.processing) { + insightLog('INFO', '沉默扫描:正在处理中,跳过本次') + return + } this.processing = true + insightLog('INFO', '开始沉默联系人扫描...') try { const silenceDays = (this.config.get('aiInsightSilenceDays') as number) || DEFAULT_SILENCE_DAYS const thresholdMs = silenceDays * 24 * 60 * 60 * 1000 const now = Date.now() + insightLog('INFO', `沉默阈值:${silenceDays} 天`) + const connectResult = await chatService.connect() - if (!connectResult.success) return + if (!connectResult.success) { + insightLog('WARN', '数据库连接失败,跳过沉默扫描') + return + } const sessionsResult = await chatService.getSessions() - if (!sessionsResult.success || !sessionsResult.sessions) return + if (!sessionsResult.success || !sessionsResult.sessions) { + insightLog('WARN', '获取会话列表失败') + return + } const sessions: ChatSession[] = sessionsResult.sessions + insightLog('INFO', `共 ${sessions.length} 个会话,开始过滤...`) + let silentCount = 0 for (const session of sessions) { const sessionId = session.username?.trim() || '' - if (!sessionId || sessionId.endsWith('@chatroom')) continue // 跳过群聊 + if (!sessionId || sessionId.endsWith('@chatroom')) continue if (sessionId.toLowerCase().includes('placeholder')) continue - if (!this.isSessionAllowed(sessionId)) continue // 白名单过滤 + if (!this.isSessionAllowed(sessionId)) continue - // lastTimestamp 单位是秒,需要转换为毫秒 const lastTimestamp = (session.lastTimestamp || 0) * 1000 if (!lastTimestamp || lastTimestamp <= 0) continue const silentMs = now - lastTimestamp if (silentMs < thresholdMs) continue - // 沉默时间满足阈值,触发见解 + silentCount++ + const silentDays = Math.floor(silentMs / (24 * 60 * 60 * 1000)) + insightLog('INFO', `发现沉默联系人:${session.displayName || sessionId},已沉默 ${silentDays} 天`) + await this.generateInsightForSession({ sessionId, displayName: session.displayName || session.username, triggerReason: 'silence', - silentDays: Math.floor(silentMs / (24 * 60 * 60 * 1000)) + silentDays }) } + insightLog('INFO', `沉默扫描完成,共发现 ${silentCount} 个沉默联系人`) } catch (e) { - console.warn('[InsightService] 沉默扫描出错:', e) + insightLog('ERROR', `沉默扫描出错: ${(e as Error).message}`) } finally { this.processing = false } @@ -371,39 +477,89 @@ class InsightService { // ── 活跃会话分析 ──────────────────────────────────────────────────────────── /** - * 在 DB 变更防抖后执行,分析最近活跃的会话, - * 判断是否有有趣的上下文值得产出见解。 + * 在 DB 变更防抖后执行,分析最近活跃的会话。 + * + * 触发条件(必须同时满足): + * 1. 会话有真正的新消息(lastTimestamp 比上次见到的更新) + * 2. 该会话距上次活跃分析已超过 2 小时冷却期 */ private async analyzeRecentActivity(): Promise { if (!this.isEnabled()) return if (this.processing) return this.processing = true + insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...') try { const connectResult = await chatService.connect() - if (!connectResult.success) return + if (!connectResult.success) { + insightLog('WARN', '数据库连接失败,跳过活跃分析') + return + } const sessionsResult = await chatService.getSessions() - if (!sessionsResult.success || !sessionsResult.sessions) return + if (!sessionsResult.success || !sessionsResult.sessions) { + insightLog('WARN', '获取会话列表失败') + return + } const sessions: ChatSession[] = sessionsResult.sessions - // 只取最近有活动的前 5 个会话(排除群聊以降低噪音,可按需调整) - const candidates = sessions - .filter((s) => { - const id = s.username?.trim() || '' - return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) - }) - .slice(0, 5) + const now = Date.now() + + // 从 config 读取冷却分钟数(0 = 无冷却) + const cooldownMinutes = (this.config.get('aiInsightCooldownMinutes') as number) ?? 120 + const cooldownMs = cooldownMinutes * 60 * 1000 + + const privateSessions = sessions.filter((s) => { + const id = s.username?.trim() || '' + return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) + }) + + insightLog('INFO', `筛选到 ${privateSessions.length} 个私聊会话(白名单过滤后),冷却期 ${cooldownMinutes} 分钟`) + + let triggeredCount = 0 + for (const session of privateSessions.slice(0, 10)) { + const sessionId = session.username?.trim() || '' + if (!sessionId) continue + + const currentTimestamp = session.lastTimestamp || 0 + const lastSeen = this.lastSeenTimestamp.get(sessionId) ?? 0 + + // 检查是否有真正的新消息 + if (currentTimestamp <= lastSeen) { + continue + } + + // 更新已见时间戳 + this.lastSeenTimestamp.set(sessionId, currentTimestamp) + + // 检查冷却期(0 分钟 = 无冷却,直接通过) + if (cooldownMs > 0) { + const lastAnalysis = this.lastActivityAnalysis.get(sessionId) ?? 0 + const cooldownRemaining = cooldownMs - (now - lastAnalysis) + if (cooldownRemaining > 0) { + insightLog('INFO', `${session.displayName || sessionId} 冷却中,还需 ${Math.ceil(cooldownRemaining / 60000)} 分钟`) + continue + } + } + + insightLog('INFO', `${session.displayName || sessionId} 有新消息,准备生成见解...`) + this.lastActivityAnalysis.set(sessionId, now) - for (const session of candidates) { await this.generateInsightForSession({ - sessionId: session.username?.trim() || '', + sessionId, displayName: session.displayName || session.username, triggerReason: 'activity' }) + triggeredCount++ + + break // 每次最多处理 1 个会话 + } + + if (triggeredCount === 0) { + insightLog('INFO', '活跃分析完成,无会话触发见解') } } catch (e) { - console.warn('[InsightService] 活跃分析出错:', e) + insightLog('ERROR', `活跃分析出错: ${(e as Error).message}`) } finally { this.processing = false } @@ -424,10 +580,16 @@ class InsightService { const apiKey = this.config.get('aiInsightApiKey') as string const model = (this.config.get('aiInsightApiModel') as string) || 'gpt-4o-mini' const allowContext = this.config.get('aiInsightAllowContext') as boolean + const contextCount = (this.config.get('aiInsightContextCount') as number) || 40 - if (!apiBaseUrl || !apiKey) return + insightLog('INFO', `generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, contextCount=${contextCount}, api=${apiBaseUrl ? '已配置' : '未配置'}`) - // ── 构建 prompt ────────────────────────────────────────────────────────── + if (!apiBaseUrl || !apiKey) { + insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成') + return + } + + // ── 构建 prompt ─────────────���──────────────────────────────────────────── // 今日触发统计(让模型具备时间与克制感) const sessionTriggerTimes = this.recordTrigger(sessionId) @@ -436,7 +598,7 @@ class InsightService { let contextSection = '' if (allowContext) { try { - const msgsResult = await chatService.getLatestMessages(sessionId, MAX_CONTEXT_MESSAGES) + const msgsResult = await chatService.getLatestMessages(sessionId, contextCount) if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) { const messages: Message[] = msgsResult.messages const msgLines = messages.map((m) => { @@ -445,10 +607,11 @@ class InsightService { const time = new Date(Number(m.createTime) * 1000).toLocaleString('zh-CN') return `[${time}] ${sender}:${content}` }) - contextSection = `\n\n��期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}` + contextSection = `\n\n近期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}` + insightLog('INFO', `已加载 ${msgLines.length} 条上下文消息`) } } catch (e) { - console.warn('[InsightService] 拉取上下文失败:', e) + insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`) } } @@ -475,6 +638,9 @@ class InsightService { 请给出你的见解(≤80字)或回复 SKIP 跳过:` + const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') + insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`) + try { const result = await callApi( apiBaseUrl, @@ -486,25 +652,27 @@ class InsightService { ] ) + insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`) + // 模型主动选择跳过 if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { - console.log(`[InsightService] 模型选择跳过 ${sessionId}`) + insightLog('INFO', `模型选择跳过 ${displayName}`) return } - const insight = result.slice(0, 120) // 防止超长截断 + const insight = result.slice(0, 120) - // 通过现有 showNotification 推送弹窗 + insightLog('INFO', `推送通知 → ${displayName}: ${insight}`) await showNotification({ sessionId, sourceName: `见解 · ${displayName}`, content: insight, - isInsight: true // 可用于通知窗口做差异化展示 + isInsight: true }) - console.log(`[InsightService] 已为 ${sessionId} 推送见解`) + insightLog('INFO', `已为 ${displayName} 推送见解`) } catch (e) { - console.warn(`[InsightService] API 调用失败 (${sessionId}):`, (e as Error).message) + insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`) } } } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index c6acd2b..2bacd86 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -225,9 +225,14 @@ 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 [isTriggeringInsightTest, setIsTriggeringInsightTest] = useState(false) + const [insightTriggerResult, setInsightTriggerResult] = useState<{ success: boolean; message: string } | null>(null) const [aiInsightWhitelistEnabled, setAiInsightWhitelistEnabled] = useState(false) const [aiInsightWhitelist, setAiInsightWhitelist] = useState>(new Set()) const [insightWhitelistSearch, setInsightWhitelistSearch] = useState('') + const [aiInsightCooldownMinutes, setAiInsightCooldownMinutes] = useState(120) + const [aiInsightScanIntervalHours, setAiInsightScanIntervalHours] = useState(4) + const [aiInsightContextCount, setAiInsightContextCount] = useState(40) const [isWayland, setIsWayland] = useState(false) useEffect(() => { @@ -460,17 +465,23 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiInsightApiKey = await configService.getAiInsightApiKey() 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)) + const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() + const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() + const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() + const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() + const savedAiInsightContextCount = await configService.getAiInsightContextCount() + setAiInsightEnabled(savedAiInsightEnabled) + setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl) + setAiInsightApiKey(savedAiInsightApiKey) + setAiInsightApiModel(savedAiInsightApiModel) + setAiInsightSilenceDays(savedAiInsightSilenceDays) + setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) + setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) + setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) + setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) + setAiInsightContextCount(savedAiInsightContextCount) } catch (e: any) { console.error('加载配置失败:', e) @@ -2609,36 +2620,121 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { /> - {/* 测试连接 */} + {/* 测试连接 + 触发测试 */}
-
- + {insightTestResult && ( + + {insightTestResult.success ? : } + {insightTestResult.message} + )} - - {insightTestResult && ( - - {insightTestResult.success ? : } - {insightTestResult.message} - - )} +
+ {/* 触发测试见解 */} +
+ + {insightTriggerResult && ( + + {insightTriggerResult.success ? : } + {insightTriggerResult.message} + + )} +
{/* 行为配置 */} +
+ + + 有新消息时触发活跃分析的冷却时间。设为 0 表示无冷却,每条新消息都可能触发见解(AI 言论自由模式)。建议按需调整,费用自理。 + + { + const val = Math.max(0, parseInt(e.target.value, 10) || 0) + setAiInsightCooldownMinutes(val) + scheduleConfigSave('aiInsightCooldownMinutes', () => configService.setAiInsightCooldownMinutes(val)) + }} + style={{ width: 120 }} + /> + {aiInsightCooldownMinutes === 0 && ( + + 无冷却 — 每次 DB 变更均可触发 + + )} +
+ +
+ + + 多久扫描一次沉默联系人。重启生效。最小 0.1 小时(6 分钟)。 + + { + const val = Math.max(0.1, parseFloat(e.target.value) || 4) + setAiInsightScanIntervalHours(val) + scheduleConfigSave('aiInsightScanIntervalHours', () => configService.setAiInsightScanIntervalHours(val)) + }} + style={{ width: 120 }} + /> +
+
- 当你与某个私聊联系人超过此天数没有发过消息时,AI 会主动扫描并尝试给出见解。每 4 小时扫描一次。 + 与某私聊联系人超过此天数没有消息往来时,触发沉默类见解。 - 开启后,AI 见解触发时会将该联系人最近 40 条真实聊天记录一并发送给 AI,使其能够基于真实内容给出有意义的分析,而非泛泛而谈。 + 开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
- 关闭时:AI 仅知道"与某人沉默了 N 天"等统计摘要,输出质量会显著降低。 + 关闭时:AI 仅知道统计摘要(沉默天数等),输出质量较低。
- 开启时:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给你选择的模型提供商。请确认你信任该服务商。 + 开启时:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
{aiInsightAllowContext ? '已授权' : '未授权'} @@ -2681,6 +2777,28 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+ {aiInsightAllowContext && ( +
+ + + 发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。 + + { + const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40)) + setAiInsightContextCount(val) + scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val)) + }} + style={{ width: 100 }} + /> +
+ )} +
{/* 对话白名单 */} @@ -2879,7 +2997,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {

触发方式一:活跃会话分析 — 每当微信数据库变化(即你收到新消息)时,经过 500ms 防抖后,对最近活跃的私聊会话进行分析。
触发方式二:沉默扫描 — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。
- 时间观念 — 每次调用时,AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。
+ 时间观念 — 每次调用时��AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。
隐私 — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。

@@ -3123,7 +3241,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { try { const verifyResult = await window.electronAPI.auth.hello('请验证您的身份以开启 Windows Hello') if (!verifyResult.success) { - showMessage(verifyResult.error || 'Windows Hello 验证失败', false) + showMessage(verifyResult.error || 'Windows Hello ��证失败', false) return } diff --git a/src/services/config.ts b/src/services/config.ts index 65253e2..f1ee16b 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -89,7 +89,10 @@ export const CONFIG_KEYS = { AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays', AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext', AI_INSIGHT_WHITELIST_ENABLED: 'aiInsightWhitelistEnabled', - AI_INSIGHT_WHITELIST: 'aiInsightWhitelist' + AI_INSIGHT_WHITELIST: 'aiInsightWhitelist', + AI_INSIGHT_COOLDOWN_MINUTES: 'aiInsightCooldownMinutes', + AI_INSIGHT_SCAN_INTERVAL_HOURS: 'aiInsightScanIntervalHours', + AI_INSIGHT_CONTEXT_COUNT: 'aiInsightContextCount' } as const export interface WxidConfig { @@ -1635,3 +1638,30 @@ export async function getAiInsightWhitelist(): Promise { export async function setAiInsightWhitelist(list: string[]): Promise { await config.set(CONFIG_KEYS.AI_INSIGHT_WHITELIST, list) } + +export async function getAiInsightCooldownMinutes(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_COOLDOWN_MINUTES) + return typeof value === 'number' && value >= 0 ? value : 120 +} + +export async function setAiInsightCooldownMinutes(minutes: number): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_COOLDOWN_MINUTES, minutes) +} + +export async function getAiInsightScanIntervalHours(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_SCAN_INTERVAL_HOURS) + return typeof value === 'number' && value > 0 ? value : 4 +} + +export async function setAiInsightScanIntervalHours(hours: number): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_SCAN_INTERVAL_HOURS, hours) +} + +export async function getAiInsightContextCount(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_CONTEXT_COUNT) + return typeof value === 'number' && value > 0 ? value : 40 +} + +export async function setAiInsightContextCount(count: number): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_CONTEXT_COUNT, count) +}