mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,3 +65,4 @@ AGENTS.md
|
||||
.claude/
|
||||
.agents/
|
||||
resources/wx_send
|
||||
概述.md
|
||||
@@ -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 || '')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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' : ''}`}>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)' },
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ContactInfo {
|
||||
displayName: string
|
||||
remark?: string
|
||||
nickname?: string
|
||||
alias?: string
|
||||
avatarUrl?: string
|
||||
type: 'friend' | 'group' | 'official' | 'former_friend' | 'other'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user