mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
fcca22a8dc
commit
769115240e
20 files changed
+397
-782
No files matched your search
@@ -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<number> {
|
||||
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<number> {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 }
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user