mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-25 07:26:47 +00:00
@@ -70,7 +70,7 @@ interface DualReportData {
|
||||
friendExclusivePhrases: Array<{ phrase: string; count: number }>
|
||||
heatmap?: number[][]
|
||||
initiative?: { initiated: number; received: number }
|
||||
response?: { avg: number; fastest: number; slowest: number; count: number }
|
||||
response?: { avg: number; fastest: number; slowest?: number; count: number }
|
||||
monthly?: Record<string, number>
|
||||
streak?: { days: number; startDate: string; endDate: string }
|
||||
}
|
||||
@@ -149,7 +149,7 @@ function DualReportWindow() {
|
||||
|
||||
const generateReport = async (friendUsername: string, year: number) => {
|
||||
const taskId = registerBackgroundTask({
|
||||
sourcePage: 'dualReport',
|
||||
sourcePage: 'annualReport',
|
||||
title: '双人报告生成',
|
||||
detail: `正在生成 ${year === 0 ? '历史以来' : year + '年'} 双人年度报告`,
|
||||
progressText: '初始化',
|
||||
@@ -302,6 +302,17 @@ function DualReportWindow() {
|
||||
const handleClose = () => { navigate('/home') }
|
||||
|
||||
const formatFileYearLabel = (year: number) => (year === 0 ? '历史以来' : String(year))
|
||||
const formatMonthDayTime = (timestamp?: number) => {
|
||||
if (!timestamp || Number.isNaN(timestamp)) return ''
|
||||
const msTimestamp = timestamp > 1e12 ? timestamp : timestamp * 1000
|
||||
const date = new Date(msTimestamp)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const waitForNextPaint = () => new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => { requestAnimationFrame(() => resolve()) })
|
||||
@@ -427,7 +438,11 @@ function DualReportWindow() {
|
||||
|
||||
// 计算第一句话数据
|
||||
const displayFirstChat = reportData.yearFirstChat || reportData.firstChat
|
||||
const firstChatArray = (reportData.yearFirstChatMessages || reportData.firstChatMessages || (displayFirstChat ? [displayFirstChat] : [])).slice(0, 3)
|
||||
const firstChatArray = (
|
||||
reportData.yearFirstChat?.firstThreeMessages ||
|
||||
reportData.firstChatMessages ||
|
||||
(displayFirstChat ? [displayFirstChat] : [])
|
||||
).slice(0, 3)
|
||||
|
||||
// 聊天火花
|
||||
const showSpark = reportData.streak && reportData.streak.days > 0
|
||||
@@ -487,8 +502,8 @@ function DualReportWindow() {
|
||||
<div className="reveal-wrap"><h2 className="reveal-inner title delay-2">故事的开始</h2></div>
|
||||
<div className="s1-messages reveal-inner delay-3">
|
||||
{firstChatArray.map((chat: any, idx: number) => (
|
||||
<div key={idx} className={`s1-message-item ${chat.sender === 'self' ? 'sent' : ''}`}>
|
||||
<span className="s1-meta">{chat.createTimeStr || formatMonthDayTime(chat.timestamp)}</span>
|
||||
<div key={idx} className={`s1-message-item ${chat.isSentByMe ? 'sent' : ''}`}>
|
||||
<span className="s1-meta">{chat.createTimeStr || formatMonthDayTime(chat.createTime)}</span>
|
||||
<div className="scene-bubble s1-bubble">{formatFirstChat(chat.content)}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -65,6 +65,7 @@ import type { SnsPost } from '../types/sns'
|
||||
import {
|
||||
cloneExportDateRange,
|
||||
cloneExportDateRangeSelection,
|
||||
createExportDateRangeSelectionFromPreset,
|
||||
createDateRangeByLastNDays,
|
||||
createDefaultDateRange,
|
||||
createDefaultExportDateRangeSelection,
|
||||
@@ -1599,6 +1600,19 @@ const areExportSelectionsEqual = (left: ExportDateRangeSelection, right: ExportD
|
||||
left.dateRange.end.getTime() === right.dateRange.end.getTime()
|
||||
)
|
||||
|
||||
const resolveDynamicExportSelection = (
|
||||
selection: ExportDateRangeSelection,
|
||||
now = new Date()
|
||||
): ExportDateRangeSelection => {
|
||||
if (selection.useAllTime) {
|
||||
return cloneExportDateRangeSelection(selection)
|
||||
}
|
||||
if (selection.preset === 'custom') {
|
||||
return cloneExportDateRangeSelection(selection)
|
||||
}
|
||||
return createExportDateRangeSelectionFromPreset(selection.preset, now)
|
||||
}
|
||||
|
||||
const pickSessionMediaMetric = (
|
||||
metricRaw: SessionExportMetric | SessionContentMetric | undefined
|
||||
): SessionContentMetric | null => {
|
||||
@@ -4790,19 +4804,20 @@ function ExportPage() {
|
||||
const clearSelection = () => setSelectedSessions(new Set())
|
||||
|
||||
const openExportDialog = useCallback((payload: Omit<ExportDialogState, 'open' | 'intent'> & { intent?: ExportDialogState['intent'] }) => {
|
||||
const dynamicDefaultRangeSelection = resolveDynamicExportSelection(exportDefaultDateRangeSelection, new Date())
|
||||
setExportDialog({ open: true, intent: payload.intent || 'manual', ...payload })
|
||||
setIsTimeRangeDialogOpen(false)
|
||||
setTimeRangeBounds(null)
|
||||
setTimeRangeSelection(exportDefaultDateRangeSelection)
|
||||
setTimeRangeSelection(dynamicDefaultRangeSelection)
|
||||
|
||||
setOptions(prev => {
|
||||
const nextDateRange = cloneExportDateRange(exportDefaultDateRangeSelection.dateRange)
|
||||
const nextDateRange = cloneExportDateRange(dynamicDefaultRangeSelection.dateRange)
|
||||
|
||||
const next: ExportOptions = {
|
||||
...prev,
|
||||
format: exportDefaultFormat,
|
||||
exportAvatars: exportDefaultAvatars,
|
||||
useAllTime: exportDefaultDateRangeSelection.useAllTime,
|
||||
useAllTime: dynamicDefaultRangeSelection.useAllTime,
|
||||
dateRange: nextDateRange,
|
||||
exportMedia: Boolean(
|
||||
exportDefaultMedia.images ||
|
||||
@@ -4863,9 +4878,13 @@ function ExportPage() {
|
||||
setTimeRangeBounds(null)
|
||||
}, [])
|
||||
|
||||
const resolveChatExportTimeRangeBounds = useCallback(async (sessionIds: string[]): Promise<TimeRangeBounds | null> => {
|
||||
const resolveChatExportTimeRangeBounds = useCallback(async (
|
||||
sessionIds: string[],
|
||||
options?: { forceRefresh?: boolean }
|
||||
): Promise<TimeRangeBounds | null> => {
|
||||
const normalizedSessionIds = Array.from(new Set((sessionIds || []).map(id => String(id || '').trim()).filter(Boolean)))
|
||||
if (normalizedSessionIds.length === 0) return null
|
||||
const forceRefresh = options?.forceRefresh === true
|
||||
|
||||
const sessionRowMap = new Map<string, SessionRow>()
|
||||
for (const session of sessions) {
|
||||
@@ -4928,29 +4947,36 @@ function ExportPage() {
|
||||
return !resolved?.hasMin || !resolved?.hasMax
|
||||
})
|
||||
|
||||
const staleSessionIds = new Set<string>()
|
||||
|
||||
if (missingSessionIds().length > 0) {
|
||||
const cacheResult = await window.electronAPI.chat.getExportSessionStats(
|
||||
missingSessionIds(),
|
||||
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
||||
)
|
||||
applyStatsResult(cacheResult)
|
||||
for (const sessionId of cacheResult?.needsRefresh || []) {
|
||||
staleSessionIds.add(String(sessionId || '').trim())
|
||||
}
|
||||
}
|
||||
|
||||
const sessionsNeedingFreshStats = Array.from(new Set([
|
||||
...missingSessionIds(),
|
||||
...Array.from(staleSessionIds).filter(Boolean)
|
||||
]))
|
||||
|
||||
if (sessionsNeedingFreshStats.length > 0) {
|
||||
if (forceRefresh) {
|
||||
applyStatsResult(await window.electronAPI.chat.getExportSessionStats(
|
||||
sessionsNeedingFreshStats,
|
||||
{ includeRelations: false }
|
||||
normalizedSessionIds,
|
||||
{ includeRelations: false, forceRefresh: true }
|
||||
))
|
||||
} else {
|
||||
const staleSessionIds = new Set<string>()
|
||||
|
||||
if (missingSessionIds().length > 0) {
|
||||
const cacheResult = await window.electronAPI.chat.getExportSessionStats(
|
||||
missingSessionIds(),
|
||||
{ includeRelations: false, allowStaleCache: true, cacheOnly: true }
|
||||
)
|
||||
applyStatsResult(cacheResult)
|
||||
for (const sessionId of cacheResult?.needsRefresh || []) {
|
||||
staleSessionIds.add(String(sessionId || '').trim())
|
||||
}
|
||||
}
|
||||
|
||||
const sessionsNeedingFreshStats = Array.from(new Set([
|
||||
...missingSessionIds(),
|
||||
...Array.from(staleSessionIds).filter(Boolean)
|
||||
]))
|
||||
|
||||
if (sessionsNeedingFreshStats.length > 0) {
|
||||
applyStatsResult(await window.electronAPI.chat.getExportSessionStats(
|
||||
sessionsNeedingFreshStats,
|
||||
{ includeRelations: false }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (missingSessionIds().length > 0) {
|
||||
@@ -4971,14 +4997,26 @@ function ExportPage() {
|
||||
if (isResolvingTimeRangeBounds) return
|
||||
setIsResolvingTimeRangeBounds(true)
|
||||
try {
|
||||
const liveSelection = resolveDynamicExportSelection(timeRangeSelection, new Date())
|
||||
if (!areExportSelectionsEqual(liveSelection, timeRangeSelection)) {
|
||||
setTimeRangeSelection(liveSelection)
|
||||
setOptions(prev => ({
|
||||
...prev,
|
||||
useAllTime: liveSelection.useAllTime,
|
||||
dateRange: cloneExportDateRange(liveSelection.dateRange)
|
||||
}))
|
||||
}
|
||||
|
||||
let nextBounds: TimeRangeBounds | null = null
|
||||
if (exportDialog.scope !== 'sns') {
|
||||
nextBounds = await resolveChatExportTimeRangeBounds(exportDialog.sessionIds)
|
||||
nextBounds = await resolveChatExportTimeRangeBounds(exportDialog.sessionIds, {
|
||||
forceRefresh: exportDialog.scope === 'single'
|
||||
})
|
||||
}
|
||||
setTimeRangeBounds(nextBounds)
|
||||
if (nextBounds) {
|
||||
const nextSelection = clampExportSelectionToBounds(timeRangeSelection, nextBounds)
|
||||
if (!areExportSelectionsEqual(nextSelection, timeRangeSelection)) {
|
||||
const nextSelection = clampExportSelectionToBounds(liveSelection, nextBounds)
|
||||
if (!areExportSelectionsEqual(nextSelection, liveSelection)) {
|
||||
setTimeRangeSelection(nextSelection)
|
||||
setOptions(prev => ({
|
||||
...prev,
|
||||
@@ -5056,47 +5094,51 @@ function ExportPage() {
|
||||
return unsubscribe
|
||||
}, [loadBaseConfig, openExportDialog])
|
||||
|
||||
const buildExportOptions = (scope: TaskScope, contentType?: ContentType): ElectronExportOptions => {
|
||||
const buildExportOptions = (
|
||||
scope: TaskScope,
|
||||
contentType?: ContentType,
|
||||
sourceOptions: ExportOptions = options
|
||||
): ElectronExportOptions => {
|
||||
const sessionLayout: SessionLayout = writeLayout === 'C' ? 'per-session' : 'shared'
|
||||
const exportMediaEnabled = Boolean(
|
||||
options.exportImages ||
|
||||
options.exportVoices ||
|
||||
options.exportVideos ||
|
||||
options.exportEmojis ||
|
||||
options.exportFiles
|
||||
sourceOptions.exportImages ||
|
||||
sourceOptions.exportVoices ||
|
||||
sourceOptions.exportVideos ||
|
||||
sourceOptions.exportEmojis ||
|
||||
sourceOptions.exportFiles
|
||||
)
|
||||
|
||||
const base: ElectronExportOptions = {
|
||||
format: options.format,
|
||||
exportAvatars: options.exportAvatars,
|
||||
format: sourceOptions.format,
|
||||
exportAvatars: sourceOptions.exportAvatars,
|
||||
exportMedia: exportMediaEnabled,
|
||||
exportImages: options.exportImages,
|
||||
exportVoices: options.exportVoices,
|
||||
exportVideos: options.exportVideos,
|
||||
exportEmojis: options.exportEmojis,
|
||||
exportFiles: options.exportFiles,
|
||||
maxFileSizeMb: options.maxFileSizeMb,
|
||||
exportVoiceAsText: options.exportVoiceAsText,
|
||||
excelCompactColumns: options.excelCompactColumns,
|
||||
txtColumns: options.txtColumns,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
exportConcurrency: options.exportConcurrency,
|
||||
exportImages: sourceOptions.exportImages,
|
||||
exportVoices: sourceOptions.exportVoices,
|
||||
exportVideos: sourceOptions.exportVideos,
|
||||
exportEmojis: sourceOptions.exportEmojis,
|
||||
exportFiles: sourceOptions.exportFiles,
|
||||
maxFileSizeMb: sourceOptions.maxFileSizeMb,
|
||||
exportVoiceAsText: sourceOptions.exportVoiceAsText,
|
||||
excelCompactColumns: sourceOptions.excelCompactColumns,
|
||||
txtColumns: sourceOptions.txtColumns,
|
||||
displayNamePreference: sourceOptions.displayNamePreference,
|
||||
exportConcurrency: sourceOptions.exportConcurrency,
|
||||
fileNamingMode: exportDefaultFileNamingMode,
|
||||
sessionLayout,
|
||||
sessionNameWithTypePrefix,
|
||||
dateRange: options.useAllTime
|
||||
dateRange: sourceOptions.useAllTime
|
||||
? null
|
||||
: options.dateRange
|
||||
: sourceOptions.dateRange
|
||||
? {
|
||||
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
||||
end: Math.floor(options.dateRange.end.getTime() / 1000)
|
||||
start: Math.floor(sourceOptions.dateRange.start.getTime() / 1000),
|
||||
end: Math.floor(sourceOptions.dateRange.end.getTime() / 1000)
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
if (scope === 'content' && contentType) {
|
||||
if (contentType === 'text') {
|
||||
const textExportConcurrency = Math.min(2, Math.max(1, base.exportConcurrency ?? options.exportConcurrency))
|
||||
const textExportConcurrency = Math.min(2, Math.max(1, base.exportConcurrency ?? sourceOptions.exportConcurrency))
|
||||
return {
|
||||
...base,
|
||||
contentType,
|
||||
@@ -5127,14 +5169,14 @@ function ExportPage() {
|
||||
return base
|
||||
}
|
||||
|
||||
const buildSnsExportOptions = () => {
|
||||
const buildSnsExportOptions = (sourceOptions: ExportOptions = options) => {
|
||||
const format: SnsTimelineExportFormat = snsExportFormat
|
||||
const dateRange = options.useAllTime
|
||||
const dateRange = sourceOptions.useAllTime
|
||||
? null
|
||||
: options.dateRange
|
||||
: sourceOptions.dateRange
|
||||
? {
|
||||
startTime: Math.floor(options.dateRange.start.getTime() / 1000),
|
||||
endTime: Math.floor(options.dateRange.end.getTime() / 1000)
|
||||
startTime: Math.floor(sourceOptions.dateRange.start.getTime() / 1000),
|
||||
endTime: Math.floor(sourceOptions.dateRange.end.getTime() / 1000)
|
||||
}
|
||||
: null
|
||||
|
||||
@@ -5946,12 +5988,27 @@ function ExportPage() {
|
||||
if (!exportDialog.open || !exportFolder) return
|
||||
if (exportDialog.scope !== 'sns' && exportDialog.sessionIds.length === 0) return
|
||||
|
||||
const effectiveRangeSelection = resolveDynamicExportSelection(timeRangeSelection, new Date())
|
||||
if (!areExportSelectionsEqual(effectiveRangeSelection, timeRangeSelection)) {
|
||||
setTimeRangeSelection(effectiveRangeSelection)
|
||||
}
|
||||
const effectiveOptionsState: ExportOptions = {
|
||||
...options,
|
||||
useAllTime: effectiveRangeSelection.useAllTime,
|
||||
dateRange: cloneExportDateRange(effectiveRangeSelection.dateRange)
|
||||
}
|
||||
setOptions(prev => ({
|
||||
...prev,
|
||||
useAllTime: effectiveOptionsState.useAllTime,
|
||||
dateRange: cloneExportDateRange(effectiveRangeSelection.dateRange)
|
||||
}))
|
||||
|
||||
const isAutomationCreateIntent = exportDialog.intent === 'automation-create'
|
||||
const exportOptions = exportDialog.scope === 'sns'
|
||||
? undefined
|
||||
: buildExportOptions(exportDialog.scope, exportDialog.contentType)
|
||||
: buildExportOptions(exportDialog.scope, exportDialog.contentType, effectiveOptionsState)
|
||||
const snsOptions = exportDialog.scope === 'sns'
|
||||
? buildSnsExportOptions()
|
||||
? buildSnsExportOptions(effectiveOptionsState)
|
||||
: undefined
|
||||
const title =
|
||||
exportDialog.scope === 'single'
|
||||
@@ -5968,7 +6025,7 @@ function ExportPage() {
|
||||
return
|
||||
}
|
||||
const { dateRange: _discard, ...optionTemplate } = exportOptions
|
||||
const normalizedRangeSelection = cloneExportDateRangeSelection(timeRangeSelection)
|
||||
const normalizedRangeSelection = cloneExportDateRangeSelection(effectiveRangeSelection)
|
||||
const scope = exportDialog.scope === 'single'
|
||||
? 'single'
|
||||
: exportDialog.scope === 'content'
|
||||
|
||||
@@ -338,6 +338,22 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.mac-key-faq-link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #0f62fe;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
margin-top: 6px;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.manual-prompt {
|
||||
background: rgba(139, 115, 85, 0.1);
|
||||
border: 1px dashed rgba(139, 115, 85, 0.3);
|
||||
|
||||
@@ -56,6 +56,7 @@ const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootp
|
||||
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||
const isWindows = !isMac && !isLinux
|
||||
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
|
||||
|
||||
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||
const dbPathPlaceholder = isMac
|
||||
@@ -225,6 +226,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
|
||||
const [showDecryptKey, setShowDecryptKey] = useState(false)
|
||||
const [dbKeyStatus, setDbKeyStatus] = useState('')
|
||||
const [dbKeyError, setDbKeyError] = useState('')
|
||||
const [imageKeyStatus, setImageKeyStatus] = useState('')
|
||||
const [isManualStartPrompt, setIsManualStartPrompt] = useState(false)
|
||||
const [isClearingAnalyticsCache, setIsClearingAnalyticsCache] = useState(false)
|
||||
@@ -1254,12 +1256,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
if (isFetchingDbKey) return
|
||||
setIsFetchingDbKey(true)
|
||||
setIsManualStartPrompt(false)
|
||||
setDbKeyError('')
|
||||
setDbKeyStatus('正在连接微信进程...')
|
||||
try {
|
||||
const result = await window.electronAPI.key.autoGetDbKey()
|
||||
if (result.success && result.key) {
|
||||
setDecryptKey(result.key)
|
||||
setDbKeyStatus('密钥获取成功')
|
||||
setDbKeyError('')
|
||||
showMessage('已自动获取解密密钥', true)
|
||||
await syncCurrentKeys({ decryptKey: result.key, wxid })
|
||||
const keysOverride = buildKeysFromInputs({ decryptKey: result.key })
|
||||
@@ -1274,17 +1278,26 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
) {
|
||||
setIsManualStartPrompt(true)
|
||||
setDbKeyStatus('需要手动启动微信')
|
||||
setDbKeyError('')
|
||||
} else {
|
||||
showMessage(result.error || '自动获取密钥失败', false)
|
||||
const failureMessage = result.error || '自动获取密钥失败'
|
||||
setDbKeyError(failureMessage)
|
||||
showMessage(failureMessage, false)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
showMessage(`自动获取密钥失败: ${e}`, false)
|
||||
const failureMessage = `自动获取密钥失败: ${e}`
|
||||
setDbKeyError(failureMessage)
|
||||
showMessage(failureMessage, false)
|
||||
} finally {
|
||||
setIsFetchingDbKey(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openMacKeyFaq = () => {
|
||||
void window.electronAPI.shell.openExternal(MAC_KEY_FAQ_URL)
|
||||
}
|
||||
|
||||
const handleManualConfirm = async () => {
|
||||
setIsManualStartPrompt(false)
|
||||
handleAutoGetDbKey()
|
||||
@@ -2207,6 +2220,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
</button>
|
||||
)}
|
||||
{dbKeyStatus && <div className="form-hint status-text">{dbKeyStatus}</div>}
|
||||
{isMac && dbKeyError && (
|
||||
<button type="button" className="mac-key-faq-link" onClick={openMacKeyFaq}>
|
||||
查看 macOS 获取密钥排障指引
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
|
||||
@@ -666,7 +666,28 @@
|
||||
font-size: 14px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-link-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #0f62fe;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-footer {
|
||||
|
||||
@@ -14,6 +14,7 @@ import './WelcomePage.scss'
|
||||
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||
const isWindows = !isMac && !isLinux
|
||||
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
|
||||
|
||||
const DB_PATH_CHINESE_ERROR = '路径包含中文字符,迁移至全英文目录后再试'
|
||||
const dbPathPlaceholder = isMac
|
||||
@@ -39,10 +40,19 @@ interface WelcomePageProps {
|
||||
|
||||
const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
|
||||
const base = String(error || '自动获取密钥失败').trim()
|
||||
const isInternalLine = (line: string): boolean => {
|
||||
const lower = line.toLowerCase()
|
||||
return lower.includes('xkey_helper')
|
||||
|| lower.includes('[debug]')
|
||||
|| lower.includes('breakpoint')
|
||||
|| lower.includes('hook installed @')
|
||||
|| lower.includes('scanner ')
|
||||
}
|
||||
const tailLogs = Array.isArray(logs)
|
||||
? logs
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
.filter(item => Boolean(item) && !isInternalLine(item))
|
||||
.map(item => item.length > 80 ? `${item.slice(0, 80)}...` : item)
|
||||
.slice(-6)
|
||||
: []
|
||||
if (tailLogs.length === 0) return base
|
||||
@@ -117,6 +127,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
const [isImageStepAutoCompleted, setIsImageStepAutoCompleted] = useState(false)
|
||||
const [hasReacquiredDbKey, setHasReacquiredDbKey] = useState(!isAddAccountMode)
|
||||
const [showDbKeyConfirm, setShowDbKeyConfirm] = useState(false)
|
||||
const [lastDbKeyError, setLastDbKeyError] = useState('')
|
||||
const imagePrefetchAttemptRef = useRef<string>('')
|
||||
|
||||
// 安全相关 state
|
||||
@@ -476,6 +487,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
setShowDbKeyConfirm(false)
|
||||
setIsFetchingDbKey(true)
|
||||
setError('')
|
||||
setLastDbKeyError('')
|
||||
setIsManualStartPrompt(false)
|
||||
setDbKeyStatus('正在连接微信进程...')
|
||||
try {
|
||||
@@ -499,20 +511,29 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
) {
|
||||
setIsManualStartPrompt(true)
|
||||
setDbKeyStatus('需要手动启动微信')
|
||||
setLastDbKeyError('')
|
||||
} else {
|
||||
if (result.error?.includes('尚未完成登录')) {
|
||||
setDbKeyStatus('请先在微信完成登录后重试')
|
||||
}
|
||||
setError(formatDbKeyFailureMessage(result.error, result.logs))
|
||||
const failureMessage = formatDbKeyFailureMessage(result.error, result.logs)
|
||||
setError(failureMessage)
|
||||
setLastDbKeyError(failureMessage)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`自动获取密钥失败: ${e}`)
|
||||
const failureMessage = `自动获取密钥失败: ${e}`
|
||||
setError(failureMessage)
|
||||
setLastDbKeyError(failureMessage)
|
||||
} finally {
|
||||
setIsFetchingDbKey(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openMacKeyFaq = () => {
|
||||
void window.electronAPI.shell.openExternal(MAC_KEY_FAQ_URL)
|
||||
}
|
||||
|
||||
const handleManualConfirm = async () => {
|
||||
setIsManualStartPrompt(false)
|
||||
handleAutoGetDbKey()
|
||||
@@ -1161,7 +1182,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<div className="error-text">{error}</div>
|
||||
{isMac && error === lastDbKeyError && (
|
||||
<button type="button" className="error-link-btn" onClick={openMacKeyFaq}>
|
||||
查看 macOS 获取密钥排障指引
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep.id === 'intro' && (
|
||||
<div className="intro-footer">
|
||||
|
||||
Reference in New Issue
Block a user