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 gate: SenderGate;
protected router: SessionRouter; protected router: SessionRouter;
protected name: string; protected name: string;
private instructedSessions: Set<string> = new Set();
constructor(name: string, config: ChannelConfig, bridge: AcpBridge) { constructor(name: string, config: ChannelConfig, bridge: AcpBridge) {
this.name = name; this.name = name;
this.config = config; this.config = config;
this.bridge = bridge; this.bridge = bridge;
this.gate = new SenderGate(config.senderPolicy, config.allowedUsers); 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>; abstract connect(): Promise<void>;
@ -37,12 +38,19 @@ export abstract class ChannelBase {
envelope.threadId, 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( console.log(
`[Channel:${this.name}] Prompting session ${sessionId}: "${envelope.text.substring(0, 80)}"`, `[Channel:${this.name}] Prompting session ${sessionId}: "${envelope.text.substring(0, 80)}"`,
); );
console.log(`[Channel:${this.name}] Waiting for prompt response...`); 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( console.log(
`[Channel:${this.name}] Got response (${response.length} chars): "${response.substring(0, 100)}"`, `[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'; import type { AcpBridge } from './AcpBridge.js';
export class SessionRouter { export class SessionRouter {
@ -7,10 +7,29 @@ export class SessionRouter {
private bridge: AcpBridge; private bridge: AcpBridge;
private cwd: string; private cwd: string;
private scope: SessionScope;
constructor(bridge: AcpBridge, cwd: string) { constructor(bridge: AcpBridge, cwd: string, scope: SessionScope = 'user') {
this.bridge = bridge; this.bridge = bridge;
this.cwd = cwd; 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( async resolve(
@ -19,7 +38,7 @@ export class SessionRouter {
chatId: string, chatId: string,
threadId?: string, threadId?: string,
): Promise<string> { ): Promise<string> {
const key = `${channelName}:${senderId}`; const key = this.routingKey(channelName, senderId, chatId, threadId);
const existing = this.toSession.get(key); const existing = this.toSession.get(key);
if (existing) { if (existing) {
return existing; return existing;

View file

@ -54,9 +54,42 @@ export const startCommand: CommandModule<object, { name: string }> = {
} }
const rawConfig = channels[name] as Record<string, unknown>; 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 = { const config: ChannelConfig = {
type: rawConfig['type'] as ChannelConfig['type'], type: channelType as ChannelConfig['type'],
token: resolveEnvVars(rawConfig['token'] as string), token,
senderPolicy: senderPolicy:
(rawConfig['senderPolicy'] as ChannelConfig['senderPolicy']) || (rawConfig['senderPolicy'] as ChannelConfig['senderPolicy']) ||
'allowlist', 'allowlist',
@ -68,13 +101,6 @@ export const startCommand: CommandModule<object, { name: string }> = {
instructions: rawConfig['instructions'] as string | undefined, 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(); const cliEntryPath = findCliEntryPath();
writeStdoutLine(`[Channel] CLI entry: ${cliEntryPath}`); writeStdoutLine(`[Channel] CLI entry: ${cliEntryPath}`);
writeStdoutLine(`[Channel] Starting "${name}" (type=${config.type})...`); writeStdoutLine(`[Channel] Starting "${name}" (type=${config.type})...`);