Compare commits

..

31 Commits
v0.2 ... master

Author SHA1 Message Date
23b08d4cf5 add API error message in response. 2023-05-17 22:16:35 +08:00
c66d325d8f 更新 'README.md' 2023-05-03 11:10:05 +08:00
833ebade22 更新 'README.md' 2023-04-20 02:49:18 +08:00
6bbdc72162 添加 'README.md' 2023-04-20 02:48:50 +08:00
839de42ab9 add repo url in homepage notice 2023-04-20 00:54:22 +08:00
e0eec9630b more robust email format 2023-04-20 00:48:52 +08:00
a9b4a08357 refine activation mail content 2023-04-19 23:30:53 +08:00
7953235912 implement activation code 2023-04-19 22:50:55 +08:00
2cebb06f76 Merge branch 'master' of https://gitea.w-q.top:520/wangjiacai/web-gpt 2023-04-19 00:46:28 +08:00
c9d9049ea2 put pip dependency into project dir 2023-04-19 00:46:16 +08:00
23a575d61b put pip dependency into project dir 2023-04-19 00:42:36 +08:00
ba3d3be2a9 Merge pull request 'dev_mail' (#18) from dev_mail into master
Reviewed-on: #18
2023-04-19 00:20:14 +08:00
f65a62606e Merge branch 'master' into dev_mail 2023-04-19 00:19:13 +08:00
ae16b99614 implement sendmail 2023-04-19 00:16:19 +08:00
46ebdd4843 use same cdn provider 2023-04-18 22:08:03 +08:00
9f515b5660 use same cdn provider 2023-04-18 22:07:37 +08:00
40554545f2 limit request rate if API fail 2023-04-18 21:56:11 +08:00
be2bb856b0 add log for openai api request failure 2023-04-18 21:38:59 +08:00
489f6b223c ctrl+enter to send message 2023-04-18 21:38:34 +08:00
6f3df5fc51 fix wrong ip address behind proxy 2023-04-18 01:28:58 +08:00
9c309dafc2 add favicon 2023-04-18 01:27:49 +08:00
19423c7f84 add placeholder for input prompt 2023-04-18 01:06:35 +08:00
a9b1e22b47 put config in app.instance_path 2023-04-17 23:59:48 +08:00
ca99215ac7 add server log 2023-04-17 23:28:00 +08:00
b7ba1cf4c1 delete chat history when deleting account 2023-04-17 22:55:37 +08:00
a4e718067d update requirements.txt 2023-04-17 22:49:08 +08:00
2eecdbaf8f use yaml for config file 2023-04-17 22:44:07 +08:00
152ce7a170 support old browsers, remove "?." operator 2023-04-17 22:42:40 +08:00
ef5022ff64 prompt and homepage notice tuning. 2023-04-08 10:42:14 +08:00
c4800ddf55 start server with waitress 2023-04-08 10:41:13 +08:00
6dcf5d757a return error message when request fail 2023-04-08 10:40:48 +08:00
14 changed files with 270 additions and 62 deletions

View File

@ -4,6 +4,6 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/re
RUN apk add git python3 py3-pip
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN python3 -m pip install --upgrade pip && pip3 install --upgrade setuptools
COPY . /web-gpt
RUN pip3 install -r /web-gpt/requirements.txt
CMD cd /web-gpt && flask --app=project run --host=0.0.0.0
COPY ./project /web-gpt/project
RUN pip3 install -r /web-gpt/project/requirements.txt
CMD cd /web-gpt && waitress-serve --listen 0.0.0.0:5000 project:app

9
README.md Normal file
View File

@ -0,0 +1,9 @@
https://chat.w-q.top:520
OpenAI官方API的套壳网站。
试图规避ChatGPT的使用门槛把科技带给更多人。
雷锋同志做好事从不留名。
如果一定要一个姓名请叫我Prometheus.

View File

@ -1,14 +0,0 @@
[app]
NAME=APP_NAME
SECRET_KEY=SOME_RANDOM_STRING
HOMEPAGE_NOTICE=1. 此网站基于openAI的API提供服务\n2. 为了限制滥用,注册后需要管理员激活才能使用\n3. 为了支持多轮对话,历史聊天会保存在服务端\n4. 网站不做关键词过滤,但请不要违反相关法律
SQLALCHEMY_DATABASE_URI=sqlite:///sqlite.db
#SQLALCHEMY_DATABASE_URI=mysql://username:password@server/db
[network]
PROXY=http://127.0.0.1:7890
[openai]
API_KEY=
MODEL_NAME=gpt-3.5-turbo
PROMPT=You are a helpful assistant

33
instance/config.yaml Normal file
View File

@ -0,0 +1,33 @@
app:
NAME: web-gpt
SECRET_KEY: SOME_RANDOM_STRING
HOMEPAGE_NOTICE: |
1. 此网站基于openAI的API提供服务
2. 为了支持多轮对话,历史聊天会保存在服务端
3. 网站不做关键词过滤,但请不要违反相关法律
4. GPT生成的任何内容不保证准确性请自行甄别
5. 项目仓库如下,欢迎提交代码
https://gitea.w-q.top:520/wangjiacai/web-gpt
SQLALCHEMY_DATABASE_URI: sqlite:///sqlite.db
# SQLALCHEMY_DATABASE_URI: mysql://username:password@server/db
# LOGGING_LEVEL: CRITICAL | FATAL | ERROR | WARN | WARNING | INFO | DEBUG | NOTSET
LOGGING_LEVEL: INFO
network:
PROXY: http://127.0.0.1:7890
openai:
API_KEY:
MODEL_NAME: gpt-3.5-turbo
PROMPT: 你是一个有用的人工智能助理,你尽力确保回答的准确性,避免给出误导信息。
mail:
host:
port:
username:
from:
password:

View File

@ -1,24 +1,37 @@
from flask import Flask
from flask import Flask, request, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from configparser import ConfigParser
import yaml
import logging
import time
import traceback
import os
import waitress
# init SQLAlchemy so we can use it later in our models
db = SQLAlchemy()
def create_app():
conf = ConfigParser()
conf.read("./config.ini")
app = Flask(__name__)
app.config['SECRET_KEY'] = conf['app']['SECRET_KEY']
app.config['SQLALCHEMY_DATABASE_URI'] = conf['app']['SQLALCHEMY_DATABASE_URI']
app.config['HOMEPAGE_NOTICE'] = conf['app']['HOMEPAGE_NOTICE']
app.config['NETWORK_PROXY'] = conf['network']['PROXY']
app.config['OPENAI_API_KEY'] = conf['openai']['API_KEY']
app.config['OPENAI_MODEL_NAME'] = conf['openai']['MODEL_NAME']
app.config['OPENAI_PROMPT'] = conf['openai']['PROMPT']
print("project instance dir: ", app.instance_path)
config_file = app.instance_path+"/config.yaml"
print("config file path: ", config_file)
with open(config_file) as config_file:
conf = yaml.safe_load(config_file)
app.config['NAME'] = conf['app']['NAME']
app.config['SECRET_KEY'] = conf['app']['SECRET_KEY']
app.config['LOGGING_LEVEL'] = conf['app']['LOGGING_LEVEL']
app.config['SQLALCHEMY_DATABASE_URI'] = conf['app']['SQLALCHEMY_DATABASE_URI']
app.config['HOMEPAGE_NOTICE'] = conf['app']['HOMEPAGE_NOTICE']
app.config['NETWORK_PROXY'] = conf['network']['PROXY']
app.config['OPENAI_API_KEY'] = conf['openai']['API_KEY']
app.config['OPENAI_MODEL_NAME'] = conf['openai']['MODEL_NAME']
app.config['OPENAI_PROMPT'] = conf['openai']['PROMPT']
app.config['mail'] = conf['mail']
app.add_template_global(app.config['NAME'], "web_title")
db.init_app(app)
login_manager = LoginManager()
@ -45,3 +58,45 @@ def create_app():
app.register_blueprint(main_blueprint)
return app
app = create_app()
logger = logging.getLogger('waitress')
logger.setLevel(app.config['LOGGING_LEVEL'])
def get_actual_addr(request):
ip = request.remote_addr
if request.headers.getlist("X-Forwarded-For"):
ip = request.headers.getlist("X-Forwarded-For")[0]
return ip
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.before_request
def before_request():
timestamp = time.strftime('[%Y-%b-%d %H:%M]')
logger.info('%s > %s %s %s %s', timestamp, get_actual_addr(request),
request.method, request.scheme, request.full_path)
@app.after_request
def after_request(response):
timestamp = time.strftime('[%Y-%b-%d %H:%M]')
logger.info('%s < %s %s %s %s %s', timestamp, get_actual_addr(request),
request.method, request.scheme, request.full_path, response.status)
return response
@app.errorhandler(Exception)
def exceptions(e):
tb = traceback.format_exc()
timestamp = time.strftime('[%Y-%b-%d %H:%M]')
logger.error('%s %s %s %s %s 5xx INTERNAL SERVER ERROR\n%s', timestamp,
get_actual_addr(request), request.method, request.scheme, request.full_path, tb)
return e.status_code

View File

@ -1,14 +1,38 @@
from flask_login import login_user, logout_user
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import login_required, current_user, login_manager
from .models import User
from .models import User, Conversation
from . import db
import time
import hashlib
auth = Blueprint('auth', __name__)
def hash_to_digit(instr: str) -> str:
outstr = hashlib.md5(instr.encode('utf-8')).hexdigest()
outstr = f"{int(outstr, 16) % 1000000:0>6d}"
return outstr
def gen_activation_code(email: str) -> str:
current_time_slot = int(time.time() // 60)
s = email + current_app.config['SECRET_KEY'] + str(current_time_slot)
activation_code = hash_to_digit(s)
return activation_code
def check_activation_code(email: str, activation_code: str) -> bool:
current_time_slot = int(time.time() // 60)
for time_slot in range(current_time_slot-10, current_time_slot+1):
s = email + current_app.config['SECRET_KEY'] + str(time_slot)
expected_code = hash_to_digit(s)
if expected_code == activation_code:
return True
return False
@auth.route('/login')
def login():
return render_template('login.html')
@ -35,6 +59,22 @@ def login_post():
return redirect(url_for('main.index'))
@auth.route('/activate', methods=['POST'])
@login_required
def activate():
activation_code = request.form.get('activation_code')
if check_activation_code(current_user.email, activation_code):
account = User.query.filter_by(
id=current_user.id, email=current_user.email, name=current_user.name).first()
if account:
if db.session.query(User).filter(User.id == account.id).update({"isActivated": True}) and not db.session.commit():
time.sleep(0.05)
return redirect(url_for('main.index'))
time.sleep(1)
flash("激活码不匹配")
return redirect(url_for('main.index'))
@auth.route('/signup')
def signup():
return render_template('signup.html')
@ -53,12 +93,12 @@ def signup_post():
flash('此邮箱已注册!')
return redirect(url_for('auth.signup'))
if not (email):
flash('Email missing!')
flash('请输入邮箱!')
return redirect(url_for('auth.signup'))
if not (name):
name = email
if not (password):
flash('Password missing!')
flash('请输入密码')
return redirect(url_for('auth.signup'))
# create a new user with the form data. Hash the password so the plaintext version isn't saved.
new_user = User(email=email,
@ -74,6 +114,14 @@ def signup_post():
# add the new user to the database
db.session.add(new_user)
db.session.commit()
activation_code = gen_activation_code(new_user.email)
from . import smtp
content = f"Hi {new_user.name},欢迎注册!\n"
content += f"您的激活码是:\n{activation_code}\n\n"
content += "此激活码十分钟内有效,过期请联系管理员激活,谢谢!"
smtp.sendmail(new_user.email, "web-gpt激活码", content)
return redirect(url_for('auth.login'))
@ -119,6 +167,9 @@ def manage_post():
account = User.query.filter_by(
id=id, email=email, name=name, role=role, isActivated=isActivated).first()
if account:
db.session.query(Conversation).filter(
Conversation.userid == id).delete()
db.session.commit()
if db.session.query(User).filter(User.id == id).delete() and not db.session.commit():
time.sleep(0.05)
return "success"

View File

@ -4,16 +4,18 @@ from .models import User, Conversation
from . import db
from datetime import datetime, timedelta
import openai
import logging
import time
main = Blueprint('main', __name__)
logger = logging.getLogger('waitress')
@main.route('/')
def index():
notice = current_app.config['HOMEPAGE_NOTICE']
if notice:
notice = notice.split("\\n")
return render_template('index.html', user=current_user, homepage_notice=notice)
@ -95,12 +97,17 @@ def chat_post():
else:
messages.append(
{"role": "user", "content": chat.request})
try:
openai_resp = openai.ChatCompletion.create(
model=current_app.config['OPENAI_MODEL_NAME'],
messages=messages
)
msg_resp = openai_resp['choices'][0]['message']['content']
except Exception as e:
logger.error("OpenAI API request failed: %s", repr(e))
msg_resp = "请求错误,请尝试重发。如果持续错误,请联系管理员检查。" + "\n"+"错误信息: " + repr(e)
time.sleep(1)
openai_resp = openai.ChatCompletion.create(
model=current_app.config['OPENAI_MODEL_NAME'],
messages=messages
)
msg_resp = openai_resp['choices'][0]['message']['content']
if msg_resp:
response = {"message": msg_resp, "status": "success"}
else:

View File

@ -2,5 +2,7 @@ Flask==2.2.3
Flask_Login==0.6.2
flask_sqlalchemy==3.0.3
openai==0.27.2
PyYAML==6.0
SQLAlchemy==2.0.7
waitress==2.1.2
Werkzeug==2.2.3

31
project/smtp.py Normal file
View File

@ -0,0 +1,31 @@
import smtplib
import logging
import email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from flask import current_app
mail_config = current_app.config['mail']
logger = logging.getLogger('waitress')
def sendmail(to: str, subject: str, message: str):
if not (mail_config['username'] and mail_config['password']):
logger.error("smtp email account is not configured")
mail = MIMEMultipart('alternative')
mail['Subject'] = Header(subject).encode()
mail['From'] = '%s <%s>' % (
Header(mail_config['from']).encode(), mail_config['username'])
mail['To'] = to
mail['Message-id'] = email.utils.make_msgid()
mail['Date'] = email.utils.formatdate()
mail.attach(MIMEText(message, "plain"))
try:
smtp = smtplib.SMTP_SSL(mail_config['host'], int(mail_config['port']))
smtp.login(mail_config['username'], mail_config['password'])
smtp.sendmail(mail_config['username'], to, mail.as_string())
except Exception as e:
logger.error("sendmail error: %s", repr(e))
smtp.quit()

BIN
project/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -6,11 +6,13 @@
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Flask</title>
<title>{{ web_title }}</title>
<link rel="shortcut icon"
href="{{ url_for('static', filename='favicon.ico') }}"/>
<link rel="stylesheet"
href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"/>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css"/>
<link href="https://cdn.staticfile.org/bulma/0.9.4/css/bulma.min.css"
rel="stylesheet"/>
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>

View File

@ -7,11 +7,16 @@
</ul>
</div>
<div class="fixed-bottom form-inline">
<textarea id="msgbox" class="form-control" style="width:85%; float: left; margin-bottom: 20px;"></textarea>
<textarea id="msgbox"
class="form-control"
style="width:85%;
float: left;
margin-bottom: 20px"
placeholder="说点什么吧"></textarea>
<button id="btn-send"
class="btn btn-info"
type="button"
style="width: 10%;"
style="width: 10%"
onclick="send_message()"
disabled>
<svg xmlns="http://www.w3.org/2000/svg"
@ -37,8 +42,14 @@
btn_send.disabled = true;
}
});
</script>
<script>
var textarea = document.getElementById("msgbox")
textarea.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.keyCode == 13) {
send_message()
}
});
function get_history() {
var msgs
$.ajax({
@ -99,7 +110,7 @@
success: null,
dataType: null
}).always(function (response) {
if (response?.status == "success") {
if (response && response.status == "success") {
var msg_list = document.getElementById("msg-list")
var li = document.createElement('li');
var pre = document.createElement('pre');

View File

@ -7,13 +7,36 @@
你好,游客!
{% endif %}
</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}<div class="notification is-danger">{{ messages[0] }}</div>{% endif %}
{% endwith %}
{% if user and user.is_authenticated %}
{% if user.isActivated %}
<a href="{{ url_for('main.chat') }}">
<button type="button" class="btn btn-primary">开始聊天</button>
</a>
{% else %}
<p class="text-warning">您的账号暂未激活,请等待管理员激活此账号。</p>
<div class="column is-4 is-offset-4">
<div class="box">
<p class="text-warning">您的账号暂未激活。</p>
<p class="text-warning">如未收到激活码邮件,请联系管理员处理</p>
<br/>
<form method="post" action="{{ url_for('auth.activate') }}">
<div class="field">
<div class="control">
<input class="input is-large"
type="text"
name="activation_code"
placeholder="激活码"
autofocus=""
required="required"/>
</div>
</div>
<button class="button is-block is-info is-large is-fullwidth">激活</button>
</form>
</div>
</div>
{% endif %}
{% else %}
<a href="{{ url_for('auth.login') }}">
@ -24,12 +47,10 @@
<button type="button" class="btn btn-default">注册</button>
</a>
{% endif %}
{% with messages = get_flashed_messages() %}
{% if messages %}<div class="notification is-danger">{{ messages[0] }}</div>{% endif %}
{% endwith %}
<div id="homepage-notice" class="row" style="margin-top: 100px;">
{% if homepage_notice %}
{% for notice in homepage_notice %}<p>{{ notice }}</p>{% endfor %}
<h3 class="subtitle">公告栏</h3>
<pre>{{ homepage_notice }}</pre>
{% endif %}
</div>
{% endblock content %}

View File

@ -65,11 +65,11 @@
var account = obj.parentElement.parentElement
var data = {
method: "update",
id: account?.children[0]?.innerHTML,
email: account?.children[1]?.innerHTML,
name: account?.children[2]?.innerHTML,
role: account?.children[3]?.children[0].value,
isActivated: account?.children[4].children[0].checked
id: account.children[0].innerHTML,
email: account.children[1].innerHTML,
name: account.children[2].innerHTML,
role: account.children[3].children[0].value,
isActivated: account.children[4].children[0].checked
}
$.ajax({
type: 'POST',
@ -89,11 +89,11 @@
var account = obj.parentElement.parentElement
var data = {
method: "delete",
id: account?.children[0]?.innerHTML,
email: account?.children[1]?.innerHTML,
name: account?.children[2]?.innerHTML,
role: account?.children[3]?.children[0].value,
isActivated: account?.children[4].children[0].checked
id: account.children[0].innerHTML,
email: account.children[1].innerHTML,
name: account.children[2].innerHTML,
role: account.children[3].children[0].value,
isActivated: account.children[4].children[0].checked
}
var ret = confirm("确认删除用户\"" + data.name + "\"吗?")
if (ret == true) {