mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
Conversation communication (#165)
* init: conversation in api3 (WIP) * feat: beta conversation * fix: fix some problems * fix: add help message
This commit is contained in:
parent
a1e72c7757
commit
a3b14b1a79
11 changed files with 658 additions and 215 deletions
0
REDIS
Normal file
0
REDIS
Normal file
8
Redis.md
Normal file
8
Redis.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
## 本插件使用的一些redis键值说明
|
||||||
|
|
||||||
|
### 官方(API3/浏览器模式)
|
||||||
|
* CHATGPT:QQ_CONVERSATION:123456789 qq号为123456789的人当前正在使用哪个对话
|
||||||
|
* CHATGPT:CONVERSATION_LAST_MESSAGE_ID:26001339-9043-435d-8394-c553a3109fdf id为`26001339-9043-435d-8394-c553a3109fdf`的对话,最后一条用户发出的消息的id
|
||||||
|
* CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:26001339-9043-435d-8394-c553a3109fdf id为`26001339-9043-435d-8394-c553a3109fdf`的对话,最后一条用户发出的消息的内容(问题)
|
||||||
|
* CHATGPT:CONVERSATION_CREATE_TIME:26001339-9043-435d-8394-c553a3109fdf id为`26001339-9043-435d-8394-c553a3109fdf`的对话创建时间
|
||||||
|
* CHATGPT:CONVERSATION_LENGTH:26001339-9043-435d-8394-c553a3109fdf id为`26001339-9043-435d-8394-c553a3109fdf`的对话当前长度(不计分支)
|
||||||
475
apps/chat.js
475
apps/chat.js
|
|
@ -1,32 +1,19 @@
|
||||||
import plugin from '../../../lib/plugins/plugin.js'
|
import plugin from '../../../lib/plugins/plugin.js'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Config } from '../config/index.js'
|
import { Config } from '../config/index.js'
|
||||||
import showdown from 'showdown'
|
|
||||||
import mjAPI from 'mathjax-node'
|
import mjAPI from 'mathjax-node'
|
||||||
import { uuid } from 'oicq/lib/common.js'
|
import { v4 as uuid } from 'uuid'
|
||||||
import delay from 'delay'
|
import delay from 'delay'
|
||||||
import { ChatGPTAPI } from 'chatgpt'
|
import { ChatGPTAPI } from 'chatgpt'
|
||||||
import { ChatGPTClient, BingAIClient } from '@waylaidwanderer/chatgpt-api'
|
import { ChatGPTClient, BingAIClient } from '@waylaidwanderer/chatgpt-api'
|
||||||
import { getMessageById, makeForwardMsg, tryTimes, upsertMessage, pTimeout } from '../utils/common.js'
|
import { getMessageById, makeForwardMsg, tryTimes, upsertMessage } from '../utils/common.js'
|
||||||
import { ChatGPTPuppeteer } from '../utils/browser.js'
|
import { ChatGPTPuppeteer } from '../utils/browser.js'
|
||||||
import { KeyvFile } from 'keyv-file'
|
import { KeyvFile } from 'keyv-file'
|
||||||
import { OfficialChatGPTClient } from '../utils/message.js'
|
import { OfficialChatGPTClient } from '../utils/message.js'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
// import puppeteer from '../utils/browser.js'
|
import { getConversations, getLatestMessageIdByConversationId } from '../utils/conversation.js'
|
||||||
// import showdownKatex from 'showdown-katex'
|
|
||||||
const blockWords = Config.blockWords
|
const blockWords = Config.blockWords
|
||||||
const converter = new showdown.Converter({
|
|
||||||
extensions: [
|
|
||||||
// showdownKatex({
|
|
||||||
// delimiters: [
|
|
||||||
// { left: '$$', right: '$$', display: false },
|
|
||||||
// { left: '$', right: '$', display: false, inline: true },
|
|
||||||
// { left: '\\(', right: '\\)', display: false },
|
|
||||||
// { left: '\\[', right: '\\]', display: true }
|
|
||||||
// ]
|
|
||||||
// })
|
|
||||||
]
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。
|
* 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。
|
||||||
* 单位:秒
|
* 单位:秒
|
||||||
|
|
@ -42,7 +29,7 @@ mjAPI.config({
|
||||||
mjAPI.start()
|
mjAPI.start()
|
||||||
|
|
||||||
export class chatgpt extends plugin {
|
export class chatgpt extends plugin {
|
||||||
constructor() {
|
constructor () {
|
||||||
let toggleMode = Config.toggleMode
|
let toggleMode = Config.toggleMode
|
||||||
super({
|
super({
|
||||||
/** 功能名称 */
|
/** 功能名称 */
|
||||||
|
|
@ -62,7 +49,7 @@ export class chatgpt extends plugin {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reg: '#chatgpt对话列表',
|
reg: '#chatgpt对话列表',
|
||||||
fnc: 'getConversations',
|
fnc: 'getAllConversations',
|
||||||
permission: 'master'
|
permission: 'master'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +82,10 @@ export class chatgpt extends plugin {
|
||||||
reg: '#OpenAI(剩余)?(余额|额度)',
|
reg: '#OpenAI(剩余)?(余额|额度)',
|
||||||
fnc: 'totalAvailable',
|
fnc: 'totalAvailable',
|
||||||
permission: 'master'
|
permission: 'master'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reg: '^#chatgpt切换对话',
|
||||||
|
fnc: 'attachConversation'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
@ -107,7 +98,7 @@ export class chatgpt extends plugin {
|
||||||
* @param e
|
* @param e
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async getConversations(e) {
|
async getConversations (e) {
|
||||||
let keys = await redis.keys('CHATGPT:CONVERSATIONS:*')
|
let keys = await redis.keys('CHATGPT:CONVERSATIONS:*')
|
||||||
if (!keys || keys.length === 0) {
|
if (!keys || keys.length === 0) {
|
||||||
await this.reply('当前没有人正在与机器人对话', true)
|
await this.reply('当前没有人正在与机器人对话', true)
|
||||||
|
|
@ -129,7 +120,7 @@ export class chatgpt extends plugin {
|
||||||
* @param e
|
* @param e
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async destroyConversations(e) {
|
async destroyConversations (e) {
|
||||||
let ats = e.message.filter(m => m.type === 'at')
|
let ats = e.message.filter(m => m.type === 'at')
|
||||||
if (ats.length === 0) {
|
if (ats.length === 0) {
|
||||||
let c = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
let c = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
||||||
|
|
@ -153,7 +144,7 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async help(e) {
|
async help (e) {
|
||||||
let response = 'chatgpt-plugin使用帮助文字版\n' +
|
let response = 'chatgpt-plugin使用帮助文字版\n' +
|
||||||
'@我+聊天内容: 发起对话与AI进行聊天\n' +
|
'@我+聊天内容: 发起对话与AI进行聊天\n' +
|
||||||
'#chatgpt对话列表: 查看当前发起的对话\n' +
|
'#chatgpt对话列表: 查看当前发起的对话\n' +
|
||||||
|
|
@ -163,7 +154,7 @@ export class chatgpt extends plugin {
|
||||||
await this.reply(response)
|
await this.reply(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async switch2Picture(e) {
|
async switch2Picture (e) {
|
||||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||||
if (!userSetting) {
|
if (!userSetting) {
|
||||||
userSetting = { usePicture: true }
|
userSetting = { usePicture: true }
|
||||||
|
|
@ -175,7 +166,7 @@ export class chatgpt extends plugin {
|
||||||
await this.reply('ChatGPT回复已转换为图片模式')
|
await this.reply('ChatGPT回复已转换为图片模式')
|
||||||
}
|
}
|
||||||
|
|
||||||
async switch2Text(e) {
|
async switch2Text (e) {
|
||||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||||
if (!userSetting) {
|
if (!userSetting) {
|
||||||
userSetting = { usePicture: false }
|
userSetting = { usePicture: false }
|
||||||
|
|
@ -191,7 +182,7 @@ export class chatgpt extends plugin {
|
||||||
* #chatgpt
|
* #chatgpt
|
||||||
* @param e oicq传递的事件参数e
|
* @param e oicq传递的事件参数e
|
||||||
*/
|
*/
|
||||||
async chatgpt(e) {
|
async chatgpt (e) {
|
||||||
let prompt
|
let prompt
|
||||||
if (this.toggleMode === 'at') {
|
if (this.toggleMode === 'at') {
|
||||||
if (!e.msg || e.msg.startsWith('#')) {
|
if (!e.msg || e.msg.startsWith('#')) {
|
||||||
|
|
@ -208,7 +199,7 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const use = await redis.get('CHATGPT:USE')
|
const use = await redis.get('CHATGPT:USE')
|
||||||
if (use != 'bing') {
|
if (use !== 'bing') {
|
||||||
let randomId = uuid()
|
let randomId = uuid()
|
||||||
// 队列队尾插入,开始排队
|
// 队列队尾插入,开始排队
|
||||||
await redis.rPush('CHATGPT:CHAT_QUEUE', [randomId])
|
await redis.rPush('CHATGPT:CHAT_QUEUE', [randomId])
|
||||||
|
|
@ -240,47 +231,80 @@ export class chatgpt extends plugin {
|
||||||
// } catch (e) {
|
// } catch (e) {
|
||||||
// await this.reply('chatgpt初始化出错:' + e.msg, true)
|
// await this.reply('chatgpt初始化出错:' + e.msg, true)
|
||||||
// }
|
// }
|
||||||
let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
let previousConversation
|
||||||
let conversation = {}
|
let conversation = {}
|
||||||
if (!previousConversation) {
|
if (use === 'api3') {
|
||||||
let ctime = new Date()
|
// api3 支持对话穿插,因此不按照qq号来进行判断了
|
||||||
previousConversation = {
|
let conversationId = await redis.get(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`)
|
||||||
sender: e.sender,
|
if (conversationId) {
|
||||||
ctime,
|
let lastMessageId = await redis.get(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${conversationId}`)
|
||||||
utime: ctime,
|
if (!lastMessageId) {
|
||||||
num: 0
|
lastMessageId = await getLatestMessageIdByConversationId(conversationId)
|
||||||
|
}
|
||||||
|
// let lastMessagePrompt = await redis.get(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${conversationId}`)
|
||||||
|
// let conversationCreateTime = await redis.get(`CHATGPT:CONVERSATION_CREATE_TIME:${conversationId}`)
|
||||||
|
// let conversationLength = await redis.get(`CHATGPT:CONVERSATION_LENGTH:${conversationId}`)
|
||||||
|
conversation = {
|
||||||
|
conversationId,
|
||||||
|
parentMessageId: lastMessageId
|
||||||
|
}
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.mark({ previousConversation })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let ctime = new Date()
|
||||||
|
previousConversation = {
|
||||||
|
sender: e.sender,
|
||||||
|
ctime,
|
||||||
|
utime: ctime,
|
||||||
|
num: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME })
|
|
||||||
} else {
|
} else {
|
||||||
previousConversation = JSON.parse(previousConversation)
|
previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
||||||
conversation = {
|
if (!previousConversation) {
|
||||||
conversationId: previousConversation.conversation.conversationId,
|
let ctime = new Date()
|
||||||
parentMessageId: previousConversation.conversation.parentMessageId,
|
previousConversation = {
|
||||||
clientId: previousConversation.clientId,
|
sender: e.sender,
|
||||||
invocationId: previousConversation.invocationId,
|
ctime,
|
||||||
conversationSignature: previousConversation.conversationSignature
|
utime: ctime,
|
||||||
|
num: 0
|
||||||
|
}
|
||||||
|
// await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME })
|
||||||
|
} else {
|
||||||
|
previousConversation = JSON.parse(previousConversation)
|
||||||
|
conversation = {
|
||||||
|
conversationId: previousConversation.conversation.conversationId,
|
||||||
|
parentMessageId: previousConversation.conversation.parentMessageId,
|
||||||
|
clientId: previousConversation.clientId,
|
||||||
|
invocationId: previousConversation.invocationId,
|
||||||
|
conversationSignature: previousConversation.conversationSignature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Config.debug) {
|
if (Config.debug) {
|
||||||
logger.mark(conversation)
|
logger.mark(conversation)
|
||||||
}
|
}
|
||||||
let chatMessage = await this.sendMessage(prompt, conversation, use)
|
let chatMessage = await this.sendMessage(prompt, conversation, use, e)
|
||||||
previousConversation.conversation = {
|
if (use !== 'api3') {
|
||||||
conversationId: chatMessage.conversationId
|
previousConversation.conversation = {
|
||||||
|
conversationId: chatMessage.conversationId
|
||||||
|
}
|
||||||
|
if (use === 'bing') {
|
||||||
|
previousConversation.clientId = chatMessage.clientId
|
||||||
|
previousConversation.invocationId = chatMessage.invocationId
|
||||||
|
previousConversation.conversationSignature = chatMessage.conversationSignature
|
||||||
|
} else {
|
||||||
|
// 或许这样切换回来不会404?
|
||||||
|
previousConversation.conversation.parentMessageId = chatMessage.id
|
||||||
|
}
|
||||||
|
console.log(chatMessage)
|
||||||
|
previousConversation.num = previousConversation.num + 1
|
||||||
|
await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), CONVERSATION_PRESERVE_TIME > 0 ? { EX: CONVERSATION_PRESERVE_TIME } : {})
|
||||||
}
|
}
|
||||||
if (use === 'bing') {
|
|
||||||
previousConversation.clientId = chatMessage.clientId
|
|
||||||
previousConversation.invocationId = chatMessage.invocationId
|
|
||||||
previousConversation.conversationSignature = chatMessage.conversationSignature
|
|
||||||
} else {
|
|
||||||
// 或许这样切换回来不会404?
|
|
||||||
previousConversation.conversation.parentMessageId = chatMessage.id
|
|
||||||
}
|
|
||||||
console.log(chatMessage)
|
|
||||||
let response = chatMessage?.text
|
let response = chatMessage?.text
|
||||||
previousConversation.num = previousConversation.num + 1
|
|
||||||
await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), CONVERSATION_PRESERVE_TIME > 0 ? { EX: CONVERSATION_PRESERVE_TIME } : {})
|
|
||||||
// 检索是否有屏蔽词
|
// 检索是否有屏蔽词
|
||||||
const blockWord = blockWords.find(word => response.toLowerCase().includes(word.toLowerCase()))
|
const blockWord = blockWords.find(word => response.toLowerCase().includes(word.toLowerCase()))
|
||||||
if (blockWord) {
|
if (blockWord) {
|
||||||
|
|
@ -297,14 +321,14 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
if (userSetting.usePicture) {
|
if (userSetting.usePicture) {
|
||||||
let endTokens = ['.', '。', '……', '!', '!', ']', ')', ')', '】', '?', '?', '~', '"', "'"]
|
let endTokens = ['.', '。', '……', '!', '!', ']', ')', ')', '】', '?', '?', '~', '"', "'"]
|
||||||
let maxTries = 3
|
let maxTries = use === 'api3' ? 3 : 0
|
||||||
while (maxTries >= 0 && !endTokens.find(token => response.trimEnd().endsWith(token))) {
|
while (maxTries >= 0 && !endTokens.find(token => response.trimEnd().endsWith(token))) {
|
||||||
maxTries--
|
maxTries--
|
||||||
// while (!response.trimEnd().endsWith('.') && !response.trimEnd().endsWith('。') && !response.trimEnd().endsWith('……') &&
|
// while (!response.trimEnd().endsWith('.') && !response.trimEnd().endsWith('。') && !response.trimEnd().endsWith('……') &&
|
||||||
// !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith(']') && !response.trimEnd().endsWith('】')
|
// !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith(']') && !response.trimEnd().endsWith('】')
|
||||||
// ) {
|
// ) {
|
||||||
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
|
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
|
||||||
let responseAppend = await this.sendMessage('Continue', conversation, use)
|
let responseAppend = await this.sendMessage('Continue', conversation, use, e)
|
||||||
previousConversation.conversation = {
|
previousConversation.conversation = {
|
||||||
conversationId: responseAppend.conversationId,
|
conversationId: responseAppend.conversationId,
|
||||||
parentMessageId: responseAppend.id
|
parentMessageId: responseAppend.id
|
||||||
|
|
@ -328,23 +352,22 @@ export class chatgpt extends plugin {
|
||||||
// logger.info(response)
|
// logger.info(response)
|
||||||
// markdown转为html
|
// markdown转为html
|
||||||
// todo部分数学公式可能还有问题
|
// todo部分数学公式可能还有问题
|
||||||
let converted = response //converter.makeHtml(response)
|
let converted = response // converter.makeHtml(response)
|
||||||
|
|
||||||
/** 最后回复消息 */
|
/** 最后回复消息 */
|
||||||
await e.runtime.render('chatgpt-plugin', use != 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: converted, prompt, senderName: e.sender.nickname })
|
await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: converted, prompt, senderName: e.sender.nickname })
|
||||||
} else {
|
} else {
|
||||||
let quotemessage = []
|
let quotemessage = []
|
||||||
if (chatMessage?.quote) {
|
if (chatMessage?.quote) {
|
||||||
chatMessage.quote.forEach(function (item, index) {
|
chatMessage.quote.forEach(function (item, index) {
|
||||||
if (item.trim() != '') {
|
if (item.trim() !== '') {
|
||||||
quotemessage.push(item)
|
quotemessage.push(item)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (response.length > 1000 ) {
|
if (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold) {
|
||||||
// 文字过多时自动切换到图片模式输出
|
// 文字过多时自动切换到图片模式输出
|
||||||
let converted = response
|
await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt, quote: quotemessage, senderName: e.sender.nickname })
|
||||||
await e.runtime.render('chatgpt-plugin', use != 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: converted, prompt, quote: quotemessage, senderName: e.sender.nickname })
|
|
||||||
} else {
|
} else {
|
||||||
await this.reply(`${response}`, e.isGroup)
|
await this.reply(`${response}`, e.isGroup)
|
||||||
if (quotemessage.length > 0) {
|
if (quotemessage.length > 0) {
|
||||||
|
|
@ -370,163 +393,169 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(prompt, conversation = {}, use) {
|
async sendMessage (prompt, conversation = {}, use, e) {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
conversation = {
|
conversation = {
|
||||||
timeoutMs: Config.defaultTimeoutMs
|
timeoutMs: Config.defaultTimeoutMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(use)
|
if (Config.debug) {
|
||||||
if (use === 'browser') {
|
logger.mark(`using ${use} mode`)
|
||||||
return await this.chatgptBrowserBased(prompt, conversation)
|
}
|
||||||
} else if (use === 'apiReverse') {
|
switch (use) {
|
||||||
const currentDate = new Date().toISOString().split('T')[0]
|
case 'browser': {
|
||||||
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
|
return await this.chatgptBrowserBased(prompt, conversation)
|
||||||
|
}
|
||||||
|
case 'apiReverse': {
|
||||||
|
const currentDate = new Date().toISOString().split('T')[0]
|
||||||
|
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
|
||||||
Current date: ${currentDate}`
|
Current date: ${currentDate}`
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
// (Optional) Support for a reverse proxy for the completions endpoint (private API server).
|
// (Optional) Support for a reverse proxy for the completions endpoint (private API server).
|
||||||
// Warning: This will expose your `openaiApiKey` to a third-party. Consider the risks before using this.
|
// Warning: This will expose your `openaiApiKey` to a third-party. Consider the risks before using this.
|
||||||
reverseProxyUrl: Config.reverseProxy || 'https://chatgpt.pawan.krd/api/completions',
|
reverseProxyUrl: Config.reverseProxy || 'https://chatgpt.pawan.krd/api/completions',
|
||||||
// (Optional) Parameters as described in https://platform.openai.com/docs/api-reference/completions
|
// (Optional) Parameters as described in https://platform.openai.com/docs/api-reference/completions
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
// You can override the model name and any other parameters here.
|
// You can override the model name and any other parameters here.
|
||||||
model: Config.plus ? 'text-davinci-002-render-paid' : 'text-davinci-002-render'
|
model: Config.plus ? 'text-davinci-002-render-paid' : 'text-davinci-002-render'
|
||||||
},
|
},
|
||||||
// (Optional) Set custom instructions instead of "You are ChatGPT...".
|
// (Optional) Set custom instructions instead of "You are ChatGPT...".
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
// (Optional) Set a custom name for the user
|
// (Optional) Set a custom name for the user
|
||||||
// userLabel: 'User',
|
// userLabel: 'User',
|
||||||
// (Optional) Set a custom name for ChatGPT
|
// (Optional) Set a custom name for ChatGPT
|
||||||
chatGptLabel: Config.assistantLabel,
|
chatGptLabel: Config.assistantLabel,
|
||||||
// (Optional) Set to true to enable `console.debug()` logging
|
// (Optional) Set to true to enable `console.debug()` logging
|
||||||
debug: Config.debug
|
debug: Config.debug
|
||||||
}
|
|
||||||
const cacheOptions = {
|
|
||||||
// Options for the Keyv cache, see https://www.npmjs.com/package/keyv
|
|
||||||
// This is used for storing conversations, and supports additional drivers (conversations are stored in memory by default)
|
|
||||||
// For example, to use a JSON file (`npm i keyv-file`) as a database:
|
|
||||||
store: new KeyvFile({ filename: 'cache.json' })
|
|
||||||
}
|
|
||||||
let accessToken = await redis.get('CHATGPT:TOKEN')
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
|
||||||
}
|
|
||||||
// console.log(accessToken)
|
|
||||||
this.chatGPTApi = new ChatGPTClient(accessToken, clientOptions, cacheOptions)
|
|
||||||
let response = await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, conversation || {}), 1)
|
|
||||||
return {
|
|
||||||
text: response.response,
|
|
||||||
conversationId: response.conversationId,
|
|
||||||
id: response.messageId,
|
|
||||||
parentMessageId: conversation?.parentMessageId
|
|
||||||
}
|
|
||||||
} else if (use === 'bing') {
|
|
||||||
let bingToken = await redis.get('CHATGPT:BING_TOKEN')
|
|
||||||
if (!bingToken) {
|
|
||||||
throw new Error('未绑定Bing Cookie,请使用#chatgpt设置必应token命令绑定Bing Cookie')
|
|
||||||
}
|
|
||||||
let cookie = undefined
|
|
||||||
if (bingToken?.indexOf('=') > -1) {
|
|
||||||
cookie = bingToken
|
|
||||||
}
|
|
||||||
const bingAIClient = new BingAIClient({
|
|
||||||
userToken: bingToken, // "_U" cookie from bing.com
|
|
||||||
cookie,
|
|
||||||
debug: Config.debug
|
|
||||||
})
|
|
||||||
let response
|
|
||||||
let reply = ''
|
|
||||||
try {
|
|
||||||
/* bingAIClient中设置了无响应2分钟超时,应该不用单独处理了,后续看情况可以删掉这些代码
|
|
||||||
const responseP = new Promise(
|
|
||||||
async (resolve, reject) => {
|
|
||||||
let bingResponse = await bingAIClient.sendMessage(prompt, conversation || {},(token) => {
|
|
||||||
reply += token
|
|
||||||
})
|
|
||||||
return resolve(bingResponse)
|
|
||||||
})
|
|
||||||
response = await pTimeout(responseP, {
|
|
||||||
milliseconds: Config.bingTimeoutMs,
|
|
||||||
message: reply != '' ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑子不好使了,不知道怎么回答!'
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
response = await bingAIClient.sendMessage(prompt, conversation || {}, (token) => {
|
|
||||||
reply += token
|
|
||||||
})
|
|
||||||
if (response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()) {
|
|
||||||
if (response.response === undefined) {
|
|
||||||
response.response = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()
|
|
||||||
}
|
|
||||||
response.response = response.response.replace(/\[\^[0-9]+\^\]/g, (str) => {
|
|
||||||
return str.replace(/[/^]/g, '')
|
|
||||||
})
|
|
||||||
response.quote = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.replace(/\[\^[0-9]+\^\]/g, '').replace(response.response, '').split('\n')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
const cacheOptions = {
|
||||||
const code = error?.data?.code || 503
|
// Options for the Keyv cache, see https://www.npmjs.com/package/keyv
|
||||||
if (code === 503) {
|
// This is used for storing conversations, and supports additional drivers (conversations are stored in memory by default)
|
||||||
logger.error(error)
|
// For example, to use a JSON file (`npm i keyv-file`) as a database:
|
||||||
|
store: new KeyvFile({ filename: 'cache.json' })
|
||||||
}
|
}
|
||||||
console.error(error)
|
let accessToken = await redis.get('CHATGPT:TOKEN')
|
||||||
const message = error?.message || error?.data?.message || '与Bing通信时出错.'
|
if (!accessToken) {
|
||||||
|
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
||||||
|
}
|
||||||
|
// console.log(accessToken)
|
||||||
|
this.chatGPTApi = new ChatGPTClient(accessToken, clientOptions, cacheOptions)
|
||||||
|
let response = await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, conversation || {}), 1)
|
||||||
return {
|
return {
|
||||||
text: message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply != '' ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message
|
text: response.response,
|
||||||
|
conversationId: response.conversationId,
|
||||||
|
id: response.messageId,
|
||||||
|
parentMessageId: conversation?.parentMessageId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
case 'bing': {
|
||||||
text: response.response,
|
let bingToken = await redis.get('CHATGPT:BING_TOKEN')
|
||||||
quote: response.quote,
|
if (!bingToken) {
|
||||||
conversationId: response.conversationId,
|
throw new Error('未绑定Bing Cookie,请使用#chatgpt设置必应token命令绑定Bing Cookie')
|
||||||
clientId: response.clientId,
|
}
|
||||||
invocationId: response.invocationId,
|
let cookie
|
||||||
conversationSignature: response.conversationSignature
|
if (bingToken?.indexOf('=') > -1) {
|
||||||
|
cookie = bingToken
|
||||||
|
}
|
||||||
|
const bingAIClient = new BingAIClient({
|
||||||
|
userToken: bingToken, // "_U" cookie from bing.com
|
||||||
|
cookie,
|
||||||
|
debug: Config.debug
|
||||||
|
})
|
||||||
|
let response
|
||||||
|
let reply = ''
|
||||||
|
try {
|
||||||
|
response = await bingAIClient.sendMessage(prompt, conversation || {}, (token) => {
|
||||||
|
reply += token
|
||||||
|
})
|
||||||
|
if (response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()) {
|
||||||
|
if (response.response === undefined) {
|
||||||
|
response.response = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()
|
||||||
|
}
|
||||||
|
response.response = response.response.replace(/\[\^[0-9]+\^\]/g, (str) => {
|
||||||
|
return str.replace(/[/^]/g, '')
|
||||||
|
})
|
||||||
|
response.quote = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.replace(/\[\^[0-9]+\^\]/g, '').replace(response.response, '').split('\n')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const code = error?.data?.code || 503
|
||||||
|
if (code === 503) {
|
||||||
|
logger.error(error)
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
const message = error?.message || error?.data?.message || '与Bing通信时出错.'
|
||||||
|
return {
|
||||||
|
text: message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply != '' ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: response.response,
|
||||||
|
quote: response.quote,
|
||||||
|
conversationId: response.conversationId,
|
||||||
|
clientId: response.clientId,
|
||||||
|
invocationId: response.invocationId,
|
||||||
|
conversationSignature: response.conversationSignature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (use === 'api3') {
|
case 'api3': {
|
||||||
// official without cloudflare
|
// official without cloudflare
|
||||||
let accessToken = await redis.get('CHATGPT:TOKEN')
|
let accessToken = await redis.get('CHATGPT:TOKEN')
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
||||||
|
}
|
||||||
|
this.chatGPTApi = new OfficialChatGPTClient({
|
||||||
|
accessToken,
|
||||||
|
apiReverseUrl: Config.api,
|
||||||
|
timeoutMs: 120000
|
||||||
|
})
|
||||||
|
let sendMessageResult = await this.chatGPTApi.sendMessage(prompt, conversation)
|
||||||
|
// 更新最后一条prompt
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${sendMessageResult.conversationId}`, prompt)
|
||||||
|
// 更新最后一条messageId
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${sendMessageResult.conversationId}`, sendMessageResult.id)
|
||||||
|
await redis.set(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`, sendMessageResult.conversationId)
|
||||||
|
if (!conversation.conversationId) {
|
||||||
|
// 如果是对话的创建者
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_CREATER_ID:${sendMessageResult.conversationId}`, e.sender.user_id)
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_CREATER_NICK_NAME:${sendMessageResult.conversationId}`, e.sender.card)
|
||||||
|
}
|
||||||
|
return sendMessageResult
|
||||||
}
|
}
|
||||||
this.chatGPTApi = new OfficialChatGPTClient({
|
default: {
|
||||||
accessToken,
|
let completionParams = {}
|
||||||
apiReverseUrl: Config.api,
|
if (Config.model) {
|
||||||
timeoutMs: 120000
|
completionParams.model = Config.model
|
||||||
})
|
}
|
||||||
return await this.chatGPTApi.sendMessage(prompt, conversation)
|
this.chatGPTApi = new ChatGPTAPI({
|
||||||
} else {
|
apiKey: Config.apiKey,
|
||||||
let completionParams = {}
|
debug: false,
|
||||||
if (Config.model) {
|
upsertMessage,
|
||||||
completionParams.model = Config.model
|
getMessageById,
|
||||||
}
|
completionParams,
|
||||||
this.chatGPTApi = new ChatGPTAPI({
|
assistantLabel: Config.assistantLabel,
|
||||||
apiKey: Config.apiKey,
|
fetch
|
||||||
debug: false,
|
})
|
||||||
upsertMessage,
|
const currentDate = new Date().toISOString().split('T')[0]
|
||||||
getMessageById,
|
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
|
||||||
completionParams,
|
|
||||||
assistantLabel: Config.assistantLabel,
|
|
||||||
fetch
|
|
||||||
})
|
|
||||||
const currentDate = new Date().toISOString().split('T')[0]
|
|
||||||
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
|
|
||||||
Current date: ${currentDate}`
|
Current date: ${currentDate}`
|
||||||
let option = {
|
let option = {
|
||||||
timeoutMs: 120000,
|
timeoutMs: 120000,
|
||||||
promptPrefix
|
promptPrefix
|
||||||
|
}
|
||||||
|
if (conversation) {
|
||||||
|
option = Object.assign(option, conversation)
|
||||||
|
}
|
||||||
|
return await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, option), 5)
|
||||||
}
|
}
|
||||||
if (conversation) {
|
|
||||||
option = Object.assign(option, conversation)
|
|
||||||
}
|
|
||||||
return await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, option), 5)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async emptyQueue(e) {
|
async emptyQueue (e) {
|
||||||
await redis.lTrim('CHATGPT:CHAT_QUEUE', 1, 0)
|
await redis.lTrim('CHATGPT:CHAT_QUEUE', 1, 0)
|
||||||
await this.reply('已清空当前等待队列')
|
await this.reply('已清空当前等待队列')
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeQueueFirst(e) {
|
async removeQueueFirst (e) {
|
||||||
let uid = await redis.lPop('CHATGPT:CHAT_QUEUE', 0)
|
let uid = await redis.lPop('CHATGPT:CHAT_QUEUE', 0)
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
await this.reply('当前等待队列为空')
|
await this.reply('当前等待队列为空')
|
||||||
|
|
@ -535,13 +564,55 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async totalAvailable(e) {
|
async getAllConversations (e) {
|
||||||
|
const use = await redis.get('CHATGPT:USE')
|
||||||
|
if (use === 'api3') {
|
||||||
|
let conversations = await getConversations(e.sender.user_id)
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.mark('all conversations: ', conversations)
|
||||||
|
}
|
||||||
|
// let conversationsFirst10 = conversations.slice(0, 10)
|
||||||
|
await e.runtime.render('chatgpt-plugin', 'conversation/chatgpt', { conversations })
|
||||||
|
let text = '对话列表\n'
|
||||||
|
text += '对话id | 对话发起者 \n'
|
||||||
|
conversations.forEach(c => {
|
||||||
|
text += c.id + '|' + (c.creater || '未知') + '\n'
|
||||||
|
})
|
||||||
|
text += '您可以通过使用命令#chatgpt切换对话+对话id来加入指定对话'
|
||||||
|
await this.reply(text)
|
||||||
|
} else {
|
||||||
|
return await this.getConversations(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async attachConversation (e) {
|
||||||
|
const use = await redis.get('CHATGPT:USE')
|
||||||
|
if (use !== 'api3') {
|
||||||
|
await this.reply('该功能目前仅支持API3模式')
|
||||||
|
} else {
|
||||||
|
let conversationId = _.trimStart(e.msg.trimStart(), '#chatgpt切换对话').trim()
|
||||||
|
if (!conversationId) {
|
||||||
|
await this.reply('无效对话id,请在#chatgpt切换对话后面加上对话id')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// todo 验证这个对话是否存在且有效
|
||||||
|
// await getLatestMessageIdByConversationId(conversationId)
|
||||||
|
await redis.set(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`, conversationId)
|
||||||
|
await this.reply('切换成功')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async totalAvailable (e) {
|
||||||
|
if (!Config.apiKey) {
|
||||||
|
this.reply('当前未配置OpenAI API key,请在插件配置文件config/index.js中配置。若使用免费的API3则无需关心计费。')
|
||||||
|
return false
|
||||||
|
}
|
||||||
// 查询OpenAI Plus剩余试用额度
|
// 查询OpenAI Plus剩余试用额度
|
||||||
fetch('https://api.openai.com/dashboard/billing/credit_grants', {
|
fetch('https://api.openai.com/dashboard/billing/credit_grants', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + Config.apiKey,
|
Authorization: 'Bearer ' + Config.apiKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|
@ -560,7 +631,7 @@ export class chatgpt extends plugin {
|
||||||
* @param prompt 问题
|
* @param prompt 问题
|
||||||
* @param conversation 对话
|
* @param conversation 对话
|
||||||
*/
|
*/
|
||||||
async chatgptBrowserBased(prompt, conversation) {
|
async chatgptBrowserBased (prompt, conversation) {
|
||||||
let option = { markdown: true }
|
let option = { markdown: true }
|
||||||
if (Config['2captchaToken']) {
|
if (Config['2captchaToken']) {
|
||||||
option.captchaToken = Config['2captchaToken']
|
option.captchaToken = Config['2captchaToken']
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,12 @@ let helpData = [
|
||||||
{
|
{
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
title: '#chatgpt对话列表',
|
title: '#chatgpt对话列表',
|
||||||
desc: '查询当前哪些人正在与机器人聊天'
|
desc: '查询当前哪些人正在与机器人聊天.目前API3模式下支持切换对话'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'switch',
|
||||||
|
title: '#chatgpt切换对话+对话id',
|
||||||
|
desc: '目前仅API3模式下可用,切换到指定的对话中'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'destroy',
|
icon: 'destroy',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import plugin from '../../../lib/plugins/plugin.js'
|
import plugin from '../../../lib/plugins/plugin.js'
|
||||||
import {Config} from "../config/index.js";
|
import { Config } from '../config/index.js'
|
||||||
import { BingAIClient } from '@waylaidwanderer/chatgpt-api'
|
import { BingAIClient } from '@waylaidwanderer/chatgpt-api'
|
||||||
|
|
||||||
export class ChatgptManagement extends plugin {
|
export class ChatgptManagement extends plugin {
|
||||||
|
|
@ -99,7 +99,7 @@ export class ChatgptManagement extends plugin {
|
||||||
this.finish('saveToken')
|
this.finish('saveToken')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let cookie = undefined
|
let cookie
|
||||||
if (token?.indexOf('=') > -1) {
|
if (token?.indexOf('=') > -1) {
|
||||||
cookie = token
|
cookie = token
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ export class ChatgptManagement extends plugin {
|
||||||
// 异步就好了,不卡着这个context了
|
// 异步就好了,不卡着这个context了
|
||||||
bingAIClient.createNewConversation().then(async res => {
|
bingAIClient.createNewConversation().then(async res => {
|
||||||
if (res.clientId) {
|
if (res.clientId) {
|
||||||
logger.info("bing token 有效")
|
logger.info('bing token 有效')
|
||||||
} else {
|
} else {
|
||||||
logger.error('bing token 无效', res)
|
logger.error('bing token 无效', res)
|
||||||
await this.reply(`经检测,Bing Token无效。来自Bing的错误提示:${res.result?.message}`)
|
await this.reply(`经检测,Bing Token无效。来自Bing的错误提示:${res.result?.message}`)
|
||||||
|
|
@ -141,8 +141,13 @@ export class ChatgptManagement extends plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async useOpenAIAPIBasedSolution (e) {
|
async useOpenAIAPIBasedSolution (e) {
|
||||||
await redis.set('CHATGPT:USE', 'api')
|
let use = await redis.get('CHATGPT:USE')
|
||||||
await this.reply('已切换到基于OpenAI API的解决方案,如果已经对话过建议执行`#结束对话`避免引起404错误')
|
if (use !== 'api') {
|
||||||
|
await redis.set('CHATGPT:USE', 'api')
|
||||||
|
await this.reply('已切换到基于OpenAI API的解决方案,如果已经对话过建议执行`#结束对话`避免引起404错误')
|
||||||
|
} else {
|
||||||
|
await this.reply('当前已经是API模式了')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async useReversedAPIBasedSolution (e) {
|
async useReversedAPIBasedSolution (e) {
|
||||||
|
|
@ -151,13 +156,33 @@ export class ChatgptManagement extends plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async useReversedAPIBasedSolution2 (e) {
|
async useReversedAPIBasedSolution2 (e) {
|
||||||
await redis.set('CHATGPT:USE', 'api3')
|
let use = await redis.get('CHATGPT:USE')
|
||||||
await this.reply('已切换到基于第三方Reversed ConversastionAPI的解决方案,如果已经对话过建议执行`#结束对话`避免引起404错误')
|
if (use !== 'api3') {
|
||||||
|
await redis.set('CHATGPT:USE', 'api3')
|
||||||
|
await this.reply('已切换到基于第三方Reversed Conversastion API(API3)的解决方案')
|
||||||
|
} else {
|
||||||
|
await this.reply('当前已经是API3模式了')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async useReversedBingSolution (e) {
|
async useReversedBingSolution (e) {
|
||||||
await redis.set('CHATGPT:USE', 'bing')
|
let use = await redis.get('CHATGPT:USE')
|
||||||
await this.reply('已切换到基于微软新必应的解决方案,如果已经对话过务必执行`#结束对话`避免引起404错误')
|
if (use !== 'bing') {
|
||||||
|
await redis.set('CHATGPT:USE', 'bing')
|
||||||
|
// 结束所有人的对话
|
||||||
|
const keys = await redis.keys('CHATGPT:CONVERSATIONS:*')
|
||||||
|
if (keys.length) {
|
||||||
|
const response = await redis.del(keys)
|
||||||
|
if (Config.debug) {
|
||||||
|
console.log('Deleted keys:', response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No keys matched the pattern')
|
||||||
|
}
|
||||||
|
await this.reply('已切换到基于微软新必应的解决方案,如果已经对话过务必执行`#结束对话`避免引起404错误')
|
||||||
|
} else {
|
||||||
|
await this.reply('当前已经是必应Bing模式了')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async modeHelp () {
|
async modeHelp () {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ export const Config = {
|
||||||
blockWords: ['屏蔽词1', '屏蔽词b'],
|
blockWords: ['屏蔽词1', '屏蔽词b'],
|
||||||
// 改为true后,全局默认以图片形式回复,并自动发出Continue命令补全回答。长回复可能会有bug。
|
// 改为true后,全局默认以图片形式回复,并自动发出Continue命令补全回答。长回复可能会有bug。
|
||||||
defaultUsePicture: false,
|
defaultUsePicture: false,
|
||||||
|
// 如果true,字数大于阈值(autoUsePictureThreshold)会自动用图片发送,即使是文本模式。
|
||||||
|
autoUsePicture: true,
|
||||||
|
// 仅当autoUsePicture为true时生效,字数大于该阈值会自动用图片发送,即使是文本模式。
|
||||||
|
autoUsePictureThreshold: 1200,
|
||||||
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
|
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
|
||||||
conversationPreserveTime: 0,
|
conversationPreserveTime: 0,
|
||||||
// 触发方式 可选值:at 或 prefix 。at模式下只有at机器人才会回复。prefix模式下不需要at,但需要添加前缀#chat
|
// 触发方式 可选值:at 或 prefix 。at模式下只有at机器人才会回复。prefix模式下不需要at,但需要添加前缀#chat
|
||||||
|
|
@ -26,6 +30,7 @@ export const Config = {
|
||||||
// ***********************************************************************************************************************************
|
// ***********************************************************************************************************************************
|
||||||
// from https://github.com/acheong08/ChatGPT
|
// from https://github.com/acheong08/ChatGPT
|
||||||
api: 'https://chatgpt.duti.tech/api/conversation',
|
api: 'https://chatgpt.duti.tech/api/conversation',
|
||||||
|
apiBaseUrl: 'https://chatgpt.duti.tech',
|
||||||
// ***********************************************************************************************************************************
|
// ***********************************************************************************************************************************
|
||||||
// 以下为API2方式的配置 *
|
// 以下为API2方式的配置 *
|
||||||
// ***********************************************************************************************************************************
|
// ***********************************************************************************************************************************
|
||||||
|
|
@ -60,7 +65,7 @@ export const Config = {
|
||||||
'2captchaToken': '',
|
'2captchaToken': '',
|
||||||
// http或socks5代理
|
// http或socks5代理
|
||||||
proxy: PROXY,
|
proxy: PROXY,
|
||||||
debug: false,
|
debug: true,
|
||||||
// 各个地方的默认超时时间
|
// 各个地方的默认超时时间
|
||||||
defaultTimeoutMs: 120000,
|
defaultTimeoutMs: 120000,
|
||||||
// bing默认超时时间,bing太慢了有的时候
|
// bing默认超时时间,bing太慢了有的时候
|
||||||
|
|
|
||||||
36
resources/conversation/chatgpt.html
Normal file
36
resources/conversation/chatgpt.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{pluResPath}}conversation/conversation.css" />
|
||||||
|
<link rel="shortcut icon" href="#" />
|
||||||
|
</head>
|
||||||
|
{{@headStyle}}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container" id="container">
|
||||||
|
<div class="head_box">
|
||||||
|
<div class="id_text">ChatGPT-Plugin</div>
|
||||||
|
<h2 class="day_text">最近对话列表</h2>
|
||||||
|
<img class="chatgpt_logo" src="{{pluResPath}}img/icon/chatgpt.png"/>
|
||||||
|
</div>
|
||||||
|
<div class="data_box">
|
||||||
|
<div class="list">
|
||||||
|
{{each conversations item}}
|
||||||
|
<div class="item-{{item.status}}">
|
||||||
|
<img class="icon" src="{{pluResPath}}img/icon/chat.png" />
|
||||||
|
<div class="title">
|
||||||
|
<div class="text">{{item.id}}</div>
|
||||||
|
<div class="dec">最近问题:{{item.lastPrompt}}</div>
|
||||||
|
<div class="creater">发起者:{{item.creater}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logo">Created By Yunzai-Bot and ChatGPT-Plugin</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
178
resources/conversation/conversation.css
Normal file
178
resources/conversation/conversation.css
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "tttgbnumber";
|
||||||
|
src: url("../../../../../resources/font/tttgbnumber.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 630px;
|
||||||
|
color: #1e1f20;
|
||||||
|
transform: scale(1.5);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 630px;
|
||||||
|
padding: 20px 15px 10px 15px;
|
||||||
|
background-color: #f5f6fb;
|
||||||
|
}
|
||||||
|
.head_box {
|
||||||
|
border-radius: 15px;
|
||||||
|
font-family: tttgbnumber;
|
||||||
|
padding: 10px 20px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||||
|
}
|
||||||
|
.head_box .id_text {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.head_box .day_text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.head_box .chatgpt_logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 15px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
.base_info {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.uid {
|
||||||
|
font-family: tttgbnumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data_box {
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 20px 0px 5px 0px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tab_lable {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -8px;
|
||||||
|
background: #d4b98c;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 15px 0px 15px 15px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.data_line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.data_line_item {
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
/*margin: 0 20px;*/
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
font-family: tttgbnumber;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.data_box .lable {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f858a;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .item-normal {
|
||||||
|
width: 575px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f1f1f1;
|
||||||
|
padding: 8px 6px 8px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 0px 10px 10px;
|
||||||
|
}
|
||||||
|
.list .item-normal .icon{
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.list .item-normal .title{
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 6px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.list .item-normal .title .text{
|
||||||
|
color: #1995A4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.list .item-normal .title .creater{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #69878B;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.list .item-using .title .dec{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.list .item-using {
|
||||||
|
width: 575px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #157985;
|
||||||
|
padding: 8px 6px 8px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 0px 10px 10px;
|
||||||
|
}
|
||||||
|
.list .item-using .icon{
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.list .item-using .title{
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 6px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.list .item-using .title .text{
|
||||||
|
color: #CBD4D5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.list .item-using .title .dec{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #CBD4D5;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.list .item-using .title .creater{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #CBD4D5;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "tttgbnumber";
|
||||||
|
text-align: center;
|
||||||
|
color: #7994a7;
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,6 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
||||||
import { getOpenAIAuth } from './openai-auth.js'
|
import { getOpenAIAuth } from './openai-auth.js'
|
||||||
import delay from 'delay'
|
import delay from 'delay'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { pTimeout } from './common.js'
|
|
||||||
console.log({ pTimeout })
|
|
||||||
const chatUrl = 'https://chat.openai.com/chat'
|
const chatUrl = 'https://chat.openai.com/chat'
|
||||||
let puppeteer = {}
|
let puppeteer = {}
|
||||||
|
|
||||||
|
|
|
||||||
117
utils/conversation.js
Normal file
117
utils/conversation.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { Config } from '../config/index.js'
|
||||||
|
|
||||||
|
export async function getConversations (qq = '') {
|
||||||
|
let accessToken = await redis.get('CHATGPT:TOKEN')
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
||||||
|
}
|
||||||
|
let response = await fetch(`${Config.apiBaseUrl}/api/conversations?offset=0&limit=20`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + accessToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let json = await response.text()
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.mark(json)
|
||||||
|
}
|
||||||
|
let conversations = JSON.parse(json)
|
||||||
|
let result = conversations.items?.sort((a, b) => b.create_time - a.create_time)
|
||||||
|
let map = {}
|
||||||
|
for (let i = 0; i < conversations.items.length; i++) {
|
||||||
|
// 老用户初次更新该功能,这里频繁请求可能会429。由并行改为串行以尽量降低频率。必要时可可能还要等待。
|
||||||
|
let item = conversations.items[i]
|
||||||
|
let cachedConversationLastMessage = await redis.get(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${item.id}`)
|
||||||
|
if (!cachedConversationLastMessage) {
|
||||||
|
map[item.id] = cachedConversationLastMessage
|
||||||
|
} else {
|
||||||
|
// 缓存中没有,就去查官方api
|
||||||
|
let conversationDetailResponse = await fetch(`${Config.apiBaseUrl}/api/conversation/${item.id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + accessToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let conversationDetail = await conversationDetailResponse.text()
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.mark('conversation detail for conversation ' + item.id, conversationDetail)
|
||||||
|
}
|
||||||
|
conversationDetail = JSON.parse(conversationDetail)
|
||||||
|
let messages = Object.values(conversationDetail.mapping)
|
||||||
|
|
||||||
|
messages = messages
|
||||||
|
.filter(message => message.message)
|
||||||
|
.map(messages => messages.message)
|
||||||
|
|
||||||
|
let messagesAssistant = messages.filter(messages => messages.role === 'assistant')
|
||||||
|
.sort((a, b) => b.create_time - a.create_time)
|
||||||
|
let messagesUser = messages.filter(messages => messages.role === 'user')
|
||||||
|
.sort((a, b) => b.create_time - a.create_time)
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LENGTH:${item.id}`, messagesUser?.length || 0)
|
||||||
|
let lastMessage = null
|
||||||
|
if (messagesUser.length > 0) {
|
||||||
|
lastMessage = messagesUser[0].content.parts[0]
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${item.id}`, lastMessage)
|
||||||
|
}
|
||||||
|
if (messagesAssistant.length > 0) {
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${item.id}`, messagesAssistant[0].id)
|
||||||
|
}
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_CREATE_TIME:${item.id}`, new Date(conversationDetail.create_time * 1000).toLocaleString())
|
||||||
|
map[item.id] = lastMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let res = []
|
||||||
|
let usingConversationId
|
||||||
|
if (qq) {
|
||||||
|
usingConversationId = await redis.get(`CHATGPT:QQ_CONVERSATION:${qq}`)
|
||||||
|
}
|
||||||
|
let promisesPostProcess = result.map(async conversation => {
|
||||||
|
conversation.lastPrompt = map[conversation.id]
|
||||||
|
conversation.create_time = new Date(conversation.create_time).toLocaleString()
|
||||||
|
// 这里的时间格式还可以。不用管了。conversation.create_time =
|
||||||
|
// title 全是 New chat,不要了
|
||||||
|
delete conversation.title
|
||||||
|
conversation.creater = await redis.get(`CHATGPT:CONVERSATION_CREATER_NICK_NAME:${conversation.id}`)
|
||||||
|
if (qq && conversation.id === usingConversationId) {
|
||||||
|
conversation.status = 'using'
|
||||||
|
} else {
|
||||||
|
conversation.status = 'normal'
|
||||||
|
}
|
||||||
|
if (conversation.lastPrompt?.length > 80) {
|
||||||
|
conversation.lastPrompt = conversation.lastPrompt.slice(0, 80) + '......'
|
||||||
|
}
|
||||||
|
res.push(conversation)
|
||||||
|
})
|
||||||
|
await Promise.all(promisesPostProcess)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestMessageIdByConversationId (conversationId) {
|
||||||
|
let accessToken = await redis.get('CHATGPT:TOKEN')
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token')
|
||||||
|
}
|
||||||
|
let conversationDetailResponse = await fetch(`${Config.apiBaseUrl}/api/conversation/${conversationId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + accessToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let conversationDetail = await conversationDetailResponse.text()
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.mark('conversation detail for conversation ' + conversationId, conversationDetail)
|
||||||
|
}
|
||||||
|
conversationDetail = JSON.parse(conversationDetail)
|
||||||
|
let messages = Object.values(conversationDetail.mapping)
|
||||||
|
messages = messages
|
||||||
|
.filter(message => message.message)
|
||||||
|
.map(messages => messages.message)
|
||||||
|
.filter(messages => messages.role === 'assistant')
|
||||||
|
.sort((a, b) => b.create_time - a.create_time)
|
||||||
|
await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${conversationId}`, messages[0].id)
|
||||||
|
return messages[0].id
|
||||||
|
}
|
||||||
0
utils/redis-key.js
Normal file
0
utils/redis-key.js
Normal file
Loading…
Add table
Add a link
Reference in a new issue