mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-06-09 07:36:46 +00:00
Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev
This commit is contained in:
@@ -1813,6 +1813,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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -468,10 +468,10 @@ export class BackupService {
|
||||
)
|
||||
if (!opened) {
|
||||
const detail = await wcdbService.getLastInitError().catch(() => null)
|
||||
return { success: false, error: detail || `目标账号 ${accountDirName} 数据库连接失败` }
|
||||
return { success: false, error: detail || `目标账号 ${accountDir} 数据库连接失败` }
|
||||
}
|
||||
|
||||
return { success: true, wxid: accountDirName, dbPath, dbStorage }
|
||||
return { success: true, wxid: accountDir, dbPath, dbStorage }
|
||||
}
|
||||
|
||||
private buildDbId(kind: BackupDbKind, index: number, dbPath: string): string {
|
||||
@@ -858,10 +858,13 @@ export class BackupService {
|
||||
if (!existsSync(manifestPath)) return { success: false, error: '备份包缺少 manifest.json' }
|
||||
const manifest = JSON.parse(await readFileAsync(manifestPath, 'utf8')) as BackupManifest
|
||||
if (manifest?.type !== 'weflow-db-snapshots' || manifest.version !== 1) {
|
||||
emitBackupProgress({ phase: 'failed', message: '不支持的备份包格式' })
|
||||
return { success: false, error: '不支持的备份包格式' }
|
||||
}
|
||||
emitBackupProgress({ phase: 'done', message: '备份包已读取' })
|
||||
return { success: true, manifest }
|
||||
} catch (e) {
|
||||
emitBackupProgress({ phase: 'failed', message: e instanceof Error ? e.message : String(e) })
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) }
|
||||
} finally {
|
||||
if (extractDir) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<SessionInsightTriggerResult> {
|
||||
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<void> {
|
||||
}): Promise<SessionInsightTriggerResult> {
|
||||
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}` }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2080,6 +2080,8 @@ window.addEventListener('scroll',function(){document.getElementById('btt').class
|
||||
const zlib = require('zlib')
|
||||
const urlObj = new URL(url)
|
||||
|
||||
console.log(`[SnsService] 开始下载图片: url=${url.substring(0, 100)}..., key=${key || 'undefined'}`)
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
@@ -2094,7 +2096,9 @@ window.addEventListener('scroll',function(){document.getElementById('btt').class
|
||||
}
|
||||
|
||||
const req = https.request(options, (res: any) => {
|
||||
console.log(`[SnsService] CDN 响应: statusCode=${res.statusCode}, x-enc=${res.headers['x-enc']}, content-type=${res.headers['content-type']}`)
|
||||
if (res.statusCode !== 200 && res.statusCode !== 206) {
|
||||
console.error(`[SnsService] CDN 请求失败: HTTP ${res.statusCode}`)
|
||||
resolve({ success: false, error: `HTTP ${res.statusCode}` })
|
||||
return
|
||||
}
|
||||
@@ -2114,9 +2118,11 @@ window.addEventListener('scroll',function(){document.getElementById('btt').class
|
||||
|
||||
let decoded = raw
|
||||
const rawMagicMime = detectImageMime(raw, '')
|
||||
console.log(`[SnsService] 原始数据: size=${raw.length}, mime=${rawMagicMime}, xEnc=${xEnc}`)
|
||||
|
||||
// 图片逻辑
|
||||
const shouldDecrypt = (xEnc === '1' || !!key) && key !== undefined && key !== null && String(key).trim().length > 0
|
||||
console.log(`[SnsService] 解密判断: shouldDecrypt=${shouldDecrypt}, key=${key || 'undefined'}`)
|
||||
if (shouldDecrypt) {
|
||||
try {
|
||||
const keyStr = String(key).trim()
|
||||
@@ -2132,6 +2138,7 @@ window.addEventListener('scroll',function(){document.getElementById('btt').class
|
||||
}
|
||||
|
||||
const decryptedMagicMime = detectImageMime(decrypted, '')
|
||||
console.log(`[SnsService] 解密后: mime=${decryptedMagicMime}`)
|
||||
if (decryptedMagicMime.startsWith('image/')) {
|
||||
decoded = decrypted
|
||||
} else if (!rawMagicMime.startsWith('image/')) {
|
||||
@@ -2144,7 +2151,9 @@ window.addEventListener('scroll',function(){document.getElementById('btt').class
|
||||
}
|
||||
|
||||
const decodedMagicMime = detectImageMime(decoded, '')
|
||||
console.log(`[SnsService] 最终结果: mime=${decodedMagicMime}, isImage=${decodedMagicMime.startsWith('image/')}`)
|
||||
if (!decodedMagicMime.startsWith('image/')) {
|
||||
console.error(`[SnsService] 图片解密失败: 原始mime=${rawMagicMime}, 解密后mime=${decodedMagicMime}, key=${key}`)
|
||||
resolve({ success: false, error: '图片解密失败:无法识别图片格式' })
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user