diff --git a/README.md b/README.md index 934a944..5a95a57 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 最新版本支持的功能如下: - [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式 -- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3, GPT-3.5, GPT-4, 文心一言, 讯飞星火 +- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3, GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火 - [x] **语音识别:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型 - [x] **图片生成:** 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate, midjourney模型 - [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结等插件 @@ -27,6 +27,7 @@ Demo made by [Visionn](https://www.wangpc.cc/) # 更新日志 +>**2023.09.01:** 接入讯飞星火,claude机器人 >**2023.08.08:** 接入百度文心一言模型,通过 [插件](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/linkai) 支持 Midjourney 绘图 @@ -157,7 +158,7 @@ pip3 install azure-cognitiveservices-speech **4.其他配置** -+ `model`: 模型名称,目前支持 `gpt-3.5-turbo`, `text-davinci-003`, `gpt-4`, `gpt-4-32k`, `wenxin` (其中gpt-4 api暂未完全开放,申请通过后可使用) ++ `model`: 模型名称,目前支持 `gpt-3.5-turbo`, `text-davinci-003`, `gpt-4`, `gpt-4-32k`, `wenxin` , `claude` , `xunfei`(其中gpt-4 api暂未完全开放,申请通过后可使用) + `temperature`,`frequency_penalty`,`presence_penalty`: Chat API接口参数,详情参考[OpenAI官方文档。](https://platform.openai.com/docs/api-reference/chat) + `proxy`:由于目前 `openai` 接口国内无法访问,需配置代理客户端的地址,详情参考 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351) + 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 `image_create_prefix ` @@ -175,6 +176,26 @@ pip3 install azure-cognitiveservices-speech + `linkai_api_key`: LinkAI Api Key,可在 [控制台](https://chat.link-ai.tech/console/interface) 创建 + `linkai_app_code`: LinkAI 应用code,选填 +**6.wenxin配置 (可选 model 为 wenxin 时生效)** + ++ `baidu_wenxin_api_key`: 文心一言官网api key。 ++ `baidu_wenxin_secret_key`: 文心一言官网secret key。 + + +**6.Claude配置 (可选 model 为 claude 时生效)** + ++ `claude_api_cookie`: claude官网聊天界面复制完整 cookie 字符串。 ++ `claude_uuid`: 可以指定对话id,默认新建对话实体。 + + +**7.xunfei配置 (可选 model 为 xunfei 时生效)** + ++ `xunfei_app_id`: 讯飞星火app id。 ++ `xunfei_api_key`: 讯飞星火 api key。 ++ `xunfei_api_secret`: 讯飞星火 secret。 + + + **本说明文档可能会未及时更新,当前所有可选的配置项均在该[`config.py`](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py)中列出。** ## 运行 diff --git a/bot/bot_factory.py b/bot/bot_factory.py index 513eb78..da12f95 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -39,4 +39,8 @@ def create_bot(bot_type): elif bot_type == const.LINKAI: from bot.linkai.link_ai_bot import LinkAIBot return LinkAIBot() + + elif bot_type == const.CLAUDEAI: + from bot.claude.claude_ai_bot import ClaudeAIBot + return ClaudeAIBot() raise RuntimeError diff --git a/bot/claude/claude_ai_bot.py b/bot/claude/claude_ai_bot.py new file mode 100644 index 0000000..d273f2c --- /dev/null +++ b/bot/claude/claude_ai_bot.py @@ -0,0 +1,218 @@ +import re +import time +import json +import uuid +from curl_cffi import requests +from bot.bot import Bot +from bot.claude.claude_ai_session import ClaudeAiSession +from bot.openai.open_ai_image import OpenAIImage +from bot.session_manager import SessionManager +from bridge.context import Context, ContextType +from bridge.reply import Reply, ReplyType +from common.log import logger +from config import conf + + +class ClaudeAIBot(Bot, OpenAIImage): + def __init__(self): + super().__init__() + self.sessions = SessionManager(ClaudeAiSession, model=conf().get("model") or "gpt-3.5-turbo") + self.claude_api_cookie = conf().get("claude_api_cookie") + self.proxy = conf().get("proxy") + self.con_uuid_dic = {} + if self.proxy: + self.proxies = { + "http": self.proxy, + "https": self.proxy + } + else: + self.proxies = None + self.org_uuid = self.get_organization_id() + + def generate_uuid(self): + random_uuid = uuid.uuid4() + random_uuid_str = str(random_uuid) + formatted_uuid = f"{random_uuid_str[0:8]}-{random_uuid_str[9:13]}-{random_uuid_str[14:18]}-{random_uuid_str[19:23]}-{random_uuid_str[24:]}" + return formatted_uuid + + def get_uuid(self): + if conf().get("claude_uuid") != None: + self.con_uuid = conf().get("claude_uuid") + else: + con_uuid = self.generate_uuid() + self.create_new_chat(con_uuid) + + def get_organization_id(self): + url = "https://claude.ai/api/organizations" + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept-Language': 'en-US,en;q=0.5', + 'Referer': 'https://claude.ai/chats', + 'Content-Type': 'application/json', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Connection': 'keep-alive', + 'Cookie': f'{self.claude_api_cookie}' + } + response = requests.get(url, headers=headers,impersonate="chrome110",proxies=self.proxies) + res = json.loads(response.text) + uuid = res[0]['uuid'] + return uuid + + def reply(self, query, context: Context = None) -> Reply: + if context.type == ContextType.TEXT: + return self._chat(query, context) + elif context.type == ContextType.IMAGE_CREATE: + ok, res = self.create_img(query, 0) + if ok: + reply = Reply(ReplyType.IMAGE_URL, res) + else: + reply = Reply(ReplyType.ERROR, res) + return reply + else: + reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) + return reply + + def get_organization_id(self): + url = "https://claude.ai/api/organizations" + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept-Language': 'en-US,en;q=0.5', + 'Referer': 'https://claude.ai/chats', + 'Content-Type': 'application/json', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Connection': 'keep-alive', + 'Cookie': f'{self.claude_api_cookie}' + } + try: + response = requests.get(url, headers=headers,impersonate="chrome110",proxies =self.proxies ) + res = json.loads(response.text) + uuid = res[0]['uuid'] + except: + print(response.text) + return uuid + + def conversation_share_check(self,session_id): + if session_id not in self.con_uuid_dic: + self.con_uuid_dic[session_id] = self.generate_uuid() + self.create_new_chat(self.con_uuid_dic[session_id]) + return self.con_uuid_dic[session_id] + + def create_new_chat(self, con_uuid): + url = f"https://claude.ai/api/organizations/{self.org_uuid}/chat_conversations" + payload = json.dumps({"uuid": con_uuid, "name": ""}) + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept-Language': 'en-US,en;q=0.5', + 'Referer': 'https://claude.ai/chats', + 'Content-Type': 'application/json', + 'Origin': 'https://claude.ai', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Cookie': self.claude_api_cookie, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'TE': 'trailers' + } + response = requests.post(url, headers=headers, data=payload,impersonate="chrome110", proxies= self.proxies) + # Returns JSON of the newly created conversation information + return response.json() + + def _chat(self, query, context, retry_count=0) -> Reply: + """ + 发起对话请求 + :param query: 请求提示词 + :param context: 对话上下文 + :param retry_count: 当前递归重试次数 + :return: 回复 + """ + if retry_count >= 2: + # exit from retry 2 times + logger.warn("[CLAUDEAI] failed after maximum number of retry times") + return Reply(ReplyType.ERROR, "请再问我一次吧") + + try: + session_id = context["session_id"] + session = self.sessions.session_query(query, session_id) + con_uuid = self.conversation_share_check(session_id) + model = conf().get("model") or "gpt-3.5-turbo" + # remove system message + if session.messages[0].get("role") == "system": + if model == "wenxin" or model == "claude": + session.messages.pop(0) + logger.info(f"[CLAUDEAI] query={query}") + + # do http request + base_url = "https://claude.ai" + payload = json.dumps({ + "completion": { + "prompt": f"{query}", + "timezone": "Asia/Kolkata", + "model": "claude-2" + }, + "organization_uuid": f"{self.org_uuid}", + "conversation_uuid": f"{con_uuid}", + "text": f"{query}", + "attachments": [] + }) + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept': 'text/event-stream, text/event-stream', + 'Accept-Language': 'en-US,en;q=0.5', + 'Referer': 'https://claude.ai/chats', + 'Content-Type': 'application/json', + 'Origin': 'https://claude.ai', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Cookie': f'{self.claude_api_cookie}', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'TE': 'trailers' + } + + res = requests.post(base_url + "/api/append_message", headers=headers, data=payload,impersonate="chrome110",proxies= self.proxies,timeout=400) + if res.status_code == 200 or "pemission" in res.text: + # execute success + decoded_data = res.content.decode("utf-8") + decoded_data = re.sub('\n+', '\n', decoded_data).strip() + data_strings = decoded_data.split('\n') + completions = [] + for data_string in data_strings: + json_str = data_string[6:].strip() + data = json.loads(json_str) + if 'completion' in data: + completions.append(data['completion']) + + reply_content = ''.join(completions) + logger.info(f"[CLAUDE] reply={reply_content}, total_tokens=invisible") + + self.sessions.session_reply(reply_content, session_id, 100) + return Reply(ReplyType.TEXT, reply_content) + else: + response = res.json() + error = response.get("error") + logger.error(f"[CLAUDE] chat failed, status_code={res.status_code}, " + f"msg={error.get('message')}, type={error.get('type')}, detail: {res.text}, uuid: {con_uuid}") + + if res.status_code >= 500: + # server error, need retry + time.sleep(2) + logger.warn(f"[CLAUDE] do retry, times={retry_count}") + return self._chat(query, context, retry_count + 1) + return Reply(ReplyType.ERROR, "提问太快啦,请休息一下再问我吧") + + except Exception as e: + logger.exception(e) + # retry + time.sleep(2) + logger.warn(f"[CLAUDE] do retry, times={retry_count}") + return self._chat(query, context, retry_count + 1) diff --git a/bot/claude/claude_ai_session b/bot/claude/claude_ai_session new file mode 100644 index 0000000..ede9e51 --- /dev/null +++ b/bot/claude/claude_ai_session @@ -0,0 +1,9 @@ +from bot.session_manager import Session + + +class ClaudeAiSession(Session): + def __init__(self, session_id, system_prompt=None, model="claude"): + super().__init__(session_id, system_prompt) + self.model = model + # claude逆向不支持role prompt + # self.reset() diff --git a/bridge/bridge.py b/bridge/bridge.py index 2022438..4a0ef4f 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -29,6 +29,8 @@ class Bridge(object): self.btype["chat"] = const.XUNFEI if conf().get("use_linkai") and conf().get("linkai_api_key"): self.btype["chat"] = const.LINKAI + if model_type in ["claude"]: + self.btype["chat"] = const.CLAUDEAI self.bots = {} def get_bot(self, typename): diff --git a/channel/chat_channel.py b/channel/chat_channel.py index be75ebb..06bc354 100644 --- a/channel/chat_channel.py +++ b/channel/chat_channel.py @@ -110,9 +110,10 @@ class ChatChannel(Channel): flag = True pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" subtract_res = re.sub(pattern, r"", content) - for at in context["msg"].at_list: - pattern = f"@{re.escape(at)}(\u2005|\u0020)" - subtract_res = re.sub(pattern, r"", subtract_res) + if isinstance(context["msg"].at_list, list): + for at in context["msg"].at_list: + pattern = f"@{re.escape(at)}(\u2005|\u0020)" + subtract_res = re.sub(pattern, r"", subtract_res) if subtract_res == content and context["msg"].self_display_name: # 前缀移除后没有变化,使用群昵称再次移除 pattern = f"@{re.escape(context['msg'].self_display_name)}(\u2005|\u0020)" diff --git a/common/const.py b/common/const.py index 505ab71..959b4c1 100644 --- a/common/const.py +++ b/common/const.py @@ -8,4 +8,5 @@ LINKAI = "linkai" VERSION = "1.3.0" -MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "xunfei"] +CLAUDEAI = "claude" +MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "xunfei","claude"] diff --git a/config.py b/config.py index 05a2649..68e6404 100644 --- a/config.py +++ b/config.py @@ -59,6 +59,11 @@ available_setting = { "xunfei_app_id": "", # 讯飞应用ID "xunfei_api_key": "", # 讯飞 API key "xunfei_api_secret": "", # 讯飞 API secret + # claude 配置 + "claude_api_cookie": "", + "claude_uuid": "", + # wework的通用配置 + "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 # 语音设置 "speech_recognition": False, # 是否开启语音识别 "group_speech_recognition": False, # 是否开启群组语音识别 @@ -121,8 +126,6 @@ available_setting = { "linkai_api_key": "", "linkai_app_code": "", "linkai_api_base": "https://api.link-ai.chat", # linkAI服务地址,若国内无法访问或延迟较高可改为 https://api.link-ai.tech - # wework的通用配置 - "wework_smart": True # 配置wework是否使用已登录的企业微信,False为多开 } diff --git a/requirements-optional.txt b/requirements-optional.txt index 1cb8a55..17c4c1f 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -28,3 +28,6 @@ chatgpt_tool_hub==0.4.6 # xunfei spark websocket-client==1.2.0 + +# claude bot +curl_cffi