feat(channels): add DM pairing flow for sender approval

- Add PairingStore for managing pending requests and approved users
- Update SenderGate to support pairing policy with code generation
- Add CLI commands: `qwen channel pairing list/approve`
- Document pairing flow with rules and usage examples

This allows unknown senders to request access via a pairing code
that the bot operator approves through the CLI.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-24 11:37:16 +00:00
parent 59ee49e0ab
commit 8753245b5f
9 changed files with 338 additions and 23 deletions

View file

@ -1,5 +1,21 @@
import type { CommandModule, Argv } from 'yargs';
import { startCommand } from './channel/start.js';
import {
pairingListCommand,
pairingApproveCommand,
} from './channel/pairing.js';
const pairingCommand: CommandModule = {
command: 'pairing',
describe: 'Manage DM pairing requests',
builder: (yargs: Argv) =>
yargs
.command(pairingListCommand)
.command(pairingApproveCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {},
};
export const channelCommand: CommandModule = {
command: 'channel',
@ -7,6 +23,7 @@ export const channelCommand: CommandModule = {
builder: (yargs: Argv) =>
yargs
.command(startCommand)
.command(pairingCommand)
.demandCommand(1, 'You need at least one command before continuing.')
.version(false),
handler: () => {},

View file

@ -0,0 +1,66 @@
import type { CommandModule } from 'yargs';
import { PairingStore } from '@qwen-code/channel-base';
import { writeStderrLine, writeStdoutLine } from '../../utils/stdioHelpers.js';
export const pairingListCommand: CommandModule<object, { name: string }> = {
command: 'list <name>',
describe: 'List pending pairing requests for a channel',
builder: (yargs) =>
yargs.positional('name', {
type: 'string',
describe: 'Channel name',
demandOption: true,
}),
handler: (argv) => {
const store = new PairingStore(argv.name);
const pending = store.listPending();
if (pending.length === 0) {
writeStdoutLine('No pending pairing requests.');
return;
}
writeStdoutLine(`Pending pairing requests for "${argv.name}":\n`);
for (const req of pending) {
const ago = Math.round((Date.now() - req.createdAt) / 60000);
writeStdoutLine(
` Code: ${req.code} Sender: ${req.senderName} (${req.senderId}) ${ago}m ago`,
);
}
},
};
export const pairingApproveCommand: CommandModule<
object,
{ name: string; code: string }
> = {
command: 'approve <name> <code>',
describe: 'Approve a pending pairing request',
builder: (yargs) =>
yargs
.positional('name', {
type: 'string',
describe: 'Channel name',
demandOption: true,
})
.positional('code', {
type: 'string',
describe: 'Pairing code',
demandOption: true,
}),
handler: (argv) => {
const store = new PairingStore(argv.name);
const request = store.approve(argv.code);
if (!request) {
writeStderrLine(
`No pending request found for code "${argv.code.toUpperCase()}". It may have expired.`,
);
process.exit(1);
}
writeStdoutLine(
`Approved: ${request.senderName} (${request.senderId}) can now use channel "${argv.name}".`,
);
},
};