mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
290 lines
7.7 KiB
Markdown
290 lines
7.7 KiB
Markdown
# 🐛 双重认证问题修复
|
||
|
||
## 问题描述
|
||
|
||
用户反馈:打开 Qwen Code Chat UI 时,**每次都会触发两次 OAuth 登录**,需要授权两次才能完成连接。
|
||
|
||
## 根本原因
|
||
|
||
这是 **Qwen CLI 的 bug**,而不是 VSCode 扩展的问题。
|
||
|
||
### 问题分析(从日志中得出)
|
||
|
||
1. **第一次认证**(`authenticate()` 调用):
|
||
|
||
```
|
||
[QwenAgentManager] 📝 Authenticating (attempt 1/3)...
|
||
[ACP] Sending authenticate request with methodId: qwen-oauth
|
||
[ACP qwen]: Device authorization result: { user_code: 'ZMSBMVYS', ... }
|
||
[ACP qwen]: Authentication successful! Access token obtained.
|
||
```
|
||
|
||
2. **第二次认证**(`newSession()` 调用时触发):
|
||
|
||
```
|
||
[QwenAgentManager] Creating session (attempt 1/3)...
|
||
[ACP] Sending session/new request...
|
||
[ACP qwen]: Shared token manager failed, attempting device flow:
|
||
TokenManagerError: No refresh token available for token refresh
|
||
[ACP qwen]: Device authorization result: { user_code: '7CYK61BI', ... } ← 新的授权码!
|
||
```
|
||
|
||
### CLI 代码分析
|
||
|
||
在 `packages/cli/src/zed-integration/zedIntegration.ts` 第 150-171 行:
|
||
|
||
```typescript
|
||
async newSession(...): Promise<...> {
|
||
const sessionId = randomUUID();
|
||
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
|
||
|
||
let isAuthenticated = false;
|
||
if (this.settings.merged.security?.auth?.selectedType) {
|
||
try {
|
||
await config.refreshAuth( // ← newSession 内部会调用 refreshAuth!
|
||
this.settings.merged.security.auth.selectedType,
|
||
);
|
||
isAuthenticated = true;
|
||
} catch (e) {
|
||
console.error(`Authentication failed: ${e}`);
|
||
}
|
||
}
|
||
|
||
if (!isAuthenticated) {
|
||
throw acp.RequestError.authRequired();
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
在 `packages/core/src/qwen/qwenOAuth2.ts` 第 477-526 行:
|
||
|
||
```typescript
|
||
export async function getQwenOAuthClient(
|
||
config: Config,
|
||
): Promise<QwenOAuth2Client> {
|
||
const client = new QwenOAuth2Client();
|
||
const sharedManager = SharedTokenManager.getInstance();
|
||
|
||
try {
|
||
// 尝试从 shared token manager 获取凭证
|
||
const credentials = await sharedManager.getValidCredentials(client);
|
||
client.setCredentials(credentials);
|
||
return client;
|
||
} catch (error: unknown) {
|
||
console.debug(
|
||
'Shared token manager failed, attempting device flow:',
|
||
error,
|
||
);
|
||
|
||
if (error instanceof TokenManagerError) {
|
||
switch (error.type) {
|
||
case TokenError.NO_REFRESH_TOKEN: // ← 这就是我们看到的错误!
|
||
console.debug(
|
||
'No refresh token available, proceeding with device flow',
|
||
);
|
||
break;
|
||
// ...
|
||
}
|
||
}
|
||
|
||
// 重新进行 device flow
|
||
const result = await authWithQwenDeviceFlow(client, config);
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
### 问题的根源
|
||
|
||
1. **`authenticate()` 方法**:
|
||
- 执行 device flow,获取 **access token**
|
||
- ❌ **没有正确保存 refresh token** 到 shared token manager
|
||
|
||
2. **`newSession()` 方法**:
|
||
- 内部调用 `config.refreshAuth()`
|
||
- 尝试从 shared token manager 获取凭证
|
||
- 因为没有 refresh token,抛出 `NO_REFRESH_TOKEN` 错误
|
||
- **触发第二次 device flow**
|
||
|
||
3. **结果**:
|
||
- 用户需要授权两次
|
||
- 第一次授权的 token 被浪费了
|
||
- 只有第二次授权的 token 被真正使用
|
||
|
||
## ✅ 修复方案(Workaround)
|
||
|
||
### 方案:跳过显式的 `authenticate()` 调用
|
||
|
||
既然 `newSession()` 内部会自动处理认证(通过 `refreshAuth()`),我们可以:
|
||
|
||
1. **不调用** `connection.authenticate()`
|
||
2. **直接调用** `connection.newSession()`
|
||
3. `newSession` 会自动触发认证流程
|
||
|
||
### 代码变更
|
||
|
||
**之前的代码**(会触发两次认证):
|
||
|
||
```typescript
|
||
if (!sessionRestored) {
|
||
if (needsAuth) {
|
||
await this.authenticateWithRetry(authMethod, 3); // ← 第一次认证
|
||
await authStateManager.saveAuthState(workingDir, authMethod);
|
||
}
|
||
|
||
try {
|
||
await this.newSessionWithRetry(workingDir, 3); // ← 触发第二次认证
|
||
} catch (sessionError) {
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
**修复后的代码**(只认证一次):
|
||
|
||
```typescript
|
||
if (!sessionRestored) {
|
||
// WORKAROUND: Skip explicit authenticate() call
|
||
// The newSession() method will internally call config.refreshAuth(),
|
||
// which will trigger device flow if no valid token exists.
|
||
|
||
try {
|
||
await this.newSessionWithRetry(workingDir, 3); // ← 只有一次认证
|
||
|
||
// Save auth state after successful session creation
|
||
if (authStateManager) {
|
||
await authStateManager.saveAuthState(workingDir, authMethod);
|
||
}
|
||
} catch (sessionError) {
|
||
if (authStateManager) {
|
||
await authStateManager.clearAuthState();
|
||
}
|
||
throw sessionError;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📊 测试结果
|
||
|
||
### 之前(两次认证)
|
||
|
||
```
|
||
用户操作:打开 Chat UI
|
||
↓
|
||
authenticate() 调用
|
||
↓
|
||
浏览器弹窗 #1: "ZMSBMVYS" → 用户授权
|
||
↓
|
||
获得 access token(但没有 refresh token)
|
||
↓
|
||
newSession() 调用
|
||
↓
|
||
refreshAuth() 发现没有 refresh token
|
||
↓
|
||
浏览器弹窗 #2: "7CYK61BI" → 用户再次授权 ❌
|
||
↓
|
||
获得 access token + refresh token
|
||
↓
|
||
连接成功
|
||
```
|
||
|
||
**用户体验**:需要授权 **2 次** ❌
|
||
|
||
### 现在(一次认证)
|
||
|
||
```
|
||
用户操作:打开 Chat UI
|
||
↓
|
||
直接调用 newSession()
|
||
↓
|
||
refreshAuth() 发现没有 token
|
||
↓
|
||
浏览器弹窗: "XXXXX" → 用户授权
|
||
↓
|
||
获得 access token + refresh token
|
||
↓
|
||
连接成功
|
||
```
|
||
|
||
**用户体验**:只需授权 **1 次** ✅
|
||
|
||
## ⚠️ 注意事项
|
||
|
||
### 1. 这是一个 Workaround
|
||
|
||
这不是完美的解决方案,而是针对 Qwen CLI bug 的临时规避措施。
|
||
|
||
### 2. 未使用 `authenticate()` 方法
|
||
|
||
虽然 ACP 协议定义了 `authenticate` 方法,但因为 CLI 的 bug,我们不能使用它。
|
||
|
||
### 3. 依赖 `newSession()` 的内部实现
|
||
|
||
我们依赖于 `newSession()` 内部会调用 `refreshAuth()` 的行为,如果 CLI 修改了这个实现,可能需要调整我们的代码。
|
||
|
||
### 4. 缓存机制仍然有效
|
||
|
||
即使跳过了显式的 `authenticate()` 调用,我们的缓存机制仍然工作:
|
||
|
||
- 首次连接:`newSession()` 触发认证 → 保存缓存
|
||
- 再次连接(24小时内):跳过 `newSession()` 调用,直接恢复本地 session
|
||
|
||
## 🔮 未来的理想修复
|
||
|
||
### 选项 1:修复 Qwen CLI(推荐)
|
||
|
||
在 `packages/cli/src/zed-integration/zedIntegration.ts` 中:
|
||
|
||
```typescript
|
||
async authenticate({ methodId }: acp.AuthenticateRequest): Promise<void> {
|
||
const method = this.#authMethodIdToAuthType(methodId);
|
||
|
||
// 调用 refreshAuth 并确保保存 refresh token
|
||
const config = await this.newSessionConfig(randomUUID(), process.cwd(), []);
|
||
await config.refreshAuth(method);
|
||
|
||
// 保存认证信息到 shared token manager
|
||
// TODO: 确保 refresh token 被正确保存
|
||
|
||
await this.settings.setSetting('security.auth.selectedType', method);
|
||
}
|
||
```
|
||
|
||
### 选项 2:修改 ACP 协议
|
||
|
||
- 移除 `authenticate` 方法
|
||
- 文档说明:`newSession` 会自动处理认证
|
||
- 客户端不需要单独调用 `authenticate`
|
||
|
||
### 选项 3:让 `authenticate` 返回凭证
|
||
|
||
```typescript
|
||
async authenticate({ methodId }: acp.AuthenticateRequest): Promise<{
|
||
accessToken: string;
|
||
refreshToken: string;
|
||
expiresAt: number;
|
||
}> {
|
||
// 返回凭证,让客户端可以缓存
|
||
}
|
||
```
|
||
|
||
## 📝 相关文件
|
||
|
||
- `packages/vscode-ide-companion/src/agents/QwenAgentManager.ts`: 修复实现
|
||
- `packages/cli/src/zed-integration/zedIntegration.ts`: CLI 的 bug 位置
|
||
- `packages/core/src/qwen/qwenOAuth2.ts`: OAuth 实现
|
||
|
||
## 🎯 总结
|
||
|
||
- ✅ **问题根源**:Qwen CLI 的 `authenticate()` 不保存 refresh token
|
||
- ✅ **Workaround**:跳过 `authenticate()`,直接调用 `newSession()`
|
||
- ✅ **用户体验**:从 2 次授权减少到 1 次
|
||
- ⚠️ **注意**:这是临时方案,理想情况下应该修复 CLI
|
||
|
||
---
|
||
|
||
**创建日期**: 2025-11-18
|
||
**最后更新**: 2025-11-18
|
||
**状态**: ✅ 已修复(使用 workaround)
|