├── flasks ├── static │ ├── css │ │ └── style.css │ ├── js │ │ └── main.js │ ├── favicon.png │ └── img │ │ ├── gpt.png │ │ └── claude.png ├── __pycache__ │ ├── utils.cpython-311.pyc │ ├── __init__.cpython-311.pyc │ └── models.cpython-311.pyc ├── blueprints │ ├── __pycache__ │ │ ├── auth.cpython-311.pyc │ │ ├── chat.cpython-311.pyc │ │ ├── main.cpython-311.pyc │ │ ├── user.cpython-311.pyc │ │ ├── admin.cpython-311.pyc │ │ ├── claude.cpython-311.pyc │ │ └── __init__.cpython-311.pyc │ ├── __init__.py │ ├── user.py │ ├── claude.py │ ├── chat.py │ ├── main.py │ ├── auth.py │ └── admin.py ├── templates │ ├── error │ │ ├── 404.html │ │ └── 500.html │ ├── admin │ │ ├── _user_table.html │ │ ├── pagination.html │ │ ├── claude.html │ │ ├── chat.html │ │ └── admin_user.html │ ├── auth │ │ ├── forgot_password.html │ │ ├── reset_password.html │ │ ├── login.html │ │ ├── register.html │ │ └── verify_otp.html │ ├── main │ │ └── index.html │ ├── user │ │ └── profile.html │ └── base.html ├── __init__.py ├── models.py └── utils.py ├── requirements.txt ├── run.py ├── instance └── config.py ├── README.md └── init_db.sql /flasks/static/css/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flasks/static/js/main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/requirements.txt -------------------------------------------------------------------------------- /flasks/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/static/favicon.png -------------------------------------------------------------------------------- /flasks/static/img/gpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/static/img/gpt.png -------------------------------------------------------------------------------- /flasks/static/img/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/static/img/claude.png -------------------------------------------------------------------------------- /flasks/__pycache__/utils.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/__pycache__/utils.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/auth.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/auth.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/chat.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/chat.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/main.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/main.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/user.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/user.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/admin.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/admin.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/claude.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/claude.cpython-311.pyc -------------------------------------------------------------------------------- /flasks/blueprints/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bear-biscuit/OAIFree_Share/HEAD/flasks/blueprints/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # 将 `flasks` 目录的上一级目录加入 Python 路径 5 | # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) 6 | from flasks import create_app 7 | 8 | app = create_app() 9 | 10 | if __name__ == '__main__': 11 | app.run(host='0.0.0.0',port=8080, debug=True) 12 | -------------------------------------------------------------------------------- /flasks/templates/error/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}页面未找到{% endblock %} 4 | 5 | {% block content %} 6 |
11 | {% endblock %} -------------------------------------------------------------------------------- /flasks/templates/error/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}服务器错误{% endblock %} 4 | 5 | {% block content %} 6 | 11 | {% endblock %} -------------------------------------------------------------------------------- /flasks/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import bp as auth_bp 2 | from .admin import bp as admin_bp 3 | from .main import bp as main_bp 4 | from .user import bp as user_bp 5 | from .chat import bp as chat_bp 6 | from .claude import bp as claude_bp 7 | 8 | # 导出所有蓝图,这样可以在工厂函数中直接使用 from .blueprints import auth, admin, main 9 | auth = auth_bp 10 | admin = admin_bp 11 | main = main_bp 12 | user = user_bp 13 | chat = chat_bp 14 | claude = claude_bp 15 | 16 | __all__ = ['auth', 'admin', 'main', 'chat', 'claude'] -------------------------------------------------------------------------------- /instance/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | 3 | # 数据库配置 4 | MYSQL_HOST = '127.0.0.1' 5 | MYSQL_USER = 'root' 6 | MYSQL_PASSWORD = 'password' 7 | MYSQL_DB = 'oaifree' 8 | 9 | # Flask配置 10 | SECRET_KEY = 'awhkwgbfflauws' # 请更改为随机字符串 11 | DEBUG = False # 生产环境设置为False 12 | 13 | # Cookie 和 Session 配置 14 | JWT_EXPIRATION_DAYS = 30 # 记住我的过期时间(天) 15 | COOKIE_SECURE = False # 仅通过HTTPS发送cookie 16 | COOKIE_HTTPONLY = True # 防止XSS攻击的httponly标志 17 | SESSION_COOKIE_SECURE = False # 与COOKIE_SECURE保持一致 18 | SESSION_COOKIE_HTTPONLY = True # 与COOKIE_HTTPONLY保持一致 19 | SESSION_COOKIE_SAMESITE = 'Lax' # 防止CSRF攻击的SameSite策略 20 | 21 | # CSRF 配置 22 | WTF_CSRF_ENABLED = True # 启用CSRF保护 23 | WTF_CSRF_TIME_LIMIT = 3600 # CSRF Token过期时间(秒) 24 | WTF_CSRF_SSL_STRICT = False # 是否严格要求HTTPS(与COOKIE_SECURE保持一致) 25 | WTF_CSRF_CHECK_DEFAULT = True # 默认检查所有POST/PUT/PATCH/DELETE请求 26 | 27 | # 数据库URI 28 | SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}/{MYSQL_DB}?ssl_disabled=true' 29 | SQLALCHEMY_TRACK_MODIFICATIONS = False 30 | 31 | # 镜像地址 32 | DOMAIN_CHATGPT = 'new.oaifree.com' 33 | DOMAIN_CLAUDE = 'demo.fuclaude.com' 34 | 35 | 36 | 37 | REGISTER = False # 默认不开启注册 38 | 39 | REG_CODE = True # 注册是否需要邀请码 默认需要 40 | 41 | # 邮箱发信相关 开启注册时必须配置 42 | EMAIL_FORNAME = 'AI共享' # 发件者名称 43 | EMAIL_TONAME = '用户' # 收件者名称 44 | EMAIL_API = 'https://email-worker.domain.top/api/send_mail' # cloudflare邮箱后端worker地址 45 | EMAIL_JWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYWRtaW5AYm9jY2hpMmIu32sawWwiYWRkcmVzc19pZCI6IjEifQ.wz8aTSwoibHG8shrbMa36pXU-mg-eVt0pRKvvcHl3RU' # 发送邮件的邮箱JWT 46 | 47 | 48 | # 前端相关 49 | DOMAIN_NAME = 'AI' # 网站名 50 | DOMAIN_EMAIL = 'your@mail.com' # “联系我”邮箱 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | 基于始皇的OAIFree服务,搭建一个共享站,方便给自己的小伙伴们使用 3 | 4 | ## 配置项 5 | 6 | 请修改instance\config.py文件中的相关配置 7 | 8 | 关于邮箱部分,使用的是[cloudflare_temp_email](https://github.com/dreamhunter2333/cloudflare_temp_email)项目,请根据作者的部署教程进行部署(非必需) 9 | 10 | ## 部署 11 | 12 | 1、克隆项目 13 | 14 | 2、安装```MySQL``` 15 | 16 | 3、运行```init_db.sql```创建数据库和表 17 | 18 | 4、修改instance\config.py文件中的配置信息 19 | 20 | 5、安装依赖```pip install -r requirements.txt``` 21 | 22 | 6、在项目目录运行```python run.py``` 23 | 24 | 7、如果看到以下字样,就代表运行成功 25 | ``` 26 | 在主进程中初始化自动刷新, 当前时间: 2024-11-07 13:05:04.351826 27 | 设置初始定时器, 延迟秒数: 250715.195757 28 | * Serving Flask app 'run.py' 29 | * Debug mode: off 30 | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. 31 | * Running on all addresses (0.0.0.0) 32 | * Running on http://127.0.0.1:5000 33 | * Running on http://172.18.0.3:5000 34 | Press CTRL+C to quit 35 | ``` 36 | 37 | 8、使用ip:端口访问 38 | 默认管理员账号 39 | ``` 40 | admin 123123 41 | ``` 42 | ## 页面预览 43 | 登录 44 |  45 | 注册 46 |  47 | 2FA 48 |  49 | 忘记密码 50 |  51 |  52 | 共享页 53 |  54 |  55 | 管理员仪表盘 56 |  57 | 用户管理 58 |  59 | GPT账号管理 60 |  61 | Claude账号管理 62 |  63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /flasks/blueprints/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint,current_app, render_template, session, request, jsonify 2 | from ..utils import login_required 3 | from ..models import User,db 4 | from werkzeug.security import generate_password_hash, check_password_hash 5 | bp = Blueprint('user', __name__) 6 | 7 | @bp.route('/profile') 8 | @login_required 9 | def index(): 10 | user = User.query.filter_by(username=session['username']).first() 11 | print(user.two_fa) 12 | return render_template('user/profile.html', user=user,email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME')) 13 | 14 | # 用户更新密码路由 15 | @bp.route('/api/user/| ID | 6 |用户名 | 7 |邮箱 | 8 |角色 | 9 |最后登录时间 | 10 |操作 | 11 |
|---|---|---|---|---|---|
| 17 | {{ user.id }} 18 | | 19 |{{ user.username }} | 20 |{{ user.email }} | 21 |22 | {{ user.role }} 23 | | 24 |25 | {% if user.last_login %} 26 | 27 | 28 | {{ user.last_login }} 29 | 30 | {% else %} 31 | 未登录 32 | {% endif %} 33 | | 34 |35 | 38 | 41 | | 42 |
39 | 40 | 返回登录 41 | 42 |
43 |还没有账号? 57 | 立即注册 58 |
59 |'
129 |
130 | def to_dict(self):
131 | return {
132 | 'code': self.code
133 | }
--------------------------------------------------------------------------------
/flasks/blueprints/chat.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, current_app, render_template, request, jsonify
2 | from sqlalchemy import func
3 | from ..models import AutoRefresh, ChatToken, Record, User, db
4 | from ..utils import admin_required, login_required,is_valid_email, refresh_access_tokens, schedule_next_refresh, usage_record
5 | import psutil
6 | from werkzeug.security import generate_password_hash
7 | from datetime import datetime, timedelta
8 | import platform
9 |
10 |
11 | bp = Blueprint('chat', __name__)
12 |
13 | # chatgpt页
14 | @bp.route('/chatgpt', methods=['GET', 'POST'])
15 | @admin_required
16 | def chatgpt():
17 | if request.method == 'GET':
18 |
19 | return render_template('admin/chat.html',email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
20 |
21 | if request.method == 'POST':
22 | # 获取更新后的 retoken 数据
23 | new_retokens = request.json.get('retokens')
24 |
25 | # 如果数据格式有效,保存到文件
26 | if new_retokens:
27 | ChatToken.refresh_token = new_retokens
28 | db.session.commit()
29 | return jsonify({"status": "success", "message": "chatToken.json 已更新!"}), 200
30 | else:
31 | return jsonify({"status": "error", "message": "无效的数据格式!"}), 400
32 |
33 |
34 | # 设定定时任务
35 | @bp.route('/set_auto_refresh', methods=['POST'])
36 | @admin_required
37 | def set_auto_refresh():
38 | data = request.json
39 | config = AutoRefresh.query.filter_by(id=1).first()
40 |
41 | # 取消现有的定时任务
42 | config.auto_refresh_enabled = data['enabled']
43 | config.refresh_interval_days = data['interval']
44 | db.session.commit()
45 |
46 | if config.auto_refresh_enabled:
47 | schedule_next_refresh()
48 |
49 | return jsonify({"status": "success", "message": "自动刷新设置已更新"})
50 |
51 | # 加载定时任务配置信息
52 | @bp.route('/get_auto_refresh_config', methods=['GET'])
53 | def get_auto_refresh_config():
54 | config = AutoRefresh.query.filter_by(id=1).first()
55 | if config is None:
56 | return jsonify({'message': '未找到配置'}), 404
57 | return jsonify(config.to_dict()), 200
58 |
59 |
60 |
61 |
62 | # 手动刷新access token
63 | @bp.route('/refresh_tokens', methods=['POST'])
64 | @admin_required
65 | def refresh_tokens():
66 | try:
67 | # 调用刷新 access_token 的函数
68 | refresh_access_tokens()
69 |
70 | return jsonify({
71 | "status": "success",
72 | }), 200
73 | except Exception as e:
74 | return jsonify({
75 | "status": "error",
76 | "message": str(e)
77 | }), 500
78 |
79 |
80 | # 加载账号
81 | @bp.route('/get_tokens')
82 | @admin_required
83 | def get_tokens():
84 | try:
85 | tokens = ChatToken.query.all()
86 | token_list = [token.to_dict() for token in tokens]
87 | return jsonify(token_list), 200
88 | except FileNotFoundError:
89 | return jsonify([]), 200 # 如果文件不存在,返回空列表
90 |
91 |
92 | # 添加新账号
93 | @bp.route('/api/tokens', methods=['POST'])
94 | @admin_required
95 | def create_tokens():
96 | data = request.get_json()
97 |
98 | if ChatToken.query.filter_by(email=data['email']).first():
99 | return jsonify({'success': False, 'message': '该账号已存在'}), 400
100 |
101 | try:
102 | # 添加新账号
103 | new_chat = ChatToken(
104 | email = data['email'],
105 | refresh_token = data['ReToken'],
106 | access_token = data['AcToken'],
107 | status = True,
108 | type ="/static/img/gpt.png",
109 | PLUS = data['PLUS']
110 | )
111 | db.session.add(new_chat)
112 | db.session.commit()
113 |
114 | return jsonify({'success': True, 'message': '添加成功'})
115 |
116 | except Exception as e:
117 | db.session.rollback()
118 |
119 | return jsonify({'success': False, 'message': '添加失败'})
120 |
121 |
122 | # 更新账号信息
123 | @bp.route('/api/tokens/', methods=['PUT'])
124 | @admin_required
125 | def update_token(email):
126 | data = request.get_json()
127 | tokens = ChatToken.query.filter(ChatToken.email == email)
128 | token = ChatToken.query.filter(ChatToken.email == email).first()
129 |
130 | if not tokens:
131 | return jsonify({'success': False, 'message': '账号不存在'}), 404
132 |
133 | # 如果提供了邮箱,则更新邮箱
134 | if data.get('email'):
135 | token.email = data['email']
136 |
137 | # 如果提供了ReToken,则更新ReToken
138 | if data.get('ReToken'):
139 | token.refresh_token = data['ReToken']
140 | else:
141 | token.refresh_token = ''
142 |
143 | # 如果提供了AcToken,则更新AcToken
144 | if data.get('AcToken'):
145 | token.access_token = data['AcToken']
146 | token.status = True
147 | else:
148 | token.access_token = ''
149 | if data.get('PLUS'):
150 | token.PLUS = data['PLUS']
151 | db.session.commit()
152 |
153 | return jsonify({'success': True, 'message': '账号更新成功'})
154 |
155 | # 删除账号
156 | @bp.route('/api/tokens/', methods=['DELETE'])
157 | @admin_required
158 | def delete_token(email):
159 |
160 | email = ChatToken.query.filter(ChatToken.email == email).first()
161 |
162 | try:
163 | db.session.delete(email)
164 | db.session.commit()
165 | return jsonify({'success': True, 'message': '账号删除成功'})
166 | except Exception as e:
167 | db.session.rollback()
168 | return jsonify({'success': False, 'message': '删除失败'}), 404
169 |
170 |
171 |
--------------------------------------------------------------------------------
/flasks/blueprints/main.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint,current_app, render_template, session, request, jsonify
2 | from ..utils import check_final_url, get_claude_login_url, getoauth, login_required, register_token
3 | from ..models import ChatToken, ClaudeToken, Record, User,db
4 | from werkzeug.security import generate_password_hash, check_password_hash
5 | bp = Blueprint('main', __name__)
6 |
7 | @bp.route('/')
8 | @login_required
9 | def index():
10 | # 读取 chatToken.json 中的 access_tokens
11 | gpt_tokens_list = ChatToken.query.all()
12 | gpt_tokens = [token.to_dict() for token in gpt_tokens_list]
13 |
14 | # 读取 claudeToken.json 中的 token
15 | claude_tokens_list = ClaudeToken.query.all()
16 | claude_tokens = [token.to_dict() for token in claude_tokens_list]
17 |
18 | # 创建两个列表分别存储PLUS和普通账号
19 | gpt_plus_tokens = []
20 | gpt_normal_tokens = []
21 |
22 | claude_plus_tokens = []
23 | claude_normal_tokens = []
24 |
25 | # 遍历所有token并分组
26 | for idx, token in enumerate(gpt_tokens):
27 | if token['status']: # 如果token有效
28 | token_info = {
29 | 'index': idx,
30 | 'type': token.get('type', 'unknown'),
31 | 'PLUS': token.get('PLUS', 'false')
32 | }
33 |
34 | # 根据PLUS状态分组
35 | if token_info['PLUS'].lower() == 'true':
36 | gpt_plus_tokens.append(token_info)
37 | else:
38 | gpt_normal_tokens.append(token_info)
39 |
40 | # 遍历所有token并分组
41 | for idx, token in enumerate(claude_tokens):
42 | if token['status']: # 如果token有效
43 | token_info = {
44 | 'index': idx,
45 | 'type': token.get('type', 'unknown'),
46 | 'PLUS': token.get('PLUS', 'false')
47 | }
48 |
49 | # 根据PLUS状态分组
50 | if token_info['PLUS'].lower() == 'true':
51 |
52 | claude_plus_tokens.append(token_info)
53 | else:
54 | claude_normal_tokens.append(token_info)
55 |
56 |
57 | # 按顺序合并两个列表,PLUS账号在前
58 | gpt_valid_tokens = gpt_plus_tokens + gpt_normal_tokens
59 |
60 | claude_valid_tokens = claude_plus_tokens + claude_normal_tokens
61 |
62 |
63 | # 渲染模板,传递有效token的详细信息
64 | return render_template(
65 | 'main/index.html',
66 | gpt_valid_tokens=gpt_valid_tokens, # 传递排序后的token列表
67 | claude_valid_tokens= claude_valid_tokens,
68 | email=current_app.config.get('DOMAIN_EMAIL'),
69 | title=current_app.config.get('DOMAIN_NAME')
70 | )
71 |
72 | # 用于处理 UNIQUE_NAME 的提交
73 | @bp.route('/submit_name', methods=['POST'])
74 | @login_required
75 | def submit_name():
76 | data = request.json
77 | unique_name = data.get('unique_name')
78 | index = data.get('index')
79 | Type = data.get('type')
80 |
81 |
82 | if Type == 'chatgpt':
83 |
84 | tokens_list = ChatToken.query.all()
85 | tokens = [token.to_dict() for token in tokens_list]
86 | valid_tokens = tokens
87 |
88 | # 确保 index 是有效的索引
89 | if index and 1 <= index <= len(valid_tokens):
90 |
91 | # 获取对应的 access_token
92 | token_info = valid_tokens[index - 1]
93 |
94 | access_token = token_info['access_token']
95 |
96 | email = token_info['email']
97 |
98 |
99 | new_access_token = access_token
100 |
101 | if 'http' in access_token or 'https' in access_token:
102 | old_url = "https://plus.aivvm.com/"
103 | new_url = "https://gpt.bocchi2b.top/"
104 |
105 | # 替换旧的 URL
106 | new_access_token = new_access_token.replace(old_url, new_url)
107 |
108 | url = new_access_token # 你要测试的原始链接
109 |
110 | target_url = 'https://gpt.bocchi2b.top/auth/login_auth0' # 你要匹配的目标地址
111 | logurl = check_final_url(url, target_url)
112 |
113 | if logurl:
114 | return jsonify({
115 | "status": "success",
116 | "login_url": access_token
117 | }), 200
118 | else:
119 | token = ChatToken.query.filter(ChatToken.access_token == access_token).first()
120 | token.status = False
121 | db.session.commit()
122 |
123 | return jsonify({
124 | "status": "error",
125 | "message": "账号失效了,换一个吧"
126 | }), 400
127 |
128 | # 注册 token 并获取对应的 token_key
129 | token_key = register_token(access_token, unique_name)
130 | # token_key = None
131 |
132 | if not token_key:
133 | # 更新 chatToken.json 中对应条目的 status 为 false
134 | token = ChatToken.query.filter(ChatToken.access_token == access_token).first()
135 | token.status = False
136 | db.session.commit()
137 |
138 | # 清除用量表中对应的access_token记录
139 | records_to_delete = Record.query.filter_by(access_token=access_token).all()
140 | for record in records_to_delete:
141 | db.session.delete(record)
142 | db.session.commit()
143 |
144 | return jsonify({
145 | "status": "error",
146 | "message": "账号失效了,换一个吧"
147 | }), 400
148 |
149 | # 检查是否存在相同的shared_token
150 | if not Record.query.filter_by(shared_token=token_key).first():
151 | new_record = Record(
152 | username=unique_name,
153 | email=email,
154 | access_token=access_token,
155 | shared_token=token_key
156 | )
157 | db.session.add(new_record)
158 | db.session.commit()
159 |
160 | # 获取 OAuth 登录 URL
161 | logurl = getoauth(token_key)
162 |
163 |
164 | return jsonify({
165 | "status": "success",
166 | "login_url": logurl
167 | }), 200
168 | else:
169 | return jsonify({
170 | "status": "error",
171 | "message": "Invalid index."
172 | }), 400
173 | else:
174 | # tokens = load_cltoken()
175 | tokens_list = ClaudeToken.query.all()
176 | tokens = [token.to_dict() for token in tokens_list]
177 | valid_tokens = tokens
178 |
179 | # 确保 index 是有效的索引
180 | if index and 1 <= index <= len(valid_tokens):
181 |
182 | # 获取对应的 token
183 | token_info = valid_tokens[index - 1]
184 |
185 | skToken = token_info['skToken']
186 |
187 | # 获取登录链接
188 | logurl = get_claude_login_url(skToken, unique_name)
189 | if not logurl:
190 | # 更新 chatToken.json 中对应条目的 status 为 false
191 | token = ClaudeToken.query.filter(ClaudeToken.skToken == skToken).first()
192 | token.status = False
193 | db.session.commit()
194 |
195 | return jsonify({
196 | "status": "error",
197 | "message": "账号失效了,换一个吧"
198 | }), 400
199 |
200 | return jsonify({
201 | "status": "success",
202 | "login_url": logurl
203 | }), 200
204 | else:
205 | return jsonify({
206 | "status": "error",
207 | "message": "Invalid index."
208 | }), 400
--------------------------------------------------------------------------------
/flasks/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}注册{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
10 |
11 | 用户注册
12 |
13 |
14 |
101 |
102 |
103 | 已有账号?
104 | 立即登录
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
193 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/templates/main/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}首页{% endblock %}
4 |
5 | {% block content %}
6 |
100 |
141 |
142 |
179 |
180 |
181 |
182 | {% if gpt_valid_tokens %}
183 | {% for token in gpt_valid_tokens %}
184 |
186 |
187 | {% if token.PLUS.lower() == 'true' %}
188 | ChatGPT PLUS {{ loop.index }}
189 | {% else %}
190 | ChatGPT普号 {{ loop.index }}
191 | {% endif %}
192 |
193 | 
194 |
195 | {% endfor %}
196 | {% else %}
197 | 暂无GPT账号
198 | {% endif %}
199 |
200 |
201 |
202 |
203 |
204 | {% if claude_valid_tokens %}
205 | {% for token in claude_valid_tokens %}
206 |
208 |
209 | {% if token.PLUS.lower() == 'true' %}
210 | Claude PLUS {{ loop.index }}
211 | {% else %}
212 | Claude普号 {{ loop.index }}
213 | {% endif %}
214 |
215 | 
216 |
217 | {% endfor %}
218 | {% else %}
219 | 暂无Claude账号
220 | {% endif %}
221 |
222 |
223 |
271 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/templates/user/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}个人中心{% endblock %}
4 |
5 | {% block content %}
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{ user.username[0].upper() }}
23 |
24 | {{ user.username }}
25 | {{ user.email }}
26 | {{ user.role }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 账户信息
37 |
38 |
39 | -
40 | 账户ID:
41 | {{ user.id }}
42 |
43 | -
44 | 注册时间:
45 | {{user.registered_at}}
46 |
47 | -
48 | 最后登录:
49 | {{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if
50 | user.last_login else '暂无记录' }}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 安全设置
62 |
63 |
64 | -
65 |
66 |
修改密码
67 | 定期更改密码以保护账户安全
68 |
69 |
72 |
73 | -
74 |
75 |
两步验证
76 | 增加额外的账户安全性
77 |
78 | {% if user.two_fa %}
79 |
80 |
82 |
83 | {% else %}
84 |
85 |
87 |
88 | {% endif %}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
138 |
139 |
140 |
245 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/templates/auth/verify_otp.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}验证码验证{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
209 |
210 |
211 |
212 |
213 |
214 | 验证你的邮箱
215 |
216 | 我们已向 {{ session.get('temp_email', '***@***.com') }} 发送验证码
217 |
218 |
219 |
220 |
260 |
261 |
262 |
263 |
264 |
414 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/blueprints/auth.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, current_app, render_template, request, redirect, url_for, flash, session, jsonify, make_response
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 |
4 | from ..models import InvitationCodes, User, db
5 | from ..utils import generate_remember_token, generate_reset_token, get_user_ip, send_otp_email, send_reset_email, send_verification_email, generate_verification_code, verify_code, verify_otp_code
6 | from datetime import datetime, timedelta
7 |
8 | bp = Blueprint('auth', __name__)
9 |
10 | @bp.route('/send-verification-code', methods=['POST'])
11 | def send_verification_code():
12 | """发送验证码的路由"""
13 | data = request.get_json()
14 | email = data.get('email')
15 |
16 | if not email:
17 | return jsonify({'success': False, 'message': '请提供邮箱地址'}), 400
18 |
19 | # 检查是否在冷却时间内
20 | last_send_time = session.get(f'last_send_time_{email}')
21 | if last_send_time:
22 | last_send_time = datetime.fromisoformat(last_send_time)
23 | if datetime.now() - last_send_time < timedelta(minutes=1):
24 | return jsonify({'success': False, 'message': '请求频率过高,请稍后再试'}), 429
25 |
26 | # 生成验证码
27 | code = generate_verification_code()
28 |
29 |
30 | # 发送验证码
31 | if send_verification_email(email, code):
32 | # 存储验证码和发送时间
33 | session[f'verification_code_{email}'] = code
34 | session[f'code_expire_time_{email}'] = (datetime.now() + timedelta(minutes=5)).isoformat()
35 | session[f'last_send_time_{email}'] = datetime.now().isoformat()
36 | return jsonify({'success': True, 'message': '验证码已发送'}), 200
37 | else:
38 | return jsonify({'success': False, 'message': '验证码发送失败'}), 500
39 |
40 |
41 | @bp.route('/register', methods=['GET', 'POST'])
42 | def register():
43 |
44 | if not current_app.config.get('REGISTER'):
45 | flash('暂未开启注册', 'danger')
46 | return redirect(url_for('auth.login'))
47 |
48 | if 'username' in session:
49 | return redirect(url_for('main.index'))
50 |
51 | if request.method == 'POST':
52 | # 获取表单数据
53 | username = request.form.get('username', '').strip()
54 | password = request.form.get('password', '').strip()
55 | email = request.form.get('email', '').strip()
56 | verification_code = request.form.get('verification_code', '').strip()
57 | vecode = request.form.get('vcode', '').strip()
58 |
59 | # 判断是否是AJAX请求
60 | is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
61 |
62 | def error_response(message):
63 | """统一错误响应处理"""
64 | flash(message, 'danger')
65 | if is_ajax:
66 | return jsonify({'success': False, 'message': message}), 400
67 | return render_template('auth/register.html')
68 |
69 |
70 | # 基础验证
71 | if not all([username, password, email, verification_code]):
72 | return error_response('请填写所有必需的字段!')
73 |
74 | # 验证码检查
75 | if not verify_code(email, verification_code):
76 | return error_response('验证码无效或已过期')
77 |
78 | # 表单验证
79 | if len(username) < 3:
80 | return error_response('用户名至少需要3个字符!')
81 |
82 | if len(password) < 6:
83 | return error_response('密码至少需要6个字符!')
84 |
85 | # 检查用户名是否已存在
86 | if User.query.filter_by(username=username).first():
87 | return error_response('用户名已存在!')
88 |
89 | # 检查邮箱是否已存在
90 | if User.query.filter_by(email=email).first():
91 | return error_response('邮箱已被注册!')
92 |
93 | if current_app.config.get('REG_CODE'):
94 | # 邀请码验证
95 | code_entry = InvitationCodes.query.filter_by(code=vecode).first()
96 |
97 | if not code_entry:
98 | return error_response('邀请码错误!')
99 |
100 | db.session.delete(code_entry)
101 | db.session.commit()
102 |
103 | try:
104 | # 创建新用户
105 | hashed_password = generate_password_hash(password)
106 | new_user = User(
107 | username=username,
108 | password=hashed_password,
109 | email=email,
110 | role='user' # 默认角色为普通用户
111 | )
112 | db.session.add(new_user)
113 | db.session.commit()
114 |
115 | # 清除验证码
116 | session.pop(f'verification_code_{email}', None)
117 | session.pop(f'code_expire_time_{email}', None)
118 |
119 | # 注册成功响应
120 | flash('注册成功!请登录。', 'success')
121 | if is_ajax:
122 | return jsonify({
123 | 'success': True,
124 | 'message': '注册成功!请登录。',
125 | 'redirect': url_for('auth.login')
126 | }), 200
127 | return redirect(url_for('auth.login'))
128 |
129 | except Exception as e:
130 | db.session.rollback()
131 | print(e)
132 | return error_response(f'注册失败,请稍后重试。{e}')
133 |
134 | # GET 请求返回注册页面
135 | return render_template('auth/register.html',email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
136 |
137 | @bp.route('/login', methods=['GET', 'POST'])
138 | def login():
139 | if 'username' in session:
140 | return redirect(url_for('main.index'))
141 |
142 | if request.method == 'POST':
143 | username = request.form['username']
144 | password = request.form['password']
145 | remember = request.form.get('remember') == 'on'
146 | user_ip = get_user_ip()
147 | user = User.query.filter_by(username=username).first()
148 | if user is None:
149 | # 处理用户未找到的情况
150 | flash(‘用户名或密码错误!’, danger')
151 | return redirect(url_for('auth.login'))
152 | if not user.two_fa or user_ip == user.last_login_ip:
153 |
154 | if user and check_password_hash(user.password, password):
155 |
156 | session['username'] = user.username
157 | session['user_id'] = user.id
158 | session['role'] = user.role
159 | # 处理"记住我"
160 | response = make_response(redirect(url_for('main.index')))
161 | if remember:
162 | # 生成记住我令牌
163 | remember_token = generate_remember_token(user.id)
164 | # 设置cookie,过期时间30天
165 | response.set_cookie(
166 | 'remember_token',
167 | remember_token,
168 | max_age=60 * 60 *24 * current_app.config.get('JWT_EXPIRATION_DAYS',30),
169 | httponly=True, # 防止XSS攻击
170 | secure = current_app.config.get('COOKIE_SECURE') # 仅通过HTTPS发送
171 | )
172 |
173 | # 更新最后登录时间
174 | user.last_login = datetime.utcnow()
175 | # 记录本次登录ip
176 | user.last_login_ip = user_ip
177 |
178 | db.session.commit()
179 |
180 | flash('登录成功!', 'success')
181 | return response
182 |
183 | flash('用户名或密码错误!', 'danger')
184 | else:
185 | if user and check_password_hash(user.password, password):
186 | # 存储临时登录信息
187 | session['temp_user_id'] = user.id
188 | session['temp_remember'] = remember
189 | session['temp_email'] = user.email
190 |
191 | # 发送OTP验证码
192 | send_otp_email(user)
193 |
194 | return redirect(url_for('auth.verify_otp'))
195 |
196 | flash('用户名或密码错误!', 'danger')
197 |
198 | return render_template('auth/login.html',email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
199 |
200 | @bp.route('/verify-otp', methods=['GET', 'POST'])
201 | def verify_otp():
202 | if 'temp_user_id' not in session:
203 | return redirect(url_for('auth.login'))
204 |
205 | user = User.query.get(session['temp_user_id'])
206 | if not user:
207 | flash('用户不存在!', 'danger')
208 | return redirect(url_for('auth.login'))
209 |
210 | if request.method == 'POST':
211 | otp_code = request.form.get('otp_code')
212 | remember = session.get('temp_remember')
213 | user_ip = get_user_ip()
214 |
215 | if verify_otp_code(user, otp_code):
216 | # 清除临时会话数据
217 | session.pop('temp_user_id', None)
218 | session.pop('temp_remember', None)
219 |
220 | # 设置登录会话
221 | session['username'] = user.username
222 | session['user_id'] = user.id
223 | session['role'] = user.role
224 |
225 | # 处理"记住我"
226 | response = make_response(redirect(url_for('main.index')))
227 | if remember:
228 | remember_token = generate_remember_token(user.id)
229 | response.set_cookie(
230 | 'remember_token',
231 | remember_token,
232 | max_age=60 * 60 * 24 * current_app.config.get('JWT_EXPIRATION_DAYS', 30),
233 | httponly=True,
234 | secure=current_app.config.get('COOKIE_SECURE')
235 | )
236 |
237 | # 更新最后登录时间
238 | user.last_login = datetime.utcnow()
239 | # 记录本次登录ip
240 | user.last_login_ip = user_ip
241 | user.otp_code = None
242 | user.otp_expiration =None
243 | db.session.commit()
244 |
245 | flash('登录成功!', 'success')
246 | return response
247 |
248 | flash('验证码错误或已过期!', 'danger')
249 |
250 | return render_template('auth/verify_otp.html',email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
251 |
252 | @bp.route('/resend-otp', methods=['POST'])
253 | def resend_otp():
254 | if 'temp_user_id' not in session:
255 | return jsonify({'error': 'Invalid session'}), 400
256 |
257 | user = User.query.get(session['temp_user_id'])
258 | if not user:
259 | return jsonify({'error': 'User not found'}), 404
260 |
261 | send_otp_email(user)
262 | return jsonify({'message': 'OTP sent successfully'}), 200
263 |
264 |
265 | reset_tokens = {}
266 |
267 | @bp.route('/forgot-password', methods=['GET', 'POST'])
268 | def forgot_password():
269 | if 'username' in session:
270 | return redirect(url_for('main.index'))
271 |
272 | if request.method == 'POST':
273 | email = request.form.get('email')
274 | if not email:
275 | flash('请输入邮箱地址', 'danger')
276 | return redirect(url_for('auth.forgot_password'))
277 |
278 | # 检查邮箱是否存在
279 | user = User.query.filter_by(email=email).first()
280 | if not user:
281 | flash('该邮箱地址未注册', 'danger')
282 | return redirect(url_for('auth.forgot_password'))
283 |
284 | # 生成重置令牌并保存
285 | token = generate_reset_token()
286 | reset_tokens[token] = {
287 | 'email': email,
288 | 'expires_at': datetime.now() + timedelta(minutes=30)
289 | }
290 |
291 | # 生成重置链接
292 | reset_link = url_for('auth.reset_password', token=token, _external=True,email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
293 |
294 | # 发送重置邮件
295 | if send_reset_email(email, reset_link):
296 | flash('重置链接已发送到您的邮箱,请查收', 'success')
297 | return redirect(url_for('auth.login'))
298 | else:
299 | flash('发送邮件失败,请稍后重试', 'danger')
300 | return redirect(url_for('auth.forgot_password'))
301 |
302 | return render_template('auth/forgot_password.html')
303 |
304 | @bp.route('/reset-password/', methods=['GET', 'POST'])
305 | def reset_password(token):
306 | if 'username' in session:
307 | return redirect(url_for('main.index'))
308 |
309 | # 检查令牌是否存在
310 | if token not in reset_tokens:
311 | flash('重置链接无效或已过期', 'danger')
312 | return redirect(url_for('auth.forgot_password'))
313 |
314 | # 检查令牌是否过期
315 | token_data = reset_tokens[token]
316 | if datetime.now() > token_data['expires_at']:
317 | del reset_tokens[token]
318 | flash('重置链接已过期,请重新申请', 'danger')
319 | return redirect(url_for('auth.forgot_password'))
320 |
321 | if request.method == 'POST':
322 | new_password = request.form.get('new_password')
323 | confirm_password = request.form.get('confirm_password')
324 |
325 | if not all([new_password, confirm_password]):
326 | flash('请填写所有字段', 'danger')
327 | return redirect(url_for('auth.reset_password', token=token))
328 |
329 | # 验证密码
330 | if new_password != confirm_password:
331 | flash('两次输入的密码不匹配', 'danger')
332 | return redirect(url_for('auth.reset_password', token=token))
333 |
334 | # 更新密码
335 | user = User.query.filter_by(email=token_data['email']).first()
336 | user.password = generate_password_hash(new_password)
337 | db.session.commit()
338 |
339 | # 清除令牌
340 | del reset_tokens[token]
341 |
342 | flash('密码重置成功,请使用新密码登录', 'success')
343 | return redirect(url_for('auth.login'))
344 |
345 | return render_template('auth/reset_password.html', token=token,email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
346 |
347 |
348 | @bp.route('/logout')
349 | def logout():
350 | session.clear()
351 | response = make_response(redirect(url_for('auth.login')))
352 | response.delete_cookie('remember_token')
353 | flash('已成功退出登录!', 'info')
354 | return response
--------------------------------------------------------------------------------
/flasks/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block title %}{% endblock %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
283 |
284 |
285 |
286 |
287 |
351 |
352 |
353 |
354 | {% with messages = get_flashed_messages(with_categories=true) %}
355 | {% if messages %}
356 | {% for category, message in messages %}
357 |
358 |
360 | {{ message }}
361 |
362 |
363 | {% endfor %}
364 | {% endif %}
365 | {% endwith %}
366 |
367 |
368 |
369 |
370 |
371 | {% block content %}{% endblock %}
372 |
373 |
374 |
375 |
390 |
391 |
392 |
415 |
416 |
417 |
418 |
419 |
--------------------------------------------------------------------------------
/flasks/templates/admin/claude.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}ChatGPT账号管理{% endblock %}
4 |
5 | {% block content %}
6 |
99 |
100 | 账号列表
101 |
105 |
106 |
107 |
108 | Email
109 | Sk Token
110 | 状态
111 | 操作
112 |
113 |
114 |
115 |
116 |
117 |
118 |
124 |
125 |
126 |
127 |
128 |
175 |
209 |
210 |
211 |
336 |
382 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/blueprints/admin.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, current_app, render_template, request, jsonify
2 | from sqlalchemy import func
3 | from ..models import AutoRefresh, ChatToken, InvitationCodes, Record, User, db
4 | from ..utils import admin_required, generate_verification_code, login_required,is_valid_email, refresh_access_tokens, schedule_next_refresh, usage_record
5 | import psutil
6 | from werkzeug.security import generate_password_hash
7 | from datetime import datetime, timedelta
8 | import platform
9 |
10 |
11 | bp = Blueprint('admin', __name__)
12 |
13 |
14 | # 分页与搜索函数
15 | def get_paginated_users(page=1, per_page=5, search=''):
16 | """
17 | 获取分页的用户列表,并支持按用户名和邮箱搜索。
18 | """
19 | page = request.args.get('page', page, type=int)
20 | search = request.args.get('search', search, type=str).strip().lower() # 获取搜索关键字
21 |
22 | query = User.query.filter()
23 |
24 | # 如果存在搜索条件,进行用户名或邮箱模糊匹配
25 | if search:
26 | query = query.filter(
27 | (User.username.ilike(f'%{search}%')) |
28 | (User.email.ilike(f'%{search}%'))
29 | )
30 |
31 | query = query.order_by(User.id) # 排序
32 | pagination = query.paginate(page=page, per_page=per_page, error_out=False)
33 |
34 | # 计算当前页显示的记录范围
35 | start_index = (page - 1) * per_page + 1
36 | end_index = min(page * per_page, pagination.total)
37 |
38 | pagination_info = {
39 | 'total': pagination.total,
40 | 'pages': pagination.pages,
41 | 'current_page': page,
42 | 'per_page': per_page,
43 | 'has_prev': pagination.has_prev,
44 | 'has_next': pagination.has_next,
45 | 'prev_num': pagination.prev_num,
46 | 'next_num': pagination.next_num,
47 | 'start_index': start_index,
48 | 'end_index': end_index
49 | }
50 |
51 | return pagination.items, pagination_info
52 |
53 | @bp.route('/admin')
54 | @login_required
55 | @admin_required
56 | def index():
57 | return render_template('admin/admin_info.html',email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
58 |
59 | # 获取系统负载信息
60 | @bp.route('/system_load')
61 | @admin_required
62 | def system_load():
63 | # 获取CPU、内存等信息
64 | cpu_percent = psutil.cpu_percent(interval=1)
65 | memory = psutil.virtual_memory()
66 | disk = psutil.disk_usage('/')
67 |
68 | # 系统版本信息
69 | system_version = platform.platform()
70 |
71 | # 上次更新时间,设为固定值
72 | last_update = "2024-10-01" # 固定的时间戳或描述字符串
73 |
74 | # 运行时长
75 | boot_time = datetime.fromtimestamp(psutil.boot_time())
76 | current_time = datetime.now()
77 | uptime = str(current_time - boot_time)
78 |
79 | # 返回 JSON 格式的系统负载信息
80 | return jsonify({
81 | 'cpu_percent': cpu_percent,
82 | 'memory_total': memory.total,
83 | 'memory_used': memory.used,
84 | 'memory_percent': memory.percent,
85 | 'disk_total': disk.total,
86 | 'disk_used': disk.used,
87 | 'disk_percent': disk.percent,
88 | 'system_version': system_version,
89 | 'last_update': last_update,
90 | 'uptime': uptime
91 | })
92 |
93 | # 获取用户统计信息
94 | @bp.route('/user_stats')
95 | @admin_required
96 | def user_stats():
97 | try:
98 | # 获取总用户数
99 | total_users = User.query.count()
100 |
101 | # 获取最近30天注册的用户数
102 | thirty_days_ago = datetime.utcnow() - timedelta(days=30)
103 | new_users_30d = User.query.filter(User.registered_at >= thirty_days_ago).count()
104 |
105 | # 获取最近7天注册的用户数
106 | seven_days_ago = datetime.utcnow() - timedelta(days=7)
107 | new_users_7d = User.query.filter(User.registered_at >= seven_days_ago).count()
108 |
109 | # 获取不同角色的用户数量
110 | role_counts = db.session.query(
111 | User.role,
112 | func.count(User.id)
113 | ).group_by(User.role).all()
114 |
115 | # 计算每个角色的用户数量
116 | role_stats = {role: count for role, count in role_counts}
117 |
118 | # 获取最近24小时内活跃(登录)的用户数
119 | last_24h = datetime.utcnow() - timedelta(hours=24)
120 | active_users_24h = User.query.filter(User.last_login >= last_24h).count()
121 |
122 | return jsonify({
123 | 'total_users': total_users,
124 | 'new_users_30d': new_users_30d,
125 | 'new_users_7d': new_users_7d,
126 | 'role_distribution': role_stats,
127 | 'active_users_24h': active_users_24h,
128 | 'growth_rate': {
129 | 'monthly': round((new_users_30d / total_users * 100), 1) if total_users > 0 else 0,
130 | 'weekly': round((new_users_7d / total_users * 100), 1) if total_users > 0 else 0
131 | },
132 | 'last_updated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
133 | })
134 |
135 | except Exception as e:
136 | return jsonify({
137 | 'error': 'Failed to fetch user statistics',
138 | 'message': str(e)
139 | }), 500
140 |
141 | # 最近用户
142 | @bp.route('/api/users/recent', methods=['GET'])
143 | @admin_required
144 | def get_recent_users():
145 | users = User.query.order_by(User.registered_at.desc()).limit(5).all()
146 | return jsonify([{
147 | 'id': user.id,
148 | 'username': user.username,
149 | 'email': user.email,
150 | 'registered_at': user.registered_at.strftime('%Y-%m-%d'),
151 | 'is_active': bool(user.last_login and (datetime.utcnow() - user.last_login).days < 7)
152 | } for user in users])
153 |
154 | # 添加用户
155 | @bp.route('/api/users', methods=['POST'])
156 | @admin_required
157 | def add_user():
158 | data = request.json
159 |
160 | # 验证请求数据的存在性
161 | required_fields = ['username', 'email', 'password']
162 | for field in required_fields:
163 | if field not in data:
164 | return jsonify({'status': 'error', 'message': f'{field} 字段缺失'}), 400
165 |
166 | # 检查用户名和邮箱的唯一性
167 | if User.query.filter_by(username=data['username']).first():
168 | return jsonify({'status': 'error', 'message': '用户名已存在'}), 400
169 | if User.query.filter_by(email=data['email']).first():
170 | return jsonify({'status': 'error', 'message': '邮箱已存在'}), 400
171 | # 验证邮箱可用性
172 | if not is_valid_email(data['email']):
173 | return jsonify({'status': 'error', 'message': '邮箱不可用'}), 400
174 |
175 | try:
176 | # 创建用户实例
177 | user = User(
178 | username=data['username'],
179 | email=data['email'],
180 | password=generate_password_hash(data['password']),
181 | role=data.get('role', 'user'), # 默认角色为 'user'
182 | registered_at=datetime.utcnow()
183 | )
184 |
185 | # 添加到数据库
186 | db.session.add(user)
187 | db.session.commit()
188 |
189 | # 返回成功的响应
190 | return jsonify({
191 | 'status': 'success',
192 | 'user': {
193 | 'id': user.id,
194 | 'username': user.username,
195 | 'email': user.email,
196 | 'registered_at': user.registered_at.strftime('%Y-%m-%d'),
197 | 'is_active': True
198 | }
199 | }), 201
200 |
201 | except Exception as e:
202 | # 出现异常时回滚
203 | db.session.rollback()
204 | return jsonify({'status': 'error', 'message': str(e)}), 400
205 |
206 | @bp.route('/api/code', methods=['POST', 'GET'])
207 | @login_required
208 | @admin_required
209 | def code():
210 | if request.method == 'POST':
211 | # 处理生成新的邀请码
212 | code = generate_verification_code()
213 |
214 | while True:
215 | if InvitationCodes.query.filter_by(code=code).first():
216 | # 如果邀请码已存在,重新生成邀请码
217 | code = generate_verification_code()
218 | else:
219 | # 如果邀请码不重复,创建新邀请码并添加到数据库
220 | newCode = InvitationCodes(code=code)
221 | db.session.add(newCode)
222 | db.session.commit()
223 | return jsonify({'message': '邀请码生成成功', 'code': code}), 201
224 |
225 | elif request.method == 'GET':
226 | # 处理返回邀请码列表
227 | codes = InvitationCodes.query.all()
228 | code_list = [code_entry.code for code_entry in codes]
229 | return jsonify({'codes': code_list}), 200
230 |
231 | return jsonify({'error': '不支持的请求方法'}), 405 # 如果请求方法不是 GET 或 POST
232 |
233 |
234 |
235 |
236 |
237 | # 管理员-用户管理面板路由
238 | @bp.route('/admin_user')
239 | @login_required
240 | @admin_required
241 | def admin_panel():
242 | page = request.args.get('page', 1, type=int)
243 | per_page = request.args.get('per_page', 5, type=int)
244 | search = request.args.get('search', '', type=str) # 获取搜索参数
245 |
246 | users, pagination = get_paginated_users(page, per_page, search)
247 |
248 |
249 | # 获取当前时间
250 | now = datetime.utcnow()
251 |
252 | # 计算15天前的日期
253 | seven_days_ago = now - timedelta(days=15)
254 |
255 | # 基础统计
256 | total_users = User.query.count()
257 | new_users_count = User.query.filter(User.registered_at >= seven_days_ago).count()
258 | active_users_count = User.query.filter(User.last_login >= seven_days_ago).count()
259 | inactive_users_count = total_users - active_users_count
260 |
261 | # 获取近15天的日期列表
262 | dates = [(now - timedelta(days=i)).strftime('%m-%d') for i in range(14, -1, -1)]
263 |
264 | # 获取每天的注册用户数
265 | registration_data = []
266 | activity_data = []
267 |
268 | for i in range(14, -1, -1):
269 | date = (now - timedelta(days=i)).date()
270 | next_date = date + timedelta(days=1)
271 |
272 | # 统计注册用户
273 | reg_count = User.query.filter(
274 | User.registered_at >= date,
275 | User.registered_at < next_date
276 | ).count()
277 | registration_data.append(reg_count)
278 |
279 | # 统计活跃用户
280 | active_count = User.query.filter(
281 | User.last_login >= date,
282 | User.last_login < next_date
283 | ).count()
284 | activity_data.append(active_count)
285 |
286 | # 检查是否为 AJAX 请求
287 | if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
288 | # 如果是 AJAX 请求,只返回表格和分页部分
289 | return render_template('admin/_user_table.html', users=users, pagination=pagination,email=current_app.config.get('DOMAIN_EMAIL'),title=current_app.config.get('DOMAIN_NAME'))
290 |
291 | # 正常请求返回完整页面
292 | return render_template('admin/admin_user.html',
293 | users=users, pagination=pagination,
294 | total_users=total_users,
295 | new_users_count=new_users_count,
296 | active_users_count=active_users_count,
297 | inactive_users_count=inactive_users_count,
298 | dates=dates,
299 | registration_data=registration_data,
300 | activity_data=activity_data,
301 | now=now,
302 | email=current_app.config.get('DOMAIN_EMAIL'),
303 | title=current_app.config.get('DOMAIN_NAME'))
304 |
305 | # 更新用户路由
306 | @bp.route('/api/users/', methods=['PUT'])
307 | @admin_required
308 | def update_user(user_id):
309 | user = User.query.get_or_404(user_id)
310 | data = request.get_json()
311 |
312 | try:
313 | # 检查用户名是否已存在(排除当前用户)
314 | if 'username' in data and data['username'] != user.username:
315 | existing_user = User.query.filter_by(username=data['username']).first()
316 | if existing_user:
317 | return jsonify({'error': '用户名已存在'}), 400
318 | user.username = data['username']
319 |
320 | # 检查邮箱是否已存在(排除当前用户)
321 | if 'email' in data and data['email'] != user.email:
322 | existing_email = User.query.filter_by(email=data['email']).first()
323 | if existing_email:
324 | return jsonify({'error': '邮箱已存在'}), 400
325 | # 验证邮箱可用性
326 | if not is_valid_email(data['email']):
327 | return jsonify({'error': '邮箱不可用'}), 400
328 | user.email = data['email']
329 |
330 | # 更新角色
331 | if 'role' in data:
332 | user.role = data['role']
333 |
334 | # 如果提供了新密码,则更新密码
335 | if 'password' in data and data['password']:
336 | user.password = generate_password_hash(data['password'])
337 |
338 | db.session.commit()
339 | return jsonify({'message': '用户更新成功'}), 200
340 | except Exception as e:
341 | db.session.rollback()
342 | return jsonify({'error': str(e)}), 400
343 |
344 | # 删除用户路由
345 | @bp.route('/api/users/', methods=['DELETE'])
346 | @admin_required
347 | def delete_user(user_id):
348 | user = User.query.get_or_404(user_id)
349 |
350 | try:
351 | db.session.delete(user)
352 | db.session.commit()
353 | return jsonify({'message': '用户删除成功'}), 200
354 | except Exception as e:
355 | db.session.rollback()
356 | return jsonify({'error': str(e)}), 400
357 |
358 |
359 | # 用量表信息
360 | @bp.route('/api/record_info/')
361 | @admin_required
362 | def record_value(value):
363 | try:
364 |
365 | values = Record.query.all()
366 | if value == 'email':
367 | value_list = [value.to_dict_email() for value in values]
368 | elif value == 'user':
369 | value_list = [value.to_dict_user() for value in values]
370 | # 使用集合去重
371 | unique_values = []
372 | seen_values = set()
373 |
374 | for info in value_list:
375 | if value == 'email':
376 | if info['email'] not in seen_values:
377 | unique_values.append(info)
378 | seen_values.add(info['email'])
379 | elif value == 'user':
380 | if info['username'] not in seen_values:
381 | unique_values.append(info)
382 | seen_values.add(info['username'])
383 |
384 | return jsonify(unique_values), 200
385 |
386 | except FileExistsError:
387 | return jsonify([]), 200
388 |
389 | # 用量查询
390 |
391 | @bp.route('/api/record//')
392 | @admin_required
393 | def record(type,value):
394 | statistics = None
395 | gpt35_limit = 0
396 | gpt4_limit = 0
397 | gpt4o_limit = 0
398 | gpt4o_mini_limit = 0
399 | o1_limit = 0
400 | o1_mini_limit = 0
401 | auto = 0
402 | if type == 'All':
403 | statistics = Record.query.all()
404 | else:
405 | if type == 'email':
406 | statistics = Record.query.filter_by(email = value).all()
407 | elif type == 'username':
408 | statistics = Record.query.filter_by(username = value).all()
409 |
410 | if statistics:
411 | for statistic in statistics:
412 | at = statistic.access_token
413 | st = statistic.shared_token
414 | result = usage_record(at,st)
415 | if result.get("usage"):
416 | if result["usage"].get("auto"):
417 | auto = auto + int(result["usage"]["auto"])
418 | if result.get("gpt35_limit"):
419 | gpt35_limit = gpt35_limit + int(result['gpt35_limit'])
420 | if result.get("gpt4_limit"):
421 | gpt4_limit = gpt4_limit + int(result['gpt4_limit'])
422 | if result.get("gpt4o_limit"):
423 | gpt4o_limit = gpt4o_limit + int(result['gpt4o_limit'])
424 | if result.get("gpt4_limit"):
425 | gpt4o_mini_limit = gpt4o_mini_limit + int(result['gpt4o_mini_limit'])
426 | if result.get("o1_limit"):
427 | o1_limit = o1_limit + int(result['o1_limit'])
428 | if result.get("o1_mini_limit"):
429 | o1_mini_limit = o1_mini_limit + int(result['o1_mini_limit'])
430 | return {"auto":auto,
431 | "gpt3.5":gpt35_limit,
432 | "gpt4":gpt4_limit,
433 | "gpt4o":gpt4o_limit,
434 | "gpt4o-mini":gpt4o_mini_limit,
435 | "o1":o1_limit,
436 | "o1-mini":o1_mini_limit}
437 |
438 |
--------------------------------------------------------------------------------
/flasks/templates/admin/chat.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}ChatGPT账号管理{% endblock %}
4 |
5 | {% block content %}
6 |
99 |
100 | 账号列表
101 |
105 |
106 |
107 |
108 |
109 | Email
110 | Re Token
111 | 状态
112 | 操作
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Access Token刷新设置
132 |
133 |
134 |
135 |
136 | 天
137 |
138 |
139 |
140 |
141 |
179 |
180 |
181 |
312 |
457 | {% endblock %}
--------------------------------------------------------------------------------
/flasks/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import secrets
3 | import threading
4 | import time
5 | from flask import current_app, request, redirect, url_for, flash, session
6 | from email_validator import validate_email, EmailNotValidError
7 | from functools import wraps
8 | from datetime import datetime, timedelta
9 | import random
10 | import string
11 | import jwt
12 | import requests
13 | from .models import AutoRefresh, ChatToken, Record, User, db
14 |
15 | def login_required(f):
16 | """登录状态装饰器"""
17 | @wraps(f)
18 | def decorated_function(*args, **kwargs):
19 | if 'username' not in session:
20 | remember_token = request.cookies.get('remember_token')
21 | if remember_token:
22 | user_id = verify_remember_token(remember_token)
23 | if user_id:
24 | user = User.query.get(user_id)
25 | if user:
26 | # 重新设置session
27 | session['username'] = user.username
28 | session['user_id'] = user.id
29 | session['role'] = user.role
30 | return f(*args, **kwargs)
31 | flash('请先登录!', 'warning')
32 | return redirect(url_for('auth.login', next=request.url))
33 | return f(*args, **kwargs)
34 | return decorated_function
35 |
36 | def admin_required(f):
37 | """管理员权限装饰器"""
38 | @wraps(f)
39 | def decorated_function(*args, **kwargs):
40 | if 'username' not in session:
41 | flash('请先登录!', 'warning')
42 | return redirect(url_for('auth.login', next=request.url))
43 |
44 | user = User.query.filter_by(username=session['username']).first()
45 | if not user or user.role != 'admin':
46 | flash('需要管理员权限!', 'danger')
47 | return redirect(url_for('main.index'))
48 | return f(*args, **kwargs)
49 | return decorated_function
50 |
51 | # 邮箱验证函数
52 | def is_valid_email(email):
53 | try:
54 | # 验证邮箱格式和域名
55 | validate_email(email)
56 | return True
57 | except EmailNotValidError as e:
58 | return False
59 |
60 | # 邮件api配置
61 |
62 | def generate_verification_code(length=6):
63 | """生成指定长度的验证码"""
64 | return ''.join(random.choices(string.digits, k=length))
65 |
66 | def send_verification_email(to_email, code):
67 | """发送验证码邮件"""
68 | send_body = {
69 | "from_name": "Bocchi2B", # 替换为你的发件人名称
70 | "to_name": "用户",
71 | "to_mail": to_email,
72 | "subject": "注册验证码",
73 | "is_html": True,
74 | "content": f"""
75 |
76 | 验证码
77 | 您好!
78 | 您的验证码是:
79 |
80 | {code}
81 |
82 | 验证码有效期为5分钟。如果不是您本人的操作,请忽略此邮件。
83 | 此邮件为系统自动发送,请勿回复。
84 |
85 | """
86 | }
87 |
88 | try:
89 | response = requests.post(
90 | url=current_app.config.get('EMAIL_API'),
91 | json=send_body,
92 | headers={
93 | "Authorization": f"Bearer {current_app.config.get('EMAIL_JWT')}",
94 | "Content-Type": "application/json"
95 | }
96 | )
97 | return response.status_code == 200
98 | except Exception as e:
99 | print(f"发送邮件失败:{str(e)}")
100 | return False
101 |
102 | def send_otp_code_email(to_email, code):
103 | """发送验证码邮件"""
104 | send_body = {
105 | "from_name": "Bocchi2B", # 替换为你的发件人名称
106 | "to_name": "用户",
107 | "to_mail": to_email,
108 | "subject": "登录验证码",
109 | "is_html": True,
110 | "content": f"""
111 |
112 | 验证码
113 | 您好!
114 | 您的登录验证码是:
115 |
116 | {code}
117 |
118 | 验证码有效期为5分钟。如果不是您本人的操作,请尽快更改您的密码。
119 | 此邮件为系统自动发送,请勿回复。
120 |
121 | """
122 | }
123 |
124 | try:
125 | response = requests.post(
126 | url=current_app.config.get('EMAIL_API'),
127 | json=send_body,
128 | headers={
129 | "Authorization": f"Bearer {current_app.config.get('EMAIL_JWT')}",
130 | "Content-Type": "application/json"
131 | }
132 | )
133 | return response.status_code == 200
134 | except Exception as e:
135 | print(f"发送邮件失败:{str(e)}")
136 | return False
137 |
138 |
139 | def verify_code(email, code):
140 | """验证验证码是否正确"""
141 | stored_code = session.get(f'verification_code_{email}')
142 | expire_time = session.get(f'code_expire_time_{email}')
143 |
144 | if not stored_code or not expire_time:
145 | return False
146 |
147 | expire_time = datetime.fromisoformat(expire_time)
148 | if datetime.now() > expire_time:
149 | # 清除过期的验证码
150 | session.pop(f'verification_code_{email}', None)
151 | session.pop(f'code_expire_time_{email}', None)
152 | return False
153 |
154 | return stored_code == code
155 |
156 | def generate_reset_token():
157 | """生成安全的重置令牌"""
158 | return secrets.token_urlsafe(32)
159 |
160 | def send_reset_email(to_email, reset_link):
161 | """发送密码重置链接邮件"""
162 | send_body = {
163 | "from_name": "Bocchi2B", # 替换为你的发件人名称
164 | "to_name": "用户",
165 | "to_mail": to_email,
166 | "subject": "密码重置",
167 | "is_html": True,
168 | "content": f"""
169 |
170 | 密码重置
171 | 您好!
172 | 我们收到了您的密码重置请求。请点击下面的链接重置您的密码:
173 |
174 |
177 | 重置密码
178 |
179 |
180 | 此链接将在30分钟后过期。如果不是您本人的操作,请忽略此邮件。
181 |
182 | 如果按钮无法点击,请复制以下链接到浏览器地址栏:
183 | {reset_link}
184 |
185 | 此邮件为系统自动发送,请勿回复。
186 |
187 | """
188 | }
189 |
190 | try:
191 | response = requests.post(
192 | url=current_app.config.get('EMAIL_API'),
193 | json=send_body,
194 | headers={
195 | "Authorization": f"Bearer {current_app.config.get('EMAIL_JWT')}",
196 | "Content-Type": "application/json"
197 | }
198 | )
199 | return response.status_code == 200
200 | except Exception as e:
201 | print(f"发送邮件失败:{str(e)}")
202 | return False
203 |
204 | # 记录用户登录ip
205 | def get_user_ip():
206 | # 尝试从请求头中获取 X-Forwarded-For
207 | if request.headers.getlist("X-Forwarded-For"):
208 | ip = request.headers.getlist("X-Forwarded-For")[0]
209 | else:
210 | ip = request.remote_addr
211 | return ip
212 |
213 | def generate_remember_token(user_id):
214 | """生成记住我令牌"""
215 | payload = {
216 | 'user_id': user_id,
217 | 'exp': datetime.utcnow() + timedelta(days=current_app.config.get('JWT_EXPIRATION_DAYS',30))
218 | }
219 | token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
220 |
221 | # 如果 token 是字节对象,则解码为字符串
222 | if isinstance(token, bytes):
223 | token = token.decode('utf-8')
224 |
225 | return token
226 |
227 | def verify_remember_token(token):
228 | """验证记住我令牌"""
229 | try:
230 | payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
231 | return payload['user_id']
232 | except:
233 | return None
234 |
235 |
236 | # 两步验证
237 | def send_otp_email(user):
238 | otp_code = generate_verification_code() # 生成一个随机OTP
239 | user.otp_code = otp_code
240 | user.otp_expiration = datetime.utcnow() + timedelta(minutes=5) # 5分钟有效期
241 | db.session.commit()
242 | # 使用邮件库发送OTP到用户邮箱
243 | send_otp_code_email(user.email, otp_code)
244 |
245 | def verify_otp_code(user, otp_code):
246 | """验证OTP码是否有效
247 |
248 | Args:
249 | user: User对象,包含otp_code和otp_expiration属性
250 | otp_code: 用户输入的OTP验证码
251 |
252 | Returns:
253 | bool: 验证是否成功
254 | """
255 | if not user.otp_code or not user.otp_expiration:
256 | return False
257 |
258 | if datetime.utcnow() > user.otp_expiration:
259 | return False
260 |
261 | return user.otp_code == otp_code
262 |
263 |
264 | # 刷新 access_token 的主函数
265 | def refresh_access_tokens():
266 | # 读取 refresh_token 列表
267 | refresh_tokens_list = ChatToken.query.all()
268 | refresh_tokens = [token.to_dict() for token in refresh_tokens_list]
269 |
270 | # 遍历 refresh_token 列表
271 | for token_info in refresh_tokens:
272 |
273 | token = ChatToken.query.filter(ChatToken.email == token_info['email']).first()
274 |
275 | refresh_token = token.refresh_token
276 |
277 | # 如果 refresh_token 为空,跳过这一行
278 | if not refresh_token:
279 | continue
280 |
281 | # 先清除用量表中的access_token
282 | records_to_delete = Record.query.filter_by(access_token=token.access_token).all()
283 | for record in records_to_delete:
284 | db.session.delete(record)
285 | db.session.commit()
286 |
287 | try:
288 | # 使用 POST 请求通过 refresh_token 获取 access_token
289 | response = requests.post(
290 | "https://token.oaifree.com/api/auth/refresh",
291 | data={"refresh_token": refresh_token}
292 | )
293 | response_data = response.json()
294 |
295 | access_token = response_data.get("access_token")
296 | if access_token: # 如果成功获取到 access_token
297 | # 更新 access_token 和状态为 True
298 | token.access_token = access_token
299 | token.status = True
300 | else:
301 | # 如果获取失败,设置状态为 False
302 | token.status = False
303 |
304 | db.session.commit()
305 | except Exception as e:
306 | # 捕获请求错误并记录失败的 token,状态为 False
307 | token.status = False
308 | db.session.commit()
309 |
310 | return refresh_tokens
311 |
312 | # 获取share token
313 | def register_token(access_token, unique_name, expire_in=0, show_userinfo=True, gpt35_limit=-1,
314 | gpt4_limit=-1, reset_limit=False, show_conversations=False, site_limit="",
315 | temporary_chat=False):
316 | """
317 | 注册共享令牌的函数。
318 |
319 | :param access_token: 用户的访问令牌
320 | :param unique_name: 独一无二的共享令牌名称
321 | :param expire_in: 共享令牌的过期秒数,默认为0
322 | :param show_userinfo: 是否显示用户信息,默认为False
323 | :param gpt35_limit: GPT-3.5模型的使用限制,默认为-1表示不限制
324 | :param gpt4_limit: GPT-4模型的使用限制,默认为-1表示不限制
325 | :param reset_limit: 是否重置使用限制,默认为False
326 | :param show_conversations: 是否显示对话记录,默认为True
327 | :param site_limit: 站点使用限制,默认为空字符串,表示不限制
328 | :param temporary_chat: 是否开启临时聊天功能,默认为True
329 |
330 | :return: token_key (str),注册成功后返回的共享令牌 key
331 | """
332 |
333 | url = 'https://chat.oaifree.com/token/register'
334 |
335 | # 数据 payload
336 | data = {
337 | "access_token": access_token,
338 | "unique_name": unique_name,
339 | "expire_in": expire_in,
340 | "show_userinfo": show_userinfo,
341 | "gpt35_limit": gpt35_limit,
342 | "gpt4_limit": gpt4_limit,
343 | "reset_limit": reset_limit,
344 | "show_conversations": show_conversations,
345 | "site_limit": site_limit,
346 | "temporary_chat": temporary_chat
347 | }
348 |
349 | # 发起 POST 请求
350 | response = requests.post(url, headers={'Content-Type': 'application/x-www-form-urlencoded'}, data=data)
351 |
352 | # 获取返回的共享令牌 key
353 | token_key = response.json().get("token_key")
354 |
355 | return token_key
356 |
357 | # 获取用量信息
358 | def usage_record(at,st):
359 | # 构建URL
360 | base_url = "https://chat.oaifree.com/token/info/"
361 | url = f"{base_url}{st}"
362 |
363 | # 设置请求头
364 | headers = {
365 | "Authorization": f"Bearer {at}",
366 | "User-Agent": "Mozilla/5.0",
367 | "Accept": "application/json"
368 | }
369 |
370 | try:
371 | # 发送GET请求
372 | response = requests.get(url, headers=headers)
373 |
374 | # 检查响应状态码
375 | response.raise_for_status()
376 |
377 | # 返回JSON响应
378 | return response.json()
379 |
380 | except requests.exceptions.RequestException as e:
381 | # print(f"请求发生错误: {e}")
382 | return None
383 |
384 | # vv佬PLUS识别
385 | def check_final_url(url, target_url):
386 | try:
387 | # 发送 GET 请求,允许重定向
388 | response = requests.get(url, allow_redirects=True)
389 |
390 | # 获取最终 URL
391 | final_url = response.url
392 |
393 | # 检查最终 URL 是否匹配目标地址
394 | if final_url != target_url:
395 |
396 | return True # 如果不匹配,返回原始链接
397 | else:
398 | return None # 如果匹配,返回 None
399 | except requests.exceptions.RequestException:
400 | return None # 请求失败,返回 None
401 |
402 | # 获取登陆链接
403 | def getoauth(token):
404 | domain = current_app.config.get('DOMAIN_CHATGPT')
405 | print(domain)
406 | share_token = token
407 |
408 | url = f'https://{domain}/api/auth/oauth_token'
409 | headers = {
410 | 'Origin': f'https://{domain}',
411 | 'Content-Type': 'application/json'
412 | }
413 | data = {
414 | 'share_token': share_token
415 | }
416 | try:
417 | response = requests.post(url, headers=headers, json=data)
418 | loginurl = response.json().get('login_url')
419 | return loginurl
420 | except requests.RequestException as e:
421 | return None
422 |
423 |
424 | # 定时刷新token
425 |
426 | def is_main_process():
427 | import os
428 | return os.environ.get('WERKZEUG_RUN_MAIN') != 'true'
429 |
430 | current_timer = None
431 | timer_lock = threading.Lock()
432 |
433 | def schedule_next_refresh():
434 |
435 | global current_timer
436 | config = AutoRefresh.query.filter_by(id=1).first()
437 |
438 | with timer_lock:
439 | if config.auto_refresh_enabled:
440 | if current_timer:
441 | current_timer.cancel()
442 |
443 | next_refresh = datetime.now() + timedelta(days=config.refresh_interval_days)
444 | config.next_refresh_time = next_refresh.isoformat()
445 | db.session.commit()
446 |
447 | current_timer = threading.Timer(
448 | (next_refresh - datetime.now()).total_seconds(),
449 | auto_refresh_tokens
450 | )
451 | current_timer.start()
452 |
453 | def auto_refresh_tokens():
454 |
455 | print('开始自动刷新')
456 | refresh_access_tokens()
457 |
458 | # 添加延时,确保两次刷新之间有足够间隔
459 | time.sleep(2) # 等待1秒
460 |
461 | # 刷新完成后,调度下一次刷新
462 | schedule_next_refresh()
463 |
464 | # 在应用启动时调用这个函数
465 | def init_auto_refresh():
466 | if not is_main_process():
467 | print("在 reloader 进程中,跳过定时器初始化")
468 | return
469 |
470 | print(f"在主进程中初始化自动刷新, 当前时间: {datetime.now()}")
471 | config = AutoRefresh.query.filter_by(id=1).first()
472 |
473 | if config.auto_refresh_enabled and config.next_refresh_time:
474 | next_refresh = datetime.fromisoformat(config.next_refresh_time)
475 |
476 | if next_refresh > datetime.now():
477 | delay_seconds = (next_refresh - datetime.now()).total_seconds()
478 | print(f"设置初始定时器, 延迟秒数: {delay_seconds}")
479 |
480 | global current_timer
481 | with timer_lock:
482 | current_timer = threading.Timer(delay_seconds, auto_refresh_tokens)
483 | current_timer.start()
484 | else:
485 | schedule_next_refresh()
486 |
487 | # claude登陆链接获取
488 | def get_claude_login_url(session_key,uname):
489 | domain = current_app.config.get('DOMAIN_CLAUDE')
490 | url = f'https://{domain}/manage-api/auth/oauth_token'
491 |
492 | # 请求体参数
493 | data = {
494 | 'session_key': session_key,
495 | 'unique_name': uname # 生成唯一标识符
496 | }
497 |
498 | # 设置请求头
499 | headers = {'Content-Type': 'application/json'}
500 |
501 | try:
502 | # 发送 POST 请求
503 | response = requests.post(url, headers=headers, data=json.dumps(data))
504 |
505 | # 检查响应状态码是否为200
506 | if response.status_code == 200:
507 | response_data = response.json()
508 |
509 | # 检查 'login_url' 是否存在
510 | if 'login_url' in response_data:
511 | login_url = response_data['login_url']
512 |
513 | # 如果URL没有以http开头,拼接基础URL
514 | if not login_url.startswith('http'):
515 | return f'https://{domain}' + login_url
516 | return login_url
517 |
518 | # 如果状态码不是200或login_url不存在,返回None
519 | return None
520 |
521 | except requests.RequestException as e:
522 | # 捕获异常并返回错误信息
523 | return None
--------------------------------------------------------------------------------
/flasks/templates/admin/admin_user.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}管理页面{% endblock %}
4 |
5 | {% block content %}
6 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
96 |
97 |
98 |
99 | 总用户数
100 | {{ total_users }}
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
112 |
113 |
114 |
115 | 新增用户
116 | {{ new_users_count }}
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
128 |
129 |
130 |
131 | 活跃用户
132 | {{ active_users_count }}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
144 |
145 |
146 |
147 | 不活跃用户
148 | {{ inactive_users_count }}
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | 近15天注册趋势
162 | 显示每日新增注册用户数量
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | 用户活跃度分析
171 | 显示每日活跃用户数量
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | 邀请码管理
188 |
191 |
192 |
193 |
194 |
195 |
196 | 邀请码
197 | 剩余(个)
198 | 操作
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | 用户列表
219 |
220 |
221 |
222 |
223 |
224 |
226 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
246 | {% include "admin/_user_table.html" %}
247 |
248 |
249 |
250 |
251 |
252 |
300 |
301 |
302 |
320 |
321 |
322 |
323 |
398 |
399 |
525 |
708 |
709 |
710 | {% endblock %}
--------------------------------------------------------------------------------