Merge pull request #365 from xunchahaha:dev

Dev
This commit is contained in:
xuncha
2026-03-05 14:31:47 +08:00
committed by GitHub
11 changed files with 118 additions and 39 deletions

1
.gitignore vendored
View File

@@ -65,3 +65,4 @@ AGENTS.md
.claude/
.agents/
resources/wx_send
概述.md

View File

@@ -136,6 +136,7 @@ export interface ContactInfo {
displayName: string
remark?: string
nickname?: string
alias?: string
avatarUrl?: string
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
}
@@ -1392,6 +1393,7 @@ class ChatService {
displayName,
remark: row.remark || undefined,
nickname: row.nick_name || undefined,
alias: row.alias || undefined,
avatarUrl: undefined,
type,
lastContactTime: lastContactTimeMap.get(username) || 0
@@ -4630,9 +4632,23 @@ class ChatService {
const result = await wcdbService.getContact(username)
if (!result.success || !result.contact) return null
const contact = result.contact as Record<string, any>
let alias = String(contact.alias || contact.Alias || '')
// DLL 有时不返回 alias 字段,补一条直接 SQL 查询兜底
if (!alias) {
try {
const safe = username.replace(/'/g, "''")
const sqlResult = await wcdbService.execQuery('contact', null,
`SELECT alias FROM contact WHERE username = '${safe}' LIMIT 1`)
if (sqlResult.success && Array.isArray(sqlResult.rows) && sqlResult.rows.length > 0) {
alias = String(sqlResult.rows[0]?.alias || sqlResult.rows[0]?.Alias || '')
}
} catch {
// 兜底失败不影响主流程
}
}
return {
username: String(contact.username || contact.user_name || contact.userName || username || ''),
alias: String(contact.alias || contact.Alias || ''),
alias,
remark: String(contact.remark || contact.Remark || ''),
// 兼容不同表结构字段,避免 nick_name 丢失导致侧边栏退化到 wxid。
nickName: String(contact.nickName || contact.nick_name || contact.nickname || contact.NickName || '')

View File

@@ -112,7 +112,7 @@
}
span {
color: #fff;
color: var(--on-primary);
font-size: 14px;
font-weight: 600;
}
@@ -183,7 +183,7 @@
&.active {
background: var(--primary);
color: white;
color: var(--on-primary);
}
}

View File

@@ -10,6 +10,7 @@ import './Sidebar.scss'
interface SidebarUserProfile {
wxid: string
displayName: string
alias?: string
avatarUrl?: string
}
@@ -29,6 +30,7 @@ const readSidebarUserProfileCache = (): SidebarUserProfile | null => {
return {
wxid: parsed.wxid,
displayName: parsed.displayName,
alias: parsed.alias,
avatarUrl: parsed.avatarUrl
}
} catch {
@@ -193,6 +195,10 @@ function Sidebar() {
if (fromContact) {
patchUserProfile({ displayName: fromContact }, resolvedWxid)
// 同步补充微信号alias
if (myContact?.alias) {
patchUserProfile({ alias: myContact.alias }, resolvedWxid)
}
return
}
@@ -209,6 +215,10 @@ function Sidebar() {
if (bestName) {
patchUserProfile({ displayName: bestName }, resolvedWxid)
}
// 降级分支也补充微信号
if (myContact?.alias) {
patchUserProfile({ alias: myContact.alias }, resolvedWxid)
}
} catch (nameError) {
console.error('加载侧边栏用户昵称失败:', nameError)
}
@@ -429,7 +439,7 @@ function Sidebar() {
)}
<div
className={`sidebar-user-card ${isAccountMenuOpen ? 'menu-open' : ''}`}
title={collapsed ? `${userProfile.displayName}${userProfile.wxid ? `\n${userProfile.wxid}` : ''}` : undefined}
title={collapsed ? `${userProfile.displayName}${(userProfile.alias || userProfile.wxid) ? `\n${userProfile.alias || userProfile.wxid}` : ''}` : undefined}
onClick={() => setIsAccountMenuOpen(prev => !prev)}
role="button"
tabIndex={0}
@@ -445,7 +455,7 @@ function Sidebar() {
</div>
<div className="user-meta">
<div className="user-name">{userProfile.displayName}</div>
<div className="user-wxid">{userProfile.wxid || 'wxid 未识别'}</div>
<div className="user-wxid">{userProfile.alias || userProfile.wxid || 'wxid 未识别'}</div>
</div>
{!collapsed && (
<span className={`user-menu-caret ${isAccountMenuOpen ? 'open' : ''}`}>

View File

@@ -5194,13 +5194,13 @@ function MessageBubble({
imageClickTimerRef.current = window.setTimeout(() => {
setImageClicked(false)
}, 800)
console.info('[UI] image decrypt click', {
console.info('[UI] image decrypt click (force HD)', {
sessionId: session.username,
imageMd5: message.imageMd5,
imageDatName: message.imageDatName,
localId: message.localId
})
void requestImageDecrypt()
void requestImageDecrypt(true)
}, [message.imageDatName, message.imageMd5, message.localId, requestImageDecrypt, session.username])
const handleOpenImageViewer = useCallback(async () => {

View File

@@ -93,7 +93,7 @@
border-radius: 12px;
padding: 12px;
display: grid;
grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
align-items: stretch;
@@ -176,14 +176,13 @@
flex-direction: column;
gap: 4px;
min-width: 0;
width: fit-content;
width: 100%;
max-width: 100%;
justify-self: start;
z-index: 40;
}
.layout-trigger {
width: auto;
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--border-color);

View File

@@ -3301,6 +3301,8 @@ function ExportPage() {
return (
(contact.displayName || '').toLowerCase().includes(keyword) ||
(contact.remark || '').toLowerCase().includes(keyword) ||
(contact.nickname || '').toLowerCase().includes(keyword) ||
(contact.alias || '').toLowerCase().includes(keyword) ||
contact.username.toLowerCase().includes(keyword)
)
})
@@ -3841,7 +3843,7 @@ function ExportPage() {
</div>
<div className="contact-info">
<div className="contact-name">{contact.displayName}</div>
<div className="contact-remark">{contact.username}</div>
<div className="contact-remark">{contact.alias || contact.username}</div>
</div>
<div className="row-message-count">
<div className="row-message-stats">

View File

@@ -36,18 +36,6 @@ interface WxidOption {
modifiedTime: number
}
const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
const base = String(error || '自动获取密钥失败').trim()
const tailLogs = Array.isArray(logs)
? logs
.map(item => String(item || '').trim())
.filter(Boolean)
.slice(-6)
: []
if (tailLogs.length === 0) return base
return `${base};最近状态:${tailLogs.join(' | ')}`
}
function SettingsPage() {
const {
isDbConnected,
@@ -115,12 +103,12 @@ function SettingsPage() {
const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false)
const [transcribeLanguages, setTranscribeLanguages] = useState<string[]>(['zh'])
const [exportDefaultFormat, setExportDefaultFormat] = useState('json')
const [exportDefaultFormat, setExportDefaultFormat] = useState('excel')
const [exportDefaultDateRange, setExportDefaultDateRange] = useState('today')
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false)
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(4)
const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2)
const [notificationEnabled, setNotificationEnabled] = useState(true)
const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'>('top-right')
@@ -298,6 +286,7 @@ function SettingsPage() {
const savedWhisperModelDir = await configService.getWhisperModelDir()
const savedAutoTranscribe = await configService.getAutoTranscribeVoice()
const savedTranscribeLanguages = await configService.getTranscribeLanguages()
const savedExportDefaultFormat = await configService.getExportDefaultFormat()
const savedExportDefaultDateRange = await configService.getExportDefaultDateRange()
const savedExportDefaultMedia = await configService.getExportDefaultMedia()
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
@@ -338,13 +327,12 @@ function SettingsPage() {
setLogEnabled(savedLogEnabled)
setAutoTranscribeVoice(savedAutoTranscribe)
setTranscribeLanguages(savedTranscribeLanguages)
setExportDefaultFormat('json')
await configService.setExportDefaultFormat('json')
setExportDefaultFormat(savedExportDefaultFormat || 'excel')
setExportDefaultDateRange(savedExportDefaultDateRange || 'today')
setExportDefaultMedia(savedExportDefaultMedia ?? false)
setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? false)
setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true)
setExportDefaultConcurrency(savedExportDefaultConcurrency ?? 4)
setExportDefaultConcurrency(savedExportDefaultConcurrency ?? 2)
setNotificationEnabled(savedNotificationEnabled)
setNotificationPosition(savedNotificationPosition)
@@ -737,10 +725,7 @@ function SettingsPage() {
setIsManualStartPrompt(true)
setDbKeyStatus('需要手动启动微信')
} else {
if (result.error?.includes('尚未完成登录')) {
setDbKeyStatus('请先在微信完成登录后重试')
}
showMessage(formatDbKeyFailureMessage(result.error, result.logs), false)
showMessage(result.error || '自动获取密钥失败', false)
}
}
} catch (e: any) {
@@ -994,8 +979,12 @@ function SettingsPage() {
<div key={theme.id} className={`theme-card ${currentTheme === theme.id ? 'active' : ''}`} onClick={() => setTheme(theme.id)}>
<div className="theme-preview" style={{
background: effectiveMode === 'dark'
? (theme.id === 'blossom-dream' ? 'linear-gradient(150deg, #151316 0%, #1A1620 50%, #131018 100%)' : 'linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%)')
: (theme.id === 'blossom-dream' ? `linear-gradient(150deg, ${theme.bgColor} 0%, #F8F2F8 45%, #F2F6FB 100%)` : `linear-gradient(135deg, ${theme.bgColor} 0%, ${theme.bgColor}dd 100%)`)
? (theme.id === 'blossom-dream' ? 'linear-gradient(150deg, #151316 0%, #1A1620 50%, #131018 100%)'
: theme.id === 'geist' ? 'linear-gradient(135deg, #1a1a1a 0%, #222222 100%)'
: 'linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%)')
: (theme.id === 'blossom-dream' ? `linear-gradient(150deg, ${theme.bgColor} 0%, #F8F2F8 45%, #F2F6FB 100%)`
: theme.id === 'geist' ? 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)'
: `linear-gradient(135deg, ${theme.bgColor} 0%, ${theme.bgColor}dd 100%)`)
}}>
<div className="theme-accent" style={{
background: theme.accentColor
@@ -1557,7 +1546,6 @@ function SettingsPage() {
{ value: 'chatlab', label: 'ChatLab', desc: '标准格式,支持其他软件导入' },
{ value: 'chatlab-jsonl', label: 'ChatLab JSONL', desc: '流式格式,适合大量消息' },
{ value: 'json', label: 'JSON', desc: '详细格式,包含完整消息信息' },
{ value: 'arkme-json', label: 'Arkme JSON', desc: '紧凑 JSON支持 sender 去重与关系统计' },
{ value: 'html', label: 'HTML', desc: '网页格式,可直接浏览' },
{ value: 'txt', label: 'TXT', desc: '纯文本,通用格式' },
{ value: 'weclone', label: 'WeClone CSV', desc: 'WeClone 兼容字段格式CSV' },

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export type ThemeId = 'cloud-dancer' | 'corundum-blue' | 'kiwi-green' | 'spicy-red' | 'teal-water' | 'blossom-dream'
export type ThemeId = 'cloud-dancer' | 'corundum-blue' | 'kiwi-green' | 'spicy-red' | 'teal-water' | 'blossom-dream' | 'geist'
export type ThemeMode = 'light' | 'dark' | 'system'
export interface ThemeInfo {
@@ -57,6 +57,13 @@ export const themes: ThemeInfo[] = [
description: 'RAL 180 80 10',
primaryColor: '#5A8A8A',
bgColor: '#E4F0F0'
},
{
id: 'geist',
name: 'Geist',
description: 'Vercel · 极简黑白',
primaryColor: '#000000',
bgColor: '#ffffff'
}
]

View File

@@ -39,6 +39,9 @@
--card-bg: rgba(255, 255, 255, 0.7);
--card-inner-bg: #FAFAF7;
--sent-card-bg: var(--primary);
// primary 色上方的前景文字色(大多数主题为白色)
--on-primary: white;
}
// ==================== 浅色主题 ====================
@@ -190,6 +193,31 @@
--sent-card-bg: var(--primary);
}
// Geist · 极简黑白 - 浅色
[data-theme="geist"][data-mode="light"],
[data-theme="geist"]:not([data-mode]) {
--primary: #444444;
--primary-rgb: 68, 68, 68;
--primary-hover: #333333;
--primary-light: rgba(68, 68, 68, 0.08);
--bg-primary: #ffffff;
--bg-secondary: rgba(250, 250, 250, 0.95);
--bg-tertiary: rgba(0, 0, 0, 0.03);
--bg-hover: rgba(0, 0, 0, 0.05);
--text-primary: #111111;
--text-secondary: #666666;
--text-tertiary: #999999;
--border-color: #eaeaea;
--border-radius: 6px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
--bg-gradient: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
--primary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%);
--card-bg: rgba(250, 250, 250, 0.95);
--card-inner-bg: #f5f5f5;
--sent-card-bg: #444444;
}
// ==================== 深色主题 ====================
// 云上舞白 - 深色
@@ -339,6 +367,33 @@
--sent-card-bg: var(--primary);
}
// Geist · 极简黑白 - 深色
[data-theme="geist"][data-mode="dark"] {
--primary: #ededed;
--primary-rgb: 237, 237, 237;
--primary-hover: #d5d5d5;
--primary-light: rgba(237, 237, 237, 0.1);
--bg-primary: #1a1a1a;
--bg-secondary: rgba(34, 34, 34, 0.95);
--bg-secondary-solid: #222222;
--bg-tertiary: rgba(255, 255, 255, 0.04);
--bg-hover: rgba(255, 255, 255, 0.07);
--text-primary: #ededed;
--text-secondary: #999999;
--text-tertiary: #666666;
--border-color: #2e2e2e;
--border-radius: 6px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
--bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #222222 100%);
--primary-gradient: linear-gradient(135deg, #ededed 0%, #cccccc 100%);
--card-bg: rgba(34, 34, 34, 0.95);
--card-inner-bg: #2a2a2a;
--sent-card-bg: #3a3a3a;
// primary 是浅灰色,上方文字需要用深色
--on-primary: #111111;
}
// 重置样式
* {
margin: 0;
@@ -395,7 +450,7 @@ body {
&-primary {
background: var(--primary);
color: white;
color: var(--on-primary);
&:hover {
background: var(--primary-hover);

View File

@@ -34,6 +34,7 @@ export interface ContactInfo {
displayName: string
remark?: string
nickname?: string
alias?: string
avatarUrl?: string
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
}