mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
@@ -1,5 +1,5 @@
|
|||||||
import './preload-env'
|
import './preload-env'
|
||||||
import { app, BrowserWindow, ipcMain, nativeTheme, session } from 'electron'
|
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
|
||||||
import { Worker } from 'worker_threads'
|
import { Worker } from 'worker_threads'
|
||||||
import { join, dirname } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import { autoUpdater } from 'electron-updater'
|
import { autoUpdater } from 'electron-updater'
|
||||||
@@ -96,6 +96,7 @@ const keyService = process.platform === 'darwin'
|
|||||||
let mainWindowReady = false
|
let mainWindowReady = false
|
||||||
let shouldShowMain = true
|
let shouldShowMain = true
|
||||||
let isAppQuitting = false
|
let isAppQuitting = false
|
||||||
|
let tray: Tray | null = null
|
||||||
|
|
||||||
// 更新下载状态管理(Issue #294 修复)
|
// 更新下载状态管理(Issue #294 修复)
|
||||||
let isDownloadInProgress = false
|
let isDownloadInProgress = false
|
||||||
@@ -352,6 +353,13 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
|||||||
callback(false)
|
callback(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
win.on('close', (e) => {
|
||||||
|
if (isAppQuitting) return
|
||||||
|
// 关闭主窗口时隐藏到状态栏而不是退出
|
||||||
|
e.preventDefault()
|
||||||
|
win.hide()
|
||||||
|
})
|
||||||
|
|
||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
if (mainWindow !== win) return
|
if (mainWindow !== win) return
|
||||||
|
|
||||||
@@ -359,7 +367,6 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
|||||||
mainWindowReady = false
|
mainWindowReady = false
|
||||||
|
|
||||||
if (process.platform !== 'darwin' && !isAppQuitting) {
|
if (process.platform !== 'darwin' && !isAppQuitting) {
|
||||||
// 隐藏通知窗也是 BrowserWindow,必须销毁,否则会阻止应用退出。
|
|
||||||
destroyNotificationWindow()
|
destroyNotificationWindow()
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
app.quit()
|
app.quit()
|
||||||
@@ -2439,6 +2446,55 @@ app.whenReady().then(async () => {
|
|||||||
updateSplashProgress(30, '正在加载界面...')
|
updateSplashProgress(30, '正在加载界面...')
|
||||||
mainWindow = createWindow({ autoShow: false })
|
mainWindow = createWindow({ autoShow: false })
|
||||||
|
|
||||||
|
// 初始化系统托盘图标(与其他窗口 icon 路径逻辑保持一致)
|
||||||
|
const resolvedTrayIcon = process.platform === 'win32'
|
||||||
|
? join(__dirname, '../public/icon.ico')
|
||||||
|
: (process.platform === 'darwin'
|
||||||
|
? join(process.resourcesPath, 'icon.icns')
|
||||||
|
: join(process.resourcesPath, 'icon.ico'))
|
||||||
|
try {
|
||||||
|
tray = new Tray(resolvedTrayIcon)
|
||||||
|
tray.setToolTip('WeFlow')
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示主窗口',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
isAppQuitting = true
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
tray.setContextMenu(contextMenu)
|
||||||
|
tray.on('click', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.focus()
|
||||||
|
} else {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tray.on('double-click', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show()
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Tray] Failed to create tray icon:', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 配置网络服务
|
// 配置网络服务
|
||||||
session.defaultSession.webRequest.onBeforeSendHeaders(
|
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||||||
{
|
{
|
||||||
@@ -2486,12 +2542,20 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
app.on('before-quit', async () => {
|
app.on('before-quit', async () => {
|
||||||
isAppQuitting = true
|
isAppQuitting = true
|
||||||
|
// 销毁 tray 图标
|
||||||
|
if (tray) { try { tray.destroy() } catch {} tray = null }
|
||||||
// 通知窗使用 hide 而非 close,退出时主动销毁,避免残留窗口阻塞进程退出。
|
// 通知窗使用 hide 而非 close,退出时主动销毁,避免残留窗口阻塞进程退出。
|
||||||
destroyNotificationWindow()
|
destroyNotificationWindow()
|
||||||
|
// 兜底:5秒后强制退出,防止某个异步任务卡住导致进程残留
|
||||||
|
const forceExitTimer = setTimeout(() => {
|
||||||
|
console.warn('[App] Force exit after timeout')
|
||||||
|
app.exit(0)
|
||||||
|
}, 5000)
|
||||||
|
forceExitTimer.unref()
|
||||||
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
||||||
try { await httpService.stop() } catch {}
|
try { await httpService.stop() } catch {}
|
||||||
// 终止 wcdb Worker 线程,避免线程阻止进程退出
|
// 终止 wcdb Worker 线程,避免线程阻止进程退出
|
||||||
try { wcdbService.shutdown() } catch {}
|
try { await wcdbService.shutdown() } catch {}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
@@ -606,34 +606,14 @@ export class KeyService {
|
|||||||
|
|
||||||
const logs: string[] = []
|
const logs: string[] = []
|
||||||
|
|
||||||
onStatus?.('正在定位微信安装路径...', 0)
|
onStatus?.('正在查找微信进程...', 0)
|
||||||
let wechatPath = await this.findWeChatInstallPath()
|
const pid = await this.findWeChatPid()
|
||||||
if (!wechatPath) {
|
if (!pid) {
|
||||||
const err = '未找到微信安装路径,请确认已安装PC微信'
|
const err = '未找到微信进程,请先启动微信'
|
||||||
onStatus?.(err, 2)
|
onStatus?.(err, 2)
|
||||||
return { success: false, error: err }
|
return { success: false, error: err }
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatus?.('正在关闭微信以进行获取...', 0)
|
|
||||||
const closed = await this.killWeChatProcesses()
|
|
||||||
if (!closed) {
|
|
||||||
const err = '无法自动关闭微信,请手动退出后重试'
|
|
||||||
onStatus?.(err, 2)
|
|
||||||
return { success: false, error: err }
|
|
||||||
}
|
|
||||||
|
|
||||||
onStatus?.('正在启动微信...', 0)
|
|
||||||
const sub = spawn(wechatPath, {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
cwd: dirname(wechatPath)
|
|
||||||
})
|
|
||||||
sub.unref()
|
|
||||||
|
|
||||||
onStatus?.('等待微信界面就绪...', 0)
|
|
||||||
const pid = await this.waitForWeChatWindow()
|
|
||||||
if (!pid) return { success: false, error: '启动微信失败或等待界面就绪超时' }
|
|
||||||
|
|
||||||
onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0)
|
onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0)
|
||||||
onStatus?.('正在检测微信界面组件...', 0)
|
onStatus?.('正在检测微信界面组件...', 0)
|
||||||
await this.waitForWeChatWindowComponents(pid, 15000)
|
await this.waitForWeChatWindowComponents(pid, 15000)
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export class KeyServiceMac {
|
|||||||
// 用 AppleScript 的 quoted form 组装命令,避免复杂 shell 拼接导致整条失败
|
// 用 AppleScript 的 quoted form 组装命令,避免复杂 shell 拼接导致整条失败
|
||||||
const scriptLines = [
|
const scriptLines = [
|
||||||
`set helperPath to ${JSON.stringify(helperPath)}`,
|
`set helperPath to ${JSON.stringify(helperPath)}`,
|
||||||
`set cmd to quoted form of helperPath & " ${pid} ${waitMs}"`,
|
`set cmd to quoted form of helperPath & " ${pid} ${waitMs} 2>&1"`,
|
||||||
'do shell script cmd with administrator privileges'
|
'do shell script cmd with administrator privileges'
|
||||||
]
|
]
|
||||||
onStatus?.('已准备就绪,现在登录微信或退出登录后重新登录微信', 0)
|
onStatus?.('已准备就绪,现在登录微信或退出登录后重新登录微信', 0)
|
||||||
@@ -380,18 +380,27 @@ export class KeyServiceMac {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lines = String(stdout).split(/\r?\n/).map(x => x.trim()).filter(Boolean)
|
const lines = String(stdout).split(/\r?\n/).map(x => x.trim()).filter(Boolean)
|
||||||
const last = lines[lines.length - 1]
|
if (!lines.length) throw new Error('elevated helper returned empty output')
|
||||||
if (!last) throw new Error('elevated helper returned empty output')
|
|
||||||
|
|
||||||
let payload: any
|
// 从所有行里提取所有 JSON 对象(同一行可能有多个拼接),找含 key/result 的那个
|
||||||
try {
|
const extractJsonObjects = (s: string): any[] => {
|
||||||
payload = JSON.parse(last)
|
const results: any[] = []
|
||||||
} catch {
|
const re = /\{[^{}]*\}/g
|
||||||
throw new Error('elevated helper returned invalid json: ' + last)
|
let m: RegExpExecArray | null
|
||||||
|
while ((m = re.exec(s)) !== null) {
|
||||||
|
try { results.push(JSON.parse(m[0])) } catch { }
|
||||||
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
if (payload?.success === true && typeof payload?.key === 'string') return payload.key
|
const fullOutput = lines.join('\n')
|
||||||
if (typeof payload?.result === 'string') return payload.result
|
const allJson = extractJsonObjects(fullOutput)
|
||||||
throw new Error('elevated helper json missing key/result')
|
// 优先找 success=true && key 字段
|
||||||
|
const successPayload = allJson.find(p => p?.success === true && typeof p?.key === 'string')
|
||||||
|
if (successPayload) return successPayload.key
|
||||||
|
// 其次找 result 字段
|
||||||
|
const resultPayload = allJson.find(p => typeof p?.result === 'string')
|
||||||
|
if (resultPayload) return resultPayload.result
|
||||||
|
throw new Error('elevated helper returned invalid json: ' + lines[lines.length - 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapDbKeyErrorMessage(code?: string, detail?: string): string {
|
private mapDbKeyErrorMessage(code?: string, detail?: string): string {
|
||||||
|
|||||||
@@ -174,10 +174,10 @@ export class WcdbService {
|
|||||||
/**
|
/**
|
||||||
* 关闭服务
|
* 关闭服务
|
||||||
*/
|
*/
|
||||||
shutdown(): void {
|
async shutdown(): Promise<void> {
|
||||||
this.close()
|
try { await this.close() } catch {}
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
this.worker.terminate()
|
try { await this.worker.terminate() } catch {}
|
||||||
this.worker = null
|
this.worker = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,10 +121,6 @@
|
|||||||
{
|
{
|
||||||
"from": "electron/assets/wasm/",
|
"from": "electron/assets/wasm/",
|
||||||
"to": "assets/wasm/"
|
"to": "assets/wasm/"
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "resources/icon.icns",
|
|
||||||
"to": "icon.icns"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Binary file not shown.
123
src/components/ConfirmDialog.scss
Normal file
123
src/components/ConfirmDialog.scss
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
.confirm-dialog-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
width: 480px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
animation: slideUp 0.2s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
padding: 40px 40px 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 0 40px 24px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
padding: 0 40px 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.btn-cancel {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-confirm {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--on-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/components/ConfirmDialog.tsx
Normal file
32
src/components/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { X } from 'lucide-react'
|
||||||
|
import './ConfirmDialog.scss'
|
||||||
|
|
||||||
|
interface ConfirmDialogProps {
|
||||||
|
open: boolean
|
||||||
|
title?: string
|
||||||
|
message: string
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfirmDialog({ open, title, message, onConfirm, onCancel }: ConfirmDialogProps) {
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="confirm-dialog-overlay" onClick={onCancel}>
|
||||||
|
<div className="confirm-dialog" onClick={e => e.stopPropagation()}>
|
||||||
|
<button className="close-btn" onClick={onCancel}>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
{title && <div className="dialog-title">{title}</div>}
|
||||||
|
<div className="dialog-content">
|
||||||
|
<p style={{ whiteSpace: 'pre-line' }}>{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-actions">
|
||||||
|
<button className="btn-cancel" onClick={onCancel}>取消</button>
|
||||||
|
<button className="btn-confirm" onClick={onConfirm}>开始获取</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5202,6 +5202,24 @@ function MessageBubble({
|
|||||||
const [emojiError, setEmojiError] = useState(false)
|
const [emojiError, setEmojiError] = useState(false)
|
||||||
const [emojiLoading, setEmojiLoading] = useState(false)
|
const [emojiLoading, setEmojiLoading] = useState(false)
|
||||||
|
|
||||||
|
// 缓存相关的 state 必须在所有 Hooks 之前声明
|
||||||
|
const cacheKey = message.emojiMd5 || message.emojiCdnUrl || ''
|
||||||
|
const [emojiLocalPath, setEmojiLocalPath] = useState<string | undefined>(
|
||||||
|
() => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath
|
||||||
|
)
|
||||||
|
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
|
||||||
|
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
|
||||||
|
() => imageDataUrlCache.get(imageCacheKey)
|
||||||
|
)
|
||||||
|
const voiceCacheKey = `voice:${message.localId}`
|
||||||
|
const [voiceDataUrl, setVoiceDataUrl] = useState<string | undefined>(
|
||||||
|
() => voiceDataUrlCache.get(voiceCacheKey)
|
||||||
|
)
|
||||||
|
const voiceTranscriptCacheKey = `voice-transcript:${message.localId}`
|
||||||
|
const [voiceTranscript, setVoiceTranscript] = useState<string | undefined>(
|
||||||
|
() => voiceTranscriptCache.get(voiceTranscriptCacheKey)
|
||||||
|
)
|
||||||
|
|
||||||
// State variables...
|
// State variables...
|
||||||
const [imageError, setImageError] = useState(false)
|
const [imageError, setImageError] = useState(false)
|
||||||
const [imageLoading, setImageLoading] = useState(false)
|
const [imageLoading, setImageLoading] = useState(false)
|
||||||
@@ -5282,24 +5300,6 @@ function MessageBubble({
|
|||||||
loadConfig()
|
loadConfig()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 从缓存获取表情包 data URL
|
|
||||||
const cacheKey = message.emojiMd5 || message.emojiCdnUrl || ''
|
|
||||||
const [emojiLocalPath, setEmojiLocalPath] = useState<string | undefined>(
|
|
||||||
() => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath
|
|
||||||
)
|
|
||||||
const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}`
|
|
||||||
const [imageLocalPath, setImageLocalPath] = useState<string | undefined>(
|
|
||||||
() => imageDataUrlCache.get(imageCacheKey)
|
|
||||||
)
|
|
||||||
const voiceCacheKey = `voice:${message.localId}`
|
|
||||||
const [voiceDataUrl, setVoiceDataUrl] = useState<string | undefined>(
|
|
||||||
() => voiceDataUrlCache.get(voiceCacheKey)
|
|
||||||
)
|
|
||||||
const voiceTranscriptCacheKey = `voice-transcript:${message.localId}`
|
|
||||||
const [voiceTranscript, setVoiceTranscript] = useState<string | undefined>(
|
|
||||||
() => voiceTranscriptCache.get(voiceTranscriptCacheKey)
|
|
||||||
)
|
|
||||||
|
|
||||||
const formatTime = (timestamp: number): string => {
|
const formatTime = (timestamp: number): string => {
|
||||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return '未知时间'
|
if (!Number.isFinite(timestamp) || timestamp <= 0) return '未知时间'
|
||||||
const date = new Date(timestamp * 1000)
|
const date = new Date(timestamp * 1000)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-page {
|
.settings-page {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: min(1160px, calc(100vw - 96px));
|
width: min(1160px, calc(100vw - 96px));
|
||||||
|
|||||||
@@ -557,24 +557,37 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validatePath = (path: string): string | null => {
|
||||||
|
if (!path) return null
|
||||||
|
if (/[\u4e00-\u9fa5]/.test(path)) {
|
||||||
|
return '路径包含中文字符,请迁移至全英文目录'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handleAutoDetectPath = async () => {
|
const handleAutoDetectPath = async () => {
|
||||||
if (isDetectingPath) return
|
if (isDetectingPath) return
|
||||||
setIsDetectingPath(true)
|
setIsDetectingPath(true)
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.dbPath.autoDetect()
|
const result = await window.electronAPI.dbPath.autoDetect()
|
||||||
if (result.success && result.path) {
|
if (result.success && result.path) {
|
||||||
setDbPath(result.path)
|
const validationError = validatePath(result.path)
|
||||||
await configService.setDbPath(result.path)
|
if (validationError) {
|
||||||
showMessage(`自动检测成功:${result.path}`, true)
|
showMessage(validationError, false)
|
||||||
|
} else {
|
||||||
|
setDbPath(result.path)
|
||||||
|
await configService.setDbPath(result.path)
|
||||||
|
showMessage(`自动检测成功:${result.path}`, true)
|
||||||
|
|
||||||
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
||||||
setWxidOptions(wxids)
|
setWxidOptions(wxids)
|
||||||
if (wxids.length === 1) {
|
if (wxids.length === 1) {
|
||||||
await applyWxidSelection(wxids[0].wxid, {
|
await applyWxidSelection(wxids[0].wxid, {
|
||||||
toastText: `已检测到账号:${wxids[0].wxid}`
|
toastText: `已检测到账号:${wxids[0].wxid}`
|
||||||
})
|
})
|
||||||
} else if (wxids.length > 1) {
|
} else if (wxids.length > 1) {
|
||||||
setShowWxidSelect(true)
|
setShowWxidSelect(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showMessage(result.error || '未能自动检测到数据库目录', false)
|
showMessage(result.error || '未能自动检测到数据库目录', false)
|
||||||
@@ -591,9 +604,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] })
|
const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] })
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
const selectedPath = result.filePaths[0]
|
const selectedPath = result.filePaths[0]
|
||||||
setDbPath(selectedPath)
|
const validationError = validatePath(selectedPath)
|
||||||
await configService.setDbPath(selectedPath)
|
if (validationError) {
|
||||||
showMessage('已选择数据库目录', true)
|
showMessage(validationError, false)
|
||||||
|
} else {
|
||||||
|
setDbPath(selectedPath)
|
||||||
|
await configService.setDbPath(selectedPath)
|
||||||
|
showMessage('已选择数据库目录', true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showMessage('选择目录失败', false)
|
showMessage('选择目录失败', false)
|
||||||
@@ -1287,7 +1305,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>数据库根目录</label>
|
<label>数据库根目录</label>
|
||||||
<span className="form-hint">xwechat_files 目录</span>
|
<span className="form-hint">xwechat_files 目录</span>
|
||||||
<span className="form-hint" style={{ color: '#ff6b6b' }}> 目录路径不可包含中文,如有中文请去微信-设置-存储位置点击更改,迁移至全英文目录</span>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="例如: C:\Users\xxx\Documents\xwechat_files"
|
placeholder="例如: C:\Users\xxx\Documents\xwechat_files"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
|
|
||||||
/* Unified Card Container */
|
/* Unified Card Container */
|
||||||
.welcome-container {
|
.welcome-container {
|
||||||
|
position: relative;
|
||||||
width: 900px;
|
width: 900px;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
height: 620px;
|
height: 620px;
|
||||||
@@ -543,6 +544,18 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&.is-success {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
border-color: rgba(34, 197, 94, 0.3);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: rgb(134, 239, 172);
|
||||||
|
border-color: rgba(34, 197, 94, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles,
|
FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles,
|
||||||
UserRound, Wand2, Minus, X, HardDrive, RotateCcw
|
UserRound, Wand2, Minus, X, HardDrive, RotateCcw
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import ConfirmDialog from '../components/ConfirmDialog'
|
||||||
import './WelcomePage.scss'
|
import './WelcomePage.scss'
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -61,6 +62,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
const [imageKeyStatus, setImageKeyStatus] = useState('')
|
const [imageKeyStatus, setImageKeyStatus] = useState('')
|
||||||
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
|
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
|
||||||
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
|
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(null)
|
||||||
|
const [showDbKeyConfirm, setShowDbKeyConfirm] = useState(false)
|
||||||
|
|
||||||
// 安全相关 state
|
// 安全相关 state
|
||||||
const [enableAuth, setEnableAuth] = useState(false)
|
const [enableAuth, setEnableAuth] = useState(false)
|
||||||
@@ -123,6 +125,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
|
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
|
||||||
setDbKeyStatus(payload.message)
|
setDbKeyStatus(payload.message)
|
||||||
|
if (payload.message.includes('现在可以登录') || payload.message.includes('Hook安装成功')) {
|
||||||
|
window.electronAPI.notification?.show({
|
||||||
|
title: 'WeFlow 准备就绪',
|
||||||
|
content: '现在可以登录微信了',
|
||||||
|
avatarUrl: './logo.png',
|
||||||
|
sessionId: 'weflow-system'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => {
|
const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => {
|
||||||
let msg = payload.message;
|
let msg = payload.message;
|
||||||
@@ -187,6 +197,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
window.electronAPI.window.close()
|
window.electronAPI.window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validatePath = (path: string): string | null => {
|
||||||
|
if (!path) return null
|
||||||
|
// 检测中文字符和其他可能有问题的特殊字符
|
||||||
|
if (/[\u4e00-\u9fa5]/.test(path)) {
|
||||||
|
return '路径包含中文字符,请迁移至全英文目录'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectPath = async () => {
|
const handleSelectPath = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await dialog.openFile({
|
const result = await dialog.openFile({
|
||||||
@@ -195,8 +214,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
setDbPath(result.filePaths[0])
|
const selectedPath = result.filePaths[0]
|
||||||
setError('')
|
const validationError = validatePath(selectedPath)
|
||||||
|
if (validationError) {
|
||||||
|
setError(validationError)
|
||||||
|
} else {
|
||||||
|
setDbPath(selectedPath)
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('选择目录失败')
|
setError('选择目录失败')
|
||||||
@@ -210,8 +235,13 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.dbPath.autoDetect()
|
const result = await window.electronAPI.dbPath.autoDetect()
|
||||||
if (result.success && result.path) {
|
if (result.success && result.path) {
|
||||||
setDbPath(result.path)
|
const validationError = validatePath(result.path)
|
||||||
setError('')
|
if (validationError) {
|
||||||
|
setError(validationError)
|
||||||
|
} else {
|
||||||
|
setDbPath(result.path)
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || '未能检测到数据库目录')
|
setError(result.error || '未能检测到数据库目录')
|
||||||
}
|
}
|
||||||
@@ -287,6 +317,11 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
|
|
||||||
const handleAutoGetDbKey = async () => {
|
const handleAutoGetDbKey = async () => {
|
||||||
if (isFetchingDbKey) return
|
if (isFetchingDbKey) return
|
||||||
|
setShowDbKeyConfirm(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDbKeyConfirm = async () => {
|
||||||
|
setShowDbKeyConfirm(false)
|
||||||
setIsFetchingDbKey(true)
|
setIsFetchingDbKey(true)
|
||||||
setError('')
|
setError('')
|
||||||
setIsManualStartPrompt(false)
|
setIsManualStartPrompt(false)
|
||||||
@@ -297,7 +332,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
setDecryptKey(result.key)
|
setDecryptKey(result.key)
|
||||||
setDbKeyStatus('密钥获取成功')
|
setDbKeyStatus('密钥获取成功')
|
||||||
setError('')
|
setError('')
|
||||||
// 获取成功后自动扫描并填入 wxid
|
|
||||||
await handleScanWxid(true)
|
await handleScanWxid(true)
|
||||||
} else {
|
} else {
|
||||||
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
|
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
|
||||||
@@ -613,9 +647,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field-hint">请选择微信-设置-存储位置对应的目录</div>
|
<div className="field-hint">请选择微信-设置-存储位置对应的目录</div>
|
||||||
<div className="field-hint warning">
|
|
||||||
目录路径不可包含中文,如有中文请先在微信中迁移至全英文目录
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -705,7 +736,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dbKeyStatus && <div className="status-message">{dbKeyStatus}</div>}
|
{dbKeyStatus && <div className={`status-message ${dbKeyStatus.includes('现在可以登录') || dbKeyStatus.includes('Hook安装成功') ? 'is-success' : ''}`}>{dbKeyStatus}</div>}
|
||||||
<div className="field-hint">点击自动获取后微信将重启,请留意弹窗提示</div>
|
<div className="field-hint">点击自动获取后微信将重启,请留意弹窗提示</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -840,6 +871,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={showDbKeyConfirm}
|
||||||
|
title="开始获取数据库密钥"
|
||||||
|
message={`当开始获取后 WeFlow 将会执行准备操作
|
||||||
|
|
||||||
|
当 WeFlow 内的提示条变为绿色显示允许登录或看到来自WeFlow的登录通知时,登录你的微信或退出当前登录并重新登录。`}
|
||||||
|
onConfirm={handleDbKeyConfirm}
|
||||||
|
onCancel={() => setShowDbKeyConfirm(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user