Update bym.js (#735)

* feat: new bing (WIP)

* fix: update CopilotAIClient.js

* fix: gemini强制调用tool;real at

* feat: add bym support
This commit is contained in:
ikechan8370 2024-12-29 19:57:55 +08:00 committed by GitHub
parent 5f6c4e5abb
commit 26444df2a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 599 additions and 30 deletions

291
client/CopilotAIClient.js Normal file
View file

@ -0,0 +1,291 @@
import WebSocket from 'ws'
import common from '../../../lib/common/common.js'
export class BingAIClient {
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 = {}) {
// 如果 options 中有 conversationId使用它否则生成一个新的 conversationId
if (options.conversationId) {
this.conversationId = options.conversationId
} else {
this.conversationId = this._generateConversationId()
}
// 建立 WebSocket 连接
await this.connectWebSocket()
// 发送消息
await this.sendInitialMessage(text)
// 等待并收集服务器的回复
const responseText = await this.collectResponse()
return responseText
}
async connectWebSocket () {
return new Promise((resolve, reject) => {
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()
})
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)
// 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)
})
})
}
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) => {
clearTimeout(timeout) // 清除超时定时器
const message = JSON.parse(data)
if (message.event === 'challenge') {
logger.info(JSON.stringify(message))
logger.warn('遇到turnstile验证码尝试使用2captcha解决')
// 如果收到 challenge处理挑战
this.handleChallenge(message)
.then(resolve)
.catch(reject)
} else {
// 否则直接进入对话
resolve()
}
})
})
}
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,
method: 'cloudflare'
}
this.ws.send(JSON.stringify(challengeResponse))
}
async collectResponse () {
return new Promise((resolve, reject) => {
const checkMessageComplete = (messageId) => {
// 如果消息已经完成,返回完整的消息内容
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
// 如果是最后一部分,标记为完成
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) {
// 这里需要根据实际情况实现获取 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)
}
/**
* 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
}
}

View file

@ -105,6 +105,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
* topP: number?,
* tokK: number?,
* replyPureTextCallback: Function,
* toolMode: 'AUTO' | 'ANY' | 'NONE'
* search: boolean,
* codeExecution: boolean
* }} opt
@ -199,9 +200,17 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
})
// ANY要笑死人的效果
let mode = opt.toolMode || 'AUTO'
let lastFuncName = opt.functionResponse?.name
const mustSendNextTurn = [
'searchImage', 'searchMusic', 'searchVideo'
]
if (lastFuncName && mustSendNextTurn.includes(lastFuncName)) {
mode = 'ANY'
}
body.tool_config = {
function_calling_config: {
mode: 'AUTO'
mode
}
}
}
@ -248,6 +257,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
const text = responseContent.parts.find(i => i.text)?.text
if (text) {
// send reply first
logger.info('send message: ' + text)
opt.replyPureTextCallback && await opt.replyPureTextCallback(text)
}
// Gemini有时候只回复一个空的functionCall,无语死了