Merge pull request #7 from Jasonzhu1207/v0/jasonzhu081207-4751-e705ab05

Enable AI insights and system-native notifications
This commit is contained in:
Jason
2026-04-06 11:39:56 +08:00
committed by GitHub
2 changed files with 54 additions and 28 deletions

View File

@@ -24,8 +24,11 @@ import { chatService, ChatSession, Message } from './chatService'
// ─── 常量 ──────────────────────────────────────────────────────────────────── // ─── 常量 ────────────────────────────────────────────────────────────────────
/** DB 变更防抖延迟(毫秒) */ /**
const DB_CHANGE_DEBOUNCE_MS = 500 * DB 变更防抖延迟(毫秒)。
* 设为 2s微信写库通常是批量操作500ms 过短会在开机/重连时产生大量连续触发。
*/
const DB_CHANGE_DEBOUNCE_MS = 2000
/** 首次沉默扫描延迟(毫秒),避免启动期间抢占资源 */ /** 首次沉默扫描延迟(毫秒),避免启动期间抢占资源 */
const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000 const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000
@@ -208,6 +211,15 @@ class InsightService {
*/ */
private lastSeenTimestamp: Map<string, number> = new Map() private lastSeenTimestamp: Map<string, number> = new Map()
/**
* 本地会话快照缓存,避免 analyzeRecentActivity 在每次 DB 变更时都做全量读取。
* 首次调用时填充,此后只在沉默扫描里刷新(沉默扫描间隔更长,更合适做全量刷新)。
*/
private sessionCache: ChatSession[] | null = null
/** sessionCache 最后刷新时间戳ms超过 5 分钟强制重新拉取 */
private sessionCacheAt = 0
private static readonly SESSION_CACHE_TTL_MS = 5 * 60 * 1000
private started = false private started = false
constructor() { constructor() {
@@ -230,7 +242,7 @@ class InsightService {
this.dbDebounceTimer = null this.dbDebounceTimer = null
} }
if (this.silenceScanTimer !== null) { if (this.silenceScanTimer !== null) {
clearInterval(this.silenceScanTimer) clearTimeout(this.silenceScanTimer)
this.silenceScanTimer = null this.silenceScanTimer = null
} }
if (this.silenceInitialDelayTimer !== null) { if (this.silenceInitialDelayTimer !== null) {
@@ -358,6 +370,29 @@ class InsightService {
return whitelist.includes(sessionId) return whitelist.includes(sessionId)
} }
/**
* 获取会话列表优先使用缓存5 分钟 TTL
* 缓存命中时不访问 DB显著减少对主线程的占用。
*/
private async getSessionsCached(forceRefresh = false): Promise<ChatSession[]> {
const now = Date.now()
if (
!forceRefresh &&
this.sessionCache !== null &&
now - this.sessionCacheAt < InsightService.SESSION_CACHE_TTL_MS
) {
return this.sessionCache
}
const connectResult = await chatService.connect()
if (!connectResult.success) return this.sessionCache ?? []
const result = await chatService.getSessions()
if (result.success && result.sessions) {
this.sessionCache = result.sessions as ChatSession[]
this.sessionCacheAt = now
}
return this.sessionCache ?? []
}
private resetIfNewDay(): void { private resetIfNewDay(): void {
const todayStart = getStartOfDay() const todayStart = getStartOfDay()
if (todayStart > this.todayDate) { if (todayStart > this.todayDate) {
@@ -426,19 +461,13 @@ class InsightService {
insightLog('INFO', `沉默阈值:${silenceDays}`) insightLog('INFO', `沉默阈值:${silenceDays}`)
const connectResult = await chatService.connect() // 沉默扫描间隔较长,强制刷新缓存以获取最新数据
if (!connectResult.success) { const sessions = await this.getSessionsCached(true)
insightLog('WARN', '数据库连接失败,跳过沉默扫描') if (sessions.length === 0) {
insightLog('WARN', '获取会话列表失败,跳过沉默扫描')
return return
} }
const sessionsResult = await chatService.getSessions()
if (!sessionsResult.success || !sessionsResult.sessions) {
insightLog('WARN', '获取会话列表失败')
return
}
const sessions: ChatSession[] = sessionsResult.sessions
insightLog('INFO', `${sessions.length} 个会话,开始过滤...`) insightLog('INFO', `${sessions.length} 个会话,开始过滤...`)
let silentCount = 0 let silentCount = 0
@@ -489,19 +518,13 @@ class InsightService {
this.processing = true this.processing = true
insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...') insightLog('INFO', 'DB 变更防抖触发,开始活跃分析...')
try { try {
const connectResult = await chatService.connect() // 使用缓存版本,避免每次 DB 变更都做全量读取5 分钟 TTL
if (!connectResult.success) { const sessions = await this.getSessionsCached()
insightLog('WARN', '数据库连接失败,跳过活跃分析') if (sessions.length === 0) {
insightLog('WARN', '会话缓存为空,跳过活跃分析')
return return
} }
const sessionsResult = await chatService.getSessions()
if (!sessionsResult.success || !sessionsResult.sessions) {
insightLog('WARN', '获取会话列表失败')
return
}
const sessions: ChatSession[] = sessionsResult.sessions
const now = Date.now() const now = Date.now()
// 从 config 读取冷却分钟数0 = 无冷却) // 从 config 读取冷却分钟数0 = 无冷却)

View File

@@ -864,16 +864,19 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
} }
useEffect(() => { useEffect(() => {
if (activeTab !== 'antiRevoke') return if (activeTab !== 'antiRevoke' && activeTab !== 'insight') return
let canceled = false let canceled = false
;(async () => { ;(async () => {
try { try {
// 两个 Tab 都需要会话列表antiRevoke 还需要额外检查防撤回状态
const sessionIds = await ensureAntiRevokeSessionsLoaded() const sessionIds = await ensureAntiRevokeSessionsLoaded()
if (canceled) return if (canceled) return
if (activeTab === 'antiRevoke') {
await handleRefreshAntiRevokeStatus(sessionIds) await handleRefreshAntiRevokeStatus(sessionIds)
}
} catch (e: any) { } catch (e: any) {
if (!canceled) { if (!canceled) {
showMessage(`加载防撤回会话失败: ${e?.message || String(e)}`, false) showMessage(`加载会话失败: ${e?.message || String(e)}`, false)
} }
} }
})() })()
@@ -1215,7 +1218,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
if (result.success && result.aesKey) { if (result.success && result.aesKey) {
if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`)
setImageAesKey(result.aesKey) setImageAesKey(result.aesKey)
setImageKeyStatus('已获取图片钥') setImageKeyStatus('已获取图片<EFBFBD><EFBFBD>钥')
showMessage('已自动获取图片密钥', true) showMessage('已自动获取图片密钥', true)
const newXorKey = typeof result.xorKey === 'number' ? result.xorKey : 0 const newXorKey = typeof result.xorKey === 'number' ? result.xorKey : 0
const newAesKey = result.aesKey const newAesKey = result.aesKey
@@ -3551,7 +3554,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="updates-hero-main"> <div className="updates-hero-main">
<span className="updates-chip"></span> <span className="updates-chip"></span>
<h2>{appVersion || '...'}</h2> <h2>{appVersion || '...'}</h2>
<p>{updateInfo?.hasUpdate ? `发现新版本 v${updateInfo.version}` : '当前已是最新版本,可手动检查更'}</p> <p>{updateInfo?.hasUpdate ? `发现新版本 v${updateInfo.version}` : '当前已是最新版本,可手动检查更<EFBFBD><EFBFBD><EFBFBD>'}</p>
</div> </div>
<div className="updates-hero-action"> <div className="updates-hero-action">
{updateInfo?.hasUpdate ? ( {updateInfo?.hasUpdate ? (