mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-08 07:25:51 +00:00
feat: add configurable AI insight settings and desktop logging
Introduce new configurable fields and log insights to desktop. Co-authored-by: Jason <159670257+Jasonzhu1207@users.noreply.github.com>
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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,27 +28,15 @@ 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
|
||||||
|
|
||||||
/**
|
|
||||||
* 同一会话活跃分析的冷却时间(毫秒),2 小时。
|
|
||||||
* 防止 DB 频繁变更时对同一会话反复调用 API。
|
|
||||||
*/
|
|
||||||
const ACTIVITY_COOLDOWN_MS = 2 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
// ─── 类型 ────────────────────────────────────────────────────────────────────
|
// ─── 类型 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface TodayTriggerRecord {
|
interface TodayTriggerRecord {
|
||||||
@@ -53,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 {
|
||||||
|
// 写文件失败静默处理,不影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +238,7 @@ class InsightService {
|
|||||||
clearTimeout(this.silenceInitialDelayTimer)
|
clearTimeout(this.silenceInitialDelayTimer)
|
||||||
this.silenceInitialDelayTimer = null
|
this.silenceInitialDelayTimer = null
|
||||||
}
|
}
|
||||||
console.log('[InsightService] 已停止')
|
insightLog('INFO', '已停止')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,7 +290,7 @@ class InsightService {
|
|||||||
* 返回触发结果描述,供设置页展示。
|
* 返回触发结果描述,供设置页展示。
|
||||||
*/
|
*/
|
||||||
async triggerTest(): Promise<{ success: boolean; message: string }> {
|
async triggerTest(): Promise<{ success: boolean; message: string }> {
|
||||||
console.log('[InsightService] 手动触发测试见解...')
|
insightLog('INFO', '手动触发测试见解...')
|
||||||
const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string
|
const apiBaseUrl = this.config.get('aiInsightApiBaseUrl') as string
|
||||||
const apiKey = this.config.get('aiInsightApiKey') as string
|
const apiKey = this.config.get('aiInsightApiKey') as string
|
||||||
if (!apiBaseUrl || !apiKey) {
|
if (!apiBaseUrl || !apiKey) {
|
||||||
@@ -285,17 +305,17 @@ class InsightService {
|
|||||||
if (!sessionsResult.success || !sessionsResult.sessions || sessionsResult.sessions.length === 0) {
|
if (!sessionsResult.success || !sessionsResult.sessions || sessionsResult.sessions.length === 0) {
|
||||||
return { success: false, message: '未找到任何会话,请确认数据库已正确连接' }
|
return { success: false, message: '未找到任何会话,请确认数据库已正确连接' }
|
||||||
}
|
}
|
||||||
// 找第一个私聊
|
// 找第一个允许的私聊
|
||||||
const session = (sessionsResult.sessions as ChatSession[]).find((s) => {
|
const session = (sessionsResult.sessions as ChatSession[]).find((s) => {
|
||||||
const id = s.username?.trim() || ''
|
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) {
|
if (!session) {
|
||||||
return { success: false, message: '未找到任何私聊会话' }
|
return { success: false, message: '未找到任何私聊会话(若已启用白名单,请检查是否有勾选的私聊)' }
|
||||||
}
|
}
|
||||||
const sessionId = session.username?.trim() || ''
|
const sessionId = session.username?.trim() || ''
|
||||||
const displayName = session.displayName || sessionId
|
const displayName = session.displayName || sessionId
|
||||||
console.log(`[InsightService] 测试目标会话:${displayName} (${sessionId})`)
|
insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`)
|
||||||
await this.generateInsightForSession({
|
await this.generateInsightForSession({
|
||||||
sessionId,
|
sessionId,
|
||||||
displayName,
|
displayName,
|
||||||
@@ -373,57 +393,62 @@ 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()) {
|
if (!this.isEnabled()) {
|
||||||
console.log('[InsightService] 沉默扫描:AI 见解未启用,跳过')
|
insightLog('INFO', '沉默扫描:AI 见解未启用,跳过')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.processing) {
|
if (this.processing) {
|
||||||
console.log('[InsightService] 沉默扫描:正在处理中,跳过本次')
|
insightLog('INFO', '沉默扫描:正在处理中,跳过本次')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
console.log('[InsightService] 开始沉默联系人扫描...')
|
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()
|
||||||
|
|
||||||
console.log(`[InsightService] 沉默阈值:${silenceDays} 天`)
|
insightLog('INFO', `沉默阈值:${silenceDays} 天`)
|
||||||
|
|
||||||
const connectResult = await chatService.connect()
|
const connectResult = await chatService.connect()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
console.log('[InsightService] 数据库连接失败,跳过沉默扫描')
|
insightLog('WARN', '数据库连接失败,跳过沉默扫描')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionsResult = await chatService.getSessions()
|
const sessionsResult = await chatService.getSessions()
|
||||||
if (!sessionsResult.success || !sessionsResult.sessions) {
|
if (!sessionsResult.success || !sessionsResult.sessions) {
|
||||||
console.log('[InsightService] 获取会话列表失败')
|
insightLog('WARN', '获取会话列表失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions: ChatSession[] = sessionsResult.sessions
|
const sessions: ChatSession[] = sessionsResult.sessions
|
||||||
console.log(`[InsightService] 共 ${sessions.length} 个会话,开始过滤...`)
|
insightLog('INFO', `共 ${sessions.length} 个会话,开始过滤...`)
|
||||||
|
|
||||||
let silentCount = 0
|
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
|
||||||
|
|
||||||
@@ -432,9 +457,8 @@ class InsightService {
|
|||||||
|
|
||||||
silentCount++
|
silentCount++
|
||||||
const silentDays = Math.floor(silentMs / (24 * 60 * 60 * 1000))
|
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({
|
await this.generateInsightForSession({
|
||||||
sessionId,
|
sessionId,
|
||||||
displayName: session.displayName || session.username,
|
displayName: session.displayName || session.username,
|
||||||
@@ -442,9 +466,9 @@ class InsightService {
|
|||||||
silentDays
|
silentDays
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(`[InsightService] 沉默扫描完成,共发现 ${silentCount} 个沉默联系人`)
|
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
|
||||||
}
|
}
|
||||||
@@ -464,30 +488,33 @@ class InsightService {
|
|||||||
if (this.processing) return
|
if (this.processing) return
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
console.log('[InsightService] DB 变更防抖触发,开始活跃分析...')
|
insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...')
|
||||||
try {
|
try {
|
||||||
const connectResult = await chatService.connect()
|
const connectResult = await chatService.connect()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
console.log('[InsightService] 数据库连接失败,跳过活跃分析')
|
insightLog('WARN', '数据库连接失败,跳过活跃分析')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionsResult = await chatService.getSessions()
|
const sessionsResult = await chatService.getSessions()
|
||||||
if (!sessionsResult.success || !sessionsResult.sessions) {
|
if (!sessionsResult.success || !sessionsResult.sessions) {
|
||||||
console.log('[InsightService] 获取会话列表失败')
|
insightLog('WARN', '获取会话列表失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions: ChatSession[] = sessionsResult.sessions
|
const sessions: ChatSession[] = sessionsResult.sessions
|
||||||
const now = Date.now()
|
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 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)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`[InsightService] 筛选到 ${privateSessions.length} 个私聊会话(已过白名单过滤)`)
|
insightLog('INFO', `筛选到 ${privateSessions.length} 个私聊会话(白名单过滤后),冷却期 ${cooldownMinutes} 分钟`)
|
||||||
|
|
||||||
let triggeredCount = 0
|
let triggeredCount = 0
|
||||||
for (const session of privateSessions.slice(0, 10)) {
|
for (const session of privateSessions.slice(0, 10)) {
|
||||||
@@ -499,21 +526,23 @@ class InsightService {
|
|||||||
|
|
||||||
// 检查是否有真正的新消息
|
// 检查是否有真正的新消息
|
||||||
if (currentTimestamp <= lastSeen) {
|
if (currentTimestamp <= lastSeen) {
|
||||||
continue // 没有新消息,跳过
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新已见时间戳
|
// 更新已见时间戳
|
||||||
this.lastSeenTimestamp.set(sessionId, currentTimestamp)
|
this.lastSeenTimestamp.set(sessionId, currentTimestamp)
|
||||||
|
|
||||||
// 检查冷却期
|
// 检查冷却期(0 分钟 = 无冷却,直接通过)
|
||||||
const lastAnalysis = this.lastActivityAnalysis.get(sessionId) ?? 0
|
if (cooldownMs > 0) {
|
||||||
const cooldownRemaining = ACTIVITY_COOLDOWN_MS - (now - lastAnalysis)
|
const lastAnalysis = this.lastActivityAnalysis.get(sessionId) ?? 0
|
||||||
if (cooldownRemaining > 0) {
|
const cooldownRemaining = cooldownMs - (now - lastAnalysis)
|
||||||
console.log(`[InsightService] ${sessionId} 冷却中,还需 ${Math.ceil(cooldownRemaining / 60000)} 分钟`)
|
if (cooldownRemaining > 0) {
|
||||||
continue
|
insightLog('INFO', `${session.displayName || sessionId} 冷却中,还需 ${Math.ceil(cooldownRemaining / 60000)} 分钟`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[InsightService] ${sessionId} 有新消息且冷却已过,准备生成见解...`)
|
insightLog('INFO', `${session.displayName || sessionId} 有新消息,准备生成见解...`)
|
||||||
this.lastActivityAnalysis.set(sessionId, now)
|
this.lastActivityAnalysis.set(sessionId, now)
|
||||||
|
|
||||||
await this.generateInsightForSession({
|
await this.generateInsightForSession({
|
||||||
@@ -523,15 +552,14 @@ class InsightService {
|
|||||||
})
|
})
|
||||||
triggeredCount++
|
triggeredCount++
|
||||||
|
|
||||||
// 每次最多处理 1 个会话,避免单次防抖后批量调用
|
break // 每次最多处理 1 个会话
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggeredCount === 0) {
|
if (triggeredCount === 0) {
|
||||||
console.log('[InsightService] 活跃分析完成,无会话触发见解')
|
insightLog('INFO', '活跃分析完成,无会话触发见解')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[InsightService] 活跃分析出错:', e)
|
insightLog('ERROR', `活跃分析出错: ${(e as Error).message}`)
|
||||||
} finally {
|
} finally {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
}
|
}
|
||||||
@@ -552,11 +580,12 @@ 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
|
||||||
|
|
||||||
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) {
|
if (!apiBaseUrl || !apiKey) {
|
||||||
console.warn('[InsightService] API 地址或 Key 未配置,跳过见解生成')
|
insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,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) => {
|
||||||
@@ -578,8 +607,13 @@ 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) {
|
||||||
|
insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[InsightService] 拉取上下文失败:', e)
|
console.warn('[InsightService] 拉取上下文失败:', e)
|
||||||
}
|
}
|
||||||
@@ -609,7 +643,7 @@ class InsightService {
|
|||||||
请给出你的见解(≤80字)或回复 SKIP 跳过:`
|
请给出你的见解(≤80字)或回复 SKIP 跳过:`
|
||||||
|
|
||||||
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions')
|
||||||
console.log(`[InsightService] 准备调用 API: ${endpoint},模型: ${model}`)
|
insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await callApi(
|
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')) {
|
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}`)
|
||||||
console.log(`[InsightService] 准备推送通知: ${insight}`)
|
|
||||||
await showNotification({
|
await showNotification({
|
||||||
sessionId,
|
sessionId,
|
||||||
sourceName: `见解 · ${displayName}`,
|
sourceName: `见解 · ${displayName}`,
|
||||||
@@ -641,9 +674,9 @@ class InsightService {
|
|||||||
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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
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(() => {
|
||||||
@@ -462,17 +465,23 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedAiInsightApiKey = await configService.getAiInsightApiKey()
|
const savedAiInsightApiKey = await configService.getAiInsightApiKey()
|
||||||
const savedAiInsightApiModel = await configService.getAiInsightApiModel()
|
const savedAiInsightApiModel = await configService.getAiInsightApiModel()
|
||||||
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays()
|
||||||
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()
|
||||||
setAiInsightEnabled(savedAiInsightEnabled)
|
const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes()
|
||||||
setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl)
|
const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours()
|
||||||
setAiInsightApiKey(savedAiInsightApiKey)
|
const savedAiInsightContextCount = await configService.getAiInsightContextCount()
|
||||||
setAiInsightApiModel(savedAiInsightApiModel)
|
setAiInsightEnabled(savedAiInsightEnabled)
|
||||||
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl)
|
||||||
setAiInsightAllowContext(savedAiInsightAllowContext)
|
setAiInsightApiKey(savedAiInsightApiKey)
|
||||||
setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled)
|
setAiInsightApiModel(savedAiInsightApiModel)
|
||||||
setAiInsightWhitelist(new Set(savedAiInsightWhitelist))
|
setAiInsightSilenceDays(savedAiInsightSilenceDays)
|
||||||
|
setAiInsightAllowContext(savedAiInsightAllowContext)
|
||||||
|
setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled)
|
||||||
|
setAiInsightWhitelist(new Set(savedAiInsightWhitelist))
|
||||||
|
setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes)
|
||||||
|
setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours)
|
||||||
|
setAiInsightContextCount(savedAiInsightContextCount)
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('加载配置失败:', e)
|
console.error('加载配置失败:', e)
|
||||||
@@ -2676,10 +2685,56 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<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"
|
||||||
@@ -2699,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>
|
||||||
@@ -2722,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" />
|
||||||
|
|
||||||
{/* 对话白名单 */}
|
{/* 对话白名单 */}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user