diff --git a/electron/main.ts b/electron/main.ts index ff1d5f3..fdc8fc8 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1819,6 +1819,14 @@ function registerIpcHandlers() { return insightService.triggerTest() }) + ipcMain.handle('insight:triggerSessionInsight', async (_, payload: { + sessionId: string + displayName?: string + avatarUrl?: string + }) => { + return insightService.triggerSessionInsight(payload) + }) + ipcMain.handle('insight:generateFootprintInsight', async (_, payload: { rangeLabel: string summary: { diff --git a/electron/preload.ts b/electron/preload.ts index 3df0c44..6d85b53 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -583,6 +583,11 @@ contextBridge.exposeInMainWorld('electronAPI', { markRecordRead: (id: string) => ipcRenderer.invoke('insight:markRecordRead', id), clearRecords: (filters?: any) => ipcRenderer.invoke('insight:clearRecords', filters), triggerTest: () => ipcRenderer.invoke('insight:triggerTest'), + triggerSessionInsight: (payload: { + sessionId: string + displayName?: string + avatarUrl?: string + }) => ipcRenderer.invoke('insight:triggerSessionInsight', payload), generateFootprintInsight: (payload: { rangeLabel: string summary: { diff --git a/electron/services/insightRecordService.ts b/electron/services/insightRecordService.ts index e2a049b..b36b203 100644 --- a/electron/services/insightRecordService.ts +++ b/electron/services/insightRecordService.ts @@ -4,7 +4,7 @@ import path from 'path' import { createHash, randomUUID } from 'crypto' import { ConfigService } from './config' -export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'message_analysis' +export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'manual' | 'message_analysis' export type InsightRecordSourceType = 'insight' | 'message_analysis' export interface MessageInsightAnalysis { diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 1934e1c..ed48173 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -84,6 +84,15 @@ interface SharedAiModelConfig { maxTokens: number } +interface SessionInsightTriggerResult { + success: boolean + message: string + recordId?: string + insight?: string + skipped?: boolean + notificationEnabled?: boolean +} + type InsightFilterMode = 'whitelist' | 'blacklist' class ApiRequestError extends Error { @@ -537,11 +546,14 @@ class InsightService { const sessionId = session.username?.trim() || '' const displayName = session.displayName || sessionId insightLog('INFO', `测试目标会话:${displayName} (${sessionId})`) - await this.generateInsightForSession({ + const result = await this.generateInsightForSession({ sessionId, displayName, triggerReason: 'test' }) + if (!result.success) { + return { success: false, message: result.message } + } const notificationEnabled = this.config.get('aiInsightNotificationEnabled') !== false return { success: true, @@ -554,6 +566,47 @@ class InsightService { } } + /** + * 手动对指定会话立即触发一次 AI 见解。 + * 只新增触发入口;实际上下文、朋友圈/微博拼接、prompt 和入库仍走 generateInsightForSession。 + */ + async triggerSessionInsight(params: { + sessionId: string + displayName?: string + avatarUrl?: string + }): Promise { + const sessionId = String(params?.sessionId || '').trim() + if (!sessionId) { + return { success: false, message: '当前会话无效,无法触发 AI 见解' } + } + if (!this.isEnabled()) { + return { success: false, message: '请先在设置中开启「AI 见解」' } + } + + const { apiBaseUrl, apiKey } = this.getSharedAiModelConfig() + if (!apiBaseUrl || !apiKey) { + return { success: false, message: '请先填写通用 AI 模型配置(API 地址和 Key)' } + } + + try { + const connectResult = await chatService.connect() + if (!connectResult.success) { + return { success: false, message: '数据库连接失败,请先在"数据库连接"页完成配置' } + } + this.dbConnected = true + + const displayName = String(params?.displayName || sessionId).trim() || sessionId + insightLog('INFO', `手动触发当前会话见解:${displayName} (${sessionId})`) + return await this.generateInsightForSession({ + sessionId, + displayName, + triggerReason: 'manual' + }) + } catch (error) { + return { success: false, message: `触发失败:${(error as Error).message}` } + } + } + /** 获取今日触发统计(供设置页展示) */ getTodayStats(): { sessionId: string; count: number; times: string[] }[] { this.resetIfNewDay() @@ -1372,10 +1425,10 @@ ${afterText} displayName: string triggerReason: InsightRecordTriggerReason silentDays?: number - }): Promise { + }): Promise { const { sessionId, displayName, triggerReason, silentDays } = params - if (!sessionId) return - if (!this.isEnabled()) return + if (!sessionId) return { success: false, message: '会话无效,无法生成见解' } + if (!this.isEnabled()) return { success: false, message: '请先在设置中开启「AI 见解」' } const { apiBaseUrl, apiKey, model, maxTokens } = this.getSharedAiModelConfig() const allowContext = this.config.get('aiInsightAllowContext') as boolean @@ -1393,7 +1446,7 @@ ${afterText} if (!apiBaseUrl || !apiKey) { insightLog('WARN', 'API 地址或 Key 未配置,跳过见解生成') - return + return { success: false, message: '请先填写通用 AI 模型配置(API 地址和 Key)' } } // ── 构建 prompt ──────────────────────────────────────────────────────────── @@ -1483,9 +1536,9 @@ ${afterText} // 模型主动选择跳过 if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`) - return + return { success: true, message: `模型判断「${resolvedDisplayName}」暂无可生成的见解`, skipped: true } } - if (!this.isEnabled()) return + if (!this.isEnabled()) return { success: false, message: 'AI 见解已关闭,生成结果未保存' } const insight = result.trim() const notifTitle = `见解 · ${resolvedDisplayName}` @@ -1550,6 +1603,15 @@ ${afterText} insightLog('INFO', `已完成 ${resolvedDisplayName} 的见解处理`) this.recordTrigger(sessionId) + return { + success: true, + message: insightNotificationEnabled + ? `已生成「${resolvedDisplayName}」的 AI 见解,请查看通知弹窗` + : `已生成「${resolvedDisplayName}」的 AI 见解,AI 见解消息通知当前已关闭`, + recordId: record.id, + insight, + notificationEnabled: insightNotificationEnabled + } } catch (e) { insightDebugSection( 'ERROR', @@ -1557,6 +1619,7 @@ ${afterText} `错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}` ) insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`) + return { success: false, message: `生成失败:${(e as Error).message}` } } } diff --git a/src/components/Export/ExportDateRangeDialog.tsx b/src/components/Export/ExportDateRangeDialog.tsx index 497251c..0bde562 100644 --- a/src/components/Export/ExportDateRangeDialog.tsx +++ b/src/components/Export/ExportDateRangeDialog.tsx @@ -557,8 +557,7 @@ export function ExportDateRangeDialog({ event.stopPropagation() onClose() }} - > -
event.stopPropagation()}> + >
event.stopPropagation()}>

{title}

+ {!standaloneSessionWindow && isGroupChat && (
)} + {sessionInsightHint && ( +
+ {isTriggeringSessionInsight ? : } + {sessionInsightHint.message} +
+ )} + setChatSnsTimelineTarget(null)} diff --git a/src/pages/InsightInboxPage.scss b/src/pages/InsightInboxPage.scss index 289a3c7..764ab48 100644 --- a/src/pages/InsightInboxPage.scss +++ b/src/pages/InsightInboxPage.scss @@ -265,6 +265,11 @@ color: #5b55a0; background: rgba(99, 102, 241, 0.12); } + + &.manual { + color: #0f766e; + background: rgba(20, 184, 166, 0.13); + } } .insight-source-pill { diff --git a/src/pages/InsightInboxPage.tsx b/src/pages/InsightInboxPage.tsx index 0a08ff4..5310039 100644 --- a/src/pages/InsightInboxPage.tsx +++ b/src/pages/InsightInboxPage.tsx @@ -67,6 +67,7 @@ function getTriggerLabel(reason: InsightRecordTriggerReason): string { if (reason === 'message_analysis') return '深度解析' if (reason === 'silence') return '沉默提醒' if (reason === 'test') return '测试见解' + if (reason === 'manual') return '手动触发' return '活跃分析' } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 2c8807a..ecfddc8 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -21,7 +21,7 @@ export interface SocialSaveWeiboCookieResult { error?: string } -export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'message_analysis' +export type InsightRecordTriggerReason = 'activity' | 'silence' | 'test' | 'manual' | 'message_analysis' export type InsightRecordSourceType = 'insight' | 'message_analysis' export interface MessageInsightAnalysis { @@ -1344,6 +1344,11 @@ export interface ElectronAPI { markRecordRead: (id: string) => Promise<{ success: boolean; error?: string }> clearRecords: (filters?: InsightRecordFilters) => Promise<{ success: boolean; removed: number; error?: string }> triggerTest: () => Promise<{ success: boolean; message: string }> + triggerSessionInsight: (payload: { + sessionId: string + displayName?: string + avatarUrl?: string + }) => Promise<{ success: boolean; message: string; recordId?: string; insight?: string; skipped?: boolean; notificationEnabled?: boolean }> generateFootprintInsight: (payload: { rangeLabel: string summary: {