From 0542c82fe60d5a5b5786ef99fc81c77230b2d83e Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:50:15 +0800 Subject: [PATCH] feat(channels): add credential write and per-account lifecycle control Add setChannelAccountConfig/removeChannelAccountConfig to CredentialManager for persisting channel tokens. Make ChannelManager.startAccount public and add stopAccount for individual account lifecycle control via IPC. Co-Authored-By: Claude Opus 4.6 --- src/agent/credentials.ts | 69 ++++++++++++++++++++++++++++++++++++++++ src/channels/manager.ts | 22 +++++++++++-- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/agent/credentials.ts b/src/agent/credentials.ts index 5abeca35..aabf3c6b 100644 --- a/src/agent/credentials.ts +++ b/src/agent/credentials.ts @@ -330,6 +330,75 @@ export class CredentialManager { this.reset(); } + /** + * Set a channel account config and save to credentials.json5. + * Creates the channels section if it doesn't exist. + * Used by the desktop Channels page to persist bot tokens. + */ + setChannelAccountConfig(channelId: string, accountId: string, accountConfig: Record): void { + const path = getCredentialsPath(); + + let config: CredentialsConfig = { version: 1 }; + if (existsSync(path)) { + try { + const raw = readFileSync(path, "utf8"); + config = JSON5.parse(raw) as CredentialsConfig; + } catch { + config = { version: 1 }; + } + } + + // Ensure channels.[channelId] structure exists + if (!config.channels) { + config.channels = {}; + } + if (!config.channels[channelId]) { + config.channels[channelId] = {}; + } + + // Set or update the account config + config.channels[channelId]![accountId] = accountConfig; + + mkdirSync(dirname(path), { recursive: true }); + const content = JSON.stringify(config, null, 2); + writeFileSync(path, content, "utf8"); + + this.reset(); + } + + /** + * Remove a channel account config from credentials.json5. + * Cleans up the parent channel section if no accounts remain. + */ + removeChannelAccountConfig(channelId: string, accountId: string): void { + const path = getCredentialsPath(); + if (!existsSync(path)) return; + + let config: CredentialsConfig; + try { + const raw = readFileSync(path, "utf8"); + config = JSON5.parse(raw) as CredentialsConfig; + } catch { + return; + } + + const channelSection = config.channels?.[channelId]; + if (!channelSection) return; + + delete channelSection[accountId]; + + // Clean up empty channel section + if (Object.keys(channelSection).length === 0) { + delete config.channels![channelId]; + } + + mkdirSync(dirname(path), { recursive: true }); + const content = JSON.stringify(config, null, 2); + writeFileSync(path, content, "utf8"); + + this.reset(); + } + /** * Set the default LLM provider and save to credentials.json5. */ diff --git a/src/channels/manager.ts b/src/channels/manager.ts index 213b6dc8..fb094065 100644 --- a/src/channels/manager.ts +++ b/src/channels/manager.ts @@ -98,8 +98,11 @@ export class ChannelManager { this.ensureSubscribed(); } - /** Start a specific channel account */ - private async startAccount( + /** + * Start a specific channel account. + * Public so the desktop IPC layer can call it after saving config. + */ + async startAccount( channelId: string, accountId: string, accountConfig: Record, @@ -433,6 +436,21 @@ export class ChannelManager { } } + /** + * Stop a specific channel account. + * Public so the desktop IPC layer can call it when removing config. + */ + stopAccount(channelId: string, accountId: string): void { + const key = `${channelId}:${accountId}`; + const handle = this.accounts.get(key); + if (!handle) return; + + handle.abortController.abort(); + handle.state = { ...handle.state, status: "stopped" }; + this.accounts.delete(key); + console.log(`[Channels] Stopped ${key}`); + } + /** Stop all running channel accounts */ stopAll(): void { console.log("[Channels] Stopping all channels...");