From 769115240e1b257ca12fdf3525067c83b17d8fc7 Mon Sep 17 00:00:00 2001 From: digua Date: Mon, 1 Jun 2026 20:28:37 +0800 Subject: [PATCH] feat(cache): migrate cache routes to shared HTTP package Create CacheServiceAdapter + FetchCacheAdapter service layer and move 7 cache routes (info, clear, data-dir, import-log, save-to-downloads, open-dir, show-in-folder) to @openchatlab/http-routes. Inject platform callbacks (shell.openPath, showItemInFolder) via HttpRouteContext. Frontend calls converge from window.cacheApi to useCacheService(). Electron IPC retains only selectDataDir/setDataDir (native dialog). Co-authored-by: Cursor --- apps/cli/src/http/routes/web/cache.ts | 229 +----------- apps/cli/src/http/routes/web/index.ts | 11 +- apps/desktop/main/internal-api.ts | 11 + apps/desktop/main/ipc/cache.ts | 342 +----------------- apps/desktop/preload/apis/utils.ts | 91 +---- apps/desktop/preload/index.d.ts | 44 +-- packages/http-routes/src/context.ts | 7 + packages/http-routes/src/register.ts | 4 + packages/http-routes/src/routes/web/cache.ts | 226 ++++++++++++ src/components/AIChat/chat/ChatStatusBar.vue | 3 +- .../analysis/SQLLab/ResultTable.vue | 3 +- .../DataStorage/StorageManageSection.vue | 30 +- src/composables/useScreenCapture.ts | 5 +- .../home/components/import/ImportArea.vue | 10 +- src/services/cache/fetch.ts | 38 ++ src/services/cache/service.ts | 6 + src/services/cache/types.ts | 33 ++ src/services/registry.ts | 80 +--- src/utils/conversationExport.ts | 3 +- src/utils/sqlExport.ts | 3 +- 20 files changed, 397 insertions(+), 782 deletions(-) create mode 100644 packages/http-routes/src/routes/web/cache.ts create mode 100644 src/services/cache/fetch.ts create mode 100644 src/services/cache/service.ts create mode 100644 src/services/cache/types.ts diff --git a/apps/cli/src/http/routes/web/cache.ts b/apps/cli/src/http/routes/web/cache.ts index e993f765..ef4903b8 100644 --- a/apps/cli/src/http/routes/web/cache.ts +++ b/apps/cli/src/http/routes/web/cache.ts @@ -1,10 +1,12 @@ -import * as fs from 'fs' -import * as fsp from 'fs/promises' +/** + * CLI-specific shell helpers for cache routes. + * + * Route registration has been migrated to packages/http-routes (shared). + * Only the platform-specific openDirectoryPath / showPathInFolder functions + * remain here, injected into HttpRouteContext by the CLI web-route initializer. + */ import * as path from 'path' import { execFile } from 'child_process' -import type { FastifyInstance } from 'fastify' -import type { PathProvider } from '@openchatlab/core' -import { loadConfig } from '@openchatlab/config' type ExecFileRunner = (file: string, args: string[], callback: (error: Error | null) => void) => unknown @@ -47,220 +49,3 @@ export async function showPathInFolder( await runExecFile('xdg-open', [path.dirname(filePath)], runner) } } - -async function getDirSize(dirPath: string): Promise { - let totalSize = 0 - try { - if (!fs.existsSync(dirPath)) return 0 - const files = await fsp.readdir(dirPath, { withFileTypes: true }) - for (const file of files) { - const filePath = path.join(dirPath, file.name) - if (file.isDirectory()) { - totalSize += await getDirSize(filePath) - } else { - const stat = await fsp.stat(filePath) - totalSize += stat.size - } - } - } catch { - // directory inaccessible - } - return totalSize -} - -async function getFileCount(dirPath: string): Promise { - let count = 0 - try { - if (!fs.existsSync(dirPath)) return 0 - const files = await fsp.readdir(dirPath, { withFileTypes: true }) - for (const file of files) { - const filePath = path.join(dirPath, file.name) - if (file.isDirectory()) { - count += await getFileCount(filePath) - } else { - count++ - } - } - } catch { - // directory inaccessible - } - return count -} - -export function registerCacheRoutes(server: FastifyInstance, pathProvider: PathProvider): void { - const downloadsDir = pathProvider.getDownloadsDir() - - // --- Storage management endpoints --- - - server.get('/_web/cache/info', async () => { - const directories = [ - { - id: 'databases', - name: 'settings.storage.cache.databases.name', - description: 'settings.storage.cache.databases.description', - path: pathProvider.getDatabaseDir(), - icon: 'i-heroicons-circle-stack', - canClear: false, - }, - { - id: 'ai', - name: 'settings.storage.cache.ai.name', - description: 'settings.storage.cache.ai.description', - path: pathProvider.getAiDataDir(), - icon: 'i-heroicons-sparkles', - canClear: false, - }, - { - id: 'cache', - name: 'settings.storage.cache.statsCache.name', - description: 'settings.storage.cache.statsCache.description', - path: pathProvider.getCacheDir(), - icon: 'i-heroicons-bolt', - canClear: true, - }, - { - id: 'logs', - name: 'settings.storage.cache.logs.name', - description: 'settings.storage.cache.logs.description', - path: pathProvider.getLogsDir(), - icon: 'i-heroicons-document-text', - canClear: true, - }, - ] - - const results = await Promise.all( - directories.map(async (dir) => { - const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]) - return { ...dir, size, fileCount, exists: fs.existsSync(dir.path) } - }) - ) - - return { - baseDir: pathProvider.getUserDataDir(), - systemDir: pathProvider.getSystemDir(), - directories: results, - totalSize: results.reduce((sum, d) => sum + d.size, 0), - } - }) - - server.post<{ Body: { cacheId: string } }>('/_web/cache/clear', async (request) => { - const { cacheId } = request.body - const allowedDirs: Record = { - cache: pathProvider.getCacheDir(), - logs: pathProvider.getLogsDir(), - } - const dirPath = allowedDirs[cacheId] - if (!dirPath) return { success: false, error: 'Not allowed to clear this directory' } - - if (!fs.existsSync(dirPath)) return { success: true } - - const files = await fsp.readdir(dirPath) - for (const file of files) { - const filePath = path.join(dirPath, file) - const stat = await fsp.stat(filePath) - if (stat.isDirectory()) { - await fsp.rm(filePath, { recursive: true }) - } else { - await fsp.unlink(filePath) - } - } - return { success: true } - }) - - server.post<{ Body: { cacheId: string } }>('/_web/cache/open-dir', async (request) => { - const { cacheId } = request.body - const dirPaths: Record = { - base: pathProvider.getUserDataDir(), - system: pathProvider.getSystemDir(), - databases: pathProvider.getDatabaseDir(), - cache: pathProvider.getCacheDir(), - ai: pathProvider.getAiDataDir(), - logs: pathProvider.getLogsDir(), - downloads: downloadsDir, - } - const dirPath = dirPaths[cacheId] - if (!dirPath) return { success: false, error: 'Unknown directory' } - - if (!fs.existsSync(dirPath)) { - await fsp.mkdir(dirPath, { recursive: true }) - } - - try { - await openDirectoryPath(dirPath) - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - return { success: true } - }) - - server.get('/_web/cache/data-dir', async () => { - const config = loadConfig() - const isCustom = Boolean(config.data.user_data_dir || process.env.CHATLAB_DATA_DIR) - return { path: pathProvider.getUserDataDir(), isCustom } - }) - - server.get('/_web/cache/latest-import-log', async () => { - const importLogDir = path.join(pathProvider.getLogsDir(), 'import') - if (!fs.existsSync(importLogDir)) { - return { success: false, error: 'Log directory not found' } - } - - const files = await fsp.readdir(importLogDir) - const logFiles = files.filter((f) => f.startsWith('import_') && f.endsWith('.log')) - if (logFiles.length === 0) { - return { success: false, error: 'No import logs found' } - } - - const fileStats = await Promise.all( - logFiles.map(async (f) => { - const filePath = path.join(importLogDir, f) - const stat = await fsp.stat(filePath) - return { name: f, path: filePath, mtime: stat.mtime.getTime() } - }) - ) - fileStats.sort((a, b) => b.mtime - a.mtime) - - return { success: true, path: fileStats[0].path, name: fileStats[0].name } - }) - - // --- Existing endpoints --- - - server.post<{ - Body: { filename: string; dataUrl: string } - }>('/_web/cache/save-to-downloads', async (request) => { - const { filename, dataUrl } = request.body - if (!filename || !dataUrl) { - return { success: false, error: 'filename and dataUrl are required' } - } - - const base64Prefix = dataUrl.indexOf(',') - const base64Data = base64Prefix >= 0 ? dataUrl.slice(base64Prefix + 1) : dataUrl - const buffer = Buffer.from(base64Data, 'base64') - - if (!fs.existsSync(downloadsDir)) { - fs.mkdirSync(downloadsDir, { recursive: true }) - } - - const filePath = path.join(downloadsDir, filename) - fs.writeFileSync(filePath, buffer) - - return { success: true, filePath } - }) - - server.post<{ - Body: { filePath: string } - }>('/_web/cache/show-in-folder', async (request) => { - const { filePath } = request.body - if (!filePath) { - return { success: false, error: 'filePath is required' } - } - - try { - await showPathInFolder(filePath) - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - - return { success: true } - }) -} diff --git a/apps/cli/src/http/routes/web/index.ts b/apps/cli/src/http/routes/web/index.ts index 46c6c752..ef128429 100644 --- a/apps/cli/src/http/routes/web/index.ts +++ b/apps/cli/src/http/routes/web/index.ts @@ -34,8 +34,9 @@ import { createDatabaseManagerAdapter } from '@openchatlab/node-runtime' import { registerSharedRoutes } from '@openchatlab/http-routes' import { MergeSessionCache } from '../../../merger/merge-cache' import { registerImportRoutes } from './import' -import { registerCacheRoutes } from './cache' +import { openDirectoryPath, showPathInFolder } from './cache' import { getVersion } from '../../../version' +import { loadConfig } from '@openchatlab/config' export interface AiContextOptions { aiDataDir: string @@ -102,6 +103,9 @@ export function registerWebRoutes( return { sessionId: result.sessionId } } + const cliConfig = loadConfig() + const isCustom = Boolean(cliConfig.data.user_data_dir || process.env.CHATLAB_DATA_DIR) + registerSharedRoutes( server, { @@ -110,6 +114,10 @@ export function registerWebRoutes( pathProvider: resolvedPathProvider, getVersion, nativeBinding: options?.nativeBinding, + openDirectory: openDirectoryPath, + showInFolder: showPathInFolder, + downloadsDir: resolvedPathProvider.getDownloadsDir(), + isCustomDataDir: isCustom, ...(mergeCache && { mergeSessionCache: mergeCache, streamImport: cliStreamImport, @@ -129,7 +137,6 @@ export function registerWebRoutes( // CLI-specific routes not yet migrated to @openchatlab/http-routes registerImportRoutes(server, dbManager) - registerCacheRoutes(server, resolvedPathProvider) server.get('/_web/system/check-update', async () => { const currentVersion = getVersion() diff --git a/apps/desktop/main/internal-api.ts b/apps/desktop/main/internal-api.ts index bc80749f..f37e6b35 100644 --- a/apps/desktop/main/internal-api.ts +++ b/apps/desktop/main/internal-api.ts @@ -126,6 +126,9 @@ export async function startInternalServer(pathProvider: PathProvider): Promise shell.openPath(dirPath).then(() => {}), + showInFolder: (filePath) => { + shell.showItemInFolder(filePath) + return Promise.resolve() + }, + downloadsDir: getDownloadsDir(), + defaultUserDataDir: getDefaultUserDataDir(), + isCustomDataDir: Boolean(getCustomDataDir()), } newServer = Fastify({ logger: false, bodyLimit: JSON_BODY_LIMIT }) diff --git a/apps/desktop/main/ipc/cache.ts b/apps/desktop/main/ipc/cache.ts index d39fa58e..fd686dc2 100644 --- a/apps/desktop/main/ipc/cache.ts +++ b/apps/desktop/main/ipc/cache.ts @@ -1,157 +1,19 @@ -// electron/main/ipc/cache.ts -import { ipcMain, shell, dialog, app } from 'electron' -import * as fs from 'fs/promises' -import * as fsSync from 'fs' -import * as path from 'path' +/** + * Cache IPC handlers — IPC-only subset + * + * Most cache operations (getInfo, clear, openDir, saveToDownloads, etc.) + * have been migrated to HTTP shared routes (packages/http-routes). + * Only selectDataDir and setDataDir remain on IPC because they require + * native Electron dialogs (showOpenDialog) and app-level config mutations. + */ +import { ipcMain, dialog, app } from 'electron' import type { IpcContext } from './types' -import { - getUserDataDir, - getDefaultUserDataDir, - getSystemDataDir, - getDatabaseDir, - getCacheDir, - getAiDataDir, - getLogsDir, - getDownloadsDir, - ensureDir, - getCustomDataDir, - setCustomDataDir, - ensureAppDirs, -} from '../paths' +import { getUserDataDir, setCustomDataDir, ensureAppDirs } from '../paths' import { isInsideAppInstallDir } from '../utils/pathUtils' -/** - * 递归计算目录大小 - */ -async function getDirSize(dirPath: string): Promise { - let totalSize = 0 - try { - const exists = fsSync.existsSync(dirPath) - if (!exists) return 0 - - const files = await fs.readdir(dirPath, { withFileTypes: true }) - for (const file of files) { - const filePath = path.join(dirPath, file.name) - if (file.isDirectory()) { - totalSize += await getDirSize(filePath) - } else { - const stat = await fs.stat(filePath) - totalSize += stat.size - } - } - } catch (error) { - console.error('[Cache] Error getting dir size:', dirPath, error) - } - return totalSize -} - -/** - * 获取目录中的文件数量 - */ -async function getFileCount(dirPath: string): Promise { - let count = 0 - try { - const exists = fsSync.existsSync(dirPath) - if (!exists) return 0 - - const files = await fs.readdir(dirPath, { withFileTypes: true }) - for (const file of files) { - const filePath = path.join(dirPath, file.name) - if (file.isDirectory()) { - count += await getFileCount(filePath) - } else { - count++ - } - } - } catch (error) { - console.error('[Cache] Error getting file count:', dirPath, error) - } - return count -} - export function registerCacheHandlers(_context: IpcContext): void { - console.log('[IPC] Registering cache handlers...') + console.log('[IPC] Registering cache handlers (IPC-only subset)...') - /** - * 获取所有缓存目录信息 - */ - ipcMain.handle('cache:getInfo', async () => { - const appDataDir = getUserDataDir() - - // 定义缓存目录(应用数据目录下的子目录) - const cacheDirectories = [ - { - id: 'databases', - name: 'settings.storage.cache.databases.name', - description: 'settings.storage.cache.databases.description', - path: getDatabaseDir(), - icon: 'i-heroicons-circle-stack', - canClear: false, // 不允许一键清理,因为是重要数据 - }, - { - id: 'ai', - name: 'settings.storage.cache.ai.name', - description: 'settings.storage.cache.ai.description', - path: getAiDataDir(), - icon: 'i-heroicons-sparkles', - canClear: false, // 不允许一键清理 - }, - { - id: 'cache', - name: 'settings.storage.cache.statsCache.name', - description: 'settings.storage.cache.statsCache.description', - path: getCacheDir(), - icon: 'i-heroicons-bolt', - canClear: true, - }, - // 临时文件已有自动清理机制(应用启动时、合并完成后),无需暴露给用户 - { - id: 'logs', - name: 'settings.storage.cache.logs.name', - description: 'settings.storage.cache.logs.description', - path: getLogsDir(), - icon: 'i-heroicons-document-text', - canClear: true, // 可以清理 - }, - ] - - // 获取每个目录的信息 - const results = await Promise.all( - cacheDirectories.map(async (dir) => { - const size = await getDirSize(dir.path) - const fileCount = await getFileCount(dir.path) - const exists = fsSync.existsSync(dir.path) - - return { - ...dir, - size, - fileCount, - exists, - } - }) - ) - - return { - baseDir: appDataDir, - directories: results, - totalSize: results.reduce((sum, dir) => sum + dir.size, 0), - } - }) - - /** - * 获取当前数据目录 - */ - ipcMain.handle('cache:getDataDir', async () => { - return { - path: getUserDataDir(), - defaultPath: getDefaultUserDataDir(), - isCustom: Boolean(getCustomDataDir()), - } - }) - - /** - * 选择数据目录(仅返回路径,不修改设置) - */ ipcMain.handle('cache:selectDataDir', async () => { try { const result = await dialog.showOpenDialog({ @@ -167,17 +29,13 @@ export function registerCacheHandlers(_context: IpcContext): void { const selectedPath = result.filePaths[0] - // 安全检查:禁止选择应用安装目录(更新时会被清空) try { const exePath = app.getPath('exe') if (isInsideAppInstallDir(selectedPath, exePath)) { - return { - success: false, - error: 'INSTALL_DIR_FORBIDDEN', - } + return { success: false, error: 'INSTALL_DIR_FORBIDDEN' } } } catch { - // 获取 exe 路径失败时跳过此检查 + // exe path unavailable, skip check } return { success: true, path: selectedPath } @@ -187,9 +45,6 @@ export function registerCacheHandlers(_context: IpcContext): void { } }) - /** - * 设置数据目录 - */ ipcMain.handle('cache:setDataDir', async (_, payload: { path?: string | null; migrate?: boolean }) => { const targetPath = typeof payload?.path === 'string' ? payload.path : null const migrate = payload?.migrate !== false @@ -199,7 +54,6 @@ export function registerCacheHandlers(_context: IpcContext): void { return { success: false, error: result.error } } - // 确保新目录结构完整 ensureAppDirs() return { @@ -208,174 +62,4 @@ export function registerCacheHandlers(_context: IpcContext): void { to: result.to, } }) - - /** - * 清理指定缓存目录 - */ - ipcMain.handle('cache:clear', async (_, cacheId: string) => { - const allowedDirs: Record = { - cache: getCacheDir(), - logs: getLogsDir(), - } - - const dirPath = allowedDirs[cacheId] - if (!dirPath) { - return { success: false, error: '不允许清理此目录' } - } - - try { - const exists = fsSync.existsSync(dirPath) - if (!exists) { - return { success: true, message: '目录不存在,无需清理' } - } - - // 删除目录下的所有文件 - const files = await fs.readdir(dirPath) - for (const file of files) { - const filePath = path.join(dirPath, file) - const stat = await fs.stat(filePath) - if (stat.isDirectory()) { - await fs.rm(filePath, { recursive: true }) - } else { - await fs.unlink(filePath) - } - } - - console.log(`[Cache] Cleared directory: ${dirPath}`) - return { success: true } - } catch (error) { - console.error('[Cache] Error clearing cache:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 保存文件到系统下载目录 - * 支持两种 data URL 格式: - * 1. base64: data:image/png;base64,xxx - * 2. URL 编码: data:text/plain;charset=utf-8,xxx - */ - ipcMain.handle('cache:saveToDownloads', async (_, filename: string, dataUrl: string) => { - const downloadsDir = getDownloadsDir() - - try { - // 系统下载目录应该已存在,但以防万一还是确保一下 - ensureDir(downloadsDir) - - let buffer: Buffer - - // 解析 data URL - if (dataUrl.includes(';base64,')) { - // Base64 编码格式(图片等二进制数据) - const base64Data = dataUrl.split(';base64,')[1] - buffer = Buffer.from(base64Data, 'base64') - } else if (dataUrl.includes('charset=utf-8,')) { - // URL 编码格式(文本数据) - const textData = dataUrl.split('charset=utf-8,')[1] - const decodedText = decodeURIComponent(textData) - buffer = Buffer.from(decodedText, 'utf-8') - } else { - // 默认尝试作为 base64 处理 - const base64Data = dataUrl.replace(/^data:[^,]+,/, '') - buffer = Buffer.from(base64Data, 'base64') - } - - // 写入文件 - const filePath = path.join(downloadsDir, filename) - await fs.writeFile(filePath, buffer) - - console.log(`[Cache] Saved file to downloads: ${filePath}`) - return { success: true, filePath } - } catch (error) { - console.error('[Cache] Error saving to downloads:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 在文件管理器中打开缓存目录 - */ - ipcMain.handle('cache:openDir', async (_, cacheId: string) => { - const dirPaths: Record = { - base: getUserDataDir(), - system: getSystemDataDir(), - databases: getDatabaseDir(), - cache: getCacheDir(), - ai: getAiDataDir(), - logs: getLogsDir(), - downloads: getDownloadsDir(), // 系统下载目录 - } - - const dirPath = dirPaths[cacheId] - if (!dirPath) { - return { success: false, error: '未知的目录' } - } - - try { - // 确保目录存在(系统下载目录应该已存在) - if (!fsSync.existsSync(dirPath)) { - await fs.mkdir(dirPath, { recursive: true }) - } - - await shell.openPath(dirPath) - return { success: true } - } catch (error) { - console.error('[Cache] Error opening directory:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 获取最新的导入日志文件路径 - */ - ipcMain.handle('cache:getLatestImportLog', async () => { - const importLogDir = path.join(getLogsDir(), 'import') - - try { - if (!fsSync.existsSync(importLogDir)) { - return { success: false, error: '日志目录不存在' } - } - - const files = await fs.readdir(importLogDir) - const logFiles = files.filter((f) => f.startsWith('import_') && f.endsWith('.log')) - - if (logFiles.length === 0) { - return { success: false, error: '没有找到导入日志' } - } - - // 按修改时间排序,获取最新的 - const fileStats = await Promise.all( - logFiles.map(async (f) => { - const filePath = path.join(importLogDir, f) - const stat = await fs.stat(filePath) - return { name: f, path: filePath, mtime: stat.mtime.getTime() } - }) - ) - - fileStats.sort((a, b) => b.mtime - a.mtime) - const latestLog = fileStats[0] - - return { success: true, path: latestLog.path, name: latestLog.name } - } catch (error) { - console.error('[Cache] Error getting latest import log:', error) - return { success: false, error: String(error) } - } - }) - - /** - * 在文件管理器中显示并高亮文件 - */ - ipcMain.handle('cache:showInFolder', async (_, filePath: string) => { - try { - if (!fsSync.existsSync(filePath)) { - return { success: false, error: '文件不存在' } - } - - shell.showItemInFolder(filePath) - return { success: true } - } catch (error) { - console.error('[Cache] Error showing file in folder:', error) - return { success: false, error: String(error) } - } - }) } diff --git a/apps/desktop/preload/apis/utils.ts b/apps/desktop/preload/apis/utils.ts index 4f548415..b2a53937 100644 --- a/apps/desktop/preload/apis/utils.ts +++ b/apps/desktop/preload/apis/utils.ts @@ -13,31 +13,6 @@ export interface ProxyConfig { url: string // 仅 manual 模式使用 } -// Cache API 类型 -export interface CacheDirectoryInfo { - id: string - name: string - description: string - path: string - icon: string - canClear: boolean - size: number - fileCount: number - exists: boolean -} - -export interface CacheInfo { - baseDir: string - directories: CacheDirectoryInfo[] - totalSize: number -} - -export interface DataDirInfo { - path: string - defaultPath?: string - isCustom: boolean -} - // Session API 类型 export interface SessionStats { sessionCount: number @@ -80,75 +55,25 @@ export const networkApi = { // ==================== Cache API ==================== +/** + * CacheApi — IPC-only subset + * + * Most cache operations (getInfo, clear, openDir, saveToDownloads, etc.) + * have been migrated to HTTP shared routes (FetchCacheAdapter). + * Only selectDataDir and setDataDir remain on IPC because they require + * native Electron dialogs and app restart capabilities. + */ export const cacheApi = { - /** - * 获取所有缓存目录信息 - */ - getInfo: (): Promise => { - return ipcRenderer.invoke('cache:getInfo') - }, - - /** - * 清理指定缓存目录 - */ - clear: (cacheId: string): Promise<{ success: boolean; error?: string; message?: string }> => { - return ipcRenderer.invoke('cache:clear', cacheId) - }, - - /** - * 在文件管理器中打开缓存目录 - */ - openDir: (cacheId: string): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('cache:openDir', cacheId) - }, - - /** - * 保存文件到下载目录 - */ - saveToDownloads: ( - filename: string, - dataUrl: string - ): Promise<{ success: boolean; filePath?: string; error?: string }> => { - return ipcRenderer.invoke('cache:saveToDownloads', filename, dataUrl) - }, - - /** - * 获取最新的导入日志文件路径 - */ - getLatestImportLog: (): Promise<{ success: boolean; path?: string; name?: string; error?: string }> => { - return ipcRenderer.invoke('cache:getLatestImportLog') - }, - - /** - * 获取当前数据目录 - */ - getDataDir: (): Promise => { - return ipcRenderer.invoke('cache:getDataDir') - }, - - /** - * 选择数据目录(只返回路径) - */ selectDataDir: (): Promise<{ success: boolean; path?: string; error?: string }> => { return ipcRenderer.invoke('cache:selectDataDir') }, - /** - * 设置数据目录 - */ setDataDir: ( path: string | null, migrate: boolean = true ): Promise<{ success: boolean; error?: string; from?: string; to?: string }> => { return ipcRenderer.invoke('cache:setDataDir', { path, migrate }) }, - - /** - * 在文件管理器中显示并高亮文件 - */ - showInFolder: (filePath: string): Promise<{ success: boolean; error?: string }> => { - return ipcRenderer.invoke('cache:showInFolder', filePath) - }, } // ==================== Session API ==================== diff --git a/apps/desktop/preload/index.d.ts b/apps/desktop/preload/index.d.ts index 1b3a5d3a..e211e681 100644 --- a/apps/desktop/preload/index.d.ts +++ b/apps/desktop/preload/index.d.ts @@ -409,42 +409,14 @@ interface AgentApi { // Assistant CRUD migrated to HTTP service layer (FetchAssistantAdapter) // Skill CRUD migrated to HTTP service layer (FetchSkillAdapter) -// Cache API 类型 -interface CacheDirectoryInfo { - id: string - name: string - description: string - path: string - icon: string - canClear: boolean - size: number - fileCount: number - exists: boolean -} - -interface CacheInfo { - baseDir: string - directories: CacheDirectoryInfo[] - totalSize: number -} - -interface DataDirInfo { - path: string - defaultPath?: string - isCustom: boolean -} - +/** + * CacheApi — IPC-only subset + * + * Most cache operations have been migrated to HTTP shared routes + * (FetchCacheAdapter). Only selectDataDir and setDataDir remain on IPC + * because they require native Electron dialogs and app restart. + */ interface CacheApi { - getInfo: () => Promise - clear: (cacheId: string) => Promise<{ success: boolean; error?: string; message?: string }> - openDir: (cacheId: string) => Promise<{ success: boolean; error?: string }> - saveToDownloads: ( - filename: string, - dataUrl: string - ) => Promise<{ success: boolean; filePath?: string; error?: string }> - getLatestImportLog: () => Promise<{ success: boolean; path?: string; name?: string; error?: string }> - showInFolder: (filePath: string) => Promise<{ success: boolean; error?: string }> - getDataDir: () => Promise selectDataDir: () => Promise<{ success: boolean; path?: string; error?: string }> setDataDir: ( path: string | null, @@ -689,8 +661,6 @@ export { ToolCatalogEntry, ToolExecuteResult, TokenUsage, - CacheDirectoryInfo, - CacheInfo, FilterMessage, ContextBlock, FilterResult, diff --git a/packages/http-routes/src/context.ts b/packages/http-routes/src/context.ts index 51858a28..14ba0e15 100644 --- a/packages/http-routes/src/context.ts +++ b/packages/http-routes/src/context.ts @@ -48,4 +48,11 @@ export interface HttpRouteContext { llmConfigStore?: LLMConfigStore customProviderStore?: CustomProviderStore customModelStore?: CustomModelStore + + /** Cache/storage — platform-specific (optional) */ + openDirectory?: (dirPath: string) => Promise + showInFolder?: (filePath: string) => Promise + downloadsDir?: string + defaultUserDataDir?: string + isCustomDataDir?: boolean } diff --git a/packages/http-routes/src/register.ts b/packages/http-routes/src/register.ts index 4ac7bd84..c7b57ab4 100644 --- a/packages/http-routes/src/register.ts +++ b/packages/http-routes/src/register.ts @@ -23,6 +23,7 @@ import { registerAiLlmRoutes } from './routes/web/ai-llm' import { registerAiConversationRoutes } from './routes/web/ai-conversations' import { registerAiSummaryRoutes } from './routes/web/ai-summaries' import { registerMergeRoutes } from './routes/web/merge' +import { registerCacheRoutes } from './routes/web/cache' export interface SharedRouteOptions { /** When true, AI routes will throw on missing dependencies instead of silently skipping */ @@ -70,4 +71,7 @@ export function registerSharedRoutes( // Merge routes (graceful skip when mergeSessionCache is absent) registerMergeRoutes(server, ctx) + + // Cache/storage routes + registerCacheRoutes(server, ctx) } diff --git a/packages/http-routes/src/routes/web/cache.ts b/packages/http-routes/src/routes/web/cache.ts new file mode 100644 index 00000000..39c53e77 --- /dev/null +++ b/packages/http-routes/src/routes/web/cache.ts @@ -0,0 +1,226 @@ +import * as fs from 'fs' +import * as fsp from 'fs/promises' +import * as path from 'path' +import type { FastifyInstance } from 'fastify' +import type { HttpRouteContext } from '../../context' + +async function getDirSize(dirPath: string): Promise { + let totalSize = 0 + try { + if (!fs.existsSync(dirPath)) return 0 + const files = await fsp.readdir(dirPath, { withFileTypes: true }) + for (const file of files) { + const filePath = path.join(dirPath, file.name) + if (file.isDirectory()) { + totalSize += await getDirSize(filePath) + } else { + const stat = await fsp.stat(filePath) + totalSize += stat.size + } + } + } catch { + /* directory inaccessible */ + } + return totalSize +} + +async function getFileCount(dirPath: string): Promise { + let count = 0 + try { + if (!fs.existsSync(dirPath)) return 0 + const files = await fsp.readdir(dirPath, { withFileTypes: true }) + for (const file of files) { + const filePath = path.join(dirPath, file.name) + if (file.isDirectory()) { + count += await getFileCount(filePath) + } else { + count++ + } + } + } catch { + /* directory inaccessible */ + } + return count +} + +export function registerCacheRoutes(server: FastifyInstance, ctx: HttpRouteContext): void { + const pp = ctx.pathProvider + const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir() + + server.get('/_web/cache/info', async () => { + const directories = [ + { + id: 'databases', + name: 'settings.storage.cache.databases.name', + description: 'settings.storage.cache.databases.description', + path: pp.getDatabaseDir(), + icon: 'i-heroicons-circle-stack', + canClear: false, + }, + { + id: 'ai', + name: 'settings.storage.cache.ai.name', + description: 'settings.storage.cache.ai.description', + path: pp.getAiDataDir(), + icon: 'i-heroicons-sparkles', + canClear: false, + }, + { + id: 'cache', + name: 'settings.storage.cache.statsCache.name', + description: 'settings.storage.cache.statsCache.description', + path: pp.getCacheDir(), + icon: 'i-heroicons-bolt', + canClear: true, + }, + { + id: 'logs', + name: 'settings.storage.cache.logs.name', + description: 'settings.storage.cache.logs.description', + path: pp.getLogsDir(), + icon: 'i-heroicons-document-text', + canClear: true, + }, + ] + + const results = await Promise.all( + directories.map(async (dir) => { + const [size, fileCount] = await Promise.all([getDirSize(dir.path), getFileCount(dir.path)]) + return { ...dir, size, fileCount, exists: fs.existsSync(dir.path) } + }) + ) + + return { + baseDir: pp.getUserDataDir(), + directories: results, + totalSize: results.reduce((sum, d) => sum + d.size, 0), + } + }) + + server.post<{ Body: { cacheId: string } }>('/_web/cache/clear', async (request) => { + const { cacheId } = request.body + const allowedDirs: Record = { + cache: pp.getCacheDir(), + logs: pp.getLogsDir(), + } + const dirPath = allowedDirs[cacheId] + if (!dirPath) return { success: false, error: 'Not allowed to clear this directory' } + + if (!fs.existsSync(dirPath)) return { success: true } + + const files = await fsp.readdir(dirPath) + for (const file of files) { + const filePath = path.join(dirPath, file) + const stat = await fsp.stat(filePath) + if (stat.isDirectory()) { + await fsp.rm(filePath, { recursive: true }) + } else { + await fsp.unlink(filePath) + } + } + return { success: true } + }) + + server.get('/_web/cache/data-dir', async () => { + return { + path: pp.getUserDataDir(), + defaultPath: ctx.defaultUserDataDir, + isCustom: ctx.isCustomDataDir ?? false, + } + }) + + server.get('/_web/cache/latest-import-log', async () => { + const importLogDir = path.join(pp.getLogsDir(), 'import') + if (!fs.existsSync(importLogDir)) { + return { success: false, error: 'Log directory not found' } + } + + const files = await fsp.readdir(importLogDir) + const logFiles = files.filter((f) => f.startsWith('import_') && f.endsWith('.log')) + if (logFiles.length === 0) { + return { success: false, error: 'No import logs found' } + } + + const fileStats = await Promise.all( + logFiles.map(async (f) => { + const filePath = path.join(importLogDir, f) + const stat = await fsp.stat(filePath) + return { name: f, path: filePath, mtime: stat.mtime.getTime() } + }) + ) + fileStats.sort((a, b) => b.mtime - a.mtime) + + return { success: true, path: fileStats[0].path, name: fileStats[0].name } + }) + + server.post<{ Body: { filename: string; dataUrl: string } }>('/_web/cache/save-to-downloads', async (request) => { + const { filename, dataUrl } = request.body + if (!filename || !dataUrl) { + return { success: false, error: 'filename and dataUrl are required' } + } + + let buffer: Buffer + if (dataUrl.includes(';base64,')) { + const base64Data = dataUrl.split(';base64,')[1] + buffer = Buffer.from(base64Data, 'base64') + } else if (dataUrl.includes('charset=utf-8,')) { + const textData = dataUrl.split('charset=utf-8,')[1] + buffer = Buffer.from(decodeURIComponent(textData), 'utf-8') + } else { + const base64Data = dataUrl.replace(/^data:[^,]+,/, '') + buffer = Buffer.from(base64Data, 'base64') + } + + if (!fs.existsSync(downloadsDir)) { + fs.mkdirSync(downloadsDir, { recursive: true }) + } + + const filePath = path.join(downloadsDir, filename) + fs.writeFileSync(filePath, buffer) + + return { success: true, filePath } + }) + + // Shell operations — require platform-specific callbacks + + server.post<{ Body: { cacheId: string } }>('/_web/cache/open-dir', async (request, reply) => { + if (!ctx.openDirectory) return reply.code(501).send({ success: false, error: 'Not supported' }) + + const { cacheId } = request.body + const dirPaths: Record = { + base: pp.getUserDataDir(), + databases: pp.getDatabaseDir(), + cache: pp.getCacheDir(), + ai: pp.getAiDataDir(), + logs: pp.getLogsDir(), + downloads: downloadsDir, + } + const dirPath = dirPaths[cacheId] + if (!dirPath) return { success: false, error: 'Unknown directory' } + + if (!fs.existsSync(dirPath)) { + await fsp.mkdir(dirPath, { recursive: true }) + } + + try { + await ctx.openDirectory(dirPath) + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + return { success: true } + }) + + server.post<{ Body: { filePath: string } }>('/_web/cache/show-in-folder', async (request, reply) => { + if (!ctx.showInFolder) return reply.code(501).send({ success: false, error: 'Not supported' }) + + const { filePath } = request.body + if (!filePath) return { success: false, error: 'filePath is required' } + + try { + await ctx.showInFolder(filePath) + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + return { success: true } + }) +} diff --git a/src/components/AIChat/chat/ChatStatusBar.vue b/src/components/AIChat/chat/ChatStatusBar.vue index 03f1b3fc..efd61130 100644 --- a/src/components/AIChat/chat/ChatStatusBar.vue +++ b/src/components/AIChat/chat/ChatStatusBar.vue @@ -10,6 +10,7 @@ import { exportConversation, type ExportFormat, type ExportMessage } from '@/uti import type { AgentRuntimeStatus } from '@electron/shared/types' import { useAIService } from '@/services' import { getSupportedThinkingLevels, type ThinkingLevel } from '@openchatlab/core' +import { useCacheService } from '@/services/cache/service' const { t } = useI18n() const toast = useToast() @@ -170,7 +171,7 @@ async function handleExportConversation() { { label: t('common.openFolder'), onClick: () => { - window.cacheApi.showInFolder(exportedFilePath) + useCacheService().showInFolder(exportedFilePath) }, }, ], diff --git a/src/components/analysis/SQLLab/ResultTable.vue b/src/components/analysis/SQLLab/ResultTable.vue index f3364ba1..4cca75df 100644 --- a/src/components/analysis/SQLLab/ResultTable.vue +++ b/src/components/analysis/SQLLab/ResultTable.vue @@ -11,6 +11,7 @@ import { usePromptStore } from '@/stores/prompt' import { useLayoutStore } from '@/stores/layout' import { exportSQLResult, type SQLExportFormat } from '@/utils/sqlExport' import { useLLMService } from '@/services' +import { useCacheService } from '@/services/cache/service' const { t, locale } = useI18n() const toast = useToast() @@ -246,7 +247,7 @@ async function exportResult() { { label: t('common.openFolder'), onClick: () => { - window.cacheApi.showInFolder(result.filePath!) + useCacheService().showInFolder(result.filePath!) }, }, ], diff --git a/src/components/common/Settings/DataStorage/StorageManageSection.vue b/src/components/common/Settings/DataStorage/StorageManageSection.vue index eb56ec9f..9257a0c7 100644 --- a/src/components/common/Settings/DataStorage/StorageManageSection.vue +++ b/src/components/common/Settings/DataStorage/StorageManageSection.vue @@ -7,29 +7,11 @@ import { ref, onMounted, computed } from 'vue' import { useI18n } from 'vue-i18n' import { usePlatformService } from '@/services' import { IS_ELECTRON } from '@/utils/platform' +import { useCacheService } from '@/services/cache/service' +import type { CacheInfo } from '@/services/cache/types' const { t } = useI18n() -// 缓存目录信息类型 -interface CacheDirectoryInfo { - id: string - name: string - description: string - path: string - icon: string - canClear: boolean - size: number - fileCount: number - exists: boolean -} - -interface CacheInfo { - baseDir: string - systemDir?: string - directories: CacheDirectoryInfo[] - totalSize: number -} - // 状态 const cacheInfo = ref(null) const isLoading = ref(false) @@ -67,7 +49,7 @@ const totalSizeFormatted = computed(() => { async function loadCacheInfo() { isLoading.value = true try { - cacheInfo.value = await window.cacheApi.getInfo() + cacheInfo.value = await useCacheService().getInfo() } catch (error) { console.error('获取缓存信息失败:', error) } finally { @@ -82,7 +64,7 @@ const canMigrateToDefault = computed(() => { async function loadDataDir() { try { - const info = await window.cacheApi.getDataDir() + const info = await useCacheService().getDataDir() dataDir.value = info.path defaultDataDir.value = info.defaultPath || '' isCustomDataDir.value = info.isCustom @@ -95,7 +77,7 @@ async function loadDataDir() { async function clearCache(cacheId: string) { clearingId.value = cacheId try { - const result = await window.cacheApi.clear(cacheId) + const result = await useCacheService().clear(cacheId) if (result.success) { // 刷新缓存信息 await loadCacheInfo() @@ -112,7 +94,7 @@ async function clearCache(cacheId: string) { // 打开目录 async function openDirectory(cacheId: string) { try { - await window.cacheApi.openDir(cacheId) + await useCacheService().openDir(cacheId) } catch (error) { console.error('打开目录失败:', error) } diff --git a/src/composables/useScreenCapture.ts b/src/composables/useScreenCapture.ts index b19cff8e..15f7f73c 100644 --- a/src/composables/useScreenCapture.ts +++ b/src/composables/useScreenCapture.ts @@ -7,6 +7,7 @@ import { captureAsImageData } from '@/utils/snapCapture' import { useToast } from '@/composables/useToast' import { useLayoutStore } from '@/stores/layout' import { usePlatformService } from '@/services' +import { useCacheService } from '@/services/cache/service' /** 默认移动端最大宽度 */ const DEFAULT_MOBILE_MAX_WIDTH = 525 @@ -51,7 +52,7 @@ export function useScreenCapture() { const filename = `chatlab-screenshot-${timestamp}.png` try { - const result = await window.cacheApi.saveToDownloads(filename, imageData) + const result = await useCacheService().saveToDownloads(filename, imageData) if (result.success) { toast.add({ title: '截图已保存', @@ -61,7 +62,7 @@ export function useScreenCapture() { { label: '打开目录', onClick: () => { - window.cacheApi.openDir('downloads') + useCacheService().openDir('downloads') }, }, ], diff --git a/src/pages/home/components/import/ImportArea.vue b/src/pages/home/components/import/ImportArea.vue index 5f30ff57..33d54941 100644 --- a/src/pages/home/components/import/ImportArea.vue +++ b/src/pages/home/components/import/ImportArea.vue @@ -10,6 +10,7 @@ import { useI18n } from 'vue-i18n' import { useSessionStore, type BatchFileInfo, type MergeFileInfo } from '@/stores/session' import { useDataService, useImportService, useSessionIndexService, usePlatformService } from '@/services' import { IS_ELECTRON } from '@/utils/platform' +import { useCacheService } from '@/services/cache/service' import { getSessionGapThreshold } from '@/composables/useUiConfig' const { t } = useI18n() @@ -108,7 +109,7 @@ async function navigateToSession(sessionId: string) { // 检查是否有导入日志 - Electron only async function checkImportLog() { if (!IS_ELECTRON) return - const result = await window.cacheApi.getLatestImportLog() + const result = await useCacheService().getLatestImportLog() hasImportLog.value = result.success && !!result.path } @@ -540,12 +541,11 @@ async function handleGoToSession(sessionId: string) { // 打开最新的导入日志文件 async function openLatestImportLog() { - const result = await window.cacheApi.getLatestImportLog() + const result = await useCacheService().getLatestImportLog() if (result.success && result.path) { - await window.cacheApi.showInFolder(result.path) + await useCacheService().showInFolder(result.path) } else { - // 没有日志文件时,打开日志目录 - await window.cacheApi.openDir('logs') + await useCacheService().openDir('logs') } } diff --git a/src/services/cache/fetch.ts b/src/services/cache/fetch.ts new file mode 100644 index 00000000..02006b13 --- /dev/null +++ b/src/services/cache/fetch.ts @@ -0,0 +1,38 @@ +import type { CacheServiceAdapter, CacheInfo, DataDirInfo } from './types' +import { get, post } from '../utils/http' + +export class FetchCacheAdapter implements CacheServiceAdapter { + async getInfo(): Promise { + return get('/cache/info') + } + + async clear(cacheId: string): Promise<{ success: boolean; error?: string; message?: string }> { + return post<{ success: boolean; error?: string; message?: string }>('/cache/clear', { cacheId }) + } + + async getDataDir(): Promise { + return get('/cache/data-dir') + } + + async getLatestImportLog(): Promise<{ success: boolean; path?: string; name?: string; error?: string }> { + return get<{ success: boolean; path?: string; name?: string; error?: string }>('/cache/latest-import-log') + } + + async saveToDownloads( + filename: string, + dataUrl: string + ): Promise<{ success: boolean; filePath?: string; error?: string }> { + return post<{ success: boolean; filePath?: string; error?: string }>('/cache/save-to-downloads', { + filename, + dataUrl, + }) + } + + async openDir(cacheId: string): Promise<{ success: boolean; error?: string }> { + return post<{ success: boolean; error?: string }>('/cache/open-dir', { cacheId }) + } + + async showInFolder(filePath: string): Promise<{ success: boolean; error?: string }> { + return post<{ success: boolean; error?: string }>('/cache/show-in-folder', { filePath }) + } +} diff --git a/src/services/cache/service.ts b/src/services/cache/service.ts new file mode 100644 index 00000000..05448e42 --- /dev/null +++ b/src/services/cache/service.ts @@ -0,0 +1,6 @@ +import { getRegisteredAdapter } from '../registry' +import type { CacheServiceAdapter } from './types' + +export function useCacheService(): CacheServiceAdapter { + return getRegisteredAdapter('cache') +} diff --git a/src/services/cache/types.ts b/src/services/cache/types.ts new file mode 100644 index 00000000..992154eb --- /dev/null +++ b/src/services/cache/types.ts @@ -0,0 +1,33 @@ +export interface CacheDirectoryInfo { + id: string + name: string + description: string + path: string + icon: string + canClear: boolean + size: number + fileCount: number + exists: boolean +} + +export interface CacheInfo { + baseDir: string + directories: CacheDirectoryInfo[] + totalSize: number +} + +export interface DataDirInfo { + path: string + defaultPath?: string + isCustom: boolean +} + +export interface CacheServiceAdapter { + getInfo(): Promise + clear(cacheId: string): Promise<{ success: boolean; error?: string; message?: string }> + getDataDir(): Promise + getLatestImportLog(): Promise<{ success: boolean; path?: string; name?: string; error?: string }> + saveToDownloads(filename: string, dataUrl: string): Promise<{ success: boolean; filePath?: string; error?: string }> + openDir(cacheId: string): Promise<{ success: boolean; error?: string }> + showInFolder(filePath: string): Promise<{ success: boolean; error?: string }> +} diff --git a/src/services/registry.ts b/src/services/registry.ts index aa594642..71d512ff 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -95,6 +95,9 @@ async function initElectronAdapters(): Promise { const { FetchSkillAdapter } = await import('./skill/fetch') registerAdapter('skill-crud', new FetchSkillAdapter()) + const { FetchCacheAdapter } = await import('./cache/fetch') + registerAdapter('cache', new FetchCacheAdapter()) + installMergeShims('electron') } @@ -129,6 +132,9 @@ async function initWebServeAdapters(): Promise { const { FetchSkillAdapter } = await import('./skill/fetch') registerAdapter('skill-crud', new FetchSkillAdapter()) + const { FetchCacheAdapter } = await import('./cache/fetch') + registerAdapter('cache', new FetchCacheAdapter()) + await installAiApiShims() } @@ -310,80 +316,6 @@ async function installAiApiShims(): Promise { ;(window as any).agentApi = agentApiImpl installMergeShims('web-serve') - ;(window as any).cacheApi = { - saveToDownloads: async (filename: string, dataUrl: string) => { - try { - const resp = await fetchWithAuth('/_web/cache/save-to-downloads', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename, dataUrl }), - }) - return await resp.json() - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - }, - showInFolder: async (filePath: string) => { - try { - const resp = await fetchWithAuth('/_web/cache/show-in-folder', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filePath }), - }) - return await resp.json() - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - }, - getInfo: async () => { - try { - const resp = await fetchWithAuth('/_web/cache/info') - return await resp.json() - } catch (error) { - return { baseDir: '', directories: [], totalSize: 0 } - } - }, - clear: async (cacheId: string) => { - try { - const resp = await fetchWithAuth('/_web/cache/clear', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cacheId }), - }) - return await resp.json() - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - }, - openDir: async (cacheId: string) => { - try { - const resp = await fetchWithAuth('/_web/cache/open-dir', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cacheId }), - }) - return await resp.json() - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - }, - getLatestImportLog: async () => { - try { - const resp = await fetchWithAuth('/_web/cache/latest-import-log') - return await resp.json() - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } - } - }, - getDataDir: async () => { - try { - const resp = await fetchWithAuth('/_web/cache/data-dir') - return await resp.json() - } catch (error) { - return { path: '', isCustom: false } - } - }, - } } /** diff --git a/src/utils/conversationExport.ts b/src/utils/conversationExport.ts index 59c84830..dbce5df1 100644 --- a/src/utils/conversationExport.ts +++ b/src/utils/conversationExport.ts @@ -121,7 +121,8 @@ export async function exportConversation( // 转换为 data URL 并保存 const dataUrl = `data:text/plain;charset=utf-8,${encodeURIComponent(content)}` - const result = await window.cacheApi.saveToDownloads(filename, dataUrl) + const { useCacheService } = await import('@/services/cache/service') + const result = await useCacheService().saveToDownloads(filename, dataUrl) return result } diff --git a/src/utils/sqlExport.ts b/src/utils/sqlExport.ts index 48a4c726..4e11a4bd 100644 --- a/src/utils/sqlExport.ts +++ b/src/utils/sqlExport.ts @@ -59,7 +59,8 @@ export async function exportSQLResult( // 转换为 data URL 并保存 const dataUrl = `data:${mimeType};charset=utf-8,${encodeURIComponent(content)}` - const result = await window.cacheApi.saveToDownloads(filename, dataUrl) + const { useCacheService } = await import('@/services/cache/service') + const result = await useCacheService().saveToDownloads(filename, dataUrl) return result }