From 9528183ca38f7ada07d2617e2644f001a358f121 Mon Sep 17 00:00:00 2001 From: Alone <675061370@qq.com> Date: Thu, 6 Feb 2025 10:01:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=94=AF=E6=8C=81=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E8=BD=AC=E5=AD=98=E5=8F=8A=E6=AF=8F=E6=97=A5=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=8C=E5=8F=AF=E8=87=AA=E8=A1=8C=E4=BA=8C=E5=BC=80?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +- app/qfadmin/view/source/category.html | 5 +- chat_channel.py | 549 -------------------------- chatgpt-on-wechat/chat_channel.py | 548 ------------------------- chatgpt-on-wechat/config.json | 37 -- chatgpt-on-wechat/config.py | 366 ----------------- chatgpt-on-wechat/docker-compose.yml | 11 - chatgpt-on-wechat/教程 | 13 - data.sql | 4 +- extend/quarkPlugin/QuarkPlugin.php | 2 +- public/robots.txt | 3 +- 11 files changed, 15 insertions(+), 1552 deletions(-) delete mode 100644 chat_channel.py delete mode 100644 chatgpt-on-wechat/chat_channel.py delete mode 100644 chatgpt-on-wechat/config.json delete mode 100644 chatgpt-on-wechat/config.py delete mode 100644 chatgpt-on-wechat/docker-compose.yml delete mode 100644 chatgpt-on-wechat/教程 diff --git a/README.md b/README.md index 9551656..97c934c 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,6 @@ 注:鉴于数据库改动大,最快升级方式:1、重新搭建新项目;2、旧项目后台导出资源表格;3、新项目导入这个表格 -## 演示 - -[前端体验](https://www.xinyueso.com) - - - - ## 后台安装教程 0、PHP(选择7.2,其它版本不兼容) @@ -76,32 +69,26 @@ location / { ## 前台截图 -![image](https://www.xinyueso.com/github/p3.png) +![image](github/p3.png) -![image](https://www.xinyueso.com/github/p2.png) +![image](github/p2.png) -![image](https://www.xinyueso.com/github/p1.png) +![image](github/p1.png) ## 后台管理截图 -1、一键转存他人链接:就是将别人的分享链接转为你自己的 +![image](github/1.png) -2、转存心悦搜剧资源:就是将心悦搜剧平台上的所有资源都转成你自己的 +![image](github/2.png) -3、每日自动更新:自动转存每天的资源并入库 - -![image](https://www.xinyueso.com/1.png) - -![image](https://www.xinyueso.com/2.png) - -![image](https://www.xinyueso.com/3.png) +![image](github/3.png) ## 如何获取夸克网盘Cookie 登录夸克网盘后,按下F12,刷新页面 -![image](https://www.xinyueso.com/cookie.jpg) +![image](github/cookie.jpg) # 免费交流社群 @@ -110,7 +97,7 @@ location / { 请加微信l1417716300 -| | | +| | | | --- | --- | 程序使用不收费,但不负责搭建,搭建过程遇到问题,可以私聊或群里咨询 diff --git a/app/qfadmin/view/source/category.html b/app/qfadmin/view/source/category.html index 6343bd8..104405f 100644 --- a/app/qfadmin/view/source/category.html +++ b/app/qfadmin/view/source/category.html @@ -93,6 +93,7 @@
+ 功能暂停使用

自动更新:转存当日及昨天的资源数据;

将此接口添加到计划任务中,计划任务每日执行一次即可;

接口地址:{{domain}}/api/source/day

@@ -104,10 +105,10 @@
-

全部转存:一键转存心悦搜索所有资源到自己的网盘及系统中

+ 功能暂停使用 +

全部转存:一键转存所有资源到自己的网盘及系统中

全部转存速度比较慢,提交后请耐心等待;名称重复的资源会跳过转存;

全部转存需要按类别转存:见下方列表一键转存按钮

-

Tips:心悦搜索:https://www.xinyueso.com;

diff --git a/chat_channel.py b/chat_channel.py deleted file mode 100644 index bc29734..0000000 --- a/chat_channel.py +++ /dev/null @@ -1,549 +0,0 @@ -import os -import re -import threading -import time -import requests -from asyncio import CancelledError -from concurrent.futures import Future, ThreadPoolExecutor - -from bridge.context import * -from bridge.reply import * -from channel.channel import Channel -from common.dequeue import Dequeue -from common import memory -from plugins import * - -try: - from voice.audio_convert import any_to_wav -except Exception as e: - pass - -handler_pool = ThreadPoolExecutor(max_workers=8) # 处理消息的线程池 - - -# 抽象类, 它包含了与消息通道无关的通用处理逻辑 -class ChatChannel(Channel): - name = None # 登录的用户名 - user_id = None # 登录的用户id - futures = {} # 记录每个session_id提交到线程池的future对象, 用于重置会话时把没执行的future取消掉,正在执行的不会被取消 - sessions = {} # 用于控制并发,每个session_id同时只能有一个context在处理 - lock = threading.Lock() # 用于控制对sessions的访问 - - def __init__(self): - _thread = threading.Thread(target=self.consume) - _thread.setDaemon(True) - _thread.start() - - # 根据消息构造context,消息内容相关的触发项写在这里 - def _compose_context(self, ctype: ContextType, content, **kwargs): - context = Context(ctype, content) - context.kwargs = kwargs - # context首次传入时,origin_ctype是None, - # 引入的起因是:当输入语音时,会嵌套生成两个context,第一步语音转文本,第二步通过文本生成文字回复。 - # origin_ctype用于第二步文本回复时,判断是否需要匹配前缀,如果是私聊的语音,就不需要匹配前缀 - if "origin_ctype" not in context: - context["origin_ctype"] = ctype - # context首次传入时,receiver是None,根据类型设置receiver - first_in = "receiver" not in context - # 群名匹配过程,设置session_id和receiver - if first_in: # context首次传入时,receiver是None,根据类型设置receiver - config = conf() - cmsg = context["msg"] - user_data = conf().get_user_data(cmsg.from_user_id) - context["openai_api_key"] = user_data.get("openai_api_key") - context["gpt_model"] = user_data.get("gpt_model") - if context.get("isgroup", False): - group_name = cmsg.other_user_nickname - group_id = cmsg.other_user_id - - group_name_white_list = config.get("group_name_white_list", []) - group_name_keyword_white_list = config.get("group_name_keyword_white_list", []) - if any( - [ - group_name in group_name_white_list, - "ALL_GROUP" in group_name_white_list, - check_contain(group_name, group_name_keyword_white_list), - ] - ): - group_chat_in_one_session = conf().get("group_chat_in_one_session", []) - session_id = cmsg.actual_user_id - if any( - [ - group_name in group_chat_in_one_session, - "ALL_GROUP" in group_chat_in_one_session, - ] - ): - session_id = group_id - else: - logger.debug(f"No need reply, groupName not in whitelist, group_name={group_name}") - return None - context["session_id"] = session_id - context["receiver"] = group_id - else: - context["session_id"] = cmsg.other_user_id - context["receiver"] = cmsg.other_user_id - e_context = PluginManager().emit_event(EventContext(Event.ON_RECEIVE_MESSAGE, {"channel": self, "context": context})) - context = e_context["context"] - if e_context.is_pass() or context is None: - return context - if cmsg.from_user_id == self.user_id and not config.get("trigger_by_self", True): - logger.debug("[chat_channel]self message skipped") - return None - - # 消息内容匹配过程,并处理content - if ctype == ContextType.TEXT: - if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息 - logger.debug(content) - logger.debug("[chat_channel]reference query skipped") - return None - - pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content) - if isinstance(context["msg"].at_list, list): - for at in context["msg"].at_list: - pattern = f"@{re.escape(at)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content_search) - if content_search == content and context["msg"].self_display_name: - # 前缀移除后没有变化,使用群昵称再次移除 - pattern = f"@{re.escape(context['msg'].self_display_name)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content) - - - # 去除字符串开头和结尾的所有空格字符 - content_search = content_search.strip() - # logger.info("[来消息了] content={}, content_search={}".format(content, content_search)) - content_search = process_string(content_search) - - - nick_name_black_list = conf().get("nick_name_black_list", []) - if context.get("isgroup", False): # 群聊 - - if any(content_search.startswith(prefix) for prefix in ["搜剧", "搜", "全网搜"]) and not content_search.startswith("搜索"): - content_search = process_string2(content_search) - user_nickname = context['msg'].actual_user_nickname - reply_text = f"@{user_nickname}" - - - contentSearch = remove_prefix(content_search, ["搜剧", "搜", "全网搜"]).strip() - - def perform_search(): - # 初次搜索 - response_data = search_question(contentSearch) if not content_search.startswith("全网搜") else [] - if not response_data: - # 通知用户深入搜索 - reply_text2 = f"@{user_nickname}\n正在深入搜索,请稍等..." - self._send_reply(context, Reply(ReplyType.TEXT, reply_text2)) - - # 启动线程进行第二次搜索 - def perform_second_search(): - response_data = search_alone(contentSearch) - send_final_reply(response_data, reply_text, context) - - second_search_thread = threading.Thread(target=perform_second_search) - second_search_thread.start() - else: - # 如果第一次搜索找到结果,发送最终回复 - send_final_reply(response_data, reply_text, context) - - def send_final_reply(response_data, reply_text, context): - is_times = 0 - if not response_data: - reply_text_final = f"{reply_text}\n未找到,可换个关键词尝试哦~" - reply_text_final += "\n⚠️宁少写,不多写、错写~" - reply_text_final += "\n--------------------" - reply_text_final += "\n可访问以下链接提交资源需求" - reply_text_final += "\nhttps://www.xinyueso.com" - # reply_text_final += "\n--------------------" - # reply_text_final += "\nGPT小助手分享" - # reply_text_final += "\n--------------------" - # reply_text_final += "\nhttps://chat.xinyuedh.com" - else: - reply_text_final = f"{reply_text}\n--------------------" - for item in response_data: - if item.get('is_time') == 1: - reply_text_final += f"\n 🌐️ {item.get('title', '未知标题')}" - is_times += 1 - else: - reply_text_final += f"\n{item.get('title', '未知标题')}" - reply_text_final += f"\n{item.get('url', '未知URL')}" - reply_text_final += "\n--------------------" - - if is_times > 0: - reply_text_final += "\n 🌐️资源来源网络,30分钟后删除" - reply_text_final += "\n--------------------" - else: - reply_text_final += "\n 不是短剧?请尝试:全网搜XX" - reply_text_final += "\n--------------------" - - reply_text_final += "\n欢迎观看!如果喜欢可以喊你的朋友一起来哦" - - reply = Reply(ReplyType.TEXT, reply_text_final) - self._send_reply(context, reply) - - - # 启动线程执行第一次搜索 - first_search_thread = threading.Thread(target=perform_search) - first_search_thread.start() - return None - - # 校验关键字 - match_prefix = check_prefix(content, conf().get("group_chat_prefix")) - match_contain = check_contain(content, conf().get("group_chat_keyword")) - flag = False - if context["msg"].to_user_id != context["msg"].actual_user_id: - if match_prefix is not None or match_contain is not None: - flag = True - if match_prefix: - content = content.replace(match_prefix, "", 1).strip() - if context["msg"].is_at: - nick_name = context["msg"].actual_user_nickname - if nick_name and nick_name in nick_name_black_list: - # 黑名单过滤 - logger.warning(f"[chat_channel] Nickname {nick_name} in In BlackList, ignore") - return None - - logger.info("[chat_channel]receive group at") - if not conf().get("group_at_off", False): - flag = True - pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" - subtract_res = re.sub(pattern, r"", content) - if subtract_res.startswith("画"): - subtract_res = "生成图片要求如下:\n" + subtract_res[1:] - - 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)" - subtract_res = re.sub(pattern, r"", content) - content = subtract_res - if not flag: - if context["origin_ctype"] == ContextType.VOICE: - logger.info("[chat_channel]receive group voice, but checkprefix didn't match") - return None - else: # 单聊 - nick_name = context["msg"].from_user_nickname - if nick_name and nick_name in nick_name_black_list: - # 黑名单过滤 - logger.warning(f"[chat_channel] Nickname '{nick_name}' in In BlackList, ignore") - return None - - match_prefix = check_prefix(content, conf().get("single_chat_prefix", [""])) - if match_prefix is not None: # 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容 - content = content.replace(match_prefix, "", 1).strip() - elif context["origin_ctype"] == ContextType.VOICE: # 如果源消息是私聊的语音消息,允许不匹配前缀,放宽条件 - pass - else: - return None - content = content.strip() - img_match_prefix = check_prefix(content, conf().get("image_create_prefix",[""])) - if img_match_prefix: - content = content.replace(img_match_prefix, "", 1) - context.type = ContextType.IMAGE_CREATE - else: - context.type = ContextType.TEXT - context.content = content.strip() - if "desire_rtype" not in context and conf().get("always_reply_voice") and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - context["desire_rtype"] = ReplyType.VOICE - elif context.type == ContextType.VOICE: - if "desire_rtype" not in context and conf().get("voice_reply_voice") and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - context["desire_rtype"] = ReplyType.VOICE - return context - - def _handle(self, context: Context): - if context is None or not context.content: - return - logger.debug("[chat_channel] ready to handle context: {}".format(context)) - # reply的构建步骤 - reply = self._generate_reply(context) - - logger.debug("[chat_channel] ready to decorate reply: {}".format(reply)) - - # reply的包装步骤 - if reply and reply.content: - reply = self._decorate_reply(context, reply) - - # reply的发送步骤 - self._send_reply(context, reply) - - def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_HANDLE_CONTEXT, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - if not e_context.is_pass(): - logger.debug("[chat_channel] ready to handle context: type={}, content={}".format(context.type, context.content)) - if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息 - context["channel"] = e_context["channel"] - reply = super().build_reply_content(context.content, context) - elif context.type == ContextType.VOICE: # 语音消息 - cmsg = context["msg"] - cmsg.prepare() - file_path = context.content - wav_path = os.path.splitext(file_path)[0] + ".wav" - try: - any_to_wav(file_path, wav_path) - except Exception as e: # 转换失败,直接使用mp3,对于某些api,mp3也可以识别 - logger.warning("[chat_channel]any to wav error, use raw path. " + str(e)) - wav_path = file_path - # 语音识别 - reply = super().build_voice_to_text(wav_path) - # 删除临时文件 - try: - os.remove(file_path) - if wav_path != file_path: - os.remove(wav_path) - except Exception as e: - pass - # logger.warning("[chat_channel]delete temp file error: " + str(e)) - - if reply.type == ReplyType.TEXT: - new_context = self._compose_context(ContextType.TEXT, reply.content, **context.kwargs) - if new_context: - reply = self._generate_reply(new_context) - else: - return - elif context.type == ContextType.IMAGE: # 图片消息,当前仅做下载保存到本地的逻辑 - memory.USER_IMAGE_CACHE[context["session_id"]] = { - "path": context.content, - "msg": context.get("msg") - } - elif context.type == ContextType.SHARING: # 分享信息,当前无默认逻辑 - pass - elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑 - pass - else: - logger.warning("[chat_channel] unknown context type: {}".format(context.type)) - return - return reply - - def _decorate_reply(self, context: Context, reply: Reply) -> Reply: - if reply and reply.type: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_DECORATE_REPLY, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - desire_rtype = context.get("desire_rtype") - if not e_context.is_pass() and reply and reply.type: - if reply.type in self.NOT_SUPPORT_REPLYTYPE: - logger.error("[chat_channel]reply type not support: " + str(reply.type)) - reply.type = ReplyType.ERROR - reply.content = "不支持发送的消息类型: " + str(reply.type) - - if reply.type == ReplyType.TEXT: - reply_text = reply.content - if desire_rtype == ReplyType.VOICE and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - reply = super().build_text_to_voice(reply.content) - return self._decorate_reply(context, reply) - if context.get("isgroup", False): - if not context.get("no_need_at", False): - reply_text = "@" + context["msg"].actual_user_nickname + "\n" + reply_text.strip() - reply_text = conf().get("group_chat_reply_prefix", "") + reply_text + conf().get("group_chat_reply_suffix", "") - else: - reply_text = conf().get("single_chat_reply_prefix", "") + reply_text + conf().get("single_chat_reply_suffix", "") - reply.content = reply_text - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - reply.content = "[" + str(reply.type) + "]\n" + reply.content - elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE or reply.type == ReplyType.FILE or reply.type == ReplyType.VIDEO or reply.type == ReplyType.VIDEO_URL: - pass - else: - logger.error("[chat_channel] unknown reply type: {}".format(reply.type)) - return - if desire_rtype and desire_rtype != reply.type and reply.type not in [ReplyType.ERROR, ReplyType.INFO]: - logger.warning("[chat_channel] desire_rtype: {}, but reply type: {}".format(context.get("desire_rtype"), reply.type)) - return reply - - def _send_reply(self, context: Context, reply: Reply): - if reply and reply.type: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_SEND_REPLY, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - if not e_context.is_pass() and reply and reply.type: - logger.debug("[chat_channel] ready to send reply: {}, context: {}".format(reply, context)) - self._send(reply, context) - - def _send(self, reply: Reply, context: Context, retry_cnt=0): - try: - self.send(reply, context) - except Exception as e: - logger.error("[chat_channel] sendMsg error: {}".format(str(e))) - if isinstance(e, NotImplementedError): - return - logger.exception(e) - if retry_cnt < 2: - time.sleep(3 + 3 * retry_cnt) - self._send(reply, context, retry_cnt + 1) - - def _success_callback(self, session_id, **kwargs): # 线程正常结束时的回调函数 - logger.debug("Worker return success, session_id = {}".format(session_id)) - - def _fail_callback(self, session_id, exception, **kwargs): # 线程异常结束时的回调函数 - logger.exception("Worker return exception: {}".format(exception)) - - def _thread_pool_callback(self, session_id, **kwargs): - def func(worker: Future): - try: - worker_exception = worker.exception() - if worker_exception: - self._fail_callback(session_id, exception=worker_exception, **kwargs) - else: - self._success_callback(session_id, **kwargs) - except CancelledError as e: - logger.info("Worker cancelled, session_id = {}".format(session_id)) - except Exception as e: - logger.exception("Worker raise exception: {}".format(e)) - with self.lock: - self.sessions[session_id][1].release() - - return func - - def produce(self, context: Context): - session_id = context["session_id"] - with self.lock: - if session_id not in self.sessions: - self.sessions[session_id] = [ - Dequeue(), - threading.BoundedSemaphore(conf().get("concurrency_in_session", 4)), - ] - if context.type == ContextType.TEXT and context.content.startswith("#"): - self.sessions[session_id][0].putleft(context) # 优先处理管理命令 - else: - self.sessions[session_id][0].put(context) - - # 消费者函数,单独线程,用于从消息队列中取出消息并处理 - def consume(self): - while True: - with self.lock: - session_ids = list(self.sessions.keys()) - for session_id in session_ids: - context_queue, semaphore = self.sessions[session_id] - if semaphore.acquire(blocking=False): # 等线程处理完毕才能删除 - if not context_queue.empty(): - context = context_queue.get() - logger.debug("[chat_channel] consume context: {}".format(context)) - future: Future = handler_pool.submit(self._handle, context) - future.add_done_callback(self._thread_pool_callback(session_id, context=context)) - if session_id not in self.futures: - self.futures[session_id] = [] - self.futures[session_id].append(future) - elif semaphore._initial_value == semaphore._value + 1: # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕 - self.futures[session_id] = [t for t in self.futures[session_id] if not t.done()] - assert len(self.futures[session_id]) == 0, "thread pool error" - del self.sessions[session_id] - else: - semaphore.release() - time.sleep(0.1) - - # 取消session_id对应的所有任务,只能取消排队的消息和已提交线程池但未执行的任务 - def cancel_session(self, session_id): - with self.lock: - if session_id in self.sessions: - for future in self.futures[session_id]: - future.cancel() - cnt = self.sessions[session_id][0].qsize() - if cnt > 0: - logger.info("Cancel {} messages in session {}".format(cnt, session_id)) - self.sessions[session_id][0] = Dequeue() - - def cancel_all_session(self): - with self.lock: - for session_id in self.sessions: - for future in self.futures[session_id]: - future.cancel() - cnt = self.sessions[session_id][0].qsize() - if cnt > 0: - logger.info("Cancel {} messages in session {}".format(cnt, session_id)) - self.sessions[session_id][0] = Dequeue() - - -def check_prefix(content, prefix_list): - if not prefix_list: - return None - for prefix in prefix_list: - if content.startswith(prefix): - return prefix - return None - - -def check_contain(content, keyword_list): - if not keyword_list: - return None - for ky in keyword_list: - if content.find(ky) != -1: - return True - return None - - -def remove_prefix(content, prefixes): - for prefix in prefixes: - if content.startswith(prefix): - return content[len(prefix):].strip() - return content.strip() - - - -def process_string(s): - # 判断是否以@开头并且包含"搜"字 - if s.startswith('@') and '搜' in s: - # 找到"搜"字的位置 - index = s.index('搜') - # 去除"搜"字前面的内容 - return s[index:] - else: - return s - -def process_string2(s): - # 判断是否包含@ - if '@' in s: - # 找到@字符的位置 - index = s.index('@') - # 删除包含@在内后面的所有字符 - return s[:index] - else: - return s - - - -def search_question(question): - url = 'https://www.xinyueso.com/api/search' - params = { - 'is_time': '1', - 'page_no': '1', - 'page_size': '5', - 'title': question - } - try: - response = requests.get(url, params=params) - response.raise_for_status() # 检查请求是否成功 - responseData = response.json().get('data', {}).get('items', []) - return responseData - except requests.exceptions.RequestException as e: - print(f"Error fetching data: {e}") - return [] - -def search_alone(question): - url = 'https://www.xinyueso.com/api/other/all_search' - payload = { - 'title': question - } - try: - response = requests.post(url, json=payload) - response.raise_for_status() - responseData = response.json().get('data', []) - return responseData - except requests.exceptions.RequestException as e: - print(f"Error fetching data: {e}") - return [] - diff --git a/chatgpt-on-wechat/chat_channel.py b/chatgpt-on-wechat/chat_channel.py deleted file mode 100644 index 1a9b7b7..0000000 --- a/chatgpt-on-wechat/chat_channel.py +++ /dev/null @@ -1,548 +0,0 @@ -import os -import re -import threading -import time -import requests -from asyncio import CancelledError -from concurrent.futures import Future, ThreadPoolExecutor - -from bridge.context import * -from bridge.reply import * -from channel.channel import Channel -from common.dequeue import Dequeue -from common import memory -from plugins import * - -try: - from voice.audio_convert import any_to_wav -except Exception as e: - pass - -handler_pool = ThreadPoolExecutor(max_workers=8) # 处理消息的线程池 - -# 抽象类, 它包含了与消息通道无关的通用处理逻辑 -class ChatChannel(Channel): - name = None # 登录的用户名 - user_id = None # 登录的用户id - futures = {} # 记录每个session_id提交到线程池的future对象, 用于重置会话时把没执行的future取消掉,正在执行的不会被取消 - sessions = {} # 用于控制并发,每个session_id同时只能有一个context在处理 - lock = threading.Lock() # 用于控制对sessions的访问 - - def __init__(self): - _thread = threading.Thread(target=self.consume) - _thread.setDaemon(True) - _thread.start() - - # 根据消息构造context,消息内容相关的触发项写在这里 - def _compose_context(self, ctype: ContextType, content, **kwargs): - context = Context(ctype, content) - context.kwargs = kwargs - # context首次传入时,origin_ctype是None, - # 引入的起因是:当输入语音时,会嵌套生成两个context,第一步语音转文本,第二步通过文本生成文字回复。 - # origin_ctype用于第二步文本回复时,判断是否需要匹配前缀,如果是私聊的语音,就不需要匹配前缀 - if "origin_ctype" not in context: - context["origin_ctype"] = ctype - # context首次传入时,receiver是None,根据类型设置receiver - first_in = "receiver" not in context - # 群名匹配过程,设置session_id和receiver - if first_in: # context首次传入时,receiver是None,根据类型设置receiver - config = conf() - cmsg = context["msg"] - user_data = conf().get_user_data(cmsg.from_user_id) - context["openai_api_key"] = user_data.get("openai_api_key") - context["gpt_model"] = user_data.get("gpt_model") - if context.get("isgroup", False): - group_name = cmsg.other_user_nickname - group_id = cmsg.other_user_id - - group_name_white_list = config.get("group_name_white_list", []) - group_name_keyword_white_list = config.get("group_name_keyword_white_list", []) - if any( - [ - group_name in group_name_white_list, - "ALL_GROUP" in group_name_white_list, - check_contain(group_name, group_name_keyword_white_list), - ] - ): - group_chat_in_one_session = conf().get("group_chat_in_one_session", []) - session_id = cmsg.actual_user_id - if any( - [ - group_name in group_chat_in_one_session, - "ALL_GROUP" in group_chat_in_one_session, - ] - ): - session_id = group_id - else: - logger.debug(f"No need reply, groupName not in whitelist, group_name={group_name}") - return None - context["session_id"] = session_id - context["receiver"] = group_id - else: - context["session_id"] = cmsg.other_user_id - context["receiver"] = cmsg.other_user_id - e_context = PluginManager().emit_event(EventContext(Event.ON_RECEIVE_MESSAGE, {"channel": self, "context": context})) - context = e_context["context"] - if e_context.is_pass() or context is None: - return context - if cmsg.from_user_id == self.user_id and not config.get("trigger_by_self", True): - logger.debug("[chat_channel]self message skipped") - return None - - # 消息内容匹配过程,并处理content - if ctype == ContextType.TEXT: - if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息 - logger.debug(content) - logger.debug("[chat_channel]reference query skipped") - return None - - pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content) - if isinstance(context["msg"].at_list, list): - for at in context["msg"].at_list: - pattern = f"@{re.escape(at)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content_search) - if content_search == content and context["msg"].self_display_name: - # 前缀移除后没有变化,使用群昵称再次移除 - pattern = f"@{re.escape(context['msg'].self_display_name)}(\u2005|\u0020)" - content_search = re.sub(pattern, r"", content) - - - # 去除字符串开头和结尾的所有空格字符 - content_search = content_search.strip() - # logger.info("[来消息了] content={}, content_search={}".format(content, content_search)) - content_search = process_string(content_search) - - - nick_name_black_list = conf().get("nick_name_black_list", []) - if context.get("isgroup", False): # 群聊 - - if any(content_search.startswith(prefix) for prefix in ["搜剧", "搜", "全网搜"]) and not content_search.startswith("搜索"): - content_search = process_string2(content_search) - user_nickname = context['msg'].actual_user_nickname - reply_text = f"@{user_nickname}" - - - contentSearch = remove_prefix(content_search, ["搜剧", "搜", "全网搜"]).strip() - - def perform_search(): - # 初次搜索 - response_data = search_question(contentSearch) if not content_search.startswith("全网搜") else [] - if not response_data: - # 通知用户深入搜索 - reply_text2 = f"@{user_nickname}\n正在深入搜索,请稍等..." - self._send_reply(context, Reply(ReplyType.TEXT, reply_text2)) - - # 启动线程进行第二次搜索 - def perform_second_search(): - response_data = search_alone(contentSearch) - send_final_reply(response_data, reply_text, context) - - second_search_thread = threading.Thread(target=perform_second_search) - second_search_thread.start() - else: - # 如果第一次搜索找到结果,发送最终回复 - send_final_reply(response_data, reply_text, context) - - def send_final_reply(response_data, reply_text, context): - is_times = 0 - if not response_data: - reply_text_final = f"{reply_text}\n未找到,可换个关键词尝试哦~" - reply_text_final += "\n⚠️宁少写,不多写、错写~" - # reply_text_final += "\n--------------------" - # reply_text_final += "\n可访问以下链接提交资源需求" - # reply_text_final += "\nhttps://www.xinyueso.com" - # reply_text_final += "\n--------------------" - # reply_text_final += "\nGPT小助手分享" - # reply_text_final += "\n--------------------" - # reply_text_final += "\nhttps://chat.xinyuedh.com" - else: - reply_text_final = f"{reply_text}\n--------------------" - for item in response_data: - if item.get('is_time') == 1: - reply_text_final += f"\n 🌐️ {item.get('title', '未知标题')}" - is_times += 1 - else: - reply_text_final += f"\n{item.get('title', '未知标题')}" - reply_text_final += f"\n{item.get('url', '未知URL')}" - reply_text_final += "\n--------------------" - - if is_times > 0: - reply_text_final += "\n 🌐️资源来源网络,30分钟后删除" - reply_text_final += "\n--------------------" - else: - reply_text_final += "\n 不是短剧?请尝试:全网搜XX" - reply_text_final += "\n--------------------" - - reply_text_final += "\n欢迎观看!如果喜欢可以喊你的朋友一起来哦" - - reply = Reply(ReplyType.TEXT, reply_text_final) - self._send_reply(context, reply) - - - # 启动线程执行第一次搜索 - first_search_thread = threading.Thread(target=perform_search) - first_search_thread.start() - return None - - # 校验关键字 - match_prefix = check_prefix(content, conf().get("group_chat_prefix")) - match_contain = check_contain(content, conf().get("group_chat_keyword")) - flag = False - if context["msg"].to_user_id != context["msg"].actual_user_id: - if match_prefix is not None or match_contain is not None: - flag = True - if match_prefix: - content = content.replace(match_prefix, "", 1).strip() - if context["msg"].is_at: - nick_name = context["msg"].actual_user_nickname - if nick_name and nick_name in nick_name_black_list: - # 黑名单过滤 - logger.warning(f"[chat_channel] Nickname {nick_name} in In BlackList, ignore") - return None - - logger.info("[chat_channel]receive group at") - if not conf().get("group_at_off", False): - flag = True - self.name = self.name if self.name is not None else "" # 部分渠道self.name可能没有赋值 - pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" - subtract_res = re.sub(pattern, r"", content) - 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)" - subtract_res = re.sub(pattern, r"", content) - content = subtract_res - if not flag: - if context["origin_ctype"] == ContextType.VOICE: - logger.info("[chat_channel]receive group voice, but checkprefix didn't match") - return None - else: # 单聊 - nick_name = context["msg"].from_user_nickname - if nick_name and nick_name in nick_name_black_list: - # 黑名单过滤 - logger.warning(f"[chat_channel] Nickname '{nick_name}' in In BlackList, ignore") - return None - - match_prefix = check_prefix(content, conf().get("single_chat_prefix", [""])) - if match_prefix is not None: # 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容 - content = content.replace(match_prefix, "", 1).strip() - elif context["origin_ctype"] == ContextType.VOICE: # 如果源消息是私聊的语音消息,允许不匹配前缀,放宽条件 - pass - else: - return None - content = content.strip() - img_match_prefix = check_prefix(content, conf().get("image_create_prefix",[""])) - if img_match_prefix: - content = content.replace(img_match_prefix, "", 1) - context.type = ContextType.IMAGE_CREATE - else: - context.type = ContextType.TEXT - context.content = content.strip() - if "desire_rtype" not in context and conf().get("always_reply_voice") and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - context["desire_rtype"] = ReplyType.VOICE - elif context.type == ContextType.VOICE: - if "desire_rtype" not in context and conf().get("voice_reply_voice") and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - context["desire_rtype"] = ReplyType.VOICE - return context - - def _handle(self, context: Context): - if context is None or not context.content: - return - logger.debug("[chat_channel] ready to handle context: {}".format(context)) - # reply的构建步骤 - reply = self._generate_reply(context) - - logger.debug("[chat_channel] ready to decorate reply: {}".format(reply)) - - # reply的包装步骤 - if reply and reply.content: - reply = self._decorate_reply(context, reply) - - # reply的发送步骤 - self._send_reply(context, reply) - - def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_HANDLE_CONTEXT, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - if not e_context.is_pass(): - logger.debug("[chat_channel] ready to handle context: type={}, content={}".format(context.type, context.content)) - if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息 - context["channel"] = e_context["channel"] - reply = super().build_reply_content(context.content, context) - elif context.type == ContextType.VOICE: # 语音消息 - cmsg = context["msg"] - cmsg.prepare() - file_path = context.content - wav_path = os.path.splitext(file_path)[0] + ".wav" - try: - any_to_wav(file_path, wav_path) - except Exception as e: # 转换失败,直接使用mp3,对于某些api,mp3也可以识别 - logger.warning("[chat_channel]any to wav error, use raw path. " + str(e)) - wav_path = file_path - # 语音识别 - reply = super().build_voice_to_text(wav_path) - # 删除临时文件 - try: - os.remove(file_path) - if wav_path != file_path: - os.remove(wav_path) - except Exception as e: - pass - # logger.warning("[chat_channel]delete temp file error: " + str(e)) - - if reply.type == ReplyType.TEXT: - new_context = self._compose_context(ContextType.TEXT, reply.content, **context.kwargs) - if new_context: - reply = self._generate_reply(new_context) - else: - return - elif context.type == ContextType.IMAGE: # 图片消息,当前仅做下载保存到本地的逻辑 - memory.USER_IMAGE_CACHE[context["session_id"]] = { - "path": context.content, - "msg": context.get("msg") - } - elif context.type == ContextType.SHARING: # 分享信息,当前无默认逻辑 - pass - elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑 - pass - else: - logger.warning("[chat_channel] unknown context type: {}".format(context.type)) - return - return reply - - def _decorate_reply(self, context: Context, reply: Reply) -> Reply: - if reply and reply.type: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_DECORATE_REPLY, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - desire_rtype = context.get("desire_rtype") - if not e_context.is_pass() and reply and reply.type: - if reply.type in self.NOT_SUPPORT_REPLYTYPE: - logger.error("[chat_channel]reply type not support: " + str(reply.type)) - reply.type = ReplyType.ERROR - reply.content = "不支持发送的消息类型: " + str(reply.type) - - if reply.type == ReplyType.TEXT: - reply_text = reply.content - if desire_rtype == ReplyType.VOICE and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE: - reply = super().build_text_to_voice(reply.content) - return self._decorate_reply(context, reply) - if context.get("isgroup", False): - if not context.get("no_need_at", False): - reply_text = "@" + context["msg"].actual_user_nickname + "\n" + reply_text.strip() - reply_text = conf().get("group_chat_reply_prefix", "") + reply_text + conf().get("group_chat_reply_suffix", "") - else: - reply_text = conf().get("single_chat_reply_prefix", "") + reply_text + conf().get("single_chat_reply_suffix", "") - reply.content = reply_text - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - reply.content = "[" + str(reply.type) + "]\n" + reply.content - elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE or reply.type == ReplyType.FILE or reply.type == ReplyType.VIDEO or reply.type == ReplyType.VIDEO_URL: - pass - else: - logger.error("[chat_channel] unknown reply type: {}".format(reply.type)) - return - if desire_rtype and desire_rtype != reply.type and reply.type not in [ReplyType.ERROR, ReplyType.INFO]: - logger.warning("[chat_channel] desire_rtype: {}, but reply type: {}".format(context.get("desire_rtype"), reply.type)) - return reply - - def _send_reply(self, context: Context, reply: Reply): - if reply and reply.type: - e_context = PluginManager().emit_event( - EventContext( - Event.ON_SEND_REPLY, - {"channel": self, "context": context, "reply": reply}, - ) - ) - reply = e_context["reply"] - if not e_context.is_pass() and reply and reply.type: - logger.debug("[chat_channel] ready to send reply: {}, context: {}".format(reply, context)) - self._send(reply, context) - - def _send(self, reply: Reply, context: Context, retry_cnt=0): - try: - self.send(reply, context) - except Exception as e: - logger.error("[chat_channel] sendMsg error: {}".format(str(e))) - if isinstance(e, NotImplementedError): - return - logger.exception(e) - if retry_cnt < 2: - time.sleep(3 + 3 * retry_cnt) - self._send(reply, context, retry_cnt + 1) - - def _success_callback(self, session_id, **kwargs): # 线程正常结束时的回调函数 - logger.debug("Worker return success, session_id = {}".format(session_id)) - - def _fail_callback(self, session_id, exception, **kwargs): # 线程异常结束时的回调函数 - logger.exception("Worker return exception: {}".format(exception)) - - def _thread_pool_callback(self, session_id, **kwargs): - def func(worker: Future): - try: - worker_exception = worker.exception() - if worker_exception: - self._fail_callback(session_id, exception=worker_exception, **kwargs) - else: - self._success_callback(session_id, **kwargs) - except CancelledError as e: - logger.info("Worker cancelled, session_id = {}".format(session_id)) - except Exception as e: - logger.exception("Worker raise exception: {}".format(e)) - with self.lock: - self.sessions[session_id][1].release() - - return func - - def produce(self, context: Context): - session_id = context["session_id"] - with self.lock: - if session_id not in self.sessions: - self.sessions[session_id] = [ - Dequeue(), - threading.BoundedSemaphore(conf().get("concurrency_in_session", 4)), - ] - if context.type == ContextType.TEXT and context.content.startswith("#"): - self.sessions[session_id][0].putleft(context) # 优先处理管理命令 - else: - self.sessions[session_id][0].put(context) - - # 消费者函数,单独线程,用于从消息队列中取出消息并处理 - def consume(self): - while True: - with self.lock: - session_ids = list(self.sessions.keys()) - for session_id in session_ids: - with self.lock: - context_queue, semaphore = self.sessions[session_id] - if semaphore.acquire(blocking=False): # 等线程处理完毕才能删除 - if not context_queue.empty(): - context = context_queue.get() - logger.debug("[chat_channel] consume context: {}".format(context)) - future: Future = handler_pool.submit(self._handle, context) - future.add_done_callback(self._thread_pool_callback(session_id, context=context)) - with self.lock: - if session_id not in self.futures: - self.futures[session_id] = [] - self.futures[session_id].append(future) - elif semaphore._initial_value == semaphore._value + 1: # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕 - with self.lock: - self.futures[session_id] = [t for t in self.futures[session_id] if not t.done()] - assert len(self.futures[session_id]) == 0, "thread pool error" - del self.sessions[session_id] - else: - semaphore.release() - time.sleep(0.2) - - # 取消session_id对应的所有任务,只能取消排队的消息和已提交线程池但未执行的任务 - def cancel_session(self, session_id): - with self.lock: - if session_id in self.sessions: - for future in self.futures[session_id]: - future.cancel() - cnt = self.sessions[session_id][0].qsize() - if cnt > 0: - logger.info("Cancel {} messages in session {}".format(cnt, session_id)) - self.sessions[session_id][0] = Dequeue() - - def cancel_all_session(self): - with self.lock: - for session_id in self.sessions: - for future in self.futures[session_id]: - future.cancel() - cnt = self.sessions[session_id][0].qsize() - if cnt > 0: - logger.info("Cancel {} messages in session {}".format(cnt, session_id)) - self.sessions[session_id][0] = Dequeue() - - -def check_prefix(content, prefix_list): - if not prefix_list: - return None - for prefix in prefix_list: - if content.startswith(prefix): - return prefix - return None - - -def check_contain(content, keyword_list): - if not keyword_list: - return None - for ky in keyword_list: - if content.find(ky) != -1: - return True - return None - - -def remove_prefix(content, prefixes): - for prefix in prefixes: - if content.startswith(prefix): - return content[len(prefix):].strip() - return content.strip() - - - -def process_string(s): - # 判断是否以@开头并且包含"搜"字 - if s.startswith('@') and '搜' in s: - # 找到"搜"字的位置 - index = s.index('搜') - # 去除"搜"字前面的内容 - return s[index:] - else: - return s - -def process_string2(s): - # 判断是否包含@ - if '@' in s: - # 找到@字符的位置 - index = s.index('@') - # 删除包含@在内后面的所有字符 - return s[:index] - else: - return s - - - -def search_question(question): - url = conf().get("duanju_url", "") + '/api/search' - params = { - 'is_time': '1', - 'page_no': '1', - 'page_size': '5', - 'title': question - } - try: - response = requests.get(url, params=params) - response.raise_for_status() # 检查请求是否成功 - responseData = response.json().get('data', {}).get('items', []) - return responseData - except requests.exceptions.RequestException as e: - print(f"Error fetching data: {e}") - return [] - -def search_alone(question): - url = conf().get("duanju_url", "") + '/api/other/all_search' - payload = { - 'title': question - } - try: - response = requests.post(url, json=payload) - response.raise_for_status() - responseData = response.json().get('data', []) - return responseData - except requests.exceptions.RequestException as e: - print(f"Error fetching data: {e}") - return [] \ No newline at end of file diff --git a/chatgpt-on-wechat/config.json b/chatgpt-on-wechat/config.json deleted file mode 100644 index eb9ebe4..0000000 --- a/chatgpt-on-wechat/config.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "debug": true, - "duanju_url": "https://www.xinyueso.com", - "channel_type": "wx", - "model": "gpt-4", - "open_ai_api_key": "", - "open_ai_api_base": "", - "text_to_image": "dall-e-3", - "claude_api_key": "YOUR API KEY", - "voice_to_text": "openai", - "text_to_voice": "openai", - "proxy": "", - "hot_reload": true, - "group_at_off": false, - "single_chat_prefix": ["bot", "@bot"], - "single_chat_reply_prefix": "[bot]", - "group_chat_prefix": [ - "@bot" - ], - "group_name_white_list": [ - "测试群群群", - "测试群群群2" - ], - "image_create_prefix": [], - "speech_recognition": true, - "group_speech_recognition": false, - "voice_reply_voice": false, - "conversation_max_tokens": 2500, - "expires_in_seconds": 3600, - "character_desc": "你是基于大语言模型的AI智能助手,旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", - "temperature": 0.7, - "subscribe_msg": "感谢您的关注!\n这里是AI智能助手,可以自由对话。\n支持语音对话。\n支持图片输入。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持tool、角色扮演和文字冒险等丰富的插件。", - "use_linkai": false, - "linkai_api_key": "", - "linkai_app_code": "", - "linkai_api_base": "" -} diff --git a/chatgpt-on-wechat/config.py b/chatgpt-on-wechat/config.py deleted file mode 100644 index 55bd56d..0000000 --- a/chatgpt-on-wechat/config.py +++ /dev/null @@ -1,366 +0,0 @@ -# encoding:utf-8 - -import json -import logging -import os -import pickle -import copy - -from common.log import logger - -# 将所有可用的配置项写在字典里, 请使用小写字母 -# 此处的配置值无实际意义,程序不会读取此处的配置,仅用于提示格式,请将配置加入到config.json中 -available_setting = { - # 你的心悦搜索网址 - "duanju_url": "", - # openai api配置 - "open_ai_api_key": "", # openai api key - # openai apibase,当use_azure_chatgpt为true时,需要设置对应的api base - "open_ai_api_base": "https://api.openai.com/v1", - "proxy": "", # openai使用的代理 - # chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称 - "model": "gpt-3.5-turbo", # 可选择: gpt-4o, pt-4o-mini, gpt-4-turbo, claude-3-sonnet, wenxin, moonshot, qwen-turbo, xunfei, glm-4, minimax, gemini等模型,全部可选模型详见common/const.py文件 - "bot_type": "", # 可选配置,使用兼容openai格式的三方服务时候,需填"chatGPT"。bot具体名称详见common/const.py文件列出的bot_type,如不填根据model名称判断, - "use_azure_chatgpt": False, # 是否使用azure的chatgpt - "azure_deployment_id": "", # azure 模型部署名称 - "azure_api_version": "", # azure api版本 - # Bot触发配置 - "single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复 - "single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人 - "single_chat_reply_suffix": "", # 私聊时自动回复的后缀,\n 可以换行 - "group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复 - "no_need_at": False, # 群聊回复时是否不需要艾特 - "group_chat_reply_prefix": "", # 群聊时自动回复的前缀 - "group_chat_reply_suffix": "", # 群聊时自动回复的后缀,\n 可以换行 - "group_chat_keyword": [], # 群聊时包含该关键词则会触发机器人回复 - "group_at_off": False, # 是否关闭群聊时@bot的触发 - "group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表 - "group_name_keyword_white_list": [], # 开启自动回复的群名称关键词列表 - "group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称 - "nick_name_black_list": [], # 用户昵称黑名单 - "group_welcome_msg": "", # 配置新人进群固定欢迎语,不配置则使用随机风格欢迎 - "trigger_by_self": False, # 是否允许机器人触发 - "text_to_image": "dall-e-2", # 图片生成模型,可选 dall-e-2, dall-e-3 - # Azure OpenAI dall-e-3 配置 - "dalle3_image_style": "vivid", # 图片生成dalle3的风格,可选有 vivid, natural - "dalle3_image_quality": "hd", # 图片生成dalle3的质量,可选有 standard, hd - # Azure OpenAI DALL-E API 配置, 当use_azure_chatgpt为true时,用于将文字回复的资源和Dall-E的资源分开. - "azure_openai_dalle_api_base": "", # [可选] azure openai 用于回复图片的资源 endpoint,默认使用 open_ai_api_base - "azure_openai_dalle_api_key": "", # [可选] azure openai 用于回复图片的资源 key,默认使用 open_ai_api_key - "azure_openai_dalle_deployment_id":"", # [可选] azure openai 用于回复图片的资源 deployment id,默认使用 text_to_image - "image_proxy": True, # 是否需要图片代理,国内访问LinkAI时需要 - "image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀 - "concurrency_in_session": 1, # 同一会话最多有多少条消息在处理中,大于1可能乱序 - "image_create_size": "256x256", # 图片大小,可选有 256x256, 512x512, 1024x1024 (dall-e-3默认为1024x1024) - "group_chat_exit_group": False, - # chatgpt会话参数 - "expires_in_seconds": 3600, # 无操作会话的过期时间 - # 人格描述 - "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", - "conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数 - # chatgpt限流配置 - "rate_limit_chatgpt": 20, # chatgpt的调用频率限制 - "rate_limit_dalle": 50, # openai dalle的调用频率限制 - # chatgpt api参数 参考https://platform.openai.com/docs/api-reference/chat/create - "temperature": 0.9, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "request_timeout": 180, # chatgpt请求超时时间,openai接口默认设置为600,对于难问题一般需要较长时间 - "timeout": 120, # chatgpt重试超时时间,在这个时间内,将会自动重试 - # Baidu 文心一言参数 - "baidu_wenxin_model": "eb-instant", # 默认使用ERNIE-Bot-turbo模型 - "baidu_wenxin_api_key": "", # Baidu api key - "baidu_wenxin_secret_key": "", # Baidu secret key - "baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model - # 讯飞星火API - "xunfei_app_id": "", # 讯飞应用ID - "xunfei_api_key": "", # 讯飞 API key - "xunfei_api_secret": "", # 讯飞 API secret - "xunfei_domain": "", # 讯飞模型对应的domain参数,Spark4.0 Ultra为 4.0Ultra,其他模型详见: https://www.xfyun.cn/doc/spark/Web.html - "xunfei_spark_url": "", # 讯飞模型对应的请求地址,Spark4.0 Ultra为 wss://spark-api.xf-yun.com/v4.0/chat,其他模型参考详见: https://www.xfyun.cn/doc/spark/Web.html - # claude 配置 - "claude_api_cookie": "", - "claude_uuid": "", - # claude api key - "claude_api_key": "", - # 通义千问API, 获取方式查看文档 https://help.aliyun.com/document_detail/2587494.html - "qwen_access_key_id": "", - "qwen_access_key_secret": "", - "qwen_agent_key": "", - "qwen_app_id": "", - "qwen_node_id": "", # 流程编排模型用到的id,如果没有用到qwen_node_id,请务必保持为空字符串 - # 阿里灵积(通义新版sdk)模型api key - "dashscope_api_key": "", - # Google Gemini Api Key - "gemini_api_key": "", - # wework的通用配置 - "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 - # 语音设置 - "speech_recognition": True, # 是否开启语音识别 - "group_speech_recognition": False, # 是否开启群组语音识别 - "voice_reply_voice": False, # 是否使用语音回复语音,需要设置对应语音合成引擎的api key - "always_reply_voice": False, # 是否一直使用语音回复 - "voice_to_text": "openai", # 语音识别引擎,支持openai,baidu,google,azure,xunfei,ali - "text_to_voice": "openai", # 语音合成引擎,支持openai,baidu,google,azure,xunfei,ali,pytts(offline),elevenlabs,edge(online) - "text_to_voice_model": "tts-1", - "tts_voice_id": "alloy", - # baidu 语音api配置, 使用百度语音识别和语音合成时需要 - "baidu_app_id": "", - "baidu_api_key": "", - "baidu_secret_key": "", - # 1536普通话(支持简单的英文识别) 1737英语 1637粤语 1837四川话 1936普通话远场 - "baidu_dev_pid": 1536, - # azure 语音api配置, 使用azure语音识别和语音合成时需要 - "azure_voice_api_key": "", - "azure_voice_region": "japaneast", - # elevenlabs 语音api配置 - "xi_api_key": "", # 获取ap的方法可以参考https://docs.elevenlabs.io/api-reference/quick-start/authentication - "xi_voice_id": "", # ElevenLabs提供了9种英式、美式等英语发音id,分别是“Adam/Antoni/Arnold/Bella/Domi/Elli/Josh/Rachel/Sam” - # 服务时间限制,目前支持itchat - "chat_time_module": False, # 是否开启服务时间限制 - "chat_start_time": "00:00", # 服务开始时间 - "chat_stop_time": "24:00", # 服务结束时间 - # 翻译api - "translate": "baidu", # 翻译api,支持baidu - # baidu翻译api的配置 - "baidu_translate_app_id": "", # 百度翻译api的appid - "baidu_translate_app_key": "", # 百度翻译api的秘钥 - # itchat的配置 - "hot_reload": False, # 是否开启热重载 - # wechaty的配置 - "wechaty_puppet_service_token": "", # wechaty的token - # wechatmp的配置 - "wechatmp_token": "", # 微信公众平台的Token - "wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443 - "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 - # 飞书配置 - "feishu_port": 80, # 飞书bot监听端口 - "feishu_app_id": "", # 飞书机器人应用APP Id - "feishu_app_secret": "", # 飞书机器人APP secret - "feishu_token": "", # 飞书 verification token - "feishu_bot_name": "", # 飞书机器人的名字 - # 钉钉配置 - "dingtalk_client_id": "", # 钉钉机器人Client ID - "dingtalk_client_secret": "", # 钉钉机器人Client Secret - "dingtalk_card_enabled": False, - - # chatgpt指令自定义触发词 - "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 - # channel配置 - "channel_type": "", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service,wechatcom_app,dingtalk} - "subscribe_msg": "", # 订阅消息, 支持: wechatmp, wechatmp_service, wechatcom_app - "debug": False, # 是否开启debug模式,开启后会打印更多日志 - "appdata_dir": "", # 数据目录 - # 插件配置 - "plugin_trigger_prefix": "$", # 规范插件提供聊天相关指令的前缀,建议不要和管理员指令前缀"#"冲突 - # 是否使用全局插件配置 - "use_global_plugin_config": False, - "max_media_send_count": 3, # 单次最大发送媒体资源的个数 - "media_send_interval": 1, # 发送图片的事件间隔,单位秒 - # 智谱AI 平台配置 - "zhipu_ai_api_key": "", - "zhipu_ai_api_base": "https://open.bigmodel.cn/api/paas/v4", - "moonshot_api_key": "", - "moonshot_base_url": "https://api.moonshot.cn/v1/chat/completions", - # LinkAI平台配置 - "use_linkai": False, - "linkai_api_key": "", - "linkai_app_code": "", - "linkai_api_base": "https://api.link-ai.tech", # linkAI服务地址 - "Minimax_api_key": "", - "Minimax_group_id": "", - "Minimax_base_url": "", - "web_port": 9899, -} - - -class Config(dict): - 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 = {} - - def __getitem__(self, key): - if key not in available_setting: - raise Exception("key {} not in available_setting".format(key)) - return super().__getitem__(key) - - def __setitem__(self, key, value): - if key not in available_setting: - raise Exception("key {} not in available_setting".format(key)) - return super().__setitem__(key, value) - - def get(self, key, default=None): - try: - return self[key] - except KeyError as e: - return default - except Exception as e: - raise e - - # Make sure to return a dictionary to ensure atomic - def get_user_data(self, user) -> dict: - if self.user_datas.get(user) is None: - self.user_datas[user] = {} - return self.user_datas[user] - - def load_user_datas(self): - try: - with open(os.path.join(get_appdata_dir(), "user_datas.pkl"), "rb") as f: - self.user_datas = pickle.load(f) - logger.info("[Config] User datas loaded.") - except FileNotFoundError as e: - logger.info("[Config] User datas file not found, ignore.") - except Exception as e: - logger.info("[Config] User datas error: {}".format(e)) - self.user_datas = {} - - def save_user_datas(self): - try: - with open(os.path.join(get_appdata_dir(), "user_datas.pkl"), "wb") as f: - pickle.dump(self.user_datas, f) - logger.info("[Config] User datas saved.") - except Exception as e: - logger.info("[Config] User datas error: {}".format(e)) - - -config = Config() - - -def drag_sensitive(config): - try: - if isinstance(config, str): - conf_dict: dict = json.loads(config) - conf_dict_copy = copy.deepcopy(conf_dict) - for key in conf_dict_copy: - if "key" in key or "secret" in key: - if isinstance(conf_dict_copy[key], str): - conf_dict_copy[key] = conf_dict_copy[key][0:3] + "*" * 5 + conf_dict_copy[key][-3:] - return json.dumps(conf_dict_copy, indent=4) - - elif isinstance(config, dict): - config_copy = copy.deepcopy(config) - for key in config: - if "key" in key or "secret" in key: - if isinstance(config_copy[key], str): - config_copy[key] = config_copy[key][0:3] + "*" * 5 + config_copy[key][-3:] - return config_copy - except Exception as e: - logger.exception(e) - return config - return config - - -def load_config(): - global config - config_path = "./config.json" - if not os.path.exists(config_path): - logger.info("配置文件不存在,将使用config-template.json模板") - config_path = "./config-template.json" - - config_str = read_file(config_path) - logger.debug("[INIT] config str: {}".format(drag_sensitive(config_str))) - - # 将json字符串反序列化为dict类型 - config = Config(json.loads(config_str)) - - # override config with environment variables. - # Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config. - for name, value in os.environ.items(): - name = name.lower() - if name in available_setting: - logger.info("[INIT] override config by environ args: {}={}".format(name, value)) - try: - config[name] = eval(value) - except: - if value == "false": - config[name] = False - elif value == "true": - config[name] = True - else: - config[name] = value - - if config.get("debug", False): - logger.setLevel(logging.DEBUG) - logger.debug("[INIT] set log level to DEBUG") - - logger.info("[INIT] load config: {}".format(drag_sensitive(config))) - - config.load_user_datas() - - -def get_root(): - return os.path.dirname(os.path.abspath(__file__)) - - -def read_file(path): - with open(path, mode="r", encoding="utf-8") as f: - return f.read() - - -def conf(): - return config - - -def get_appdata_dir(): - data_path = os.path.join(get_root(), conf().get("appdata_dir", "")) - if not os.path.exists(data_path): - 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) - - -# global plugin config -plugin_config = {} - - -def write_plugin_config(pconf: dict): - """ - 写入插件全局配置 - :param pconf: 全量插件配置 - """ - global plugin_config - for k in pconf: - plugin_config[k.lower()] = pconf[k] - -def remove_plugin_config(name: str): - """ - 移除待重新加载的插件全局配置 - :param name: 待重载的插件名 - """ - global plugin_config - plugin_config.pop(name.lower(), None) - - -def pconf(plugin_name: str) -> dict: - """ - 根据插件名称获取配置 - :param plugin_name: 插件名称 - :return: 该插件的配置项 - """ - return plugin_config.get(plugin_name.lower()) - - -# 全局配置,用于存放全局生效的状态 -global_config = {"admin_users": []} \ No newline at end of file diff --git a/chatgpt-on-wechat/docker-compose.yml b/chatgpt-on-wechat/docker-compose.yml deleted file mode 100644 index 1b6579b..0000000 --- a/chatgpt-on-wechat/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '2.0' -services: - chatgpt-on-wechat: - image: zhayujie/chatgpt-on-wechat - container_name: chatgpt-on-wechat - security_opt: - - seccomp:unconfined - volumes: - - ./config.json:/app/config.json - - ./config.py:/app/config.py - - ./chat_channel.py:/app/channel/chat_channel.py diff --git a/chatgpt-on-wechat/教程 b/chatgpt-on-wechat/教程 deleted file mode 100644 index 5db77bd..0000000 --- a/chatgpt-on-wechat/教程 +++ /dev/null @@ -1,13 +0,0 @@ -打开config.json 修改第3行网址即可,其它功能看官方教程 - -在chatgpt-on-wechat所在目录下执行以下命令启动容器: -sudo docker compose up -d - - - -用python搭建的不用换,一段时间后该出现断开还是会出现无响应断开的情况 -不清楚啥原因,大概率服务器问题 - - - -该项目地址:https://github.com/zhayujie/chatgpt-on-wechat \ No newline at end of file diff --git a/data.sql b/data.sql index 946328b..166c659 100644 --- a/data.sql +++ b/data.sql @@ -147,7 +147,7 @@ INSERT INTO `qf_conf` VALUES (31, 'app_subname', 'Hello World', '网站宣传语 INSERT INTO `qf_conf` VALUES (32, 'home_bg', '', '大图背景', '', 0, 4, '', 3, 1, 75, 1, 1725007588, 1725007613); INSERT INTO `qf_conf` VALUES (33, 'home_background', NULL, '背景颜色', '默认:#fafafa', 0, 7, NULL, 3, 1, 74, 1, 1725007770, 1725027349); INSERT INTO `qf_conf` VALUES (34, 'footer_dec', '声明:本站是网盘索引系统,所有内容均来自互联网所提供的公开引用资源,未提供资源上传、存储服务。', '底部介绍', '示例:声明:本站是网盘索引系统,所有内容均来自互联网所提供的公开引用资源,未提供资源上传、存储服务。', 0, 1, NULL, 0, 1, 90, 1, 1725025185, 1725325534); -INSERT INTO `qf_conf` VALUES (35, 'footer_copyright', '© 2024 心悦搜剧 Powered by 心悦导航', '底部版权', '示例:© 2024 心悦搜剧 Powered by 心悦导航', 0, 1, NULL, 0, 1, 89, 1, 1725025262, 1725325624); +INSERT INTO `qf_conf` VALUES (35, 'footer_copyright', '© 2024 心悦 Powered by 心悦', '底部版权', '示例:© 2024 心悦 Powered by 心悦', 0, 1, NULL, 0, 1, 89, 1, 1725025262, 1725325624); INSERT INTO `qf_conf` VALUES (36, 'home_color', NULL, '文字颜色', '默认文字颜色:#000000', 0, 7, NULL, 3, 1, 73, 1, 1725027432, 1725027445); INSERT INTO `qf_conf` VALUES (37, 'home_theme', NULL, '主题色', '默认:#1e80ff', 0, 7, NULL, 3, 1, 72, 1, 1725027499, 1725027504); INSERT INTO `qf_conf` VALUES (38, 'other_background', NULL, '其它元素背景', '搜索框及其它元素北背景色 默认:#ffffff', 0, 7, NULL, 3, 1, 71, 1, 1725028468, 1725028478); @@ -157,7 +157,7 @@ INSERT INTO `qf_conf` VALUES (41, 'home_css', '', '自定义CSS', '直接写css INSERT INTO `qf_conf` VALUES (42, 'seo_statistics', '', '统计代码', '直接填写统计代码即可,如51LA: ', 0, 1, NULL, 9, 1, 995, 1, 1725325341, 1725411486); INSERT INTO `qf_conf` VALUES (43, 'app_icon', '', '网站icon', '', 0, 4, NULL, 0, 1, 92, 1, 1725326071, 1725326071); INSERT INTO `qf_conf` VALUES (44, 'app_demand', '0', '提交需求', '前台是否开启此功能 ; 默认开启', 0, 2, '开启=>0\n关闭=>1', 3, 1, 81, 1, 1725326640, 1725326707); -INSERT INTO `qf_conf` VALUES (45, 'app_links', '更多资源', '顶部其他外链', '一行一个外链(a标签):更多资源', 0, 1, NULL, 3, 1, 80, 1, 1725326838, 1725326838); +INSERT INTO `qf_conf` VALUES (45, 'app_links', '更多资源', '顶部其他外链', '一行一个外链(a标签):更多资源', 0, 1, NULL, 3, 1, 80, 1, 1725326838, 1725326838); INSERT INTO `qf_conf` VALUES (46, 'app_name_hide', '0', '隐藏网站名称', '默认显示:logo包含文字的可以隐藏网站名称', 0, 2, '显示=>0\n隐藏=>1', 0, 1, 98, 1, 1725411632, 1725411763); INSERT INTO `qf_conf` VALUES (47, 'ranking_m_num', '6', '移动端限制数量', '释:移动端最多显示数量', 0, 0, NULL, 3, 1, 77, 1, 1725412329, 1725412329); INSERT INTO `qf_conf` VALUES (48, 'search_tips', '', '未搜索提示词', '为空时默认:未找到,可换个关键词尝试哦~', 0, 0, NULL, 1, 1, 0, 1, 1726108804, 1726108804); diff --git a/extend/quarkPlugin/QuarkPlugin.php b/extend/quarkPlugin/QuarkPlugin.php index badba8f..def4fd3 100644 --- a/extend/quarkPlugin/QuarkPlugin.php +++ b/extend/quarkPlugin/QuarkPlugin.php @@ -17,7 +17,7 @@ class QuarkPlugin public function __construct() { //第三方转存接口地址 - $this->url = "https://www.xinyueso.com"; + $this->url = ""; $this->model = new SourceModel(); $this->SourceLogModel = new SourceLogModel(); $this->source_category_id = 0; diff --git a/public/robots.txt b/public/robots.txt index 771983c..6f27bb6 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,2 @@ User-agent: * -Disallow: -Sitemap: https://www.xinyueso.com/sitemap.xml \ No newline at end of file +Disallow: \ No newline at end of file