综合更新
This commit is contained in:
1 parent
d733c21145
commit
147f3b7045
57 files changed
+3038
-800
No files matched your search
@@ -449,6 +449,7 @@ class ChatService {
|
||||
private readonly visibilityAnomalyLogBurst = 3
|
||||
private visibilityAnomalyLogState = new Map<string, { windowStart: number; total: number; suppressed: number }>()
|
||||
private readonly contactLabelNameMapCacheTtlMs = 10 * 60 * 1000
|
||||
private connectInFlight: Promise<{ success: boolean; error?: string }> | null = null
|
||||
private contactsLoadInFlight: { mode: 'lite' | 'full'; promise: Promise<{ success: boolean; contacts?: ContactInfo[]; error?: string }> } | null = null
|
||||
private contactsMemoryCache = new Map<'lite' | 'full', { scope: string; updatedAt: number; contacts: ContactInfo[] }>()
|
||||
private readonly contactsMemoryCacheTtlMs = 3 * 60 * 1000
|
||||
@@ -561,9 +562,27 @@ class ChatService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接数据库
|
||||
* 连接数据库(并发调用共享同一次连接过程,避免重复 open)
|
||||
*/
|
||||
async connect(): Promise<{ success: boolean; error?: string }> {
|
||||
if (this.connected && wcdbService.isReady()) {
|
||||
return { success: true }
|
||||
}
|
||||
if (this.connectInFlight) {
|
||||
return this.connectInFlight
|
||||
}
|
||||
const promise = this.connectInternal()
|
||||
this.connectInFlight = promise
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
if (this.connectInFlight === promise) {
|
||||
this.connectInFlight = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async connectInternal(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const wxid = String(this.runtimeConfig?.myWxid || this.configService.get('myWxid') || '').trim()
|
||||
const dbPath = String(this.runtimeConfig?.dbPath || this.configService.get('dbPath') || '').trim()
|
||||
|
||||
@@ -5,34 +5,10 @@ import { buildGroupNicknameIdCandidates } from '../../export/contacts/groupNickn
|
||||
import { appendTransferDesc, isTransferExportContent, resolveTransferDesc } from '../../export/parsers/transferParser';
|
||||
import { formatTimestamp } from '../../export/utils/timestamp';
|
||||
import { parallelLimit } from '../../export/utils/parallelLimit';
|
||||
import { buildMarkdownBlockquote, escapeMarkdownLinkText, escapeMarkdownText, toMarkdownUrl } from '../../export/utils/markdown';
|
||||
import { ExportDisplayProfile, MediaExportItem } from '../../export/types';
|
||||
import { wcdbService } from '../../wcdbService';
|
||||
|
||||
const escapeMarkdownText = (value: unknown): string => {
|
||||
return String(value ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/([`*_{}\[\]<>#+\-.!|])/g, '\\$1')
|
||||
}
|
||||
|
||||
const escapeMarkdownLinkText = (value: unknown): string => {
|
||||
return escapeMarkdownText(value).replace(/\r?\n/g, ' ')
|
||||
}
|
||||
|
||||
const toMarkdownUrl = (value: string): string => {
|
||||
return encodeURI(value)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/</g, '%3C')
|
||||
.replace(/>/g, '%3E')
|
||||
}
|
||||
|
||||
const buildMarkdownBlockquote = (value: string): string => {
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.map(line => `> ${line ? escapeMarkdownText(line) : ''}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
export class MarkdownFormatter {
|
||||
constructor(private exportService: any) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const escapeMarkdownText = (value: unknown): string => {
|
||||
return String(value ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/([`*_{}\[\]<>#+\-.!|])/g, '\\$1')
|
||||
}
|
||||
|
||||
export const escapeMarkdownLinkText = (value: unknown): string => {
|
||||
return escapeMarkdownText(value).replace(/\r?\n/g, ' ')
|
||||
}
|
||||
|
||||
export const toMarkdownUrl = (value: string): string => {
|
||||
return encodeURI(value)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/</g, '%3C')
|
||||
.replace(/>/g, '%3E')
|
||||
}
|
||||
|
||||
export const buildMarkdownBlockquote = (value: string): string => {
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.map(line => `> ${line ? escapeMarkdownText(line) : ''}`)
|
||||
.join('\n')
|
||||
}
|
||||
@@ -1453,9 +1453,9 @@ class HttpService {
|
||||
}
|
||||
|
||||
const rawFormat = String(url.searchParams.get('format') || 'json').trim().toLowerCase()
|
||||
const format = rawFormat === 'arkme-json' ? 'arkmejson' : rawFormat
|
||||
if (!['json', 'html', 'arkmejson'].includes(format)) {
|
||||
this.sendError(res, 400, 'Invalid format, supported: json/html/arkmejson')
|
||||
const format = rawFormat === 'arkme-json' ? 'arkmejson' : rawFormat === 'md' ? 'markdown' : rawFormat
|
||||
if (!['json', 'html', 'arkmejson', 'markdown'].includes(format)) {
|
||||
this.sendError(res, 400, 'Invalid format, supported: json/html/arkmejson/markdown')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1466,7 +1466,7 @@ class HttpService {
|
||||
|
||||
const options: {
|
||||
outputDir: string
|
||||
format: 'json' | 'html' | 'arkmejson'
|
||||
format: 'json' | 'html' | 'arkmejson' | 'markdown'
|
||||
usernames?: string[]
|
||||
keyword?: string
|
||||
exportMedia?: boolean
|
||||
@@ -1477,7 +1477,7 @@ class HttpService {
|
||||
endTime?: number
|
||||
} = {
|
||||
outputDir,
|
||||
format: format as 'json' | 'html' | 'arkmejson',
|
||||
format: format as 'json' | 'html' | 'arkmejson' | 'markdown',
|
||||
usernames,
|
||||
keyword,
|
||||
exportMedia: this.parseBooleanParam(url, ['exportMedia'], false)
|
||||
|
||||
+125
-23
@@ -8,6 +8,7 @@ import { basename, join } from 'path'
|
||||
import crypto from 'crypto'
|
||||
import { WasmService } from './wasmService'
|
||||
import zlib from 'zlib'
|
||||
import { escapeMarkdownLinkText, escapeMarkdownText, toMarkdownUrl } from './export/utils/markdown'
|
||||
|
||||
export interface SnsLivePhoto {
|
||||
url: string
|
||||
@@ -332,6 +333,30 @@ const decodeXmlText = (text: string): string => {
|
||||
.replace(/'/gi, "'")
|
||||
}
|
||||
|
||||
const normalizeSnsLocationText = (value?: string): string => (
|
||||
decodeXmlText(String(value || '')).replace(/\s+/g, ' ').trim()
|
||||
)
|
||||
|
||||
/** 解析朋友圈位置的展示文本(POI 优先,附加国家/城市信息) */
|
||||
const resolveSnsLocationText = (location?: SnsLocation): string => {
|
||||
if (!location) return ''
|
||||
const primaryCandidates = [
|
||||
normalizeSnsLocationText(location.poiName),
|
||||
normalizeSnsLocationText(location.poiAddressName),
|
||||
normalizeSnsLocationText(location.label),
|
||||
normalizeSnsLocationText(location.poiAddress)
|
||||
].filter(Boolean)
|
||||
const primary = primaryCandidates[0] || ''
|
||||
const region = [
|
||||
normalizeSnsLocationText(location.country),
|
||||
normalizeSnsLocationText(location.city)
|
||||
].filter(Boolean).join(' ')
|
||||
if (primary && region && !primary.includes(region)) {
|
||||
return `${primary} · ${region}`
|
||||
}
|
||||
return primary || region
|
||||
}
|
||||
|
||||
class SnsService {
|
||||
private configService: ConfigService
|
||||
private contactCache: ContactCacheService
|
||||
@@ -1358,7 +1383,7 @@ class SnsService {
|
||||
*/
|
||||
async exportTimeline(options: {
|
||||
outputDir: string
|
||||
format: 'json' | 'html' | 'arkmejson'
|
||||
format: 'json' | 'html' | 'arkmejson' | 'markdown'
|
||||
usernames?: string[]
|
||||
keyword?: string
|
||||
exportMedia?: boolean
|
||||
@@ -1735,6 +1760,11 @@ class SnsService {
|
||||
}
|
||||
recordCreatedFileBeforeWrite(outputFilePath)
|
||||
await writeFile(outputFilePath, JSON.stringify(exportData, null, 2), 'utf-8')
|
||||
} else if (format === 'markdown') {
|
||||
outputFilePath = join(outputDir, `朋友圈导出_${timestamp}.md`)
|
||||
const markdown = this.generateMarkdown(allPosts, { usernames, keyword })
|
||||
recordCreatedFileBeforeWrite(outputFilePath)
|
||||
await writeFile(outputFilePath, markdown, 'utf-8')
|
||||
} else {
|
||||
// HTML 格式
|
||||
outputFilePath = join(outputDir, `朋友圈导出_${timestamp}.html`)
|
||||
@@ -1752,6 +1782,99 @@ class SnsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成朋友圈 Markdown 导出文件
|
||||
*/
|
||||
private generateMarkdown(posts: SnsPost[], filters: { usernames?: string[]; keyword?: string }): string {
|
||||
const formatTime = (ts: number) => {
|
||||
const d = new Date(ts * 1000)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
// 逐行转义正文,行尾补两个空格以在渲染时保留换行
|
||||
const formatMultilineText = (text: string): string => text
|
||||
.split(/\r?\n/)
|
||||
.map(line => escapeMarkdownText(line))
|
||||
.join(' \n')
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push('# 朋友圈导出')
|
||||
lines.push('')
|
||||
lines.push(`- 动态数量: ${posts.length}`)
|
||||
if (filters.keyword) lines.push(`- 筛选关键词: ${escapeMarkdownText(filters.keyword)}`)
|
||||
if (filters.usernames && filters.usernames.length > 0) lines.push(`- 筛选用户: ${filters.usernames.length} 人`)
|
||||
lines.push(`- 导出时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||
lines.push('- 导出工具: WeFlow')
|
||||
|
||||
for (const post of posts) {
|
||||
lines.push('')
|
||||
lines.push('---')
|
||||
lines.push('')
|
||||
lines.push(`## ${escapeMarkdownText(post.nickname)} · ${formatTime(post.createTime)}`)
|
||||
lines.push('')
|
||||
|
||||
if (post.contentDesc) {
|
||||
lines.push(formatMultilineText(post.contentDesc))
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
const locationText = resolveSnsLocationText(post.location)
|
||||
if (locationText) {
|
||||
lines.push(`📍 ${escapeMarkdownText(locationText)}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
const mediaLines: string[] = []
|
||||
post.media.forEach((m, mi) => {
|
||||
const indexSuffix = post.media.length > 1 ? ` ${mi + 1}` : ''
|
||||
const localPath = (m as any).localPath as string | undefined
|
||||
const isVideo = isVideoUrl(m.url)
|
||||
if (localPath) {
|
||||
mediaLines.push(isVideo
|
||||
? `[视频${indexSuffix}](${toMarkdownUrl(localPath)})`
|
||||
: `})`)
|
||||
} else if (m.url) {
|
||||
mediaLines.push(`[${isVideo ? '视频' : '图片'}${indexSuffix}](${toMarkdownUrl(m.url)})`)
|
||||
}
|
||||
const livePhotoLocalPath = m.livePhoto ? (m.livePhoto as any).localPath as string | undefined : undefined
|
||||
if (livePhotoLocalPath) {
|
||||
mediaLines.push(`[实况视频${indexSuffix}](${toMarkdownUrl(livePhotoLocalPath)})`)
|
||||
}
|
||||
})
|
||||
if (mediaLines.length > 0) {
|
||||
// 行尾两个空格产生硬换行,让每个媒体渲染时各占一行
|
||||
lines.push(mediaLines.join(' \n'))
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (post.linkTitle && post.linkUrl) {
|
||||
lines.push(`🔗 [${escapeMarkdownLinkText(post.linkTitle)}](${toMarkdownUrl(post.linkUrl)})`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (post.likes.length > 0) {
|
||||
lines.push(`♥ ${post.likes.map(l => escapeMarkdownText(l)).join('、')}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (post.comments.length > 0) {
|
||||
lines.push('💬 评论:')
|
||||
lines.push('')
|
||||
for (const c of post.comments) {
|
||||
const ref = c.refNickname ? ` 回复 **${escapeMarkdownText(c.refNickname)}**` : ''
|
||||
lines.push(`- **${escapeMarkdownText(c.nickname)}**${ref}:${escapeMarkdownLinkText(c.content)}`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// 去掉每条动态末尾多余的空行,统一由分隔符控制间距
|
||||
while (lines[lines.length - 1] === '') lines.pop()
|
||||
}
|
||||
|
||||
lines.push('')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成朋友圈 HTML 导出文件
|
||||
*/
|
||||
@@ -1778,27 +1901,6 @@ class SnsService {
|
||||
const ch = name.charAt(0)
|
||||
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 = ''
|
||||
if (filters.keyword) filterInfo += `关键词: "${escapeHtml(filters.keyword)}" `
|
||||
@@ -1822,7 +1924,7 @@ class SnsService {
|
||||
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>`
|
||||
: ''
|
||||
const locationText = resolveLocationText(post.location)
|
||||
const locationText = resolveSnsLocationText(post.location)
|
||||
const locationHtml = locationText
|
||||
? `<div class="loc"><span class="loc-i">📍</span><span class="loc-t">${escapeHtml(locationText)}</span></div>`
|
||||
: ''
|
||||
|
||||
+28
-41
@@ -1,7 +1,11 @@
|
||||
import { Notification } from "electron";
|
||||
import { avatarFileCache, AvatarFileCacheService } from "./avatarFileCacheService";
|
||||
import { avatarFileCache } from "./avatarFileCacheService";
|
||||
|
||||
export interface LinuxNotificationData {
|
||||
// 系统通知服务(Linux / macOS):走各自系统的通知中心(Linux 底层为
|
||||
// D-Bus/libnotify,macOS 为通知中心),Windows 使用特制的液态玻璃通知窗口。
|
||||
// 声音、勿扰模式、通知历史均由系统按用户设置管理
|
||||
|
||||
export interface SystemNotificationData {
|
||||
sessionId?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
@@ -14,6 +18,9 @@ export interface LinuxNotificationData {
|
||||
|
||||
type NotificationCallback = (payload: unknown) => void;
|
||||
|
||||
const isSupportedPlatform =
|
||||
process.platform === "linux" || process.platform === "darwin";
|
||||
|
||||
let notificationCallbacks: NotificationCallback[] = [];
|
||||
let notificationCounter = 1;
|
||||
const activeNotifications: Map<number, Notification> = new Map();
|
||||
@@ -39,20 +46,20 @@ function triggerNotificationCallback(payload: unknown): void {
|
||||
try {
|
||||
callback(payload);
|
||||
} catch (error) {
|
||||
console.error("[LinuxNotification] Callback error:", error);
|
||||
console.error("[SystemNotification] Callback error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function showLinuxNotification(
|
||||
data: LinuxNotificationData,
|
||||
export async function showSystemNotification(
|
||||
data: SystemNotificationData,
|
||||
): Promise<number | null> {
|
||||
if (process.platform !== "linux") {
|
||||
if (!isSupportedPlatform) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
console.warn("[LinuxNotification] Notification API is not supported");
|
||||
console.warn("[SystemNotification] Notification API is not supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -91,12 +98,15 @@ export async function showLinuxNotification(
|
||||
});
|
||||
|
||||
notification.on("failed", (_, error) => {
|
||||
console.error("[LinuxNotification] Notification failed:", error);
|
||||
console.error("[SystemNotification] Notification failed:", error);
|
||||
clearNotificationState(notificationId);
|
||||
});
|
||||
|
||||
// Linux 的部分通知服务不会自动过期,需要手动关闭兜底;
|
||||
// macOS 横幅由系统自动收起并保留在通知中心,手动 close 反而会把它
|
||||
// 从通知中心移除,因此不做超时关闭
|
||||
const expireTimeout = data.expireTimeout ?? 5000;
|
||||
if (expireTimeout > 0) {
|
||||
if (process.platform === "linux" && expireTimeout > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
const currentNotification = activeNotifications.get(notificationId);
|
||||
if (currentNotification) {
|
||||
@@ -109,37 +119,16 @@ export async function showLinuxNotification(
|
||||
notification.show();
|
||||
|
||||
console.log(
|
||||
`[LinuxNotification] Shown notification ${notificationId}: ${data.title}`,
|
||||
`[SystemNotification] Shown notification ${notificationId}: ${data.title}`,
|
||||
);
|
||||
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error("[LinuxNotification] Failed to show notification:", error);
|
||||
console.error("[SystemNotification] Failed to show notification:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeLinuxNotification(
|
||||
notificationId: number,
|
||||
): Promise<void> {
|
||||
const notification = activeNotifications.get(notificationId);
|
||||
if (!notification) return;
|
||||
notification.close();
|
||||
clearNotificationState(notificationId);
|
||||
}
|
||||
|
||||
export async function getCapabilities(): Promise<string[]> {
|
||||
if (process.platform !== "linux") {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ["native-notification", "click"];
|
||||
}
|
||||
|
||||
export function onNotificationAction(callback: NotificationCallback): void {
|
||||
notificationCallbacks.push(callback);
|
||||
}
|
||||
@@ -153,23 +142,21 @@ export function removeNotificationCallback(
|
||||
}
|
||||
}
|
||||
|
||||
export async function initLinuxNotificationService(): Promise<void> {
|
||||
if (process.platform !== "linux") {
|
||||
console.log("[LinuxNotification] Not on Linux, skipping init");
|
||||
export async function initSystemNotificationService(): Promise<void> {
|
||||
if (!isSupportedPlatform) {
|
||||
console.log("[SystemNotification] Platform uses custom window, skipping init");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
console.warn("[LinuxNotification] Notification API is not supported");
|
||||
console.warn("[SystemNotification] Notification API is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
const caps = await getCapabilities();
|
||||
console.log("[LinuxNotification] Service initialized with native API:", caps);
|
||||
console.log("[SystemNotification] Service initialized for", process.platform);
|
||||
}
|
||||
|
||||
export async function shutdownLinuxNotificationService(): Promise<void> {
|
||||
// 清理所有活动的通知
|
||||
export async function shutdownSystemNotificationService(): Promise<void> {
|
||||
for (const [id, notification] of activeNotifications) {
|
||||
try {
|
||||
notification.close();
|
||||
@@ -182,5 +169,5 @@ export async function shutdownLinuxNotificationService(): Promise<void> {
|
||||
await avatarFileCache.clearCache();
|
||||
} catch {}
|
||||
|
||||
console.log("[LinuxNotification] Service shutdown complete");
|
||||
console.log("[SystemNotification] Service shutdown complete");
|
||||
}
|
||||
@@ -161,13 +161,18 @@ class VideoService {
|
||||
return `${dbPath}::${this.cleanWxid(wxid)}`.toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 dbPath 本身是否就是账号目录。
|
||||
* 无论 wxid_ 开头还是自定义微信号,账号目录下必然存在 db_storage 子目录,
|
||||
* 以此为判据。不可用路径子串匹配 wxid(#1128:Windows 用户名与微信号相同时,
|
||||
* `C:\Users\<同名>` 会让子串匹配误判,导致视频目录解析到错误位置)。
|
||||
*/
|
||||
private isAccountDir(dirPath: string): boolean {
|
||||
return existsSync(join(dirPath, 'db_storage'))
|
||||
}
|
||||
|
||||
private resolveVideoBaseDir(dbPath: string, wxid: string): string {
|
||||
const cleanedWxid = this.cleanWxid(wxid)
|
||||
const dbPathLower = dbPath.toLowerCase()
|
||||
const wxidLower = wxid.toLowerCase()
|
||||
const cleanedWxidLower = cleanedWxid.toLowerCase()
|
||||
const dbPathContainsWxid = dbPathLower.includes(wxidLower) || dbPathLower.includes(cleanedWxidLower)
|
||||
if (dbPathContainsWxid) {
|
||||
if (this.isAccountDir(dbPath)) {
|
||||
return join(dbPath, 'msg', 'video')
|
||||
}
|
||||
|
||||
@@ -182,12 +187,7 @@ class VideoService {
|
||||
}
|
||||
|
||||
private getHardlinkDbPaths(dbPath: string, wxid: string, cleanedWxid: string): string[] {
|
||||
const dbPathLower = dbPath.toLowerCase()
|
||||
const wxidLower = wxid.toLowerCase()
|
||||
const cleanedWxidLower = cleanedWxid.toLowerCase()
|
||||
const dbPathContainsWxid = dbPathLower.includes(wxidLower) || dbPathLower.includes(cleanedWxidLower)
|
||||
|
||||
if (dbPathContainsWxid) {
|
||||
if (this.isAccountDir(dbPath)) {
|
||||
return [join(dbPath, 'db_storage', 'hardlink', 'hardlink.db')]
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user