diff --git a/apps/management.js b/apps/management.js index c89b901..65cb76e 100644 --- a/apps/management.js +++ b/apps/management.js @@ -1258,7 +1258,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, async setClaudeKey (e) { this.setContext('saveClaudeKey') - await this.reply('请发送Claude API Key', true) + await this.reply('请发送Claude API Key。\n如果要设置多个key请用逗号隔开。\n此操作会覆盖当前配置,请谨慎操作', true) return false } @@ -1697,7 +1697,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, async setClaudeModel (e) { this.setContext('saveClaudeModel') - await this.reply('请发送Claude模型,官方推荐模型:\nclaude-3-opus-20240229\nclaude-3-sonnet-20240229', true) + await this.reply('请发送Claude模型,官方推荐模型:\nclaude-3-opus-20240229\nclaude-3-sonnet-20240229\nclaude-3-haiku-20240307', true) return false } diff --git a/apps/prompts.js b/apps/prompts.js index b360db7..481d2ad 100644 --- a/apps/prompts.js +++ b/apps/prompts.js @@ -151,6 +151,20 @@ export class help extends plugin { if (use === 'xh') { Config.xhPromptSerialize = false } + if (use === 'bing') { + /** + * @type {{user: string, bot: string}[]} examples + */ + let examples = prompt.example + for (let i = 1; i <= 3; i++) { + Config[`chatExampleUser${i}`] = '' + Config[`chatExampleBot${i}`] = '' + } + for (let i = 1; i <= examples.length; i++) { + Config[`chatExampleUser${i}`] = examples[i - 1].user + Config[`chatExampleBot${i}`] = examples[i - 1].bot + } + } await redis.set(`CHATGPT:PROMPT_USE_${use}`, promptName) await e.reply(`你当前正在使用${use}模式,已将该模式设定应用为"${promptName}"。更该设定后建议结束对话以使设定更好生效`, true) } else { @@ -330,13 +344,23 @@ export class help extends plugin { let extraData = JSON.parse(await redis.get('CHATGPT:UPLOAD_PROMPT')) const { currentUse, description } = extraData const { content } = getPromptByName(currentUse) + let examples = [] + for (let i = 1; i < 4; i++) { + if (Config[`chatExampleUser${i}`]) { + examples.push({ + user: Config[`chatExampleUser${i}`], + bot: Config[`chatExampleBot${i}`] + }) + } + } let toUploadBody = { title: currentUse, prompt: content, qq: master || (getUin(this.e) + ''), // 上传者设定为主人qq或机器人qq use: extraData.use === 'bing' ? 'Bing' : 'ChatGPT', r18, - description + description, + examples } logger.info(toUploadBody) let response = await fetch('https://chatgpt.roki.best/prompt', { @@ -431,8 +455,8 @@ export class help extends plugin { await e.reply('没有这个设定', true) return true } - const { prompt, title } = r.data - saveOnePrompt(title, prompt) + const { prompt, title, examples } = r.data + saveOnePrompt(title, prompt, examples) e.reply(`导入成功。您现在可以使用 #chatgpt使用设定${title} 来体验这个设定了。`) } else { await e.reply('导入失败:' + r.msg) diff --git a/client/ClaudeAPIClient.js b/client/ClaudeAPIClient.js index 0752e8d..15072b9 100644 --- a/client/ClaudeAPIClient.js +++ b/client/ClaudeAPIClient.js @@ -46,7 +46,10 @@ const BASEURL = 'https://api.anthropic.com' * input_tokens: number, * output_tokens: number, * }>} usage - * + * @property {{ + * type: string, + * message: string, + * }} error * Claude响应的基本格式 */ @@ -172,6 +175,10 @@ export class ClaudeAPIClient extends BaseClient { if (this.debug) { console.log(JSON.stringify(response)) } + if (response.type === 'error') { + logger.error(response.error.message) + throw new Error(response.error.type) + } await this.upsertMessage(thisMessage) const respMessage = Object.assign(response, { id: idModel, diff --git a/guoba.support.js b/guoba.support.js index b0b0a0f..019d665 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -301,6 +301,42 @@ export function supportGuoba () { bottomHelpMessage: '开启Sydney的图片识别功能,建议和OCR只保留一个开启', component: 'Switch' }, + { + field: 'chatExampleUser1', + label: '前置对话第一轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot1', + label: '前置对话第一轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleUser2', + label: '前置对话第二轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot2', + label: '前置对话第二轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleUser3', + label: '前置对话第三轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot3', + label: '前置对话第三轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, { label: '以下为API3方式的配置', component: 'Divider' @@ -346,7 +382,7 @@ export function supportGuoba () { { field: 'claudeApiKey', label: 'claude API Key', - bottomHelpMessage: '前往 https://console.anthropic.com/settings/keys 注册和生成', + bottomHelpMessage: '前往 https://console.anthropic.com/settings/keys 注册和生成。可以填写多个,用英文逗号隔开', component: 'InputPassword' }, { diff --git a/model/core.js b/model/core.js index 7d5ff91..235c035 100644 --- a/model/core.js +++ b/model/core.js @@ -424,20 +424,52 @@ class Core { return await this.chatGPTApi.sendMessage(prompt, conversation) } else if (use === 'claude') { // slack已经不可用,移除 - const client = new ClaudeAPIClient({ - key: Config.claudeApiKey, - model: Config.claudeApiModel || 'claude-3-sonnet-20240229', - debug: true, - baseUrl: Config.claudeApiBaseUrl - // temperature: Config.claudeApiTemperature || 0.5 - }) - let rsp = await client.sendMessage(prompt, { - stream: false, - parentMessageId: conversation.parentMessageId, - conversationId: conversation.conversationId, - system: Config.claudeSystemPrompt - }) - return rsp + let keys = Config.claudeApiKey?.split(/[,;]/).map(key => key.trim()).filter(key => key) + let choiceIndex = Math.floor(Math.random() * keys.length) + let key = keys[choiceIndex] + logger.info(`使用API Key:${key}`) + while (keys.length >= 0) { + let errorMessage = '' + const client = new ClaudeAPIClient({ + key, + model: Config.claudeApiModel || 'claude-3-sonnet-20240229', + debug: true, + baseUrl: Config.claudeApiBaseUrl + // temperature: Config.claudeApiTemperature || 0.5 + }) + try { + let rsp = await client.sendMessage(prompt, { + stream: false, + parentMessageId: conversation.parentMessageId, + conversationId: conversation.conversationId, + system: Config.claudeSystemPrompt + }) + return rsp + } catch (err) { + errorMessage = err.message + switch (err.message) { + case 'rate_limit_error': { + // api没钱了或者当月/日/时/分额度耗尽 + // throw new Error('claude API额度耗尽或触发速率限制') + break + } + case 'authentication_error': { + // 无效的key + // throw new Error('claude API key无效') + break + } + default: + } + logger.warn(`claude api 错误:[${key}] ${errorMessage}`) + } + if (keys.length === 0) { + throw new Error(errorMessage) + } + keys.splice(choiceIndex, 1) + choiceIndex = Math.floor(Math.random() * keys.length) + key = keys[choiceIndex] + logger.info(`使用API Key:${key}`) + } } else if (use === 'claude2') { let { conversationId } = conversation let client = new ClaudeAIClient({ diff --git a/utils/SydneyAIClient.js b/utils/SydneyAIClient.js index 7b1825d..7d3334b 100644 --- a/utils/SydneyAIClient.js +++ b/utils/SydneyAIClient.js @@ -81,7 +81,7 @@ export default class SydneyAIClient { 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', // cookie: this.opts.cookies || `_U=${this.opts.userToken}`, Referer: 'https://edgeservices.bing.com/edgesvc/chat?udsframed=1&form=SHORUN&clientscopes=chat,noheader,channelstable,', - 'Referrer-Policy': 'origin-when-cross-origin', + 'Referrer-Policy': 'origin-when-cross-origin' // Workaround for request being blocked due to geolocation // 'x-forwarded-for': '1.1.1.1' } @@ -94,9 +94,11 @@ export default class SydneyAIClient { } else { fetchOptions.headers.cookie = this.opts.cookies } - let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + this.opts.userToken) + // let hash = md5(this.opts.cookies || this.opts.userToken) + let hash = crypto.createHash('md5').update(this.opts.cookies || this.opts.userToken).digest('hex') + let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + hash) if (!proTag) { - let indexContentRes = await fetch('https://www.bing.com', { + let indexContentRes = await fetch('https://www.bing.com/chat', { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', Cookie: `_U=${this.opts.userToken}` @@ -108,7 +110,7 @@ export default class SydneyAIClient { } else { proTag = 'false' } - await redis.set('CHATGPT:COPILOT_PRO_TAG:' + this.opts.userToken, proTag, { EX: 7200 }) + await redis.set('CHATGPT:COPILOT_PRO_TAG:' + hash, proTag, { EX: 7200 }) } if (proTag === 'true') { logger.info('当前账户为copilot pro用户') @@ -338,6 +340,21 @@ export default class SydneyAIClient { if (!text) { previousMessages = pm } else { + let example = [] + for (let i = 1; i < 4; i++) { + if (Config[`chatExampleUser${i}`]) { + example.push(...[ + { + text: Config[`chatExampleUser${i}`], + author: 'user' + }, + { + text: Config[`chatExampleBot${i}`], + author: 'bot' + } + ]) + } + } previousMessages = [ { text, @@ -347,6 +364,7 @@ export default class SydneyAIClient { text: '好的。', author: 'bot' }, + ...example, ...pm ] } diff --git a/utils/config.js b/utils/config.js index 9a7b712..d8319c2 100644 --- a/utils/config.js +++ b/utils/config.js @@ -44,6 +44,12 @@ const defaultConfig = { sydneyGPTs: 'Copilot', sydneyImageRecognition: false, sydneyMoodTip: 'Your response should be divided into two parts, namely, the text and your mood. The mood available to you can only include: blandness, happy, shy, frustrated, disgusted, and frightened.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.', + chatExampleUser1: '', + chatExampleUser2: '', + chatExampleUser3: '', + chatExampleBot1: '', + chatExampleBot2: '', + chatExampleBot3: '', enableSuggestedResponses: false, sydneyEnableSearch: false, api: defaultChatGPTAPI, @@ -66,13 +72,8 @@ const defaultConfig = { xhRetReplace: '', promptPrefixOverride: 'Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.', assistantLabel: 'ChatGPT', - // thinkingTips: true, - username: '', - password: '', - UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', headless: false, chromePath: '', - '2captchaToken': '', proxy: '', debug: true, defaultTimeoutMs: 120000, diff --git a/utils/prompts.js b/utils/prompts.js index 286e361..8ca32ad 100644 --- a/utils/prompts.js +++ b/utils/prompts.js @@ -11,9 +11,19 @@ export function readPrompts () { txtFiles.forEach(txtFile => { let name = _.trimEnd(txtFile, '.txt') const content = fs.readFileSync(`${_path}/plugins/chatgpt-plugin/prompts/${txtFile}`, 'utf8') + let example = [] + try { + if (fs.existsSync(`${_path}/plugins/chatgpt-plugin/prompts/${name}_example.json`)) { + example = fs.readFileSync(`${_path}/plugins/chatgpt-plugin/prompts/${name}_example.json`, 'utf8') + example = JSON.parse(example) + } + } catch (err) { + logger.debug(err) + } prompts.push({ name, - content + content, + example }) }) } @@ -34,11 +44,15 @@ export function getPromptByName (name) { } } -export function saveOnePrompt (name, content) { +export function saveOnePrompt (name, content, examples) { const _path = process.cwd() mkdirs(`${_path}/plugins/chatgpt-plugin/prompts`) let filePath = `${_path}/plugins/chatgpt-plugin/prompts/${name}.txt` fs.writeFileSync(filePath, content) + if (examples) { + let examplePath = `${_path}/plugins/chatgpt-plugin/prompts/${name}_example.json` + fs.writeFileSync(examplePath, JSON.stringify(examples)) + } } export function deleteOnePrompt (name) { @@ -46,4 +60,8 @@ export function deleteOnePrompt (name) { mkdirs(`${_path}/plugins/chatgpt-plugin/prompts`) let filePath = `${_path}/plugins/chatgpt-plugin/prompts/${name}.txt` fs.unlinkSync(filePath) + try { + let examplePath = `${_path}/plugins/chatgpt-plugin/prompts/${name}_example.json` + fs.unlinkSync(examplePath) + } catch (err) {} }