Merge pull request #129 from xunchahaha/dev

Dev
This commit is contained in:
xuncha
2026-01-28 20:35:46 +08:00
committed by GitHub
6 changed files with 130 additions and 27 deletions

View File

@@ -26,6 +26,7 @@ interface ConfigSchema {
whisperDownloadSource: string whisperDownloadSource: string
autoTranscribeVoice: boolean autoTranscribeVoice: boolean
transcribeLanguages: string[] transcribeLanguages: string[]
exportDefaultConcurrency: number
} }
export class ConfigService { export class ConfigService {
@@ -54,7 +55,8 @@ export class ConfigService {
whisperModelDir: '', whisperModelDir: '',
whisperDownloadSource: 'tsinghua', whisperDownloadSource: 'tsinghua',
autoTranscribeVoice: false, autoTranscribeVoice: false,
transcribeLanguages: ['zh'] transcribeLanguages: ['zh'],
exportDefaultConcurrency: 2
} }
}) })
} }

View File

@@ -78,6 +78,7 @@ export interface ExportOptions {
txtColumns?: string[] txtColumns?: string[]
sessionLayout?: 'shared' | 'per-session' sessionLayout?: 'shared' | 'per-session'
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
exportConcurrency?: number
} }
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [ const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
@@ -1288,6 +1289,7 @@ class ExportService {
): Promise<{ rows: any[]; memberSet: Map<string, { member: ChatLabMember; avatarUrl?: string }>; firstTime: number | null; lastTime: number | null }> { ): Promise<{ rows: any[]; memberSet: Map<string, { member: ChatLabMember; avatarUrl?: string }>; firstTime: number | null; lastTime: number | null }> {
const rows: any[] = [] const rows: any[] = []
const memberSet = new Map<string, { member: ChatLabMember; avatarUrl?: string }>() const memberSet = new Map<string, { member: ChatLabMember; avatarUrl?: string }>()
const senderSet = new Set<string>()
let firstTime: number | null = null let firstTime: number | null = null
let lastTime: number | null = null let lastTime: number | null = null
@@ -1321,16 +1323,7 @@ class ExportService {
const localId = parseInt(row.local_id || row.localId || '0', 10) const localId = parseInt(row.local_id || row.localId || '0', 10)
const actualSender = isSend ? cleanedMyWxid : (senderUsername || sessionId) const actualSender = isSend ? cleanedMyWxid : (senderUsername || sessionId)
const memberInfo = await this.getContactInfo(actualSender) senderSet.add(actualSender)
if (!memberSet.has(actualSender)) {
memberSet.set(actualSender, {
member: {
platformId: actualSender,
accountName: memberInfo.displayName
},
avatarUrl: memberInfo.avatarUrl
})
}
// 提取媒体相关字段 // 提取媒体相关字段
let imageMd5: string | undefined let imageMd5: string | undefined
@@ -1375,6 +1368,30 @@ class ExportService {
await wcdbService.closeMessageCursor(cursor.cursor) await wcdbService.closeMessageCursor(cursor.cursor)
} }
if (senderSet.size > 0) {
const usernames = Array.from(senderSet)
const [nameResult, avatarResult] = await Promise.all([
wcdbService.getDisplayNames(usernames),
wcdbService.getAvatarUrls(usernames)
])
const nameMap = nameResult.success && nameResult.map ? nameResult.map : {}
const avatarMap = avatarResult.success && avatarResult.map ? avatarResult.map : {}
for (const username of usernames) {
const displayName = nameMap[username] || username
const avatarUrl = avatarMap[username]
memberSet.set(username, {
member: {
platformId: username,
accountName: displayName
},
avatarUrl
})
this.contactCache.set(username, { displayName, avatarUrl })
}
}
return { rows, memberSet, firstTime, lastTime } return { rows, memberSet, firstTime, lastTime }
} }
@@ -1856,6 +1873,16 @@ class ExportService {
const sessionInfo = await this.getContactInfo(sessionId) const sessionInfo = await this.getContactInfo(sessionId)
const myInfo = await this.getContactInfo(cleanedMyWxid) const myInfo = await this.getContactInfo(cleanedMyWxid)
const contactCache = new Map<string, { success: boolean; contact?: any; error?: string }>()
const getContactCached = async (username: string) => {
if (contactCache.has(username)) {
return contactCache.get(username)!
}
const result = await wcdbService.getContact(username)
contactCache.set(username, result)
return result
}
onProgress?.({ onProgress?.({
current: 0, current: 0,
total: 100, total: 100,
@@ -1962,7 +1989,7 @@ class ExportService {
// 获取发送者信息用于名称显示 // 获取发送者信息用于名称显示
const senderWxid = msg.senderUsername const senderWxid = msg.senderUsername
const contact = await wcdbService.getContact(senderWxid) const contact = await getContactCached(senderWxid)
const senderNickname = contact.success && contact.contact?.nickName const senderNickname = contact.success && contact.contact?.nickName
? contact.contact.nickName ? contact.contact.nickName
: (senderInfo.displayName || senderWxid) : (senderInfo.displayName || senderWxid)
@@ -2005,7 +2032,7 @@ class ExportService {
const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup) const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup)
// 获取会话的昵称和备注信息 // 获取会话的昵称和备注信息
const sessionContact = await wcdbService.getContact(sessionId) const sessionContact = await getContactCached(sessionId)
const sessionNickname = sessionContact.success && sessionContact.contact?.nickName const sessionNickname = sessionContact.success && sessionContact.contact?.nickName
? sessionContact.contact.nickName ? sessionContact.contact.nickName
: sessionInfo.displayName : sessionInfo.displayName
@@ -2098,8 +2125,18 @@ class ExportService {
const sessionInfo = await this.getContactInfo(sessionId) const sessionInfo = await this.getContactInfo(sessionId)
const myInfo = await this.getContactInfo(cleanedMyWxid) const myInfo = await this.getContactInfo(cleanedMyWxid)
const contactCache = new Map<string, { success: boolean; contact?: any; error?: string }>()
const getContactCached = async (username: string) => {
if (contactCache.has(username)) {
return contactCache.get(username)!
}
const result = await wcdbService.getContact(username)
contactCache.set(username, result)
return result
}
// 获取会话的备注信息 // 获取会话的备注信息
const sessionContact = await wcdbService.getContact(sessionId) const sessionContact = await getContactCached(sessionId)
const sessionRemark = sessionContact.success && sessionContact.contact?.remark ? sessionContact.contact.remark : '' const sessionRemark = sessionContact.success && sessionContact.contact?.remark ? sessionContact.contact.remark : ''
const sessionNickname = sessionContact.success && sessionContact.contact?.nickName ? sessionContact.contact.nickName : sessionId const sessionNickname = sessionContact.success && sessionContact.contact?.nickName ? sessionContact.contact.nickName : sessionId
@@ -2328,7 +2365,7 @@ class ExportService {
senderWxid = msg.senderUsername senderWxid = msg.senderUsername
// 用 getContact 获取联系人详情,分别取昵称和备注 // 用 getContact 获取联系人详情,分别取昵称和备注
const contactDetail = await wcdbService.getContact(msg.senderUsername) const contactDetail = await getContactCached(msg.senderUsername)
if (contactDetail.success && contactDetail.contact) { if (contactDetail.success && contactDetail.contact) {
// nickName 才是真正的昵称 // nickName 才是真正的昵称
senderNickname = contactDetail.contact.nickName || msg.senderUsername senderNickname = contactDetail.contact.nickName || msg.senderUsername
@@ -2343,7 +2380,7 @@ class ExportService {
} else { } else {
// 单聊对方消息 - 用 getContact 获取联系人详情 // 单聊对方消息 - 用 getContact 获取联系人详情
senderWxid = sessionId senderWxid = sessionId
const contactDetail = await wcdbService.getContact(sessionId) const contactDetail = await getContactCached(sessionId)
if (contactDetail.success && contactDetail.contact) { if (contactDetail.success && contactDetail.contact) {
senderNickname = contactDetail.contact.nickName || sessionId senderNickname = contactDetail.contact.nickName || sessionId
senderRemark = contactDetail.contact.remark || '' senderRemark = contactDetail.contact.remark || ''
@@ -2567,7 +2604,7 @@ class ExportService {
senderNickname = myInfo.displayName || cleanedMyWxid senderNickname = myInfo.displayName || cleanedMyWxid
} else if (isGroup && msg.senderUsername) { } else if (isGroup && msg.senderUsername) {
senderWxid = msg.senderUsername senderWxid = msg.senderUsername
const contactDetail = await wcdbService.getContact(msg.senderUsername) const contactDetail = await getContactCached(msg.senderUsername)
if (contactDetail.success && contactDetail.contact) { if (contactDetail.success && contactDetail.contact) {
senderNickname = contactDetail.contact.nickName || msg.senderUsername senderNickname = contactDetail.contact.nickName || msg.senderUsername
senderRemark = contactDetail.contact.remark || '' senderRemark = contactDetail.contact.remark || ''
@@ -2578,7 +2615,7 @@ class ExportService {
} }
} else { } else {
senderWxid = sessionId senderWxid = sessionId
const contactDetail = await wcdbService.getContact(sessionId) const contactDetail = await getContactCached(sessionId)
if (contactDetail.success && contactDetail.contact) { if (contactDetail.success && contactDetail.contact) {
senderNickname = contactDetail.contact.nickName || sessionId senderNickname = contactDetail.contact.nickName || sessionId
senderRemark = contactDetail.contact.remark || '' senderRemark = contactDetail.contact.remark || ''
@@ -3005,13 +3042,20 @@ class ExportService {
const sessionLayout = exportMediaEnabled const sessionLayout = exportMediaEnabled
? (options.sessionLayout ?? 'per-session') ? (options.sessionLayout ?? 'per-session')
: 'shared' : 'shared'
let completedCount = 0
const rawConcurrency = typeof options.exportConcurrency === 'number'
? Math.floor(options.exportConcurrency)
: 2
const clampedConcurrency = Math.max(1, Math.min(rawConcurrency, 6))
const sessionConcurrency = (exportMediaEnabled && sessionLayout === 'shared')
? 1
: clampedConcurrency
for (let i = 0; i < sessionIds.length; i++) { await parallelLimit(sessionIds, sessionConcurrency, async (sessionId) => {
const sessionId = sessionIds[i]
const sessionInfo = await this.getContactInfo(sessionId) const sessionInfo = await this.getContactInfo(sessionId)
onProgress?.({ onProgress?.({
current: i + 1, current: completedCount,
total: sessionIds.length, total: sessionIds.length,
currentSession: sessionInfo.displayName, currentSession: sessionInfo.displayName,
phase: 'exporting' phase: 'exporting'
@@ -3053,7 +3097,15 @@ class ExportService {
failCount++ failCount++
console.error(`导出 ${sessionId} 失败:`, result.error) console.error(`导出 ${sessionId} 失败:`, result.error)
} }
}
completedCount++
onProgress?.({
current: completedCount,
total: sessionIds.length,
currentSession: sessionInfo.displayName,
phase: 'exporting'
})
})
onProgress?.({ onProgress?.({
current: sessionIds.length, current: sessionIds.length,

View File

@@ -24,6 +24,7 @@ interface ExportOptions {
excelCompactColumns: boolean excelCompactColumns: boolean
txtColumns: string[] txtColumns: string[]
displayNamePreference: 'group-nickname' | 'remark' | 'nickname' displayNamePreference: 'group-nickname' | 'remark' | 'nickname'
exportConcurrency: number
} }
interface ExportResult { interface ExportResult {
@@ -68,7 +69,8 @@ function ExportPage() {
exportVoiceAsText: true, exportVoiceAsText: true,
excelCompactColumns: true, excelCompactColumns: true,
txtColumns: defaultTxtColumns, txtColumns: defaultTxtColumns,
displayNamePreference: 'remark' displayNamePreference: 'remark',
exportConcurrency: 2
}) })
const buildDateRangeFromPreset = (preset: string) => { const buildDateRangeFromPreset = (preset: string) => {
@@ -133,14 +135,16 @@ function ExportPage() {
savedMedia, savedMedia,
savedVoiceAsText, savedVoiceAsText,
savedExcelCompactColumns, savedExcelCompactColumns,
savedTxtColumns savedTxtColumns,
savedConcurrency
] = await Promise.all([ ] = await Promise.all([
configService.getExportDefaultFormat(), configService.getExportDefaultFormat(),
configService.getExportDefaultDateRange(), configService.getExportDefaultDateRange(),
configService.getExportDefaultMedia(), configService.getExportDefaultMedia(),
configService.getExportDefaultVoiceAsText(), configService.getExportDefaultVoiceAsText(),
configService.getExportDefaultExcelCompactColumns(), configService.getExportDefaultExcelCompactColumns(),
configService.getExportDefaultTxtColumns() configService.getExportDefaultTxtColumns(),
configService.getExportDefaultConcurrency()
]) ])
const preset = savedRange || 'today' const preset = savedRange || 'today'
@@ -155,7 +159,8 @@ function ExportPage() {
exportMedia: savedMedia ?? false, exportMedia: savedMedia ?? false,
exportVoiceAsText: savedVoiceAsText ?? true, exportVoiceAsText: savedVoiceAsText ?? true,
excelCompactColumns: savedExcelCompactColumns ?? true, excelCompactColumns: savedExcelCompactColumns ?? true,
txtColumns txtColumns,
exportConcurrency: savedConcurrency ?? 2
})) }))
} catch (e) { } catch (e) {
console.error('加载导出默认设置失败:', e) console.error('加载导出默认设置失败:', e)
@@ -286,6 +291,7 @@ function ExportPage() {
excelCompactColumns: options.excelCompactColumns, excelCompactColumns: options.excelCompactColumns,
txtColumns: options.txtColumns, txtColumns: options.txtColumns,
displayNamePreference: options.displayNamePreference, displayNamePreference: options.displayNamePreference,
exportConcurrency: options.exportConcurrency,
sessionLayout, sessionLayout,
dateRange: options.useAllTime ? null : options.dateRange ? { dateRange: options.useAllTime ? null : options.dateRange ? {
start: Math.floor(options.dateRange.start.getTime() / 1000), start: Math.floor(options.dateRange.start.getTime() / 1000),

View File

@@ -62,6 +62,7 @@ function SettingsPage() {
const [exportDefaultMedia, setExportDefaultMedia] = useState(false) const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true)
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
const [isLoading, setIsLoadingState] = useState(false) const [isLoading, setIsLoadingState] = useState(false)
const [isTesting, setIsTesting] = useState(false) const [isTesting, setIsTesting] = useState(false)
@@ -139,6 +140,7 @@ function SettingsPage() {
const savedExportDefaultMedia = await configService.getExportDefaultMedia() const savedExportDefaultMedia = await configService.getExportDefaultMedia()
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
const savedExportDefaultConcurrency = await configService.getExportDefaultConcurrency()
if (savedPath) setDbPath(savedPath) if (savedPath) setDbPath(savedPath)
if (savedWxid) setWxid(savedWxid) if (savedWxid) setWxid(savedWxid)
@@ -166,6 +168,7 @@ function SettingsPage() {
setExportDefaultMedia(savedExportDefaultMedia ?? false) setExportDefaultMedia(savedExportDefaultMedia ?? false)
setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true)
setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true)
setExportDefaultConcurrency(savedExportDefaultConcurrency ?? 2)
// 如果语言列表为空,保存默认值 // 如果语言列表为空,保存默认值
if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) {
@@ -1113,6 +1116,32 @@ function SettingsPage() {
</div> </div>
</div> </div>
<div className="form-group">
<label>{'\u9ed8\u8ba4\u5bfc\u51fa\u5e76\u53d1\u6570'}</label>
<span className="form-hint">{'\u540c\u65f6\u5bfc\u51fa\u7684\u4f1a\u8bdd\u6570\u91cf\uff0c\u5efa\u8bae 1-3'}</span>
<input
type="number"
min={1}
max={6}
value={exportDefaultConcurrency}
onChange={(e) => {
const value = Number(e.target.value)
if (Number.isNaN(value)) {
setExportDefaultConcurrency(1)
return
}
setExportDefaultConcurrency(value)
}}
onBlur={async () => {
const clamped = Math.max(1, Math.min(Math.floor(exportDefaultConcurrency || 1), 6))
setExportDefaultConcurrency(clamped)
await configService.setExportDefaultConcurrency(clamped)
showMessage(`\u5df2\u66f4\u65b0\u5bfc\u51fa\u5e76\u53d1\u6570\u4e3a ${clamped}`, true)
}}
/>
</div>
<div className="form-group"> <div className="form-group">
<label>Excel </label> <label>Excel </label>
<span className="form-hint"> Excel </span> <span className="form-hint"> Excel </span>

View File

@@ -29,7 +29,8 @@ export const CONFIG_KEYS = {
EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia', EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia',
EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText', EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText',
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns', EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns' EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency'
} as const } as const
export interface WxidConfig { export interface WxidConfig {
@@ -352,3 +353,15 @@ export async function getExportDefaultTxtColumns(): Promise<string[] | null> {
export async function setExportDefaultTxtColumns(columns: string[]): Promise<void> { export async function setExportDefaultTxtColumns(columns: string[]): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns) await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns)
} }
// 获取导出默认并发数
export async function getExportDefaultConcurrency(): Promise<number | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY)
if (typeof value === 'number' && Number.isFinite(value)) return value
return null
}
// 设置导出默认并发数
export async function setExportDefaultConcurrency(concurrency: number): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
}

View File

@@ -367,6 +367,7 @@ export interface ExportOptions {
txtColumns?: string[] txtColumns?: string[]
sessionLayout?: 'shared' | 'per-session' sessionLayout?: 'shared' | 'per-session'
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
exportConcurrency?: number
} }
export interface ExportProgress { export interface ExportProgress {