const crypto = require('crypto') const { getProxyAgent, getChatBaseUrl, applyProxyToFetchOptions } = require('./proxy-helper') /** * 为 PKCE 生成随机代码验证器 * @returns {string} 43-128个字符的随机字符串 */ function generateCodeVerifier() { return crypto.randomBytes(32).toString('base64url') } /** * 使用 SHA-256 从代码验证器生成代码挑战 * @param {string} codeVerifier - 代码验证器字符串 * @returns {string} 代码挑战字符串 */ function generateCodeChallenge(codeVerifier) { const hash = crypto.createHash('sha256') hash.update(codeVerifier) return hash.digest('base64url') } /** * 生成 PKCE 代码验证器和挑战对 * @returns {Object} 包含 code_verifier 和 code_challenge 的对象 */ function generatePKCEPair() { const codeVerifier = generateCodeVerifier() const codeChallenge = generateCodeChallenge(codeVerifier) return { code_verifier: codeVerifier, code_challenge: codeChallenge } } class CliAuthManager { /** * 启动 OAuth 设备授权流程 * @returns {Promise} 包含设备代码、验证URL和代码验证器的对象 */ async initiateDeviceFlow() { // 生成 PKCE 代码验证器和挑战 const { code_verifier, code_challenge } = generatePKCEPair() const bodyData = new URLSearchParams({ client_id: "f0304373b74a44d2b584a3fb70ca9e56", scope: "openid profile email model.completion", code_challenge: code_challenge, code_challenge_method: 'S256', }) const chatBaseUrl = getChatBaseUrl() const proxyAgent = getProxyAgent() const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: bodyData, } // 添加代理配置 if (proxyAgent) { fetchOptions.agent = proxyAgent } try { const response = await fetch(`${chatBaseUrl}/api/v1/oauth2/device/code`, fetchOptions) if (response.ok) { const result = await response.json() return { status: true, ...result, code_verifier: code_verifier } } else { throw new Error() } } catch (error) { return { status: false, device_code: null, user_code: null, verification_uri: null, verification_uri_complete: null, expires_in: null, code_verifier: null } } } /** * 授权登录 * @param {string} user_code - 用户代码 * @param {string} access_token - 访问令牌 * @returns {Promise} 是否授权成功 */ async authorizeLogin(user_code, access_token) { try { const chatBaseUrl = getChatBaseUrl() const proxyAgent = getProxyAgent() const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', "authorization": `Bearer ${access_token}`, }, body: JSON.stringify({ "approved": true, "user_code": user_code }) } if (proxyAgent) { fetchOptions.agent = proxyAgent } const response = await fetch(`${chatBaseUrl}/api/v2/oauth2/authorize`, fetchOptions) if (response.ok) { return true } else { throw new Error() } } catch (error) { return false } } /** * 轮询获取访问令牌 * @param {string} device_code - 设备代码 * @param {string} code_verifier - 代码验证器 * @returns {Promise} 访问令牌信息 */ async pollForToken(device_code, code_verifier) { let pollInterval = 5000 const maxAttempts = 60 const chatBaseUrl = getChatBaseUrl() const proxyAgent = getProxyAgent() for (let attempt = 0; attempt < maxAttempts; attempt++) { const bodyData = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", client_id: "f0304373b74a44d2b584a3fb70ca9e56", device_code: device_code, code_verifier: code_verifier, }) const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: bodyData, } if (proxyAgent) { fetchOptions.agent = proxyAgent } try { const response = await fetch(`${chatBaseUrl}/api/v1/oauth2/token`, fetchOptions) if (response.ok) { const tokenData = await response.json() // 转换为凭据格式 const credentials = { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token || undefined, expiry_date: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined, } return credentials } // 等待5秒, 然后继续轮询 await new Promise(resolve => setTimeout(resolve, pollInterval)) } catch (error) { // 等待5秒, 然后继续轮询 await new Promise(resolve => setTimeout(resolve, pollInterval)) console.log(`轮询尝试 ${attempt + 1}/${maxAttempts} 失败:`, error) continue } } return { status: false, access_token: null, refresh_token: null, expiry_date: null } } /** * 初始化 CLI 账户 * @param {string} access_token - 访问令牌 * @returns {Promise} 账户信息 */ async initCliAccount(access_token) { const deviceFlow = await this.initiateDeviceFlow() if (!deviceFlow.status || !await this.authorizeLogin(deviceFlow.user_code, access_token)) { return { status: false, access_token: null, refresh_token: null, expiry_date: null } } return await this.pollForToken(deviceFlow.device_code, deviceFlow.code_verifier) } /** * 刷新访问令牌 * @param {Object} CliAccount - 账户信息 * @returns {Promise} 账户信息 */ async refreshAccessToken(CliAccount) { try { if (!CliAccount || !CliAccount.refresh_token) { throw new Error() } const chatBaseUrl = getChatBaseUrl() const proxyAgent = getProxyAgent() const bodyData = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: CliAccount.refresh_token, client_id: "f0304373b74a44d2b584a3fb70ca9e56", }) const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body: bodyData } if (proxyAgent) { fetchOptions.agent = proxyAgent } const response = await fetch(`${chatBaseUrl}/api/v1/oauth2/token`, fetchOptions) if (response.ok) { const tokenData = await response.json() return { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token || CliAccount.refresh_token, expiry_date: Date.now() + tokenData.expires_in * 1000, } } } catch (error) { return { status: false, access_token: null, refresh_token: null, expiry_date: null } } } } module.exports = new CliAuthManager()