diff --git a/README.md b/README.md index 2fad81b..80004df 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ pip3 install azure-cognitiveservices-speech cp config-template.json config.json ``` -然后在`config.json`中填入配置,以下是对默认配置的说明,可根据需要进行自定义修改: +然后在`config.json`中填入配置,以下是对默认配置的说明,可根据需要进行自定义修改(请去掉注释): ```bash # config.json文件内容示例 @@ -115,7 +115,9 @@ pip3 install azure-cognitiveservices-speech "speech_recognition": false, # 是否开启语音识别 "group_speech_recognition": false, # 是否开启群组语音识别 "use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/ - "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述, + "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述 + # 订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复,可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。 + "subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持角色扮演和文字冒险等丰富插件。\n输入{trigger_prefix}#help 查看详细指令。" } ``` **配置说明:** @@ -150,6 +152,7 @@ pip3 install azure-cognitiveservices-speech + `clear_memory_commands`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。 + `hot_reload`: 程序退出后,暂存微信扫码状态,默认关闭。 + `character_desc` 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 [issue](https://github.com/zhayujie/chatgpt-on-wechat/issues/43)) ++ `subscribe_msg`:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。 **所有可选的配置项均在该[文件](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/config.py)中列出。** diff --git a/app.py b/app.py index 637b6e4..4f3da19 100644 --- a/app.py +++ b/app.py @@ -43,7 +43,7 @@ def run(): # os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:9001' channel = channel_factory.create_channel(channel_name) - if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service"]: + if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app"]: PluginManager().load_plugins() # startup channel diff --git a/channel/channel_factory.py b/channel/channel_factory.py index ebd9732..96f3e5f 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -29,4 +29,8 @@ def create_channel(channel_type): from channel.wechatmp.wechatmp_channel import WechatMPChannel return WechatMPChannel(passive_reply=False) + elif channel_type == "wechatcom_app": + from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel + + return WechatComAppChannel() raise RuntimeError diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index 16d788c..d8249a1 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -29,7 +29,7 @@ from plugins import * @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE]) def handler_single_msg(msg): try: - cmsg = WeChatMessage(msg, False) + cmsg = WechatMessage(msg, False) except NotImplementedError as e: logger.debug("[WX]single message {} skipped: {}".format(msg["MsgId"], e)) return None @@ -40,7 +40,7 @@ def handler_single_msg(msg): @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE], isGroupChat=True) def handler_group_msg(msg): try: - cmsg = WeChatMessage(msg, True) + cmsg = WechatMessage(msg, True) except NotImplementedError as e: logger.debug("[WX]group message {} skipped: {}".format(msg["MsgId"], e)) return None diff --git a/channel/wechat/wechat_message.py b/channel/wechat/wechat_message.py index 63c2254..5d9bf28 100644 --- a/channel/wechat/wechat_message.py +++ b/channel/wechat/wechat_message.py @@ -8,7 +8,7 @@ from lib import itchat from lib.itchat.content import * -class WeChatMessage(ChatMessage): +class WechatMessage(ChatMessage): def __init__(self, itchat_msg, is_group=False): super().__init__(itchat_msg) self.msg_id = itchat_msg["MsgId"] diff --git a/channel/wechatcom/README.md b/channel/wechatcom/README.md new file mode 100644 index 0000000..e3b4843 --- /dev/null +++ b/channel/wechatcom/README.md @@ -0,0 +1,59 @@ +# 企业微信应用号channel + +企业微信官方提供了客服、应用等API,本channel使用的是企业微信的应用API的能力。 + +因为未来可能还会开发客服能力,所以本channel的类型名叫作`wechatcom_app`。 + +`wechatcom_app` channel支持插件系统和图片声音交互等能力,除了无法加入群聊,作为个人使用的私人助理已绰绰有余。 + +## 开始之前 + +- 在企业中确认自己拥有在企业内自建应用的权限。 +- 如果没有权限或者是个人用户,也可创建未认证的企业。操作方式:登录手机企业微信,选择`创建/加入企业`来创建企业,类型请选择企业,企业名称可随意填写。 + 未认证的企业有100人的服务人数上限,其他功能与认证企业没有差异。 + +本channel需安装的依赖与公众号一致,需要安装`wechatpy`和`web.py`,它们包含在`requirements-optional.txt`中。 + +## 使用方法 + +1.查看企业ID + +- 扫码登陆[企业微信后台](https://work.weixin.qq.com) +- 选择`我的企业`,点击`企业信息`,记住该`企业ID` + +2.创建自建应用 + +- 选择应用管理, 在自建区选创建应用来创建企业自建应用 +- 上传应用logo,填写应用名称等项 +- 创建应用后进入应用详情页面,记住`AgentId`和`Secert` + +3.配置应用 + +- 在详情页如果点击`企业可信IP`的配置(没看到可以不管),填入你服务器的公网IP +- 点击`接收消息`下的启用API接收消息 +- `URL`填写格式为`http://url:port/wxcomapp`,`port`是程序监听的端口,默认是9898 + 如果是未认证的企业,url可直接使用服务器的IP。如果是认证企业,需要使用备案的域名,可使用二级域名。 +- `Token`可随意填写,停留在这个页面 +- 在程序根目录`config.json`中增加配置(**去掉注释**),`wechatcomapp_aes_key`是当前页面的`wechatcomapp_aes_key` + +```python + "channel_type": "wechatcom_app", + "wechatcom_corp_id": "", # 企业微信公司的corpID + "wechatcomapp_token": "", # 企业微信app的token + "wechatcomapp_port": 9898, # 企业微信app的服务端口, 不需要端口转发 + "wechatcomapp_secret": "", # 企业微信app的secret + "wechatcomapp_agent_id": "", # 企业微信app的agent_id + "wechatcomapp_aes_key": "", # 企业微信app的aes_key +``` + +- 运行程序,在页面中点击保存,保存成功说明验证成功 + +4.连接个人微信 + +选择`我的企业`,点击`微信插件`,下面有个邀请关注的二维码。微信扫码后,即可在微信中看到对应企业,在这里你便可以和机器人沟通。 + +## 测试体验 + +AIGC开放社区中已经部署了多个可免费使用的Bot,扫描下方的二维码会自动邀请你来体验。 + + \ No newline at end of file diff --git a/channel/wechatcom/wechatcomapp_channel.py b/channel/wechatcom/wechatcomapp_channel.py new file mode 100644 index 0000000..d4c090d --- /dev/null +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -0,0 +1,168 @@ +# -*- coding=utf-8 -*- +import io +import os +import time + +import requests +import web +from wechatpy.enterprise import create_reply, parse_message +from wechatpy.enterprise.crypto import WeChatCrypto +from wechatpy.enterprise.exceptions import InvalidCorpIdException +from wechatpy.exceptions import InvalidSignatureException, WeChatClientException + +from bridge.context import Context +from bridge.reply import Reply, ReplyType +from channel.chat_channel import ChatChannel +from channel.wechatcom.wechatcomapp_client import WechatComAppClient +from channel.wechatcom.wechatcomapp_message import WechatComAppMessage +from common.log import logger +from common.singleton import singleton +from common.utils import compress_imgfile, fsize, split_string_by_utf8_length +from config import conf, subscribe_msg +from voice.audio_convert import any_to_amr + +MAX_UTF8_LEN = 2048 + + +@singleton +class WechatComAppChannel(ChatChannel): + NOT_SUPPORT_REPLYTYPE = [] + + def __init__(self): + super().__init__() + self.corp_id = conf().get("wechatcom_corp_id") + self.secret = conf().get("wechatcomapp_secret") + self.agent_id = conf().get("wechatcomapp_agent_id") + self.token = conf().get("wechatcomapp_token") + self.aes_key = conf().get("wechatcomapp_aes_key") + print(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) + logger.info( + "[wechatcom] init: corp_id: {}, secret: {}, agent_id: {}, token: {}, aes_key: {}".format(self.corp_id, self.secret, self.agent_id, self.token, self.aes_key) + ) + self.crypto = WeChatCrypto(self.token, self.aes_key, self.corp_id) + self.client = WechatComAppClient(self.corp_id, self.secret) + + def startup(self): + # start message listener + urls = ("/wxcomapp", "channel.wechatcom.wechatcomapp_channel.Query") + app = web.application(urls, globals(), autoreload=False) + port = conf().get("wechatcomapp_port", 9898) + web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) + + def send(self, reply: Reply, context: Context): + receiver = context["receiver"] + if reply.type in [ReplyType.TEXT, ReplyType.ERROR, ReplyType.INFO]: + reply_text = reply.content + texts = split_string_by_utf8_length(reply_text, MAX_UTF8_LEN) + if len(texts) > 1: + logger.info("[wechatcom] text too long, split into {} parts".format(len(texts))) + for i, text in enumerate(texts): + self.client.message.send_text(self.agent_id, receiver, text) + if i != len(texts) - 1: + time.sleep(0.5) # 休眠0.5秒,防止发送过快乱序 + logger.info("[wechatcom] Do send text to {}: {}".format(receiver, reply_text)) + elif reply.type == ReplyType.VOICE: + try: + file_path = reply.content + amr_file = os.path.splitext(file_path)[0] + ".amr" + any_to_amr(file_path, amr_file) + response = self.client.media.upload("voice", open(amr_file, "rb")) + logger.debug("[wechatcom] upload voice response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload voice failed: {}".format(e)) + return + try: + os.remove(file_path) + if amr_file != file_path: + os.remove(amr_file) + except Exception: + pass + self.client.message.send_voice(self.agent_id, receiver, response["media_id"]) + logger.info("[wechatcom] sendVoice={}, receiver={}".format(reply.content, receiver)) + elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 + img_url = reply.content + pic_res = requests.get(img_url, stream=True) + image_storage = io.BytesIO() + for block in pic_res.iter_content(1024): + image_storage.write(block) + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: + logger.info("[wechatcom] image too large, ready to compress, sz={}".format(sz)) + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) + logger.info("[wechatcom] image compressed, sz={}".format(fsize(image_storage))) + image_storage.seek(0) + try: + response = self.client.media.upload("image", image_storage) + logger.debug("[wechatcom] upload image response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload image failed: {}".format(e)) + return + + self.client.message.send_image(self.agent_id, receiver, response["media_id"]) + logger.info("[wechatcom] sendImage url={}, receiver={}".format(img_url, receiver)) + elif reply.type == ReplyType.IMAGE: # 从文件读取图片 + image_storage = reply.content + if (sz := fsize(image_storage)) >= 10 * 1024 * 1024: + logger.info("[wechatcom] image too large, ready to compress, sz={}".format(sz)) + image_storage = compress_imgfile(image_storage, 10 * 1024 * 1024 - 1) + logger.info("[wechatcom] image compressed, sz={}".format(fsize(image_storage))) + image_storage.seek(0) + try: + response = self.client.media.upload("image", image_storage) + logger.debug("[wechatcom] upload image response: {}".format(response)) + except WeChatClientException as e: + logger.error("[wechatcom] upload image failed: {}".format(e)) + return + self.client.message.send_image(self.agent_id, receiver, response["media_id"]) + logger.info("[wechatcom] sendImage, receiver={}".format(receiver)) + + +class Query: + def GET(self): + channel = WechatComAppChannel() + params = web.input() + logger.info("[wechatcom] receive params: {}".format(params)) + try: + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce + echostr = params.echostr + echostr = channel.crypto.check_signature(signature, timestamp, nonce, echostr) + except InvalidSignatureException: + raise web.Forbidden() + return echostr + + def POST(self): + channel = WechatComAppChannel() + params = web.input() + logger.info("[wechatcom] receive params: {}".format(params)) + try: + signature = params.msg_signature + timestamp = params.timestamp + nonce = params.nonce + message = channel.crypto.decrypt_message(web.data(), signature, timestamp, nonce) + except (InvalidSignatureException, InvalidCorpIdException): + raise web.Forbidden() + msg = parse_message(message) + logger.debug("[wechatcom] receive message: {}, msg= {}".format(message, msg)) + if msg.type == "event": + if msg.event == "subscribe": + reply_content = subscribe_msg() + if reply_content: + reply = create_reply(reply_content, msg).render() + res = channel.crypto.encrypt_message(reply, nonce, timestamp) + return res + else: + try: + wechatcom_msg = WechatComAppMessage(msg, client=channel.client) + except NotImplementedError as e: + logger.debug("[wechatcom] " + str(e)) + return "success" + context = channel._compose_context( + wechatcom_msg.ctype, + wechatcom_msg.content, + isgroup=False, + msg=wechatcom_msg, + ) + if context: + channel.produce(context) + return "success" diff --git a/channel/wechatcom/wechatcomapp_client.py b/channel/wechatcom/wechatcomapp_client.py new file mode 100644 index 0000000..c0feb7a --- /dev/null +++ b/channel/wechatcom/wechatcomapp_client.py @@ -0,0 +1,21 @@ +import threading +import time + +from wechatpy.enterprise import WeChatClient + + +class WechatComAppClient(WeChatClient): + def __init__(self, corp_id, secret, access_token=None, session=None, timeout=None, auto_retry=True): + super(WechatComAppClient, self).__init__(corp_id, secret, access_token, session, timeout, auto_retry) + self.fetch_access_token_lock = threading.Lock() + + def fetch_access_token(self): # 重载父类方法,加锁避免多线程重复获取access_token + with self.fetch_access_token_lock: + access_token = self.session.get(self.access_token_key) + if access_token: + if not self.expires_at: + return access_token + timestamp = time.time() + if self.expires_at - timestamp > 60: + return access_token + return super().fetch_access_token() diff --git a/channel/wechatcom/wechatcomapp_message.py b/channel/wechatcom/wechatcomapp_message.py new file mode 100644 index 0000000..a70f755 --- /dev/null +++ b/channel/wechatcom/wechatcomapp_message.py @@ -0,0 +1,52 @@ +from wechatpy.enterprise import WeChatClient + +from bridge.context import ContextType +from channel.chat_message import ChatMessage +from common.log import logger +from common.tmp_dir import TmpDir + + +class WechatComAppMessage(ChatMessage): + def __init__(self, msg, client: WeChatClient, is_group=False): + super().__init__(msg) + self.msg_id = msg.id + self.create_time = msg.time + self.is_group = is_group + + if msg.type == "text": + self.ctype = ContextType.TEXT + self.content = msg.content + elif msg.type == "voice": + self.ctype = ContextType.VOICE + self.content = TmpDir().path() + msg.media_id + "." + msg.format # content直接存临时目录路径 + + def download_voice(): + # 如果响应状态码是200,则将响应内容写入本地文件 + response = client.media.download(msg.media_id) + if response.status_code == 200: + with open(self.content, "wb") as f: + f.write(response.content) + else: + logger.info(f"[wechatcom] Failed to download voice file, {response.content}") + + self._prepare_fn = download_voice + elif msg.type == "image": + self.ctype = ContextType.IMAGE + self.content = TmpDir().path() + msg.media_id + ".png" # content直接存临时目录路径 + + def download_image(): + # 如果响应状态码是200,则将响应内容写入本地文件 + response = client.media.download(msg.media_id) + if response.status_code == 200: + with open(self.content, "wb") as f: + f.write(response.content) + else: + logger.info(f"[wechatcom] Failed to download image file, {response.content}") + + self._prepare_fn = download_image + else: + raise NotImplementedError("Unsupported message type: Type:{} ".format(msg.type)) + + self.from_user_id = msg.source + self.to_user_id = msg.target + self.other_user_id = msg.source diff --git a/channel/wechatmp/active_reply.py b/channel/wechatmp/active_reply.py index 12975a5..10649cd 100644 --- a/channel/wechatmp/active_reply.py +++ b/channel/wechatmp/active_reply.py @@ -10,7 +10,7 @@ from channel.wechatmp.common import * from channel.wechatmp.wechatmp_channel import WechatMPChannel from channel.wechatmp.wechatmp_message import WeChatMPMessage from common.log import logger -from config import conf +from config import conf, subscribe_msg # This class is instantiated once per query @@ -66,13 +66,14 @@ class Query: logger.info("[wechatmp] Event {} from {}".format(msg.event, msg.source)) if msg.event in ["subscribe", "subscribe_scan"]: reply_text = subscribe_msg() - replyPost = create_reply(reply_text, msg) - return encrypt_func(replyPost.render()) + if reply_text: + replyPost = create_reply(reply_text, msg) + return encrypt_func(replyPost.render()) else: return "success" else: logger.info("暂且不处理") - return "success" + return "success" except Exception as exc: logger.exception(exc) return exc diff --git a/channel/wechatmp/common.py b/channel/wechatmp/common.py index b6f206c..e1cbe7b 100644 --- a/channel/wechatmp/common.py +++ b/channel/wechatmp/common.py @@ -1,5 +1,3 @@ -import textwrap - import web from wechatpy.crypto import WeChatCrypto from wechatpy.exceptions import InvalidSignatureException @@ -27,36 +25,3 @@ def verify_server(data): raise web.Forbidden("Invalid signature") except Exception as e: raise web.Forbidden(str(e)) - - -def subscribe_msg(): - trigger_prefix = conf().get("single_chat_prefix", [""])[0] - msg = textwrap.dedent( - f"""\ - 感谢您的关注! - 这里是ChatGPT,可以自由对话。 - 资源有限,回复较慢,请勿着急。 - 支持语音对话。 - 支持图片输入。 - 支持图片输出,画字开头的消息将按要求创作图片。 - 支持tool、角色扮演和文字冒险等丰富的插件。 - 输入'{trigger_prefix}#帮助' 查看详细指令。""" - ) - return msg - - -def split_string_by_utf8_length(string, max_length, max_split=0): - encoded = string.encode("utf-8") - start, end = 0, 0 - result = [] - while end < len(encoded): - if max_split > 0 and len(result) >= max_split: - result.append(encoded[start:].decode("utf-8")) - break - end = min(start + max_length, len(encoded)) - # 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止 - while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000: - end -= 1 - result.append(encoded[start:end].decode("utf-8")) - start = end - return result diff --git a/channel/wechatmp/passive_reply.py b/channel/wechatmp/passive_reply.py index cd0f012..d926b2a 100644 --- a/channel/wechatmp/passive_reply.py +++ b/channel/wechatmp/passive_reply.py @@ -11,7 +11,8 @@ from channel.wechatmp.common import * from channel.wechatmp.wechatmp_channel import WechatMPChannel from channel.wechatmp.wechatmp_message import WeChatMPMessage from common.log import logger -from config import conf +from common.utils import split_string_by_utf8_length +from config import conf, subscribe_msg # This class is instantiated once per query @@ -199,14 +200,14 @@ class Query: logger.info("[wechatmp] Event {} from {}".format(msg.event, msg.source)) if msg.event in ["subscribe", "subscribe_scan"]: reply_text = subscribe_msg() - replyPost = create_reply(reply_text, msg) - return encrypt_func(replyPost.render()) + if reply_text: + replyPost = create_reply(reply_text, msg) + return encrypt_func(replyPost.render()) else: return "success" - else: logger.info("暂且不处理") - return "success" + return "success" except Exception as exc: logger.exception(exc) return exc diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index aa1fc74..0c54a1d 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -18,6 +18,7 @@ from channel.wechatmp.common import * from channel.wechatmp.wechatmp_client import WechatMPClient from common.log import logger from common.singleton import singleton +from common.utils import split_string_by_utf8_length from config import conf from voice.audio_convert import any_to_mp3 @@ -140,8 +141,10 @@ class WechatMPChannel(ChatChannel): texts = split_string_by_utf8_length(reply_text, MAX_UTF8_LEN) if len(texts) > 1: logger.info("[wechatmp] text too long, split into {} parts".format(len(texts))) - for text in texts: + for i, text in enumerate(texts): self.client.message.send_text(receiver, text) + if i != len(texts) - 1: + time.sleep(0.5) # 休眠0.5秒,防止发送过快乱序 logger.info("[wechatmp] Do send text to {}: {}".format(receiver, reply_text)) elif reply.type == ReplyType.VOICE: try: diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..966a7cf --- /dev/null +++ b/common/utils.py @@ -0,0 +1,51 @@ +import io +import os + +from PIL import Image + + +def fsize(file): + if isinstance(file, io.BytesIO): + return file.getbuffer().nbytes + elif isinstance(file, str): + return os.path.getsize(file) + elif hasattr(file, "seek") and hasattr(file, "tell"): + pos = file.tell() + file.seek(0, os.SEEK_END) + size = file.tell() + file.seek(pos) + return size + else: + raise TypeError("Unsupported type") + + +def compress_imgfile(file, max_size): + if fsize(file) <= max_size: + return file + file.seek(0) + img = Image.open(file) + rgb_image = img.convert("RGB") + quality = 95 + while True: + out_buf = io.BytesIO() + rgb_image.save(out_buf, "JPEG", quality=quality) + if fsize(out_buf) <= max_size: + return out_buf + quality -= 5 + + +def split_string_by_utf8_length(string, max_length, max_split=0): + encoded = string.encode("utf-8") + start, end = 0, 0 + result = [] + while end < len(encoded): + if max_split > 0 and len(result) >= max_split: + result.append(encoded[start:].decode("utf-8")) + break + end = min(start + max_length, len(encoded)) + # 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止 + while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000: + end -= 1 + result.append(encoded[start:end].decode("utf-8")) + start = end + return result diff --git a/config-template.json b/config-template.json index 864aa03..51187c4 100644 --- a/config-template.json +++ b/config-template.json @@ -27,5 +27,6 @@ "voice_reply_voice": false, "conversation_max_tokens": 1000, "expires_in_seconds": 3600, - "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。" + "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", + "subcribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输入。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持tool、角色扮演和文字冒险等丰富的插件。\n输入{trigger_prefix}#help 查看详细指令。" } diff --git a/config.py b/config.py index b385519..ae1cfd7 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,7 @@ import pickle from common.log import logger # 将所有可用的配置项写在字典里, 请使用小写字母 +# 此处的配置值无实际意义,程序不会读取此处的配置,仅用于提示格式,请将配置加入到config.json中 available_setting = { # openai api配置 "open_ai_api_key": "", # openai api key @@ -81,10 +82,19 @@ available_setting = { "wechatmp_app_id": "", # 微信公众平台的appID "wechatmp_app_secret": "", # 微信公众平台的appsecret "wechatmp_aes_key": "", # 微信公众平台的EncodingAESKey,加密模式需要 + # wechatcom的通用配置 + "wechatcom_corp_id": "", # 企业微信公司的corpID + # wechatcomapp的配置 + "wechatcomapp_token": "", # 企业微信app的token + "wechatcomapp_port": 9898, # 企业微信app的服务端口,不需要端口转发 + "wechatcomapp_secret": "", # 企业微信app的secret + "wechatcomapp_agent_id": "", # 企业微信app的agent_id + "wechatcomapp_aes_key": "", # 企业微信app的aes_key # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 # channel配置 - "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service} + "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service,wechatcom_app} + "subscribe_msg": "", # 订阅消息, 支持: wechatmp, wechatmp_service, wechatcom_app "debug": False, # 是否开启debug模式,开启后会打印更多日志 "appdata_dir": "", # 数据目录 # 插件配置 @@ -93,8 +103,12 @@ available_setting = { class Config(dict): - def __init__(self, d: dict = {}): - super().__init__(d) + def __init__(self, d=None): + super().__init__() + if d is None: + d = {} + for k, v in d.items(): + self[k] = v # user_datas: 用户数据,key为用户名,value为用户数据,也是dict self.user_datas = {} @@ -202,3 +216,9 @@ def get_appdata_dir(): logger.info("[INIT] data path not exists, create it: {}".format(data_path)) os.makedirs(data_path) return data_path + + +def subscribe_msg(): + trigger_prefix = conf().get("single_chat_prefix", [""])[0] + msg = conf().get("subscribe_msg", "") + return msg.format(trigger_prefix=trigger_prefix) diff --git a/docs/images/aigcopen.png b/docs/images/aigcopen.png new file mode 100644 index 0000000..76a20c6 Binary files /dev/null and b/docs/images/aigcopen.png differ diff --git a/requirements-optional.txt b/requirements-optional.txt index 0bcd196..8e73bd0 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -18,7 +18,7 @@ wechaty>=0.10.7 wechaty_puppet>=0.4.23 pysilk_mod>=1.6.0 # needed by send voice -# wechatmp +# wechatmp wechatcom web.py wechatpy diff --git a/voice/audio_convert.py b/voice/audio_convert.py index 6101700..f9cc2f5 100644 --- a/voice/audio_convert.py +++ b/voice/audio_convert.py @@ -80,6 +80,20 @@ def any_to_sil(any_path, sil_path): return audio.duration_seconds * 1000 +def any_to_amr(any_path, amr_path): + """ + 把任意格式转成amr文件 + """ + if any_path.endswith(".amr"): + shutil.copy2(any_path, amr_path) + return + if any_path.endswith(".sil") or any_path.endswith(".silk") or any_path.endswith(".slk"): + raise NotImplementedError("Not support file type: {}".format(any_path)) + audio = AudioSegment.from_file(any_path) + audio = audio.set_frame_rate(8000) # only support 8000 + audio.export(amr_path, format="amr") + + def sil_to_wav(silk_path, wav_path, rate: int = 24000): """ silk 文件转 wav