mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
fix: 完善 AI 日报详情状态与解析容错
This commit is contained in:
@@ -106,9 +106,7 @@ const enrichAvatarsFromGroup = async (metadata: GroupReportMetadata): Promise<vo
|
||||
const snapshot = getGroupSnapshot(resolved.md5)
|
||||
if (!snapshot) {
|
||||
metadata.warnings = metadata.warnings ?? []
|
||||
metadata.warnings.push(
|
||||
`enrich skipped: group snapshot not available for "${metadata.talker}"`
|
||||
)
|
||||
metadata.warnings.push(`enrich skipped: group snapshot not available for "${metadata.talker}"`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -331,14 +329,30 @@ export const exportGroupReport = async (
|
||||
const baseName = `${sanitizeFileName(request.metadata.groupName)}日报_${request.metadata.reportDate}_可视化长图`
|
||||
const htmlPath = path.join(outputDir, `${baseName}.html`)
|
||||
const pngPath = path.join(outputDir, `${baseName}.png`)
|
||||
const htmlStartedAt = new Date()
|
||||
const html = await renderReportHtml(request)
|
||||
await fs.writeFile(htmlPath, html, 'utf8')
|
||||
const htmlEndedAt = new Date()
|
||||
const pngStartedAt = new Date()
|
||||
const imageDataUrl = await captureFullPage(htmlPath, pngPath)
|
||||
const pngEndedAt = new Date()
|
||||
return {
|
||||
success: true,
|
||||
htmlPath,
|
||||
pngPath,
|
||||
imageDataUrl,
|
||||
exportTimings: {
|
||||
html: {
|
||||
startedAt: htmlStartedAt.toISOString(),
|
||||
endedAt: htmlEndedAt.toISOString(),
|
||||
duration: htmlEndedAt.getTime() - htmlStartedAt.getTime()
|
||||
},
|
||||
png: {
|
||||
startedAt: pngStartedAt.toISOString(),
|
||||
endedAt: pngEndedAt.toISOString(),
|
||||
duration: pngEndedAt.getTime() - pngStartedAt.getTime()
|
||||
}
|
||||
},
|
||||
warnings: request.metadata.warnings?.length ? request.metadata.warnings : undefined
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
+60
-52
@@ -21,15 +21,8 @@ import { DatabaseKeyStore } from './database-key-store'
|
||||
import { KeyServiceMac } from './key-service-mac'
|
||||
import { KeyService as KeyServiceWin } from './key-service-win'
|
||||
import * as chat from './services/chat-service'
|
||||
import {
|
||||
apiServer
|
||||
} from './http-server'
|
||||
import {
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
getSettingsPath,
|
||||
AppSettings
|
||||
} from './services/settings-store'
|
||||
import { apiServer } from './http-server'
|
||||
import { loadSettings, saveSettings, getSettingsPath, AppSettings } from './services/settings-store'
|
||||
import {
|
||||
getBootstrapCache,
|
||||
getCachedMessages,
|
||||
@@ -57,7 +50,8 @@ let tray: Tray | null = null
|
||||
// WCDB's Windows runtime checks the host application name during wcdb_init.
|
||||
// Mirroring WeFlow's name unblocks the -1006 init failure on Windows.
|
||||
app.setName(process.platform === 'win32' ? 'WeFlow' : 'WechatExplorer')
|
||||
let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null = null
|
||||
let dbInitInFlight: Promise<{ success: boolean; monitoring?: boolean; error?: string }> | null =
|
||||
null
|
||||
const BUILD_MARK = 'wechat4-local-http-api-2026-07-03'
|
||||
const TRAY_MODE =
|
||||
process.argv.includes('--tray') || (process.env['WXE_TRAY'] || '').toString() === '1'
|
||||
@@ -69,7 +63,10 @@ function normalizeImageXorKey(value: unknown): string {
|
||||
? Number.parseInt(raw.slice(2), 16)
|
||||
: Number.parseInt(raw, 10)
|
||||
if (!Number.isFinite(parsed)) return raw
|
||||
return `0x${Math.max(0, parsed & 0xff).toString(16).toUpperCase().padStart(2, '0')}`
|
||||
return `0x${Math.max(0, parsed & 0xff)
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
.padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getConfiguredImageKeys(): { xorKey: string; aesKey: string } {
|
||||
@@ -80,7 +77,6 @@ function getConfiguredImageKeys(): { xorKey: string; aesKey: string } {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createWindow(): void {
|
||||
// 鍒涘缓娴忚鍣ㄧ獥鍙?
|
||||
const mainWindow = new BrowserWindow({
|
||||
@@ -145,40 +141,40 @@ app.whenReady().then(async () => {
|
||||
if (dbInitInFlight) return dbInitInFlight
|
||||
|
||||
dbInitInFlight = (async () => {
|
||||
try {
|
||||
const trimmedKey = String(key || '').trim()
|
||||
console.log(`db:init build=${BUILD_MARK} keyLength=${trimmedKey.length}`)
|
||||
const settings = loadSettings()
|
||||
if (
|
||||
chat.isReady() &&
|
||||
chat.getCurrentKey().replace(/^0x/i, '').trim() === trimmedKey.replace(/^0x/i, '') &&
|
||||
(!settings.dbRoot || chat.getCurrentAccountRoot() === settings.dbRoot)
|
||||
) {
|
||||
console.log('[WCDB4] db:init reuse current connection')
|
||||
return { success: true, monitoring: true }
|
||||
}
|
||||
const nextWechatDb = await WechatDb.create(key, settings.dbRoot)
|
||||
const resolvedRoot = nextWechatDb.getWcdb4Client().getAccountRoot()
|
||||
if (resolvedRoot && resolvedRoot !== settings.dbRoot) {
|
||||
saveSettings({ ...settings, dbRoot: resolvedRoot })
|
||||
}
|
||||
chat.setChatDb(nextWechatDb)
|
||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||
voiceService = new VoiceService(wcdb4Client)
|
||||
stickerService = new StickerService(wcdb4Client)
|
||||
const monitoring = wcdb4Client.startMonitor((type, json) => {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json })
|
||||
try {
|
||||
const trimmedKey = String(key || '').trim()
|
||||
console.log(`db:init build=${BUILD_MARK} keyLength=${trimmedKey.length}`)
|
||||
const settings = loadSettings()
|
||||
if (
|
||||
chat.isReady() &&
|
||||
chat.getCurrentKey().replace(/^0x/i, '').trim() === trimmedKey.replace(/^0x/i, '') &&
|
||||
(!settings.dbRoot || chat.getCurrentAccountRoot() === settings.dbRoot)
|
||||
) {
|
||||
console.log('[WCDB4] db:init reuse current connection')
|
||||
return { success: true, monitoring: true }
|
||||
}
|
||||
})
|
||||
imageDecryptService = null
|
||||
return { success: true, monitoring }
|
||||
} catch (error) {
|
||||
console.error('Failed to init DB:', error)
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
} finally {
|
||||
dbInitInFlight = null
|
||||
}
|
||||
const nextWechatDb = await WechatDb.create(key, settings.dbRoot)
|
||||
const resolvedRoot = nextWechatDb.getWcdb4Client().getAccountRoot()
|
||||
if (resolvedRoot && resolvedRoot !== settings.dbRoot) {
|
||||
saveSettings({ ...settings, dbRoot: resolvedRoot })
|
||||
}
|
||||
chat.setChatDb(nextWechatDb)
|
||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||
voiceService = new VoiceService(wcdb4Client)
|
||||
stickerService = new StickerService(wcdb4Client)
|
||||
const monitoring = wcdb4Client.startMonitor((type, json) => {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json })
|
||||
}
|
||||
})
|
||||
imageDecryptService = null
|
||||
return { success: true, monitoring }
|
||||
} catch (error) {
|
||||
console.error('Failed to init DB:', error)
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
} finally {
|
||||
dbInitInFlight = null
|
||||
}
|
||||
})()
|
||||
|
||||
return dbInitInFlight
|
||||
@@ -191,7 +187,9 @@ app.whenReady().then(async () => {
|
||||
return databaseKeyStore.save(clipboardKey)
|
||||
})
|
||||
|
||||
ipcMain.handle('key:saveDbKey', async (_, key: string) => databaseKeyStore.save(String(key || '')))
|
||||
ipcMain.handle('key:saveDbKey', async (_, key: string) =>
|
||||
databaseKeyStore.save(String(key || ''))
|
||||
)
|
||||
|
||||
ipcMain.handle('key:clearSavedDbKey', async () => databaseKeyStore.clear())
|
||||
|
||||
@@ -258,7 +256,9 @@ app.whenReady().then(async () => {
|
||||
|
||||
ipcMain.handle('db:getContacts', (_, filter?: string) => {
|
||||
const accountRoot = chat.getCurrentAccountRoot()
|
||||
const contacts = accountRoot ? mergeCachedContactAvatars(accountRoot, chat.listContacts(filter)) : chat.listContacts(filter)
|
||||
const contacts = accountRoot
|
||||
? mergeCachedContactAvatars(accountRoot, chat.listContacts(filter))
|
||||
: chat.listContacts(filter)
|
||||
if (!filter && chat.isReady() && accountRoot) {
|
||||
saveBootstrapContacts(accountRoot, contacts)
|
||||
}
|
||||
@@ -316,7 +316,18 @@ app.whenReady().then(async () => {
|
||||
messages: messages as any,
|
||||
model: model
|
||||
})
|
||||
return { success: true, data: completion.choices[0].message.content }
|
||||
return {
|
||||
success: true,
|
||||
data: completion.choices[0].message.content,
|
||||
usage: completion.usage
|
||||
? {
|
||||
input: completion.usage.prompt_tokens,
|
||||
output: completion.usage.completion_tokens,
|
||||
total: completion.usage.total_tokens,
|
||||
estimated: false
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('AI API Error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
@@ -441,10 +452,7 @@ app.whenReady().then(async () => {
|
||||
ipcMain.handle('settings:set', (_, patch: Partial<AppSettings>) => {
|
||||
const before = loadSettings()
|
||||
const merged = saveSettings({ ...before, ...patch })
|
||||
if (
|
||||
before.imageXorKey !== merged.imageXorKey ||
|
||||
before.imageAesKey !== merged.imageAesKey
|
||||
) {
|
||||
if (before.imageXorKey !== merged.imageXorKey || before.imageAesKey !== merged.imageAesKey) {
|
||||
imageDecryptService = null
|
||||
}
|
||||
return { settings: merged, settingsPath: getSettingsPath() }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from 'electron'
|
||||
import { app, nativeImage } from 'electron'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import type {
|
||||
@@ -50,6 +50,26 @@ const readPngAsDataUrl = async (filePath?: string): Promise<string | undefined>
|
||||
return `data:image/png;base64,${content.toString('base64')}`
|
||||
}
|
||||
|
||||
const readPngSize = async (
|
||||
filePath?: string
|
||||
): Promise<{ width: number; height: number } | undefined> => {
|
||||
if (!filePath || !(await exists(filePath))) return undefined
|
||||
const image = nativeImage.createFromPath(filePath)
|
||||
if (image.isEmpty()) return undefined
|
||||
const size = image.getSize()
|
||||
return size.width && size.height ? size : undefined
|
||||
}
|
||||
|
||||
const readFileSize = async (filePath?: string): Promise<number | undefined> => {
|
||||
if (!filePath) return undefined
|
||||
try {
|
||||
const stat = await fs.stat(filePath)
|
||||
return stat.isFile() ? stat.size : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeRecord = async (
|
||||
record: GeneratedReportRecord,
|
||||
jsonPath: string
|
||||
@@ -61,7 +81,12 @@ const normalizeRecord = async (
|
||||
jsonPath,
|
||||
htmlStatus,
|
||||
pngStatus,
|
||||
generatedImage: await readPngAsDataUrl(record.pngPath)
|
||||
generatedImage: await readPngAsDataUrl(record.pngPath),
|
||||
imageSize: await readPngSize(record.pngPath),
|
||||
fileSize: {
|
||||
html: await readFileSize(record.htmlPath),
|
||||
png: await readFileSize(record.pngPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +134,7 @@ export async function saveGeneratedReport(
|
||||
): Promise<SaveGeneratedReportResult> {
|
||||
try {
|
||||
const generatedAtDate = new Date(request.generatedAt)
|
||||
const timestamp = Number.isFinite(generatedAtDate.getTime())
|
||||
? generatedAtDate
|
||||
: new Date()
|
||||
const timestamp = Number.isFinite(generatedAtDate.getTime()) ? generatedAtDate : new Date()
|
||||
const year = String(timestamp.getFullYear())
|
||||
const month = pad2(timestamp.getMonth() + 1)
|
||||
const directory = path.join(getReportsRoot(), year, month)
|
||||
@@ -152,7 +175,16 @@ export async function saveGeneratedReport(
|
||||
pngPath: savedPngPath,
|
||||
jsonPath,
|
||||
htmlStatus: savedHtmlPath ? 'ready' : 'missing',
|
||||
pngStatus: savedPngPath ? 'ready' : 'missing'
|
||||
pngStatus: savedPngPath ? 'ready' : 'missing',
|
||||
imageSize: await readPngSize(savedPngPath),
|
||||
duration: request.duration,
|
||||
modelName: request.modelName,
|
||||
tokenUsage: request.tokenUsage,
|
||||
fileSize: {
|
||||
html: await readFileSize(savedHtmlPath),
|
||||
png: await readFileSize(savedPngPath)
|
||||
},
|
||||
generationLogs: request.generationLogs
|
||||
}
|
||||
|
||||
await fs.writeFile(jsonPath, JSON.stringify(record, null, 2), 'utf8')
|
||||
@@ -168,7 +200,9 @@ export async function saveGeneratedReport(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGeneratedReport(reportId: string): Promise<DeleteGeneratedReportResult> {
|
||||
export async function deleteGeneratedReport(
|
||||
reportId: string
|
||||
): Promise<DeleteGeneratedReportResult> {
|
||||
try {
|
||||
const jsonFiles = await walkJsonFiles(getReportsRoot())
|
||||
for (const jsonPath of jsonFiles) {
|
||||
@@ -191,7 +225,10 @@ export async function deleteGeneratedReport(reportId: string): Promise<DeleteGen
|
||||
)
|
||||
return { success: true, deletedId: reportId }
|
||||
} catch (error) {
|
||||
console.warn(`[ReportHistory] skip invalid report record while deleting: ${jsonPath}`, error)
|
||||
console.warn(
|
||||
`[ReportHistory] skip invalid report record while deleting: ${jsonPath}`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
return { success: false, error: '未找到要删除的日报记录' }
|
||||
|
||||
Reference in New Issue
Block a user