Files
qwen-code/packages/vscode-ide-companion/docs-tmp/DOUBLE_AUTH_FIX.md
yiliang114 732220e651 wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧
- 添加 ChatHeader 组件,实现会话下拉菜单
- 替换模态框为紧凑型下拉菜单
- 更新会话切换逻辑,显示当前标题
- 清理旧的会话选择器样式
基于 Claude Code v2.0.43 UI 分析实现。
2025-11-19 00:16:45 +08:00

290 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🐛 双重认证问题修复
## 问题描述
用户反馈:打开 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