mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 01:37:50 +00:00
wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { AcpConnection } from '../acp/AcpConnection.js';
|
||||
import type {
|
||||
AcpSessionUpdate,
|
||||
@@ -15,317 +14,100 @@ import {
|
||||
type QwenSession,
|
||||
} from '../services/QwenSessionReader.js';
|
||||
import type { AuthStateManager } from '../auth/AuthStateManager.js';
|
||||
import type {
|
||||
ChatMessage,
|
||||
PlanEntry,
|
||||
ToolCallUpdateData,
|
||||
QwenAgentCallbacks,
|
||||
} from './QwenTypes.js';
|
||||
import { QwenConnectionHandler } from './QwenConnectionHandler.js';
|
||||
import { QwenSessionUpdateHandler } from './QwenSessionUpdateHandler.js';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ToolCallUpdateData {
|
||||
toolCallId: string;
|
||||
kind?: string;
|
||||
title?: string;
|
||||
status?: string;
|
||||
rawInput?: unknown;
|
||||
content?: Array<Record<string, unknown>>;
|
||||
locations?: Array<{ path: string; line?: number | null }>;
|
||||
}
|
||||
// 重新导出类型以保持向后兼容
|
||||
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
|
||||
|
||||
/**
|
||||
* Qwen Agent管理器
|
||||
*
|
||||
* 协调各个模块,提供统一的接口
|
||||
*/
|
||||
export class QwenAgentManager {
|
||||
private connection: AcpConnection;
|
||||
private sessionReader: QwenSessionReader;
|
||||
private onMessageCallback?: (message: ChatMessage) => void;
|
||||
private onStreamChunkCallback?: (chunk: string) => void;
|
||||
private onToolCallCallback?: (update: ToolCallUpdateData) => void;
|
||||
private onPermissionRequestCallback?: (
|
||||
request: AcpPermissionRequest,
|
||||
) => Promise<string>;
|
||||
private connectionHandler: QwenConnectionHandler;
|
||||
private sessionUpdateHandler: QwenSessionUpdateHandler;
|
||||
private currentWorkingDir: string = process.cwd();
|
||||
|
||||
// 回调函数存储
|
||||
private callbacks: QwenAgentCallbacks = {};
|
||||
|
||||
constructor() {
|
||||
this.connection = new AcpConnection();
|
||||
this.sessionReader = new QwenSessionReader();
|
||||
this.connectionHandler = new QwenConnectionHandler();
|
||||
this.sessionUpdateHandler = new QwenSessionUpdateHandler({});
|
||||
|
||||
// Setup session update handler
|
||||
// 设置ACP连接的回调
|
||||
this.connection.onSessionUpdate = (data: AcpSessionUpdate) => {
|
||||
this.handleSessionUpdate(data);
|
||||
this.sessionUpdateHandler.handleSessionUpdate(data);
|
||||
};
|
||||
|
||||
// Setup permission request handler
|
||||
this.connection.onPermissionRequest = async (
|
||||
data: AcpPermissionRequest,
|
||||
) => {
|
||||
if (this.onPermissionRequestCallback) {
|
||||
const optionId = await this.onPermissionRequestCallback(data);
|
||||
if (this.callbacks.onPermissionRequest) {
|
||||
const optionId = await this.callbacks.onPermissionRequest(data);
|
||||
return { optionId };
|
||||
}
|
||||
return { optionId: 'allow_once' };
|
||||
};
|
||||
|
||||
// Setup end turn handler
|
||||
this.connection.onEndTurn = () => {
|
||||
// Notify UI that response is complete
|
||||
// 通知UI响应完成
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到Qwen服务
|
||||
*
|
||||
* @param workingDir - 工作目录
|
||||
* @param authStateManager - 认证状态管理器(可选)
|
||||
*/
|
||||
async connect(
|
||||
workingDir: string,
|
||||
authStateManager?: AuthStateManager,
|
||||
): Promise<void> {
|
||||
const connectId = Date.now();
|
||||
console.log(`\n========================================`);
|
||||
console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`);
|
||||
console.log(`[QwenAgentManager] Call stack:\n${new Error().stack}`);
|
||||
console.log(`========================================\n`);
|
||||
|
||||
this.currentWorkingDir = workingDir;
|
||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||
const cliPath = config.get<string>('qwen.cliPath', 'qwen');
|
||||
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
|
||||
const openaiBaseUrl = config.get<string>('qwen.openaiBaseUrl', '');
|
||||
const model = config.get<string>('qwen.model', '');
|
||||
const proxy = config.get<string>('qwen.proxy', '');
|
||||
|
||||
// Build additional CLI arguments
|
||||
const extraArgs: string[] = [];
|
||||
if (openaiApiKey) {
|
||||
extraArgs.push('--openai-api-key', openaiApiKey);
|
||||
}
|
||||
if (openaiBaseUrl) {
|
||||
extraArgs.push('--openai-base-url', openaiBaseUrl);
|
||||
}
|
||||
if (model) {
|
||||
extraArgs.push('--model', model);
|
||||
}
|
||||
if (proxy) {
|
||||
extraArgs.push('--proxy', proxy);
|
||||
console.log('[QwenAgentManager] Using proxy:', proxy);
|
||||
}
|
||||
|
||||
await this.connection.connect('qwen', cliPath, workingDir, extraArgs);
|
||||
|
||||
// Determine auth method based on configuration
|
||||
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
|
||||
|
||||
// Check if we have valid cached authentication
|
||||
let needsAuth = true;
|
||||
if (authStateManager) {
|
||||
const hasValidAuth = await authStateManager.hasValidAuth(
|
||||
workingDir,
|
||||
authMethod,
|
||||
);
|
||||
if (hasValidAuth) {
|
||||
console.log('[QwenAgentManager] Using cached authentication');
|
||||
needsAuth = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to restore existing session or create new one
|
||||
let sessionRestored = false;
|
||||
|
||||
// Try to get sessions from local files
|
||||
console.log('[QwenAgentManager] Reading local session files...');
|
||||
try {
|
||||
const sessions = await this.sessionReader.getAllSessions(workingDir);
|
||||
|
||||
if (sessions.length > 0) {
|
||||
// Use the most recent session
|
||||
console.log(
|
||||
'[QwenAgentManager] Found existing sessions:',
|
||||
sessions.length,
|
||||
);
|
||||
const lastSession = sessions[0]; // Already sorted by lastUpdated
|
||||
|
||||
// Try to switch to it (this may fail if not supported)
|
||||
try {
|
||||
await this.connection.switchSession(lastSession.sessionId);
|
||||
console.log(
|
||||
'[QwenAgentManager] Restored session:',
|
||||
lastSession.sessionId,
|
||||
);
|
||||
sessionRestored = true;
|
||||
// If session restored successfully, we don't need to authenticate
|
||||
needsAuth = false;
|
||||
} catch (switchError) {
|
||||
console.log(
|
||||
'[QwenAgentManager] session/switch not supported or failed:',
|
||||
switchError instanceof Error
|
||||
? switchError.message
|
||||
: String(switchError),
|
||||
);
|
||||
// Will create new session below
|
||||
}
|
||||
} else {
|
||||
console.log('[QwenAgentManager] No existing sessions found');
|
||||
}
|
||||
} catch (error) {
|
||||
// If reading local sessions fails, log and continue
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.log(
|
||||
'[QwenAgentManager] Failed to read local sessions:',
|
||||
errorMessage,
|
||||
);
|
||||
// Will create new session below
|
||||
}
|
||||
|
||||
// Create new session if we couldn't restore one
|
||||
if (!sessionRestored) {
|
||||
console.log('[QwenAgentManager] Creating new session...');
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ WORKAROUND: Skipping explicit authenticate() call`,
|
||||
);
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ Reason: newSession() internally calls refreshAuth(), which triggers device flow`,
|
||||
);
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ Calling authenticate() first causes double authentication`,
|
||||
);
|
||||
|
||||
// WORKAROUND: Skip explicit authenticate() call
|
||||
// The newSession() method will internally call config.refreshAuth(),
|
||||
// which will trigger device flow if no valid token exists.
|
||||
// Calling authenticate() first causes a duplicate OAuth flow due to a bug in Qwen CLI
|
||||
// where authenticate() doesn't properly save refresh token for newSession() to use.
|
||||
|
||||
// Try to create session (which will trigger auth internally if needed)
|
||||
try {
|
||||
console.log(
|
||||
`\n🔐 [AUTO AUTH] newSession will handle authentication automatically\n`,
|
||||
);
|
||||
await this.newSessionWithRetry(workingDir, 3);
|
||||
console.log('[QwenAgentManager] New session created successfully');
|
||||
|
||||
// Save auth state after successful session creation
|
||||
if (authStateManager) {
|
||||
console.log(
|
||||
'[QwenAgentManager] Saving auth state after successful session creation',
|
||||
);
|
||||
await authStateManager.saveAuthState(workingDir, authMethod);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.log(`\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`);
|
||||
console.log(`[QwenAgentManager] Error details:`, sessionError);
|
||||
|
||||
// If session creation failed, clear cache and let user retry
|
||||
if (authStateManager) {
|
||||
console.log('[QwenAgentManager] Clearing auth cache due to failure');
|
||||
await authStateManager.clearAuthState();
|
||||
}
|
||||
|
||||
throw sessionError;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n========================================`);
|
||||
console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`);
|
||||
console.log(`========================================\n`);
|
||||
await this.connectionHandler.connect(
|
||||
this.connection,
|
||||
this.sessionReader,
|
||||
workingDir,
|
||||
authStateManager,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with retry logic
|
||||
* 发送消息
|
||||
*
|
||||
* @param message - 消息内容
|
||||
*/
|
||||
private async authenticateWithRetry(
|
||||
authMethod: string,
|
||||
maxRetries: number,
|
||||
): Promise<void> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const callStack = new Error().stack;
|
||||
console.log(
|
||||
`[QwenAgentManager] 🔐 AUTHENTICATION CALL STARTED at ${timestamp}`,
|
||||
);
|
||||
console.log(
|
||||
`[QwenAgentManager] Auth method: ${authMethod}, Max retries: ${maxRetries}`,
|
||||
);
|
||||
console.log(`[QwenAgentManager] Call stack:\n${callStack}`);
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
console.log(
|
||||
`[QwenAgentManager] 📝 Authenticating (attempt ${attempt}/${maxRetries})...`,
|
||||
);
|
||||
await this.connection.authenticate(authMethod);
|
||||
console.log(
|
||||
`[QwenAgentManager] ✅ Authentication successful on attempt ${attempt}`,
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`[QwenAgentManager] ❌ Authentication attempt ${attempt} failed:`,
|
||||
errorMessage,
|
||||
);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(
|
||||
`Authentication failed after ${maxRetries} attempts: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Wait before retrying (exponential backoff)
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||
console.log(
|
||||
`[QwenAgentManager] ⏳ Retrying in ${delay}ms... (${maxRetries - attempt} retries remaining)`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new session with retry logic
|
||||
*/
|
||||
private async newSessionWithRetry(
|
||||
workingDir: string,
|
||||
maxRetries: number,
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
console.log(
|
||||
`[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`,
|
||||
);
|
||||
await this.connection.newSession(workingDir);
|
||||
console.log('[QwenAgentManager] Session created successfully');
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`[QwenAgentManager] Session creation attempt ${attempt} failed:`,
|
||||
errorMessage,
|
||||
);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(
|
||||
`Session creation failed after ${maxRetries} attempts: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||
console.log(`[QwenAgentManager] Retrying in ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(message: string): Promise<void> {
|
||||
await this.connection.sendPrompt(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
* @returns 会话列表
|
||||
*/
|
||||
async getSessionList(): Promise<Array<Record<string, unknown>>> {
|
||||
try {
|
||||
// Read from local session files instead of ACP protocol
|
||||
// Get all sessions from all projects
|
||||
const sessions = await this.sessionReader.getAllSessions(undefined, true);
|
||||
console.log(
|
||||
'[QwenAgentManager] Session list from files (all projects):',
|
||||
sessions.length,
|
||||
);
|
||||
|
||||
// Transform to UI-friendly format
|
||||
return sessions.map(
|
||||
(session: QwenSession): Record<string, unknown> => ({
|
||||
id: session.sessionId,
|
||||
@@ -344,6 +126,12 @@ export class QwenAgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
* @returns 消息列表
|
||||
*/
|
||||
async getSessionMessages(sessionId: string): Promise<ChatMessage[]> {
|
||||
try {
|
||||
const session = await this.sessionReader.getSession(
|
||||
@@ -354,7 +142,6 @@ export class QwenAgentManager {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert Qwen messages to ChatMessage format
|
||||
return session.messages.map(
|
||||
(msg: { type: string; content: string; timestamp: string }) => ({
|
||||
role:
|
||||
@@ -372,132 +159,112 @@ export class QwenAgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*
|
||||
* @param workingDir - 工作目录
|
||||
*/
|
||||
async createNewSession(workingDir: string): Promise<void> {
|
||||
console.log('[QwenAgentManager] Creating new session...');
|
||||
await this.connection.newSession(workingDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定会话
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
*/
|
||||
async switchToSession(sessionId: string): Promise<void> {
|
||||
await this.connection.switchSession(sessionId);
|
||||
}
|
||||
|
||||
private handleSessionUpdate(data: AcpSessionUpdate): void {
|
||||
const update = data.update;
|
||||
|
||||
switch (update.sessionUpdate) {
|
||||
case 'user_message_chunk':
|
||||
// Handle user message chunks if needed
|
||||
if (update.content?.text && this.onStreamChunkCallback) {
|
||||
this.onStreamChunkCallback(update.content.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_message_chunk':
|
||||
// Handle assistant message chunks
|
||||
if (update.content?.text && this.onStreamChunkCallback) {
|
||||
this.onStreamChunkCallback(update.content.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_thought_chunk':
|
||||
// Handle thinking chunks - could be displayed differently in UI
|
||||
if (update.content?.text && this.onStreamChunkCallback) {
|
||||
this.onStreamChunkCallback(update.content.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_call': {
|
||||
// Handle new tool call
|
||||
if (this.onToolCallCallback && 'toolCallId' in update) {
|
||||
this.onToolCallCallback({
|
||||
toolCallId: update.toolCallId as string,
|
||||
kind: (update.kind as string) || undefined,
|
||||
title: (update.title as string) || undefined,
|
||||
status: (update.status as string) || undefined,
|
||||
rawInput: update.rawInput,
|
||||
content: update.content as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined,
|
||||
locations: update.locations as
|
||||
| Array<{ path: string; line?: number | null }>
|
||||
| undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_call_update': {
|
||||
// Handle tool call status update
|
||||
if (this.onToolCallCallback && 'toolCallId' in update) {
|
||||
this.onToolCallCallback({
|
||||
toolCallId: update.toolCallId as string,
|
||||
kind: (update.kind as string) || undefined,
|
||||
title: (update.title as string) || undefined,
|
||||
status: (update.status as string) || undefined,
|
||||
rawInput: update.rawInput,
|
||||
content: update.content as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined,
|
||||
locations: update.locations as
|
||||
| Array<{ path: string; line?: number | null }>
|
||||
| undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plan': {
|
||||
// Handle plan updates - could be displayed as a task list
|
||||
if ('entries' in update && this.onStreamChunkCallback) {
|
||||
const entries = update.entries as Array<{
|
||||
content: string;
|
||||
priority: string;
|
||||
status: string;
|
||||
}>;
|
||||
const planText =
|
||||
'\n📋 Plan:\n' +
|
||||
entries
|
||||
.map(
|
||||
(entry, i) => `${i + 1}. [${entry.priority}] ${entry.content}`,
|
||||
)
|
||||
.join('\n');
|
||||
this.onStreamChunkCallback(planText);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log('[QwenAgentManager] Unhandled session update type');
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 取消当前提示
|
||||
*/
|
||||
async cancelCurrentPrompt(): Promise<void> {
|
||||
console.log('[QwenAgentManager] Cancelling current prompt');
|
||||
await this.connection.cancelSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息回调
|
||||
*
|
||||
* @param callback - 消息回调函数
|
||||
*/
|
||||
onMessage(callback: (message: ChatMessage) => void): void {
|
||||
this.onMessageCallback = callback;
|
||||
this.callbacks.onMessage = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册流式文本块回调
|
||||
*
|
||||
* @param callback - 流式文本块回调函数
|
||||
*/
|
||||
onStreamChunk(callback: (chunk: string) => void): void {
|
||||
this.onStreamChunkCallback = callback;
|
||||
this.callbacks.onStreamChunk = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册思考文本块回调
|
||||
*
|
||||
* @param callback - 思考文本块回调函数
|
||||
*/
|
||||
onThoughtChunk(callback: (chunk: string) => void): void {
|
||||
this.callbacks.onThoughtChunk = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具调用回调
|
||||
*
|
||||
* @param callback - 工具调用回调函数
|
||||
*/
|
||||
onToolCall(callback: (update: ToolCallUpdateData) => void): void {
|
||||
this.onToolCallCallback = callback;
|
||||
this.callbacks.onToolCall = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册计划回调
|
||||
*
|
||||
* @param callback - 计划回调函数
|
||||
*/
|
||||
onPlan(callback: (entries: PlanEntry[]) => void): void {
|
||||
this.callbacks.onPlan = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册权限请求回调
|
||||
*
|
||||
* @param callback - 权限请求回调函数
|
||||
*/
|
||||
onPermissionRequest(
|
||||
callback: (request: AcpPermissionRequest) => Promise<string>,
|
||||
): void {
|
||||
this.onPermissionRequestCallback = callback;
|
||||
this.callbacks.onPermissionRequest = callback;
|
||||
this.sessionUpdateHandler.updateCallbacks(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.connection.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.connection.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话ID
|
||||
*/
|
||||
get currentSessionId(): string | null {
|
||||
return this.connection.currentSessionId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Qwen连接处理器
|
||||
*
|
||||
* 负责Qwen Agent的连接建立、认证和会话创建
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Qwen连接处理器类
|
||||
* 处理连接、认证和会话初始化
|
||||
*/
|
||||
export class QwenConnectionHandler {
|
||||
/**
|
||||
* 连接到Qwen服务并建立会话
|
||||
*
|
||||
* @param connection - ACP连接实例
|
||||
* @param sessionReader - 会话读取器实例
|
||||
* @param workingDir - 工作目录
|
||||
* @param authStateManager - 认证状态管理器(可选)
|
||||
*/
|
||||
async connect(
|
||||
connection: AcpConnection,
|
||||
sessionReader: QwenSessionReader,
|
||||
workingDir: string,
|
||||
authStateManager?: AuthStateManager,
|
||||
): Promise<void> {
|
||||
const connectId = Date.now();
|
||||
console.log(`\n========================================`);
|
||||
console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`);
|
||||
console.log(`[QwenAgentManager] Call stack:\n${new Error().stack}`);
|
||||
console.log(`========================================\n`);
|
||||
|
||||
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||
const cliPath = config.get<string>('qwen.cliPath', 'qwen');
|
||||
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
|
||||
const openaiBaseUrl = config.get<string>('qwen.openaiBaseUrl', '');
|
||||
const model = config.get<string>('qwen.model', '');
|
||||
const proxy = config.get<string>('qwen.proxy', '');
|
||||
|
||||
// 构建额外的CLI参数
|
||||
const extraArgs: string[] = [];
|
||||
if (openaiApiKey) {
|
||||
extraArgs.push('--openai-api-key', openaiApiKey);
|
||||
}
|
||||
if (openaiBaseUrl) {
|
||||
extraArgs.push('--openai-base-url', openaiBaseUrl);
|
||||
}
|
||||
if (model) {
|
||||
extraArgs.push('--model', model);
|
||||
}
|
||||
if (proxy) {
|
||||
extraArgs.push('--proxy', proxy);
|
||||
console.log('[QwenAgentManager] Using proxy:', proxy);
|
||||
}
|
||||
|
||||
await connection.connect('qwen', cliPath, workingDir, extraArgs);
|
||||
|
||||
// 确定认证方法
|
||||
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
|
||||
|
||||
// 检查是否有有效的缓存认证
|
||||
if (authStateManager) {
|
||||
const hasValidAuth = await authStateManager.hasValidAuth(
|
||||
workingDir,
|
||||
authMethod,
|
||||
);
|
||||
if (hasValidAuth) {
|
||||
console.log('[QwenAgentManager] Using cached authentication');
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试恢复现有会话或创建新会话
|
||||
let sessionRestored = false;
|
||||
|
||||
// 尝试从本地文件获取会话
|
||||
console.log('[QwenAgentManager] Reading local session files...');
|
||||
try {
|
||||
const sessions = await sessionReader.getAllSessions(workingDir);
|
||||
|
||||
if (sessions.length > 0) {
|
||||
console.log(
|
||||
'[QwenAgentManager] Found existing sessions:',
|
||||
sessions.length,
|
||||
);
|
||||
const lastSession = sessions[0]; // 已按lastUpdated排序
|
||||
|
||||
try {
|
||||
await connection.switchSession(lastSession.sessionId);
|
||||
console.log(
|
||||
'[QwenAgentManager] Restored session:',
|
||||
lastSession.sessionId,
|
||||
);
|
||||
sessionRestored = true;
|
||||
} catch (switchError) {
|
||||
console.log(
|
||||
'[QwenAgentManager] session/switch not supported or failed:',
|
||||
switchError instanceof Error
|
||||
? switchError.message
|
||||
: String(switchError),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log('[QwenAgentManager] No existing sessions found');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.log(
|
||||
'[QwenAgentManager] Failed to read local sessions:',
|
||||
errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
// 如果无法恢复会话则创建新会话
|
||||
if (!sessionRestored) {
|
||||
console.log('[QwenAgentManager] Creating new session...');
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ WORKAROUND: Skipping explicit authenticate() call`,
|
||||
);
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ Reason: newSession() internally calls refreshAuth(), which triggers device flow`,
|
||||
);
|
||||
console.log(
|
||||
`[QwenAgentManager] ⚠️ Calling authenticate() first causes double authentication`,
|
||||
);
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`\n🔐 [AUTO AUTH] newSession will handle authentication automatically\n`,
|
||||
);
|
||||
await this.newSessionWithRetry(connection, workingDir, 3);
|
||||
console.log('[QwenAgentManager] New session created successfully');
|
||||
|
||||
// 保存认证状态
|
||||
if (authStateManager) {
|
||||
console.log(
|
||||
'[QwenAgentManager] Saving auth state after successful session creation',
|
||||
);
|
||||
await authStateManager.saveAuthState(workingDir, authMethod);
|
||||
}
|
||||
} catch (sessionError) {
|
||||
console.log(`\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`);
|
||||
console.log(`[QwenAgentManager] Error details:`, sessionError);
|
||||
|
||||
// 清除缓存
|
||||
if (authStateManager) {
|
||||
console.log('[QwenAgentManager] Clearing auth cache due to failure');
|
||||
await authStateManager.clearAuthState();
|
||||
}
|
||||
|
||||
throw sessionError;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n========================================`);
|
||||
console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`);
|
||||
console.log(`========================================\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话(带重试)
|
||||
*
|
||||
* @param connection - ACP连接实例
|
||||
* @param workingDir - 工作目录
|
||||
* @param maxRetries - 最大重试次数
|
||||
*/
|
||||
private async newSessionWithRetry(
|
||||
connection: AcpConnection,
|
||||
workingDir: string,
|
||||
maxRetries: number,
|
||||
): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
console.log(
|
||||
`[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`,
|
||||
);
|
||||
await connection.newSession(workingDir);
|
||||
console.log('[QwenAgentManager] Session created successfully');
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`[QwenAgentManager] Session creation attempt ${attempt} failed:`,
|
||||
errorMessage,
|
||||
);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw new Error(
|
||||
`Session creation failed after ${maxRetries} attempts: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||
console.log(`[QwenAgentManager] Retrying in ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Qwen会话更新处理器
|
||||
*
|
||||
* 负责处理来自ACP的会话更新,并分发到相应的回调函数
|
||||
*/
|
||||
|
||||
import type { AcpSessionUpdate } from '../shared/acpTypes.js';
|
||||
import type { QwenAgentCallbacks } from './QwenTypes.js';
|
||||
|
||||
/**
|
||||
* Qwen会话更新处理器类
|
||||
* 处理各种会话更新事件并调用相应的回调
|
||||
*/
|
||||
export class QwenSessionUpdateHandler {
|
||||
private callbacks: QwenAgentCallbacks;
|
||||
|
||||
constructor(callbacks: QwenAgentCallbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新回调函数
|
||||
*
|
||||
* @param callbacks - 新的回调函数集合
|
||||
*/
|
||||
updateCallbacks(callbacks: QwenAgentCallbacks): void {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理会话更新
|
||||
*
|
||||
* @param data - ACP会话更新数据
|
||||
*/
|
||||
handleSessionUpdate(data: AcpSessionUpdate): void {
|
||||
const update = data.update;
|
||||
|
||||
switch (update.sessionUpdate) {
|
||||
case 'user_message_chunk':
|
||||
// 处理用户消息块
|
||||
if (update.content?.text && this.callbacks.onStreamChunk) {
|
||||
this.callbacks.onStreamChunk(update.content.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_message_chunk':
|
||||
// 处理助手消息块
|
||||
if (update.content?.text && this.callbacks.onStreamChunk) {
|
||||
this.callbacks.onStreamChunk(update.content.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_thought_chunk':
|
||||
// 处理思考块 - 使用特殊回调
|
||||
if (update.content?.text) {
|
||||
if (this.callbacks.onThoughtChunk) {
|
||||
this.callbacks.onThoughtChunk(update.content.text);
|
||||
} else if (this.callbacks.onStreamChunk) {
|
||||
// 回退到常规流处理
|
||||
this.callbacks.onStreamChunk(update.content.text);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_call': {
|
||||
// 处理新的工具调用
|
||||
if (this.callbacks.onToolCall && 'toolCallId' in update) {
|
||||
this.callbacks.onToolCall({
|
||||
toolCallId: update.toolCallId as string,
|
||||
kind: (update.kind as string) || undefined,
|
||||
title: (update.title as string) || undefined,
|
||||
status: (update.status as string) || undefined,
|
||||
rawInput: update.rawInput,
|
||||
content: update.content as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined,
|
||||
locations: update.locations as
|
||||
| Array<{ path: string; line?: number | null }>
|
||||
| undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_call_update': {
|
||||
// 处理工具调用状态更新
|
||||
if (this.callbacks.onToolCall && 'toolCallId' in update) {
|
||||
this.callbacks.onToolCall({
|
||||
toolCallId: update.toolCallId as string,
|
||||
kind: (update.kind as string) || undefined,
|
||||
title: (update.title as string) || undefined,
|
||||
status: (update.status as string) || undefined,
|
||||
rawInput: update.rawInput,
|
||||
content: update.content as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined,
|
||||
locations: update.locations as
|
||||
| Array<{ path: string; line?: number | null }>
|
||||
| undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plan': {
|
||||
// 处理计划更新
|
||||
if ('entries' in update) {
|
||||
const entries = update.entries as Array<{
|
||||
content: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
}>;
|
||||
|
||||
if (this.callbacks.onPlan) {
|
||||
this.callbacks.onPlan(entries);
|
||||
} else if (this.callbacks.onStreamChunk) {
|
||||
// 回退到流处理
|
||||
const planText =
|
||||
'\n📋 Plan:\n' +
|
||||
entries
|
||||
.map(
|
||||
(entry, i) =>
|
||||
`${i + 1}. [${entry.priority}] ${entry.content}`,
|
||||
)
|
||||
.join('\n');
|
||||
this.callbacks.onStreamChunk(planText);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log('[QwenAgentManager] Unhandled session update type');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
packages/vscode-ide-companion/src/agents/QwenTypes.ts
Normal file
75
packages/vscode-ide-companion/src/agents/QwenTypes.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Qwen Agent Manager 类型定义
|
||||
*
|
||||
* 包含所有相关的接口和类型定义
|
||||
*/
|
||||
|
||||
import type { AcpPermissionRequest } from '../shared/acpTypes.js';
|
||||
|
||||
/**
|
||||
* 聊天消息
|
||||
*/
|
||||
export interface ChatMessage {
|
||||
/** 消息角色:用户或助手 */
|
||||
role: 'user' | 'assistant';
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划条目
|
||||
*/
|
||||
export interface PlanEntry {
|
||||
/** 条目内容 */
|
||||
content: string;
|
||||
/** 优先级 */
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
/** 状态 */
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用更新数据
|
||||
*/
|
||||
export interface ToolCallUpdateData {
|
||||
/** 工具调用ID */
|
||||
toolCallId: string;
|
||||
/** 工具类型 */
|
||||
kind?: string;
|
||||
/** 工具标题 */
|
||||
title?: string;
|
||||
/** 状态 */
|
||||
status?: string;
|
||||
/** 原始输入 */
|
||||
rawInput?: unknown;
|
||||
/** 内容 */
|
||||
content?: Array<Record<string, unknown>>;
|
||||
/** 位置信息 */
|
||||
locations?: Array<{ path: string; line?: number | null }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调函数集合
|
||||
*/
|
||||
export interface QwenAgentCallbacks {
|
||||
/** 消息回调 */
|
||||
onMessage?: (message: ChatMessage) => void;
|
||||
/** 流式文本块回调 */
|
||||
onStreamChunk?: (chunk: string) => void;
|
||||
/** 思考文本块回调 */
|
||||
onThoughtChunk?: (chunk: string) => void;
|
||||
/** 工具调用回调 */
|
||||
onToolCall?: (update: ToolCallUpdateData) => void;
|
||||
/** 计划回调 */
|
||||
onPlan?: (entries: PlanEntry[]) => void;
|
||||
/** 权限请求回调 */
|
||||
onPermissionRequest?: (request: AcpPermissionRequest) => Promise<string>;
|
||||
}
|
||||
Reference in New Issue
Block a user