Files changed (3) hide show
  1. app.py +38 -92
  2. templates/login.html +0 -77
  3. templates/manager.html +0 -1018
app.py CHANGED
@@ -7,13 +7,18 @@ import sys
7
  import inspect
8
  import secrets
9
  from loguru import logger
10
- from pathlib import Path
11
 
12
  import requests
13
  from flask import Flask, request, Response, jsonify, stream_with_context, render_template, redirect, session
14
  from curl_cffi import requests as curl_requests
15
  from werkzeug.middleware.proxy_fix import ProxyFix
16
 
 
 
 
 
 
17
  class Logger:
18
  def __init__(self, level="INFO", colorize=True, format=None):
19
  logger.remove()
@@ -81,6 +86,7 @@ class Logger:
81
 
82
  logger = Logger(level="INFO")
83
 
 
84
  CONFIG = {
85
  "MODELS": {
86
  'grok-2': 'grok-latest',
@@ -90,38 +96,37 @@ CONFIG = {
90
  "grok-3-search": "grok-3",
91
  "grok-3-imageGen": "grok-3",
92
  "grok-3-deepsearch": "grok-3",
93
- "grok-3-deepersearch": "grok-3",
94
  "grok-3-reasoning": "grok-3"
95
  },
96
  "API": {
97
- "IS_TEMP_CONVERSATION": os.environ.get("IS_TEMP_CONVERSATION", "true").lower() == "true",
98
- "IS_CUSTOM_SSO": os.environ.get("IS_CUSTOM_SSO", "false").lower() == "true",
99
  "BASE_URL": "https://grok.com",
100
- "API_KEY": os.environ.get("API_KEY", "sk-123456"),
101
  "SIGNATURE_COOKIE": None,
102
- "PICGO_KEY": os.environ.get("PICGO_KEY") or None,
103
- "TUMY_KEY": os.environ.get("TUMY_KEY") or None,
104
  "RETRY_TIME": 1000,
105
- "PROXY": os.environ.get("PROXY") or None
106
  },
107
  "ADMIN": {
108
- "MANAGER_SWITCH": os.environ.get("MANAGER_SWITCH") or None,
109
- "PASSWORD": os.environ.get("ADMINPASSWORD") or None
110
  },
111
  "SERVER": {
112
  "COOKIE": None,
113
- "CF_CLEARANCE":os.environ.get("CF_CLEARANCE") or None,
114
- "PORT": int(os.environ.get("PORT", 5200))
115
  },
116
  "RETRY": {
117
  "RETRYSWITCH": False,
118
  "MAX_ATTEMPTS": 2
119
  },
120
- "SHOW_THINKING": os.environ.get("SHOW_THINKING") == "true",
121
  "IS_THINKING": False,
122
  "IS_IMG_GEN": False,
123
  "IS_IMG_GEN2": False,
124
- "ISSHOW_SEARCH_RESULTS": os.environ.get("ISSHOW_SEARCH_RESULTS", "true").lower() == "true"
125
  }
126
 
127
 
@@ -140,8 +145,7 @@ DEFAULT_HEADERS = {
140
  'Sec-Fetch-Dest': 'empty',
141
  'Sec-Fetch-Mode': 'cors',
142
  'Sec-Fetch-Site': 'same-origin',
143
- 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c',
144
- 'x-statsig-id': 'ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk='# 暂用固定x-statsig-id,失效再看
145
  }
146
 
147
  class AuthTokenManager:
@@ -163,10 +167,6 @@ class AuthTokenManager:
163
  "RequestFrequency": 10,
164
  "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
165
  },
166
- "grok-3-deepersearch": {
167
- "RequestFrequency": 3,
168
- "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
169
- },
170
  "grok-3-reasoning": {
171
  "RequestFrequency": 10,
172
  "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
@@ -174,25 +174,8 @@ class AuthTokenManager:
174
  }
175
  self.token_reset_switch = False
176
  self.token_reset_timer = None
177
- self.load_token_status() # 加载令牌状态
178
- def save_token_status(self):
179
- try:
180
- with open(CONFIG["TOKEN_STATUS_FILE"], 'w', encoding='utf-8') as f:
181
- json.dump(self.token_status_map, f, indent=2, ensure_ascii=False)
182
- logger.info("令牌状态已保存到配置文件", "TokenManager")
183
- except Exception as error:
184
- logger.error(f"保存令牌状态失败: {str(error)}", "TokenManager")
185
-
186
- def load_token_status(self):
187
- try:
188
- token_status_file = Path(CONFIG["TOKEN_STATUS_FILE"])
189
- if token_status_file.exists():
190
- with open(token_status_file, 'r', encoding='utf-8') as f:
191
- self.token_status_map = json.load(f)
192
- logger.info("已从配置文件加载令牌状态", "TokenManager")
193
- except Exception as error:
194
- logger.error(f"加载令牌状态失败: {str(error)}", "TokenManager")
195
- def add_token(self, token,isinitialization=False):
196
  sso = token.split("sso=")[1].split(";")[0]
197
  for model in self.model_config.keys():
198
  if model not in self.token_model_map:
@@ -216,8 +199,6 @@ class AuthTokenManager:
216
  "invalidatedTime": None,
217
  "totalRequestCount": 0
218
  }
219
- if not isinitialization:
220
- self.save_token_status()
221
 
222
  def set_token(self, token):
223
  models = list(self.model_config.keys())
@@ -243,8 +224,6 @@ class AuthTokenManager:
243
 
244
  if sso in self.token_status_map:
245
  del self.token_status_map[sso]
246
-
247
- self.save_token_status()
248
 
249
  logger.info(f"令牌已成功移除: {token}", "TokenManager")
250
  return True
@@ -316,8 +295,6 @@ class AuthTokenManager:
316
  self.token_status_map[sso][normalized_model]["invalidatedTime"] = int(time.time() * 1000)
317
  self.token_status_map[sso][normalized_model]["totalRequestCount"] += 1
318
 
319
- self.save_token_status()
320
-
321
  return token_entry["token"]
322
 
323
  return None
@@ -548,6 +525,7 @@ class GrokApiClient:
548
  },
549
  json=upload_data,
550
  impersonate="chrome133a",
 
551
  **proxy_options
552
  )
553
 
@@ -593,6 +571,7 @@ class GrokApiClient:
593
  },
594
  json=upload_data,
595
  impersonate="chrome133a",
 
596
  **proxy_options
597
  )
598
 
@@ -607,33 +586,13 @@ class GrokApiClient:
607
  except Exception as error:
608
  logger.error(str(error), "Server")
609
  return ''
610
- # def convert_system_messages(self, messages):
611
- # try:
612
- # system_prompt = []
613
- # i = 0
614
- # while i < len(messages):
615
- # if messages[i].get('role') != 'system':
616
- # break
617
-
618
- # system_prompt.append(self.process_message_content(messages[i].get('content')))
619
- # i += 1
620
-
621
- # messages = messages[i:]
622
- # system_prompt = '\n'.join(system_prompt)
623
-
624
- # if not messages:
625
- # raise ValueError("没有找到用户或者AI消息")
626
- # return {"system_prompt":system_prompt,"messages":messages}
627
- # except Exception as error:
628
- # logger.error(str(error), "Server")
629
- # raise ValueError(error)
630
  def prepare_chat_request(self, request):
631
  if ((request["model"] == 'grok-2-imageGen' or request["model"] == 'grok-3-imageGen') and
632
  not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"] and
633
  request.get("stream", False)):
634
  raise ValueError("该模型流式输出需要配置PICGO或者TUMY图床密钥!")
635
 
636
- # system_message, todo_messages = self.convert_system_messages(request["messages"]).values()
637
  todo_messages = request["messages"]
638
  if request["model"] in ['grok-2-imageGen', 'grok-3-imageGen', 'grok-3-deepsearch']:
639
  last_message = todo_messages[-1]
@@ -648,11 +607,6 @@ class GrokApiClient:
648
  convert_to_file = False
649
  last_message_content = ''
650
  search = request["model"] in ['grok-2-search', 'grok-3-search']
651
- deepsearchPreset = ''
652
- if request["model"] == 'grok-3-deepsearch':
653
- deepsearchPreset = 'default'
654
- elif request["model"] == 'grok-3-deepersearch':
655
- deepsearchPreset = 'deeper'
656
 
657
  # 移除<think>标签及其内容和base64图片
658
  def remove_think_tags(text):
@@ -676,6 +630,7 @@ class GrokApiClient:
676
  elif content["type"] == 'text':
677
  return remove_think_tags(content["text"])
678
  return remove_think_tags(self.process_message_content(content))
 
679
  for current in todo_messages:
680
  role = 'assistant' if current["role"] == 'assistant' else 'user'
681
  is_last_message = current == todo_messages[-1]
@@ -747,11 +702,11 @@ class GrokApiClient:
747
  "xPostAnalyze": search
748
  },
749
  "enableSideBySide": True,
 
750
  "sendFinalMetadata": True,
751
- "customPersonality": "",
752
- "deepsearchPreset": deepsearchPreset,
753
- "isReasoning": request["model"] == 'grok-3-reasoning',
754
- "disableTextFollowUps": True
755
  }
756
 
757
  class MessageProcessor:
@@ -806,7 +761,7 @@ def process_model_response(response, model):
806
  result["token"] = response.get("token")
807
  elif model == 'grok-3':
808
  result["token"] = response.get("token")
809
- elif model in ['grok-3-deepsearch', 'grok-3-deepersearch']:
810
  if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]:
811
  return result
812
  if response.get("messageStepId") and not CONFIG["IS_THINKING"]:
@@ -816,11 +771,7 @@ def process_model_response(response, model):
816
  result["token"] = "</think>" + response.get("token", "")
817
  CONFIG["IS_THINKING"] = False
818
  elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final":
819
- result["token"] = response.get("token","")
820
- elif (CONFIG["IS_THINKING"] and response.get("token","").get("action","") == "webSearch"):
821
- result["token"] = response.get("token","").get("action_input","").get("query","")
822
- elif (CONFIG["IS_THINKING"] and response.get("webSearchResults")):
823
- result["token"] = Utils.organize_search_results(response['webSearchResults'])
824
  elif model == 'grok-3-reasoning':
825
  if response.get("isThinking") and not CONFIG["SHOW_THINKING"]:
826
  return result
@@ -984,7 +935,6 @@ def handle_stream_response(response, model):
984
  continue
985
  try:
986
  line_json = json.loads(chunk.decode("utf-8").strip())
987
- print(line_json)
988
  if line_json.get("error"):
989
  logger.error(json.dumps(line_json, indent=2), "Server")
990
  yield json.dumps({"error": "RateLimitError"}) + "\n\n"
@@ -1017,13 +967,11 @@ def handle_stream_response(response, model):
1017
  return generate()
1018
 
1019
  def initialization():
1020
- sso_array = os.environ.get("SSO", "").split(',')
1021
  logger.info("开始加载令牌", "Server")
1022
- token_manager.load_token_status()
1023
  for sso in sso_array:
1024
  if sso:
1025
- token_manager.add_token(f"sso-rw={sso};sso={sso}",True)
1026
- token_manager.save_token_status()
1027
 
1028
  logger.info(f"成功加载令牌: {json.dumps(token_manager.get_all_tokens(), indent=2)}", "Server")
1029
  logger.info(f"令牌加载完成,共加载: {len(token_manager.get_all_tokens())}个令牌", "Server")
@@ -1036,7 +984,7 @@ logger.info("初始化完成", "Server")
1036
 
1037
  app = Flask(__name__)
1038
  app.wsgi_app = ProxyFix(app.wsgi_app)
1039
- app.secret_key = os.environ.get('FLASK_SECRET_KEY') or secrets.token_hex(16)
1040
  app.json.sort_keys = False
1041
 
1042
  @app.route('/manager/login', methods=['GET', 'POST'])
@@ -1198,7 +1146,7 @@ def chat_completions():
1198
  retry_count = 0
1199
  grok_client = GrokApiClient(model)
1200
  request_payload = grok_client.prepare_chat_request(data)
1201
- logger.info(json.dumps(request_payload,indent=2))
1202
 
1203
  while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]:
1204
  retry_count += 1
@@ -1225,8 +1173,9 @@ def chat_completions():
1225
  **DEFAULT_HEADERS,
1226
  "Cookie":CONFIG["SERVER"]['COOKIE']
1227
  },
1228
- data=json.dumps(request_payload),
1229
  impersonate="chrome133a",
 
1230
  stream=True,
1231
  **proxy_options)
1232
  logger.info(CONFIG["SERVER"]['COOKIE'],"Server")
@@ -1256,9 +1205,6 @@ def chat_completions():
1256
  token_manager.reduce_token_request_count(model,1)#重置去除当前因为错误未成功请求的次数,确保不会因为错误未成功请求的次数导致次数上限
1257
  if token_manager.get_token_count_for_model(model) == 0:
1258
  raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1259
- print("状态码:", response.status_code)
1260
- print("响应头:", response.headers)
1261
- print("响应内容:", response.text)
1262
  raise ValueError(f"IP暂时被封无法破盾,请稍后重试或者更换ip")
1263
  elif response.status_code == 429:
1264
  response_status_code = 429
 
7
  import inspect
8
  import secrets
9
  from loguru import logger
10
+ from dotenv import load_dotenv
11
 
12
  import requests
13
  from flask import Flask, request, Response, jsonify, stream_with_context, render_template, redirect, session
14
  from curl_cffi import requests as curl_requests
15
  from werkzeug.middleware.proxy_fix import ProxyFix
16
 
17
+ current_dir = os.path.dirname(os.path.abspath(__file__))
18
+ env_path = os.path.join(current_dir, '.env')
19
+
20
+ load_dotenv(env_path)
21
+
22
  class Logger:
23
  def __init__(self, level="INFO", colorize=True, format=None):
24
  logger.remove()
 
86
 
87
  logger = Logger(level="INFO")
88
 
89
+
90
  CONFIG = {
91
  "MODELS": {
92
  'grok-2': 'grok-latest',
 
96
  "grok-3-search": "grok-3",
97
  "grok-3-imageGen": "grok-3",
98
  "grok-3-deepsearch": "grok-3",
 
99
  "grok-3-reasoning": "grok-3"
100
  },
101
  "API": {
102
+ "IS_TEMP_CONVERSATION": os.getenv("IS_TEMP_CONVERSATION", "true").lower() == "true",
103
+ "IS_CUSTOM_SSO": os.getenv("IS_CUSTOM_SSO", "false").lower() == "true",
104
  "BASE_URL": "https://grok.com",
105
+ "API_KEY": os.getenv("API_KEY", "sk-123456"),
106
  "SIGNATURE_COOKIE": None,
107
+ "PICGO_KEY": os.getenv("PICGO_KEY") or None,
108
+ "TUMY_KEY": os.getenv("TUMY_KEY") or None,
109
  "RETRY_TIME": 1000,
110
+ "PROXY": os.getenv("PROXY") or None
111
  },
112
  "ADMIN": {
113
+ "MANAGER_SWITCH": os.getenv("MANAGER_SWITCH") or None,
114
+ "PASSWORD": os.getenv("ADMINPASSWORD") or None
115
  },
116
  "SERVER": {
117
  "COOKIE": None,
118
+ "CF_CLEARANCE":os.getenv("CF_CLEARANCE") or None,
119
+ "PORT": int(os.getenv("PORT", 5200))
120
  },
121
  "RETRY": {
122
  "RETRYSWITCH": False,
123
  "MAX_ATTEMPTS": 2
124
  },
125
+ "SHOW_THINKING": os.getenv("SHOW_THINKING") == "true",
126
  "IS_THINKING": False,
127
  "IS_IMG_GEN": False,
128
  "IS_IMG_GEN2": False,
129
+ "ISSHOW_SEARCH_RESULTS": os.getenv("ISSHOW_SEARCH_RESULTS", "true").lower() == "true"
130
  }
131
 
132
 
 
145
  'Sec-Fetch-Dest': 'empty',
146
  'Sec-Fetch-Mode': 'cors',
147
  'Sec-Fetch-Site': 'same-origin',
148
+ 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
 
149
  }
150
 
151
  class AuthTokenManager:
 
167
  "RequestFrequency": 10,
168
  "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
169
  },
 
 
 
 
170
  "grok-3-reasoning": {
171
  "RequestFrequency": 10,
172
  "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
 
174
  }
175
  self.token_reset_switch = False
176
  self.token_reset_timer = None
177
+
178
+ def add_token(self, token):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  sso = token.split("sso=")[1].split(";")[0]
180
  for model in self.model_config.keys():
181
  if model not in self.token_model_map:
 
199
  "invalidatedTime": None,
200
  "totalRequestCount": 0
201
  }
 
 
202
 
203
  def set_token(self, token):
204
  models = list(self.model_config.keys())
 
224
 
225
  if sso in self.token_status_map:
226
  del self.token_status_map[sso]
 
 
227
 
228
  logger.info(f"令牌已成功移除: {token}", "TokenManager")
229
  return True
 
295
  self.token_status_map[sso][normalized_model]["invalidatedTime"] = int(time.time() * 1000)
296
  self.token_status_map[sso][normalized_model]["totalRequestCount"] += 1
297
 
 
 
298
  return token_entry["token"]
299
 
300
  return None
 
525
  },
526
  json=upload_data,
527
  impersonate="chrome133a",
528
+ verify=False,
529
  **proxy_options
530
  )
531
 
 
571
  },
572
  json=upload_data,
573
  impersonate="chrome133a",
574
+ verify=False,
575
  **proxy_options
576
  )
577
 
 
586
  except Exception as error:
587
  logger.error(str(error), "Server")
588
  return ''
589
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  def prepare_chat_request(self, request):
591
  if ((request["model"] == 'grok-2-imageGen' or request["model"] == 'grok-3-imageGen') and
592
  not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"] and
593
  request.get("stream", False)):
594
  raise ValueError("该模型流式输出需要配置PICGO或者TUMY图床密钥!")
595
 
 
596
  todo_messages = request["messages"]
597
  if request["model"] in ['grok-2-imageGen', 'grok-3-imageGen', 'grok-3-deepsearch']:
598
  last_message = todo_messages[-1]
 
607
  convert_to_file = False
608
  last_message_content = ''
609
  search = request["model"] in ['grok-2-search', 'grok-3-search']
 
 
 
 
 
610
 
611
  # 移除<think>标签及其内容和base64图片
612
  def remove_think_tags(text):
 
630
  elif content["type"] == 'text':
631
  return remove_think_tags(content["text"])
632
  return remove_think_tags(self.process_message_content(content))
633
+
634
  for current in todo_messages:
635
  role = 'assistant' if current["role"] == 'assistant' else 'user'
636
  is_last_message = current == todo_messages[-1]
 
702
  "xPostAnalyze": search
703
  },
704
  "enableSideBySide": True,
705
+ "isPreset": False,
706
  "sendFinalMetadata": True,
707
+ "customInstructions": "",
708
+ "deepsearchPreset": "default" if request["model"] == 'grok-3-deepsearch' else "",
709
+ "isReasoning": request["model"] == 'grok-3-reasoning'
 
710
  }
711
 
712
  class MessageProcessor:
 
761
  result["token"] = response.get("token")
762
  elif model == 'grok-3':
763
  result["token"] = response.get("token")
764
+ elif model == 'grok-3-deepsearch':
765
  if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]:
766
  return result
767
  if response.get("messageStepId") and not CONFIG["IS_THINKING"]:
 
771
  result["token"] = "</think>" + response.get("token", "")
772
  CONFIG["IS_THINKING"] = False
773
  elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final":
774
+ result["token"] = response.get("token")
 
 
 
 
775
  elif model == 'grok-3-reasoning':
776
  if response.get("isThinking") and not CONFIG["SHOW_THINKING"]:
777
  return result
 
935
  continue
936
  try:
937
  line_json = json.loads(chunk.decode("utf-8").strip())
 
938
  if line_json.get("error"):
939
  logger.error(json.dumps(line_json, indent=2), "Server")
940
  yield json.dumps({"error": "RateLimitError"}) + "\n\n"
 
967
  return generate()
968
 
969
  def initialization():
970
+ sso_array = os.getenv("SSO", "").split(',')
971
  logger.info("开始加载令牌", "Server")
 
972
  for sso in sso_array:
973
  if sso:
974
+ token_manager.add_token(f"sso-rw={sso};sso={sso}")
 
975
 
976
  logger.info(f"成功加载令牌: {json.dumps(token_manager.get_all_tokens(), indent=2)}", "Server")
977
  logger.info(f"令牌加载完成,共加载: {len(token_manager.get_all_tokens())}个令牌", "Server")
 
984
 
985
  app = Flask(__name__)
986
  app.wsgi_app = ProxyFix(app.wsgi_app)
987
+ app.secret_key = os.getenv('FLASK_SECRET_KEY') or secrets.token_hex(16)
988
  app.json.sort_keys = False
989
 
990
  @app.route('/manager/login', methods=['GET', 'POST'])
 
1146
  retry_count = 0
1147
  grok_client = GrokApiClient(model)
1148
  request_payload = grok_client.prepare_chat_request(data)
1149
+
1150
 
1151
  while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]:
1152
  retry_count += 1
 
1173
  **DEFAULT_HEADERS,
1174
  "Cookie":CONFIG["SERVER"]['COOKIE']
1175
  },
1176
+ json=request_payload,
1177
  impersonate="chrome133a",
1178
+ verify=False,
1179
  stream=True,
1180
  **proxy_options)
1181
  logger.info(CONFIG["SERVER"]['COOKIE'],"Server")
 
1205
  token_manager.reduce_token_request_count(model,1)#重置去除当前因为错误未成功请求的次数,确保不会因为错误未成功请求的次数导致次数上限
1206
  if token_manager.get_token_count_for_model(model) == 0:
1207
  raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
 
 
 
1208
  raise ValueError(f"IP暂时被封无法破盾,请稍后重试或者更换ip")
1209
  elif response.status_code == 429:
1210
  response_status_code = 429
templates/login.html DELETED
@@ -1,77 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>登录</title>
6
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
7
- <style>
8
- :root {
9
- --bg-primary: #f4f6f9;
10
- --card-bg: #ffffff;
11
- --text-primary: #2c3e50;
12
- --text-secondary: #6c757d;
13
- --border-color: #e2e8f0;
14
- --primary-color: #3498db;
15
- }
16
- * { box-sizing: border-box; margin: 0; padding: 0; }
17
- body {
18
- font-family: 'Inter', sans-serif;
19
- background-color: var(--bg-primary);
20
- color: var(--text-primary);
21
- display: flex;
22
- justify-content: center;
23
- align-items: center;
24
- height: 100vh;
25
- }
26
- .login-form {
27
- background: var(--card-bg);
28
- padding: 40px;
29
- border-radius: 16px;
30
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
31
- width: 100%;
32
- max-width: 400px;
33
- }
34
- h2 {
35
- text-align: center;
36
- margin-bottom: 20px;
37
- color: var(--text-primary);
38
- }
39
- input {
40
- width: 100%;
41
- padding: 12px 15px;
42
- margin: 10px 0;
43
- border: 1px solid var(--border-color);
44
- border-radius: 8px;
45
- font-size: 16px;
46
- }
47
- button {
48
- width: 100%;
49
- padding: 12px;
50
- background-color: var(--primary-color);
51
- color: white;
52
- border: none;
53
- border-radius: 8px;
54
- cursor: pointer;
55
- transition: background-color 0.3s;
56
- }
57
- button:hover { background-color: #2980b9; }
58
- </style>
59
- </head>
60
- <body>
61
- <div class="login-form">
62
- <h2>管理员登录</h2>
63
- <form action="/manager/login" method="post">
64
- <input type="password" name="password" placeholder="输入管理员密码" required>
65
- <button type="submit">登录</button>
66
- </form>
67
- </div>
68
- {% if error %}
69
- <div id="notification" style="position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: #f44336; color: white; padding: 10px 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: block; z-index: 1000;">密码错误</div>
70
- <script>
71
- setTimeout(() => {
72
- document.getElementById('notification').style.display = 'none';
73
- }, 2000);
74
- </script>
75
- {% endif %}
76
- </body>
77
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/manager.html DELETED
@@ -1,1018 +0,0 @@
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>GrokAPI管理面板</title>
7
- <style type="text/css">@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}</style>
8
- <style>
9
- :root {
10
- --primary: #3498db;
11
- --primary-hover: #1575b5;
12
- --secondary: #64748B;
13
- --success: #10B981;
14
- --danger: #EF4444;
15
- --warning: #F59E0B;
16
- --bg-light: #F8FAFC;
17
- --bg-white: #FFFFFF;
18
- --text-dark: #1F2937;
19
- --text-muted: #64748B;
20
- --border: #E2E8F0;
21
- --shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
22
- --card-border: #E2E8F0;
23
- --progress-bg: #E2E8F0;
24
- --status-active-bg: rgba(16, 185, 129, 0.1);
25
- --status-expired-bg: rgba(239, 68, 68, 0.1);
26
- --progress-fill-success: #10B981;
27
- --progress-fill-warning: #F59E0B;
28
- --progress-fill-danger: #EF4444;
29
- }
30
-
31
- * { box-sizing: border-box; margin: 0; padding: 0; }
32
- body { font-family: 'Inter', sans-serif; background: var(--bg-light); color: var(--text-dark); line-height: 1.6; min-height: 100vh; padding-top: 5rem; }
33
- .container { max-width: 1280px; margin: 0 auto; padding: 0 1.5rem; }
34
- .card { background: var(--bg-white); border-radius: 0.75rem; box-shadow: var(--shadow); padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid var(--card-border); }
35
-
36
- .search-section {
37
- position: fixed;
38
- top: 0;
39
- left: 0;
40
- right: 0;
41
- z-index: 100;
42
- background: var(--bg-white);
43
- box-shadow: var(--shadow);
44
- padding: 1rem 1.5rem;
45
- margin: 0;
46
- }
47
- .search-section .card {
48
- padding: 0;
49
- margin: 0;
50
- box-shadow: none;
51
- border: none;
52
- }
53
- .search-input {
54
- width: 100%;
55
- padding: 0.75rem 1rem 0.75rem 2.5rem;
56
- border: 1px solid #CBD5E1;
57
- border-radius: 0.5rem;
58
- font-size: 0.9rem;
59
- background: #F9FAFB url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="%2364748B" stroke-width="2"><circle cx="7" cy="7" r="5"/><path d="M11 11l4 4"/></svg>') no-repeat 0.75rem center;
60
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
61
- }
62
- .search-input:focus {
63
- border-color: #0f5fc3;
64
- outline: none;
65
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
66
- }
67
- .btn-base { padding: 0.5rem 1rem; border-radius: 0.5rem; border: none; cursor: pointer; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: transform 0.2s ease, background 0.3s ease, box-shadow 0.2s ease; }
68
- .btn { background: linear-gradient(135deg, var(--primary) 0%, #0f5fc3 100%); color: white; }
69
- .btn:hover { background: linear-gradient(135deg, var(--primary-hover) 0%, #0f5fc3 100%); transform: translateY(-2px); box-shadow: 0 4px 15px rgba(107, 70, 193, 0.3); }
70
- .btn-secondary { background: linear-gradient(135deg, var(--primary) 0%, #0f5fc3 100%); color: white; width: 2rem; height: 2rem; padding: 0; justify-content: center; }
71
- .btn-secondary:hover { background: linear-gradient(135deg, var(--primary-hover) 0%, #0f5fc3 100%); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3); }
72
- .btn-danger { background: var(--danger); width: 2rem; height: 2rem; padding: 0; justify-content: center; }
73
- .btn-danger:hover { background: #DC2626; }
74
- .token-manage-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; margin-top: 1rem; }
75
- .input-group { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
76
- .input-field { flex: 1; min-width: 0; padding: 0.75rem; border: 1px solid #CBD5E1; border-radius: 0.5rem; font-size: 0.9rem; background: #F9FAFB; transition: border-color 0.2s ease, box-shadow 0.2s ease; }
77
- .input-field:focus { border-color: #0f5fc3; outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2); }
78
- #statusFilter, #modelSelect { background: #F9FAFB; border: 1px solid #CBD5E1; color: var(--text-dark); }
79
- #statusFilter:focus, #modelSelect:focus { border-color: #0f5fc3; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2); }
80
- .token-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
81
- .token-card { background: var(--bg-white); border: 1px solid var(--card-border); border-radius: 0.75rem; box-shadow: var(--shadow); padding: 1rem; display: flex; flex-direction: column; gap: 1rem; transition: transform 0.2s ease; max-width: 100%; overflow: hidden; }
82
- .token-card:hover { transform: translateY(-4px); }
83
- .token-checkbox { display: none; }
84
- .token-checkbox.show { display: block; position: absolute; top: 1rem; left: 1rem; }
85
- .token-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; }
86
- .token-title { font-weight: 600; font-size: 0.9rem; color: var(--text-dark); flex: 1 1 auto; max-width: calc(100% - 80px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
87
- .token-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
88
- .model-list { display: grid; gap: 0.75rem; }
89
- .model-item { display: grid; grid-template-columns: 1fr 2fr 80px; gap: 0.5rem; align-items: center; padding: 0.5rem; background: rgba(248, 250, 252, 0.5); border-radius: 0.5rem; }
90
- .model-name { font-size: 0.85rem; font-weight: 500; color: var(--text-dark); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
91
- .progress-container { display: flex; align-items: center; gap: 0.5rem; }
92
- .progress-bar { flex: 1; height: 0.375rem; background: var(--progress-bg); border-radius: 1rem; overflow: hidden; }
93
- .progress-fill { height: 100%; border-radius: 1rem; transition: width 0.3s ease; }
94
- .progress-text { font-size: 0.75rem; color: var(--text-muted); min-width: 40px; }
95
- .status { font-size: 0.75rem; padding: 0.25rem 0.75rem; border-radius: 1rem; text-align: center; font-weight: 500; }
96
- .status-active { background: var(--status-active-bg); color: var(--success); }
97
- .status-expired { background: var(--status-expired-bg); color: var(--danger); position: relative; }
98
- .status-expired .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--text-dark); color: white; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s; margin-bottom: 0.5rem; }
99
- .status-expired:hover .tooltip { opacity: 1; visibility: visible; }
100
- .notification { position: fixed; top: 5rem; left: 50%; transform: translateX(-50%); background: var(--primary); color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; box-shadow: var(--shadow); z-index: 1000; display: none; animation: slideIn 0.3s ease; }
101
- @keyframes slideIn { from { transform: translateX(-50%) translateY(-100%); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } }
102
-
103
- .overview-section { background: var(--bg-white); border-radius: 1rem; padding: 1.5rem; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06); position: relative; overflow: hidden; }
104
- .overview-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
105
- .overview-title { font-size: 1.5rem; font-weight: 700; color: var(--text-dark); }
106
- .overview-actions { display: flex; gap: 0.75rem; }
107
- .overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
108
- .overview-card { background: linear-gradient(135deg, #F9FAFB 0%, #F1F5F9 100%); border-radius: 0.75rem; padding: 1rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; align-items: center; gap: 1rem; position: relative; border: 1px solid rgba(107, 70, 193, 0.1); }
109
- .overview-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(107, 70, 193, 0.15); }
110
- .overview-icon { width: 2.5rem; height: 2.5rem; background: linear-gradient(135deg, rgba(107, 70, 193, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; color: var(--primary); flex-shrink: 0; }
111
- .overview-content { display: flex; flex-direction: column; gap: 0.25rem; }
112
- .overview-label { font-size: 0.9rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05px; }
113
- .overview-value { font-size: 1.75rem; font-weight: 700; color: var(--primary); }
114
- .overview-card::after { content: attr(data-tooltip); position: absolute; top: -2.5rem; left: 50%; transform: translateX(-50%); background: rgba(31, 41, 55, 0.9); color: white; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s ease; pointer-events: none; }
115
- .overview-card:hover::after { opacity: 1; visibility: visible; }
116
-
117
- .pagination { display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1.5rem; }
118
- .pagination-btn { padding: 0.5rem 1rem; border-radius: 0.5rem; border: 1px solid var(--border); background: var(--bg-white); cursor: pointer; font-size: 0.9rem; transition: background 0.2s ease; }
119
- .pagination-btn:hover { background: var(--bg-light); }
120
- .pagination-btn:disabled { opacity: 0.5; cursor: not-allowed; }
121
- .pagination-select { padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.5rem; font-size: 0.9rem; }
122
-
123
- @media (max-width: 768px) {
124
- .overview-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; }
125
- .overview-card { padding: 0.75rem; }
126
- .overview-icon { width: 2rem; height: 2rem; }
127
- .overview-value { font-size: 1.5rem; }
128
- .overview-label { font-size: 0.85rem; }
129
- .token-grid { grid-template-columns: 1fr; gap: 1rem; }
130
- .token-card { padding: 0.75rem; }
131
- }
132
-
133
- @media (min-width: 768px) {
134
- .token-grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
135
- .token-card { min-height: 240px; }
136
- .token-manage-grid { grid-template-columns: repeat(2, 1fr); }
137
- }
138
-
139
- @media (max-width: 480px) {
140
- body { padding: 5rem 1rem 1rem; }
141
- .card { padding: 1rem; }
142
- .overview-section { padding: 1rem; }
143
- .overview-header { flex-direction: column; align-items: flex-start; gap: 1rem; }
144
- .overview-actions { width: 100%; justify-content: flex-end; }
145
- .overview-grid { grid-template-columns: 1fr; gap: 0.75rem; }
146
- .overview-card { padding: 1rem; }
147
- .overview-value { font-size: 1.5rem; }
148
- .search-section { padding: 0.5rem 1rem; }
149
- .search-section .input-group { flex-direction: column; gap: 0.5rem; }
150
- .search-input, #statusFilter { width: 100%; }
151
- .pagination { flex-wrap: wrap; gap: 0.75rem; }
152
- .input-group label { width: 100%; margin-bottom: 0.5rem; }
153
- .input-field { width: 100%; }
154
- }
155
- </style>
156
- </head>
157
- <body>
158
- <div class="search-section">
159
- <div class="card">
160
- <div class="input-group">
161
- <input type="text" class="search-input" id="searchInput" placeholder="搜索 Token..." aria-label="搜索 Token" style="flex: 1;">
162
- <select class="input-field" id="statusFilter" style="width: 120px;" aria-label="筛选 Token 状态">
163
- <option value="all">全部</option>
164
- <option value="active">活跃</option>
165
- <option value="expired">失效</option>
166
- </select>
167
- </div>
168
- </div>
169
- </div>
170
-
171
- <div class="container">
172
- <div class="overview-section card">
173
- <div class="overview-header">
174
- <h2 class="overview-title">概览</h2>
175
- <div class="overview-actions">
176
- <button class="btn btn-base" id="batchDeleteTokens" aria-label="批量删除选中的 Token">
177
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
- <path d="M3 6h18M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M10 11v6M14 11v6M4 6v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6"/>
179
- </svg>
180
- 批量删除
181
- </button>
182
- <button class="btn btn-base" id="refreshTokens" aria-label="刷新 Token 列表">
183
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
184
- <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
185
- <path d="M21 3v5h-5"/>
186
- <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
187
- <path d="M3 21v-5h5"/>
188
- </svg>
189
- 刷新
190
- </button>
191
- </div>
192
- </div>
193
- <div class="overview-grid">
194
- <div class="overview-card" data-tooltip="总共管理的Token数量">
195
- <div class="overview-icon">
196
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
197
- <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
198
- <rect x="8" y="2" width="8" height="4" rx="1"/>
199
- </svg>
200
- </div>
201
- <div class="overview-content">
202
- <span class="overview-label">Token 总数</span>
203
- <span class="overview-value" id="totalTokens">0</span>
204
- </div>
205
- </div>
206
- <div class="overview-card" data-tooltip="grok-2 模型剩余可用次数">
207
- <div class="overview-icon">
208
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
209
- <path d="M12 2v20M2 12h20"/>
210
- </svg>
211
- </div>
212
- <div class="overview-content">
213
- <span class="overview-label">grok-2</span>
214
- <span class="overview-value" id="grok-2-count">0</span>
215
- </div>
216
- </div>
217
- <div class="overview-card" data-tooltip="grok-3 模型剩余可用次数">
218
- <div class="overview-icon">
219
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
- <circle cx="12" cy="12" r="10"/>
221
- <path d="M12 2v4M12 18v4"/>
222
- </svg>
223
- </div>
224
- <div class="overview-content">
225
- <span class="overview-label">grok-3</span>
226
- <span class="overview-value" id="grok-3-count">0</span>
227
- </div>
228
- </div>
229
- <div class="overview-card" data-tooltip="grok-3-deepsearch 模型剩余可用次数">
230
- <div class="overview-icon">
231
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
232
- <circle cx="11" cy="11" r="8"/>
233
- <path d="M21 21l-4.35-4.35"/>
234
- </svg>
235
- </div>
236
- <div class="overview-content">
237
- <span class="overview-label">grok-3-deepsearch</span>
238
- <span class="overview-value" id="grok-3-deepsearch-count">0</span>
239
- </div>
240
- </div>
241
- <div class="overview-card" data-tooltip="grok-3-deepersearch 模型剩余可用次数">
242
- <div class="overview-icon">
243
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
244
- <circle cx="11" cy="11" r="8"/>
245
- <path d="M21 21l-4.35-4.35"/>
246
- </svg>
247
- </div>
248
- <div class="overview-content">
249
- <span class="overview-label">grok-3-deepersearch</span>
250
- <span class="overview-value" id="grok-3-deepersearch-count">0</span>
251
- </div>
252
- </div>
253
- <div class="overview-card" data-tooltip="grok-3-reasoning 模型剩余可用次数">
254
- <div class="overview-icon">
255
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
256
- <path d="M12 2a10 10 0 0 0-10 10c0 5 8 13 10 13s10-8 10-13a10 10 0 0 0-10-10z"/>
257
- </svg>
258
- </div>
259
- <div class="overview-content">
260
- <span class="overview-label">grok-3-reasoning</span>
261
- <span class="overview-value" id="grok-3-reasoning-count">0</span>
262
- </div>
263
- </div>
264
- </div>
265
- </div>
266
-
267
- <div class="card">
268
- <h3>Token 管理</h3>
269
- <div class="token-manage-grid">
270
- <div>
271
- <h4 style="margin-bottom: 0.75rem;">添加单个 SSO Token</h4>
272
- <div class="input-group">
273
- <input type="text" class="input-field" id="singleTokenInput" placeholder="输入单个 SSO Token" aria-label="输入单个 SSO Token">
274
- <button class="btn btn-base" id="addSingleTokenBtn" aria-label="添加单个 SSO Token">添加</button>
275
- </div>
276
- </div>
277
- <div>
278
- <h4 style="margin-bottom: 0.75rem;">设置 CF Clearance</h4>
279
- <div class="input-group">
280
- <input type="text" class="input-field" id="cfInput" placeholder="输入 CF Clearance" aria-label="输入 CF Clearance">
281
- <button class="btn btn-base" id="setCfBtn" aria-label="设置 CF Clearance">设置</button>
282
- </div>
283
- </div>
284
- <div>
285
- <h4 style="margin-bottom: 0.75rem;">批量添加 SSO Token</h4>
286
- <div class="input-group">
287
- <input type="text" class="input-field" id="batchTokenInput" placeholder="输入多个 SSO Token(用逗号隔开,如 ey1,ey2)" aria-label="输入多个 SSO Token">
288
- <button class="btn btn-base" id="addBatchTokenBtn" aria-label="批量添加 SSO Token">添加</button>
289
- </div>
290
- </div>
291
- <div>
292
- <h4 style="margin-bottom: 0.75rem;">检测模型可用性</h4>
293
- <div class="input-group">
294
- <select class="input-field" id="modelSelect" style="width: 150px;" aria-label="选择要检测的模型">
295
- <option value="grok-2">grok-2</option>
296
- <option value="grok-3" selected>grok-3</option>
297
- <option value="grok-3-reasoning">grok-3-reasoning</option>
298
- <option value="grok-3-deepsearch">grok-3-deepsearch</option>
299
- </select>
300
- <button class="btn btn-base" id="testAvailabilityBtn" aria-label="检测模型可用性">检测</button>
301
- </div>
302
- </div>
303
- <div>
304
- <h4 style="margin-bottom: 0.75rem;">Base URL</h4>
305
- <div class="input-group">
306
- <input type="text" class="input-field" id="baseUrl" readonly aria-label="Base URL" value="">
307
- <button class="btn btn-base" id="copyBaseUrlBtn" aria-label="复制 Base URL">
308
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
309
- <rect x="9" y="9" width="13" height="13" rx="2"/>
310
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
311
- </svg>
312
- 复制
313
- </button>
314
- </div>
315
- </div>
316
- <div>
317
- <h4 style="margin-bottom: 0.75rem;">API Key</h4>
318
- <div class="input-group">
319
- <input type="text" class="input-field" id="apiKey" aria-label="API Key">
320
- <button class="btn btn-base" id="copyApiKeyBtn" aria-label="复制 API Key">
321
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
322
- <rect x="9" y="9" width="13" height="13" rx="2"/>
323
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
324
- </svg>
325
- 复制
326
- </button>
327
- </div>
328
- </div>
329
- </div>
330
- </div>
331
-
332
- <div class="token-grid" id="tokenGrid"></div>
333
- <div class="pagination" id="pagination">
334
- <button class="pagination-btn" id="prevPage" aria-label="上一页">上一页</button>
335
- <select class="pagination-select" id="pageSelect" aria-label="选择页面"></select>
336
- <button class="pagination-btn" id="nextPage" aria-label="下一页">下一页</button>
337
- </div>
338
- </div>
339
-
340
- <div class="notification" id="notification"></div>
341
-
342
- <script>
343
- const modelConfig = {
344
- "grok-2": { RequestFrequency: 30, ExpirationTime: 3600000 },
345
- "grok-3": { RequestFrequency: 20, ExpirationTime: 7200000 },
346
- "grok-3-deepsearch": { RequestFrequency: 10, ExpirationTime: 86400000 },
347
- "grok-3-deepersearch": { RequestFrequency: 3, ExpirationTime: 86400000 },
348
- "grok-3-reasoning": { RequestFrequency: 10, ExpirationTime: 86400000 }
349
- };
350
-
351
- let tokenMap = {};
352
- let batchDeleteMode = false;
353
- let lastUpdateTime = 0;
354
- let currentPage = 1;
355
- const itemsPerPage = 30;
356
-
357
- function getProgressColor(percentage, isValid) {
358
- if (!isValid) return '#94A3B8';
359
- if (percentage > 70) return 'var(--progress-fill-danger)';
360
- if (percentage > 30) return 'var(--progress-fill-warning)';
361
- return 'var(--progress-fill-success)';
362
- }
363
-
364
- function calculateModelRemaining() {
365
- const modelRemaining = {};
366
- Object.keys(modelConfig).forEach(modelName => {
367
- const maxRequests = modelConfig[modelName].RequestFrequency;
368
- modelRemaining[modelName] = 0;
369
- Object.values(tokenMap).forEach(tokenData => {
370
- const modelData = tokenData[modelName];
371
- if (modelData.isValid) {
372
- modelRemaining[modelName] += maxRequests - modelData.totalRequestCount;
373
- }
374
- });
375
- });
376
- return modelRemaining;
377
- }
378
-
379
- function updateTokenCounters() {
380
- const totalTokensElement = document.getElementById('totalTokens');
381
- if (totalTokensElement) {
382
- totalTokensElement.textContent = Object.keys(tokenMap).length;
383
- } else {
384
- console.warn('Element with ID "totalTokens" not found.');
385
- }
386
-
387
- const modelRemaining = calculateModelRemaining();
388
- const modelIds = ['grok-2', 'grok-3', 'grok-3-deepsearch', 'grok-3-deepersearch', 'grok-3-reasoning'];
389
- modelIds.forEach(modelName => {
390
- const countElement = document.getElementById(`${modelName}-count`);
391
- if (countElement) {
392
- countElement.textContent = modelRemaining[modelName] || 0;
393
- } else {
394
- console.warn(`Element with ID "${modelName}-count" not found.`);
395
- }
396
- });
397
- }
398
-
399
- async function updateExpiredTokenTimers() {
400
- const currentTime = Date.now();
401
- if (currentTime - lastUpdateTime < 5000) return;
402
-
403
- let shouldRefresh = false;
404
- Object.values(tokenMap).forEach(tokenData => {
405
- Object.entries(tokenData).forEach(([modelName, modelData]) => {
406
- if (!modelData.isValid) {
407
- const recoveryTime = modelData.invalidatedTime + modelConfig[modelName].ExpirationTime;
408
- if (recoveryTime <= currentTime) {
409
- shouldRefresh = true;
410
- }
411
- }
412
- });
413
- });
414
-
415
- if (shouldRefresh) {
416
- lastUpdateTime = currentTime;
417
- await fetchTokenMap();
418
- showNotification('Token 状态已自动更新');
419
- } else {
420
- renderTokenDiff(tokenMap);
421
- }
422
- }
423
-
424
- function getTooltipText(invalidatedTime, expirationTime) {
425
- const currentTime = Date.now();
426
- const recoveryTime = invalidatedTime + expirationTime;
427
- const remainingTime = recoveryTime - currentTime;
428
- if (remainingTime > 0) {
429
- const minutes = Math.floor(remainingTime / 60000);
430
- const seconds = Math.floor((remainingTime % 60000) / 1000);
431
- return `${minutes}分${seconds}秒后恢复`;
432
- }
433
- return '已可恢复';
434
- }
435
-
436
- function createTokenCard(token, tokenData) {
437
- const tokenCard = document.createElement('div');
438
- tokenCard.className = 'token-card';
439
- tokenCard.setAttribute('data-token', token);
440
-
441
- const checkbox = document.createElement('input');
442
- checkbox.type = 'checkbox';
443
- checkbox.className = `token-checkbox ${batchDeleteMode ? 'show' : ''}`;
444
- checkbox.value = token;
445
-
446
- const tokenHeader = document.createElement('div');
447
- tokenHeader.className = 'token-header';
448
-
449
- const tokenTitle = document.createElement('div');
450
- tokenTitle.className = 'token-title';
451
- tokenTitle.textContent = token;
452
- tokenTitle.title = token;
453
-
454
- const tokenActions = document.createElement('div');
455
- tokenActions.className = 'token-actions';
456
-
457
- const copyBtn = document.createElement('button');
458
- copyBtn.className = 'btn btn-secondary btn-base';
459
- copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
460
- copyBtn.setAttribute('aria-label', '复制 Token 到剪贴板');
461
- copyBtn.addEventListener('click', async () => {
462
- try {
463
- await navigator.clipboard.writeText(token);
464
- showNotification('Token 已复制到剪贴板');
465
- } catch (err) {
466
- showNotification('复制失败,请重试');
467
- }
468
- });
469
-
470
- const deleteBtn = document.createElement('button');
471
- deleteBtn.className = 'btn btn-danger btn-base';
472
- deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M10 11v6M14 11v6M4 6v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6"/></svg>';
473
- deleteBtn.setAttribute('aria-label', `删除 Token ${token}`);
474
- deleteBtn.addEventListener('click', async () => {
475
- if (confirm(`确认删除 token: ${token}?`)) {
476
- try {
477
- const baseUrl = document.getElementById('baseUrl').value;
478
- const response = await fetch(`${baseUrl}/manager/api/delete`, {
479
- method: 'POST',
480
- headers: { 'Content-Type': 'application/json' },
481
- body: JSON.stringify({ sso: token })
482
- });
483
- if (response.ok) {
484
- await fetchTokenMap();
485
- showNotification('Token 删除成功');
486
- } else {
487
- showNotification('删除 Token 失败');
488
- }
489
- } catch (error) {
490
- showNotification('删除 Token 出错');
491
- }
492
- }
493
- });
494
-
495
- tokenActions.appendChild(copyBtn);
496
- tokenActions.appendChild(deleteBtn);
497
- tokenHeader.appendChild(tokenTitle);
498
- tokenHeader.appendChild(tokenActions);
499
-
500
- const modelList = document.createElement('div');
501
- modelList.className = 'model-list';
502
-
503
- Object.entries(modelConfig).forEach(([modelName, config]) => {
504
- const modelItem = document.createElement('div');
505
- modelItem.className = 'model-item';
506
-
507
- const modelNameSpan = document.createElement('div');
508
- modelNameSpan.className = 'model-name';
509
- modelNameSpan.textContent = modelName;
510
-
511
- const progressContainer = document.createElement('div');
512
- progressContainer.className = 'progress-container';
513
-
514
- const progressBar = document.createElement('div');
515
- progressBar.className = 'progress-bar';
516
- const progressFill = document.createElement('div');
517
- progressFill.className = 'progress-fill';
518
-
519
- const modelData = tokenData[modelName];
520
- const requestCount = modelData.totalRequestCount;
521
- const maxRequests = config.RequestFrequency;
522
- const percentage = Math.min((requestCount / maxRequests) * 100, 100);
523
- progressFill.style.width = `${percentage}%`;
524
- progressFill.style.backgroundColor = getProgressColor(percentage, modelData.isValid);
525
- progressBar.appendChild(progressFill);
526
-
527
- const progressText = document.createElement('div');
528
- progressText.className = 'progress-text';
529
- progressText.textContent = `${requestCount}/${maxRequests}`;
530
-
531
- const status = document.createElement('div');
532
- status.className = 'status';
533
- if (!modelData.isValid) {
534
- status.classList.add('status-expired');
535
- status.textContent = '失效';
536
- status.setAttribute('data-invalidated-time', modelData.invalidatedTime);
537
- status.setAttribute('data-expiration-time', config.ExpirationTime);
538
- const tooltip = document.createElement('div');
539
- tooltip.className = 'tooltip';
540
- tooltip.textContent = getTooltipText(modelData.invalidatedTime, config.ExpirationTime);
541
- status.appendChild(tooltip);
542
- } else {
543
- status.classList.add('status-active');
544
- status.textContent = '活跃';
545
- }
546
-
547
- progressContainer.appendChild(progressBar);
548
- progressContainer.appendChild(progressText);
549
-
550
- modelItem.appendChild(modelNameSpan);
551
- modelItem.appendChild(progressContainer);
552
- modelItem.appendChild(status);
553
- modelList.appendChild(modelItem);
554
- });
555
-
556
- tokenCard.appendChild(checkbox);
557
- tokenCard.appendChild(tokenHeader);
558
- tokenCard.appendChild(modelList);
559
- return tokenCard;
560
- }
561
-
562
- function updateTokenCard(token, tokenData) {
563
- const tokenCard = document.querySelector(`[data-token="${token}"]`);
564
- const modelItems = tokenCard.querySelectorAll('.model-item');
565
- let index = 0;
566
- Object.entries(modelConfig).forEach(([modelName, config]) => {
567
- const modelItem = modelItems[index++];
568
- const modelData = tokenData[modelName];
569
- const requestCount = modelData.totalRequestCount;
570
- const maxRequests = config.RequestFrequency;
571
- const percentage = Math.min((requestCount / maxRequests) * 100, 100);
572
-
573
- const progressFill = modelItem.querySelector('.progress-fill');
574
- progressFill.style.width = `${percentage}%`;
575
- progressFill.style.backgroundColor = getProgressColor(percentage, modelData.isValid);
576
-
577
- const progressText = modelItem.querySelector('.progress-text');
578
- progressText.textContent = `${requestCount}/${maxRequests}`;
579
-
580
- const status = modelItem.querySelector('.status');
581
- status.className = 'status';
582
- if (!modelData.isValid) {
583
- status.classList.add('status-expired');
584
- status.textContent = '失效';
585
- status.setAttribute('data-invalidated-time', modelData.invalidatedTime);
586
- status.setAttribute('data-expiration-time', config.ExpirationTime);
587
- const tooltip = status.querySelector('.tooltip') || document.createElement('div');
588
- tooltip.className = 'tooltip';
589
- tooltip.textContent = getTooltipText(modelData.invalidatedTime, config.ExpirationTime);
590
- if (!status.contains(tooltip)) status.appendChild(tooltip);
591
- } else {
592
- status.classList.add('status-active');
593
- status.textContent = '活跃';
594
- const tooltip = status.querySelector('.tooltip');
595
- if (tooltip) tooltip.remove();
596
- }
597
- });
598
- const checkbox = tokenCard.querySelector('.token-checkbox');
599
- checkbox.className = `token-checkbox ${batchDeleteMode ? 'show' : ''}`;
600
- }
601
-
602
- function renderTokenDiff(newTokenMap) {
603
- const tokenGrid = document.getElementById('tokenGrid');
604
- if (!tokenGrid) {
605
- console.error('Token grid element not found.');
606
- return;
607
- }
608
- const existingTokens = new Set(Array.from(tokenGrid.children).map(card => card.getAttribute('data-token')));
609
- const filteredTokens = filterTokensArray(Object.entries(newTokenMap));
610
- const totalItems = filteredTokens.length;
611
- const totalPages = Math.ceil(totalItems / itemsPerPage);
612
- currentPage = Math.min(currentPage, totalPages) || 1;
613
-
614
- const startIndex = (currentPage - 1) * itemsPerPage;
615
- const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
616
- const tokensToRender = filteredTokens.slice(startIndex, endIndex);
617
-
618
- const newTokens = new Set(tokensToRender.map(([token]) => token));
619
-
620
- existingTokens.forEach(token => {
621
- if (!newTokens.has(token)) {
622
- tokenGrid.querySelector(`[data-token="${token}"]`).remove();
623
- }
624
- });
625
-
626
- tokensToRender.forEach(([token, tokenData]) => {
627
- if (!existingTokens.has(token)) {
628
- const tokenCard = createTokenCard(token, tokenData);
629
- tokenGrid.appendChild(tokenCard);
630
- } else {
631
- updateTokenCard(token, tokenData);
632
- }
633
- });
634
-
635
- updateTokenCounters();
636
- renderPagination(totalPages);
637
- }
638
-
639
- function filterTokensArray(tokenEntries) {
640
- const searchInput = document.getElementById('searchInput');
641
- const statusFilter = document.getElementById('statusFilter');
642
- if (!searchInput || !statusFilter) {
643
- console.error('Search input or status filter element not found.');
644
- return tokenEntries;
645
- }
646
- const searchTerm = searchInput.value.toLowerCase();
647
- const statusFilterValue = statusFilter.value;
648
-
649
- return tokenEntries.filter(([token, tokenData]) => {
650
- const hasActive = Object.values(tokenData).some(data => data.isValid);
651
- const hasExpired = Object.values(tokenData).some(data => !data.isValid);
652
-
653
- let display = true;
654
- if (!token.toLowerCase().includes(searchTerm)) display = false;
655
- if (statusFilterValue === 'active' && !hasActive) display = false;
656
- if (statusFilterValue === 'expired' && !hasExpired) display = false;
657
-
658
- return display;
659
- });
660
- }
661
-
662
- function renderPagination(totalPages) {
663
- const pageSelect = document.getElementById('pageSelect');
664
- if (!pageSelect) {
665
- console.error('Page select element not found.');
666
- return;
667
- }
668
- pageSelect.innerHTML = '';
669
- for (let i = 1; i <= totalPages; i++) {
670
- const option = document.createElement('option');
671
- option.value = i;
672
- option.textContent = `第 ${i} 页`;
673
- if (i === currentPage) option.selected = true;
674
- pageSelect.appendChild(option);
675
- }
676
-
677
- const prevPage = document.getElementById('prevPage');
678
- const nextPage = document.getElementById('nextPage');
679
- if (prevPage) prevPage.disabled = currentPage === 1;
680
- if (nextPage) nextPage.disabled = currentPage === totalPages || totalPages === 0;
681
- }
682
-
683
- async function fetchTokenMap() {
684
- console.log('开始获取 tokenMap');
685
- try {
686
- const baseUrlElement = document.getElementById('baseUrl');
687
- if (!baseUrlElement) {
688
- throw new Error('Base URL 元素未找到');
689
- }
690
- const baseUrl = baseUrlElement.value;
691
- console.log('请求 URL:', `${baseUrl}/manager/api/get`);
692
- const response = await fetch(`${baseUrl}/manager/api/get`);
693
- if (!response.ok) {
694
- const errorText = await response.text();
695
- throw new Error(`获取 Token 失败: ${response.status} - ${errorText}`);
696
- }
697
- const data = await response.json();
698
- console.log('获取到的数据:', data);
699
- if (!data || typeof data !== 'object') {
700
- throw new Error('返回的数据不是有效的 Token Map');
701
- }
702
- tokenMap = data;
703
- renderTokenDiff(tokenMap);
704
- console.log('tokenMap 更新成功');
705
- } catch (error) {
706
- console.error('获取 Token 出错:', error);
707
- showNotification(`获取 Token 出错: ${error.message}`);
708
- }
709
- }
710
-
711
- document.addEventListener('DOMContentLoaded', () => {
712
- const baseUrlInput = document.getElementById('baseUrl');
713
- if (baseUrlInput) {
714
- baseUrlInput.value = window.location.origin;
715
- } else {
716
- console.error('Base URL input element not found.');
717
- }
718
-
719
- const apiKeyInput = document.getElementById('apiKey');
720
- if (apiKeyInput) {
721
- const savedApiKey = localStorage.getItem('apiKey');
722
- apiKeyInput.value = savedApiKey || 'sk-1234567';
723
- apiKeyInput.addEventListener('change', () => {
724
- const newApiKey = apiKeyInput.value.trim();
725
- if (newApiKey) {
726
- localStorage.setItem('apiKey', newApiKey);
727
- showNotification(`API Key 已更新为: ${newApiKey}`);
728
- } else {
729
- apiKeyInput.value = localStorage.getItem('apiKey') || 'sk-1234567';
730
- showNotification('API Key 不能为空,已恢复为上次保存的值');
731
- }
732
- });
733
- } else {
734
- console.error('API Key input element not found.');
735
- }
736
-
737
- const copyBaseUrlBtn = document.getElementById('copyBaseUrlBtn');
738
- if (copyBaseUrlBtn) {
739
- copyBaseUrlBtn.addEventListener('click', async () => {
740
- try {
741
- await navigator.clipboard.writeText(baseUrlInput.value);
742
- showNotification('Base URL 已复制到剪贴板');
743
- } catch (err) {
744
- showNotification('复制失败,请重试');
745
- }
746
- });
747
- }
748
-
749
- const copyApiKeyBtn = document.getElementById('copyApiKeyBtn');
750
- if (copyApiKeyBtn) {
751
- copyApiKeyBtn.addEventListener('click', async () => {
752
- try {
753
- await navigator.clipboard.writeText(apiKeyInput.value);
754
- showNotification('API Key 已复制到剪贴板');
755
- } catch (err) {
756
- showNotification('复制失败,请重试');
757
- }
758
- });
759
- }
760
-
761
- const addSingleTokenBtn = document.getElementById('addSingleTokenBtn');
762
- if (addSingleTokenBtn) {
763
- addSingleTokenBtn.addEventListener('click', async () => {
764
- const tokenInput = document.getElementById('singleTokenInput');
765
- const tokenText = tokenInput.value.trim();
766
- if (tokenText) {
767
- try {
768
- const baseUrl = document.getElementById('baseUrl').value;
769
- const response = await fetch(`${baseUrl}/manager/api/add`, {
770
- method: 'POST',
771
- headers: { 'Content-Type': 'application/json' },
772
- body: JSON.stringify({ sso: tokenText })
773
- });
774
- if (response.ok) {
775
- tokenInput.value = '';
776
- await fetchTokenMap();
777
- showNotification('Token 添加成功');
778
- } else {
779
- showNotification('添加 Token 失败');
780
- }
781
- } catch (error) {
782
- showNotification('添加 Token 出错');
783
- }
784
- } else {
785
- showNotification('请输入 Token');
786
- }
787
- });
788
- }
789
-
790
- const addBatchTokenBtn = document.getElementById('addBatchTokenBtn');
791
- if (addBatchTokenBtn) {
792
- addBatchTokenBtn.addEventListener('click', async () => {
793
- const tokenInput = document.getElementById('batchTokenInput');
794
- const tokenText = tokenInput.value.trim();
795
- if (tokenText) {
796
- const tokens = tokenText.split(',').map(t => t.trim()).filter(t => t.length > 0);
797
- if (tokens.length === 0) {
798
- showNotification('请输入至少一个有效的 Token');
799
- return;
800
- }
801
- try {
802
- const baseUrl = document.getElementById('baseUrl').value;
803
- const successes = [];
804
- const failures = [];
805
- for (const token of tokens) {
806
- const response = await fetch(`${baseUrl}/manager/api/add`, {
807
- method: 'POST',
808
- headers: { 'Content-Type': 'application/json' },
809
- body: JSON.stringify({ sso: token })
810
- });
811
- if (response.ok) {
812
- successes.push(token);
813
- } else {
814
- failures.push(token);
815
- }
816
- }
817
- tokenInput.value = '';
818
- await fetchTokenMap();
819
- if (successes.length > 0 && failures.length === 0) {
820
- showNotification(`成功添加 ${successes.length} 个 Token`);
821
- } else if (successes.length > 0 && failures.length > 0) {
822
- showNotification(`成功添加 ${successes.length} 个 Token,失败 ${failures.length} 个`);
823
- } else {
824
- showNotification('所有 Token 添加失败');
825
- }
826
- } catch (error) {
827
- showNotification('添加 Token 时出错');
828
- }
829
- } else {
830
- showNotification('请输入 Token');
831
- }
832
- });
833
- }
834
-
835
- const setCfBtn = document.getElementById('setCfBtn');
836
- if (setCfBtn) {
837
- setCfBtn.addEventListener('click', async () => {
838
- const cfInput = document.getElementById('cfInput');
839
- const newCf = cfInput.value.trim();
840
- if (newCf) {
841
- try {
842
- const baseUrl = document.getElementById('baseUrl').value;
843
- const response = await fetch(`${baseUrl}/manager/api/cf_clearance`, {
844
- method: 'POST',
845
- headers: { 'Content-Type': 'application/json' },
846
- body: JSON.stringify({ cf_clearance: newCf })
847
- });
848
- if (response.ok) {
849
- cfInput.value = '';
850
- showNotification('CF Clearance 设置成功');
851
- } else {
852
- showNotification('设置 CF Clearance 失败');
853
- }
854
- } catch (error) {
855
- showNotification('设置 CF Clearance 出错');
856
- }
857
- }
858
- });
859
- }
860
-
861
- const testAvailabilityBtn = document.getElementById('testAvailabilityBtn');
862
- if (testAvailabilityBtn) {
863
- testAvailabilityBtn.addEventListener('click', async () => {
864
- const selectedModel = document.getElementById('modelSelect').value;
865
- const baseUrl = document.getElementById('baseUrl').value;
866
- const apiKey = document.getElementById('apiKey').value.trim();
867
- const apiUrl = `${baseUrl}/v1/chat/completions`;
868
- try {
869
- const response = await fetch(apiUrl, {
870
- method: 'POST',
871
- headers: {
872
- 'Content-Type': 'application/json',
873
- 'Authorization': `Bearer ${apiKey}`
874
- },
875
- body: JSON.stringify({
876
- model: selectedModel,
877
- messages: [{ role: 'user', content: 'Hello' }],
878
- max_tokens: 10
879
- })
880
- });
881
- if (response.ok) {
882
- const data = await response.json();
883
- if (data.choices && data.choices.length > 0) {
884
- showNotification(`${selectedModel} 模型可用`);
885
- } else {
886
- showNotification(`${selectedModel} 模型不可用`);
887
- }
888
- } else {
889
- const errorText = await response.text();
890
- showNotification(`${selectedModel} 模型不可用(错误: ${response.status} - ${errorText})`);
891
- }
892
- } catch (error) {
893
- console.error('检测模型出错:', error);
894
- showNotification(`检测 ${selectedModel} 失败,请检查网络或接口`);
895
- }
896
- });
897
- }
898
-
899
- const batchDeleteTokens = document.getElementById('batchDeleteTokens');
900
- if (batchDeleteTokens) {
901
- batchDeleteTokens.addEventListener('click', async () => {
902
- if (!batchDeleteMode) {
903
- batchDeleteMode = true;
904
- renderTokenDiff(tokenMap);
905
- showNotification('请选择要删除的 Token');
906
- } else {
907
- const selectedTokens = Array.from(document.querySelectorAll('.token-checkbox:checked')).map(cb => cb.value);
908
- if (selectedTokens.length === 0) {
909
- showNotification('请至少选择一个 Token');
910
- return;
911
- }
912
- if (confirm(`确认删除 ${selectedTokens.length} 个 Token?`)) {
913
- try {
914
- const baseUrl = document.getElementById('baseUrl').value;
915
- const successes = [];
916
- const failures = [];
917
- for (const token of selectedTokens) {
918
- const response = await fetch(`${baseUrl}/manager/api/delete`, {
919
- method: 'POST',
920
- headers: { 'Content-Type': 'application/json' },
921
- body: JSON.stringify({ sso: token })
922
- });
923
- if (response.ok) {
924
- successes.push(token);
925
- } else {
926
- failures.push(token);
927
- }
928
- }
929
- batchDeleteMode = false;
930
- await fetchTokenMap();
931
- if (successes.length > 0 && failures.length === 0) {
932
- showNotification(`成功删除 ${successes.length} 个 Token`);
933
- } else if (successes.length > 0 && failures.length > 0) {
934
- showNotification(`成功删除 ${successes.length} 个 Token,失败 ${failures.length} 个`);
935
- } else {
936
- showNotification('所有 Token 删除失败');
937
- }
938
- } catch (error) {
939
- showNotification('删除 Token 时出错');
940
- }
941
- }
942
- }
943
- });
944
- }
945
-
946
- const searchInput = document.getElementById('searchInput');
947
- if (searchInput) searchInput.addEventListener('input', () => renderTokenDiff(tokenMap));
948
-
949
- const statusFilter = document.getElementById('statusFilter');
950
- if (statusFilter) statusFilter.addEventListener('change', () => renderTokenDiff(tokenMap));
951
-
952
- const refreshTokens = document.getElementById('refreshTokens');
953
- if (refreshTokens) {
954
- refreshTokens.addEventListener('click', async () => {
955
- await fetchTokenMap();
956
- showNotification('Token 列表已刷新');
957
- });
958
- } else {
959
- console.error('Refresh Tokens button not found.');
960
- }
961
-
962
- const prevPage = document.getElementById('prevPage');
963
- if (prevPage) {
964
- prevPage.addEventListener('click', () => {
965
- if (currentPage > 1) {
966
- currentPage--;
967
- renderTokenDiff(tokenMap);
968
- }
969
- });
970
- }
971
-
972
- const nextPage = document.getElementById('nextPage');
973
- if (nextPage) {
974
- nextPage.addEventListener('click', () => {
975
- const totalPages = Math.ceil(filterTokensArray(Object.entries(tokenMap)).length / itemsPerPage);
976
- if (currentPage < totalPages) {
977
- currentPage++;
978
- renderTokenDiff(tokenMap);
979
- }
980
- });
981
- }
982
-
983
- const pageSelect = document.getElementById('pageSelect');
984
- if (pageSelect) {
985
- pageSelect.addEventListener('change', (e) => {
986
- currentPage = parseInt(e.target.value, 10);
987
- renderTokenDiff(tokenMap);
988
- });
989
- }
990
-
991
- fetchTokenMap(); // 页面加载时获取 Token
992
-
993
- let timer = setInterval(updateExpiredTokenTimers, 60000);
994
- document.addEventListener('visibilitychange', () => {
995
- if (document.hidden) {
996
- clearInterval(timer);
997
- } else {
998
- timer = setInterval(updateExpiredTokenTimers, 60000);
999
- }
1000
- });
1001
- });
1002
-
1003
- function showNotification(message) {
1004
- const notification = document.getElementById('notification');
1005
- if (notification) {
1006
- notification.textContent = message;
1007
- notification.style.display = 'block';
1008
- const duration = Math.max(2000, message.length * 100);
1009
- setTimeout(() => {
1010
- notification.style.display = 'none';
1011
- }, duration);
1012
- } else {
1013
- console.warn('Notification element not found.');
1014
- }
1015
- }
1016
- </script>
1017
- </body>
1018
- </html>