mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
fix(channels): apply proxy settings to channel start command (#3136)
`qwen channel start` never calls `loadCliConfig`, so the proxy configured via `--proxy` or `HTTPS_PROXY`/`HTTP_PROXY` env vars was not applied. This caused Telegram's `getMe` (and all other channel HTTP traffic) to bypass the proxy entirely. The fix has two parts: 1. Resolve proxy in `start.ts` bootstrap and call `setGlobalDispatcher(new ProxyAgent(...))` for native fetch() calls (file downloads, other channels). This mirrors the same pattern used by Config constructor in the main CLI path. 2. Thread the proxy URL through `ChannelBaseOptions` so adapters can configure their own HTTP clients. TelegramAdapter passes an `HttpsProxyAgent` to grammy's `baseFetchConfig.agent` since grammy uses node-fetch which ignores undici's global dispatcher. Fixes #3122
This commit is contained in:
parent
e216ab35fc
commit
7219469285
5 changed files with 52 additions and 8 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -16907,6 +16907,7 @@
|
|||
"dependencies": {
|
||||
"@qwen-code/channel-base": "file:../base",
|
||||
"grammy": "^1.41.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"telegram-markdown-formatter": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { AcpBridge, ToolCallEvent } from './AcpBridge.js';
|
|||
|
||||
export interface ChannelBaseOptions {
|
||||
router?: SessionRouter;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
/** Handler for a slash command. Return true if handled, false to forward to agent. */
|
||||
|
|
@ -20,6 +21,8 @@ export abstract class ChannelBase {
|
|||
protected gate: SenderGate;
|
||||
protected router: SessionRouter;
|
||||
protected name: string;
|
||||
/** Resolved proxy URL, available to subclasses for adapter-specific clients. */
|
||||
protected proxy?: string;
|
||||
private instructedSessions: Set<string> = new Set();
|
||||
private commands: Map<string, CommandHandler> = new Map();
|
||||
/** Per-session promise chain to serialize prompt + send (followup mode). */
|
||||
|
|
@ -45,6 +48,7 @@ export abstract class ChannelBase {
|
|||
this.name = name;
|
||||
this.config = config;
|
||||
this.bridge = bridge;
|
||||
this.proxy = options?.proxy;
|
||||
|
||||
this.groupGate = new GroupGate(config.groupPolicy, config.groups);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"dependencies": {
|
||||
"@qwen-code/channel-base": "file:../base",
|
||||
"grammy": "^1.41.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"telegram-markdown-formatter": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|||
import { basename, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { Bot } from 'grammy';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import {
|
||||
telegramFormat,
|
||||
splitHtmlForTelegram,
|
||||
|
|
@ -27,7 +28,14 @@ export class TelegramChannel extends ChannelBase {
|
|||
options?: ChannelBaseOptions,
|
||||
) {
|
||||
super(name, config, bridge, options);
|
||||
this.bot = new Bot(config.token);
|
||||
const botConfig = this.proxy
|
||||
? {
|
||||
client: {
|
||||
baseFetchConfig: { agent: new HttpsProxyAgent(this.proxy) },
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
this.bot = new Bot(config.token, botConfig);
|
||||
}
|
||||
|
||||
private getFileUrl(filePath: string): string {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
import { normalizeProxyUrl } from '@qwen-code/qwen-code-core';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { writeStderrLine, writeStdoutLine } from '../../utils/stdioHelpers.js';
|
||||
import { AcpBridge, SessionRouter } from '@qwen-code/channel-base';
|
||||
|
|
@ -22,6 +24,31 @@ const MAX_CRASH_RESTARTS = 3;
|
|||
const CRASH_WINDOW_MS = 5 * 60 * 1000; // 5-minute window for counting crashes
|
||||
const RESTART_DELAY_MS = 3000;
|
||||
|
||||
/**
|
||||
* Resolve and apply proxy settings for the channel service process.
|
||||
*
|
||||
* The normal CLI path applies proxy via loadCliConfig → Config constructor →
|
||||
* setGlobalDispatcher, but `channel start` never calls loadCliConfig. This
|
||||
* replicates the same resolution logic (--proxy flag → HTTPS_PROXY →
|
||||
* HTTP_PROXY) and applies the global dispatcher for native fetch() calls.
|
||||
* The resolved URL is also passed to channels via ChannelBaseOptions so
|
||||
* adapters can configure their own HTTP clients (e.g. grammy uses node-fetch
|
||||
* which needs a separate agent).
|
||||
*/
|
||||
function resolveProxy(cliProxy?: string): string | undefined {
|
||||
const proxyUrl = normalizeProxyUrl(
|
||||
cliProxy ||
|
||||
process.env['HTTPS_PROXY'] ||
|
||||
process.env['https_proxy'] ||
|
||||
process.env['HTTP_PROXY'] ||
|
||||
process.env['http_proxy'],
|
||||
);
|
||||
if (proxyUrl) {
|
||||
setGlobalDispatcher(new ProxyAgent(proxyUrl));
|
||||
}
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
function sessionsPath(): string {
|
||||
return path.join(os.homedir(), '.qwen', 'channels', 'sessions.json');
|
||||
}
|
||||
|
|
@ -100,7 +127,7 @@ function createChannel(
|
|||
name: string,
|
||||
config: ReturnType<typeof parseChannelConfig>,
|
||||
bridge: AcpBridge,
|
||||
options?: { router?: SessionRouter },
|
||||
options?: { router?: SessionRouter; proxy?: string },
|
||||
): ChannelBase {
|
||||
const channelPlugin = getPlugin(config.type);
|
||||
if (!channelPlugin) {
|
||||
|
|
@ -138,7 +165,7 @@ function checkDuplicateInstance(): void {
|
|||
}
|
||||
|
||||
/** Start a single channel with its own bridge + crash recovery. */
|
||||
async function startSingle(name: string): Promise<void> {
|
||||
async function startSingle(name: string, proxy?: string): Promise<void> {
|
||||
checkDuplicateInstance();
|
||||
const channelsConfig = loadChannelsConfig();
|
||||
|
||||
|
|
@ -180,7 +207,7 @@ async function startSingle(name: string): Promise<void> {
|
|||
);
|
||||
const channels: Map<string, ChannelBase> = new Map();
|
||||
|
||||
const channel = createChannel(name, config, bridge, { router });
|
||||
const channel = createChannel(name, config, bridge, { router, proxy });
|
||||
channels.set(name, channel);
|
||||
registerToolCallDispatch(bridge, router, channels);
|
||||
|
||||
|
|
@ -256,7 +283,7 @@ async function startSingle(name: string): Promise<void> {
|
|||
}
|
||||
|
||||
/** Start all configured channels with a shared bridge + crash recovery. */
|
||||
async function startAll(): Promise<void> {
|
||||
async function startAll(proxy?: string): Promise<void> {
|
||||
checkDuplicateInstance();
|
||||
const channelsConfig = loadChannelsConfig();
|
||||
|
||||
|
|
@ -323,7 +350,7 @@ async function startAll(): Promise<void> {
|
|||
);
|
||||
|
||||
for (const { name, config } of parsed) {
|
||||
channels.set(name, createChannel(name, config, bridge, { router }));
|
||||
channels.set(name, createChannel(name, config, bridge, { router, proxy }));
|
||||
}
|
||||
registerToolCallDispatch(bridge, router, channels);
|
||||
|
||||
|
|
@ -433,10 +460,13 @@ export const startCommand: CommandModule<object, { name?: string }> = {
|
|||
describe: 'Channel name (omit to start all configured channels)',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const proxy = resolveProxy(
|
||||
(argv as Record<string, unknown>)['proxy'] as string | undefined,
|
||||
);
|
||||
if (argv.name) {
|
||||
await startSingle(argv.name);
|
||||
await startSingle(argv.name, proxy);
|
||||
} else {
|
||||
await startAll();
|
||||
await startAll(proxy);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue