mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-07 15:08:41 +00:00
Merge branch 'main' into fix/http-api-security
This commit is contained in:
@@ -5,6 +5,9 @@ interface ExportWorkerConfig {
|
||||
sessionIds: string[]
|
||||
outputDir: string
|
||||
options: ExportOptions
|
||||
dbPath?: string
|
||||
decryptKey?: string
|
||||
myWxid?: string
|
||||
resourcesPath?: string
|
||||
userDataPath?: string
|
||||
logEnabled?: boolean
|
||||
@@ -29,6 +32,11 @@ async function run() {
|
||||
|
||||
wcdbService.setPaths(config.resourcesPath || '', config.userDataPath || '')
|
||||
wcdbService.setLogEnabled(config.logEnabled === true)
|
||||
exportService.setRuntimeConfig({
|
||||
dbPath: config.dbPath,
|
||||
decryptKey: config.decryptKey,
|
||||
myWxid: config.myWxid
|
||||
})
|
||||
|
||||
const result = await exportService.exportSessions(
|
||||
Array.isArray(config.sessionIds) ? config.sessionIds : [],
|
||||
|
||||
@@ -2362,6 +2362,9 @@ function registerIpcHandlers() {
|
||||
const cfg = configService || new ConfigService()
|
||||
configService = cfg
|
||||
const logEnabled = cfg.get('logEnabled')
|
||||
const dbPath = String(cfg.get('dbPath') || '').trim()
|
||||
const decryptKey = String(cfg.get('decryptKey') || '').trim()
|
||||
const myWxid = String(cfg.get('myWxid') || '').trim()
|
||||
const resourcesPath = app.isPackaged
|
||||
? join(process.resourcesPath, 'resources')
|
||||
: join(app.getAppPath(), 'resources')
|
||||
@@ -2375,6 +2378,9 @@ function registerIpcHandlers() {
|
||||
sessionIds,
|
||||
outputDir,
|
||||
options,
|
||||
dbPath,
|
||||
decryptKey,
|
||||
myWxid,
|
||||
resourcesPath,
|
||||
userDataPath,
|
||||
logEnabled
|
||||
|
||||
@@ -4486,15 +4486,16 @@ class ChatService {
|
||||
*/
|
||||
private parseQuoteMessage(content: string): { content?: string; sender?: string } {
|
||||
try {
|
||||
const normalizedContent = this.decodeHtmlEntities(content || '')
|
||||
// 提取 refermsg 部分
|
||||
const referMsgStart = content.indexOf('<refermsg>')
|
||||
const referMsgEnd = content.indexOf('</refermsg>')
|
||||
const referMsgStart = normalizedContent.indexOf('<refermsg>')
|
||||
const referMsgEnd = normalizedContent.indexOf('</refermsg>')
|
||||
|
||||
if (referMsgStart === -1 || referMsgEnd === -1) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const referMsgXml = content.substring(referMsgStart, referMsgEnd + 11)
|
||||
const referMsgXml = normalizedContent.substring(referMsgStart, referMsgEnd + 11)
|
||||
|
||||
// 提取发送者名称
|
||||
let displayName = this.extractXmlValue(referMsgXml, 'displayname')
|
||||
@@ -4511,8 +4512,8 @@ class ChatService {
|
||||
let displayContent = referContent
|
||||
switch (referType) {
|
||||
case '1':
|
||||
// 文本消息,清理可能的 wxid
|
||||
displayContent = this.sanitizeQuotedContent(referContent)
|
||||
// 文本消息优先取“部分引用”字段,缺失时再回退到完整 content
|
||||
displayContent = this.extractPreferredQuotedText(referMsgXml)
|
||||
break
|
||||
case '3':
|
||||
displayContent = '[图片]'
|
||||
@@ -4552,6 +4553,76 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
private extractPreferredQuotedText(referMsgXml: string): string {
|
||||
if (!referMsgXml) return ''
|
||||
|
||||
const sources = [this.decodeHtmlEntities(referMsgXml)]
|
||||
const rawMsgSource = this.extractXmlValue(referMsgXml, 'msgsource')
|
||||
if (rawMsgSource) {
|
||||
const decodedMsgSource = this.decodeHtmlEntities(rawMsgSource)
|
||||
if (decodedMsgSource) {
|
||||
sources.push(decodedMsgSource)
|
||||
}
|
||||
}
|
||||
|
||||
const fullContent = this.sanitizeQuotedContent(this.extractXmlValue(sources[0] || referMsgXml, 'content'))
|
||||
const partialText = this.extractPartialQuotedText(sources[0] || referMsgXml, fullContent)
|
||||
if (partialText) return partialText
|
||||
|
||||
const candidateTags = [
|
||||
'selectedcontent',
|
||||
'selectedtext',
|
||||
'selectcontent',
|
||||
'selecttext',
|
||||
'quotecontent',
|
||||
'quotetext',
|
||||
'partcontent',
|
||||
'parttext',
|
||||
'excerpt',
|
||||
'summary',
|
||||
'preview'
|
||||
]
|
||||
|
||||
for (const source of sources) {
|
||||
for (const tag of candidateTags) {
|
||||
const value = this.sanitizeQuotedContent(this.extractXmlValue(source, tag))
|
||||
if (value) return value
|
||||
}
|
||||
}
|
||||
|
||||
return fullContent
|
||||
}
|
||||
|
||||
private extractPartialQuotedText(xml: string, fullContent: string): string {
|
||||
if (!xml || !fullContent) return ''
|
||||
|
||||
const startChar = this.extractXmlValue(xml, 'start')
|
||||
const endChar = this.extractXmlValue(xml, 'end')
|
||||
const startIndexRaw = this.extractXmlValue(xml, 'startindex')
|
||||
const endIndexRaw = this.extractXmlValue(xml, 'endindex')
|
||||
const startIndex = Number.parseInt(startIndexRaw, 10)
|
||||
const endIndex = Number.parseInt(endIndexRaw, 10)
|
||||
|
||||
if (startChar && endChar) {
|
||||
const startPos = fullContent.indexOf(startChar)
|
||||
if (startPos !== -1) {
|
||||
const endPos = fullContent.indexOf(endChar, startPos + startChar.length - 1)
|
||||
if (endPos !== -1 && endPos >= startPos) {
|
||||
const sliced = fullContent.slice(startPos, endPos + endChar.length).trim()
|
||||
if (sliced) return sliced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(startIndex) && Number.isFinite(endIndex) && endIndex >= startIndex) {
|
||||
const chars = Array.from(fullContent)
|
||||
const sliced = chars.slice(startIndex, endIndex + 1).join('').trim()
|
||||
if (sliced) return sliced
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析名片消息
|
||||
* 格式: <msg username="wxid_xxx" nickname="昵称" ... />
|
||||
|
||||
@@ -5,6 +5,13 @@ import Store from 'electron-store'
|
||||
|
||||
// 加密前缀标记
|
||||
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
|
||||
const isSafeStorageAvailable = (): boolean => {
|
||||
try {
|
||||
return typeof safeStorage?.isEncryptionAvailable === 'function' && safeStorage.isEncryptionAvailable()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const LOCK_PREFIX = 'lock:' // 密码派生密钥加密(锁定模式)
|
||||
|
||||
interface ConfigSchema {
|
||||
@@ -257,7 +264,7 @@ export class ConfigService {
|
||||
private safeEncrypt(plaintext: string): string {
|
||||
if (!plaintext) return ''
|
||||
if (plaintext.startsWith(SAFE_PREFIX)) return plaintext
|
||||
if (!safeStorage.isEncryptionAvailable()) return plaintext
|
||||
if (!isSafeStorageAvailable()) return plaintext
|
||||
const encrypted = safeStorage.encryptString(plaintext)
|
||||
return SAFE_PREFIX + encrypted.toString('base64')
|
||||
}
|
||||
@@ -265,7 +272,7 @@ export class ConfigService {
|
||||
private safeDecrypt(stored: string): string {
|
||||
if (!stored) return ''
|
||||
if (!stored.startsWith(SAFE_PREFIX)) return stored
|
||||
if (!safeStorage.isEncryptionAvailable()) return ''
|
||||
if (!isSafeStorageAvailable()) return ''
|
||||
try {
|
||||
const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64')
|
||||
return safeStorage.decryptString(buf)
|
||||
|
||||
@@ -254,6 +254,7 @@ async function parallelLimit<T, R>(
|
||||
|
||||
class ExportService {
|
||||
private configService: ConfigService
|
||||
private runtimeConfig: { dbPath?: string; decryptKey?: string; myWxid?: string } | null = null
|
||||
private contactCache: LRUCache<string, { displayName: string; avatarUrl?: string }>
|
||||
private inlineEmojiCache: LRUCache<string, string>
|
||||
private htmlStyleCache: string | null = null
|
||||
@@ -295,6 +296,10 @@ class ExportService {
|
||||
return error
|
||||
}
|
||||
|
||||
setRuntimeConfig(config: { dbPath?: string; decryptKey?: string; myWxid?: string } | null): void {
|
||||
this.runtimeConfig = config
|
||||
}
|
||||
|
||||
private normalizeSessionIds(sessionIds: string[]): string[] {
|
||||
return Array.from(
|
||||
new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))
|
||||
@@ -1316,9 +1321,9 @@ class ExportService {
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<{ success: boolean; cleanedWxid?: string; error?: string }> {
|
||||
const wxid = this.configService.get('myWxid')
|
||||
const dbPath = this.configService.get('dbPath')
|
||||
const decryptKey = this.configService.get('decryptKey')
|
||||
const wxid = String(this.runtimeConfig?.myWxid || this.configService.get('myWxid') || '').trim()
|
||||
const dbPath = String(this.runtimeConfig?.dbPath || this.configService.get('dbPath') || '').trim()
|
||||
const decryptKey = String(this.runtimeConfig?.decryptKey || this.configService.get('decryptKey') || '').trim()
|
||||
if (!wxid) return { success: false, error: '请先在设置页面配置微信ID' }
|
||||
if (!dbPath) return { success: false, error: '请先在设置页面配置数据库路径' }
|
||||
if (!decryptKey) return { success: false, error: '请先在设置页面配置解密密钥' }
|
||||
@@ -2254,7 +2259,7 @@ class ExportService {
|
||||
const referMsgXml = normalized.substring(referMsgStart, referMsgEnd + 11)
|
||||
const quoteInfo = this.parseQuoteMessage(normalized)
|
||||
const replyText = this.stripSenderPrefix(this.extractXmlValue(normalized, 'title') || '')
|
||||
const quotedPreview = this.formatQuotedReferencePreview(
|
||||
const quotedPreview = quoteInfo.content || this.formatQuotedReferencePreview(
|
||||
this.extractXmlValue(referMsgXml, 'content'),
|
||||
this.extractXmlValue(referMsgXml, 'type')
|
||||
)
|
||||
@@ -2960,7 +2965,7 @@ class ExportService {
|
||||
|
||||
switch (referType) {
|
||||
case '1':
|
||||
displayContent = this.sanitizeQuotedContent(referContent)
|
||||
displayContent = this.extractPreferredQuotedText(referMsgXml)
|
||||
break
|
||||
case '3':
|
||||
displayContent = '[图片]'
|
||||
@@ -3001,6 +3006,76 @@ class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private extractPreferredQuotedText(referMsgXml: string): string {
|
||||
if (!referMsgXml) return ''
|
||||
|
||||
const sources = [this.decodeHtmlEntities(referMsgXml)]
|
||||
const rawMsgSource = this.extractXmlValue(referMsgXml, 'msgsource')
|
||||
if (rawMsgSource) {
|
||||
const decodedMsgSource = this.decodeHtmlEntities(rawMsgSource)
|
||||
if (decodedMsgSource) {
|
||||
sources.push(decodedMsgSource)
|
||||
}
|
||||
}
|
||||
|
||||
const fullContent = this.sanitizeQuotedContent(this.extractXmlValue(sources[0] || referMsgXml, 'content'))
|
||||
const partialText = this.extractPartialQuotedText(sources[0] || referMsgXml, fullContent)
|
||||
if (partialText) return partialText
|
||||
|
||||
const candidateTags = [
|
||||
'selectedcontent',
|
||||
'selectedtext',
|
||||
'selectcontent',
|
||||
'selecttext',
|
||||
'quotecontent',
|
||||
'quotetext',
|
||||
'partcontent',
|
||||
'parttext',
|
||||
'excerpt',
|
||||
'summary',
|
||||
'preview'
|
||||
]
|
||||
|
||||
for (const source of sources) {
|
||||
for (const tag of candidateTags) {
|
||||
const value = this.sanitizeQuotedContent(this.extractXmlValue(source, tag))
|
||||
if (value) return value
|
||||
}
|
||||
}
|
||||
|
||||
return fullContent
|
||||
}
|
||||
|
||||
private extractPartialQuotedText(xml: string, fullContent: string): string {
|
||||
if (!xml || !fullContent) return ''
|
||||
|
||||
const startChar = this.extractXmlValue(xml, 'start')
|
||||
const endChar = this.extractXmlValue(xml, 'end')
|
||||
const startIndexRaw = this.extractXmlValue(xml, 'startindex')
|
||||
const endIndexRaw = this.extractXmlValue(xml, 'endindex')
|
||||
const startIndex = Number.parseInt(startIndexRaw, 10)
|
||||
const endIndex = Number.parseInt(endIndexRaw, 10)
|
||||
|
||||
if (startChar && endChar) {
|
||||
const startPos = fullContent.indexOf(startChar)
|
||||
if (startPos !== -1) {
|
||||
const endPos = fullContent.indexOf(endChar, startPos + startChar.length - 1)
|
||||
if (endPos !== -1 && endPos >= startPos) {
|
||||
const sliced = fullContent.slice(startPos, endPos + endChar.length).trim()
|
||||
if (sliced) return sliced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(startIndex) && Number.isFinite(endIndex) && endIndex >= startIndex) {
|
||||
const chars = Array.from(fullContent)
|
||||
const sliced = chars.slice(startIndex, endIndex + 1).join('').trim()
|
||||
if (sliced) return sliced
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private extractChatLabReplyToMessageId(content: string): string | undefined {
|
||||
try {
|
||||
const normalized = this.normalizeAppMessageContent(content || '')
|
||||
|
||||
Reference in New Issue
Block a user