From 8bc9bea5a14d10c57c1be056b8999831a0afaabc Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 28 Nov 2025 01:13:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20=E6=B7=BB=E5=8A=A0=20CLI=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=A3=80=E6=B5=8B=E5=92=8C=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CLI 版本检测功能,支持检测 CLI 版本并缓存结果 - 实现会话验证方法,用于检查当前会话是否有效 - 在连接处理中集成 CLI 版本检测和会话验证逻辑 - 优化 WebViewProvider 中的初始化流程,支持背景初始化 - 更新消息处理逻辑,增加与 CLI 相关的错误处理 --- .../src/agents/qwenAgentManager.ts | 35 ++- .../src/agents/qwenConnectionHandler.ts | 21 ++ .../src/cli/cliContextManager.ts | 126 +++++++++ .../src/{utils => cli}/cliDetector.ts | 0 .../src/cli/cliVersionManager.ts | 249 ++++++++++++++++++ .../src/utils/CliInstaller.ts | 2 +- .../src/webview/WebViewProvider.ts | 145 +++++++++- .../webview/handlers/SessionMessageHandler.ts | 36 ++- .../src/webview/hooks/useWebViewMessages.ts | 35 +++ 9 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 packages/vscode-ide-companion/src/cli/cliContextManager.ts rename packages/vscode-ide-companion/src/{utils => cli}/cliDetector.ts (100%) create mode 100644 packages/vscode-ide-companion/src/cli/cliVersionManager.ts diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 7f126e46..1a27d620 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -99,7 +99,40 @@ export class QwenAgentManager { } /** - * Get session list + * Validate if current session is still active + * This is a lightweight check to verify session validity + * + * @returns True if session is valid, false otherwise + */ + async validateCurrentSession(): Promise { + try { + // If we don't have a current session, it's definitely not valid + if (!this.connection.currentSessionId) { + return false; + } + + // Try to get session list to verify our session still exists + const sessions = await this.getSessionList(); + const currentSessionId = this.connection.currentSessionId; + + // Check if our current session exists in the session list + const sessionExists = sessions.some( + (session: Record) => + session.id === currentSessionId || + session.sessionId === currentSessionId, + ); + + return sessionExists; + } catch (error) { + console.warn('[QwenAgentManager] Session validation failed:', error); + // If we can't validate, assume session is invalid + return false; + } + } + + /** + * Get session list with version-aware strategy + * First tries ACP method if CLI version supports it, falls back to file system method * * @returns Session list */ diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index a9641fdf..43f22f46 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -14,6 +14,8 @@ import * as vscode from 'vscode'; import type { AcpConnection } from '../acp/acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; import type { AuthStateManager } from '../auth/authStateManager.js'; +import { CliVersionManager } from '../cli/cliVersionManager.js'; +import { CliContextManager } from '../cli/cliContextManager.js'; /** * Qwen Connection Handler class @@ -41,6 +43,25 @@ export class QwenConnectionHandler { console.log(`[QwenAgentManager] Call stack:\n${new Error().stack}`); console.log(`========================================\n`); + // Check CLI version and features + const cliVersionManager = CliVersionManager.getInstance(); + const versionInfo = await cliVersionManager.detectCliVersion(); + console.log('[QwenAgentManager] CLI version info:', versionInfo); + + // Store CLI context + const cliContextManager = CliContextManager.getInstance(); + cliContextManager.setCurrentVersionInfo(versionInfo); + + // Show warning if CLI version is below minimum requirement + if (!versionInfo.isSupported) { + console.warn( + `[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.2.4'}`, + ); + // vscode.window.showWarningMessage( + // `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version 0.2.4 or later.`, + // ); + } + const config = vscode.workspace.getConfiguration('qwenCode'); const cliPath = config.get('qwen.cliPath', 'qwen'); const openaiApiKey = config.get('qwen.openaiApiKey', ''); diff --git a/packages/vscode-ide-companion/src/cli/cliContextManager.ts b/packages/vscode-ide-companion/src/cli/cliContextManager.ts new file mode 100644 index 00000000..8257003f --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliContextManager.ts @@ -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; + } +} diff --git a/packages/vscode-ide-companion/src/utils/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts similarity index 100% rename from packages/vscode-ide-companion/src/utils/cliDetector.ts rename to packages/vscode-ide-companion/src/cli/cliDetector.ts diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts new file mode 100644 index 00000000..2caa8a9b --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts @@ -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 { + 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 { + 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 { + 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 { + const versionInfo = await this.detectCliVersion(forceRefresh); + return versionInfo.features.supportsSessionSave; + } +} diff --git a/packages/vscode-ide-companion/src/utils/CliInstaller.ts b/packages/vscode-ide-companion/src/utils/CliInstaller.ts index 28610b77..c49a4ea6 100644 --- a/packages/vscode-ide-companion/src/utils/CliInstaller.ts +++ b/packages/vscode-ide-companion/src/utils/CliInstaller.ts @@ -5,7 +5,7 @@ */ import * as vscode from 'vscode'; -import { CliDetector } from './cliDetector.js'; +import { CliDetector } from '../cli/cliDetector.js'; /** * CLI Detection and Installation Handler diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 52660969..30f02a7d 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { QwenAgentManager } from '../agents/qwenAgentManager.js'; import { ConversationStore } from '../storage/conversationStore.js'; import type { AcpPermissionRequest } from '../constants/acpTypes.js'; -import { CliDetector } from '../utils/cliDetector.js'; +import { CliDetector } from '../cli/cliDetector.js'; import { AuthStateManager } from '../auth/authStateManager.js'; import { PanelManager } from './PanelManager.js'; import { MessageHandler } from './MessageHandler.js'; @@ -300,6 +300,12 @@ export class WebViewProvider { }); } + // // Initialize empty conversation immediately for fast UI rendering + // await this.initializeEmptyConversation(); + + // // Perform background CLI detection and connection without blocking UI + // this.performBackgroundInitialization(); + // Smart login restore: Check if we have valid cached auth and restore connection if available const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); @@ -403,6 +409,12 @@ export class WebViewProvider { // Load messages from the current Qwen session await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); } catch (error) { console.error('[WebViewProvider] Agent connection error:', error); // Clear auth cache on error (might be auth issue) @@ -412,6 +424,14 @@ export class WebViewProvider { ); // Fallback to empty conversation await this.initializeEmptyConversation(); + + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: error instanceof Error ? error.message : String(error), + }, + }); } } } else { @@ -421,6 +441,124 @@ export class WebViewProvider { } } + /** + * Perform background initialization without blocking UI + * This method runs CLI detection and connection in the background + */ + private async performBackgroundInitialization(): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + const config = vscode.workspace.getConfiguration('qwenCode'); + const qwenEnabled = config.get('qwen.enabled', true); + + if (qwenEnabled) { + // Check if we have valid cached authentication + const openaiApiKey = config.get('qwen.openaiApiKey', ''); + const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; + + let hasValidAuth = false; + if (this.authStateManager) { + hasValidAuth = await this.authStateManager.hasValidAuth( + workingDir, + authMethod, + ); + console.log( + '[WebViewProvider] Has valid cached auth in background init:', + hasValidAuth, + ); + } + + // Perform CLI detection in background + const cliDetection = await CliDetector.detectQwenCli(); + + if (!cliDetection.isInstalled) { + console.log( + '[WebViewProvider] Qwen CLI not detected in background check', + ); + console.log( + '[WebViewProvider] CLI detection error:', + cliDetection.error, + ); + + // Notify webview that CLI is not installed + this.sendMessageToWebView({ + type: 'cliNotInstalled', + data: { + error: cliDetection.error, + }, + }); + } else { + console.log( + '[WebViewProvider] Qwen CLI detected in background check, attempting connection...', + ); + console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); + console.log('[WebViewProvider] CLI version:', cliDetection.version); + + if (hasValidAuth && !this.agentInitialized) { + console.log( + '[WebViewProvider] Found valid cached auth, attempting to restore connection in background...', + ); + try { + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect( + workingDir, + this.authStateManager, + cliDetection.cliPath, + ); + console.log( + '[WebViewProvider] Connection restored successfully in background', + ); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } catch (error) { + console.error( + '[WebViewProvider] Failed to restore connection in background:', + error, + ); + // Clear auth cache on error + await this.authStateManager.clearAuthState(); + + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: + error instanceof Error ? error.message : String(error), + }, + }); + } + } else if (this.agentInitialized) { + console.log( + '[WebViewProvider] Agent already initialized, no need to reconnect in background', + ); + } else { + console.log( + '[WebViewProvider] No valid cached auth, skipping background connection', + ); + } + } + } else { + console.log( + '[WebViewProvider] Qwen agent is disabled in settings (background)', + ); + } + } catch (error) { + console.error( + '[WebViewProvider] Background initialization failed:', + error, + ); + } + } + /** * Force re-login by clearing auth cache and reconnecting * Called when user explicitly uses /login command @@ -711,6 +849,11 @@ export class WebViewProvider { console.log('[WebViewProvider] Panel restored successfully'); + // TODO: + // await this.initializeEmptyConversation(); + // // Perform background initialization without blocking UI + // this.performBackgroundInitialization(); + // Smart login restore: Check if we have valid cached auth and restore connection if available const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index dbd3898f..40518be1 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -260,6 +260,32 @@ export class SessionMessageHandler extends BaseMessageHandler { return; } + // // Validate current session before sending message + // const isSessionValid = await this.agentManager.validateCurrentSession(); + // if (!isSessionValid) { + // console.warn('[SessionMessageHandler] Current session is not valid'); + + // // Show non-modal notification with Login button + // const result = await vscode.window.showWarningMessage( + // 'Your session has expired. Please login again to continue using Qwen Code.', + // 'Login Now', + // ); + + // if (result === 'Login Now') { + // // Use login handler directly + // if (this.loginHandler) { + // await this.loginHandler(); + // } else { + // // Fallback to command + // vscode.window.showInformationMessage( + // 'Please wait while we connect to Qwen Code...', + // ); + // await vscode.commands.executeCommand('qwenCode.login'); + // } + // } + // return; + // } + // Send to agent try { this.resetStreamContent(); @@ -327,9 +353,15 @@ export class SessionMessageHandler extends BaseMessageHandler { console.error('[SessionMessageHandler] Error sending message:', error); const errorMsg = String(error); - if (errorMsg.includes('No active ACP session')) { + // Check for session not found error and handle it appropriately + if ( + errorMsg.includes('Session not found') || + errorMsg.includes('No active ACP session') + ) { + // Clear auth cache since session is invalid + // Note: We would need access to authStateManager for this, but for now we'll just show login prompt const result = await vscode.window.showWarningMessage( - 'You need to login first to use Qwen Code.', + 'Your session has expired. Please login again to continue using Qwen Code.', 'Login Now', ); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 787f327c..0d239d7f 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -146,6 +146,41 @@ export const useWebViewMessages = ({ break; } + // case 'cliNotInstalled': { + // // Show CLI not installed message + // const errorMsg = + // (message?.data?.error as string) || + // 'Qwen Code CLI is not installed. Please install it to enable full functionality.'; + + // handlers.messageHandling.addMessage({ + // role: 'assistant', + // content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`, + // timestamp: Date.now(), + // }); + // break; + // } + + // case 'agentConnected': { + // // Agent connected successfully + // handlers.messageHandling.clearWaitingForResponse(); + // break; + // } + + // case 'agentConnectionError': { + // // Agent connection failed + // handlers.messageHandling.clearWaitingForResponse(); + // const errorMsg = + // (message?.data?.message as string) || + // 'Failed to connect to Qwen agent.'; + + // handlers.messageHandling.addMessage({ + // role: 'assistant', + // content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, + // timestamp: Date.now(), + // }); + // break; + // } + case 'loginError': { // Clear loading state and show error notice handlers.messageHandling.clearWaitingForResponse();