mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 21:50:52 +00:00
feat(channels): add WeChat/Weixin channel support
- Add WeixinAdapter with accounts, api, login, monitor, send modules - Add channel configure command for interactive setup - Update TelegramAdapter for consistency Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
90236465d3
commit
24c9b0f333
18 changed files with 954 additions and 21 deletions
|
|
@ -4,6 +4,7 @@ import {
|
|||
pairingListCommand,
|
||||
pairingApproveCommand,
|
||||
} from './channel/pairing.js';
|
||||
import { configureWeixinCommand } from './channel/configure.js';
|
||||
|
||||
const pairingCommand: CommandModule = {
|
||||
command: 'pairing',
|
||||
|
|
@ -24,6 +25,7 @@ export const channelCommand: CommandModule = {
|
|||
yargs
|
||||
.command(startCommand)
|
||||
.command(pairingCommand)
|
||||
.command(configureWeixinCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {},
|
||||
|
|
|
|||
85
packages/cli/src/commands/channel/configure.ts
Normal file
85
packages/cli/src/commands/channel/configure.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import type { CommandModule } from 'yargs';
|
||||
import { writeStderrLine, writeStdoutLine } from '../../utils/stdioHelpers.js';
|
||||
import {
|
||||
loadAccount,
|
||||
saveAccount,
|
||||
clearAccount,
|
||||
DEFAULT_BASE_URL,
|
||||
} from '@qwen-code/channel-weixin/accounts';
|
||||
import { startLogin, waitForLogin } from '@qwen-code/channel-weixin/login';
|
||||
|
||||
export const configureWeixinCommand: CommandModule<
|
||||
object,
|
||||
{ action: string | undefined }
|
||||
> = {
|
||||
command: 'configure-weixin [action]',
|
||||
describe: 'Configure WeChat channel (login via QR code)',
|
||||
builder: (yargs) =>
|
||||
yargs.positional('action', {
|
||||
type: 'string',
|
||||
describe: '"clear" to remove stored credentials, omit to login',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const { action } = argv;
|
||||
|
||||
if (action === 'clear') {
|
||||
clearAccount();
|
||||
writeStdoutLine('WeChat credentials cleared.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'status') {
|
||||
const account = loadAccount();
|
||||
if (account) {
|
||||
writeStdoutLine(`WeChat account configured (saved ${account.savedAt})`);
|
||||
writeStdoutLine(` Base URL: ${account.baseUrl}`);
|
||||
if (account.userId) {
|
||||
writeStdoutLine(` User ID: ${account.userId}`);
|
||||
}
|
||||
} else {
|
||||
writeStdoutLine('WeChat account not configured.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default action: login
|
||||
const existing = loadAccount();
|
||||
if (existing) {
|
||||
writeStdoutLine(
|
||||
`Existing WeChat credentials found (saved ${existing.savedAt}).`,
|
||||
);
|
||||
writeStdoutLine('Re-running login will overwrite them.\n');
|
||||
}
|
||||
|
||||
const baseUrl = DEFAULT_BASE_URL;
|
||||
|
||||
writeStdoutLine('Starting WeChat QR code login...\n');
|
||||
|
||||
try {
|
||||
const qrcodeId = await startLogin(baseUrl);
|
||||
const result = await waitForLogin({ qrcodeId, apiBaseUrl: baseUrl });
|
||||
|
||||
if (result.connected && result.token) {
|
||||
saveAccount({
|
||||
token: result.token,
|
||||
baseUrl: result.baseUrl || baseUrl,
|
||||
userId: result.userId,
|
||||
savedAt: new Date().toISOString(),
|
||||
});
|
||||
writeStdoutLine('\n' + result.message);
|
||||
writeStdoutLine(
|
||||
'Credentials saved. You can now start a weixin channel with:',
|
||||
);
|
||||
writeStdoutLine(' qwen channel start <name>');
|
||||
} else {
|
||||
writeStderrLine('\n' + result.message);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
writeStderrLine(
|
||||
`Login failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ 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 { WeixinChannel } from '@qwen-code/channel-weixin';
|
||||
import * as path from 'node:path';
|
||||
|
||||
function resolveEnvVars(value: string): string {
|
||||
|
|
@ -62,29 +63,33 @@ export const startCommand: CommandModule<object, { name: string }> = {
|
|||
);
|
||||
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') {
|
||||
const supportedTypes = ['telegram', 'weixin'];
|
||||
if (!supportedTypes.includes(channelType)) {
|
||||
writeStderrLine(
|
||||
`Error: Channel type "${channelType}" is not yet supported. Only "telegram" is available.`,
|
||||
`Error: Channel type "${channelType}" is not supported. Available: ${supportedTypes.join(', ')}`,
|
||||
);
|
||||
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);
|
||||
// Token is required for telegram, not for weixin (uses account.json)
|
||||
let token = '';
|
||||
if (channelType !== 'weixin') {
|
||||
if (!rawConfig['token']) {
|
||||
writeStderrLine(
|
||||
`Error: Channel "${name}" is missing required field "token".`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
token = resolveEnvVars(rawConfig['token'] as string);
|
||||
} catch (err) {
|
||||
writeStderrLine(
|
||||
`Error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const config: ChannelConfig = {
|
||||
|
|
@ -105,6 +110,12 @@ export const startCommand: CommandModule<object, { name: string }> = {
|
|||
groups: (rawConfig['groups'] as ChannelConfig['groups']) || {},
|
||||
};
|
||||
|
||||
// Pass through weixin-specific config
|
||||
const extendedConfig = {
|
||||
...config,
|
||||
baseUrl: rawConfig['baseUrl'] as string | undefined,
|
||||
};
|
||||
|
||||
const cliEntryPath = findCliEntryPath();
|
||||
writeStdoutLine(`[Channel] CLI entry: ${cliEntryPath}`);
|
||||
writeStdoutLine(`[Channel] Starting "${name}" (type=${config.type})...`);
|
||||
|
|
@ -112,7 +123,12 @@ export const startCommand: CommandModule<object, { name: string }> = {
|
|||
const bridge = new AcpBridge({ cliEntryPath, cwd: config.cwd });
|
||||
await bridge.start();
|
||||
|
||||
const channel = new TelegramChannel(name, config, bridge);
|
||||
let channel: TelegramChannel | WeixinChannel;
|
||||
if (channelType === 'weixin') {
|
||||
channel = new WeixinChannel(name, extendedConfig, bridge);
|
||||
} else {
|
||||
channel = new TelegramChannel(name, config, bridge);
|
||||
}
|
||||
await channel.connect();
|
||||
|
||||
writeStdoutLine(`[Channel] "${name}" is running. Press Ctrl+C to stop.`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue