mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
朋友圈支持定位解析;导出时表情包支持语义化补充导出
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -45,6 +45,8 @@ jobs:
|
|||||||
- name: Package and Publish macOS arm64 (unsigned DMG)
|
- name: Package and Publish macOS arm64 (unsigned DMG)
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
||||||
|
WF_SIGNING_REQUIRED: "1"
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --mac dmg --arm64 --publish always
|
npx electron-builder --mac dmg --arm64 --publish always
|
||||||
@@ -82,6 +84,8 @@ jobs:
|
|||||||
- name: Package and Publish Linux
|
- name: Package and Publish Linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
||||||
|
WF_SIGNING_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --linux --publish always
|
npx electron-builder --linux --publish always
|
||||||
|
|
||||||
@@ -118,6 +122,8 @@ jobs:
|
|||||||
- name: Package and Publish
|
- name: Package and Publish
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
WF_SIGN_PRIVATE_KEY: ${{ secrets.WF_SIGN_PRIVATE_KEY }}
|
||||||
|
WF_SIGNING_REQUIRED: "1"
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --publish always
|
npx electron-builder --publish always
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ npm run build
|
|||||||
|
|
||||||
打包产物在 `release` 目录下。
|
打包产物在 `release` 目录下。
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 发布版打包已启用签名清单注入:`electron-builder` 的 `afterPack` 会读取 `WF_SIGN_PRIVATE_KEY`(Ed25519 私钥,支持 PEM 或 `PKCS8 DER Base64`)并生成 `resources/.wf_manifest.json` + `resources/.wf_manifest.sig`。
|
||||||
|
> GitHub Release Workflow 需要在 **Settings → Secrets and variables → Actions → Repository secrets** 配置 `WF_SIGN_PRIVATE_KEY`,否则构建会失败(不要填到 Environment secrets)。
|
||||||
|
> 可使用 `node scripts/generate-signing-key.cjs` 生成密钥;脚本会同时输出可直接粘贴到 `wcdb/wcdb_api.cpp` 的公钥 XOR 数组。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|||||||
@@ -265,6 +265,13 @@ class ExportService {
|
|||||||
private readonly mediaFileCacheMaxBytes = 6 * 1024 * 1024 * 1024
|
private readonly mediaFileCacheMaxBytes = 6 * 1024 * 1024 * 1024
|
||||||
private readonly mediaFileCacheMaxFiles = 120000
|
private readonly mediaFileCacheMaxFiles = 120000
|
||||||
private readonly mediaFileCacheTtlMs = 45 * 24 * 60 * 60 * 1000
|
private readonly mediaFileCacheTtlMs = 45 * 24 * 60 * 60 * 1000
|
||||||
|
private emojiCaptionCache = new Map<string, string | null>()
|
||||||
|
private emojiCaptionPending = new Map<string, Promise<string | null>>()
|
||||||
|
private emojiMd5ByCdnCache = new Map<string, string | null>()
|
||||||
|
private emojiMd5ByCdnPending = new Map<string, Promise<string | null>>()
|
||||||
|
private emoticonDbPathCache: string | null = null
|
||||||
|
private emoticonDbPathCacheToken = ''
|
||||||
|
private readonly emojiCaptionLookupConcurrency = 8
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configService = new ConfigService()
|
this.configService = new ConfigService()
|
||||||
@@ -915,7 +922,7 @@ class ExportService {
|
|||||||
|
|
||||||
private shouldDecodeMessageContentInFastMode(localType: number): boolean {
|
private shouldDecodeMessageContentInFastMode(localType: number): boolean {
|
||||||
// 这些类型在文本导出里只需要占位符,无需解码完整 XML / 压缩内容
|
// 这些类型在文本导出里只需要占位符,无需解码完整 XML / 压缩内容
|
||||||
if (localType === 3 || localType === 34 || localType === 42 || localType === 43 || localType === 47) {
|
if (localType === 3 || localType === 34 || localType === 42 || localType === 43) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -989,6 +996,292 @@ class ExportService {
|
|||||||
return `${localType}_${this.getStableMessageKey(msg)}`
|
return `${localType}_${this.getStableMessageKey(msg)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeEmojiMd5(value: unknown): string | undefined {
|
||||||
|
const md5 = String(value || '').trim().toLowerCase()
|
||||||
|
if (!/^[a-f0-9]{32}$/.test(md5)) return undefined
|
||||||
|
return md5
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeEmojiCaption(value: unknown): string | null {
|
||||||
|
const caption = String(value || '').trim()
|
||||||
|
if (!caption) return null
|
||||||
|
return caption
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatEmojiSemanticText(caption?: string | null): string {
|
||||||
|
const normalizedCaption = this.normalizeEmojiCaption(caption)
|
||||||
|
if (!normalizedCaption) return '[表情包]'
|
||||||
|
return `[表情包:${normalizedCaption}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractLooseHexMd5(content: string): string | undefined {
|
||||||
|
if (!content) return undefined
|
||||||
|
const keyedMatch =
|
||||||
|
/(?:emoji|sticker|md5)[^a-fA-F0-9]{0,32}([a-fA-F0-9]{32})/i.exec(content) ||
|
||||||
|
/([a-fA-F0-9]{32})/i.exec(content)
|
||||||
|
return this.normalizeEmojiMd5(keyedMatch?.[1] || keyedMatch?.[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeEmojiCdnUrl(value: unknown): string | undefined {
|
||||||
|
let url = String(value || '').trim()
|
||||||
|
if (!url) return undefined
|
||||||
|
url = url.replace(/&/g, '&')
|
||||||
|
try {
|
||||||
|
if (url.includes('%')) {
|
||||||
|
url = decodeURIComponent(url)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// keep original URL if decoding fails
|
||||||
|
}
|
||||||
|
return url.trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveStrictEmoticonDbPath(): string | null {
|
||||||
|
const dbPath = String(this.configService.get('dbPath') || '').trim()
|
||||||
|
const rawWxid = String(this.configService.get('myWxid') || '').trim()
|
||||||
|
const cleanedWxid = this.cleanAccountDirName(rawWxid)
|
||||||
|
const token = `${dbPath}::${rawWxid}::${cleanedWxid}`
|
||||||
|
if (token === this.emoticonDbPathCacheToken) {
|
||||||
|
return this.emoticonDbPathCache
|
||||||
|
}
|
||||||
|
this.emoticonDbPathCacheToken = token
|
||||||
|
this.emoticonDbPathCache = null
|
||||||
|
|
||||||
|
const dbStoragePath =
|
||||||
|
this.resolveDbStoragePathForExport(dbPath, cleanedWxid) ||
|
||||||
|
this.resolveDbStoragePathForExport(dbPath, rawWxid)
|
||||||
|
if (!dbStoragePath) return null
|
||||||
|
|
||||||
|
const strictPath = path.join(dbStoragePath, 'emoticon', 'emoticon.db')
|
||||||
|
if (fs.existsSync(strictPath)) {
|
||||||
|
this.emoticonDbPathCache = strictPath
|
||||||
|
return strictPath
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveDbStoragePathForExport(basePath: string, wxid: string): string | null {
|
||||||
|
if (!basePath) return null
|
||||||
|
const normalized = basePath.replace(/[\\/]+$/, '')
|
||||||
|
if (normalized.toLowerCase().endsWith('db_storage') && fs.existsSync(normalized)) {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
const direct = path.join(normalized, 'db_storage')
|
||||||
|
if (fs.existsSync(direct)) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
|
if (!wxid) return null
|
||||||
|
|
||||||
|
const viaWxid = path.join(normalized, wxid, 'db_storage')
|
||||||
|
if (fs.existsSync(viaWxid)) {
|
||||||
|
return viaWxid
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(normalized)
|
||||||
|
const lowerWxid = wxid.toLowerCase()
|
||||||
|
const candidates = entries.filter((entry) => {
|
||||||
|
const entryPath = path.join(normalized, entry)
|
||||||
|
try {
|
||||||
|
if (!fs.statSync(entryPath).isDirectory()) return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const lowerEntry = entry.toLowerCase()
|
||||||
|
return lowerEntry === lowerWxid || lowerEntry.startsWith(`${lowerWxid}_`)
|
||||||
|
})
|
||||||
|
for (const entry of candidates) {
|
||||||
|
const candidate = path.join(normalized, entry, 'db_storage')
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// keep null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryEmojiMd5ByCdnUrlFallback(cdnUrlRaw: string): Promise<string | null> {
|
||||||
|
const cdnUrl = this.normalizeEmojiCdnUrl(cdnUrlRaw)
|
||||||
|
if (!cdnUrl) return null
|
||||||
|
const emoticonDbPath = this.resolveStrictEmoticonDbPath()
|
||||||
|
if (!emoticonDbPath) return null
|
||||||
|
|
||||||
|
const candidates = Array.from(new Set([
|
||||||
|
cdnUrl,
|
||||||
|
cdnUrl.replace(/&/g, '&')
|
||||||
|
]))
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const escaped = candidate.replace(/'/g, "''")
|
||||||
|
const result = await wcdbService.execQuery(
|
||||||
|
'message',
|
||||||
|
emoticonDbPath,
|
||||||
|
`SELECT md5, lower(hex(md5)) AS md5_hex FROM kNonStoreEmoticonTable WHERE cdn_url = '${escaped}' COLLATE NOCASE LIMIT 1`
|
||||||
|
)
|
||||||
|
const row = result.success && Array.isArray(result.rows) ? result.rows[0] : null
|
||||||
|
const md5 = this.normalizeEmojiMd5(this.getRowField(row || {}, ['md5', 'md5_hex']))
|
||||||
|
if (md5) return md5
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getEmojiMd5ByCdnUrl(cdnUrlRaw: string): Promise<string | null> {
|
||||||
|
const cdnUrl = this.normalizeEmojiCdnUrl(cdnUrlRaw)
|
||||||
|
if (!cdnUrl) return null
|
||||||
|
|
||||||
|
if (this.emojiMd5ByCdnCache.has(cdnUrl)) {
|
||||||
|
return this.emojiMd5ByCdnCache.get(cdnUrl) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = this.emojiMd5ByCdnPending.get(cdnUrl)
|
||||||
|
if (pending) return pending
|
||||||
|
|
||||||
|
const task = (async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
return await this.queryEmojiMd5ByCdnUrlFallback(cdnUrl)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.emojiMd5ByCdnPending.set(cdnUrl, task)
|
||||||
|
try {
|
||||||
|
const md5 = await task
|
||||||
|
this.emojiMd5ByCdnCache.set(cdnUrl, md5)
|
||||||
|
return md5
|
||||||
|
} finally {
|
||||||
|
this.emojiMd5ByCdnPending.delete(cdnUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getEmojiCaptionByMd5(md5Raw: string): Promise<string | null> {
|
||||||
|
const md5 = this.normalizeEmojiMd5(md5Raw)
|
||||||
|
if (!md5) return null
|
||||||
|
|
||||||
|
if (this.emojiCaptionCache.has(md5)) {
|
||||||
|
return this.emojiCaptionCache.get(md5) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = this.emojiCaptionPending.get(md5)
|
||||||
|
if (pending) return pending
|
||||||
|
|
||||||
|
const task = (async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const nativeResult = await wcdbService.getEmoticonCaptionStrict(md5)
|
||||||
|
if (nativeResult.success) {
|
||||||
|
const nativeCaption = this.normalizeEmojiCaption(nativeResult.caption)
|
||||||
|
if (nativeCaption) return nativeCaption
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore and return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
|
||||||
|
this.emojiCaptionPending.set(md5, task)
|
||||||
|
try {
|
||||||
|
const caption = await task
|
||||||
|
if (caption) {
|
||||||
|
this.emojiCaptionCache.set(md5, caption)
|
||||||
|
} else {
|
||||||
|
this.emojiCaptionCache.delete(md5)
|
||||||
|
}
|
||||||
|
return caption
|
||||||
|
} finally {
|
||||||
|
this.emojiCaptionPending.delete(md5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async hydrateEmojiCaptionsForMessages(
|
||||||
|
sessionId: string,
|
||||||
|
messages: any[],
|
||||||
|
control?: ExportTaskControl
|
||||||
|
): Promise<void> {
|
||||||
|
if (!Array.isArray(messages) || messages.length === 0) return
|
||||||
|
|
||||||
|
// 某些环境下游标行缺失 47 的 md5,先按 localId 回填详情再做 caption 查询。
|
||||||
|
await this.backfillMediaFieldsFromMessageDetail(sessionId, messages, new Set([47]), control)
|
||||||
|
|
||||||
|
const unresolvedByUrl = new Map<string, any[]>()
|
||||||
|
|
||||||
|
const uniqueMd5s = new Set<string>()
|
||||||
|
let scanIndex = 0
|
||||||
|
for (const msg of messages) {
|
||||||
|
if ((scanIndex++ & 0x7f) === 0) {
|
||||||
|
this.throwIfStopRequested(control)
|
||||||
|
}
|
||||||
|
if (Number(msg?.localType) !== 47) continue
|
||||||
|
|
||||||
|
const content = String(msg?.content || '')
|
||||||
|
const normalizedMd5 = this.normalizeEmojiMd5(msg?.emojiMd5)
|
||||||
|
|| this.extractEmojiMd5(content)
|
||||||
|
|| this.extractLooseHexMd5(content)
|
||||||
|
const normalizedCdnUrl = this.normalizeEmojiCdnUrl(msg?.emojiCdnUrl || this.extractEmojiUrl(content))
|
||||||
|
if (normalizedCdnUrl) {
|
||||||
|
msg.emojiCdnUrl = normalizedCdnUrl
|
||||||
|
}
|
||||||
|
if (!normalizedMd5) {
|
||||||
|
if (normalizedCdnUrl) {
|
||||||
|
const bucket = unresolvedByUrl.get(normalizedCdnUrl) || []
|
||||||
|
bucket.push(msg)
|
||||||
|
unresolvedByUrl.set(normalizedCdnUrl, bucket)
|
||||||
|
} else {
|
||||||
|
msg.emojiMd5 = undefined
|
||||||
|
msg.emojiCaption = undefined
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.emojiMd5 = normalizedMd5
|
||||||
|
uniqueMd5s.add(normalizedMd5)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unresolvedUrls = Array.from(unresolvedByUrl.keys())
|
||||||
|
if (unresolvedUrls.length > 0) {
|
||||||
|
await parallelLimit(unresolvedUrls, this.emojiCaptionLookupConcurrency, async (url, index) => {
|
||||||
|
if ((index & 0x0f) === 0) {
|
||||||
|
this.throwIfStopRequested(control)
|
||||||
|
}
|
||||||
|
const resolvedMd5 = await this.getEmojiMd5ByCdnUrl(url)
|
||||||
|
if (!resolvedMd5) return
|
||||||
|
const attached = unresolvedByUrl.get(url) || []
|
||||||
|
for (const msg of attached) {
|
||||||
|
msg.emojiMd5 = resolvedMd5
|
||||||
|
uniqueMd5s.add(resolvedMd5)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const md5List = Array.from(uniqueMd5s)
|
||||||
|
if (md5List.length > 0) {
|
||||||
|
await parallelLimit(md5List, this.emojiCaptionLookupConcurrency, async (md5, index) => {
|
||||||
|
if ((index & 0x0f) === 0) {
|
||||||
|
this.throwIfStopRequested(control)
|
||||||
|
}
|
||||||
|
await this.getEmojiCaptionByMd5(md5)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let assignIndex = 0
|
||||||
|
for (const msg of messages) {
|
||||||
|
if ((assignIndex++ & 0x7f) === 0) {
|
||||||
|
this.throwIfStopRequested(control)
|
||||||
|
}
|
||||||
|
if (Number(msg?.localType) !== 47) continue
|
||||||
|
const md5 = this.normalizeEmojiMd5(msg?.emojiMd5)
|
||||||
|
if (!md5) {
|
||||||
|
msg.emojiCaption = undefined
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const caption = this.emojiCaptionCache.get(md5) ?? null
|
||||||
|
msg.emojiCaption = caption || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
||||||
const wxid = this.configService.get('myWxid')
|
const wxid = this.configService.get('myWxid')
|
||||||
const dbPath = this.configService.get('dbPath')
|
const dbPath = this.configService.get('dbPath')
|
||||||
@@ -1574,8 +1867,12 @@ class ExportService {
|
|||||||
createTime?: number,
|
createTime?: number,
|
||||||
myWxid?: string,
|
myWxid?: string,
|
||||||
senderWxid?: string,
|
senderWxid?: string,
|
||||||
isSend?: boolean
|
isSend?: boolean,
|
||||||
|
emojiCaption?: string
|
||||||
): string | null {
|
): string | null {
|
||||||
|
if (!content && localType === 47) {
|
||||||
|
return this.formatEmojiSemanticText(emojiCaption)
|
||||||
|
}
|
||||||
if (!content) return null
|
if (!content) return null
|
||||||
|
|
||||||
const normalizedContent = this.normalizeAppMessageContent(content)
|
const normalizedContent = this.normalizeAppMessageContent(content)
|
||||||
@@ -1601,7 +1898,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
case 42: return '[名片]'
|
case 42: return '[名片]'
|
||||||
case 43: return '[视频]'
|
case 43: return '[视频]'
|
||||||
case 47: return '[动画表情]'
|
case 47: return this.formatEmojiSemanticText(emojiCaption)
|
||||||
case 48: {
|
case 48: {
|
||||||
const normalized48 = this.normalizeAppMessageContent(content)
|
const normalized48 = this.normalizeAppMessageContent(content)
|
||||||
const locPoiname = this.extractXmlAttribute(normalized48, 'location', 'poiname') || this.extractXmlValue(normalized48, 'poiname') || this.extractXmlValue(normalized48, 'poiName')
|
const locPoiname = this.extractXmlAttribute(normalized48, 'location', 'poiname') || this.extractXmlValue(normalized48, 'poiname') || this.extractXmlValue(normalized48, 'poiName')
|
||||||
@@ -1711,7 +2008,8 @@ class ExportService {
|
|||||||
voiceTranscript?: string,
|
voiceTranscript?: string,
|
||||||
myWxid?: string,
|
myWxid?: string,
|
||||||
senderWxid?: string,
|
senderWxid?: string,
|
||||||
isSend?: boolean
|
isSend?: boolean,
|
||||||
|
emojiCaption?: string
|
||||||
): string {
|
): string {
|
||||||
const safeContent = content || ''
|
const safeContent = content || ''
|
||||||
|
|
||||||
@@ -1741,6 +2039,9 @@ class ExportService {
|
|||||||
const seconds = lengthValue ? this.parseDurationSeconds(lengthValue) : null
|
const seconds = lengthValue ? this.parseDurationSeconds(lengthValue) : null
|
||||||
return seconds ? `[视频]${seconds}s` : '[视频]'
|
return seconds ? `[视频]${seconds}s` : '[视频]'
|
||||||
}
|
}
|
||||||
|
if (localType === 47) {
|
||||||
|
return this.formatEmojiSemanticText(emojiCaption)
|
||||||
|
}
|
||||||
if (localType === 48) {
|
if (localType === 48) {
|
||||||
const normalized = this.normalizeAppMessageContent(safeContent)
|
const normalized = this.normalizeAppMessageContent(safeContent)
|
||||||
const locPoiname = this.extractXmlAttribute(normalized, 'location', 'poiname') || this.extractXmlValue(normalized, 'poiname') || this.extractXmlValue(normalized, 'poiName')
|
const locPoiname = this.extractXmlAttribute(normalized, 'location', 'poiname') || this.extractXmlValue(normalized, 'poiname') || this.extractXmlValue(normalized, 'poiName')
|
||||||
@@ -2481,7 +2782,7 @@ class ExportService {
|
|||||||
case 3: return '[图片]'
|
case 3: return '[图片]'
|
||||||
case 34: return '[语音消息]'
|
case 34: return '[语音消息]'
|
||||||
case 43: return '[视频]'
|
case 43: return '[视频]'
|
||||||
case 47: return '[动画表情]'
|
case 47: return '[表情包]'
|
||||||
case 49:
|
case 49:
|
||||||
case 8: return title ? `[文件] ${title}` : '[文件]'
|
case 8: return title ? `[文件] ${title}` : '[文件]'
|
||||||
case 17: return item.chatRecordDesc || title || '[聊天记录]'
|
case 17: return item.chatRecordDesc || title || '[聊天记录]'
|
||||||
@@ -2622,7 +2923,7 @@ class ExportService {
|
|||||||
displayContent = '[视频]'
|
displayContent = '[视频]'
|
||||||
break
|
break
|
||||||
case '47':
|
case '47':
|
||||||
displayContent = '[动画表情]'
|
displayContent = '[表情包]'
|
||||||
break
|
break
|
||||||
case '49':
|
case '49':
|
||||||
displayContent = '[链接]'
|
displayContent = '[链接]'
|
||||||
@@ -2935,7 +3236,17 @@ class ExportService {
|
|||||||
return rendered.join('')
|
return rendered.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatHtmlMessageText(content: string, localType: number, myWxid?: string, senderWxid?: string, isSend?: boolean): string {
|
private formatHtmlMessageText(
|
||||||
|
content: string,
|
||||||
|
localType: number,
|
||||||
|
myWxid?: string,
|
||||||
|
senderWxid?: string,
|
||||||
|
isSend?: boolean,
|
||||||
|
emojiCaption?: string
|
||||||
|
): string {
|
||||||
|
if (!content && localType === 47) {
|
||||||
|
return this.formatEmojiSemanticText(emojiCaption)
|
||||||
|
}
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
|
|
||||||
if (localType === 1) {
|
if (localType === 1) {
|
||||||
@@ -2943,10 +3254,10 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (localType === 34) {
|
if (localType === 34) {
|
||||||
return this.parseMessageContent(content, localType, undefined, undefined, myWxid, senderWxid, isSend) || ''
|
return this.parseMessageContent(content, localType, undefined, undefined, myWxid, senderWxid, isSend, emojiCaption) || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatPlainExportContent(content, localType, { exportVoiceAsText: false }, undefined, myWxid, senderWxid, isSend)
|
return this.formatPlainExportContent(content, localType, { exportVoiceAsText: false }, undefined, myWxid, senderWxid, isSend, emojiCaption)
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractHtmlLinkCard(content: string, localType: number): { title: string; url: string } | null {
|
private extractHtmlLinkCard(content: string, localType: number): { title: string; url: string } | null {
|
||||||
@@ -3487,8 +3798,11 @@ class ExportService {
|
|||||||
*/
|
*/
|
||||||
private extractEmojiMd5(content: string): string | undefined {
|
private extractEmojiMd5(content: string): string | undefined {
|
||||||
if (!content) return undefined
|
if (!content) return undefined
|
||||||
const match = /md5="([^"]+)"/i.exec(content) || /<md5>([^<]+)<\/md5>/i.exec(content)
|
const match =
|
||||||
return match?.[1]
|
/md5\s*=\s*['"]([a-fA-F0-9]{32})['"]/i.exec(content) ||
|
||||||
|
/md5\s*=\s*([a-fA-F0-9]{32})/i.exec(content) ||
|
||||||
|
/<md5>([a-fA-F0-9]{32})<\/md5>/i.exec(content)
|
||||||
|
return this.normalizeEmojiMd5(match?.[1]) || this.extractLooseHexMd5(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractVideoMd5(content: string): string | undefined {
|
private extractVideoMd5(content: string): string | undefined {
|
||||||
@@ -3777,6 +4091,7 @@ class ExportService {
|
|||||||
let locationPoiname: string | undefined
|
let locationPoiname: string | undefined
|
||||||
let locationLabel: string | undefined
|
let locationLabel: string | undefined
|
||||||
let chatRecordList: any[] | undefined
|
let chatRecordList: any[] | undefined
|
||||||
|
let emojiCaption: string | undefined
|
||||||
|
|
||||||
if (localType === 48 && content) {
|
if (localType === 48 && content) {
|
||||||
const locationMeta = this.extractLocationMeta(content, localType)
|
const locationMeta = this.extractLocationMeta(content, localType)
|
||||||
@@ -3788,22 +4103,30 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localType === 47) {
|
||||||
|
emojiCdnUrl = String(row.emoji_cdn_url || row.emojiCdnUrl || '').trim() || undefined
|
||||||
|
emojiMd5 = this.normalizeEmojiMd5(row.emoji_md5 || row.emojiMd5) || undefined
|
||||||
|
const packedInfoRaw = String(row.packed_info || row.packedInfo || row.PackedInfo || '')
|
||||||
|
const reserved0Raw = String(row.reserved0 || row.Reserved0 || '')
|
||||||
|
const supplementalPayload = `${this.decodeMaybeCompressed(packedInfoRaw)}\n${this.decodeMaybeCompressed(reserved0Raw)}`
|
||||||
|
if (content) {
|
||||||
|
emojiCdnUrl = emojiCdnUrl || this.extractEmojiUrl(content)
|
||||||
|
emojiMd5 = emojiMd5 || this.normalizeEmojiMd5(this.extractEmojiMd5(content))
|
||||||
|
}
|
||||||
|
emojiCdnUrl = emojiCdnUrl || this.extractEmojiUrl(supplementalPayload)
|
||||||
|
emojiMd5 = emojiMd5 || this.extractEmojiMd5(supplementalPayload) || this.extractLooseHexMd5(supplementalPayload)
|
||||||
|
}
|
||||||
|
|
||||||
if (collectMode === 'full' || collectMode === 'media-fast') {
|
if (collectMode === 'full' || collectMode === 'media-fast') {
|
||||||
// 优先复用游标返回的字段,缺失时再回退到 XML 解析。
|
// 优先复用游标返回的字段,缺失时再回退到 XML 解析。
|
||||||
imageMd5 = String(row.image_md5 || row.imageMd5 || '').trim() || undefined
|
imageMd5 = String(row.image_md5 || row.imageMd5 || '').trim() || undefined
|
||||||
imageDatName = String(row.image_dat_name || row.imageDatName || '').trim() || undefined
|
imageDatName = String(row.image_dat_name || row.imageDatName || '').trim() || undefined
|
||||||
emojiCdnUrl = String(row.emoji_cdn_url || row.emojiCdnUrl || '').trim() || undefined
|
|
||||||
emojiMd5 = String(row.emoji_md5 || row.emojiMd5 || '').trim() || undefined
|
|
||||||
videoMd5 = String(row.video_md5 || row.videoMd5 || '').trim() || undefined
|
videoMd5 = String(row.video_md5 || row.videoMd5 || '').trim() || undefined
|
||||||
|
|
||||||
if (localType === 3 && content) {
|
if (localType === 3 && content) {
|
||||||
// 图片消息
|
// 图片消息
|
||||||
imageMd5 = imageMd5 || this.extractImageMd5(content)
|
imageMd5 = imageMd5 || this.extractImageMd5(content)
|
||||||
imageDatName = imageDatName || this.extractImageDatName(content)
|
imageDatName = imageDatName || this.extractImageDatName(content)
|
||||||
} else if (localType === 47 && content) {
|
|
||||||
// 动画表情
|
|
||||||
emojiCdnUrl = emojiCdnUrl || this.extractEmojiUrl(content)
|
|
||||||
emojiMd5 = emojiMd5 || this.extractEmojiMd5(content)
|
|
||||||
} else if (localType === 43 && content) {
|
} else if (localType === 43 && content) {
|
||||||
// 视频消息
|
// 视频消息
|
||||||
videoMd5 = videoMd5 || this.extractVideoMd5(content)
|
videoMd5 = videoMd5 || this.extractVideoMd5(content)
|
||||||
@@ -3830,6 +4153,7 @@ class ExportService {
|
|||||||
imageDatName,
|
imageDatName,
|
||||||
emojiCdnUrl,
|
emojiCdnUrl,
|
||||||
emojiMd5,
|
emojiMd5,
|
||||||
|
emojiCaption,
|
||||||
videoMd5,
|
videoMd5,
|
||||||
locationLat,
|
locationLat,
|
||||||
locationLng,
|
locationLng,
|
||||||
@@ -3898,7 +4222,7 @@ class ExportService {
|
|||||||
const needsBackfill = rows.filter((msg) => {
|
const needsBackfill = rows.filter((msg) => {
|
||||||
if (!targetMediaTypes.has(msg.localType)) return false
|
if (!targetMediaTypes.has(msg.localType)) return false
|
||||||
if (msg.localType === 3) return !msg.imageMd5 && !msg.imageDatName
|
if (msg.localType === 3) return !msg.imageMd5 && !msg.imageDatName
|
||||||
if (msg.localType === 47) return !msg.emojiMd5 && !msg.emojiCdnUrl
|
if (msg.localType === 47) return !msg.emojiMd5
|
||||||
if (msg.localType === 43) return !msg.videoMd5
|
if (msg.localType === 43) return !msg.videoMd5
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -3915,9 +4239,16 @@ class ExportService {
|
|||||||
if (!detail.success || !detail.message) return
|
if (!detail.success || !detail.message) return
|
||||||
|
|
||||||
const row = detail.message as any
|
const row = detail.message as any
|
||||||
const rawMessageContent = row.message_content ?? row.messageContent ?? row.msg_content ?? row.msgContent ?? ''
|
const rawMessageContent = this.getRowField(row, [
|
||||||
const rawCompressContent = row.compress_content ?? row.compressContent ?? row.msg_compress_content ?? row.msgCompressContent ?? ''
|
'message_content', 'messageContent', 'msg_content', 'msgContent', 'strContent', 'content', 'WCDB_CT_message_content'
|
||||||
|
]) ?? ''
|
||||||
|
const rawCompressContent = this.getRowField(row, [
|
||||||
|
'compress_content', 'compressContent', 'msg_compress_content', 'msgCompressContent', 'WCDB_CT_compress_content'
|
||||||
|
]) ?? ''
|
||||||
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
|
||||||
|
const packedInfoRaw = this.getRowField(row, ['packed_info', 'packedInfo', 'PackedInfo', 'WCDB_CT_packed_info']) ?? ''
|
||||||
|
const reserved0Raw = this.getRowField(row, ['reserved0', 'Reserved0', 'WCDB_CT_Reserved0']) ?? ''
|
||||||
|
const supplementalPayload = `${this.decodeMaybeCompressed(String(packedInfoRaw || ''))}\n${this.decodeMaybeCompressed(String(reserved0Raw || ''))}`
|
||||||
|
|
||||||
if (msg.localType === 3) {
|
if (msg.localType === 3) {
|
||||||
const imageMd5 = String(row.image_md5 || row.imageMd5 || '').trim() || this.extractImageMd5(content)
|
const imageMd5 = String(row.image_md5 || row.imageMd5 || '').trim() || this.extractImageMd5(content)
|
||||||
@@ -3928,8 +4259,15 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.localType === 47) {
|
if (msg.localType === 47) {
|
||||||
const emojiMd5 = String(row.emoji_md5 || row.emojiMd5 || '').trim() || this.extractEmojiMd5(content)
|
const emojiMd5 =
|
||||||
const emojiCdnUrl = String(row.emoji_cdn_url || row.emojiCdnUrl || '').trim() || this.extractEmojiUrl(content)
|
this.normalizeEmojiMd5(row.emoji_md5 || row.emojiMd5) ||
|
||||||
|
this.extractEmojiMd5(content) ||
|
||||||
|
this.extractEmojiMd5(supplementalPayload) ||
|
||||||
|
this.extractLooseHexMd5(supplementalPayload)
|
||||||
|
const emojiCdnUrl =
|
||||||
|
String(row.emoji_cdn_url || row.emojiCdnUrl || '').trim() ||
|
||||||
|
this.extractEmojiUrl(content) ||
|
||||||
|
this.extractEmojiUrl(supplementalPayload)
|
||||||
if (emojiMd5) msg.emojiMd5 = emojiMd5
|
if (emojiMd5) msg.emojiMd5 = emojiMd5
|
||||||
if (emojiCdnUrl) msg.emojiCdnUrl = emojiCdnUrl
|
if (emojiCdnUrl) msg.emojiCdnUrl = emojiCdnUrl
|
||||||
return
|
return
|
||||||
@@ -4409,6 +4747,8 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, allMessages, control)
|
||||||
|
|
||||||
const voiceMessages = options.exportVoiceAsText
|
const voiceMessages = options.exportVoiceAsText
|
||||||
? allMessages.filter(msg => msg.localType === 34)
|
? allMessages.filter(msg => msg.localType === 34)
|
||||||
: []
|
: []
|
||||||
@@ -4634,7 +4974,8 @@ class ExportService {
|
|||||||
msg.createTime,
|
msg.createTime,
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4726,7 +5067,7 @@ class ExportService {
|
|||||||
break
|
break
|
||||||
case 47:
|
case 47:
|
||||||
recordType = 5 // EMOJI
|
recordType = 5 // EMOJI
|
||||||
recordContent = '[动画表情]'
|
recordContent = '[表情包]'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
recordType = 0
|
recordType = 0
|
||||||
@@ -4936,6 +5277,8 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||||
|
|
||||||
const voiceMessages = options.exportVoiceAsText
|
const voiceMessages = options.exportVoiceAsText
|
||||||
? collected.rows.filter(msg => msg.localType === 34)
|
? collected.rows.filter(msg => msg.localType === 34)
|
||||||
: []
|
: []
|
||||||
@@ -5114,7 +5457,7 @@ class ExportService {
|
|||||||
|
|
||||||
if (msg.localType === 34 && options.exportVoiceAsText) {
|
if (msg.localType === 34 && options.exportVoiceAsText) {
|
||||||
content = voiceTranscriptMap.get(this.getStableMessageKey(msg)) || '[语音消息 - 转文字失败]'
|
content = voiceTranscriptMap.get(this.getStableMessageKey(msg)) || '[语音消息 - 转文字失败]'
|
||||||
} else if (mediaItem) {
|
} else if (mediaItem && msg.localType !== 47) {
|
||||||
content = mediaItem.relativePath
|
content = mediaItem.relativePath
|
||||||
} else {
|
} else {
|
||||||
content = this.parseMessageContent(
|
content = this.parseMessageContent(
|
||||||
@@ -5124,7 +5467,8 @@ class ExportService {
|
|||||||
undefined,
|
undefined,
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5185,6 +5529,12 @@ class ExportService {
|
|||||||
senderAvatarKey: msg.senderUsername
|
senderAvatarKey: msg.senderUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.localType === 47) {
|
||||||
|
if (msg.emojiMd5) msgObj.emojiMd5 = msg.emojiMd5
|
||||||
|
if (msg.emojiCdnUrl) msgObj.emojiCdnUrl = msg.emojiCdnUrl
|
||||||
|
if (msg.emojiCaption) msgObj.emojiCaption = msg.emojiCaption
|
||||||
|
}
|
||||||
|
|
||||||
const platformMessageId = this.getExportPlatformMessageId(msg)
|
const platformMessageId = this.getExportPlatformMessageId(msg)
|
||||||
if (platformMessageId) msgObj.platformMessageId = platformMessageId
|
if (platformMessageId) msgObj.platformMessageId = platformMessageId
|
||||||
|
|
||||||
@@ -5420,6 +5770,9 @@ class ExportService {
|
|||||||
if (message.linkTitle) compactMessage.linkTitle = message.linkTitle
|
if (message.linkTitle) compactMessage.linkTitle = message.linkTitle
|
||||||
if (message.linkUrl) compactMessage.linkUrl = message.linkUrl
|
if (message.linkUrl) compactMessage.linkUrl = message.linkUrl
|
||||||
if (message.linkThumb) compactMessage.linkThumb = message.linkThumb
|
if (message.linkThumb) compactMessage.linkThumb = message.linkThumb
|
||||||
|
if (message.emojiMd5) compactMessage.emojiMd5 = message.emojiMd5
|
||||||
|
if (message.emojiCdnUrl) compactMessage.emojiCdnUrl = message.emojiCdnUrl
|
||||||
|
if (message.emojiCaption) compactMessage.emojiCaption = message.emojiCaption
|
||||||
if (message.finderTitle) compactMessage.finderTitle = message.finderTitle
|
if (message.finderTitle) compactMessage.finderTitle = message.finderTitle
|
||||||
if (message.finderDesc) compactMessage.finderDesc = message.finderDesc
|
if (message.finderDesc) compactMessage.finderDesc = message.finderDesc
|
||||||
if (message.finderUsername) compactMessage.finderUsername = message.finderUsername
|
if (message.finderUsername) compactMessage.finderUsername = message.finderUsername
|
||||||
@@ -5650,6 +6003,8 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||||
|
|
||||||
const voiceMessages = options.exportVoiceAsText
|
const voiceMessages = options.exportVoiceAsText
|
||||||
? collected.rows.filter(msg => msg.localType === 34)
|
? collected.rows.filter(msg => msg.localType === 34)
|
||||||
: []
|
: []
|
||||||
@@ -6007,9 +6362,10 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
: (mediaItem?.relativePath
|
: ((msg.localType !== 47 ? mediaItem?.relativePath : undefined)
|
||||||
|| this.formatPlainExportContent(
|
|| this.formatPlainExportContent(
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.localType,
|
msg.localType,
|
||||||
@@ -6017,7 +6373,8 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
))
|
))
|
||||||
|
|
||||||
// 转账消息:追加 "谁转账给谁" 信息
|
// 转账消息:追加 "谁转账给谁" 信息
|
||||||
@@ -6269,9 +6626,10 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
: (mediaItem?.relativePath
|
: ((msg.localType !== 47 ? mediaItem?.relativePath : undefined)
|
||||||
|| this.formatPlainExportContent(
|
|| this.formatPlainExportContent(
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.localType,
|
msg.localType,
|
||||||
@@ -6279,7 +6637,8 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
))
|
))
|
||||||
|
|
||||||
let enrichedContentValue = contentValue
|
let enrichedContentValue = contentValue
|
||||||
@@ -6468,6 +6827,8 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||||
|
|
||||||
const voiceMessages = options.exportVoiceAsText
|
const voiceMessages = options.exportVoiceAsText
|
||||||
? collected.rows.filter(msg => msg.localType === 34)
|
? collected.rows.filter(msg => msg.localType === 34)
|
||||||
: []
|
: []
|
||||||
@@ -6635,9 +6996,10 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
: (mediaItem?.relativePath
|
: ((msg.localType !== 47 ? mediaItem?.relativePath : undefined)
|
||||||
|| this.formatPlainExportContent(
|
|| this.formatPlainExportContent(
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.localType,
|
msg.localType,
|
||||||
@@ -6645,7 +7007,8 @@ class ExportService {
|
|||||||
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
voiceTranscriptMap.get(this.getStableMessageKey(msg)),
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
))
|
))
|
||||||
|
|
||||||
// 转账消息:追加 "谁转账给谁" 信息
|
// 转账消息:追加 "谁转账给谁" 信息
|
||||||
@@ -6828,6 +7191,8 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||||
|
|
||||||
const senderUsernames = new Set<string>()
|
const senderUsernames = new Set<string>()
|
||||||
let senderScanIndex = 0
|
let senderScanIndex = 0
|
||||||
for (const msg of collected.rows) {
|
for (const msg of collected.rows) {
|
||||||
@@ -7046,7 +7411,8 @@ class ExportService {
|
|||||||
msg.createTime,
|
msg.createTime,
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
) || '')
|
) || '')
|
||||||
const src = this.getWeCloneSource(msg, typeName, mediaItem)
|
const src = this.getWeCloneSource(msg, typeName, mediaItem)
|
||||||
const platformMessageId = this.getExportPlatformMessageId(msg) || ''
|
const platformMessageId = this.getExportPlatformMessageId(msg) || ''
|
||||||
@@ -7255,6 +7621,8 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
const totalMessages = collected.rows.length
|
const totalMessages = collected.rows.length
|
||||||
|
|
||||||
|
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
|
||||||
|
|
||||||
const senderUsernames = new Set<string>()
|
const senderUsernames = new Set<string>()
|
||||||
let senderScanIndex = 0
|
let senderScanIndex = 0
|
||||||
for (const msg of collected.rows) {
|
for (const msg of collected.rows) {
|
||||||
@@ -7545,12 +7913,13 @@ class ExportService {
|
|||||||
msg.localType,
|
msg.localType,
|
||||||
cleanedMyWxid,
|
cleanedMyWxid,
|
||||||
msg.senderUsername,
|
msg.senderUsername,
|
||||||
msg.isSend
|
msg.isSend,
|
||||||
|
msg.emojiCaption
|
||||||
)
|
)
|
||||||
if (msg.localType === 34 && useVoiceTranscript) {
|
if (msg.localType === 34 && useVoiceTranscript) {
|
||||||
textContent = voiceTranscriptMap.get(this.getStableMessageKey(msg)) || '[语音消息 - 转文字失败]'
|
textContent = voiceTranscriptMap.get(this.getStableMessageKey(msg)) || '[语音消息 - 转文字失败]'
|
||||||
}
|
}
|
||||||
if (mediaItem && (msg.localType === 3 || msg.localType === 47)) {
|
if (mediaItem && msg.localType === 3) {
|
||||||
textContent = ''
|
textContent = ''
|
||||||
}
|
}
|
||||||
if (this.isTransferExportContent(textContent) && msg.content) {
|
if (this.isTransferExportContent(textContent) && msg.content) {
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ export interface SnsMedia {
|
|||||||
livePhoto?: SnsLivePhoto
|
livePhoto?: SnsLivePhoto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SnsLocation {
|
||||||
|
latitude?: number
|
||||||
|
longitude?: number
|
||||||
|
city?: string
|
||||||
|
country?: string
|
||||||
|
poiName?: string
|
||||||
|
poiAddress?: string
|
||||||
|
poiAddressName?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SnsPost {
|
export interface SnsPost {
|
||||||
id: string
|
id: string
|
||||||
tid?: string // 数据库主键(雪花 ID),用于精确删除
|
tid?: string // 数据库主键(雪花 ID),用于精确删除
|
||||||
@@ -39,6 +50,7 @@ export interface SnsPost {
|
|||||||
media: SnsMedia[]
|
media: SnsMedia[]
|
||||||
likes: string[]
|
likes: string[]
|
||||||
comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: { url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }[] }[]
|
comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: { url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }[] }[]
|
||||||
|
location?: SnsLocation
|
||||||
rawXml?: string
|
rawXml?: string
|
||||||
linkTitle?: string
|
linkTitle?: string
|
||||||
linkUrl?: string
|
linkUrl?: string
|
||||||
@@ -287,6 +299,17 @@ function parseCommentsFromXml(xml: string): ParsedCommentItem[] {
|
|||||||
return comments
|
return comments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decodeXmlText = (text: string): string => {
|
||||||
|
if (!text) return ''
|
||||||
|
return text
|
||||||
|
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
|
||||||
|
.replace(/&/gi, '&')
|
||||||
|
.replace(/</gi, '<')
|
||||||
|
.replace(/>/gi, '>')
|
||||||
|
.replace(/"/gi, '"')
|
||||||
|
.replace(/'/gi, "'")
|
||||||
|
}
|
||||||
|
|
||||||
class SnsService {
|
class SnsService {
|
||||||
private configService: ConfigService
|
private configService: ConfigService
|
||||||
private contactCache: ContactCacheService
|
private contactCache: ContactCacheService
|
||||||
@@ -647,6 +670,110 @@ class SnsService {
|
|||||||
return { media, videoKey }
|
return { media, videoKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toOptionalNumber(value: unknown): number | undefined {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||||
|
if (typeof value !== 'string') return undefined
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return undefined
|
||||||
|
const parsed = Number.parseFloat(trimmed)
|
||||||
|
return Number.isFinite(parsed) ? parsed : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeLocation(input: unknown): SnsLocation | undefined {
|
||||||
|
if (!input || typeof input !== 'object') return undefined
|
||||||
|
|
||||||
|
const row = input as Record<string, unknown>
|
||||||
|
const normalizeText = (value: unknown): string | undefined => {
|
||||||
|
if (typeof value !== 'string') return undefined
|
||||||
|
return this.toOptionalString(decodeXmlText(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const location: SnsLocation = {}
|
||||||
|
const latitude = this.toOptionalNumber(row.latitude ?? row.lat ?? row.x)
|
||||||
|
const longitude = this.toOptionalNumber(row.longitude ?? row.lng ?? row.y)
|
||||||
|
const city = normalizeText(row.city)
|
||||||
|
const country = normalizeText(row.country)
|
||||||
|
const poiName = normalizeText(row.poiName ?? row.poiname)
|
||||||
|
const poiAddress = normalizeText(row.poiAddress ?? row.poiaddress)
|
||||||
|
const poiAddressName = normalizeText(row.poiAddressName ?? row.poiaddressname)
|
||||||
|
const label = normalizeText(row.label)
|
||||||
|
|
||||||
|
if (latitude !== undefined) location.latitude = latitude
|
||||||
|
if (longitude !== undefined) location.longitude = longitude
|
||||||
|
if (city) location.city = city
|
||||||
|
if (country) location.country = country
|
||||||
|
if (poiName) location.poiName = poiName
|
||||||
|
if (poiAddress) location.poiAddress = poiAddress
|
||||||
|
if (poiAddressName) location.poiAddressName = poiAddressName
|
||||||
|
if (label) location.label = label
|
||||||
|
|
||||||
|
return Object.keys(location).length > 0 ? location : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseLocationFromXml(xml: string): SnsLocation | undefined {
|
||||||
|
if (!xml) return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const locationTagMatch = xml.match(/<location\b([^>]*)>/i)
|
||||||
|
const locationAttrs = locationTagMatch?.[1] || ''
|
||||||
|
const readAttr = (name: string): string | undefined => {
|
||||||
|
if (!locationAttrs) return undefined
|
||||||
|
const match = locationAttrs.match(new RegExp(`${name}\\s*=\\s*["']([\\s\\S]*?)["']`, 'i'))
|
||||||
|
if (!match?.[1]) return undefined
|
||||||
|
return this.toOptionalString(decodeXmlText(match[1]))
|
||||||
|
}
|
||||||
|
const readTag = (name: string): string | undefined => {
|
||||||
|
const match = xml.match(new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, 'i'))
|
||||||
|
if (!match?.[1]) return undefined
|
||||||
|
return this.toOptionalString(decodeXmlText(match[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const location: SnsLocation = {}
|
||||||
|
const latitude = this.toOptionalNumber(readAttr('latitude') || readAttr('x') || readTag('latitude') || readTag('x'))
|
||||||
|
const longitude = this.toOptionalNumber(readAttr('longitude') || readAttr('y') || readTag('longitude') || readTag('y'))
|
||||||
|
const city = readAttr('city') || readTag('city')
|
||||||
|
const country = readAttr('country') || readTag('country')
|
||||||
|
const poiName = readAttr('poiName') || readAttr('poiname') || readTag('poiName') || readTag('poiname')
|
||||||
|
const poiAddress = readAttr('poiAddress') || readAttr('poiaddress') || readTag('poiAddress') || readTag('poiaddress')
|
||||||
|
const poiAddressName = readAttr('poiAddressName') || readAttr('poiaddressname') || readTag('poiAddressName') || readTag('poiaddressname')
|
||||||
|
const label = readAttr('label') || readTag('label')
|
||||||
|
|
||||||
|
if (latitude !== undefined) location.latitude = latitude
|
||||||
|
if (longitude !== undefined) location.longitude = longitude
|
||||||
|
if (city) location.city = city
|
||||||
|
if (country) location.country = country
|
||||||
|
if (poiName) location.poiName = poiName
|
||||||
|
if (poiAddress) location.poiAddress = poiAddress
|
||||||
|
if (poiAddressName) location.poiAddressName = poiAddressName
|
||||||
|
if (label) location.label = label
|
||||||
|
|
||||||
|
return Object.keys(location).length > 0 ? location : undefined
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SnsService] 解析位置 XML 失败:', e)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeLocation(primary?: SnsLocation, fallback?: SnsLocation): SnsLocation | undefined {
|
||||||
|
if (!primary && !fallback) return undefined
|
||||||
|
|
||||||
|
const merged: SnsLocation = {}
|
||||||
|
const setValue = <K extends keyof SnsLocation>(key: K, value: SnsLocation[K] | undefined) => {
|
||||||
|
if (value !== undefined) merged[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue('latitude', primary?.latitude ?? fallback?.latitude)
|
||||||
|
setValue('longitude', primary?.longitude ?? fallback?.longitude)
|
||||||
|
setValue('city', primary?.city ?? fallback?.city)
|
||||||
|
setValue('country', primary?.country ?? fallback?.country)
|
||||||
|
setValue('poiName', primary?.poiName ?? fallback?.poiName)
|
||||||
|
setValue('poiAddress', primary?.poiAddress ?? fallback?.poiAddress)
|
||||||
|
setValue('poiAddressName', primary?.poiAddressName ?? fallback?.poiAddressName)
|
||||||
|
setValue('label', primary?.label ?? fallback?.label)
|
||||||
|
|
||||||
|
return Object.keys(merged).length > 0 ? merged : undefined
|
||||||
|
}
|
||||||
|
|
||||||
private getSnsCacheDir(): string {
|
private getSnsCacheDir(): string {
|
||||||
const cachePath = this.configService.getCacheBasePath()
|
const cachePath = this.configService.getCacheBasePath()
|
||||||
const snsCacheDir = join(cachePath, 'sns_cache')
|
const snsCacheDir = join(cachePath, 'sns_cache')
|
||||||
@@ -948,7 +1075,12 @@ class SnsService {
|
|||||||
const enrichedTimeline = result.timeline.map((post: any) => {
|
const enrichedTimeline = result.timeline.map((post: any) => {
|
||||||
const contact = this.contactCache.get(post.username)
|
const contact = this.contactCache.get(post.username)
|
||||||
const isVideoPost = post.type === 15
|
const isVideoPost = post.type === 15
|
||||||
const videoKey = extractVideoKey(post.rawXml || '')
|
const rawXml = post.rawXml || ''
|
||||||
|
const videoKey = extractVideoKey(rawXml)
|
||||||
|
const location = this.mergeLocation(
|
||||||
|
this.normalizeLocation((post as { location?: unknown }).location),
|
||||||
|
this.parseLocationFromXml(rawXml)
|
||||||
|
)
|
||||||
|
|
||||||
const fixedMedia = (post.media || []).map((m: any) => ({
|
const fixedMedia = (post.media || []).map((m: any) => ({
|
||||||
url: fixSnsUrl(m.url, m.token, isVideoPost),
|
url: fixSnsUrl(m.url, m.token, isVideoPost),
|
||||||
@@ -971,7 +1103,6 @@ class SnsService {
|
|||||||
// 如果 DLL 评论缺少表情包信息,回退到从 rawXml 重新解析
|
// 如果 DLL 评论缺少表情包信息,回退到从 rawXml 重新解析
|
||||||
const dllComments: any[] = post.comments || []
|
const dllComments: any[] = post.comments || []
|
||||||
const hasEmojisInDll = dllComments.some((c: any) => c.emojis && c.emojis.length > 0)
|
const hasEmojisInDll = dllComments.some((c: any) => c.emojis && c.emojis.length > 0)
|
||||||
const rawXml = post.rawXml || ''
|
|
||||||
|
|
||||||
let finalComments: any[]
|
let finalComments: any[]
|
||||||
if (dllComments.length > 0 && (hasEmojisInDll || !rawXml)) {
|
if (dllComments.length > 0 && (hasEmojisInDll || !rawXml)) {
|
||||||
@@ -990,7 +1121,8 @@ class SnsService {
|
|||||||
avatarUrl: contact?.avatarUrl,
|
avatarUrl: contact?.avatarUrl,
|
||||||
nickname: post.nickname || contact?.displayName || post.username,
|
nickname: post.nickname || contact?.displayName || post.username,
|
||||||
media: fixedMedia,
|
media: fixedMedia,
|
||||||
comments: finalComments
|
comments: finalComments,
|
||||||
|
location
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1346,6 +1478,7 @@ class SnsService {
|
|||||||
})),
|
})),
|
||||||
likes: p.likes,
|
likes: p.likes,
|
||||||
comments: p.comments,
|
comments: p.comments,
|
||||||
|
location: p.location,
|
||||||
linkTitle: (p as any).linkTitle,
|
linkTitle: (p as any).linkTitle,
|
||||||
linkUrl: (p as any).linkUrl
|
linkUrl: (p as any).linkUrl
|
||||||
}))
|
}))
|
||||||
@@ -1397,6 +1530,7 @@ class SnsService {
|
|||||||
})),
|
})),
|
||||||
likes: post.likes,
|
likes: post.likes,
|
||||||
comments: post.comments,
|
comments: post.comments,
|
||||||
|
location: post.location,
|
||||||
likesDetail,
|
likesDetail,
|
||||||
commentsDetail,
|
commentsDetail,
|
||||||
linkTitle: (post as any).linkTitle,
|
linkTitle: (post as any).linkTitle,
|
||||||
@@ -1479,6 +1613,27 @@ class SnsService {
|
|||||||
const ch = name.charAt(0)
|
const ch = name.charAt(0)
|
||||||
return escapeHtml(ch || '?')
|
return escapeHtml(ch || '?')
|
||||||
}
|
}
|
||||||
|
const normalizeLocationText = (value?: string): string => (
|
||||||
|
decodeXmlText(String(value || '')).replace(/\s+/g, ' ').trim()
|
||||||
|
)
|
||||||
|
const resolveLocationText = (location?: SnsLocation): string => {
|
||||||
|
if (!location) return ''
|
||||||
|
const primaryCandidates = [
|
||||||
|
normalizeLocationText(location.poiName),
|
||||||
|
normalizeLocationText(location.poiAddressName),
|
||||||
|
normalizeLocationText(location.label),
|
||||||
|
normalizeLocationText(location.poiAddress)
|
||||||
|
].filter(Boolean)
|
||||||
|
const primary = primaryCandidates[0] || ''
|
||||||
|
const region = [
|
||||||
|
normalizeLocationText(location.country),
|
||||||
|
normalizeLocationText(location.city)
|
||||||
|
].filter(Boolean).join(' ')
|
||||||
|
if (primary && region && !primary.includes(region)) {
|
||||||
|
return `${primary} · ${region}`
|
||||||
|
}
|
||||||
|
return primary || region
|
||||||
|
}
|
||||||
|
|
||||||
let filterInfo = ''
|
let filterInfo = ''
|
||||||
if (filters.keyword) filterInfo += `关键词: "${escapeHtml(filters.keyword)}" `
|
if (filters.keyword) filterInfo += `关键词: "${escapeHtml(filters.keyword)}" `
|
||||||
@@ -1502,6 +1657,10 @@ class SnsService {
|
|||||||
const linkHtml = post.linkTitle && post.linkUrl
|
const linkHtml = post.linkTitle && post.linkUrl
|
||||||
? `<a class="lk" href="${escapeHtml(post.linkUrl)}" target="_blank"><span class="lk-t">${escapeHtml(post.linkTitle)}</span><span class="lk-a">›</span></a>`
|
? `<a class="lk" href="${escapeHtml(post.linkUrl)}" target="_blank"><span class="lk-t">${escapeHtml(post.linkTitle)}</span><span class="lk-a">›</span></a>`
|
||||||
: ''
|
: ''
|
||||||
|
const locationText = resolveLocationText(post.location)
|
||||||
|
const locationHtml = locationText
|
||||||
|
? `<div class="loc"><span class="loc-i">📍</span><span class="loc-t">${escapeHtml(locationText)}</span></div>`
|
||||||
|
: ''
|
||||||
|
|
||||||
const likesHtml = post.likes.length > 0
|
const likesHtml = post.likes.length > 0
|
||||||
? `<div class="interactions"><div class="likes">♥ ${post.likes.map(l => `<span>${escapeHtml(l)}</span>`).join('、')}</div></div>`
|
? `<div class="interactions"><div class="likes">♥ ${post.likes.map(l => `<span>${escapeHtml(l)}</span>`).join('、')}</div></div>`
|
||||||
@@ -1524,6 +1683,7 @@ ${avatarHtml}
|
|||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="hd"><span class="nick">${escapeHtml(post.nickname)}</span><span class="tm">${formatTime(post.createTime)}</span></div>
|
<div class="hd"><span class="nick">${escapeHtml(post.nickname)}</span><span class="tm">${formatTime(post.createTime)}</span></div>
|
||||||
${post.contentDesc ? `<div class="txt">${escapeHtml(post.contentDesc)}</div>` : ''}
|
${post.contentDesc ? `<div class="txt">${escapeHtml(post.contentDesc)}</div>` : ''}
|
||||||
|
${locationHtml}
|
||||||
${mediaHtml ? `<div class="mg ${gridClass}">${mediaHtml}</div>` : ''}
|
${mediaHtml ? `<div class="mg ${gridClass}">${mediaHtml}</div>` : ''}
|
||||||
${linkHtml}
|
${linkHtml}
|
||||||
${likesHtml}
|
${likesHtml}
|
||||||
@@ -1559,6 +1719,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hira
|
|||||||
.nick{font-size:15px;font-weight:700;color:var(--accent);margin-bottom:2px}
|
.nick{font-size:15px;font-weight:700;color:var(--accent);margin-bottom:2px}
|
||||||
.tm{font-size:12px;color:var(--t3)}
|
.tm{font-size:12px;color:var(--t3)}
|
||||||
.txt{font-size:15px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin-bottom:12px}
|
.txt{font-size:15px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin-bottom:12px}
|
||||||
|
.loc{display:flex;align-items:flex-start;gap:6px;font-size:13px;color:var(--t2);margin:-4px 0 12px}
|
||||||
|
.loc-i{line-height:1.3}
|
||||||
|
.loc-t{line-height:1.45;word-break:break-word}
|
||||||
|
|
||||||
/* 媒体网格 */
|
/* 媒体网格 */
|
||||||
.mg{display:grid;gap:6px;margin-bottom:12px;max-width:320px}
|
.mg{display:grid;gap:6px;margin-bottom:12px;max-width:320px}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export class WcdbCore {
|
|||||||
private wcdbListMediaDbs: any = null
|
private wcdbListMediaDbs: any = null
|
||||||
private wcdbGetMessageById: any = null
|
private wcdbGetMessageById: any = null
|
||||||
private wcdbGetEmoticonCdnUrl: any = null
|
private wcdbGetEmoticonCdnUrl: any = null
|
||||||
|
private wcdbGetEmoticonCaption: any = null
|
||||||
|
private wcdbGetEmoticonCaptionStrict: any = null
|
||||||
private wcdbGetDbStatus: any = null
|
private wcdbGetDbStatus: any = null
|
||||||
private wcdbGetVoiceData: any = null
|
private wcdbGetVoiceData: any = null
|
||||||
private wcdbGetVoiceDataBatch: any = null
|
private wcdbGetVoiceDataBatch: any = null
|
||||||
@@ -296,6 +298,24 @@ export class WcdbCore {
|
|||||||
return candidates[0] || libName
|
return candidates[0] || libName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatInitProtectionError(code: number): string {
|
||||||
|
switch (code) {
|
||||||
|
case -101: return '安全校验失败:授权已过期(-101)'
|
||||||
|
case -102: return '安全校验失败:关键环境文件缺失(-102)'
|
||||||
|
case -2201: return '安全校验失败:未找到签名清单(-2201)'
|
||||||
|
case -2202: return '安全校验失败:缺少签名文件(-2202)'
|
||||||
|
case -2203: return '安全校验失败:读取签名清单失败(-2203)'
|
||||||
|
case -2204: return '安全校验失败:读取签名文件失败(-2204)'
|
||||||
|
case -2205: return '安全校验失败:签名内容格式无效(-2205)'
|
||||||
|
case -2206: return '安全校验失败:签名清单解析失败(-2206)'
|
||||||
|
case -2207: return '安全校验失败:清单平台与当前平台不匹配(-2207)'
|
||||||
|
case -2208: return '安全校验失败:目标文件哈希读取失败(-2208)'
|
||||||
|
case -2209: return '安全校验失败:目标文件哈希不匹配(-2209)'
|
||||||
|
case -2210: return '安全校验失败:签名无效(-2210)'
|
||||||
|
default: return `安全校验失败(错误码: ${code})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private isLogEnabled(): boolean {
|
private isLogEnabled(): boolean {
|
||||||
// 移除 Worker 线程的日志禁用逻辑,允许在 Worker 中记录日志
|
// 移除 Worker 线程的日志禁用逻辑,允许在 Worker 中记录日志
|
||||||
if (process.env.WCDB_LOG_ENABLED === '1') return true
|
if (process.env.WCDB_LOG_ENABLED === '1') return true
|
||||||
@@ -621,7 +641,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
// InitProtection (Added for security)
|
// InitProtection (Added for security)
|
||||||
try {
|
try {
|
||||||
this.wcdbInitProtection = this.lib.func('bool InitProtection(const char* resourcePath)')
|
this.wcdbInitProtection = this.lib.func('int32 InitProtection(const char* resourcePath)')
|
||||||
|
|
||||||
// 尝试多个可能的资源路径
|
// 尝试多个可能的资源路径
|
||||||
const resourcePaths = [
|
const resourcePaths = [
|
||||||
@@ -634,26 +654,29 @@ export class WcdbCore {
|
|||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
let protectionOk = false
|
let protectionOk = false
|
||||||
|
let protectionCode = -1
|
||||||
for (const resPath of resourcePaths) {
|
for (const resPath of resourcePaths) {
|
||||||
try {
|
try {
|
||||||
//
|
protectionCode = Number(this.wcdbInitProtection(resPath))
|
||||||
protectionOk = this.wcdbInitProtection(resPath)
|
if (protectionCode === 0) {
|
||||||
if (protectionOk) {
|
protectionOk = true
|
||||||
//
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
this.writeLog(`[bootstrap] InitProtection rc=${protectionCode} path=${resPath}`, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.warn(`[WCDB] InitProtection 失败 (${resPath}):`, e)
|
this.writeLog(`[bootstrap] InitProtection exception path=${resPath}: ${String(e)}`, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!protectionOk) {
|
if (!protectionOk) {
|
||||||
// console.warn('[WCDB] Core security check failed - 继续运行但可能不稳定')
|
lastDllInitError = this.formatInitProtectionError(protectionCode)
|
||||||
// this.writeLog('InitProtection 失败,继续运行')
|
this.writeLog(`[bootstrap] InitProtection failed finalCode=${protectionCode}`, true)
|
||||||
// 不返回 false,允许继续运行
|
return false
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.warn('InitProtection symbol not found:', e)
|
lastDllInitError = `InitProtection symbol not found: ${String(e)}`
|
||||||
|
this.writeLog(`[bootstrap] InitProtection symbol load failed: ${String(e)}`, true)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义类型
|
// 定义类型
|
||||||
@@ -852,6 +875,22 @@ export class WcdbCore {
|
|||||||
// wcdb_status wcdb_get_emoticon_cdn_url(wcdb_handle handle, const char* db_path, const char* md5, char** out_url)
|
// wcdb_status wcdb_get_emoticon_cdn_url(wcdb_handle handle, const char* db_path, const char* md5, char** out_url)
|
||||||
this.wcdbGetEmoticonCdnUrl = this.lib.func('int32 wcdb_get_emoticon_cdn_url(int64 handle, const char* dbPath, const char* md5, _Out_ void** outUrl)')
|
this.wcdbGetEmoticonCdnUrl = this.lib.func('int32 wcdb_get_emoticon_cdn_url(int64 handle, const char* dbPath, const char* md5, _Out_ void** outUrl)')
|
||||||
|
|
||||||
|
// wcdb_status wcdb_get_emoticon_caption(wcdb_handle handle, const char* db_path, const char* md5, char** out_caption)
|
||||||
|
try {
|
||||||
|
this.wcdbGetEmoticonCaption = this.lib.func('int32 wcdb_get_emoticon_caption(int64 handle, const char* dbPath, const char* md5, _Out_ void** outCaption)')
|
||||||
|
} catch (e) {
|
||||||
|
this.wcdbGetEmoticonCaption = null
|
||||||
|
this.writeLog(`[diag:emoji] symbol missing wcdb_get_emoticon_caption: ${String(e)}`, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcdb_status wcdb_get_emoticon_caption_strict(wcdb_handle handle, const char* md5, char** out_caption)
|
||||||
|
try {
|
||||||
|
this.wcdbGetEmoticonCaptionStrict = this.lib.func('int32 wcdb_get_emoticon_caption_strict(int64 handle, const char* md5, _Out_ void** outCaption)')
|
||||||
|
} catch (e) {
|
||||||
|
this.wcdbGetEmoticonCaptionStrict = null
|
||||||
|
this.writeLog(`[diag:emoji] symbol missing wcdb_get_emoticon_caption_strict: ${String(e)}`, true)
|
||||||
|
}
|
||||||
|
|
||||||
// wcdb_status wcdb_list_message_dbs(wcdb_handle handle, char** out_json)
|
// wcdb_status wcdb_list_message_dbs(wcdb_handle handle, char** out_json)
|
||||||
this.wcdbListMessageDbs = this.lib.func('int32 wcdb_list_message_dbs(int64 handle, _Out_ void** outJson)')
|
this.wcdbListMessageDbs = this.lib.func('int32 wcdb_list_message_dbs(int64 handle, _Out_ void** outJson)')
|
||||||
|
|
||||||
@@ -2700,6 +2739,48 @@ export class WcdbCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEmoticonCaption(dbPath: string, md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
||||||
|
if (!this.ensureReady()) {
|
||||||
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
|
}
|
||||||
|
if (!this.wcdbGetEmoticonCaption) {
|
||||||
|
return { success: false, error: '接口未就绪: wcdb_get_emoticon_caption' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbGetEmoticonCaption(this.handle, dbPath || '', md5, outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) {
|
||||||
|
return { success: false, error: `获取表情释义失败: ${result}` }
|
||||||
|
}
|
||||||
|
const captionStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (captionStr === null) return { success: false, error: '解析表情释义失败' }
|
||||||
|
return { success: true, caption: captionStr || undefined }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
||||||
|
if (!this.ensureReady()) {
|
||||||
|
return { success: false, error: 'WCDB 未连接' }
|
||||||
|
}
|
||||||
|
if (!this.wcdbGetEmoticonCaptionStrict) {
|
||||||
|
return { success: false, error: '接口未就绪: wcdb_get_emoticon_caption_strict' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const outPtr = [null as any]
|
||||||
|
const result = this.wcdbGetEmoticonCaptionStrict(this.handle, md5, outPtr)
|
||||||
|
if (result !== 0 || !outPtr[0]) {
|
||||||
|
return { success: false, error: `获取表情释义失败(strict): ${result}` }
|
||||||
|
}
|
||||||
|
const captionStr = this.decodeJsonPtr(outPtr[0])
|
||||||
|
if (captionStr === null) return { success: false, error: '解析表情释义失败(strict)' }
|
||||||
|
return { success: true, caption: captionStr || undefined }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async listMessageDbs(): Promise<{ success: boolean; data?: string[]; error?: string }> {
|
async listMessageDbs(): Promise<{ success: boolean; data?: string[]; error?: string }> {
|
||||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -455,6 +455,20 @@ export class WcdbService {
|
|||||||
return this.callWorker('getEmoticonCdnUrl', { dbPath, md5 })
|
return this.callWorker('getEmoticonCdnUrl', { dbPath, md5 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表情包释义
|
||||||
|
*/
|
||||||
|
async getEmoticonCaption(dbPath: string, md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
||||||
|
return this.callWorker('getEmoticonCaption', { dbPath, md5 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表情包释义(严格 DLL 接口)
|
||||||
|
*/
|
||||||
|
async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> {
|
||||||
|
return this.callWorker('getEmoticonCaptionStrict', { md5 })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出消息数据库
|
* 列出消息数据库
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ if (parentPort) {
|
|||||||
case 'getEmoticonCdnUrl':
|
case 'getEmoticonCdnUrl':
|
||||||
result = await core.getEmoticonCdnUrl(payload.dbPath, payload.md5)
|
result = await core.getEmoticonCdnUrl(payload.dbPath, payload.md5)
|
||||||
break
|
break
|
||||||
|
case 'getEmoticonCaption':
|
||||||
|
result = await core.getEmoticonCaption(payload.dbPath, payload.md5)
|
||||||
|
break
|
||||||
|
case 'getEmoticonCaptionStrict':
|
||||||
|
result = await core.getEmoticonCaptionStrict(payload.md5)
|
||||||
|
break
|
||||||
case 'listMessageDbs':
|
case 'listMessageDbs':
|
||||||
result = await core.listMessageDbs()
|
result = await core.listMessageDbs()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.WeFlow.app",
|
"appId": "com.WeFlow.app",
|
||||||
|
"afterPack": "scripts/afterPack-sign-manifest.cjs",
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "hicccc77",
|
"owner": "hicccc77",
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react'
|
import React, { useState, useMemo, useEffect } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Heart, ChevronRight, ImageIcon, Code, Trash2 } from 'lucide-react'
|
import { Heart, ChevronRight, ImageIcon, Code, Trash2, MapPin } from 'lucide-react'
|
||||||
import { SnsPost, SnsLinkCardData } from '../../types/sns'
|
import { SnsPost, SnsLinkCardData, SnsLocation } from '../../types/sns'
|
||||||
import { Avatar } from '../Avatar'
|
import { Avatar } from '../Avatar'
|
||||||
import { SnsMediaGrid } from './SnsMediaGrid'
|
import { SnsMediaGrid } from './SnsMediaGrid'
|
||||||
import { getEmojiPath } from 'wechat-emojis'
|
import { getEmojiPath } from 'wechat-emojis'
|
||||||
@@ -134,6 +134,30 @@ const buildLinkCardData = (post: SnsPost): SnsLinkCardData | null => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildLocationText = (location?: SnsLocation): string => {
|
||||||
|
if (!location) return ''
|
||||||
|
|
||||||
|
const normalize = (value?: string): string => (
|
||||||
|
decodeHtmlEntities(String(value || '')).replace(/\s+/g, ' ').trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
const primary = [
|
||||||
|
normalize(location.poiName),
|
||||||
|
normalize(location.poiAddressName),
|
||||||
|
normalize(location.label),
|
||||||
|
normalize(location.poiAddress)
|
||||||
|
].find(Boolean) || ''
|
||||||
|
|
||||||
|
const region = [normalize(location.country), normalize(location.city)]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
if (primary && region && !primary.includes(region)) {
|
||||||
|
return `${primary} · ${region}`
|
||||||
|
}
|
||||||
|
return primary || region
|
||||||
|
}
|
||||||
|
|
||||||
const SnsLinkCard = ({ card }: { card: SnsLinkCardData }) => {
|
const SnsLinkCard = ({ card }: { card: SnsLinkCardData }) => {
|
||||||
const [thumbFailed, setThumbFailed] = useState(false)
|
const [thumbFailed, setThumbFailed] = useState(false)
|
||||||
const hostname = useMemo(() => {
|
const hostname = useMemo(() => {
|
||||||
@@ -254,6 +278,7 @@ export const SnsPostItem: React.FC<SnsPostItemProps> = ({ post, onPreview, onDeb
|
|||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
const linkCard = buildLinkCardData(post)
|
const linkCard = buildLinkCardData(post)
|
||||||
|
const locationText = useMemo(() => buildLocationText(post.location), [post.location])
|
||||||
const hasVideoMedia = post.type === 15 || post.media.some((item) => isSnsVideoUrl(item.url))
|
const hasVideoMedia = post.type === 15 || post.media.some((item) => isSnsVideoUrl(item.url))
|
||||||
const showLinkCard = Boolean(linkCard) && post.media.length <= 1 && !hasVideoMedia
|
const showLinkCard = Boolean(linkCard) && post.media.length <= 1 && !hasVideoMedia
|
||||||
const showMediaGrid = post.media.length > 0 && !showLinkCard
|
const showMediaGrid = post.media.length > 0 && !showLinkCard
|
||||||
@@ -379,6 +404,13 @@ export const SnsPostItem: React.FC<SnsPostItemProps> = ({ post, onPreview, onDeb
|
|||||||
<div className="post-text">{renderTextWithEmoji(decodeHtmlEntities(post.contentDesc))}</div>
|
<div className="post-text">{renderTextWithEmoji(decodeHtmlEntities(post.contentDesc))}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{locationText && (
|
||||||
|
<div className="post-location" title={locationText}>
|
||||||
|
<MapPin size={14} />
|
||||||
|
<span className="post-location-text">{locationText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showLinkCard && linkCard && (
|
{showLinkCard && linkCard && (
|
||||||
<SnsLinkCard card={linkCard} />
|
<SnsLinkCard card={linkCard} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1026,7 +1026,7 @@ const toSessionRowsWithContacts = (
|
|||||||
kind: toKindByContact(contact),
|
kind: toKindByContact(contact),
|
||||||
wechatId: contact.username,
|
wechatId: contact.username,
|
||||||
displayName: contact.displayName || session?.displayName || contact.username,
|
displayName: contact.displayName || session?.displayName || contact.username,
|
||||||
avatarUrl: contact.avatarUrl || session?.avatarUrl,
|
avatarUrl: session?.avatarUrl || contact.avatarUrl,
|
||||||
hasSession: Boolean(session)
|
hasSession: Boolean(session)
|
||||||
} as SessionRow
|
} as SessionRow
|
||||||
})
|
})
|
||||||
@@ -1046,7 +1046,7 @@ const toSessionRowsWithContacts = (
|
|||||||
kind: toKindByContactType(session, contact),
|
kind: toKindByContactType(session, contact),
|
||||||
wechatId: contact?.username || session.username,
|
wechatId: contact?.username || session.username,
|
||||||
displayName: contact?.displayName || session.displayName || session.username,
|
displayName: contact?.displayName || session.displayName || session.username,
|
||||||
avatarUrl: contact?.avatarUrl || session.avatarUrl,
|
avatarUrl: session.avatarUrl || contact?.avatarUrl,
|
||||||
hasSession: true
|
hasSession: true
|
||||||
} as SessionRow
|
} as SessionRow
|
||||||
})
|
})
|
||||||
@@ -5582,6 +5582,45 @@ function ExportPage() {
|
|||||||
return map
|
return map
|
||||||
}, [contactsList])
|
}, [contactsList])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSessionDetailPanel) return
|
||||||
|
const sessionId = String(sessionDetail?.wxid || '').trim()
|
||||||
|
if (!sessionId) return
|
||||||
|
|
||||||
|
const mappedSession = sessionRowByUsername.get(sessionId)
|
||||||
|
const mappedContact = contactByUsername.get(sessionId)
|
||||||
|
if (!mappedSession && !mappedContact) return
|
||||||
|
|
||||||
|
setSessionDetail((prev) => {
|
||||||
|
if (!prev || prev.wxid !== sessionId) return prev
|
||||||
|
|
||||||
|
const nextDisplayName = mappedSession?.displayName || mappedContact?.displayName || prev.displayName || sessionId
|
||||||
|
const nextRemark = mappedContact?.remark ?? prev.remark
|
||||||
|
const nextNickName = mappedContact?.nickname ?? prev.nickName
|
||||||
|
const nextAlias = mappedContact?.alias ?? prev.alias
|
||||||
|
const nextAvatarUrl = mappedSession?.avatarUrl || mappedContact?.avatarUrl || prev.avatarUrl
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextDisplayName === prev.displayName &&
|
||||||
|
nextRemark === prev.remark &&
|
||||||
|
nextNickName === prev.nickName &&
|
||||||
|
nextAlias === prev.alias &&
|
||||||
|
nextAvatarUrl === prev.avatarUrl
|
||||||
|
) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
displayName: nextDisplayName,
|
||||||
|
remark: nextRemark,
|
||||||
|
nickName: nextNickName,
|
||||||
|
alias: nextAlias,
|
||||||
|
avatarUrl: nextAvatarUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [contactByUsername, sessionDetail?.wxid, sessionRowByUsername, showSessionDetailPanel])
|
||||||
|
|
||||||
const currentSessionExportRecords = useMemo(() => {
|
const currentSessionExportRecords = useMemo(() => {
|
||||||
const sessionId = String(sessionDetail?.wxid || '').trim()
|
const sessionId = String(sessionDetail?.wxid || '').trim()
|
||||||
if (!sessionId) return [] as configService.ExportSessionRecordEntry[]
|
if (!sessionId) return [] as configService.ExportSessionRecordEntry[]
|
||||||
@@ -5987,7 +6026,11 @@ function ExportPage() {
|
|||||||
loadSnsUserPostCounts({ force: true })
|
loadSnsUserPostCounts({ force: true })
|
||||||
])
|
])
|
||||||
|
|
||||||
if (String(sessionDetail?.wxid || '').trim()) {
|
const currentDetailSessionId = showSessionDetailPanel
|
||||||
|
? String(sessionDetail?.wxid || '').trim()
|
||||||
|
: ''
|
||||||
|
if (currentDetailSessionId) {
|
||||||
|
await loadSessionDetail(currentDetailSessionId)
|
||||||
void loadSessionRelationStats({ forceRefresh: true })
|
void loadSessionRelationStats({ forceRefresh: true })
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -5998,11 +6041,13 @@ function ExportPage() {
|
|||||||
filteredContacts,
|
filteredContacts,
|
||||||
isSessionCountStageReady,
|
isSessionCountStageReady,
|
||||||
loadContactsList,
|
loadContactsList,
|
||||||
|
loadSessionDetail,
|
||||||
loadSessionRelationStats,
|
loadSessionRelationStats,
|
||||||
loadSnsStats,
|
loadSnsStats,
|
||||||
loadSnsUserPostCounts,
|
loadSnsUserPostCounts,
|
||||||
resetSessionMutualFriendsLoader,
|
resetSessionMutualFriendsLoader,
|
||||||
scheduleSessionMutualFriendsWorker,
|
scheduleSessionMutualFriendsWorker,
|
||||||
|
showSessionDetailPanel,
|
||||||
sessionDetail?.wxid
|
sessionDetail?.wxid
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -759,6 +759,26 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-location {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
margin: -4px 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-location-text {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.post-media-container {
|
.post-media-container {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/types/electron.d.ts
vendored
10
src/types/electron.d.ts
vendored
@@ -790,6 +790,16 @@ export interface ElectronAPI {
|
|||||||
}>
|
}>
|
||||||
likes: Array<string>
|
likes: Array<string>
|
||||||
comments: Array<{ id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> }>
|
comments: Array<{ id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> }>
|
||||||
|
location?: {
|
||||||
|
latitude?: number
|
||||||
|
longitude?: number
|
||||||
|
city?: string
|
||||||
|
country?: string
|
||||||
|
poiName?: string
|
||||||
|
poiAddress?: string
|
||||||
|
poiAddressName?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
rawXml?: string
|
rawXml?: string
|
||||||
}>
|
}>
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ export interface SnsComment {
|
|||||||
emojis?: SnsCommentEmoji[]
|
emojis?: SnsCommentEmoji[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SnsLocation {
|
||||||
|
latitude?: number
|
||||||
|
longitude?: number
|
||||||
|
city?: string
|
||||||
|
country?: string
|
||||||
|
poiName?: string
|
||||||
|
poiAddress?: string
|
||||||
|
poiAddressName?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SnsPost {
|
export interface SnsPost {
|
||||||
id: string
|
id: string
|
||||||
tid?: string // 数据库主键(雪花 ID),用于精确删除
|
tid?: string // 数据库主键(雪花 ID),用于精确删除
|
||||||
@@ -46,6 +57,7 @@ export interface SnsPost {
|
|||||||
media: SnsMedia[]
|
media: SnsMedia[]
|
||||||
likes: string[]
|
likes: string[]
|
||||||
comments: SnsComment[]
|
comments: SnsComment[]
|
||||||
|
location?: SnsLocation
|
||||||
rawXml?: string
|
rawXml?: string
|
||||||
linkTitle?: string
|
linkTitle?: string
|
||||||
linkUrl?: string
|
linkUrl?: string
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import electron from 'vite-plugin-electron'
|
|||||||
import renderer from 'vite-plugin-electron-renderer'
|
import renderer from 'vite-plugin-electron-renderer'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
const handleElectronOnStart = (options: { reload: () => void }) => {
|
||||||
|
options.reload()
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
server: {
|
server: {
|
||||||
@@ -23,6 +27,7 @@ export default defineConfig({
|
|||||||
electron([
|
electron([
|
||||||
{
|
{
|
||||||
entry: 'electron/main.ts',
|
entry: 'electron/main.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -43,6 +48,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/annualReportWorker.ts',
|
entry: 'electron/annualReportWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -61,6 +67,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/dualReportWorker.ts',
|
entry: 'electron/dualReportWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -79,6 +86,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/imageSearchWorker.ts',
|
entry: 'electron/imageSearchWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -93,6 +101,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/wcdbWorker.ts',
|
entry: 'electron/wcdbWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -112,6 +121,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/transcribeWorker.ts',
|
entry: 'electron/transcribeWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -129,6 +139,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/exportWorker.ts',
|
entry: 'electron/exportWorker.ts',
|
||||||
|
onstart: handleElectronOnStart,
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
@@ -149,9 +160,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/preload.ts',
|
entry: 'electron/preload.ts',
|
||||||
onstart(options) {
|
onstart: handleElectronOnStart,
|
||||||
options.reload()
|
|
||||||
},
|
|
||||||
vite: {
|
vite: {
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-electron'
|
outDir: 'dist-electron'
|
||||||
|
|||||||
Reference in New Issue
Block a user