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 a22b6db..816a2d1 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,27 +28,15 @@ 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 -/** - * 同一会话活跃分析的冷却时间(毫秒),2 小时。 - * 防止 DB 频繁变更时对同一会话反复调用 API。 - */ -const ACTIVITY_COOLDOWN_MS = 2 * 60 * 60 * 1000 - // ─── 类型 ──────────────────────────────────────────────────────────────────── interface TodayTriggerRecord { @@ -53,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 { + // 写文件失败静默处理,不影响主流程 + } +} + // ─── 工具函数 ───────────────────────────────────────────────────────────────── /** @@ -200,7 +220,7 @@ class InsightService { start(): void { if (this.started) return this.started = true - console.log('[InsightService] 已启动') + insightLog('INFO', '已启动') this.scheduleSilenceScan() } @@ -218,7 +238,7 @@ class InsightService { clearTimeout(this.silenceInitialDelayTimer) this.silenceInitialDelayTimer = null } - console.log('[InsightService] 已停止') + insightLog('INFO', '已停止') } /** @@ -270,7 +290,7 @@ class InsightService { * 返回触发结果描述,供设置页展示。 */ async triggerTest(): Promise<{ success: boolean; message: string }> { - console.log('[InsightService] 手动触发测试见解...') + insightLog('INFO', '手动触发测试见解...') const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string const apiKey = this.config.get('aiInsightApiKey') as string if (!apiBaseUrl || !apiKey) { @@ -285,17 +305,17 @@ class InsightService { 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') + return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) }) if (!session) { - return { success: false, message: '未找到任何私聊会话' } + return { success: false, message: '未找到任何私聊会话(若已启用白名单,请检查是否有勾选的私聊)' } } const sessionId = session.username?.trim() || '' const displayName = session.displayName || sessionId - console.log(`[InsightService] 测试目标会话:${displayName} (${sessionId})`) + insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`) await this.generateInsightForSession({ sessionId, displayName, @@ -373,57 +393,62 @@ 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()) { - console.log('[InsightService] 沉默扫描:AI 见解未启用,跳过') + insightLog('INFO', '沉默扫描:AI 见解未启用,跳过') return } if (this.processing) { - console.log('[InsightService] 沉默扫描:正在处理中,跳过本次') + insightLog('INFO', '沉默扫描:正在处理中,跳过本次') return } this.processing = true - console.log('[InsightService] 开始沉默联系人扫描...') + 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() - console.log(`[InsightService] 沉默阈值:${silenceDays} 天`) + insightLog('INFO', `沉默阈值:${silenceDays} 天`) const connectResult = await chatService.connect() if (!connectResult.success) { - console.log('[InsightService] 数据库连接失败,跳过沉默扫描') + insightLog('WARN', '数据库连接失败,跳过沉默扫描') return } const sessionsResult = await chatService.getSessions() if (!sessionsResult.success || !sessionsResult.sessions) { - console.log('[InsightService] 获取会话列表失败') + insightLog('WARN', '获取会话列表失败') return } const sessions: ChatSession[] = sessionsResult.sessions - console.log(`[InsightService] 共 ${sessions.length} 个会话,开始过滤...`) + 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 @@ -432,9 +457,8 @@ class InsightService { silentCount++ const silentDays = Math.floor(silentMs / (24 * 60 * 60 * 1000)) - console.log(`[InsightService] 发现沉默联系人:${session.displayName || sessionId},已沉默 ${silentDays} 天`) + insightLog('INFO', `发现沉默联系人:${session.displayName || sessionId},已沉默 ${silentDays} 天`) - // 沉默时间满足阈值,触发见解 await this.generateInsightForSession({ sessionId, displayName: session.displayName || session.username, @@ -442,9 +466,9 @@ class InsightService { silentDays }) } - console.log(`[InsightService] 沉默扫描完成,共发现 ${silentCount} 个沉默联系人`) + insightLog('INFO', `沉默扫描完成,共发现 ${silentCount} 个沉默联系人`) } catch (e) { - console.warn('[InsightService] 沉默扫描出错:', e) + insightLog('ERROR', `沉默扫描出错: ${(e as Error).message}`) } finally { this.processing = false } @@ -464,30 +488,33 @@ class InsightService { if (this.processing) return this.processing = true - console.log('[InsightService] DB 变更防抖触发,开始活跃分析...') + insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...') try { const connectResult = await chatService.connect() if (!connectResult.success) { - console.log('[InsightService] 数据库连接失败,跳过活跃分析') + insightLog('WARN', '数据库连接失败,跳过活跃分析') return } const sessionsResult = await chatService.getSessions() if (!sessionsResult.success || !sessionsResult.sessions) { - console.log('[InsightService] 获取会话列表失败') + insightLog('WARN', '获取会话列表失败') return } const sessions: ChatSession[] = sessionsResult.sessions 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) }) - console.log(`[InsightService] 筛选到 ${privateSessions.length} 个私聊会话(已过白名单过滤)`) + insightLog('INFO', `筛选到 ${privateSessions.length} 个私聊会话(白名单过滤后),冷却期 ${cooldownMinutes} 分钟`) let triggeredCount = 0 for (const session of privateSessions.slice(0, 10)) { @@ -499,21 +526,23 @@ class InsightService { // 检查是否有真正的新消息 if (currentTimestamp <= lastSeen) { - continue // 没有新消息,跳过 + continue } // 更新已见时间戳 this.lastSeenTimestamp.set(sessionId, currentTimestamp) - // 检查冷却期 - const lastAnalysis = this.lastActivityAnalysis.get(sessionId) ?? 0 - const cooldownRemaining = ACTIVITY_COOLDOWN_MS - (now - lastAnalysis) - if (cooldownRemaining > 0) { - console.log(`[InsightService] ${sessionId} 冷却中,还需 ${Math.ceil(cooldownRemaining / 60000)} 分钟`) - continue + // 检查冷却期(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 + } } - console.log(`[InsightService] ${sessionId} 有新消息且冷却已过,准备生成见解...`) + insightLog('INFO', `${session.displayName || sessionId} 有新消息,准备生成见解...`) this.lastActivityAnalysis.set(sessionId, now) await this.generateInsightForSession({ @@ -523,15 +552,14 @@ class InsightService { }) triggeredCount++ - // 每次最多处理 1 个会话,避免单次防抖后批量调用 - break + break // 每次最多处理 1 个会话 } if (triggeredCount === 0) { - console.log('[InsightService] 活跃分析完成,无会话触发见解') + insightLog('INFO', '活跃分析完成,无会话触发见解') } } catch (e) { - console.warn('[InsightService] 活跃分析出错:', e) + insightLog('ERROR', `活跃分析出错: ${(e as Error).message}`) } finally { this.processing = false } @@ -552,11 +580,12 @@ 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 - console.log(`[InsightService] generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, apiBaseUrl=${apiBaseUrl ? '已配置' : '未配置'}, apiKey=${apiKey ? '已配置' : '未配置'}`) + insightLog('INFO', `generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, contextCount=${contextCount}, api=${apiBaseUrl ? '已配置' : '未配置'}`) if (!apiBaseUrl || !apiKey) { - console.warn('[InsightService] API 地址或 Key 未配置,跳过见解生成') + insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成') return } @@ -569,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) => { @@ -578,8 +607,13 @@ 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) { + insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`) + } + } } catch (e) { console.warn('[InsightService] 拉取上下文失败:', e) } @@ -609,7 +643,7 @@ class InsightService { 请给出你的见解(≤80字)或回复 SKIP 跳过:` const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') - console.log(`[InsightService] 准备调用 API: ${endpoint},模型: ${model}`) + insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`) try { const result = await callApi( @@ -622,18 +656,17 @@ class InsightService { ] ) - console.log(`[InsightService] API 返回原文: ${result.slice(0, 100)}`) + 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 推送弹窗 - console.log(`[InsightService] 准备推送通知: ${insight}`) + insightLog('INFO', `推送通知 → ${displayName}: ${insight}`) await showNotification({ sessionId, sourceName: `见解 · ${displayName}`, @@ -641,9 +674,9 @@ class InsightService { 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 c7afc35..2bacd86 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -230,6 +230,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 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(() => { @@ -462,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) @@ -2676,10 +2685,56 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
{/* 行为配置 */} +
+ + + 有新消息时触发活跃分析的冷却时间。设为 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 ? '已授权' : '未授权'} @@ -2722,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 }} + /> +
+ )} +
{/* 对话白名单 */} 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) +}