fix: 修复 issue #92 中的 4 个 bug

- fix(MomentsWindow): 修复朋友圈刷新崩溃问题
  - 新增 postsRef 追踪最新 posts 状态,避免触发重渲染
  - loadPosts 中改用 postsRef.current 读取数据
  - 从 useCallback 依赖数组移除 posts,断开依赖循环

- fix(exportService): 修复群聊导出数据串问题
  - 改进 findMessageTable 哈希匹配为大小写无关的精确匹配
  - 匹配失败返回 null,不再回退到第一个表

- fix(exportService): 修复导出 HTML 实体转义乱码问题
  - 增强 decodeHtmlEntities,支持   
 等常见实体
  - 对 content、senderDisplayName、formattedTime 字段解码

- fix(exportService): 修复红包/群收款显示原始 XML 问题
  - parseChatHistory 中过滤 datatype=2001 和 2002 的消息

- fix(htmlExportGenerator): 新增浏览器端二次解码兜底

关闭 #92
This commit is contained in:
ILoveBinglu
2026-03-18 11:27:11 +08:00
parent 7142eddef0
commit a4bb3b8637
3 changed files with 54 additions and 9 deletions

View File

@@ -301,13 +301,24 @@ class ExportService {
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'"
).all() as any[]
const hash = this.getTableNameHash(sessionId)
const hash = this.getTableNameHash(sessionId).toLowerCase()
// 1. 精确哈希提取匹配(大小写无关):从表名中提取 32 位 hex 片段后比对
for (const table of tables) {
if ((table.name as string).includes(hash)) {
return table.name
const name = table.name as string
const hexMatch = name.match(/[0-9a-fA-F]{32}/)
if (hexMatch && hexMatch[0].toLowerCase() === hash) {
return name
}
}
// 2. 包含匹配(大小写无关)
for (const table of tables) {
const name = table.name as string
if (name.toLowerCase().includes(hash)) {
return name
}
}
} catch { }
// 匹配失败时返回 null不回退到第一个表避免数据串
return null
}
@@ -1137,6 +1148,9 @@ class ExportService {
const fileext = this.extractXmlValue(body, 'fileext')
const datasize = parseInt(this.extractXmlValue(body, 'datasize') || '0')
// 过滤红包2001和群收款2002消息不在导出中显示
if (datatype === 2001 || datatype === 2002) continue
items.push({
datatype,
sourcename,
@@ -1168,6 +1182,15 @@ class ExportService {
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
// 常见十六进制数字实体
.replace(/ /gi, ' ')
.replace(/
/gi, '\n')
.replace(/	/gi, '\t')
.replace(/
/gi, '\r')
// 通用十六进制实体 &#xHH;
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
// 通用十进制实体 &#NN;
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)))
}
/**
@@ -1230,11 +1253,18 @@ class ExportService {
content = record.datadesc || record.datatitle || '[消息]'
}
// 对关键字段进行 HTML 实体解码,防止导出时出现   等转义字符
content = this.decodeHtmlEntities(content)
const senderDisplayName = this.decodeHtmlEntities(record.sourcename || 'unknown')
const formattedTime = this.decodeHtmlEntities(
timestamp > 0 ? this.formatTimestamp(timestamp) : (record.sourcetime || '')
)
const chatRecord: any = {
sender: record.sourcename || 'unknown',
senderDisplayName: record.sourcename || 'unknown',
senderDisplayName,
timestamp,
formattedTime: timestamp > 0 ? this.formatTimestamp(timestamp) : record.sourcetime,
formattedTime,
type: typeName,
datatype: record.datatype,
content

View File

@@ -878,10 +878,19 @@ body {
return 'c' + (Math.abs(hash) % 8);
}
// HTML 实体解码(防止导出数据中残留   等转义字符)
function decodeEntities(text) {
if (!text) return '';
const d = document.createElement('textarea');
d.innerHTML = text;
return d.value;
}
// HTML 转义
function esc(text) {
const decoded = decodeEntities(String(text || ''));
const d = document.createElement('div');
d.textContent = text;
d.textContent = decoded;
return d.innerHTML;
}

View File

@@ -747,6 +747,12 @@ function MomentsWindow() {
const [isLoading, setIsLoading] = useState(true)
const [loadingNewer, setLoadingNewer] = useState(false)
const [posts, setPosts] = useState<SnsPost[]>([])
const postsRef = useRef<SnsPost[]>([])
// 同步 postsRef 与 posts state
useEffect(() => {
postsRef.current = posts
}, [posts])
const [deletedPostIds, setDeletedPostIds] = useState<Set<string>>(new Set())
const [error, setError] = useState<string | null>(null)
@@ -877,7 +883,7 @@ function MomentsWindow() {
}
currentOffset = 0
} else if (direction === 'newer') {
const topPost = posts[0]
const topPost = postsRef.current[0]
if (topPost) {
startTs = topPost.createTime + 1
}
@@ -885,7 +891,7 @@ function MomentsWindow() {
endTs = undefined // Ensure endTs is cleared for newer posts check
} else {
// Load older
currentOffset = posts.length
currentOffset = postsRef.current.length
// Maintain jumpTargetDate filter if active
if (jumpTargetDate) {
@@ -946,7 +952,7 @@ function MomentsWindow() {
setLoadingNewer(false)
loadingRef.current = false
}
}, [posts, selectedUsernames, searchKeyword, jumpTargetDate])
}, [selectedUsernames, searchKeyword, jumpTargetDate])
// 监听筛选条件变化,自动重置加载
useEffect(() => {