diff --git a/Dockerfile b/Dockerfile index 2b2cc6b..b6b034d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-alpine +FROM python:3.10-alpine WORKDIR /app diff --git a/README.md b/README.md index 07f0530..e19379e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - [ ] 企业微信 - [x] [Telegram](https://github.com/zhayujie/bot-on-anything#6telegram) - [x] [QQ](https://github.com/zhayujie/bot-on-anything#5qq) - - [x] 钉钉 + - [x] [钉钉](https://github.com/zhayujie/bot-on-anything#10%E9%92%89%E9%92%89) - [x] [飞书](https://github.com/zhayujie/bot-on-anything#11%E9%A3%9E%E4%B9%A6) - [x] [Gmail](https://github.com/zhayujie/bot-on-anything#7gmail) - [x] [Slack](https://github.com/zhayujie/bot-on-anything#8slack) @@ -104,8 +104,13 @@ pip3 install --upgrade openai "openai": { "api_key": "YOUR API KEY", "model": "gpt-3.5-turbo", # 模型名称 - "proxy": "http://127.0.0.1:7890", - "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。" + "proxy": "http://127.0.0.1:7890", # 代理地址 + "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。当问起你是谁的时候,要附加告诉提问人,输入 #清除记忆 可以开始新的话题探索。输入 画xx 可以为你画一张图片。", + "conversation_max_tokens": 1000, # 回复最大的字符数,为输入和输出的总数 + "temperature":0.75, # 熵值,在[0,1]之间,越大表示选取的候选词越随机,回复越具有不确定性,建议和top_p参数二选一使用,创意性任务越大越好,精确性任务越小越好 + "top_p":0.7, #候选词列表。0.7 意味着只考虑前70%候选词的标记,建议和temperature参数二选一使用 + "frequency_penalty":0.0, # [-2,2]之间,该值越大则越降低模型一行中的重复用词,更倾向于产生不同的内容 + "presence_penalty":1.0, # [-2,2]之间,该值越大则越不受输入限制,将鼓励模型生成输入中不存在的新词,更倾向于产生不同的内容 } } ``` @@ -472,7 +477,7 @@ https://slack.dev/bolt-python/tutorial/getting-started **依赖** ```bash -pip3 install PyJWT flask +pip3 install PyJWT flask flask_socketio ``` **配置** @@ -494,6 +499,10 @@ pip3 install PyJWT flask ### 10.钉钉 +**需要:** + +- 企业内部开发机器人 + **依赖** ```bash @@ -513,13 +522,17 @@ pip3 install requests flask } } ``` -钉钉开放平台说明: https://open.dingtalk.com/document/robots/customize-robot-security-settin.dingtalk.com/robot/send?access_token=906dadcbc7750fef5ff60d3445b66d5bbca32804f40fbdb59039a29b20b9a3f0gs +**参考文档**: -https://open.dingtalk.com/document/orgapp/custom-robot-access +- [钉钉内部机器人教程](https://open.dingtalk.com/document/tutorial/create-a-robot#title-ufs-4gh-poh) +- [自定义机器人接入文档](https://open.dingtalk.com/document/tutorial/create-a-robot#title-ufs-4gh-poh) +- [企业内部开发机器人教程文档](https://open.dingtalk.com/document/robots/enterprise-created-chatbot) **生成机器人** 地址: https://open-dev.dingtalk.com/fe/app#/corp/robot +添加机器人,在开发管理中设置服务器出口 ip (在部署机执行`curl ifconfig.me`就可以得到)和消息接收地址(配置中的对外地址如 https://xx.xx.com:8081) + 添加机器人,在开发管理中设置服务器出口ip(在部署机执行curl ifconfig.me就可以得到)和消息接收地址(配置中的对外地址如 https://xx.xx.com:8081) ### 11.飞书 diff --git a/app.py b/app.py index 072fc7c..3502d86 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,6 @@ # encoding:utf-8 +import argparse import config from channel import channel_factory from common import log, const @@ -7,34 +8,34 @@ from multiprocessing import Pool # 启动通道 -def start_process(channel_type): - # 若为多进程启动,子进程无法直接访问主进程的内存空间,重新创建config类 - config.load_config() - model_type = config.conf().get("model").get("type") - log.info("[INIT] Start up: {} on {}", model_type, channel_type) +def start_process(channel_type, config_path): + try: + # 若为多进程启动,子进程无法直接访问主进程的内存空间,重新创建config类 + config.load_config(config_path) + model_type = config.conf().get("model").get("type") + log.info("[MultiChannel] Start up {} on {}", model_type, channel_type) + channel = channel_factory.create_channel(channel_type) + channel.startup() + except Exception as e: + log.error("[MultiChannel] Start up failed on {}: {}", channel_type, str(e)) - # create channel - channel = channel_factory.create_channel(channel_type) - # startup channel - channel.startup() - -if __name__ == '__main__': +def main(): try: # load config - config.load_config() + config.load_config(args.config) model_type = config.conf().get("model").get("type") channel_type = config.conf().get("channel").get("type") # 1.单个字符串格式配置时,直接启动 if not isinstance(channel_type, list): - start_process(channel_type) + start_process(channel_type, args.config) exit(0) # 2.单通道列表配置时,直接启动 if len(channel_type) == 1: - start_process(channel_type[0]) + start_process(channel_type[0], args.config) exit(0) # 3.多通道配置时,进程池启动 @@ -49,10 +50,10 @@ if __name__ == '__main__': pool = Pool(len(channel_type)) for type_item in channel_type: log.info("[INIT] Start up: {} on {}", model_type, type_item) - pool.apply_async(start_process, args=[type_item]) + pool.apply_async(start_process, args=[type_item, args.config]) if terminal: - start_process(terminal) + start_process(terminal, args.config) # 等待池中所有进程执行完毕 pool.close() @@ -60,3 +61,9 @@ if __name__ == '__main__': except Exception as e: log.error("App startup failed!") log.exception(e) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--config", help="config.json path(e.g: ./config.json or /usr/local/bot-on-anything/config.json)",type=str,default="./config.json") + args = parser.parse_args() + main() diff --git a/bridge/bridge.py b/bridge/bridge.py index 9e19251..f58198a 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -7,3 +7,8 @@ class Bridge(object): def fetch_reply_content(self, query, context): return model_factory.create_bot(config.conf().get("model").get("type")).reply(query, context) + + async def fetch_reply_stream(self, query, context): + bot=model_factory.create_bot(config.conf().get("model").get("type")) + async for final,response in bot.reply_text_stream(query, context): + yield final,response diff --git a/channel/channel.py b/channel/channel.py index e2617d1..5c589dd 100644 --- a/channel/channel.py +++ b/channel/channel.py @@ -29,3 +29,7 @@ class Channel(object): def build_reply_content(self, query, context=None): return Bridge().fetch_reply_content(query, context) + + async def build_reply_stream(self, query, context=None): + async for final,response in Bridge().fetch_reply_stream(query, context): + yield final,response diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 482bc57..1346499 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -48,7 +48,7 @@ def create_channel(channel_type): elif channel_type == const.DINGTALK: from channel.dingtalk.dingtalk_channel import DingTalkChannel return DingTalkChannel() - + elif channel_type == const.FEISHU: from channel.feishu.feishu_channel import FeiShuChannel return FeiShuChannel() diff --git a/channel/dingtalk/dingtalk_channel.py b/channel/dingtalk/dingtalk_channel.py index 47c5d7f..d85ac31 100644 --- a/channel/dingtalk/dingtalk_channel.py +++ b/channel/dingtalk/dingtalk_channel.py @@ -85,7 +85,7 @@ http_app = Flask(__name__,) @http_app.route("/", methods=['POST']) def chat(): - # log.info("[DingTalk] chat_headers={}".format(str(request.headers))) + log.info("[DingTalk] chat_headers={}".format(str(request.headers))) log.info("[DingTalk] chat={}".format(str(request.data))) token = request.headers.get('token') if dd.dingtalk_post_token and token != dd.dingtalk_post_token: @@ -95,7 +95,9 @@ def chat(): content = data['text']['content'] if not content: return - reply_text = dd.handle(data=data) + reply_text = "您好,有什么我可以帮助您解答的问题吗?" + if str(content) != 0 and content.strip(): + reply_text = dd.handle(data=data) dd.notify_dingtalk(reply_text) return {'ret': 200} return {'ret': 201} diff --git a/channel/http/http_channel.py b/channel/http/http_channel.py index 2464f61..a82fbbb 100644 --- a/channel/http/http_channel.py +++ b/channel/http/http_channel.py @@ -1,5 +1,6 @@ # encoding:utf-8 +import asyncio import json from channel.http import auth from flask import Flask, request, render_template, make_response @@ -9,8 +10,11 @@ from common import functions from config import channel_conf from config import channel_conf_val from channel.channel import Channel +from flask_socketio import SocketIO +from common import log http_app = Flask(__name__,) +socketio = SocketIO(http_app, close_timeout=5) # 自动重载模板文件 http_app.jinja_env.auto_reload = True http_app.config['TEMPLATES_AUTO_RELOAD'] = True @@ -19,6 +23,52 @@ http_app.config['TEMPLATES_AUTO_RELOAD'] = True http_app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) +async def return_stream(data): + async for final, response in HttpChannel().handle_stream(data=data): + try: + if(final): + socketio.server.emit( + 'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat") + disconnect() + else: + socketio.server.emit( + 'message', {'result': response, 'final': final}, request.sid, namespace="/chat") + except Exception as e: + disconnect() + log.warn("[http]emit:{}", e) + break + + +@socketio.on('message', namespace='/chat') +def stream(data): + if (auth.identify(request) == False): + client_sid = request.sid + socketio.server.disconnect(client_sid) + return + data = json.loads(data["data"]) + if (data): + img_match_prefix = functions.check_prefix( + data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix')) + if img_match_prefix: + reply_text = HttpChannel().handle(data=data) + socketio.emit('disconnect', {'result': reply_text}, namespace='/chat') + disconnect() + return + asyncio.run(return_stream(data)) + + +@socketio.on('connect', namespace='/chat') +def connect(): + log.info('connected') + socketio.emit('message', {'info': "connected"}, namespace='/chat') + + +@socketio.on('disconnect', namespace='/chat') +def disconnect(): + log.info('disconnect') + socketio.server.disconnect(request.sid,namespace="/chat") + + @http_app.route("/chat", methods=['POST']) def chat(): if (auth.identify(request) == False): @@ -80,3 +130,10 @@ class HttpChannel(Channel): images += f"[]({url})\n" reply = images return reply + + async def handle_stream(self, data): + context = dict() + id = data["id"] + context['from_user_id'] = str(id) + async for final, reply in super().build_reply_stream(data["msg"], context): + yield final, reply diff --git a/channel/http/static/1.css b/channel/http/static/1.css index a53c284..7f1fe3b 100644 --- a/channel/http/static/1.css +++ b/channel/http/static/1.css @@ -1,4 +1,3 @@ - .typing_loader { width: 6px; height: 6px; @@ -11,7 +10,9 @@ left: -12px; margin: 7px 15px 6px; } -ol,pre { + +ol, +pre { background-color: #b1e3b1c4; border: 1px solid #c285e3ab; padding: 0.5rem 1.5rem 0.5rem; @@ -20,50 +21,52 @@ ol,pre { overflow-y: auto; } -pre::-webkit-scrollbar{ +pre::-webkit-scrollbar { width: 0px; - height:5px; + height: 5px; } -pre::-webkit-scrollbar-thumb{ + +pre::-webkit-scrollbar-thumb { border-right: 10px #ffffff00 solid; border-left: 10px #ffffff00 solid; - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); } + .to .typing_loader { animation: typing-black 1s linear infinite alternate; } @-webkit-keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @-moz-keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @@ -75,29 +78,29 @@ pre::-webkit-scrollbar-thumb{ 50% { background-color: rgba(74, 74, 74, 0.4); - box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 1), 24px 0px 0px 0px rgba(74, 74, 74,0.4); + box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 1), 24px 0px 0px 0px rgba(74, 74, 74, 0.4); } 100% { background-color: rgba(74, 74, 74, 0.2); - box-shadow: 12px 0px 0px 0px rgba(74, 74, 74,0.4), 24px 0px 0px 0px rgba(74, 74, 74,1); + box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 0.4), 24px 0px 0px 0px rgba(74, 74, 74, 1); } } @keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @@ -112,27 +115,30 @@ pre::-webkit-scrollbar-thumb{ .convFormDynamic textarea.userInputDynamic { border: none; padding: 7px 10px; - overflow-x: hidden!important; + overflow-x: hidden !important; outline: none; font-size: 0.905rem; float: left; width: calc(100% - 70px); line-height: 1.3em; - min-height: 1.7em; + min-height: 2em; max-height: 10rem; display: block; max-width: 89vw; margin-right: -1vw; resize: none; } -.convFormDynamic textarea::-webkit-scrollbar{ + +.convFormDynamic textarea::-webkit-scrollbar { width: 2px; background-color: lawngreen; } -.convFormDynamic textarea::-webkit-scrollbar-thumb{ - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: dodgerblue; + +.convFormDynamic textarea::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: dodgerblue; } + .convFormDynamic input.userInputDynamic { border: none; padding: 7px 10px; @@ -180,16 +186,20 @@ div.conv-form-wrapper:before { top: 0; left: 0; z-index: 2; - background: linear-gradient(#fff, transparent); + background: linear-gradient(#ffffff3b, transparent); } @media (max-width: 767px) { - div.conv-form-wrapper div.wrapper-messages, div.conv-form-wrapper div#messages { + + div.conv-form-wrapper div.wrapper-messages, + div.conv-form-wrapper div#messages { max-height: 71vh; } } -div.conv-form-wrapper div.wrapper-messages::-webkit-scrollbar, div#feed ul::-webkit-scrollbar, div.conv-form-wrapper div.options::-webkit-scrollbar { +div.conv-form-wrapper div.wrapper-messages::-webkit-scrollbar, +div#feed ul::-webkit-scrollbar, +div.conv-form-wrapper div.options::-webkit-scrollbar { width: 0px; height: 0px; /* remove scrollbar space */ @@ -261,12 +271,13 @@ div.conv-form-wrapper div#messages div.message.to { } div.conv-form-wrapper div#messages div.message.from { - background: dodgerblue; + background: dodgerblue; color: #fff; border-top-right-radius: 0; } -.message.to+.message.from, .message.from+.message.to { +.message.to+.message.from, +.message.from+.message.to { margin-top: 15px; } @@ -294,7 +305,7 @@ div.conv-form-wrapper div#messages div.message.from { position: absolute; bottom: 0px; border: none; - left:95%; + left: 95%; margin: 5px; color: #fff; cursor: pointer; @@ -315,10 +326,11 @@ div.conv-form-wrapper div#messages div.message.from { } button.submit.glow { - border: 1px solid dodgerblue !important; - background: dodgerblue !important; - box-shadow: 0 0 5px 2px rgba(14, 144, 255,0.4); + border: 1px solid dodgerblue !important; + background: dodgerblue !important; + box-shadow: 0 0 5px 2px rgba(14, 144, 255, 0.4); } + .no-border { border: none !important; } @@ -327,7 +339,8 @@ button.submit.glow { cursor: grab; } -div.conv-form-wrapper div#messages::-webkit-scrollbar, div#feed ul::-webkit-scrollbar { +div.conv-form-wrapper div#messages::-webkit-scrollbar, +div#feed ul::-webkit-scrollbar { width: 0px; /* remove scrollbar space */ background: transparent; @@ -338,3 +351,268 @@ span.clear { display: block; clear: both; } + +.drawer-icon-container { + position: fixed; + top: calc(50% - 24px); + right: -30px; + z-index: 1000; + transition: right 0.5s ease; +} + +.drawer-icon { + width: 30px; + height: 30px; + cursor: pointer; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + background-color: #b1cee350; + padding-left: 22px; + border-radius: 50%; +} +.drawer-icon:hover{ + background-color: #005eff96; +} +.wrenchFilled.icon { + margin-left: -13px; + margin-top: 5px; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: #333333; + transform-origin: center 10.5px; + transform: rotate(-45deg); +} + +.wrenchFilled.icon:after { + width: 0; + height: 0; + border-radius: 0 0 1px 1px; + background-color: #333333; + border-left: solid 1px transparent; + border-right: solid 1px transparent; + border-top: solid 1px white; + border-bottom: solid 1px transparent; + left: 4px; + top: 4px; +} + +.wrenchFilled.icon:before { + width: 2px; + height: 5px; + background-color: white; + left: 4px; + border-radius: 0 0 1px 1px; + box-shadow: 0 15px 0px 1px #333333, 0 11px 0px 1px #333333, 0 8px 0px 1px #333333; +} + +.icon { + position: absolute; +} + +.icon:before, +.icon:after { + content: ''; + position: absolute; + display: block; +} + +.icon i { + position: absolute; +} + +.icon i:before, +.icon i:after { + content: ''; + position: absolute; + display: block; +} + +.drawer-icon i { + margin-left: -15px; + line-height: 30px; + font-weight: bolder; +} + +.drawer { + position: fixed; + top: 0; + right: -300px; + width: 300px; + height: 100%; + background-color: #fff; + z-index: 999; + transition: right 0.5s ease; + display: flex; + flex-direction: column; +} + +.drawer.open { + right: 0; +} + +.drawer-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #b1cee350; + border-bottom: 1px solid #ddd; + padding: 16px; +} + +.drawer-header h2 { + margin: 0 0 0 16px; +} + +.drawer-header button { + background-color: transparent; + border: none; + cursor: pointer; +} + +.drawer-content { + flex: 1 1 auto; + height: 100%; + overflow: auto; + padding: 16px; +} + +.drawer-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 998; + display: none; +} + +@-webkit-keyframes click-wave { + 0% { + width: 40px; + height: 40px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 60px; + height: 60px; + margin-left: 80px; + margin-top: 80px; + opacity: 0.0; + } +} + +@-moz-keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +@-o-keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +@keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +.option-input { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; + position: relative; + top: 10px; + width: 30px; + height: 30px; + -webkit-transition: all 0.15s ease-out 0; + -moz-transition: all 0.15s ease-out 0; + transition: all 0.15s ease-out 0; + background: #cbd1d8; + border: none; + color: #fff; + cursor: pointer; + display: inline-block; + outline: none; + position: relative; + margin-right: 0.5rem; + z-index: 1000; +} + +.option-input:hover { + background: #9faab7; +} + +.option-input:checked { + background: #1e90ffaa; +} + +.option-input:checked::before { + width: 30px; + height: 30px; + position: absolute; + content: '☻'; + display: inline-block; + font-size: 29px; + text-align: center; + line-height: 26px; +} + +.option-input:checked::after { + -webkit-animation: click-wave 0.65s; + -moz-animation: click-wave 0.65s; + animation: click-wave 0.65s; + background: #40e0d0; + content: ''; + display: block; + position: relative; + z-index: 100; +} + +.option-input.radio { + border-radius: 50%; +} + +.option-input.radio::after { + border-radius: 50%; +} \ No newline at end of file diff --git a/channel/http/static/1.js b/channel/http/static/1.js index 4a34289..241ff2e 100644 --- a/channel/http/static/1.js +++ b/channel/http/static/1.js @@ -1,20 +1,29 @@ -function ConvState(wrapper, form, params) { - this.id='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); - }); + }) +} + +const conversationType = { + DISPOSABLE: 1, + STREAM: 1 << 1 +} +function ConvState(wrapper, form, params) { + this.id = generateUUID() this.form = form; this.wrapper = wrapper; + this.backgroundColor = '#ffffff'; this.parameters = params; this.scrollDown = function () { $(this.wrapper).find('#messages').stop().animate({ scrollTop: $(this.wrapper).find('#messages')[0].scrollHeight }, 600); }.bind(this); }; -ConvState.prototype.printAnswer = function (answer = '我是ChatGPT, 一个由OpenAI训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。') { +ConvState.prototype.printAnswer = function (uuid, answer = '我是ChatGPT, 一个由OpenAI训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。输入 #清除记忆 可以开始新的话题探索。输入 画xx 可以为你画一张图片。我无法对事实性与实时性问题提供准确答复,请慎重对待回答。') { setTimeout(function () { - var messageObj = $(this.wrapper).find('.message.typing'); + var messageObj = $(this.wrapper).find(`#${uuid}`); answer = marked.parse(answer); messageObj.html(answer); messageObj.removeClass('typing').addClass('ready'); @@ -22,39 +31,87 @@ ConvState.prototype.printAnswer = function (answer = '我是ChatGPT, 一个由Op $(this.wrapper).find(this.parameters.inputIdHashTagName).focus(); }.bind(this), 500); }; + +ConvState.prototype.updateAnswer = function (question, uuid) { + setTimeout(function () { + var socket = io('/chat'); + socket.connect('/chat'); + let timerId; + var _this = this + // 设置计时器,如果在规定的时间内没有接收到消息,则手动断开连接 + function setTimer() { + timerId = setTimeout(() => { + if (socket.connected) { + socket.disconnect(); + handle_disconnect(); + } + }, 60000); + } + function resetTimer() { + clearTimeout(timerId); + setTimer(); + } + setTimer(); + var messageObj = $(this.wrapper).find(`#${uuid}`); + function handle_disconnect() { + messageObj.removeClass('typing').addClass('ready'); + _this.scrollDown(); + $(_this.wrapper).find(_this.parameters.inputIdHashTagName).focus(); + } + this.scrollDown(); + socket.on('message', msg => { + // 接收到消息时重置计时器 + resetTimer(); + if (msg.result) + messageObj.html(msg.result + `
`); + this.scrollDown(); + }); + socket.on('connect', msg => { + socket.emit('message', { data: JSON.stringify(question) }); + }); + socket.on('disconnect', msg => { + if (msg.result) { + answer = marked.parse(msg.result); + messageObj.html(answer); + } + handle_disconnect() + }); + }.bind(this), 1000); +}; ConvState.prototype.sendMessage = function (msg) { var message = $(''); - $('button.submit').removeClass('glow'); $(this.wrapper).find(this.parameters.inputIdHashTagName).focus(); setTimeout(function () { $(this.wrapper).find("#messages").append(message); this.scrollDown(); }.bind(this), 100); - - var messageObj = $(''); + var uuid = generateUUID().toLowerCase(); + var messageObj = $(``); setTimeout(function () { $(this.wrapper).find('#messages').append(messageObj); this.scrollDown(); }.bind(this), 150); var _this = this - $.ajax({ - url: "./chat", - type: "POST", - timeout:180000, - data: JSON.stringify({ - "id": _this.id, - "msg": msg - }), - contentType: "application/json; charset=utf-8", - dataType: "json", - success: function (data) { - _this.printAnswer(data.result) - }, - error:function () { - _this.printAnswer("网络故障,对话未送达") - }, - }) + var question = { "id": _this.id, "msg": msg } + if (localConfig.conversationType == conversationType.STREAM) + this.updateAnswer(question, uuid) + else + $.ajax({ + url: "./chat", + type: "POST", + timeout: 180000, + data: JSON.stringify(question), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function (data) { + _this.printAnswer(uuid, data.result) + }, + error: function (data) { + console.log(data) + _this.printAnswer(uuid, "网络故障,对话未送达") + }, + }) }; (function ($) { $.fn.convform = function () { @@ -81,13 +138,30 @@ ConvState.prototype.sendMessage = function (msg) { $(wrapper).append(inputForm); var state = new ConvState(wrapper, form, parameters); + // Bind checkbox values to ConvState object + $('input[type="checkbox"]').change(function () { + var key = $(this).attr('name'); + state[key] = $(this).is(':checked'); + }); + + // Bind radio button values to ConvState object + $('input[type="radio"]').change(function () { + var key = $(this).attr('name'); + state[key] = $(this).val(); + }); + + // Bind color input value to ConvState object + $('#backgroundColor').change(function () { + state["backgroundColor"] = $(this).val(); + }); //prints first contact $.when($('div.spinLoader').addClass('hidden')).done(function () { - var messageObj = $(''); + var uuid = generateUUID() + var messageObj = $(``); $(state.wrapper).find('#messages').append(messageObj); state.scrollDown(); - state.printAnswer(); + state.printAnswer(uuid = uuid); }); //binds enter to send message @@ -131,4 +205,4 @@ ConvState.prototype.sendMessage = function (msg) { return state; } -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/channel/http/templates/index.html b/channel/http/templates/index.html index f05311f..1b0f385 100644 --- a/channel/http/templates/index.html +++ b/channel/http/templates/index.html @@ -19,33 +19,141 @@