mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 21:37:11 +00:00
添加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>
This commit is contained in:
parent
3b1641ca5f
commit
c2c6ea43de
9 changed files with 1174 additions and 87 deletions
373
utils/bard.js
Normal file
373
utils/bard.js
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
// 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;
|
||||
|
|
@ -50,6 +50,17 @@ const defaultConfig = {
|
|||
plus: false,
|
||||
useGPT4: false,
|
||||
xinghuoToken: '',
|
||||
xhmode: 'web',
|
||||
xhAppId: '',
|
||||
xhAPISecret: '',
|
||||
xhAPIKey: '',
|
||||
xhAssistants: '',
|
||||
xhTemperature: 0.5,
|
||||
xhMaxTokens: 1024,
|
||||
xhPromptSerialize: false,
|
||||
xhPrompt: '',
|
||||
xhRetRegExp: '',
|
||||
xhRetReplace: '',
|
||||
promptPrefixOverride: 'Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.',
|
||||
assistantLabel: 'ChatGPT',
|
||||
// thinkingTips: true,
|
||||
|
|
@ -113,6 +124,9 @@ const defaultConfig = {
|
|||
slackClaudeEnableGlobalPreset: true,
|
||||
slackClaudeGlobalPreset: '',
|
||||
slackClaudeSpecifiedChannel: '',
|
||||
bardPsid: '',
|
||||
bardReverseProxy: '',
|
||||
bardForceUseReverse: false,
|
||||
cloudTranscode: 'https://silk.201666.xyz',
|
||||
cloudRender: false,
|
||||
cloudMode: 'url',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import fetch from 'node-fetch'
|
|||
import { Config } from '../config.js'
|
||||
import { createParser } from 'eventsource-parser'
|
||||
import https from 'https'
|
||||
import WebSocket from 'ws'
|
||||
import { config } from 'process'
|
||||
|
||||
const referer = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2NoYXQ/aWQ9')
|
||||
const origin = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNu')
|
||||
|
|
@ -13,8 +15,24 @@ try {
|
|||
} catch (err) {
|
||||
logger.warn('未安装form-data,无法使用星火模式')
|
||||
}
|
||||
let crypto
|
||||
try {
|
||||
crypto = (await import('crypto')).default
|
||||
} catch (err) {
|
||||
logger.warn('未安装crypto,无法使用星火api模式')
|
||||
}
|
||||
async function getKeyv() {
|
||||
let Keyv
|
||||
try {
|
||||
Keyv = (await import('keyv')).default
|
||||
} catch (error) {
|
||||
throw new Error('keyv依赖未安装,请使用pnpm install keyv安装')
|
||||
}
|
||||
return Keyv
|
||||
}
|
||||
export default class XinghuoClient {
|
||||
constructor (opts) {
|
||||
constructor(opts) {
|
||||
this.cache = opts.cache
|
||||
this.ssoSessionId = opts.ssoSessionId
|
||||
this.headers = {
|
||||
Referer: referer,
|
||||
|
|
@ -24,19 +42,249 @@ export default class XinghuoClient {
|
|||
}
|
||||
}
|
||||
|
||||
async sendMessage (prompt, chatId) {
|
||||
apiErrorInfo(code) {
|
||||
switch (code) {
|
||||
case 10000: return '升级为ws出现错误'
|
||||
case 10001: return '通过ws读取用户的消息出错'
|
||||
case 10002: return '通过ws向用户发送消息 错'
|
||||
case 10003: return '用户的消息格式有错误'
|
||||
case 10004: return '用户数据的schema错误'
|
||||
case 10005: return '用户参数值有错误'
|
||||
case 10006: return '用户并发错误:当前用户已连接,同一用户不能多处同时连接。'
|
||||
case 10007: return '用户流量受限:服务正在处理用户当前的问题,需等待处理完成后再发送新的请求。(必须要等大模型完全回复之后,才能发送下一个问题)'
|
||||
case 10008: return '服务容量不足,联系工作人员'
|
||||
case 10009: return '和引擎建立连接失败'
|
||||
case 10010: return '接收引擎数据的错误'
|
||||
case 10011: return '发送数据给引擎的错误'
|
||||
case 10012: return '引擎内部错误'
|
||||
case 10013: return '输入内容审核不通过,涉嫌违规,请重新调整输入内容'
|
||||
case 10014: return '输出内容涉及敏感信息,审核不通过,后续结果无法展示给用户'
|
||||
case 10015: return 'appid在黑名单中'
|
||||
case 10016: return 'appid授权类的错误。比如:未开通此功能,未开通对应版本,token不足,并发超过授权 等等'
|
||||
case 10017: return '清除历史失败'
|
||||
case 10019: return '表示本次会话内容有涉及违规信息的倾向;建议开发者收到此错误码后给用户一个输入涉及违规的提示'
|
||||
case 10110: return '服务忙,请稍后再试'
|
||||
case 10163: return '请求引擎的参数异常 引擎的schema 检查不通过'
|
||||
case 10222: return '引擎网络异常'
|
||||
case 10907: return 'token数量超过上限。对话历史+问题的字数太多,需要精简输入'
|
||||
case 11200: return '授权错误:该appId没有相关功能的授权 或者 业务量超过限制'
|
||||
case 11201: return '授权错误:日流控超限。超过当日最大访问量的限制'
|
||||
case 11202: return '授权错误:秒级流控超限。秒级并发超过授权路数限制'
|
||||
case 11203: return '授权错误:并发流控超限。并发路数超过授权路数限制'
|
||||
default: return '无效错误代码'
|
||||
}
|
||||
}
|
||||
|
||||
promptBypassPreset(prompt) {
|
||||
switch (prompt) {
|
||||
case '你是谁':
|
||||
return '你是谁,叫什么'
|
||||
case '你是谁啊':
|
||||
return '你是谁啊,叫什么'
|
||||
default:
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
|
||||
async initCache() {
|
||||
if (!this.conversationsCache) {
|
||||
const cacheOptions = this.cache || {}
|
||||
cacheOptions.namespace = cacheOptions.namespace || 'xh'
|
||||
let Keyv = await getKeyv()
|
||||
this.conversationsCache = new Keyv(cacheOptions)
|
||||
}
|
||||
}
|
||||
|
||||
async getWsUrl() {
|
||||
if (!crypto) return false
|
||||
const APISecret = Config.xhAPISecret
|
||||
const APIKey = Config.xhAPIKey
|
||||
let APILink = '/v1.1/chat'
|
||||
if (Config.xhmode == 'apiv2') {
|
||||
APILink = '/v2.1/chat'
|
||||
}
|
||||
const date = new Date().toGMTString()
|
||||
const algorithm = 'hmac-sha256'
|
||||
const headers = 'host date request-line'
|
||||
const signatureOrigin = `host: spark-api.xf-yun.com\ndate: ${date}\nGET ${APILink} HTTP/1.1`
|
||||
const hmac = crypto.createHmac('sha256', APISecret)
|
||||
hmac.update(signatureOrigin)
|
||||
const signature = hmac.digest('base64')
|
||||
const authorizationOrigin = `api_key="${APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
|
||||
const authorization = Buffer.from(authorizationOrigin).toString('base64')
|
||||
const v = {
|
||||
authorization: authorization,
|
||||
date: date,
|
||||
host: "spark-api.xf-yun.com"
|
||||
}
|
||||
const url = `wss://spark-api.xf-yun.com${APILink}?${Object.keys(v).map(key => `${key}=${v[key]}`).join('&')}`
|
||||
return url
|
||||
}
|
||||
|
||||
async uploadImage(url) {
|
||||
// 获取图片
|
||||
let response = await fetch(url, {
|
||||
method: 'GET',
|
||||
})
|
||||
const blob = await response.blob()
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
// 上传oss
|
||||
const formData = new FormData()
|
||||
formData.append('file', buffer, 'image.png')
|
||||
const respOss = await fetch('https://xinghuo.xfyun.cn/iflygpt/oss/sign', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: 'ssoSessionId=' + this.ssoSessionId + ';',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
if (respOss.ok) {
|
||||
const ossData = await respOss.json()
|
||||
// 上传接口
|
||||
const sparkdeskUrl = `${ossData.data.url}&authorization=${Buffer.from(ossData.data.authorization).toString('base64')}&date=${ossData.data.date}&host=${ossData.data.host}`
|
||||
const respSparkdes = await fetch(sparkdeskUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: 'ssoSessionId=' + this.ssoSessionId + ';',
|
||||
authorization: Buffer.from(ossData.data.authorization).toString('base64')
|
||||
},
|
||||
body: buffer
|
||||
})
|
||||
if (respSparkdes.ok) {
|
||||
const sparkdesData = await respSparkdes.json()
|
||||
return {
|
||||
url: sparkdesData.data.link,
|
||||
file: buffer
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const sparkdesData = await respSparkdes.json()
|
||||
logger.error('星火图片Sparkdes:发送失败' + sparkdesData.desc)
|
||||
} catch (error) {
|
||||
logger.error('星火图片Sparkdes:发送失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const ossData = await respOss.json()
|
||||
logger.error('星火图片OSS:上传失败' + ossData.desc)
|
||||
} catch (error) {
|
||||
logger.error('星火图片OSS:上传失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async apiMessage(prompt, chatId, ePrompt = []) {
|
||||
if (!chatId) chatId = (Math.floor(Math.random() * 1000000) + 100000).toString()
|
||||
|
||||
// 初始化缓存
|
||||
await this.initCache()
|
||||
const conversationKey = `ChatXH_${chatId}`
|
||||
const conversation = (await this.conversationsCache.get(conversationKey)) || {
|
||||
messages: [],
|
||||
createdAt: Date.now()
|
||||
}
|
||||
|
||||
// 获取ws链接
|
||||
const wsUrl = Config.xhmode == 'assistants' ? Config.xhAssistants : await this.getWsUrl()
|
||||
if (!wsUrl) throw new Error('缺少依赖:crypto。请安装依赖后重试')
|
||||
|
||||
// 编写消息内容
|
||||
const wsSendData = {
|
||||
header: {
|
||||
app_id: Config.xhAppId,
|
||||
uid: chatId
|
||||
},
|
||||
parameter: {
|
||||
chat: {
|
||||
domain: Config.xhmode == 'api' ? "general" : "generalv2",
|
||||
temperature: Config.xhTemperature, // 核采样阈值
|
||||
max_tokens: Config.xhMaxTokens, // tokens最大长度
|
||||
chat_id: chatId
|
||||
}
|
||||
},
|
||||
payload: {
|
||||
message: {
|
||||
"text": [
|
||||
...ePrompt,
|
||||
...conversation.messages,
|
||||
{ "role": "user", "content": prompt }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Config.debug) {
|
||||
logger.info(wsSendData.payload.message.text)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(wsUrl)
|
||||
let resMessage = ''
|
||||
socket.on('open', () => {
|
||||
socket.send(JSON.stringify(wsSendData))
|
||||
})
|
||||
socket.on('message', async (message) => {
|
||||
try {
|
||||
const messageData = JSON.parse(message)
|
||||
if (messageData.header.code != 0) {
|
||||
reject(`接口发生错误:Error Code ${messageData.header.code} ,${this.apiErrorInfo(messageData.header.code)}`)
|
||||
}
|
||||
if (messageData.header.status == 0 || messageData.header.status == 1) {
|
||||
resMessage += messageData.payload.choices.text[0].content
|
||||
}
|
||||
if (messageData.header.status == 2) {
|
||||
resMessage += messageData.payload.choices.text[0].content
|
||||
conversation.messages.push({
|
||||
role: 'user',
|
||||
content: prompt
|
||||
})
|
||||
conversation.messages.push({
|
||||
role: 'assistant',
|
||||
content: resMessage
|
||||
})
|
||||
// 超过规定token去除一半曾经的对话记录
|
||||
if (messageData.payload.usage.text.total_tokens >= Config.xhMaxTokens) {
|
||||
const half = Math.floor(conversation.messages.length / 2)
|
||||
conversation.messages.splice(0, half)
|
||||
}
|
||||
await this.conversationsCache.set(conversationKey, conversation)
|
||||
resolve(resMessage)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error(error))
|
||||
}
|
||||
})
|
||||
socket.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async webMessage(prompt, chatId, botId) {
|
||||
if (!FormData) {
|
||||
throw new Error('缺少依赖:form-data。请安装依赖后重试')
|
||||
}
|
||||
if (!chatId) {
|
||||
chatId = (await this.createChatList()).chatListId
|
||||
}
|
||||
let requestP = new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let formData = new FormData()
|
||||
formData.setBoundary('----WebKitFormBoundarycATE2QFHDn9ffeWF')
|
||||
formData.append('clientType', '2')
|
||||
formData.append('chatId', chatId)
|
||||
formData.append('text', prompt)
|
||||
if (prompt.image) {
|
||||
prompt.text = prompt.text.replace("[图片]", "") // 清理消息中中首个被使用的图片
|
||||
const imgdata = await this.uploadImage(prompt.image)
|
||||
if (imgdata) {
|
||||
formData.append('fileUrl', imgdata.url)
|
||||
formData.append('file', imgdata.file, 'image.png')
|
||||
}
|
||||
}
|
||||
formData.append('text', prompt.text)
|
||||
if (botId) {
|
||||
formData.append('isBot', '1')
|
||||
formData.append('botId', botId)
|
||||
}
|
||||
let randomNumber = Math.floor(Math.random() * 1000)
|
||||
let fd = '439' + randomNumber.toString().padStart(3, '0')
|
||||
formData.append('fd', fd)
|
||||
|
|
@ -57,7 +305,7 @@ export default class XinghuoClient {
|
|||
logger.error('星火statusCode:' + statusCode)
|
||||
}
|
||||
let response = ''
|
||||
function onMessage (data) {
|
||||
function onMessage(data) {
|
||||
// console.log(data)
|
||||
if (data === '<end>') {
|
||||
return resolve({
|
||||
|
|
@ -65,8 +313,18 @@ export default class XinghuoClient {
|
|||
response
|
||||
})
|
||||
}
|
||||
if (data.charAt(0) === '{') {
|
||||
try {
|
||||
response = JSON.parse(data).value
|
||||
if (Config.debug) {
|
||||
logger.info(response)
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (data) {
|
||||
if (data && data !== '[error]') {
|
||||
response += atob(data.trim())
|
||||
if (Config.debug) {
|
||||
logger.info(response)
|
||||
|
|
@ -112,22 +370,81 @@ export default class XinghuoClient {
|
|||
// req.write(formData.stringify())
|
||||
req.end()
|
||||
})
|
||||
const { response } = await requestP
|
||||
// logger.info(response)
|
||||
// let responseText = atob(response)
|
||||
return {
|
||||
conversationId: chatId,
|
||||
text: response
|
||||
}
|
||||
|
||||
async sendMessage(prompt, chatId, image) {
|
||||
// 对星火预设的问题进行重写,避免收到预设回答
|
||||
prompt = this.promptBypassPreset(prompt)
|
||||
if (Config.xhmode == 'api' || Config.xhmode == 'apiv2' || Config.xhmode == 'assistants') {
|
||||
if (!Config.xhAppId || !Config.xhAPISecret || !Config.xhAPIKey) throw new Error('未配置api')
|
||||
let Prompt = []
|
||||
// 设定
|
||||
if (Config.xhPromptSerialize) {
|
||||
try {
|
||||
Prompt = JSON.parse(Config.xhPrompt)
|
||||
} catch (error) {
|
||||
Prompt = []
|
||||
logger.warn('星火设定序列化失败,本次对话不附带设定')
|
||||
}
|
||||
} else {
|
||||
Prompt = Config.xhPrompt ? [{ "role": "user", "content": Config.xhPrompt }] : []
|
||||
}
|
||||
let response = await this.apiMessage(prompt, chatId, Prompt)
|
||||
if (Config.xhRetRegExp) {
|
||||
response = response.replace(new RegExp(Config.xhRetRegExp, 'g'), Config.xhRetReplace)
|
||||
}
|
||||
return {
|
||||
conversationId: chatId,
|
||||
text: response
|
||||
}
|
||||
} else if (Config.xhmode == 'web') {
|
||||
let botId = false
|
||||
if (chatId && typeof chatId === 'object') {
|
||||
chatId = chatId.chatid
|
||||
botId = chatId.botid
|
||||
}
|
||||
if (!chatId) {
|
||||
chatId = (await this.createChatList()).chatListId
|
||||
}
|
||||
let { response } = await this.webMessage({ text: prompt, image: image }, chatId, botId)
|
||||
// logger.info(response)
|
||||
// let responseText = atob(response)
|
||||
// 处理图片
|
||||
let images
|
||||
if (response.includes('multi_image_url')) {
|
||||
images = [{
|
||||
tag: '',
|
||||
url: JSON.parse(/{([^}]*)}/g.exec(response)[0]).url
|
||||
}]
|
||||
response = '我已经完成作品,欢迎您提出宝贵的意见和建议,帮助我快速进步~~'
|
||||
}
|
||||
if (botId) {
|
||||
chatId = {
|
||||
chatid: chatId,
|
||||
botid: botId
|
||||
}
|
||||
}
|
||||
if (Config.xhRetRegExp) {
|
||||
response = response.replace(new RegExp(Config.xhRetRegExp, 'g'), Config.xhRetReplace)
|
||||
}
|
||||
return {
|
||||
conversationId: chatId,
|
||||
text: response,
|
||||
images: images
|
||||
}
|
||||
} else {
|
||||
throw new Error('星火模式错误')
|
||||
}
|
||||
}
|
||||
|
||||
async createChatList () {
|
||||
async createChatList(bot = false) {
|
||||
let createChatListRes = await fetch(createChatUrl, {
|
||||
method: 'POST',
|
||||
headers: Object.assign(this.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Botweb: bot ? 1 : 0
|
||||
}),
|
||||
body: '{}'
|
||||
body: bot ? `{"BotWeb": 1, "botId": "${bot}"}` : '{}'
|
||||
})
|
||||
if (createChatListRes.status !== 200) {
|
||||
let errorRes = await createChatListRes.text()
|
||||
|
|
@ -139,8 +456,8 @@ export default class XinghuoClient {
|
|||
if (createChatListRes.data?.id) {
|
||||
logger.info('星火对话创建成功:' + createChatListRes.data.id)
|
||||
} else {
|
||||
logger.error('星火对话创建成功: ' + JSON.stringify(createChatListRes))
|
||||
throw new Error('星火对话创建成功:' + JSON.stringify(createChatListRes))
|
||||
logger.error('星火对话创建失败: ' + JSON.stringify(createChatListRes))
|
||||
throw new Error('星火对话创建失败:' + JSON.stringify(createChatListRes))
|
||||
}
|
||||
return {
|
||||
chatListId: createChatListRes.data?.id,
|
||||
|
|
@ -149,6 +466,6 @@ export default class XinghuoClient {
|
|||
}
|
||||
}
|
||||
|
||||
function atob (s) {
|
||||
function atob(s) {
|
||||
return Buffer.from(s, 'base64').toString()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue