mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(cli): implement storage management for CLI Web
CLI Web's Settings > Storage Management was completely non-functional (stub cacheApi returning empty data). This adds real HTTP endpoints and wires the frontend to use them. Backend (cache.ts): - GET /_web/cache/info — scan databases/ai/cache/logs directory sizes - POST /_web/cache/clear — clear cache or logs directory (allowlist) - POST /_web/cache/open-dir — open directory in system file manager - GET /_web/cache/data-dir — return current data directory path - GET /_web/cache/latest-import-log — find latest import log file Frontend: - Replace cacheApi stubs with real HTTP calls in registry.ts - Hide data directory migration UI in CLI Web (IS_ELECTRON guard) - Add "Open Root Dir" button for ~/.chatlab/ in header area Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
9e88dcbd09
commit
8819dff62c
8 files changed
+362
-108
No files matched your search
@@ -1,8 +1,191 @@
|
||||
import * as fs from 'fs'
|
||||
import * as fsp from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import { loadConfig } from '@openchatlab/config'
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
const { exec } = await import('child_process')
|
||||
const platform = process.platform
|
||||
if (platform === 'darwin') {
|
||||
exec(`open "${dirPath}"`)
|
||||
} else if (platform === 'win32') {
|
||||
exec(`explorer "${dirPath}"`)
|
||||
} else {
|
||||
exec(`xdg-open "${dirPath}"`)
|
||||
}
|
||||
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 ---
|
||||
|
||||
export function registerCacheRoutes(server: FastifyInstance, downloadsDir: string): void {
|
||||
server.post<{
|
||||
Body: { filename: string; dataUrl: string }
|
||||
}>('/_web/cache/save-to-downloads', async (request) => {
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
* import – File / directory / incremental import + demo
|
||||
* merge – Merge parse / conflicts / execute
|
||||
* export – Markdown export
|
||||
* cache – Save to downloads + show in folder
|
||||
* cache – Storage management + save to downloads + show in folder
|
||||
*/
|
||||
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import { createDatabaseManagerAdapter } from '@openchatlab/node-runtime'
|
||||
import { MergeSessionCache } from '../../../merger/merge-cache'
|
||||
@@ -37,7 +38,7 @@ import { registerCacheRoutes } from './cache'
|
||||
export function registerWebRoutes(
|
||||
server: FastifyInstance,
|
||||
dbManager: DatabaseManager,
|
||||
options?: { pathProvider?: import('@openchatlab/core').PathProvider; nativeBinding?: string }
|
||||
options?: { pathProvider?: PathProvider; nativeBinding?: string }
|
||||
): void {
|
||||
const adapter = createDatabaseManagerAdapter(dbManager)
|
||||
|
||||
@@ -46,7 +47,18 @@ export function registerWebRoutes(
|
||||
: null
|
||||
mergeCache?.cleanupOrphans()
|
||||
|
||||
const downloadsDir = options?.pathProvider?.getDownloadsDir() || path.join(os.homedir(), 'Downloads')
|
||||
const fallbackPathProvider: PathProvider = {
|
||||
getSystemDir: () => path.join(os.homedir(), '.chatlab'),
|
||||
getUserDataDir: () => path.join(os.homedir(), '.chatlab', 'data'),
|
||||
getDatabaseDir: () => path.join(os.homedir(), '.chatlab', 'data', 'databases'),
|
||||
getAiDataDir: () => path.join(os.homedir(), '.chatlab', 'ai'),
|
||||
getSettingsDir: () => path.join(os.homedir(), '.chatlab', 'settings'),
|
||||
getCacheDir: () => path.join(os.homedir(), '.chatlab', 'cache'),
|
||||
getTempDir: () => path.join(os.homedir(), '.chatlab', 'temp'),
|
||||
getLogsDir: () => path.join(os.homedir(), '.chatlab', 'logs'),
|
||||
getDownloadsDir: () => path.join(os.homedir(), 'Downloads'),
|
||||
}
|
||||
const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider
|
||||
|
||||
registerSessionRoutes(server, adapter)
|
||||
registerMemberRoutes(server, adapter)
|
||||
@@ -59,5 +71,5 @@ export function registerWebRoutes(
|
||||
registerMergeRoutes(server, dbManager, mergeCache)
|
||||
}
|
||||
registerExportRoutes(server, adapter)
|
||||
registerCacheRoutes(server, downloadsDir)
|
||||
registerCacheRoutes(server, resolvedPathProvider)
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePlatformService } from '@/services'
|
||||
import { IS_ELECTRON } from '@/utils/platform'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -24,6 +25,7 @@ interface CacheDirectoryInfo {
|
||||
|
||||
interface CacheInfo {
|
||||
baseDir: string
|
||||
systemDir?: string
|
||||
directories: CacheDirectoryInfo[]
|
||||
totalSize: number
|
||||
}
|
||||
@@ -182,10 +184,9 @@ async function relaunchApp() {
|
||||
await usePlatformService().relaunch()
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCacheInfo()
|
||||
loadDataDir()
|
||||
if (IS_ELECTRON) loadDataDir()
|
||||
})
|
||||
|
||||
// 暴露刷新方法
|
||||
@@ -206,6 +207,15 @@ defineExpose({
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('settings.storage.description') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
v-if="cacheInfo?.systemDir"
|
||||
icon="i-heroicons-folder-open"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="openDirectory('system')"
|
||||
>
|
||||
{{ t('settings.storage.openRootDir') }}
|
||||
</UButton>
|
||||
<!-- 总大小 -->
|
||||
<div class="rounded-lg bg-gray-100 px-3 py-1.5 dark:bg-gray-800">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.storage.totalUsage') }}</span>
|
||||
@@ -295,116 +305,118 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据目录设置 -->
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.title') }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.description') }}
|
||||
</p>
|
||||
<!-- 数据目录设置(仅 Electron 支持切换) -->
|
||||
<template v-if="IS_ELECTRON">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.title') }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<UButton icon="i-heroicons-folder-open" variant="ghost" size="xs" @click="openBaseDir">
|
||||
{{ t('settings.storage.dataLocation.open') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<UButton icon="i-heroicons-folder-open" variant="ghost" size="xs" @click="openBaseDir">
|
||||
{{ t('settings.storage.dataLocation.open') }}
|
||||
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<UInput v-model="dataDir" readonly size="sm" class="min-w-0 flex-1" />
|
||||
<UButton
|
||||
size="sm"
|
||||
variant="soft"
|
||||
:loading="isUpdatingDataDir"
|
||||
:disabled="isUpdatingDataDir"
|
||||
@click="selectDataDir"
|
||||
>
|
||||
{{ t('settings.storage.dataLocation.choose') }}
|
||||
</UButton>
|
||||
<UButton v-if="isCustomDataDir" size="sm" variant="ghost" :disabled="isUpdatingDataDir" @click="resetDataDir">
|
||||
{{ t('settings.storage.dataLocation.reset') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.storage.dataLocation.restartTip') }}
|
||||
</p>
|
||||
<p v-if="dataDirError" class="mt-1 text-xs text-red-500">
|
||||
{{ dataDirError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<UInput v-model="dataDir" readonly size="sm" class="min-w-0 flex-1" />
|
||||
<UButton
|
||||
size="sm"
|
||||
variant="soft"
|
||||
:loading="isUpdatingDataDir"
|
||||
:disabled="isUpdatingDataDir"
|
||||
@click="selectDataDir"
|
||||
>
|
||||
{{ t('settings.storage.dataLocation.choose') }}
|
||||
</UButton>
|
||||
<UButton v-if="isCustomDataDir" size="sm" variant="ghost" :disabled="isUpdatingDataDir" @click="resetDataDir">
|
||||
{{ t('settings.storage.dataLocation.reset') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.storage.dataLocation.restartTip') }}
|
||||
</p>
|
||||
<p v-if="dataDirError" class="mt-1 text-xs text-red-500">
|
||||
{{ dataDirError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 切换数据目录确认弹窗 -->
|
||||
<UModal v-model:open="showConfirmModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
<!-- 切换数据目录确认弹窗 -->
|
||||
<UModal v-model:open="showConfirmModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.confirmTitle') }}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.confirmTitle') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>{{ t('settings.storage.dataLocation.confirmMessage') }}</p>
|
||||
<div class="rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.newPath') }}
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-sm text-gray-900 dark:text-white">
|
||||
{{ pendingNewDir || t('settings.storage.dataLocation.defaultPath') }}
|
||||
</p>
|
||||
<div class="space-y-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>{{ t('settings.storage.dataLocation.confirmMessage') }}</p>
|
||||
<div class="rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.newPath') }}
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-sm text-gray-900 dark:text-white">
|
||||
{{ pendingNewDir || t('settings.storage.dataLocation.defaultPath') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800/50 dark:bg-amber-900/20"
|
||||
>
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
||||
{{ t('settings.storage.dataLocation.confirmWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800/50 dark:bg-amber-900/20"
|
||||
>
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
||||
{{ t('settings.storage.dataLocation.confirmWarning') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex justify-end gap-2">
|
||||
<UButton variant="ghost" @click="cancelDataDirChange">
|
||||
{{ t('settings.storage.dataLocation.cancel') }}
|
||||
</UButton>
|
||||
<UButton color="primary" @click="confirmDataDirChange">
|
||||
{{ t('settings.storage.dataLocation.confirm') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<div class="mt-5 flex justify-end gap-2">
|
||||
<UButton variant="ghost" @click="cancelDataDirChange">
|
||||
{{ t('settings.storage.dataLocation.cancel') }}
|
||||
</UButton>
|
||||
<UButton color="primary" @click="confirmDataDirChange">
|
||||
{{ t('settings.storage.dataLocation.confirm') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- 迁移成功后强制重启弹窗 -->
|
||||
<UModal v-model:open="showRelaunchModal" :dismissible="false" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<UIcon name="i-heroicons-check-circle" class="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<!-- 迁移成功后强制重启弹窗 -->
|
||||
<UModal v-model:open="showRelaunchModal" :dismissible="false" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<UIcon name="i-heroicons-check-circle" class="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.migrationSuccessTitle') }}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.storage.dataLocation.migrationSuccessTitle') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.migrationSuccessMessage') }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.storage.dataLocation.migrationSuccessMessage') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<UButton color="primary" @click="relaunchApp">
|
||||
{{ t('settings.storage.dataLocation.relaunchNow') }}
|
||||
</UButton>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<UButton color="primary" @click="relaunchApp">
|
||||
{{ t('settings.storage.dataLocation.relaunchNow') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -222,6 +222,7 @@
|
||||
"migrationSuccessMessage": "The app needs to restart for changes to take effect.",
|
||||
"relaunchNow": "Restart Now"
|
||||
},
|
||||
"openRootDir": "Open Root",
|
||||
"totalUsage": "Total: ",
|
||||
"loading": "Loading...",
|
||||
"files": "files",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
"migrationSuccessMessage": "変更を反映するにはアプリの再起動が必要です。",
|
||||
"relaunchNow": "今すぐ再起動"
|
||||
},
|
||||
"openRootDir": "ルートを開く",
|
||||
"totalUsage": "合計使用量:",
|
||||
"loading": "読み込み中...",
|
||||
"files": "ファイル",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
"migrationSuccessMessage": "应用需要重启以使更改生效。",
|
||||
"relaunchNow": "立即重启"
|
||||
},
|
||||
"openRootDir": "打开根目录",
|
||||
"totalUsage": "总占用:",
|
||||
"loading": "加载中...",
|
||||
"files": "文件",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
"migrationSuccessMessage": "應用程式需要重新啟動以使變更生效。",
|
||||
"relaunchNow": "立即重新啟動"
|
||||
},
|
||||
"openRootDir": "開啟根目錄",
|
||||
"totalUsage": "總佔用:",
|
||||
"loading": "載入中...",
|
||||
"files": "檔案",
|
||||
|
||||
@@ -433,11 +433,54 @@ async function installAiApiShims(): Promise<void> {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
},
|
||||
getInfo: () => Promise.resolve({ success: true, caches: [] }),
|
||||
clear: () => Promise.resolve({ success: true }),
|
||||
openDir: () => Promise.resolve({ success: true }),
|
||||
getLatestImportLog: () => Promise.resolve({ success: true }),
|
||||
getDataDir: () => Promise.resolve({ success: true, path: '' }),
|
||||
getInfo: async () => {
|
||||
try {
|
||||
const resp = await fetch('/_web/cache/info')
|
||||
return await resp.json()
|
||||
} catch (error) {
|
||||
return { baseDir: '', directories: [], totalSize: 0 }
|
||||
}
|
||||
},
|
||||
clear: async (cacheId: string) => {
|
||||
try {
|
||||
const resp = await fetch('/_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 fetch('/_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 fetch('/_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 fetch('/_web/cache/data-dir')
|
||||
return await resp.json()
|
||||
} catch (error) {
|
||||
return { path: '', isCustom: false }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user