mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 20:50:34 +00:00
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.
This commit is contained in:
parent
aebe889b31
commit
3eedc43238
18 changed files with 736 additions and 4 deletions
107
packages/cli/src/commands/channel/start.ts
Normal file
107
packages/cli/src/commands/channel/start.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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);
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue