chatgpt-plugin/utils/xinghuo/xinghuo.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

471 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')
const createChatUrl = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2lmbHlncHQvdS9jaGF0LWxpc3QvdjEvY3JlYXRlLWNoYXQtbGlzdA==')
const chatUrl = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2lmbHlncHQtY2hhdC91L2NoYXRfbWVzc2FnZS9jaGF0')
let FormData
try {
FormData = (await import('form-data')).default
} 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) {
this.cache = opts.cache
this.ssoSessionId = opts.ssoSessionId
this.headers = {
Referer: referer,
Cookie: 'ssoSessionId=' + this.ssoSessionId + ';',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.69 Mobile/15E148 Safari/604.1',
Origin: origin
}
}
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。请安装依赖后重试')
}
return new Promise(async (resolve, reject) => {
let formData = new FormData()
formData.setBoundary('----WebKitFormBoundarycATE2QFHDn9ffeWF')
formData.append('clientType', '2')
formData.append('chatId', chatId)
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)
this.headers.Referer = referer + chatId
let option = {
method: 'POST',
headers: Object.assign(this.headers, {
Accept: 'text/event-stream',
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundarycATE2QFHDn9ffeWF'
}),
// body: formData,
referrer: this.headers.Referer
}
let statusCode
const req = https.request(chatUrl, option, (res) => {
statusCode = res.statusCode
if (statusCode !== 200) {
logger.error('星火statusCode' + statusCode)
}
let response = ''
function onMessage(data) {
// console.log(data)
if (data === '<end>') {
return resolve({
error: null,
response
})
}
if (data.charAt(0) === '{') {
try {
response = JSON.parse(data).value
if (Config.debug) {
logger.info(response)
}
} catch (err) {
reject(err)
}
}
try {
if (data && data !== '[error]') {
response += atob(data.trim())
if (Config.debug) {
logger.info(response)
}
}
} catch (err) {
console.warn('fetchSSE onMessage unexpected error', err)
reject(err)
}
}
const parser = createParser((event) => {
if (event.type === 'event') {
onMessage(event.data)
}
})
const errBody = []
res.on('data', (chunk) => {
if (statusCode === 200) {
let str = chunk.toString()
parser.feed(str)
}
errBody.push(chunk)
})
// const body = []
// res.on('data', (chunk) => body.push(chunk))
res.on('end', () => {
const resString = Buffer.concat(errBody).toString()
// logger.info({ resString })
reject(resString)
})
})
formData.pipe(req)
req.on('error', (err) => {
logger.error(err)
reject(err)
})
req.on('timeout', () => {
req.destroy()
reject(new Error('Request time out'))
})
// req.write(formData.stringify())
req.end()
})
}
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(bot = false) {
let createChatListRes = await fetch(createChatUrl, {
method: 'POST',
headers: Object.assign(this.headers, {
'Content-Type': 'application/json',
Botweb: bot ? 1 : 0
}),
body: bot ? `{"BotWeb": 1, "botId": "${bot}"}` : '{}'
})
if (createChatListRes.status !== 200) {
let errorRes = await createChatListRes.text()
let errorText = '星火对话创建失败:' + errorRes
logger.error(errorText)
throw new Error(errorText)
}
createChatListRes = await createChatListRes.json()
if (createChatListRes.data?.id) {
logger.info('星火对话创建成功:' + createChatListRes.data.id)
} else {
logger.error('星火对话创建失败: ' + JSON.stringify(createChatListRes))
throw new Error('星火对话创建失败:' + JSON.stringify(createChatListRes))
}
return {
chatListId: createChatListRes.data?.id,
title: createChatListRes.data?.title
}
}
}
function atob(s) {
return Buffer.from(s, 'base64').toString()
}