diff --git a/src/main/events.ts b/src/main/events.ts index 130c392e2..1d959c698 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -43,7 +43,8 @@ export const CONFIG_EVENTS = { DEFAULT_SYSTEM_PROMPT_CHANGED: 'config:default-system-prompt-changed', // Default system prompt changed event CUSTOM_PROMPTS_CHANGED: 'config:custom-prompts-changed', // 自定义提示词变更事件 NOWLEDGE_MEM_CONFIG_UPDATED: 'config:nowledge-mem-config-updated', // Nowledge-mem configuration updated event - DEFAULT_PROJECT_PATH_CHANGED: 'config:default-project-path-changed' + DEFAULT_PROJECT_PATH_CHANGED: 'config:default-project-path-changed', + AGENTS_CHANGED: 'config:agents-changed' } // Provider DB(聚合 JSON)相关事件 @@ -120,6 +121,7 @@ export const WINDOW_EVENTS = { // Settings related events export const SETTINGS_EVENTS = { + READY: 'settings:ready', NAVIGATE: 'settings:navigate', CHECK_FOR_UPDATES: 'settings:check-for-updates' } diff --git a/src/main/presenter/agentRepository/index.ts b/src/main/presenter/agentRepository/index.ts new file mode 100644 index 000000000..ceb5ddc84 --- /dev/null +++ b/src/main/presenter/agentRepository/index.ts @@ -0,0 +1,482 @@ +import { nanoid } from 'nanoid' +import type { + AcpAgentConfig, + AcpAgentInstallState, + AcpAgentState, + AcpManualAgent, + AcpRegistryAgent +} from '@shared/presenter' +import type { + Agent, + AgentAvatar, + DeepChatAgentConfig, + CreateDeepChatAgentInput, + UpdateDeepChatAgentInput +} from '@shared/types/agent-interface' +import type { SQLitePresenter } from '../sqlitePresenter' +import type { AgentRow } from '../sqlitePresenter/tables/agents' + +type StoredAgentState = { + envOverride?: Record + installState?: AcpAgentInstallState | null +} + +type StoredAcpManualConfig = { + command: string + args?: string[] + env?: Record +} + +type StoredAcpRegistryConfig = { + version?: string + distribution?: AcpRegistryAgent['distribution'] +} + +const BUILTIN_DEEPCHAT_AGENT_ID = 'deepchat' + +const parseJson = (raw?: string | null): T | null => { + if (!raw) { + return null + } + + try { + return JSON.parse(raw) as T + } catch { + return null + } +} + +const stringifyJson = (value: unknown): string | null => { + if (value === undefined || value === null) { + return null + } + return JSON.stringify(value) +} + +const sanitizeString = (value?: string | null): string | null => { + const normalized = value?.trim() + return normalized ? normalized : null +} + +const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T + +const mergeDeepChatConfig = ( + baseConfig: DeepChatAgentConfig, + overrideConfig: DeepChatAgentConfig +): DeepChatAgentConfig => ({ + defaultModelPreset: overrideConfig.defaultModelPreset ?? baseConfig.defaultModelPreset ?? null, + assistantModel: overrideConfig.assistantModel ?? baseConfig.assistantModel ?? null, + visionModel: overrideConfig.visionModel ?? baseConfig.visionModel ?? null, + defaultProjectPath: overrideConfig.defaultProjectPath ?? baseConfig.defaultProjectPath ?? null, + systemPrompt: overrideConfig.systemPrompt ?? baseConfig.systemPrompt ?? '', + permissionMode: overrideConfig.permissionMode ?? baseConfig.permissionMode ?? 'full_access', + disabledAgentTools: overrideConfig.disabledAgentTools ?? baseConfig.disabledAgentTools ?? [], + autoCompactionEnabled: + overrideConfig.autoCompactionEnabled ?? baseConfig.autoCompactionEnabled ?? true, + autoCompactionTriggerThreshold: + overrideConfig.autoCompactionTriggerThreshold ?? + baseConfig.autoCompactionTriggerThreshold ?? + 80, + autoCompactionRetainRecentPairs: + overrideConfig.autoCompactionRetainRecentPairs ?? + baseConfig.autoCompactionRetainRecentPairs ?? + 2 +}) + +export class AgentRepository { + constructor(private readonly sqlitePresenter: SQLitePresenter) {} + + listAgents(filters?: { agentType?: 'deepchat' | 'acp'; enabled?: boolean }): Agent[] { + const rows = this.sqlitePresenter.agentsTable.list({ + agentType: filters?.agentType, + enabled: filters?.enabled + }) + return rows.map((row) => this.toAgent(row)) + } + + getAgent(agentId: string): Agent | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + return row ? this.toAgent(row) : null + } + + getAgentType(agentId: string): 'deepchat' | 'acp' | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + return row?.agent_type ?? null + } + + ensureBuiltinDeepChatAgent(defaults?: { + name?: string + icon?: string | null + avatar?: AgentAvatar | null + config?: DeepChatAgentConfig | null + }): Agent { + const existing = this.sqlitePresenter.agentsTable.get(BUILTIN_DEEPCHAT_AGENT_ID) + if (!existing) { + this.sqlitePresenter.agentsTable.create({ + id: BUILTIN_DEEPCHAT_AGENT_ID, + agentType: 'deepchat', + source: 'builtin', + name: defaults?.name?.trim() || 'DeepChat', + enabled: true, + protected: true, + icon: sanitizeString(defaults?.icon), + avatarJson: stringifyJson(defaults?.avatar ?? null), + configJson: stringifyJson(defaults?.config ?? null) + }) + return this.getAgent(BUILTIN_DEEPCHAT_AGENT_ID) as Agent + } + + this.sqlitePresenter.agentsTable.update(BUILTIN_DEEPCHAT_AGENT_ID, { + enabled: true, + protected: true + }) + return this.getAgent(BUILTIN_DEEPCHAT_AGENT_ID) as Agent + } + + createDeepChatAgent(input: CreateDeepChatAgentInput): Agent { + const id = `deepchat-${nanoid(8)}` + this.sqlitePresenter.agentsTable.create({ + id, + agentType: 'deepchat', + source: 'manual', + name: input.name.trim(), + enabled: input.enabled !== false, + protected: false, + description: sanitizeString(input.description), + icon: sanitizeString(input.icon), + avatarJson: stringifyJson(input.avatar ?? null), + configJson: stringifyJson(input.config ?? null) + }) + return this.getAgent(id) as Agent + } + + updateDeepChatAgent(agentId: string, updates: UpdateDeepChatAgentInput): Agent | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'deepchat') { + return null + } + + const currentConfig = parseJson(row.config_json) ?? {} + const nextConfig = + updates.config === undefined + ? currentConfig + : { ...currentConfig, ...clone(updates.config ?? {}) } + + this.sqlitePresenter.agentsTable.update(agentId, { + name: updates.name?.trim() || row.name, + enabled: updates.enabled ?? row.enabled === 1, + description: + updates.description === undefined ? row.description : sanitizeString(updates.description), + icon: updates.icon === undefined ? row.icon : sanitizeString(updates.icon), + avatarJson: + updates.avatar === undefined ? row.avatar_json : stringifyJson(updates.avatar ?? null), + configJson: updates.config === undefined ? row.config_json : stringifyJson(nextConfig) + }) + + return this.getAgent(agentId) + } + + deleteDeepChatAgent(agentId: string): boolean { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'deepchat' || row.protected === 1) { + return false + } + + this.sqlitePresenter.newSessionsTable.reassignAgentId(agentId, BUILTIN_DEEPCHAT_AGENT_ID) + this.sqlitePresenter.agentsTable.delete(agentId) + return true + } + + getDeepChatAgentConfig(agentId: string): DeepChatAgentConfig | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'deepchat') { + return null + } + return parseJson(row.config_json) + } + + resolveDeepChatAgentConfig(agentId: string): DeepChatAgentConfig { + const builtin = this.getDeepChatAgentConfig(BUILTIN_DEEPCHAT_AGENT_ID) ?? {} + if (agentId === BUILTIN_DEEPCHAT_AGENT_ID) { + return mergeDeepChatConfig({}, builtin) + } + + const current = this.getDeepChatAgentConfig(agentId) ?? {} + return mergeDeepChatConfig(builtin, current) + } + + listManualAcpAgents(): AcpManualAgent[] { + return this.sqlitePresenter.agentsTable + .list({ agentType: 'acp', source: 'manual' }) + .map((row) => this.toAcpManualAgent(row)) + .filter((agent): agent is AcpManualAgent => Boolean(agent)) + } + + getManualAcpAgent(agentId: string): AcpManualAgent | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp' || row.source !== 'manual') { + return null + } + return this.toAcpManualAgent(row) + } + + createManualAcpAgent( + agent: Omit & { id?: string } + ): AcpManualAgent { + const id = agent.id?.trim() || nanoid(8) + this.sqlitePresenter.agentsTable.upsert({ + id, + agentType: 'acp', + source: 'manual', + name: agent.name.trim(), + enabled: agent.enabled, + protected: false, + description: sanitizeString(agent.description), + icon: sanitizeString(agent.icon), + configJson: stringifyJson({ + command: agent.command, + args: agent.args, + env: agent.env + } satisfies StoredAcpManualConfig), + stateJson: stringifyJson({}) + }) + return this.getManualAcpAgent(id) as AcpManualAgent + } + + updateManualAcpAgent( + agentId: string, + updates: Partial> + ): AcpManualAgent | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp' || row.source !== 'manual') { + return null + } + + const currentConfig = parseJson(row.config_json) ?? { command: '' } + const nextConfig: StoredAcpManualConfig = { + command: updates.command?.trim() || currentConfig.command, + args: updates.args ?? currentConfig.args, + env: updates.env ?? currentConfig.env + } + + this.sqlitePresenter.agentsTable.update(agentId, { + name: updates.name?.trim() || row.name, + enabled: updates.enabled ?? row.enabled === 1, + description: + updates.description === undefined ? row.description : sanitizeString(updates.description), + icon: updates.icon === undefined ? row.icon : sanitizeString(updates.icon), + configJson: stringifyJson(nextConfig) + }) + + return this.getManualAcpAgent(agentId) + } + + removeManualAcpAgent(agentId: string): boolean { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp' || row.source !== 'manual') { + return false + } + this.sqlitePresenter.agentsTable.delete(agentId) + return true + } + + syncRegistryAgents( + agents: AcpRegistryAgent[], + legacyStateById?: Record, + legacyInstallStateById?: Record + ): void { + for (const agent of agents) { + const currentRow = this.sqlitePresenter.agentsTable.get(agent.id) + const currentState = parseJson(currentRow?.state_json) ?? {} + const legacyState = legacyStateById?.[agent.id] + const legacyInstallState = legacyInstallStateById?.[agent.id] + const mergedState: StoredAgentState = { + envOverride: currentState.envOverride ?? legacyState?.envOverride, + installState: currentState.installState ?? legacyInstallState ?? null + } + + this.sqlitePresenter.agentsTable.upsert({ + id: agent.id, + agentType: 'acp', + source: 'registry', + name: agent.name, + enabled: currentRow ? currentRow.enabled === 1 : (legacyState?.enabled ?? false), + protected: false, + description: sanitizeString(agent.description), + icon: sanitizeString(agent.icon), + configJson: stringifyJson({ + version: agent.version, + distribution: agent.distribution + } satisfies StoredAcpRegistryConfig), + stateJson: stringifyJson(mergedState), + createdAt: currentRow?.created_at, + updatedAt: Date.now() + }) + } + } + + getAcpAgentState(agentId: string): AcpAgentState | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp') { + return null + } + + const state = parseJson(row.state_json) ?? {} + return { + agentId: row.id, + enabled: row.enabled === 1, + envOverride: state.envOverride, + updatedAt: row.updated_at + } + } + + setAgentEnabled(agentId: string, enabled: boolean): boolean { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row) { + return false + } + this.sqlitePresenter.agentsTable.update(agentId, { enabled }) + return true + } + + setAgentEnvOverride(agentId: string, env: Record): boolean { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp') { + return false + } + + const state = parseJson(row.state_json) ?? {} + this.sqlitePresenter.agentsTable.update(agentId, { + stateJson: stringifyJson({ + ...state, + envOverride: clone(env) + } satisfies StoredAgentState) + }) + return true + } + + getAgentInstallState(agentId: string): AcpAgentInstallState | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp') { + return null + } + return parseJson(row.state_json)?.installState ?? null + } + + setAgentInstallState(agentId: string, installState: AcpAgentInstallState | null): boolean { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp') { + return false + } + + const state = parseJson(row.state_json) ?? {} + this.sqlitePresenter.agentsTable.update(agentId, { + stateJson: stringifyJson({ + ...state, + installState + } satisfies StoredAgentState) + }) + return true + } + + toAcpAgentConfig( + agentId: string, + preview?: Pick + ): AcpAgentConfig | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp') { + return null + } + + if (row.source === 'manual') { + const manual = this.toAcpManualAgent(row) + if (!manual) { + return null + } + return { + id: manual.id, + name: manual.name, + command: manual.command, + args: manual.args, + env: manual.env, + description: manual.description, + icon: manual.icon, + source: 'manual', + installState: null + } + } + + if (!preview) { + return null + } + + return { + id: row.id, + name: row.name, + command: preview.command, + args: preview.args, + description: row.description ?? undefined, + icon: row.icon ?? undefined, + source: 'registry', + installState: this.getAgentInstallState(row.id) + } + } + + getAcpRegistryOverlay(agentId: string): { + enabled: boolean + envOverride?: Record + installState?: AcpAgentInstallState | null + } | null { + const row = this.sqlitePresenter.agentsTable.get(agentId) + if (!row || row.agent_type !== 'acp' || row.source !== 'registry') { + return null + } + const state = parseJson(row.state_json) ?? {} + return { + enabled: row.enabled === 1, + envOverride: state.envOverride, + installState: state.installState ?? null + } + } + + private toAcpManualAgent(row: AgentRow): AcpManualAgent | null { + const config = parseJson(row.config_json) + if (!config?.command) { + return null + } + + return { + id: row.id, + name: row.name, + command: config.command, + args: config.args, + env: config.env, + enabled: row.enabled === 1, + description: row.description ?? undefined, + icon: row.icon ?? undefined, + source: 'manual' + } + } + + private toAgent(row: AgentRow): Agent { + return { + id: row.id, + name: row.name, + type: row.agent_type, + agentType: row.agent_type, + enabled: row.enabled === 1, + protected: row.protected === 1, + icon: row.icon ?? undefined, + description: row.description ?? undefined, + source: row.source, + avatar: parseJson(row.avatar_json), + config: + row.agent_type === 'deepchat' + ? (parseJson(row.config_json) ?? null) + : null, + installState: this.getAgentInstallState(row.id) + } + } +} + +export { BUILTIN_DEEPCHAT_AGENT_ID } diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 3c0dc6852..12a9a7fb9 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -53,11 +53,19 @@ import { AcpRegistryService } from './acpRegistryService' import { AcpLaunchSpecService } from './acpLaunchSpecService' import { AcpProvider } from '../llmProviderPresenter/providers/acpProvider' import { resolveAcpAgentAlias } from './acpRegistryConstants' +import { AgentRepository, BUILTIN_DEEPCHAT_AGENT_ID } from '../agentRepository' import type { HookEventName, HookTestResult, HooksNotificationsSettings } from '@shared/hooksNotifications' +import type { + Agent, + AgentType, + CreateDeepChatAgentInput, + DeepChatAgentConfig, + UpdateDeepChatAgentInput +} from '@shared/types/agent-interface' import { createDefaultHooksNotificationsConfig, normalizeHooksNotificationsConfig @@ -97,6 +105,7 @@ interface IAppSettings { defaultVisionModel?: { providerId: string; modelId: string } // Default vision model for image tools defaultProjectPath?: string | null acpRegistryMigrationVersion?: number + unifiedAgentsMigrationVersion?: number [key: string]: unknown // Allow arbitrary keys, using unknown type instead of any } @@ -116,6 +125,7 @@ const defaultProviders = DEFAULT_PROVIDERS.map((provider) => ({ })) const PROVIDERS_STORE_KEY = 'providers' +const UNIFIED_AGENTS_MIGRATION_VERSION = 1 type AnthropicLegacyProvider = LLM_PROVIDER & { authMode?: 'apikey' | 'oauth' } type ModelSelection = { providerId: string; modelId: string } type AnthropicModelSettingKey = 'defaultModel' | 'assistantModel' | 'defaultVisionModel' @@ -197,6 +207,7 @@ export class ConfigPresenter implements IConfigPresenter { private providerModelHelper: ProviderModelHelper private systemPromptHelper: SystemPromptHelper private uiSettingsHelper: UiSettingsHelper + private agentRepository: AgentRepository | null = null // Custom prompts cache for high-frequency read operations private customPromptsCache: Prompt[] | null = null @@ -296,6 +307,7 @@ export class ConfigPresenter implements IConfigPresenter { void this.acpRegistryService .initialize() .then(() => { + this.syncRegistryAgentsToRepository() this.notifyAcpAgentsChanged() }) .catch((error) => { @@ -347,6 +359,119 @@ export class ConfigPresenter implements IConfigPresenter { } } + setAgentRepository(agentRepository: AgentRepository): void { + this.agentRepository = agentRepository + this.initializeUnifiedAgents() + } + + private getAgentRepositoryOrThrow(): AgentRepository { + if (!this.agentRepository) { + throw new Error('Unified agent repository is not attached.') + } + return this.agentRepository + } + + private initializeUnifiedAgents(): void { + const repository = this.getAgentRepositoryOrThrow() + + repository.ensureBuiltinDeepChatAgent({ + name: 'DeepChat', + config: this.buildLegacyBuiltinDeepChatConfig() + }) + + const migratedVersion = this.getSetting('unifiedAgentsMigrationVersion') ?? 0 + if (migratedVersion < UNIFIED_AGENTS_MIGRATION_VERSION) { + this.acpConfHelper.getManualAgents().forEach((agent) => { + repository.createManualAcpAgent(agent) + }) + + this.syncRegistryAgentsToRepository( + this.acpConfHelper.getRegistryStates(), + this.acpConfHelper.getInstallStates() + ) + this.store.set('unifiedAgentsMigrationVersion', UNIFIED_AGENTS_MIGRATION_VERSION) + return + } + + this.syncRegistryAgentsToRepository() + } + + private buildLegacyBuiltinDeepChatConfig(): DeepChatAgentConfig { + const defaultModel = this.store.get('defaultModel') as ModelSelection | undefined + const assistantModel = this.store.get('assistantModel') as ModelSelection | undefined + const visionModel = this.store.get('defaultVisionModel') as ModelSelection | undefined + const autoCompactionEnabled = this.store.get('autoCompactionEnabled') + const autoCompactionTriggerThreshold = this.store.get('autoCompactionTriggerThreshold') + const autoCompactionRetainRecentPairs = this.store.get('autoCompactionRetainRecentPairs') + + return { + defaultModelPreset: + defaultModel?.providerId && defaultModel?.modelId + ? { + providerId: defaultModel.providerId, + modelId: defaultModel.modelId + } + : null, + assistantModel: + assistantModel?.providerId && assistantModel?.modelId + ? { + providerId: assistantModel.providerId, + modelId: assistantModel.modelId + } + : null, + visionModel: + visionModel?.providerId && visionModel?.modelId + ? { + providerId: visionModel.providerId, + modelId: visionModel.modelId + } + : null, + systemPrompt: (this.store.get('default_system_prompt') as string | undefined) ?? '', + permissionMode: 'full_access', + disabledAgentTools: [], + autoCompactionEnabled: + typeof autoCompactionEnabled === 'boolean' ? autoCompactionEnabled : true, + autoCompactionTriggerThreshold: + typeof autoCompactionTriggerThreshold === 'number' ? autoCompactionTriggerThreshold : 80, + autoCompactionRetainRecentPairs: + typeof autoCompactionRetainRecentPairs === 'number' ? autoCompactionRetainRecentPairs : 2 + } + } + + private syncRegistryAgentsToRepository( + legacyStateById?: Record, + legacyInstallStateById?: Record + ): void { + if (!this.agentRepository) { + return + } + + try { + this.agentRepository.syncRegistryAgents( + this.acpRegistryService.listAgents(), + legacyStateById, + legacyInstallStateById + ) + } catch (error) { + console.warn('[Agents] Failed to sync ACP registry agents into sqlite:', error) + } + } + + private getBuiltinDeepChatConfig(): DeepChatAgentConfig { + return this.agentRepository?.resolveDeepChatAgentConfig(BUILTIN_DEEPCHAT_AGENT_ID) ?? {} + } + + private updateBuiltinDeepChatConfig(updates: Partial): void { + if (!this.agentRepository) { + return + } + + this.agentRepository.updateDeepChatAgent(BUILTIN_DEEPCHAT_AGENT_ID, { + config: updates + }) + this.notifyAcpAgentsChanged() + } + private initProviderModelsDir(): void { const modelsDir = path.join(this.userDataPath, PROVIDER_MODELS_DIR) if (!fs.existsSync(modelsDir)) { @@ -648,6 +773,20 @@ export class ConfigPresenter implements IConfigPresenter { getSetting(key: string): T | undefined { try { + if (this.agentRepository) { + if (key === 'defaultModel') { + return this.getDefaultModel() as T | undefined + } + if (key === 'assistantModel') { + return this.getBuiltinDeepChatConfig().assistantModel as T | undefined + } + if (key === 'defaultVisionModel') { + return this.getDefaultVisionModel() as T | undefined + } + if (key === 'default_system_prompt') { + return this.getBuiltinDeepChatConfig().systemPrompt as T | undefined + } + } return this.store.get(key) as T } catch (error) { console.error(`[Config] Failed to get setting ${key}:`, error) @@ -657,6 +796,31 @@ export class ConfigPresenter implements IConfigPresenter { setSetting(key: string, value: T): void { try { + if (this.agentRepository) { + if (key === 'defaultModel') { + this.setDefaultModel(value as { providerId: string; modelId: string } | undefined) + return + } + if (key === 'assistantModel') { + this.updateBuiltinDeepChatConfig({ + assistantModel: value as { providerId: string; modelId: string } | null | undefined + }) + eventBus.sendToMain(CONFIG_EVENTS.SETTING_CHANGED, key, value) + return + } + if (key === 'defaultVisionModel') { + this.setDefaultVisionModel(value as { providerId: string; modelId: string } | undefined) + return + } + if (key === 'default_system_prompt') { + this.updateBuiltinDeepChatConfig({ + systemPrompt: typeof value === 'string' ? value : '' + }) + eventBus.sendToMain(CONFIG_EVENTS.SETTING_CHANGED, key, value) + return + } + } + this.store.set(key, value) // Trigger setting change event (main process internal use only) eventBus.sendToMain(CONFIG_EVENTS.SETTING_CHANGED, key, value) @@ -1082,27 +1246,42 @@ export class ConfigPresenter implements IConfigPresenter { } getAutoCompactionEnabled(): boolean { - return this.uiSettingsHelper.getAutoCompactionEnabled() + return ( + this.getBuiltinDeepChatConfig().autoCompactionEnabled ?? + this.uiSettingsHelper.getAutoCompactionEnabled() + ) } setAutoCompactionEnabled(enabled: boolean): void { - this.uiSettingsHelper.setAutoCompactionEnabled(enabled) + this.updateBuiltinDeepChatConfig({ + autoCompactionEnabled: Boolean(enabled) + }) } getAutoCompactionTriggerThreshold(): number { - return this.uiSettingsHelper.getAutoCompactionTriggerThreshold() + return ( + this.getBuiltinDeepChatConfig().autoCompactionTriggerThreshold ?? + this.uiSettingsHelper.getAutoCompactionTriggerThreshold() + ) } setAutoCompactionTriggerThreshold(threshold: number): void { - this.uiSettingsHelper.setAutoCompactionTriggerThreshold(threshold) + this.updateBuiltinDeepChatConfig({ + autoCompactionTriggerThreshold: threshold + }) } getAutoCompactionRetainRecentPairs(): number { - return this.uiSettingsHelper.getAutoCompactionRetainRecentPairs() + return ( + this.getBuiltinDeepChatConfig().autoCompactionRetainRecentPairs ?? + this.uiSettingsHelper.getAutoCompactionRetainRecentPairs() + ) } setAutoCompactionRetainRecentPairs(count: number): void { - this.uiSettingsHelper.setAutoCompactionRetainRecentPairs(count) + this.updateBuiltinDeepChatConfig({ + autoCompactionRetainRecentPairs: count + }) } getContentProtectionEnabled(): boolean { @@ -1255,23 +1434,27 @@ export class ConfigPresenter implements IConfigPresenter { // ===================== ACP configuration methods ===================== async listAcpRegistryAgents(): Promise { + this.syncRegistryAgentsToRepository() const registryAgents = this.acpRegistryService.listAgents() - const registryStates = this.acpConfHelper.getRegistryStates() - const installStates = this.acpConfHelper.getInstallStates() return registryAgents.map((agent) => { - const state = registryStates[agent.id] + const overlay = this.agentRepository?.getAcpRegistryOverlay(agent.id) ?? { + enabled: this.acpConfHelper.getRegistryStates()[agent.id]?.enabled ?? false, + envOverride: this.acpConfHelper.getRegistryStates()[agent.id]?.envOverride, + installState: this.acpConfHelper.getInstallStates()[agent.id] ?? null + } return { ...agent, - enabled: state?.enabled ?? false, - envOverride: state?.envOverride, - installState: installStates[agent.id] ?? null + enabled: overlay.enabled, + envOverride: overlay.envOverride, + installState: overlay.installState ?? null } }) } async refreshAcpRegistry(force = true): Promise { await this.acpRegistryService.refresh(force) + this.syncRegistryAgentsToRepository() const agents = await this.listAcpRegistryAgents() this.notifyAcpAgentsChanged() return agents @@ -1282,12 +1465,12 @@ export class ConfigPresenter implements IConfigPresenter { } async getAcpAgentState(agentId: string): Promise { - return this.acpConfHelper.getAgentState(agentId) + return this.agentRepository?.getAcpAgentState(resolveAcpAgentAlias(agentId)) ?? null } async setAcpAgentEnabled(agentId: string, enabled: boolean): Promise { const resolvedId = resolveAcpAgentAlias(agentId) - this.acpConfHelper.setAgentEnabled(resolvedId, enabled) + this.getAgentRepositoryOrThrow().setAgentEnabled(resolvedId, enabled) this.handleAcpAgentsMutated([resolvedId]) if (enabled) { @@ -1299,17 +1482,17 @@ export class ConfigPresenter implements IConfigPresenter { async setAcpAgentEnvOverride(agentId: string, env: Record): Promise { const resolvedId = resolveAcpAgentAlias(agentId) - const installState = this.acpConfHelper.getInstallState(resolvedId) + const installState = this.getAgentRepositoryOrThrow().getAgentInstallState(resolvedId) if (installState?.status !== 'installed') { throw new Error(`ACP registry agent is not installed: ${resolvedId}`) } - this.acpConfHelper.setAgentEnvOverride(resolvedId, env) + this.getAgentRepositoryOrThrow().setAgentEnvOverride(resolvedId, env) this.handleAcpAgentsMutated([resolvedId]) } async ensureAcpAgentInstalled(agentId: string): Promise { const registryAgent = this.getRegistryAgentOrThrow(agentId) - const currentState = this.acpConfHelper.getInstallState(registryAgent.id) + const currentState = this.getAgentRepositoryOrThrow().getAgentInstallState(registryAgent.id) const installingState: AcpAgentInstallState = { status: 'installing', version: registryAgent.version, @@ -1320,7 +1503,7 @@ export class ConfigPresenter implements IConfigPresenter { installDir: currentState?.installDir ?? null, error: null } - this.acpConfHelper.setInstallState(registryAgent.id, installingState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, installingState) this.notifyAcpAgentsChanged() try { @@ -1328,7 +1511,7 @@ export class ConfigPresenter implements IConfigPresenter { registryAgent, currentState ) - this.acpConfHelper.setInstallState(registryAgent.id, installedState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, installedState) this.notifyAcpAgentsChanged() return installedState } catch (error) { @@ -1342,7 +1525,7 @@ export class ConfigPresenter implements IConfigPresenter { installDir: currentState?.installDir ?? null, error: error instanceof Error ? error.message : String(error) } - this.acpConfHelper.setInstallState(registryAgent.id, failedState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, failedState) this.notifyAcpAgentsChanged() throw error } @@ -1350,7 +1533,7 @@ export class ConfigPresenter implements IConfigPresenter { async repairAcpAgent(agentId: string): Promise { const registryAgent = this.getRegistryAgentOrThrow(agentId) - const currentState = this.acpConfHelper.getInstallState(registryAgent.id) + const currentState = this.getAgentRepositoryOrThrow().getAgentInstallState(registryAgent.id) const repairingState: AcpAgentInstallState = { status: 'installing', version: registryAgent.version, @@ -1361,7 +1544,7 @@ export class ConfigPresenter implements IConfigPresenter { installDir: currentState?.installDir ?? null, error: null } - this.acpConfHelper.setInstallState(registryAgent.id, repairingState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, repairingState) this.notifyAcpAgentsChanged() try { @@ -1370,7 +1553,7 @@ export class ConfigPresenter implements IConfigPresenter { currentState, { repair: true } ) - this.acpConfHelper.setInstallState(registryAgent.id, installedState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, installedState) this.handleAcpAgentsMutated([registryAgent.id]) return installedState } catch (error) { @@ -1384,24 +1567,24 @@ export class ConfigPresenter implements IConfigPresenter { installDir: currentState?.installDir ?? null, error: error instanceof Error ? error.message : String(error) } - this.acpConfHelper.setInstallState(registryAgent.id, failedState) + this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, failedState) this.notifyAcpAgentsChanged() throw error } } async getAcpAgentInstallStatus(agentId: string): Promise { - return this.acpConfHelper.getInstallState(agentId) + return this.agentRepository?.getAgentInstallState(resolveAcpAgentAlias(agentId)) ?? null } async listManualAcpAgents(): Promise { - return this.acpConfHelper.getManualAgents() + return this.getAgentRepositoryOrThrow().listManualAcpAgents() } async addManualAcpAgent( agent: Omit & { id?: string } ): Promise { - const created = this.acpConfHelper.addManualAgent(agent) + const created = this.getAgentRepositoryOrThrow().createManualAcpAgent(agent) this.handleAcpAgentsMutated([created.id]) return created } @@ -1410,7 +1593,7 @@ export class ConfigPresenter implements IConfigPresenter { agentId: string, updates: Partial> ): Promise { - const updated = this.acpConfHelper.updateManualAgent(agentId, updates) + const updated = this.getAgentRepositoryOrThrow().updateManualAcpAgent(agentId, updates) if (updated) { this.handleAcpAgentsMutated([updated.id]) } @@ -1418,7 +1601,7 @@ export class ConfigPresenter implements IConfigPresenter { } async removeManualAcpAgent(agentId: string): Promise { - const removed = this.acpConfHelper.removeManualAgent(agentId) + const removed = this.getAgentRepositoryOrThrow().removeManualAcpAgent(agentId) if (removed) { this.handleAcpAgentsMutated([agentId]) } @@ -1449,15 +1632,13 @@ export class ConfigPresenter implements IConfigPresenter { async resolveAcpLaunchSpec(agentId: string, _workdir?: string): Promise { const resolvedId = resolveAcpAgentAlias(agentId) - const manualAgent = this.acpConfHelper - .getManualAgents() - .find((agent) => agent.id === resolvedId) + const manualAgent = this.getAgentRepositoryOrThrow().getManualAcpAgent(resolvedId) if (manualAgent) { return this.acpLaunchSpecService.resolveManualLaunchSpec(manualAgent) } const registryAgent = this.getRegistryAgentOrThrow(resolvedId) - const installState = this.acpConfHelper.getInstallState(registryAgent.id) + const installState = this.getAgentRepositoryOrThrow().getAgentInstallState(registryAgent.id) const launchSpec = await this.acpLaunchSpecService.resolveRegistryLaunchSpec( registryAgent, installState @@ -1472,7 +1653,7 @@ export class ConfigPresenter implements IConfigPresenter { installDir: launchSpec.installDir ?? null, error: null } - this.acpConfHelper.setInstallState(resolvedId, nextInstallState) + this.getAgentRepositoryOrThrow().setAgentInstallState(resolvedId, nextInstallState) return launchSpec } @@ -1485,6 +1666,53 @@ export class ConfigPresenter implements IConfigPresenter { this.handleAcpAgentsMutated() } + async listAgents(): Promise { + return this.getAgentRepositoryOrThrow().listAgents() + } + + async getAgent(agentId: string): Promise { + return this.getAgentRepositoryOrThrow().getAgent(agentId) + } + + async getAgentType(agentId: string): Promise { + return this.getAgentRepositoryOrThrow().getAgentType(agentId) + } + + async getDeepChatAgentConfig(agentId: string): Promise { + return this.getAgentRepositoryOrThrow().getDeepChatAgentConfig(agentId) + } + + async resolveDeepChatAgentConfig(agentId: string): Promise { + return this.getAgentRepositoryOrThrow().resolveDeepChatAgentConfig( + agentId || BUILTIN_DEEPCHAT_AGENT_ID + ) + } + + async createDeepChatAgent(input: CreateDeepChatAgentInput): Promise { + const created = this.getAgentRepositoryOrThrow().createDeepChatAgent(input) + this.notifyAcpAgentsChanged() + return created + } + + async updateDeepChatAgent( + agentId: string, + updates: UpdateDeepChatAgentInput + ): Promise { + const updated = this.getAgentRepositoryOrThrow().updateDeepChatAgent(agentId, updates) + if (updated) { + this.notifyAcpAgentsChanged() + } + return updated + } + + async deleteDeepChatAgent(agentId: string): Promise { + const removed = this.getAgentRepositoryOrThrow().deleteDeepChatAgent(agentId) + if (removed) { + this.notifyAcpAgentsChanged() + } + return removed + } + async getAgentMcpSelections(agentId: string, isBuiltin?: boolean): Promise { return await this.acpConfHelper.getAgentMcpSelections(agentId, isBuiltin) } @@ -1572,6 +1800,7 @@ export class ConfigPresenter implements IConfigPresenter { private notifyAcpAgentsChanged() { console.log('[ACP] notifyAcpAgentsChanged: sending MODEL_LIST_CHANGED event for provider "acp"') eventBus.sendToRenderer(CONFIG_EVENTS.MODEL_LIST_CHANGED, SendTarget.ALL_WINDOWS, 'acp') + eventBus.sendToRenderer(CONFIG_EVENTS.AGENTS_CHANGED, SendTarget.ALL_WINDOWS) eventBus.sendToRenderer(SESSION_EVENTS.LIST_UPDATED, SendTarget.ALL_WINDOWS) } @@ -2060,19 +2289,53 @@ export class ConfigPresenter implements IConfigPresenter { } getDefaultModel(): { providerId: string; modelId: string } | undefined { - return this.getSetting<{ providerId: string; modelId: string }>('defaultModel') + const selection = this.getBuiltinDeepChatConfig().defaultModelPreset + if (selection?.providerId && selection?.modelId) { + return { + providerId: selection.providerId, + modelId: selection.modelId + } + } + return this.store.get('defaultModel') as { providerId: string; modelId: string } | undefined } setDefaultModel(model: { providerId: string; modelId: string } | undefined): void { - this.setSetting('defaultModel', model) + this.updateBuiltinDeepChatConfig({ + defaultModelPreset: + model?.providerId && model?.modelId + ? { + providerId: model.providerId, + modelId: model.modelId + } + : null + }) + eventBus.sendToMain(CONFIG_EVENTS.SETTING_CHANGED, 'defaultModel', model) } getDefaultVisionModel(): { providerId: string; modelId: string } | undefined { - return this.getSetting<{ providerId: string; modelId: string }>('defaultVisionModel') + const selection = this.getBuiltinDeepChatConfig().visionModel + if (selection?.providerId && selection?.modelId) { + return { + providerId: selection.providerId, + modelId: selection.modelId + } + } + return this.store.get('defaultVisionModel') as + | { providerId: string; modelId: string } + | undefined } setDefaultVisionModel(model: { providerId: string; modelId: string } | undefined): void { - this.setSetting('defaultVisionModel', model) + this.updateBuiltinDeepChatConfig({ + visionModel: + model?.providerId && model?.modelId + ? { + providerId: model.providerId, + modelId: model.modelId + } + : null + }) + eventBus.sendToMain(CONFIG_EVENTS.SETTING_CHANGED, 'defaultVisionModel', model) } getDefaultProjectPath(): string | null { diff --git a/src/main/presenter/deepchatAgentPresenter/compactionService.ts b/src/main/presenter/deepchatAgentPresenter/compactionService.ts index 3057f8b4a..aa107c373 100644 --- a/src/main/presenter/deepchatAgentPresenter/compactionService.ts +++ b/src/main/presenter/deepchatAgentPresenter/compactionService.ts @@ -3,7 +3,8 @@ import type { ChatMessageRecord, SendMessageInput, AssistantMessageBlock, - MessageMetadata + MessageMetadata, + DeepChatAgentConfig } from '@shared/types/agent-interface' import type { IConfigPresenter, ILlmProviderPresenter } from '@shared/presenter' import type { DeepChatMessageStore } from './messageStore' @@ -198,10 +199,13 @@ export class CompactionService { private readonly sessionStore: DeepChatSessionStore, private readonly messageStore: DeepChatMessageStore, private readonly llmProviderPresenter: ILlmProviderPresenter, - private readonly configPresenter: IConfigPresenter + private readonly configPresenter: IConfigPresenter, + private readonly resolveSessionConfig: ( + sessionId: string + ) => Promise = async () => ({}) ) {} - prepareForNextUserTurn(params: { + async prepareForNextUserTurn(params: { sessionId: string providerId: string modelId: string @@ -211,8 +215,8 @@ export class CompactionService { supportsVision: boolean preserveInterleavedReasoning: boolean newUserContent: string | SendMessageInput - }): CompactionIntent | null { - const settings = this.getCompactionSettings() + }): Promise { + const settings = await this.getCompactionSettings(params.sessionId) if (!settings.enabled) { return null } @@ -231,7 +235,7 @@ export class CompactionService { }) } - prepareForResumeTurn(params: { + async prepareForResumeTurn(params: { sessionId: string messageId: string providerId: string @@ -241,8 +245,8 @@ export class CompactionService { reserveTokens: number supportsVision: boolean preserveInterleavedReasoning: boolean - }): CompactionIntent | null { - const settings = this.getCompactionSettings() + }): Promise { + const settings = await this.getCompactionSettings(params.sessionId) if (!settings.enabled) { return null } @@ -278,6 +282,7 @@ export class CompactionService { async applyCompaction(intent: CompactionIntent): Promise { try { const nextSummary = await this.generateRollingSummary({ + sessionId: intent.sessionId, previousSummary: intent.previousState.summaryText, summaryBlocks: intent.summaryBlocks, currentModel: intent.currentModel, @@ -393,11 +398,12 @@ export class CompactionService { } } - private getCompactionSettings(): CompactionSettings { + private async getCompactionSettings(sessionId: string): Promise { + const config = await this.resolveSessionConfig(sessionId) return { - enabled: this.configPresenter.getAutoCompactionEnabled(), - triggerThreshold: this.configPresenter.getAutoCompactionTriggerThreshold(), - retainRecentPairs: this.configPresenter.getAutoCompactionRetainRecentPairs() + enabled: config.autoCompactionEnabled ?? true, + triggerThreshold: config.autoCompactionTriggerThreshold ?? 80, + retainRecentPairs: config.autoCompactionRetainRecentPairs ?? 2 } } @@ -414,10 +420,11 @@ export class CompactionService { } } - private getAssistantModelSpec(currentModel: ModelSpec): ModelSpec | null { - const assistantModel = this.configPresenter.getSetting<{ providerId: string; modelId: string }>( - 'assistantModel' - ) + private async getAssistantModelSpec( + sessionId: string, + currentModel: ModelSpec + ): Promise { + const assistantModel = (await this.resolveSessionConfig(sessionId)).assistantModel const providerId = assistantModel?.providerId?.trim() const modelId = assistantModel?.modelId?.trim() if (!providerId || !modelId) { @@ -484,13 +491,14 @@ export class CompactionService { } private async generateRollingSummary(params: { + sessionId: string previousSummary: string | null summaryBlocks: string[] currentModel: ModelSpec reserveTokens: number }): Promise { const currentModel = params.currentModel - const assistantModel = this.getAssistantModelSpec(currentModel) + const assistantModel = await this.getAssistantModelSpec(params.sessionId, currentModel) const previousSummaryTokens = approximateTokenSize(params.previousSummary || '') const blockTokens = params.summaryBlocks.reduce( (total, block) => total + approximateTokenSize(block), diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/deepchatAgentPresenter/index.ts index 949460e37..4775c67a4 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/deepchatAgentPresenter/index.ts @@ -151,7 +151,11 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.sessionStore, this.messageStore, this.llmProviderPresenter, - this.configPresenter + this.configPresenter, + async (sessionId) => { + const agentId = this.getSessionAgentId(sessionId) ?? 'deepchat' + return await this.configPresenter.resolveDeepChatAgentConfig(agentId) + } ) this.toolOutputGuard = new ToolOutputGuard() this.hooksBridge = hooksBridge @@ -388,7 +392,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { think: false } - const compactionIntent = this.compactionService.prepareForNextUserTurn({ + const compactionIntent = await this.compactionService.prepareForNextUserTurn({ sessionId, providerId: state.providerId, modelId: state.modelId, @@ -2997,7 +3001,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { supportsVision: boolean preserveInterleavedReasoning: boolean }): Promise { - const intent = this.compactionService.prepareForResumeTurn(params) + const intent = await this.compactionService.prepareForResumeTurn(params) return await this.applyCompactionIntent(params.sessionId, intent) } diff --git a/src/main/presenter/floatingButtonPresenter/layout.ts b/src/main/presenter/floatingButtonPresenter/layout.ts index 1bc0703dd..b8a9ea358 100644 --- a/src/main/presenter/floatingButtonPresenter/layout.ts +++ b/src/main/presenter/floatingButtonPresenter/layout.ts @@ -55,7 +55,7 @@ export function buildFloatingWidgetSnapshot( expanded: boolean ): FloatingWidgetSnapshot { const mappedSessions: FloatingWidgetSessionItem[] = sessions - .filter((session) => session.agentId === 'deepchat' && !session.isDraft) + .filter((session) => session.providerId !== 'acp' && !session.isDraft) .map((session) => ({ id: session.id, title: session.title.trim(), diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 8f01bb612..88236e504 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -66,6 +66,8 @@ import { NewSessionHooksBridge } from './hooksNotifications/newSessionBridge' import { NewAgentPresenter } from './newAgentPresenter' import { DeepChatAgentPresenter } from './deepchatAgentPresenter' import { ProjectPresenter } from './projectPresenter' +import { AgentRepository } from './agentRepository' +import type { SQLitePresenter } from './sqlitePresenter' // IPC调用上下文接口 interface IPCCallContext { @@ -126,6 +128,12 @@ export class Presenter implements IPresenter { const context = lifecycleManager.getLifecycleContext() this.configPresenter = context.config as IConfigPresenter this.sqlitePresenter = context.database as ISQLitePresenter + const agentRepository = new AgentRepository(this.sqlitePresenter as unknown as SQLitePresenter) + ;( + this.configPresenter as IConfigPresenter & { + setAgentRepository?: (repository: AgentRepository) => void + } + ).setAgentRepository?.(agentRepository) // 初始化各个 Presenter 实例及其依赖 this.windowPresenter = new WindowPresenter(this.configPresenter) diff --git a/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts b/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts index d87d045cb..8a13972bb 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts @@ -89,13 +89,6 @@ export const eventListenerSetupHook: LifecycleHook = { navigateToAbout() triggerUpdateCheck() - - setTimeout(() => { - if (presenter.windowPresenter.getSettingsWindowId() === settingsWindowId) { - navigateToAbout() - triggerUpdateCheck() - } - }, 250) } catch (error) { console.error( 'eventListenerSetupHook: Failed to route tray update check to settings window:', diff --git a/src/main/presenter/newAgentPresenter/index.ts b/src/main/presenter/newAgentPresenter/index.ts index 2e2313ae6..5920a302c 100644 --- a/src/main/presenter/newAgentPresenter/index.ts +++ b/src/main/presenter/newAgentPresenter/index.ts @@ -104,19 +104,50 @@ export class NewAgentPresenter { async createSession(input: CreateSessionInput, webContentsId: number): Promise { const agentId = input.agentId || 'deepchat' console.log(`[NewAgentPresenter] createSession agent=${agentId} webContentsId=${webContentsId}`) - const projectDir = input.projectDir?.trim() ? input.projectDir.trim() : null const normalizedInput = this.normalizeCreateSessionInput(input) + const agentType = await this.getAgentType(agentId) + const deepChatAgentConfig = + agentType === 'deepchat' + ? await this.configPresenter.resolveDeepChatAgentConfig(agentId) + : null + const projectDir = + input.projectDir?.trim() || + deepChatAgentConfig?.defaultProjectPath?.trim() || + this.configPresenter.getDefaultProjectPath() || + null const disabledAgentTools = - agentId === 'deepchat' ? this.normalizeDisabledAgentTools(input.disabledAgentTools) : [] + agentType === 'deepchat' + ? this.normalizeDisabledAgentTools( + input.disabledAgentTools ?? deepChatAgentConfig?.disabledAgentTools + ) + : [] const agent = await this.resolveAgentImplementation(agentId) // Resolve provider/model const defaultModel = this.configPresenter.getDefaultModel() - const providerId = input.providerId ?? defaultModel?.providerId ?? '' - const modelId = input.modelId ?? defaultModel?.modelId ?? '' + const providerId = + input.providerId ?? + deepChatAgentConfig?.defaultModelPreset?.providerId ?? + defaultModel?.providerId ?? + '' + const modelId = + input.modelId ?? + deepChatAgentConfig?.defaultModelPreset?.modelId ?? + defaultModel?.modelId ?? + '' const permissionMode: PermissionMode = - input.permissionMode === 'default' ? 'default' : 'full_access' + input.permissionMode !== undefined + ? input.permissionMode === 'default' + ? 'default' + : 'full_access' + : deepChatAgentConfig?.permissionMode === 'default' + ? 'default' + : 'full_access' + const generationSettings = this.mergeDeepChatDefaultGenerationSettings( + deepChatAgentConfig, + input.generationSettings + ) console.log(`[NewAgentPresenter] resolved provider=${providerId} model=${modelId}`) if (!providerId || !modelId) { @@ -147,8 +178,8 @@ export class NewAgentPresenter { projectDir, permissionMode } - if (input.generationSettings) { - initConfig.generationSettings = input.generationSettings + if (generationSettings) { + initConfig.generationSettings = generationSettings } try { await this.initializeSessionRuntime(agent, sessionId, initConfig) @@ -284,8 +315,7 @@ export class NewAgentPresenter { const state = await agent.getSessionState(sessionId) let providerId = state?.providerId ?? '' if (!providerId) { - const acpAgents = await this.configPresenter.getAcpAgents() - if (acpAgents.some((item) => item.id === session.agentId)) { + if ((await this.getAgentType(session.agentId)) === 'acp') { providerId = 'acp' } } @@ -339,8 +369,7 @@ export class NewAgentPresenter { let providerId = (await agent.getSessionState(sessionId))?.providerId ?? '' if (!providerId) { - const acpAgents = await this.configPresenter.getAcpAgents() - if (acpAgents.some((item) => item.id === currentSession.agentId)) { + if ((await this.getAgentType(currentSession.agentId)) === 'acp') { providerId = 'acp' } } @@ -753,19 +782,20 @@ export class NewAgentPresenter { return this.messageManager.getMessage(messageId) } - async translateText(text: string, locale?: string): Promise { + async translateText(text: string, locale?: string, agentId?: string): Promise { const input = text?.trim() if (!input) { return '' } - const assistantModel = this.configPresenter.getSetting<{ - providerId: string - modelId: string - }>('assistantModel') const defaultModel = this.configPresenter.getDefaultModel() - const providerId = assistantModel?.providerId || defaultModel?.providerId || '' - const modelId = assistantModel?.modelId || defaultModel?.modelId || '' + const assistantSelection = await this.resolveAssistantModelSelection( + agentId ?? 'deepchat', + defaultModel?.providerId || '', + defaultModel?.modelId || '' + ) + const providerId = assistantSelection.providerId + const modelId = assistantSelection.modelId if (!providerId || !modelId) { throw new Error('No provider or model configured. Please set a default model in settings.') } @@ -818,24 +848,12 @@ export class NewAgentPresenter { } async getAgents(): Promise { - const builtins = this.agentRegistry.getAll() - const acpAgents = await this.configPresenter.getAcpAgents() - const acpList: Agent[] = acpAgents.map((agent) => ({ - id: agent.id, - name: agent.name, - type: 'acp', - enabled: true, - icon: agent.icon, - description: agent.description, - source: agent.source, - installState: agent.installState ?? null - })) - - const map = new Map() - for (const item of [...builtins, ...acpList]) { - map.set(item.id, item) - } - return Array.from(map.values()) + const [agents, acpEnabled] = await Promise.all([ + this.configPresenter.listAgents(), + this.configPresenter.getAcpEnabled() + ]) + + return agents.filter((agent) => agent.type === 'deepchat' || acpEnabled) } async renameSession(sessionId: string, title: string): Promise { @@ -895,7 +913,7 @@ export class NewAgentPresenter { const providerId = state?.providerId?.trim() ?? '' const modelId = state?.modelId?.trim() ?? '' - const conversation = this.buildExportConversation( + const conversation = await this.buildExportConversation( session, providerId, modelId, @@ -919,8 +937,7 @@ export class NewAgentPresenter { const state = await agent.getSessionState(sessionId) let providerId = state?.providerId ?? '' if (!providerId) { - const acpAgents = await this.configPresenter.getAcpAgents() - if (acpAgents.some((item) => item.id === session.agentId)) { + if ((await this.getAgentType(session.agentId)) === 'acp') { providerId = 'acp' } } @@ -1041,8 +1058,7 @@ export class NewAgentPresenter { throw new Error('setSessionModel requires providerId and modelId.') } - const acpAgents = await this.configPresenter.getAcpAgents() - if (session.agentId !== 'deepchat' && acpAgents.some((item) => item.id === session.agentId)) { + if ((await this.getAgentType(session.agentId)) === 'acp') { throw new Error('ACP session model is locked.') } @@ -1141,12 +1157,13 @@ export class NewAgentPresenter { const titleMessages = this.buildTitleMessages(records) if (titleMessages.length === 0) return - const assistantModel = this.configPresenter.getSetting<{ - providerId: string - modelId: string - }>('assistantModel') - const preferredProviderId = assistantModel?.providerId || fallbackProviderId - const preferredModelId = assistantModel?.modelId || fallbackModelId + const assistantSelection = await this.resolveAssistantModelSelection( + currentSession.agentId, + fallbackProviderId, + fallbackModelId + ) + const preferredProviderId = assistantSelection.providerId + const preferredModelId = assistantSelection.modelId let generatedTitle: string try { @@ -1242,9 +1259,8 @@ export class NewAgentPresenter { return this.agentRegistry.resolve(resolvedAgentId) } - const acpAgents = await this.configPresenter.getAcpAgents() - const isAcpAgent = acpAgents.some((agent) => agent.id === resolvedAgentId) - if (isAcpAgent) { + const agentType = await this.getAgentType(resolvedAgentId) + if (agentType === 'deepchat' || agentType === 'acp') { return this.agentRegistry.resolve('deepchat') } @@ -1253,20 +1269,63 @@ export class NewAgentPresenter { private async assertAcpAgent(agentId: string): Promise { const resolvedAgentId = resolveAcpAgentAlias(agentId) - const acpAgents = await this.configPresenter.getAcpAgents() - if (!acpAgents.some((agent) => agent.id === resolvedAgentId)) { + if ((await this.getAgentType(resolvedAgentId)) !== 'acp') { throw new Error(`Agent ${agentId} is not an ACP agent.`) } } + private async getAgentType(agentId: string): Promise<'deepchat' | 'acp' | null> { + return await this.configPresenter.getAgentType(resolveAcpAgentAlias(agentId)) + } + + private async resolveAssistantModelSelection( + agentId: string, + fallbackProviderId: string, + fallbackModelId: string + ): Promise<{ providerId: string; modelId: string }> { + if ((await this.getAgentType(agentId)) === 'deepchat') { + const config = await this.configPresenter.resolveDeepChatAgentConfig(agentId) + const providerId = config.assistantModel?.providerId?.trim() + const modelId = config.assistantModel?.modelId?.trim() + if (providerId && modelId) { + return { + providerId, + modelId + } + } + } + + return { + providerId: fallbackProviderId, + modelId: fallbackModelId + } + } + + private mergeDeepChatDefaultGenerationSettings( + config: Awaited> | null, + overrides?: Partial + ): Partial | undefined { + const defaults: Partial = {} + + if (typeof config?.systemPrompt === 'string') { + defaults.systemPrompt = config.systemPrompt + } + + const merged = { + ...defaults, + ...overrides + } + + return Object.keys(merged).length > 0 ? merged : undefined + } + private async isAcpBackedSession(sessionId: string, agentId: string): Promise { const resolvedAgentId = resolveAcpAgentAlias(agentId) const agent = await this.resolveAgentImplementation(agentId) const state = await agent.getSessionState(sessionId) let providerId = state?.providerId ?? '' if (!providerId) { - const acpAgents = await this.configPresenter.getAcpAgents() - if (acpAgents.some((item) => item.id === resolvedAgentId)) { + if ((await this.getAgentType(resolvedAgentId)) === 'acp') { providerId = 'acp' } } @@ -1407,14 +1466,15 @@ export class NewAgentPresenter { this.sessionManager.delete(sessionId) } - private buildExportConversation( + private async buildExportConversation( session: SessionRecord, providerId: string, modelId: string, generationSettings: SessionGenerationSettings | null - ): CONVERSATION { - const resolvedProviderId = providerId || (session.agentId !== 'deepchat' ? 'acp' : '') - const resolvedModelId = modelId || (session.agentId !== 'deepchat' ? session.agentId : '') + ): Promise { + const isAcpAgent = (await this.getAgentType(session.agentId)) === 'acp' + const resolvedProviderId = providerId || (isAcpAgent ? 'acp' : '') + const resolvedModelId = modelId || (isAcpAgent ? session.agentId : '') const modelConfig = resolvedProviderId && resolvedModelId ? this.configPresenter.getModelConfig(resolvedModelId, resolvedProviderId) diff --git a/src/main/presenter/sqlitePresenter/index.ts b/src/main/presenter/sqlitePresenter/index.ts index 3591c8b14..1d41056a4 100644 --- a/src/main/presenter/sqlitePresenter/index.ts +++ b/src/main/presenter/sqlitePresenter/index.ts @@ -23,6 +23,7 @@ import { DeepChatMessageSearchResultsTable } from './tables/deepchatMessageSearc import { DeepChatPendingInputsTable } from './tables/deepchatPendingInputs' import { DeepChatUsageStatsTable } from './tables/deepchatUsageStats' import { LegacyImportStatusTable } from './tables/legacyImportStatus' +import { AgentsTable } from './tables/agents' /** * 导入模式枚举 @@ -48,6 +49,7 @@ export class SQLitePresenter implements ISQLitePresenter { public deepchatPendingInputsTable!: DeepChatPendingInputsTable public deepchatUsageStatsTable!: DeepChatUsageStatsTable public legacyImportStatusTable!: LegacyImportStatusTable + public agentsTable!: AgentsTable private currentVersion: number = 0 private dbPath: string private password?: string @@ -168,6 +170,7 @@ export class SQLitePresenter implements ISQLitePresenter { this.deepchatPendingInputsTable = new DeepChatPendingInputsTable(this.db) this.deepchatUsageStatsTable = new DeepChatUsageStatsTable(this.db) this.legacyImportStatusTable = new LegacyImportStatusTable(this.db) + this.agentsTable = new AgentsTable(this.db) // Create only active tables for the new stack. this.acpSessionsTable.createTable() @@ -181,6 +184,7 @@ export class SQLitePresenter implements ISQLitePresenter { this.deepchatPendingInputsTable.createTable() this.deepchatUsageStatsTable.createTable() this.legacyImportStatusTable.createTable() + this.agentsTable.createTable() } private initVersionTable() { @@ -212,7 +216,8 @@ export class SQLitePresenter implements ISQLitePresenter { this.deepchatMessageSearchResultsTable, this.deepchatPendingInputsTable, this.deepchatUsageStatsTable, - this.legacyImportStatusTable + this.legacyImportStatusTable, + this.agentsTable ] // 获取最新的迁移版本 diff --git a/src/main/presenter/sqlitePresenter/tables/agents.ts b/src/main/presenter/sqlitePresenter/tables/agents.ts new file mode 100644 index 000000000..d18153a4e --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/agents.ts @@ -0,0 +1,262 @@ +import Database from 'better-sqlite3-multiple-ciphers' +import { BaseTable } from './baseTable' + +export interface AgentRow { + id: string + agent_type: 'deepchat' | 'acp' + source: 'builtin' | 'manual' | 'registry' + name: string + enabled: number + protected: number + description: string | null + icon: string | null + avatar_json: string | null + config_json: string | null + state_json: string | null + created_at: number + updated_at: number +} + +type AgentCreateInput = { + id: string + agentType: AgentRow['agent_type'] + source: AgentRow['source'] + name: string + enabled?: boolean + protected?: boolean + description?: string | null + icon?: string | null + avatarJson?: string | null + configJson?: string | null + stateJson?: string | null + createdAt?: number + updatedAt?: number +} + +type AgentUpdateInput = Partial<{ + name: string + enabled: boolean + protected: boolean + description: string | null + icon: string | null + avatarJson: string | null + configJson: string | null + stateJson: string | null +}> + +export class AgentsTable extends BaseTable { + constructor(db: Database.Database) { + super(db, 'agents') + } + + getCreateTableSQL(): string { + return ` + CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + agent_type TEXT NOT NULL, + source TEXT NOT NULL, + name TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + protected INTEGER NOT NULL DEFAULT 0, + description TEXT, + icon TEXT, + avatar_json TEXT, + config_json TEXT, + state_json TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_agents_type ON agents(agent_type); + CREATE INDEX IF NOT EXISTS idx_agents_enabled ON agents(enabled); + ` + } + + getMigrationSQL(version: number): string | null { + if (version === 20) { + return this.getCreateTableSQL() + } + return null + } + + getLatestVersion(): number { + return 20 + } + + create(input: AgentCreateInput): void { + const now = Date.now() + const createdAt = input.createdAt ?? now + const updatedAt = input.updatedAt ?? createdAt + this.db + .prepare( + `INSERT INTO agents ( + id, + agent_type, + source, + name, + enabled, + protected, + description, + icon, + avatar_json, + config_json, + state_json, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + input.id, + input.agentType, + input.source, + input.name, + input.enabled === false ? 0 : 1, + input.protected ? 1 : 0, + input.description ?? null, + input.icon ?? null, + input.avatarJson ?? null, + input.configJson ?? null, + input.stateJson ?? null, + createdAt, + updatedAt + ) + } + + upsert(input: AgentCreateInput): void { + const now = Date.now() + const createdAt = input.createdAt ?? now + const updatedAt = input.updatedAt ?? now + this.db + .prepare( + `INSERT INTO agents ( + id, + agent_type, + source, + name, + enabled, + protected, + description, + icon, + avatar_json, + config_json, + state_json, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + agent_type = excluded.agent_type, + source = excluded.source, + name = excluded.name, + enabled = excluded.enabled, + protected = excluded.protected, + description = excluded.description, + icon = excluded.icon, + avatar_json = excluded.avatar_json, + config_json = excluded.config_json, + state_json = excluded.state_json, + updated_at = excluded.updated_at` + ) + .run( + input.id, + input.agentType, + input.source, + input.name, + input.enabled === false ? 0 : 1, + input.protected ? 1 : 0, + input.description ?? null, + input.icon ?? null, + input.avatarJson ?? null, + input.configJson ?? null, + input.stateJson ?? null, + createdAt, + updatedAt + ) + } + + get(id: string): AgentRow | undefined { + return this.db.prepare('SELECT * FROM agents WHERE id = ?').get(id) as AgentRow | undefined + } + + list(filters?: { + agentType?: AgentRow['agent_type'] + source?: AgentRow['source'] + enabled?: boolean + }): AgentRow[] { + let sql = 'SELECT * FROM agents' + const conditions: string[] = [] + const params: unknown[] = [] + + if (filters?.agentType) { + conditions.push('agent_type = ?') + params.push(filters.agentType) + } + + if (filters?.source) { + conditions.push('source = ?') + params.push(filters.source) + } + + if (typeof filters?.enabled === 'boolean') { + conditions.push('enabled = ?') + params.push(filters.enabled ? 1 : 0) + } + + if (conditions.length > 0) { + sql += ` WHERE ${conditions.join(' AND ')}` + } + + sql += ' ORDER BY protected DESC, updated_at DESC, created_at ASC' + return this.db.prepare(sql).all(...params) as AgentRow[] + } + + update(id: string, input: AgentUpdateInput): void { + const updates: string[] = [] + const params: unknown[] = [] + + if (Object.prototype.hasOwnProperty.call(input, 'name')) { + updates.push('name = ?') + params.push(input.name) + } + if (Object.prototype.hasOwnProperty.call(input, 'enabled')) { + updates.push('enabled = ?') + params.push(input.enabled ? 1 : 0) + } + if (Object.prototype.hasOwnProperty.call(input, 'protected')) { + updates.push('protected = ?') + params.push(input.protected ? 1 : 0) + } + if (Object.prototype.hasOwnProperty.call(input, 'description')) { + updates.push('description = ?') + params.push(input.description ?? null) + } + if (Object.prototype.hasOwnProperty.call(input, 'icon')) { + updates.push('icon = ?') + params.push(input.icon ?? null) + } + if (Object.prototype.hasOwnProperty.call(input, 'avatarJson')) { + updates.push('avatar_json = ?') + params.push(input.avatarJson ?? null) + } + if (Object.prototype.hasOwnProperty.call(input, 'configJson')) { + updates.push('config_json = ?') + params.push(input.configJson ?? null) + } + if (Object.prototype.hasOwnProperty.call(input, 'stateJson')) { + updates.push('state_json = ?') + params.push(input.stateJson ?? null) + } + + if (updates.length === 0) { + return + } + + updates.push('updated_at = ?') + params.push(Date.now()) + params.push(id) + + this.db.prepare(`UPDATE agents SET ${updates.join(', ')} WHERE id = ?`).run(...params) + } + + delete(id: string): void { + this.db.prepare('DELETE FROM agents WHERE id = ?').run(id) + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/newSessions.ts b/src/main/presenter/sqlitePresenter/tables/newSessions.ts index 1f91bfa00..b700dd35a 100644 --- a/src/main/presenter/sqlitePresenter/tables/newSessions.ts +++ b/src/main/presenter/sqlitePresenter/tables/newSessions.ts @@ -231,6 +231,12 @@ export class NewSessionsTable extends BaseTable { this.update(id, { disabled_agent_tools: JSON.stringify(disabledAgentTools) }) } + reassignAgentId(fromAgentId: string, toAgentId: string): void { + this.db + .prepare('UPDATE new_sessions SET agent_id = ?, updated_at = ? WHERE agent_id = ?') + .run(toAgentId, Date.now(), fromAgentId) + } + private parseActiveSkills(raw: string | null | undefined): string[] { return this.parseStringArray(raw) } diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index 6b1ff811b..94532ad1d 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -13,13 +13,24 @@ import iconWin from '../../../../resources/icon.ico?asset' // App icon (Windows) import { is } from '@electron-toolkit/utils' // Electron utilities import { IConfigPresenter, IWindowPresenter } from '@shared/presenter' // Window Presenter interface import { eventBus } from '@/eventbus' // Event bus -import { CONFIG_EVENTS, SHORTCUT_EVENTS, SYSTEM_EVENTS, WINDOW_EVENTS } from '@/events' // System/Window/Config/Shortcut event constants +import { + CONFIG_EVENTS, + SETTINGS_EVENTS, + SHORTCUT_EVENTS, + SYSTEM_EVENTS, + WINDOW_EVENTS +} from '@/events' // System/Window/Config/Shortcut event constants import { presenter } from '../' // Global presenter registry import windowStateManager from 'electron-window-state' // Window state manager // TrayPresenter is globally managed in main/index.ts, this Presenter is not responsible for its lifecycle import { TabPresenter } from '../tabPresenter' // TabPresenter type import { FloatingChatWindow } from './FloatingChatWindow' // Floating chat window +type PendingSettingsMessage = { + channel: string + args: unknown[] +} + /** * Window Presenter, responsible for managing all BrowserWindow instances and their lifecycles. * Including creation, destruction, minimization, maximization, hiding, showing, focus management, and interaction with tabs. @@ -36,6 +47,8 @@ export class WindowPresenter implements IWindowPresenter { private mainWindowId: number | null = null private floatingChatWindow: FloatingChatWindow | null = null private settingsWindow: BrowserWindow | null = null + private settingsWindowReady = false + private pendingSettingsMessages: PendingSettingsMessage[] = [] constructor(configPresenter: IConfigPresenter) { this.windows = new Map() @@ -95,6 +108,10 @@ export class WindowPresenter implements IWindowPresenter { } }) + ipcMain.on(SETTINGS_EVENTS.READY, (event) => { + this.handleSettingsWindowReady(event.sender.id) + }) + // 监听系统主题更新事件,通知所有窗口 Renderer eventBus.on(SYSTEM_EVENTS.SYSTEM_THEME_UPDATED, (isDark: boolean) => { console.log('System theme updated, notifying all windows.') @@ -426,6 +443,10 @@ export class WindowPresenter implements IWindowPresenter { !this.settingsWindow.isDestroyed() && this.settingsWindow.id === windowId ) { + if (this.shouldQueueSettingsMessage(channel)) { + this.pendingSettingsMessages.push({ channel, args }) + return true + } try { this.settingsWindow.webContents.send(channel, ...args) return true @@ -1220,6 +1241,7 @@ export class WindowPresenter implements IWindowPresenter { } this.settingsWindow = settingsWindow + this.resetSettingsWindowState() const windowId = settingsWindow.id // Manage window state to track position and size changes @@ -1256,11 +1278,18 @@ export class WindowPresenter implements IWindowPresenter { } }) + settingsWindow.webContents.on('did-start-loading', () => { + if (this.settingsWindow?.id === windowId) { + this.settingsWindowReady = false + } + }) + settingsWindow.on('closed', () => { console.log(`Settings window ${windowId} closed.`) // Unmanage window state when window is closed settingsWindowState.unmanage() this.settingsWindow = null + this.resetSettingsWindowState(true) }) // Load settings renderer HTML @@ -1318,6 +1347,53 @@ export class WindowPresenter implements IWindowPresenter { return this.settingsWindow !== null && !this.settingsWindow.isDestroyed() } + private shouldQueueSettingsMessage(channel: string): boolean { + return channel.startsWith('settings:') && !this.settingsWindowReady + } + + private handleSettingsWindowReady(senderWebContentsId: number): void { + if ( + !this.settingsWindow || + this.settingsWindow.isDestroyed() || + this.settingsWindow.webContents.isDestroyed() || + this.settingsWindow.webContents.id !== senderWebContentsId + ) { + return + } + + this.settingsWindowReady = true + this.flushPendingSettingsMessages() + } + + private flushPendingSettingsMessages(): void { + if ( + !this.settingsWindow || + this.settingsWindow.isDestroyed() || + this.settingsWindow.webContents.isDestroyed() || + !this.settingsWindowReady || + this.pendingSettingsMessages.length === 0 + ) { + return + } + + const pending = [...this.pendingSettingsMessages] + this.pendingSettingsMessages = [] + pending.forEach(({ channel, args }) => { + try { + this.settingsWindow?.webContents.send(channel, ...args) + } catch (error) { + console.error(`Error flushing settings message "${channel}":`, error) + } + }) + } + + private resetSettingsWindowState(clearQueue = false): void { + this.settingsWindowReady = false + if (clearQueue) { + this.pendingSettingsMessages = [] + } + } + public isApplicationQuitting(): boolean { return this.isQuitting } diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index 4988aca60..4ccc31704 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -124,6 +124,10 @@ const handleSettingsNavigate = async ( if (window?.electron?.ipcRenderer) { window.electron.ipcRenderer.on(SETTINGS_EVENTS.NAVIGATE, handleSettingsNavigate) } + +const notifySettingsReady = () => { + window.electron?.ipcRenderer?.send(SETTINGS_EVENTS.READY) +} const settings: Ref< { title: string @@ -285,6 +289,7 @@ onMounted(async () => { // Wait for router to be ready await router.isReady() + notifySettingsReady() // Check for pending MCP install from localStorage try { diff --git a/src/renderer/settings/components/CommonSettings.vue b/src/renderer/settings/components/CommonSettings.vue index bb4a8d8ca..46510f768 100644 --- a/src/renderer/settings/components/CommonSettings.vue +++ b/src/renderer/settings/components/CommonSettings.vue @@ -2,8 +2,6 @@
- - +
+ + +
+
+
+
+
+ +
+
+
+ {{ + form.id + ? t('settings.deepchatAgents.editTitle') + : t('settings.deepchatAgents.createTitle') + }} +
+
+ {{ form.name.trim() || t('settings.deepchatAgents.unnamed') }} +
+
+
+ +
+ + + +
+
+ +
+ + +