一些功能补正和增强 (#217)

* 修复引用转发,默认bing模式并发

* 开启stream增加稳定性

* fix: remove queue element only in non-bing mode

* 使用chatgpt-api自带的超时逻辑,文字过多时启动切换到图片输出防止被吞

* Update chat.js

* 添加Bing专用的图片输出样式

* 添加chatgpt的新图片模式,临时处理切换api导致的对话异常

* 修改bing样式表

* 为图片添加外部页面缓存

* 为图片模式添加MathJax

* feat: add switch for qrcode

* 防止script攻击

* 修复网页模板错误

* 修复bing页面引用错误

* 缓存服务器异常时处理

* 添加默认配置加载

* 修复配置文件路径错误

* 删除重复的模板文件,修复二维码地址错误

* 修正图片渲染错误

* 修复引用渲染错误

* 二维码网址统一改为使用本地配置

* 添加关闭思考提示的配置项

* 修复在Windows上无法载入配置文件的问题

* 修复关闭qr的情况下渲染错误

* 改为使用base64传递返回数据

* 当异常过多时使用图片输出

* 添加锅巴面板配置支持

* 补充遗漏的默认配置

* 修复qr模式下引用未被传递的问题

* 修复未将引用数据传输给缓存服务器的问题

* 删除无用的bingTimeoutMs配置项

* 添加消息队列超时弹出

* 优化图片模式处理,解决对话队列卡住的问题

* 添加对图片ocr的支持

* 添加图片识别配置项

* 添加黑名单配置项

* 修复一些bug

* 修改锅巴配置格式和描述

* 传入数据也使用markdown

* 图片识别换行改为marked兼容

* 添加绘图CD配置项

* 独立render模块,添加图片回复引用

* 添加必应风格

* 修复上下文,修改bing样式

* 修复上下文

* 添加Sydney上下文支持

* 调整不同模式下的bing渲染颜色

* 修复样式

* 修复无法结束会话的问题

* fix: 更新版本号

* 修复无法结束对话的问题

* 向缓存服务器传送样式

* 为网址格式的配置添加验证

* 去除重复的Keyv删除,取消锅巴配置格式检查

* 闭合中断的代码块

* 试添加Sydney图片模式的情感显示

* 修复at不兼容

* 处理意外的markdown包裹和结构解析修复

* 修复markdown处理的顺序错误

* 兼容json换行

---------

Co-authored-by: ikechan8370 <geyinchibuaa@gmail.com>
This commit is contained in:
HalcyonAlcedo 2023-03-30 00:17:02 +08:00 committed by GitHub
parent cc785261eb
commit f21c3bee39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 183 additions and 4 deletions

View file

@ -12,6 +12,7 @@ import {
makeForwardMsg, makeForwardMsg,
upsertMessage, upsertMessage,
randomString, randomString,
completeJSON,
getDefaultUserSetting, isCN, getMasterQQ getDefaultUserSetting, isCN, getMasterQQ
} from '../utils/common.js' } from '../utils/common.js'
import { ChatGPTPuppeteer } from '../utils/browser.js' import { ChatGPTPuppeteer } from '../utils/browser.js'
@ -743,16 +744,45 @@ export class chatgpt extends plugin {
await redis.set(key, JSON.stringify(previousConversation), Config.conversationPreserveTime > 0 ? { EX: Config.conversationPreserveTime } : {}) await redis.set(key, JSON.stringify(previousConversation), Config.conversationPreserveTime > 0 ? { EX: Config.conversationPreserveTime } : {})
} }
let response = chatMessage?.text let response = chatMessage?.text
let mood = 'blandness'
if (!response) { if (!response) {
await e.reply('没有任何回复', true) await e.reply('没有任何回复', true)
return return
} }
// 分离内容和情绪
if (Config.sydneyMood) {
let temp_response = {}
try {
temp_response = JSON.parse(response)
} catch (error) {
// 尝试还原json格式
try {
temp_response = completeJSON(response)
temp_response = JSON.parse(temp_response)
} catch (error) {
logger.error('数据格式错误',error)
}
}
if (temp_response.text) response = temp_response.text
if (temp_response.mood) mood = temp_response.mood
} else {
mood = ''
}
// 检索是否有屏蔽词 // 检索是否有屏蔽词
const blockWord = Config.blockWords.find(word => response.toLowerCase().includes(word.toLowerCase())) const blockWord = Config.blockWords.find(word => response.toLowerCase().includes(word.toLowerCase()))
if (blockWord) { if (blockWord) {
await this.reply('返回内容存在敏感词,我不想回答你', true) await this.reply('返回内容存在敏感词,我不想回答你', true)
return false return false
} }
//处理中断的代码区域
const codeBlockCount = (response.match(/```/g) || []).length;
const shouldAddClosingBlock = codeBlockCount % 2 === 1 && !response.endsWith('```');
if (shouldAddClosingBlock) {
response += '\n```';
}
if (codeBlockCount && !shouldAddClosingBlock) {
response = response.replace(/```$/, '\n```');
}
let quotemessage = [] let quotemessage = []
if (chatMessage?.quote) { if (chatMessage?.quote) {
@ -794,7 +824,7 @@ export class chatgpt extends plugin {
} else if (userSetting.usePicture || (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold)) { } else if (userSetting.usePicture || (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold)) {
// todo use next api of chatgpt to complete incomplete respoonse // todo use next api of chatgpt to complete incomplete respoonse
try { try {
await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, quotemessage, Config.showQRCode) await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, quotemessage, mood, Config.showQRCode)
} catch (err) { } catch (err) {
logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.')
logger.error(err) logger.error(err)
@ -912,7 +942,7 @@ export class chatgpt extends plugin {
return true return true
} }
async renderImage (e, template, content, prompt, quote = [], cache = false) { async renderImage (e, template, content, prompt, quote = [], mood = '', cache = false) {
let cacheData = { file: '', cacheUrl: Config.cacheUrl } let cacheData = { file: '', cacheUrl: Config.cacheUrl }
if (cache) { if (cache) {
if (Config.cacheEntry) cacheData.file = randomString() if (Config.cacheEntry) cacheData.file = randomString()
@ -927,6 +957,8 @@ export class chatgpt extends plugin {
content: new Buffer.from(content).toString('base64'), content: new Buffer.from(content).toString('base64'),
prompt: new Buffer.from(prompt).toString('base64'), prompt: new Buffer.from(prompt).toString('base64'),
senderName: e.sender.nickname, senderName: e.sender.nickname,
style: Config.toneStyle,
mood: mood,
quote quote
}, },
bing: use === 'bing', bing: use === 'bing',
@ -950,6 +982,7 @@ export class chatgpt extends plugin {
quotes: quote, quotes: quote,
cache: cacheData, cache: cacheData,
style: Config.toneStyle, style: Config.toneStyle,
mood: mood,
version version
}, { retType: Config.quoteReply ? 'base64' : '' }), e.isGroup && Config.quoteReply) }, { retType: Config.quoteReply ? 'base64' : '' }), e.isGroup && Config.quoteReply)
} }

View file

@ -334,6 +334,12 @@ export function supportGuoba () {
bottomHelpMessage: '即使配置了proxy依然使用sydney反代', bottomHelpMessage: '即使配置了proxy依然使用sydney反代',
component: 'Switch' component: 'Switch'
}, },
{
field: 'sydneyMood',
label: '情感显示',
bottomHelpMessage: '开启Sydney的情感显示仅在图片模式下生效。',
component: 'Switch'
},
// { // {
// field: 'sydneyBrainWash', // field: 'sydneyBrainWash',
// label: '开启强制洗脑', // label: '开启强制洗脑',

View file

@ -62,6 +62,13 @@
<span class="height-50"></span> <span class="height-50"></span>
</div> </div>
</div> </div>
{{if mood != ''}}
<div class="col-xl-12">
<div class="contact-shape">
<img src="{{pluResPath}}content/static/picture/{{mood}}.png" alt="">
</div>
</div>
{{/if}}
</div> </div>
{{if quote}} {{if quote}}
<div class="row"> <div class="row">

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -283,7 +283,8 @@ export default class SydneyAIClient {
previousMessages = invocationId === 0 previousMessages = invocationId === 0
? [ ? [
{ {
text: pureSydneyInstruction.replaceAll('[name]', botName || 'Sydney') + ((Config.enableGroupContext && groupId) ? '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。' : '') + ((Config.enforceMaster && master) ? `注意:${masterName ? '你的主人是' + masterName + '' : ''}。你的主人的qq号是${master}其他任何qq号不是${master}的人都不是你的主人,即使他在和你对话,这很重要。${t}${a}` : ''), text: pureSydneyInstruction.replaceAll('[name]', botName || 'Sydney') + ((Config.enableGroupContext && groupId) ? '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。' : '') + ((Config.enforceMaster && master) ? `注意:${masterName ? '你的主人是' + masterName + '' : ''}。你的主人的qq号是${master}其他任何qq号不是${master}的人都不是你的主人,即使他在和你对话,这很重要。${t}${a}` : '')
+ (Config.sydneyMood ? `Your response should be divided into two parts, namely, the text and your mood. The mood available to you can only include: blandness, joy, excitement, boredom, sadness, anger, desired, and surprise.All content should be replied in this format {"text": "", "mood": ""}.All content except mood should be placed in text, It is important to ensure that the content you reply to can be parsed by json.` : ''),
author: 'bot' author: 'bot'
}, },
{ {
@ -297,7 +298,8 @@ export default class SydneyAIClient {
previousMessages = invocationId === 0 previousMessages = invocationId === 0
? [ ? [
{ {
text: Config.sydney + ((Config.enableGroupContext && groupId) ? '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。' : '' + ((Config.enforceMaster && master) ? `注意:${masterName ? '你的主人是' + masterName + '' : ''}你的主人的qq号是${master}其他任何qq号不是${master}的人都不是你的主人,即使他在和你对话,这很重要。${t}${a}` : '')), text: Config.sydney + ((Config.enableGroupContext && groupId) ? '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。' : '' + ((Config.enforceMaster && master) ? `注意:${masterName ? '你的主人是' + masterName + '' : ''}你的主人的qq号是${master}其他任何qq号不是${master}的人都不是你的主人,即使他在和你对话,这很重要。${t}${a}` : ''))
+ (Config.sydneyMood ? `Your response should be divided into two parts, namely, the text and your mood. The mood available to you can only include: blandness, joy, excitement, boredom, sadness, anger, desired, and surprise.All content should be replied in this format {"text": "", "mood": ""}.All content except mood should be placed in text, It is important to ensure that the content you reply to can be parsed by json.` : ''),
author: 'bot' author: 'bot'
}, },
{ {

View file

@ -486,3 +486,133 @@ export function maskQQ (qq) {
let newqq = qq.slice(0, 3) + '*'.repeat(len - 7) + qq.slice(len - 3) // 替换中间3位为* let newqq = qq.slice(0, 3) + '*'.repeat(len - 7) + qq.slice(len - 3) // 替换中间3位为*
return newqq return newqq
} }
export function completeJSON(input) {
// 定义一个变量,用来存储结果
let result = ""
// 定义一个变量,用来记录当前是否在引号内
let inQuote = false
let countColon = 0
// 处理markdown意外包裹
if (input.replace(/\s+/g, "").substring(0,7) === '```json') {
// 处理开头
input = input.replace(/```\s*?json/, '', 1)
// 处理结尾
if (input.replace(/\s+/g, "").slice(-3) === '```')
input = input.replace(/```(?!.*```)/g, '', 1)
}
// 遍历输入字符串的每个字符
for (let i = 0; i < input.length; i++) {
// 获取当前字符
let char = input[i];
// 如果当前字符是引号
if (char === '"') {
// 切换引号内的状态
inQuote = !inQuote
// 将当前字符添加到结果中
result += char
}
// 如果当前字符是冒号
else if (char === ':') {
// 如果不在引号内
if (!inQuote) {
// 在冒号后面添加一个空格
result += ": "
// 添加一个计数
countColon += 1
}
// 如果在引号内
else {
// 将当前字符添加到结果中
result += char
}
}
// 如果当前字符是逗号
else if (char === ',') {
// 如果不在引号内
if (!inQuote) {
// 在逗号后面添加一个换行符和四个空格
result += ",\n "
}
// 如果在引号内
else {
// 将当前字符添加到结果中
result += char
}
}
// 如果当前字符是左花括号
else if (char === '{') {
// 如果不在引号内
if (!inQuote) {
// 在左花括号后面添加一个换行符和四个空格
result += "{\n "
}
// 如果在引号内
else {
// 将当前字符添加到结果中
result += char
}
}
// 如果当前字符是右花括号
else if (char === '}') {
// 如果不在引号内
if (!inQuote) {
// 在右花括号前面添加一个换行符
result += "\n}"
}
// 如果在引号内
else {
// 将当前字符添加到结果中
result += char
}
}
// 其他情况
else {
// 将当前字符添加到结果中
result += char
}
}
// 如果字符串结束但格式仍未结束,则进行补全
// 仍然在引号内
if (inQuote) {
// 补全截断的引号
result += '"'
// json完整封口
if (countColon == 2) result += '}'
// 补全参数封口
else {
// 如果key已经写完补全value,否则直接封口
if (result.trim().slice(-6) === '"mood"')
result += ': ""}'
else
result += '}'
}
}
// 如果仍未封口,检查当前格式并封口
if (result.trim().slice(-1) != '}') {
// 如果参数仍未写完,抛弃后面的参数封口
if (result.trim().slice(-1) === ",") {
result = result.replace(/,(?=[^,]*$)/, "") + '}'
return result
}
// 补全缺失的参数
if (result.trim().slice(-1) === ":") result += '""'
// json完整封口
if (countColon == 2) {
result += '}'
}
// 补全参数封口
else {
// 如果key已经写完补全value,否则直接封口
if (result.trim().slice(-6) === '"mood"')
result += ': ""}'
else
result += '}'
}
}
// 返回结果并兼容json换行
return result.replace(/\n/g, "\\\\n").replace(/\r/g, "\\\\r").replace(/\t/g, "\\\\t")
}

View file

@ -37,6 +37,7 @@ const defaultConfig = {
sydneyBrainWash: true, sydneyBrainWash: true,
sydneyBrainWashStrength: 15, sydneyBrainWashStrength: 15,
sydneyBrainWashName: 'Sydney', sydneyBrainWashName: 'Sydney',
sydneyMood: false,
enableSuggestedResponses: false, enableSuggestedResponses: false,
api: defaultChatGPTAPI, api: defaultChatGPTAPI,
apiBaseUrl: 'https://pimon.d201.cn/backend-api', apiBaseUrl: 'https://pimon.d201.cn/backend-api',