初步增加web-http聊天功能

This commit is contained in:
RA
2023-03-07 01:30:10 +08:00
parent d786904601
commit cd76cabcc1
10 changed files with 794 additions and 1 deletions

View File

@@ -11,7 +11,7 @@
**应用:** **应用:**
- [x] [终端](https://github.com/zhayujie/bot-on-anything#1%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%BB%88%E7%AB%AF) - [x] [终端](https://github.com/zhayujie/bot-on-anything#1%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%BB%88%E7%AB%AF)
- [ ] Web - [x] [Web](https://github.com/zhayujie/bot-on-anything#9web)
- [x] [个人微信](https://github.com/zhayujie/bot-on-anything#2%E4%B8%AA%E4%BA%BA%E5%BE%AE%E4%BF%A1) - [x] [个人微信](https://github.com/zhayujie/bot-on-anything#2%E4%B8%AA%E4%BA%BA%E5%BE%AE%E4%BF%A1)
- [x] [订阅号](https://github.com/zhayujie/bot-on-anything#3%E4%B8%AA%E4%BA%BA%E8%AE%A2%E9%98%85%E5%8F%B7) - [x] [订阅号](https://github.com/zhayujie/bot-on-anything#3%E4%B8%AA%E4%BA%BA%E8%AE%A2%E9%98%85%E5%8F%B7)
- [x] [服务号](https://github.com/zhayujie/bot-on-anything#4%E4%BC%81%E4%B8%9A%E6%9C%8D%E5%8A%A1%E5%8F%B7) - [x] [服务号](https://github.com/zhayujie/bot-on-anything#4%E4%BC%81%E4%B8%9A%E6%9C%8D%E5%8A%A1%E5%8F%B7)
@@ -393,3 +393,35 @@ http:/你的固定公网ip或者域名:端口/slack/events
``` ```
https://slack.dev/bolt-python/tutorial/getting-started https://slack.dev/bolt-python/tutorial/getting-started
``` ```
### 9.Web
#### http
**需要:** 服务器
**依赖**
```bash
pip3 install PyJWT flask
```
**配置**
```bash
"channel": {
"type": "http",
"http": {
"http_auth_secret_key": "6d25a684-9558-11e9-aa94-efccd7a0659b",//jwt认证秘钥
"http_auth_password": "6.67428e-11",//认证密码,仅仅只是自用,最初步的防御别人扫描端口后DDOS浪费tokens
"port": "80"//端口
}
}
```
URL如果端口是 80 ,可不填
```
http:/你的固定公网ip或者域名:端口/
```

View File

@@ -41,5 +41,9 @@ def create_channel(channel_type):
from channel.slack.slack_channel import SlackChannel from channel.slack.slack_channel import SlackChannel
return SlackChannel() return SlackChannel()
elif channel_type == const.HTTP:
from channel.http.http_channel import HttpChannel
return HttpChannel()
else: else:
raise RuntimeError raise RuntimeError

107
channel/http/auth.py Normal file
View File

@@ -0,0 +1,107 @@
# encoding:utf-8
import jwt
import datetime
import time
from flask import jsonify, request
from common import const
from config import channel_conf
class Auth():
def __init__(self, login):
# argument 'privilegeRequired' is to set up your method's privilege
# name
self.login = login
super(Auth, self).__init__()
@staticmethod
def encode_auth_token(user_id, login_time):
"""
生成认证Token
:param user_id: int
:param login_time: datetime
:return: string
"""
try:
payload = {
'iss': 'ken', # 签名
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, hours=10), # 过期时间
'iat': datetime.datetime.utcnow(), # 开始时间
'data': {
'id': user_id,
'login_time': login_time
}
}
return jwt.encode(
payload,
channel_conf(const.HTTP).get('http_auth_secret_key'),
algorithm='HS256'
) # 加密生成字符串
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
验证Token
:param auth_token:
:return: integer|string
"""
try:
# 取消过期时间验证
payload = jwt.decode(auth_token, channel_conf(const.HTTP).get(
'http_auth_secret_key'), algorithms='HS256') # options={'verify_exp': False} 加上后不验证token过期时间
if ('data' in payload and 'id' in payload['data']):
return payload
else:
raise jwt.InvalidTokenError
except jwt.ExpiredSignatureError:
return 'Token过期'
except jwt.InvalidTokenError:
return '无效Token'
def authenticate(password):
"""
用户登录登录成功返回token
:param password:
:return: json
"""
authPassword = channel_conf(const.HTTP).get('http_auth_password')
if (authPassword != password):
return False
else:
login_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
token = Auth.encode_auth_token(password, login_time)
return token
def identify(request):
"""
用户鉴权
:return: list
"""
try:
if (request is None):
return False
authorization = request.cookies.get('Authorization')
if (authorization):
payload = Auth.decode_auth_token(authorization)
if not isinstance(payload, str):
authPassword = channel_conf(
const.HTTP).get('http_auth_password')
password = payload['data']['id']
if (password != authPassword):
return False
else:
return True
return False
except jwt.ExpiredSignatureError:
#result = 'Token已更改请重新登录获取'
return False
except jwt.InvalidTokenError:
#result = '没有提供认证token'
return False

View File

@@ -0,0 +1,66 @@
# encoding:utf-8
import json
from channel.http import auth
from flask import Flask, request, render_template, make_response
from datetime import timedelta
from common import const
from config import channel_conf
from channel.channel import Channel
http_app = Flask(__name__,)
# 自动重载模板文件
http_app.jinja_env.auto_reload = True
http_app.config['TEMPLATES_AUTO_RELOAD'] = True
# 设置静态文件缓存过期时间
http_app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1)
@http_app.route("/chat", methods=['POST'])
def chat():
if (auth.identify(request) == False):
return
data = json.loads(request.data)
if data:
msg = data['msg']
if not msg:
return
reply_text = HttpChannel().handle(data=data)
return {'result': reply_text}
@http_app.route("/", methods=['GET'])
def index():
if (auth.identify(request) == False):
return login()
return render_template('index.html')
@http_app.route("/login", methods=['POST', 'GET'])
def login():
response = make_response("<html></html>",301)
response.headers.add_header('content-type','text/plain')
response.headers.add_header('location','./')
if (auth.identify(request) == True):
return response
else:
if request.method == "POST":
token = auth.authenticate(request.form['password'])
if (token != False):
response.set_cookie(key='Authorization', value=token)
return response
else:
return render_template('login.html')
response.headers.set('location','./login?err=登录失败')
return response
class HttpChannel(Channel):
def startup(self):
http_app.run(host='0.0.0.0', port=channel_conf(const.HTTP).get('port'))
def handle(self, data):
context = dict()
id = data["id"]
context['from_user_id'] = str(id)
return super().build_reply_content(data["msg"], context)

329
channel/http/static/1.css Normal file
View File

@@ -0,0 +1,329 @@
.typing_loader {
width: 6px;
height: 6px;
border-radius: 50%;
-webkit-animation: typing 1s linear infinite alternate;
-moz-animation: typing 1s linear infinite alternate;
-ms-animation: typing 1s linear infinite alternate;
animation: typing 1s linear infinite alternate;
position: relative;
left: -12px;
margin: 7px 15px 6px;
}
ol,pre {
background-color: #b1e3b1c4;
border: 1px solid #c285e3ab;
padding: 0.5rem 1.5rem 0.5rem;
color: black;
border-radius: 10px;
}
.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);
}
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);
}
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);
}
}
@-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);
}
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);
}
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);
}
}
@keyframes typing-black {
0% {
background-color: 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, 0.2);
}
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);
}
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);
}
}
@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);
}
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);
}
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);
}
}
.convFormDynamic {
text-align: center;
margin: 10px 10px;
padding: 0 !important;
position: relative;
border: 2px solid rgba(0, 40, 100, 0.12);
}
.convFormDynamic textarea.userInputDynamic {
border: none;
padding: 7px 10px;
overflow-x: hidden!important;
outline: none;
font-size: 0.905rem;
float: left;
width: calc(100% - 70px);
line-height: 1.3em;
min-height: 1.7em;
max-height: 10rem;
display: block;
max-width: 89vw;
margin-right: -1vw;
resize: none;
}
.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 input.userInputDynamic {
border: none;
padding: 7px 10px;
outline: none;
font-size: 0.905rem;
float: left;
width: calc(100% - 70px);
line-height: 1.3em;
min-height: 1.7em;
max-height: 10rem;
display: block;
max-width: 89vw;
margin-right: -1vw;
}
div.conv-form-wrapper div#messages {
max-height: 71vh;
height: auto !important;
overflow-y: scroll;
}
div.conv-form-wrapper div#messages:after {
content: '';
display: table;
clear: both;
}
div.conv-form-wrapper {
position: relative;
}
div.conv-form-wrapper div.wrapper-messages {
position: relative;
height: 76vh;
max-height: 80vh;
overflow-y: scroll;
}
div.conv-form-wrapper:before {
content: '';
position: absolute;
width: 100%;
display: block;
height: 30px;
top: 0;
left: 0;
z-index: 2;
background: linear-gradient(#fff, transparent);
}
@media (max-width: 767px) {
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 {
width: 0px;
height: 0px;
/* remove scrollbar space */
background: transparent;
/* optional: just make scrollbar invisible */
}
input[type="text"].userInputDynamic.error {
color: #ac0000 !important;
}
input[type="text"].userInputDynamic {
border-radius: 3px;
margin: 7px 10px;
}
textarea.userInputDynamic.error {
color: #ac0000 !important;
}
textarea.userInputDynamic {
border-radius: 3px;
margin: 7px 10px;
}
div.conv-form-wrapper div#messages {
transition: bottom 0.15s, padding-bottom 0.15s;
position: absolute;
bottom: 0;
height: auto !important;
width: 100%;
padding-bottom: 20px;
/*max-height: 71vh;*/
}
div.conv-form-wrapper div.message {
animation: slideTop 0.15s ease;
}
div.conv-form-wrapper div.message:after {
content: '';
display: table;
clear: both;
}
div.conv-form-wrapper div.message.ready {
animation: bounceIn 0.2s ease;
transform-origin: 0 0 0;
}
div.conv-form-wrapper div#messages div.message {
border-radius: 20px;
padding: 12px 22px;
font-size: 0.905rem;
display: inline-block;
padding: 10px 15px 8px;
border-radius: 20px;
margin-bottom: 5px;
float: right;
clear: both;
max-width: 65%;
word-wrap: break-word;
}
div.conv-form-wrapper div#messages div.message.to {
float: left;
background: lawngreen;
border-top-left-radius: 0;
}
div.conv-form-wrapper div#messages div.message.from {
background: dodgerblue;
color: #fff;
border-top-right-radius: 0;
}
.message.to+.message.from, .message.from+.message.to {
margin-top: 15px;
}
@keyframes slideTop {
0% {
margin-bottom: -25px;
}
100% {
margin-bottom: 0;
}
}
@keyframes bounceIn {
0% {
transform: scale(0.75, 0.75);
}
100% {
transform: scale(1.0, 1.0);
}
}
.convFormDynamic button.submit {
position: absolute;
bottom: 0px;
border: none;
left:95%;
margin: 5px;
color: #fff;
cursor: pointer;
border-radius: 8px;
font-size: 1.6rem;
width: 50px;
height: 42px;
border: 1px solid #b7b7b7;
background: #c3c3c3;
outline: none !important;
}
.center-block {
margin-right: 0;
margin-left: 0;
float: none;
text-align: center;
}
button.submit.glow {
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;
}
.dragscroll {
cursor: grab;
}
div.conv-form-wrapper div#messages::-webkit-scrollbar, div#feed ul::-webkit-scrollbar {
width: 0px;
/* remove scrollbar space */
background: transparent;
/* optional: just make scrollbar invisible */
}
span.clear {
display: block;
clear: both;
}

134
channel/http/static/1.js Normal file
View File

@@ -0,0 +1,134 @@
function ConvState(wrapper, form, params) {
this.id='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);
});
this.form = form;
this.wrapper = wrapper;
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训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。') {
setTimeout(function () {
var messageObj = $(this.wrapper).find('.message.typing');
answer = marked.parse(answer);
messageObj.html(answer);
messageObj.removeClass('typing').addClass('ready');
this.scrollDown();
$(this.wrapper).find(this.parameters.inputIdHashTagName).focus();
}.bind(this), 500);
};
ConvState.prototype.sendMessage = function (msg) {
var message = $('<div class="message from">' + msg + '</div>');
$('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 = $('<div class="message to typing"><div class="typing_loader"></div></div>');
setTimeout(function () {
$(this.wrapper).find('#messages').append(messageObj);
this.scrollDown();
}.bind(this), 150);
var _this = this
$.ajax({
url: "./chat",
type: "POST",
timeout:60000,
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("网络故障,对话未送达")
},
})
};
(function ($) {
$.fn.convform = function () {
var wrapper = this;
$(this).addClass('conv-form-wrapper');
var parameters = $.extend(true, {}, {
placeHolder: 'Type Here',
typeInputUi: 'textarea',
formIdName: 'convForm',
inputIdName: 'userInput',
buttonText: '▶'
});
//hides original form so users cant interact with it
var form = $(wrapper).find('form').hide();
var inputForm;
parameters.inputIdHashTagName = '#' + parameters.inputIdName;
inputForm = $('<div id="' + parameters.formIdName + '" class="convFormDynamic"><div class="options dragscroll"></div><textarea id="' + parameters.inputIdName + '" rows="1" placeholder="' + parameters.placeHolder + '" class="userInputDynamic"></textarea><button type="submit" class="submit">' + parameters.buttonText + '</button><span class="clear"></span></form>');
//appends messages wrapper and newly created form with the spinner load
$(wrapper).append('<div class="wrapper-messages"><div class="spinLoader"></div><div id="messages"></div></div>');
$(wrapper).append(inputForm);
var state = new ConvState(wrapper, form, parameters);
//prints first contact
$.when($('div.spinLoader').addClass('hidden')).done(function () {
var messageObj = $('<div class="message to typing"><div class="typing_loader"></div></div>');
$(state.wrapper).find('#messages').append(messageObj);
state.scrollDown();
state.printAnswer();
});
//binds enter to send message
$(inputForm).find(parameters.inputIdHashTagName).keypress(function (e) {
if (e.which == 13) {
var input = $(this).val();
e.preventDefault();
if (input.trim() != '' && !state.wrapper.find(parameters.inputIdHashTagName).hasClass("error")) {
$(parameters.inputIdHashTagName).val("");
state.sendMessage(input);
} else {
$(state.wrapper).find(parameters.inputIdHashTagName).focus();
}
}
autosize.update($(state.wrapper).find(parameters.inputIdHashTagName));
})
$(inputForm).find(parameters.inputIdHashTagName).on('input', function (e) {
if ($(this).val().length > 0) {
$('button.submit').addClass('glow');
} else {
$('button.submit').removeClass('glow');
}
});
$(inputForm).find('button.submit').click(function (e) {
var input = $(state.wrapper).find(parameters.inputIdHashTagName).val();
e.preventDefault();
if (input.trim() != '' && !state.wrapper.find(parameters.inputIdHashTagName).hasClass("error")) {
$(parameters.inputIdHashTagName).val("");
state.sendMessage(input);
} else {
$(state.wrapper).find(parameters.inputIdHashTagName).focus();
}
autosize.update($(state.wrapper).find(parameters.inputIdHashTagName));
});
if (typeof autosize == 'function') {
$textarea = $(state.wrapper).find(parameters.inputIdHashTagName);
autosize($textarea);
}
return state;
}
})(jQuery);

View File

@@ -0,0 +1,51 @@
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge"><!-- Title -->
<title>ChatGPT</title><!-- Bootstrap Css -->
<link href="./static/1.css" rel="stylesheet" />
<style>
button {
font-family: 'Microsoft YaHei';
}
</style>
</head>
<body class="">
<div class="no-border">
<div id="chat" class="conv-form-wrapper">
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/marked/4.2.12/marked.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/autosize.js/6.0.1/autosize.min.js"></script>
<script src="./static/1.js"></script>
<script>
var rollbackTo = false;
var originalState = false;
function storeState(a) {
rollbackTo = a.current
}
function rollback(a) {
if (rollbackTo != false) {
if (originalState == false) {
originalState = a.current.next
}
a.current.next = rollbackTo
}
}
function restore(a) {
if (originalState != false) {
a.current.next = originalState
}
}
jQuery(function (a) {
var b = a("#chat").convform()
});
</script>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge"><!-- Title -->
<title>登录</title><!-- Bootstrap Css -->
<style>
.login-form {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.5);
border-radius: 8px;
width: 350px;
max-width: 100%;
padding: 15px 35px 15px;
margin: auto;
position: absolute;
top: 50%;
left: 50%;
margin: -160px 0 0 -200px;
}
.Button {
width: 80px;
margin: 3px 1px 0 5px;
padding: 0 10px;
background-color: #16a0d3;
border: none;
display: inline-block;
font-family: "Microsoft Yahei";
font-size: 12px;
height: 27px;
color: #FFF;
border-radius: 5px;
}
</style>
</head>
<body class="">
<form name="login" class="login-form" action="./login" method="post" autocomplete="off">
<input type="password" name="password" placeholder="Password" style="border: none; height: 25px;width: 250px;"
required>
</input>
<input type="submit" class="Button" value="登录" />
<span style="color:red">
<p id="err"></p>
</span>
</form>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
$(function () {
var err=getUrlParam('err')
$('#err')[0].innerHTML=err
});
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return decodeURI(r[2]); return null;
}
</script>
</html>

View File

@@ -7,6 +7,7 @@ QQ = "qq"
GMAIL = "gmail" GMAIL = "gmail"
TELEGRAM = "telegram" TELEGRAM = "telegram"
SLACK = "slack" SLACK = "slack"
HTTP = "http"
# model # model
OPEN_AI = "openai" OPEN_AI = "openai"

View File

@@ -41,6 +41,12 @@
"slack": { "slack": {
"slack_bot_token": "xoxb-xxxx", "slack_bot_token": "xoxb-xxxx",
"slack_signing_secret": "xxxx" "slack_signing_secret": "xxxx"
},
"http": {
"http_auth_secret_key": "6d25a684-9558-11e9-aa94-efccd7a0659b",
"http_auth_password": "6.67428e-11",
"port": "80"
} }
} }
} }