Merge pull request #4 from Jasonzhu1207/v0/jasonzhu081207-4751-507441fc

Enable AI insights and whitelist management in settings
This commit is contained in:
Jason
2026-04-06 01:22:46 +08:00
committed by GitHub
6 changed files with 416 additions and 86 deletions

View File

@@ -1404,6 +1404,10 @@ function registerIpcHandlers() {
return insightService.getTodayStats() return insightService.getTodayStats()
}) })
ipcMain.handle('insight:triggerTest', async () => {
return insightService.triggerTest()
})
ipcMain.handle('config:clear', async () => { ipcMain.handle('config:clear', async () => {
if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) { if (isLaunchAtStartupSupported() && getSystemLaunchAtStartup()) {
const result = setSystemLaunchAtStartup(false) const result = setSystemLaunchAtStartup(false)

View File

@@ -447,6 +447,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
// AI 见解 // AI 见解
insight: { insight: {
testConnection: () => ipcRenderer.invoke('insight:testConnection'), testConnection: () => ipcRenderer.invoke('insight:testConnection'),
getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats') getTodayStats: () => ipcRenderer.invoke('insight:getTodayStats'),
triggerTest: () => ipcRenderer.invoke('insight:triggerTest')
} }
}) })

View File

@@ -72,6 +72,12 @@ interface ConfigSchema {
aiInsightAllowContext: boolean aiInsightAllowContext: boolean
aiInsightWhitelistEnabled: boolean aiInsightWhitelistEnabled: boolean
aiInsightWhitelist: string[] aiInsightWhitelist: string[]
/** 活跃分析冷却时间分钟0 表示无冷却 */
aiInsightCooldownMinutes: number
/** 沉默联系人扫描间隔(小时) */
aiInsightScanIntervalHours: number
/** 发送上下文时的最大消息条数 */
aiInsightContextCount: number
} }
// 需要 safeStorage 加密的字段(普通模式) // 需要 safeStorage 加密的字段(普通模式)
@@ -153,7 +159,10 @@ export class ConfigService {
aiInsightSilenceDays: 3, aiInsightSilenceDays: 3,
aiInsightAllowContext: false, aiInsightAllowContext: false,
aiInsightWhitelistEnabled: false, aiInsightWhitelistEnabled: false,
aiInsightWhitelist: [] aiInsightWhitelist: [],
aiInsightCooldownMinutes: 120,
aiInsightScanIntervalHours: 4,
aiInsightContextCount: 40
} }
const storeOptions: any = { const storeOptions: any = {

View File

@@ -15,7 +15,10 @@
import https from 'https' import https from 'https'
import http from 'http' import http from 'http'
import fs from 'fs'
import path from 'path'
import { URL } from 'url' import { URL } from 'url'
import { app } from 'electron'
import { ConfigService } from './config' import { ConfigService } from './config'
import { chatService, ChatSession, Message } from './chatService' import { chatService, ChatSession, Message } from './chatService'
import { showNotification } from '../windows/notificationWindow' import { showNotification } from '../windows/notificationWindow'
@@ -25,18 +28,12 @@ import { showNotification } from '../windows/notificationWindow'
/** DB 变更防抖延迟(毫秒) */ /** DB 变更防抖延迟(毫秒) */
const DB_CHANGE_DEBOUNCE_MS = 500 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 const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000
/** 单次 API 请求超时(毫秒) */ /** 单次 API 请求超时(毫秒) */
const API_TIMEOUT_MS = 45_000 const API_TIMEOUT_MS = 45_000
/** 最大上下文消息数(授权发送真实上下文时) */
const MAX_CONTEXT_MESSAGES = 40
/** 沉默天数阈值默认值 */ /** 沉默天数阈值默认值 */
const DEFAULT_SILENCE_DAYS = 3 const DEFAULT_SILENCE_DAYS = 3
@@ -47,6 +44,35 @@ interface TodayTriggerRecord {
timestamps: number[] 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<string, TodayTriggerRecord> = new Map() private todayTriggers: Map<string, TodayTriggerRecord> = new Map()
private todayDate = getStartOfDay() private todayDate = getStartOfDay()
/**
* 活跃分析冷却记录sessionId -> 上次分析时间戳(毫秒)
* 同一会话 2 小时内不重复触发活跃分析,防止 DB 频繁变更时爆量调用 API。
*/
private lastActivityAnalysis: Map<string, number> = new Map()
/**
* 跟踪每个会话上次见到的最新消息时间戳,用于判断是否有真正的新消息。
* sessionId -> lastMessageTimestamp与微信 DB 保持一致)
*/
private lastSeenTimestamp: Map<string, number> = new Map()
private started = false private started = false
constructor() { constructor() {
@@ -182,7 +220,7 @@ class InsightService {
start(): void { start(): void {
if (this.started) return if (this.started) return
this.started = true this.started = true
console.log('[InsightService] 已启动') insightLog('INFO', '已启动')
this.scheduleSilenceScan() this.scheduleSilenceScan()
} }
@@ -200,7 +238,7 @@ class InsightService {
clearTimeout(this.silenceInitialDelayTimer) clearTimeout(this.silenceInitialDelayTimer)
this.silenceInitialDelayTimer = null this.silenceInitialDelayTimer = null
} }
console.log('[InsightService] 已停止') insightLog('INFO', '已停止')
} }
/** /**
@@ -221,7 +259,7 @@ class InsightService {
} }
/** /**
* 测 API 连接,返回 { success, message }。 * 测<EFBFBD><EFBFBD><EFBFBD> API 连接,返回 { success, message }。
* 供设置页"测试连接"按钮调用。 * 供设置页"测试连接"按钮调用。
*/ */
async testConnection(): Promise<{ success: boolean; message: string }> { 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[] }[] { getTodayStats(): { sessionId: string; count: number; times: string[] }[] {
this.resetIfNewDay() this.resetIfNewDay()
@@ -313,56 +393,82 @@ class InsightService {
// ── 沉默联系人扫描 ────────────────────────────────────────────────────────── // ── 沉默联系人扫描 ──────────────────────────────────────────────────────────
private scheduleSilenceScan(): void { private scheduleSilenceScan(): void {
// 延迟 3 分钟后首次扫描,之后每 4 小时一次
this.silenceInitialDelayTimer = setTimeout(() => { this.silenceInitialDelayTimer = setTimeout(() => {
void this.runSilenceScan() void this.runSilenceScan()
this.silenceScanTimer = setInterval(() => { // 每次扫描完毕后重新读取间隔配置,允许用户动态调整不需要重启
void this.runSilenceScan() const scheduleNext = () => {
}, SILENCE_SCAN_INTERVAL_MS) 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) }, SILENCE_SCAN_INITIAL_DELAY_MS)
} }
private async runSilenceScan(): Promise<void> { private async runSilenceScan(): Promise<void> {
if (!this.isEnabled()) return if (!this.isEnabled()) {
if (this.processing) return insightLog('INFO', '沉默扫描AI 见解未启用,跳过')
return
}
if (this.processing) {
insightLog('INFO', '沉默扫描:正在处理中,跳过本次')
return
}
this.processing = true this.processing = true
insightLog('INFO', '开始沉默联系人扫描...')
try { try {
const silenceDays = (this.config.get('aiInsightSilenceDays') as number) || DEFAULT_SILENCE_DAYS const silenceDays = (this.config.get('aiInsightSilenceDays') as number) || DEFAULT_SILENCE_DAYS
const thresholdMs = silenceDays * 24 * 60 * 60 * 1000 const thresholdMs = silenceDays * 24 * 60 * 60 * 1000
const now = Date.now() const now = Date.now()
insightLog('INFO', `沉默阈值:${silenceDays}`)
const connectResult = await chatService.connect() const connectResult = await chatService.connect()
if (!connectResult.success) return if (!connectResult.success) {
insightLog('WARN', '数据库连接失败,跳过沉默扫描')
return
}
const sessionsResult = await chatService.getSessions() const sessionsResult = await chatService.getSessions()
if (!sessionsResult.success || !sessionsResult.sessions) return if (!sessionsResult.success || !sessionsResult.sessions) {
insightLog('WARN', '获取会话列表失败')
return
}
const sessions: ChatSession[] = sessionsResult.sessions const sessions: ChatSession[] = sessionsResult.sessions
insightLog('INFO', `${sessions.length} 个会话,开始过滤...`)
let silentCount = 0
for (const session of sessions) { for (const session of sessions) {
const sessionId = session.username?.trim() || '' const sessionId = session.username?.trim() || ''
if (!sessionId || sessionId.endsWith('@chatroom')) continue // 跳过群聊 if (!sessionId || sessionId.endsWith('@chatroom')) continue
if (sessionId.toLowerCase().includes('placeholder')) continue if (sessionId.toLowerCase().includes('placeholder')) continue
if (!this.isSessionAllowed(sessionId)) continue // 白名单过滤 if (!this.isSessionAllowed(sessionId)) continue
// lastTimestamp 单位是秒,需要转换为毫秒
const lastTimestamp = (session.lastTimestamp || 0) * 1000 const lastTimestamp = (session.lastTimestamp || 0) * 1000
if (!lastTimestamp || lastTimestamp <= 0) continue if (!lastTimestamp || lastTimestamp <= 0) continue
const silentMs = now - lastTimestamp const silentMs = now - lastTimestamp
if (silentMs < thresholdMs) continue if (silentMs < thresholdMs) continue
// 沉默时间满足阈值,触发见解 silentCount++
const silentDays = Math.floor(silentMs / (24 * 60 * 60 * 1000))
insightLog('INFO', `发现沉默联系人:${session.displayName || sessionId},已沉默 ${silentDays}`)
await this.generateInsightForSession({ await this.generateInsightForSession({
sessionId, sessionId,
displayName: session.displayName || session.username, displayName: session.displayName || session.username,
triggerReason: 'silence', triggerReason: 'silence',
silentDays: Math.floor(silentMs / (24 * 60 * 60 * 1000)) silentDays
}) })
} }
insightLog('INFO', `沉默扫描完成,共发现 ${silentCount} 个沉默联系人`)
} catch (e) { } catch (e) {
console.warn('[InsightService] 沉默扫描出错:', e) insightLog('ERROR', `沉默扫描出错: ${(e as Error).message}`)
} finally { } finally {
this.processing = false this.processing = false
} }
@@ -371,39 +477,89 @@ class InsightService {
// ── 活跃会话分析 ──────────────────────────────────────────────────────────── // ── 活跃会话分析 ────────────────────────────────────────────────────────────
/** /**
* 在 DB 变更防抖后执行,分析最近活跃的会话 * 在 DB 变更防抖后执行,分析最近活跃的会话
* 判断是否有有趣的上下文值得产出见解。 *
* 触发条件(必须同时满足):
* 1. 会话有真正的新消息lastTimestamp 比上次见到的更新)
* 2. 该会话距上次活跃分析已超过 2 小时冷却期
*/ */
private async analyzeRecentActivity(): Promise<void> { private async analyzeRecentActivity(): Promise<void> {
if (!this.isEnabled()) return if (!this.isEnabled()) return
if (this.processing) return if (this.processing) return
this.processing = true this.processing = true
insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...')
try { try {
const connectResult = await chatService.connect() const connectResult = await chatService.connect()
if (!connectResult.success) return if (!connectResult.success) {
insightLog('WARN', '数据库连接失败,跳过活跃分析')
return
}
const sessionsResult = await chatService.getSessions() const sessionsResult = await chatService.getSessions()
if (!sessionsResult.success || !sessionsResult.sessions) return if (!sessionsResult.success || !sessionsResult.sessions) {
insightLog('WARN', '获取会话列表失败')
return
}
const sessions: ChatSession[] = sessionsResult.sessions const sessions: ChatSession[] = sessionsResult.sessions
// 只取最近有活动的前 5 个会话(排除群聊以降低噪音,可按需调整) const now = Date.now()
const candidates = sessions
.filter((s) => { // 从 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() || '' const id = s.username?.trim() || ''
return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id) return id && !id.endsWith('@chatroom') && !id.toLowerCase().includes('placeholder') && this.isSessionAllowed(id)
}) })
.slice(0, 5)
for (const session of candidates) { 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)
await this.generateInsightForSession({ await this.generateInsightForSession({
sessionId: session.username?.trim() || '', sessionId,
displayName: session.displayName || session.username, displayName: session.displayName || session.username,
triggerReason: 'activity' triggerReason: 'activity'
}) })
triggeredCount++
break // 每次最多处理 1 个会话
}
if (triggeredCount === 0) {
insightLog('INFO', '活跃分析完成,无会话触发见解')
} }
} catch (e) { } catch (e) {
console.warn('[InsightService] 活跃分析出错:', e) insightLog('ERROR', `活跃分析出错: ${(e as Error).message}`)
} finally { } finally {
this.processing = false this.processing = false
} }
@@ -424,10 +580,16 @@ class InsightService {
const apiKey = this.config.get('aiInsightApiKey') as string const apiKey = this.config.get('aiInsightApiKey') as string
const model = (this.config.get('aiInsightApiModel') as string) || 'gpt-4o-mini' const model = (this.config.get('aiInsightApiModel') as string) || 'gpt-4o-mini'
const allowContext = this.config.get('aiInsightAllowContext') as boolean 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 ─────────────<E29480><E29480><EFBFBD>────────────────────────────────────────────
// 今日触发统计(让模型具备时间与克制感) // 今日触发统计(让模型具备时间与克制感)
const sessionTriggerTimes = this.recordTrigger(sessionId) const sessionTriggerTimes = this.recordTrigger(sessionId)
@@ -436,7 +598,7 @@ class InsightService {
let contextSection = '' let contextSection = ''
if (allowContext) { if (allowContext) {
try { 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) { if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) {
const messages: Message[] = msgsResult.messages const messages: Message[] = msgsResult.messages
const msgLines = messages.map((m) => { const msgLines = messages.map((m) => {
@@ -445,10 +607,11 @@ class InsightService {
const time = new Date(Number(m.createTime) * 1000).toLocaleString('zh-CN') const time = new Date(Number(m.createTime) * 1000).toLocaleString('zh-CN')
return `[${time}] ${sender}${content}` return `[${time}] ${sender}${content}`
}) })
contextSection = `\n\n<EFBFBD><EFBFBD>期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}` contextSection = `\n\n期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}`
insightLog('INFO', `已加载 ${msgLines.length} 条上下文消息`)
} }
} catch (e) { } catch (e) {
console.warn('[InsightService] 拉取上下文失败:', e) insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`)
} }
} }
@@ -475,6 +638,9 @@ class InsightService {
请给出你的见解≤80字或回复 SKIP 跳过:` 请给出你的见解≤80字或回复 SKIP 跳过:`
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
try { try {
const result = await callApi( const result = await callApi(
apiBaseUrl, apiBaseUrl,
@@ -486,25 +652,27 @@ class InsightService {
] ]
) )
insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`)
// 模型主动选择跳过 // 模型主动选择跳过
if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) {
console.log(`[InsightService] 模型选择跳过 ${sessionId}`) insightLog('INFO', `模型选择跳过 ${displayName}`)
return return
} }
const insight = result.slice(0, 120) // 防止超长截断 const insight = result.slice(0, 120)
// 通过现有 showNotification 推送弹窗 insightLog('INFO', `推送通知 → ${displayName}: ${insight}`)
await showNotification({ await showNotification({
sessionId, sessionId,
sourceName: `见解 · ${displayName}`, sourceName: `见解 · ${displayName}`,
content: insight, content: insight,
isInsight: true // 可用于通知窗口做差异化展示 isInsight: true
}) })
console.log(`[InsightService] 已为 ${sessionId} 推送见解`) insightLog('INFO', `已为 ${displayName} 推送见解`)
} catch (e) { } catch (e) {
console.warn(`[InsightService] API 调用失败 (${sessionId}):`, (e as Error).message) insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`)
} }
} }
} }

View File

@@ -225,9 +225,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [isTestingInsight, setIsTestingInsight] = useState(false) const [isTestingInsight, setIsTestingInsight] = useState(false)
const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null) const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null)
const [showInsightApiKey, setShowInsightApiKey] = useState(false) 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 [aiInsightWhitelistEnabled, setAiInsightWhitelistEnabled] = useState(false)
const [aiInsightWhitelist, setAiInsightWhitelist] = useState<Set<string>>(new Set()) const [aiInsightWhitelist, setAiInsightWhitelist] = useState<Set<string>>(new Set())
const [insightWhitelistSearch, setInsightWhitelistSearch] = useState('') const [insightWhitelistSearch, setInsightWhitelistSearch] = useState('')
const [aiInsightCooldownMinutes, setAiInsightCooldownMinutes] = useState(120)
const [aiInsightScanIntervalHours, setAiInsightScanIntervalHours] = useState(4)
const [aiInsightContextCount, setAiInsightContextCount] = useState(40)
const [isWayland, setIsWayland] = useState(false) const [isWayland, setIsWayland] = useState(false)
useEffect(() => { useEffect(() => {
@@ -463,6 +468,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() const savedAiInsightAllowContext = await configService.getAiInsightAllowContext()
const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled()
const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() const savedAiInsightWhitelist = await configService.getAiInsightWhitelist()
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours()
const savedAiInsightContextCount = await configService.getAiInsightContextCount()
setAiInsightEnabled(savedAiInsightEnabled) setAiInsightEnabled(savedAiInsightEnabled)
setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl) setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl)
setAiInsightApiKey(savedAiInsightApiKey) setAiInsightApiKey(savedAiInsightApiKey)
@@ -471,6 +479,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setAiInsightAllowContext(savedAiInsightAllowContext) setAiInsightAllowContext(savedAiInsightAllowContext)
setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled)
setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) setAiInsightWhitelist(new Set(savedAiInsightWhitelist))
setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes)
setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours)
setAiInsightContextCount(savedAiInsightContextCount)
} catch (e: any) { } catch (e: any) {
console.error('加载配置失败:', e) console.error('加载配置失败:', e)
@@ -2609,8 +2620,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
/> />
</div> </div>
{/* 测试连接 */} {/* 测试连接 + 触发测试 */}
<div className="form-group"> <div className="form-group">
<label></label>
<span className="form-hint">
"测试 API 连接" Key URL "立即触发测试见解"API
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
{/* 测试 API 连接 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<button <button
className="btn btn-secondary" className="btn btn-secondary"
@@ -2618,7 +2635,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
disabled={isTestingInsight || !aiInsightApiBaseUrl || !aiInsightApiKey} disabled={isTestingInsight || !aiInsightApiBaseUrl || !aiInsightApiKey}
> >
{isTestingInsight ? ( {isTestingInsight ? (
<><Loader2 size={14} style={{ marginRight: 4, animation: 'spin 1s linear infinite' }} /> ...</> <><Loader2 size={14} style={{ marginRight: 4, animation: 'spin 1s linear infinite' }} />...</>
) : ( ) : (
<> API </> <> API </>
)} )}
@@ -2630,15 +2647,94 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</span> </span>
)} )}
</div> </div>
{/* 触发测试见解 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
<button
className="btn btn-secondary"
onClick={async () => {
setIsTriggeringInsightTest(true)
setInsightTriggerResult(null)
try {
const result = await (window.electronAPI as any).insight.triggerTest()
setInsightTriggerResult(result)
} catch (e: any) {
setInsightTriggerResult({ success: false, message: `调用失败:${e?.message || String(e)}` })
} finally {
setIsTriggeringInsightTest(false)
}
}}
disabled={isTriggeringInsightTest || !aiInsightEnabled || !aiInsightApiBaseUrl || !aiInsightApiKey}
title={!aiInsightEnabled ? '请先开启 AI 见解总开关' : ''}
>
{isTriggeringInsightTest ? (
<><Loader2 size={14} style={{ marginRight: 4, animation: 'spin 1s linear infinite' }} />...</>
) : (
<></>
)}
</button>
{insightTriggerResult && (
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: insightTriggerResult.success ? 'var(--color-success, #22c55e)' : 'var(--color-danger, #ef4444)' }}>
{insightTriggerResult.success ? <CheckCircle2 size={14} /> : <XCircle size={14} />}
{insightTriggerResult.message}
</span>
)}
</div>
</div>
</div> </div>
<div className="divider" /> <div className="divider" />
{/* 行为配置 */} {/* 行为配置 */}
<div className="form-group">
<label></label>
<span className="form-hint">
<strong>0</strong> AI
</span>
<input
type="number"
className="field-input"
value={aiInsightCooldownMinutes}
min={0}
max={10080}
onChange={(e) => {
const val = Math.max(0, parseInt(e.target.value, 10) || 0)
setAiInsightCooldownMinutes(val)
scheduleConfigSave('aiInsightCooldownMinutes', () => configService.setAiInsightCooldownMinutes(val))
}}
style={{ width: 120 }}
/>
{aiInsightCooldownMinutes === 0 && (
<span style={{ marginLeft: 10, fontSize: 12, color: 'var(--color-warning, #f59e0b)' }}>
DB
</span>
)}
</div>
<div className="form-group">
<label></label>
<span className="form-hint">
0.1 6
</span>
<input
type="number"
className="field-input"
value={aiInsightScanIntervalHours}
min={0.1}
max={168}
step={0.5}
onChange={(e) => {
const val = Math.max(0.1, parseFloat(e.target.value) || 4)
setAiInsightScanIntervalHours(val)
scheduleConfigSave('aiInsightScanIntervalHours', () => configService.setAiInsightScanIntervalHours(val))
}}
style={{ width: 120 }}
/>
</div>
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"> <span className="form-hint">
AI 4
</span> </span>
<input <input
type="number" type="number"
@@ -2658,11 +2754,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"> <span className="form-hint">
AI 40 AI使 N AI
<br /> <br />
<strong></strong>AI "与某人沉默了 N 天" <strong></strong>AI
<br /> <br />
<strong></strong> API <strong></strong> API
</span> </span>
<div className="log-toggle-line"> <div className="log-toggle-line">
<span className="log-status">{aiInsightAllowContext ? '已授权' : '未授权'}</span> <span className="log-status">{aiInsightAllowContext ? '已授权' : '未授权'}</span>
@@ -2681,6 +2777,28 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div> </div>
</div> </div>
{aiInsightAllowContext && (
<div className="form-group">
<label></label>
<span className="form-hint">
AI token
</span>
<input
type="number"
className="field-input"
value={aiInsightContextCount}
min={1}
max={200}
onChange={(e) => {
const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40))
setAiInsightContextCount(val)
scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val))
}}
style={{ width: 100 }}
/>
</div>
)}
<div className="divider" /> <div className="divider" />
{/* 对话白名单 */} {/* 对话白名单 */}
@@ -2879,7 +2997,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<p className="api-desc" style={{ lineHeight: 1.7 }}> <p className="api-desc" style={{ lineHeight: 1.7 }}>
<strong></strong> 500ms <br /> <strong></strong> 500ms <br />
<strong></strong> 4 <br /> <strong></strong> 4 <br />
<strong></strong> AI AI <br /> <strong></strong> <EFBFBD><EFBFBD>AI AI <br />
<strong></strong> API WeFlow <strong></strong> API WeFlow
</p> </p>
</div> </div>
@@ -3123,7 +3241,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
try { try {
const verifyResult = await window.electronAPI.auth.hello('请验证您的身份以开启 Windows Hello') const verifyResult = await window.electronAPI.auth.hello('请验证您的身份以开启 Windows Hello')
if (!verifyResult.success) { if (!verifyResult.success) {
showMessage(verifyResult.error || 'Windows Hello 证失败', false) showMessage(verifyResult.error || 'Windows Hello <EFBFBD><EFBFBD>证失败', false)
return return
} }

View File

@@ -89,7 +89,10 @@ export const CONFIG_KEYS = {
AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays', AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays',
AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext', AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext',
AI_INSIGHT_WHITELIST_ENABLED: 'aiInsightWhitelistEnabled', 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 } as const
export interface WxidConfig { export interface WxidConfig {
@@ -1635,3 +1638,30 @@ export async function getAiInsightWhitelist(): Promise<string[]> {
export async function setAiInsightWhitelist(list: string[]): Promise<void> { export async function setAiInsightWhitelist(list: string[]): Promise<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_WHITELIST, list) await config.set(CONFIG_KEYS.AI_INSIGHT_WHITELIST, list)
} }
export async function getAiInsightCooldownMinutes(): Promise<number> {
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<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_COOLDOWN_MINUTES, minutes)
}
export async function getAiInsightScanIntervalHours(): Promise<number> {
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<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_SCAN_INTERVAL_HOURS, hours)
}
export async function getAiInsightContextCount(): Promise<number> {
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<void> {
await config.set(CONFIG_KEYS.AI_INSIGHT_CONTEXT_COUNT, count)
}