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

7.7 KiB
Raw Blame History

🐛 双重认证问题修复

问题描述

用户反馈:打开 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.
  1. 第二次认证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 行:

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 行:

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 会自动触发认证流程

代码变更

之前的代码(会触发两次认证):

if (!sessionRestored) {
  if (needsAuth) {
    await this.authenticateWithRetry(authMethod, 3); // ← 第一次认证
    await authStateManager.saveAuthState(workingDir, authMethod);
  }

  try {
    await this.newSessionWithRetry(workingDir, 3); // ← 触发第二次认证
  } catch (sessionError) {
    // ...
  }
}

修复后的代码(只认证一次):

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 中:

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

选项 3authenticate 返回凭证

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