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
This commit is contained in:
tanzhenxin 2026-03-24 06:20:16 +00:00
parent 2985201317
commit 615ccd08f2
3 changed files with 67 additions and 14 deletions

View file

@ -9,13 +9,14 @@ export abstract class ChannelBase {
protected gate: SenderGate;
protected router: SessionRouter;
protected name: string;
private instructedSessions: Set<string> = 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<void>;
@ -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)}"`,
);

View file

@ -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<string> {
const key = `${channelName}:${senderId}`;
const key = this.routingKey(channelName, senderId, chatId, threadId);
const existing = this.toSession.get(key);
if (existing) {
return existing;

View file

@ -54,9 +54,42 @@ export const startCommand: CommandModule<object, { name: string }> = {
}
const rawConfig = channels[name] as Record<string, unknown>;
// 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<object, { name: string }> = {
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})...`);