在Node終端實現NewBing對話功能
目錄
前言
ChatGPT在當下已然成為炙手可熱的話題了,隨著GPT-4的推出,網上關于其接口的文章也越來越多。但是今天,我們不聊GPT,說說它的老朋友:newbing
之前我發布了幾篇關于對接openAI以及chatGPT的文章:Node搭建GPT接口,Node機器人,語音識別及合成,大家對此類文章的興趣度還是挺高的,于是我決定深入探索一下NewBing的接口及對話方式,如果有興趣的話就繼續往下看吧
準備工作
工作原理
首先我們看看NewBing的實現原理是什么
掛VPN,打開必應,登錄bing賬號

如果顯示使用Edge打開,我們可以下載一個Edge或者使用chathub插件。
這里我以Edge為例,在Edge中我們可以點擊立即聊天開始使用

我們打開F12,進入網絡菜單進行hack,輸入一句對話并發送,開啟與newbing的聊天

可以看到,在發送和接收對話時,瀏覽器發送了一個請求用于新建對話,并建立了websocket連接,最后將對話結果發送到頁面


功能設計
知道了程序運行的原理,實現功能就有思路了,我的計劃是在node控制臺中實現一個與NewBing對話的功能,思路如下:
簡述一下上面的流程,使用者通過命令打開newBing控制臺,直接輸入要發送的對話,等待一段時間后,收到消息反饋,繼續下面的對話
這種方式不僅僅可以在控制臺中使用,也可以嘗試寫成服務或websocket的形式,提供接口或消息給客戶端調用,這里我就拋磚引玉,將后續的功能留給各位大佬實現
實現過程
基礎概念
代理
使用proxy-agent模塊可以讓請求和socket代理到VPN所在的端口通過代理訪問Bing獲取消息
import ProxyAgent from "proxy-agent"
const agent = ProxyAgent('http://127.0.0.1:10240')// 訪問vpn代理地址
通過agent參數使用代理功能
請求
請求函數使用的是我之前寫的一個工具包,配合配套的catchAwait函數食用更佳
import { Request, catchAwait } from "utils-lib-js"
const bingRequest = new Request('https://www.bing.com')// 初始化請求地址
bingRequest.use("error", console.error)// 攔截拋錯
const [err, res] = await catchAwait(this.bingRequest.GET("/turing/conversation/create"))// 發起請求
socket
WebSocket的使用可以參照之前的文章
控制臺輸入模塊
使用readline模塊可以接收控制臺的輸入內容
import readline from "readline";
readline.createInterface({
input: process.stdin,
output: process.stdout,
}).question('請輸入:', ()=>{
// 輸入完成,敲擊了回車
})
配置文件
需要注意的是:bing的cookie可以通過在任意瀏覽器打開NewBing的網站按下F12獲取(前提是登錄了賬號),直接輸入document.cookie獲取

export const config = {
cookie: "必應的cookie",
bingUrl: "https://www.bing.com",
proxyUrl: "http://127.0.0.1:10240",
bingSocketUrl: "wss://sydney.bing.com",
};
export const conversationTemplate = {
arguments: [
{
source: "cib",
optionsSets: [
"deepleo",
"nlu_direct_response_filter",
"disable_emoji_spoken_text",
"responsible_ai_policy_235",
"enablemm",
"dtappid",
"rai253",
"dv3sugg",
"h3imaginative",
],
allowedMessageTypes: ["Chat", "InternalSearchQuery"],
isStartOfSession: true,
message: {
author: "user",
inputMethod: "Keyboard",
text: "",
messageType: "Chat",
},
conversationId: "",
conversationSignature: "",
participant: {
id: "",
},
},
],
invocationId: "0",
target: "chat",
type: 4,
};
bingServer請求
請求就一個接口,暴露接口給外部獲取
import { Request, catchAwait, MessageCenter } from "utils-lib-js"
import { config } from "../config.js"
// 請求對話信息接口的響應信息
export type IBingInfo = {
clientId: string
conversationId: string
conversationSignature: string
result: {
message: unknown
value: string
}
}
// 切換可選項,防止報錯
export type IBingInfoPartial = Partial<IBingInfo>
// 靜態配置項結構
export type IConfig = {
cookie: string
proxyUrl: string
bingUrl: string
bingSocketUrl: string
}
// NewBingServer的構造函數配置
export type IOpts = {
agent?: any
}
export class NewBingServer extends MessageCenter {
bingInfo: IBingInfo
readonly bingRequest: Request
constructor(private opts: IOpts, private _config: IConfig = config) {
super()
const { bingUrl } = this._config
this.bingRequest = new Request(bingUrl)// 初始化請求地址
this.initServer()// 初始化request: 攔截器等
}
// 拋錯事件
throwErr(err: any) {
this.emit("new-bing:server:error", err)
}
// 重置當前請求
async reset() {
this.clearBing()
const bingInfo = await this.createConversation()
this.init(bingInfo)
}
// 清除當前請求的信息
clearBing() {
this.bingInfo = null
}
// 賦值當前請求的信息
init(bingInfo) {
this.bingInfo = bingInfo
}
// 初始化request
initServer() {
this.bingRequest.use("error", console.error)
// .use("response", console.log)
}
// 發起請求
private async createConversation() {
const { _config, opts, bingInfo } = this
const { agent } = opts
if (bingInfo) return bingInfo
const { cookie } = _config
const [err, res] = await catchAwait(this.bingRequest.GET("/turing/conversation/create", {}, null, {
headers: { cookie },
agent
}))
if (err) return this.throwErr(err)
return res
}
}
bingSocket消息
socket內容比較多,主要是針對不同的message的type進行區分
import WebSocket, { MessageEvent, Event, ErrorEvent, CloseEvent } from "ws";
import { getType, IObject, jsonToString, MessageCenter, stringToJson } from "utils-lib-js"
import { ClientRequestArgs } from "http"
import { config } from "../config.js"
import { IConfig, IBingInfoPartial } from "../server/index.js"
import { setConversationTemplate, Conversation } from '../helpers/index.js'
const fixStr = ''// 每段對話的標識符,發送接收都有
// websocket配置
export type IWsConfig = {
address: string | URL
options: WebSocket.ClientOptions | ClientRequestArgs
protocols: string | string[]
}
// 發送socket消息的類型
export type IMessageOpts = {
message: string | IObject<any>
}
// 發送對話的結構
export type IConversationMessage = {
message: string
invocationId: string | number
}
export class NewBingSocket extends MessageCenter {
private ws: WebSocket // ws實例
private bingInfo: IBingInfoPartial // 請求拿到的conversation信息
private convTemp: Conversation.IConversationTemplate // 對話發送的消息模板
private pingInterval: NodeJS.Timeout | string | number // ping計時器
constructor(public wsConfig: Partial<IWsConfig>, private _config: IConfig = config) {
super()
const { bingSocketUrl } = this._config
const { address } = wsConfig
wsConfig.address = bingSocketUrl + address
}
// 將conversation信息賦值到消息模板中
mixBingInfo(bingInfo: IBingInfoPartial) {
const { conversationId, conversationSignature, clientId } = bingInfo
this.bingInfo = bingInfo
this.convTemp = setConversationTemplate({
conversationId, conversationSignature, clientId
})
return this
}
// 創建ws
createWs() {
const { wsConfig, ws } = this
if (ws) return this
const { address, options, protocols } = wsConfig
this.ws = new WebSocket(address, protocols, options)
return this
}
// 重置ws
clearWs() {
const { ws } = this
if (ws) {
ws.close(4999, 'clearWs')
}
this.clearInterval()
return this
}
// 拋錯事件
private throwErr(err: any) {
this.emit("new-bing:socket:error", err)
}
// 開啟ws后初始化事件
initEvent() {
const { ws, error, close, open, message } = this
if (!ws) this.throwErr("ws未定義,不能初始化事件")
ws.onerror = error
ws.onclose = close
ws.onopen = open
ws.onmessage = message
return this
}
// 發消息,兼容Object和string
sendMessage = (opts: IMessageOpts) => {
const { bingInfo, convTemp, ws } = this
const { message } = opts
if (!bingInfo || !convTemp) this.throwErr("對話信息未獲取,或模板信息未配置,請重新獲取信息")
const __type = getType(message)
let str = ""
if (__type === "string") {
str = message as string
} else if (__type === "object") {
str = jsonToString(message as IObject<unknown>)
}
this.emit("send-message", str)
ws.send(str + fixStr)
}
// 收到消息
private message = (e: MessageEvent) => {
this.emit("message", e)
onMessage.call(this, e)
}
// ws連接成功
private open = (e: Event) => {
this.emit("open", e)
const { sendMessage } = this
sendMessage({ message: { "protocol": "json", "version": 1 } })// 初始化
}
// ws關閉
private close = (e: CloseEvent) => {
const { ws } = this
ws.removeAllListeners()
this.ws = null
this.emit("close", e)
}
// ws出錯
private error = (e: ErrorEvent) => {
this.emit("error", e)
console.log("error");
}
// 斷線檢測
sendPingMsg() {
const { ws } = this
if (!ws) this.throwErr("ws未定義,無法發送Ping")
this.startInterval()
this.emit("init:finish", {})
}
// 開啟斷線定時器
private startInterval() {
this.clearInterval()
this.pingInterval = setInterval(() => {
this.sendMessage({ message: { "type": 6 } })
}, 20 * 1000)
}
// 清空斷線定時器
private clearInterval() {
const { pingInterval } = this
if (pingInterval) {
clearInterval(pingInterval)
this.pingInterval = null
}
}
}
// 接收到消息
export function onMessage(e: MessageEvent) {
const dataSource = e.data.toString().split(fixStr)[0]
const data = stringToJson(dataSource)
const { type } = data ?? {}
switch (type) {
case 1://對話中
this.emit("message:ing", data.arguments?.[0]?.messages?.[0]?.text)
break;
case 2://對話完成
this.emit("message:finish", data.item?.messages?.[1]?.text)
break;
case 6://斷線檢測
// console.log(data);
break;
case 7://Connection closed with an error
console.log(data);
break;
default:// 初始化響應
this.sendPingMsg()
break;
}
}
// 發送聊天消息
export function sendConversationMessage(params?: IConversationMessage) {
const { message, invocationId } = params
const arg = this.convTemp.arguments[0]
arg.message.text = message
arg.isStartOfSession = invocationId === 0// 是否是新對話
this.convTemp.invocationId = invocationId.toString()// 第幾段對話
this.sendMessage({ message: this.convTemp })
}
子線程入口部分
然后通過startBingConversation作為入口函數,對上面的兩個模塊進行調用
import { NewBingServer, IBingInfoPartial } from "./server/index.js"
import { NewBingSocket, sendConversationMessage } from "./socket/index.js"
import { config } from "./config.js"
import ProxyAgent from "proxy-agent"
import { parentPort } from "worker_threads";
const { proxyUrl } = config// 代理地址
const agent = ProxyAgent(proxyUrl)// 訪問vpn代理地址
// 初始化bing請求
const bingServer = new NewBingServer({
agent
})
// 初始化bing的websocket消息
const bingSocket = new NewBingSocket({
address: "/sydney/ChatHub",
options: {
agent
}
})
let invocationId = -1// 同一段對話的id
let bingInfo: IBingInfoPartial// bing的conversation信息,BingServer請求的結果
const startBingConversation = async () => {
initEvent()
await initBingServer()
initBingSocket()
}
const initEvent = () => {
bingServer.on("new-bing:server:error", (...args) => { throw new Error(...args) })// 請求拋錯
bingSocket.on("new-bing:socket:error", (...args) => { throw new Error(...args) })// 消息拋錯
// 接收主線程的消息
parentPort.on("message", (res) => {
const { type } = res
if (type === "sendMessage") {
// 發送消息
sendConversationMessage.call(bingSocket, { message: res.message, invocationId: ++invocationId })
}
})
}
const initBingServer = async () => {
await bingServer.reset()// 重置請求
bingInfo = bingServer.bingInfo
}
const initBingSocket = () => {
bingSocket.mixBingInfo(bingInfo).createWs().initEvent().on("init:finish", () => {// socket初始化完成
parentPort.postMessage({
type: "init:finish"
})
}).on("message:finish", (data = "") => {
// 一段對話完成
parentPort.postMessage({
type: "message:finish",
data
})
}).on("message:ing", (data = "") => {
// 對話時,觸發主線程loading操作
parentPort.postMessage({
type: "message:ing",
data
})
})
}
startBingConversation()
主線程部分
主線程可以參照之前的打包工具,注冊成系統命令,使用bing啟動,通過readline進行對話交互
#!/usr/bin/env node
import { Worker } from "worker_threads";
import readline from "readline";
import { defer, logLoop, logOneLine } from "utils-lib-js";
const NewBing = new Worker("./src/index.js");
// 工廠模式
const readlineFactory = () => {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
};
let rl, loading;
// 解決node低版本無readline/promises模塊,將異步函數換成promise
const readlinePromise = (...args) => {
const { promise, resolve } = defer();
rl.question(...args, resolve);
return promise;
};
// 啟動命令輸入
const start = () => {
readlinePromise("請輸入:").then((res) => {
console.log(`你:${res}`);
NewBing.postMessage({ type: "sendMessage", message: res });
loading = logLoop(); // 加載中動畫
});
};
// 關閉命令輸入
const clear = () => {
rl.close();
rl = null;
};
// 重置
const reset = () => {
if (rl) {
clear();
}
rl = readlineFactory();
};
// 初始化當前命令窗口
const initBing = () => {
reset();
NewBing.on("message", (res) => {
switch (res.type) {
case "message:finish": // 收到消息,重置輸入框,換行
loading.isStop = true;
logOneLine(`Bing:${res.data}`, true, true);
case "init:finish": // 初始化完成
start();
break;
case "message:ing": // 對話中
// loading = logLoop(loadList);
break;
}
});
};
initBing();
工具函數
import { conversationTemplate } from "../config.js"
import { readFileSync, writeFileSync } from "fs"
let conTemp: Conversation.IConversationTemplate = conversationTemplate
export namespace Conversation {
// 對話模型類型
// Creative:創造力的,Precise:精確的,Balanced:平衡的
type ConversationStyle = 'Creative' | 'Precise' | 'Balanced'
// 對話方式
type ConversationType = 'SearchQuery' | 'Chat' // bing搜索,聊天
// 模型映射
export enum ConversationStr {
Creative = 'h3imaginative',
Precise = 'h3precise',
Balanced = 'galileo'
}
// 發起對話時傳入的參數
export type IConversationOpts = {
convStyle: ConversationStyle
messageType: ConversationType
conversationId: string
conversationSignature: string
clientId: string
}
type IMessage = {
author: string,
text: string,
messageType: ConversationType,
}
type IArguments = {
source: string
optionsSets: string[]
allowedMessageTypes: string[]
isStartOfSession: boolean
message: IMessage
conversationId: string
conversationSignature: string
participant: {
id: string
}
}
// 發起對話的模板
export type IConversationTemplate = {
arguments: IArguments[]
invocationId: string
target: string
type: number
}
}
// 默認使用平衡類型
const { Balanced } = Conversation.ConversationStr
// 數據文件緩存(暫時沒用上,調試的時候用的)
export function ctrlTemp(path?: string): any
export function ctrlTemp(path?: string, file?: any): void
export function ctrlTemp(path: string = "./temp", file?: string) {
try {
if (file) {
return writeFileSync(path, file, "utf8")
}
return readFileSync(path, "utf8")
} catch (error) { }
}
// 配置socket鑒權及消息模板
export function setConversationTemplate(params: Partial<Conversation.IConversationOpts> = {}): Conversation.IConversationTemplate {
const { convStyle = Balanced, messageType = "Chat", conversationId,
conversationSignature, clientId } = params
if (!conversationId || !conversationSignature || !clientId) return null
const args = conTemp.arguments[0]
conTemp.arguments[0] = {
...args,
conversationId,
conversationSignature,
participant: { id: clientId }
}
args.optionsSets.push(convStyle)// 這里傳入對話風格
args.message.messageType = messageType// 這里傳入對話類型
return conTemp
}
效果展示
我們使用npm link綁定全局命令
然后使用bing運行命令,并輸入對話

寫在最后
以上就是文章全部內容了,文章主要講述了在node中實現一個與newbing對話的案例,希望能對你有幫助,對文章有任何問題歡迎評論或私信。
感謝你看到了這里,如果覺得文章不錯的話,還望三連支持一下,非常感謝!

浙公網安備 33010602011771號