mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51101387f7 | ||
|
|
641a3bf2ab | ||
|
|
58f22f4bb2 | ||
|
|
562eac4249 |
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -119,7 +119,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npx electron-builder --publish always
|
||||
npx electron-builder --win nsis --x64 --publish always '--config.artifactName=${productName}-${version}-x64-Setup.${ext}'
|
||||
|
||||
release-windows-arm64:
|
||||
runs-on: windows-latest
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npx electron-builder --win nsis --arm64 --publish always '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
|
||||
npx electron-builder --win nsis --arm64 --publish always '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
|
||||
|
||||
update-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -184,7 +184,10 @@ jobs:
|
||||
echo "$ASSETS_JSON" | jq -r --arg p "$pattern" '[.assets[].name | select(test($p))][0] // ""'
|
||||
}
|
||||
|
||||
WINDOWS_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("\\.exe$")) | select(test("arm64") | not)][0] // ""')"
|
||||
WINDOWS_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("x64.*\\.exe$"))][0] // ""')"
|
||||
if [ -z "$WINDOWS_ASSET" ]; then
|
||||
WINDOWS_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("\\.exe$")) | select(test("arm64") | not)][0] // ""')"
|
||||
fi
|
||||
WINDOWS_ARM64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("arm64.*\\.exe$"))][0] // ""')"
|
||||
MAC_ASSET="$(pick_asset "\\.dmg$")"
|
||||
LINUX_DEB_ASSET="$(pick_asset "\\.deb$")"
|
||||
@@ -220,6 +223,12 @@ jobs:
|
||||
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
|
||||
- linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
|
||||
|
||||
## macOS 安装提示(未知来源)
|
||||
- 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。
|
||||
- 如果仍被系统拦截,请在终端执行以下命令去除隔离标记:
|
||||
- `xattr -dr com.apple.quarantine "/Applications/WeFlow.app"`
|
||||
- 执行后重新打开 WeFlow。
|
||||
|
||||
> 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE
|
||||
EOF
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ import { messagePushService } from './services/messagePushService'
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
|
||||
// Windows x64 与 arm64 使用不同更新通道,避免 latest.yml 互相覆盖导致下错架构安装包。
|
||||
if (process.platform === 'win32' && process.arch === 'arm64') {
|
||||
autoUpdater.channel = 'latest-arm64'
|
||||
}
|
||||
const AUTO_UPDATE_ENABLED =
|
||||
process.env.AUTO_UPDATE_ENABLED === 'true' ||
|
||||
process.env.AUTO_UPDATE_ENABLED === '1' ||
|
||||
@@ -142,33 +146,47 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => {
|
||||
|
||||
if (!merged.trim()) return ''
|
||||
|
||||
const shouldStripReleaseSection = (headingRaw: string): boolean => {
|
||||
const heading = headingRaw.trim().toLowerCase()
|
||||
if (heading === '下载' || heading === 'download') return true
|
||||
|
||||
const compactHeading = heading.replace(/\s+/g, '')
|
||||
if (compactHeading.startsWith('macos安装提示')) return true
|
||||
if (compactHeading.startsWith('mac安装提示')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// 兼容 electron-updater 直接返回 HTML 的场景
|
||||
const removeDownloadSectionFromHtml = (input: string): string => {
|
||||
return input.replace(
|
||||
/<h[1-6][^>]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
|
||||
''
|
||||
)
|
||||
return input
|
||||
.replace(
|
||||
/<h[1-6][^>]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/<h[1-6][^>]*>\s*(?:mac\s*os|mac)\s*安装提示(?:\s*[((]\s*未知来源\s*[))])?\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
// 兼容 Markdown 场景(Action 最终 release note 模板)
|
||||
const removeDownloadSectionFromMarkdown = (input: string): string => {
|
||||
const lines = input.split(/\r?\n/)
|
||||
const output: string[] = []
|
||||
let skipDownloadSection = false
|
||||
let skipSection = false
|
||||
|
||||
for (const line of lines) {
|
||||
const headingMatch = line.match(/^\s*#{1,6}\s*(.+?)\s*$/)
|
||||
if (headingMatch) {
|
||||
const heading = headingMatch[1].trim().toLowerCase()
|
||||
if (heading === '下载' || heading === 'download') {
|
||||
skipDownloadSection = true
|
||||
if (shouldStripReleaseSection(headingMatch[1])) {
|
||||
skipSection = true
|
||||
continue
|
||||
}
|
||||
if (skipDownloadSection) {
|
||||
skipDownloadSection = false
|
||||
if (skipSection) {
|
||||
skipSection = false
|
||||
}
|
||||
}
|
||||
if (!skipDownloadSection) {
|
||||
if (!skipSection) {
|
||||
output.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5096,14 +5096,35 @@ class ChatService {
|
||||
}
|
||||
|
||||
// 如果是群聊,尝试获取群昵称
|
||||
let groupNicknames: Record<string, string> = {}
|
||||
const groupNicknames = new Map<string, string>()
|
||||
if (chatroomId.endsWith('@chatroom')) {
|
||||
const nickResult = await wcdbService.getGroupNicknames(chatroomId)
|
||||
if (nickResult.success && nickResult.nicknames) {
|
||||
groupNicknames = nickResult.nicknames
|
||||
const nicknameBuckets = new Map<string, Set<string>>()
|
||||
for (const [memberIdRaw, nicknameRaw] of Object.entries(nickResult.nicknames)) {
|
||||
const memberId = String(memberIdRaw || '').trim().toLowerCase()
|
||||
const nickname = String(nicknameRaw || '').trim()
|
||||
if (!memberId || !nickname) continue
|
||||
const slot = nicknameBuckets.get(memberId)
|
||||
if (slot) {
|
||||
slot.add(nickname)
|
||||
} else {
|
||||
nicknameBuckets.set(memberId, new Set([nickname]))
|
||||
}
|
||||
}
|
||||
for (const [memberId, nicknameSet] of nicknameBuckets.entries()) {
|
||||
if (nicknameSet.size !== 1) continue
|
||||
groupNicknames.set(memberId, Array.from(nicknameSet)[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lookupGroupNickname = (username?: string | null): string => {
|
||||
const key = String(username || '').trim().toLowerCase()
|
||||
if (!key) return ''
|
||||
return groupNicknames.get(key) || ''
|
||||
}
|
||||
|
||||
// 获取当前用户 wxid,用于识别"自己"
|
||||
const myWxid = this.configService.get('myWxid')
|
||||
const cleanedMyWxid = myWxid ? this.cleanAccountDirName(myWxid) : ''
|
||||
@@ -5113,7 +5134,7 @@ class ChatService {
|
||||
// 特判:如果是当前用户自己(contact 表通常不包含自己)
|
||||
if (myWxid && (username === myWxid || username === cleanedMyWxid)) {
|
||||
// 先查群昵称中是否有自己
|
||||
const myGroupNick = groupNicknames[username]
|
||||
const myGroupNick = lookupGroupNickname(username) || lookupGroupNickname(myWxid)
|
||||
if (myGroupNick) return myGroupNick
|
||||
// 尝试从缓存获取自己的昵称
|
||||
const cached = this.avatarCache.get(username) || this.avatarCache.get(myWxid)
|
||||
@@ -5122,7 +5143,7 @@ class ChatService {
|
||||
}
|
||||
|
||||
// 先查群昵称
|
||||
const groupNick = groupNicknames[username]
|
||||
const groupNick = lookupGroupNickname(username)
|
||||
if (groupNick) return groupNick
|
||||
|
||||
// 再查联系人信息
|
||||
|
||||
@@ -1404,33 +1404,60 @@ class ExportService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群成员群昵称。优先使用 DLL,必要时回退到 `contact.chat_room.ext_buffer` 解析。
|
||||
* 获取群成员群昵称。后端结果为唯一业务真值,前端仅做冲突净化防串号。
|
||||
*/
|
||||
async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
||||
const nicknameMap = new Map<string, string>()
|
||||
|
||||
try {
|
||||
const dllResult = await wcdbService.getGroupNicknames(chatroomId)
|
||||
if (dllResult.success && dllResult.nicknames) {
|
||||
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames))
|
||||
if (!dllResult.success || !dllResult.nicknames) {
|
||||
return new Map<string, string>()
|
||||
}
|
||||
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
||||
} catch (e) {
|
||||
console.error('getGroupNicknamesForRoom dll error:', e)
|
||||
return new Map<string, string>()
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeGroupNicknameIdentity(value: string): string {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw) return ''
|
||||
return raw.toLowerCase()
|
||||
}
|
||||
|
||||
private buildTrustedGroupNicknameMap(
|
||||
entries: Iterable<[string, string]>,
|
||||
candidates: string[] = []
|
||||
): Map<string, string> {
|
||||
const candidateSet = new Set(
|
||||
this.buildGroupNicknameIdCandidates(candidates)
|
||||
.map((id) => this.normalizeGroupNicknameIdentity(id))
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const buckets = new Map<string, Set<string>>()
|
||||
for (const [memberIdRaw, nicknameRaw] of entries) {
|
||||
const identity = this.normalizeGroupNicknameIdentity(memberIdRaw || '')
|
||||
if (!identity) continue
|
||||
if (candidateSet.size > 0 && !candidateSet.has(identity)) continue
|
||||
|
||||
const nickname = this.normalizeGroupNickname(nicknameRaw || '')
|
||||
if (!nickname) continue
|
||||
|
||||
const slot = buckets.get(identity)
|
||||
if (slot) {
|
||||
slot.add(nickname)
|
||||
} else {
|
||||
buckets.set(identity, new Set([nickname]))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await wcdbService.getChatRoomExtBuffer(chatroomId)
|
||||
if (!result.success || !result.extBuffer) {
|
||||
return nicknameMap
|
||||
}
|
||||
const extBuffer = this.decodeExtBuffer(result.extBuffer)
|
||||
if (!extBuffer) return nicknameMap
|
||||
this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries())
|
||||
return nicknameMap
|
||||
} catch (e) {
|
||||
console.error('getGroupNicknamesForRoom error:', e)
|
||||
return nicknameMap
|
||||
const trusted = new Map<string, string>()
|
||||
for (const [identity, nicknameSet] of buckets.entries()) {
|
||||
if (nicknameSet.size !== 1) continue
|
||||
trusted.set(identity, Array.from(nicknameSet)[0])
|
||||
}
|
||||
return trusted
|
||||
}
|
||||
|
||||
private mergeGroupNicknameEntries(
|
||||
@@ -1680,8 +1707,6 @@ class ExportService {
|
||||
const raw = String(rawValue || '').trim()
|
||||
if (!raw) continue
|
||||
set.add(raw)
|
||||
const cleaned = this.cleanAccountDirName(raw)
|
||||
if (cleaned && cleaned !== raw) set.add(cleaned)
|
||||
}
|
||||
return Array.from(set)
|
||||
}
|
||||
@@ -1690,29 +1715,20 @@ class ExportService {
|
||||
const idCandidates = this.buildGroupNicknameIdCandidates(candidates)
|
||||
if (idCandidates.length === 0) return ''
|
||||
|
||||
let resolved = ''
|
||||
for (const id of idCandidates) {
|
||||
const exact = this.normalizeGroupNickname(groupNicknamesMap.get(id) || '')
|
||||
if (exact) return exact
|
||||
const lower = this.normalizeGroupNickname(groupNicknamesMap.get(id.toLowerCase()) || '')
|
||||
if (lower) return lower
|
||||
}
|
||||
|
||||
for (const id of idCandidates) {
|
||||
const lower = id.toLowerCase()
|
||||
let found = ''
|
||||
let matched = 0
|
||||
for (const [key, value] of groupNicknamesMap.entries()) {
|
||||
if (String(key || '').toLowerCase() !== lower) continue
|
||||
const normalized = this.normalizeGroupNickname(value || '')
|
||||
if (!normalized) continue
|
||||
found = normalized
|
||||
matched += 1
|
||||
if (matched > 1) return ''
|
||||
const normalizedId = this.normalizeGroupNicknameIdentity(id)
|
||||
if (!normalizedId) continue
|
||||
const candidateNickname = this.normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '')
|
||||
if (!candidateNickname) continue
|
||||
if (!resolved) {
|
||||
resolved = candidateNickname
|
||||
continue
|
||||
}
|
||||
if (matched === 1 && found) return found
|
||||
if (resolved !== candidateNickname) return ''
|
||||
}
|
||||
|
||||
return ''
|
||||
return resolved
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -257,34 +257,60 @@ class GroupAnalyticsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DLL 获取群成员的群昵称
|
||||
* 从后端获取群成员群昵称,并在前端进行唯一性净化防串号。
|
||||
*/
|
||||
private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
|
||||
const nicknameMap = new Map<string, string>()
|
||||
|
||||
try {
|
||||
const dllResult = await wcdbService.getGroupNicknames(chatroomId)
|
||||
if (dllResult.success && dllResult.nicknames) {
|
||||
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames))
|
||||
if (!dllResult.success || !dllResult.nicknames) {
|
||||
return new Map<string, string>()
|
||||
}
|
||||
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
|
||||
} catch (e) {
|
||||
console.error('getGroupNicknamesForRoom dll error:', e)
|
||||
return new Map<string, string>()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await wcdbService.getChatRoomExtBuffer(chatroomId)
|
||||
if (!result.success || !result.extBuffer) {
|
||||
return nicknameMap
|
||||
private normalizeGroupNicknameIdentity(value: string): string {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw) return ''
|
||||
return raw.toLowerCase()
|
||||
}
|
||||
|
||||
private buildTrustedGroupNicknameMap(
|
||||
entries: Iterable<[string, string]>,
|
||||
candidates: string[] = []
|
||||
): Map<string, string> {
|
||||
const candidateSet = new Set(
|
||||
this.buildGroupNicknameIdCandidates(candidates)
|
||||
.map((id) => this.normalizeGroupNicknameIdentity(id))
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const buckets = new Map<string, Set<string>>()
|
||||
for (const [memberIdRaw, nicknameRaw] of entries) {
|
||||
const identity = this.normalizeGroupNicknameIdentity(memberIdRaw || '')
|
||||
if (!identity) continue
|
||||
if (candidateSet.size > 0 && !candidateSet.has(identity)) continue
|
||||
|
||||
const nickname = this.normalizeGroupNickname(nicknameRaw || '')
|
||||
if (!nickname) continue
|
||||
|
||||
const slot = buckets.get(identity)
|
||||
if (slot) {
|
||||
slot.add(nickname)
|
||||
} else {
|
||||
buckets.set(identity, new Set([nickname]))
|
||||
}
|
||||
|
||||
const extBuffer = this.decodeExtBuffer(result.extBuffer)
|
||||
if (!extBuffer) return nicknameMap
|
||||
this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries())
|
||||
return nicknameMap
|
||||
} catch (e) {
|
||||
console.error('getGroupNicknamesForRoom error:', e)
|
||||
return nicknameMap
|
||||
}
|
||||
|
||||
const trusted = new Map<string, string>()
|
||||
for (const [identity, nicknameSet] of buckets.entries()) {
|
||||
if (nicknameSet.size !== 1) continue
|
||||
trusted.set(identity, Array.from(nicknameSet)[0])
|
||||
}
|
||||
return trusted
|
||||
}
|
||||
|
||||
private mergeGroupNicknameEntries(
|
||||
@@ -475,6 +501,16 @@ class GroupAnalyticsService {
|
||||
return Array.from(set)
|
||||
}
|
||||
|
||||
private buildGroupNicknameIdCandidates(values: Array<string | undefined | null>): string[] {
|
||||
const set = new Set<string>()
|
||||
for (const rawValue of values) {
|
||||
const raw = String(rawValue || '').trim()
|
||||
if (!raw) continue
|
||||
set.add(raw)
|
||||
}
|
||||
return Array.from(set)
|
||||
}
|
||||
|
||||
private toNonNegativeInteger(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed)) return 0
|
||||
@@ -663,30 +699,23 @@ class GroupAnalyticsService {
|
||||
}
|
||||
|
||||
private resolveGroupNicknameByCandidates(groupNicknames: Map<string, string>, candidates: string[]): string {
|
||||
const idCandidates = this.buildIdCandidates(candidates)
|
||||
const idCandidates = this.buildGroupNicknameIdCandidates(candidates)
|
||||
if (idCandidates.length === 0) return ''
|
||||
|
||||
let resolved = ''
|
||||
for (const id of idCandidates) {
|
||||
const exact = this.normalizeGroupNickname(groupNicknames.get(id) || '')
|
||||
if (exact) return exact
|
||||
}
|
||||
|
||||
for (const id of idCandidates) {
|
||||
const lower = id.toLowerCase()
|
||||
let found = ''
|
||||
let matched = 0
|
||||
for (const [key, value] of groupNicknames.entries()) {
|
||||
if (String(key || '').toLowerCase() !== lower) continue
|
||||
const normalized = this.normalizeGroupNickname(value || '')
|
||||
if (!normalized) continue
|
||||
found = normalized
|
||||
matched += 1
|
||||
if (matched > 1) return ''
|
||||
const normalizedId = this.normalizeGroupNicknameIdentity(id)
|
||||
if (!normalizedId) continue
|
||||
const candidateNickname = this.normalizeGroupNickname(groupNicknames.get(normalizedId) || '')
|
||||
if (!candidateNickname) continue
|
||||
if (!resolved) {
|
||||
resolved = candidateNickname
|
||||
continue
|
||||
}
|
||||
if (matched === 1 && found) return found
|
||||
if (resolved !== candidateNickname) return ''
|
||||
}
|
||||
|
||||
return ''
|
||||
return resolved
|
||||
}
|
||||
|
||||
private sanitizeWorksheetName(name: string): string {
|
||||
|
||||
@@ -1017,13 +1017,31 @@ class HttpService {
|
||||
}
|
||||
|
||||
private lookupGroupNickname(groupNicknamesMap: Map<string, string>, sender: string): string {
|
||||
if (!sender) return ''
|
||||
const cleaned = this.normalizeAccountId(sender)
|
||||
return groupNicknamesMap.get(sender)
|
||||
|| groupNicknamesMap.get(sender.toLowerCase())
|
||||
|| groupNicknamesMap.get(cleaned)
|
||||
|| groupNicknamesMap.get(cleaned.toLowerCase())
|
||||
|| ''
|
||||
const key = String(sender || '').trim().toLowerCase()
|
||||
if (!key) return ''
|
||||
return groupNicknamesMap.get(key) || ''
|
||||
}
|
||||
|
||||
private buildTrustedGroupNicknameMap(nicknames: Record<string, string>): Map<string, string> {
|
||||
const buckets = new Map<string, Set<string>>()
|
||||
for (const [memberIdRaw, nicknameRaw] of Object.entries(nicknames || {})) {
|
||||
const memberId = String(memberIdRaw || '').trim().toLowerCase()
|
||||
const nickname = String(nicknameRaw || '').trim()
|
||||
if (!memberId || !nickname) continue
|
||||
const slot = buckets.get(memberId)
|
||||
if (slot) {
|
||||
slot.add(nickname)
|
||||
} else {
|
||||
buckets.set(memberId, new Set([nickname]))
|
||||
}
|
||||
}
|
||||
|
||||
const trusted = new Map<string, string>()
|
||||
for (const [memberId, nicknameSet] of buckets.entries()) {
|
||||
if (nicknameSet.size !== 1) continue
|
||||
trusted.set(memberId, Array.from(nicknameSet)[0])
|
||||
}
|
||||
return trusted
|
||||
}
|
||||
|
||||
private resolveChatLabSenderInfo(
|
||||
@@ -1094,21 +1112,7 @@ class HttpService {
|
||||
try {
|
||||
const result = await wcdbService.getGroupNicknames(talkerId)
|
||||
if (result.success && result.nicknames) {
|
||||
groupNicknamesMap = new Map()
|
||||
for (const [memberIdRaw, nicknameRaw] of Object.entries(result.nicknames)) {
|
||||
const memberId = String(memberIdRaw || '').trim()
|
||||
const nickname = String(nicknameRaw || '').trim()
|
||||
if (!memberId || !nickname) continue
|
||||
|
||||
groupNicknamesMap.set(memberId, nickname)
|
||||
groupNicknamesMap.set(memberId.toLowerCase(), nickname)
|
||||
|
||||
const cleaned = this.normalizeAccountId(memberId)
|
||||
if (cleaned) {
|
||||
groupNicknamesMap.set(cleaned, nickname)
|
||||
groupNicknamesMap.set(cleaned.toLowerCase(), nickname)
|
||||
}
|
||||
}
|
||||
groupNicknamesMap = this.buildTrustedGroupNicknameMap(result.nicknames)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[HttpService] Failed to get group nicknames:', e)
|
||||
@@ -1389,4 +1393,3 @@ class HttpService {
|
||||
}
|
||||
|
||||
export const httpService = new HttpService()
|
||||
|
||||
|
||||
@@ -304,11 +304,8 @@ class MessagePushService {
|
||||
}
|
||||
|
||||
const groupNicknames = await this.getGroupNicknames(chatroomId)
|
||||
const normalizedSender = this.normalizeAccountId(senderUsername)
|
||||
const nickname = groupNicknames[senderUsername]
|
||||
|| groupNicknames[senderUsername.toLowerCase()]
|
||||
|| groupNicknames[normalizedSender]
|
||||
|| groupNicknames[normalizedSender.toLowerCase()]
|
||||
const senderKey = senderUsername.toLowerCase()
|
||||
const nickname = groupNicknames[senderKey]
|
||||
|
||||
if (nickname) {
|
||||
return nickname
|
||||
@@ -328,22 +325,33 @@ class MessagePushService {
|
||||
}
|
||||
|
||||
const result = await wcdbService.getGroupNicknames(cacheKey)
|
||||
const nicknames = result.success && result.nicknames ? result.nicknames : {}
|
||||
const nicknames = result.success && result.nicknames
|
||||
? this.sanitizeGroupNicknames(result.nicknames)
|
||||
: {}
|
||||
this.groupNicknameCache.set(cacheKey, { nicknames, updatedAt: Date.now() })
|
||||
return nicknames
|
||||
}
|
||||
|
||||
private normalizeAccountId(value: string): string {
|
||||
const trimmed = String(value || '').trim()
|
||||
if (!trimmed) return trimmed
|
||||
|
||||
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
||||
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
||||
return match ? match[1] : trimmed
|
||||
private sanitizeGroupNicknames(nicknames: Record<string, string>): Record<string, string> {
|
||||
const buckets = new Map<string, Set<string>>()
|
||||
for (const [memberIdRaw, nicknameRaw] of Object.entries(nicknames || {})) {
|
||||
const memberId = String(memberIdRaw || '').trim().toLowerCase()
|
||||
const nickname = String(nicknameRaw || '').trim()
|
||||
if (!memberId || !nickname) continue
|
||||
const slot = buckets.get(memberId)
|
||||
if (slot) {
|
||||
slot.add(nickname)
|
||||
} else {
|
||||
buckets.set(memberId, new Set([nickname]))
|
||||
}
|
||||
}
|
||||
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
return suffixMatch ? suffixMatch[1] : trimmed
|
||||
const trusted: Record<string, string> = {}
|
||||
for (const [memberId, nicknameSet] of buckets.entries()) {
|
||||
if (nicknameSet.size !== 1) continue
|
||||
trusted[memberId] = Array.from(nicknameSet)[0]
|
||||
}
|
||||
return trusted
|
||||
}
|
||||
|
||||
private isRecentMessage(messageKey: string): boolean {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -625,7 +625,7 @@
|
||||
|
||||
.bubble-content {
|
||||
background: var(--primary-gradient);
|
||||
color: #fff;
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
@@ -1962,7 +1962,7 @@
|
||||
|
||||
.bubble-content {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
}
|
||||
@@ -2453,15 +2453,15 @@
|
||||
|
||||
// 自己发送的消息中的引用样式
|
||||
.message-bubble.sent .quoted-message {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-left-color: rgba(255, 255, 255, 0.5);
|
||||
background: color-mix(in srgb, var(--on-primary) 12%, var(--primary));
|
||||
border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary));
|
||||
|
||||
.quoted-sender {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
|
||||
}
|
||||
|
||||
.quoted-text {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1145,9 +1145,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.quote-layout-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.quote-layout-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -1155,32 +1159,146 @@
|
||||
.quote-layout-card {
|
||||
position: relative;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 14px;
|
||||
padding: 14px 14px 12px;
|
||||
background: color-mix(in srgb, var(--primary) 6%, var(--bg-primary));
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background 0.2s ease;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color));
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--primary) 32%, var(--border-color));
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 92%, var(--primary) 8%);
|
||||
border-color: color-mix(in srgb, var(--primary) 68%, var(--border-color));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary));
|
||||
}
|
||||
}
|
||||
|
||||
.quote-layout-card-header {
|
||||
.quote-layout-card-check {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid color-mix(in srgb, var(--primary) 46%, var(--border-color));
|
||||
background: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary) 78%, #6f8653);
|
||||
transform: scale(0);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: color-mix(in srgb, var(--primary) 78%, var(--border-color));
|
||||
}
|
||||
|
||||
&.active::after {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.quote-layout-preview-shell {
|
||||
padding-left: 22px;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-width: 70%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble.sent {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble.sent .bubble-content {
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .bubble-content {
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .quoted-message {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-left: 2px solid var(--primary);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .quoted-sender {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .quoted-sender::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .quoted-text {
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble.sent .quoted-message {
|
||||
background: color-mix(in srgb, var(--on-primary) 12%, var(--primary));
|
||||
border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary));
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble.sent .quoted-sender {
|
||||
color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .message-bubble.sent .quoted-text {
|
||||
color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .bubble-content.quote-layout-top .quoted-message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-chat .bubble-content.quote-layout-bottom .quoted-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quote-layout-card-footer {
|
||||
margin-top: 8px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.quote-layout-card-title-group {
|
||||
@@ -1190,89 +1308,22 @@
|
||||
}
|
||||
|
||||
.quote-layout-card-title {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.quote-layout-card-desc {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.quote-layout-card-check {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color);
|
||||
color: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
@media (max-width: 760px) {
|
||||
.quote-layout-picker {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-layout-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary));
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
min-height: 112px;
|
||||
|
||||
&.quote-bottom {
|
||||
.quote-layout-preview-message {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.quote-layout-preview-quote {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quote-layout-preview-quote {
|
||||
padding: 8px 10px;
|
||||
border-left: 2px solid var(--primary);
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.quote-layout-preview-sender {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.quote-layout-preview-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quote-layout-preview-message {
|
||||
align-self: flex-start;
|
||||
max-width: 88%;
|
||||
padding: 9px 12px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--primary) 14%, var(--bg-primary));
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
|
||||
@@ -1061,9 +1061,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>引用样式</label>
|
||||
<span className="form-hint">选择聊天中引用消息与正文的上下顺序,右侧预览会同步展示布局差异。</span>
|
||||
<div className="form-group quote-layout-group">
|
||||
<label>引用消息样式</label>
|
||||
<span className="form-hint">选择聊天中引用消息与正文的上下顺序,下方预览会同步展示布局差异。</span>
|
||||
<div className="quote-layout-picker" role="radiogroup" aria-label="引用样式选择">
|
||||
{[
|
||||
{
|
||||
@@ -1080,15 +1080,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
}
|
||||
].map(option => {
|
||||
const selected = quoteLayout === option.value
|
||||
const quotePreview = (
|
||||
<div className="quote-layout-preview-quote">
|
||||
<span className="quote-layout-preview-sender">张三</span>
|
||||
<span className="quote-layout-preview-text">这是一条被引用的消息</span>
|
||||
</div>
|
||||
)
|
||||
const messagePreview = (
|
||||
<div className="quote-layout-preview-message">这是当前发送的回复内容</div>
|
||||
)
|
||||
const isQuoteBottom = option.value === 'quote-bottom'
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -1104,27 +1096,37 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
>
|
||||
<div className="quote-layout-card-header">
|
||||
<span className={`quote-layout-card-check ${selected ? 'active' : ''}`} aria-hidden="true" />
|
||||
<div className="quote-layout-preview-shell">
|
||||
<div className="quote-layout-preview-chat">
|
||||
<div className="message-bubble sent">
|
||||
<div className={`bubble-content ${isQuoteBottom ? 'quote-layout-bottom' : 'quote-layout-top'}`}>
|
||||
{isQuoteBottom ? (
|
||||
<>
|
||||
<div className="message-text">拍得真不错!</div>
|
||||
<div className="quoted-message">
|
||||
<span className="quoted-sender">张三</span>
|
||||
<span className="quoted-text">那天去爬山的照片...</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="quoted-message">
|
||||
<span className="quoted-sender">张三</span>
|
||||
<span className="quoted-text">那天去爬山的照片...</span>
|
||||
</div>
|
||||
<div className="message-text">拍得真不错!</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quote-layout-card-footer">
|
||||
<div className="quote-layout-card-title-group">
|
||||
<span className="quote-layout-card-title">{option.label}</span>
|
||||
<span className="quote-layout-card-desc">{option.description}</span>
|
||||
</div>
|
||||
<span className={`quote-layout-card-check ${selected ? 'active' : ''}`}>
|
||||
<Check size={14} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={`quote-layout-preview ${option.value}`}>
|
||||
{option.value === 'quote-bottom' ? (
|
||||
<>
|
||||
{messagePreview}
|
||||
{quotePreview}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{quotePreview}
|
||||
{messagePreview}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import './WelcomePage.scss'
|
||||
|
||||
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||
const isWindows = !isMac && !isLinux
|
||||
|
||||
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||
const dbPathPlaceholder = isMac
|
||||
@@ -46,6 +47,19 @@ const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
|
||||
return `${base};最近状态:${tailLogs.join(' | ')}`
|
||||
}
|
||||
|
||||
const normalizeDbKeyStatusMessage = (message: string): string => {
|
||||
if (isWindows && message.includes('Hook安装成功')) {
|
||||
return '已准备就绪,现在登录微信或退出登录后重新登录微信'
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
const isDbKeyReadyMessage = (message: string): boolean => (
|
||||
message.includes('现在可以登录')
|
||||
|| message.includes('Hook安装成功')
|
||||
|| message.includes('已准备就绪,现在登录微信或退出登录后重新登录微信')
|
||||
)
|
||||
|
||||
function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
const navigate = useNavigate()
|
||||
const { isDbConnected, setDbConnected, setLoading } = useAppStore()
|
||||
@@ -139,8 +153,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
|
||||
setDbKeyStatus(payload.message)
|
||||
if (payload.message.includes('现在可以登录') || payload.message.includes('Hook安装成功')) {
|
||||
const normalizedMessage = normalizeDbKeyStatusMessage(payload.message)
|
||||
setDbKeyStatus(normalizedMessage)
|
||||
if (isDbKeyReadyMessage(normalizedMessage)) {
|
||||
window.electronAPI.notification?.show({
|
||||
title: 'WeFlow 准备就绪',
|
||||
content: '现在可以登录微信了',
|
||||
@@ -761,8 +776,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dbKeyStatus && <div className={`status-message ${dbKeyStatus.includes('现在可以登录') || dbKeyStatus.includes('Hook安装成功') ? 'is-success' : ''}`}>{dbKeyStatus}</div>}
|
||||
<div className="field-hint">点击自动获取后微信将重启,请留意弹窗提示</div>
|
||||
{dbKeyStatus && <div className={`status-message ${isDbKeyReadyMessage(dbKeyStatus) ? 'is-success' : ''}`}>{dbKeyStatus}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user