mirror of
https://github.com/adnaan-worker/adnify.git
synced 2026-09-01 15:12:10 +08:00
feat: update stroge and tool's ui
This commit is contained in:
+14
-11
@@ -33,9 +33,12 @@ import {
|
||||
export interface IPCContext {
|
||||
getMainWindow: () => BrowserWindow | null
|
||||
createWindow: () => BrowserWindow
|
||||
mainStore: Store
|
||||
/** 根据 key 路由到正确的 store */
|
||||
resolveStore: (key: string) => Store
|
||||
credentialsStore: Store
|
||||
preferencesStore: Store
|
||||
workspaceMetaStore: Store
|
||||
bootstrapStore: Store
|
||||
setMainStore: (store: Store) => void
|
||||
// 窗口-工作区管理(用于单项目单窗口模式)
|
||||
findWindowByWorkspace?: (roots: string[]) => BrowserWindow | null
|
||||
setWindowWorkspace?: (windowId: number, roots: string[]) => void
|
||||
@@ -46,13 +49,13 @@ export interface IPCContext {
|
||||
* 注册所有安全的 IPC handlers
|
||||
*/
|
||||
export function registerAllHandlers(context: IPCContext) {
|
||||
const { getMainWindow, createWindow, mainStore, bootstrapStore, setMainStore } = context
|
||||
const { getMainWindow, createWindow, resolveStore, preferencesStore, workspaceMetaStore, bootstrapStore } = context
|
||||
|
||||
// 窗口控制
|
||||
registerWindowHandlers(createWindow)
|
||||
|
||||
// 文件操作(安全版)
|
||||
registerSecureFileHandlers(getMainWindow, mainStore, (event) => {
|
||||
registerSecureFileHandlers(getMainWindow, workspaceMetaStore, (event) => {
|
||||
// 优先使用请求来源窗口的工作区(支持多窗口隔离)
|
||||
if (event && context.getWindowWorkspace) {
|
||||
const windowId = event.sender.id
|
||||
@@ -62,14 +65,14 @@ export function registerAllHandlers(context: IPCContext) {
|
||||
}
|
||||
}
|
||||
// 回退到全局存储
|
||||
return mainStore.get('lastWorkspaceSession') as { roots: string[] } | null
|
||||
return workspaceMetaStore.get('lastWorkspaceSession') as { roots: string[] } | null
|
||||
}, {
|
||||
findWindowByWorkspace: context.findWindowByWorkspace,
|
||||
setWindowWorkspace: context.setWindowWorkspace,
|
||||
})
|
||||
|
||||
// 设置(传入安全模块引用)
|
||||
registerSettingsHandlers(mainStore, bootstrapStore, setMainStore, {
|
||||
// 设置(传入 resolveStore 和各 store 引用)
|
||||
registerSettingsHandlers(resolveStore, preferencesStore, bootstrapStore, {
|
||||
securityManager,
|
||||
updateWhitelist,
|
||||
getWhitelist
|
||||
@@ -86,7 +89,7 @@ export function registerAllHandlers(context: IPCContext) {
|
||||
}
|
||||
}
|
||||
// 回退到全局存储
|
||||
return mainStore.get('lastWorkspaceSession') as { roots: string[] } | null
|
||||
return workspaceMetaStore.get('lastWorkspaceSession') as { roots: string[] } | null
|
||||
}, context.getWindowWorkspace)
|
||||
|
||||
// 搜索
|
||||
@@ -95,11 +98,11 @@ export function registerAllHandlers(context: IPCContext) {
|
||||
// LLM
|
||||
registerLLMHandlers(getMainWindow)
|
||||
|
||||
// 索引 - 传入 mainStore 以读取保存的 embedding 配置
|
||||
registerIndexingHandlers(getMainWindow, mainStore)
|
||||
// 索引 - 传入 workspaceMetaStore 以读取保存的 embedding 配置
|
||||
registerIndexingHandlers(getMainWindow, workspaceMetaStore)
|
||||
|
||||
// LSP 语言服务
|
||||
registerLspHandlers(mainStore)
|
||||
registerLspHandlers(preferencesStore)
|
||||
|
||||
// HTTP 请求(用于 web_search / read_url)
|
||||
registerHttpHandlers()
|
||||
|
||||
@@ -8,21 +8,21 @@ import { getIndexService, initIndexServiceWithConfig, EmbeddingConfig, IndexMode
|
||||
import { ok, failFromError, Result } from '@shared/types/result'
|
||||
import Store from 'electron-store'
|
||||
|
||||
let _mainStore: Store | null = null
|
||||
let _configStore: Store | null = null
|
||||
|
||||
function getSavedConfig(): Partial<IndexConfig> | undefined {
|
||||
if (!_mainStore) return undefined
|
||||
return _mainStore.get('indexConfig') as Partial<IndexConfig> | undefined
|
||||
if (!_configStore) return undefined
|
||||
return _configStore.get('indexConfig') as Partial<IndexConfig> | undefined
|
||||
}
|
||||
|
||||
function saveConfig(updates: Partial<IndexConfig>): void {
|
||||
if (!_mainStore) return
|
||||
if (!_configStore) return
|
||||
const current = getSavedConfig() || {}
|
||||
_mainStore.set('indexConfig', { ...current, ...updates })
|
||||
_configStore.set('indexConfig', { ...current, ...updates })
|
||||
}
|
||||
|
||||
export function registerIndexingHandlers(getMainWindow: () => BrowserWindow | null, mainStore?: Store) {
|
||||
_mainStore = mainStore || null
|
||||
export function registerIndexingHandlers(getMainWindow: () => BrowserWindow | null, configStore?: Store) {
|
||||
_configStore = configStore || null
|
||||
|
||||
// 初始化
|
||||
ipcMain.handle('index:initialize', async (_, workspacePath: string): Promise<Result<void>> => {
|
||||
|
||||
+11
-11
@@ -7,8 +7,8 @@ import { toAppError } from '@shared/utils/errorHandler'
|
||||
import { ipcMain } from 'electron'
|
||||
import { lspManager, LanguageId } from '../lspManager'
|
||||
import { EXTENSION_TO_LANGUAGE } from '@shared/languages'
|
||||
import {
|
||||
getLspServerStatus,
|
||||
import {
|
||||
getLspServerStatus,
|
||||
installServer,
|
||||
installBasicServers,
|
||||
getLspBinDir,
|
||||
@@ -45,11 +45,11 @@ async function getServerForUri(uri: string, workspacePath: string): Promise<stri
|
||||
return lspManager.ensureServerForFile(filePath, languageId, workspacePath)
|
||||
}
|
||||
|
||||
// mainStore 引用,用于保存 LSP 配置
|
||||
let _mainStore: any = null
|
||||
// preferencesStore 引用,用于保存 LSP 配置
|
||||
let _preferencesStore: any = null
|
||||
|
||||
export function registerLspHandlers(mainStore?: any): void {
|
||||
_mainStore = mainStore
|
||||
export function registerLspHandlers(preferencesStore?: any): void {
|
||||
_preferencesStore = preferencesStore
|
||||
|
||||
// 启动服务器
|
||||
ipcMain.handle('lsp:start', async (_, workspacePath: string) => {
|
||||
@@ -329,7 +329,7 @@ export function registerLspHandlers(mainStore?: any): void {
|
||||
// 先获取 call hierarchy item
|
||||
const items = await lspManager.prepareCallHierarchy(serverName, params.uri, params.line, params.character)
|
||||
if (!items || items.length === 0) return []
|
||||
|
||||
|
||||
// 获取 incoming calls
|
||||
return await lspManager.getIncomingCalls(serverName, items[0])
|
||||
} catch {
|
||||
@@ -345,7 +345,7 @@ export function registerLspHandlers(mainStore?: any): void {
|
||||
// 先获取 call hierarchy item
|
||||
const items = await lspManager.prepareCallHierarchy(serverName, params.uri, params.line, params.character)
|
||||
if (!items || items.length === 0) return []
|
||||
|
||||
|
||||
// 获取 outgoing calls
|
||||
return await lspManager.getOutgoingCalls(serverName, items[0])
|
||||
} catch {
|
||||
@@ -411,11 +411,11 @@ export function registerLspHandlers(mainStore?: any): void {
|
||||
ipcMain.handle('lsp:setCustomBinDir', (_, customPath: string | null) => {
|
||||
setCustomLspBinDir(customPath)
|
||||
// 保存到配置文件
|
||||
if (_mainStore) {
|
||||
if (_preferencesStore) {
|
||||
if (customPath) {
|
||||
_mainStore.set('lspSettings.customBinDir', customPath)
|
||||
_preferencesStore.set('lspSettings.customBinDir', customPath)
|
||||
} else {
|
||||
_mainStore.delete('lspSettings.customBinDir')
|
||||
_preferencesStore.delete('lspSettings.customBinDir')
|
||||
}
|
||||
}
|
||||
return { success: true }
|
||||
|
||||
+13
-21
@@ -20,9 +20,9 @@ interface SecurityModuleRef {
|
||||
let securityRef: SecurityModuleRef | null = null
|
||||
|
||||
export function registerSettingsHandlers(
|
||||
mainStore: Store,
|
||||
resolveStore: (key: string) => Store,
|
||||
preferencesStore: Store,
|
||||
_bootstrapStore: Store,
|
||||
setMainStore: (store: Store) => void,
|
||||
securityModule?: SecurityModuleRef
|
||||
) {
|
||||
// 保存安全模块引用
|
||||
@@ -31,18 +31,19 @@ export function registerSettingsHandlers(
|
||||
}
|
||||
|
||||
// 获取设置
|
||||
ipcMain.handle('settings:get', (_, key: string) => mainStore.get(key))
|
||||
ipcMain.handle('settings:get', (_, key: string) => resolveStore(key).get(key))
|
||||
|
||||
// 设置值(自动清理无效字段)
|
||||
ipcMain.handle('settings:set', (_event, key: string, value: unknown) => {
|
||||
const store = resolveStore(key)
|
||||
// 清理配置值,移除不存在的字段
|
||||
const cleanedValue = cleanConfigValue(key, value)
|
||||
|
||||
// electron-store 不允许设置 undefined,需要使用 delete
|
||||
if (cleanedValue === undefined) {
|
||||
mainStore.delete(key as any)
|
||||
store.delete(key as any)
|
||||
} else {
|
||||
mainStore.set(key, cleanedValue)
|
||||
store.set(key, cleanedValue)
|
||||
}
|
||||
|
||||
// 广播给所有窗口
|
||||
@@ -87,14 +88,14 @@ export function registerSettingsHandlers(
|
||||
securityRef.updateWhitelist(defaultShellCommands, defaultGitCommands)
|
||||
}
|
||||
|
||||
// 保存到配置
|
||||
const currentSecuritySettings = mainStore.get('securitySettings', {}) as any
|
||||
// 保存到配置(安全设置在 preferencesStore 中)
|
||||
const currentSecuritySettings = preferencesStore.get('securitySettings', {}) as any
|
||||
const newSecuritySettings = {
|
||||
...currentSecuritySettings,
|
||||
allowedShellCommands: defaultShellCommands,
|
||||
allowedGitSubcommands: defaultGitCommands
|
||||
}
|
||||
mainStore.set('securitySettings', newSecuritySettings)
|
||||
preferencesStore.set('securitySettings', newSecuritySettings)
|
||||
|
||||
return { shell: defaultShellCommands, git: defaultGitCommands }
|
||||
})
|
||||
@@ -104,22 +105,13 @@ export function registerSettingsHandlers(
|
||||
return getUserConfigDir()
|
||||
})
|
||||
|
||||
// 设置配置路径
|
||||
// 设置配置路径(不再支持迁移整个 store,只设置路径)
|
||||
ipcMain.handle('settings:setConfigPath', async (_, newPath: string) => {
|
||||
try {
|
||||
if (!fs.existsSync(newPath)) {
|
||||
fs.mkdirSync(newPath, { recursive: true })
|
||||
}
|
||||
|
||||
// 保存新路径
|
||||
setUserConfigDir(newPath)
|
||||
|
||||
// 迁移当前配置到新位置
|
||||
const currentData = mainStore.store
|
||||
const newStore = new Store({ name: 'config', cwd: newPath })
|
||||
newStore.store = currentData
|
||||
setMainStore(newStore)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.ipc.error('[Settings] Failed to set config path:', err)
|
||||
@@ -127,9 +119,9 @@ export function registerSettingsHandlers(
|
||||
}
|
||||
})
|
||||
|
||||
// 恢复工作区 (Legacy fallback, secureFile.ts has a better one)
|
||||
// 恢复工作区 (Legacy fallback)
|
||||
ipcMain.handle('workspace:restore:legacy', () => {
|
||||
return mainStore.get('lastWorkspacePath')
|
||||
return resolveStore('lastWorkspacePath').get('lastWorkspacePath')
|
||||
})
|
||||
|
||||
// 获取用户数据路径
|
||||
@@ -142,7 +134,7 @@ export function registerSettingsHandlers(
|
||||
try {
|
||||
const path = require('path')
|
||||
const logPath = path.join(getUserConfigDir(), 'logs', 'main.log')
|
||||
|
||||
|
||||
if (fs.existsSync(logPath)) {
|
||||
const content = fs.readFileSync(logPath, 'utf-8')
|
||||
// 返回最后 10000 行或 1MB 的内容
|
||||
|
||||
+53
-16
@@ -33,19 +33,54 @@ const WINDOW_CONFIG = {
|
||||
// Store(延迟初始化)
|
||||
// ==========================================
|
||||
let bootstrapStore: Store<Record<string, unknown>>
|
||||
let mainStore: Store<Record<string, unknown>>
|
||||
let credentialsStore: Store<Record<string, unknown>>
|
||||
let preferencesStore: Store<Record<string, unknown>>
|
||||
let workspaceMetaStore: Store<Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* 辅助函数:根据 key 路由到正确的 store
|
||||
*
|
||||
* 路由规则:
|
||||
* - credentials.* / providerConfigs → credentialsStore
|
||||
* - workspace.* / lastWorkspacePath / recentWorkspaces / embeddingConfig / indexOptions → workspaceMetaStore
|
||||
* - 其余 → preferencesStore
|
||||
*/
|
||||
function resolveStore(key: string): Store<Record<string, unknown>> {
|
||||
if (key.startsWith('credentials.') || key === 'providerConfigs') return credentialsStore
|
||||
if (
|
||||
key.startsWith('workspace.') ||
|
||||
key === 'lastWorkspacePath' ||
|
||||
key === 'lastWorkspaceSession' ||
|
||||
key === 'recentWorkspaces' ||
|
||||
key === 'embeddingConfig' ||
|
||||
key === 'indexOptions'
|
||||
) return workspaceMetaStore
|
||||
return preferencesStore
|
||||
}
|
||||
|
||||
async function initStores() {
|
||||
const fs = await import('fs')
|
||||
const { default: Store } = await import('electron-store')
|
||||
|
||||
|
||||
bootstrapStore = new Store({ name: 'bootstrap' })
|
||||
const customConfigPath = bootstrapStore.get('customConfigPath') as string | undefined
|
||||
const storeOptions: { name: string; cwd?: string } = { name: 'config' }
|
||||
if (customConfigPath && fs.existsSync(customConfigPath)) {
|
||||
storeOptions.cwd = customConfigPath
|
||||
const baseCwd = (customConfigPath && fs.existsSync(customConfigPath)) ? customConfigPath : undefined
|
||||
|
||||
const mkOpts = (name: string) => baseCwd ? { name, cwd: baseCwd } : { name }
|
||||
|
||||
credentialsStore = new Store(mkOpts('credentials'))
|
||||
preferencesStore = new Store(mkOpts('preferences'))
|
||||
workspaceMetaStore = new Store(mkOpts('workspace-meta'))
|
||||
|
||||
// 迁移旧 config.json(如果存在)
|
||||
try {
|
||||
const { migrateLegacyConfig } = await import('./services/configMigration')
|
||||
const { getUserConfigDir } = await import('./services/configPath')
|
||||
const configDir = baseCwd || getUserConfigDir()
|
||||
migrateLegacyConfig(configDir, credentialsStore, preferencesStore, workspaceMetaStore)
|
||||
} catch (err) {
|
||||
logger.system.error('[Main] Config migration error:', err)
|
||||
}
|
||||
mainStore = new Store(storeOptions)
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -255,20 +290,20 @@ async function initializeModules(firstWin: BrowserWindow) {
|
||||
securityManager = security.securityManager
|
||||
|
||||
// 从配置加载自定义 LSP 安装路径
|
||||
const customLspPath = mainStore.get('lspSettings.customBinDir') as string | undefined
|
||||
const customLspPath = preferencesStore.get('lspSettings.customBinDir') as string | undefined
|
||||
if (customLspPath) {
|
||||
lspInstaller.setCustomLspBinDir(customLspPath)
|
||||
}
|
||||
|
||||
// 注册窗口控制
|
||||
windowIpc.registerWindowHandlers(createWindow)
|
||||
|
||||
|
||||
// 注册更新服务
|
||||
updaterIpc.registerUpdaterHandlers()
|
||||
updaterService.updateService.initialize(firstWin)
|
||||
|
||||
// 配置安全模块
|
||||
const securityConfig = mainStore.get('securitySettings', {
|
||||
const securityConfig = preferencesStore.get('securitySettings', {
|
||||
enablePermissionConfirm: true,
|
||||
enableAuditLog: true,
|
||||
strictWorkspaceMode: true,
|
||||
@@ -286,9 +321,11 @@ async function initializeModules(firstWin: BrowserWindow) {
|
||||
ipc.registerAllHandlers({
|
||||
getMainWindow,
|
||||
createWindow,
|
||||
mainStore,
|
||||
resolveStore,
|
||||
credentialsStore,
|
||||
preferencesStore,
|
||||
workspaceMetaStore,
|
||||
bootstrapStore,
|
||||
setMainStore: (store: Store<Record<string, unknown>>) => { mainStore = store },
|
||||
findWindowByWorkspace,
|
||||
setWindowWorkspace: (id: number, roots: string[]) => windowWorkspaces.set(id, roots),
|
||||
getWindowWorkspace: (id: number) => windowWorkspaces.get(id) || null,
|
||||
@@ -325,12 +362,12 @@ async function initializeModules(firstWin: BrowserWindow) {
|
||||
// 捕获未处理的异常(包括原生模块异常)
|
||||
process.on('uncaughtException', (error: Error) => {
|
||||
logger.system.error('[Main] Uncaught Exception:', error)
|
||||
|
||||
|
||||
// 如果是 node-pty 相关的错误,提供更友好的提示
|
||||
if (error.message?.includes('Napi::Error') || error.message?.includes('node-pty')) {
|
||||
logger.system.error('[Main] node-pty native module error detected. Please run: npm run rebuild')
|
||||
}
|
||||
|
||||
|
||||
// 不退出应用,让用户继续使用其他功能
|
||||
// 只在开发模式下显示错误
|
||||
if (!app.isPackaged) {
|
||||
@@ -341,7 +378,7 @@ process.on('uncaughtException', (error: Error) => {
|
||||
// 捕获未处理的 Promise 拒绝
|
||||
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||
logger.system.error('[Main] Unhandled Rejection:', reason)
|
||||
|
||||
|
||||
if (!app.isPackaged) {
|
||||
console.error('Unhandled rejection at:', promise, 'reason:', reason)
|
||||
}
|
||||
@@ -356,10 +393,10 @@ app.whenReady().then(async () => {
|
||||
await initStores()
|
||||
|
||||
// 2. 检查是否启用文件日志
|
||||
const appSettings = mainStore.get('app-settings') as any
|
||||
const appSettings = preferencesStore.get('app-settings') as any
|
||||
const enableFileLogging = appSettings?.enableFileLogging ?? false
|
||||
logger.system.info('[Main] File logging setting loaded:', { enableFileLogging, type: typeof enableFileLogging })
|
||||
|
||||
|
||||
if (enableFileLogging) {
|
||||
const { getUserConfigDir } = await import('./services/configPath')
|
||||
const logPath = path.join(getUserConfigDir(), 'logs', 'main.log')
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 旧 config.json → 新三文件结构迁移
|
||||
*
|
||||
* 将单一 config.json 拆分为:
|
||||
* - credentials.json — API keys、provider 凭证
|
||||
* - preferences.json — 用户偏好、agent/editor/security 配置
|
||||
* - workspace-meta.json — 工作区历史、embedding 配置
|
||||
*
|
||||
* 迁移完成后删除旧 config.json
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { logger } from '@shared/utils/Logger'
|
||||
import type Store from 'electron-store'
|
||||
|
||||
// 凭证字段(provider 级别)
|
||||
const CREDENTIAL_FIELDS = ['apiKey', 'baseUrl', 'timeout', 'headers'] as const
|
||||
|
||||
// workspace-meta 顶层 key
|
||||
const WORKSPACE_META_KEYS = [
|
||||
'lastWorkspacePath',
|
||||
'lastWorkspaceSession',
|
||||
'recentWorkspaces',
|
||||
'embeddingConfig',
|
||||
'indexOptions',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 检测并执行迁移
|
||||
*
|
||||
* @returns true 如果执行了迁移
|
||||
*/
|
||||
export function migrateLegacyConfig(
|
||||
configDir: string,
|
||||
credentialsStore: Store,
|
||||
preferencesStore: Store,
|
||||
workspaceMetaStore: Store,
|
||||
): boolean {
|
||||
const legacyPath = path.join(configDir, 'config.json')
|
||||
|
||||
if (!fs.existsSync(legacyPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果目标文件已存在,说明已迁移,跳过
|
||||
const credentialsPath = path.join(configDir, 'credentials.json')
|
||||
if (fs.existsSync(credentialsPath)) {
|
||||
// 已迁移但旧文件未删除,直接删
|
||||
try { fs.unlinkSync(legacyPath) } catch { /* ignore */ }
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(legacyPath, 'utf-8')
|
||||
const legacy = JSON.parse(raw) as Record<string, unknown>
|
||||
|
||||
// ---- 提取凭证 ----
|
||||
const credentialsData: Record<string, unknown> = {}
|
||||
const appSettings = (legacy['app-settings'] || {}) as Record<string, unknown>
|
||||
const providerConfigs = (appSettings.providerConfigs || {}) as Record<string, Record<string, unknown>>
|
||||
|
||||
// 从 providerConfigs 提取凭证(apiKey, baseUrl 等)
|
||||
const cleanedProviders: Record<string, Record<string, unknown>> = {}
|
||||
const preferencesProviders: Record<string, Record<string, unknown>> = {}
|
||||
|
||||
for (const [id, config] of Object.entries(providerConfigs)) {
|
||||
const cred: Record<string, unknown> = {}
|
||||
const pref: Record<string, unknown> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if ((CREDENTIAL_FIELDS as readonly string[]).includes(key)) {
|
||||
cred[key] = value
|
||||
} else {
|
||||
pref[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(cred).length > 0) cleanedProviders[id] = cred
|
||||
if (Object.keys(pref).length > 0) preferencesProviders[id] = pref
|
||||
}
|
||||
|
||||
credentialsData.providerConfigs = cleanedProviders
|
||||
|
||||
// ---- 提取偏好 ----
|
||||
const preferencesData: Record<string, unknown> = {}
|
||||
|
||||
// 从 app-settings 提取(排除 providerConfigs)
|
||||
const { providerConfigs: _, ...appSettingsRest } = appSettings
|
||||
preferencesData['app-settings'] = {
|
||||
...appSettingsRest,
|
||||
// 保留非凭证的 provider 配置(customModels, protocol, displayName 等)
|
||||
providerConfigs: preferencesProviders,
|
||||
}
|
||||
|
||||
// 顶层偏好 key
|
||||
if (legacy.themeId !== undefined) preferencesData.themeId = legacy.themeId
|
||||
if (legacy.currentTheme !== undefined) preferencesData.currentTheme = legacy.currentTheme
|
||||
if (legacy.customThemes !== undefined) preferencesData.customThemes = legacy.customThemes
|
||||
// editorConfig 和 securitySettings 可能直接在顶层
|
||||
if (legacy.editorConfig !== undefined) preferencesData.editorConfig = legacy.editorConfig
|
||||
if (legacy.securitySettings !== undefined) preferencesData.securitySettings = legacy.securitySettings
|
||||
// LSP
|
||||
if (legacy.lspSettings !== undefined) preferencesData.lspSettings = legacy.lspSettings
|
||||
|
||||
// ---- 提取工作区元数据 ----
|
||||
const workspaceData: Record<string, unknown> = {}
|
||||
for (const key of WORKSPACE_META_KEYS) {
|
||||
if (legacy[key] !== undefined) {
|
||||
workspaceData[key] = legacy[key]
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 写入新 store ----
|
||||
credentialsStore.store = credentialsData
|
||||
preferencesStore.store = preferencesData
|
||||
workspaceMetaStore.store = workspaceData
|
||||
|
||||
// ---- 删除旧文件 ----
|
||||
fs.unlinkSync(legacyPath)
|
||||
|
||||
logger.system.info('[ConfigMigration] Successfully migrated config.json to split stores')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.system.error('[ConfigMigration] Migration failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* MCP OAuth 认证存储
|
||||
* 持久化存储 OAuth tokens 和客户端信息
|
||||
*
|
||||
* 改进:
|
||||
* - 使用 fs.promises 异步 IO,不阻塞主进程
|
||||
* - 原子写入(写临时文件 + rename),防止写入中断导致数据损坏
|
||||
* - 读写锁防止并发 read-modify-write 竞态
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
@@ -30,6 +35,31 @@ export interface McpAuthEntry {
|
||||
serverUrl?: string
|
||||
}
|
||||
|
||||
// ============ 原子写入 + 读写锁 ============
|
||||
|
||||
/**
|
||||
* 原子写入:先写临时文件再 rename,防止写入中途断电/崩溃导致数据损坏
|
||||
*/
|
||||
async function atomicWrite(filepath: string, data: string): Promise<void> {
|
||||
const tmpPath = `${filepath}.${process.pid}.tmp`
|
||||
await fs.promises.writeFile(tmpPath, data, { mode: 0o600 })
|
||||
await fs.promises.rename(tmpPath, filepath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的 Promise 链锁,保证串行执行
|
||||
*/
|
||||
let writeLock: Promise<void> = Promise.resolve()
|
||||
|
||||
function withLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const prev = writeLock
|
||||
let resolve: () => void
|
||||
writeLock = new Promise(r => { resolve = r! })
|
||||
return prev.then(fn).finally(() => resolve())
|
||||
}
|
||||
|
||||
// ============ McpAuthStore ============
|
||||
|
||||
export namespace McpAuthStore {
|
||||
const getFilePath = () => path.join(app.getPath('userData'), 'mcp-auth.json')
|
||||
|
||||
@@ -49,8 +79,12 @@ export namespace McpAuthStore {
|
||||
export async function all(): Promise<Record<string, McpAuthEntry>> {
|
||||
try {
|
||||
const filepath = getFilePath()
|
||||
if (!fs.existsSync(filepath)) return {}
|
||||
const content = fs.readFileSync(filepath, 'utf-8')
|
||||
try {
|
||||
await fs.promises.access(filepath, fs.constants.F_OK)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
const content = await fs.promises.readFile(filepath, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
} catch {
|
||||
return {}
|
||||
@@ -58,28 +92,32 @@ export namespace McpAuthStore {
|
||||
}
|
||||
|
||||
export async function set(mcpName: string, entry: McpAuthEntry, serverUrl?: string): Promise<void> {
|
||||
try {
|
||||
const filepath = getFilePath()
|
||||
const data = await all()
|
||||
if (serverUrl) {
|
||||
entry.serverUrl = serverUrl
|
||||
await withLock(async () => {
|
||||
try {
|
||||
const filepath = getFilePath()
|
||||
const data = await all()
|
||||
if (serverUrl) {
|
||||
entry.serverUrl = serverUrl
|
||||
}
|
||||
data[mcpName] = entry
|
||||
await atomicWrite(filepath, JSON.stringify(data, null, 2))
|
||||
} catch (err) {
|
||||
logger.mcp?.error('[McpAuthStore] Failed to save:', err)
|
||||
}
|
||||
data[mcpName] = entry
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { mode: 0o600 })
|
||||
} catch (err) {
|
||||
logger.mcp?.error('[McpAuthStore] Failed to save:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove(mcpName: string): Promise<void> {
|
||||
try {
|
||||
const filepath = getFilePath()
|
||||
const data = await all()
|
||||
delete data[mcpName]
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { mode: 0o600 })
|
||||
} catch (err) {
|
||||
logger.mcp?.error('[McpAuthStore] Failed to remove:', err)
|
||||
}
|
||||
await withLock(async () => {
|
||||
try {
|
||||
const filepath = getFilePath()
|
||||
const data = await all()
|
||||
delete data[mcpName]
|
||||
await atomicWrite(filepath, JSON.stringify(data, null, 2))
|
||||
} catch (err) {
|
||||
logger.mcp?.error('[McpAuthStore] Failed to remove:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateTokens(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Agent 数据持久化存储
|
||||
*
|
||||
* 使用 adnifyDir 服务将数据存储到 .adnify/sessions.json
|
||||
* 通过 setSessionsPartialDirty 实现延迟批量写入
|
||||
* 使用 adnifyDir 服务将数据存储到 .adnify/sessions/ 目录
|
||||
* 每个线程对应一个独立 JSON 文件,通过 dirty flag 延迟批量写入
|
||||
*/
|
||||
|
||||
import { logger } from '@utils/Logger'
|
||||
@@ -11,31 +11,29 @@ import { adnifyDir } from '@services/adnifyDirService'
|
||||
|
||||
/**
|
||||
* 自定义 Zustand Storage
|
||||
* 通过 adnifyDir 服务存储到 .adnify/sessions.json
|
||||
* 使用 dirty flag 机制,由 adnifyDir 统一调度刷盘
|
||||
* 通过 adnifyDir 服务存储到 .adnify/sessions/ 目录
|
||||
*
|
||||
* getItem: 从 _meta.json + 各线程文件组装完整的 store 数据
|
||||
* setItem: 拆分到线程级文件,只标记变化的线程为 dirty
|
||||
* removeItem: 清除所有 session 数据
|
||||
*/
|
||||
export const agentStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
const sessions = await adnifyDir.getSessions()
|
||||
if (sessions[name]) {
|
||||
return JSON.stringify(sessions[name])
|
||||
}
|
||||
return null
|
||||
getItem: async (_name: string): Promise<string | null> => {
|
||||
const data = await adnifyDir.getFullSessionData()
|
||||
if (!data) return null
|
||||
return JSON.stringify(data)
|
||||
},
|
||||
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
// 使用 dirty flag 机制,延迟写入
|
||||
adnifyDir.setSessionsPartialDirty(name, parsed)
|
||||
adnifyDir.setFullSessionDataDirty(name, parsed)
|
||||
} catch (error) {
|
||||
logger.agent.error('[AgentStorage] Failed to parse:', error)
|
||||
}
|
||||
},
|
||||
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
const sessions = await adnifyDir.getSessions()
|
||||
delete sessions[name]
|
||||
await adnifyDir.saveSessions(sessions)
|
||||
removeItem: async (_name: string): Promise<void> => {
|
||||
await adnifyDir.clearAllSessions()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,58 +95,111 @@ const ToolCallCard = memo(function ToolCallCard({
|
||||
}
|
||||
}, [isRunning, isStreaming])
|
||||
|
||||
// 获取简短描述
|
||||
const description = useMemo(() => {
|
||||
// 获取动态状态文本 (替换原有的重复文件名逻辑)
|
||||
const statusText = useMemo(() => {
|
||||
const name = toolCall.name
|
||||
// 终端命令
|
||||
const status = toolCall.status
|
||||
const isRunning = status === 'running' || status === 'pending' || isStreaming
|
||||
const isSuccess = status === 'success'
|
||||
const isError = status === 'error'
|
||||
|
||||
const formatPath = (p: string | unknown) => p ? getFileName(p as string) : ''
|
||||
|
||||
// 终端
|
||||
if (name === 'run_command') {
|
||||
return args.command as string
|
||||
const cmd = args.command as string
|
||||
if (!cmd) return isRunning ? 'Preparing cmd...' : ''
|
||||
if (isRunning) return `Executing ${cmd}`
|
||||
if (isSuccess) return `Executed ${cmd}`
|
||||
if (isError) return `Command failed: ${cmd}`
|
||||
return cmd
|
||||
}
|
||||
// 文件路径类工具
|
||||
if ([
|
||||
'read_file', 'write_file', 'create_file', 'edit_file',
|
||||
'create_file_or_folder', 'delete_file_or_folder',
|
||||
'replace_file_content', 'get_lint_errors',
|
||||
'find_references', 'go_to_definition', 'get_hover_info', 'get_document_symbols'
|
||||
].includes(name)) {
|
||||
const path = args.path as string
|
||||
return path ? getFileName(path) : ''
|
||||
}
|
||||
// 多文件读取
|
||||
|
||||
// 读取多文件
|
||||
if (name === 'read_multiple_files') {
|
||||
const paths = args.paths as string[]
|
||||
return paths?.length ? `${paths.length} files` : ''
|
||||
const count = (args.paths as string[])?.length || 0
|
||||
if (isRunning) return `Reading ${count} files...`
|
||||
if (isSuccess) return `Read ${count} files`
|
||||
if (isError) return `Failed to read files`
|
||||
return `Reading files`
|
||||
}
|
||||
// 目录类
|
||||
if (name === 'list_directory' || name === 'get_dir_tree') {
|
||||
const path = args.path as string
|
||||
return path ? getFileName(path) || '.' : '.'
|
||||
|
||||
// 读取单文件或目录
|
||||
if (['read_file', 'list_directory', 'get_dir_tree'].includes(name)) {
|
||||
const target = formatPath(args.path)
|
||||
if (!target) return isRunning ? 'Reading...' : ''
|
||||
if (isRunning) return `Reading ${target}...`
|
||||
if (isSuccess) return `Read ${target}`
|
||||
if (isError) return `Failed to read ${target}`
|
||||
return `Reading ${target}`
|
||||
}
|
||||
// 搜索类
|
||||
if (name === 'search_files') {
|
||||
const pattern = (args.pattern || args.query) as string
|
||||
return pattern ? `"${pattern}"` : ''
|
||||
|
||||
// 写入/创建
|
||||
if (['write_file', 'create_file', 'create_file_or_folder'].includes(name)) {
|
||||
const target = formatPath(args.path)
|
||||
if (!target) return isRunning ? 'Creating...' : ''
|
||||
if (isRunning) return `Creating ${target}...`
|
||||
if (isSuccess) return `Created ${target}`
|
||||
if (isError) return `Failed to create ${target}`
|
||||
return `Creating ${target}`
|
||||
}
|
||||
if (name === 'codebase_search' || name === 'web_search' || name === 'uiux_search') {
|
||||
const query = args.query as string
|
||||
return query ? `"${query}"` : ''
|
||||
|
||||
// 编辑
|
||||
if (['edit_file', 'replace_file_content'].includes(name)) {
|
||||
const target = formatPath(args.path)
|
||||
if (!target) return isRunning ? 'Editing...' : ''
|
||||
if (isRunning) return `Editing ${target}...`
|
||||
if (isSuccess) return `Updated ${target}`
|
||||
if (isError) return `Failed to edit ${target}`
|
||||
return `Editing ${target}`
|
||||
}
|
||||
|
||||
// 删除
|
||||
if (name === 'delete_file_or_folder') {
|
||||
const target = formatPath(args.path)
|
||||
if (!target) return isRunning ? 'Deleting...' : ''
|
||||
if (isRunning) return `Deleting ${target}...`
|
||||
if (isSuccess) return `Deleted ${target}`
|
||||
if (isError) return `Failed to delete ${target}`
|
||||
return `Deleting ${target}`
|
||||
}
|
||||
|
||||
// 搜索
|
||||
if (['search_files', 'codebase_search', 'web_search', 'uiux_search'].includes(name)) {
|
||||
const query = (args.pattern || args.query) as string
|
||||
const qStr = query ? `"${query}"` : ''
|
||||
if (!qStr) return isRunning ? 'Searching...' : ''
|
||||
if (isRunning) return `Searching ${qStr}...`
|
||||
if (isSuccess) return `Searched ${qStr}`
|
||||
if (isError) return `Search failed`
|
||||
return `Searching ${qStr}`
|
||||
}
|
||||
|
||||
// URL
|
||||
if (name === 'read_url') {
|
||||
const url = args.url as string
|
||||
return url ? new URL(url).hostname : ''
|
||||
}
|
||||
// Plan
|
||||
if (name === 'ask_user') {
|
||||
const question = args.question as string
|
||||
return question ? question.slice(0, 30) + (question.length > 30 ? '...' : '') : ''
|
||||
let hostname = ''
|
||||
if (url) { try { hostname = new URL(url).hostname } catch { hostname = url } }
|
||||
if (!hostname) return isRunning ? 'Reading URL...' : ''
|
||||
if (isRunning) return `Reading ${hostname}...`
|
||||
if (isSuccess) return `Read ${hostname}`
|
||||
if (isError) return `Failed to read ${hostname}`
|
||||
return `Reading ${hostname}`
|
||||
}
|
||||
|
||||
// Return null/empty string by default
|
||||
return ''
|
||||
}, [toolCall.name, args])
|
||||
// LSP类
|
||||
if (['get_lint_errors', 'find_references', 'go_to_definition', 'get_hover_info', 'get_document_symbols'].includes(name)) {
|
||||
const target = formatPath(args.path)
|
||||
if (!target) return isRunning ? 'Analyzing...' : ''
|
||||
if (isRunning) return `Analyzing ${target}...`
|
||||
if (isSuccess) return `Analyzed ${target}`
|
||||
if (isError) return `Analysis failed`
|
||||
return `Analyzing ${target}`
|
||||
}
|
||||
|
||||
const isMissingDescription = !description && (isStreaming || isRunning)
|
||||
// 默认 fallback
|
||||
return isRunning ? 'Processing...' : ''
|
||||
}, [toolCall.name, toolCall.status, args, isStreaming])
|
||||
|
||||
const handleCopyResult = () => {
|
||||
if (toolCall.result) {
|
||||
@@ -448,11 +501,11 @@ const ToolCallCard = memo(function ToolCallCard({
|
||||
|
||||
return (
|
||||
<div className={`group my-1 rounded-xl border overflow-hidden ${cardStyle} animate-slide-in-right relative`}>
|
||||
{/* Animated Dashed Border for running state */}
|
||||
{/* Sweeping Light Effect for running state */}
|
||||
{(isStreaming || isRunning) && (
|
||||
<div className="absolute inset-0 pointer-events-none rounded-xl border border-dashed border-accent/40 animate-[spin_10s_linear_infinite]"
|
||||
style={{ WebkitMaskImage: 'linear-gradient(to bottom right, black, transparent)', opacity: 0.5 }}
|
||||
/>
|
||||
<div className="absolute inset-0 pointer-events-none rounded-xl overflow-hidden">
|
||||
<div className="absolute inset-0 w-[200%] h-full bg-gradient-to-r from-transparent via-accent/20 to-transparent animate-shimmer" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
@@ -485,15 +538,10 @@ const ToolCallCard = memo(function ToolCallCard({
|
||||
>
|
||||
{TOOL_LABELS[toolCall.name] || toolCall.name}
|
||||
</span>
|
||||
{description ? (
|
||||
{statusText ? (
|
||||
<>
|
||||
<span className="text-text-muted/30">|</span>
|
||||
<span className="text-xs truncate font-mono text-text-muted">{description}</span>
|
||||
</>
|
||||
) : isMissingDescription ? (
|
||||
<>
|
||||
<span className="text-text-muted/30">|</span>
|
||||
<span className="text-xs truncate font-mono text-text-muted italic opacity-70">editing...</span>
|
||||
<span className="text-xs truncate text-text-muted">{statusText}</span>
|
||||
</>
|
||||
) : (isStreaming || isRunning) && (
|
||||
<span className="text-xs text-text-muted/50 italic animate-pulse ml-2">Processing...</span>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/**
|
||||
* 模式状态管理
|
||||
*
|
||||
* 通过 electron-store (preferencesStore) 持久化,
|
||||
* 与其他设置统一存储后端,通过 IPC 调用 settings:get/set
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import { WorkMode } from './types'
|
||||
import { api } from '@/renderer/services/electronAPI'
|
||||
|
||||
const STORE_KEY = 'modeStore'
|
||||
|
||||
interface ModeState {
|
||||
/** 当前工作模式 */
|
||||
@@ -24,6 +30,32 @@ interface ModeActions {
|
||||
|
||||
type ModeStore = ModeState & ModeActions
|
||||
|
||||
/**
|
||||
* 自定义 Storage:通过 IPC 存到 electron-store 的 preferencesStore
|
||||
* 统一与其他设置的存储后端,避免使用 localStorage
|
||||
*/
|
||||
const electronStoreStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
try {
|
||||
const value = await api.settings.get(`${STORE_KEY}.${name}`)
|
||||
return value ? JSON.stringify(value) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
await api.settings.set(`${STORE_KEY}.${name}`, parsed)
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
removeItem: async (name: string): Promise<void> => {
|
||||
try {
|
||||
await api.settings.set(`${STORE_KEY}.${name}`, undefined)
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
}
|
||||
|
||||
export const useModeStore = create<ModeStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -54,6 +86,7 @@ export const useModeStore = create<ModeStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'adnify-mode-store',
|
||||
storage: createJSONStorage(() => electronStoreStorage),
|
||||
partialize: (state) => ({
|
||||
currentMode: state.currentMode
|
||||
})
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* 所有项目级数据都存储在 .adnify 目录下:
|
||||
* .adnify/
|
||||
* ├── index/ # 代码库向量索引
|
||||
* ├── sessions.json # Agent 会话历史(包含检查点)
|
||||
* ├── sessions/ # Agent 会话(按线程拆分)
|
||||
* │ ├── _meta.json # 线程元数据(currentThreadId, threadIds)
|
||||
* │ └── {threadId}.json # 单个线程数据
|
||||
* ├── settings.json # 项目级设置
|
||||
* ├── workspace-state.json # 工作区状态(打开的文件等)
|
||||
* └── rules.md # 项目 AI 规则
|
||||
@@ -19,7 +21,7 @@ export const ADNIFY_DIR_NAME = '.adnify'
|
||||
// 子目录和文件
|
||||
export const ADNIFY_FILES = {
|
||||
INDEX_DIR: 'index',
|
||||
SESSIONS: 'sessions.json',
|
||||
SESSIONS_DIR: 'sessions',
|
||||
SETTINGS: 'settings.json',
|
||||
WORKSPACE_STATE: 'workspace-state.json',
|
||||
RULES: 'rules.md',
|
||||
@@ -29,18 +31,13 @@ type AdnifyFile = typeof ADNIFY_FILES[keyof typeof ADNIFY_FILES]
|
||||
|
||||
// ============ 数据类型定义 ============
|
||||
|
||||
/** Agent 会话数据 */
|
||||
export interface SessionsData {
|
||||
/** zustand store 数据 */
|
||||
'adnify-agent-store'?: {
|
||||
state: {
|
||||
threads: Record<string, unknown>
|
||||
currentThreadId: string | null
|
||||
}
|
||||
version: number
|
||||
}
|
||||
/** 其他会话相关数据 */
|
||||
[key: string]: unknown
|
||||
/** 线程元数据 */
|
||||
export interface SessionMeta {
|
||||
currentThreadId: string | null
|
||||
threadIds: string[]
|
||||
/** 非线程数据(branches, messageCheckpoints 等) */
|
||||
extra: Record<string, unknown>
|
||||
version: number
|
||||
}
|
||||
|
||||
/** 工作区状态 */
|
||||
@@ -101,6 +98,13 @@ const DEFAULT_PROJECT_SETTINGS: ProjectSettingsData = {
|
||||
},
|
||||
}
|
||||
|
||||
const DEFAULT_SESSION_META: SessionMeta = {
|
||||
currentThreadId: null,
|
||||
threadIds: [],
|
||||
extra: {},
|
||||
version: 0,
|
||||
}
|
||||
|
||||
// ============ 服务实现 ============
|
||||
|
||||
class AdnifyDirService {
|
||||
@@ -110,22 +114,26 @@ class AdnifyDirService {
|
||||
|
||||
// 内存缓存
|
||||
private cache: {
|
||||
sessions: SessionsData | null
|
||||
sessionMeta: SessionMeta | null
|
||||
threads: Map<string, unknown>
|
||||
workspaceState: WorkspaceStateData | null
|
||||
settings: ProjectSettingsData | null
|
||||
} = {
|
||||
sessions: null,
|
||||
sessionMeta: null,
|
||||
threads: new Map(),
|
||||
workspaceState: null,
|
||||
settings: null,
|
||||
}
|
||||
|
||||
// 脏标记
|
||||
private dirty: {
|
||||
sessions: boolean
|
||||
sessionMeta: boolean
|
||||
dirtyThreads: Set<string>
|
||||
workspaceState: boolean
|
||||
settings: boolean
|
||||
} = {
|
||||
sessions: false,
|
||||
sessionMeta: false,
|
||||
dirtyThreads: new Set(),
|
||||
workspaceState: false,
|
||||
settings: false,
|
||||
}
|
||||
@@ -142,7 +150,6 @@ class AdnifyDirService {
|
||||
try {
|
||||
const adnifyPath = `${rootPath}/${ADNIFY_DIR_NAME}`
|
||||
const exists = await api.file.exists(adnifyPath)
|
||||
|
||||
if (!exists) {
|
||||
await api.file.ensureDir(adnifyPath)
|
||||
}
|
||||
@@ -154,6 +161,13 @@ class AdnifyDirService {
|
||||
await api.file.ensureDir(indexPath)
|
||||
}
|
||||
|
||||
// 创建 sessions 子目录
|
||||
const sessionsPath = `${adnifyPath}/${ADNIFY_FILES.SESSIONS_DIR}`
|
||||
const sessionsExists = await api.file.exists(sessionsPath)
|
||||
if (!sessionsExists) {
|
||||
await api.file.ensureDir(sessionsPath)
|
||||
}
|
||||
|
||||
this.initializedRoots.add(rootPath)
|
||||
logger.system.info('[AdnifyDir] Root initialized:', rootPath)
|
||||
return true
|
||||
@@ -176,6 +190,7 @@ class AdnifyDirService {
|
||||
|
||||
this.primaryRoot = rootPath
|
||||
await this.initialize(rootPath)
|
||||
await this.migrateOldSessions()
|
||||
await this.loadAllData()
|
||||
this.initialized = true
|
||||
logger.system.info('[AdnifyDir] Primary root set:', rootPath)
|
||||
@@ -185,8 +200,8 @@ class AdnifyDirService {
|
||||
this.primaryRoot = null
|
||||
this.initializedRoots.clear()
|
||||
this.initialized = false
|
||||
this.cache = { sessions: null, workspaceState: null, settings: null }
|
||||
this.dirty = { sessions: false, workspaceState: false, settings: false }
|
||||
this.cache = { sessionMeta: null, threads: new Map(), workspaceState: null, settings: null }
|
||||
this.dirty = { sessionMeta: false, dirtyThreads: new Set(), workspaceState: false, settings: false }
|
||||
logger.system.info('[AdnifyDir] Reset')
|
||||
}
|
||||
|
||||
@@ -201,11 +216,21 @@ class AdnifyDirService {
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
if (this.dirty.sessions && this.cache.sessions) {
|
||||
promises.push(this.writeJsonFile(ADNIFY_FILES.SESSIONS, this.cache.sessions))
|
||||
this.dirty.sessions = false
|
||||
// 刷新 session meta
|
||||
if (this.dirty.sessionMeta && this.cache.sessionMeta) {
|
||||
promises.push(this.writeSessionFile('_meta.json', this.cache.sessionMeta))
|
||||
this.dirty.sessionMeta = false
|
||||
}
|
||||
|
||||
// 刷新 dirty 线程(只写变化的)
|
||||
for (const threadId of this.dirty.dirtyThreads) {
|
||||
const data = this.cache.threads.get(threadId)
|
||||
if (data) {
|
||||
promises.push(this.writeSessionFile(`${threadId}.json`, data))
|
||||
}
|
||||
}
|
||||
this.dirty.dirtyThreads.clear()
|
||||
|
||||
if (this.dirty.workspaceState && this.cache.workspaceState) {
|
||||
promises.push(this.writeJsonFile(ADNIFY_FILES.WORKSPACE_STATE, this.cache.workspaceState))
|
||||
this.dirty.workspaceState = false
|
||||
@@ -253,50 +278,149 @@ class AdnifyDirService {
|
||||
return `${this.getDirPath(rootPath)}/${file}`
|
||||
}
|
||||
|
||||
// ============ 数据操作 (基于 Primary Root) ============
|
||||
// ============ Session 操作(线程级别) ============
|
||||
|
||||
async getSessions(): Promise<SessionsData> {
|
||||
if (this.cache.sessions) return this.cache.sessions
|
||||
if (!this.isInitialized()) return {}
|
||||
const data = await this.readJsonFile<SessionsData>(ADNIFY_FILES.SESSIONS)
|
||||
this.cache.sessions = data || {}
|
||||
return this.cache.sessions
|
||||
async getSessionMeta(): Promise<SessionMeta> {
|
||||
if (this.cache.sessionMeta) return this.cache.sessionMeta
|
||||
if (!this.isInitialized()) return { ...DEFAULT_SESSION_META }
|
||||
const data = await this.readSessionFile<SessionMeta>('_meta.json')
|
||||
this.cache.sessionMeta = data || { ...DEFAULT_SESSION_META }
|
||||
return this.cache.sessionMeta
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 sessions(立即写入,用于关键操作)
|
||||
*/
|
||||
async saveSessions(data: SessionsData): Promise<void> {
|
||||
this.cache.sessions = data
|
||||
this.dirty.sessions = true
|
||||
if (this.isInitialized()) {
|
||||
await this.writeJsonFile(ADNIFY_FILES.SESSIONS, data)
|
||||
this.dirty.sessions = false
|
||||
async getThreadData(threadId: string): Promise<unknown | null> {
|
||||
if (this.cache.threads.has(threadId)) return this.cache.threads.get(threadId)!
|
||||
if (!this.isInitialized()) return null
|
||||
const data = await this.readSessionFile<unknown>(`${threadId}.json`)
|
||||
if (data) {
|
||||
this.cache.threads.set(threadId, data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 sessions 部分数据(立即写入,用于关键操作)
|
||||
* 设置线程数据为脏(延迟写入)
|
||||
* 这是 agentStorage 调用的主入口
|
||||
*/
|
||||
async updateSessionsPartial(key: string, value: unknown): Promise<void> {
|
||||
const sessions = await this.getSessions()
|
||||
sessions[key] = value
|
||||
await this.saveSessions(sessions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 sessions 部分数据为脏(延迟写入,用于频繁更新)
|
||||
* 这是推荐的高频更新方法
|
||||
*/
|
||||
setSessionsPartialDirty(key: string, value: unknown): void {
|
||||
if (!this.cache.sessions) {
|
||||
this.cache.sessions = {}
|
||||
}
|
||||
this.cache.sessions[key] = value
|
||||
this.dirty.sessions = true
|
||||
setThreadDirty(threadId: string, data: unknown): void {
|
||||
this.cache.threads.set(threadId, data)
|
||||
this.dirty.dirtyThreads.add(threadId)
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 session meta 为脏(延迟写入)
|
||||
*/
|
||||
setSessionMetaDirty(meta: SessionMeta): void {
|
||||
this.cache.sessionMeta = meta
|
||||
this.dirty.sessionMeta = true
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除线程数据文件
|
||||
*/
|
||||
async deleteThreadData(threadId: string): Promise<void> {
|
||||
this.cache.threads.delete(threadId)
|
||||
this.dirty.dirtyThreads.delete(threadId)
|
||||
|
||||
// 更新 meta
|
||||
const meta = await this.getSessionMeta()
|
||||
meta.threadIds = meta.threadIds.filter(id => id !== threadId)
|
||||
this.setSessionMetaDirty(meta)
|
||||
|
||||
// 删除文件
|
||||
if (this.isInitialized()) {
|
||||
try {
|
||||
const filePath = `${this.getDirPath()}/${ADNIFY_FILES.SESSIONS_DIR}/${threadId}.json`
|
||||
await api.file.delete(filePath)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有 session 数据
|
||||
*/
|
||||
async clearAllSessions(): Promise<void> {
|
||||
const meta = await this.getSessionMeta()
|
||||
for (const threadId of meta.threadIds) {
|
||||
this.cache.threads.delete(threadId)
|
||||
try {
|
||||
const filePath = `${this.getDirPath()}/${ADNIFY_FILES.SESSIONS_DIR}/${threadId}.json`
|
||||
await api.file.delete(filePath)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
this.cache.sessionMeta = { ...DEFAULT_SESSION_META }
|
||||
this.dirty.sessionMeta = true
|
||||
this.dirty.dirtyThreads.clear()
|
||||
await this.writeSessionFile('_meta.json', this.cache.sessionMeta)
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容方法:供 agentStorage 使用
|
||||
* 从线程级文件构建完整的 store persist 数据
|
||||
*/
|
||||
async getFullSessionData(): Promise<Record<string, unknown> | null> {
|
||||
const meta = await this.getSessionMeta()
|
||||
if (meta.threadIds.length === 0 && !meta.currentThreadId) return null
|
||||
|
||||
const threads: Record<string, unknown> = {}
|
||||
for (const threadId of meta.threadIds) {
|
||||
const data = await this.getThreadData(threadId)
|
||||
if (data) threads[threadId] = data
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
threads,
|
||||
currentThreadId: meta.currentThreadId,
|
||||
...meta.extra,
|
||||
},
|
||||
version: meta.version,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容方法:供 agentStorage 使用
|
||||
* 将完整 store persist 数据拆分到线程级文件
|
||||
*/
|
||||
setFullSessionDataDirty(_storeKey: string, parsed: Record<string, unknown>): void {
|
||||
const state = parsed.state as Record<string, unknown> | undefined
|
||||
if (!state) return
|
||||
|
||||
const threads = (state.threads || {}) as Record<string, unknown>
|
||||
const currentThreadId = state.currentThreadId as string | null
|
||||
const { threads: _, currentThreadId: __, ...extra } = state
|
||||
|
||||
// 更新 meta
|
||||
const threadIds = Object.keys(threads)
|
||||
const meta: SessionMeta = {
|
||||
currentThreadId,
|
||||
threadIds,
|
||||
extra,
|
||||
version: (parsed.version as number) || 0,
|
||||
}
|
||||
this.setSessionMetaDirty(meta)
|
||||
|
||||
// 标记变化的线程为 dirty
|
||||
for (const [threadId, data] of Object.entries(threads)) {
|
||||
const cached = this.cache.threads.get(threadId)
|
||||
// 只在数据有变化时才标记(简单引用比较)
|
||||
if (cached !== data) {
|
||||
this.setThreadDirty(threadId, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已删除的线程缓存
|
||||
for (const cachedId of this.cache.threads.keys()) {
|
||||
if (!threads[cachedId]) {
|
||||
this.cache.threads.delete(cachedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ workspace / settings 操作 ============
|
||||
|
||||
async getWorkspaceState(): Promise<WorkspaceStateData> {
|
||||
if (this.cache.workspaceState) return this.cache.workspaceState
|
||||
if (!this.isInitialized()) return { ...DEFAULT_WORKSPACE_STATE }
|
||||
@@ -364,18 +488,89 @@ class AdnifyDirService {
|
||||
|
||||
// ============ 内部方法 ============
|
||||
|
||||
/**
|
||||
* 旧 sessions.json → 新 sessions/ 目录迁移
|
||||
*/
|
||||
private async migrateOldSessions(): Promise<void> {
|
||||
if (!this.primaryRoot) return
|
||||
|
||||
const oldPath = `${this.getDirPath()}/sessions.json`
|
||||
try {
|
||||
const exists = await api.file.exists(oldPath)
|
||||
if (!exists) return
|
||||
|
||||
const content = await api.file.read(oldPath)
|
||||
if (!content) return
|
||||
|
||||
const oldData = JSON.parse(content)
|
||||
|
||||
// 旧格式:{ 'adnify-agent-store': { state: { threads, currentThreadId }, version } }
|
||||
const storeData = oldData['adnify-agent-store']
|
||||
if (!storeData?.state?.threads) {
|
||||
// 无有效数据,删除旧文件
|
||||
await api.file.delete(oldPath)
|
||||
return
|
||||
}
|
||||
|
||||
const { threads, currentThreadId, ...extra } = storeData.state as Record<string, unknown>
|
||||
const threadsMap = threads as Record<string, unknown>
|
||||
const threadIds = Object.keys(threadsMap)
|
||||
|
||||
// 写入各线程文件
|
||||
for (const [threadId, data] of Object.entries(threadsMap)) {
|
||||
await this.writeSessionFile(`${threadId}.json`, data)
|
||||
}
|
||||
|
||||
// 写入 meta
|
||||
const meta: SessionMeta = {
|
||||
currentThreadId: currentThreadId as string | null,
|
||||
threadIds,
|
||||
extra,
|
||||
version: storeData.version || 0,
|
||||
}
|
||||
await this.writeSessionFile('_meta.json', meta)
|
||||
|
||||
// 删除旧文件
|
||||
await api.file.delete(oldPath)
|
||||
logger.system.info(`[AdnifyDir] Migrated sessions.json → sessions/ (${threadIds.length} threads)`)
|
||||
} catch (error) {
|
||||
logger.system.error('[AdnifyDir] Sessions migration failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAllData(): Promise<void> {
|
||||
const [sessions, workspaceState, settings] = await Promise.all([
|
||||
this.readJsonFile<SessionsData>(ADNIFY_FILES.SESSIONS),
|
||||
const [sessionMeta, workspaceState, settings] = await Promise.all([
|
||||
this.readSessionFile<SessionMeta>('_meta.json'),
|
||||
this.readJsonFile<WorkspaceStateData>(ADNIFY_FILES.WORKSPACE_STATE),
|
||||
this.readJsonFile<ProjectSettingsData>(ADNIFY_FILES.SETTINGS),
|
||||
])
|
||||
this.cache.sessions = sessions || {}
|
||||
this.cache.sessionMeta = sessionMeta || { ...DEFAULT_SESSION_META }
|
||||
this.cache.workspaceState = workspaceState || { ...DEFAULT_WORKSPACE_STATE }
|
||||
this.cache.settings = settings ? { ...DEFAULT_PROJECT_SETTINGS, ...settings } : { ...DEFAULT_PROJECT_SETTINGS }
|
||||
logger.system.info('[AdnifyDir] Loaded all data from disk')
|
||||
}
|
||||
|
||||
private async readSessionFile<T>(fileName: string): Promise<T | null> {
|
||||
try {
|
||||
const filePath = `${this.getDirPath()}/${ADNIFY_FILES.SESSIONS_DIR}/${fileName}`
|
||||
const content = await api.file.read(filePath)
|
||||
if (!content) return null
|
||||
return JSON.parse(content) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async writeSessionFile<T>(fileName: string, data: T): Promise<void> {
|
||||
try {
|
||||
const filePath = `${this.getDirPath()}/${ADNIFY_FILES.SESSIONS_DIR}/${fileName}`
|
||||
const content = JSON.stringify(data, null, 2)
|
||||
await api.file.write(filePath, content)
|
||||
} catch (error) {
|
||||
logger.system.error(`[AdnifyDir] Failed to write session file ${fileName}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
private async readJsonFile<T>(file: AdnifyFile): Promise<T | null> {
|
||||
try {
|
||||
const content = await api.file.read(this.getFilePath(file))
|
||||
|
||||
@@ -69,6 +69,7 @@ export default {
|
||||
'scale-in': 'scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'pulse-glow': 'pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'shimmer': 'shimmer 2s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
@@ -98,6 +99,10 @@ export default {
|
||||
pulseGlow: {
|
||||
'0%, 100%': { opacity: '1', boxShadow: '0 0 0px rgb(var(--accent) / 0)' },
|
||||
'50%': { opacity: '0.9', boxShadow: '0 0 15px rgb(var(--accent) / 0.3)' },
|
||||
},
|
||||
shimmer: {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(50%)' },
|
||||
}
|
||||
},
|
||||
backdropBlur: {
|
||||
|
||||
Reference in New Issue
Block a user