diff --git a/apps/cli/src/http/index.ts b/apps/cli/src/http/index.ts index 7a87fbfa..995421b9 100644 --- a/apps/cli/src/http/index.ts +++ b/apps/cli/src/http/index.ts @@ -32,7 +32,7 @@ import { getAssistantManager, getSkillManagerCore } from '../ai/manager-factory' import { createCliRunAgentStream } from '../ai/agent-stream-runner' import { initSync, cleanupSync } from '../sync' import { resolveCliPath } from '../paths' -import { resolveApiKey, writeAuthProfile } from '@openchatlab/config' +import { resolveApiKey, writeAuthProfile, deleteAuthProfile } from '@openchatlab/config' import { assertCliDataDirCompatible } from '../runtime-compat' let server: FastifyInstance | null = null @@ -164,6 +164,10 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{ writeAuthProfile(profileName, { type: 'api_key', provider: config.provider, key: apiKey }) return profileName }, + onApiKeyDeleted: (config) => { + const profileName = (config as unknown as Record).authProfile as string | undefined + if (profileName) deleteAuthProfile(profileName) + }, }) initServerAiLogger(pathProvider.getLogsDir()) diff --git a/apps/desktop/main/internal-api.ts b/apps/desktop/main/internal-api.ts index ca772a2c..6ec34597 100644 --- a/apps/desktop/main/internal-api.ts +++ b/apps/desktop/main/internal-api.ts @@ -38,7 +38,7 @@ import { serverError, } from '@openchatlab/http-routes' import type { HttpRouteContext } from '@openchatlab/http-routes' -import { resolveApiKey, writeAuthProfile } from '@openchatlab/config' +import { resolveApiKey, writeAuthProfile, deleteAuthProfile } from '@openchatlab/config' import { getManager as getAIChatManager } from './ai/chats' import { getManager as getAssistantManager } from './ai/assistant/manager' import { getManager as getSkillManager } from './ai/skills/manager' @@ -102,6 +102,10 @@ export async function startInternalServer(pathProvider: PathProvider): Promise { + const profileName = (config as unknown as Record).authProfile as string | undefined + if (profileName) deleteAuthProfile(profileName) + }, }) const configStorage = createFileConfigStorage(aiDataDir) diff --git a/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts b/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts index 4db2e054..1dcc6323 100644 --- a/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts +++ b/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts @@ -175,6 +175,31 @@ describe('LLMConfigStore', () => { assert.equal(result.config!.apiKey, '', 'returned config should not leak apiKey') }) + it('calls onApiKeyDeleted with the deleted config when deleteConfig is called', () => { + const deleted: AIServiceConfig[] = [] + const storeWithHook = new LLMConfigStore(storage, { + generateId: () => `id-${++idCounter}`, + onApiKeyCreated: (config) => { + const name = config.name.toLowerCase().replace(/\s+/g, '-') + ;(config as unknown as Record).authProfile = name + return name + }, + onApiKeyDeleted: (config) => { + deleted.push(config) + }, + }) + storeWithHook.addConfig({ name: 'My Service', provider: 'openai', apiKey: 'sk-test' }) + const id = storeWithHook.getAllConfigs()[0].id + storeWithHook.deleteConfig(id) + assert.equal(deleted.length, 1, 'onApiKeyDeleted should be called once') + assert.equal( + (deleted[0] as unknown as Record).authProfile, + 'my-service', + 'deleted config should retain authProfile for cleanup' + ) + assert.equal(storeWithHook.getAllConfigs().length, 0, 'config should be removed') + }) + it('resolves correct key for same-provider configs with different authProfiles', () => { const profiles = new Map() const storeWithAuth = new LLMConfigStore(storage, { diff --git a/packages/node-runtime/src/ai/llm-config-store.ts b/packages/node-runtime/src/ai/llm-config-store.ts index 6acf9f29..714769a8 100644 --- a/packages/node-runtime/src/ai/llm-config-store.ts +++ b/packages/node-runtime/src/ai/llm-config-store.ts @@ -44,6 +44,8 @@ export interface LLMConfigStoreDeps { generateId?: () => string /** Returns the auth profile name so LLMConfigStore can persist it on the config */ onApiKeyCreated?: (config: AIServiceConfig, apiKey: string) => string | void + /** Called after a config is removed; use to clean up the corresponding auth profile */ + onApiKeyDeleted?: (config: AIServiceConfig) => void resolveApiKey?: (provider: string, authProfile?: string) => string | undefined onStoreLoaded?: (configs: AIServiceConfig[]) => void } @@ -71,6 +73,7 @@ export class LLMConfigStore { private t: (key: string, options?: Record) => string private generateId: () => string private onApiKeyCreated?: (config: AIServiceConfig, apiKey: string) => string | void + private onApiKeyDeleted?: (config: AIServiceConfig) => void private resolveApiKey?: (provider: string, authProfile?: string) => string | undefined private onStoreLoaded?: (configs: AIServiceConfig[]) => void @@ -79,6 +82,7 @@ export class LLMConfigStore { this.t = deps.t || defaultT this.generateId = deps.generateId || defaultGenerateId this.onApiKeyCreated = deps.onApiKeyCreated + this.onApiKeyDeleted = deps.onApiKeyDeleted this.resolveApiKey = deps.resolveApiKey this.onStoreLoaded = deps.onStoreLoaded } @@ -233,6 +237,7 @@ export class LLMConfigStore { return { success: false, error: this.t('llm.configNotFound') } } + const deleted = store.configs[index] store.configs.splice(index, 1) const fallback = store.configs[0] @@ -244,6 +249,7 @@ export class LLMConfigStore { } this.saveStore(store) + this.onApiKeyDeleted?.(deleted) return { success: true } }