malt666 commited on
Commit
256d363
·
verified ·
1 Parent(s): 3fb80b3

Upload 5 files

Browse files
Files changed (4) hide show
  1. Dockerfile +23 -23
  2. app.py +306 -171
  3. templates/dashboard.html +1031 -1031
  4. templates/login.html +461 -461
Dockerfile CHANGED
@@ -1,24 +1,24 @@
1
- FROM python:3.11-slim
2
-
3
- # 设置用户为root
4
- USER root
5
-
6
- WORKDIR /app
7
-
8
- COPY requirements.txt .
9
- RUN pip install --no-cache-dir -r requirements.txt
10
-
11
- COPY . .
12
-
13
- # 设置环境变量
14
- ENV HOST=0.0.0.0
15
- ENV PORT=7860
16
-
17
- # 删除敏感文件
18
- RUN rm -f config.json password.txt
19
-
20
- # 暴露端口(Hugging Face默认使用7860端口)
21
- EXPOSE 7860
22
-
23
- # 启动命令
24
  CMD ["python", "app.py"]
 
1
+ FROM python:3.11-slim
2
+
3
+ # 设置用户为root
4
+ USER root
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ # 设置环境变量
14
+ ENV HOST=0.0.0.0
15
+ ENV PORT=7860
16
+
17
+ # 删除敏感文件
18
+ RUN rm -f config.json password.txt
19
+
20
+ # 暴露端口(Hugging Face默认使用7860端口)
21
+ EXPOSE 7860
22
+
23
+ # 启动命令
24
  CMD ["python", "app.py"]
app.py CHANGED
@@ -12,11 +12,21 @@ 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
 
@@ -230,7 +240,7 @@ def is_token_expired(token):
230
  return True
231
 
232
 
233
- def refresh_token(session, cookies):
234
  """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
235
  headers = {
236
  "accept": "application/json, text/plain, */*",
@@ -251,13 +261,14 @@ def refresh_token(session, cookies):
251
  }
252
 
253
  try:
254
- response = session.post(
 
255
  USER_INFO_URL,
256
  headers=headers,
257
  json={},
258
- cookies=None
259
  )
260
-
261
  if response.status_code == 200:
262
  response_data = response.json()
263
  if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
@@ -273,7 +284,7 @@ def refresh_token(session, cookies):
273
  return None
274
 
275
 
276
- def get_model_map(session, cookies, session_token):
277
  """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
278
  headers = {
279
  "accept": "application/json, text/plain, */*",
@@ -300,11 +311,12 @@ def get_model_map(session, cookies, session_token):
300
  models_set = set()
301
 
302
  try:
303
- response = session.post(
 
304
  MODEL_LIST_URL,
305
  headers=headers,
306
  json={},
307
- cookies=None
308
  )
309
 
310
  if response.status_code != 200:
@@ -345,74 +357,102 @@ def get_model_map(session, cookies, session_token):
345
  raise
346
 
347
 
348
- def init_session():
349
  get_password()
350
  global USER_NUM, MODELS, USER_DATA
351
 
352
  config_list = resolve_config()
353
  user_num = len(config_list)
354
  all_models = set()
355
-
 
 
 
356
  for i in range(user_num):
357
  user = config_list[i]
358
  cookies = user.get("cookies")
359
  conversation_id = user.get("conversation_id")
360
- session = requests.Session()
361
-
362
- session_token = refresh_token(session, cookies)
363
- if not session_token:
364
- print(f"无法获取cookie {i+1}的token")
365
- continue
366
-
367
- try:
368
- model_map, models_set = get_model_map(session, cookies, session_token)
369
- all_models.update(models_set)
370
- USER_DATA.append((session, cookies, session_token, conversation_id, model_map, i))
371
-
372
- # 对第一个成功配置的用户,初始化计算点数记录功能
373
- if i == 0:
374
- try:
375
- headers = {
376
- "accept": "application/json, text/plain, */*",
377
- "accept-language": "zh-CN,zh;q=0.9",
378
- "content-type": "application/json",
379
- "reai-ui": "1",
380
- "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
381
- "sec-ch-ua-mobile": "?0",
382
- "sec-ch-ua-platform": "\"Windows\"",
383
- "sec-fetch-dest": "empty",
384
- "sec-fetch-mode": "cors",
385
- "sec-fetch-site": "same-site",
386
- "x-abacus-org-host": "apps",
387
- "session-token": session_token
388
- }
389
-
390
- response = session.post(
391
- COMPUTE_POINT_TOGGLE_URL,
392
- headers=headers,
393
- json={"alwaysDisplay": True},
394
- cookies=None
395
- )
396
-
397
- if response.status_code == 200:
398
- result = response.json()
399
- if result.get("success"):
400
- print("成功初始化计算点数记录功能为开启状态")
 
 
 
 
 
 
401
  else:
402
- print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
403
- else:
404
- print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
405
- except Exception as e:
406
- print(f"初始化计算点数记录功能时出错: {e}")
407
- except Exception as e:
408
- print(f"配置用户 {i+1} 失败: {e}")
409
- continue
410
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  USER_NUM = len(USER_DATA)
412
  if USER_NUM == 0:
413
  print("No user available, exiting...")
414
- exit(1)
415
-
 
416
  MODELS = all_models
417
  print(f"启动完成,共配置 {USER_NUM} 个用户")
418
 
@@ -430,79 +470,28 @@ def update_cookie(session, cookies):
430
  return cookies
431
 
432
 
433
- user_data = init_session()
434
-
435
-
436
- @app.route("/v1/models", methods=["GET"])
437
- @require_auth
438
- def get_models():
439
- if len(MODELS) == 0:
440
- return jsonify({"error": "No models available"}), 500
441
- model_list = []
442
- for model in MODELS:
443
- model_list.append(
444
- {
445
- "id": model,
446
- "object": "model",
447
- "created": int(time.time()),
448
- "owned_by": "Elbert",
449
- "name": model,
450
- }
451
- )
452
- return jsonify({"object": "list", "data": model_list})
453
-
454
-
455
- @app.route("/v1/chat/completions", methods=["POST"])
456
- @require_auth
457
- def chat_completions():
458
- openai_request = request.get_json()
459
- stream = openai_request.get("stream", False)
460
- messages = openai_request.get("messages")
461
- if messages is None:
462
- return jsonify({"error": "Messages is required", "status": 400}), 400
463
- model = openai_request.get("model")
464
- if model not in MODELS:
465
- return (
466
- jsonify(
467
- {
468
- "error": "Model not available, check if it is configured properly",
469
- "status": 404,
470
- }
471
- ),
472
- 404,
473
- )
474
- message = format_message(messages)
475
- think = (
476
- openai_request.get("think", False) if model == "Claude Sonnet 3.7" else False
477
- )
478
- return (
479
- send_message(message, model, think)
480
- if stream
481
- else send_message_non_stream(message, model, think)
482
- )
483
-
484
-
485
- def get_user_data():
486
  global CURRENT_USER
487
  CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
488
  print(f"使用配置 {CURRENT_USER+1}")
489
-
490
  # Akiru uzantajn datumojn
491
- session, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[CURRENT_USER]
492
-
493
  # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
494
  if is_token_expired(session_token):
495
  print(f"Cookie {CURRENT_USER+1}的token已过期或即将过期,正在刷新...")
496
- new_token = refresh_token(session, cookies)
497
  if new_token:
498
  # Ĝisdatigu la globale konservitan tokenon
499
- USER_DATA[CURRENT_USER] = (session, cookies, new_token, conversation_id, model_map, user_index)
500
  session_token = new_token
501
  print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
502
  else:
503
  print(f"警告:无法刷新Cookie {CURRENT_USER+1}的token,继续使用当前token")
504
-
505
- return (session, cookies, session_token, conversation_id, model_map, user_index)
506
 
507
 
508
  def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
@@ -666,11 +655,11 @@ def send_message(message, model, think=False):
666
 
667
  trace_id, sentry_trace = generate_trace_id()
668
 
669
- # 计算输入token
670
- print("\n----- 计算输入token -----")
671
- prompt_tokens, calculation_method = num_tokens_from_string(message, model)
672
- print(f"输入token数: {prompt_tokens}")
673
- print(f"计算方法: {calculation_method}")
674
  completion_buffer = io.StringIO() # 收集所有输出用于计算token
675
 
676
  headers = {
@@ -740,11 +729,14 @@ def send_message(message, model, think=False):
740
  return ""
741
 
742
  def generate():
 
743
  id = ""
744
  think_state = 2
745
 
 
746
  yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
747
 
 
748
  for line in response.iter_lines():
749
  if line:
750
  decoded_line = line.decode("utf-8")
@@ -781,18 +773,27 @@ def send_message(message, model, think=False):
781
  except Exception as e:
782
  print(f"处理响应出错: {e}")
783
 
 
784
  yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
785
  yield "data: [DONE]\n\n"
786
 
787
- # 在流式传输完成后计算token并更新统计
788
- completion_result, _ = num_tokens_from_string(completion_buffer.getvalue(), model)
789
-
790
- # 保存对话历史并获取计算点数
791
- _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
792
-
793
- # 更新统计信息
794
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
 
 
 
 
 
 
 
795
 
 
796
  return Response(generate(), mimetype="text/event-stream")
797
  except requests.exceptions.RequestException as e:
798
  error_details = str(e)
@@ -823,7 +824,10 @@ def send_message(message, model, think=False):
823
  USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
824
 
825
  # 保存到配置文件
826
- update_conversation_id(user_index, new_conversation_id)
 
 
 
827
 
828
  # 修改payload使用新会话ID,并移除regenerate和editPrompt
829
  payload["deploymentConversationId"] = new_conversation_id
@@ -843,27 +847,55 @@ def send_message(message, model, think=False):
843
  )
844
 
845
  response.raise_for_status()
846
- # 重用现有提取逻辑...
847
- # 但这里代码重复太多,实际应该重构为共享函数
 
 
 
 
 
 
 
 
 
 
 
 
848
  buffer = io.StringIO()
849
 
850
  for line in response.iter_lines():
851
  if line:
852
  decoded_line = line.decode("utf-8")
853
- segment = extract_segment(decoded_line)
854
  if segment:
855
  buffer.write(segment)
856
 
857
  response_content = buffer.getvalue()
858
 
859
- # 计算输出token并更新统计信息
860
- completion_result, _ = num_tokens_from_string(response_content, model)
 
861
 
862
  # 保存对话历史并获取计算点数
863
  _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
864
 
865
  # 更新统计信息
866
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
867
 
868
  return jsonify({
869
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -880,13 +912,20 @@ def send_message(message, model, think=False):
880
  }],
881
  "usage": {
882
  "prompt_tokens": prompt_tokens,
883
- "completion_tokens": completion_result,
884
- "total_tokens": prompt_tokens + completion_result
885
  }
886
  })
887
  except Exception as retry_e:
888
  print(f"重试失败: {retry_e}")
889
 
 
 
 
 
 
 
 
890
  return jsonify({"error": f"Failed to send message: {error_details}"}), 500
891
 
892
 
@@ -912,11 +951,10 @@ def send_message_non_stream(message, model, think=False):
912
 
913
  trace_id, sentry_trace = generate_trace_id()
914
 
915
- # 计算输入token
916
- print("\n----- 计算输入token -----")
917
- prompt_tokens, calculation_method = num_tokens_from_string(message, model)
918
- print(f"输入token数: {prompt_tokens}")
919
- print(f"计算方法: {calculation_method}")
920
 
921
  headers = {
922
  "accept": "text/event-stream",
@@ -971,7 +1009,6 @@ def send_message_non_stream(message, model, think=False):
971
  )
972
 
973
  response.raise_for_status()
974
- buffer = io.StringIO()
975
 
976
  def extract_segment(line_data):
977
  try:
@@ -1020,14 +1057,31 @@ def send_message_non_stream(message, model, think=False):
1020
  think_content = think_buffer.getvalue()
1021
  response_content = content_buffer.getvalue()
1022
 
1023
- # 计算输出token并更新统计信息
1024
- completion_result, _ = num_tokens_from_string(think_content + response_content, model)
 
 
1025
 
1026
  # 保存对话历史并获取计算点数
1027
  _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
1028
 
1029
  # 更新统计信息
1030
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
 
1032
  return jsonify({
1033
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -1044,11 +1098,13 @@ def send_message_non_stream(message, model, think=False):
1044
  }],
1045
  "usage": {
1046
  "prompt_tokens": prompt_tokens,
1047
- "completion_tokens": completion_result,
1048
- "total_tokens": prompt_tokens + completion_result
1049
  }
1050
  })
1051
  else:
 
 
1052
  for line in response.iter_lines():
1053
  if line:
1054
  decoded_line = line.decode("utf-8")
@@ -1058,14 +1114,30 @@ def send_message_non_stream(message, model, think=False):
1058
 
1059
  response_content = buffer.getvalue()
1060
 
1061
- # 计算输出token并更新统计信息
1062
- completion_result, _ = num_tokens_from_string(response_content, model)
 
1063
 
1064
  # 保存对话历史并获取计算点数
1065
  _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
1066
 
1067
  # 更新统计信息
1068
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
 
1070
  return jsonify({
1071
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -1082,8 +1154,8 @@ def send_message_non_stream(message, model, think=False):
1082
  }],
1083
  "usage": {
1084
  "prompt_tokens": prompt_tokens,
1085
- "completion_tokens": completion_result,
1086
- "total_tokens": prompt_tokens + completion_result
1087
  }
1088
  })
1089
  except requests.exceptions.RequestException as e:
@@ -1115,7 +1187,10 @@ def send_message_non_stream(message, model, think=False):
1115
  USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
1116
 
1117
  # 保存到配置文件
1118
- update_conversation_id(user_index, new_conversation_id)
 
 
 
1119
 
1120
  # 修改payload使用新会话ID,并移除regenerate和editPrompt
1121
  payload["deploymentConversationId"] = new_conversation_id
@@ -1135,27 +1210,55 @@ def send_message_non_stream(message, model, think=False):
1135
  )
1136
 
1137
  response.raise_for_status()
1138
- # 重用现有提取逻辑...
1139
- # 但这里代码重复太多,实际应该重构为共享函数
 
 
 
 
 
 
 
 
 
 
 
 
1140
  buffer = io.StringIO()
1141
 
1142
  for line in response.iter_lines():
1143
  if line:
1144
  decoded_line = line.decode("utf-8")
1145
- segment = extract_segment(decoded_line)
1146
  if segment:
1147
  buffer.write(segment)
1148
 
1149
  response_content = buffer.getvalue()
1150
 
1151
- # 计算输出token并更新统计信息
1152
- completion_result, _ = num_tokens_from_string(response_content, model)
 
1153
 
1154
  # 保存对话历史并获取计算点数
1155
  _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
1156
 
1157
  # 更新统计信息
1158
- update_model_stats(model, prompt_tokens, completion_result, calculation_method, compute_points_used)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1159
 
1160
  return jsonify({
1161
  "id": f"chatcmpl-{str(uuid.uuid4())}",
@@ -1172,8 +1275,8 @@ def send_message_non_stream(message, model, think=False):
1172
  }],
1173
  "usage": {
1174
  "prompt_tokens": prompt_tokens,
1175
- "completion_tokens": completion_result,
1176
- "total_tokens": prompt_tokens + completion_result
1177
  }
1178
  })
1179
  except Exception as retry_e:
@@ -1318,9 +1421,39 @@ def num_tokens_from_string(string, model=""):
1318
 
1319
 
1320
  # 更新模型使用统计
1321
- def update_model_stats(model, prompt_tokens, completion_tokens, calculation_method="estimate", compute_points=None):
1322
  global model_usage_stats, total_tokens, model_usage_records
1323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
  # 添加调用记录
1325
  # 获取UTC时间
1326
  utc_now = datetime.utcnow()
@@ -1333,7 +1466,7 @@ def update_model_stats(model, prompt_tokens, completion_tokens, calculation_meth
1333
  "call_time": call_time,
1334
  "prompt_tokens": prompt_tokens,
1335
  "completion_tokens": completion_tokens,
1336
- "calculation_method": calculation_method, # 直接使用传入的值
1337
  "compute_points": compute_points
1338
  }
1339
  model_usage_records.append(record)
@@ -1362,6 +1495,8 @@ def update_model_stats(model, prompt_tokens, completion_tokens, calculation_meth
1362
  total_tokens["prompt"] += prompt_tokens
1363
  total_tokens["completion"] += completion_tokens
1364
  total_tokens["total"] += (prompt_tokens + completion_tokens)
 
 
1365
 
1366
 
1367
  # 获取计算点信息
@@ -1539,7 +1674,7 @@ def dashboard():
1539
  days = uptime.days
1540
  hours, remainder = divmod(uptime.seconds, 3600)
1541
  minutes, seconds = divmod(remainder, 60)
1542
-
1543
  if days > 0:
1544
  uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
1545
  elif hours > 0:
 
12
  import os
13
  import threading
14
  from datetime import datetime, timedelta
15
+ import asyncio
16
+ import httpx
17
+ from concurrent.futures import ThreadPoolExecutor
18
+ import atexit
19
 
20
  app = Flask(__name__, template_folder='templates')
21
  app.secret_key = os.environ.get("SECRET_KEY", "abacus_chat_proxy_secret_key")
22
  app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
23
 
24
+ # 创建一个线程池用于执行 token 计算
25
+ token_executor = ThreadPoolExecutor(max_workers=5)
26
+
27
+ # 确保程序退出时关闭线程池
28
+ atexit.register(token_executor.shutdown, wait=True)
29
+
30
  # 添加tokenizer服务URL
31
  TOKENIZER_SERVICE_URL = "https://malt666-tokenizer.hf.space/count_tokens"
32
 
 
240
  return True
241
 
242
 
243
+ async def refresh_token(client: httpx.AsyncClient, cookies):
244
  """Uzu kuketon por refreŝigi session token, nur revenigu novan tokenon"""
245
  headers = {
246
  "accept": "application/json, text/plain, */*",
 
261
  }
262
 
263
  try:
264
+ # 使用 httpx.AsyncClient 发送异步请求
265
+ response = await client.post(
266
  USER_INFO_URL,
267
  headers=headers,
268
  json={},
269
+ # cookies=None # httpx doesn't use cookies param like this with client
270
  )
271
+
272
  if response.status_code == 200:
273
  response_data = response.json()
274
  if response_data.get('success') and 'sessionToken' in response_data.get('result', {}):
 
284
  return None
285
 
286
 
287
+ async def get_model_map(client: httpx.AsyncClient, cookies, session_token):
288
  """Akiru disponeblan modelan liston kaj ĝiajn mapajn rilatojn"""
289
  headers = {
290
  "accept": "application/json, text/plain, */*",
 
311
  models_set = set()
312
 
313
  try:
314
+ # 使用 httpx.AsyncClient 发送异步请求
315
+ response = await client.post(
316
  MODEL_LIST_URL,
317
  headers=headers,
318
  json={},
319
+ # cookies=None
320
  )
321
 
322
  if response.status_code != 200:
 
357
  raise
358
 
359
 
360
+ async def init_session():
361
  get_password()
362
  global USER_NUM, MODELS, USER_DATA
363
 
364
  config_list = resolve_config()
365
  user_num = len(config_list)
366
  all_models = set()
367
+
368
+ # 创建一个列表来收集异步任务
369
+ init_tasks = []
370
+
371
  for i in range(user_num):
372
  user = config_list[i]
373
  cookies = user.get("cookies")
374
  conversation_id = user.get("conversation_id")
375
+ # 创建 httpx.AsyncClient 实例
376
+ client = httpx.AsyncClient(timeout=30.0) # 设置超时
377
+
378
+ # 将每个用户的初始化变成一个异步任务
379
+ async def initialize_user(i, client, cookies, conversation_id):
380
+ session_token = await refresh_token(client, cookies) # await 调用异步函数
381
+ if not session_token:
382
+ print(f"无法获取cookie {i+1}的token")
383
+ await client.aclose() # 关闭 client
384
+ return None
385
+
386
+ try:
387
+ model_map, models_set = await get_model_map(client, cookies, session_token) # await 调用异步函数
388
+ # 用户数据现在包含 httpx.AsyncClient
389
+ user_entry = (client, cookies, session_token, conversation_id, model_map, i)
390
+
391
+ # 对第一个成功配置的用户,初始化计算点数记录功能 (改为异步)
392
+ if i == 0:
393
+ try:
394
+ headers = {
395
+ "accept": "application/json, text/plain, */*",
396
+ "accept-language": "zh-CN,zh;q=0.9",
397
+ "content-type": "application/json",
398
+ "reai-ui": "1",
399
+ "sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
400
+ "sec-ch-ua-mobile": "?0",
401
+ "sec-ch-ua-platform": "\"Windows\"",
402
+ "sec-fetch-dest": "empty",
403
+ "sec-fetch-mode": "cors",
404
+ "sec-fetch-site": "same-site",
405
+ "x-abacus-org-host": "apps",
406
+ "session-token": session_token
407
+ }
408
+
409
+ response = await client.post( # await 调用异步函数
410
+ COMPUTE_POINT_TOGGLE_URL,
411
+ headers=headers,
412
+ json={"alwaysDisplay": True},
413
+ # cookies=None
414
+ )
415
+
416
+ if response.status_code == 200:
417
+ result = response.json()
418
+ if result.get("success"):
419
+ print("成功初始化计算点数记录功能为开启状态")
420
+ else:
421
+ print(f"初始化计算点数记录功能失败: {result.get('error', '未知错误')}")
422
  else:
423
+ print(f"初始化计算点数记录功能失败,状态码: {response.status_code}")
424
+ except Exception as e:
425
+ print(f"初始化计算点数记录功能时出错: {e}")
426
+
427
+ return user_entry, models_set # 返回用户数据和模型集合
428
+ except Exception as e:
429
+ print(f"配置用户 {i+1} 失败: {e}")
430
+ await client.aclose() # 关闭 client
431
+ return None
432
+
433
+ # 添加任务到列表
434
+ init_tasks.append(initialize_user(i, client, cookies, conversation_id))
435
+
436
+ # 并发执行所有用户初始化任务
437
+ results = await asyncio.gather(*init_tasks)
438
+
439
+ # 处理结果
440
+ temp_user_data = []
441
+ for result in results:
442
+ if result:
443
+ user_entry, models_set = result
444
+ temp_user_data.append(user_entry)
445
+ all_models.update(models_set)
446
+
447
+ # 按原始索引排序 (虽然 gather 可能保持顺序,但显式排序更安全)
448
+ USER_DATA = sorted(temp_user_data, key=lambda x: x[5])
449
+
450
  USER_NUM = len(USER_DATA)
451
  if USER_NUM == 0:
452
  print("No user available, exiting...")
453
+ # 在异步函数中不能直接 exit(1),可以引发异常或返回特殊值
454
+ raise RuntimeError("No user available")
455
+
456
  MODELS = all_models
457
  print(f"启动完成,共配置 {USER_NUM} 个用户")
458
 
 
470
  return cookies
471
 
472
 
473
+ # 修改 get_user_data 以处理异步 refresh_token
474
+ async def get_user_data():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  global CURRENT_USER
476
  CURRENT_USER = (CURRENT_USER + 1) % USER_NUM
477
  print(f"使用配置 {CURRENT_USER+1}")
478
+
479
  # Akiru uzantajn datumojn
480
+ client, cookies, session_token, conversation_id, model_map, user_index = USER_DATA[CURRENT_USER]
481
+
482
  # Kontrolu ĉu la tokeno eksvalidiĝis, se jes, refreŝigu ĝin
483
  if is_token_expired(session_token):
484
  print(f"Cookie {CURRENT_USER+1}的token已过期或即将过期,正在刷新...")
485
+ new_token = await refresh_token(client, cookies)
486
  if new_token:
487
  # Ĝisdatigu la globale konservitan tokenon
488
+ USER_DATA[CURRENT_USER] = (client, cookies, new_token, conversation_id, model_map, user_index)
489
  session_token = new_token
490
  print(f"成功更新token: {session_token[:15]}...{session_token[-15:]}")
491
  else:
492
  print(f"警告:无法刷新Cookie {CURRENT_USER+1}的token,继续使用当前token")
493
+
494
+ return (client, cookies, session_token, conversation_id, model_map, user_index)
495
 
496
 
497
  def create_conversation(session, cookies, session_token, external_application_id=None, deployment_id=None):
 
655
 
656
  trace_id, sentry_trace = generate_trace_id()
657
 
658
+ # 计算输入token - 改为异步提交
659
+ print("\n----- 提交输入token计算任务 -----")
660
+ prompt_token_future = token_executor.submit(num_tokens_from_string, message, model)
661
+ print("输入token计算已提交到后台")
662
+
663
  completion_buffer = io.StringIO() # 收集所有输出用于计算token
664
 
665
  headers = {
 
729
  return ""
730
 
731
  def generate():
732
+ # 流式处理函数
733
  id = ""
734
  think_state = 2
735
 
736
+ # 开始流式输出
737
  yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {"role": "assistant"}}]}) + "\n\n"
738
 
739
+ # 处理API返回的流式内容
740
  for line in response.iter_lines():
741
  if line:
742
  decoded_line = line.decode("utf-8")
 
773
  except Exception as e:
774
  print(f"处理响应出错: {e}")
775
 
776
+ # 结束标记
777
  yield "data: " + json.dumps({"object": "chat.completion.chunk", "choices": [{"delta": {}, "finish_reason": "stop"}]}) + "\n\n"
778
  yield "data: [DONE]\n\n"
779
 
780
+ # 内容处理完毕后进行操作(其实不会执行到这里,因为响应已经流式传输给客户端)
781
+ try:
782
+ # 获取完整响应内容并计算输出Token
783
+ completion_text = completion_buffer.getvalue()
784
+ # 异步提交输出Token计算
785
+ completion_token_future = token_executor.submit(num_tokens_from_string, completion_text, model)
786
+ print("输出token计��已提交到后台")
787
+
788
+ # 保存对话历史并获取计算点数
789
+ _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
790
+
791
+ # 更新统计信息
792
+ update_model_stats(model, prompt_token_future, completion_token_future, compute_points_used)
793
+ except Exception as post_e:
794
+ print(f"流结束后处理出错: {post_e}")
795
 
796
+ # 返回流式响应
797
  return Response(generate(), mimetype="text/event-stream")
798
  except requests.exceptions.RequestException as e:
799
  error_details = str(e)
 
824
  USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
825
 
826
  # 保存到配置文件
827
+ try:
828
+ update_conversation_id(user_index, new_conversation_id)
829
+ except Exception as config_e:
830
+ print(f"更新配置文件失败但继续重试: {config_e}")
831
 
832
  # 修改payload使用新会话ID,并移除regenerate和editPrompt
833
  payload["deploymentConversationId"] = new_conversation_id
 
847
  )
848
 
849
  response.raise_for_status()
850
+
851
+ # 重新定义extract_segment函数(解决作用域问题)
852
+ def retry_extract_segment(line_data):
853
+ try:
854
+ data = json.loads(line_data)
855
+ if "segment" in data:
856
+ if isinstance(data["segment"], str):
857
+ return data["segment"]
858
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
859
+ return data["segment"]["segment"]
860
+ return ""
861
+ except:
862
+ return ""
863
+
864
  buffer = io.StringIO()
865
 
866
  for line in response.iter_lines():
867
  if line:
868
  decoded_line = line.decode("utf-8")
869
+ segment = retry_extract_segment(decoded_line)
870
  if segment:
871
  buffer.write(segment)
872
 
873
  response_content = buffer.getvalue()
874
 
875
+ # 异步计算输出token
876
+ completion_token_future = token_executor.submit(num_tokens_from_string, response_content, model)
877
+ print("重试后的输出token计算已提交到后台")
878
 
879
  # 保存对话历史并获取计算点数
880
  _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
881
 
882
  # 更新统计信息
883
+ update_model_stats(model, prompt_token_future, completion_token_future, compute_points_used)
884
+
885
+ # 获取最终结果用于返回
886
+ try:
887
+ prompt_tokens = prompt_token_future.result(timeout=10)
888
+ if isinstance(prompt_tokens, tuple):
889
+ prompt_tokens = prompt_tokens[0] # 提取token数值
890
+ completion_tokens = completion_token_future.result(timeout=10)
891
+ if isinstance(completion_tokens, tuple):
892
+ completion_tokens = completion_tokens[0] # 提取token数值
893
+ total_tokens = prompt_tokens + completion_tokens
894
+ except Exception as token_e:
895
+ print(f"获取token结果失败但继续返回: {token_e}")
896
+ prompt_tokens = 0
897
+ completion_tokens = 0
898
+ total_tokens = 0
899
 
900
  return jsonify({
901
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
912
  }],
913
  "usage": {
914
  "prompt_tokens": prompt_tokens,
915
+ "completion_tokens": completion_tokens,
916
+ "total_tokens": total_tokens
917
  }
918
  })
919
  except Exception as retry_e:
920
  print(f"重试失败: {retry_e}")
921
 
922
+ # 取消token计算任务,避免资源浪费
923
+ try:
924
+ if 'prompt_token_future' in locals() and not prompt_token_future.done():
925
+ prompt_token_future.cancel()
926
+ except Exception as cancel_e:
927
+ print(f"取消token计算任务失败: {cancel_e}")
928
+
929
  return jsonify({"error": f"Failed to send message: {error_details}"}), 500
930
 
931
 
 
951
 
952
  trace_id, sentry_trace = generate_trace_id()
953
 
954
+ # 计算输入token - 改为异步提交
955
+ print("\n----- 提交输入token计算任务 -----")
956
+ prompt_token_future = token_executor.submit(num_tokens_from_string, message, model)
957
+ print("输入token计算已提交到后台")
 
958
 
959
  headers = {
960
  "accept": "text/event-stream",
 
1009
  )
1010
 
1011
  response.raise_for_status()
 
1012
 
1013
  def extract_segment(line_data):
1014
  try:
 
1057
  think_content = think_buffer.getvalue()
1058
  response_content = content_buffer.getvalue()
1059
 
1060
+ # 异步计算输出token
1061
+ full_content = f"<think>\n{think_content}\n</think>\n{response_content}"
1062
+ completion_token_future = token_executor.submit(num_tokens_from_string, full_content, model)
1063
+ print("输出token计算已提交到后台")
1064
 
1065
  # 保存对话历史并获取计算点数
1066
  _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
1067
 
1068
  # 更新统计信息
1069
+ update_model_stats(model, prompt_token_future, completion_token_future, compute_points_used)
1070
+
1071
+ # 获取最终结果用于返回
1072
+ try:
1073
+ prompt_tokens = prompt_token_future.result(timeout=10)
1074
+ if isinstance(prompt_tokens, tuple):
1075
+ prompt_tokens = prompt_tokens[0] # 提取token数值
1076
+ completion_tokens = completion_token_future.result(timeout=10)
1077
+ if isinstance(completion_tokens, tuple):
1078
+ completion_tokens = completion_tokens[0] # 提取token数值
1079
+ total_tokens = prompt_tokens + completion_tokens
1080
+ except Exception as token_e:
1081
+ print(f"获取token结果失败但继续返回: {token_e}")
1082
+ prompt_tokens = 0
1083
+ completion_tokens = 0
1084
+ total_tokens = 0
1085
 
1086
  return jsonify({
1087
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
1098
  }],
1099
  "usage": {
1100
  "prompt_tokens": prompt_tokens,
1101
+ "completion_tokens": completion_tokens,
1102
+ "total_tokens": total_tokens
1103
  }
1104
  })
1105
  else:
1106
+ buffer = io.StringIO()
1107
+
1108
  for line in response.iter_lines():
1109
  if line:
1110
  decoded_line = line.decode("utf-8")
 
1114
 
1115
  response_content = buffer.getvalue()
1116
 
1117
+ # 异���计算输出token
1118
+ completion_token_future = token_executor.submit(num_tokens_from_string, response_content, model)
1119
+ print("输出token计算已提交到后台")
1120
 
1121
  # 保存对话历史并获取计算点数
1122
  _, compute_points_used = save_conversation_history(session, cookies, session_token, conversation_id)
1123
 
1124
  # 更新统计信息
1125
+ update_model_stats(model, prompt_token_future, completion_token_future, compute_points_used)
1126
+
1127
+ # 获取最终结果用于返回
1128
+ try:
1129
+ prompt_tokens = prompt_token_future.result(timeout=10)
1130
+ if isinstance(prompt_tokens, tuple):
1131
+ prompt_tokens = prompt_tokens[0] # 提取token数值
1132
+ completion_tokens = completion_token_future.result(timeout=10)
1133
+ if isinstance(completion_tokens, tuple):
1134
+ completion_tokens = completion_tokens[0] # 提取token数值
1135
+ total_tokens = prompt_tokens + completion_tokens
1136
+ except Exception as token_e:
1137
+ print(f"获取token结果失败但继续返回: {token_e}")
1138
+ prompt_tokens = 0
1139
+ completion_tokens = 0
1140
+ total_tokens = 0
1141
 
1142
  return jsonify({
1143
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
1154
  }],
1155
  "usage": {
1156
  "prompt_tokens": prompt_tokens,
1157
+ "completion_tokens": completion_tokens,
1158
+ "total_tokens": total_tokens
1159
  }
1160
  })
1161
  except requests.exceptions.RequestException as e:
 
1187
  USER_DATA[CURRENT_USER] = (session, cookies, session_token, new_conversation_id, model_map, user_index)
1188
 
1189
  # 保存到配置文件
1190
+ try:
1191
+ update_conversation_id(user_index, new_conversation_id)
1192
+ except Exception as config_e:
1193
+ print(f"更新配置文件失败但继续重试: {config_e}")
1194
 
1195
  # 修改payload使用新会话ID,并移除regenerate和editPrompt
1196
  payload["deploymentConversationId"] = new_conversation_id
 
1210
  )
1211
 
1212
  response.raise_for_status()
1213
+
1214
+ # 重新定义extract_segment函数(解决作用域问题)
1215
+ def retry_extract_segment(line_data):
1216
+ try:
1217
+ data = json.loads(line_data)
1218
+ if "segment" in data:
1219
+ if isinstance(data["segment"], str):
1220
+ return data["segment"]
1221
+ elif isinstance(data["segment"], dict) and "segment" in data["segment"]:
1222
+ return data["segment"]["segment"]
1223
+ return ""
1224
+ except:
1225
+ return ""
1226
+
1227
  buffer = io.StringIO()
1228
 
1229
  for line in response.iter_lines():
1230
  if line:
1231
  decoded_line = line.decode("utf-8")
1232
+ segment = retry_extract_segment(decoded_line)
1233
  if segment:
1234
  buffer.write(segment)
1235
 
1236
  response_content = buffer.getvalue()
1237
 
1238
+ # 异步计算输出token
1239
+ completion_token_future = token_executor.submit(num_tokens_from_string, response_content, model)
1240
+ print("重试后的输出token计算已提交到后台")
1241
 
1242
  # 保存对话历史并获取计算点数
1243
  _, compute_points_used = save_conversation_history(session, cookies, session_token, new_conversation_id)
1244
 
1245
  # 更新统计信息
1246
+ update_model_stats(model, prompt_token_future, completion_token_future, compute_points_used)
1247
+
1248
+ # 获取最终结果用于返回
1249
+ try:
1250
+ prompt_tokens = prompt_token_future.result(timeout=10)
1251
+ if isinstance(prompt_tokens, tuple):
1252
+ prompt_tokens = prompt_tokens[0] # 提取token数值
1253
+ completion_tokens = completion_token_future.result(timeout=10)
1254
+ if isinstance(completion_tokens, tuple):
1255
+ completion_tokens = completion_tokens[0] # 提取token数值
1256
+ total_tokens = prompt_tokens + completion_tokens
1257
+ except Exception as token_e:
1258
+ print(f"获取token结果失败但继续返回: {token_e}")
1259
+ prompt_tokens = 0
1260
+ completion_tokens = 0
1261
+ total_tokens = 0
1262
 
1263
  return jsonify({
1264
  "id": f"chatcmpl-{str(uuid.uuid4())}",
 
1275
  }],
1276
  "usage": {
1277
  "prompt_tokens": prompt_tokens,
1278
+ "completion_tokens": completion_tokens,
1279
+ "total_tokens": total_tokens
1280
  }
1281
  })
1282
  except Exception as retry_e:
 
1421
 
1422
 
1423
  # 更新模型使用统计
1424
+ def update_model_stats(model, prompt_token_future, completion_token_future, compute_points=None):
1425
  global model_usage_stats, total_tokens, model_usage_records
1426
 
1427
+ prompt_tokens = 0
1428
+ prompt_calc_method = "估算(获取失败)"
1429
+ try:
1430
+ # 在这里等待并获取输入 token 计算结果,设置超时
1431
+ print("等待输入 token 计算结果...")
1432
+ prompt_tokens, prompt_calc_method = prompt_token_future.result(timeout=20) # 超时时间设长一点
1433
+ print(f"输入 token 结果: {prompt_tokens} ({prompt_calc_method})")
1434
+ except TimeoutError:
1435
+ print("获取输入 token 计算结果超时")
1436
+ prompt_calc_method = "估算(超时)"
1437
+ # 估算 - 如果收到了原始输入,可以在这里估算
1438
+ except Exception as e:
1439
+ print(f"获取输入 token 计算结果失败: {e}")
1440
+ # 保持 0 和失败标记
1441
+
1442
+ completion_tokens = 0
1443
+ completion_calc_method = "估算(获取失败)"
1444
+ try:
1445
+ # 在这里等待并获取输出 token 计算结果,设置超时
1446
+ print("等待输出 token 计算结果...")
1447
+ completion_tokens, completion_calc_method = completion_token_future.result(timeout=20) # 超时时间设长一点
1448
+ print(f"输出 token 结果: {completion_tokens} ({completion_calc_method})")
1449
+ except TimeoutError:
1450
+ print("获取输出 token 计算结果超时")
1451
+ completion_calc_method = "估算(超时)"
1452
+ # 可以选择估算或保持 0
1453
+ except Exception as e:
1454
+ print(f"获取输出 token 计算结果失败: {e}")
1455
+ # 保持 0 和失败标记
1456
+
1457
  # 添加调用记录
1458
  # 获取UTC时间
1459
  utc_now = datetime.utcnow()
 
1466
  "call_time": call_time,
1467
  "prompt_tokens": prompt_tokens,
1468
  "completion_tokens": completion_tokens,
1469
+ "calculation_method": f"输入: {prompt_calc_method}, 输出: {completion_calc_method}",
1470
  "compute_points": compute_points
1471
  }
1472
  model_usage_records.append(record)
 
1495
  total_tokens["prompt"] += prompt_tokens
1496
  total_tokens["completion"] += completion_tokens
1497
  total_tokens["total"] += (prompt_tokens + completion_tokens)
1498
+
1499
+ print("模型统计信息已更新")
1500
 
1501
 
1502
  # 获取计算点信息
 
1674
  days = uptime.days
1675
  hours, remainder = divmod(uptime.seconds, 3600)
1676
  minutes, seconds = divmod(remainder, 60)
1677
+
1678
  if days > 0:
1679
  uptime_str = f"{days}天 {hours}小时 {minutes}分钟"
1680
  elif hours > 0:
templates/dashboard.html CHANGED
@@ -1,1032 +1,1032 @@
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: 4px 8px;
465
- border-radius: 4px;
466
- font-size: 0.85rem;
467
- font-weight: 500;
468
- }
469
-
470
- .token-method-exact {
471
- background-color: rgba(54, 211, 153, 0.2);
472
- color: #36d399;
473
- }
474
-
475
- .token-method-estimate {
476
- background-color: rgba(251, 189, 35, 0.2);
477
- color: #fbbd23;
478
- }
479
-
480
- /* 时间日期样式 */
481
- .datetime {
482
- font-family: 'Consolas', monospace;
483
- color: rgba(230, 230, 255, 0.8);
484
- font-size: 0.9rem;
485
- }
486
-
487
- /* 媒体查询 */
488
- @media (max-width: 768px) {
489
- .container {
490
- padding: 1rem;
491
- }
492
-
493
- .navbar {
494
- padding: 1rem;
495
- }
496
-
497
- .card {
498
- padding: 1rem;
499
- }
500
-
501
- .grid {
502
- grid-template-columns: 1fr;
503
- }
504
- }
505
-
506
- .token-model-table td, .token-model-table th {
507
- white-space: nowrap;
508
- }
509
-
510
- /* 开关按钮样式 */
511
- .toggle-switch-container {
512
- display: flex;
513
- align-items: center;
514
- gap: 10px;
515
- }
516
-
517
- .toggle-switch {
518
- position: relative;
519
- display: inline-block;
520
- width: 50px;
521
- height: 24px;
522
- }
523
-
524
- .toggle-switch input {
525
- opacity: 0;
526
- width: 0;
527
- height: 0;
528
- }
529
-
530
- .toggle-slider {
531
- position: absolute;
532
- cursor: pointer;
533
- top: 0;
534
- left: 0;
535
- right: 0;
536
- bottom: 0;
537
- background-color: rgba(100, 100, 150, 0.3);
538
- transition: .4s;
539
- border-radius: 24px;
540
- }
541
-
542
- .toggle-slider:before {
543
- position: absolute;
544
- content: "";
545
- height: 18px;
546
- width: 18px;
547
- left: 3px;
548
- bottom: 3px;
549
- background-color: #e6e6ff;
550
- transition: .4s;
551
- border-radius: 50%;
552
- }
553
-
554
- input:checked + .toggle-slider {
555
- background-color: var(--primary-color);
556
- }
557
-
558
- input:checked + .toggle-slider:before {
559
- transform: translateX(26px);
560
- }
561
-
562
- .toggle-status {
563
- font-weight: 600;
564
- }
565
-
566
- .info-text {
567
- font-size: 0.85rem;
568
- color: rgba(230, 230, 255, 0.7);
569
- }
570
-
571
- /* 通知样式 */
572
- .notification {
573
- position: fixed;
574
- top: 20px;
575
- right: 20px;
576
- padding: 12px 20px;
577
- border-radius: 8px;
578
- color: white;
579
- font-weight: 500;
580
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
581
- z-index: 1000;
582
- transform: translateY(-20px);
583
- opacity: 0;
584
- transition: all 0.3s ease;
585
- max-width: 300px;
586
- }
587
-
588
- .notification.show {
589
- transform: translateY(0);
590
- opacity: 1;
591
- }
592
-
593
- .notification.success {
594
- background-color: var(--success-color);
595
- }
596
-
597
- .notification.error {
598
- background-color: var(--error-color);
599
- }
600
-
601
- .notification.info {
602
- background-color: var(--accent-color);
603
- }
604
-
605
- /* 响应式样式 */
606
- @media (max-width: 768px) {
607
- .container {
608
- padding: 1rem;
609
- }
610
-
611
- .navbar {
612
- padding: 1rem;
613
- }
614
-
615
- .card {
616
- padding: 1rem;
617
- }
618
-
619
- .grid {
620
- grid-template-columns: 1fr;
621
- }
622
- }
623
- </style>
624
- </head>
625
- <body>
626
- <div class="grid-background"></div>
627
-
628
- <nav class="navbar">
629
- <a href="/" class="navbar-brand">
630
- <span class="navbar-logo">🤖</span>
631
- <span class="navbar-title">Abacus Chat Proxy</span>
632
- </a>
633
- <div class="navbar-actions">
634
- <a href="/logout" class="btn-logout">
635
- <span>退出</span>
636
- <span>↗</span>
637
- </a>
638
- </div>
639
- </nav>
640
-
641
- <div class="container">
642
- <div class="card">
643
- <div class="card-header">
644
- <h2 class="card-title">
645
- <span class="card-icon">📊</span>
646
- 系统状态
647
- </h2>
648
- </div>
649
- <div class="status-item">
650
- <span class="status-label">服务状态</span>
651
- <span class="status-value success">运行中</span>
652
- </div>
653
- <div class="status-item">
654
- <span class="status-label">运行时间</span>
655
- <span class="status-value">{{ uptime }}</span>
656
- </div>
657
- <div class="status-item">
658
- <span class="status-label">健康检查次数</span>
659
- <span class="status-value">{{ health_checks }}</span>
660
- </div>
661
- <div class="status-item">
662
- <span class="status-label">已配置用户数</span>
663
- <span class="status-value">{{ user_count }}</span>
664
- </div>
665
- <div class="status-item">
666
- <span class="status-label">可用模型</span>
667
- <div class="models-list">
668
- {% for model in models %}
669
- <span class="model-tag">{{ model }}</span>
670
- {% endfor %}
671
- </div>
672
- </div>
673
- </div>
674
-
675
- <div class="grid">
676
- <div class="card">
677
- <div class="card-header">
678
- <h2 class="card-title">
679
- <span class="card-icon">💰</span>
680
- 计算点总计
681
- </h2>
682
- </div>
683
- <div class="status-item">
684
- <span class="status-label">总计算点</span>
685
- <span class="status-value compute-points">{{ compute_points.total|int }}</span>
686
- </div>
687
- <div class="status-item">
688
- <span class="status-label">已使用</span>
689
- <span class="status-value compute-points">{{ compute_points.used|int }}</span>
690
- </div>
691
- <div class="status-item">
692
- <span class="status-label">剩余</span>
693
- <span class="status-value compute-points">{{ compute_points.left|int }}</span>
694
- </div>
695
- <div class="status-item">
696
- <span class="status-label">使用比例</span>
697
- <div style="width: 100%; text-align: right;">
698
- <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
699
- {{ compute_points.percentage }}%
700
- </span>
701
- <div class="progress-container">
702
- <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
703
- </div>
704
- </div>
705
- </div>
706
- {% if compute_points.last_update %}
707
- <div class="status-item">
708
- <span class="status-label">最后更新时间</span>
709
- <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
710
- </div>
711
- {% endif %}
712
- </div>
713
-
714
- <div class="card">
715
- <div class="card-header">
716
- <h2 class="card-title">
717
- <span class="card-icon">🔍</span>
718
- Token 使用统计
719
- </h2>
720
- </div>
721
- <div class="status-item">
722
- <span class="status-label">总输入Token</span>
723
- <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
724
- </div>
725
- <div class="status-item">
726
- <span class="status-label">总输出Token</span>
727
- <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
728
- </div>
729
- <div class="token-note">
730
- <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
731
- </div>
732
- <div class="table-container">
733
- <table class="data-table token-model-table">
734
- <thead>
735
- <tr>
736
- <th>模型</th>
737
- <th>调用次数</th>
738
- <th>输入Token</th>
739
- <th>输出Token</th>
740
- </tr>
741
- </thead>
742
- <tbody>
743
- {% for model, stats in model_stats.items() %}
744
- <tr>
745
- <td>{{ model }}</td>
746
- <td class="call-count">{{ stats.count }}</td>
747
- <td class="token-count">{{ stats.prompt_tokens|int }}</td>
748
- <td class="token-count">{{ stats.completion_tokens|int }}</td>
749
- </tr>
750
- {% endfor %}
751
- </tbody>
752
- </table>
753
- </div>
754
- </div>
755
- </div>
756
-
757
- {% if users_compute_points|length > 0 %}
758
- <div class="card">
759
- <div class="card-header">
760
- <h2 class="card-title">
761
- <span class="card-icon">👥</span>
762
- 用户计算点详情
763
- </h2>
764
- </div>
765
- <div class="table-container">
766
- <table class="data-table">
767
- <thead>
768
- <tr>
769
- <th>用户</th>
770
- <th>总计算点</th>
771
- <th>已使用</th>
772
- <th>剩余</th>
773
- <th>使用比例</th>
774
- </tr>
775
- </thead>
776
- <tbody>
777
- {% for user in users_compute_points %}
778
- <tr>
779
- <td>用户 {{ user.user_id }}</td>
780
- <td class="compute-points">{{ user.total|int }}</td>
781
- <td class="compute-points">{{ user.used|int }}</td>
782
- <td class="compute-points">{{ user.left|int }}</td>
783
- <td>
784
- <div style="width: 100%; position: relative;">
785
- <span class="status-value compute-points {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}">
786
- {{ user.percentage }}%
787
- </span>
788
- <div class="progress-container">
789
- <div class="progress-bar {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}" style="width: {{ user.percentage }}%"></div>
790
- </div>
791
- </div>
792
- </td>
793
- </tr>
794
- {% endfor %}
795
- </tbody>
796
- </table>
797
- </div>
798
- </div>
799
- {% endif %}
800
-
801
- <div class="card">
802
- <div class="card-header">
803
- <h2 class="card-title">
804
- <span class="card-icon">📊</span>
805
- 计算点使用日志
806
- </h2>
807
- </div>
808
- <div class="table-container">
809
- <table class="data-table">
810
- <thead>
811
- <tr>
812
- {% for key, value in compute_points_log.columns.items() %}
813
- <th>{{ value }}</th>
814
- {% endfor %}
815
- </tr>
816
- </thead>
817
- <tbody>
818
- {% for entry in compute_points_log.log %}
819
- <tr>
820
- {% for key, value in compute_points_log.columns.items() %}
821
- <td class="compute-points">{{ entry.get(key, 0) }}</td>
822
- {% endfor %}
823
- </tr>
824
- {% endfor %}
825
- </tbody>
826
- </table>
827
- </div>
828
- </div>
829
-
830
- <div class="card">
831
- <div class="card-header">
832
- <h2 class="card-title">
833
- <span class="card-icon">📈</span>
834
- 模型调用记录
835
- </h2>
836
- <button id="toggleModelStats" class="btn-toggle">显示全部</button>
837
- </div>
838
- <div class="table-container">
839
- <table class="data-table">
840
- <thead>
841
- <tr>
842
- <th>调用时间 (北京时间)</th>
843
- <th>模型</th>
844
- <th>输入Token</th>
845
- <th>输出Token</th>
846
- <th>总Token</th>
847
- <th>计算方式</th>
848
- <th>计算点数</th>
849
- </tr>
850
- </thead>
851
- <tbody>
852
- {% for record in model_usage_records|reverse %}
853
- <tr class="model-row {% if loop.index > 10 %}hidden-model{% endif %}">
854
- <td class="datetime">{{ record.call_time }}</td>
855
- <td>{{ record.model }}</td>
856
- <td class="token-count">{{ record.prompt_tokens }}</td>
857
- <td class="token-count">{{ record.completion_tokens }}</td>
858
- <td>{{ record.prompt_tokens + record.completion_tokens }}</td>
859
- <td>
860
- {% if record.calculation_method == "精确" %}
861
- <span class="token-method token-method-exact">精确</span>
862
- {% else %}
863
- <span class="token-method token-method-estimate">估算</span>
864
- {% endif %}
865
- </td>
866
- <td>{{ record.compute_points if record.compute_points is not none and record.compute_points != 0 else 'null' }}</td>
867
- </tr>
868
- {% endfor %}
869
- </tbody>
870
- </table>
871
- <div class="token-note">
872
- <small>* Token计算方式:<span class="token-method token-method-exact">精确</span> 表示调用官方API精确计算,<span class="token-method token-method-estimate">估算</span> 表示使用gpt-4o模型估算。所有统计数据仅供参考,不代表实际计费标准。</small>
873
- </div>
874
- </div>
875
- </div>
876
-
877
- <div class="card">
878
- <div class="card-header">
879
- <h2 class="card-title">
880
- <span class="card-icon">📡</span>
881
- API 端点
882
- </h2>
883
- </div>
884
- <div class="endpoint-item">
885
- <p>获取模型列表:</p>
886
- {% if space_url %}
887
- <a href="{{ space_url }}/v1/models" class="endpoint-url" target="_blank">GET {{ space_url }}/v1/models</a>
888
- {% else %}
889
- <a href="/v1/models" class="endpoint-url" target="_blank">GET /v1/models</a>
890
- {% endif %}
891
- </div>
892
- <div class="endpoint-item">
893
- <p>聊天补全:</p>
894
- {% if space_url %}
895
- <code class="endpoint-url">POST {{ space_url }}/v1/chat/completions</code>
896
- {% else %}
897
- <code class="endpoint-url">POST /v1/chat/completions</code>
898
- {% endif %}
899
- </div>
900
- <div class="endpoint-item">
901
- <p>健康检查:</p>
902
- {% if space_url %}
903
- <a href="{{ space_url }}/health" class="endpoint-url" target="_blank">GET {{ space_url }}/health</a>
904
- {% else %}
905
- <a href="/health" class="endpoint-url" target="_blank">GET /health</a>
906
- {% endif %}
907
- </div>
908
- </div>
909
-
910
- <div class="footer">
911
- <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
912
- </div>
913
- </div>
914
-
915
- <a href="#" class="float-btn" title="回到顶部">↑</a>
916
-
917
- <script>
918
- // 回到顶部按钮
919
- document.querySelector('.float-btn').addEventListener('click', (e) => {
920
- e.preventDefault();
921
- window.scrollTo({ top: 0, behavior: 'smooth' });
922
- });
923
-
924
- // 显示/隐藏回到顶部按钮
925
- window.addEventListener('scroll', () => {
926
- const floatBtn = document.querySelector('.float-btn');
927
- if (window.pageYOffset > 300) {
928
- floatBtn.style.opacity = '1';
929
- } else {
930
- floatBtn.style.opacity = '0';
931
- }
932
- });
933
-
934
- // 初始化隐藏回到顶部按钮
935
- document.querySelector('.float-btn').style.opacity = '0';
936
-
937
- // 模型统计折叠功能
938
- const toggleBtn = document.getElementById('toggleModelStats');
939
- const hiddenModels = document.querySelectorAll('.hidden-model');
940
- let isExpanded = false;
941
-
942
- if (toggleBtn) {
943
- toggleBtn.addEventListener('click', () => {
944
- hiddenModels.forEach(model => {
945
- model.classList.toggle('hidden-model');
946
- });
947
-
948
- isExpanded = !isExpanded;
949
- toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
950
- });
951
- }
952
-
953
- document.addEventListener('DOMContentLoaded', function() {
954
- initCharts();
955
-
956
- // 显示/隐藏更多模型使用记录
957
- const toggleModelStats = document.getElementById('toggleModelStats');
958
- if (toggleModelStats) {
959
- toggleModelStats.addEventListener('click', function() {
960
- const hiddenRows = document.querySelectorAll('.hidden-model');
961
- hiddenRows.forEach(row => {
962
- row.classList.toggle('show-model');
963
- });
964
- toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部';
965
- });
966
- }
967
-
968
- // 处理计算点数记录开关
969
- const computePointToggle = document.getElementById('compute-point-toggle');
970
- const computePointStatus = document.getElementById('compute-point-status');
971
-
972
- if (computePointToggle && computePointStatus) {
973
- computePointToggle.addEventListener('change', function() {
974
- const isChecked = this.checked;
975
- computePointStatus.textContent = isChecked ? '开启' : '关闭';
976
-
977
- // 发送更新请求到后端
978
- fetch('/update_compute_point_toggle', {
979
- method: 'POST',
980
- headers: {
981
- 'Content-Type': 'application/json',
982
- },
983
- body: JSON.stringify({ always_display: isChecked })
984
- })
985
- .then(response => response.json())
986
- .then(data => {
987
- if (data.success) {
988
- // 显示成功提示
989
- showNotification(isChecked ? '已开启计算点数记录功能' : '已关闭计算点数记录功能', 'success');
990
- } else {
991
- // 显示错误提示
992
- showNotification('设置更新失败: ' + data.error, 'error');
993
- // 回滚UI状态
994
- computePointToggle.checked = !isChecked;
995
- computePointStatus.textContent = !isChecked ? '开启' : '关闭';
996
- }
997
- })
998
- .catch(error => {
999
- console.error('更新设置出错:', error);
1000
- showNotification('更新设置失败,请重试', 'error');
1001
- // 回滚UI状态
1002
- computePointToggle.checked = !isChecked;
1003
- computePointStatus.textContent = !isChecked ? '开启' : '关闭';
1004
- });
1005
- });
1006
- }
1007
- });
1008
-
1009
- // 通知函数
1010
- function showNotification(message, type = 'info') {
1011
- const notification = document.createElement('div');
1012
- notification.className = `notification ${type}`;
1013
- notification.textContent = message;
1014
-
1015
- document.body.appendChild(notification);
1016
-
1017
- // 显示动画
1018
- setTimeout(() => {
1019
- notification.classList.add('show');
1020
- }, 10);
1021
-
1022
- // 3秒后淡出
1023
- setTimeout(() => {
1024
- notification.classList.remove('show');
1025
- setTimeout(() => {
1026
- notification.remove();
1027
- }, 300);
1028
- }, 3000);
1029
- }
1030
- </script>
1031
- </body>
1032
  </html>
 
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: 4px 8px;
465
+ border-radius: 4px;
466
+ font-size: 0.85rem;
467
+ font-weight: 500;
468
+ }
469
+
470
+ .token-method-exact {
471
+ background-color: rgba(54, 211, 153, 0.2);
472
+ color: #36d399;
473
+ }
474
+
475
+ .token-method-estimate {
476
+ background-color: rgba(251, 189, 35, 0.2);
477
+ color: #fbbd23;
478
+ }
479
+
480
+ /* 时间日期样式 */
481
+ .datetime {
482
+ font-family: 'Consolas', monospace;
483
+ color: rgba(230, 230, 255, 0.8);
484
+ font-size: 0.9rem;
485
+ }
486
+
487
+ /* 媒体查询 */
488
+ @media (max-width: 768px) {
489
+ .container {
490
+ padding: 1rem;
491
+ }
492
+
493
+ .navbar {
494
+ padding: 1rem;
495
+ }
496
+
497
+ .card {
498
+ padding: 1rem;
499
+ }
500
+
501
+ .grid {
502
+ grid-template-columns: 1fr;
503
+ }
504
+ }
505
+
506
+ .token-model-table td, .token-model-table th {
507
+ white-space: nowrap;
508
+ }
509
+
510
+ /* 开关按钮样式 */
511
+ .toggle-switch-container {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 10px;
515
+ }
516
+
517
+ .toggle-switch {
518
+ position: relative;
519
+ display: inline-block;
520
+ width: 50px;
521
+ height: 24px;
522
+ }
523
+
524
+ .toggle-switch input {
525
+ opacity: 0;
526
+ width: 0;
527
+ height: 0;
528
+ }
529
+
530
+ .toggle-slider {
531
+ position: absolute;
532
+ cursor: pointer;
533
+ top: 0;
534
+ left: 0;
535
+ right: 0;
536
+ bottom: 0;
537
+ background-color: rgba(100, 100, 150, 0.3);
538
+ transition: .4s;
539
+ border-radius: 24px;
540
+ }
541
+
542
+ .toggle-slider:before {
543
+ position: absolute;
544
+ content: "";
545
+ height: 18px;
546
+ width: 18px;
547
+ left: 3px;
548
+ bottom: 3px;
549
+ background-color: #e6e6ff;
550
+ transition: .4s;
551
+ border-radius: 50%;
552
+ }
553
+
554
+ input:checked + .toggle-slider {
555
+ background-color: var(--primary-color);
556
+ }
557
+
558
+ input:checked + .toggle-slider:before {
559
+ transform: translateX(26px);
560
+ }
561
+
562
+ .toggle-status {
563
+ font-weight: 600;
564
+ }
565
+
566
+ .info-text {
567
+ font-size: 0.85rem;
568
+ color: rgba(230, 230, 255, 0.7);
569
+ }
570
+
571
+ /* 通知样式 */
572
+ .notification {
573
+ position: fixed;
574
+ top: 20px;
575
+ right: 20px;
576
+ padding: 12px 20px;
577
+ border-radius: 8px;
578
+ color: white;
579
+ font-weight: 500;
580
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
581
+ z-index: 1000;
582
+ transform: translateY(-20px);
583
+ opacity: 0;
584
+ transition: all 0.3s ease;
585
+ max-width: 300px;
586
+ }
587
+
588
+ .notification.show {
589
+ transform: translateY(0);
590
+ opacity: 1;
591
+ }
592
+
593
+ .notification.success {
594
+ background-color: var(--success-color);
595
+ }
596
+
597
+ .notification.error {
598
+ background-color: var(--error-color);
599
+ }
600
+
601
+ .notification.info {
602
+ background-color: var(--accent-color);
603
+ }
604
+
605
+ /* 响应式样式 */
606
+ @media (max-width: 768px) {
607
+ .container {
608
+ padding: 1rem;
609
+ }
610
+
611
+ .navbar {
612
+ padding: 1rem;
613
+ }
614
+
615
+ .card {
616
+ padding: 1rem;
617
+ }
618
+
619
+ .grid {
620
+ grid-template-columns: 1fr;
621
+ }
622
+ }
623
+ </style>
624
+ </head>
625
+ <body>
626
+ <div class="grid-background"></div>
627
+
628
+ <nav class="navbar">
629
+ <a href="/" class="navbar-brand">
630
+ <span class="navbar-logo">🤖</span>
631
+ <span class="navbar-title">Abacus Chat Proxy</span>
632
+ </a>
633
+ <div class="navbar-actions">
634
+ <a href="/logout" class="btn-logout">
635
+ <span>退出</span>
636
+ <span>↗</span>
637
+ </a>
638
+ </div>
639
+ </nav>
640
+
641
+ <div class="container">
642
+ <div class="card">
643
+ <div class="card-header">
644
+ <h2 class="card-title">
645
+ <span class="card-icon">📊</span>
646
+ 系统状态
647
+ </h2>
648
+ </div>
649
+ <div class="status-item">
650
+ <span class="status-label">服务状态</span>
651
+ <span class="status-value success">运行中</span>
652
+ </div>
653
+ <div class="status-item">
654
+ <span class="status-label">运行时间</span>
655
+ <span class="status-value">{{ uptime }}</span>
656
+ </div>
657
+ <div class="status-item">
658
+ <span class="status-label">健康检查次数</span>
659
+ <span class="status-value">{{ health_checks }}</span>
660
+ </div>
661
+ <div class="status-item">
662
+ <span class="status-label">已配置用户数</span>
663
+ <span class="status-value">{{ user_count }}</span>
664
+ </div>
665
+ <div class="status-item">
666
+ <span class="status-label">可用模型</span>
667
+ <div class="models-list">
668
+ {% for model in models %}
669
+ <span class="model-tag">{{ model }}</span>
670
+ {% endfor %}
671
+ </div>
672
+ </div>
673
+ </div>
674
+
675
+ <div class="grid">
676
+ <div class="card">
677
+ <div class="card-header">
678
+ <h2 class="card-title">
679
+ <span class="card-icon">💰</span>
680
+ 计算点总计
681
+ </h2>
682
+ </div>
683
+ <div class="status-item">
684
+ <span class="status-label">总计算点</span>
685
+ <span class="status-value compute-points">{{ compute_points.total|int }}</span>
686
+ </div>
687
+ <div class="status-item">
688
+ <span class="status-label">已使用</span>
689
+ <span class="status-value compute-points">{{ compute_points.used|int }}</span>
690
+ </div>
691
+ <div class="status-item">
692
+ <span class="status-label">剩余</span>
693
+ <span class="status-value compute-points">{{ compute_points.left|int }}</span>
694
+ </div>
695
+ <div class="status-item">
696
+ <span class="status-label">使用比例</span>
697
+ <div style="width: 100%; text-align: right;">
698
+ <span class="status-value compute-points {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}">
699
+ {{ compute_points.percentage }}%
700
+ </span>
701
+ <div class="progress-container">
702
+ <div class="progress-bar {% if compute_points.percentage > 80 %}danger{% elif compute_points.percentage > 50 %}warning{% endif %}" style="width: {{ compute_points.percentage }}%"></div>
703
+ </div>
704
+ </div>
705
+ </div>
706
+ {% if compute_points.last_update %}
707
+ <div class="status-item">
708
+ <span class="status-label">最后更新时间</span>
709
+ <span class="status-value">{{ compute_points.last_update.strftime('%Y-%m-%d %H:%M:%S') }}</span>
710
+ </div>
711
+ {% endif %}
712
+ </div>
713
+
714
+ <div class="card">
715
+ <div class="card-header">
716
+ <h2 class="card-title">
717
+ <span class="card-icon">🔍</span>
718
+ Token 使用统计
719
+ </h2>
720
+ </div>
721
+ <div class="status-item">
722
+ <span class="status-label">总输入Token</span>
723
+ <span class="status-value token-count">{{ total_tokens.prompt|int }}</span>
724
+ </div>
725
+ <div class="status-item">
726
+ <span class="status-label">总输出Token</span>
727
+ <span class="status-value token-count">{{ total_tokens.completion|int }}</span>
728
+ </div>
729
+ <div class="token-note">
730
+ <small>* 以上数据仅统计通过本代理使用的token数量,不包含在Abacus官网直接使用的token。数值为粗略估计,可能与实际计费有差异。</small>
731
+ </div>
732
+ <div class="table-container">
733
+ <table class="data-table token-model-table">
734
+ <thead>
735
+ <tr>
736
+ <th>模型</th>
737
+ <th>调用次数</th>
738
+ <th>输入Token</th>
739
+ <th>输出Token</th>
740
+ </tr>
741
+ </thead>
742
+ <tbody>
743
+ {% for model, stats in model_stats.items() %}
744
+ <tr>
745
+ <td>{{ model }}</td>
746
+ <td class="call-count">{{ stats.count }}</td>
747
+ <td class="token-count">{{ stats.prompt_tokens|int }}</td>
748
+ <td class="token-count">{{ stats.completion_tokens|int }}</td>
749
+ </tr>
750
+ {% endfor %}
751
+ </tbody>
752
+ </table>
753
+ </div>
754
+ </div>
755
+ </div>
756
+
757
+ {% if users_compute_points|length > 0 %}
758
+ <div class="card">
759
+ <div class="card-header">
760
+ <h2 class="card-title">
761
+ <span class="card-icon">👥</span>
762
+ 用户计算点详情
763
+ </h2>
764
+ </div>
765
+ <div class="table-container">
766
+ <table class="data-table">
767
+ <thead>
768
+ <tr>
769
+ <th>用户</th>
770
+ <th>总计算点</th>
771
+ <th>已使用</th>
772
+ <th>剩余</th>
773
+ <th>使用比例</th>
774
+ </tr>
775
+ </thead>
776
+ <tbody>
777
+ {% for user in users_compute_points %}
778
+ <tr>
779
+ <td>用户 {{ user.user_id }}</td>
780
+ <td class="compute-points">{{ user.total|int }}</td>
781
+ <td class="compute-points">{{ user.used|int }}</td>
782
+ <td class="compute-points">{{ user.left|int }}</td>
783
+ <td>
784
+ <div style="width: 100%; position: relative;">
785
+ <span class="status-value compute-points {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}">
786
+ {{ user.percentage }}%
787
+ </span>
788
+ <div class="progress-container">
789
+ <div class="progress-bar {% if user.percentage > 80 %}danger{% elif user.percentage > 50 %}warning{% endif %}" style="width: {{ user.percentage }}%"></div>
790
+ </div>
791
+ </div>
792
+ </td>
793
+ </tr>
794
+ {% endfor %}
795
+ </tbody>
796
+ </table>
797
+ </div>
798
+ </div>
799
+ {% endif %}
800
+
801
+ <div class="card">
802
+ <div class="card-header">
803
+ <h2 class="card-title">
804
+ <span class="card-icon">📊</span>
805
+ 计算点使用日志
806
+ </h2>
807
+ </div>
808
+ <div class="table-container">
809
+ <table class="data-table">
810
+ <thead>
811
+ <tr>
812
+ {% for key, value in compute_points_log.columns.items() %}
813
+ <th>{{ value }}</th>
814
+ {% endfor %}
815
+ </tr>
816
+ </thead>
817
+ <tbody>
818
+ {% for entry in compute_points_log.log %}
819
+ <tr>
820
+ {% for key, value in compute_points_log.columns.items() %}
821
+ <td class="compute-points">{{ entry.get(key, 0) }}</td>
822
+ {% endfor %}
823
+ </tr>
824
+ {% endfor %}
825
+ </tbody>
826
+ </table>
827
+ </div>
828
+ </div>
829
+
830
+ <div class="card">
831
+ <div class="card-header">
832
+ <h2 class="card-title">
833
+ <span class="card-icon">📈</span>
834
+ 模型调用记录
835
+ </h2>
836
+ <button id="toggleModelStats" class="btn-toggle">显示全部</button>
837
+ </div>
838
+ <div class="table-container">
839
+ <table class="data-table">
840
+ <thead>
841
+ <tr>
842
+ <th>调用时间 (北京时间)</th>
843
+ <th>模型</th>
844
+ <th>输入Token</th>
845
+ <th>输出Token</th>
846
+ <th>总Token</th>
847
+ <th>计算方式</th>
848
+ <th>计算点数</th>
849
+ </tr>
850
+ </thead>
851
+ <tbody>
852
+ {% for record in model_usage_records|reverse %}
853
+ <tr class="model-row {% if loop.index > 10 %}hidden-model{% endif %}">
854
+ <td class="datetime">{{ record.call_time }}</td>
855
+ <td>{{ record.model }}</td>
856
+ <td class="token-count">{{ record.prompt_tokens }}</td>
857
+ <td class="token-count">{{ record.completion_tokens }}</td>
858
+ <td>{{ record.prompt_tokens + record.completion_tokens }}</td>
859
+ <td>
860
+ {% if record.calculation_method == "精确" %}
861
+ <span class="token-method token-method-exact">精确</span>
862
+ {% else %}
863
+ <span class="token-method token-method-estimate">估算</span>
864
+ {% endif %}
865
+ </td>
866
+ <td>{{ record.compute_points if record.compute_points is not none and record.compute_points != 0 else 'null' }}</td>
867
+ </tr>
868
+ {% endfor %}
869
+ </tbody>
870
+ </table>
871
+ <div class="token-note">
872
+ <small>* Token计算方式:<span class="token-method token-method-exact">精确</span> 表示调用官方API精确计算,<span class="token-method token-method-estimate">估算</span> 表示使用gpt-4o模型估算。所有统计数据仅供参考,不代表实际计费标准。</small>
873
+ </div>
874
+ </div>
875
+ </div>
876
+
877
+ <div class="card">
878
+ <div class="card-header">
879
+ <h2 class="card-title">
880
+ <span class="card-icon">📡</span>
881
+ API 端点
882
+ </h2>
883
+ </div>
884
+ <div class="endpoint-item">
885
+ <p>获取模型列表:</p>
886
+ {% if space_url %}
887
+ <a href="{{ space_url }}/v1/models" class="endpoint-url" target="_blank">GET {{ space_url }}/v1/models</a>
888
+ {% else %}
889
+ <a href="/v1/models" class="endpoint-url" target="_blank">GET /v1/models</a>
890
+ {% endif %}
891
+ </div>
892
+ <div class="endpoint-item">
893
+ <p>聊天补全:</p>
894
+ {% if space_url %}
895
+ <code class="endpoint-url">POST {{ space_url }}/v1/chat/completions</code>
896
+ {% else %}
897
+ <code class="endpoint-url">POST /v1/chat/completions</code>
898
+ {% endif %}
899
+ </div>
900
+ <div class="endpoint-item">
901
+ <p>健康检查:</p>
902
+ {% if space_url %}
903
+ <a href="{{ space_url }}/health" class="endpoint-url" target="_blank">GET {{ space_url }}/health</a>
904
+ {% else %}
905
+ <a href="/health" class="endpoint-url" target="_blank">GET /health</a>
906
+ {% endif %}
907
+ </div>
908
+ </div>
909
+
910
+ <div class="footer">
911
+ <p>© {{ year }} Abacus Chat Proxy. 保持简单,保持可靠。</p>
912
+ </div>
913
+ </div>
914
+
915
+ <a href="#" class="float-btn" title="回到顶部">↑</a>
916
+
917
+ <script>
918
+ // 回到顶部按钮
919
+ document.querySelector('.float-btn').addEventListener('click', (e) => {
920
+ e.preventDefault();
921
+ window.scrollTo({ top: 0, behavior: 'smooth' });
922
+ });
923
+
924
+ // 显示/隐藏回到顶部按钮
925
+ window.addEventListener('scroll', () => {
926
+ const floatBtn = document.querySelector('.float-btn');
927
+ if (window.pageYOffset > 300) {
928
+ floatBtn.style.opacity = '1';
929
+ } else {
930
+ floatBtn.style.opacity = '0';
931
+ }
932
+ });
933
+
934
+ // 初始化隐藏回到顶部按钮
935
+ document.querySelector('.float-btn').style.opacity = '0';
936
+
937
+ // 模型统计折叠功能
938
+ const toggleBtn = document.getElementById('toggleModelStats');
939
+ const hiddenModels = document.querySelectorAll('.hidden-model');
940
+ let isExpanded = false;
941
+
942
+ if (toggleBtn) {
943
+ toggleBtn.addEventListener('click', () => {
944
+ hiddenModels.forEach(model => {
945
+ model.classList.toggle('hidden-model');
946
+ });
947
+
948
+ isExpanded = !isExpanded;
949
+ toggleBtn.textContent = isExpanded ? '隐藏部分' : '显示全部';
950
+ });
951
+ }
952
+
953
+ document.addEventListener('DOMContentLoaded', function() {
954
+ initCharts();
955
+
956
+ // 显示/隐藏更多模型使用记录
957
+ const toggleModelStats = document.getElementById('toggleModelStats');
958
+ if (toggleModelStats) {
959
+ toggleModelStats.addEventListener('click', function() {
960
+ const hiddenRows = document.querySelectorAll('.hidden-model');
961
+ hiddenRows.forEach(row => {
962
+ row.classList.toggle('show-model');
963
+ });
964
+ toggleModelStats.textContent = toggleModelStats.textContent === '显示全部' ? '隐藏部分' : '显示全部';
965
+ });
966
+ }
967
+
968
+ // 处理计算点数记录开关
969
+ const computePointToggle = document.getElementById('compute-point-toggle');
970
+ const computePointStatus = document.getElementById('compute-point-status');
971
+
972
+ if (computePointToggle && computePointStatus) {
973
+ computePointToggle.addEventListener('change', function() {
974
+ const isChecked = this.checked;
975
+ computePointStatus.textContent = isChecked ? '开启' : '关闭';
976
+
977
+ // 发送更新请求到后端
978
+ fetch('/update_compute_point_toggle', {
979
+ method: 'POST',
980
+ headers: {
981
+ 'Content-Type': 'application/json',
982
+ },
983
+ body: JSON.stringify({ always_display: isChecked })
984
+ })
985
+ .then(response => response.json())
986
+ .then(data => {
987
+ if (data.success) {
988
+ // 显示成功提示
989
+ showNotification(isChecked ? '已开启计算点数记录功能' : '已关闭计算点数记录功能', 'success');
990
+ } else {
991
+ // 显示错误提示
992
+ showNotification('设置更新失败: ' + data.error, 'error');
993
+ // 回滚UI状态
994
+ computePointToggle.checked = !isChecked;
995
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
996
+ }
997
+ })
998
+ .catch(error => {
999
+ console.error('更新设置出错:', error);
1000
+ showNotification('更新设置失败,请重试', 'error');
1001
+ // 回滚UI状态
1002
+ computePointToggle.checked = !isChecked;
1003
+ computePointStatus.textContent = !isChecked ? '开启' : '关闭';
1004
+ });
1005
+ });
1006
+ }
1007
+ });
1008
+
1009
+ // 通知函数
1010
+ function showNotification(message, type = 'info') {
1011
+ const notification = document.createElement('div');
1012
+ notification.className = `notification ${type}`;
1013
+ notification.textContent = message;
1014
+
1015
+ document.body.appendChild(notification);
1016
+
1017
+ // 显示动画
1018
+ setTimeout(() => {
1019
+ notification.classList.add('show');
1020
+ }, 10);
1021
+
1022
+ // 3秒后淡出
1023
+ setTimeout(() => {
1024
+ notification.classList.remove('show');
1025
+ setTimeout(() => {
1026
+ notification.remove();
1027
+ }, 300);
1028
+ }, 3000);
1029
+ }
1030
+ </script>
1031
+ </body>
1032
  </html>
templates/login.html CHANGED
@@ -1,462 +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>
 
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>