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

3
.gitignore vendored
View File

@@ -64,4 +64,5 @@ chatlab-format.md
AGENTS.md AGENTS.md
.claude/ .claude/
.agents/ .agents/
resources/wx_send resources/wx_send
概述.md

View File

@@ -136,6 +136,7 @@ export interface ContactInfo {
displayName: string displayName: string
remark?: string remark?: string
nickname?: string nickname?: string
alias?: string
avatarUrl?: string avatarUrl?: string
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
} }
@@ -1392,6 +1393,7 @@ class ChatService {
displayName, displayName,
remark: row.remark || undefined, remark: row.remark || undefined,
nickname: row.nick_name || undefined, nickname: row.nick_name || undefined,
alias: row.alias || undefined,
avatarUrl: undefined, avatarUrl: undefined,
type, type,
lastContactTime: lastContactTimeMap.get(username) || 0 lastContactTime: lastContactTimeMap.get(username) || 0
@@ -4630,9 +4632,23 @@ class ChatService {
const result = await wcdbService.getContact(username) const result = await wcdbService.getContact(username)
if (!result.success || !result.contact) return null if (!result.success || !result.contact) return null
const contact = result.contact as Record<string, any> 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 { return {
username: String(contact.username || contact.user_name || contact.userName || username || ''), username: String(contact.username || contact.user_name || contact.userName || username || ''),
alias: String(contact.alias || contact.Alias || ''), alias,
remark: String(contact.remark || contact.Remark || ''), remark: String(contact.remark || contact.Remark || ''),
// 兼容不同表结构字段,避免 nick_name 丢失导致侧边栏退化到 wxid。 // 兼容不同表结构字段,避免 nick_name 丢失导致侧边栏退化到 wxid。
nickName: String(contact.nickName || contact.nick_name || contact.nickname || contact.NickName || '') nickName: String(contact.nickName || contact.nick_name || contact.nickname || contact.NickName || '')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' 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 type ThemeMode = 'light' | 'dark' | 'system'
export interface ThemeInfo { export interface ThemeInfo {
@@ -57,6 +57,13 @@ export const themes: ThemeInfo[] = [
description: 'RAL 180 80 10', description: 'RAL 180 80 10',
primaryColor: '#5A8A8A', primaryColor: '#5A8A8A',
bgColor: '#E4F0F0' 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-bg: rgba(255, 255, 255, 0.7);
--card-inner-bg: #FAFAF7; --card-inner-bg: #FAFAF7;
--sent-card-bg: var(--primary); --sent-card-bg: var(--primary);
// primary 色上方的前景文字色(大多数主题为白色)
--on-primary: white;
} }
// ==================== 浅色主题 ==================== // ==================== 浅色主题 ====================
@@ -190,6 +193,31 @@
--sent-card-bg: var(--primary); --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); --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; margin: 0;
@@ -395,7 +450,7 @@ body {
&-primary { &-primary {
background: var(--primary); background: var(--primary);
color: white; color: var(--on-primary);
&:hover { &:hover {
background: var(--primary-hover); background: var(--primary-hover);

View File

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