@qwen-code/channel-base 0.14.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export interface AcpBridgeOptions {
3
+ cliEntryPath: string;
4
+ cwd: string;
5
+ model?: string;
6
+ }
7
+ export interface AvailableCommand {
8
+ name: string;
9
+ description: string;
10
+ }
11
+ export interface ToolCallEvent {
12
+ sessionId: string;
13
+ toolCallId: string;
14
+ kind: string;
15
+ title: string;
16
+ status: string;
17
+ rawInput?: Record<string, unknown>;
18
+ }
19
+ export declare class AcpBridge extends EventEmitter {
20
+ private child;
21
+ private connection;
22
+ private options;
23
+ private _availableCommands;
24
+ constructor(options: AcpBridgeOptions);
25
+ get availableCommands(): AvailableCommand[];
26
+ start(): Promise<void>;
27
+ newSession(cwd: string): Promise<string>;
28
+ loadSession(sessionId: string, cwd: string): Promise<string>;
29
+ prompt(sessionId: string, text: string, options?: {
30
+ imageBase64?: string;
31
+ imageMimeType?: string;
32
+ }): Promise<string>;
33
+ stop(): void;
34
+ get isConnected(): boolean;
35
+ private handleSessionUpdate;
36
+ private ensureConnection;
37
+ }
@@ -0,0 +1,166 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { Readable, Writable } from 'node:stream';
3
+ import { EventEmitter } from 'node:events';
4
+ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
5
+ export class AcpBridge extends EventEmitter {
6
+ child = null;
7
+ connection = null;
8
+ options;
9
+ _availableCommands = [];
10
+ constructor(options) {
11
+ super();
12
+ this.options = options;
13
+ }
14
+ get availableCommands() {
15
+ return this._availableCommands;
16
+ }
17
+ async start() {
18
+ const { cliEntryPath, cwd } = this.options;
19
+ const args = [cliEntryPath, '--acp'];
20
+ if (this.options.model) {
21
+ args.push('--model', this.options.model);
22
+ }
23
+ this.child = spawn(process.execPath, args, {
24
+ cwd,
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ env: { ...process.env },
27
+ shell: false,
28
+ });
29
+ this.child.stderr?.on('data', (data) => {
30
+ const msg = data.toString().trim();
31
+ if (msg) {
32
+ process.stderr.write(`[AcpBridge] ${msg}\n`);
33
+ }
34
+ });
35
+ this.child.on('exit', (code, signal) => {
36
+ process.stderr.write(`[AcpBridge] Process exited (code=${code}, signal=${signal})\n`);
37
+ this.connection = null;
38
+ this.child = null;
39
+ this.emit('disconnected', code, signal);
40
+ });
41
+ // Give the process a moment to start
42
+ await new Promise((resolve) => setTimeout(resolve, 1000));
43
+ if (!this.child || this.child.killed) {
44
+ throw new Error('ACP process failed to start');
45
+ }
46
+ const stdout = Readable.toWeb(this.child.stdout);
47
+ const stdin = Writable.toWeb(this.child.stdin);
48
+ const stream = ndJsonStream(stdin, stdout);
49
+ this.connection = new ClientSideConnection(() => ({
50
+ sessionUpdate: (params) => {
51
+ this.handleSessionUpdate(params);
52
+ return Promise.resolve();
53
+ },
54
+ requestPermission: async (params) => {
55
+ // Auto-approve for now; Phase 5 will add interactive approval
56
+ const options = Array.isArray(params.options) ? params.options : [];
57
+ const optionId = options.find((o) => o.optionId === 'proceed_once')?.optionId ||
58
+ options[0]?.optionId ||
59
+ 'proceed_once';
60
+ return { outcome: { outcome: 'selected', optionId } };
61
+ },
62
+ extNotification: async () => { },
63
+ }), stream);
64
+ await this.connection.initialize({
65
+ protocolVersion: PROTOCOL_VERSION,
66
+ clientCapabilities: {},
67
+ });
68
+ }
69
+ async newSession(cwd) {
70
+ const conn = this.ensureConnection();
71
+ const response = await conn.newSession({ cwd, mcpServers: [] });
72
+ return response.sessionId;
73
+ }
74
+ async loadSession(sessionId, cwd) {
75
+ const conn = this.ensureConnection();
76
+ const response = await conn.loadSession({
77
+ sessionId,
78
+ cwd,
79
+ mcpServers: [],
80
+ });
81
+ return response.sessionId;
82
+ }
83
+ async prompt(sessionId, text, options) {
84
+ const conn = this.ensureConnection();
85
+ const chunks = [];
86
+ const onChunk = (sid, chunk) => {
87
+ if (sid === sessionId)
88
+ chunks.push(chunk);
89
+ };
90
+ this.on('textChunk', onChunk);
91
+ const prompt = [];
92
+ if (options?.imageBase64 && options.imageMimeType) {
93
+ prompt.push({
94
+ type: 'image',
95
+ data: options.imageBase64,
96
+ mimeType: options.imageMimeType,
97
+ });
98
+ }
99
+ prompt.push({ type: 'text', text });
100
+ try {
101
+ await conn.prompt({
102
+ sessionId,
103
+ prompt: prompt,
104
+ });
105
+ }
106
+ finally {
107
+ this.off('textChunk', onChunk);
108
+ }
109
+ return chunks.join('');
110
+ }
111
+ stop() {
112
+ if (this.child) {
113
+ this.child.kill();
114
+ this.child = null;
115
+ }
116
+ this.connection = null;
117
+ }
118
+ get isConnected() {
119
+ return (this.child !== null && !this.child.killed && this.child.exitCode === null);
120
+ }
121
+ handleSessionUpdate(params) {
122
+ const { sessionId } = params;
123
+ const update = params['update'];
124
+ if (!update)
125
+ return;
126
+ const type = update['sessionUpdate'];
127
+ switch (type) {
128
+ case 'agent_message_chunk': {
129
+ const content = update['content'];
130
+ if (content?.type === 'text' && content.text) {
131
+ this.emit('textChunk', sessionId, content.text);
132
+ }
133
+ break;
134
+ }
135
+ case 'tool_call': {
136
+ const event = {
137
+ sessionId,
138
+ toolCallId: update['toolCallId'],
139
+ kind: update['kind'] || '',
140
+ title: update['title'] || '',
141
+ status: update['status'] || 'pending',
142
+ rawInput: update['rawInput'],
143
+ };
144
+ this.emit('toolCall', event);
145
+ break;
146
+ }
147
+ case 'available_commands_update': {
148
+ if (Array.isArray(update['availableCommands'])) {
149
+ this._availableCommands = update['availableCommands'];
150
+ }
151
+ break;
152
+ }
153
+ default:
154
+ // Ignore other session update types
155
+ break;
156
+ }
157
+ this.emit('sessionUpdate', params);
158
+ }
159
+ ensureConnection() {
160
+ if (!this.connection || !this.isConnected) {
161
+ throw new Error('Not connected to ACP agent');
162
+ }
163
+ return this.connection;
164
+ }
165
+ }
166
+ //# sourceMappingURL=AcpBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AcpBridge.js","sourceRoot":"","sources":["../src/AcpBridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { ChannelConfig, Envelope } from './types.js';
2
+ import { GroupGate } from './GroupGate.js';
3
+ import { SenderGate } from './SenderGate.js';
4
+ import { SessionRouter } from './SessionRouter.js';
5
+ import type { AcpBridge, ToolCallEvent } from './AcpBridge.js';
6
+ export interface ChannelBaseOptions {
7
+ router?: SessionRouter;
8
+ }
9
+ /** Handler for a slash command. Return true if handled, false to forward to agent. */
10
+ type CommandHandler = (envelope: Envelope, args: string) => Promise<boolean>;
11
+ export declare abstract class ChannelBase {
12
+ protected config: ChannelConfig;
13
+ protected bridge: AcpBridge;
14
+ protected groupGate: GroupGate;
15
+ protected gate: SenderGate;
16
+ protected router: SessionRouter;
17
+ protected name: string;
18
+ private instructedSessions;
19
+ private commands;
20
+ /** Per-session promise chain to serialize prompt + send. */
21
+ private sessionQueues;
22
+ constructor(name: string, config: ChannelConfig, bridge: AcpBridge, options?: ChannelBaseOptions);
23
+ abstract connect(): Promise<void>;
24
+ abstract sendMessage(chatId: string, text: string): Promise<void>;
25
+ abstract disconnect(): void;
26
+ /** Replace the bridge instance (used after crash recovery restart). */
27
+ setBridge(bridge: AcpBridge): void;
28
+ onToolCall(_chatId: string, _event: ToolCallEvent): void;
29
+ /**
30
+ * Register a slash command handler. Subclasses can call this to add
31
+ * platform-specific commands (e.g., /start for Telegram).
32
+ * Overrides shared commands if the same name is registered.
33
+ */
34
+ protected registerCommand(name: string, handler: CommandHandler): void;
35
+ /** Register shared slash commands. Called from constructor. */
36
+ private registerSharedCommands;
37
+ /** Check if a message text matches a registered local command. */
38
+ protected isLocalCommand(text: string): boolean;
39
+ /**
40
+ * Parse a slash command from message text.
41
+ * Returns { command, args } or null if not a slash command.
42
+ */
43
+ private parseCommand;
44
+ handleInbound(envelope: Envelope): Promise<void>;
45
+ protected onPairingRequired(chatId: string, code: string | null): Promise<void>;
46
+ }
47
+ export {};
@@ -0,0 +1,183 @@
1
+ import { GroupGate } from './GroupGate.js';
2
+ import { SenderGate } from './SenderGate.js';
3
+ import { PairingStore } from './PairingStore.js';
4
+ import { SessionRouter } from './SessionRouter.js';
5
+ export class ChannelBase {
6
+ config;
7
+ bridge;
8
+ groupGate;
9
+ gate;
10
+ router;
11
+ name;
12
+ instructedSessions = new Set();
13
+ commands = new Map();
14
+ /** Per-session promise chain to serialize prompt + send. */
15
+ sessionQueues = new Map();
16
+ constructor(name, config, bridge, options) {
17
+ this.name = name;
18
+ this.config = config;
19
+ this.bridge = bridge;
20
+ this.groupGate = new GroupGate(config.groupPolicy, config.groups);
21
+ const pairingStore = config.senderPolicy === 'pairing' ? new PairingStore(name) : undefined;
22
+ this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
23
+ this.router =
24
+ options?.router ||
25
+ new SessionRouter(bridge, config.cwd, config.sessionScope);
26
+ this.registerSharedCommands();
27
+ // When running standalone (no gateway), register toolCall listener directly.
28
+ // In gateway mode, the ChannelManager dispatches events instead.
29
+ if (!options?.router) {
30
+ bridge.on('toolCall', (event) => {
31
+ const target = this.router.getTarget(event.sessionId);
32
+ if (target) {
33
+ this.onToolCall(target.chatId, event);
34
+ }
35
+ });
36
+ }
37
+ }
38
+ /** Replace the bridge instance (used after crash recovery restart). */
39
+ setBridge(bridge) {
40
+ this.bridge = bridge;
41
+ }
42
+ onToolCall(_chatId, _event) { }
43
+ /**
44
+ * Register a slash command handler. Subclasses can call this to add
45
+ * platform-specific commands (e.g., /start for Telegram).
46
+ * Overrides shared commands if the same name is registered.
47
+ */
48
+ registerCommand(name, handler) {
49
+ this.commands.set(name.toLowerCase(), handler);
50
+ }
51
+ /** Register shared slash commands. Called from constructor. */
52
+ registerSharedCommands() {
53
+ const clearHandler = async (envelope) => {
54
+ const removed = this.router.removeSession(this.name, envelope.senderId, envelope.chatId);
55
+ if (removed) {
56
+ this.instructedSessions.clear();
57
+ await this.sendMessage(envelope.chatId, 'Session cleared. Your next message will start a fresh conversation.');
58
+ }
59
+ else {
60
+ await this.sendMessage(envelope.chatId, 'No active session to clear.');
61
+ }
62
+ return true;
63
+ };
64
+ this.registerCommand('clear', clearHandler);
65
+ this.registerCommand('reset', clearHandler);
66
+ this.registerCommand('new', clearHandler);
67
+ this.registerCommand('help', async (envelope) => {
68
+ const lines = [
69
+ 'Commands:',
70
+ '/help — Show this help',
71
+ '/clear — Clear your session (aliases: /reset, /new)',
72
+ '/status — Show session info',
73
+ ];
74
+ // Platform-specific commands (registered by adapters, not shared ones)
75
+ const sharedCmds = new Set(['help', 'clear', 'reset', 'new', 'status']);
76
+ const platformCmds = [...this.commands.keys()].filter((c) => !sharedCmds.has(c));
77
+ if (platformCmds.length > 0) {
78
+ for (const cmd of platformCmds) {
79
+ lines.push(`/${cmd}`);
80
+ }
81
+ }
82
+ const agentCommands = this.bridge.availableCommands;
83
+ if (agentCommands.length > 0) {
84
+ lines.push('', 'Agent commands (forwarded to Qwen Code):');
85
+ for (const cmd of agentCommands) {
86
+ lines.push(`/${cmd.name} — ${cmd.description}`);
87
+ }
88
+ }
89
+ lines.push('', 'Send any text to chat with the agent.');
90
+ await this.sendMessage(envelope.chatId, lines.join('\n'));
91
+ return true;
92
+ });
93
+ this.registerCommand('status', async (envelope) => {
94
+ const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId);
95
+ const policy = this.config.senderPolicy;
96
+ const lines = [
97
+ `Session: ${hasSession ? 'active' : 'none'}`,
98
+ `Access: ${policy}`,
99
+ `Channel: ${this.name}`,
100
+ ];
101
+ await this.sendMessage(envelope.chatId, lines.join('\n'));
102
+ return true;
103
+ });
104
+ }
105
+ /** Check if a message text matches a registered local command. */
106
+ isLocalCommand(text) {
107
+ const parsed = this.parseCommand(text);
108
+ return parsed !== null && this.commands.has(parsed.command);
109
+ }
110
+ /**
111
+ * Parse a slash command from message text.
112
+ * Returns { command, args } or null if not a slash command.
113
+ */
114
+ parseCommand(text) {
115
+ if (!text.startsWith('/'))
116
+ return null;
117
+ // Handle /command@botname format (Telegram groups)
118
+ const match = text.match(/^\/([a-zA-Z0-9_]+)(?:@\S+)?\s*(.*)/s);
119
+ if (!match)
120
+ return null;
121
+ return { command: match[1].toLowerCase(), args: match[2].trim() };
122
+ }
123
+ async handleInbound(envelope) {
124
+ // 1. Group gate: policy + allowlist + mention gating
125
+ const groupResult = this.groupGate.check(envelope);
126
+ if (!groupResult.allowed) {
127
+ return; // silently drop — no pairing, no reply
128
+ }
129
+ // 2. Sender gate: allowlist / pairing / open
130
+ const result = this.gate.check(envelope.senderId, envelope.senderName);
131
+ if (!result.allowed) {
132
+ if (result.pairingCode !== undefined) {
133
+ await this.onPairingRequired(envelope.chatId, result.pairingCode);
134
+ }
135
+ return;
136
+ }
137
+ // 3. Slash command handling — before session/agent routing
138
+ const parsed = this.parseCommand(envelope.text);
139
+ if (parsed) {
140
+ const handler = this.commands.get(parsed.command);
141
+ if (handler) {
142
+ const handled = await handler(envelope, parsed.args);
143
+ if (handled)
144
+ return;
145
+ }
146
+ // Unrecognized commands fall through to the agent
147
+ }
148
+ const sessionId = await this.router.resolve(this.name, envelope.senderId, envelope.chatId, envelope.threadId, this.config.cwd);
149
+ // Prepend referenced (quoted) message text for reply context
150
+ let promptText = envelope.text;
151
+ if (envelope.referencedText) {
152
+ promptText = `[Replying to: "${envelope.referencedText}"]\n\n${promptText}`;
153
+ }
154
+ // Prepend channel instructions on first message of a session
155
+ if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
156
+ promptText = `${this.config.instructions}\n\n${promptText}`;
157
+ this.instructedSessions.add(sessionId);
158
+ }
159
+ // Serialize prompt + send per session to prevent textChunk listener
160
+ // pollution when concurrent messages hit the same session.
161
+ const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
162
+ const current = prev.then(async () => {
163
+ const response = await this.bridge.prompt(sessionId, promptText, {
164
+ imageBase64: envelope.imageBase64,
165
+ imageMimeType: envelope.imageMimeType,
166
+ });
167
+ if (response) {
168
+ await this.sendMessage(envelope.chatId, response);
169
+ }
170
+ });
171
+ this.sessionQueues.set(sessionId, current.catch(() => { }));
172
+ await current;
173
+ }
174
+ async onPairingRequired(chatId, code) {
175
+ if (code) {
176
+ await this.sendMessage(chatId, `Your pairing code is: ${code}\n\nAsk the bot operator to approve you with:\n qwen channel pairing approve ${this.name} ${code}`);
177
+ }
178
+ else {
179
+ await this.sendMessage(chatId, 'Too many pending pairing requests. Please try again later.');
180
+ }
181
+ }
182
+ }
183
+ //# sourceMappingURL=ChannelBase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChannelBase.js","sourceRoot":"","sources":["../src/ChannelBase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { GroupPolicy, GroupConfig, Envelope } from './types.js';
2
+ export interface GroupCheckResult {
3
+ allowed: boolean;
4
+ reason?: 'disabled' | 'not_allowlisted' | 'mention_required';
5
+ }
6
+ export declare class GroupGate {
7
+ private policy;
8
+ private groups;
9
+ constructor(policy?: GroupPolicy, groups?: Record<string, GroupConfig>);
10
+ /**
11
+ * Full group check: policy + allowlist + mention gating.
12
+ * Evaluation order:
13
+ * 1. groupPolicy (disabled → drop)
14
+ * 2. group allowlist (allowlist mode, no match → drop)
15
+ * 3. mention gating (requireMention + not mentioned → drop silently)
16
+ *
17
+ * Mention gating runs before sender gate so that unmentioned messages
18
+ * in groups don't trigger pairing flows.
19
+ */
20
+ check(envelope: Envelope): GroupCheckResult;
21
+ }
@@ -0,0 +1,41 @@
1
+ export class GroupGate {
2
+ policy;
3
+ groups;
4
+ constructor(policy = 'disabled', groups = {}) {
5
+ this.policy = policy;
6
+ this.groups = groups;
7
+ }
8
+ /**
9
+ * Full group check: policy + allowlist + mention gating.
10
+ * Evaluation order:
11
+ * 1. groupPolicy (disabled → drop)
12
+ * 2. group allowlist (allowlist mode, no match → drop)
13
+ * 3. mention gating (requireMention + not mentioned → drop silently)
14
+ *
15
+ * Mention gating runs before sender gate so that unmentioned messages
16
+ * in groups don't trigger pairing flows.
17
+ */
18
+ check(envelope) {
19
+ if (!envelope.isGroup) {
20
+ return { allowed: true };
21
+ }
22
+ if (this.policy === 'disabled') {
23
+ return { allowed: false, reason: 'disabled' };
24
+ }
25
+ if (this.policy === 'allowlist') {
26
+ // In allowlist mode, "*" is only a default config — not a wildcard allow.
27
+ // The group must be explicitly listed by ID.
28
+ if (!this.groups[envelope.chatId]) {
29
+ return { allowed: false, reason: 'not_allowlisted' };
30
+ }
31
+ }
32
+ // Per-group config, falling back to "*" defaults, then built-in defaults
33
+ const groupConfig = this.groups[envelope.chatId] || this.groups['*'] || {};
34
+ const requireMention = groupConfig.requireMention ?? true;
35
+ if (requireMention && !envelope.isMentioned && !envelope.isReplyToBot) {
36
+ return { allowed: false, reason: 'mention_required' };
37
+ }
38
+ return { allowed: true };
39
+ }
40
+ }
41
+ //# sourceMappingURL=GroupGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroupGate.js","sourceRoot":"","sources":["../src/GroupGate.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,SAAS;IACZ,MAAM,CAAc;IACpB,MAAM,CAA8B;IAE5C,YACE,SAAsB,UAAU,EAChC,SAAsC,EAAE;QAExC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,QAAkB;QACtB,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QAChD,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,0EAA0E;YAC1E,6CAA6C;YAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;YACvD,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3E,MAAM,cAAc,GAAG,WAAW,CAAC,cAAc,IAAI,IAAI,CAAC;QAE1D,IAAI,cAAc,IAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;YACtE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QACxD,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ export interface PairingRequest {
2
+ senderId: string;
3
+ senderName: string;
4
+ code: string;
5
+ createdAt: number;
6
+ }
7
+ export declare class PairingStore {
8
+ private dir;
9
+ private pendingPath;
10
+ private allowlistPath;
11
+ constructor(channelName: string);
12
+ isApproved(senderId: string): boolean;
13
+ /**
14
+ * Create a pairing request for an unknown sender.
15
+ * Returns the code if created, or null if the pending cap is reached.
16
+ * If the sender already has a non-expired pending request, returns that code.
17
+ */
18
+ createRequest(senderId: string, senderName: string): string | null;
19
+ /**
20
+ * Approve a pairing request by code.
21
+ * Returns the sender ID if found, or null if not found / expired.
22
+ */
23
+ approve(code: string): PairingRequest | null;
24
+ listPending(): PairingRequest[];
25
+ getAllowlist(): string[];
26
+ private ensureDir;
27
+ private readPending;
28
+ private writePending;
29
+ private readAllowlist;
30
+ private writeAllowlist;
31
+ }
@@ -0,0 +1,114 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ // Alphabet without ambiguous chars: 0/O, 1/I
5
+ const SAFE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
6
+ const CODE_LENGTH = 8;
7
+ const EXPIRY_MS = 60 * 60 * 1000; // 1 hour
8
+ const MAX_PENDING = 3;
9
+ export class PairingStore {
10
+ dir;
11
+ pendingPath;
12
+ allowlistPath;
13
+ constructor(channelName) {
14
+ this.dir = path.join(os.homedir(), '.qwen', 'channels');
15
+ this.pendingPath = path.join(this.dir, `${channelName}-pairing.json`);
16
+ this.allowlistPath = path.join(this.dir, `${channelName}-allowlist.json`);
17
+ }
18
+ isApproved(senderId) {
19
+ const list = this.readAllowlist();
20
+ return list.includes(senderId);
21
+ }
22
+ /**
23
+ * Create a pairing request for an unknown sender.
24
+ * Returns the code if created, or null if the pending cap is reached.
25
+ * If the sender already has a non-expired pending request, returns that code.
26
+ */
27
+ createRequest(senderId, senderName) {
28
+ const pending = this.readPending();
29
+ // Purge expired
30
+ const now = Date.now();
31
+ const active = pending.filter((r) => now - r.createdAt < EXPIRY_MS);
32
+ // Check if sender already has a pending request
33
+ const existing = active.find((r) => r.senderId === senderId);
34
+ if (existing) {
35
+ return existing.code;
36
+ }
37
+ // Cap check
38
+ if (active.length >= MAX_PENDING) {
39
+ return null;
40
+ }
41
+ const code = generateCode();
42
+ active.push({ senderId, senderName, code, createdAt: now });
43
+ this.writePending(active);
44
+ return code;
45
+ }
46
+ /**
47
+ * Approve a pairing request by code.
48
+ * Returns the sender ID if found, or null if not found / expired.
49
+ */
50
+ approve(code) {
51
+ const pending = this.readPending();
52
+ const now = Date.now();
53
+ const idx = pending.findIndex((r) => r.code === code.toUpperCase() && now - r.createdAt < EXPIRY_MS);
54
+ if (idx === -1)
55
+ return null;
56
+ const request = pending[idx];
57
+ pending.splice(idx, 1);
58
+ this.writePending(pending);
59
+ // Add to allowlist
60
+ const list = this.readAllowlist();
61
+ if (!list.includes(request.senderId)) {
62
+ list.push(request.senderId);
63
+ this.writeAllowlist(list);
64
+ }
65
+ return request;
66
+ }
67
+ listPending() {
68
+ const pending = this.readPending();
69
+ const now = Date.now();
70
+ return pending.filter((r) => now - r.createdAt < EXPIRY_MS);
71
+ }
72
+ getAllowlist() {
73
+ return this.readAllowlist();
74
+ }
75
+ ensureDir() {
76
+ if (!fs.existsSync(this.dir)) {
77
+ fs.mkdirSync(this.dir, { recursive: true });
78
+ }
79
+ }
80
+ readPending() {
81
+ try {
82
+ const data = fs.readFileSync(this.pendingPath, 'utf-8');
83
+ return JSON.parse(data);
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ }
89
+ writePending(requests) {
90
+ this.ensureDir();
91
+ fs.writeFileSync(this.pendingPath, JSON.stringify(requests, null, 2));
92
+ }
93
+ readAllowlist() {
94
+ try {
95
+ const data = fs.readFileSync(this.allowlistPath, 'utf-8');
96
+ return JSON.parse(data);
97
+ }
98
+ catch {
99
+ return [];
100
+ }
101
+ }
102
+ writeAllowlist(list) {
103
+ this.ensureDir();
104
+ fs.writeFileSync(this.allowlistPath, JSON.stringify(list, null, 2));
105
+ }
106
+ }
107
+ function generateCode() {
108
+ let code = '';
109
+ for (let i = 0; i < CODE_LENGTH; i++) {
110
+ code += SAFE_ALPHABET[Math.floor(Math.random() * SAFE_ALPHABET.length)];
111
+ }
112
+ return code;
113
+ }
114
+ //# sourceMappingURL=PairingStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PairingStore.js","sourceRoot":"","sources":["../src/PairingStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { SenderPolicy } from './types.js';
2
+ import type { PairingStore } from './PairingStore.js';
3
+ export interface SenderCheckResult {
4
+ allowed: boolean;
5
+ pairingCode?: string | null;
6
+ }
7
+ export declare class SenderGate {
8
+ private policy;
9
+ private allowedUsers;
10
+ private pairingStore;
11
+ constructor(policy: SenderPolicy, allowedUsers?: string[], pairingStore?: PairingStore);
12
+ check(senderId: string, senderName?: string): SenderCheckResult;
13
+ }
@@ -0,0 +1,34 @@
1
+ export class SenderGate {
2
+ policy;
3
+ allowedUsers;
4
+ pairingStore;
5
+ constructor(policy, allowedUsers = [], pairingStore) {
6
+ this.policy = policy;
7
+ this.allowedUsers = new Set(allowedUsers);
8
+ this.pairingStore = pairingStore || null;
9
+ }
10
+ check(senderId, senderName) {
11
+ switch (this.policy) {
12
+ case 'open':
13
+ return { allowed: true };
14
+ case 'allowlist':
15
+ return { allowed: this.allowedUsers.has(senderId) };
16
+ case 'pairing': {
17
+ // Check static allowlist first
18
+ if (this.allowedUsers.has(senderId)) {
19
+ return { allowed: true };
20
+ }
21
+ // Check dynamic approved list
22
+ if (this.pairingStore?.isApproved(senderId)) {
23
+ return { allowed: true };
24
+ }
25
+ // Generate pairing code
26
+ const code = this.pairingStore?.createRequest(senderId, senderName || senderId);
27
+ return { allowed: false, pairingCode: code ?? null };
28
+ }
29
+ default:
30
+ throw new Error(`Unknown sender policy: ${this.policy}`);
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=SenderGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SenderGate.js","sourceRoot":"","sources":["../src/SenderGate.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,UAAU;IACb,MAAM,CAAe;IACrB,YAAY,CAAc;IAC1B,YAAY,CAAsB;IAE1C,YACE,MAAoB,EACpB,eAAyB,EAAE,EAC3B,YAA2B;QAE3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;QAC1C,IAAI,CAAC,YAAY,GAAG,YAAY,IAAI,IAAI,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,QAAgB,EAAE,UAAmB;QACzC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,KAAK,MAAM;gBACT,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,KAAK,WAAW;gBACd,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,+BAA+B;gBAC/B,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;gBAC3B,CAAC;gBACD,8BAA8B;gBAC9B,IAAI,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;gBAC3B,CAAC;gBACD,wBAAwB;gBACxB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,aAAa,CAC3C,QAAQ,EACR,UAAU,IAAI,QAAQ,CACvB,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;YACvD,CAAC;YACD;gBACE,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,38 @@
1
+ import type { SessionScope, SessionTarget } from './types.js';
2
+ import type { AcpBridge } from './AcpBridge.js';
3
+ export declare class SessionRouter {
4
+ private toSession;
5
+ private toTarget;
6
+ private toCwd;
7
+ private bridge;
8
+ private defaultCwd;
9
+ private scope;
10
+ private persistPath;
11
+ constructor(bridge: AcpBridge, defaultCwd: string, scope?: SessionScope, persistPath?: string);
12
+ /** Replace the bridge instance (used after crash recovery restart). */
13
+ setBridge(bridge: AcpBridge): void;
14
+ private routingKey;
15
+ resolve(channelName: string, senderId: string, chatId: string, threadId?: string, cwd?: string): Promise<string>;
16
+ getTarget(sessionId: string): SessionTarget | undefined;
17
+ hasSession(channelName: string, senderId: string, chatId?: string): boolean;
18
+ removeSession(channelName: string, senderId: string, chatId?: string): boolean;
19
+ private deleteByKey;
20
+ /** Get all session entries for crash recovery. */
21
+ getAll(): Array<{
22
+ key: string;
23
+ sessionId: string;
24
+ target: SessionTarget;
25
+ }>;
26
+ /**
27
+ * Restore session mappings from a previous bridge.
28
+ * Called after bridge restart — attempts loadSession for each saved mapping.
29
+ * Failed loads are silently dropped (new session on next message).
30
+ */
31
+ restoreSessions(): Promise<{
32
+ restored: number;
33
+ failed: number;
34
+ }>;
35
+ /** Clear in-memory state and delete persist file. Used on clean shutdown. */
36
+ clearAll(): void;
37
+ private persist;
38
+ }
@@ -0,0 +1,172 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ export class SessionRouter {
3
+ toSession = new Map(); // routing key → session ID
4
+ toTarget = new Map(); // session ID → target
5
+ toCwd = new Map(); // session ID → cwd
6
+ bridge;
7
+ defaultCwd;
8
+ scope;
9
+ persistPath;
10
+ constructor(bridge, defaultCwd, scope = 'user', persistPath) {
11
+ this.bridge = bridge;
12
+ this.defaultCwd = defaultCwd;
13
+ this.scope = scope;
14
+ this.persistPath = persistPath;
15
+ }
16
+ /** Replace the bridge instance (used after crash recovery restart). */
17
+ setBridge(bridge) {
18
+ this.bridge = bridge;
19
+ }
20
+ routingKey(channelName, senderId, chatId, threadId) {
21
+ switch (this.scope) {
22
+ case 'thread':
23
+ return `${channelName}:${threadId || chatId}`;
24
+ case 'single':
25
+ return `${channelName}:__single__`;
26
+ case 'user':
27
+ default:
28
+ return `${channelName}:${senderId}:${chatId}`;
29
+ }
30
+ }
31
+ async resolve(channelName, senderId, chatId, threadId, cwd) {
32
+ const key = this.routingKey(channelName, senderId, chatId, threadId);
33
+ const existing = this.toSession.get(key);
34
+ if (existing) {
35
+ return existing;
36
+ }
37
+ const sessionCwd = cwd || this.defaultCwd;
38
+ const sessionId = await this.bridge.newSession(sessionCwd);
39
+ this.toSession.set(key, sessionId);
40
+ this.toTarget.set(sessionId, { channelName, senderId, chatId, threadId });
41
+ this.toCwd.set(sessionId, sessionCwd);
42
+ this.persist();
43
+ return sessionId;
44
+ }
45
+ getTarget(sessionId) {
46
+ return this.toTarget.get(sessionId);
47
+ }
48
+ hasSession(channelName, senderId, chatId) {
49
+ const key = chatId
50
+ ? this.routingKey(channelName, senderId, chatId)
51
+ : `${channelName}:${senderId}`;
52
+ // If chatId is provided, do exact lookup; otherwise prefix-scan for any match
53
+ if (chatId)
54
+ return this.toSession.has(key);
55
+ for (const k of this.toSession.keys()) {
56
+ if (k.startsWith(`${channelName}:${senderId}`))
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ removeSession(channelName, senderId, chatId) {
62
+ if (chatId) {
63
+ const key = this.routingKey(channelName, senderId, chatId);
64
+ return this.deleteByKey(key);
65
+ }
66
+ // No chatId: remove all sessions for this sender on this channel
67
+ let removed = false;
68
+ const prefix = `${channelName}:${senderId}`;
69
+ for (const k of [...this.toSession.keys()]) {
70
+ if (k.startsWith(prefix)) {
71
+ this.deleteByKey(k);
72
+ removed = true;
73
+ }
74
+ }
75
+ if (removed)
76
+ this.persist();
77
+ return removed;
78
+ }
79
+ deleteByKey(key) {
80
+ const sessionId = this.toSession.get(key);
81
+ if (!sessionId)
82
+ return false;
83
+ this.toSession.delete(key);
84
+ this.toTarget.delete(sessionId);
85
+ this.toCwd.delete(sessionId);
86
+ return true;
87
+ }
88
+ /** Get all session entries for crash recovery. */
89
+ getAll() {
90
+ const entries = [];
91
+ for (const [key, sessionId] of this.toSession) {
92
+ const target = this.toTarget.get(sessionId);
93
+ if (target) {
94
+ entries.push({ key, sessionId, target });
95
+ }
96
+ }
97
+ return entries;
98
+ }
99
+ /**
100
+ * Restore session mappings from a previous bridge.
101
+ * Called after bridge restart — attempts loadSession for each saved mapping.
102
+ * Failed loads are silently dropped (new session on next message).
103
+ */
104
+ async restoreSessions() {
105
+ if (!this.persistPath || !existsSync(this.persistPath)) {
106
+ return { restored: 0, failed: 0 };
107
+ }
108
+ let entries;
109
+ try {
110
+ entries = JSON.parse(readFileSync(this.persistPath, 'utf-8'));
111
+ }
112
+ catch {
113
+ return { restored: 0, failed: 0 };
114
+ }
115
+ let restored = 0;
116
+ let failed = 0;
117
+ for (const [key, entry] of Object.entries(entries)) {
118
+ try {
119
+ const sessionId = await this.bridge.loadSession(entry.sessionId, entry.cwd);
120
+ this.toSession.set(key, sessionId);
121
+ this.toTarget.set(sessionId, entry.target);
122
+ this.toCwd.set(sessionId, entry.cwd);
123
+ restored++;
124
+ }
125
+ catch {
126
+ // Session can't be loaded — will create fresh on next message
127
+ failed++;
128
+ }
129
+ }
130
+ // Update persist file to only include successfully restored sessions
131
+ if (failed > 0) {
132
+ this.persist();
133
+ }
134
+ return { restored, failed };
135
+ }
136
+ /** Clear in-memory state and delete persist file. Used on clean shutdown. */
137
+ clearAll() {
138
+ this.toSession.clear();
139
+ this.toTarget.clear();
140
+ this.toCwd.clear();
141
+ if (this.persistPath && existsSync(this.persistPath)) {
142
+ try {
143
+ unlinkSync(this.persistPath);
144
+ }
145
+ catch {
146
+ // best-effort
147
+ }
148
+ }
149
+ }
150
+ persist() {
151
+ if (!this.persistPath)
152
+ return;
153
+ const data = {};
154
+ for (const [key, sessionId] of this.toSession) {
155
+ const target = this.toTarget.get(sessionId);
156
+ if (target) {
157
+ data[key] = {
158
+ sessionId,
159
+ target,
160
+ cwd: this.toCwd.get(sessionId) || this.defaultCwd,
161
+ };
162
+ }
163
+ }
164
+ try {
165
+ writeFileSync(this.persistPath, JSON.stringify(data, null, 2), 'utf-8');
166
+ }
167
+ catch {
168
+ // best-effort — don't break message flow for persistence failure
169
+ }
170
+ }
171
+ }
172
+ //# sourceMappingURL=SessionRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRouter.js","sourceRoot":"","sources":["../src/SessionRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export { AcpBridge } from './AcpBridge.js';
2
+ export type { AcpBridgeOptions, AvailableCommand, ToolCallEvent, } from './AcpBridge.js';
3
+ export { ChannelBase } from './ChannelBase.js';
4
+ export type { ChannelBaseOptions } from './ChannelBase.js';
5
+ export { PairingStore } from './PairingStore.js';
6
+ export type { PairingRequest } from './PairingStore.js';
7
+ export { GroupGate } from './GroupGate.js';
8
+ export type { GroupCheckResult } from './GroupGate.js';
9
+ export { SenderGate } from './SenderGate.js';
10
+ export type { SenderCheckResult } from './SenderGate.js';
11
+ export { SessionRouter } from './SessionRouter.js';
12
+ export type { ChannelConfig, ChannelPlugin, ChannelType, Envelope, GroupConfig, GroupPolicy, SenderPolicy, SessionScope, SessionTarget, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { AcpBridge } from './AcpBridge.js';
2
+ export { ChannelBase } from './ChannelBase.js';
3
+ export { PairingStore } from './PairingStore.js';
4
+ export { GroupGate } from './GroupGate.js';
5
+ export { SenderGate } from './SenderGate.js';
6
+ export { SessionRouter } from './SessionRouter.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAM3C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { AcpBridge } from './AcpBridge.js';
2
+ import type { ChannelBase, ChannelBaseOptions } from './ChannelBase.js';
3
+ export type SenderPolicy = 'allowlist' | 'pairing' | 'open';
4
+ export type SessionScope = 'user' | 'thread' | 'single';
5
+ export type ChannelType = string;
6
+ export type GroupPolicy = 'disabled' | 'allowlist' | 'open';
7
+ export interface GroupConfig {
8
+ requireMention?: boolean;
9
+ }
10
+ export interface ChannelConfig {
11
+ type: ChannelType;
12
+ token: string;
13
+ clientId?: string;
14
+ clientSecret?: string;
15
+ senderPolicy: SenderPolicy;
16
+ allowedUsers: string[];
17
+ sessionScope: SessionScope;
18
+ cwd: string;
19
+ approvalMode?: string;
20
+ instructions?: string;
21
+ model?: string;
22
+ groupPolicy: GroupPolicy;
23
+ groups: Record<string, GroupConfig>;
24
+ }
25
+ export interface Envelope {
26
+ channelName: string;
27
+ senderId: string;
28
+ senderName: string;
29
+ chatId: string;
30
+ text: string;
31
+ threadId?: string;
32
+ /** Platform-specific message ID for response correlation. */
33
+ messageId?: string;
34
+ isGroup: boolean;
35
+ isMentioned: boolean;
36
+ isReplyToBot: boolean;
37
+ /** Text of the message being replied to (quoted/referenced message). */
38
+ referencedText?: string;
39
+ /** Base64-encoded image data (e.g. from WeChat CDN download). */
40
+ imageBase64?: string;
41
+ /** MIME type for the image (e.g. "image/jpeg", "image/png"). */
42
+ imageMimeType?: string;
43
+ }
44
+ export interface SessionTarget {
45
+ channelName: string;
46
+ senderId: string;
47
+ chatId: string;
48
+ threadId?: string;
49
+ }
50
+ /**
51
+ * A channel plugin registers a channel type and provides a factory
52
+ * to create adapter instances. Both built-in adapters and external
53
+ * plugins conform to this interface.
54
+ */
55
+ export interface ChannelPlugin {
56
+ /** Unique channel type ID (e.g., "telegram", "tmcp-dingtalk"). */
57
+ channelType: string;
58
+ /** Human-readable name for CLI output. */
59
+ displayName: string;
60
+ /**
61
+ * Config fields required by this channel type, beyond the shared
62
+ * ChannelConfig fields. Validated at startup.
63
+ */
64
+ requiredConfigFields?: string[];
65
+ /** Create a channel adapter instance. */
66
+ createChannel(name: string, config: ChannelConfig & Record<string, unknown>, bridge: AcpBridge, options?: ChannelBaseOptions): ChannelBase;
67
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@qwen-code/channel-base",
3
+ "version": "0.14.0-preview.1",
4
+ "description": "Base channel infrastructure for Qwen Code",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc --build"
19
+ },
20
+ "dependencies": {
21
+ "@agentclientprotocol/sdk": "^0.14.1"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.0.0"
25
+ }
26
+ }