diff --git a/client/CopilotAIClient.js b/client/CopilotAIClient.js new file mode 100644 index 0000000..c5774f6 --- /dev/null +++ b/client/CopilotAIClient.js @@ -0,0 +1,156 @@ +import WebSocket from 'ws'; + +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(); + } + + async sendMessage(text, options = {}) { + // If conversationId is provided, use it, otherwise create a new one + if (options.conversationId) { + this.conversationId = options.conversationId; + } else { + this.conversationId = this._generateConversationId(); + } + + // Connect WebSocket + await this.connectWebSocket(); + + // Send the initial message or challenge response + await this.sendInitialMessage(); + + // 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; + } + + async connectWebSocket() { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.baseUrl); + + this.ws.on('open', () => { + console.log('WebSocket connection established.'); + resolve(); + }); + + this.ws.on('message', (data) => this.handleServerMessage(data)); + + this.ws.on('close', () => { + console.log('WebSocket connection closed.'); + }); + + this.ws.on('error', (err) => { + reject(err); + }); + }); + } + + async sendInitialMessage() { + return new Promise((resolve, reject) => { + this.ws.once('message', (data) => { + const message = JSON.parse(data); + if (message.event === 'challenge') { + // Handle challenge by sending the challenge response + this.handleChallenge(message) + .then(resolve) + .catch(reject); + } else { + resolve(); // Proceed if no challenge event + } + }); + }); + } + + async handleChallenge(challenge) { + // Get the token by calling getTurnstile function (you need to define this function) + const token = await this.getTurnstile(challenge.conversationId); + + const challengeResponse = { + event: 'challengeResponse', + token: token, + method: 'cloudflare', + }; + + this.ws.send(JSON.stringify(challengeResponse)); + } + + 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); + } + }; + + this.ws.on('message', (data) => { + const message = JSON.parse(data); + + switch (message.event) { + case 'received': + break; + + case 'startMessage': + this.currentMessageId = message.messageId; + break; + + case 'appendText': + if (!this.partialMessages.has(message.messageId)) { + this.partialMessages.set(message.messageId, { text: '', done: false }); + } + + 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; + } + + checkMessageComplete(message.messageId); + break; + + case 'partCompleted': + break; + + case 'done': + checkMessageComplete(message.messageId); + break; + + default: + 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'; + } + + _generateConversationId() { + return 'conversation-' + Math.random().toString(36).substring(2, 15); + } +} + diff --git a/utils/SydneyAIClient.js b/utils/SydneyAIClient.js index 26186c1..2ffad5e 100644 --- a/utils/SydneyAIClient.js +++ b/utils/SydneyAIClient.js @@ -95,27 +95,27 @@ export default class SydneyAIClient { fetchOptions.headers.cookie = this.opts.cookies } // let hash = md5(this.opts.cookies || this.opts.userToken) - let hash = crypto.createHash('md5').update(this.opts.cookies || this.opts.userToken).digest('hex') - let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + hash) - if (!proTag) { - let indexContentRes = await fetch('https://www.bing.com/chat', { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', - Cookie: `_U=${this.opts.userToken}` - } - }) - let indexContent = await indexContentRes.text() - if (indexContent?.includes('b_proTag')) { - proTag = 'true' - } else { - proTag = 'false' - } - await redis.set('CHATGPT:COPILOT_PRO_TAG:' + hash, proTag, { EX: 7200 }) - } - if (proTag === 'true') { - logger.info('当前账户为copilot pro用户') - this.pro = true - } + // let hash = crypto.createHash('md5').update(this.opts.cookies || this.opts.userToken).digest('hex') + // let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + hash) + // if (!proTag) { + // let indexContentRes = await fetch('https://www.bing.com/chat', { + // headers: { + // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', + // Cookie: `_U=${this.opts.userToken}` + // } + // }) + // let indexContent = await indexContentRes.text() + // if (indexContent?.includes('b_proTag')) { + // proTag = 'true' + // } else { + // proTag = 'false' + // } + // await redis.set('CHATGPT:COPILOT_PRO_TAG:' + hash, proTag, { EX: 7200 }) + // } + // if (proTag === 'true') { + // logger.info('当前账户为copilot pro用户') + // this.pro = true + // } } else { fetchOptions.headers.cookie = initCk }