feique 1.4.0 → 1.5.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,372 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import { spawn } from 'node:child_process';
5
+ import { registerBackend } from './registry.js';
6
+ export class QwenBackend {
7
+ config;
8
+ qwenHomeDir;
9
+ name = 'qwen';
10
+ constructor(config, qwenHomeDir = resolveQwenHomeDir()) {
11
+ this.config = config;
12
+ this.qwenHomeDir = qwenHomeDir;
13
+ }
14
+ async run(options) {
15
+ const args = this.buildArgs(options);
16
+ const spawnSpec = this.buildSpawnSpec(args);
17
+ options.logger.info({ command: spawnSpec.command, args: spawnSpec.args, workdir: options.workdir }, 'Starting Qwen turn');
18
+ return await new Promise((resolve, reject) => {
19
+ const processRef = spawn(spawnSpec.command, spawnSpec.args, {
20
+ cwd: options.workdir,
21
+ env: {
22
+ ...process.env,
23
+ NO_COLOR: '1',
24
+ },
25
+ stdio: ['ignore', 'pipe', 'pipe'],
26
+ });
27
+ let stderr = '';
28
+ let stdoutBuffer = '';
29
+ let sessionId = options.sessionId;
30
+ let finalMessage = '';
31
+ let inputTokens;
32
+ let outputTokens;
33
+ let settled = false;
34
+ let timeoutHandle;
35
+ let abortCleanup;
36
+ const finishReject = (error) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ cleanup();
41
+ reject(error);
42
+ };
43
+ const finishResolve = (result) => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ cleanup();
48
+ resolve(result);
49
+ };
50
+ const cleanup = () => {
51
+ if (timeoutHandle)
52
+ clearTimeout(timeoutHandle);
53
+ abortCleanup?.();
54
+ abortCleanup = undefined;
55
+ };
56
+ const abortRun = (reason) => {
57
+ if (processRef.killed)
58
+ return;
59
+ const message = typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : 'Qwen run aborted';
60
+ const error = new Error(message);
61
+ error.name = 'AbortError';
62
+ processRef.kill('SIGTERM');
63
+ setTimeout(() => {
64
+ if (!processRef.killed)
65
+ processRef.kill('SIGKILL');
66
+ }, 3000).unref();
67
+ finishReject(error);
68
+ };
69
+ if (options.timeoutMs && options.timeoutMs > 0) {
70
+ timeoutHandle = setTimeout(() => {
71
+ abortRun(`Qwen timed out after ${options.timeoutMs}ms`);
72
+ }, options.timeoutMs);
73
+ timeoutHandle.unref();
74
+ }
75
+ if (options.signal) {
76
+ const onAbort = () => abortRun(options.signal?.reason ?? 'Qwen run aborted');
77
+ if (options.signal.aborted) {
78
+ onAbort();
79
+ return;
80
+ }
81
+ options.signal.addEventListener('abort', onAbort, { once: true });
82
+ abortCleanup = () => options.signal?.removeEventListener('abort', onAbort);
83
+ }
84
+ if (typeof processRef.pid === 'number') {
85
+ void options.onSpawn?.(processRef.pid);
86
+ }
87
+ processRef.stdout.on('data', async (chunk) => {
88
+ stdoutBuffer += chunk.toString('utf8');
89
+ const lines = stdoutBuffer.split(/\r?\n/);
90
+ stdoutBuffer = lines.pop() ?? '';
91
+ for (const line of lines) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed.startsWith('{'))
94
+ continue;
95
+ try {
96
+ const event = JSON.parse(trimmed);
97
+ // Session id is emitted on both init and result events.
98
+ if (event.session_id) {
99
+ sessionId = event.session_id;
100
+ }
101
+ // Extract final text + usage from result event.
102
+ if (event.type === 'result') {
103
+ if (typeof event.result === 'string') {
104
+ finalMessage = event.result;
105
+ }
106
+ if (event.usage) {
107
+ if (typeof event.usage.input_tokens === 'number')
108
+ inputTokens = event.usage.input_tokens;
109
+ if (typeof event.usage.output_tokens === 'number')
110
+ outputTokens = event.usage.output_tokens;
111
+ }
112
+ }
113
+ // Fall back to assistant text content if result event never arrives.
114
+ if (event.type === 'assistant' && event.message?.content) {
115
+ const texts = event.message.content
116
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
117
+ .map(c => c.text)
118
+ .join('\n');
119
+ if (texts) {
120
+ finalMessage = texts;
121
+ }
122
+ }
123
+ const backendEvent = {
124
+ type: event.type,
125
+ session_id: event.session_id ?? sessionId,
126
+ message: typeof event.result === 'string' ? event.result : undefined,
127
+ };
128
+ await options.onEvent?.(backendEvent);
129
+ }
130
+ catch {
131
+ options.logger.debug({ line }, 'Ignoring unparsable Qwen line');
132
+ }
133
+ }
134
+ });
135
+ processRef.stderr.on('data', (chunk) => {
136
+ stderr += chunk.toString('utf8');
137
+ });
138
+ processRef.on('error', (error) => {
139
+ finishReject(error instanceof Error ? error : new Error(String(error)));
140
+ });
141
+ processRef.on('close', (exitCode) => {
142
+ if (settled)
143
+ return;
144
+ if ((exitCode ?? 1) !== 0 && !finalMessage) {
145
+ finishReject(new Error(`Qwen exited with code ${exitCode ?? 1}: ${stderr.trim() || 'no stderr output'}`));
146
+ return;
147
+ }
148
+ finishResolve({
149
+ sessionId,
150
+ finalMessage: finalMessage.trim(),
151
+ stderr: stderr.trim(),
152
+ exitCode: exitCode ?? 0,
153
+ inputTokens,
154
+ outputTokens,
155
+ });
156
+ });
157
+ });
158
+ }
159
+ summarizeEvent(event) {
160
+ if (event.type === 'error' && typeof event.message === 'string') {
161
+ return `Qwen 错误:${event.message}`;
162
+ }
163
+ return null;
164
+ }
165
+ async listProjectSessions(projectRoot, limit = 10) {
166
+ const sessions = await this.scanSessions();
167
+ const matches = [];
168
+ for (const session of sessions) {
169
+ const match = scoreSessionMatch(projectRoot, session.cwd);
170
+ if (!match)
171
+ continue;
172
+ matches.push({
173
+ ...session,
174
+ matchKind: match.kind,
175
+ matchScore: match.score,
176
+ });
177
+ }
178
+ return matches
179
+ .sort((a, b) => {
180
+ const scoreDelta = (b.matchScore ?? 0) - (a.matchScore ?? 0);
181
+ if (scoreDelta !== 0)
182
+ return scoreDelta;
183
+ return b.updatedAt.localeCompare(a.updatedAt);
184
+ })
185
+ .slice(0, limit);
186
+ }
187
+ async findLatestSession(projectRoot) {
188
+ const [session] = await this.listProjectSessions(projectRoot, 1);
189
+ return session ?? null;
190
+ }
191
+ async findSessionById(projectRoot, sessionId) {
192
+ const sessions = await this.scanSessions();
193
+ const candidate = sessions.find(s => s.sessionId === sessionId);
194
+ if (!candidate)
195
+ return null;
196
+ const match = scoreSessionMatch(projectRoot, candidate.cwd);
197
+ if (!match)
198
+ return null;
199
+ return { ...candidate, matchKind: match.kind, matchScore: match.score };
200
+ }
201
+ buildArgs(options) {
202
+ const args = ['-p'];
203
+ args.push('--output-format', 'stream-json');
204
+ const approvalMode = options.projectConfig?.approvalMode ?? this.config.defaultApprovalMode;
205
+ args.push('--approval-mode', approvalMode);
206
+ const model = options.projectConfig?.model ?? this.config.defaultModel;
207
+ if (model) {
208
+ args.push('--model', model);
209
+ }
210
+ const allowedTools = options.projectConfig?.allowedTools ?? this.config.allowedTools;
211
+ if (allowedTools && allowedTools.length > 0) {
212
+ args.push('--allowed-tools', ...allowedTools);
213
+ }
214
+ const systemPromptAppend = options.projectConfig?.systemPromptAppend ?? this.config.systemPromptAppend;
215
+ if (systemPromptAppend) {
216
+ args.push('--append-system-prompt', systemPromptAppend);
217
+ }
218
+ if (options.sessionId) {
219
+ args.push('--resume', options.sessionId);
220
+ }
221
+ args.push(options.prompt);
222
+ return args;
223
+ }
224
+ buildSpawnSpec(qwenArgs) {
225
+ if (!this.config.preExec) {
226
+ return { command: this.config.bin, args: qwenArgs };
227
+ }
228
+ const shell = this.config.shell ?? process.env.SHELL ?? '/bin/zsh';
229
+ const chainedCommand = `${this.config.preExec} && ${quoteShellCommand([this.config.bin, ...qwenArgs])}`;
230
+ return { command: shell, args: ['-ic', chainedCommand] };
231
+ }
232
+ /**
233
+ * Walk `~/.qwen/projects/<slug>/chats/*.jsonl` and parse the first
234
+ * line of each file to extract `sessionId` + `cwd` + `timestamp`.
235
+ *
236
+ * Qwen encodes the project directory as the slug (e.g. `/Users/dh` →
237
+ * `-Users-dh`). We don't depend on that mapping — we walk every slug
238
+ * and match by parsed `cwd`.
239
+ */
240
+ async scanSessions() {
241
+ const projectsDir = path.join(this.qwenHomeDir, 'projects');
242
+ const sessions = new Map();
243
+ let slugEntries;
244
+ try {
245
+ slugEntries = await fs.readdir(projectsDir);
246
+ }
247
+ catch {
248
+ return [];
249
+ }
250
+ for (const slug of slugEntries) {
251
+ const chatsDir = path.join(projectsDir, slug, 'chats');
252
+ const chatFiles = await fs.readdir(chatsDir).catch(() => []);
253
+ for (const chatFile of chatFiles) {
254
+ if (!chatFile.endsWith('.jsonl'))
255
+ continue;
256
+ const filePath = path.join(chatsDir, chatFile);
257
+ const stat = await fs.stat(filePath).catch(() => null);
258
+ if (!stat?.isFile())
259
+ continue;
260
+ try {
261
+ const content = await fs.readFile(filePath, 'utf8');
262
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trim();
263
+ if (!firstLine)
264
+ continue;
265
+ const parsed = JSON.parse(firstLine);
266
+ const sessionId = parsed.sessionId;
267
+ const cwd = parsed.cwd;
268
+ if (!sessionId || !cwd)
269
+ continue;
270
+ const createdAt = parsed.timestamp;
271
+ const updatedAt = new Date(stat.mtimeMs).toISOString();
272
+ const existing = sessions.get(sessionId);
273
+ if (!existing || existing.updatedAt < updatedAt) {
274
+ const record = {
275
+ sessionId,
276
+ cwd,
277
+ updatedAt,
278
+ filePath,
279
+ source: 'sessions',
280
+ backend: 'qwen',
281
+ };
282
+ if (createdAt) {
283
+ record.createdAt = createdAt;
284
+ }
285
+ sessions.set(sessionId, record);
286
+ }
287
+ }
288
+ catch {
289
+ continue;
290
+ }
291
+ }
292
+ }
293
+ return [...sessions.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
294
+ }
295
+ }
296
+ function resolveQwenHomeDir() {
297
+ const configured = process.env.QWEN_HOME?.trim();
298
+ if (!configured)
299
+ return path.join(os.homedir(), '.qwen');
300
+ if (configured === '~')
301
+ return os.homedir();
302
+ if (configured.startsWith('~/'))
303
+ return path.join(os.homedir(), configured.slice(2));
304
+ return path.resolve(configured);
305
+ }
306
+ function quoteShellArg(value) {
307
+ if (value.length === 0)
308
+ return "''";
309
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
310
+ }
311
+ function quoteShellCommand(parts) {
312
+ return parts.map(quoteShellArg).join(' ');
313
+ }
314
+ const FUZZY_SUFFIX_TOKENS = new Set(['bridge', 'repo', 'project', 'workspace']);
315
+ function scoreSessionMatch(projectRoot, sessionCwd) {
316
+ const normalizedProjectRoot = path.resolve(projectRoot).replace(/\/+$/, '').toLowerCase();
317
+ const normalizedSessionRoot = path.resolve(sessionCwd).replace(/\/+$/, '').toLowerCase();
318
+ if (normalizedProjectRoot === normalizedSessionRoot) {
319
+ return { kind: 'exact-root', score: 100 };
320
+ }
321
+ const projectBase = path.basename(normalizedProjectRoot);
322
+ const sessionBase = path.basename(normalizedSessionRoot);
323
+ if (projectBase === sessionBase) {
324
+ return { kind: 'basename', score: 80 };
325
+ }
326
+ const normalizedProjectName = normalizeProjectName(projectBase);
327
+ const normalizedSessionName = normalizeProjectName(sessionBase);
328
+ if (normalizedProjectName && normalizedProjectName === normalizedSessionName) {
329
+ return { kind: 'normalized-name', score: 60 };
330
+ }
331
+ if (normalizedProjectName.length >= 5 && normalizedSessionName.includes(normalizedProjectName)) {
332
+ return { kind: 'basename-contains', score: 40 };
333
+ }
334
+ return null;
335
+ }
336
+ function normalizeProjectName(input) {
337
+ const tokens = input
338
+ .toLowerCase()
339
+ .replace(/\.git$/, '')
340
+ .split(/[^a-z0-9]+/)
341
+ .filter(Boolean);
342
+ const filtered = tokens.filter(token => !FUZZY_SUFFIX_TOKENS.has(token));
343
+ return (filtered.length > 0 ? filtered : tokens).join('-');
344
+ }
345
+ // ---------------------------------------------------------------------------
346
+ // Registry definition
347
+ // ---------------------------------------------------------------------------
348
+ export const qwenBackendDefinition = {
349
+ name: 'qwen',
350
+ create(config) {
351
+ return new QwenBackend({
352
+ bin: config.qwen?.bin ?? 'qwen',
353
+ shell: config.qwen?.shell ?? config.codex.shell,
354
+ preExec: config.qwen?.pre_exec ?? config.codex.pre_exec,
355
+ defaultApprovalMode: config.qwen?.default_approval_mode ?? 'default',
356
+ defaultModel: config.qwen?.default_model,
357
+ allowedTools: config.qwen?.allowed_tools,
358
+ systemPromptAppend: config.qwen?.system_prompt_append,
359
+ runTimeoutMs: config.qwen?.run_timeout_ms ?? config.codex.run_timeout_ms,
360
+ });
361
+ },
362
+ probeSpec(config) {
363
+ return {
364
+ bin: config.qwen?.bin ?? 'qwen',
365
+ shell: config.qwen?.shell ?? config.codex.shell,
366
+ preExec: config.qwen?.pre_exec ?? config.codex.pre_exec,
367
+ };
368
+ },
369
+ defaultFallback: ['claude', 'codex'],
370
+ };
371
+ registerBackend(qwenBackendDefinition);
372
+ //# sourceMappingURL=qwen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qwen.js","sourceRoot":"","sources":["../../src/backend/qwen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import type { BridgeConfig } from '../config/schema.js';
2
+ import type { Backend, BackendName } from './types.js';
3
+ import type { ProbeSpec } from './probe.js';
4
+ import type { CodexSessionIndex } from '../codex/session-index.js';
5
+ /**
6
+ * Extensible backend registry.
7
+ *
8
+ * Every backend (codex, claude, qwen, ...) contributes a BackendDefinition
9
+ * that knows how to construct itself from BridgeConfig and how to expose
10
+ * its probe spec for startup failover. Adding a new backend is purely an
11
+ * additive change: drop a new file under src/backend/, export its
12
+ * definition, call registerBackend() at module load, and wire it into
13
+ * src/backend/factory.ts's side-effect import list.
14
+ *
15
+ * BackendName is intentionally widened to `string` (see types.ts) —
16
+ * compile-time literal unions were incompatible with a registry because
17
+ * every new backend would require touching the type. Runtime validation
18
+ * happens in requireBackendDefinition().
19
+ */
20
+ export interface BackendDependencies {
21
+ /**
22
+ * Shared CodexSessionIndex, passed in by FeiqueService. Any backend
23
+ * that wants access can read it from here; codex relies on it for
24
+ * resume semantics.
25
+ */
26
+ codexSessionIndex?: CodexSessionIndex;
27
+ }
28
+ export interface BackendDefinition {
29
+ /** Unique registry key, e.g. 'codex', 'claude', 'qwen'. */
30
+ readonly name: string;
31
+ /**
32
+ * Build a live Backend instance from config. Called on every run so
33
+ * hot-reloaded config is picked up immediately.
34
+ */
35
+ create(config: BridgeConfig, deps: BackendDependencies): Backend;
36
+ /**
37
+ * Extract the probe spec (bin + shell + pre_exec) from config.
38
+ * Used by the failover resolver before each run.
39
+ */
40
+ probeSpec(config: BridgeConfig): ProbeSpec;
41
+ /**
42
+ * Optional default fallback chain. Used when neither the project nor
43
+ * the global config supplies an explicit fallback list. If omitted,
44
+ * the resolver falls back to "every other registered backend" in
45
+ * registration order.
46
+ *
47
+ * Example: codex's default fallback is ['claude'] so that when codex
48
+ * is broken on a box, users get claude automatically without needing
49
+ * to set anything.
50
+ */
51
+ readonly defaultFallback?: readonly string[];
52
+ }
53
+ export declare function registerBackend(definition: BackendDefinition): void;
54
+ export declare function getBackendDefinition(name: string): BackendDefinition | undefined;
55
+ export declare function requireBackendDefinition(name: string): BackendDefinition;
56
+ export declare function listBackendNames(): BackendName[];
57
+ /** Visible for tests only. */
58
+ export declare function clearBackendRegistry(): void;
@@ -0,0 +1,23 @@
1
+ const registry = new Map();
2
+ export function registerBackend(definition) {
3
+ registry.set(definition.name, definition);
4
+ }
5
+ export function getBackendDefinition(name) {
6
+ return registry.get(name);
7
+ }
8
+ export function requireBackendDefinition(name) {
9
+ const def = registry.get(name);
10
+ if (!def) {
11
+ const known = [...registry.keys()].join(', ') || '(none)';
12
+ throw new Error(`Unknown backend: ${name}. Registered backends: ${known}`);
13
+ }
14
+ return def;
15
+ }
16
+ export function listBackendNames() {
17
+ return [...registry.keys()];
18
+ }
19
+ /** Visible for tests only. */
20
+ export function clearBackendRegistry() {
21
+ registry.clear();
22
+ }
23
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/backend/registry.ts"],"names":[],"mappings":"AA2DA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;AAEtD,MAAM,UAAU,eAAe,CAAC,UAA6B;IAC3D,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,0BAA0B,KAAK,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,oBAAoB;IAClC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC"}
@@ -1,5 +1,10 @@
1
1
  import type { Logger } from '../logging.js';
2
- export type BackendName = 'codex' | 'claude';
2
+ /**
3
+ * Backend identifier. Intentionally typed as `string` (not a literal union)
4
+ * because backends are registered at runtime via src/backend/registry.ts.
5
+ * Use requireBackendDefinition() to validate at the boundary.
6
+ */
7
+ export type BackendName = string;
3
8
  export interface BackendEvent {
4
9
  type?: string;
5
10
  session_id?: string;
@@ -90,6 +90,7 @@ export type BridgeCommand = {
90
90
  value?: string;
91
91
  } | {
92
92
  kind: 'backend';
93
+ action?: 'list';
93
94
  name?: string;
94
95
  } | {
95
96
  kind: 'team';
@@ -38,8 +38,13 @@ export function parseBridgeCommand(input) {
38
38
  return parseSessionCommand(argument);
39
39
  case '/admin':
40
40
  return parseAdminCommand(argument);
41
- case '/backend':
42
- return { kind: 'backend', name: argument || undefined };
41
+ case '/backend': {
42
+ const arg = (argument || '').trim().toLowerCase();
43
+ if (arg === 'list' || arg === 'ls' || arg === '列表' || arg === '所有') {
44
+ return { kind: 'backend', action: 'list' };
45
+ }
46
+ return { kind: 'backend', name: arg || undefined };
47
+ }
43
48
  case '/team':
44
49
  return { kind: 'team' };
45
50
  case '/learn':
@@ -85,7 +90,7 @@ export function buildHelpText() {
85
90
  '/projects 查看可用项目',
86
91
  '/project <名称> 切换项目',
87
92
  '/status 查看当前状态',
88
- '/backend codex|claude 切换后端',
93
+ '/backend codex|claude|qwen 切换后端',
89
94
  '/team 查看团队成员活动',
90
95
  '/learn <内容> 记录团队知识',
91
96
  '/recall <关键词> 检索知识',
@@ -113,8 +118,10 @@ export function buildFullHelpText() {
113
118
  '',
114
119
  '后端切换',
115
120
  '/backend 查看当前项目的活跃后端',
121
+ '/backend list 列出所有已注册后端 + fallback 链 + failover 开关',
116
122
  '/backend codex 切换当前项目到 Codex 后端',
117
123
  '/backend claude 切换当前项目到 Claude Code 后端',
124
+ '/backend qwen 切换当前项目到 Qwen Code 后端',
118
125
  '',
119
126
  '会话管理',
120
127
  '/session list 列出当前项目保存过的会话',
@@ -283,6 +290,8 @@ export function describeBridgeCommand(command) {
283
290
  }
284
291
  return `管理员操作: ${command.resource} ${command.action}${'value' in command && command.value ? ` ${command.value}` : ''}`;
285
292
  case 'backend':
293
+ if (command.action === 'list')
294
+ return '列出所有已注册后端';
286
295
  return command.name ? `切换后端到 ${command.name}` : '查看当前后端';
287
296
  case 'team':
288
297
  return '查看团队协作态势';
@@ -337,7 +346,7 @@ export function isReadOnlyCommand(command) {
337
346
  case 'session':
338
347
  return command.action === 'list';
339
348
  case 'backend':
340
- return !command.name;
349
+ return command.action === 'list' || !command.name;
341
350
  case 'team':
342
351
  case 'recall':
343
352
  case 'insights':
@@ -498,17 +507,17 @@ function parseNaturalLanguageCommand(input) {
498
507
  }
499
508
  // ── /backend: 后端切换(必须在项目切换之前,否则 "切到 claude" 会被误判为切换项目)──
500
509
  // Pattern 1: 带"后端"关键词 — "切换后端到 claude" / "后端换成 codex"
501
- const backendWithKeyword = normalized.match(/^(?:切换(?:到|为)?|使用|换(?:到|成)?|改(?:到|为|用)?)?\s*(?:后端(?:(?:切换)?(?:到|为)?)?)\s*(codex|claude)\s*(?:后端)?$/i);
510
+ const backendWithKeyword = normalized.match(/^(?:切换(?:到|为)?|使用|换(?:到|成)?|改(?:到|为|用)?)?\s*(?:后端(?:(?:切换)?(?:到|为)?)?)\s*(codex|claude|qwen)\s*(?:后端)?$/i);
502
511
  if (backendWithKeyword) {
503
512
  return { kind: 'backend', name: backendWithKeyword[1].toLowerCase() };
504
513
  }
505
514
  // Pattern 2: 不带"后端"— "用 claude" / "换成 codex" / "切到 claude" / "改用 codex"
506
- const backendDirect = normalized.match(/^(?:用|使用|换(?:成|到)?|切(?:换?(?:到|成)?)?|改(?:用|成|到)?|转(?:到|成)?)\s*(codex|claude)(?:\s*(?:吧|看看|试试|帮我|来))?$/i);
515
+ const backendDirect = normalized.match(/^(?:用|使用|换(?:成|到)?|切(?:换?(?:到|成)?)?|改(?:用|成|到)?|转(?:到|成)?)\s*(codex|claude|qwen)(?:\s*(?:吧|看看|试试|帮我|来))?$/i);
507
516
  if (backendDirect) {
508
517
  return { kind: 'backend', name: backendDirect[1].toLowerCase() };
509
518
  }
510
519
  // Pattern 3: "codex/claude + 动词" — "claude 来" / "codex 帮我"
511
- const backendNameFirst = normalized.match(/^(codex|claude)\s*(?:来(?:吧)?|帮我|试试|处理|干活|上)$/i);
520
+ const backendNameFirst = normalized.match(/^(codex|claude|qwen)\s*(?:来(?:吧)?|帮我|试试|处理|干活|上)$/i);
512
521
  if (backendNameFirst) {
513
522
  return { kind: 'backend', name: backendNameFirst[1].toLowerCase() };
514
523
  }
@@ -520,13 +529,27 @@ function parseNaturalLanguageCommand(input) {
520
529
  if (/^(?:现在|当前)?用的(?:什么|哪个)(?:后端|backend)?$/.test(normalized)) {
521
530
  return { kind: 'backend' };
522
531
  }
532
+ // Pattern 5b: 列出所有后端 / 有哪些后端 / 后端有哪些 / 支持哪些后端
533
+ if (/^(?:列出|列|查看|看|看看|显示|给我看|show|list)?\s*(?:所有|全部|已(?:注册|支持))?\s*(?:(?:可用|支持)(?:的)?)?\s*(?:后端|backend)s?$/i.test(normalized)
534
+ || /^(?:有|支持)哪些(?:后端|backend)s?$/.test(normalized)
535
+ || /^(?:后端|backend)s?\s*(?:有|列表|清单|列出)$/i.test(normalized)
536
+ || /^(?:后端|backend)s?\s*有哪些$/i.test(normalized)
537
+ || /^(?:list|show)\s+(?:all\s+)?backends?$/i.test(normalized)
538
+ || /^(?:which|what)\s+backends?(?:\s+are\s+(?:available|supported))?\??$/i.test(normalized)) {
539
+ return { kind: 'backend', action: 'list' };
540
+ }
541
+ // Pattern 5c: 查看 fallback 链 / 降级顺序
542
+ if (/^(?:查看?|看|显示)?\s*(?:fallback|降级|备用|故障转移)\s*(?:链|顺序|列表|配置)?$/i.test(normalized)
543
+ || /^fallback\s*chain\??$/i.test(normalized)) {
544
+ return { kind: 'backend', action: 'list' };
545
+ }
523
546
  // Pattern 6: English — "switch to claude" / "use codex" / "change to claude"
524
- const backendEnglish = normalized.match(/^(?:switch(?:\s+backend)?(?:\s+to)?|use|change(?:\s+backend)?(?:\s+to)?|backend)\s+(codex|claude)$/i);
547
+ const backendEnglish = normalized.match(/^(?:switch(?:\s+backend)?(?:\s+to)?|use|change(?:\s+backend)?(?:\s+to)?|backend)\s+(codex|claude|qwen)$/i);
525
548
  if (backendEnglish) {
526
549
  return { kind: 'backend', name: backendEnglish[1].toLowerCase() };
527
550
  }
528
551
  // Pattern 7: English name-first — "claude please" / "codex go"
529
- const backendEnglishNameFirst = normalized.match(/^(codex|claude)\s+(?:please|go|now|backend)$/i);
552
+ const backendEnglishNameFirst = normalized.match(/^(codex|claude|qwen)\s+(?:please|go|now|backend)$/i);
530
553
  if (backendEnglishNameFirst) {
531
554
  return { kind: 'backend', name: backendEnglishNameFirst[1].toLowerCase() };
532
555
  }