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 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-09 12:50:15 +08:00
parent 6a02fd29be
commit 0542c82fe6
2 changed files with 89 additions and 2 deletions

View file

@ -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<string, unknown>): 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.
*/

View file

@ -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<string, unknown>,
@ -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...");