Qwen2API-A / src /utils /cli.manager.js
github-actions[bot]
Sync from GitHub Viciy2023/Qwen2API-A@ae093476e9bc5b0a599620b5925df3a20057038e
f120063
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<Object>} 包含设备代码、验证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<boolean>} 是否授权成功
*/
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<Object>} 访问令牌信息
*/
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<Object>} 账户信息
*/
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<Object>} 账户信息
*/
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()