diff --git a/Dockerfile b/Dockerfile
index 2b2cc6b2..b6b034dc 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 247ba7ae..696c84a8 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
- [x] [GPT-3.0](https://github.com/zhayujie/bot-on-anything#2gpt-30)
- [x] [文心一言 (测试版)](https://github.com/zhayujie/bot-on-anything#3%E6%96%87%E5%BF%83%E4%B8%80%E8%A8%80-%E6%B5%8B%E8%AF%95%E7%89%88)
- [x] [New Bing](https://github.com/zhayujie/bot-on-anything#4newbing)
+ - [x] [Google Bard](https://github.com/zhayujie/bot-on-anything#5bard)
**应用:**
@@ -20,8 +21,8 @@
- [ ] 企业微信
- [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 +105,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]之间,该值越大则越不受输入限制,将鼓励模型生成输入中不存在的新词,更倾向于产生不同的内容
}
}
```
@@ -186,6 +192,20 @@ cookie示例:
]
```
+### 5.Bard
+
+#### 配置项说明
+
+```bash
+{
+ "model": {
+ "type" : "bard",
+ "cookies":""
+ //登录https://bard.google.com/ 获取name为"__Secure-1PSID"的Cookie Value
+ }
+}
+```
+
## 三、选择应用
### 1.命令行终端
@@ -472,7 +492,7 @@ https://slack.dev/bolt-python/tutorial/getting-started
**依赖**
```bash
-pip3 install PyJWT flask
+pip3 install PyJWT flask flask_socketio
```
**配置**
@@ -494,6 +514,10 @@ pip3 install PyJWT flask
### 10.钉钉
+**需要:**
+
+- 企业内部开发机器人
+
**依赖**
```bash
@@ -513,15 +537,59 @@ 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.飞书
+
+**依赖**
+
+```bash
+pip3 install requests flask
+```
+**配置**
+
+```json
+"channel": {
+ "type": "dingtalk",
+ "feishu": {
+ "image_create_prefix": [
+ "画",
+ "draw",
+ "Draw"
+ ],
+ "port": "8082",//对外端口
+ "app_id": "xxx", //应用app_id
+ "app_secret": "xxx",//应用Secret
+ "verification_token": "xxx" //事件订阅 Verification Token
+ }
+}
+```
+
+**生成机器人**
+
+地址: https://open.feishu.cn/app/
+1. 添加企业自建应用
+2. 开通权限
+ - im:message
+ - im:message.group_at_msg
+ - im:message.group_at_msg:readonly
+ - im:message.p2p_msg
+ - im:message.p2p_msg:readonly
+ - im:message:send_as_bot
+3. 订阅菜单添加事件(接收消息v2.0) 配置请求地址(配置中的对外地址如 https://xx.xx.com:8081)
+4. 版本管理与发布中上架应用,app中会收到审核信息,通过审核后在群里添加自建应用
+
### 通用配置
+ `clear_memory_commands`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
@@ -529,4 +597,4 @@ https://open.dingtalk.com/document/orgapp/custom-robot-access
# 教程
-1.视频教程:https://www.bilibili.com/video/BV1KM4y167e8
+1.视频教程:https://www.bilibili.com/video/BV1KM4y167e8
\ No newline at end of file
diff --git a/app.py b/app.py
index 072fc7c9..3502d863 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 @@
# 启动通道
-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)
-
- # create channel
- channel = channel_factory.create_channel(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))
- # 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 @@ def start_process(channel_type):
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 @@ def start_process(channel_type):
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 9e19251c..f58198a9 100644
--- a/bridge/bridge.py
+++ b/bridge/bridge.py
@@ -7,3 +7,8 @@ def __init__(self):
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 e2617d1f..5c589dd1 100644
--- a/channel/channel.py
+++ b/channel/channel.py
@@ -29,3 +29,7 @@ def send(self, msg, receiver):
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 78a1a493..13464999 100644
--- a/channel/channel_factory.py
+++ b/channel/channel_factory.py
@@ -49,5 +49,9 @@ def create_channel(channel_type):
from channel.dingtalk.dingtalk_channel import DingTalkChannel
return DingTalkChannel()
+ elif channel_type == const.FEISHU:
+ from channel.feishu.feishu_channel import FeiShuChannel
+ return FeiShuChannel()
+
else:
raise RuntimeError("unknown channel_type in config.json: " + channel_type)
diff --git a/channel/dingtalk/dingtalk_channel.py b/channel/dingtalk/dingtalk_channel.py
index 47c5d7f5..d85ac31c 100644
--- a/channel/dingtalk/dingtalk_channel.py
+++ b/channel/dingtalk/dingtalk_channel.py
@@ -85,7 +85,7 @@ def handle(self, data):
@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/feishu/feishu_channel.py b/channel/feishu/feishu_channel.py
new file mode 100644
index 00000000..07877d8d
--- /dev/null
+++ b/channel/feishu/feishu_channel.py
@@ -0,0 +1,185 @@
+# encoding:utf-8
+import json
+import hmac
+import hashlib
+import base64
+import time
+import requests
+from urllib.parse import quote_plus
+from common import log
+from flask import Flask, request, render_template, make_response
+from common import const
+from common import functions
+from config import channel_conf
+from config import channel_conf_val
+from channel.channel import Channel
+from urllib import request as url_request
+from channel.feishu.store import MemoryStore
+
+class FeiShuChannel(Channel):
+ def __init__(self):
+ self.app_id = channel_conf(
+ const.FEISHU).get('app_id')
+ self.app_secret = channel_conf(
+ const.FEISHU).get('app_secret')
+ self.verification_token = channel_conf(
+ const.FEISHU).get('verification_token')
+ log.info("[FeiShu] app_id={}, app_secret={} verification_token={}".format(
+ self.app_id, self.app_secret, self.verification_token))
+ self.memory_store = MemoryStore()
+
+ def startup(self):
+ http_app.run(host='0.0.0.0', port=channel_conf(
+ const.FEISHU).get('port'))
+
+ def get_tenant_access_token(self):
+ url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
+ headers = {
+ "Content-Type": "application/json"
+ }
+ req_body = {
+ "app_id": self.app_id,
+ "app_secret": self.app_secret
+ }
+
+ data = bytes(json.dumps(req_body), encoding='utf8')
+ req = url_request.Request(url=url, data=data,
+ headers=headers, method='POST')
+ try:
+ response = url_request.urlopen(req)
+ except Exception as e:
+ print(e.read().decode())
+ return ""
+
+ rsp_body = response.read().decode('utf-8')
+ rsp_dict = json.loads(rsp_body)
+ code = rsp_dict.get("code", -1)
+ if code != 0:
+ print("get tenant_access_token error, code =", code)
+ return ""
+ return rsp_dict.get("tenant_access_token", "")
+
+ def notify_feishu(self, token, receive_type, receive_id, at_id, answer):
+ log.info("notify_feishu.receive_type = {} receive_id={}",
+ receive_type, receive_id)
+
+ url = "https://open.feishu.cn/open-apis/im/v1/messages"
+ params = {"receive_id_type": receive_type}
+
+ # text = at_id and "%s" % (
+ # at_id, answer.lstrip()) or answer.lstrip()
+ text = answer.lstrip()
+ log.info("notify_feishu.text = {}", text)
+ msgContent = {
+ "text": text,
+ }
+ req = {
+ "receive_id": receive_id, # chat id
+ "msg_type": "text",
+ "content": json.dumps(msgContent),
+ }
+ payload = json.dumps(req)
+ headers = {
+ # your access token
+ "Authorization": "Bearer " + token,
+ "Content-Type": "application/json",
+ }
+ response = requests.request(
+ "POST", url, params=params, headers=headers, data=payload
+ )
+ log.info("notify_feishu.response.content = {}", response.content)
+
+ def handle(self, message):
+ event = message["event"]
+ msg = event["message"]
+ messageId = msg["message_id"]
+ chat_type = msg["chat_type"]
+ sender_id = event["sender"]["sender_id"]["open_id"]
+
+ prompt = json.loads(msg["content"])["text"]
+ prompt = prompt.replace("@_user_1", "")
+
+ #重复
+ r, v = self.memory_store.get(messageId)
+ if v:
+ return {'ret': 200}
+
+ self.memory_store.set(messageId, True)
+
+ # 非文本不处理
+ message_type = msg["message_type"]
+ if message_type != "text":
+ return {'ret': 200}
+ if chat_type == "group":
+ mentions = msg["mentions"]
+ # 日常群沟通要@才生效
+ if not mentions:
+ return {'ret': 200}
+ receive_type = "chat_id"
+ receive_id = msg.get("chat_id")
+ at_id = sender_id
+ elif chat_type == "p2p":
+ receive_type = "open_id"
+ receive_id = sender_id
+ at_id = None
+
+ # 调用发消息 API 之前,先要获取 API 调用凭证:tenant_access_token
+ access_token = self.get_tenant_access_token()
+ if access_token == "":
+ log.error("send message access_token is empty")
+ return {'ret': 204}
+
+ context = dict()
+ img_match_prefix = functions.check_prefix(
+ prompt, channel_conf_val(const.DINGTALK, 'image_create_prefix'))
+ if img_match_prefix:
+ prompt = prompt.split(img_match_prefix, 1)[1].strip()
+ context['type'] = 'IMAGE_CREATE'
+ context['from_user_id'] = str(sender_id)
+ reply = super().build_reply_content(prompt, context)
+ if img_match_prefix:
+ if not isinstance(reply, list):
+ return {'ret': 204}
+ images = ""
+ for url in reply:
+ images += f"[!['IMAGE_CREATE']({url})]({url})\n"
+ reply = images
+ # 机器人 echo 收到的消息
+ self.notify_feishu(access_token, receive_type,
+ receive_id, at_id, reply)
+ return {'ret': 200}
+
+ def handle_request_url_verify(self, post_obj):
+ # 原样返回 challenge 字段内容
+ challenge = post_obj.get("challenge", "")
+ return {'challenge': challenge}
+
+
+feishu = FeiShuChannel()
+http_app = Flask(__name__,)
+
+
+@http_app.route("/", methods=['POST'])
+def chat():
+ # log.info("[FeiShu] chat_headers={}".format(str(request.headers)))
+ log.info("[FeiShu] chat={}".format(str(request.data)))
+ obj = json.loads(request.data)
+ if not obj:
+ return {'ret': 201}
+ # 校验 verification token 是否匹配,token 不匹配说明该回调并非来自开发平台
+ headers = obj.get("header")
+ if not headers:
+ return {'ret': 201}
+ token = headers.get("token", "")
+ if token != feishu.verification_token:
+ log.error("verification token not match, token = {}", token)
+ return {'ret': 201}
+
+ # 根据 type 处理不同类型事件
+ t = obj.get("type", "")
+ if "url_verification" == t: # 验证请求 URL 是否有效
+ return feishu.handle_request_url_verify(obj)
+ elif headers.get("event_type", None) == "im.message.receive_v1": # 事件回调
+ return feishu.handle(obj)
+ return {'ret': 202}
+
diff --git a/channel/feishu/store.py b/channel/feishu/store.py
new file mode 100644
index 00000000..e9caf52b
--- /dev/null
+++ b/channel/feishu/store.py
@@ -0,0 +1,67 @@
+# -*- coding: UTF-8 -*-
+
+import time
+from threading import Lock
+
+
+class Store(object):
+ """
+ This is an interface to storage (Key, Value) pairs for sdk.
+ """
+
+ def get(self, key): # type: (str) -> Tuple[bool, str]
+ return False, ''
+
+ def set(self, key, value, expire): # type: (str, str, int) -> None
+ """
+ storage key, value into the store, value has an expire time.(unit: second)
+ """
+ pass
+
+
+class ExpireValue(object):
+ def __init__(self, value, expireTime): # type: (str, int) -> None
+ self.value = value
+ self.expireTime = expireTime
+
+
+class MemoryStore(Store):
+ """
+ This is an implement of `StoreInterface` which stores data in the memory
+ """
+
+ def __init__(self): # type: () -> None
+ self.data = {} # type: Dict[str, ExpireValue]
+ self.mutex = Lock() # type: Lock
+
+ def get(self, key): # type: (str) -> Tuple[bool, str]
+ # print('get %s' % key)
+ self.mutex.acquire()
+ try:
+ val = self.data.get(key)
+ if val is None:
+ return False, ""
+ else:
+ if val.expireTime == -1:
+ return True, val.value
+ elif val.expireTime < int(time.time()):
+ self.data.pop(key)
+ return False, ""
+ else:
+ return True, val.value
+ finally:
+ self.mutex.release()
+
+ def set(self, key, value, expire=None): # type: (str, str, int) -> None
+ # print('put %s=%s, expire=%s' % (key, value, expire))
+ """
+ storage key, value into the store, value has an expire time.(unit: second)
+ """
+ self.mutex.acquire()
+ try:
+ self.data[key] = ExpireValue(
+ value, expire == None and -1 or int(time.time()) + expire)
+ finally:
+ self.mutex.release()
+
+
diff --git a/channel/http/http_channel.py b/channel/http/http_channel.py
index 2464f619..a82fbbb1 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 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['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 @@ def handle(self, data):
images += f"[!['IMAGE_CREATE']({url})]({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 a53c284e..7f1fe3b2 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 ae39d4d0..241ff2ef 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训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。输入 #清除记忆 可以开始新的话题探索。输入 画xx 可以为你画一张图片。我无法对事实性与实时性问题提供准确答复,请慎重对待回答。') {
+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 = $('' + msg + '
');
-
$('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
diff --git a/channel/http/templates/index.html b/channel/http/templates/index.html
index f05311f6..1b0f3856 100644
--- a/channel/http/templates/index.html
+++ b/channel/http/templates/index.html
@@ -19,33 +19,141 @@
-
-
-
-
-
+
+
+
+
+
+
+ var ConvStateMap = {
+ bold: false,
+ italic: false,
+ backgroundColor: '#ffffff',
+ conversationType: conversationType.DISPOSABLE
+ };
+
+ // Create a Proxy object to watch all properties of the "ConvStateMap" object
+ var localConfig = new Proxy(ConvStateMap, {
+ set: function (target, prop, val) {
+ target[prop] = val;
+ // Call your function here
+ localStorage.setItem('botOnAnyThingConfig', JSON.stringify(localConfig))
+ switch (prop) {
+ case 'backgroundColor':
+ $('body').css('background-color', val);
+ $(`#backgroundColor`)?.val(val);
+ break;
+ case 'conversationType':
+ if (val)
+ $(`#option${val}`)?.prop("checked", true);
+ }
+ }
+ });
+ $(document).ready(function () {
+ let config = localStorage.getItem('botOnAnyThingConfig')
+ if (config) {
+ config = JSON.parse(config)
+ Object.keys(config).forEach(item => localConfig[item] = config[item])
+ }
+ // Open drawer
+ $('.drawer-icon').click(function () {
+ if (!$('.drawer').hasClass('open')) {
+ $('.drawer').toggleClass('open');
+ $('.drawer-overlay').fadeIn();
+ $('.drawer-icon-container').toggleClass('open').css('right', '270px');
+ } else
+ closeDrawer()
+ });
+
+ // Close drawer
+ $('#close-drawer, .drawer-overlay').click(closeDrawer);
+
+ function closeDrawer() {
+ $('.drawer').removeClass('open');
+ $('.drawer-overlay').fadeOut();
+ $('.drawer-icon-container').removeClass('open').css('right', '-30px');
+ }
+ });
+ // Bind checkbox values to ConvStateMap object
+ $('input[type="checkbox"]').change(function () {
+ var key = $(this).attr('name');
+ if (key)
+ localConfig[key] = $(this).is(':checked');
+ });
+
+ // Bind radio button values to ConvStateMap object
+ $('input[type="radio"]').change(function () {
+ var key = $(this).attr('name');
+ if (key)
+ localConfig[key] = $(this).val();
+ });
+
+ // Bind color input value to ConvStateMap object
+ $('#backgroundColor').on("input", function (e) {
+ localConfig.backgroundColor = $(this).val();
+ });
+
+ $(window).on('unload', function () {
+ socket.disconnect();
+ });
+
+ jQuery(function (a) {
+ var b = a("#chat").convform()
+ });
+