feat(cli): 添加 CLI 版本检测和会话验证功能

- 新增 CLI 版本检测功能,支持检测 CLI 版本并缓存结果
- 实现会话验证方法,用于检查当前会话是否有效
- 在连接处理中集成 CLI 版本检测和会话验证逻辑
- 优化 WebViewProvider 中的初始化流程,支持背景初始化
- 更新消息处理逻辑,增加与 CLI 相关的错误处理
This commit is contained in:
yiliang114
2025-11-28 01:13:57 +08:00
parent b986692f94
commit 8bc9bea5a1
9 changed files with 644 additions and 5 deletions

View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js';
/**
* CLI Context Manager
*
* Manages the current CLI context including version information and feature availability
*/
export class CliContextManager {
private static instance: CliContextManager;
private currentVersionInfo: CliVersionInfo | null = null;
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): CliContextManager {
if (!CliContextManager.instance) {
CliContextManager.instance = new CliContextManager();
}
return CliContextManager.instance;
}
/**
* Set current CLI version information
*
* @param versionInfo - CLI version information
*/
setCurrentVersionInfo(versionInfo: CliVersionInfo): void {
this.currentVersionInfo = versionInfo;
}
/**
* Get current CLI version information
*
* @returns Current CLI version information or null if not set
*/
getCurrentVersionInfo(): CliVersionInfo | null {
return this.currentVersionInfo;
}
/**
* Get current CLI feature flags
*
* @returns Current CLI feature flags or default flags if not set
*/
getCurrentFeatures(): CliFeatureFlags {
if (this.currentVersionInfo) {
return this.currentVersionInfo.features;
}
// Return default feature flags (all disabled)
return {
supportsSessionList: false,
supportsSessionLoad: false,
supportsSessionSave: false,
};
}
/**
* Check if current CLI supports session/list method
*
* @returns Whether session/list is supported
*/
supportsSessionList(): boolean {
return this.getCurrentFeatures().supportsSessionList;
}
/**
* Check if current CLI supports session/load method
*
* @returns Whether session/load is supported
*/
supportsSessionLoad(): boolean {
return this.getCurrentFeatures().supportsSessionLoad;
}
/**
* Check if current CLI supports session/save method
*
* @returns Whether session/save is supported
*/
supportsSessionSave(): boolean {
return this.getCurrentFeatures().supportsSessionSave;
}
/**
* Check if CLI is installed and detected
*
* @returns Whether CLI is installed
*/
isCliInstalled(): boolean {
return this.currentVersionInfo?.detectionResult.isInstalled ?? false;
}
/**
* Get CLI version string
*
* @returns CLI version string or undefined if not detected
*/
getCliVersion(): string | undefined {
return this.currentVersionInfo?.version;
}
/**
* Check if CLI version is supported
*
* @returns Whether CLI version is supported
*/
isCliVersionSupported(): boolean {
return this.currentVersionInfo?.isSupported ?? false;
}
/**
* Clear current CLI context
*/
clearContext(): void {
this.currentVersionInfo = null;
}
}

View File

@@ -0,0 +1,129 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export interface CliDetectionResult {
isInstalled: boolean;
cliPath?: string;
version?: string;
error?: string;
}
/**
* Detects if Qwen Code CLI is installed and accessible
*/
export class CliDetector {
private static cachedResult: CliDetectionResult | null = null;
private static lastCheckTime: number = 0;
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
/**
* Checks if the Qwen Code CLI is installed
* @param forceRefresh - Force a new check, ignoring cache
* @returns Detection result with installation status and details
*/
static async detectQwenCli(
forceRefresh = false,
): Promise<CliDetectionResult> {
const now = Date.now();
// Return cached result if available and not expired
if (
!forceRefresh &&
this.cachedResult &&
now - this.lastCheckTime < this.CACHE_DURATION_MS
) {
return this.cachedResult;
}
try {
const isWindows = process.platform === 'win32';
const whichCommand = isWindows ? 'where' : 'which';
// Check if qwen command exists
try {
const { stdout } = await execAsync(`${whichCommand} qwen`, {
timeout: 5000,
});
const cliPath = stdout.trim().split('\n')[0];
// Try to get version
let version: string | undefined;
try {
const { stdout: versionOutput } = await execAsync('qwen --version', {
timeout: 5000,
});
version = versionOutput.trim();
} catch {
// Version check failed, but CLI is installed
}
this.cachedResult = {
isInstalled: true,
cliPath,
version,
};
this.lastCheckTime = now;
return this.cachedResult;
} catch (_error) {
// CLI not found
this.cachedResult = {
isInstalled: false,
error: `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`,
};
this.lastCheckTime = now;
return this.cachedResult;
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.cachedResult = {
isInstalled: false,
error: `Failed to detect Qwen Code CLI: ${errorMessage}`,
};
this.lastCheckTime = now;
return this.cachedResult;
}
}
/**
* Clears the cached detection result
*/
static clearCache(): void {
this.cachedResult = null;
this.lastCheckTime = 0;
}
/**
* Gets installation instructions based on the platform
*/
static getInstallationInstructions(): {
title: string;
steps: string[];
documentationUrl: string;
} {
return {
title: 'Qwen Code CLI is not installed',
steps: [
'Install via npm:',
' npm install -g @qwen-code/qwen-code@latest',
'',
'Or install from source:',
' git clone https://github.com/QwenLM/qwen-code.git',
' cd qwen-code',
' npm install',
' npm install -g .',
'',
'After installation, reload VS Code or restart the extension.',
],
documentationUrl: 'https://github.com/QwenLM/qwen-code#installation',
};
}
}

View File

@@ -0,0 +1,249 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
/**
* Minimum CLI version that supports session/list and session/load ACP methods
*/
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.2.4';
/**
* CLI Feature Flags based on version
*/
export interface CliFeatureFlags {
/**
* Whether the CLI supports session/list ACP method
*/
supportsSessionList: boolean;
/**
* Whether the CLI supports session/load ACP method
*/
supportsSessionLoad: boolean;
/**
* Whether the CLI supports session/save ACP method
*/
supportsSessionSave: boolean;
}
/**
* CLI Version Information
*/
export interface CliVersionInfo {
/**
* Detected version string
*/
version: string | undefined;
/**
* Whether the version meets the minimum requirement
*/
isSupported: boolean;
/**
* Feature flags based on version
*/
features: CliFeatureFlags;
/**
* Raw detection result
*/
detectionResult: CliDetectionResult;
}
/**
* CLI Version Manager
*
* Manages CLI version detection and feature availability based on version
*/
export class CliVersionManager {
private static instance: CliVersionManager;
private cachedVersionInfo: CliVersionInfo | null = null;
private lastCheckTime: number = 0;
private static readonly CACHE_DURATION_MS = 30000; // 30 seconds
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): CliVersionManager {
if (!CliVersionManager.instance) {
CliVersionManager.instance = new CliVersionManager();
}
return CliVersionManager.instance;
}
/**
* Check if CLI version meets minimum requirements
*
* @param version - Version string to check
* @param minVersion - Minimum required version
* @returns Whether version meets requirements
*/
private isVersionSupported(
version: string | undefined,
minVersion: string,
): boolean {
if (!version) {
return false;
}
// Simple version comparison (assuming semantic versioning)
try {
const versionParts = version.split('.').map(Number);
const minVersionParts = minVersion.split('.').map(Number);
for (
let i = 0;
i < Math.min(versionParts.length, minVersionParts.length);
i++
) {
if (versionParts[i] > minVersionParts[i]) {
return true;
} else if (versionParts[i] < minVersionParts[i]) {
return false;
}
}
// If all compared parts are equal, check if version has more parts
return versionParts.length >= minVersionParts.length;
} catch (error) {
console.error('[CliVersionManager] Failed to parse version:', error);
return false;
}
}
/**
* Get feature flags based on CLI version
*
* @param version - CLI version string
* @returns Feature flags
*/
private getFeatureFlags(version: string | undefined): CliFeatureFlags {
const isSupportedVersion = this.isVersionSupported(
version,
MIN_CLI_VERSION_FOR_SESSION_METHODS,
);
return {
supportsSessionList: isSupportedVersion,
supportsSessionLoad: isSupportedVersion,
supportsSessionSave: false, // Not yet supported in any version
};
}
/**
* Detect CLI version and features
*
* @param forceRefresh - Force a new check, ignoring cache
* @returns CLI version information
*/
async detectCliVersion(forceRefresh = false): Promise<CliVersionInfo> {
const now = Date.now();
// Return cached result if available and not expired
if (
!forceRefresh &&
this.cachedVersionInfo &&
now - this.lastCheckTime < CliVersionManager.CACHE_DURATION_MS
) {
console.log('[CliVersionManager] Returning cached version info');
return this.cachedVersionInfo;
}
console.log('[CliVersionManager] Detecting CLI version...');
try {
// Detect CLI installation
const detectionResult = await CliDetector.detectQwenCli(forceRefresh);
const versionInfo: CliVersionInfo = {
version: detectionResult.version,
isSupported: this.isVersionSupported(
detectionResult.version,
MIN_CLI_VERSION_FOR_SESSION_METHODS,
),
features: this.getFeatureFlags(detectionResult.version),
detectionResult,
};
// Cache the result
this.cachedVersionInfo = versionInfo;
this.lastCheckTime = now;
console.log(
'[CliVersionManager] CLI version detection result:',
versionInfo,
);
return versionInfo;
} catch (error) {
console.error('[CliVersionManager] Failed to detect CLI version:', error);
// Return fallback result
const fallbackResult: CliVersionInfo = {
version: undefined,
isSupported: false,
features: {
supportsSessionList: false,
supportsSessionLoad: false,
supportsSessionSave: false,
},
detectionResult: {
isInstalled: false,
error: error instanceof Error ? error.message : String(error),
},
};
return fallbackResult;
}
}
/**
* Clear cached version information
*/
clearCache(): void {
this.cachedVersionInfo = null;
this.lastCheckTime = 0;
CliDetector.clearCache();
}
/**
* Check if CLI supports session/list method
*
* @param forceRefresh - Force a new check, ignoring cache
* @returns Whether session/list is supported
*/
async supportsSessionList(forceRefresh = false): Promise<boolean> {
const versionInfo = await this.detectCliVersion(forceRefresh);
return versionInfo.features.supportsSessionList;
}
/**
* Check if CLI supports session/load method
*
* @param forceRefresh - Force a new check, ignoring cache
* @returns Whether session/load is supported
*/
async supportsSessionLoad(forceRefresh = false): Promise<boolean> {
const versionInfo = await this.detectCliVersion(forceRefresh);
return versionInfo.features.supportsSessionLoad;
}
/**
* Check if CLI supports session/save method
*
* @param forceRefresh - Force a new check, ignoring cache
* @returns Whether session/save is supported
*/
async supportsSessionSave(forceRefresh = false): Promise<boolean> {
const versionInfo = await this.detectCliVersion(forceRefresh);
return versionInfo.features.supportsSessionSave;
}
}