qwen-code/packages/cli/src/commands/channel/start.ts
tanzhenxin 3eedc43238 feat(channels): add Telegram channel integration with ACP bridge
Implements the channels infrastructure for connecting external messaging
platforms to Qwen Code via ACP. Phase 1 supports plain text round-trip:
Telegram user sends message -> AcpBridge -> qwen-code --acp -> response
back to Telegram.

New packages:
- @qwen-code/channel-base: AcpBridge, SessionRouter, SenderGate, ChannelBase
- @qwen-code/channel-telegram: TelegramAdapter using telegraf

CLI: `qwen channel start <name>` reads from settings.json channels config,
spawns ACP agent, connects to Telegram via polling.
2026-03-24 06:33:36 +00:00

107 lines
3.4 KiB
TypeScript

import type { CommandModule } from 'yargs';
import { loadSettings } from '../../config/settings.js';
import { writeStderrLine, writeStdoutLine } from '../../utils/stdioHelpers.js';
import { AcpBridge } from '@qwen-code/channel-base';
import type { ChannelConfig } from '@qwen-code/channel-base';
import { TelegramChannel } from '@qwen-code/channel-telegram';
import * as path from 'node:path';
function resolveEnvVars(value: string): string {
if (value.startsWith('$')) {
const envName = value.substring(1);
const envValue = process.env[envName];
if (!envValue) {
throw new Error(
`Environment variable ${envName} is not set (referenced as ${value})`,
);
}
return envValue;
}
return value;
}
function findCliEntryPath(): string {
// When running from bundled dist/cli.js, use that same file for --acp
const mainModule = process.argv[1];
if (mainModule) {
return path.resolve(mainModule);
}
throw new Error('Cannot determine CLI entry path');
}
export const startCommand: CommandModule<object, { name: string }> = {
command: 'start <name>',
describe: 'Start a messaging channel',
builder: (yargs) =>
yargs.positional('name', {
type: 'string',
describe: 'Name of the channel (as configured in settings.json)',
demandOption: true,
}),
handler: async (argv) => {
const { name } = argv;
const settings = loadSettings(process.cwd());
const channels = (
settings.merged as unknown as { channels?: Record<string, unknown> }
).channels;
if (!channels || !channels[name]) {
writeStderrLine(
`Error: Channel "${name}" not found in settings. Add it to channels.${name} in settings.json.`,
);
process.exit(1);
}
const rawConfig = channels[name] as Record<string, unknown>;
const config: ChannelConfig = {
type: rawConfig['type'] as ChannelConfig['type'],
token: resolveEnvVars(rawConfig['token'] as string),
senderPolicy:
(rawConfig['senderPolicy'] as ChannelConfig['senderPolicy']) ||
'allowlist',
allowedUsers: (rawConfig['allowedUsers'] as string[]) || [],
sessionScope:
(rawConfig['sessionScope'] as ChannelConfig['sessionScope']) || 'user',
cwd: (rawConfig['cwd'] as string) || process.cwd(),
approvalMode: rawConfig['approvalMode'] 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();
writeStdoutLine(`[Channel] CLI entry: ${cliEntryPath}`);
writeStdoutLine(`[Channel] Starting "${name}" (type=${config.type})...`);
const bridge = new AcpBridge({ cliEntryPath, cwd: config.cwd });
await bridge.start();
const channel = new TelegramChannel(name, config, bridge);
await channel.connect();
writeStdoutLine(`[Channel] "${name}" is running. Press Ctrl+C to stop.`);
// Keep process alive until interrupted
await new Promise<void>((resolve) => {
process.on('SIGINT', () => {
writeStdoutLine('\n[Channel] Shutting down...');
channel.disconnect();
bridge.stop();
resolve();
});
process.on('SIGTERM', () => {
channel.disconnect();
bridge.stop();
resolve();
});
});
process.exit(0);
},
};