chatgpt-plugin/utils/bard.js
HalcyonAlcedo c2c6ea43de
添加Bard和星火API支持 (#551)
* 修复后台API反代地址未能正确显示的问题

* 更新渲染页面配置

* 添加个人聊天模式配置

* 将用户数据获取改到common中

* 修复错误的渲染页面参数

* 修复bug

* 添加Live2D

* 修复渲染页面错误

* 修复渲染传入值

* 更新渲染

* 修复图表渲染bug

* 调整live2d模型大小

* 修复live2d无法关闭问题

* 修复错误的传值

* 修复ai命名

* 更新渲染

* 添加用户独立设定

* 更新渲染配置适配个人设置

* 修复合并导致的渲染文件异常删除

* 修复用户数据缺失问题

* 修复旧版本数据缺失问题

* 修复bing参数不存在问题,兼容miao的截图

* 修复受限token重试时不被排除的问题

* 修复个人模式下结束对话的模式错误

* 更新渲染页面,将预览版转为正式版

* 修复传统渲染无法调用截图功能的问题

* 文字模式也进行一次缓存

* 更新README

* Update README.md

* 更新渲染

* 更新渲染页面

* 添加版本信息

* 遗漏参数

* 丢失引用

* 补充路由

* 添加云转码功能

* 判断node-silk是否正常合成

* 云转码提示

* 修复图片渲染出错

* 云转码支持发送Buffer

* 添加云转码模式支持

* 更新描述

* 更新后台渲染页面

* 更新配置

* 更新渲染页面

* 添加云渲染

* 修复错误的接口调用

* 修复遗漏的数据转换

* 修复获取的图片数据异常问题

* 更新后台配置

* 更新渲染页面

* 修复云渲染访问地址错误

* 更新渲染页面

* 修复遗漏的模型文件

* 修复live2d问题

* 更新live2d以及相关配置

* 修复遗漏的数据参数

* 修复新live2d情况下云渲染错误的问题

* 适配云渲染1.1.2等待参数

* 添加云服务api检查

* 更新渲染页面

* 添加live2d加载检测

* 修复错误的属性判断

* 添加云渲染DPR

* 更新sydney支持内容生成

* 修改文件模式语音转码接收模式

* 添加云转码时recordUrl检查

* 更新后台配置项

* 修复错误的文本描述

* 更新后台页面

* 添加全局对话模式设置,更新后台面板

* 添加第三方渲染服务适配

* 修复第三方服务器live2d加载导致的渲染失败问题

* 修复后台地址无法实时保存的问题

* 添加live2d模型透明度设置

* 合并更新

* 更新渲染页面

* 更新渲染页面

* 使dpr对本地渲染也生效

* 更新渲染页面

* 添加网页截图功能

* 添加后台配置项

* 添加配置导出和导入功能

* 运行通过参数传递用户token

* 登录时将token作为参数返回

* 修复错误

* 添加密码修改和用户删除接口

* 修正密码比对

* 修复user错误

* 优化数据保存时的返回值

* 添加系统额外服务检查api

* 添加AccessToken配置

* 修复错误的导入提示

* 添加ws连接

* 添加ws用户信息获取

* 修复错误的循环

* 修正ws参数

* 添加群消息获取权限

* 添加用户多端登录支持

* 修复错误的路径引用

* 修复错误的路径引用

* 修复错误

* 修复页面数据获取失败问题

* 修复异常的中断

* 添加配置视图

* 更新配置面板

* 添加用户版本信息

* 更新配置视图

* 修复错误的视图绑定

* 修改视图文件位置,添加mediaLink相关代码

* 修复错误的视图配置绑定

* 更新依赖,添加qq消息组件初始化信息获取

* 修复异常的群名称无法获取问题

* 修改注释

* 撤销对management的错误合并

* 添加Sydney图片识别功能

* 更新配置文件和后台页面

* 修改view配置

* 修复node版本过低导致的FormData无法调用,尝试添加反代

* 国外图片识别不使用反代

* fix: 修复云转码导致的语音重复发送问题

* fix: 修复qq消息可越权获取的问题

* feat: 添加代理post操作

* fix: 修复一些字符串格式的数字导致的配置加载错误

* fix: 修复错误的云服务api网址格式

* fix: 修复错误的云转码接口调用格式

* fix: 修复错误的精度

* 添加配置项适配

* feat: 添加群消息合并功能

* 添加历史记录的消息已读标签

* feat: 添加设定相关接口

* improvement: 在多少绘图失败后尝试使用cn进行绘图

* fix: 修复bing可能存在的无标题引用导致的错误

* fix: 修复绘图获取失败的问题

* 添加bard支持

* feat: 添加图片处理

* 添加bard图片处理能力

* 添加锅巴描述

* fix: 修复空图片链接报错问题

* 添加星火api支持,尚未连接上下文

* feat: 添加星火api上下文

* feat: 增加星火设定

* 添加调试信息

* 添加星火v2支持

* 修复连接地址错误

* feat: 添加星火助手功能

* 添加图片处理和群组消息结束

* feat: 添加星火图片识别支持

* 改回全部使用form-data,优化文本

* 添加对私人星火助手的支持

* 添加xhweb错误内容输出

* 添加星火预设问题库重写

* feat: 添加星火回复内容替换,添加锅巴配置

* 添加星火助手支持

* fix: 修复星火空设定时额外占用消息

---------

Co-authored-by: ikechan8370 <geyinchibuaa@gmail.com>
2023-08-24 22:33:23 +08:00

373 lines
11 KiB
JavaScript

// https://github.com/EvanZhouDev/bard-ai
class Bard {
static JSON = "json";
static MD = "markdown";
// ID derived from Cookie
SNlM0e;
// HTTPS Headers
#headers;
// Resolution status of initialization call
#initPromise;
#bardURL = "https://bard.google.com";
// Wether or not to log events to console
#verbose = false;
// Fetch function
#fetch = fetch;
constructor(cookie, config) {
// Register some settings
if (config?.verbose == true) this.#verbose = true;
if (config?.fetch) this.#fetch = config.fetch;
// 可变更访问地址,利用反向代理绕过区域限制
if (config?.bardURL) this.#bardURL = config.bardURL;
// If a Cookie is provided, initialize
if (cookie) {
this.#initPromise = this.#init(cookie);
} else {
throw new Error("Please provide a Cookie when initializing Bard.");
}
this.cookie = cookie;
}
// You can also choose to initialize manually
async #init(cookie) {
this.#verbose && console.log("🚀 Starting intialization");
// Assign headers
this.#headers = {
Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1],
"X-Same-Domain": "1",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
Origin: this.#bardURL,
Referer: this.#bardURL,
Cookie: (typeof cookie === "object") ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join("")) : ("__Secure-1PSID=" + cookie),
};
let responseText;
// Attempt to retrieve SNlM0e
try {
this.#verbose &&
console.log("🔒 Authenticating your Google account");
responseText = await this.#fetch(this.#bardURL, {
method: "GET",
headers: this.#headers,
credentials: "include",
})
.then((response) => response.text())
} catch (e) {
// Failure to get server
throw new Error(
"Could not fetch Google Bard. You may be disconnected from internet: " +
e
);
}
try {
const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1];
// Assign SNlM0e and return it
this.SNlM0e = SNlM0e;
this.#verbose && console.log("✅ Initialization finished\n");
return SNlM0e;
} catch {
throw new Error(
"Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit."
);
}
}
async #uploadImage(name, buffer) {
this.#verbose && console.log("🖼️ Starting image processing");
let size = buffer.byteLength;
let formBody = [
`${encodeURIComponent("File name")}=${encodeURIComponent([name])}`,
];
try {
this.#verbose &&
console.log("💻 Finding Google server destination");
let response = await this.#fetch(
"https://content-push.googleapis.com/upload/",
{
method: "POST",
headers: {
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Header-Content-Length": size,
"X-Tenant-Id": "bard-storage",
"Push-Id": "feeds/mcudyrk2a4khkz",
},
body: formBody,
credentials: "include",
}
);
const uploadUrl = response.headers.get("X-Goog-Upload-URL");
this.#verbose && console.log("📤 Sending your image");
response = await this.#fetch(uploadUrl, {
method: "POST",
headers: {
"X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": 0,
"X-Tenant-Id": "bard-storage",
},
body: buffer,
credentials: "include",
});
const imageFileLocation = await response.text();
this.#verbose && console.log("✅ Image finished working\n");
return imageFileLocation;
} catch (e) {
throw new Error(
"Could not fetch Google Bard. You may be disconnected from internet: " +
e
);
}
}
// Query Bard
async #query(message, config) {
let formatMarkdown = (text, images) => {
if (!images) return text;
for (let imageData of images) {
const formattedTag = `!${imageData.tag}(${imageData.url})`;
text = text.replace(
new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`),
formattedTag
);
}
return text;
}
let { ids, imageBuffer } = config;
// Wait until after init
await this.#initPromise;
this.#verbose && console.log("🔎 Starting Bard Query");
// If user has not run init
if (!this.SNlM0e) {
throw new Error(
"Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)."
);
}
this.#verbose && console.log("🏗️ Building Request");
// HTTPS parameters
const params = {
bl: "boq_assistant-bard-web-server_20230711.08_p0",
_reqID: ids?._reqID ?? "0",
rt: "c",
};
// If IDs are provided, but doesn't have every one of the expected IDs, error
const messageStruct = [
[message],
null,
[null, null, null],
];
if (imageBuffer) {
let imageLocation = await this.#uploadImage(
`bard-ai_upload`,
imageBuffer
);
messageStruct[0].push(0, null, [
[[imageLocation, 1], "bard-ai_upload"],
]);
}
if (ids) {
const { conversationID, responseID, choiceID } = ids;
messageStruct[2] = [conversationID, responseID, choiceID];
}
// HTTPs data
const data = {
"f.req": JSON.stringify([null, JSON.stringify(messageStruct)]),
at: this.SNlM0e,
};
// URL that we are submitting to
const url = new URL(
"/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
this.#bardURL
);
// Append parameters to the URL
for (const key in params) {
url.searchParams.append(key, params[key]);
}
// Encode the data
const formBody = Object.entries(data)
.map(
([property, value]) =>
`${encodeURIComponent(property)}=${encodeURIComponent(
value
)}`
)
.join("&");
this.#verbose && console.log("💭 Sending message to Bard");
// Send the fetch request
const chatData = await this.#fetch(url.toString(), {
method: "POST",
headers: this.#headers,
body: formBody,
credentials: "include",
})
.then((response) => {
return response.text();
})
.then((text) => {
return JSON.parse(text.split("\n")[3])[0][2];
})
.then((rawData) => JSON.parse(rawData));
this.#verbose && console.log("🧩 Parsing output");
// Get first Bard-recommended answer
const answer = chatData[4][0];
// Text of that answer
const text = answer[1][0];
// Get data about images in that answer
const images =
answer[4]?.map((x) => ({
tag: x[2],
url: x[3][0][0],
info: {
raw: x[0][0][0],
source: x[1][0][0],
alt: x[0][4],
website: x[1][1],
favicon: x[1][3],
},
})) ?? [];
this.#verbose && console.log("✅ All done!\n");
// Put everything together and return
return {
content: formatMarkdown(text, images),
images: images,
ids: {
conversationID: chatData[1][0],
responseID: chatData[1][1],
choiceID: answer[0],
_reqID: String(parseInt(ids?._reqID ?? 0) + 100000),
},
};
}
async #parseConfig(config) {
let result = {
useJSON: false,
imageBuffer: undefined, // Returns as {extension, filename}
ids: undefined,
};
// Verify that format is one of the two types
if (config?.format) {
switch (config.format) {
case Bard.JSON:
result.useJSON = true;
break;
case Bard.MD:
result.useJSON = false;
break;
default:
throw new Error(
"Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output."
);
}
}
// Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer
if (config?.image) {
if (
config.image instanceof ArrayBuffer
) {
result.imageBuffer = config.image;
} else if (
typeof config.image === "string" &&
/\.(jpeg|jpg|png|webp)$/.test(config.image)
) {
let fs;
try {
fs = await import("fs")
} catch {
throw new Error(
"Loading from an image file path is not supported in a browser environment.",
);
}
result.imageBuffer = fs.readFileSync(
config.image,
).buffer;
} else {
throw new Error(
"Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer."
);
}
}
// Verify that all values in IDs exist
if (config?.ids) {
if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) {
result.ids = config.ids;
} else {
throw new Error(
"Please provide the IDs exported exactly as given."
);
}
}
return result;
}
// Ask Bard a question!
async ask(message, config) {
let { useJSON, imageBuffer, ids } = await this.#parseConfig(config);
let response = await this.#query(message, { imageBuffer, ids });
return useJSON ? response : response.content;
}
createChat(ids) {
let bard = this;
class Chat {
ids = ids;
async ask(message, config) {
let { useJSON, imageBuffer } = await bard.#parseConfig(config);
let response = await bard.#query(message, {
imageBuffer,
ids: this.ids,
});
this.ids = response.ids;
return useJSON ? response : response.content;
}
export() {
return this.ids;
}
}
return new Chat();
}
}
export default Bard;