From 615ccd08f2b9b8d3581769e90845e8dc5d9d1ee1 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 24 Mar 2026 06:20:16 +0000 Subject: [PATCH] feat(channels): add config validation, instructions, and sessionScope support - Validate required fields (type, token) with clear error messages - Prepend channel instructions to first prompt of each session - SessionRouter respects sessionScope (user/thread/single) for routing keys --- packages/channels/base/src/ChannelBase.ts | 12 +++++- packages/channels/base/src/SessionRouter.ts | 25 ++++++++++-- packages/cli/src/commands/channel/start.ts | 44 ++++++++++++++++----- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/channels/base/src/ChannelBase.ts b/packages/channels/base/src/ChannelBase.ts index 2ffdb4c5f..89b875264 100644 --- a/packages/channels/base/src/ChannelBase.ts +++ b/packages/channels/base/src/ChannelBase.ts @@ -9,13 +9,14 @@ export abstract class ChannelBase { protected gate: SenderGate; protected router: SessionRouter; protected name: string; + private instructedSessions: Set = new Set(); constructor(name: string, config: ChannelConfig, bridge: AcpBridge) { this.name = name; this.config = config; this.bridge = bridge; this.gate = new SenderGate(config.senderPolicy, config.allowedUsers); - this.router = new SessionRouter(bridge, config.cwd); + this.router = new SessionRouter(bridge, config.cwd, config.sessionScope); } abstract connect(): Promise; @@ -37,12 +38,19 @@ export abstract class ChannelBase { envelope.threadId, ); + // Prepend channel instructions on first message of a session + let promptText = envelope.text; + if (this.config.instructions && !this.instructedSessions.has(sessionId)) { + promptText = `${this.config.instructions}\n\n${envelope.text}`; + this.instructedSessions.add(sessionId); + } + console.log( `[Channel:${this.name}] Prompting session ${sessionId}: "${envelope.text.substring(0, 80)}"`, ); console.log(`[Channel:${this.name}] Waiting for prompt response...`); - const response = await this.bridge.prompt(sessionId, envelope.text); + const response = await this.bridge.prompt(sessionId, promptText); console.log( `[Channel:${this.name}] Got response (${response.length} chars): "${response.substring(0, 100)}"`, ); diff --git a/packages/channels/base/src/SessionRouter.ts b/packages/channels/base/src/SessionRouter.ts index 4097a1870..302879f65 100644 --- a/packages/channels/base/src/SessionRouter.ts +++ b/packages/channels/base/src/SessionRouter.ts @@ -1,4 +1,4 @@ -import type { SessionTarget } from './types.js'; +import type { SessionScope, SessionTarget } from './types.js'; import type { AcpBridge } from './AcpBridge.js'; export class SessionRouter { @@ -7,10 +7,29 @@ export class SessionRouter { private bridge: AcpBridge; private cwd: string; + private scope: SessionScope; - constructor(bridge: AcpBridge, cwd: string) { + constructor(bridge: AcpBridge, cwd: string, scope: SessionScope = 'user') { this.bridge = bridge; this.cwd = cwd; + this.scope = scope; + } + + private routingKey( + channelName: string, + senderId: string, + chatId: string, + threadId?: string, + ): string { + switch (this.scope) { + case 'thread': + return `${channelName}:${threadId || chatId}`; + case 'single': + return `${channelName}:__single__`; + case 'user': + default: + return `${channelName}:${senderId}`; + } } async resolve( @@ -19,7 +38,7 @@ export class SessionRouter { chatId: string, threadId?: string, ): Promise { - const key = `${channelName}:${senderId}`; + const key = this.routingKey(channelName, senderId, chatId, threadId); const existing = this.toSession.get(key); if (existing) { return existing; diff --git a/packages/cli/src/commands/channel/start.ts b/packages/cli/src/commands/channel/start.ts index d1ac96a35..a26d07ff7 100644 --- a/packages/cli/src/commands/channel/start.ts +++ b/packages/cli/src/commands/channel/start.ts @@ -54,9 +54,42 @@ export const startCommand: CommandModule = { } const rawConfig = channels[name] as Record; + + // Validate required fields + if (!rawConfig['type']) { + writeStderrLine( + `Error: Channel "${name}" is missing required field "type".`, + ); + process.exit(1); + } + if (!rawConfig['token']) { + writeStderrLine( + `Error: Channel "${name}" is missing required field "token".`, + ); + process.exit(1); + } + + const channelType = rawConfig['type'] as string; + if (channelType !== 'telegram') { + writeStderrLine( + `Error: Channel type "${channelType}" is not yet supported. Only "telegram" is available.`, + ); + process.exit(1); + } + + let token: string; + try { + token = resolveEnvVars(rawConfig['token'] as string); + } catch (err) { + writeStderrLine( + `Error: ${err instanceof Error ? err.message : String(err)}`, + ); + process.exit(1); + } + const config: ChannelConfig = { - type: rawConfig['type'] as ChannelConfig['type'], - token: resolveEnvVars(rawConfig['token'] as string), + type: channelType as ChannelConfig['type'], + token, senderPolicy: (rawConfig['senderPolicy'] as ChannelConfig['senderPolicy']) || 'allowlist', @@ -68,13 +101,6 @@ export const startCommand: CommandModule = { instructions: rawConfig['instructions'] as string | undefined, }; - if (config.type !== 'telegram') { - writeStderrLine( - `Error: Channel type "${config.type}" is not yet supported. Only "telegram" is available.`, - ); - process.exit(1); - } - const cliEntryPath = findCliEntryPath(); writeStdoutLine(`[Channel] CLI entry: ${cliEntryPath}`); writeStdoutLine(`[Channel] Starting "${name}" (type=${config.type})...`);