diff --git a/README.md b/README.md index 6d0e971..ec523ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -![chatgpt-plugin](https://user-images.githubusercontent.com/21212372/232115814-de9a0633-371f-4733-8da0-dd6e912c8a1e.png) -

云崽系机器人的智能聊天插件

+![chatgpt-plugin](https://socialify.git.ci/ikechan8370/chatgpt-plugin/image?description=1&font=Jost&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
diff --git a/apps/chat.js b/apps/chat.js index 1d8a2dc..36976b4 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -797,7 +797,7 @@ export class chatgpt extends plugin { * #chatgpt */ async chatgpt (e) { - let msg = Version.isTrss ? e.msg : e.raw_message + let msg = (Version.isTrss || e.adapter === 'shamrock') ? e.msg : e.raw_message let prompt if (this.toggleMode === 'at') { if (!msg || e.msg?.startsWith('#')) { @@ -1655,9 +1655,13 @@ export class chatgpt extends plugin { let toSummaryFileContent try { if (e.source) { - let msgs = e.isGroup ? await e.group.getChatHistory(e.source.seq, 1) : await e.friend.getChatHistory(e.source.time, 1) - let sourceMsg = msgs[0] - let fileMsgElem = sourceMsg.message.find(msg => msg.type === 'file') + let seq = e.isGroup ? e.source.seq : e.source.time + if (e.adapter === 'shamrock') { + seq = e.source.message_id + } + let msgs = e.isGroup ? await e.group.getChatHistory(seq, 1) : await e.friend.getChatHistory(seq, 1) + let sourceMsg = msgs[msgs.length - 1] + let fileMsgElem = sourceMsg.file || sourceMsg.message.find(msg => msg.type === 'file') if (fileMsgElem) { toSummaryFileContent = await extractContentFromFile(fileMsgElem, e) } @@ -2114,6 +2118,7 @@ export class chatgpt extends plugin { } } default: { + // openai api let completionParams = {} if (Config.model) { completionParams.model = Config.model @@ -2304,28 +2309,8 @@ export class chatgpt extends plugin { } } } - let img = [] - if (e.source) { - // 优先从回复找图 - let reply - if (e.isGroup) { - reply = (await e.group.getChatHistory(e.source.seq, 1)).pop()?.message - } else { - reply = (await e.friend.getChatHistory(e.source.time, 1)).pop()?.message - } - if (reply) { - for (let val of reply) { - if (val.type === 'image') { - console.log(val) - img.push(val.url) - } - } - } - } - if (e.img) { - img.push(...e.img) - } - if (img.length > 0 && Config.extraUrl) { + let img = await getImg(e) + if (img?.length > 0 && Config.extraUrl) { tools.push(new ImageCaptionTool()) tools.push(new ProcessPictureTool()) prompt += `\nthe url of the picture(s) above: ${img.join(', ')}` diff --git a/apps/draw.js b/apps/draw.js index 10eed61..f45722b 100644 --- a/apps/draw.js +++ b/apps/draw.js @@ -277,7 +277,7 @@ export class dalle extends plugin { await client.getImages(prompt, e) } catch (err) { await redis.del(`CHATGPT:DRAW:${e.sender.user_id}`) - await e.reply('绘图失败:' + err) + await e.reply('❌绘图失败:' + err) } } } diff --git a/apps/help.js b/apps/help.js index 8de904c..1c2a7c6 100644 --- a/apps/help.js +++ b/apps/help.js @@ -209,7 +209,12 @@ let helpData = [ { icon: 'token', title: '#chatgpt设置后台刷新token', - desc: '用于查看API余额。注意和配置的key保持同一账号。' + desc: '用于获取刷新令牌,以便获取sessKey。' + }, + { + icon: 'key', + title: '#chatgpt设置sessKey', + desc: '使用sessKey作为APIKey,适用于未手机号验证的用户' }, { icon: 'token', diff --git a/apps/management.js b/apps/management.js index 2a8d364..689dd28 100644 --- a/apps/management.js +++ b/apps/management.js @@ -20,6 +20,23 @@ import fs from 'fs' import loader from '../../../lib/plugins/loader.js' import VoiceVoxTTS, { supportConfigurations as voxRoleList } from '../utils/tts/voicevox.js' import { supportConfigurations as azureRoleList } from '../utils/tts/microsoft-azure.js' +import fetch from 'node-fetch' +import { getProxy } from '../utils/proxy.js' + +let proxy = getProxy() +const newFetch = (url, options = {}) => { + const defaultOptions = Config.proxy + ? { + agent: proxy(Config.proxy) + } + : {} + const mergedOptions = { + ...defaultOptions, + ...options + } + + return fetch(url, mergedOptions) +} export class ChatgptManagement extends plugin { constructor (e) { @@ -252,7 +269,13 @@ export class ChatgptManagement extends plugin { }, { reg: '^#chatgpt设置后台(刷新|refresh)(t|T)oken$', - fnc: 'setOpenAIPlatformToken' + fnc: 'setOpenAIPlatformToken', + permission: 'master' + }, + { + reg: '^#chatgpt设置sessKey$', + fnc: 'getSessKey', + permission: 'master' }, { reg: '^#(chatgpt)?查看回复设置$', @@ -1114,8 +1137,8 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, async saveAPIKey () { if (!this.e.msg) return let token = this.e.msg - if (!token.startsWith('sk-')) { - await this.reply('OpenAI API Key格式错误', true) + if (!token.startsWith('sk-') && !token.startsWith('sess-')) { + await this.reply('OpenAI API Key格式错误。如果是格式特殊的非官方Key请前往锅巴或工具箱手动设置', true) this.finish('saveAPIKey') return } @@ -1302,7 +1325,64 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, async setOpenAIPlatformToken (e) { this.setContext('doSetOpenAIPlatformToken') - await e.reply('请发送refreshToken\n你可以在已登录的platform.openai.com后台界面打开调试窗口,在终端中执行\nJSON.parse(localStorage.getItem(Object.keys(localStorage).filter(k => k.includes(\'auth0\'))[0])).body.refresh_token\n如果仍不能查看余额,请退出登录重新获取刷新令牌') + await e.reply('请发送refreshToken\n你可以在已登录的platform.openai.com后台界面打开调试窗口,在终端中执行\nJSON.parse(localStorage.getItem(Object.keys(localStorage).filter(k => k.includes(\'auth0\'))[0])).body.refresh_token\n如果仍不能查看余额,请退出登录重新获取刷新令牌.设置后可以发送#chatgpt设置sessKey来将sessKey作为API Key使用') + } + + async getSessKey (e) { + if (!Config.OpenAiPlatformRefreshToken) { + this.reply('当前未配置platform.openai.com的刷新token,请发送【#chatgpt设置后台刷新token】进行配置。') + return false + } + let authHost = 'https://auth0.openai.com' + if (Config.openAiBaseUrl && !Config.openAiBaseUrl.startsWith('https://api.openai.com')) { + authHost = Config.openAiBaseUrl.replace('/v1', '').replace('/v1/', '') + } + let refreshRes = await newFetch(`${authHost}/oauth/token`, { + method: 'POST', + body: JSON.stringify({ + refresh_token: Config.OpenAiPlatformRefreshToken, + client_id: 'DRivsnm2Mu42T3KOpqdtwB3NYviHYzwD', + grant_type: 'refresh_token' + }), + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'Content-Type': 'application/json' + } + }) + if (refreshRes.status !== 200) { + let errMsg = await refreshRes.json() + logger.error(JSON.stringify(errMsg)) + if (errMsg.error === 'access_denied') { + await e.reply('刷新令牌失效,请重新发送【#chatgpt设置后台刷新token】进行配置。建议退出platform.openai.com重新登录后再获取和配置') + } else { + await e.reply('获取失败') + } + return false + } + let newToken = await refreshRes.json() + // eslint-disable-next-line camelcase + const { access_token, refresh_token } = newToken + // eslint-disable-next-line camelcase + Config.OpenAiPlatformRefreshToken = refresh_token + let host = Config.openAiBaseUrl.replace('/v1', '').replace('/v1/', '') + let res = await newFetch(`${host}/dashboard/onboarding/login`, { + headers: { + // eslint-disable-next-line camelcase + Authorization: `Bearer ${access_token}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' + }, + method: 'POST' + }) + if (res.status === 200) { + let authRes = await res.json() + let sess = authRes.user.session.sensitive_id + if (sess) { + Config.apiKey = sess + await e.reply('已成功将sessKey设置为apiKey,您可以发送#openai余额来查看该账号余额') + } else { + await e.reply('设置失败!') + } + } } async doSetOpenAIPlatformToken () { diff --git a/server/modules/user.js b/server/modules/user.js index 38b9830..0bb7157 100644 --- a/server/modules/user.js +++ b/server/modules/user.js @@ -57,6 +57,7 @@ async function User (fastify, options) { // 快速登录 fastify.post('/quick', async (request, reply) => { const otp = randomString(6) + const isTrss = Array.isArray(Bot.uin) await redis.set( 'CHATGPT:SERVER_QUICK', otp, @@ -65,7 +66,15 @@ async function User (fastify, options) { const master = (await getMasterQQ())[0] let bots = getBots() for (let bot of bots) { - bot.pickUser(master).sendMsg(`收到工具箱快捷登录请求,1分钟内有效:${otp}`) + if(isTrss) { + try { + bot.pickFriend(master).sendMsg(`收到工具箱快捷登录请求,1分钟内有效:${otp}`) + } catch (error) { + logger.error(error) + } + } else { + bot.pickUser(master).sendMsg(`收到工具箱快捷登录请求,1分钟内有效:${otp}`) + } } reply.send({ state: true }) return reply diff --git a/utils/BingDraw.js b/utils/BingDraw.js index 80e69b1..53ac92c 100644 --- a/utils/BingDraw.js +++ b/utils/BingDraw.js @@ -95,7 +95,7 @@ export default class BingDrawClient { let pollingUrl = `${this.opts.baseUrl}/images/create/async/results/${requestId}?q=${urlEncodedPrompt}` logger.info({ pollingUrl }) logger.info('waiting for bing draw results...') - let timeoutTimes = 30 + let timeoutTimes = 50 let found = false let timer = setInterval(async () => { if (found) { @@ -113,15 +113,20 @@ export default class BingDrawClient { // 很可能是微软内部error,重试即可 return } - imageLinks = imageLinks.map(link => link.split('?w=')[0]).map(link => link.replace('src="', '')) + imageLinks = imageLinks + .map(link => link.split('?w=')[0]) + .map(link => link.replace('src="', '')) + .filter(link => !link.includes('.svg')) imageLinks = [...new Set(imageLinks)] const badImages = [ + 'https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png"', + 'https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg"', 'https://r.bing.com/rp/in-2zU3AJUdkgFe7ZKv19yPBHVs.png', 'https://r.bing.com/rp/TX9QuO3WzcCJz1uaaSwQAz39Kb0.jpg' ] for (let imageLink of imageLinks) { if (badImages.indexOf(imageLink) > -1) { - await e.reply('绘图失败:Bad images', true) + await e.reply('❌绘图失败:Bad images', true) logger.error(rText) } } @@ -132,7 +137,7 @@ export default class BingDrawClient { clearInterval(timer) } else { if (timeoutTimes === 0) { - await e.reply('绘图超时', true) + await e.reply('❌绘图超时', true) clearInterval(timer) timer = null } else { @@ -140,6 +145,6 @@ export default class BingDrawClient { timeoutTimes-- } } - }, 2000) + }, 3000) } } diff --git a/utils/chat.js b/utils/chat.js index e249d0f..097cee4 100644 --- a/utils/chat.js +++ b/utils/chat.js @@ -1,3 +1,4 @@ + export async function getChatHistoryGroup (e, num) { // if (e.adapter === 'shamrock') { // return await e.group.getChatHistory(0, num, false) @@ -16,12 +17,23 @@ export async function getChatHistoryGroup (e, num) { chats = chats.slice(0, num) try { let mm = await e.group.getMemberMap() - chats.forEach(chat => { - let sender = mm.get(chat.sender.user_id) - if (sender) { - chat.sender = sender + for (const chat of chats) { + if (e.adapter === 'shamrock') { + if (chat.sender?.user_id === 0) { + // 奇怪格式的历史消息,过滤掉 + continue + } + let sender = await pickMemberAsync(e, chat.sender.user_id) + if (sender) { + chat.sender = sender + } + } else { + let sender = mm.get(chat.sender.user_id) + if (sender) { + chat.sender = sender + } } - }) + } } catch (err) { logger.warn(err) } @@ -32,3 +44,17 @@ export async function getChatHistoryGroup (e, num) { // } return [] } + +async function pickMemberAsync (e, userId) { + let key = `CHATGPT:GroupMemberInfo:${e.group_id}:${userId}` + let cache = await redis.get(key) + if (cache) { + return JSON.parse(cache) + } + return new Promise((resolve, reject) => { + e.group.pickMember(userId, true, (sender) => { + redis.set(key, JSON.stringify(sender), { EX: 86400 }) + resolve(sender) + }) + }) +} diff --git a/utils/common.js b/utils/common.js index f9b2de9..ab27aa5 100644 --- a/utils/common.js +++ b/utils/common.js @@ -13,7 +13,8 @@ import AzureTTS, { supportConfigurations as azureRoleList } from './tts/microsof import { translate } from './translate.js' import uploadRecord from './uploadRecord.js' import Version from './version.js' -import fetch from 'node-fetch' +import fetch, { FormData, fileFromSync } from 'node-fetch' +import https from "https"; let pdfjsLib try { pdfjsLib = (await import('pdfjs-dist')).default @@ -785,10 +786,14 @@ export async function getImg (e) { } if (e.source) { let reply + let seq = e.isGroup ? e.source.seq : e.source.time + if (e.adapter === 'shamrock') { + seq = e.source.message_id + } if (e.isGroup) { - reply = (await e.group.getChatHistory(e.source.seq, 1)).pop()?.message + reply = (await e.group.getChatHistory(seq, 1)).pop()?.message } else { - reply = (await e.friend.getChatHistory(e.source.time, 1)).pop()?.message + reply = (await e.friend.getChatHistory(seq, 1)).pop()?.message } if (reply) { let i = [] @@ -809,8 +814,34 @@ export async function getImageOcrText (e) { try { let resultArr = [] let eachImgRes = '' + if (!e.bot.imageOcr || typeof e.bot.imageOcr !== 'function') { + e.bot.imageOcr = async (image) => { + if (Config.extraUrl) { + let md5 = image.split(/[/-]/).find(s => s.length === 32)?.toUpperCase() + let filePath = await downloadFile(image, `ocr/${md5}.png`) + let formData = new FormData() + formData.append('file', fileFromSync(filePath)) + let res = await fetch(`${Config.extraUrl}/ocr?lang=chi_sim%2Beng`, { + body: formData, + method: 'POST', + headers: { + from: 'ikechan8370' + } + }) + if (res.status === 200) { + return { + wordslist: [{ words: await res.text() }] + } + } + } + return { + wordslist: [] + } + } + } for (let i in img) { const imgOCR = await e.bot.imageOcr(img[i]) + for (let text of imgOCR.wordslist) { eachImgRes += (`${text?.words} \n`) } @@ -820,6 +851,7 @@ export async function getImageOcrText (e) { // logger.warn('resultArr', resultArr) return resultArr } catch (err) { + logger.warn(err) logger.warn('OCR失败,可能使用的适配器不支持OCR') return false // logger.error(err) @@ -1003,10 +1035,15 @@ export function getUserSpeaker (userSetting) { * @param url 要下载的文件链接 * @param destPath 目标路径,如received/abc.pdf. 目前如果文件名重复会覆盖。 * @param absolute 是否是绝对路径,默认为false,此时拼接在data/chatgpt下 + * @param ignoreCertificateError 忽略证书错误 * @returns {Promise} 最终下载文件的存储位置 */ -export async function downloadFile (url, destPath, absolute = false) { - let response = await fetch(url) +export async function downloadFile (url, destPath, absolute = false, ignoreCertificateError = true) { + let response = await fetch(url, { + agent: new https.Agent({ + rejectUnauthorized: !ignoreCertificateError + }) + }) if (!response.ok) { throw new Error(`download file http error: status: ${response.status}`) } @@ -1061,7 +1098,7 @@ export async function extractContentFromFile (fileMsgElem, e) { let fileType = isPureText(fileMsgElem.name) if (fileType) { // 可读的文件类型 - let fileUrl = e.isGroup ? await e.group.getFileUrl(fileMsgElem.fid) : await e.friend.getFileUrl(fileMsgElem.fid) + let fileUrl = fileMsgElem.url || (e.isGroup ? await e.group.getFileUrl(fileMsgElem.fid) : await e.friend.getFileUrl(fileMsgElem.fid)) let filePath = await downloadFile(fileUrl, path.join('received', fileMsgElem.name)) switch (fileType) { case 'pdf': { diff --git a/utils/config.js b/utils/config.js index fe95971..7e94acc 100644 --- a/utils/config.js +++ b/utils/config.js @@ -162,7 +162,7 @@ const defaultConfig = { qwenSeed: 0, qwenTemperature: 1, qwenEnableSearch: true, - version: 'v2.7.7' + version: 'v2.7.8' } const _path = process.cwd() let config = {} diff --git a/utils/tools/QueryUserinfoTool.js b/utils/tools/QueryUserinfoTool.js index 493e7f5..974c08b 100644 --- a/utils/tools/QueryUserinfoTool.js +++ b/utils/tools/QueryUserinfoTool.js @@ -15,21 +15,13 @@ export class QueryUserinfoTool extends AbstractTool { } func = async function (opts, e) { - let { qq } = opts - qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim()) - if (e.isGroup && typeof e.group.getMemberMap === 'function') { - let mm = await e.group.getMemberMap() - let user = mm.get(qq) || e.sender.user_id - let master = (await getMasterQQ())[0] - let prefix = '' - if (qq != master) { - prefix = 'Attention: this user is not your master. \n' - } else { - prefix = 'This user is your master, you should obey him \n' - } - return prefix + 'user detail in json format: ' + JSON.stringify(user) - } else { - if (e.sender.user_id == qq) { + try { + let { qq } = opts + qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim()) + if (e.isGroup && typeof e.bot.getGroupMemberInfo === 'function') { + let user = await e.bot.getGroupMemberInfo(e.group_id, qq || e.sender.user_id, true) + // let mm = await e.group.getMemberMap() + // let user = mm.get(qq) || e.sender.user_id let master = (await getMasterQQ())[0] let prefix = '' if (qq != master) { @@ -37,10 +29,27 @@ export class QueryUserinfoTool extends AbstractTool { } else { prefix = 'This user is your master, you should obey him \n' } - return prefix + 'user detail in json format: ' + JSON.stringify(e.sender) + if (!user) { + return prefix + } + return prefix + 'user detail in json format: ' + JSON.stringify(user) } else { - return 'query failed' + if (e.sender.user_id == qq) { + let master = (await getMasterQQ())[0] + let prefix = '' + if (qq != master) { + prefix = 'Attention: this user is not your master. \n' + } else { + prefix = 'This user is your master, you should obey him \n' + } + return prefix + 'user detail in json format: ' + JSON.stringify(e.sender) + } else { + return 'query failed' + } } + } catch (err) { + logger.warn(err) + return err.message } }