From 56d6b50aef7dcad4ed9409086f03bb032ff935c3 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 8 Sep 2023 12:32:40 +0800 Subject: [PATCH] feat: Claude2 from claude.ai (#561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: claude2 test * fix: node-fetch的bug,使用redirect暂时解决 * fix: field error * fix: field error * fix: 引用文件 * fix: filename error * fix: ignore convert document error --- apps/chat.js | 161 +++++++++++++++++++++------------------ apps/management.js | 17 ++++- guoba.support.js | 16 ++++ utils/claude.ai/index.js | 159 ++++++++++++++++++++++++++++++++++++++ utils/config.js | 5 +- 5 files changed, 283 insertions(+), 75 deletions(-) create mode 100644 utils/claude.ai/index.js diff --git a/apps/chat.js b/apps/chat.js index 8d7d3a4..377b270 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -25,7 +25,7 @@ import { getUserReplySetting, getImageOcrText, getImg, - getMaxModelTokens, formatDate, generateAudio, formatDate2 + getMaxModelTokens, formatDate, generateAudio, formatDate2, mkdirs } from '../utils/common.js' import { ChatGPTPuppeteer } from '../utils/browser.js' import { KeyvFile } from 'keyv-file' @@ -68,6 +68,8 @@ import { SendAvatarTool } from '../utils/tools/SendAvatarTool.js' import { SendMessageToSpecificGroupOrUserTool } from '../utils/tools/SendMessageToSpecificGroupOrUserTool.js' import { SetTitleTool } from '../utils/tools/SetTitleTool.js' import { createCaptcha, solveCaptcha, solveCaptchaOneShot } from '../utils/bingCaptcha.js' +import { ClaudeAIClient } from '../utils/claude.ai/index.js' +import fs from 'fs' try { await import('@azure/openai') @@ -156,6 +158,12 @@ export class chatgpt extends plugin { reg: '^#claude开启新对话', fnc: 'newClaudeConversation' }, + { + /** 命令正则匹配 */ + reg: '^#claude2[sS]*', + /** 执行方法 */ + fnc: 'claude2' + }, { /** 命令正则匹配 */ reg: '^#claude[sS]*', @@ -249,43 +257,11 @@ export class chatgpt extends plugin { fnc: 'deleteConversation', permission: 'master' } - // { - // reg: '^#chatgpt必应验证码', - // fnc: 'bingCaptcha' - // } ] }) this.toggleMode = toggleMode } - /** - * deprecated - * @param e - * @returns {Promise} - */ - async bingCaptcha(e) { - let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) - if (!bingTokens) { - await e.reply('尚未绑定必应token:必应过码必须绑定token') - return - } - bingTokens = bingTokens.map(token => token.Token) - let index = e.msg.replace(/^#chatgpt必应验证码/, '') - if (!index) { - await e.reply('指令不完整:请输入#chatgpt必应验证码+token序号(从1开始),如#chatgpt必应验证码1') - return - } - index = parseInt(index) - 1 - let bingToken = bingTokens[index] - let { id, regionId, image } = await createCaptcha(e, bingToken) - e.bingCaptchaId = id - e.regionId = regionId - e.token = bingToken - await e.reply(['请崽60秒内输入下面图片以通过必应人机验证', segment.image(`base64://${image}`)]) - this.setContext('solveBingCaptcha', false, 60) - return false - } - /** * 获取chatgpt当前对话列表 * @param e @@ -328,6 +304,11 @@ export class chatgpt extends plugin { await e.reply('claude对话已结束') return } + if (use === 'claude2') { + await redis.del(`CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}`) + await e.reply('claude2对话已结束') + return + } if (use === 'xh') { await redis.del(`CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) await e.reply('星火对话已结束') @@ -1032,6 +1013,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_BROWSER:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } + case 'claude2': { + key = `CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}` + break + } case 'xh': { key = `CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break @@ -1416,7 +1401,26 @@ export class chatgpt extends plugin { return true } - async claude(e) { + async claude2 (e) { + if (!Config.allowOtherMode) { + return false + } + let ats = e.message.filter(m => m.type === 'at') + if (!e.atme && ats.length > 0) { + if (Config.debug) { + logger.mark('艾特别人了,没艾特我,忽略#claude2') + } + return false + } + let prompt = _.replace(e.raw_message.trimStart(), '#claude2', '').trim() + if (prompt.length === 0) { + return false + } + await this.abstractChat(e, prompt, 'claude2') + return true + } + + async claude (e) { if (!Config.allowOtherMode) { return false } @@ -1858,6 +1862,56 @@ export class chatgpt extends plugin { text } } + case 'claude2': { + let { conversationId } = conversation + let client = new ClaudeAIClient({ + organizationId: Config.claudeAIOrganizationId, + sessionKey: Config.claudeAISessionKey, + debug: Config.debug, + proxy: Config.proxy + }) + let fileUrl, filename, attachments + if (e.source && e.source.message === '[文件]') { + if (e.isGroup) { + let source = (await e.group.getChatHistory(e.source.seq, 1))[0] + let file = source.message.find(m => m.type === 'file') + if (file) { + filename = file.name + fileUrl = await e.group.getFileUrl(file.fid) + } + } else { + let source = (await e.friend.getChatHistory(e.source.time, 1))[0] + let file = source.message.find(m => m.type === 'file') + if (file) { + filename = file.name + fileUrl = await e.group.getFileUrl(file.fid) + } + } + } + if (fileUrl) { + logger.info('文件地址:' + fileUrl) + mkdirs('data/chatgpt/files') + let destinationPath = 'data/chatgpt/files/' + filename + const response = await fetch(fileUrl) + const fileStream = fs.createWriteStream(destinationPath) + await new Promise((resolve, reject) => { + response.body.pipe(fileStream) + response.body.on('error', (err) => { + reject(err) + }) + fileStream.on('finish', () => { + resolve() + }) + }) + attachments = [await client.convertDocument(destinationPath, filename)] + } + if (conversationId) { + return await client.sendMessage(prompt, conversationId, attachments) + } else { + let conv = await client.createConversation() + return await client.sendMessage(prompt, conv.uuid, attachments) + } + } case 'xh': { const cacheOptions = { namespace: 'xh', @@ -2544,45 +2598,6 @@ export class chatgpt extends plugin { } return await this.chatGPTApi.sendMessage(prompt, sendMessageOption) } - - async solveBingCaptcha(e) { - try { - let id = e.bingCaptchaId - let regionId = e.regionId - let text = this.e.msg - let solveResult = await solveCaptcha(id, regionId, text, e.token) - if (solveResult.result) { - logger.mark('验证码正确:' + JSON.stringify(solveResult.detail)) - const cacheOptions = { - namespace: Config.toneStyle, - store: new KeyvFile({ filename: 'cache.json' }) - } - const bingAIClient = new SydneyAIClient({ - userToken: e.token, // "_U" cookie from bing.com - debug: Config.debug, - cache: cacheOptions, - user: e.sender.user_id, - proxy: Config.proxy - }) - try { - let response = await bingAIClient.sendMessage('hello', Object.assign({ invocationId: '1' }, e.bingConversation)) - if (response.response) { - await e.reply('验证码已通过') - } else { - await e.reply('验证码正确,但账户未解决验证码') - } - } catch (err) { - logger.error(err) - await e.reply('验证码正确,但账户未解决验证码') - } - } else { - await e.reply('验证码失败:' + JSON.stringify(solveResult.detail)) - } - } catch (err) { - this.finish('solveBingCaptcha') - } - this.finish('solveBingCaptcha') - } } async function getAvailableBingToken(conversation, throttled = []) { diff --git a/apps/management.js b/apps/management.js index 4d73d48..40b48b8 100644 --- a/apps/management.js +++ b/apps/management.js @@ -104,6 +104,11 @@ export class ChatgptManagement extends plugin { fnc: 'useSlackClaudeBasedSolution', permission: 'master' }, + { + reg: '^#chatgpt切换(Claude2|claude2|claude.ai)$', + fnc: 'useClaudeAISolution', + permission: 'master' + }, { reg: '^#chatgpt切换星火$', fnc: 'useXinghuoBasedSolution', @@ -840,6 +845,16 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, } } + async useClaudeAISolution () { + let use = await redis.get('CHATGPT:USE') + if (use !== 'claude2') { + await redis.set('CHATGPT:USE', 'claude2') + await this.reply('已切换到基于claude.ai的解决方案') + } else { + await this.reply('当前已经是claude2模式了') + } + } + async useXinghuoBasedSolution () { let use = await redis.get('CHATGPT:USE') if (use !== 'xh') { @@ -1404,4 +1419,4 @@ Poe 模式会调用 Poe 中的 Claude-instant 进行对话。需要提供 Cookie await e.reply('好的,已经关闭智能模式') } } -} \ No newline at end of file +} diff --git a/guoba.support.js b/guoba.support.js index 269d5cc..b2d0449 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -585,6 +585,22 @@ export function supportGuoba () { bottomHelpMessage: '若启用全局设定,每个人都会默认使用这里的设定。', component: 'Input' }, + { + label: '以下为Claude2方式的配置', + component: 'Divider' + }, + { + field: 'claudeAIOrganizationId', + label: 'claude2 OrganizationId', + bottomHelpMessage: 'claude.ai的OrganizationId', + component: 'Input' + }, + { + field: 'claudeAISessionKey', + label: 'claude2 SessionKey', + bottomHelpMessage: 'claude.ai Cookie中的SessionKey', + component: 'Input' + }, { label: '以下为ChatGLM方式的配置', component: 'Divider' diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js new file mode 100644 index 0000000..04da747 --- /dev/null +++ b/utils/claude.ai/index.js @@ -0,0 +1,159 @@ +import fetch, { File, FormData, Headers } from 'node-fetch' +import fs from 'fs' +import crypto from 'crypto' +import HttpsProxyAgent from 'https-proxy-agent' + +export class ClaudeAIClient { + constructor (opts) { + const { organizationId, sessionKey, proxy, debug = false } = opts + this.organizationId = organizationId + this.sessionKey = sessionKey + this.debug = debug + let headers = new Headers() + headers.append('Cookie', `sessionKey=${sessionKey}`) + headers.append('referrer', 'https://claude.ai/chat/360f8c2c-56e8-4193-99c6-8d52fad3ecc8') + headers.append('origin', 'https://claude.ai') + headers.append('Content-Type', 'application/json') + this.headers = headers + this.proxy = proxy + this.fetch = (url, options = {}) => { + const defaultOptions = proxy + ? { + agent: HttpsProxyAgent(proxy) + } + : {} + const mergedOptions = { + ...defaultOptions, + ...options + } + + return fetch(url, mergedOptions) + } + } + + /** + * 抽取文件文本内容,https://claude.ai/api/convert_document + * @param filePath 文件路径 + * @param filename + * @returns {Promise} + */ + async convertDocument (filePath, filename = 'file.pdf') { + let formData = new FormData() + formData.append('orgUuid', this.organizationId) + let buffer = fs.readFileSync(filePath) + formData.append('file', new File([buffer], filename)) + let result = await this.fetch('https://claude.ai/api/convert_document', { + body: formData, + headers: this.headers, + method: 'POST', + redirect: 'manual' + }) + if (result.statusCode === 307) { + throw new Error('claude.ai目前不支持你所在的地区') + } + if (result.statusCode !== 200) { + console.warn('failed to parse document convert result: ' + result.statusCode + ' ' + result.statusText) + return null + } + let raw = await result.text() + try { + return JSON.parse(raw) + } catch (e) { + console.warn('failed to parse document convert result: ' + raw) + return null + } + } + + /** + * 创建新的对话 + * @param uuid + * @param name + * @returns {Promise} + */ + async createConversation (uuid = crypto.randomUUID(), name = '') { + let body = { + name, + uuid + } + body = JSON.stringify(body) + let result = await this.fetch(`https://claude.ai/api/organizations/${this.organizationId}/chat_conversations`, { + body, + headers: this.headers, + method: 'POST', + redirect: 'manual' + }) + if (result.statusCode === 307) { + throw new Error('claude.ai目前不支持你所在的地区') + } + let jsonRes = await result.json() + if (this.debug) { + console.log(jsonRes) + } + if (!jsonRes?.uuid) { + console.error(jsonRes) + throw new Error('conversation create error') + } + return jsonRes + } + + async sendMessage (text, conversationId, attachments = []) { + let body = { + conversation_uuid: conversationId, + organization_uuid: this.organizationId, + text, + attachments, + completion: { + incremental: true, + model: 'claude-2', + prompt: text, + timezone: 'Asia/Hong_Kong' + } + } + let url = 'https://claude.ai/api/append_message' + let streamDataRes = await this.fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: this.headers, + redirect: 'manual' + }) + if (streamDataRes.statusCode === 307) { + throw new Error('claude.ai目前不支持你所在的地区') + } + let streamData = await streamDataRes.text() + let responseText = '' + let streams = streamData.split('\n\n') + streams.forEach(s => { + let jsonStr = s.replace('data: ', '').trim() + try { + let jsonObj = JSON.parse(jsonStr) + if (jsonObj && jsonObj.completion) { + responseText += jsonObj.completion + } + } catch (err) { + // ignore error + if (this.debug) { + console.log(jsonStr) + } + } + }) + let response = { + text: responseText.trim(), + conversationId + } + return response + } +} + +async function testClaudeAI () { + let client = new ClaudeAIClient({ + organizationId: '', + sessionKey: '', + debug: true, + proxy: 'http://127.0.0.1:7890' + }) + let conv = await client.createConversation() + let result = await client.sendMessage('hello, who are you', conv.uuid) + console.log(result.response) +} + +// testClaudeAI() diff --git a/utils/config.js b/utils/config.js index 77c3dc8..dc893bb 100644 --- a/utils/config.js +++ b/utils/config.js @@ -144,7 +144,10 @@ const defaultConfig = { extraUrl: 'https://cpe.ikechan8370.com', smartMode: false, bingCaptchaOneShotUrl: 'http://bingcaptcha.ikechan8370.com/bing', - version: 'v2.7.3' + // claude2 + claudeAIOrganizationId: '', + claudeAISessionKey: '', + version: 'v2.7.4' } const _path = process.cwd() let config = {}