fix: 修复了一些情况下无法触发语音转文字的功能

Fix exporting voice-to-text for JSON and Excel exports
This commit is contained in:
xuncha
2026-01-20 02:13:40 +08:00
committed by GitHub
5 changed files with 299 additions and 26 deletions

View File

@@ -71,6 +71,7 @@ export interface ExportOptions {
exportVoices?: boolean exportVoices?: boolean
exportEmojis?: boolean exportEmojis?: boolean
exportVoiceAsText?: boolean exportVoiceAsText?: boolean
excelCompactColumns?: boolean
} }
interface MediaExportItem { interface MediaExportItem {
@@ -1250,13 +1251,18 @@ class ExportService {
const sourceMatch = /<msgsource>[\s\S]*?<\/msgsource>/i.exec(msg.content || '') const sourceMatch = /<msgsource>[\s\S]*?<\/msgsource>/i.exec(msg.content || '')
const source = sourceMatch ? sourceMatch[0] : '' const source = sourceMatch ? sourceMatch[0] : ''
let content = this.parseMessageContent(msg.content, msg.localType)
if (msg.localType === 34 && options.exportVoiceAsText) {
content = await this.transcribeVoice(sessionId, String(msg.localId))
}
allMessages.push({ allMessages.push({
localId: allMessages.length + 1, localId: allMessages.length + 1,
createTime: msg.createTime, createTime: msg.createTime,
formattedTime: this.formatTimestamp(msg.createTime), formattedTime: this.formatTimestamp(msg.createTime),
type: this.getMessageTypeName(msg.localType), type: this.getMessageTypeName(msg.localType),
localType: msg.localType, localType: msg.localType,
content: this.parseMessageContent(msg.content, msg.localType), content,
isSend: msg.isSend ? 1 : 0, isSend: msg.isSend ? 1 : 0,
senderUsername: msg.senderUsername, senderUsername: msg.senderUsername,
senderDisplayName: senderInfo.displayName, senderDisplayName: senderInfo.displayName,
@@ -1379,8 +1385,9 @@ class ExportService {
let currentRow = 1 let currentRow = 1
const useCompactColumns = options.excelCompactColumns === true
// 第一行:会话信息标题 // 第一行:会话信息标题
worksheet.mergeCells(currentRow, 1, currentRow, 8)
const titleCell = worksheet.getCell(currentRow, 1) const titleCell = worksheet.getCell(currentRow, 1)
titleCell.value = '会话信息' titleCell.value = '会话信息'
titleCell.font = { name: 'Calibri', bold: true, size: 11 } titleCell.font = { name: 'Calibri', bold: true, size: 11 }
@@ -1436,7 +1443,9 @@ class ExportService {
currentRow++ currentRow++
// 表头行 // 表头行
const headers = ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容'] const headers = useCompactColumns
? ['序号', '时间', '发送者身份', '消息类型', '内容']
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容']
const headerRow = worksheet.getRow(currentRow) const headerRow = worksheet.getRow(currentRow)
headerRow.height = 22 headerRow.height = 22
@@ -1456,12 +1465,18 @@ class ExportService {
// 设置列宽 // 设置列宽
worksheet.getColumn(1).width = 8 // 序号 worksheet.getColumn(1).width = 8 // 序号
worksheet.getColumn(2).width = 20 // 时间 worksheet.getColumn(2).width = 20 // 时间
if (useCompactColumns) {
worksheet.getColumn(3).width = 18 // 发送者身份
worksheet.getColumn(4).width = 12 // 消息类型
worksheet.getColumn(5).width = 50 // 内容
} else {
worksheet.getColumn(3).width = 18 // 发送者昵称 worksheet.getColumn(3).width = 18 // 发送者昵称
worksheet.getColumn(4).width = 25 // 发送者微信ID worksheet.getColumn(4).width = 25 // 发送者微信ID
worksheet.getColumn(5).width = 18 // 发送者备注 worksheet.getColumn(5).width = 18 // 发送者备注
worksheet.getColumn(6).width = 15 // 发送者身份 worksheet.getColumn(6).width = 15 // 发送者身份
worksheet.getColumn(7).width = 12 // 消息类型 worksheet.getColumn(7).width = 12 // 消息类型
worksheet.getColumn(8).width = 50 // 内容 worksheet.getColumn(8).width = 50 // 内容
}
// 填充数据 // 填充数据
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime) const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
@@ -1541,9 +1556,12 @@ class ExportService {
row.height = 24 row.height = 24
// 确定内容:如果有媒体文件导出成功则显示相对路径,否则显示解析后的内容 // 确定内容:如果有媒体文件导出成功则显示相对路径,否则显示解析后的内容
const contentValue = mediaItem let contentValue = mediaItem
? mediaItem.relativePath ? mediaItem.relativePath
: (this.parseMessageContent(msg.content, msg.localType) || '') : (this.parseMessageContent(msg.content, msg.localType) || '')
if (!mediaItem && msg.localType === 34 && options.exportVoiceAsText) {
contentValue = await this.transcribeVoice(sessionId, String(msg.localId))
}
// 调试日志 // 调试日志
if (msg.localType === 3 || msg.localType === 47) { if (msg.localType === 3 || msg.localType === 47) {
@@ -1551,15 +1569,22 @@ class ExportService {
worksheet.getCell(currentRow, 1).value = i + 1 worksheet.getCell(currentRow, 1).value = i + 1
worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime) worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime)
if (useCompactColumns) {
worksheet.getCell(currentRow, 3).value = senderRole
worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType)
worksheet.getCell(currentRow, 5).value = contentValue
} else {
worksheet.getCell(currentRow, 3).value = senderNickname worksheet.getCell(currentRow, 3).value = senderNickname
worksheet.getCell(currentRow, 4).value = senderWxid worksheet.getCell(currentRow, 4).value = senderWxid
worksheet.getCell(currentRow, 5).value = senderRemark worksheet.getCell(currentRow, 5).value = senderRemark
worksheet.getCell(currentRow, 6).value = senderRole worksheet.getCell(currentRow, 6).value = senderRole
worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType)
worksheet.getCell(currentRow, 8).value = contentValue worksheet.getCell(currentRow, 8).value = contentValue
}
// 设置每个单元格的样式 // 设置每个单元格的样式
for (let col = 1; col <= 8; col++) { const maxColumns = useCompactColumns ? 5 : 8
for (let col = 1; col <= maxColumns; col++) {
const cell = worksheet.getCell(currentRow, col) const cell = worksheet.getCell(currentRow, col)
cell.font = { name: 'Calibri', size: 11 } cell.font = { name: 'Calibri', size: 11 }
cell.alignment = { vertical: 'middle', wrapText: false } cell.alignment = { vertical: 'middle', wrapText: false }
@@ -1689,4 +1714,3 @@ class ExportService {
} }
export const exportService = new ExportService() export const exportService = new ExportService()

View File

@@ -21,6 +21,7 @@ interface ExportOptions {
exportVoices: boolean exportVoices: boolean
exportEmojis: boolean exportEmojis: boolean
exportVoiceAsText: boolean exportVoiceAsText: boolean
excelCompactColumns: boolean
} }
interface ExportResult { interface ExportResult {
@@ -45,20 +46,40 @@ function ExportPage() {
const [selectingStart, setSelectingStart] = useState(true) const [selectingStart, setSelectingStart] = useState(true)
const [options, setOptions] = useState<ExportOptions>({ const [options, setOptions] = useState<ExportOptions>({
format: 'chatlab', format: 'excel',
dateRange: { dateRange: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), start: new Date(new Date().setHours(0, 0, 0, 0)),
end: new Date() end: new Date()
}, },
useAllTime: true, useAllTime: false,
exportAvatars: true, exportAvatars: true,
exportMedia: false, exportMedia: false,
exportImages: true, exportImages: true,
exportVoices: true, exportVoices: true,
exportEmojis: true, exportEmojis: true,
exportVoiceAsText: false exportVoiceAsText: true,
excelCompactColumns: true
}) })
const buildDateRangeFromPreset = (preset: string) => {
const now = new Date()
if (preset === 'all') {
return { useAllTime: true, dateRange: { start: now, end: now } }
}
let rangeMs = 0
if (preset === '7d') rangeMs = 7 * 24 * 60 * 60 * 1000
if (preset === '30d') rangeMs = 30 * 24 * 60 * 60 * 1000
if (preset === '90d') rangeMs = 90 * 24 * 60 * 60 * 1000
if (preset === 'today' || rangeMs === 0) {
const start = new Date(now)
start.setHours(0, 0, 0, 0)
return { useAllTime: false, dateRange: { start, end: now } }
}
const start = new Date(now.getTime() - rangeMs)
start.setHours(0, 0, 0, 0)
return { useAllTime: false, dateRange: { start, end: now } }
}
const loadSessions = useCallback(async () => { const loadSessions = useCallback(async () => {
setIsLoading(true) setIsLoading(true)
try { try {
@@ -94,10 +115,44 @@ function ExportPage() {
} }
}, []) }, [])
const loadExportDefaults = useCallback(async () => {
try {
const [
savedFormat,
savedRange,
savedMedia,
savedVoiceAsText,
savedExcelCompactColumns
] = await Promise.all([
configService.getExportDefaultFormat(),
configService.getExportDefaultDateRange(),
configService.getExportDefaultMedia(),
configService.getExportDefaultVoiceAsText(),
configService.getExportDefaultExcelCompactColumns()
])
const preset = savedRange || 'today'
const rangeDefaults = buildDateRangeFromPreset(preset)
setOptions((prev) => ({
...prev,
format: (savedFormat as ExportOptions['format']) || 'excel',
useAllTime: rangeDefaults.useAllTime,
dateRange: rangeDefaults.dateRange,
exportMedia: savedMedia ?? false,
exportVoiceAsText: savedVoiceAsText ?? true,
excelCompactColumns: savedExcelCompactColumns ?? true
}))
} catch (e) {
console.error('加载导出默认设置失败:', e)
}
}, [])
useEffect(() => { useEffect(() => {
loadSessions() loadSessions()
loadExportPath() loadExportPath()
}, [loadSessions, loadExportPath]) loadExportDefaults()
}, [loadSessions, loadExportPath, loadExportDefaults])
useEffect(() => { useEffect(() => {
if (!searchKeyword.trim()) { if (!searchKeyword.trim()) {
@@ -161,6 +216,7 @@ function ExportPage() {
exportVoices: options.exportMedia && options.exportVoices, exportVoices: options.exportMedia && options.exportVoices,
exportEmojis: options.exportMedia && options.exportEmojis, exportEmojis: options.exportMedia && options.exportEmojis,
exportVoiceAsText: options.exportVoiceAsText, // 独立于 exportMedia exportVoiceAsText: options.exportVoiceAsText, // 独立于 exportMedia
excelCompactColumns: options.excelCompactColumns,
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),
// 将结束日期设置为当天的 23:59:59,以包含当天的所有消息 // 将结束日期设置为当天的 23:59:59,以包含当天的所有消息

View File

@@ -11,12 +11,13 @@ import {
} from 'lucide-react' } from 'lucide-react'
import './SettingsPage.scss' import './SettingsPage.scss'
type SettingsTab = 'appearance' | 'database' | 'whisper' | 'cache' | 'about' type SettingsTab = 'appearance' | 'database' | 'whisper' | 'export' | 'cache' | 'about'
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'appearance', label: '外观', icon: Palette }, { id: 'appearance', label: '外观', icon: Palette },
{ id: 'database', label: '数据库连接', icon: Database }, { id: 'database', label: '数据库连接', icon: Database },
{ id: 'whisper', label: '语音识别模型', icon: Mic }, { id: 'whisper', label: '语音识别模型', icon: Mic },
{ id: 'export', label: '导出', icon: Download },
{ id: 'cache', label: '缓存', icon: HardDrive }, { id: 'cache', label: '缓存', icon: HardDrive },
{ id: 'about', label: '关于', icon: Info } { id: 'about', label: '关于', icon: Info }
] ]
@@ -49,6 +50,11 @@ function SettingsPage() {
const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null) const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null)
const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false) const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false)
const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh']) const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh'])
const [exportDefaultFormat, setExportDefaultFormat] = useState('excel')
const [exportDefaultDateRange, setExportDefaultDateRange] = useState('today')
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true)
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
const [isLoading, setIsLoadingState] = useState(false) const [isLoading, setIsLoadingState] = useState(false)
const [isTesting, setIsTesting] = useState(false) const [isTesting, setIsTesting] = useState(false)
@@ -114,6 +120,11 @@ function SettingsPage() {
const savedWhisperModelDir = await configService.getWhisperModelDir() const savedWhisperModelDir = await configService.getWhisperModelDir()
const savedAutoTranscribe = await configService.getAutoTranscribeVoice() const savedAutoTranscribe = await configService.getAutoTranscribeVoice()
const savedTranscribeLanguages = await configService.getTranscribeLanguages() const savedTranscribeLanguages = await configService.getTranscribeLanguages()
const savedExportDefaultFormat = await configService.getExportDefaultFormat()
const savedExportDefaultDateRange = await configService.getExportDefaultDateRange()
const savedExportDefaultMedia = await configService.getExportDefaultMedia()
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
if (savedKey) setDecryptKey(savedKey) if (savedKey) setDecryptKey(savedKey)
if (savedPath) setDbPath(savedPath) if (savedPath) setDbPath(savedPath)
@@ -126,6 +137,11 @@ function SettingsPage() {
setLogEnabled(savedLogEnabled) setLogEnabled(savedLogEnabled)
setAutoTranscribeVoice(savedAutoTranscribe) setAutoTranscribeVoice(savedAutoTranscribe)
setTranscribeLanguages(savedTranscribeLanguages) setTranscribeLanguages(savedTranscribeLanguages)
setExportDefaultFormat(savedExportDefaultFormat || 'excel')
setExportDefaultDateRange(savedExportDefaultDateRange || 'today')
setExportDefaultMedia(savedExportDefaultMedia ?? false)
setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true)
setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true)
// 如果语言列表为空,保存默认值 // 如果语言列表为空,保存默认值
if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) {
@@ -853,6 +869,115 @@ function SettingsPage() {
</div> </div>
</div> </div>
) )
const renderExportTab = () => (
<div className="tab-content">
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<select
value={exportDefaultFormat}
onChange={async (e) => {
const value = e.target.value
setExportDefaultFormat(value)
await configService.setExportDefaultFormat(value)
showMessage('已更新导出格式默认值', true)
}}
>
<option value="excel">Excel</option>
<option value="chatlab">ChatLab</option>
<option value="chatlab-jsonl">ChatLab JSONL</option>
<option value="json">JSON</option>
<option value="html">HTML</option>
<option value="txt">TXT</option>
<option value="sql">PostgreSQL</option>
</select>
</div>
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<select
value={exportDefaultDateRange}
onChange={async (e) => {
const value = e.target.value
setExportDefaultDateRange(value)
await configService.setExportDefaultDateRange(value)
showMessage('已更新默认导出时间范围', true)
}}
>
<option value="today"></option>
<option value="7d">7</option>
<option value="30d">30</option>
<option value="90d">90</option>
<option value="all"></option>
</select>
</div>
<div className="form-group">
<label></label>
<span className="form-hint">//</span>
<div className="log-toggle-line">
<span className="log-status">{exportDefaultMedia ? '已开启' : '已关闭'}</span>
<label className="switch" htmlFor="export-default-media">
<input
id="export-default-media"
className="switch-input"
type="checkbox"
checked={exportDefaultMedia}
onChange={async (e) => {
const enabled = e.target.checked
setExportDefaultMedia(enabled)
await configService.setExportDefaultMedia(enabled)
showMessage(enabled ? '已开启默认媒体导出' : '已关闭默认媒体导出', true)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<div className="log-toggle-line">
<span className="log-status">{exportDefaultVoiceAsText ? '已开启' : '已关闭'}</span>
<label className="switch" htmlFor="export-default-voice-as-text">
<input
id="export-default-voice-as-text"
className="switch-input"
type="checkbox"
checked={exportDefaultVoiceAsText}
onChange={async (e) => {
const enabled = e.target.checked
setExportDefaultVoiceAsText(enabled)
await configService.setExportDefaultVoiceAsText(enabled)
showMessage(enabled ? '已开启默认语音转文字' : '已关闭默认语音转文字', true)
}}
/>
<span className="switch-slider" />
</label>
</div>
</div>
<div className="form-group">
<label>Excel </label>
<span className="form-hint"> Excel </span>
<select
value={exportDefaultExcelCompactColumns ? 'compact' : 'full'}
onChange={async (e) => {
const compact = e.target.value === 'compact'
setExportDefaultExcelCompactColumns(compact)
await configService.setExportDefaultExcelCompactColumns(compact)
showMessage(compact ? '已启用精简列' : '已启用完整列', true)
}}
>
<option value="compact"></option>
<option value="full">/ID/</option>
</select>
</div>
</div>
)
const renderCacheTab = () => ( const renderCacheTab = () => (
<div className="tab-content"> <div className="tab-content">
<p className="section-desc"></p> <p className="section-desc"></p>
@@ -992,6 +1117,7 @@ function SettingsPage() {
{activeTab === 'appearance' && renderAppearanceTab()} {activeTab === 'appearance' && renderAppearanceTab()}
{activeTab === 'database' && renderDatabaseTab()} {activeTab === 'database' && renderDatabaseTab()}
{activeTab === 'whisper' && renderWhisperTab()} {activeTab === 'whisper' && renderWhisperTab()}
{activeTab === 'export' && renderExportTab()}
{activeTab === 'cache' && renderCacheTab()} {activeTab === 'cache' && renderCacheTab()}
{activeTab === 'about' && renderAboutTab()} {activeTab === 'about' && renderAboutTab()}
</div> </div>
@@ -1001,4 +1127,3 @@ function SettingsPage() {
export default SettingsPage export default SettingsPage

View File

@@ -22,7 +22,12 @@ export const CONFIG_KEYS = {
WHISPER_MODEL_DIR: 'whisperModelDir', WHISPER_MODEL_DIR: 'whisperModelDir',
WHISPER_DOWNLOAD_SOURCE: 'whisperDownloadSource', WHISPER_DOWNLOAD_SOURCE: 'whisperDownloadSource',
AUTO_TRANSCRIBE_VOICE: 'autoTranscribeVoice', AUTO_TRANSCRIBE_VOICE: 'autoTranscribeVoice',
TRANSCRIBE_LANGUAGES: 'transcribeLanguages' TRANSCRIBE_LANGUAGES: 'transcribeLanguages',
EXPORT_DEFAULT_FORMAT: 'exportDefaultFormat',
EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange',
EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia',
EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText',
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns'
} as const } as const
// 获取解密密钥 // 获取解密密钥
@@ -243,3 +248,61 @@ export async function getTranscribeLanguages(): Promise<string[]> {
export async function setTranscribeLanguages(languages: string[]): Promise<void> { export async function setTranscribeLanguages(languages: string[]): Promise<void> {
await config.set(CONFIG_KEYS.TRANSCRIBE_LANGUAGES, languages) await config.set(CONFIG_KEYS.TRANSCRIBE_LANGUAGES, languages)
} }
// 获取导出默认格式
export async function getExportDefaultFormat(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT)
return (value as string) || null
}
// 设置导出默认格式
export async function setExportDefaultFormat(format: string): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT, format)
}
// 获取导出默认时间范围
export async function getExportDefaultDateRange(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE)
return (value as string) || null
}
// 设置导出默认时间范围
export async function setExportDefaultDateRange(range: string): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range)
}
// 获取导出默认媒体设置
export async function getExportDefaultMedia(): Promise<boolean | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA)
if (typeof value === 'boolean') return value
return null
}
// 设置导出默认媒体设置
export async function setExportDefaultMedia(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA, enabled)
}
// 获取导出默认语音转文字
export async function getExportDefaultVoiceAsText(): Promise<boolean | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT)
if (typeof value === 'boolean') return value
return null
}
// 设置导出默认语音转文字
export async function setExportDefaultVoiceAsText(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT, enabled)
}
// 获取导出默认 Excel 列模式
export async function getExportDefaultExcelCompactColumns(): Promise<boolean | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS)
if (typeof value === 'boolean') return value
return null
}
// 设置导出默认 Excel 列模式
export async function setExportDefaultExcelCompactColumns(enabled: boolean): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled)
}

View File

@@ -327,6 +327,11 @@ export interface ExportOptions {
dateRange?: { start: number; end: number } | null dateRange?: { start: number; end: number } | null
exportMedia?: boolean exportMedia?: boolean
exportAvatars?: boolean exportAvatars?: boolean
exportImages?: boolean
exportVoices?: boolean
exportEmojis?: boolean
exportVoiceAsText?: boolean
excelCompactColumns?: boolean
} }
export interface WxidInfo { export interface WxidInfo {