mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-17 22:07:10 +00:00
fix: update CopilotAIClient.js
This commit is contained in:
parent
4aa634b01c
commit
c13f4dec05
1 changed files with 222 additions and 87 deletions
|
|
@ -1,156 +1,291 @@
|
|||
import WebSocket from 'ws';
|
||||
import WebSocket from 'ws'
|
||||
import common from '../../../lib/common/common.js'
|
||||
|
||||
export class BingAIClient {
|
||||
constructor(baseUrl = 'wss://copilot.microsoft.com/c/api/chat') {
|
||||
this.baseUrl = baseUrl;
|
||||
this.ws = null;
|
||||
this.conversationId = null;
|
||||
this.currentMessageId = null;
|
||||
this.partialMessages = new Map();
|
||||
constructor (accessToken, baseUrl = 'wss://copilot.microsoft.com/c/api/chat', debug, _2captchaKey, clientId, scope, refreshToken, oid) {
|
||||
this.accessToken = accessToken
|
||||
this.baseUrl = baseUrl
|
||||
this.ws = null
|
||||
this.conversationId = null
|
||||
this.partialMessages = new Map()
|
||||
this.debug = debug
|
||||
this._2captchaKey = _2captchaKey
|
||||
this.clientId = clientId
|
||||
this.scope = scope
|
||||
this.refreshToken = refreshToken
|
||||
this.oid = oid
|
||||
}
|
||||
|
||||
async sendMessage(text, options = {}) {
|
||||
// If conversationId is provided, use it, otherwise create a new one
|
||||
async sendMessage (text, options = {}) {
|
||||
// 如果 options 中有 conversationId,使用它;否则生成一个新的 conversationId
|
||||
if (options.conversationId) {
|
||||
this.conversationId = options.conversationId;
|
||||
this.conversationId = options.conversationId
|
||||
} else {
|
||||
this.conversationId = this._generateConversationId();
|
||||
this.conversationId = this._generateConversationId()
|
||||
}
|
||||
|
||||
// Connect WebSocket
|
||||
await this.connectWebSocket();
|
||||
// 建立 WebSocket 连接
|
||||
await this.connectWebSocket()
|
||||
|
||||
// Send the initial message or challenge response
|
||||
await this.sendInitialMessage();
|
||||
// 发送消息
|
||||
await this.sendInitialMessage(text)
|
||||
|
||||
// Send the text message
|
||||
const messagePayload = {
|
||||
event: 'send',
|
||||
conversationId: this.conversationId,
|
||||
content: [{ type: 'text', text }],
|
||||
mode: 'chat',
|
||||
context: { edge: 'NonContextual' },
|
||||
};
|
||||
|
||||
this.ws.send(JSON.stringify(messagePayload));
|
||||
|
||||
// Wait for the response and collect the full message
|
||||
const responseText = await this.collectResponse();
|
||||
return responseText;
|
||||
// 等待并收集服务器的回复
|
||||
const responseText = await this.collectResponse()
|
||||
return responseText
|
||||
}
|
||||
|
||||
async connectWebSocket() {
|
||||
async connectWebSocket () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new WebSocket(this.baseUrl);
|
||||
let url = `${this.baseUrl}?api-version=2`
|
||||
if (this.accessToken) {
|
||||
url += '&accessToken=' + this.accessToken
|
||||
}
|
||||
this.ws = new WebSocket(url)
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('WebSocket connection established.');
|
||||
resolve();
|
||||
});
|
||||
console.log('WebSocket connection established.')
|
||||
resolve()
|
||||
})
|
||||
|
||||
this.ws.on('message', (data) => this.handleServerMessage(data));
|
||||
if (this.debug) {
|
||||
this.ws.on('message', (message) => {
|
||||
logger.info(JSON.stringify(message))
|
||||
})
|
||||
}
|
||||
this.ws.on('close', (code, reason) => {
|
||||
console.log('WebSocket connection closed. Code:', code, 'Reason:', reason)
|
||||
|
||||
this.ws.on('close', () => {
|
||||
console.log('WebSocket connection closed.');
|
||||
});
|
||||
// 401 错误码通常是未授权,可以根据实际情况修改
|
||||
if (code === 401) {
|
||||
logger.error('token expired. try to refresh with refresh token')
|
||||
this.doRefreshToken(this.clientId, this.scope, this.refreshToken, this.oid)
|
||||
}
|
||||
})
|
||||
|
||||
this.ws.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async sendInitialMessage() {
|
||||
async sendInitialMessage (text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messagePayload = {
|
||||
event: 'send',
|
||||
conversationId: this.conversationId,
|
||||
content: [{ type: 'text', text }],
|
||||
mode: 'chat',
|
||||
context: { edge: 'NonContextual' }
|
||||
}
|
||||
|
||||
// 直接发送消息
|
||||
this.ws.send(JSON.stringify(messagePayload))
|
||||
|
||||
// 设置超时机制,防止长时间未收到消息
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('No response from server within timeout period.'))
|
||||
}, 5000) // 设置 5 秒的超时时间
|
||||
|
||||
// 一旦收到消息,处理逻辑
|
||||
this.ws.once('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
clearTimeout(timeout) // 清除超时定时器
|
||||
const message = JSON.parse(data)
|
||||
|
||||
if (message.event === 'challenge') {
|
||||
// Handle challenge by sending the challenge response
|
||||
logger.info(JSON.stringify(message))
|
||||
logger.warn('遇到turnstile验证码,尝试使用2captcha解决')
|
||||
// 如果收到 challenge,处理挑战
|
||||
this.handleChallenge(message)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
.catch(reject)
|
||||
} else {
|
||||
resolve(); // Proceed if no challenge event
|
||||
// 否则直接进入对话
|
||||
resolve()
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async handleChallenge(challenge) {
|
||||
// Get the token by calling getTurnstile function (you need to define this function)
|
||||
const token = await this.getTurnstile(challenge.conversationId);
|
||||
async handleChallenge (challenge) {
|
||||
// 获取 challenge 的 token(你需要根据实际情况实现此方法)
|
||||
if (!this._2captchaKey) {
|
||||
throw new Error('No 2captchaKey')
|
||||
}
|
||||
const token = await this.getTurnstile(challenge.conversationId)
|
||||
|
||||
const challengeResponse = {
|
||||
event: 'challengeResponse',
|
||||
token: token,
|
||||
method: 'cloudflare',
|
||||
};
|
||||
token,
|
||||
method: 'cloudflare'
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(challengeResponse));
|
||||
this.ws.send(JSON.stringify(challengeResponse))
|
||||
}
|
||||
|
||||
async collectResponse() {
|
||||
async collectResponse () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fullResponse = '';
|
||||
|
||||
const checkMessageComplete = (messageId) => {
|
||||
// Wait for the complete message
|
||||
// 如果消息已经完成,返回完整的消息内容
|
||||
if (this.partialMessages.has(messageId) && this.partialMessages.get(messageId).done) {
|
||||
const completeMessage = this.partialMessages.get(messageId).text;
|
||||
resolve(completeMessage);
|
||||
const completeMessage = this.partialMessages.get(messageId).text
|
||||
resolve(completeMessage)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
const message = JSON.parse(data)
|
||||
|
||||
switch (message.event) {
|
||||
case 'received':
|
||||
break;
|
||||
break
|
||||
|
||||
case 'startMessage':
|
||||
this.currentMessageId = message.messageId;
|
||||
break;
|
||||
this.currentMessageId = message.messageId
|
||||
break
|
||||
|
||||
case 'appendText':
|
||||
if (!this.partialMessages.has(message.messageId)) {
|
||||
this.partialMessages.set(message.messageId, { text: '', done: false });
|
||||
this.partialMessages.set(message.messageId, { text: '', done: false })
|
||||
}
|
||||
|
||||
this.partialMessages.get(message.messageId).text += message.text;
|
||||
this.partialMessages.get(message.messageId).text += message.text
|
||||
|
||||
// Check if this part is the last one
|
||||
// 如果是最后一部分,标记为完成
|
||||
if (message.partId === '0') {
|
||||
this.partialMessages.get(message.messageId).done = true;
|
||||
this.partialMessages.get(message.messageId).done = true
|
||||
}
|
||||
|
||||
checkMessageComplete(message.messageId);
|
||||
break;
|
||||
checkMessageComplete(message.messageId)
|
||||
break
|
||||
|
||||
case 'partCompleted':
|
||||
break;
|
||||
break
|
||||
|
||||
case 'done':
|
||||
checkMessageComplete(message.messageId);
|
||||
break;
|
||||
checkMessageComplete(message.messageId)
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn('Unexpected event:', message.event);
|
||||
break;
|
||||
console.warn('Unexpected event:', message.event)
|
||||
break
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getTurnstile(conversationId) {
|
||||
// This is a mock implementation, you should replace this with the actual logic
|
||||
// to interact with the turnstile system to get the token.
|
||||
// In a real-world scenario, this would involve making a request to some endpoint.
|
||||
return '0.EHn0JUxxxx';
|
||||
async getTurnstile (conversationId) {
|
||||
// 这里需要根据实际情况实现获取 challenge token 的方法
|
||||
const myHeaders = new Headers()
|
||||
myHeaders.append('Content-Type', 'application/json')
|
||||
|
||||
const raw = JSON.stringify({
|
||||
clientKey: this._2captchaKey,
|
||||
task: {
|
||||
type: 'TurnstileTaskProxyless',
|
||||
websiteURL: 'https://copilot.microsoft.com/chats/' + conversationId,
|
||||
websiteKey: '0x4AAAAAAAg146IpY3lPNWte'
|
||||
}
|
||||
})
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.2captcha.com/createTask', requestOptions)
|
||||
const createTaskRsp = await response.json()
|
||||
const taskId = createTaskRsp.taskId
|
||||
|
||||
const raw2 = JSON.stringify({
|
||||
taskId,
|
||||
clientKey: this._2captchaKey
|
||||
})
|
||||
async function getTaskResult () {
|
||||
const requestOptions2 = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw2,
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
const response2 = await fetch('https://api.2captcha.com/getTaskResult', requestOptions2)
|
||||
const taskResponse = await response2.json()
|
||||
if (this.debug) {
|
||||
logger.info(JSON.stringify(taskResponse))
|
||||
}
|
||||
const token = taskResponse?.solution?.token
|
||||
return token
|
||||
}
|
||||
let retry = 90
|
||||
let token = await getTaskResult()
|
||||
while (retry > 0 && !token) {
|
||||
await common.sleep(1000)
|
||||
token = await getTaskResult()
|
||||
retry--
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error('No response from server within timeout period.')
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
_generateConversationId() {
|
||||
return 'conversation-' + Math.random().toString(36).substring(2, 15);
|
||||
_generateConversationId () {
|
||||
return 'conversation-' + Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
|
||||
/**
|
||||
* refresh token
|
||||
* @param clientId
|
||||
* @param scope
|
||||
* @param refreshToken
|
||||
* @param oid
|
||||
* @returns {Promise<{
|
||||
* token_type: string,
|
||||
* scope: string,
|
||||
* expires_in: number,
|
||||
* ext_expires_in: number,
|
||||
* access_token: string,
|
||||
* refresh_token: string
|
||||
* }>}
|
||||
*/
|
||||
async doRefreshToken (clientId, scope, refreshToken, oid) {
|
||||
const myHeaders = new Headers()
|
||||
myHeaders.append('user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0')
|
||||
myHeaders.append('priority', 'u=1, i')
|
||||
myHeaders.append('referer', 'https://copilot.microsoft.com/')
|
||||
myHeaders.append('origin', 'https://copilot.microsoft.com')
|
||||
myHeaders.append('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
const urlencoded = new URLSearchParams()
|
||||
urlencoded.append('client_id', clientId)
|
||||
urlencoded.append('redirect_uri', 'https://copilot.microsoft.com')
|
||||
urlencoded.append('scope', scope)
|
||||
urlencoded.append('grant_type', 'refresh_token')
|
||||
urlencoded.append('client_info', '1')
|
||||
urlencoded.append('x-client-SKU', 'msal.js.browser')
|
||||
urlencoded.append('x-client-VER', '3.26.1')
|
||||
urlencoded.append('x-ms-lib-capability', 'retry-after, h429')
|
||||
urlencoded.append('x-client-current-telemetry', '5|61,0,,,|,')
|
||||
urlencoded.append('x-client-last-telemetry', '5|3|||0,0')
|
||||
urlencoded.append('client-request-id', '0193875c-0737-703c-a2e7-6730dd56aa4a')
|
||||
urlencoded.append('refresh_token', refreshToken)
|
||||
urlencoded.append('X-AnchorMailbox', 'Oid:' + oid)
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', requestOptions)
|
||||
const tokenJson = await tokenResponse.json()
|
||||
if (this.debug) {
|
||||
logger.info(JSON.stringify(tokenJson))
|
||||
}
|
||||
return tokenJson
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue