mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 20:19:09 +08:00
fix: 完善交互、知识库目录与 Windows 运行库提示
This commit is contained in:
+23
-2
@@ -97,7 +97,7 @@ import { agentHubService } from './services/agent-hub-service'
|
|||||||
import { appLogger } from './app-logger'
|
import { appLogger } from './app-logger'
|
||||||
import type { AppLogEntry } from '../shared/app-log'
|
import type { AppLogEntry } from '../shared/app-log'
|
||||||
import { appUpdateService } from './services/app-update-service'
|
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 type { CacheClearScope } from './services/cache-service'
|
||||||
import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-archive-service'
|
import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-archive-service'
|
||||||
import { VideoAssetService } from './video-asset-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 { VoiceBatchRequest, VoiceMessageReference } from '../shared/voice-recognition'
|
||||||
import type { AiSearchPipelineRequest } from '../shared/ai-search'
|
import type { AiSearchPipelineRequest } from '../shared/ai-search'
|
||||||
import type { KnowledgeSearchIpcRequest, KnowledgeSearchIpcResult } from '../shared/knowledge'
|
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 { KnowledgeSearchService } from './knowledge/knowledge-search-service'
|
||||||
import { AiSearchPipelineService } from './services/ai-search-pipeline-service'
|
import { AiSearchPipelineService } from './services/ai-search-pipeline-service'
|
||||||
|
|
||||||
@@ -506,6 +510,13 @@ app.whenReady().then(async () => {
|
|||||||
wcdbBootstrapPromise = bootstrapWcdbNativeAsync().then(() => {
|
wcdbBootstrapPromise = bootstrapWcdbNativeAsync().then(() => {
|
||||||
console.log('[WCDB4] async bootstrap complete')
|
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
|
// 设置应用程序用户模型 ID
|
||||||
electronApp.setAppUserModelId('com.wechatexplorer.app')
|
electronApp.setAppUserModelId('com.wechatexplorer.app')
|
||||||
@@ -529,6 +540,7 @@ app.whenReady().then(async () => {
|
|||||||
ipcMain.handle('app-update:download', () => appUpdateService.download())
|
ipcMain.handle('app-update:download', () => appUpdateService.download())
|
||||||
ipcMain.handle('app-update:install', () => appUpdateService.install())
|
ipcMain.handle('app-update:install', () => appUpdateService.install())
|
||||||
ipcMain.handle('cache:getSummary', () => getCacheSummary())
|
ipcMain.handle('cache:getSummary', () => getCacheSummary())
|
||||||
|
ipcMain.handle('cache:openKnowledgeDirectory', () => openKnowledgeDirectory())
|
||||||
ipcMain.handle('cache:clear', async (_, scope: CacheClearScope) => {
|
ipcMain.handle('cache:clear', async (_, scope: CacheClearScope) => {
|
||||||
const allowedScopes: CacheClearScope[] = ['bootstrap', 'electron', 'knowledge', 'all']
|
const allowedScopes: CacheClearScope[] = ['bootstrap', 'electron', 'knowledge', 'all']
|
||||||
if (!allowedScopes.includes(scope)) return getCacheSummary()
|
if (!allowedScopes.includes(scope)) return getCacheSummary()
|
||||||
@@ -625,7 +637,16 @@ app.whenReady().then(async () => {
|
|||||||
return { success: true, monitoring }
|
return { success: true, monitoring }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to init DB:', 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 {
|
} finally {
|
||||||
dbInitInFlight = null
|
dbInitInFlight = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { app, session } from 'electron'
|
import { app, session, shell } from 'electron'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { clearBootstrapCache } from './bootstrap-cache'
|
import { clearBootstrapCache } from './bootstrap-cache'
|
||||||
@@ -62,7 +62,8 @@ export function getCacheSummary(): CacheSummary {
|
|||||||
{
|
{
|
||||||
id: 'knowledge',
|
id: 'knowledge',
|
||||||
label: '本地知识库索引',
|
label: '本地知识库索引',
|
||||||
description: '为问问微信建立的所有账号本地检索索引。清理后需手动重新建立,不影响微信原始数据。',
|
description:
|
||||||
|
'为问问微信建立的所有账号本地检索索引。清理后需手动重新建立,不影响微信原始数据。',
|
||||||
...knowledge
|
...knowledge
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -90,3 +91,13 @@ export async function clearCache(
|
|||||||
}
|
}
|
||||||
return getCacheSummary()
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import type {
|
|||||||
DatabaseKeyValidationCode,
|
DatabaseKeyValidationCode,
|
||||||
DatabaseKeyValidationResult
|
DatabaseKeyValidationResult
|
||||||
} from '../../shared/database-key'
|
} from '../../shared/database-key'
|
||||||
|
import {
|
||||||
|
isWindowsVcRuntimeMissingError,
|
||||||
|
WINDOWS_VC_RUNTIME_ERROR_MESSAGE
|
||||||
|
} from '../../shared/windows-runtime'
|
||||||
import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service'
|
import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service'
|
||||||
import type { ExportImageQuality } from '../../shared/image-quality'
|
import type { ExportImageQuality } from '../../shared/image-quality'
|
||||||
|
|
||||||
@@ -675,11 +679,13 @@ const DATABASE_KEY_ERROR_MESSAGES: Record<DatabaseKeyValidationCode, string> = {
|
|||||||
ACCOUNT_MISMATCH: '密钥与当前账号不匹配',
|
ACCOUNT_MISMATCH: '密钥与当前账号不匹配',
|
||||||
ROOT_UNAVAILABLE: '当前数据库目录不可用',
|
ROOT_UNAVAILABLE: '当前数据库目录不可用',
|
||||||
DATABASE_FILE_MISSING: '数据库文件缺失',
|
DATABASE_FILE_MISSING: '数据库文件缺失',
|
||||||
|
VC_RUNTIME_MISSING: WINDOWS_VC_RUNTIME_ERROR_MESSAGE,
|
||||||
UNKNOWN_VALIDATION_ERROR: '未知验证错误'
|
UNKNOWN_VALIDATION_ERROR: '未知验证错误'
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapConnectionError(detail: string): DatabaseKeyValidationCode {
|
function mapConnectionError(detail: string): DatabaseKeyValidationCode {
|
||||||
const normalized = detail.toLowerCase()
|
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('-1005') || normalized.includes('不匹配')) return 'ACCOUNT_MISMATCH'
|
||||||
if (normalized.includes('session.db') || normalized.includes('数据库文件')) {
|
if (normalized.includes('session.db') || normalized.includes('数据库文件')) {
|
||||||
return 'DATABASE_FILE_MISSING'
|
return 'DATABASE_FILE_MISSING'
|
||||||
|
|||||||
Vendored
+3
-4
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '../shared/report-history'
|
} from '../shared/report-history'
|
||||||
import type {
|
import type {
|
||||||
DatabaseKeyEnvironment,
|
DatabaseKeyEnvironment,
|
||||||
|
DatabaseInitResult,
|
||||||
DatabaseKeyStorageResult,
|
DatabaseKeyStorageResult,
|
||||||
DatabaseKeyValidationResult,
|
DatabaseKeyValidationResult,
|
||||||
AccountDiscoveryResult
|
AccountDiscoveryResult
|
||||||
@@ -141,10 +142,8 @@ declare global {
|
|||||||
onAppUpdateState: (callback: (state: AppUpdateState) => void) => () => void
|
onAppUpdateState: (callback: (state: AppUpdateState) => void) => () => void
|
||||||
getCacheSummary: () => Promise<CacheSummary>
|
getCacheSummary: () => Promise<CacheSummary>
|
||||||
clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all') => Promise<CacheSummary>
|
clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all') => Promise<CacheSummary>
|
||||||
initDb: (
|
openKnowledgeDirectory: () => Promise<{ success: boolean; error?: string }>
|
||||||
key: string,
|
initDb: (key: string, accountRoot: string) => Promise<boolean | DatabaseInitResult>
|
||||||
accountRoot: string
|
|
||||||
) => Promise<boolean | { success: boolean; error?: string; monitoring?: boolean }>
|
|
||||||
discoverAccounts: (inputPath: string) => Promise<AccountDiscoveryResult>
|
discoverAccounts: (inputPath: string) => Promise<AccountDiscoveryResult>
|
||||||
getBootstrapCache: () => Promise<{
|
getBootstrapCache: () => Promise<{
|
||||||
self?: { wxid: string; nickname: string; avatar?: string; accountRoot: string }
|
self?: { wxid: string; nickname: string; avatar?: string; accountRoot: string }
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ const api = {
|
|||||||
getCacheSummary: (): Promise<CacheSummary> => ipcRenderer.invoke('cache:getSummary'),
|
getCacheSummary: (): Promise<CacheSummary> => ipcRenderer.invoke('cache:getSummary'),
|
||||||
clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all'): Promise<CacheSummary> =>
|
clearCache: (scope: 'bootstrap' | 'electron' | 'knowledge' | 'all'): Promise<CacheSummary> =>
|
||||||
ipcRenderer.invoke('cache:clear', scope),
|
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),
|
initDb: (key: string, accountRoot: string) => ipcRenderer.invoke('db:init', key, accountRoot),
|
||||||
discoverAccounts: (inputPath: string): Promise<AccountDiscoveryResult> =>
|
discoverAccounts: (inputPath: string): Promise<AccountDiscoveryResult> =>
|
||||||
ipcRenderer.invoke('accounts:discover', inputPath),
|
ipcRenderer.invoke('accounts:discover', inputPath),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { DatabaseKeyEnvironment, WechatAccountCandidate } from '../../../shared/database-key'
|
import type { DatabaseKeyEnvironment, WechatAccountCandidate } from '../../../shared/database-key'
|
||||||
|
import { WINDOWS_VC_RUNTIME_DOWNLOAD_URL } from '../../../shared/windows-runtime'
|
||||||
|
|
||||||
const GUIDE_URL =
|
const GUIDE_URL =
|
||||||
'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md'
|
'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md'
|
||||||
@@ -461,7 +462,12 @@ export function DatabaseConnectionPage({
|
|||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Windows 已完整支持,不需要关闭 SIP。'
|
<>
|
||||||
|
Windows 需要 Microsoft Visual C++ 2015-2022 x64 运行库。{' '}
|
||||||
|
<a href={WINDOWS_VC_RUNTIME_DOWNLOAD_URL} target="_blank" rel="noreferrer">
|
||||||
|
下载运行库
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{showMacKeyFaq && isMac && (
|
{showMacKeyFaq && isMac && (
|
||||||
@@ -521,6 +527,14 @@ export function DatabaseConnectionPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status && <div className={`database-login-message ${statusKind}`}>{status}</div>}
|
{status && <div className={`database-login-message ${statusKind}`}>{status}</div>}
|
||||||
|
{platform === 'win32' && (
|
||||||
|
<p className="database-login-platform-note">
|
||||||
|
无法加载数据库组件时,请安装 Microsoft Visual C++ 2015-2022 x64 运行库。{' '}
|
||||||
|
<a href={WINDOWS_VC_RUNTIME_DOWNLOAD_URL} target="_blank" rel="noreferrer">
|
||||||
|
下载运行库
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="database-login-primary"
|
className="database-login-primary"
|
||||||
|
|||||||
@@ -1392,6 +1392,13 @@ export function AISearchWorkspace({
|
|||||||
ref={composerRef}
|
ref={composerRef}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
void runAnalysis()
|
||||||
|
}}
|
||||||
placeholder="例如:技术交流群最近讨论了哪些 Windows 性能问题?"
|
placeholder="例如:技术交流群最近讨论了哪些 Windows 性能问题?"
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ function formatBytes(value: number): string {
|
|||||||
return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`
|
return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) => void }): React.ReactElement {
|
export function CacheCleanupPage({
|
||||||
|
onNotice
|
||||||
|
}: {
|
||||||
|
onNotice: (message: string) => void
|
||||||
|
}): React.ReactElement {
|
||||||
const [summary, setSummary] = useState<CacheSummary | null>(null)
|
const [summary, setSummary] = useState<CacheSummary | null>(null)
|
||||||
const [busyScope, setBusyScope] = useState<
|
const [busyScope, setBusyScope] = useState<
|
||||||
'bootstrap' | 'electron' | 'knowledge' | 'all' | 'local' | null
|
'bootstrap' | 'electron' | 'knowledge' | 'knowledge-directory' | 'all' | 'local' | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
const refresh = useCallback(async (): Promise<void> => {
|
const refresh = useCallback(async (): Promise<void> => {
|
||||||
@@ -52,6 +56,19 @@ export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openKnowledge = async (): Promise<void> => {
|
||||||
|
setBusyScope('knowledge-directory')
|
||||||
|
try {
|
||||||
|
const result = await window.api.openKnowledgeDirectory()
|
||||||
|
if (!result.success) throw new Error(result.error || '无法打开知识库文件夹')
|
||||||
|
onNotice('已打开知识库文件夹')
|
||||||
|
} catch (error) {
|
||||||
|
onNotice(error instanceof Error ? error.message : '无法打开知识库文件夹')
|
||||||
|
} finally {
|
||||||
|
setBusyScope(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-page">
|
<div className="settings-page">
|
||||||
<header className="settings-page-header">
|
<header className="settings-page-header">
|
||||||
@@ -88,8 +105,20 @@ export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) =>
|
|||||||
<div>
|
<div>
|
||||||
<h3>{item.label}</h3>
|
<h3>{item.label}</h3>
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
<small>{formatBytes(item.sizeBytes)} · {item.fileCount} 个文件</small>
|
<small>
|
||||||
|
{formatBytes(item.sizeBytes)} · {item.fileCount} 个文件
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="settings-cache-actions">
|
||||||
|
{item.id === 'knowledge' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busyScope !== null}
|
||||||
|
onClick={() => void openKnowledge()}
|
||||||
|
>
|
||||||
|
{busyScope === 'knowledge-directory' ? '打开中...' : '打开文件夹'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={busyScope !== null}
|
disabled={busyScope !== null}
|
||||||
@@ -97,6 +126,7 @@ export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) =>
|
|||||||
>
|
>
|
||||||
{busyScope === item.id ? '清理中...' : '清理'}
|
{busyScope === item.id ? '清理中...' : '清理'}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
<section className="settings-card settings-cache-item">
|
<section className="settings-card settings-cache-item">
|
||||||
@@ -113,7 +143,9 @@ export function CacheCleanupPage({ onNotice }: { onNotice: (message: string) =>
|
|||||||
|
|
||||||
<div className="settings-inline-note">
|
<div className="settings-inline-note">
|
||||||
<strong>说明</strong>
|
<strong>说明</strong>
|
||||||
<span>缓存没有过期时间,只有在这里手动清理,或应用检测到格式需要迁移时才会被替换。</span>
|
<span>
|
||||||
|
缓存没有过期时间,只有在这里手动清理,或应用检测到格式需要迁移时才会被替换。
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.settings-primary-button,
|
.settings-primary-button,
|
||||||
.settings-danger-button,
|
.settings-danger-button,
|
||||||
.settings-cache-item > button,
|
.settings-cache-item > button,
|
||||||
|
.settings-cache-actions button,
|
||||||
.about-links-card button {
|
.about-links-card button {
|
||||||
border: 1px solid var(--wxex-border);
|
border: 1px solid var(--wxex-border);
|
||||||
border-radius: var(--wxex-radius-sm);
|
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-inline-note,
|
||||||
.settings-footnote {
|
.settings-footnote {
|
||||||
color: var(--wxex-text-muted);
|
color: var(--wxex-text-muted);
|
||||||
@@ -820,4 +827,12 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-cache-actions {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,18 @@ export type DatabaseKeyValidationCode =
|
|||||||
| 'ACCOUNT_MISMATCH'
|
| 'ACCOUNT_MISMATCH'
|
||||||
| 'ROOT_UNAVAILABLE'
|
| 'ROOT_UNAVAILABLE'
|
||||||
| 'DATABASE_FILE_MISSING'
|
| 'DATABASE_FILE_MISSING'
|
||||||
|
| 'VC_RUNTIME_MISSING'
|
||||||
| 'UNKNOWN_VALIDATION_ERROR'
|
| 'UNKNOWN_VALIDATION_ERROR'
|
||||||
|
|
||||||
|
export type DatabaseInitCode = DatabaseKeyValidationCode | 'ACCOUNT_SELECTION_REQUIRED'
|
||||||
|
|
||||||
|
export interface DatabaseInitResult {
|
||||||
|
success: boolean
|
||||||
|
code?: DatabaseInitCode
|
||||||
|
error?: string
|
||||||
|
monitoring?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface DatabaseKeyValidationResult {
|
export interface DatabaseKeyValidationResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
code?: DatabaseKeyValidationCode
|
code?: DatabaseKeyValidationCode
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -376,6 +376,56 @@ describe('AISearchWorkspace cache privacy boundary', () => {
|
|||||||
expect(api.runAiSearch).not.toHaveBeenCalled()
|
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(
|
||||||
|
<AISearchWorkspace
|
||||||
|
contacts={[]}
|
||||||
|
selectedContact={null}
|
||||||
|
dbReady
|
||||||
|
aiModelConfig={{
|
||||||
|
configured: true,
|
||||||
|
providerName: 'Local Provider',
|
||||||
|
model: 'model',
|
||||||
|
modelName: 'Model',
|
||||||
|
status: 'connected'
|
||||||
|
}}
|
||||||
|
onSelectContact={vi.fn()}
|
||||||
|
onOpenEvidence={vi.fn()}
|
||||||
|
onOpenAISettings={vi.fn()}
|
||||||
|
onNotice={vi.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
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 () => {
|
it('cancels an active analysis and ignores its late result', async () => {
|
||||||
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
api.getAiSearchProviderStatus.mockResolvedValue({ configured: true, requiresConsent: false })
|
||||||
let resolveSearch: ((value: unknown) => void) | undefined
|
let resolveSearch: ((value: unknown) => void) | undefined
|
||||||
|
|||||||
@@ -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(<CacheCleanupPage onNotice={onNotice} />)
|
||||||
|
|
||||||
|
await userEvent.click(await screen.findByRole('button', { name: '打开文件夹' }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(api.openKnowledgeDirectory).toHaveBeenCalledOnce())
|
||||||
|
expect(api.clearCache).not.toHaveBeenCalled()
|
||||||
|
expect(onNotice).toHaveBeenCalledWith('已打开知识库文件夹')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -180,4 +180,16 @@ describe('DatabaseConnectionPage', () => {
|
|||||||
expect(screen.getByRole('button', { name: '返回上一步' })).toBeEnabled()
|
expect(screen.getByRole('button', { name: '返回上一步' })).toBeEnabled()
|
||||||
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'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ describe('preload IPC contract', () => {
|
|||||||
expect(invoke).toHaveBeenLastCalledWith('knowledge:startIndex')
|
expect(invoke).toHaveBeenLastCalledWith('knowledge:startIndex')
|
||||||
await api.clearCache('knowledge')
|
await api.clearCache('knowledge')
|
||||||
expect(invoke).toHaveBeenLastCalledWith('cache:clear', 'knowledge')
|
expect(invoke).toHaveBeenLastCalledWith('cache:clear', 'knowledge')
|
||||||
|
await api.openKnowledgeDirectory()
|
||||||
|
expect(invoke).toHaveBeenLastCalledWith('cache:openKnowledgeDirectory')
|
||||||
|
|
||||||
await api.getImage('fixture-md5', 'fixture.dat', 'fixture-session', {
|
await api.getImage('fixture-md5', 'fixture.dat', 'fixture-session', {
|
||||||
force: true,
|
force: true,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user