import * as vscode from "vscode"; import * as http from "http"; import * as path from "path"; import * as fs from "fs"; import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService"; import { getConfig } from "../config/settings"; import { resetInvitationVerification } from "./apiClient"; /** * IC Coder Authentication Provider * 集成到 VSCode 账户系统 */ export class ICCoderAuthenticationProvider implements vscode.AuthenticationProvider { private static readonly AUTH_TYPE = "iccoder"; private static readonly AUTH_NAME = "IC Coder"; private static loginServer: http.Server | null = null; private static currentPort: number | null = null; private _onDidChangeSessions = new vscode.EventEmitter(); public readonly onDidChangeSessions = this._onDidChangeSessions.event; private _sessions: vscode.AuthenticationSession[] = []; constructor(private readonly context: vscode.ExtensionContext) { // 从存储中恢复会话(同步执行) this.loadSessionsSync(); } /** * 从存储中加载会话(同步版本) */ private loadSessionsSync(): void { const storedSessions = this.context.globalState.get< vscode.AuthenticationSession[] >("icCoderSessions", []); this._sessions = storedSessions; console.log("[AuthProvider] 同步加载 sessions, 数量:", this._sessions.length); if (this._sessions.length > 0) { console.log("[AuthProvider] Session ID:", this._sessions[0].id); console.log("[AuthProvider] Account:", this._sessions[0].account.label); } } /** * 从存储中加载会话 */ private async loadSessions(): Promise { const storedSessions = this.context.globalState.get< vscode.AuthenticationSession[] >("icCoderSessions", []); this._sessions = storedSessions; } /** * 保存会话到存储 */ private async saveSessions(): Promise { console.log("[AuthProvider] 保存 sessions, 数量:", this._sessions.length); await this.context.globalState.update("icCoderSessions", this._sessions); console.log("[AuthProvider] sessions 已保存到 globalState"); } /** * 获取会话列表 */ async getSessions( scopes?: readonly string[] ): Promise { console.log("[AuthProvider] getSessions 被调用, 当前 sessions 数量:", this._sessions.length); return [...this._sessions]; } /** * 创建会话(登录) */ async createSession( scopes: readonly string[] ): Promise { try { // 先删除旧的 session(静默删除,不弹窗、不重载窗口) if (this._sessions.length > 0) { const oldSession = this._sessions[0]; this._sessions = []; await this.saveSessions(); await clearUserInfo(); this._onDidChangeSessions.fire({ added: [], removed: [oldSession], changed: [], }); console.log("🔄 已清除旧的 session"); } const token = await this.login(); // 获取到 token 后立即调用用户信息接口 const userInfo = await onTokenReceived(token); // 创建会话 const session: vscode.AuthenticationSession = { id: this.generateSessionId(), accessToken: token, account: { id: userInfo?.userId || "iccoder-user", label: userInfo?.nickname || userInfo?.username || "IC Coder 用户", }, scopes: [...scopes], }; this._sessions.push(session); await this.saveSessions(); // 触发会话变化事件 this._onDidChangeSessions.fire({ added: [session], removed: [], changed: [], }); vscode.window.showInformationMessage("登录成功!窗口将自动刷新..."); // 延迟 1 秒后重新加载窗口,让用户看到成功消息 setTimeout(() => { vscode.commands.executeCommand("workbench.action.reloadWindow"); }, 1000); return session; } catch (error) { vscode.window.showErrorMessage( `登录失败: ${error instanceof Error ? error.message : String(error)}` ); throw error; } } /** * 删除会话(登出) */ async removeSession(sessionId: string): Promise { const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId); if (sessionIndex > -1) { const session = this._sessions[sessionIndex]; // 1. 先调用后端重置邀请码验证状态 try { await resetInvitationVerification(); console.log("[AuthProvider] 邀请码验证状态已重置"); } catch (error) { console.warn("[AuthProvider] 重置邀请码验证状态失败,但继续退出流程:", error); // 即使失败也继续退出流程 } // 2. 清除本地 session this._sessions.splice(sessionIndex, 1); await this.saveSessions(); // 3. 清除用户信息缓存 await clearUserInfo(); // 4. 触发会话变化事件 this._onDidChangeSessions.fire({ added: [], removed: [session], changed: [], }); vscode.window.showInformationMessage("已退出登录!窗口将自动刷新..."); // 延迟 1 秒后重新加载窗口,让用户看到成功消息 setTimeout(() => { vscode.commands.executeCommand("workbench.action.reloadWindow"); }, 1000); } } /** * Clear local authentication state without window reload. * Used by re-login flow when session is expired. */ async clearSessionsForRelogin(): Promise { if (this._sessions.length === 0) { await clearUserInfo(); return; } const removed = [...this._sessions]; this._sessions = []; await this.saveSessions(); await clearUserInfo(); this._onDidChangeSessions.fire({ added: [], removed, changed: [], }); } /** * 生成会话 ID */ private generateSessionId(): string { return `iccoder-${Date.now()}-${Math.random().toString(36).substring(7)}`; } /** * 登录逻辑(打开浏览器并等待回调) */ private async login(): Promise { // 如果已有服务器在运行,先关闭 if (ICCoderAuthenticationProvider.loginServer) { ICCoderAuthenticationProvider.loginServer.close(); ICCoderAuthenticationProvider.loginServer = null; } // 创建本地服务器监听回调 const { server, port } = await this.createCallbackServer(); ICCoderAuthenticationProvider.loginServer = server; ICCoderAuthenticationProvider.currentPort = port; // 构建登录 URL const callbackUrl = `http://localhost:${port}/callback`; const config = getConfig(); const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`; console.log("🔐 登录服务器已启动,监听端口:", port); console.log("🌐 登录 URL:", loginUrl); // 打开浏览器登录 await vscode.env.openExternal(vscode.Uri.parse(loginUrl)); vscode.window.showInformationMessage( "请在浏览器中完成登录,登录成功后将自动返回..." ); // 等待 token(通过 Promise) return new Promise((resolve, reject) => { const timeout = setTimeout(() => { if (ICCoderAuthenticationProvider.loginServer) { ICCoderAuthenticationProvider.loginServer.close(); ICCoderAuthenticationProvider.loginServer = null; reject(new Error("登录超时")); } }, 5 * 60 * 1000); // 将 resolve 和 reject 保存到服务器上下文 (server as any)._loginResolve = resolve; (server as any)._loginReject = reject; (server as any)._loginTimeout = timeout; }); } /** * 创建本地回调服务器 */ private createCallbackServer(): Promise<{ server: http.Server; port: number; }> { return new Promise((resolve, reject) => { // 读取 icon.png 并转换为 Base64 const iconPath = path.join( this.context.extensionPath, "media", "icon.png" ); let iconBase64 = ""; try { const iconBuffer = fs.readFileSync(iconPath); iconBase64 = `data:image/png;base64,${iconBuffer.toString("base64")}`; } catch (error) { console.warn("无法读取 icon.png:", error); } const server = http.createServer(async (req, res) => { try { console.log("📥 收到回调请求:", req.url); const url = new URL( req.url!, `http://localhost:${ICCoderAuthenticationProvider.currentPort}` ); console.log("📍 路径:", url.pathname); console.log("📋 所有参数:", Object.fromEntries(url.searchParams)); if (url.pathname === "/callback") { const token = url.searchParams.get("token"); console.log("🔑 Token:", token ? "已获取" : "未找到"); if (token) { // 返回成功页面 res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", }); res.end(this.getSuccessPage(iconBase64)); // 关闭服务器 server.close(); ICCoderAuthenticationProvider.loginServer = null; // 清除超时 if ((server as any)._loginTimeout) { clearTimeout((server as any)._loginTimeout); } // 返回 token if ((server as any)._loginResolve) { (server as any)._loginResolve(token); } } else { res.writeHead(400, { "Content-Type": "text/html; charset=utf-8", }); res.end(` 登录失败

❌ 登录失败

未获取到有效的 Token

`); if ((server as any)._loginReject) { (server as any)._loginReject(new Error("未获取到有效的 Token")); } } } else { res.writeHead(404); res.end("Not Found"); } } catch (error) { res.writeHead(500); res.end("Internal Server Error"); if ((server as any)._loginReject) { (server as any)._loginReject(error); } } }); // 监听端口(使用 0 表示自动分配可用端口) server.listen(0, () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 3000; resolve({ server, port }); }); // 处理错误 server.on("error", (error: NodeJS.ErrnoException) => { reject(error); }); }); } /** * 获取登录成功页面 HTML */ private getSuccessPage(iconBase64: string): string { return ` 登录成功 - IC Coder

登录成功!

您已成功登录 IC Coder
现在可以返回 VSCode 继续使用

IC Coder
`; } }