feat(vscode-ide-companion): 0.2.4 版本ACP 协议检测和实现

- 新增 session/list 方法支持
- 改进 session/load 方法兼容性
- 优化代理环境变量设置
- 调整 CLI 安装流程
- 移除未使用的随机加载消息功能
This commit is contained in:
yiliang114
2025-11-26 19:23:25 +08:00
parent f78b1eff93
commit dc340daf8b
13 changed files with 205 additions and 34 deletions

View File

@@ -198,7 +198,6 @@
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1", "@modelcontextprotocol/sdk": "^1.15.1",
"@qwen-code/qwen-code-core": "file:../core", "@qwen-code/qwen-code-core": "file:../core",
"@qwen-code/qwen-code": "file:../cli",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0", "express": "^5.1.0",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -42,9 +42,6 @@ import { AcpSessionManager } from './acpSessionManager.js';
* ✅ session/cancel - Cancel current generation * ✅ session/cancel - Cancel current generation
* ✅ session/load - Load previous session * ✅ session/load - Load previous session
* ✅ session/save - Save current session * ✅ session/save - Save current session
*
* Custom Methods (Not in standard ACP):
* ⚠️ session/list - List available sessions (custom extension)
*/ */
export class AcpConnection { export class AcpConnection {
private child: ChildProcess | null = null; private child: ChildProcess | null = null;
@@ -101,10 +98,10 @@ export class AcpConnection {
const proxyUrl = extraArgs[proxyIndex + 1]; const proxyUrl = extraArgs[proxyIndex + 1];
console.log('[ACP] Setting proxy environment variables:', proxyUrl); console.log('[ACP] Setting proxy environment variables:', proxyUrl);
env.HTTP_PROXY = proxyUrl; env['HTTP_PROXY'] = proxyUrl;
env.HTTPS_PROXY = proxyUrl; env['HTTPS_PROXY'] = proxyUrl;
env.http_proxy = proxyUrl; env['http_proxy'] = proxyUrl;
env.https_proxy = proxyUrl; env['https_proxy'] = proxyUrl;
} }
let spawnCommand: string; let spawnCommand: string;

View File

@@ -18,7 +18,8 @@
* ✅ initialize - Protocol initialization * ✅ initialize - Protocol initialization
* ✅ authenticate - User authentication * ✅ authenticate - User authentication
* ✅ session/new - Create new session * ✅ session/new - Create new session
* ✅ session/load - Load existing session * ✅ session/load - Load existing session (v0.2.4+)
* ✅ session/list - List available sessions (v0.2.4+)
* ✅ session/prompt - Send user message to agent * ✅ session/prompt - Send user message to agent
* ✅ session/cancel - Cancel current generation * ✅ session/cancel - Cancel current generation
* ✅ session/save - Save current session * ✅ session/save - Save current session
@@ -27,6 +28,7 @@ export const AGENT_METHODS = {
authenticate: 'authenticate', authenticate: 'authenticate',
initialize: 'initialize', initialize: 'initialize',
session_cancel: 'session/cancel', session_cancel: 'session/cancel',
session_list: 'session/list',
session_load: 'session/load', session_load: 'session/load',
session_new: 'session/new', session_new: 'session/new',
session_prompt: 'session/prompt', session_prompt: 'session/prompt',

View File

@@ -23,6 +23,7 @@ import type {
} from './qwenTypes.js'; } from './qwenTypes.js';
import { QwenConnectionHandler } from './qwenConnectionHandler.js'; import { QwenConnectionHandler } from './qwenConnectionHandler.js';
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
import { CliContextManager } from '../cli/cliContextManager.js';
export type { ChatMessage, PlanEntry, ToolCallUpdateData }; export type { ChatMessage, PlanEntry, ToolCallUpdateData };
@@ -137,14 +138,64 @@ export class QwenAgentManager {
* @returns Session list * @returns Session list
*/ */
async getSessionList(): Promise<Array<Record<string, unknown>>> { async getSessionList(): Promise<Array<Record<string, unknown>>> {
console.log(
'[QwenAgentManager] Getting session list with version-aware strategy',
);
// Check if CLI supports session/list method
const cliContextManager = CliContextManager.getInstance();
const supportsSessionList = cliContextManager.supportsSessionList();
console.log(
'[QwenAgentManager] CLI supports session/list:',
supportsSessionList,
);
// Try ACP method first if supported
if (supportsSessionList) {
try { try {
console.log(
'[QwenAgentManager] Attempting to get session list via ACP method',
);
const response = await this.connection.listSessions();
console.log('[QwenAgentManager] ACP session list response:', response);
if (response.result && Array.isArray(response.result)) {
const sessions = response.result.map((session) => ({
id: session.sessionId || session.id,
sessionId: session.sessionId || session.id,
title: session.title || session.name || 'Untitled Session',
name: session.title || session.name || 'Untitled Session',
startTime: session.startTime,
lastUpdated: session.lastUpdated,
messageCount: session.messageCount || 0,
projectHash: session.projectHash,
}));
console.log(
'[QwenAgentManager] Sessions retrieved via ACP:',
sessions.length,
);
return sessions;
}
} catch (error) {
console.warn(
'[QwenAgentManager] ACP session list failed, falling back to file system method:',
error,
);
}
}
// Always fall back to file system method
try {
console.log('[QwenAgentManager] Getting session list from file system');
const sessions = await this.sessionReader.getAllSessions(undefined, true); const sessions = await this.sessionReader.getAllSessions(undefined, true);
console.log( console.log(
'[QwenAgentManager] Session list from files (all projects):', '[QwenAgentManager] Session list from file system (all projects):',
sessions.length, sessions.length,
); );
return sessions.map( const result = sessions.map(
(session: QwenSession): Record<string, unknown> => ({ (session: QwenSession): Record<string, unknown> => ({
id: session.sessionId, id: session.sessionId,
sessionId: session.sessionId, sessionId: session.sessionId,
@@ -156,8 +207,17 @@ export class QwenAgentManager {
projectHash: session.projectHash, projectHash: session.projectHash,
}), }),
); );
console.log(
'[QwenAgentManager] Sessions retrieved from file system:',
result.length,
);
return result;
} catch (error) { } catch (error) {
console.error('[QwenAgentManager] Failed to get session list:', error); console.error(
'[QwenAgentManager] Failed to get session list from file system:',
error,
);
return []; return [];
} }
} }
@@ -370,12 +430,22 @@ export class QwenAgentManager {
/** /**
* Try to load session via ACP session/load method * Try to load session via ACP session/load method
* This is a test method to verify if CLI supports session/load * This method will only be used if CLI version supports it
* *
* @param sessionId - Session ID * @param sessionId - Session ID
* @returns Load response or error * @returns Load response or error
*/ */
async loadSessionViaAcp(sessionId: string): Promise<unknown> { async loadSessionViaAcp(sessionId: string): Promise<unknown> {
// Check if CLI supports session/load method
const cliContextManager = CliContextManager.getInstance();
const supportsSessionLoad = cliContextManager.supportsSessionLoad();
if (!supportsSessionLoad) {
throw new Error(
`CLI version does not support session/load method. Please upgrade to version 0.2.4 or later.`,
);
}
try { try {
console.log( console.log(
'[QwenAgentManager] Attempting session/load via ACP for session:', '[QwenAgentManager] Attempting session/load via ACP for session:',
@@ -419,23 +489,91 @@ export class QwenAgentManager {
} }
/** /**
* Load session directly from file system (without relying on ACP) * Load session with version-aware strategy
* First tries ACP method if CLI version supports it, falls back to file system method
* *
* @param sessionId - Session ID * @param sessionId - Session ID to load
* @returns Loaded session messages or null * @returns Loaded session messages or null
*/ */
async loadSessionDirect(sessionId: string): Promise<ChatMessage[] | null> { async loadSession(sessionId: string): Promise<ChatMessage[] | null> {
try { console.log(
console.log('[QwenAgentManager] Loading session directly:', sessionId); '[QwenAgentManager] Loading session with version-aware strategy:',
sessionId,
);
// Load session // Check if CLI supports session/load method
const cliContextManager = CliContextManager.getInstance();
const supportsSessionLoad = cliContextManager.supportsSessionLoad();
console.log(
'[QwenAgentManager] CLI supports session/load:',
supportsSessionLoad,
);
// Try ACP method first if supported
if (supportsSessionLoad) {
try {
console.log(
'[QwenAgentManager] Attempting to load session via ACP method',
);
await this.loadSessionViaAcp(sessionId);
console.log('[QwenAgentManager] Session loaded successfully via ACP');
// After loading via ACP, we still need to get messages from file system
// In future, we might get them directly from the ACP response
} catch (error) {
console.warn(
'[QwenAgentManager] ACP session load failed, falling back to file system method:',
error,
);
}
}
// Always fall back to file system method
try {
console.log(
'[QwenAgentManager] Loading session messages from file system',
);
const messages = await this.loadSessionMessagesFromFile(sessionId);
console.log(
'[QwenAgentManager] Session messages loaded successfully from file system',
);
return messages;
} catch (error) {
console.error(
'[QwenAgentManager] Failed to load session messages from file system:',
error,
);
return null;
}
}
/**
* Load session messages from file system
*
* @param sessionId - Session ID to load
* @returns Loaded session messages
*/
private async loadSessionMessagesFromFile(
sessionId: string,
): Promise<ChatMessage[] | null> {
try {
console.log(
'[QwenAgentManager] Loading session from file system:',
sessionId,
);
// Load session from file system
const session = await this.sessionManager.loadSession( const session = await this.sessionManager.loadSession(
sessionId, sessionId,
this.currentWorkingDir, this.currentWorkingDir,
); );
if (!session) { if (!session) {
console.log('[QwenAgentManager] Session not found:', sessionId); console.log(
'[QwenAgentManager] Session not found in file system:',
sessionId,
);
return null; return null;
} }
@@ -446,14 +584,26 @@ export class QwenAgentManager {
timestamp: new Date(msg.timestamp).getTime(), timestamp: new Date(msg.timestamp).getTime(),
})); }));
console.log('[QwenAgentManager] Session loaded directly:', sessionId);
return messages; return messages;
} catch (error) { } catch (error) {
console.error('[QwenAgentManager] Session load directly failed:', error); console.error(
return null; '[QwenAgentManager] Session load from file system failed:',
error,
);
throw error;
} }
} }
/**
* Load session, preferring ACP method if CLI version supports it
*
* @param sessionId - Session ID
* @returns Loaded session messages or null
*/
async loadSessionDirect(sessionId: string): Promise<ChatMessage[] | null> {
return this.loadSession(sessionId);
}
/** /**
* Create new session * Create new session
* *

View File

@@ -57,6 +57,8 @@ export class QwenConnectionHandler {
console.warn( console.warn(
`[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.2.4'}`, `[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.2.4'}`,
); );
// TODO: 暂时注释
// vscode.window.showWarningMessage( // 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.`, // `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.`,
// ); // );

View File

@@ -93,6 +93,33 @@ export class CliInstaller {
const execAsync = promisify(exec); const execAsync = promisify(exec);
try { try {
// Use NVM environment to ensure we get the same Node.js version
// as when they run 'node -v' in terminal
// Fallback chain: default alias -> node alias -> current version
const installCommand =
process.platform === 'win32'
? 'npm install -g @qwen-code/qwen-code@latest'
: 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); npm install -g @qwen-code/qwen-code@latest';
console.log(
'[CliInstaller] Installing with command:',
installCommand,
);
console.log(
'[CliInstaller] Current process PATH:',
process.env['PATH'],
);
// Also log Node.js version being used by VS Code
console.log(
'[CliInstaller] VS Code Node.js version:',
process.version,
);
console.log(
'[CliInstaller] VS Code Node.js execPath:',
process.execPath,
);
const { stdout, stderr } = await execAsync( const { stdout, stderr } = await execAsync(
'npm install -g @qwen-code/qwen-code@latest', 'npm install -g @qwen-code/qwen-code@latest',
{ timeout: 120000 }, // 2 minutes timeout { timeout: 120000 }, // 2 minutes timeout

View File

@@ -140,9 +140,6 @@ export const WITTY_LOADING_PHRASES = [
"New line? That's Ctrl+J.", "New line? That's Ctrl+J.",
]; ];
/**
* Get random loading message
*/
export const getRandomLoadingMessage = (): string => export const getRandomLoadingMessage = (): string =>
WITTY_LOADING_PHRASES[ WITTY_LOADING_PHRASES[
Math.floor(Math.random() * WITTY_LOADING_PHRASES.length) Math.floor(Math.random() * WITTY_LOADING_PHRASES.length)

View File

@@ -152,7 +152,6 @@ export async function activate(context: vscode.ExtensionContext) {
console.log('[Extension] Panel restore completed'); console.log('[Extension] Panel restore completed');
log('WebView panel restored from serialization'); log('WebView panel restored from serialization');
return true;
}, },
}), }),
); );

View File

@@ -13,7 +13,7 @@ import { AuthStateManager } from '../auth/authStateManager.js';
import { PanelManager } from './PanelManager.js'; import { PanelManager } from './PanelManager.js';
import { MessageHandler } from './MessageHandler.js'; import { MessageHandler } from './MessageHandler.js';
import { WebViewContent } from './WebViewContent.js'; import { WebViewContent } from './WebViewContent.js';
import { CliInstaller } from '../utils/CliInstaller.js'; import { CliInstaller } from '../cli/CliInstaller.js';
import { getFileName } from './utils/webviewUtils.js'; import { getFileName } from './utils/webviewUtils.js';
export class WebViewProvider { export class WebViewProvider {

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { QwenAgentManager } from '../agents/qwenAgentManager.js'; import type { QwenAgentManager } from '../../agents/qwenAgentManager.js';
import type { ConversationStore } from '../storage/conversationStore.js'; import type { ConversationStore } from '../../storage/conversationStore.js';
/** /**
* Base message handler interface * Base message handler interface

View File

@@ -42,8 +42,6 @@ export class SettingsMessageHandler extends BaseMessageHandler {
try { try {
// Open settings in a side panel // Open settings in a side panel
await vscode.commands.executeCommand('workbench.action.openSettings', { await vscode.commands.executeCommand('workbench.action.openSettings', {
// TODO:
// openToSide: true,
query: 'qwenCode', query: 'qwenCode',
}); });
} catch (error) { } catch (error) {

View File

@@ -5,7 +5,7 @@
*/ */
import { useState, useCallback, useRef } from 'react'; import { useState, useCallback, useRef } from 'react';
import type { VSCodeAPI } from '../hooks/useVSCode.js'; import type { VSCodeAPI } from '../../hooks/useVSCode.js';
/** /**
* File context management Hook * File context management Hook

View File

@@ -5,7 +5,7 @@
*/ */
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
import type { VSCodeAPI } from '../hooks/useVSCode.js'; import type { VSCodeAPI } from '../../hooks/useVSCode.js';
/** /**
* Session management Hook * Session management Hook