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/package.json b/package.json index e6a60d7..f45922a 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-recaptcha": "^3.6.8", "puppeteer-extra-plugin-stealth": "^2.11.2", - "sharp": "^0.32.3" + "sharp": "^0.32.3", + "cycletls": "^1.0.21" }, "devDependencies": { "ts-node": "^10.9.1", diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js new file mode 100644 index 0000000..c362175 --- /dev/null +++ b/utils/claude.ai/index.js @@ -0,0 +1,197 @@ +import { File, FormData, Headers } from 'node-fetch' +import fs from 'fs' +import crypto from 'crypto' +// import initCycleTLS from 'cycletls' +let initCycleTLS +try { + initCycleTLS = (await import('cycletls')).default +} catch (err) { + console.warn('未安装cycletls,无法使用claude2功能。') +} +export class ClaudeAIClient { + constructor (opts) { + if (!initCycleTLS) { + throw new Error('CycleTLS is not installed') + } + 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') + headers.append('origin', 'https://claude.ai') + headers.append('Content-Type', 'application/json') + headers.append('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36') + // headers.append('sec-ch-ua', '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"') + // headers.append('Sec-Ch-Ua-Mobile', '?0') + // headers.append('Sec-Ch-Ua-Platform', '"Windows"') + headers.append('Sec-Fetch-Dest', 'empty') + headers.append('Sec-Fetch-Mode', 'cors') + headers.append('Sec-Fetch-Site', 'same-origin') + headers.append('Connection', 'keep-alive') + headers.append('TE', 'trailers') + headers.append('Accept-Encoding', 'gzip, deflate, br') + headers.append('Accept-Language', 'en-US,en;q=0.5') + headers.append('Dnt', '1') + headers.append('Accept', '*/*') + // headers.append('sentry-trace', 'd1c13c8e760c4e9e969a5e1aed6a38cf-a854f94e3d1a4bc7-0') + // headers.append('anthropic-client-sha', 'cab849b55d41c73804c1b2b87a7a7fdb84263dc9') + // headers.append('anthropic-client-version', '1') + // headers.append('baggage', 'sentry-environment=production,sentry-release=cab849b55d41c73804c1b2b87a7a7fdb84263dc9,sentry-public_key=58e9b9d0fc244061a1b54fe288b0e483,sentry-trace_id=d1c13c8e760c4e9e969a5e1aed6a38cf') + this.JA3 = '772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,5-27-45-35-65281-16-18-10-17513-43-13-23-51-0-11,29-23-24,0' + + this.headers = headers + this.rawHeaders = {} + Array.from(this.headers.keys()).forEach(key => { + this.rawHeaders[key] = this.headers.get(key) + }) + this.proxy = proxy + } + + /** + * 抽取文件文本内容,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', + // referrer: 'https://claude.ai/chat/bba5a67d-ee59-4196-a371-ece8a35db1f2' + // }) + // 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' + // // referrer: 'https://claude.ai/chat/bba5a67d-ee59-4196-a371-ece8a35db1f2' + // }) + const cycleTLS = await initCycleTLS() + let result = await cycleTLS(`https://claude.ai/api/organizations/${this.organizationId}/chat_conversations`, { + ja3: this.JA3, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36', + proxy: this.proxy, + body, + headers: this.rawHeaders, + disableRedirect: true + }, 'post') + if (result.status === 307) { + throw new Error('claude.ai目前不支持你所在的地区') + } + let jsonRes = result.body + if (this.debug) { + console.log(jsonRes) + } + if (!jsonRes?.uuid) { + console.error(jsonRes) + // console.log(result.headers) + 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' + const cycleTLS = await initCycleTLS() + let streamDataRes = await cycleTLS(url, { + ja3: this.JA3, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36', + proxy: this.proxy, + body: JSON.stringify(body), + headers: this.rawHeaders, + disableRedirect: true + }, 'post') + if (streamDataRes.status === 307) { + throw new Error('claude.ai目前不支持你所在的地区') + } + let streamData = streamDataRes.body + // console.log(streamData) + let responseText = '' + let streams = streamData.split('\n\n') + for (let s of streams) { + let jsonStr = s.replace('data: ', '').trim() + try { + let jsonObj = JSON.parse(jsonStr) + if (jsonObj && jsonObj.completion) { + responseText += jsonObj.completion + } + if (this.debug) { + console.log(jsonObj) + } + // console.log(responseText) + } catch (err) { + // ignore error + if (this.debug) { + console.log(jsonStr) + } + } + } + return { + text: responseText.trim(), + conversationId + } + } +} + +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.text) + return result +} + +// 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 = {}