mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 05:00:46 +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
|
|
@ -40,6 +40,8 @@
|
|||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@qwen-code/channel-base": "file:../channels/base",
|
||||
"@qwen-code/channel-telegram": "file:../channels/telegram",
|
||||
"@qwen-code/qwen-code-core": "file:../core",
|
||||
"@qwen-code/web-templates": "file:../web-templates",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
|
|
|
|||
13
packages/cli/src/commands/channel.ts
Normal file
13
packages/cli/src/commands/channel.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { CommandModule, Argv } from 'yargs';
|
||||
import { startCommand } from './channel/start.js';
|
||||
|
||||
export const channelCommand: CommandModule = {
|
||||
command: 'channel',
|
||||
describe: 'Manage messaging channels (Telegram, Discord, etc.)',
|
||||
builder: (yargs: Argv) =>
|
||||
yargs
|
||||
.command(startCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {},
|
||||
};
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
|
@ -51,6 +51,7 @@ import { getCliVersion } from '../utils/version.js';
|
|||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { appEvents } from '../utils/events.js';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { channelCommand } from '../commands/channel.js';
|
||||
|
||||
// UUID v4 regex pattern for validation
|
||||
const SESSION_ID_REGEX =
|
||||
|
|
@ -590,7 +591,9 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||
// Register Auth subcommands
|
||||
.command(authCommand)
|
||||
// Register Hooks subcommands
|
||||
.command(hooksCommand);
|
||||
.command(hooksCommand)
|
||||
// Register Channel subcommands
|
||||
.command(channelCommand);
|
||||
|
||||
yargsInstance
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
|
|
@ -611,7 +614,8 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||
result._.length > 0 &&
|
||||
(result._[0] === 'mcp' ||
|
||||
result._[0] === 'extensions' ||
|
||||
result._[0] === 'hooks')
|
||||
result._[0] === 'hooks' ||
|
||||
result._[0] === 'channel')
|
||||
) {
|
||||
// MCP/Extensions/Hooks commands handle their own execution and process exit
|
||||
process.exit(0);
|
||||
|
|
|
|||
|
|
@ -189,6 +189,18 @@ const SETTINGS_SCHEMA = {
|
|||
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
|
||||
},
|
||||
|
||||
// Channels configuration (Telegram, Discord, etc.)
|
||||
channels: {
|
||||
type: 'object',
|
||||
label: 'Channels',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {} as Record<string, Record<string, unknown>>,
|
||||
description: 'Configuration for messaging channels.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
|
||||
},
|
||||
|
||||
// Model providers configuration grouped by authType
|
||||
modelProviders: {
|
||||
type: 'object',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue