From 8a0d3b02d9132a5bcbbe445d8e7ecdae7918f599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com> Date: Sun, 9 Aug 2026 14:42:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E3=80=81=E7=9F=A5=E8=AF=86=E5=BA=93=E7=9B=AE=E5=BD=95=E4=B8=8E?= =?UTF-8?q?=20Windows=20=E8=BF=90=E8=A1=8C=E5=BA=93=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/index.ts | 25 ++++++++- src/main/services/cache-service.ts | 15 +++++- src/main/services/chat-service.ts | 6 +++ src/preload/index.d.ts | 7 ++- src/preload/index.ts | 2 + .../src/components/DatabaseConnectionPage.tsx | 16 +++++- .../components/search/AISearchWorkspace.tsx | 7 +++ .../settings/pages/CacheCleanupPage.tsx | 54 +++++++++++++++---- .../src/styles/settings-preferences.scss | 15 ++++++ src/shared/database-key.ts | 10 ++++ src/shared/windows-runtime.ts | 13 +++++ .../ai-search-cache-consent.test.tsx | 50 +++++++++++++++++ tests/component/cache-cleanup.test.tsx | 42 +++++++++++++++ tests/component/database-connection.test.tsx | 12 +++++ tests/integration/preload-contract.test.ts | 2 + tests/unit/windows-runtime.test.ts | 27 ++++++++++ 16 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 src/shared/windows-runtime.ts create mode 100644 tests/component/cache-cleanup.test.tsx create mode 100644 tests/unit/windows-runtime.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index 7de6595..ec5ee08 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -97,7 +97,7 @@ import { agentHubService } from './services/agent-hub-service' import { appLogger } from './app-logger' import type { AppLogEntry } from '../shared/app-log' import { appUpdateService } from './services/app-update-service' -import { clearCache, getCacheSummary } from './services/cache-service' +import { clearCache, getCacheSummary, openKnowledgeDirectory } from './services/cache-service' import type { CacheClearScope } from './services/cache-service' import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-archive-service' import { VideoAssetService } from './video-asset-service' @@ -109,6 +109,10 @@ import { VoiceBatchService } from './voice-pipeline/voice-batch-service' import type { VoiceBatchRequest, VoiceMessageReference } from '../shared/voice-recognition' import type { AiSearchPipelineRequest } from '../shared/ai-search' import type { KnowledgeSearchIpcRequest, KnowledgeSearchIpcResult } from '../shared/knowledge' +import { + isWindowsVcRuntimeMissingError, + WINDOWS_VC_RUNTIME_ERROR_MESSAGE +} from '../shared/windows-runtime' import { KnowledgeSearchService } from './knowledge/knowledge-search-service' import { AiSearchPipelineService } from './services/ai-search-pipeline-service' @@ -506,6 +510,13 @@ app.whenReady().then(async () => { wcdbBootstrapPromise = bootstrapWcdbNativeAsync().then(() => { console.log('[WCDB4] async bootstrap complete') }) + void wcdbBootstrapPromise.catch((error) => { + appLogger.write({ + level: 'error', + scope: 'wcdb-bootstrap', + message: error instanceof Error ? error.message : String(error) + }) + }) // 设置应用程序用户模型 ID electronApp.setAppUserModelId('com.wechatexplorer.app') @@ -529,6 +540,7 @@ app.whenReady().then(async () => { ipcMain.handle('app-update:download', () => appUpdateService.download()) ipcMain.handle('app-update:install', () => appUpdateService.install()) ipcMain.handle('cache:getSummary', () => getCacheSummary()) + ipcMain.handle('cache:openKnowledgeDirectory', () => openKnowledgeDirectory()) ipcMain.handle('cache:clear', async (_, scope: CacheClearScope) => { const allowedScopes: CacheClearScope[] = ['bootstrap', 'electron', 'knowledge', 'all'] if (!allowedScopes.includes(scope)) return getCacheSummary() @@ -625,7 +637,16 @@ app.whenReady().then(async () => { return { success: true, monitoring } } catch (error) { console.error('Failed to init DB:', error) - return { success: false, error: error instanceof Error ? error.message : String(error) } + const detail = error instanceof Error ? error.message : String(error) + if (isWindowsVcRuntimeMissingError(detail, process.platform)) { + return { + success: false, + code: 'VC_RUNTIME_MISSING', + error: WINDOWS_VC_RUNTIME_ERROR_MESSAGE, + monitoring: false + } + } + return { success: false, error: detail } } finally { dbInitInFlight = null } diff --git a/src/main/services/cache-service.ts b/src/main/services/cache-service.ts index 337607a..d8ff7a5 100644 --- a/src/main/services/cache-service.ts +++ b/src/main/services/cache-service.ts @@ -1,4 +1,4 @@ -import { app, session } from 'electron' +import { app, session, shell } from 'electron' import fs from 'fs-extra' import path from 'path' import { clearBootstrapCache } from './bootstrap-cache' @@ -62,7 +62,8 @@ export function getCacheSummary(): CacheSummary { { id: 'knowledge', label: '本地知识库索引', - description: '为问问微信建立的所有账号本地检索索引。清理后需手动重新建立,不影响微信原始数据。', + description: + '为问问微信建立的所有账号本地检索索引。清理后需手动重新建立,不影响微信原始数据。', ...knowledge } ] @@ -90,3 +91,13 @@ export async function clearCache( } return getCacheSummary() } + +export async function openKnowledgeDirectory(): Promise<{ success: boolean; error?: string }> { + try { + await fs.ensureDir(KNOWLEDGE_CACHE_DIR) + const error = await shell.openPath(KNOWLEDGE_CACHE_DIR) + return error ? { success: false, error } : { success: true } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index 6b8a511..b7f517d 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -9,6 +9,10 @@ import type { DatabaseKeyValidationCode, DatabaseKeyValidationResult } from '../../shared/database-key' +import { + isWindowsVcRuntimeMissingError, + WINDOWS_VC_RUNTIME_ERROR_MESSAGE +} from '../../shared/windows-runtime' import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service' import type { ExportImageQuality } from '../../shared/image-quality' @@ -675,11 +679,13 @@ const DATABASE_KEY_ERROR_MESSAGES: Record = { ACCOUNT_MISMATCH: '密钥与当前账号不匹配', ROOT_UNAVAILABLE: '当前数据库目录不可用', DATABASE_FILE_MISSING: '数据库文件缺失', + VC_RUNTIME_MISSING: WINDOWS_VC_RUNTIME_ERROR_MESSAGE, UNKNOWN_VALIDATION_ERROR: '未知验证错误' } function mapConnectionError(detail: string): DatabaseKeyValidationCode { const normalized = detail.toLowerCase() + if (isWindowsVcRuntimeMissingError(detail, process.platform)) return 'VC_RUNTIME_MISSING' if (normalized.includes('-1005') || normalized.includes('不匹配')) return 'ACCOUNT_MISMATCH' if (normalized.includes('session.db') || normalized.includes('数据库文件')) { return 'DATABASE_FILE_MISSING' diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index b0a0b02..2e5eaeb 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -10,6 +10,7 @@ import { } from '../shared/report-history' import type { DatabaseKeyEnvironment, + DatabaseInitResult, DatabaseKeyStorageResult, DatabaseKeyValidationResult, AccountDiscoveryResult @@ -141,10 +142,8 @@ declare global { onAppUpdateState: (callback: (state: AppUpdateState) => void) => () => void getCacheSummary: () => Promise clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all') => Promise - initDb: ( - key: string, - accountRoot: string - ) => Promise + openKnowledgeDirectory: () => Promise<{ success: boolean; error?: string }> + initDb: (key: string, accountRoot: string) => Promise discoverAccounts: (inputPath: string) => Promise getBootstrapCache: () => Promise<{ self?: { wxid: string; nickname: string; avatar?: string; accountRoot: string } diff --git a/src/preload/index.ts b/src/preload/index.ts index a0e87eb..b080f64 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -66,6 +66,8 @@ const api = { getCacheSummary: (): Promise => ipcRenderer.invoke('cache:getSummary'), clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all'): Promise => ipcRenderer.invoke('cache:clear', scope), + openKnowledgeDirectory: (): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke('cache:openKnowledgeDirectory'), initDb: (key: string, accountRoot: string) => ipcRenderer.invoke('db:init', key, accountRoot), discoverAccounts: (inputPath: string): Promise => ipcRenderer.invoke('accounts:discover', inputPath), diff --git a/src/renderer/src/components/DatabaseConnectionPage.tsx b/src/renderer/src/components/DatabaseConnectionPage.tsx index 5980d1f..fb4aa7e 100644 --- a/src/renderer/src/components/DatabaseConnectionPage.tsx +++ b/src/renderer/src/components/DatabaseConnectionPage.tsx @@ -1,5 +1,6 @@ import React from 'react' import type { DatabaseKeyEnvironment, WechatAccountCandidate } from '../../../shared/database-key' +import { WINDOWS_VC_RUNTIME_DOWNLOAD_URL } from '../../../shared/windows-runtime' const GUIDE_URL = 'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md' @@ -461,7 +462,12 @@ export function DatabaseConnectionPage({ ) : ( - 'Windows 已完整支持,不需要关闭 SIP。' + <> + Windows 需要 Microsoft Visual C++ 2015-2022 x64 运行库。{' '} + + 下载运行库 + + )}

{showMacKeyFaq && isMac && ( @@ -521,6 +527,14 @@ export function DatabaseConnectionPage({ )} {status &&
{status}
} + {platform === 'win32' && ( +

+ 无法加载数据库组件时,请安装 Microsoft Visual C++ 2015-2022 x64 运行库。{' '} + + 下载运行库 + +

+ )} + )} + - ))}
@@ -113,7 +143,9 @@ export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) =>
说明 - 缓存没有过期时间,只有在这里手动清理,或应用检测到格式需要迁移时才会被替换。 + + 缓存没有过期时间,只有在这里手动清理,或应用检测到格式需要迁移时才会被替换。 +
diff --git a/src/renderer/src/styles/settings-preferences.scss b/src/renderer/src/styles/settings-preferences.scss index 2b407bd..55fa414 100644 --- a/src/renderer/src/styles/settings-preferences.scss +++ b/src/renderer/src/styles/settings-preferences.scss @@ -2,6 +2,7 @@ .settings-primary-button, .settings-danger-button, .settings-cache-item > button, +.settings-cache-actions button, .about-links-card button { border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); @@ -101,6 +102,12 @@ } } +.settings-cache-actions { + display: flex; + flex: 0 0 auto; + gap: 8px; +} + .settings-inline-note, .settings-footnote { color: var(--wxex-text-muted); @@ -820,4 +827,12 @@ align-items: flex-start; flex-direction: column; } + + .settings-cache-actions { + width: 100%; + + button { + flex: 1; + } + } } diff --git a/src/shared/database-key.ts b/src/shared/database-key.ts index f92634e..12ba0aa 100644 --- a/src/shared/database-key.ts +++ b/src/shared/database-key.ts @@ -4,8 +4,18 @@ export type DatabaseKeyValidationCode = | 'ACCOUNT_MISMATCH' | 'ROOT_UNAVAILABLE' | 'DATABASE_FILE_MISSING' + | 'VC_RUNTIME_MISSING' | 'UNKNOWN_VALIDATION_ERROR' +export type DatabaseInitCode = DatabaseKeyValidationCode | 'ACCOUNT_SELECTION_REQUIRED' + +export interface DatabaseInitResult { + success: boolean + code?: DatabaseInitCode + error?: string + monitoring?: boolean +} + export interface DatabaseKeyValidationResult { success: boolean code?: DatabaseKeyValidationCode diff --git a/src/shared/windows-runtime.ts b/src/shared/windows-runtime.ts new file mode 100644 index 0000000..e18fea4 --- /dev/null +++ b/src/shared/windows-runtime.ts @@ -0,0 +1,13 @@ +export const WINDOWS_VC_RUNTIME_DOWNLOAD_URL = 'https://aka.ms/vc14/vc_redist.x64.exe' + +export const WINDOWS_VC_RUNTIME_ERROR_MESSAGE = `当前 Windows 缺少 Microsoft Visual C++ 2015-2022 x64 运行库,无法加载微信数据库组件。请下载安装后重新启动 WechatExplorer:${WINDOWS_VC_RUNTIME_DOWNLOAD_URL}` + +const VC_RUNTIME_LIBRARY_PATTERN = + /(?:vcruntime140(?:_1)?\.dll|msvcp140(?:_[12])?\.dll|concrt140\.dll|ucrtbase\.dll|api-ms-win-crt)/i +const WINDOWS_DEPENDENCY_LOAD_PATTERN = + /(?:the specified module could not be found|找不到指定的模块|找不到指定模块|win32 error\s*126|error\s*126|err_dlopen_failed)/i + +export function isWindowsVcRuntimeMissingError(detail: string, platform: string): boolean { + if (platform !== 'win32') return false + return VC_RUNTIME_LIBRARY_PATTERN.test(detail) || WINDOWS_DEPENDENCY_LOAD_PATTERN.test(detail) +} diff --git a/tests/component/ai-search-cache-consent.test.tsx b/tests/component/ai-search-cache-consent.test.tsx index a7ba453..97bedc2 100644 --- a/tests/component/ai-search-cache-consent.test.tsx +++ b/tests/component/ai-search-cache-consent.test.tsx @@ -376,6 +376,56 @@ describe('AISearchWorkspace cache privacy boundary', () => { expect(api.runAiSearch).not.toHaveBeenCalled() }) + it('submits with Enter and keeps Shift+Enter available for a new line', async () => { + api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false }) + api.runAiSearch.mockResolvedValue({ + requestId: 'enter-submit', + status: 'completed', + answer: 'answer', + plan: { intent: 'global_topic_search' }, + knowledge: { indexedMessageCount: 1, indexedChunkCount: 1, totalMessages: 1 }, + candidateEvidenceCount: 0, + contextEvidenceCount: 0, + evidence: [], + aggregation: { + messageCount: 0, + peopleCount: 0, + conversationCount: 0, + people: [], + conversations: [] + }, + agent: { mode: 'agent', toolCalls: 1, trace: [] }, + timings: {}, + elapsedMs: 1 + } as never) + render( + + ) + + const input = screen.getByRole('textbox') + await userEvent.type(input, 'Enter submit question') + fireEvent.keyDown(input, { key: 'Enter', shiftKey: true }) + expect(api.runAiSearch).not.toHaveBeenCalled() + + fireEvent.keyDown(input, { key: 'Enter' }) + await waitFor(() => expect(api.runAiSearch).toHaveBeenCalledOnce()) + }) + it('cancels an active analysis and ignores its late result', async () => { api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false }) let resolveSearch: ((value: unknown) => void) | undefined diff --git a/tests/component/cache-cleanup.test.tsx b/tests/component/cache-cleanup.test.tsx new file mode 100644 index 0000000..1a7350c --- /dev/null +++ b/tests/component/cache-cleanup.test.tsx @@ -0,0 +1,42 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CacheCleanupPage } from '../../src/renderer/src/features/settings/pages/CacheCleanupPage' + +const api = { + getCacheSummary: vi.fn(), + clearCache: vi.fn(), + openKnowledgeDirectory: vi.fn() +} + +describe('CacheCleanupPage', () => { + beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(window, 'api', { configurable: true, value: api }) + api.getCacheSummary.mockResolvedValue({ + items: [ + { + id: 'knowledge', + label: '本地知识库索引', + description: '问一问微信使用的本地检索索引', + sizeBytes: 1024, + fileCount: 1 + } + ], + totalBytes: 1024, + updatedAt: Date.now() + }) + api.openKnowledgeDirectory.mockResolvedValue({ success: true }) + }) + + it('opens the real knowledge directory without clearing it', async () => { + const onNotice = vi.fn() + render() + + await userEvent.click(await screen.findByRole('button', { name: '打开文件夹' })) + + await waitFor(() => expect(api.openKnowledgeDirectory).toHaveBeenCalledOnce()) + expect(api.clearCache).not.toHaveBeenCalled() + expect(onNotice).toHaveBeenCalledWith('已打开知识库文件夹') + }) +}) diff --git a/tests/component/database-connection.test.tsx b/tests/component/database-connection.test.tsx index 8143151..4e44656 100644 --- a/tests/component/database-connection.test.tsx +++ b/tests/component/database-connection.test.tsx @@ -180,4 +180,16 @@ describe('DatabaseConnectionPage', () => { expect(screen.getByRole('button', { name: '返回上一步' })).toBeEnabled() expect(screen.getByRole('button', { name: '取消并重新检查' })).toBeEnabled() }) + it('provides the official Visual C++ runtime download on Windows', () => { + renderPage({ + status: '当前 Windows 缺少 Microsoft Visual C++ 运行库', + statusKind: 'error' + }) + + expect(screen.getByRole('link', { name: '下载运行库' })).toHaveAttribute( + 'href', + 'https://aka.ms/vc14/vc_redist.x64.exe' + ) + }) + }) diff --git a/tests/integration/preload-contract.test.ts b/tests/integration/preload-contract.test.ts index 81129be..c1e9524 100644 --- a/tests/integration/preload-contract.test.ts +++ b/tests/integration/preload-contract.test.ts @@ -62,6 +62,8 @@ describe('preload IPC contract', () => { expect(invoke).toHaveBeenLastCalledWith('knowledge:startIndex') await api.clearCache('knowledge') expect(invoke).toHaveBeenLastCalledWith('cache:clear', 'knowledge') + await api.openKnowledgeDirectory() + expect(invoke).toHaveBeenLastCalledWith('cache:openKnowledgeDirectory') await api.getImage('fixture-md5', 'fixture.dat', 'fixture-session', { force: true, diff --git a/tests/unit/windows-runtime.test.ts b/tests/unit/windows-runtime.test.ts new file mode 100644 index 0000000..c5834cc --- /dev/null +++ b/tests/unit/windows-runtime.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { + isWindowsVcRuntimeMissingError, + WINDOWS_VC_RUNTIME_DOWNLOAD_URL, + WINDOWS_VC_RUNTIME_ERROR_MESSAGE +} from '../../src/shared/windows-runtime' + +describe('Windows Visual C++ runtime diagnostics', () => { + it.each([ + 'VCRUNTIME140_1.dll was not found', + 'MSVCP140.dll is missing', + 'The specified module could not be found', + 'Win32 error 126', + '找不到指定的模块' + ])('recognizes a Windows native dependency load failure: %s', (detail) => { + expect(isWindowsVcRuntimeMissingError(detail, 'win32')).toBe(true) + }) + + it('does not relabel unrelated or non-Windows failures', () => { + expect(isWindowsVcRuntimeMissingError('invalid database key', 'win32')).toBe(false) + expect(isWindowsVcRuntimeMissingError('VCRUNTIME140.dll was not found', 'darwin')).toBe(false) + }) + + it('keeps the official download URL in the actionable error message', () => { + expect(WINDOWS_VC_RUNTIME_ERROR_MESSAGE).toContain(WINDOWS_VC_RUNTIME_DOWNLOAD_URL) + }) +})