malt666 commited on
Commit
cf988af
·
verified ·
1 Parent(s): 2033651

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +1584 -0
  2. templates/dashboard.html +1102 -0
  3. templates/login.html +462 -0
app.py ADDED
@@ -0,0 +1,1584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, Response, render_template_string, render_template, redirect, url_for, session as flask_session
2
+ import requests
3
+ import time
4
+ import json
5
+ import uuid
6
+ import random
7
+ import io
8
+ import re
9
+ from functools import wraps
10
+ import hashlib
11
+ import jwt
12
+ import os
13
+ import threading
14
+ from datetime import datetime, timedelta
15
+
16
+ app = Flask(__name__, template_folder='templates')
17
+ app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
18
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
19
+
20
+ # 添加tokenizer服务URL
21
+ TOKENIZER_SERVICE_URL = "https://malt666-tokenizer.hf.space/count_tokens"
22
+
23
+ API_ENDPOINT_URL = "https://abacus.ai/api/v0/describeDeployment"
24
+ MODEL_LIST_URL = "https://abacus.ai/api/v0/listExternalApplications"
25
+ CHAT_URL = "https://apps.abacus.ai/api/_chatLLMSendMessageSSE"
26
+ USER_INFO_URL = "https://abacus.ai/api/v0/_getUserInfo"
27
+ COMPUTE_POINTS_URL = "https://apps.abacus.ai/api/_getOrganizationComputePoints"
28
+ COMPUTE_POINTS_LOG_URL = "https://abacus.ai/api/v0/_getOrganizationComputePointLog"
29
+ CREATE_CONVERSATION_URL = "https://apps.abacus.ai/api/createDeploymentConversation"
30
+ DELETE_CONVERSATION_URL = "https://apps.abacus.ai/api/deleteDeploymentConversation"
31
+ GET_CONVERSATION_URL = "https://apps.abacus.ai/api/getDeploymentConversation"
32
+ COMPUTE_POINT_TOGGLE_URL = "https://abacus.ai/api/v0/_updateOrganizationComputePointToggle"
33
+
34
+
35
+ USER_AGENTS = [
36
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
37
+ ]
38
+
39
+
40
+ PASSWORD = None
41
+ USER_NUM = 0
42
+ USER_DATA = []
43
+ CURRENT_USER = -1
44
+ MODELS = set()
45
+
46
+ # 添加记录上一个conversation_id的变量和删除标记
47
+ LAST_CONVERSATION_IDS = [None] * 100 # 为每个用户记录上一个conversation_id
48
+ DELETE_CHAT = True # 是否在对话结束后删除上一个对话
49
+
50
+ TRACE_ID = "3042e28b3abf475d8d973c7e904935af"
51
+ SENTRY_TRACE = f"{TRACE_ID}-80d9d2538b2682d0"
52
+
53
+
54
+ # 添加一个计数器记录健康检查次数
55
+ health_check_counter = 0
56
+
57
+
58
+ # 添加统计变量
59
+ model_usage_stats = {} # 模型使用次数统计
60
+ total_tokens = {
61
+ "prompt": 0, # 输入token统计
62
+ "completion": 0, # 输出token统计
63
+ "total": 0 # 总token统计
64
+ }
65
+
66
+ # 模型调用记录
67
+ model_usage_records = [] # 每次调用详细记录
68
+ MODEL_USAGE_RECORDS_FILE = "model_usage_records.json" # 调用记录保存文件
69
+
70
+ # 计算点信息
71
+ compute_points = {
72
+ "left": 0, # 剩余计算点
73
+ "total": 0, # 总计算点
74
+ "used": 0, # 已使用计算点
75
+ "percentage": 0, # 使用百分比
76
+ "last_update": None # 最后更新时间
77
+ }
78
+
79
+ # 计算点使用日志
80
+ compute_points_log = {
81
+ "columns": {}, # 列名
82
+ "log": [] # 日志数据
83
+ }
84
+
85
+ # 多用户计算点信息
86
+ users_compute_points = []
87
+
88
+ # 记录启动时间
89
+ START_TIME = datetime.utcnow() + timedelta(hours=8) # 北京时间
90
+
91
+
92
+ # 自定义JSON编码器,处理datetime对象
93
+ class DateTimeEncoder(json.JSONEncoder):
94
+ def default(self, obj):
95
+ if isinstance(obj, datetime):
96
+ return obj.strftime('%Y-%m-%d %H:%M:%S')
97
+ return super(DateTimeEncoder, self).default(obj)
98
+
99
+
100
+ # 加载模型调用记录
101
+ def load_model_usage_records():
102
+ global model_usage_records
103
+ try:
104
+ if os.path.exists(MODEL_USAGE_RECORDS_FILE):
105
+ with open(MODEL_USAGE_RECORDS_FILE, 'r', encoding='utf-8') as f:
106
+ records = json.load(f)
107
+ if isinstance(records, list):
108
+ model_usage_records = records
109
+ print(f"成功加载 {len(model_usage_records)} 条模型调用记录")
110
+ else:
111
+ print("调用记录文件格式不正确,初始化为空列表")
112
+ except Exception as e:
113
+ print(f"加载模型调用记录失败: {e}")
114
+ model_usage_records = []
115
+
116
+ # 保存模型调用记录
117
+ def save_model_usage_records():
118
+ try:
119
+ with open(MODEL_USAGE_RECORDS_FILE, 'w', encoding='utf-8') as f:
120
+ json.dump(model_usage_records, f, ensure_ascii=False, indent=2, cls=DateTimeEncoder)
121
+ print(f"成功保存 {len(model_usage_records)} 条模型调用记录")
122
+ except Exception as e:
123
+ print(f"保存模型调用记录失败: {e}")
124
+
125
+
126
+ def update_conversation_id(user_index, conversation_id):
127
+ """更新用户的conversation_id并保存到配置文件"""
128
+ try:
129
+ with open("config.json", "r") as f:
130
+ config = json.load(f)
131
+
132
+ if "config" in config and user_index < len(config["config"]):
133
+ config["config"][user_index]["conversation_id"] = conversation_id
134
+
135
+ # 保存到配置文件
136
+ with open("config.json", "w") as f:
137
+ json.dump(config, f, indent=4)
138
+
139
+ print(f"已将用户 {user_index+1} 的conversation_id更新为: {conversation_id}")
140
+ else:
141
+ print(f"更新conversation_id失败: 配置文件格式错误或���户索引越界")
142
+ except Exception as e:
143
+ print(f"更新conversation_id失败: {e}")
144
+
145
+
146
+ def resolve_config():
147
+ # 从环境变量读取多组配置
148
+ config_list = []
149
+ i = 1
150
+ while True:
151
+ cookie = os.environ.get(f"cookie_{i}")
152
+ if not cookie:
153
+ break
154
+
155
+ # 为每个cookie创建一个配置项,conversation_id初始为空
156
+ config_list.append({
157
+ "conversation_id": "", # 初始为空,将通过get_or_create_conversation自动创建
158
+ "cookies": cookie
159
+ })
160
+ i += 1
161
+
162
+ # 如果环境变量存在配置,使用环境变量的配置
163
+ if config_list:
164
+ print(f"从环境变量加载了 {len(config_list)} 个配置")
165
+ return config_list
166
+
167
+ # 如果环境变量不存在,从文件读取
168
+ try:
169
+ with open("config.json", "r") as f:
170
+ config = json.load(f)
171
+ config_list = config.get("config")
172
+ return config_list
173
+ except FileNotFoundError:
174
+ print("未找到config.json文件")
175
+ return []
176
+ except json.JSONDecodeError:
177
+ print("config.json格式错误")
178
+ return []
179
+
180
+
181
+ def get_password():
182
+ global PASSWORD
183
+ # 从环境变量读取密码
184
+ env_password = os.environ.get("password")
185
+ if env_password:
186
+ PASSWORD = hashlib.sha256(env_password.encode()).hexdigest()
187
+ return
188
+
189
+ # 如果环境变量不存在,从文件读取
190
+ try:
191
+ with open("password.txt", "r") as f:
192
+ PASSWORD = f.read().strip()
193
+ except FileNotFoundError:
194
+ with open("password.txt", "w") as f:
195
+ PASSWORD = None
196
+
197
+
198
+ def require_auth(f):
199
+ @wraps(f)
200
+ def decorated(*args, **kwargs):
201
+ if not PASSWORD:
202
+ return f(*args, **kwargs)
203
+
204
+ # 检查Flask会话是否已登录
205
+ if flask_session.get('logged_in'):
206
+ return f(*args, **kwargs)
207
+
208
+ # 如果是API请求,检查Authorization头
209
+ auth = request.authorization
210
+ if not auth or not check_auth(auth.token):
211
+ # 如果是浏览器请求,重定向到登录页面
212
+ if request.headers.get('Accept', '').find('text/html') >= 0:
213
+ return redirect(url_for('login'))
214
+ return jsonify({"error": "Unauthorized access"}), 401
215
+ return f(*args, **kwargs)
216
+
217
+ return decorated
218
+
219
+
220
+ def check_auth(token):
221
+ return hashlib.sha256(token.encode()).hexdigest() == PASSWORD
222
+
223
+
224
+ def is_token_expired(token):
225
+ if not token:
226
+ return True
227
+
228
+ try:
229
+ # Malkodi tokenon sen validigo de subskribo
230
+ payload = jwt.decode(token, options={"verify_signature": False})
231
+ # Akiru eksvalidiĝan tempon, konsideru eksvalidiĝinta 5 minutojn antaŭe
232
+ return payload.get('exp', 0) - time.time() < 300
233
+ except:
234
+ return True
235
+
236
+
237
+ def refresh_token(session, cookies):
238
+ """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
239
+ headers = {
240
+ "accept": "application/json, text/plain, */*",
241
+ "accept-language": "zh-CN,zh;q=0.9",
242
+ "content-type": "application/json",
243
+ "reai-ui": "1",
244
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
245
+ "sec-ch-ua-mobile": "?0",
246
+ "sec-ch-ua-platform": "\"Windows\"",
247
+ "sec-fetch-dest": "empty",
248
+ "sec-fetch-mode": "cors",
249
+ "sec-fetch-site": "same-site",
250
+ "x-abacus-org-host": "apps",
251
+ "user-agent": random.choice(USER_AGENTS),
252
+ "origin": "https://apps.abacus.ai",
253
+ "referer": "https://apps.abacus.ai/",
254
+ "cookie": cookies
255
+ }
256
+
257
+ try:
258
+ response = session.post(
259
+ USER_INFO_URL,
260
+ headers=headers,
261
+ json={},
262
+ cookies=None
263
+ )
264
+
265
+ if response.status_code == 200:
266
+ response_data = response.json()
267
+ if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
268
+ return response_data['result']['sessionToken']
269
+ else:
270
+ print(f"刷新token失败: {response_data.get('error', '未知错误')}")
271
+ return None
272
+ else:
273
+ print(f"刷新token失败,状态码: {response.status_code}")
274
+ return None
275
+ except Exception as e:
276
+ print(f"刷新token异常: {e}")
277
+ return None
278
+
279
+
280
+ def get_model_map(session, cookies, session_token):
281
+ """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
282
+ headers = {
283
+ "accept": "application/json, text/plain, */*",
284
+ "accept-language": "zh-CN,zh;q=0.9",
285
+ "content-type": "application/json",
286
+ "reai-ui": "1",
287
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
288
+ "sec-ch-ua-mobile": "?0",
289
+ "sec-ch-ua-platform": "\"Windows\"",
290
+ "sec-fetch-dest": "empty",
291
+ "sec-fetch-mode": "cors",
292
+ "sec-fetch-site": "same-site",
293
+ "x-abacus-org-host": "apps",
294
+ "user-agent": random.choice(USER_AGENTS),
295
+ "origin": "https://apps.abacus.ai",
296
+ "referer": "https://apps.abacus.ai/",
297
+ "cookie": cookies
298
+ }
299
+
300
+ if session_token:
301
+ headers["session-token"] = session_token
302
+
303
+ model_map = {}
304
+ models_set = set()
305
+
306
+ try:
307
+ response = session.post(
308
+ MODEL_LIST_URL,
309
+ headers=headers,
310
+ json={},
311
+ cookies=None
312
+ )
313
+
314
+ if response.status_code != 200:
315
+ print(f"获取模型列表失败,状态码: {response.status_code}")
316
+ raise Exception("API请求失败")
317
+
318
+ data = response.json()
319
+ if not data.get('success'):
320
+ print(f"获取模型列表失败: {data.get('error', '未知错误')}")
321
+ raise Exception("API返回错误")
322
+
323
+ applications = []
324
+ if isinstance(data.get('result'), dict):
325
+ applications = data.get('result', {}).get('externalApplications', [])
326
+ elif isinstance(data.get('result'), list):
327
+ applications = data.get('result', [])
328
+
329
+ for app in applications:
330
+ app_name = app.get('name', '')
331
+ app_id = app.get('externalApplicationId', '')
332
+ prediction_overrides = app.get('predictionOverrides', {})
333
+ llm_name = prediction_overrides.get('llmName', '') if prediction_overrides else ''
334
+
335
+ if not (app_name and app_id and llm_name):
336
+ continue
337
+
338
+ model_name = app_name
339
+ model_map[model_name] = (app_id, llm_name)
340
+ models_set.add(model_name)
341
+
342
+ if not model_map:
343
+ raise Exception("未找到任何可用模型")
344
+
345
+ return model_map, models_set
346
+
347
+ except Exception as e:
348
+ print(f"获取模型列表异常: {e}")
349
+ raise
350
+
351
+
352
+ def init_session():
353
+ get_password()
354
+ global USER_NUM, MODELS, USER_DATA, DELETE_CHAT
355
+
356
+ # 从环境变量读取是否删除上一个对话的设置
357
+ delete_chat_env = os.environ.get("DELETE_CHAT", "true").lower()
358
+ DELETE_CHAT = delete_chat_env in ["true", "1", "yes", "y"]
359
+ print(f"删除上一个对话设置: {DELETE_CHAT}")
360
+
361
+ config_list = resolve_config()
362
+ user_num = len(config_list)
363
+ all_models = set()
364
+
365
+ for i in range(user_num):
366
+ user = config_list[i]
367
+ cookies = user.get("cookies")
368
+ conversation_id = user.get("conversation_id")
369
+ session = requests.Session()
370
+
371
+ session_token = refresh_token(session, cookies)
372
+ if not session_token:
373
+ print(f"无法获取cookie {i+1}的token")
374
+ continue
375
+
376
+ try:
377
+ model_map, models_set = get_model_map(session, cookies, session_token)
378
+ all_models.update(models_set)
379
+ USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
380
+
381
+ # 对第一个成功配置的用户,初始化计算点数记录功能
382
+ if i == 0:
383
+ try:
384
+ headers = {
385
+ "accept": "application/json, text/plain, */*",
386
+ "accept-language": "zh-CN,zh;q=0.9",
387
+ "content-type": "application/json",
388
+ "reai-ui": "1",
389
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
390
+ "sec-ch-ua-mobile": "?0",
391
+ "sec-ch-ua-platform": "\"Windows\"",
392
+ "sec-fetch-dest": "empty",
393
+ "sec-fetch-mode": "cors",
394
+ "sec-fetch-site": "same-site",
395
+ "x-abacus-org-host": "apps",
396
+ "session-token": session_token
397
+ }
398
+
399
+ response = session.post(
400
+ COMPUTE_POINT_TOGGLE_URL,
401
+ headers=headers,
402
+ json={"alwaysDisplay": True},
403
+ cookies=None
404
+ )
405
+
406
+ if response.status_code == 200:
407
+ result = response.json()
408
+ if result.get("success"):
409
+ print("成功初始化计算点数记录功能为开启状态")
410
+ else:
411
+ print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
412
+ else:
413
+ print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
414
+ except Exception as e:
415
+ print(f"初始化计算点数记录功能时出错: {e}")
416
+ except Exception as e:
417
+ print(f"配置用户 {i+1} 失败: {e}")
418
+ continue
419
+
420
+ USER_NUM = len(USER_DATA)
421
+ if USER_NUM == 0:
422
+ print("No user available, exiting...")
423
+ exit(1)
424
+
425
+ MODELS = all_models
426
+ print(f"启动完成,共配置 {USER_NUM} 个用户")
427
+
428
+
429
+ def update_cookie(session, cookies):
430
+ cookie_jar = {}
431
+ for key, value in session.cookies.items():
432
+ cookie_jar[key] = value
433
+ cookie_dict = {}
434
+ for item in cookies.split(";"):
435
+ key, value = item.strip().split("=", 1)
436
+ cookie_dict[key] = value
437
+ cookie_dict.update(cookie_jar)
438
+ cookies = "; ".join([f"{key}={value}" for key, value in cookie_dict.items()])
439
+ return cookies
440
+
441
+
442
+ user_data = init_session()
443
+
444
+
445
+ @app.route("/v1/models", methods=["GET"])
446
+ @require_auth
447
+ def get_models():
448
+ if len(MODELS) == 0:
449
+ return jsonify({"error": "No models available"}), 500
450
+ model_list = []
451
+ for model in MODELS:
452
+ model_list.append(
453
+ {
454
+ "id": model,
455
+ "object": "model",
456
+ "created": int(time.time()),
457
+ "owned_by": "Elbert",
458
+ "name": model,
459
+ }
460
+ )
461
+ return jsonify({"object": "list", "data": model_list})
462
+
463
+
464
+ @app.route("/v1/chat/completions", methods=["POST"])
465
+ @require_auth
466
+ def chat_completions():
467
+ openai_request = request.get_json()
468
+ stream = openai_request.get("stream", False)
469
+ messages = openai_request.get("messages")
470
+ if messages is None:
471
+ return jsonify({"error": "Messages is required", "status": 400}), 400
472
+ model = openai_request.get("model")
473
+ if model not in MODELS:
474
+ return (
475
+ jsonify(
476
+ {
477
+ "error": "Model not available, check if it is configured properly",
478
+ "status": 404,
479
+ }
480
+ ),
481
+ 404,
482
+ )
483
+ message = format_message(messages)
484
+ think = (
485
+ openai_request.get("think", False) if model == "Claude Sonnet 3.7" else False
486
+ )
487
+ return (
488
+ send_message(message, model, think)
489
+ if stream
490
+ else send_message_non_stream(message, model, think)
491
+ )
492
+
493
+
494
+ def get_user_data():
495
+ global CURRENT_USER
496
+ CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
497
+ print(f"使用配置 {CURRENT_USER+1}")
498
+
499
+ # Akiru uzantajn datumojn
500
+ session, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[CURRENT_USER]
501
+
502
+ # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
503
+ if is_token_expired(session_token):
504
+ print(f"Cookie {CURRENT_USER+1}的token已过期或即将过期,正在刷新...")
505
+ new_token = refresh_token(session, cookies)
506
+ if new_token:
507
+ # Ĝisdatigu la globale konservitan tokenon
508
+ USER_DATA[CURRENT_USER] = (session, cookies, new_token, conversation_id, model_map, user_index)
509
+ session_token = new_token
510
+ print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
511
+ else:
512
+ print(f"警告:无法刷新Cookie {CURRENT_USER+1}的token,继续使用当前token")
513
+
514
+ return (session, cookies, session_token, conversation_id, model_map, user_index)
515
+
516
+
517
+ def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
518
+ """创建新的会话"""
519
+ if not (external_application_id and deployment_id):
520
+ print("无法创建新会话: 缺少必要参数")
521
+ return None
522
+
523
+ headers = {
524
+ "accept": "application/json, text/plain, */*",
525
+ "accept-language": "zh-CN,zh;q=0.9",
526
+ "content-type": "application/json",
527
+ "cookie": cookies,
528
+ "user-agent": random.choice(USER_AGENTS),
529
+ "x-abacus-org-host": "apps"
530
+ }
531
+
532
+ if session_token:
533
+ headers["session-token"] = session_token
534
+
535
+ create_payload = {
536
+ "deploymentId": deployment_id,
537
+ "name": "New Chat",
538
+ "externalApplicationId": external_application_id
539
+ }
540
+
541
+ try:
542
+ response = session.post(
543
+ CREATE_CONVERSATION_URL,
544
+ headers=headers,
545
+ json=create_payload
546
+ )
547
+
548
+ if response.status_code == 200:
549
+ data = response.json()
550
+ if data.get("success", False):
551
+ new_conversation_id = data.get("result", {}).get("deploymentConversationId")
552
+ if new_conversation_id:
553
+ print(f"成功创建新的conversation: {new_conversation_id}")
554
+ return new_conversation_id
555
+
556
+ print(f"创建会话失败: {response.status_code} - {response.text[:100]}")
557
+ return None
558
+ except Exception as e:
559
+ print(f"创建会话时出错: {e}")
560
+ return None
561
+
562
+
563
+ def delete_conversation(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
564
+ """删除指定的对话"""
565
+ if not conversation_id:
566
+ print("无法删除对话: 缺少conversation_id")
567
+ return False
568
+
569
+ headers = {
570
+ "accept": "application/json, text/plain, */*",
571
+ "accept-language": "zh-CN,zh;q=0.9",
572
+ "content-type": "application/json",
573
+ "cookie": cookies,
574
+ "user-agent": random.choice(USER_AGENTS),
575
+ "x-abacus-org-host": "apps"
576
+ }
577
+
578
+ if session_token:
579
+ headers["session-token"] = session_token
580
+
581
+ delete_payload = {
582
+ "deploymentId": deployment_id,
583
+ "deploymentConversationId": conversation_id
584
+ }
585
+
586
+ try:
587
+ response = session.post(
588
+ DELETE_CONVERSATION_URL,
589
+ headers=headers,
590
+ json=delete_payload
591
+ )
592
+
593
+ if response.status_code == 200:
594
+ data = response.json()
595
+ if data.get("success", False):
596
+ print(f"成功删除对话: {conversation_id}")
597
+ return True
598
+
599
+ print(f"删除对话失败: {response.status_code} - {response.text[:100]}")
600
+ return False
601
+ except Exception as e:
602
+ print(f"删除对话时出错: {e}")
603
+ return False
604
+
605
+
606
+ def is_conversation_valid(session, cookies, session_token, conversation_id, model_map, model):
607
+ """检查会话ID是否有效"""
608
+ if not conversation_id:
609
+ return False
610
+
611
+ # 如果没有这些信息,无法验证
612
+ if not (model in model_map and len(model_map[model]) >= 2):
613
+ return False
614
+
615
+ external_app_id = model_map[model][0]
616
+
617
+ # 尝试发送一个空消息来测试会话ID是否有效
618
+ headers = {
619
+ "accept": "text/event-stream",
620
+ "content-type": "text/plain;charset=UTF-8",
621
+ "cookie": cookies,
622
+ "user-agent": random.choice(USER_AGENTS)
623
+ }
624
+
625
+ if session_token:
626
+ headers["session-token"] = session_token
627
+
628
+ payload = {
629
+ "requestId": str(uuid.uuid4()),
630
+ "deploymentConversationId": conversation_id,
631
+ "message": "", # 空消息
632
+ "isDesktop": False,
633
+ "externalApplicationId": external_app_id
634
+ }
635
+
636
+ try:
637
+ response = session.post(
638
+ CHAT_URL,
639
+ headers=headers,
640
+ data=json.dumps(payload),
641
+ stream=False
642
+ )
643
+
644
+ # 即使返回错误,只要不是缺少ID的错误,也说明ID是有效的
645
+ if response.status_code == 200:
646
+ return True
647
+
648
+ error_text = response.text
649
+ if "Missing required parameter" in error_text:
650
+ return False
651
+
652
+ # 其他类型的错误,可能ID是有效的但有其他问题
653
+ return True
654
+ except:
655
+ # 如果请求出错,无法确定,返回False让系统创建新ID
656
+ return False
657
+
658
+
659
+ def get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index):
660
+ """获取有效的会话ID,如果无效则创建新会话"""
661
+ # 修改为总是创建新的conversation_id
662
+ print("将为每次对话创建新会话")
663
+ need_create = True
664
+
665
+ # 如果需要创建新会话
666
+ if need_create:
667
+ if model in model_map and len(model_map[model]) >= 2:
668
+ external_app_id = model_map[model][0]
669
+ # 创建会话时需要deployment_id,我们先使用一个固定值
670
+ # 在实际应用中应从API响应中获取
671
+ deployment_id = "14b2a314cc" # 这是从您提供的请求中获取的
672
+
673
+ new_conversation_id = create_conversation(
674
+ session, cookies, session_token,
675
+ external_application_id=external_app_id,
676
+ deployment_id=deployment_id
677
+ )
678
+
679
+ if new_conversation_id:
680
+ # 获取当前用户的上一个conversation_id
681
+ global USER_DATA, CURRENT_USER, LAST_CONVERSATION_IDS, DELETE_CHAT
682
+ last_conversation_id = LAST_CONVERSATION_IDS[user_index]
683
+
684
+ # 更新全局存储的会话ID
685
+ session, cookies, session_token, _, model_map, _ = USER_DATA[CURRENT_USER]
686
+ USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
687
+
688
+ # 保存到配置文件
689
+ update_conversation_id(user_index, new_conversation_id)
690
+
691
+ # 保存新的会话ID为下次调用时的"上一个ID"
692
+ LAST_CONVERSATION_IDS[user_index] = new_conversation_id
693
+
694
+ return new_conversation_id
695
+
696
+ # 如果无法创建,返回原始ID
697
+ return conversation_id
698
+
699
+
700
+ def generate_trace_id():
701
+ """Generu novan trace_id kaj sentry_trace"""
702
+ trace_id = str(uuid.uuid4()).replace('-', '')
703
+ sentry_trace = f"{trace_id}-{str(uuid.uuid4())[:16]}"
704
+ return trace_id, sentry_trace
705
+
706
+
707
+ def send_message(message, model, think=False):
708
+ """Flua traktado kaj plusendo de mesaĝoj"""
709
+ global DELETE_CHAT, LAST_CONVERSATION_IDS
710
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
711
+
712
+ # 获取并保存当��的conversation_id(可能是旧的,用于稍后删除)
713
+ last_conversation_id = conversation_id
714
+
715
+ # 确保有有效的会话ID
716
+ conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
717
+
718
+ trace_id, sentry_trace = generate_trace_id()
719
+
720
+ # 计算输入token
721
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
722
+ completion_buffer = io.StringIO() # 收集所有输出用于计算token
723
+
724
+ headers = {
725
+ "accept": "text/event-stream",
726
+ "accept-language": "zh-CN,zh;q=0.9",
727
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
728
+ "content-type": "text/plain;charset=UTF-8",
729
+ "cookie": cookies,
730
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
731
+ "sec-ch-ua-mobile": "?0",
732
+ "sec-ch-ua-platform": "\"Windows\"",
733
+ "sec-fetch-dest": "empty",
734
+ "sec-fetch-mode": "cors",
735
+ "sec-fetch-site": "same-origin",
736
+ "sentry-trace": sentry_trace,
737
+ "user-agent": random.choice(USER_AGENTS),
738
+ "x-abacus-org-host": "apps"
739
+ }
740
+
741
+ if session_token:
742
+ headers["session-token"] = session_token
743
+
744
+ payload = {
745
+ "requestId": str(uuid.uuid4()),
746
+ "deploymentConversationId": conversation_id,
747
+ "message": message,
748
+ "isDesktop": False,
749
+ "chatConfig": {
750
+ "timezone": "Asia/Shanghai",
751
+ "language": "zh-CN"
752
+ },
753
+ "llmName": model_map[model][1],
754
+ "externalApplicationId": model_map[model][0]
755
+ }
756
+
757
+ if think:
758
+ payload["useThinking"] = think
759
+
760
+ try:
761
+ response = session.post(
762
+ CHAT_URL,
763
+ headers=headers,
764
+ data=json.dumps(payload),
765
+ stream=True,
766
+ cookies=None
767
+ )
768
+
769
+ response.raise_for_status()
770
+
771
+ def extract_segment(line_data):
772
+ try:
773
+ data = json.loads(line_data)
774
+ if "segment" in data:
775
+ if isinstance(data["segment"], str):
776
+ return data["segment"]
777
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
778
+ return data["segment"]["segment"]
779
+ return ""
780
+ except:
781
+ return ""
782
+
783
+ def generate():
784
+ id = ""
785
+ think_state = 2
786
+
787
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
788
+
789
+ for line in response.iter_lines():
790
+ if line:
791
+ decoded_line = line.decode("utf-8")
792
+ try:
793
+ if think:
794
+ data = json.loads(decoded_line)
795
+ if data.get("type") != "text":
796
+ continue
797
+ elif think_state == 2:
798
+ id = data.get("messageId")
799
+ segment = "<think>\n" + data.get("segment", "")
800
+ completion_buffer.write(segment) # 收集输出
801
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
802
+ think_state = 1
803
+ elif think_state == 1:
804
+ if data.get("messageId") != id:
805
+ segment = data.get("segment", "")
806
+ completion_buffer.write(segment) # 收集输出
807
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
808
+ else:
809
+ segment = "\n</think>\n" + data.get("segment", "")
810
+ completion_buffer.write(segment) # 收集输出
811
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
812
+ think_state = 0
813
+ else:
814
+ segment = data.get("segment", "")
815
+ completion_buffer.write(segment) # 收集输出
816
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
817
+ else:
818
+ segment = extract_segment(decoded_line)
819
+ if segment:
820
+ completion_buffer.write(segment) # 收集输出
821
+ yield f"data: {json.dumps({'object': 'chat.completion.chunk', 'choices': [{'delta': {'content': segment}}]})}\n\n"
822
+ except Exception as e:
823
+ print(f"处理响应出错: {e}")
824
+
825
+ yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
826
+ yield "data: [DONE]\n\n"
827
+
828
+ # 在流式传输完成后计算token并更新统计
829
+ completion_result, _ = num_tokens_from_string(completion_buffer.getvalue(), model)
830
+
831
+ # 保存对话历史并获取计算点数
832
+ _, compute_points = save_conversation_history(session, cookies, session_token, conversation_id)
833
+
834
+ # 更新统计信息
835
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points)
836
+
837
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
838
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
839
+ delete_conversation(session, cookies, session_token, last_conversation_id)
840
+
841
+ return Response(generate(), mimetype="text/event-stream")
842
+ except requests.exceptions.RequestException as e:
843
+ error_details = str(e)
844
+ if hasattr(e, 'response') and e.response is not None:
845
+ if hasattr(e.response, 'text'):
846
+ error_details += f" - Response: {e.response.text[:200]}"
847
+ print(f"发送消息失败: {error_details}")
848
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
849
+
850
+
851
+ def send_message_non_stream(message, model, think=False):
852
+ """Ne-flua traktado de mesaĝoj"""
853
+ global DELETE_CHAT, LAST_CONVERSATION_IDS
854
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
855
+
856
+ # 获取并保存当前的conversation_id(可能是旧的,用于稍后删除)
857
+ last_conversation_id = conversation_id
858
+
859
+ # 确保有有效的会话ID
860
+ conversation_id = get_or_create_conversation(session, cookies, session_token, conversation_id, model_map, model, user_index)
861
+
862
+ trace_id, sentry_trace = generate_trace_id()
863
+
864
+ # 计算输入token
865
+ prompt_tokens, calculation_method = num_tokens_from_string(message, model)
866
+
867
+ headers = {
868
+ "accept": "text/event-stream",
869
+ "accept-language": "zh-CN,zh;q=0.9",
870
+ "baggage": f"sentry-environment=production,sentry-release=975eec6685013679c139fc88db2c48e123d5c604,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={trace_id}",
871
+ "content-type": "text/plain;charset=UTF-8",
872
+ "cookie": cookies,
873
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
874
+ "sec-ch-ua-mobile": "?0",
875
+ "sec-ch-ua-platform": "\"Windows\"",
876
+ "sec-fetch-dest": "empty",
877
+ "sec-fetch-mode": "cors",
878
+ "sec-fetch-site": "same-origin",
879
+ "sentry-trace": sentry_trace,
880
+ "user-agent": random.choice(USER_AGENTS),
881
+ "x-abacus-org-host": "apps"
882
+ }
883
+
884
+ if session_token:
885
+ headers["session-token"] = session_token
886
+
887
+ payload = {
888
+ "requestId": str(uuid.uuid4()),
889
+ "deploymentConversationId": conversation_id,
890
+ "message": message,
891
+ "isDesktop": False,
892
+ "chatConfig": {
893
+ "timezone": "Asia/Shanghai",
894
+ "language": "zh-CN"
895
+ },
896
+ "llmName": model_map[model][1],
897
+ "externalApplicationId": model_map[model][0]
898
+ }
899
+
900
+ if think:
901
+ payload["useThinking"] = think
902
+
903
+ try:
904
+ response = session.post(
905
+ CHAT_URL,
906
+ headers=headers,
907
+ data=json.dumps(payload),
908
+ stream=True,
909
+ cookies=None
910
+ )
911
+
912
+ response.raise_for_status()
913
+ buffer = io.StringIO()
914
+
915
+ def extract_segment(line_data):
916
+ try:
917
+ data = json.loads(line_data)
918
+ if "segment" in data:
919
+ if isinstance(data["segment"], str):
920
+ return data["segment"]
921
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
922
+ return data["segment"]["segment"]
923
+ return ""
924
+ except:
925
+ return ""
926
+
927
+ if think:
928
+ id = ""
929
+ think_state = 2
930
+ think_buffer = io.StringIO()
931
+ content_buffer = io.StringIO()
932
+
933
+ for line in response.iter_lines():
934
+ if line:
935
+ decoded_line = line.decode("utf-8")
936
+ try:
937
+ data = json.loads(decoded_line)
938
+ if data.get("type") != "text":
939
+ continue
940
+ elif think_state == 2:
941
+ id = data.get("messageId")
942
+ segment = data.get("segment", "")
943
+ think_buffer.write(segment)
944
+ think_state = 1
945
+ elif think_state == 1:
946
+ if data.get("messageId") != id:
947
+ segment = data.get("segment", "")
948
+ content_buffer.write(segment)
949
+ else:
950
+ segment = data.get("segment", "")
951
+ think_buffer.write(segment)
952
+ think_state = 0
953
+ else:
954
+ segment = data.get("segment", "")
955
+ content_buffer.write(segment)
956
+ except Exception as e:
957
+ print(f"处理响应出错: {e}")
958
+
959
+ think_content = think_buffer.getvalue()
960
+ response_content = content_buffer.getvalue()
961
+
962
+ # 计算输出token并更新统计信息
963
+ completion_result, _ = num_tokens_from_string(think_content + response_content, model)
964
+
965
+ # 保存对话历史并获取计算点数
966
+ _, compute_points = save_conversation_history(session, cookies, session_token, conversation_id)
967
+
968
+ # 更新统计信息
969
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points)
970
+
971
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
972
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
973
+ delete_conversation(session, cookies, session_token, last_conversation_id)
974
+
975
+ return jsonify({
976
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
977
+ "object": "chat.completion",
978
+ "created": int(time.time()),
979
+ "model": model,
980
+ "choices": [{
981
+ "index": 0,
982
+ "message": {
983
+ "role": "assistant",
984
+ "content": f"<think>\n{think_content}\n</think>\n{response_content}"
985
+ },
986
+ "finish_reason": "stop"
987
+ }],
988
+ "usage": {
989
+ "prompt_tokens": prompt_tokens,
990
+ "completion_tokens": completion_result,
991
+ "total_tokens": prompt_tokens + completion_result
992
+ }
993
+ })
994
+ else:
995
+ for line in response.iter_lines():
996
+ if line:
997
+ decoded_line = line.decode("utf-8")
998
+ segment = extract_segment(decoded_line)
999
+ if segment:
1000
+ buffer.write(segment)
1001
+
1002
+ response_content = buffer.getvalue()
1003
+
1004
+ # 计算输出token并更新统计信息
1005
+ completion_result, _ = num_tokens_from_string(response_content, model)
1006
+
1007
+ # 保存对话历史并获取计算点数
1008
+ _, compute_points = save_conversation_history(session, cookies, session_token, conversation_id)
1009
+
1010
+ # 更新统计信息
1011
+ update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points)
1012
+
1013
+ # 如果需要删除上一个对话且上一个对话ID不为空且与当前不同
1014
+ if DELETE_CHAT and last_conversation_id and last_conversation_id != conversation_id:
1015
+ delete_conversation(session, cookies, session_token, last_conversation_id)
1016
+
1017
+ return jsonify({
1018
+ "id": f"chatcmpl-{str(uuid.uuid4())}",
1019
+ "object": "chat.completion",
1020
+ "created": int(time.time()),
1021
+ "model": model,
1022
+ "choices": [{
1023
+ "index": 0,
1024
+ "message": {
1025
+ "role": "assistant",
1026
+ "content": response_content
1027
+ },
1028
+ "finish_reason": "stop"
1029
+ }],
1030
+ "usage": {
1031
+ "prompt_tokens": prompt_tokens,
1032
+ "completion_tokens": completion_result,
1033
+ "total_tokens": prompt_tokens + completion_result
1034
+ }
1035
+ })
1036
+ except requests.exceptions.RequestException as e:
1037
+ error_details = str(e)
1038
+ if hasattr(e, 'response') and e.response is not None:
1039
+ if hasattr(e.response, 'text'):
1040
+ error_details += f" - Response: {e.response.text[:200]}"
1041
+ print(f"发送消息失败: {error_details}")
1042
+ return jsonify({"error": f"Failed to send message: {error_details}"}), 500
1043
+
1044
+
1045
+ def format_message(messages):
1046
+ buffer = io.StringIO()
1047
+ role_map, prefix, messages = extract_role(messages)
1048
+ for message in messages:
1049
+ role = message.get("role")
1050
+ role = "\b" + role_map[role] if prefix else role_map[role]
1051
+ content = message.get("content").replace("\\n", "\n")
1052
+ pattern = re.compile(r"<\|removeRole\|>\n")
1053
+ if pattern.match(content):
1054
+ content = pattern.sub("", content)
1055
+ buffer.write(f"{content}\n")
1056
+ else:
1057
+ buffer.write(f"{role}: {content}\n\n")
1058
+ formatted_message = buffer.getvalue()
1059
+ return formatted_message
1060
+
1061
+
1062
+ def extract_role(messages):
1063
+ role_map = {"user": "Human", "assistant": "Assistant", "system": "System"}
1064
+ prefix = False
1065
+ first_message = messages[0]["content"]
1066
+ pattern = re.compile(
1067
+ r"""
1068
+ <roleInfo>\s*
1069
+ user:\s*(?P<user>[^\n]*)\s*
1070
+ assistant:\s*(?P<assistant>[^\n]*)\s*
1071
+ system:\s*(?P<system>[^\n]*)\s*
1072
+ prefix:\s*(?P<prefix>[^\n]*)\s*
1073
+ </roleInfo>\n
1074
+ """,
1075
+ re.VERBOSE,
1076
+ )
1077
+ match = pattern.search(first_message)
1078
+ if match:
1079
+ role_map = {
1080
+ "user": match.group("user"),
1081
+ "assistant": match.group("assistant"),
1082
+ "system": match.group("system"),
1083
+ }
1084
+ prefix = match.group("prefix") == "1"
1085
+ messages[0]["content"] = pattern.sub("", first_message)
1086
+ print(f"Extracted role map:")
1087
+ print(
1088
+ f"User: {role_map['user']}, Assistant: {role_map['assistant']}, System: {role_map['system']}"
1089
+ )
1090
+ print(f"Using prefix: {prefix}")
1091
+ return (role_map, prefix, messages)
1092
+
1093
+
1094
+ @app.route("/health", methods=["GET"])
1095
+ def health_check():
1096
+ global health_check_counter
1097
+ health_check_counter += 1
1098
+ return jsonify({
1099
+ "status": "healthy",
1100
+ "timestamp": datetime.now().isoformat(),
1101
+ "checks": health_check_counter
1102
+ })
1103
+
1104
+
1105
+ def keep_alive():
1106
+ """每20分钟进行一次自我健康检查"""
1107
+ while True:
1108
+ try:
1109
+ requests.get("http://127.0.0.1:7860/health")
1110
+ time.sleep(1200) # 20分钟
1111
+ except:
1112
+ pass # 忽略错误,保持运行
1113
+
1114
+
1115
+ @app.route("/", methods=["GET"])
1116
+ def index():
1117
+ # 如果需要密码且用户未登录,重定向到登录页面
1118
+ if PASSWORD and not flask_session.get('logged_in'):
1119
+ return redirect(url_for('login'))
1120
+
1121
+ # 否则重定向到仪表盘
1122
+ return redirect(url_for('dashboard'))
1123
+
1124
+
1125
+ def num_tokens_from_string(string, model=""):
1126
+ try:
1127
+ request_data = {
1128
+ "model": model,
1129
+ "messages": [{"role": "user", "content": string}]
1130
+ }
1131
+
1132
+ response = requests.post(
1133
+ TOKENIZER_SERVICE_URL,
1134
+ json=request_data,
1135
+ timeout=10
1136
+ )
1137
+
1138
+ # 修改这里:200状态码表示精确计算,400表示估算
1139
+ if response.status_code == 200:
1140
+ result = response.json()
1141
+ input_tokens = result.get("input_tokens", 0)
1142
+ return input_tokens, "api" # 返回精确
1143
+ elif response.status_code == 400:
1144
+ result = response.json()
1145
+ if "input_tokens" in result:
1146
+ print(f"使用估算token值: {result.get('input_tokens')}")
1147
+ return result.get("input_tokens", 0), "estimate" # 返回估算
1148
+ return len(string) // 4, "estimate" # 返回估算
1149
+ else:
1150
+ print(f"Tokenizer服务错误: {response.status_code} - {response.text}")
1151
+ return len(string) // 4, "estimate" # 返回估算
1152
+ except Exception as e:
1153
+ print(f"计算token错误: {e}")
1154
+ return len(string) // 4, "estimate" # 返回估算
1155
+
1156
+
1157
+ # 更新模型使用统计
1158
+ def update_model_stats(model, prompt_tokens, completion_tokens, calculation_method="estimate", compute_points=None):
1159
+ global model_usage_stats, total_tokens, model_usage_records
1160
+
1161
+ # 添加调用记录
1162
+ # 获取UTC时间
1163
+ utc_now = datetime.utcnow()
1164
+ # 转换为北京时间 (UTC+8)
1165
+ beijing_time = utc_now + timedelta(hours=8)
1166
+ call_time = beijing_time.strftime('%Y-%m-%d %H:%M:%S') # 北京时间
1167
+
1168
+ record = {
1169
+ "model": model,
1170
+ "call_time": call_time,
1171
+ "prompt_tokens": prompt_tokens,
1172
+ "completion_tokens": completion_tokens,
1173
+ "calculation_method": "精确" if calculation_method == "api" else "估算",
1174
+ "compute_points": compute_points
1175
+ }
1176
+ model_usage_records.append(record)
1177
+
1178
+ # 限制记录数量,保留最新的500条
1179
+ if len(model_usage_records) > 500:
1180
+ model_usage_records.pop(0)
1181
+
1182
+ # 保存调用记录到本地文件
1183
+ save_model_usage_records()
1184
+
1185
+ # 更新聚合统计
1186
+ if model not in model_usage_stats:
1187
+ model_usage_stats[model] = {
1188
+ "count": 0,
1189
+ "prompt_tokens": 0,
1190
+ "completion_tokens": 0,
1191
+ "total_tokens": 0
1192
+ }
1193
+
1194
+ model_usage_stats[model]["count"] += 1
1195
+ model_usage_stats[model]["prompt_tokens"] += prompt_tokens
1196
+ model_usage_stats[model]["completion_tokens"] += completion_tokens
1197
+ model_usage_stats[model]["total_tokens"] += (prompt_tokens + completion_tokens)
1198
+
1199
+ total_tokens["prompt"] += prompt_tokens
1200
+ total_tokens["completion"] += completion_tokens
1201
+ total_tokens["total"] += (prompt_tokens + completion_tokens)
1202
+
1203
+
1204
+ # 获取计算点信息
1205
+ def get_compute_points():
1206
+ global compute_points, USER_DATA, users_compute_points
1207
+
1208
+ if USER_NUM == 0:
1209
+ return
1210
+
1211
+ # 清空用户计算点列表
1212
+ users_compute_points = []
1213
+
1214
+ # 累计总计算点
1215
+ total_left = 0
1216
+ total_points = 0
1217
+
1218
+ # 获取每个用户的计算点信息
1219
+ for i, user_data in enumerate(USER_DATA):
1220
+ try:
1221
+ session, cookies, session_token, _, _, _ = user_data
1222
+
1223
+ # 检查token是否有效
1224
+ if is_token_expired(session_token):
1225
+ session_token = refresh_token(session, cookies)
1226
+ if not session_token:
1227
+ print(f"用户{i+1}刷新token失败,无法获取计算点信息")
1228
+ continue
1229
+ USER_DATA[i] = (session, cookies, session_token, user_data[3], user_data[4], i)
1230
+
1231
+ headers = {
1232
+ "accept": "application/json, text/plain, */*",
1233
+ "accept-language": "zh-CN,zh;q=0.9",
1234
+ "baggage": f"sentry-environment=production,sentry-release=93da8385541a6ce339b1f41b0c94428c70657e22,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
1235
+ "reai-ui": "1",
1236
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1237
+ "sec-ch-ua-mobile": "?0",
1238
+ "sec-ch-ua-platform": "\"Windows\"",
1239
+ "sec-fetch-dest": "empty",
1240
+ "sec-fetch-mode": "cors",
1241
+ "sec-fetch-site": "same-origin",
1242
+ "sentry-trace": SENTRY_TRACE,
1243
+ "session-token": session_token,
1244
+ "x-abacus-org-host": "apps",
1245
+ "cookie": cookies
1246
+ }
1247
+
1248
+ response = session.get(
1249
+ COMPUTE_POINTS_URL,
1250
+ headers=headers
1251
+ )
1252
+
1253
+ if response.status_code == 200:
1254
+ result = response.json()
1255
+ if result.get("success") and "result" in result:
1256
+ data = result["result"]
1257
+ left = data.get("computePointsLeft", 0)
1258
+ total = data.get("totalComputePoints", 0)
1259
+ used = total - left
1260
+ percentage = round((used / total) * 100, 2) if total > 0 else 0
1261
+
1262
+ # 获取北京时间
1263
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
1264
+
1265
+ # 添加到用户列表
1266
+ user_points = {
1267
+ "user_id": i + 1, # 用户ID从1开始
1268
+ "left": left,
1269
+ "total": total,
1270
+ "used": used,
1271
+ "percentage": percentage,
1272
+ "last_update": beijing_now
1273
+ }
1274
+ users_compute_points.append(user_points)
1275
+
1276
+ # 累计总数
1277
+ total_left += left
1278
+ total_points += total
1279
+
1280
+ print(f"用户{i+1}计算点信息更新成功: 剩余 {left}, 总计 {total}")
1281
+
1282
+ # 对于第一个用户,获取计算点使用日志
1283
+ if i == 0:
1284
+ get_compute_points_log(session, cookies, session_token)
1285
+ else:
1286
+ print(f"获取用户{i+1}计算点信息失败: {result.get('error', '未知错误')}")
1287
+ else:
1288
+ print(f"获取用户{i+1}计算点信息失败,状态码: {response.status_code}")
1289
+ except Exception as e:
1290
+ print(f"获取用户{i+1}计算点信息异常: {e}")
1291
+
1292
+ # 更新全局计算点信息(所有用户总和)
1293
+ if users_compute_points:
1294
+ compute_points["left"] = total_left
1295
+ compute_points["total"] = total_points
1296
+ compute_points["used"] = total_points - total_left
1297
+ compute_points["percentage"] = round((compute_points["used"] / compute_points["total"]) * 100, 2) if compute_points["total"] > 0 else 0
1298
+ compute_points["last_update"] = datetime.utcnow() + timedelta(hours=8) # 北京时间
1299
+ print(f"所有用户计算点总计: 剩余 {total_left}, 总计 {total_points}")
1300
+
1301
+ # 获取计算点使用日志
1302
+ def get_compute_points_log(session, cookies, session_token):
1303
+ global compute_points_log
1304
+
1305
+ try:
1306
+ headers = {
1307
+ "accept": "application/json, text/plain, */*",
1308
+ "accept-language": "zh-CN,zh;q=0.9",
1309
+ "content-type": "application/json",
1310
+ "reai-ui": "1",
1311
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1312
+ "sec-ch-ua-mobile": "?0",
1313
+ "sec-ch-ua-platform": "\"Windows\"",
1314
+ "sec-fetch-dest": "empty",
1315
+ "sec-fetch-mode": "cors",
1316
+ "sec-fetch-site": "same-site",
1317
+ "session-token": session_token,
1318
+ "x-abacus-org-host": "apps",
1319
+ "cookie": cookies
1320
+ }
1321
+
1322
+ response = session.post(
1323
+ COMPUTE_POINTS_LOG_URL,
1324
+ headers=headers,
1325
+ json={"byLlm": True}
1326
+ )
1327
+
1328
+ if response.status_code == 200:
1329
+ result = response.json()
1330
+ if result.get("success") and "result" in result:
1331
+ data = result["result"]
1332
+ compute_points_log["columns"] = data.get("columns", {})
1333
+ compute_points_log["log"] = data.get("log", [])
1334
+ print(f"计算点使用日志更新成功,获取到 {len(compute_points_log['log'])} 条记录")
1335
+ else:
1336
+ print(f"获取计算点使用日志失败: {result.get('error', '未知错误')}")
1337
+ else:
1338
+ print(f"获取计算点使用日志失败,状态码: {response.status_code}")
1339
+ except Exception as e:
1340
+ print(f"获取计算点使用日志异常: {e}")
1341
+
1342
+
1343
+ # 添加登录相关路由
1344
+ @app.route("/login", methods=["GET", "POST"])
1345
+ def login():
1346
+ error = None
1347
+ if request.method == "POST":
1348
+ password = request.form.get("password")
1349
+ if password and hashlib.sha256(password.encode()).hexdigest() == PASSWORD:
1350
+ flask_session['logged_in'] = True
1351
+ flask_session.permanent = True
1352
+ return redirect(url_for('dashboard'))
1353
+ else:
1354
+ # 密码错误时提示使用环境变量密码
1355
+ error = "密码不正确。请使用设置的环境变量 password 或 password.txt 中的值作为密码和API认证密钥。"
1356
+
1357
+ # 传递空间URL给模板
1358
+ return render_template('login.html', error=error, space_url=SPACE_URL)
1359
+
1360
+
1361
+ @app.route("/logout")
1362
+ def logout():
1363
+ flask_session.clear()
1364
+ return redirect(url_for('login'))
1365
+
1366
+
1367
+ @app.route("/dashboard")
1368
+ @require_auth
1369
+ def dashboard():
1370
+ # 在每次访问仪表盘时更新计算点信息
1371
+ get_compute_points()
1372
+
1373
+ # 计算运行时间(使用北京时间)
1374
+ beijing_now = datetime.utcnow() + timedelta(hours=8)
1375
+ uptime = beijing_now - START_TIME
1376
+ days = uptime.days
1377
+ hours, remainder = divmod(uptime.seconds, 3600)
1378
+ minutes, seconds = divmod(remainder, 60)
1379
+
1380
+ if days > 0:
1381
+ uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
1382
+ elif hours > 0:
1383
+ uptime_str = f"{hours}小时 {minutes}分钟"
1384
+ else:
1385
+ uptime_str = f"{minutes}分钟 {seconds}秒"
1386
+
1387
+ # 当前北京年份
1388
+ beijing_year = beijing_now.year
1389
+
1390
+ return render_template(
1391
+ 'dashboard.html',
1392
+ uptime=uptime_str,
1393
+ health_checks=health_check_counter,
1394
+ user_count=USER_NUM,
1395
+ models=sorted(list(MODELS)),
1396
+ year=beijing_year,
1397
+ model_stats=model_usage_stats,
1398
+ total_tokens=total_tokens,
1399
+ compute_points=compute_points,
1400
+ compute_points_log=compute_points_log,
1401
+ space_url=SPACE_URL, # 传递空间URL
1402
+ users_compute_points=users_compute_points, # 传递用户计算点信息
1403
+ model_usage_records=model_usage_records, # 传递模型使用记录
1404
+ delete_chat=DELETE_CHAT # 传递删除对话设置
1405
+ )
1406
+
1407
+
1408
+ # 添加更新删除对话设置的路由
1409
+ @app.route("/update_delete_chat_setting", methods=["POST"])
1410
+ @require_auth
1411
+ def update_delete_chat_setting():
1412
+ try:
1413
+ data = request.get_json()
1414
+ if data and "delete_chat" in data:
1415
+ global DELETE_CHAT
1416
+ DELETE_CHAT = bool(data["delete_chat"])
1417
+
1418
+ # 将设置保存到环境变量中,以便重启后保留设置
1419
+ os.environ["DELETE_CHAT"] = "true" if DELETE_CHAT else "false"
1420
+
1421
+ print(f"更新删除对话设置为: {DELETE_CHAT}")
1422
+ return jsonify({"success": True})
1423
+ else:
1424
+ return jsonify({"success": False, "error": "缺少delete_chat参数"})
1425
+ except Exception as e:
1426
+ print(f"更新删除对话设置失败: {e}")
1427
+ return jsonify({"success": False, "error": str(e)})
1428
+
1429
+
1430
+ # 添加更新计算点数记录设置的路由
1431
+ @app.route("/update_compute_point_toggle", methods=["POST"])
1432
+ @require_auth
1433
+ def update_compute_point_toggle():
1434
+ try:
1435
+ (session, cookies, session_token, conversation_id, model_map, user_index) = get_user_data()
1436
+ data = request.get_json()
1437
+ if data and "always_display" in data:
1438
+ headers = {
1439
+ "accept": "application/json, text/plain, */*",
1440
+ "accept-language": "zh-CN,zh;q=0.9",
1441
+ "content-type": "application/json",
1442
+ "reai-ui": "1",
1443
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1444
+ "sec-ch-ua-mobile": "?0",
1445
+ "sec-ch-ua-platform": "\"Windows\"",
1446
+ "sec-fetch-dest": "empty",
1447
+ "sec-fetch-mode": "cors",
1448
+ "sec-fetch-site": "same-site",
1449
+ "x-abacus-org-host": "apps"
1450
+ }
1451
+
1452
+ if session_token:
1453
+ headers["session-token"] = session_token
1454
+
1455
+ response = session.post(
1456
+ COMPUTE_POINT_TOGGLE_URL,
1457
+ headers=headers,
1458
+ json={"alwaysDisplay": data["always_display"]},
1459
+ cookies=None
1460
+ )
1461
+
1462
+ if response.status_code == 200:
1463
+ result = response.json()
1464
+ if result.get("success"):
1465
+ print(f"更新计算点数记录设置为: {data['always_display']}")
1466
+ return jsonify({"success": True})
1467
+
1468
+ return jsonify({"success": False, "error": "API调用失败"})
1469
+ else:
1470
+ return jsonify({"success": False, "error": "缺少always_display参数"})
1471
+ except Exception as e:
1472
+ print(f"更新计算点数记录设置失败: {e}")
1473
+ return jsonify({"success": False, "error": str(e)})
1474
+
1475
+
1476
+ # 获取Hugging Face Space URL
1477
+ def get_space_url():
1478
+ # 尝试从环境变量获取
1479
+ space_url = os.environ.get("SPACE_URL")
1480
+ if space_url:
1481
+ return space_url
1482
+
1483
+ # 如果SPACE_URL不存在,尝试从SPACE_ID构建
1484
+ space_id = os.environ.get("SPACE_ID")
1485
+ if space_id:
1486
+ username, space_name = space_id.split("/")
1487
+ # 将空间名称中的下划线替换为连字符
1488
+ # 注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)
1489
+ # 例如:"abacus_chat_proxy" 会变成 "abacus-chat-proxy"
1490
+ space_name = space_name.replace("_", "-")
1491
+ return f"https://{username}-{space_name}.hf.space"
1492
+
1493
+ # 如果以上都不存在,尝试从单独的用户名和空间名构建
1494
+ username = os.environ.get("SPACE_USERNAME")
1495
+ space_name = os.environ.get("SPACE_NAME")
1496
+ if username and space_name:
1497
+ # 将空间名称中的下划线替换为连字符
1498
+ # 同上,Hugging Face会自动进行此转换
1499
+ space_name = space_name.replace("_", "-")
1500
+ return f"https://{username}-{space_name}.hf.space"
1501
+
1502
+ # 默认返回None
1503
+ return None
1504
+
1505
+ # 获取空间URL
1506
+ SPACE_URL = get_space_url()
1507
+ if SPACE_URL:
1508
+ print(f"Space URL: {SPACE_URL}")
1509
+ print("注意:Hugging Face生成的URL会自动将空间名称中的下划线(_)替换为连字符(-)")
1510
+
1511
+
1512
+ def save_conversation_history(session, cookies, session_token, conversation_id, deployment_id="14b2a314cc"):
1513
+ """保存对话历史,返回使用的计算点数"""
1514
+ if not conversation_id:
1515
+ return False, None
1516
+
1517
+ headers = {
1518
+ "accept": "application/json, text/plain, */*",
1519
+ "accept-language": "zh-CN,zh;q=0.9",
1520
+ "baggage": f"sentry-environment=production,sentry-release=946244517de08b08598b94f18098411f5a5352d5,sentry-public_key=3476ea6df1585dd10e92cdae3a66ff49,sentry-trace_id={TRACE_ID}",
1521
+ "reai-ui": "1",
1522
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
1523
+ "sec-ch-ua-mobile": "?0",
1524
+ "sec-ch-ua-platform": "\"Windows\"",
1525
+ "sec-fetch-dest": "empty",
1526
+ "sec-fetch-mode": "cors",
1527
+ "sec-fetch-site": "same-origin",
1528
+ "sentry-trace": f"{TRACE_ID}-800cb7f4613dec52",
1529
+ "x-abacus-org-host": "apps"
1530
+ }
1531
+
1532
+ if session_token:
1533
+ headers["session-token"] = session_token
1534
+
1535
+ params = {
1536
+ "deploymentId": deployment_id,
1537
+ "deploymentConversationId": conversation_id,
1538
+ "skipDocumentBoundingBoxes": "true",
1539
+ "filterIntermediateConversationEvents": "false",
1540
+ "getUnusedDocumentUploads": "false"
1541
+ }
1542
+
1543
+ try:
1544
+ response = session.get(
1545
+ GET_CONVERSATION_URL,
1546
+ headers=headers,
1547
+ params=params,
1548
+ cookies=None
1549
+ )
1550
+
1551
+ if response.status_code == 200:
1552
+ data = response.json()
1553
+ if data.get("success"):
1554
+ # 从最后一条BOT消息中获取计算点数
1555
+ history = data.get("result", {}).get("history", [])
1556
+ compute_points = None
1557
+ for msg in reversed(history):
1558
+ if msg.get("role") == "BOT":
1559
+ compute_points = msg.get("computePointsUsed")
1560
+ break
1561
+ print(f"成功保存对话历史: {conversation_id}, 使用计算点: {compute_points}")
1562
+ return True, compute_points
1563
+ else:
1564
+ print(f"保存对话历史失败: {data.get('error', '未知错误')}")
1565
+ else:
1566
+ print(f"保存对话历史失败,状态码: {response.status_code}")
1567
+ return False, None
1568
+ except Exception as e:
1569
+ print(f"保存对话历史时出错: {e}")
1570
+ return False, None
1571
+
1572
+
1573
+ if __name__ == "__main__":
1574
+ # 启动保活线程
1575
+ threading.Thread(target=keep_alive, daemon=True).start()
1576
+
1577
+ # 加载历史模型调用记录
1578
+ load_model_usage_records()
1579
+
1580
+ # 获取初始计算点信息
1581
+ get_compute_points()
1582
+
1583
+ port = int(os.environ.get("PORT", 9876))
1584
+ app.run(port=port, host="0.0.0.0")
templates/dashboard.html ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 仪表盘</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6f42c1;
10
+ --secondary-color: #4a32a8;
11
+ --accent-color: #5e85f1;
12
+ --bg-color: #0a0a1a;
13
+ --text-color: #e6e6ff;
14
+ --card-bg: rgba(30, 30, 60, 0.7);
15
+ --input-bg: rgba(40, 40, 80, 0.6);
16
+ --success-color: #36d399;
17
+ --warning-color: #fbbd23;
18
+ --error-color: #f87272;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ body {
29
+ min-height: 100vh;
30
+ background-color: var(--bg-color);
31
+ background-image:
32
+ radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
33
+ radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
34
+ color: var(--text-color);
35
+ position: relative;
36
+ overflow-x: hidden;
37
+ }
38
+
39
+ /* 动态背景网格 */
40
+ .grid-background {
41
+ position: fixed;
42
+ top: 0;
43
+ left: 0;
44
+ width: 100%;
45
+ height: 100%;
46
+ background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
47
+ linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
48
+ background-size: 30px 30px;
49
+ z-index: -1;
50
+ animation: grid-move 20s linear infinite;
51
+ }
52
+
53
+ @keyframes grid-move {
54
+ 0% {
55
+ transform: translateY(0);
56
+ }
57
+ 100% {
58
+ transform: translateY(30px);
59
+ }
60
+ }
61
+
62
+ /* 顶部导航栏 */
63
+ .navbar {
64
+ padding: 1rem 2rem;
65
+ background: rgba(15, 15, 30, 0.8);
66
+ backdrop-filter: blur(10px);
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: 100;
74
+ }
75
+
76
+ .navbar-brand {
77
+ display: flex;
78
+ align-items: center;
79
+ text-decoration: none;
80
+ color: var(--text-color);
81
+ }
82
+
83
+ .navbar-logo {
84
+ font-size: 1.5rem;
85
+ margin-right: 0.75rem;
86
+ animation: pulse 2s infinite alternate;
87
+ }
88
+
89
+ @keyframes pulse {
90
+ 0% {
91
+ transform: scale(1);
92
+ text-shadow: 0 0 5px rgba(111, 66, 193, 0.5);
93
+ }
94
+ 100% {
95
+ transform: scale(1.05);
96
+ text-shadow: 0 0 15px rgba(111, 66, 193, 0.8);
97
+ }
98
+ }
99
+
100
+ .navbar-title {
101
+ font-size: 1.25rem;
102
+ font-weight: 600;
103
+ background: linear-gradient(45deg, #6f42c1, #5181f1);
104
+ -webkit-background-clip: text;
105
+ -webkit-text-fill-color: transparent;
106
+ }
107
+
108
+ .navbar-actions {
109
+ display: flex;
110
+ gap: 1rem;
111
+ }
112
+
113
+ .btn-logout {
114
+ background: rgba(255, 255, 255, 0.1);
115
+ color: var(--text-color);
116
+ border: none;
117
+ padding: 0.5rem 1rem;
118
+ border-radius: 6px;
119
+ cursor: pointer;
120
+ transition: all 0.2s;
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .btn-logout:hover {
127
+ background: rgba(255, 255, 255, 0.2);
128
+ }
129
+
130
+ /* 主内容区域 */
131
+ .container {
132
+ max-width: 1200px;
133
+ margin: 0 auto;
134
+ padding: 2rem;
135
+ }
136
+
137
+ /* 信息卡片样式 */
138
+ .card {
139
+ background: var(--card-bg);
140
+ border-radius: 12px;
141
+ padding: 1.5rem;
142
+ margin-bottom: 2rem;
143
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
144
+ backdrop-filter: blur(8px);
145
+ border: 1px solid rgba(255, 255, 255, 0.1);
146
+ animation: card-fade-in 0.6s ease-out;
147
+ }
148
+
149
+ @keyframes card-fade-in {
150
+ from {
151
+ opacity: 0;
152
+ transform: translateY(20px);
153
+ }
154
+ to {
155
+ opacity: 1;
156
+ transform: translateY(0);
157
+ }
158
+ }
159
+
160
+ .card-header {
161
+ margin-bottom: 1rem;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: space-between;
165
+ }
166
+
167
+ .card-title {
168
+ font-size: 1.25rem;
169
+ font-weight: 600;
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 0.75rem;
173
+ }
174
+
175
+ .card-icon {
176
+ width: 32px;
177
+ height: 32px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ background: linear-gradient(45deg, rgba(111, 66, 193, 0.2), rgba(94, 133, 241, 0.2));
182
+ border-radius: 8px;
183
+ font-size: 1.25rem;
184
+ }
185
+
186
+ /* 状态项样式 */
187
+ .status-item {
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
191
+ padding: 0.75rem 0;
192
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
193
+ }
194
+
195
+ .status-item:last-child {
196
+ border-bottom: none;
197
+ }
198
+
199
+ .status-label {
200
+ color: rgba(230, 230, 255, 0.7);
201
+ font-weight: 500;
202
+ }
203
+
204
+ .status-value {
205
+ color: var(--text-color);
206
+ font-weight: 600;
207
+ }
208
+
209
+ .status-value.success {
210
+ color: var(--success-color);
211
+ }
212
+
213
+ .status-value.warning {
214
+ color: var(--warning-color);
215
+ }
216
+
217
+ .status-value.danger {
218
+ color: var(--error-color);
219
+ }
220
+
221
+ /* 模型标签 */
222
+ .models-list {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0.5rem;
226
+ }
227
+
228
+ .model-tag {
229
+ background: rgba(111, 66, 193, 0.2);
230
+ padding: 0.25rem 0.75rem;
231
+ border-radius: 16px;
232
+ font-size: 0.875rem;
233
+ color: var(--text-color);
234
+ border: 1px solid rgba(111, 66, 193, 0.3);
235
+ }
236
+
237
+ /* 表格样式 */
238
+ .table-container {
239
+ overflow-x: auto;
240
+ margin-top: 1rem;
241
+ }
242
+
243
+ .data-table {
244
+ width: 100%;
245
+ border-collapse: collapse;
246
+ text-align: left;
247
+ }
248
+
249
+ .data-table th {
250
+ background-color: rgba(50, 50, 100, 0.3);
251
+ padding: 0.75rem 1rem;
252
+ font-weight: 600;
253
+ color: rgba(230, 230, 255, 0.9);
254
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
255
+ }
256
+
257
+ .data-table td {
258
+ padding: 0.75rem 1rem;
259
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
260
+ }
261
+
262
+ .data-table tbody tr {
263
+ transition: background-color 0.2s;
264
+ }
265
+
266
+ .data-table tbody tr:hover {
267
+ background-color: rgba(50, 50, 100, 0.2);
268
+ }
269
+
270
+ /* 特殊值样式 */
271
+ .token-count {
272
+ font-family: 'Consolas', monospace;
273
+ color: var(--accent-color);
274
+ font-weight: bold;
275
+ }
276
+
277
+ .call-count {
278
+ font-family: 'Consolas', monospace;
279
+ color: var(--success-color);
280
+ font-weight: bold;
281
+ }
282
+
283
+ .compute-points {
284
+ font-family: 'Consolas', monospace;
285
+ color: var(--primary-color);
286
+ font-weight: bold;
287
+ }
288
+
289
+ /* 进度条 */
290
+ .progress-container {
291
+ width: 100%;
292
+ height: 8px;
293
+ background-color: rgba(100, 100, 150, 0.2);
294
+ border-radius: 4px;
295
+ margin-top: 0.5rem;
296
+ overflow: hidden;
297
+ position: relative;
298
+ }
299
+
300
+ .progress-bar {
301
+ height: 100%;
302
+ border-radius: 4px;
303
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
304
+ position: relative;
305
+ overflow: hidden;
306
+ }
307
+
308
+ .progress-bar.warning {
309
+ background: linear-gradient(90deg, #fbbd23, #f59e0b);
310
+ }
311
+
312
+ .progress-bar.danger {
313
+ background: linear-gradient(90deg, #f87272, #ef4444);
314
+ }
315
+
316
+ /* 添加进度条闪光效果 */
317
+ .progress-bar::after {
318
+ content: '';
319
+ position: absolute;
320
+ top: 0;
321
+ left: -100%;
322
+ width: 100%;
323
+ height: 100%;
324
+ background: linear-gradient(90deg,
325
+ transparent,
326
+ rgba(255, 255, 255, 0.2),
327
+ transparent);
328
+ animation: progress-shine 3s infinite;
329
+ }
330
+
331
+ @keyframes progress-shine {
332
+ 0% {
333
+ left: -100%;
334
+ }
335
+ 50%, 100% {
336
+ left: 100%;
337
+ }
338
+ }
339
+
340
+ /* API端点卡片 */
341
+ .endpoint-item {
342
+ background: rgba(50, 50, 100, 0.2);
343
+ padding: 1rem;
344
+ border-radius: 8px;
345
+ margin-bottom: 1rem;
346
+ border-left: 3px solid var(--primary-color);
347
+ }
348
+
349
+ .endpoint-url {
350
+ font-family: 'Consolas', monospace;
351
+ background: rgba(0, 0, 0, 0.2);
352
+ padding: 0.5rem;
353
+ border-radius: 4px;
354
+ margin-top: 0.25rem;
355
+ display: inline-block;
356
+ color: var(--text-color);
357
+ text-decoration: none;
358
+ transition: all 0.2s;
359
+ }
360
+
361
+ .endpoint-url:hover {
362
+ background: rgba(111, 66, 193, 0.3);
363
+ color: var(--text-color);
364
+ }
365
+
366
+ /* 响应式布局 */
367
+ .grid {
368
+ display: grid;
369
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
370
+ gap: 1.5rem;
371
+ }
372
+
373
+ /* 页脚 */
374
+ .footer {
375
+ text-align: center;
376
+ padding: 2rem 0;
377
+ color: rgba(230, 230, 255, 0.5);
378
+ font-size: 0.9rem;
379
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
380
+ margin-top: 2rem;
381
+ }
382
+
383
+ /* 悬浮图标按钮 */
384
+ .float-btn {
385
+ position: fixed;
386
+ bottom: 2rem;
387
+ right: 2rem;
388
+ width: 50px;
389
+ height: 50px;
390
+ border-radius: 50%;
391
+ background: linear-gradient(45deg, var(--primary-color), var(--accent-color));
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ color: white;
396
+ font-size: 1.5rem;
397
+ box-shadow: 0 4px 20px rgba(111, 66, 193, 0.4);
398
+ cursor: pointer;
399
+ transition: all 0.3s;
400
+ z-index: 50;
401
+ }
402
+
403
+ .float-btn:hover {
404
+ transform: translateY(-5px);
405
+ box-shadow: 0 8px 25px rgba(111, 66, 193, 0.5);
406
+ }
407
+
408
+ /* 滚动条美化 */
409
+ ::-webkit-scrollbar {
410
+ width: 8px;
411
+ height: 8px;
412
+ }
413
+
414
+ ::-webkit-scrollbar-track {
415
+ background: rgba(50, 50, 100, 0.1);
416
+ }
417
+
418
+ ::-webkit-scrollbar-thumb {
419
+ background: rgba(111, 66, 193, 0.5);
420
+ border-radius: 4px;
421
+ }
422
+
423
+ ::-webkit-scrollbar-thumb:hover {
424
+ background: rgba(111, 66, 193, 0.7);
425
+ }
426
+
427
+ /* 模型统计折叠样式 */
428
+ .hidden-model {
429
+ display: none;
430
+ }
431
+
432
+ .btn-toggle {
433
+ background: rgba(111, 66, 193, 0.2);
434
+ border: 1px solid rgba(111, 66, 193, 0.3);
435
+ border-radius: 4px;
436
+ padding: 0.3rem 0.7rem;
437
+ color: rgba(230, 230, 255, 0.9);
438
+ cursor: pointer;
439
+ transition: all 0.2s;
440
+ font-size: 0.85rem;
441
+ margin-left: auto;
442
+ }
443
+
444
+ .btn-toggle:hover {
445
+ background: rgba(111, 66, 193, 0.4);
446
+ }
447
+
448
+ /* Token注释样式 */
449
+ .token-note {
450
+ margin-top: 0.75rem;
451
+ color: rgba(230, 230, 255, 0.6);
452
+ font-style: italic;
453
+ line-height: 1.4;
454
+ padding: 0.5rem;
455
+ border-top: 1px dashed rgba(255, 255, 255, 0.1);
456
+ }
457
+
458
+ .token-model-table {
459
+ margin-top: 1rem;
460
+ }
461
+
462
+ /* Token计算方式标签 */
463
+ .token-method {
464
+ padding: 2px 6px;
465
+ border-radius: 4px;
466
+ font-size: 12px;
467
+ }
468
+
469
+ .token-method.api {
470
+ background-color: #e6f7ff;
471
+ color: #1890ff;
472
+ }
473
+
474
+ .token-method.estimate {
475
+ background-color: #fff7e6;
476
+ color: #fa8c16;
477
+ }
478
+
479
+ /* 时间日期样式 */
480
+ .datetime {
481
+ font-family: 'Consolas', monospace;
482
+ color: rgba(230, 230, 255, 0.8);
483
+ font-size: 0.9rem;
484
+ }
485
+
486
+ /* 媒体查询 */
487
+ @media (max-width: 768px) {
488
+ .container {
489
+ padding: 1rem;
490
+ }
491
+
492
+ .navbar {
493
+ padding: 1rem;
494
+ }
495
+
496
+ .card {
497
+ padding: 1rem;
498
+ }
499
+
500
+ .grid {
501
+ grid-template-columns: 1fr;
502
+ }
503
+ }
504
+
505
+ .token-model-table td, .token-model-table th {
506
+ white-space: nowrap;
507
+ }
508
+
509
+ /* 开关按钮样式 */
510
+ .toggle-switch-container {
511
+ display: flex;
512
+ align-items: center;
513
+ gap: 10px;
514
+ }
515
+
516
+ .toggle-switch {
517
+ position: relative;
518
+ display: inline-block;
519
+ width: 50px;
520
+ height: 24px;
521
+ }
522
+
523
+ .toggle-switch input {
524
+ opacity: 0;
525
+ width: 0;
526
+ height: 0;
527
+ }
528
+
529
+ .toggle-slider {
530
+ position: absolute;
531
+ cursor: pointer;
532
+ top: 0;
533
+ left: 0;
534
+ right: 0;
535
+ bottom: 0;
536
+ background-color: rgba(100, 100, 150, 0.3);
537
+ transition: .4s;
538
+ border-radius: 24px;
539
+ }
540
+
541
+ .toggle-slider:before {
542
+ position: absolute;
543
+ content: "";
544
+ height: 18px;
545
+ width: 18px;
546
+ left: 3px;
547
+ bottom: 3px;
548
+ background-color: #e6e6ff;
549
+ transition: .4s;
550
+ border-radius: 50%;
551
+ }
552
+
553
+ input:checked + .toggle-slider {
554
+ background-color: var(--primary-color);
555
+ }
556
+
557
+ input:checked + .toggle-slider:before {
558
+ transform: translateX(26px);
559
+ }
560
+
561
+ .toggle-status {
562
+ font-weight: 600;
563
+ }
564
+
565
+ .info-text {
566
+ font-size: 0.85rem;
567
+ color: rgba(230, 230, 255, 0.7);
568
+ }
569
+
570
+ /* 通知样式 */
571
+ .notification {
572
+ position: fixed;
573
+ top: 20px;
574
+ right: 20px;
575
+ padding: 12px 20px;
576
+ border-radius: 8px;
577
+ color: white;
578
+ font-weight: 500;
579
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
580
+ z-index: 1000;
581
+ transform: translateY(-20px);
582
+ opacity: 0;
583
+ transition: all 0.3s ease;
584
+ max-width: 300px;
585
+ }
586
+
587
+ .notification.show {
588
+ transform: translateY(0);
589
+ opacity: 1;
590
+ }
591
+
592
+ .notification.success {
593
+ background-color: var(--success-color);
594
+ }
595
+
596
+ .notification.error {
597
+ background-color: var(--error-color);
598
+ }
599
+
600
+ .notification.info {
601
+ background-color: var(--accent-color);
602
+ }
603
+
604
+ /* 响应式样式 */
605
+ @media (max-width: 768px) {
606
+ .container {
607
+ padding: 1rem;
608
+ }
609
+
610
+ .navbar {
611
+ padding: 1rem;
612
+ }
613
+
614
+ .card {
615
+ padding: 1rem;
616
+ }
617
+
618
+ .grid {
619
+ grid-template-columns: 1fr;
620
+ }
621
+ }
622
+ </style>
623
+ </head>
624
+ <body>
625
+ <div class="grid-background"></div>
626
+
627
+ <nav class="navbar">
628
+ <a href="/" class="navbar-brand">
629
+ <span class="navbar-logo">🤖</span>
630
+ <span class="navbar-title">Abacus Chat Proxy</span>
631
+ </a>
632
+ <div class="navbar-actions">
633
+ <a href="/logout" class="btn-logout">
634
+ <span>退出</span>
635
+ <span>↗</span>
636
+ </a>
637
+ </div>
638
+ </nav>
639
+
640
+ <div class="container">
641
+ <div class="card">
642
+ <div class="card-header">
643
+ <h2 class="card-title">
644
+ <span class="card-icon">📊</span>
645
+ 系统状态
646
+ </h2>
647
+ </div>
648
+ <div class="status-item">
649
+ <span class="status-label">服务状态</span>
650
+ <span class="status-value success">运行中</span>
651
+ </div>
652
+ <div class="status-item">
653
+ <span class="status-label">运行时间</span>
654
+ <span class="status-value">{{ uptime }}</span>
655
+ </div>
656
+ <div class="status-item">
657
+ <span class="status-label">健康检查次数</span>
658
+ <span class="status-value">{{ health_checks }}</span>
659
+ </div>
660
+ <div class="status-item">
661
+ <span class="status-label">已配置用户数</span>
662
+ <span class="status-value">{{ user_count }}</span>
663
+ </div>
664
+ <div class="status-item">
665
+ <span class="status-label">可用模型</span>
666
+ <div class="models-list">
667
+ {% for model in models %}
668
+ <span class="model-tag">{{ model }}</span>
669
+ {% endfor %}
670
+ </div>
671
+ </div>
672
+ </div>
673
+
674
+ <div class="card">
675
+ <div class="card-header">
676
+ <h2 class="card-title">
677
+ <span class="card-icon">🗑️</span>
678
+ 对话管理设置
679
+ </h2>
680
+ </div>
681
+ <div class="status-item">
682
+ <span class="status-label">是否自动删除上一个对话</span>
683
+ <div class="toggle-switch-container">
684
+ <label class="toggle-switch">
685
+ <input type="checkbox" id="delete-chat-toggle" {% if delete_chat %}checked{% endif %}>
686
+ <span class="toggle-slider"></span>
687
+ </label>
688
+ <span class="toggle-status" id="delete-chat-status">{{ "开启" if delete_chat else "关闭" }}</span>
689
+ </div>
690
+ </div>
691
+ <div class="status-item">
692
+ <span class="status-label">设置说明</span>
693
+ <span class="status-value info-text">开启后,系统将在每次对话完成后自��删除上一次对话,只保留最新对话</span>
694
+ </div>
695
+ <div class="status-item">
696
+ <span class="status-label">是否记录计算点数</span>
697
+ <div class="toggle-switch-container">
698
+ <label class="toggle-switch">
699
+ <input type="checkbox" id="compute-point-toggle" checked>
700
+ <span class="toggle-slider"></span>
701
+ </label>
702
+ <span class="toggle-status" id="compute-point-status">开启</span>
703
+ </div>
704
+ </div>
705
+ <div class="status-item">
706
+ <span class="status-label">设置说明</span>
707
+ <span class="status-value info-text">开启后,系统将记录每次对话使用的计算点数,用于统计和分析</span>
708
+ </div>
709
+ </div>
710
+
711
+ <div class="grid">
712
+ <div class="card">
713
+ <div class="card-header">
714
+ <h2 class="card-title">
715
+ <span class="card-icon">💰</span>
716
+ 计算点总计
717
+ </h2>
718
+ </div>
719
+ <div class="status-item">
720
+ <span class="status-label">总计算点</span>
721
+ <span class="status-value compute-points">{{ compute_points.total|int }}</span>
722
+ </div>
723
+ <div class="status-item">
724
+ <span class="status-label">已使用</span>
725
+ <span class="status-value compute-points">{{ compute_points.used|int }}</span>
726
+ </div>
727
+ <div class="status-item">
728
+ <span class="status-label">剩余</span>
729
+ <span class="status-value compute-points">{{ compute_points.left|int }}</span>
730
+ </div>
731
+ <div class="status-item">
732
+ <span class="status-label">使用比例</span>
733
+ <div style="width: 100%; text-align: right;">
734
+ <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
735
+ {{ compute_points.percentage }}%
736
+ </span>
737
+ <div class="progress-container">
738
+ <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
739
+ </div>
740
+ </div>
741
+ </div>
742
+ {% if compute_points.last_update %}
743
+ <div class="status-item">
744
+ <span class="status-label">最后更新时间</span>
745
+ <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
746
+ </div>
747
+ {% endif %}
748
+ </div>
749
+
750
+ <div class="card">
751
+ <div class="card-header">
752
+ <h2 class="card-title">
753
+ <span class="card-icon">🔍</span>
754
+ Token 使用统计
755
+ </h2>
756
+ </div>
757
+ <div class="status-item">
758
+ <span class="status-label">总输入Token</span>
759
+ <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
760
+ </div>
761
+ <div class="status-item">
762
+ <span class="status-label">总输出Token</span>
763
+ <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
764
+ </div>
765
+ <div class="token-note">
766
+ <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
767
+ </div>
768
+ <div class="table-container">
769
+ <table class="data-table token-model-table">
770
+ <thead>
771
+ <tr>
772
+ <th>模型</th>
773
+ <th>调用次数</th>
774
+ <th>输入Token</th>
775
+ <th>输出Token</th>
776
+ </tr>
777
+ </thead>
778
+ <tbody>
779
+ {% for model, stats in model_stats.items() %}
780
+ <tr>
781
+ <td>{{ model }}</td>
782
+ <td class="call-count">{{ stats.count }}</td>
783
+ <td class="token-count">{{ stats.prompt_tokens|int }}</td>
784
+ <td class="token-count">{{ stats.completion_tokens|int }}</td>
785
+ </tr>
786
+ {% endfor %}
787
+ </tbody>
788
+ </table>
789
+ </div>
790
+ </div>
791
+ </div>
792
+
793
+ {% if users_compute_points|length > 0 %}
794
+ <div class="card">
795
+ <div class="card-header">
796
+ <h2 class="card-title">
797
+ <span class="card-icon">👥</span>
798
+ 用户计算点详情
799
+ </h2>
800
+ </div>
801
+ <div class="table-container">
802
+ <table class="data-table">
803
+ <thead>
804
+ <tr>
805
+ <th>用户</th>
806
+ <th>总计算点</th>
807
+ <th>已使用</th>
808
+ <th>剩余</th>
809
+ <th>使用比例</th>
810
+ </tr>
811
+ </thead>
812
+ <tbody>
813
+ {% for user in users_compute_points %}
814
+ <tr>
815
+ <td>用户 {{ user.user_id }}</td>
816
+ <td class="compute-points">{{ user.total|int }}</td>
817
+ <td class="compute-points">{{ user.used|int }}</td>
818
+ <td class="compute-points">{{ user.left|int }}</td>
819
+ <td>
820
+ <div style="width: 100%; position: relative;">
821
+ <span class="status-value compute-points {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}">
822
+ {{ user.percentage }}%
823
+ </span>
824
+ <div class="progress-container">
825
+ <div class="progress-bar {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}" style="width: {{ user.percentage }}%"></div>
826
+ </div>
827
+ </div>
828
+ </td>
829
+ </tr>
830
+ {% endfor %}
831
+ </tbody>
832
+ </table>
833
+ </div>
834
+ </div>
835
+ {% endif %}
836
+
837
+ <div class="card">
838
+ <div class="card-header">
839
+ <h2 class="card-title">
840
+ <span class="card-icon">📊</span>
841
+ 计算点使用日志
842
+ </h2>
843
+ </div>
844
+ <div class="table-container">
845
+ <table class="data-table">
846
+ <thead>
847
+ <tr>
848
+ {% for key, value in compute_points_log.columns.items() %}
849
+ <th>{{ value }}</th>
850
+ {% endfor %}
851
+ </tr>
852
+ </thead>
853
+ <tbody>
854
+ {% for entry in compute_points_log.log %}
855
+ <tr>
856
+ {% for key, value in compute_points_log.columns.items() %}
857
+ <td class="compute-points">{{ entry.get(key, 0) }}</td>
858
+ {% endfor %}
859
+ </tr>
860
+ {% endfor %}
861
+ </tbody>
862
+ </table>
863
+ </div>
864
+ </div>
865
+
866
+ <div class="card">
867
+ <div class="card-header">
868
+ <h2 class="card-title">
869
+ <span class="card-icon">📈</span>
870
+ 模型调用记录
871
+ </h2>
872
+ <button id="toggleModelStats" class="btn-toggle">显示全部</button>
873
+ </div>
874
+ <div class="table-container">
875
+ <table class="data-table">
876
+ <thead>
877
+ <tr>
878
+ <th>调用时间 (北京时间)</th>
879
+ <th>模型</th>
880
+ <th>输入Token</th>
881
+ <th>输出Token</th>
882
+ <th>总Token</th>
883
+ <th>计算方式</th>
884
+ <th>计算点数</th>
885
+ </tr>
886
+ </thead>
887
+ <tbody>
888
+ {% for record in model_usage_records|reverse %}
889
+ <tr class="model-row {% if loop.index > 10 %}hidden-model{% endif %}">
890
+ <td class="datetime">{{ record.call_time }}</td>
891
+ <td>{{ record.model }}</td>
892
+ <td class="token-count">{{ record.prompt_tokens }}</td>
893
+ <td class="token-count">{{ record.completion_tokens }}</td>
894
+ <td>{{ record.prompt_tokens + record.completion_tokens }}</td>
895
+ <td><span class="token-method {{ record.calculation_method }}">{{ "精确" if record.calculation_method == "api" else "估算" }}</span></td>
896
+ <td>{{ record.compute_points if record.compute_points is not none else 'null' }}</td>
897
+ </tr>
898
+ {% endfor %}
899
+ </tbody>
900
+ </table>
901
+ <div class="token-note">
902
+ <small>* Token计算方式:<span class="token-method api">精确</span> 表示调用官方API精确计算,<span class="token-method estimate">估算</span> 表示使用gpt-4o模型估算。所有统计数据仅供参考,不代表实际计费标准。</small>
903
+ </div>
904
+ </div>
905
+ </div>
906
+
907
+ <div class="card">
908
+ <div class="card-header">
909
+ <h2 class="card-title">
910
+ <span class="card-icon">📡</span>
911
+ API 端点
912
+ </h2>
913
+ </div>
914
+ <div class="endpoint-item">
915
+ <p>获取模型列表:</p>
916
+ {% if space_url %}
917
+ <a href="{{ space_url }}/v1/models" class="endpoint-url" target="_blank">GET {{ space_url }}/v1/models</a>
918
+ {% else %}
919
+ <a href="/v1/models" class="endpoint-url" target="_blank">GET /v1/models</a>
920
+ {% endif %}
921
+ </div>
922
+ <div class="endpoint-item">
923
+ <p>聊天补全:</p>
924
+ {% if space_url %}
925
+ <code class="endpoint-url">POST {{ space_url }}/v1/chat/completions</code>
926
+ {% else %}
927
+ <code class="endpoint-url">POST /v1/chat/completions</code>
928
+ {% endif %}
929
+ </div>
930
+ <div class="endpoint-item">
931
+ <p>健康检查:</p>
932
+ {% if space_url %}
933
+ <a href="{{ space_url }}/health" class="endpoint-url" target="_blank">GET {{ space_url }}/health</a>
934
+ {% else %}
935
+ <a href="/health" class="endpoint-url" target="_blank">GET /health</a>
936
+ {% endif %}
937
+ </div>
938
+ </div>
939
+
940
+ <div class="footer">
941
+ <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
942
+ </div>
943
+ </div>
944
+
945
+ <a href="#" class="float-btn" title="回到顶部">↑</a>
946
+
947
+ <script>
948
+ // 回到顶部按钮
949
+ document.querySelector('.float-btn').addEventListener('click', (e) => {
950
+ e.preventDefault();
951
+ window.scrollTo({ top: 0, behavior: 'smooth' });
952
+ });
953
+
954
+ // 显示/隐藏回到顶部按钮
955
+ window.addEventListener('scroll', () => {
956
+ const floatBtn = document.querySelector('.float-btn');
957
+ if (window.pageYOffset > 300) {
958
+ floatBtn.style.opacity = '1';
959
+ } else {
960
+ floatBtn.style.opacity = '0';
961
+ }
962
+ });
963
+
964
+ // 初始化隐藏回到顶部按钮
965
+ document.querySelector('.float-btn').style.opacity = '0';
966
+
967
+ // 模型统计折叠功能
968
+ const toggleBtn = document.getElementById('toggleModelStats');
969
+ const hiddenModels = document.querySelectorAll('.hidden-model');
970
+ let isExpanded = false;
971
+
972
+ if (toggleBtn) {
973
+ toggleBtn.addEventListener('click', () => {
974
+ hiddenModels.forEach(model => {
975
+ model.classList.toggle('hidden-model');
976
+ });
977
+
978
+ isExpanded = !isExpanded;
979
+ toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
980
+ });
981
+ }
982
+
983
+ document.addEventListener('DOMContentLoaded', function() {
984
+ initCharts();
985
+
986
+ // 显示/隐藏更多模型使用记录
987
+ const toggleModelStats = document.getElementById('toggleModelStats');
988
+ if (toggleModelStats) {
989
+ toggleModelStats.addEventListener('click', function() {
990
+ const hiddenRows = document.querySelectorAll('.hidden-model');
991
+ hiddenRows.forEach(row => {
992
+ row.classList.toggle('show-model');
993
+ });
994
+ toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部';
995
+ });
996
+ }
997
+
998
+ // 处理删除对话开关
999
+ const deleteToggle = document.getElementById('delete-chat-toggle');
1000
+ const deleteStatus = document.getElementById('delete-chat-status');
1001
+
1002
+ if (deleteToggle && deleteStatus) {
1003
+ deleteToggle.addEventListener('change', function() {
1004
+ const isChecked = this.checked;
1005
+ deleteStatus.textContent = isChecked ? '开启' : '关闭';
1006
+
1007
+ // 发送更新请求到后端
1008
+ fetch('/update_delete_chat_setting', {
1009
+ method: 'POST',
1010
+ headers: {
1011
+ 'Content-Type': 'application/json',
1012
+ },
1013
+ body: JSON.stringify({ delete_chat: isChecked })
1014
+ })
1015
+ .then(response => response.json())
1016
+ .then(data => {
1017
+ if (data.success) {
1018
+ // 显示成功提示
1019
+ showNotification(isChecked ? '已开启自动删除对话功能' : '已关闭自动删除对话功能', 'success');
1020
+ } else {
1021
+ // 显示错误提示
1022
+ showNotification('设置更新失败: ' + data.error, 'error');
1023
+ // 回滚UI状态
1024
+ deleteToggle.checked = !isChecked;
1025
+ deleteStatus.textContent = !isChecked ? '开启' : '关闭';
1026
+ }
1027
+ })
1028
+ .catch(error => {
1029
+ console.error('更新设置出错:', error);
1030
+ showNotification('更新设置失败,请重试', 'error');
1031
+ // 回滚UI状态
1032
+ deleteToggle.checked = !isChecked;
1033
+ deleteStatus.textContent = !isChecked ? '开启' : '关闭';
1034
+ });
1035
+ });
1036
+ }
1037
+
1038
+ // 处理计算点数记录开关
1039
+ const computePointToggle = document.getElementById('compute-point-toggle');
1040
+ const computePointStatus = document.getElementById('compute-point-status');
1041
+
1042
+ if (computePointToggle && computePointStatus) {
1043
+ computePointToggle.addEventListener('change', function() {
1044
+ const isChecked = this.checked;
1045
+ computePointStatus.textContent = isChecked ? '开启' : '关闭';
1046
+
1047
+ // 发送更新请求到后端
1048
+ fetch('/update_compute_point_toggle', {
1049
+ method: 'POST',
1050
+ headers: {
1051
+ 'Content-Type': 'application/json',
1052
+ },
1053
+ body: JSON.stringify({ always_display: isChecked })
1054
+ })
1055
+ .then(response => response.json())
1056
+ .then(data => {
1057
+ if (data.success) {
1058
+ // 显示成功提示
1059
+ showNotification(isChecked ? '已开启计算点数记录功能' : '已关闭计算点数记录功能', 'success');
1060
+ } else {
1061
+ // 显示错误提示
1062
+ showNotification('设置更新失败: ' + data.error, 'error');
1063
+ // 回滚UI状态
1064
+ computePointToggle.checked = !isChecked;
1065
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
1066
+ }
1067
+ })
1068
+ .catch(error => {
1069
+ console.error('更新设置出错:', error);
1070
+ showNotification('更新设置失败,请重试', 'error');
1071
+ // 回滚UI状态
1072
+ computePointToggle.checked = !isChecked;
1073
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
1074
+ });
1075
+ });
1076
+ }
1077
+ });
1078
+
1079
+ // 通知函数
1080
+ function showNotification(message, type = 'info') {
1081
+ const notification = document.createElement('div');
1082
+ notification.className = `notification ${type}`;
1083
+ notification.textContent = message;
1084
+
1085
+ document.body.appendChild(notification);
1086
+
1087
+ // 显示动画
1088
+ setTimeout(() => {
1089
+ notification.classList.add('show');
1090
+ }, 10);
1091
+
1092
+ // 3秒后淡出
1093
+ setTimeout(() => {
1094
+ notification.classList.remove('show');
1095
+ setTimeout(() => {
1096
+ notification.remove();
1097
+ }, 300);
1098
+ }, 3000);
1099
+ }
1100
+ </script>
1101
+ </body>
1102
+ </html>
templates/login.html ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Abacus Chat Proxy - 登录</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #6f42c1;
10
+ --secondary-color: #4a32a8;
11
+ --bg-color: #0a0a1a;
12
+ --text-color: #e6e6ff;
13
+ --card-bg: rgba(30, 30, 60, 0.7);
14
+ --input-bg: rgba(40, 40, 80, 0.6);
15
+ --success-color: #36d399;
16
+ --error-color: #f87272;
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
+ }
25
+
26
+ body {
27
+ min-height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background-color: var(--bg-color);
32
+ background-image:
33
+ radial-gradient(circle at 20% 35%, rgba(111, 66, 193, 0.15) 0%, transparent 40%),
34
+ radial-gradient(circle at 80% 10%, rgba(70, 111, 171, 0.1) 0%, transparent 40%);
35
+ color: var(--text-color);
36
+ position: relative;
37
+ overflow: hidden;
38
+ }
39
+
40
+ /* 科幻蜘蛛网动画 */
41
+ .web-container {
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ z-index: -2;
48
+ opacity: 0.6;
49
+ }
50
+
51
+ .web {
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+
56
+ /* 动态背景网格 */
57
+ .grid-background {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 100%;
63
+ background-image: linear-gradient(rgba(50, 50, 100, 0.05) 1px, transparent 1px),
64
+ linear-gradient(90deg, rgba(50, 50, 100, 0.05) 1px, transparent 1px);
65
+ background-size: 30px 30px;
66
+ z-index: -1;
67
+ animation: grid-move 20s linear infinite;
68
+ }
69
+
70
+ @keyframes grid-move {
71
+ 0% {
72
+ transform: translateY(0);
73
+ }
74
+ 100% {
75
+ transform: translateY(30px);
76
+ }
77
+ }
78
+
79
+ /* 浮动粒子效果 */
80
+ .particles {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ width: 100%;
85
+ height: 100%;
86
+ overflow: hidden;
87
+ z-index: -1;
88
+ }
89
+
90
+ .particle {
91
+ position: absolute;
92
+ display: block;
93
+ pointer-events: none;
94
+ width: 6px;
95
+ height: 6px;
96
+ background-color: rgba(111, 66, 193, 0.2);
97
+ border-radius: 50%;
98
+ animation: float 20s infinite ease-in-out;
99
+ }
100
+
101
+ @keyframes float {
102
+ 0%, 100% {
103
+ transform: translateY(0) translateX(0);
104
+ opacity: 0;
105
+ }
106
+ 50% {
107
+ opacity: 0.5;
108
+ }
109
+ 25%, 75% {
110
+ transform: translateY(-100px) translateX(50px);
111
+ }
112
+ }
113
+
114
+ .login-card {
115
+ width: 420px;
116
+ padding: 2.5rem;
117
+ border-radius: 16px;
118
+ background: var(--card-bg);
119
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
120
+ backdrop-filter: blur(8px);
121
+ border: 1px solid rgba(255, 255, 255, 0.1);
122
+ z-index: 10;
123
+ animation: card-fade-in 0.6s ease-out;
124
+ }
125
+
126
+ @keyframes card-fade-in {
127
+ from {
128
+ opacity: 0;
129
+ transform: translateY(20px);
130
+ }
131
+ to {
132
+ opacity: 1;
133
+ transform: translateY(0);
134
+ }
135
+ }
136
+
137
+ .login-header {
138
+ text-align: center;
139
+ margin-bottom: 2rem;
140
+ }
141
+
142
+ .login-header h1 {
143
+ font-size: 2rem;
144
+ font-weight: 600;
145
+ margin-bottom: 0.5rem;
146
+ background: linear-gradient(45deg, #6f42c1, #5181f1);
147
+ -webkit-background-clip: text;
148
+ -webkit-text-fill-color: transparent;
149
+ letter-spacing: 0.5px;
150
+ }
151
+
152
+ .login-header p {
153
+ color: rgba(230, 230, 255, 0.7);
154
+ font-size: 0.95rem;
155
+ }
156
+
157
+ .space-info {
158
+ text-align: center;
159
+ background: rgba(50, 50, 150, 0.2);
160
+ padding: 0.75rem;
161
+ border-radius: 8px;
162
+ margin-bottom: 1.5rem;
163
+ font-size: 0.9rem;
164
+ border: 1px solid rgba(111, 66, 193, 0.3);
165
+ }
166
+
167
+ .space-info a {
168
+ color: var(--primary-color);
169
+ text-decoration: none;
170
+ font-weight: bold;
171
+ transition: all 0.2s;
172
+ }
173
+
174
+ .space-info a:hover {
175
+ text-decoration: underline;
176
+ color: var(--secondary-color);
177
+ }
178
+
179
+ .login-form {
180
+ display: flex;
181
+ flex-direction: column;
182
+ }
183
+
184
+ .form-group {
185
+ margin-bottom: 1.5rem;
186
+ position: relative;
187
+ }
188
+
189
+ .form-group label {
190
+ display: block;
191
+ margin-bottom: 0.5rem;
192
+ font-size: 0.9rem;
193
+ font-weight: 500;
194
+ color: rgba(230, 230, 255, 0.9);
195
+ }
196
+
197
+ .form-control {
198
+ width: 100%;
199
+ padding: 0.75rem 1rem;
200
+ font-size: 1rem;
201
+ line-height: 1.5;
202
+ color: var(--text-color);
203
+ background-color: var(--input-bg);
204
+ border: 1px solid rgba(255, 255, 255, 0.1);
205
+ border-radius: 8px;
206
+ transition: all 0.2s ease;
207
+ outline: none;
208
+ }
209
+
210
+ .form-control:focus {
211
+ border-color: var(--primary-color);
212
+ box-shadow: 0 0 0 3px rgba(111, 66, 193, 0.2);
213
+ }
214
+
215
+ .btn {
216
+ display: inline-block;
217
+ font-weight: 500;
218
+ text-align: center;
219
+ vertical-align: middle;
220
+ cursor: pointer;
221
+ padding: 0.75rem 1rem;
222
+ font-size: 1rem;
223
+ line-height: 1.5;
224
+ border-radius: 8px;
225
+ transition: all 0.15s ease-in-out;
226
+ border: none;
227
+ }
228
+
229
+ .btn-primary {
230
+ color: #fff;
231
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
232
+ box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
233
+ position: relative;
234
+ overflow: hidden;
235
+ }
236
+
237
+ .btn-primary:hover {
238
+ transform: translateY(-2px);
239
+ box-shadow: 0 6px 15px rgba(111, 66, 193, 0.4);
240
+ }
241
+
242
+ .btn-primary:active {
243
+ transform: translateY(0);
244
+ }
245
+
246
+ /* 添加光效效果 */
247
+ .btn-primary::before {
248
+ content: '';
249
+ position: absolute;
250
+ top: -50%;
251
+ left: -50%;
252
+ width: 200%;
253
+ height: 200%;
254
+ background: linear-gradient(
255
+ to bottom right,
256
+ rgba(255, 255, 255, 0) 0%,
257
+ rgba(255, 255, 255, 0.1) 50%,
258
+ rgba(255, 255, 255, 0) 100%
259
+ );
260
+ transform: rotate(45deg);
261
+ animation: btn-shine 3s infinite;
262
+ }
263
+
264
+ @keyframes btn-shine {
265
+ 0% {
266
+ left: -50%;
267
+ }
268
+ 100% {
269
+ left: 150%;
270
+ }
271
+ }
272
+
273
+ .error-message {
274
+ background-color: rgba(248, 114, 114, 0.2);
275
+ color: var(--error-color);
276
+ padding: 0.75rem;
277
+ border-radius: 6px;
278
+ margin-bottom: 1.5rem;
279
+ font-size: 0.9rem;
280
+ border-left: 3px solid var(--error-color);
281
+ display: {{ 'block' if error else 'none' }};
282
+ }
283
+
284
+ .logo {
285
+ margin-bottom: 1rem;
286
+ font-size: 3rem;
287
+ animation: glow 2s infinite alternate;
288
+ }
289
+
290
+ @keyframes glow {
291
+ from {
292
+ text-shadow: 0 0 5px rgba(111, 66, 193, 0.5), 0 0 10px rgba(111, 66, 193, 0.5);
293
+ }
294
+ to {
295
+ text-shadow: 0 0 10px rgba(111, 66, 193, 0.8), 0 0 20px rgba(111, 66, 193, 0.8);
296
+ }
297
+ }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <div class="grid-background"></div>
302
+ <div class="particles">
303
+ <!-- 粒子元素会由JS生成 -->
304
+ </div>
305
+ <div class="web-container">
306
+ <canvas class="web" id="webCanvas"></canvas>
307
+ </div>
308
+
309
+ <div class="login-card">
310
+ <div class="login-header">
311
+ <div class="logo">🤖</div>
312
+ <h1>Abacus Chat Proxy</h1>
313
+ <p>请输入访问密码</p>
314
+ </div>
315
+
316
+ {% if space_url %}
317
+ <div class="space-info">
318
+ api接口为{{ space_url }}/v1,请点击 <a href="{{ space_url }}" target="_blank">{{ space_url }}</a> 来登录并查看使用情况,
319
+ </div>
320
+ {% endif %}
321
+
322
+ <div class="error-message" id="error-message">
323
+ {{ error }}
324
+ </div>
325
+
326
+ <form class="login-form" method="post" action="/login">
327
+ <div class="form-group">
328
+ <label for="password">密码</label>
329
+ <input type="password" class="form-control" id="password" name="password" placeholder="请输入访问密码" required>
330
+ </div>
331
+
332
+ <button type="submit" class="btn btn-primary">登录</button>
333
+ </form>
334
+ </div>
335
+
336
+ <script>
337
+ // 创建浮动粒子
338
+ function createParticles() {
339
+ const particlesContainer = document.querySelector('.particles');
340
+ const particleCount = 20;
341
+
342
+ for (let i = 0; i < particleCount; i++) {
343
+ const particle = document.createElement('div');
344
+ particle.className = 'particle';
345
+
346
+ // 随机位置和大小
347
+ const size = Math.random() * 5 + 2;
348
+ const x = Math.random() * 100;
349
+ const y = Math.random() * 100;
350
+
351
+ particle.style.width = `${size}px`;
352
+ particle.style.height = `${size}px`;
353
+ particle.style.left = `${x}%`;
354
+ particle.style.top = `${y}%`;
355
+
356
+ // 随机动画延迟
357
+ particle.style.animationDelay = `${Math.random() * 10}s`;
358
+ particle.style.animationDuration = `${Math.random() * 10 + 10}s`;
359
+
360
+ // 随机透明度
361
+ particle.style.opacity = Math.random() * 0.5;
362
+
363
+ particlesContainer.appendChild(particle);
364
+ }
365
+ }
366
+
367
+ // 科幻蜘蛛网效果
368
+ function initWebCanvas() {
369
+ const canvas = document.getElementById('webCanvas');
370
+ const ctx = canvas.getContext('2d');
371
+ let width = window.innerWidth;
372
+ let height = window.innerHeight;
373
+
374
+ // 设置canvas尺寸
375
+ canvas.width = width;
376
+ canvas.height = height;
377
+
378
+ // 节点类
379
+ class Node {
380
+ constructor(x, y) {
381
+ this.x = x;
382
+ this.y = y;
383
+ this.vx = (Math.random() - 0.5) * 0.5;
384
+ this.vy = (Math.random() - 0.5) * 0.5;
385
+ this.radius = Math.random() * 2 + 1;
386
+ }
387
+
388
+ update() {
389
+ if (this.x < 0 || this.x > width) this.vx = -this.vx;
390
+ if (this.y < 0 || this.y > height) this.vy = -this.vy;
391
+
392
+ this.x += this.vx;
393
+ this.y += this.vy;
394
+ }
395
+
396
+ draw() {
397
+ ctx.beginPath();
398
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
399
+ ctx.fillStyle = 'rgba(111, 66, 193, 0.4)';
400
+ ctx.fill();
401
+ }
402
+ }
403
+
404
+ // 创建节点
405
+ const nodeCount = Math.floor(width * height / 15000);
406
+ const nodes = [];
407
+
408
+ for (let i = 0; i < nodeCount; i++) {
409
+ nodes.push(new Node(Math.random() * width, Math.random() * height));
410
+ }
411
+
412
+ // 绘制线条
413
+ function drawWeb() {
414
+ ctx.clearRect(0, 0, width, height);
415
+
416
+ // 更新节点
417
+ nodes.forEach(node => {
418
+ node.update();
419
+ node.draw();
420
+ });
421
+
422
+ // 绘制连线
423
+ for (let i = 0; i < nodes.length; i++) {
424
+ for (let j = i + 1; j < nodes.length; j++) {
425
+ const dx = nodes[i].x - nodes[j].x;
426
+ const dy = nodes[i].y - nodes[j].y;
427
+ const distance = Math.sqrt(dx * dx + dy * dy);
428
+
429
+ if (distance < 150) {
430
+ ctx.beginPath();
431
+ ctx.moveTo(nodes[i].x, nodes[i].y);
432
+ ctx.lineTo(nodes[j].x, nodes[j].y);
433
+ ctx.strokeStyle = `rgba(111, 66, 193, ${0.2 * (1 - distance / 150)})`;
434
+ ctx.lineWidth = 0.5;
435
+ ctx.stroke();
436
+ }
437
+ }
438
+ }
439
+
440
+ requestAnimationFrame(drawWeb);
441
+ }
442
+
443
+ // 监听窗口大小变化
444
+ window.addEventListener('resize', () => {
445
+ width = window.innerWidth;
446
+ height = window.innerHeight;
447
+ canvas.width = width;
448
+ canvas.height = height;
449
+ });
450
+
451
+ // 开始动画
452
+ drawWeb();
453
+ }
454
+
455
+ // 页面加载时初始化效果
456
+ window.addEventListener('load', () => {
457
+ createParticles();
458
+ initWebCanvas();
459
+ });
460
+ </script>
461
+ </body>
462
+ </html>