diff --git a/.github/workflows/dev-daily-fixed.yml b/.github/workflows/dev-daily-fixed.yml
index 03bdc42..fb63aff 100644
--- a/.github/workflows/dev-daily-fixed.yml
+++ b/.github/workflows/dev-daily-fixed.yml
@@ -55,6 +55,8 @@ jobs:
gh release delete "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag
fi
gh release create "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "Daily Dev Build" --notes "开发版发布页" --prerelease --target "$TARGET_BRANCH"
+ RELEASE_REST_ID="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$FIXED_DEV_TAG" --jq '.id')"
+ gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$RELEASE_REST_ID" -f draft=false -f prerelease=true >/dev/null
dev-mac-arm64:
needs: prepare
@@ -327,4 +329,9 @@ jobs:
- 如某个平台资源暂未生成,请进入[发布页]($RELEASE_PAGE)查看最新状态
EOF
- gh release edit "$TAG" --repo "$REPO" --title "Daily Dev Build" --notes-file dev_release_notes.md
+ RELEASE_REST_ID="$(gh api "repos/$REPO/releases/tags/$TAG" --jq '.id')"
+ jq -n --rawfile body dev_release_notes.md \
+ '{name:"Daily Dev Build", body:$body, draft:false, prerelease:true}' \
+ > release_update_payload.json
+ gh api --method PATCH "repos/$REPO/releases/$RELEASE_REST_ID" --input release_update_payload.json >/dev/null
+ gh release view "$TAG" --repo "$REPO" --json isDraft,isPrerelease,url
diff --git a/.github/workflows/preview-nightly-main.yml b/.github/workflows/preview-nightly-main.yml
index 186c7c0..751d227 100644
--- a/.github/workflows/preview-nightly-main.yml
+++ b/.github/workflows/preview-nightly-main.yml
@@ -81,6 +81,8 @@ jobs:
gh release delete "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag
fi
gh release create "$FIXED_PREVIEW_TAG" --repo "$GITHUB_REPOSITORY" --title "Preview Nightly Build" --notes "预览版发布页" --prerelease --target "$TARGET_BRANCH"
+ RELEASE_REST_ID="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$FIXED_PREVIEW_TAG" --jq '.id')"
+ gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$RELEASE_REST_ID" -f draft=false -f prerelease=true >/dev/null
preview-mac-arm64:
needs: prepare
@@ -369,4 +371,9 @@ jobs:
> 如某个平台链接暂未生成,请前往[发布页]($RELEASE_PAGE)查看最新资源
EOF
- gh release edit "$TAG" --repo "$REPO" --notes-file preview_release_notes.md
+ RELEASE_REST_ID="$(gh api "repos/$REPO/releases/tags/$TAG" --jq '.id')"
+ jq -n --rawfile body preview_release_notes.md \
+ '{name:"Preview Nightly Build", body:$body, draft:false, prerelease:true}' \
+ > release_update_payload.json
+ gh api --method PATCH "repos/$REPO/releases/$RELEASE_REST_ID" --input release_update_payload.json >/dev/null
+ gh release view "$TAG" --repo "$REPO" --json isDraft,isPrerelease,url
diff --git a/.gitignore b/.gitignore
index 99f4414..ae6f9bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,4 +72,5 @@ pnpm-lock.yaml
/pnpm-workspace.yaml
wechat-research-site
.codex
-weflow-web-offical
\ No newline at end of file
+weflow-web-offical
+Insight
\ No newline at end of file
diff --git a/electron/exportWorker.ts b/electron/exportWorker.ts
index 1f98439..dfa4ba3 100644
--- a/electron/exportWorker.ts
+++ b/electron/exportWorker.ts
@@ -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 : [],
diff --git a/electron/main.ts b/electron/main.ts
index ce578dc..6b692b4 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -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
diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts
index bfeaf4a..270b4dc 100644
--- a/electron/services/chatService.ts
+++ b/electron/services/chatService.ts
@@ -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('')
- const referMsgEnd = content.indexOf('')
+ const referMsgStart = normalizedContent.indexOf('')
+ const referMsgEnd = normalizedContent.indexOf('')
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 ''
+ }
+
/**
* 解析名片消息
* 格式:
diff --git a/electron/services/config.ts b/electron/services/config.ts
index 0c81f59..85da9fc 100644
--- a/electron/services/config.ts
+++ b/electron/services/config.ts
@@ -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)
diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts
index 95800b6..6363e98 100644
--- a/electron/services/exportService.ts
+++ b/electron/services/exportService.ts
@@ -254,6 +254,7 @@ async function parallelLimit(
class ExportService {
private configService: ConfigService
+ private runtimeConfig: { dbPath?: string; decryptKey?: string; myWxid?: string } | null = null
private contactCache: LRUCache
private inlineEmojiCache: LRUCache
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 || '')
diff --git a/package.json b/package.json
index 28736e3..5361a0e 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,9 @@
"lodash": ">=4.17.21",
"brace-expansion": ">=1.1.11",
"picomatch": ">=2.3.1",
- "ajv": ">=8.18.0"
+ "ajv": ">=8.18.0",
+ "ajv-keywords@3>ajv": "^6.12.6",
+ "@develar/schema-utils>ajv": "^6.12.6"
}
},
"build": {
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx
index 36b7cc1..5e86cc5 100644
--- a/src/pages/ChatPage.tsx
+++ b/src/pages/ChatPage.tsx
@@ -8695,6 +8695,28 @@ function MessageBubble({
appMsgTextCache.set(selector, value)
return value
}, [appMsgDoc, appMsgTextCache])
+ const queryPreferredQuotedContent = useCallback((): string => {
+ if (message.quotedContent) return message.quotedContent
+ const candidates = [
+ 'refermsg > selectedcontent',
+ 'refermsg > selectedtext',
+ 'refermsg > selectcontent',
+ 'refermsg > selecttext',
+ 'refermsg > quotecontent',
+ 'refermsg > quotetext',
+ 'refermsg > partcontent',
+ 'refermsg > parttext',
+ 'refermsg > excerpt',
+ 'refermsg > summary',
+ 'refermsg > preview',
+ 'refermsg > content'
+ ]
+ for (const selector of candidates) {
+ const value = queryAppMsgText(selector)
+ if (value) return value
+ }
+ return ''
+ }, [message.quotedContent, queryAppMsgText])
const appMsgThumbRawCandidate = useMemo(() => (
message.linkThumb ||
message.appMsgThumbUrl ||
@@ -8712,7 +8734,7 @@ function MessageBubble({
queryAppMsgText('refermsg > fromusr'),
queryAppMsgText('refermsg > chatusr')
)
- const quotedContent = message.quotedContent || queryAppMsgText('refermsg > content') || ''
+ const quotedContent = queryPreferredQuotedContent()
const quotedSenderFallbackName = useMemo(
() => resolveQuotedSenderFallbackDisplayName(
session.username,
@@ -9262,7 +9284,7 @@ function MessageBubble({
// type 57: 引用回复消息,解析 refermsg 渲染为引用样式
if (xmlType === '57') {
const replyText = q('title') || cleanedParsedContent || ''
- const referContent = q('refermsg > content') || ''
+ const referContent = queryPreferredQuotedContent()
const referType = q('refermsg > type') || ''
// 根据被引用消息类型渲染对应内容
@@ -9385,7 +9407,7 @@ function MessageBubble({
if (kind === 'quote') {
// 引用回复消息(appMsgKind='quote',xmlType=57)
const replyText = message.linkTitle || q('title') || cleanedParsedContent || ''
- const referContent = message.quotedContent || q('refermsg > content') || ''
+ const referContent = queryPreferredQuotedContent()
return (
renderBubbleWithQuote(
renderQuotedMessageBlock(renderTextWithEmoji(cleanMessageContent(referContent))),
@@ -9576,7 +9598,7 @@ function MessageBubble({
// 引用回复消息 (type=57),防止被误判为链接
if (appMsgType === '57') {
const replyText = parsedDoc?.querySelector('title')?.textContent?.trim() || cleanedParsedContent || ''
- const referContent = parsedDoc?.querySelector('refermsg > content')?.textContent?.trim() || ''
+ const referContent = queryPreferredQuotedContent()
const referType = parsedDoc?.querySelector('refermsg > type')?.textContent?.trim() || ''
const renderReferContent2 = () => {