mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 17:27:54 +00:00
feat(cli): 添加 CLI 版本检测和会话验证功能
- 新增 CLI 版本检测功能,支持检测 CLI 版本并缓存结果 - 实现会话验证方法,用于检查当前会话是否有效 - 在连接处理中集成 CLI 版本检测和会话验证逻辑 - 优化 WebViewProvider 中的初始化流程,支持背景初始化 - 更新消息处理逻辑,增加与 CLI 相关的错误处理
This commit is contained in:
126
packages/vscode-ide-companion/src/cli/cliContextManager.ts
Normal file
126
packages/vscode-ide-companion/src/cli/cliContextManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
129
packages/vscode-ide-companion/src/cli/cliDetector.ts
Normal file
129
packages/vscode-ide-companion/src/cli/cliDetector.ts
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
249
packages/vscode-ide-companion/src/cli/cliVersionManager.ts
Normal file
249
packages/vscode-ide-companion/src/cli/cliVersionManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user