feat: Claude2 from claude.ai (#561)

* feat: claude2 test

* fix: node-fetch的bug,使用redirect暂时解决

* fix: field error

* fix: field error

* fix: 引用文件

* fix: filename error

* fix: ignore convert document error
This commit is contained in:
ikechan8370 2023-09-08 12:32:40 +08:00 committed by GitHub
parent bf761c24da
commit 56d6b50aef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 75 deletions

View file

@ -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<boolean>}
*/
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 = []) {

View file

@ -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') {

View file

@ -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'

159
utils/claude.ai/index.js Normal file
View file

@ -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<void>}
*/
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<unknown>}
*/
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()

View file

@ -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 = {}