nanoppa commited on
Commit
766735c
·
verified ·
1 Parent(s): 6ae2cda

Upload 31 files

Browse files
Dockerfile CHANGED
@@ -1,39 +1,49 @@
1
- FROM python:3.11-slim AS base
 
2
 
3
- WORKDIR /app
4
-
5
- # 安装依赖阶段
6
- FROM base AS dependencies
7
 
8
- # 安装运行时需要的系统依赖
9
- RUN apt-get update && \
10
- apt-get install -y --no-install-recommends \
11
- gcc \
12
- && rm -rf /var/lib/apt/lists/*
13
-
14
- # 复制并安装 Python 依赖
15
  COPY requirements.txt .
16
- RUN pip install --no-cache-dir --upgrade pip && \
17
- pip install --no-cache-dir -r requirements.txt
18
-
19
- # 最终运行阶段
20
- FROM python:3.11-slim AS runtime
 
 
 
 
 
 
21
 
22
  WORKDIR /app
23
 
24
- # 复制必要 Python 依赖
25
- COPY --from=dependencies /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
26
- COPY --from=dependencies /usr/local/bin /usr/local/bin
 
 
 
 
 
27
 
28
- # 复制应用代码
29
- COPY . .
30
 
31
  # 创建必要的目录和文件
32
- RUN mkdir -p /app/logs /app/data/temp && \
33
  echo '{"ssoNormal": {}, "ssoSuper": {}}' > /app/data/token.json
34
 
35
- RUN chmod -R 777 /app
 
 
 
 
 
 
 
36
 
37
- EXPOSE 7860
38
 
39
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # 构建阶段
2
+ FROM python:3.11-slim AS builder
3
 
4
+ WORKDIR /build
 
 
 
5
 
6
+ # 安装依赖到独立目录
 
 
 
 
 
 
7
  COPY requirements.txt .
8
+ RUN pip install --no-cache-dir --only-binary=:all: --prefix=/install -r requirements.txt && \
9
+ find /install -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \
10
+ find /install -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \
11
+ find /install -type d -name "test" -exec rm -rf {} + 2>/dev/null || true && \
12
+ find /install -type d -name "*.dist-info" -exec sh -c 'rm -f "$1"/RECORD "$1"/INSTALLER' _ {} \; && \
13
+ find /install -type f -name "*.pyc" -delete && \
14
+ find /install -type f -name "*.pyo" -delete && \
15
+ find /install -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
16
+
17
+ # 运行阶段 - 使用最小镜像
18
+ FROM python:3.11-slim
19
 
20
  WORKDIR /app
21
 
22
+ # 清理基础镜像中冗余文件
23
+ RUN rm -rf /usr/share/doc/* \
24
+ /usr/share/man/* \
25
+ /usr/share/locale/* \
26
+ /var/cache/apt/* \
27
+ /var/lib/apt/lists/* \
28
+ /tmp/* \
29
+ /var/tmp/*
30
 
31
+ # 从构建阶段复制已安装的包
32
+ COPY --from=builder /install /usr/local
33
 
34
  # 创建必要的目录和文件
35
+ RUN mkdir -p /app/logs /app/data/temp/image /app/data/temp/video && \
36
  echo '{"ssoNormal": {}, "ssoSuper": {}}' > /app/data/token.json
37
 
38
+ # 复制应用代码
39
+ COPY app/ ./app/
40
+ COPY main.py .
41
+ COPY data/setting.toml ./data/
42
+
43
+ # 删除 Python 字节码和缓存
44
+ ENV PYTHONDONTWRITEBYTECODE=1 \
45
+ PYTHONUNBUFFERED=1
46
 
47
+ EXPOSE 8000
48
 
49
+ CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
app/api/admin/manage.py CHANGED
@@ -69,6 +69,8 @@ class TokenInfo(BaseModel):
69
  remaining_queries: int
70
  heavy_remaining_queries: int
71
  status: str # "未使用"、"限流中"、"失效"、"正常"
 
 
72
 
73
 
74
  class TokenListResponse(BaseModel):
@@ -300,7 +302,9 @@ async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListRespo
300
  created_time=parse_created_time(data.get("createdTime")),
301
  remaining_queries=data.get("remainingQueries", -1),
302
  heavy_remaining_queries=data.get("heavyremainingQueries", -1),
303
- status=get_token_status(data, "sso")
 
 
304
  ))
305
 
306
  # 处理Super Token
@@ -312,7 +316,9 @@ async def list_tokens(_: bool = Depends(verify_admin_session)) -> TokenListRespo
312
  created_time=parse_created_time(data.get("createdTime")),
313
  remaining_queries=data.get("remainingQueries", -1),
314
  heavy_remaining_queries=data.get("heavyremainingQueries", -1),
315
- status=get_token_status(data, "ssoSuper")
 
 
316
  ))
317
 
318
  normal_count = len(normal_tokens)
@@ -719,3 +725,235 @@ async def get_storage_mode(_: bool = Depends(verify_admin_session)) -> Dict[str,
719
  status_code=500,
720
  detail={"error": f"获取存储模式失败: {str(e)}", "code": "STORAGE_MODE_ERROR"}
721
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  remaining_queries: int
70
  heavy_remaining_queries: int
71
  status: str # "未使用"、"限流中"、"失效"、"正常"
72
+ tags: List[str] = [] # 标签列表
73
+ note: str = "" # 备注
74
 
75
 
76
  class TokenListResponse(BaseModel):
 
302
  created_time=parse_created_time(data.get("createdTime")),
303
  remaining_queries=data.get("remainingQueries", -1),
304
  heavy_remaining_queries=data.get("heavyremainingQueries", -1),
305
+ status=get_token_status(data, "sso"),
306
+ tags=data.get("tags", []), # 向后兼容,如果没有tags字段则返回空列表
307
+ note=data.get("note", "") # 向后兼容,如果没有note字段则返回空字符串
308
  ))
309
 
310
  # 处理Super Token
 
316
  created_time=parse_created_time(data.get("createdTime")),
317
  remaining_queries=data.get("remainingQueries", -1),
318
  heavy_remaining_queries=data.get("heavyremainingQueries", -1),
319
+ status=get_token_status(data, "ssoSuper"),
320
+ tags=data.get("tags", []), # 向后兼容,如果没有tags字段则返回空列表
321
+ note=data.get("note", "") # 向后兼容,如果没有note字段则返回空字符串
322
  ))
323
 
324
  normal_count = len(normal_tokens)
 
725
  status_code=500,
726
  detail={"error": f"获取存储模式失败: {str(e)}", "code": "STORAGE_MODE_ERROR"}
727
  )
728
+
729
+
730
+ class UpdateTokenTagsRequest(BaseModel):
731
+ """更新Token标签请求"""
732
+ token: str
733
+ token_type: str
734
+ tags: List[str]
735
+
736
+
737
+ @router.post("/api/tokens/tags")
738
+ async def update_token_tags(
739
+ request: UpdateTokenTagsRequest,
740
+ _: bool = Depends(verify_admin_session)
741
+ ) -> Dict[str, Any]:
742
+ """
743
+ 更新Token标签
744
+
745
+ 为指定Token添加或修改标签。
746
+ """
747
+ try:
748
+ logger.debug(f"[Admin] 更新Token标签 - Token: {request.token[:10]}..., Tags: {request.tags}")
749
+
750
+ # 验证并转换token类型
751
+ token_type = validate_token_type(request.token_type)
752
+
753
+ # 更新标签
754
+ await token_manager.update_token_tags(request.token, token_type, request.tags)
755
+
756
+ logger.debug(f"[Admin] Token标签更新成功 - Token: {request.token[:10]}..., Tags: {request.tags}")
757
+
758
+ return {
759
+ "success": True,
760
+ "message": "标签更新成功",
761
+ "tags": request.tags
762
+ }
763
+
764
+ except HTTPException:
765
+ raise
766
+ except Exception as e:
767
+ logger.error(f"[Admin] Token标签更新异常 - Token: {request.token[:10]}..., 错误: {str(e)}")
768
+ raise HTTPException(
769
+ status_code=500,
770
+ detail={"error": f"更新标签失败: {str(e)}", "code": "UPDATE_TAGS_ERROR"}
771
+ )
772
+
773
+
774
+ @router.get("/api/tokens/tags/all")
775
+ async def get_all_tags(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
776
+ """
777
+ 获取所有标签
778
+
779
+ 返回系统中所有Token使用的标签列表(去重)。
780
+ """
781
+ try:
782
+ logger.debug("[Admin] 获取所有标签")
783
+
784
+ all_tokens_data = token_manager.get_tokens()
785
+ tags_set = set()
786
+
787
+ # 收集所有标签
788
+ for token_type_data in all_tokens_data.values():
789
+ for token_data in token_type_data.values():
790
+ tags = token_data.get("tags", [])
791
+ if isinstance(tags, list):
792
+ tags_set.update(tags)
793
+
794
+ tags_list = sorted(list(tags_set))
795
+ logger.debug(f"[Admin] 标签获取成功 - 共 {len(tags_list)} 个标签")
796
+
797
+ return {
798
+ "success": True,
799
+ "data": tags_list
800
+ }
801
+
802
+ except Exception as e:
803
+ logger.error(f"[Admin] 获取标签异常 - 错误: {str(e)}")
804
+ raise HTTPException(
805
+ status_code=500,
806
+ detail={"error": f"获取标签失败: {str(e)}", "code": "GET_TAGS_ERROR"}
807
+ )
808
+
809
+
810
+ class UpdateTokenNoteRequest(BaseModel):
811
+ """更新Token备注请求"""
812
+ token: str
813
+ token_type: str
814
+ note: str
815
+
816
+
817
+ @router.post("/api/tokens/note")
818
+ async def update_token_note(
819
+ request: UpdateTokenNoteRequest,
820
+ _: bool = Depends(verify_admin_session)
821
+ ) -> Dict[str, Any]:
822
+ """
823
+ 更新Token备注
824
+
825
+ 为指定Token添加或修改备注信息。
826
+ """
827
+ try:
828
+ logger.debug(f"[Admin] 更新Token备注 - Token: {request.token[:10]}...")
829
+
830
+ # 验证并转换token类型
831
+ token_type = validate_token_type(request.token_type)
832
+
833
+ # 更新备注
834
+ await token_manager.update_token_note(request.token, token_type, request.note)
835
+
836
+ logger.debug(f"[Admin] Token备注更新成功 - Token: {request.token[:10]}...")
837
+
838
+ return {
839
+ "success": True,
840
+ "message": "备注更新成功",
841
+ "note": request.note
842
+ }
843
+
844
+ except HTTPException:
845
+ raise
846
+ except Exception as e:
847
+ logger.error(f"[Admin] Token备注更新异常 - Token: {request.token[:10]}..., 错误: {str(e)}")
848
+ raise HTTPException(
849
+ status_code=500,
850
+ detail={"error": f"更新备注失败: {str(e)}", "code": "UPDATE_NOTE_ERROR"}
851
+ )
852
+
853
+
854
+ class TestTokenRequest(BaseModel):
855
+ """测试Token请求"""
856
+ token: str
857
+ token_type: str
858
+
859
+
860
+ @router.post("/api/tokens/test")
861
+ async def test_token(
862
+ request: TestTokenRequest,
863
+ _: bool = Depends(verify_admin_session)
864
+ ) -> Dict[str, Any]:
865
+ """
866
+ 测试Token可用性
867
+
868
+ 通过发送速率限制检查请求来验证Token是否有效。
869
+ 根据不同的HTTP状态码进行相应的处理:
870
+ - 401: Token失效
871
+ - 403: 服务器被block,不改变Token状态
872
+ - 其他错误: 设置为限流状态
873
+ """
874
+ try:
875
+ logger.debug(f"[Admin] 测试Token - Token: {request.token[:10]}...")
876
+
877
+ # 验证并转换token类型
878
+ token_type = validate_token_type(request.token_type)
879
+
880
+ # 构造完整的auth token
881
+ auth_token = f"sso-rw={request.token};sso={request.token}"
882
+
883
+ # 使用check_limits方法测试token
884
+ result = await token_manager.check_limits(auth_token, "grok-4-fast")
885
+
886
+ if result:
887
+ logger.debug(f"[Admin] Token测试成功 - Token: {request.token[:10]}...")
888
+ return {
889
+ "success": True,
890
+ "message": "Token有效",
891
+ "data": {
892
+ "valid": True,
893
+ "remaining_queries": result.get("remainingTokens", -1),
894
+ "limit": result.get("limit", -1)
895
+ }
896
+ }
897
+ else:
898
+ # 测试失败,check_limits方法已经调用了record_failure处理错误
899
+ # 现在检查token的状态来判断错误类型
900
+ logger.warning(f"[Admin] Token测试失败 - Token: {request.token[:10]}...")
901
+
902
+ # 检查token当前状态
903
+ all_tokens = token_manager.get_tokens()
904
+ token_data = all_tokens.get(token_type.value, {}).get(request.token)
905
+
906
+ if token_data:
907
+ if token_data.get("status") == "expired":
908
+ # Token被标记为失效(401错误)
909
+ return {
910
+ "success": False,
911
+ "message": "Token已失效",
912
+ "data": {
913
+ "valid": False,
914
+ "error_type": "expired",
915
+ "error_code": 401
916
+ }
917
+ }
918
+ elif token_data.get("remainingQueries") == 0:
919
+ # Token被设置为限流状态(其他错误)
920
+ return {
921
+ "success": False,
922
+ "message": "Token已被限流",
923
+ "data": {
924
+ "valid": False,
925
+ "error_type": "limited",
926
+ "error_code": "other"
927
+ }
928
+ }
929
+ else:
930
+ # 可能是403错误或其他网络问题,token状态未变
931
+ return {
932
+ "success": False,
933
+ "message": "服务器被block或网络错误",
934
+ "data": {
935
+ "valid": False,
936
+ "error_type": "blocked",
937
+ "error_code": 403
938
+ }
939
+ }
940
+ else:
941
+ # 找不到token数据
942
+ return {
943
+ "success": False,
944
+ "message": "Token数据异常",
945
+ "data": {
946
+ "valid": False,
947
+ "error_type": "unknown",
948
+ "error_code": "data_error"
949
+ }
950
+ }
951
+
952
+ except HTTPException:
953
+ raise
954
+ except Exception as e:
955
+ logger.error(f"[Admin] Token测试异常 - Token: {request.token[:10]}..., 错误: {str(e)}")
956
+ raise HTTPException(
957
+ status_code=500,
958
+ detail={"error": f"测试Token失败: {str(e)}", "code": "TEST_TOKEN_ERROR"}
959
+ )
app/api/v1/chat.py CHANGED
@@ -41,7 +41,7 @@ async def chat_completions(
41
  HTTPException: 当请求处理失败时
42
  """
43
  try:
44
- logger.info(f"[Chat] 聊天请求 - 模型: {request.model}")
45
 
46
  # 调用Grok客户端处理请求
47
  result = await GrokClient.openai_to_grok(request.model_dump())
 
41
  HTTPException: 当请求处理失败时
42
  """
43
  try:
44
+ logger.info(f"[Chat] 收到聊天请求")
45
 
46
  # 调用Grok客户端处理请求
47
  result = await GrokClient.openai_to_grok(request.model_dump())
app/core/auth.py CHANGED
@@ -46,7 +46,7 @@ class AuthManager:
46
  }
47
  )
48
 
49
- logger.debug("[Auth] 令牌认证成功")
50
  return credentials.credentials
51
 
52
 
 
46
  }
47
  )
48
 
49
+ logger.debug("[Auth] 令牌认证成功")
50
  return credentials.credentials
51
 
52
 
app/services/grok/cache.py CHANGED
@@ -29,7 +29,7 @@ class CacheService:
29
  """下载并缓存文件"""
30
  cache_path = self._cache_path(file_path)
31
  if cache_path.exists():
32
- logger.debug(f"[{self.cache_type.upper()}Cache] 文件缓存: {cache_path}")
33
  return cache_path
34
 
35
  try:
@@ -54,7 +54,7 @@ class CacheService:
54
  logger.debug(f"[{self.cache_type.upper()}Cache] 使用缓存代理: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
55
 
56
  async with AsyncSession() as session:
57
- logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: https://assets.grok.com{file_path}")
58
  response = await session.get(
59
  f"https://assets.grok.com{file_path}",
60
  headers=headers,
@@ -65,7 +65,7 @@ class CacheService:
65
  )
66
  response.raise_for_status()
67
  cache_path.write_bytes(response.content)
68
- logger.debug(f"[{self.cache_type.upper()}Cache] 文件缓存: {cache_path} ({len(response.content)} bytes)")
69
  asyncio.create_task(self.cleanup_cache())
70
  return cache_path
71
  except Exception as e:
@@ -88,10 +88,10 @@ class CacheService:
88
  total_size = sum(size for _, size, _ in files)
89
 
90
  if total_size <= max_size_bytes:
91
- logger.debug(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB,未限")
92
  return
93
 
94
- logger.info(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB,开始清理")
95
  files.sort(key=lambda x: x[2])
96
 
97
  for file_path, size, _ in files:
@@ -99,9 +99,9 @@ class CacheService:
99
  break
100
  file_path.unlink()
101
  total_size -= size
102
- logger.debug(f"[{self.cache_type.upper()}Cache] 已删除缓存文件: {file_path}")
103
 
104
- logger.info(f"[{self.cache_type.upper()}Cache] 缓存清理完成,当前大小 {total_size / 1024 / 1024:.2f}MB")
105
  except Exception as e:
106
  logger.error(f"[{self.cache_type.upper()}Cache] 清理缓存失败: {e}")
107
 
 
29
  """下载并缓存文件"""
30
  cache_path = self._cache_path(file_path)
31
  if cache_path.exists():
32
+ logger.debug(f"[{self.cache_type.upper()}Cache] 文件缓存成功")
33
  return cache_path
34
 
35
  try:
 
54
  logger.debug(f"[{self.cache_type.upper()}Cache] 使用缓存代理: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
55
 
56
  async with AsyncSession() as session:
57
+ logger.debug(f"[{self.cache_type.upper()}Cache] 缓存生成视频文件: https://assets.grok.com{file_path}")
58
  response = await session.get(
59
  f"https://assets.grok.com{file_path}",
60
  headers=headers,
 
65
  )
66
  response.raise_for_status()
67
  cache_path.write_bytes(response.content)
68
+ logger.debug(f"[{self.cache_type.upper()}Cache] 文件缓存成功")
69
  asyncio.create_task(self.cleanup_cache())
70
  return cache_path
71
  except Exception as e:
 
88
  total_size = sum(size for _, size, _ in files)
89
 
90
  if total_size <= max_size_bytes:
91
+ logger.debug(f"[{self.cache_type.upper()}Cache] 缓存大小统计: {total_size / 1024 / 1024:.2f}MB,未达到")
92
  return
93
 
94
+ logger.info(f"[{self.cache_type.upper()}Cache] 清理缓存,当前大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB")
95
  files.sort(key=lambda x: x[2])
96
 
97
  for file_path, size, _ in files:
 
99
  break
100
  file_path.unlink()
101
  total_size -= size
102
+ logger.debug(f"[{self.cache_type.upper()}Cache] 清理缓存文件: {file_path}")
103
 
104
+ logger.info(f"[{self.cache_type.upper()}Cache] 清理缓存完成,当前大小 {total_size / 1024 / 1024:.2f}MB")
105
  except Exception as e:
106
  logger.error(f"[{self.cache_type.upper()}Cache] 清理缓存失败: {e}")
107
 
app/services/grok/client.py CHANGED
@@ -13,6 +13,7 @@ from app.services.grok.processer import GrokResponseProcessor
13
  from app.services.grok.statsig import get_dynamic_headers
14
  from app.services.grok.token import token_manager
15
  from app.services.grok.upload import ImageUploadManager
 
16
  from app.core.exception import GrokApiException
17
 
18
  # 常量定义
@@ -32,8 +33,6 @@ class GrokClient:
32
  messages = openai_request["messages"]
33
  stream = openai_request.get("stream", False)
34
 
35
- logger.debug(f"[Client] 处理请求 - 模型:{model}, 消息数:{len(messages)}, 流式:{stream}")
36
-
37
  # 提取消息内容和图片URL
38
  content, image_urls = GrokClient._extract_content(messages)
39
  model_name, model_mode = Models.to_grok(model)
@@ -44,8 +43,6 @@ class GrokClient:
44
  if len(image_urls) > 1:
45
  logger.warning(f"[Client] 视频模型只允许一张图片,当前有{len(image_urls)}张,只使用第一张")
46
  image_urls = image_urls[:1]
47
- content = f"{content} --mode=custom"
48
- logger.debug(f"[Client] 视频模型文本处理: {content}")
49
 
50
  # 重试逻辑
51
  return await GrokClient._try(model, content, image_urls, model_name, model_mode, is_video_model, stream)
@@ -61,11 +58,23 @@ class GrokClient:
61
  auth_token = token_manager.get_token(model)
62
 
63
  # 上传图片
64
- imgs = await GrokClient._upload_imgs(image_urls, auth_token)
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  # 构建并发送请求
67
- payload = GrokClient._build_payload(content, model_name, model_mode, imgs, is_video)
68
- return await GrokClient._send_request(payload, auth_token, model, stream)
69
 
70
  except GrokApiException as e:
71
  last_err = e
@@ -95,7 +104,7 @@ class GrokClient:
95
  for msg in messages:
96
  msg_content = msg.get("content", "")
97
 
98
- # 处理复杂消息格式(包含文本和图片)
99
  if isinstance(msg_content, list):
100
  for item in msg_content:
101
  item_type = item.get("type")
@@ -112,23 +121,25 @@ class GrokClient:
112
  return "".join(content_parts), image_urls
113
 
114
  @staticmethod
115
- async def _upload_imgs(image_urls: List[str], auth_token: str) -> List[str]:
116
  """上传图片并返回附件ID列表"""
117
  image_attachments = []
 
118
  # 并发上传所有图片
119
  tasks = [ImageUploadManager.upload(url, auth_token) for url in image_urls]
120
  results = await asyncio.gather(*tasks, return_exceptions=True)
121
 
122
- for url, result in zip(image_urls, results):
123
- if isinstance(result, Exception):
124
- logger.warning(f"[Client] 图片上传失败: {url}, 错误: {result}")
125
- elif result:
126
- image_attachments.append(result)
 
127
 
128
- return image_attachments
129
 
130
  @staticmethod
131
- def _build_payload(content: str, model_name: str, model_mode: str, image_attachments: List[str], is_video_model: bool = False) -> Dict[str, Any]:
132
  """构建Grok API请求载荷"""
133
  payload = {
134
  "temporary": setting.grok_config.get("temporary", True),
@@ -156,15 +167,28 @@ class GrokClient:
156
  "isAsyncChat": False
157
  }
158
 
159
- # 视��模型特殊配置
160
- if is_video_model:
161
- payload["toolOverrides"] = {"videoGen": True}
162
- logger.debug("[Client] 视频模型载荷配置: toolOverrides.videoGen = True")
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  return payload
165
 
166
  @staticmethod
167
- async def _send_request(payload: dict, auth_token: str, model: str, stream: bool):
168
  """发送HTTP请求到Grok API"""
169
  # 验证认证令牌
170
  if not auth_token:
@@ -173,14 +197,17 @@ class GrokClient:
173
  try:
174
  # 构建请求头
175
  headers = GrokClient._build_headers(auth_token)
 
 
 
 
 
 
176
 
177
  # 使用服务代理
178
  proxy_url = setting.get_service_proxy()
179
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
180
 
181
- if proxy_url:
182
- logger.debug(f"[Client] 使用服务代理: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
183
-
184
  # 构建请求参数
185
  request_kwargs = {
186
  "headers": headers,
@@ -198,8 +225,6 @@ class GrokClient:
198
  **request_kwargs
199
  )
200
 
201
- logger.debug(f"[Client] API响应状态码: {response.status_code}")
202
-
203
  # 处理非成功响应
204
  if response.status_code != 200:
205
  GrokClient._handle_error(response, auth_token)
@@ -234,12 +259,19 @@ class GrokClient:
234
  @staticmethod
235
  def _handle_error(response, auth_token: str):
236
  """处理错误响应"""
237
- try:
238
- error_data = response.json()
239
- error_message = str(error_data)
240
- except Exception as e:
241
- error_data = response.text
242
- error_message = error_data[:200] if error_data else e
 
 
 
 
 
 
 
243
 
244
  # 记录Token失败
245
  asyncio.create_task(token_manager.record_failure(auth_token, response.status_code, error_message))
 
13
  from app.services.grok.statsig import get_dynamic_headers
14
  from app.services.grok.token import token_manager
15
  from app.services.grok.upload import ImageUploadManager
16
+ from app.services.grok.create import PostCreateManager
17
  from app.core.exception import GrokApiException
18
 
19
  # 常量定义
 
33
  messages = openai_request["messages"]
34
  stream = openai_request.get("stream", False)
35
 
 
 
36
  # 提取消息内容和图片URL
37
  content, image_urls = GrokClient._extract_content(messages)
38
  model_name, model_mode = Models.to_grok(model)
 
43
  if len(image_urls) > 1:
44
  logger.warning(f"[Client] 视频模型只允许一张图片,当前有{len(image_urls)}张,只使用第一张")
45
  image_urls = image_urls[:1]
 
 
46
 
47
  # 重试逻辑
48
  return await GrokClient._try(model, content, image_urls, model_name, model_mode, is_video_model, stream)
 
58
  auth_token = token_manager.get_token(model)
59
 
60
  # 上传图片
61
+ imgs, uris = await GrokClient._upload_imgs(image_urls, auth_token)
62
+
63
+ # 视频模型 - 创建会话
64
+ post_id = None
65
+ if is_video and imgs and uris:
66
+ try:
67
+ create_result = await PostCreateManager.create(imgs[0], uris[0], auth_token)
68
+ if create_result and create_result.get("success"):
69
+ post_id = create_result.get("post_id")
70
+ else:
71
+ logger.warning(f"[Client] 创建会话失败,继续使用原有流程")
72
+ except Exception as e:
73
+ logger.warning(f"[Client] 创建会话异常: {e},继续使用原有流程")
74
 
75
  # 构建并发送请求
76
+ payload = GrokClient._build_payload(content, model_name, model_mode, imgs, uris, is_video, post_id)
77
+ return await GrokClient._send_request(payload, auth_token, model, stream, post_id)
78
 
79
  except GrokApiException as e:
80
  last_err = e
 
104
  for msg in messages:
105
  msg_content = msg.get("content", "")
106
 
107
+ # 处理复杂消息格式
108
  if isinstance(msg_content, list):
109
  for item in msg_content:
110
  item_type = item.get("type")
 
121
  return "".join(content_parts), image_urls
122
 
123
  @staticmethod
124
+ async def _upload_imgs(image_urls: List[str], auth_token: str) -> Tuple[List[str], List[str]]:
125
  """上传图片并返回附件ID列表"""
126
  image_attachments = []
127
+ image_uris = []
128
  # 并发上传所有图片
129
  tasks = [ImageUploadManager.upload(url, auth_token) for url in image_urls]
130
  results = await asyncio.gather(*tasks, return_exceptions=True)
131
 
132
+ for url, (file_id, file_uri) in zip(image_urls, results):
133
+ if isinstance(file_id, Exception):
134
+ logger.warning(f"[Client] 图片上传失败: {url}, 错误: {file_id}")
135
+ elif file_id:
136
+ image_attachments.append(file_id)
137
+ image_uris.append(file_uri)
138
 
139
+ return image_attachments, image_uris
140
 
141
  @staticmethod
142
+ def _build_payload(content: str, model_name: str, model_mode: str, image_attachments: List[str], image_uris: List[str], is_video_model: bool = False, post_id: str = None) -> Dict[str, Any]:
143
  """构建Grok API请求载荷"""
144
  payload = {
145
  "temporary": setting.grok_config.get("temporary", True),
 
167
  "isAsyncChat": False
168
  }
169
 
170
+ # 视模型配置
171
+ if is_video_model and image_uris:
172
+ image_url = image_uris[0]
173
+
174
+ # 构建 URL 消息
175
+ if post_id:
176
+ image_message = f"https://grok.com/imagine/{post_id} {content} --mode=custom"
177
+ else:
178
+ image_message = f"https://assets.grok.com/post/{image_url} {content} --mode=custom"
179
+
180
+ payload = {
181
+ "temporary": True,
182
+ "modelName": "grok-3",
183
+ "message": image_message,
184
+ "fileAttachments": image_attachments,
185
+ "toolOverrides": {"videoGen": True}
186
+ }
187
 
188
  return payload
189
 
190
  @staticmethod
191
+ async def _send_request(payload: dict, auth_token: str, model: str, stream: bool, post_id: str = None):
192
  """发送HTTP请求到Grok API"""
193
  # 验证认证令牌
194
  if not auth_token:
 
197
  try:
198
  # 构建请求头
199
  headers = GrokClient._build_headers(auth_token)
200
+ if model == "grok-imagine-0.9":
201
+ # 传入会话ID
202
+ file_attachments = payload.get("fileAttachments", [])
203
+ referer_id = post_id if post_id else (file_attachments[0] if file_attachments else "")
204
+ if referer_id:
205
+ headers["Referer"] = f"https://grok.com/imagine/{referer_id}"
206
 
207
  # 使用服务代理
208
  proxy_url = setting.get_service_proxy()
209
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
210
 
 
 
 
211
  # 构建请求参数
212
  request_kwargs = {
213
  "headers": headers,
 
225
  **request_kwargs
226
  )
227
 
 
 
228
  # 处理非成功响应
229
  if response.status_code != 200:
230
  GrokClient._handle_error(response, auth_token)
 
259
  @staticmethod
260
  def _handle_error(response, auth_token: str):
261
  """处理错误响应"""
262
+ # 处理 403 错误
263
+ if response.status_code == 403:
264
+ error_message = "服务器IP被Block,请尝试 1. 更换服务器IP 2. 使用代理IP 3. 服务器登陆Grok.com,过盾后F12找到CF值填入后台设置"
265
+ error_data = {"cf_blocked": True, "status": 403}
266
+ logger.warning(f"[Client] {error_message}")
267
+ else:
268
+ # 其他错误尝试解析 JSON
269
+ try:
270
+ error_data = response.json()
271
+ error_message = str(error_data)
272
+ except Exception as e:
273
+ error_data = response.text
274
+ error_message = error_data[:200] if error_data else str(e)
275
 
276
  # 记录Token失败
277
  asyncio.create_task(token_manager.record_failure(auth_token, response.status_code, error_message))
app/services/grok/create.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Post创建管理器"""
2
+
3
+ import json
4
+ from typing import Dict, Any, Optional
5
+ from curl_cffi.requests import AsyncSession
6
+
7
+ from app.services.grok.statsig import get_dynamic_headers
8
+ from app.core.exception import GrokApiException
9
+ from app.core.config import setting
10
+ from app.core.logger import logger
11
+
12
+ # 常量定义
13
+ CREATE_ENDPOINT = "https://grok.com/rest/media/post/create"
14
+ REQUEST_TIMEOUT = 30
15
+ IMPERSONATE_BROWSER = "chrome133a"
16
+
17
+
18
+ class PostCreateManager:
19
+ """
20
+ Grok 会话创建管理器
21
+
22
+ 提供图片会话创建功能,用于视频生成前的准备工作
23
+ """
24
+
25
+ @staticmethod
26
+ async def create(file_id: str, file_uri: str, auth_token: str) -> Optional[Dict[str, Any]]:
27
+ """
28
+ 创建会话记录
29
+
30
+ Args:
31
+ file_id: 上传后的文件ID
32
+ file_uri: 上传后的文件URI
33
+ auth_token: 认证令牌
34
+
35
+ Returns:
36
+ 创建的会话信息,包含会话ID等
37
+ """
38
+ try:
39
+ # 验证参数
40
+ if not file_id or not file_uri:
41
+ raise GrokApiException("会话ID或URI缺失", "INVALID_PARAMS")
42
+
43
+ if not auth_token:
44
+ raise GrokApiException("认证令牌缺失", "NO_AUTH_TOKEN")
45
+
46
+ # 构建创建数据
47
+ media_url = f"https://assets.grok.com/{file_uri}"
48
+
49
+ create_data = {
50
+ "media_url": media_url,
51
+ "media_type": "MEDIA_POST_TYPE_IMAGE"
52
+ }
53
+
54
+ # 获取认证令牌和cookie
55
+ cf_clearance = setting.grok_config.get("cf_clearance", "")
56
+ cookie = f"{auth_token};{cf_clearance}" if cf_clearance else auth_token
57
+
58
+ # 获取代理配置
59
+ proxy_url = setting.grok_config.get("proxy_url", "")
60
+ proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
61
+
62
+ # 发送异步请求
63
+ async with AsyncSession() as session:
64
+ response = await session.post(
65
+ CREATE_ENDPOINT,
66
+ headers={
67
+ **get_dynamic_headers("/rest/media/post/create"),
68
+ "Cookie": cookie,
69
+ },
70
+ json=create_data,
71
+ impersonate=IMPERSONATE_BROWSER,
72
+ timeout=REQUEST_TIMEOUT,
73
+ proxies=proxies,
74
+ )
75
+
76
+ # 检查响应
77
+ if response.status_code == 200:
78
+ result = response.json()
79
+ post_id = result.get("post", {}).get("id", "")
80
+ logger.debug(f"[PostCreate] 创建会话成功,会话ID: {post_id}")
81
+ return {
82
+ "post_id": post_id,
83
+ "file_id": file_id,
84
+ "file_uri": file_uri,
85
+ "success": True,
86
+ "data": result
87
+ }
88
+ else:
89
+ error_msg = f"状态码: {response.status_code}"
90
+ try:
91
+ error_data = response.json()
92
+ error_msg = f"{error_msg}, 详情: {error_data}"
93
+ except:
94
+ error_msg = f"{error_msg}, 详情: {response.text[:200]}"
95
+
96
+ logger.error(f"[PostCreate] 创建会话失败: {error_msg}")
97
+ raise GrokApiException(f"创建会话失败: {error_msg}", "CREATE_ERROR")
98
+
99
+ except GrokApiException:
100
+ raise
101
+ except Exception as e:
102
+ logger.error(f"[PostCreate] 创建会话异常: {e}")
103
+ raise GrokApiException(f"创建会话异常: {e}", "CREATE_ERROR") from e
app/services/grok/processer.py CHANGED
@@ -232,6 +232,7 @@ class GrokResponseProcessor:
232
  video_progress_started = False
233
  last_video_progress = -1
234
  response_closed = False
 
235
 
236
  # 初始化超时管理器
237
  timeout_manager = StreamTimeoutManager(
@@ -268,7 +269,7 @@ class GrokResponseProcessor:
268
  yield "data: [DONE]\n\n"
269
  return
270
 
271
- logger.debug(f"[Processor] 接收到数据块: {len(chunk)} bytes")
272
  if not chunk:
273
  continue
274
 
@@ -285,9 +286,12 @@ class GrokResponseProcessor:
285
 
286
  # 提取响应数据
287
  grok_resp = data.get("result", {}).get("response", {})
288
- logger.debug(f"[Processor] 解析响应数据: {len(grok_resp)} 字段")
289
  if not grok_resp:
290
  continue
 
 
 
291
 
292
  # 更新模型名称
293
  if user_resp := grok_resp.get("userResponse"):
@@ -297,40 +301,46 @@ class GrokResponseProcessor:
297
  # 提取视频数据
298
  if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
299
  progress = video_resp.get("progress", 0)
 
300
 
 
301
  if progress > last_video_progress:
302
  last_video_progress = progress
303
 
304
- # 添加 <think> 标签
305
- if not video_progress_started:
306
- content = f"<think>视频已生成{progress}%\n"
307
- video_progress_started = True
308
- elif progress < 100:
309
- content = f"视频已生成{progress}%\n"
310
- else:
311
- # 进度100%时关闭 <think> 标签并立即处理视频
312
- content = f"视频已生成{progress}%</think>\n"
313
-
314
- # 立即下载并缓存视频
315
- if v_url := video_resp.get("videoUrl"):
316
- logger.debug(f"[Processor] 视频生成完成: {v_url}")
317
- full_video_url = f"https://assets.grok.com/{v_url}"
318
-
319
- try:
320
- cache_path = await video_cache_service.download_video(f"/{v_url}", auth_token)
321
- if cache_path:
322
- video_path = v_url.replace('/', '-')
323
- base_url = setting.global_config.get("base_url", "")
324
- local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
325
- content += f'<video src="{local_video_url}" controls="controls"></video>\n'
326
- else:
327
- content += f'<video src="{full_video_url}" controls="controls"></video>\n'
328
- except Exception as e:
329
- logger.warning(f"[Processor] 缓存视频失败: {e}")
330
- content += f'<video src="{full_video_url}" controls="controls"></video>\n'
331
 
332
- yield make_chunk(content)
333
- timeout_manager.mark_chunk_received()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  chunk_index += 1
335
 
336
  continue
@@ -371,7 +381,6 @@ class GrokResponseProcessor:
371
 
372
  # 发送前缀
373
  yield make_chunk(markdown_prefix + mime_part)
374
- timeout_manager.mark_chunk_received()
375
  chunk_index += 1
376
 
377
  # 分块发送 base64 数据
@@ -379,24 +388,19 @@ class GrokResponseProcessor:
379
  for i in range(0, len(b64_data), chunk_size):
380
  chunk_data = b64_data[i:i + chunk_size]
381
  yield make_chunk(chunk_data)
382
- timeout_manager.mark_chunk_received()
383
  chunk_index += 1
384
 
385
  # 发送后缀
386
  yield make_chunk(markdown_suffix)
387
- timeout_manager.mark_chunk_received()
388
  chunk_index += 1
389
  else:
390
  yield make_chunk(f"![Generated Image]({base64_str})\n")
391
- timeout_manager.mark_chunk_received()
392
  chunk_index += 1
393
  else:
394
  yield make_chunk(f"![Generated Image]({base64_str})\n")
395
- timeout_manager.mark_chunk_received()
396
  chunk_index += 1
397
  else:
398
  yield make_chunk(f"![Generated Image](https://assets.grok.com/{img})\n")
399
- timeout_manager.mark_chunk_received()
400
  chunk_index += 1
401
  else:
402
  # url 模式:缓存并返回链接
@@ -412,11 +416,9 @@ class GrokResponseProcessor:
412
 
413
  # 发送内容
414
  yield make_chunk(content.strip(), "stop")
415
- timeout_manager.mark_chunk_received()
416
  return
417
  elif token:
418
  yield make_chunk(token)
419
- timeout_manager.mark_chunk_received()
420
  chunk_index += 1
421
 
422
  # 提取对话数据
@@ -441,14 +443,18 @@ class GrokResponseProcessor:
441
  if grok_resp.get("toolUsageCardId"):
442
  if web_search := grok_resp.get("webSearchResults"):
443
  if current_is_thinking:
444
- # 封装搜索结果
445
- for result in web_search.get("results", []):
446
- title = result.get("title", "")
447
- url = result.get("url", "")
448
- preview = result.get("preview", "")
449
- preview_clean = preview.replace("\n", "") if isinstance(preview, str) else ""
450
- token += f'\n- [{title}]({url} "{preview_clean}")'
451
- token += "\n"
 
 
 
 
452
  else:
453
  # 有 webSearchResults 但 isThinking 为 false
454
  continue
@@ -464,15 +470,28 @@ class GrokResponseProcessor:
464
  content = f"\n\n{token}\n\n"
465
 
466
  # is_thinking 状态切换
 
467
  if not is_thinking and current_is_thinking:
468
- content = f"<think>\n{content}"
 
 
 
 
469
  elif is_thinking and not current_is_thinking:
470
- content = f"\n</think>\n{content}"
 
 
471
  thinking_finished = True
472
-
473
- yield make_chunk(content)
474
- timeout_manager.mark_chunk_received()
475
- chunk_index += 1
 
 
 
 
 
 
476
  is_thinking = current_is_thinking
477
 
478
  except (json.JSONDecodeError, UnicodeDecodeError) as e:
@@ -492,7 +511,7 @@ class GrokResponseProcessor:
492
  logger.info(f"[Processor] 流式响应完成,总耗时: {timeout_manager.get_total_duration():.2f}秒")
493
 
494
  except Exception as e:
495
- logger.error(f"[Processor] 流式处理严重错误: {e}")
496
  yield make_chunk(f"处理错误: {e}", "error")
497
  # 发送流结束标记
498
  yield "data: [DONE]\n\n"
 
232
  video_progress_started = False
233
  last_video_progress = -1
234
  response_closed = False
235
+ show_thinking = setting.grok_config.get("show_thinking", True)
236
 
237
  # 初始化超时管理器
238
  timeout_manager = StreamTimeoutManager(
 
269
  yield "data: [DONE]\n\n"
270
  return
271
 
272
+ logger.debug(f"[Processor] 接收到数据块, 长度: {len(chunk)} bytes")
273
  if not chunk:
274
  continue
275
 
 
286
 
287
  # 提取响应数据
288
  grok_resp = data.get("result", {}).get("response", {})
289
+ logger.debug(f"[Processor] 解析响应数据, 长度: {len(grok_resp)} bytes")
290
  if not grok_resp:
291
  continue
292
+
293
+ # 更新超时计时器
294
+ timeout_manager.mark_chunk_received()
295
 
296
  # 更新模型名称
297
  if user_resp := grok_resp.get("userResponse"):
 
301
  # 提取视频数据
302
  if video_resp := grok_resp.get("streamingVideoGenerationResponse"):
303
  progress = video_resp.get("progress", 0)
304
+ v_url = video_resp.get("videoUrl")
305
 
306
+ # 处理进度更新
307
  if progress > last_video_progress:
308
  last_video_progress = progress
309
 
310
+ # 思考状态
311
+ if show_thinking:
312
+ # 添加 <think> 标签
313
+ if not video_progress_started:
314
+ content = f"<think>视频已生成{progress}%\n"
315
+ video_progress_started = True
316
+ elif progress < 100:
317
+ content = f"视频已生成{progress}%\n"
318
+ else:
319
+ # 关闭 <think> 标签
320
+ content = f"视频已生成{progress}%</think>\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ yield make_chunk(content)
323
+ chunk_index += 1
324
+
325
+ # 处理视频URL
326
+ if v_url:
327
+ logger.debug(f"[Processor] 视频生成完成")
328
+ full_video_url = f"https://assets.grok.com/{v_url}"
329
+
330
+ try:
331
+ cache_path = await video_cache_service.download_video(f"/{v_url}", auth_token)
332
+ if cache_path:
333
+ video_path = v_url.replace('/', '-')
334
+ base_url = setting.global_config.get("base_url", "")
335
+ local_video_url = f"{base_url}/images/{video_path}" if base_url else f"/images/{video_path}"
336
+ video_content = f'<video src="{local_video_url}" controls="controls"></video>\n'
337
+ else:
338
+ video_content = f'<video src="{full_video_url}" controls="controls"></video>\n'
339
+ except Exception as e:
340
+ logger.warning(f"[Processor] 缓存视频失败: {e}")
341
+ video_content = f'<video src="{full_video_url}" controls="controls"></video>\n'
342
+
343
+ yield make_chunk(video_content)
344
  chunk_index += 1
345
 
346
  continue
 
381
 
382
  # 发送前缀
383
  yield make_chunk(markdown_prefix + mime_part)
 
384
  chunk_index += 1
385
 
386
  # 分块发送 base64 数据
 
388
  for i in range(0, len(b64_data), chunk_size):
389
  chunk_data = b64_data[i:i + chunk_size]
390
  yield make_chunk(chunk_data)
 
391
  chunk_index += 1
392
 
393
  # 发送后缀
394
  yield make_chunk(markdown_suffix)
 
395
  chunk_index += 1
396
  else:
397
  yield make_chunk(f"![Generated Image]({base64_str})\n")
 
398
  chunk_index += 1
399
  else:
400
  yield make_chunk(f"![Generated Image]({base64_str})\n")
 
401
  chunk_index += 1
402
  else:
403
  yield make_chunk(f"![Generated Image](https://assets.grok.com/{img})\n")
 
404
  chunk_index += 1
405
  else:
406
  # url 模式:缓存并返回链接
 
416
 
417
  # 发送内容
418
  yield make_chunk(content.strip(), "stop")
 
419
  return
420
  elif token:
421
  yield make_chunk(token)
 
422
  chunk_index += 1
423
 
424
  # 提取对话数据
 
443
  if grok_resp.get("toolUsageCardId"):
444
  if web_search := grok_resp.get("webSearchResults"):
445
  if current_is_thinking:
446
+ if show_thinking:
447
+ # 封装搜索结��
448
+ for result in web_search.get("results", []):
449
+ title = result.get("title", "")
450
+ url = result.get("url", "")
451
+ preview = result.get("preview", "")
452
+ preview_clean = preview.replace("\n", "") if isinstance(preview, str) else ""
453
+ token += f'\n- [{title}]({url} "{preview_clean}")'
454
+ token += "\n"
455
+ else:
456
+ # show_thinking=false 时跳过 thinking 状态下的搜索结果
457
+ continue
458
  else:
459
  # 有 webSearchResults 但 isThinking 为 false
460
  continue
 
470
  content = f"\n\n{token}\n\n"
471
 
472
  # is_thinking 状态切换
473
+ should_skip = False
474
  if not is_thinking and current_is_thinking:
475
+ # 进入 thinking 状态
476
+ if show_thinking:
477
+ content = f"<think>\n{content}"
478
+ else:
479
+ should_skip = True
480
  elif is_thinking and not current_is_thinking:
481
+ # 退出 thinking 状态
482
+ if show_thinking:
483
+ content = f"\n</think>\n{content}"
484
  thinking_finished = True
485
+ elif current_is_thinking:
486
+ # 处于 thinking 状态中
487
+ if not show_thinking:
488
+ should_skip = True
489
+
490
+ # 只在不需要跳过时才发送
491
+ if not should_skip:
492
+ yield make_chunk(content)
493
+ chunk_index += 1
494
+
495
  is_thinking = current_is_thinking
496
 
497
  except (json.JSONDecodeError, UnicodeDecodeError) as e:
 
511
  logger.info(f"[Processor] 流式响应完成,总耗时: {timeout_manager.get_total_duration():.2f}秒")
512
 
513
  except Exception as e:
514
+ logger.error(f"[Processor] 流式处理发生严重错误: {e}")
515
  yield make_chunk(f"处理错误: {e}", "error")
516
  # 发送流结束标记
517
  yield "data: [DONE]\n\n"
app/services/grok/statsig.py CHANGED
@@ -1,11 +1,62 @@
1
  """Grok 请求头管理模块"""
2
 
 
 
 
3
  import uuid
4
  from typing import Dict
5
 
 
6
  from app.core.config import setting
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def get_dynamic_headers(pathname: str = "/rest/app-chat/conversations/new") -> Dict[str, str]:
10
  """获取请求头
11
 
@@ -15,10 +66,19 @@ def get_dynamic_headers(pathname: str = "/rest/app-chat/conversations/new") -> D
15
  Returns:
16
  请求头字典
17
  """
18
- # 获取配置的 x-statsig-id
19
- statsig_id = setting.grok_config.get("x_statsig_id")
20
- if not statsig_id:
21
- raise ValueError("配置文件中未设置 x_statsig_id")
 
 
 
 
 
 
 
 
 
22
 
23
  # 构建基础请求头
24
  headers = {
@@ -36,7 +96,7 @@ def get_dynamic_headers(pathname: str = "/rest/app-chat/conversations/new") -> D
36
  "Sec-Fetch-Dest": "empty",
37
  "Sec-Fetch-Mode": "cors",
38
  "Sec-Fetch-Site": "same-origin",
39
- "Baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
40
  "x-statsig-id": statsig_id,
41
  "x-xai-request-id": str(uuid.uuid4())
42
  }
 
1
  """Grok 请求头管理模块"""
2
 
3
+ import base64
4
+ import random
5
+ import string
6
  import uuid
7
  from typing import Dict
8
 
9
+ from app.core.logger import logger
10
  from app.core.config import setting
11
 
12
 
13
+ def _generate_random_string(length: int, use_letters: bool = True) -> str:
14
+ """生成随机字符串
15
+
16
+ Args:
17
+ length: 字符串长度
18
+ use_letters: 是否使用字母(True)或数字+字母(False)
19
+
20
+ Returns:
21
+ 随机字符串
22
+ """
23
+ if use_letters:
24
+ # 生成随机字母(小写)
25
+ return ''.join(random.choices(string.ascii_lowercase, k=length))
26
+ else:
27
+ # 生成随机数字和字母组合(小写)
28
+ return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
29
+
30
+
31
+ def _generate_statsig_id() -> str:
32
+ """动态生成 x-statsig-id
33
+
34
+ 随机选择两种格式之一:
35
+ 1. e:TypeError: Cannot read properties of null (reading 'children['xxxxx']')
36
+ 其中 xxxxx 是5位随机字符串
37
+ 2. e:TypeError: Cannot read properties of undefined (reading 'xxxxxxxxxx')
38
+ 其中 xxxxxxxxxx 是10位随机字母
39
+
40
+ Returns:
41
+ base64 编码后的字符串
42
+ """
43
+ # 随机选择一种格式
44
+ format_type = random.choice([1, 2])
45
+
46
+ if format_type == 1:
47
+ # 格式1: children['xxxxx']
48
+ random_str = _generate_random_string(5, use_letters=False)
49
+ error_msg = f"e:TypeError: Cannot read properties of null (reading 'children['{random_str}']')"
50
+ else:
51
+ # 格式2: 'xxxxxxxxxx'
52
+ random_str = _generate_random_string(10, use_letters=True)
53
+ error_msg = f"e:TypeError: Cannot read properties of undefined (reading '{random_str}')"
54
+
55
+ # base64 编码
56
+ encoded = base64.b64encode(error_msg.encode('utf-8')).decode('utf-8')
57
+ return encoded
58
+
59
+
60
  def get_dynamic_headers(pathname: str = "/rest/app-chat/conversations/new") -> Dict[str, str]:
61
  """获取请求头
62
 
 
66
  Returns:
67
  请求头字典
68
  """
69
+ # 检查是否启用动态生成
70
+ dynamic_statsig = setting.grok_config.get("dynamic_statsig", False)
71
+
72
+ if dynamic_statsig:
73
+ # 动态生成 x-statsig-id
74
+ statsig_id = _generate_statsig_id()
75
+ logger.debug(f"[Statsig] 动态生成值 {statsig_id}")
76
+ else:
77
+ # 使用配置文件中的固定值
78
+ statsig_id = setting.grok_config.get("x_statsig_id")
79
+ logger.debug(f"[Statsig] 使用固定值 {statsig_id}")
80
+ if not statsig_id:
81
+ raise ValueError("配置文件中未设置 x_statsig_id")
82
 
83
  # 构建基础请求头
84
  headers = {
 
96
  "Sec-Fetch-Dest": "empty",
97
  "Sec-Fetch-Mode": "cors",
98
  "Sec-Fetch-Site": "same-origin",
99
+ "Baggage": "sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
100
  "x-statsig-id": statsig_id,
101
  "x-xai-request-id": str(uuid.uuid4())
102
  }
app/services/grok/token.py CHANGED
@@ -134,7 +134,9 @@ class GrokTokenManager:
134
  "status": "active",
135
  "failedCount": 0,
136
  "lastFailureTime": None,
137
- "lastFailureReason": None
 
 
138
  }
139
  added_count += 1
140
 
@@ -157,6 +159,38 @@ class GrokTokenManager:
157
 
158
  await self._save_data()
159
  logger.info(f"[Token] 成功删除 {deleted_count} 个 {token_type.value} Token")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  def get_tokens(self) -> Dict[str, Any]:
162
  """获取所有Token数据"""
@@ -238,14 +272,13 @@ class GrokTokenManager:
238
  )
239
 
240
  status_text = "未使用" if max_remaining == -1 else f"剩余{max_remaining}次"
241
- logger.debug(f"[Token] 为模型 {model} 选择Token ({status_text})")
242
  return max_token_key
243
 
244
  async def check_limits(self, auth_token: str, model: str) -> Optional[Dict[str, Any]]:
245
  """检查并更新模型速率限制"""
246
  try:
247
  rate_limit_model_name = Models.to_rate_limit(model)
248
- logger.debug(f"[Token] 检查模型 {model} (接口模型: {rate_limit_model_name}) 的速率限制")
249
 
250
  # 准备请求
251
  payload = {"requestKind": "DEFAULT", "modelName": rate_limit_model_name}
@@ -259,9 +292,6 @@ class GrokTokenManager:
259
  proxy_url = setting.grok_config.get("proxy_url", "")
260
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
261
 
262
- if proxy_url:
263
- logger.debug(f"[Token] 使用代理: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
264
-
265
  # 发送异步请求
266
  async with AsyncSession() as session:
267
  response = await session.post(
@@ -275,21 +305,34 @@ class GrokTokenManager:
275
 
276
  if response.status_code == 200:
277
  rate_limit_data = response.json()
278
- logger.debug(f"[Token] 成功获取速率限制信息")
279
 
280
  # 保存速率限制信息
281
  sso_value = self._extract_sso(auth_token)
282
  if sso_value:
283
  if model == "grok-4-heavy":
284
  await self.update_limits(sso_value, normal=None, heavy=rate_limit_data.get("remainingQueries", -1))
285
- logger.info(f"[Token] 更新限制: sso={sso_value[:10]}..., heavy={rate_limit_data.get('remainingQueries', -1)}")
286
  else:
287
  await self.update_limits(sso_value, normal=rate_limit_data.get("remainingTokens", -1), heavy=None)
288
- logger.info(f"[Token] 更新限制: sso={sso_value[:10]}..., 通用={rate_limit_data.get('remainingTokens', -1)}")
289
 
290
  return rate_limit_data
291
  else:
292
  logger.warning(f"[Token] 获取速率限制失败,状态码: {response.status_code}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  return None
294
 
295
  except Exception as e:
 
134
  "status": "active",
135
  "failedCount": 0,
136
  "lastFailureTime": None,
137
+ "lastFailureReason": None,
138
+ "tags": [],
139
+ "note": ""
140
  }
141
  added_count += 1
142
 
 
159
 
160
  await self._save_data()
161
  logger.info(f"[Token] 成功删除 {deleted_count} 个 {token_type.value} Token")
162
+
163
+ async def update_token_tags(self, token: str, token_type: TokenType, tags: list[str]) -> None:
164
+ """更新Token的标签"""
165
+ if token not in self.token_data[token_type.value]:
166
+ logger.warning(f"[Token] Token不存在: {token[:10]}...")
167
+ raise GrokApiException(
168
+ "Token不存在",
169
+ "TOKEN_NOT_FOUND",
170
+ {"token": token[:10]}
171
+ )
172
+
173
+ # 确保tags是列表且不包含空字符串
174
+ cleaned_tags = [tag.strip() for tag in tags if tag and tag.strip()]
175
+ self.token_data[token_type.value][token]["tags"] = cleaned_tags
176
+
177
+ await self._save_data()
178
+ logger.info(f"[Token] 成功更新Token {token[:10]}... 的标签: {cleaned_tags}")
179
+
180
+ async def update_token_note(self, token: str, token_type: TokenType, note: str) -> None:
181
+ """更新Token的备注"""
182
+ if token not in self.token_data[token_type.value]:
183
+ logger.warning(f"[Token] Token不存在: {token[:10]}...")
184
+ raise GrokApiException(
185
+ "Token不存在",
186
+ "TOKEN_NOT_FOUND",
187
+ {"token": token[:10]}
188
+ )
189
+
190
+ self.token_data[token_type.value][token]["note"] = note.strip()
191
+
192
+ await self._save_data()
193
+ logger.info(f"[Token] 成功更新Token {token[:10]}... 的备注")
194
 
195
  def get_tokens(self) -> Dict[str, Any]:
196
  """获取所有Token数据"""
 
272
  )
273
 
274
  status_text = "未使用" if max_remaining == -1 else f"剩余{max_remaining}次"
275
+ logger.debug(f"[Token] 正在为模型 {model} 分配Token ({status_text})")
276
  return max_token_key
277
 
278
  async def check_limits(self, auth_token: str, model: str) -> Optional[Dict[str, Any]]:
279
  """检查并更新模型速率限制"""
280
  try:
281
  rate_limit_model_name = Models.to_rate_limit(model)
 
282
 
283
  # 准备请求
284
  payload = {"requestKind": "DEFAULT", "modelName": rate_limit_model_name}
 
292
  proxy_url = setting.grok_config.get("proxy_url", "")
293
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
294
 
 
 
 
295
  # 发送异步请求
296
  async with AsyncSession() as session:
297
  response = await session.post(
 
305
 
306
  if response.status_code == 200:
307
  rate_limit_data = response.json()
 
308
 
309
  # 保存速率限制信息
310
  sso_value = self._extract_sso(auth_token)
311
  if sso_value:
312
  if model == "grok-4-heavy":
313
  await self.update_limits(sso_value, normal=None, heavy=rate_limit_data.get("remainingQueries", -1))
314
+ logger.info(f"[Token] 更新 Token 限制: sso={sso_value[:10]}..., heavy={rate_limit_data.get('remainingQueries', -1)}")
315
  else:
316
  await self.update_limits(sso_value, normal=rate_limit_data.get("remainingTokens", -1), heavy=None)
317
+ logger.info(f"[Token] 更新 Token 限制: sso={sso_value[:10]}..., basic={rate_limit_data.get('remainingTokens', -1)}")
318
 
319
  return rate_limit_data
320
  else:
321
  logger.warning(f"[Token] 获取速率限制失败,状态码: {response.status_code}")
322
+
323
+ # 根据HTTP状态码处理不同的错误
324
+ sso_value = self._extract_sso(auth_token)
325
+ if sso_value:
326
+ if response.status_code == 401:
327
+ # Token失效
328
+ await self.record_failure(auth_token, 401, "Token authentication failed")
329
+ elif response.status_code == 403:
330
+ # 服务器被block,不改变token状态,但记录失败信息
331
+ await self.record_failure(auth_token, 403, "Server blocked (Cloudflare)")
332
+ else:
333
+ # 其他错误,设置为限流状态
334
+ await self.record_failure(auth_token, response.status_code, f"Rate limit or other error: {response.status_code}")
335
+
336
  return None
337
 
338
  except Exception as e:
app/services/grok/upload.py CHANGED
@@ -63,9 +63,6 @@ class ImageUploadManager:
63
  cookie = f"{auth_token};{cf_clearance}" if cf_clearance else auth_token
64
 
65
  proxy_url = setting.grok_config.get("proxy_url", "")
66
- if proxy_url:
67
- logger.debug(f"[Upload] 使用代理: {proxy_url.split('@')[-1] if '@' in proxy_url else proxy_url}")
68
-
69
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
70
 
71
  # 发送异步请求
@@ -86,10 +83,11 @@ class ImageUploadManager:
86
  if response.status_code == 200:
87
  result = response.json()
88
  file_id = result.get("fileMetadataId", "")
 
89
  logger.debug(f"[Upload] 图片上传成功,文件ID: {file_id}")
90
- return file_id
91
 
92
- return ""
93
 
94
  except Exception as e:
95
  logger.warning(f"[Upload] 上传图片失败: {e}")
 
63
  cookie = f"{auth_token};{cf_clearance}" if cf_clearance else auth_token
64
 
65
  proxy_url = setting.grok_config.get("proxy_url", "")
 
 
 
66
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
67
 
68
  # 发送异步请求
 
83
  if response.status_code == 200:
84
  result = response.json()
85
  file_id = result.get("fileMetadataId", "")
86
+ file_uri = result.get("fileUri", "")
87
  logger.debug(f"[Upload] 图片上传成功,文件ID: {file_id}")
88
+ return file_id, file_uri
89
 
90
+ return "", ""
91
 
92
  except Exception as e:
93
  logger.warning(f"[Upload] 上传图片失败: {e}")
app/template/admin.html CHANGED
@@ -20,9 +20,17 @@
20
  .hover-card-content.top::after{top:100%;border-top-color:hsl(0 0% 3.9%)}
21
  .hover-card-content.bottom::after{bottom:100%;border-bottom-color:hsl(0 0% 3.9%)}
22
  .hover-card-trigger:hover+.hover-card-content{opacity:1;visibility:visible}
 
 
 
 
 
 
23
  [title]{position:relative}
24
- [title]:hover::after{content:attr(title);position:absolute;bottom:100%;left:50%;transform:translateX(-50%) translateY(-4px);background:hsl(0 0% 3.9%);color:hsl(0 0% 98%);padding:8px 12px;border-radius:4px;font-size:11px;white-space:pre-line;max-width:500px;min-width:300px;word-wrap:break-word;z-index:1000;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}
25
- [title]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:hsl(0 0% 3.9%);z-index:1000}
 
 
26
  </style>
27
  <script>
28
  tailwind.config={theme:{extend:{colors:{border:"hsl(0 0% 89%)",input:"hsl(0 0% 89%)",ring:"hsl(0 0% 3.9%)",background:"hsl(0 0% 100%)",foreground:"hsl(0 0% 3.9%)",primary:{DEFAULT:"hsl(0 0% 9%)",foreground:"hsl(0 0% 98%)"},secondary:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 9%)"},muted:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 45.1%)"},accent:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 9%)"},destructive:{DEFAULT:"hsl(0 84.2% 60.2%)",foreground:"hsl(0 0% 98%)"}}}}}
@@ -138,55 +146,39 @@
138
  <div class="flex items-center justify-between gap-4 p-4 border-b border-border">
139
  <div class="flex items-center gap-3 flex-1">
140
  <div class="flex items-center gap-2">
141
- <select id="filterType" onchange="filterTokens()" class="h-8 px-3 text-sm rounded-md border border-input bg-background focus:outline-none focus:ring-1 focus:ring-ring">
142
  <option value="all">全部类型</option>
143
  <option value="sso">SSO</option>
144
  <option value="ssoSuper">SuperSSO</option>
145
  </select>
146
- <select id="filterStatus" onchange="filterTokens()" class="h-8 px-3 text-sm rounded-md border border-input bg-background focus:outline-none focus:ring-1 focus:ring-ring">
147
  <option value="all">全部状态</option>
148
  <option value="未使用">未使用</option>
149
  <option value="限流中">限流中</option>
150
  <option value="失效">失效</option>
151
  <option value="正常">正常</option>
152
  </select>
 
 
 
153
  </div>
154
  </div>
155
 
156
  <div class="flex items-center gap-2">
157
- <button
158
- onclick="refreshTokens()"
159
- class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground h-8 w-8"
160
- title="刷新列表"
161
- >
162
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
163
- <polyline points="23 4 23 10 17 10"/>
164
- <polyline points="1 20 1 14 7 14"/>
165
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
166
  </svg>
167
  </button>
168
  <div id="batchActions" class="hidden items-center gap-2">
169
- <button
170
- onclick="exportSelected()"
171
- class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground h-8 w-8"
172
- title="导出选中项"
173
- >
174
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
175
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
176
- <polyline points="7 10 12 15 17 10"/>
177
- <line x1="12" y1="15" x2="12" y2="3"/>
178
  </svg>
179
  </button>
180
- <button
181
- onclick="batchDelete()"
182
- class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-destructive/10 hover:text-destructive h-8 w-8"
183
- title="批量删除"
184
- >
185
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
186
- <polyline points="3 6 5 6 21 6"/>
187
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
188
- <line x1="10" y1="11" x2="10" y2="17"/>
189
- <line x1="14" y1="11" x2="14" y2="17"/>
190
  </svg>
191
  </button>
192
  </div>
@@ -220,10 +212,12 @@
220
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-72">Token</th>
221
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">类型</th>
222
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">状态</th>
223
- <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">普通剩余</th>
224
- <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">高级剩余</th>
225
- <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-40">创建时间</th>
226
- <th class="h-10 px-3 text-right align-middle text-sm font-medium text-muted-foreground w-20">操作</th>
 
 
227
  </tr>
228
  </thead>
229
  <tbody id="tokenTableBody" class="divide-y divide-border">
@@ -256,27 +250,16 @@
256
  <h3 class="text-sm font-semibold mb-4">系统设置</h3>
257
  <div class="space-y-4">
258
  <div>
259
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
260
- 登陆账户
261
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="登录管理后台的用户名">?</span>
262
- </label>
263
- <input id="cfgAdminUser" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="admin">
264
  </div>
265
  <div>
266
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
267
- 登陆密码
268
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="登录管理后台的密码,留空表示不修改当前密码">?</span>
269
- </label>
270
- <input id="cfgAdminPass" type="password" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空则不修改">
271
  </div>
272
  <div>
273
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
274
- 日志级别
275
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="日志详细程度。DEBUG:最详细 | INFO:一般信息 | WARNING:警告 | ERROR:仅错误">?</span>
276
- </label>
277
- <select id="cfgLogLevel" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
278
- <option>DEBUG</option><option>INFO</option><option>WARNING</option><option>ERROR</option>
279
- </select>
280
  </div>
281
  </div>
282
  </div>
@@ -286,36 +269,36 @@
286
  <h3 class="text-sm font-semibold mb-4">媒体设置</h3>
287
  <div class="space-y-4">
288
  <div>
289
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
290
  图片模式
291
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="返回图片的方式。URL:图片链接,支持图片缓存 | Base64:base64编码,不支持缓存">?</span>
292
  </label>
293
- <select id="cfgImageMode" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
294
- <option value="url">URL (图片链接)</option>
295
- <option value="base64">Base64 (base64编码)</option>
296
  </select>
297
  </div>
298
  <div>
299
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
300
  服务网址
301
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="服务器的公网访问地址,用于构建图片URL链接(仅在图片模式为URL时需要)">?</span>
302
  </label>
303
- <input id="cfgBaseUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://localhost:8000">
304
  </div>
305
  <div class="grid grid-cols-2 gap-3">
306
  <div>
307
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
308
  图片缓存 (MB)
309
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="图片缓存的最大容量(MB),超过后会自动清理旧缓存">?</span>
310
  </label>
311
- <input id="cfgImageCacheMaxSize" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="500">
312
  </div>
313
  <div>
314
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
315
  视频缓存 (MB)
316
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="视频缓存的最大容量(MB),超过后会自动清理旧缓存">?</span>
317
  </label>
318
- <input id="cfgVideoCacheMaxSize" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="1000">
319
  </div>
320
  </div>
321
  </div>
@@ -375,32 +358,35 @@
375
  <h3 class="text-sm font-semibold mb-4">基础设置</h3>
376
  <div class="space-y-4">
377
  <div>
378
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
379
  API Key
380
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="接口调用的身份验证密钥,用于保护API访问安全">?</span>
381
  </label>
382
- <input id="cfgApiKey" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="">
383
  </div>
384
  <div>
385
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
386
- X Statsig ID
387
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="Statsig统计ID,用于功能实验和统计">?</span>
388
  </label>
389
- <input id="cfgStatsigId" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="">
390
  </div>
391
  <div>
392
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
393
- 过滤标签
394
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="需要过滤响应标签,多个标签用逗号分隔。如:xaiartifact,xai:tool_usage_card">?</span>
395
  </label>
396
- <input id="cfgFilteredTags" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="xaiartifact,xai:tool_usage_card">
 
 
 
397
  </div>
398
  <div>
399
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
400
  临时会话
401
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="开启后每次对话都创建新会话,不保留历史;关闭后可以继续之前的对话">?</span>
402
  </label>
403
- <select id="cfgTemporary" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
404
  <option value="false">关闭</option>
405
  <option value="true">开启</option>
406
  </select>
@@ -413,25 +399,41 @@
413
  <h3 class="text-sm font-semibold mb-4">代理设置</h3>
414
  <div class="space-y-4">
415
  <div>
416
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  CF Clearance
418
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="Cloudflare验证cookie,用于绕过Cloudflare人机验证。">?</span>
419
  </label>
420
- <input id="cfgCfClearance" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="">
421
  </div>
422
  <div>
423
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
424
  Proxy Url (服务代理)
425
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="API请求和上传使用的代理。支持 http、https、socks5。格式:socks5://user:pass@host:port">?</span>
426
  </label>
427
- <input id="cfgProxyUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="socks5://username:password@127.0.0.1:7890">
428
  </div>
429
  <div>
430
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
431
  Cache Proxy Url (缓存代理)
432
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="图片/视频缓存下载专用代理,不设置则使用服务代理。Grok的图片/视频获取接口对IP风控要求不高,可使用便宜的大流量节点">?</span>
433
  </label>
434
- <input id="cfgCacheProxyUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="socks5://username:password@127.0.0.1:7890">
435
  </div>
436
  </div>
437
  </div>
@@ -441,25 +443,25 @@
441
  <h3 class="text-sm font-semibold mb-4">超时设置</h3>
442
  <div class="space-y-4">
443
  <div>
444
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
445
  首次响应超时 (秒)
446
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="等待API首次返回数据的最大时间(秒)。超时后会报错,建议30-60秒">?</span>
447
  </label>
448
- <input id="cfgStreamFirstResponseTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="30">
449
  </div>
450
  <div>
451
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
452
  流式间隔超时 (秒)
453
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="两次数据块之间的最大间隔时间(秒)。如果超过此时间没有收到新数据则断开,建议60-180秒">?</span>
454
  </label>
455
- <input id="cfgStreamChunkTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="120">
456
  </div>
457
  <div>
458
- <label class="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1">
459
  生成总过程超时 (秒)
460
- <span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="整个对话生成的最大总时长(秒)。适用于超长对话,建议300-900秒">?</span>
461
  </label>
462
- <input id="cfgStreamTotalTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="600">
463
  </div>
464
  </div>
465
  </div>
@@ -471,7 +473,8 @@
471
  <div class="text-xs text-gray-800 leading-relaxed">
472
  <div class="text-base font-medium text-gray-900 mb-2.5">部分说明</div>
473
  <div class="space-y-1.5">
474
- <div><span class="font-medium">X Statsig ID:</span>反机器人验证参数,非必要请勿修改 X Statsig ID</div>
 
475
  <div><span class="font-medium">服务网址:</span>图片/视频链接返回时需要拼接您的服务网址(如 https://yourdomain.com),若您不使用视频功能且图片使用Base64模式则可留空</div>
476
  <div><span class="font-medium">代理设置:</span>服务代理用于访问Grok API和上传图片;缓存代理专门用于下载图片和视频缓存。若仅设置服务代理,缓存将使用相同的代理;若都设置,则分别使用不同的代理</div>
477
  <div><span class="font-medium">请求 403:</span>通常是被 CF 拦截了,可采用以下办法之一:1. 更换服务器IP | 2. 配置代理IP | 3.在服务器中访问 grok.com 通过 CF 验证后 F12 获取 cf_clearance</div>
@@ -481,6 +484,59 @@
481
  </div>
482
  </main>
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  <!-- 添加 Token 模态框 -->
485
  <div id="addModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
486
  <div class="bg-background rounded-lg border border-border w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-xl">
@@ -529,43 +585,54 @@
529
  </div>
530
 
531
  <script>
532
- let allTokens=[],filteredTokens=[],selectedTokens=new Set();
533
  const $=(id)=>document.getElementById(id),
534
  checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t},
535
  apiRequest=async(url,opts={})=>{const t=checkAuth();if(!t)return null;const r=await fetch(url,{...opts,headers:{...opts.headers,Authorization:`Bearer ${t}`,'Content-Type':'application/json'}});return r.status===401?(localStorage.removeItem('adminToken'),location.href='/login',null):r},
536
  loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();if(d.success){const s=d.data;$('statTotal').textContent=s.total||0;['Unused','Limited','Expired','Active'].forEach((k,i)=>$(`stat${k}`).textContent=(s.normal?.[k.toLowerCase()]||0)+(s.super?.[k.toLowerCase()]||0))}}catch(e){console.error('加载统计失败:',e)}},
537
  calcRemaining=()=>{let n=0,h=0;allTokens.forEach(t=>{if(t.remaining_queries>0)n+=t.remaining_queries;if(t.heavy_remaining_queries>0)h+=t.heavy_remaining_queries});return{normal:n,heavy:h,total:n+h}},
538
- loadTokens=async()=>{try{const r=await apiRequest('/api/tokens');if(!r)return;const d=await r.json();d.success&&(allTokens=d.data,filteredTokens=allTokens,selectedTokens.clear(),renderTokens(),updateRemaining())}catch(e){console.error('加载列表失败:',e)}},
539
  updateRemaining=()=>{const r=calcRemaining();['Total','Normal','Heavy'].forEach(k=>$(`stat${k}Remaining`).textContent=r[k.toLowerCase()]===0?'-':r[k.toLowerCase()].toLocaleString())}
540
 
541
- const renderTokens=()=>{const tb=$('tokenTableBody'),es=$('emptyState'),ss={'未使用':'bg-muted text-muted-foreground','限流中':'bg-orange-50 text-orange-700 border-orange-200','失效':'bg-destructive/10 text-destructive border-destructive/20','正常':'bg-green-50 text-green-700 border-green-200'},ts={sso:'bg-blue-50 text-blue-700 border-blue-200',ssoSuper:'bg-purple-50 text-purple-700 border-purple-200'},tl={sso:'SSO',ssoSuper:'SuperSSO'};if(!filteredTokens.length){tb.innerHTML='';es.classList.remove('hidden');$('selectAll').checked=false;return updateBatchActions()}es.classList.add('hidden');tb.innerHTML=filteredTokens.map(t=>`<tr class="transition-colors"><td class="py-2.5 px-3 align-middle w-12"><input type="checkbox" class="token-checkbox h-3.5 w-3.5 rounded border border-input focus:ring-1 focus:ring-ring" data-token="${t.token}" data-type="${t.token_type}" ${selectedTokens.has(t.token)?'checked':''} onchange="toggleToken('${t.token}')"></td><td class="py-2.5 px-3 align-middle w-80"><div class="flex items-center gap-2"><span class="font-mono text-xs">${t.token.substring(0,30)}...</span><button onclick="copyToken('${t.token.replace(/'/g,"\\'")}',event)" class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent h-6 w-6" title="复制完整 Token"><svg class="h-3 w-3 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ts[t.token_type]}">${tl[t.token_type]}</span></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ss[t.status]}">${t.status}</span></td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.remaining_queries===-1?'-':t.remaining_queries}</td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.heavy_remaining_queries===-1?'-':t.heavy_remaining_queries}</td><td class="py-2.5 px-3 align-middle w-32 text-xs text-muted-foreground">${t.created_time?new Date(t.created_time).toLocaleString('zh-CN',{dateStyle:'short',timeStyle:'short'}):'-'}</td><td class="py-2.5 px-3 align-middle text-right w-16"><button onclick="deleteToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-destructive/10 hover:text-destructive h-7 w-7"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></td></tr>`).join('');updateBatchActions()},
542
  toggleToken=t=>selectedTokens[selectedTokens.has(t)?'delete':'add'](t)||updateBatchActions(),
543
  toggleSelectAll=()=>{const sa=$('selectAll');sa.checked?filteredTokens.forEach(t=>selectedTokens.add(t.token)):selectedTokens.clear();renderTokens()},
544
  updateBatchActions=()=>{const ba=$('batchActions'),sc=$('selectedCount'),c=selectedTokens.size;ba.classList[c>0?'add':'remove']('flex');ba.classList[c>0?'remove':'add']('hidden');c>0&&(sc.textContent=`已选择 ${c} 项`);$('selectAll').checked=filteredTokens.length>0&&c===filteredTokens.length},
545
- filterTokens=()=>{const tf=$('filterType').value,sf=$('filterStatus').value;filteredTokens=allTokens.filter(t=>(tf==='all'||t.token_type===tf)&&(sf==='all'||t.status===sf));selectedTokens.clear();renderTokens()},
 
546
  refreshTokens=async()=>{await loadTokens();await loadStats()},
547
  openAddModal=()=>$('addModal').classList.remove('hidden'),
548
  closeAddModal=()=>{$('addModal').classList.add('hidden');$('addTokenList').value=''},
549
- deleteToken=async(t,tt)=>{if(!confirm('确定要删除这个 Token 吗?'))return;try{const r=await apiRequest('/api/tokens/delete',{method:'POST',body:JSON.stringify({tokens:[t],token_type:tt})});if(!r)return;const d=await r.json();d.success?await refreshTokens():alert('删除失败: '+(d.error||'未知错误'))}catch(e){alert('删除失败: '+e.message)}},
550
- batchDelete=async()=>{if(!selectedTokens.size||!confirm(`确定要删除选中的 ${selectedTokens.size} 个 Token 吗?此操作不可恢复!`))return;const tbt={sso:[],ssoSuper:[]};document.querySelectorAll('.token-checkbox:checked').forEach(cb=>tbt[cb.dataset.type].push(cb.dataset.token));try{const ps=[];['sso','ssoSuper'].forEach(k=>tbt[k].length&&ps.push(apiRequest('/api/tokens/delete',{method:'POST',body:JSON.stringify({tokens:tbt[k],token_type:k})})));await Promise.all(ps);await refreshTokens()}catch(e){alert('批量删除失败: '+e.message)}},
551
- submitAddTokens=async()=>{const tt=$('addTokenType').value,tks=$('addTokenList').value.split('\n').map(t=>t.trim()).filter(t=>t);if(!tks.length)return alert('请输入至少一个 Token');try{const r=await apiRequest('/api/tokens/add',{method:'POST',body:JSON.stringify({tokens:tks,token_type:tt})});if(!r)return;const d=await r.json();d.success?(closeAddModal(),await refreshTokens()):alert('添加失败: '+(d.error||'未知错误'))}catch(e){alert('添加失败: '+e.message)}},
552
  copyToken=async(t,e)=>{e.stopPropagation();try{await navigator.clipboard.writeText(t);showToast('Token 已复制到剪贴板','success')}catch(err){console.error('复制失败:',err);showToast('复制失败,请手动复制','error')}}
 
 
 
 
 
 
 
 
553
 
554
  const exportSelected=()=>{if(!selectedTokens.size)return showToast('请先选择要导出的 Token','error');const sd=allTokens.filter(t=>selectedTokens.has(t.token)),csv=[['Token','类型','状态','普通调用剩余','高级调用剩余','创建时间'].join(','),...sd.map(t=>[`"${t.token}"`,t.token_type==='sso'?'SSO':'SuperSSO',t.status,t.remaining_queries===-1?'未使用':t.remaining_queries,t.heavy_remaining_queries===-1?'未使用':t.heavy_remaining_queries,`"${t.created_time?new Date(t.created_time).toLocaleString('zh-CN'):'-'}"`].join(','))].join('\n'),l=document.createElement('a');l.href=URL.createObjectURL(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}));l.download=`grok_tokens_${new Date().toISOString().slice(0,10)}.csv`;l.style.display='none';document.body.appendChild(l);l.click();document.body.removeChild(l);URL.revokeObjectURL(l.href);showToast(`已导出 ${selectedTokens.size} 个 Token`,'success')}
555
  showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)}
556
  logout=async()=>{if(!confirm('确定要退出登录吗?'))return;try{await apiRequest('/api/logout',{method:'POST'})}catch(e){console.error('登出失败:',e)}finally{localStorage.removeItem('adminToken');location.href='/login'}},
557
- switchTab=t=>{['tokens','settings'].forEach(n=>{$(`panel${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('hidden');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'add':'remove']('border-primary','text-primary');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('border-transparent','text-muted-foreground')});t==='settings'&&loadSettings()},
558
- updateCacheProxyReadonly=()=>{const proxyUrl=$('cfgProxyUrl').value.trim(),cacheProxyInput=$('cfgCacheProxyUrl');if(proxyUrl){cacheProxyInput.readOnly=false;cacheProxyInput.classList.remove('bg-muted');cacheProxyInput.placeholder='socks5://username:password@127.0.0.1:7890'}else{cacheProxyInput.readOnly=true;cacheProxyInput.classList.add('bg-muted');cacheProxyInput.value='';cacheProxyInput.placeholder='设置服务代理后自动启用'}};
559
- loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgImageMode').value=g.image_mode||'url';$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCacheProxyUrl').value=k.cache_proxy_url||'';$('cfgCfClearance').value=k.cf_clearance||'';$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgTemporary').value=k.temporary!==false?'true':'false';$('cfgStreamChunkTimeout').value=k.stream_chunk_timeout||120;$('cfgStreamFirstResponseTimeout').value=k.stream_first_response_timeout||30;$('cfgStreamTotalTimeout').value=k.stream_total_timeout||600;updateCacheProxyReadonly();await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}},
560
- loadCacheSize=async()=>{try{const r=await apiRequest('/api/cache/size');if(!r)return;const d=await r.json();if(d.success){$('imageCacheSize').value=d.data.image_size||'0 MB';$('videoCacheSize').value=d.data.video_size||'0 MB';$('totalCacheSize').value=d.data.total_size||'0 MB'}}catch(e){console.error('加载缓存大小失败:',e);$('imageCacheSize').value='0 MB';$('videoCacheSize').value='0 MB';$('totalCacheSize').value='0 MB'}},
561
- clearImageCache=async()=>{if(!confirm('确定要清理图片缓存吗?此操作将删除所有图片缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/images',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`图片缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
562
- clearVideoCache=async()=>{if(!confirm('确定要清理视频缓存吗?此操作将删除所有视频缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/videos',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`视频缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
563
- clearCache=async()=>{if(!confirm('确定要清理缓存吗?此操作将删除 /data/temp 目录中的所有文件!'))return;try{const r=await apiRequest('/api/cache/clear',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
 
 
564
  saveGlobalSettings=async()=>{const gc={admin_username:$('cfgAdminUser').value,log_level:$('cfgLogLevel').value,image_cache_max_size_mb:parseInt($('cfgImageCacheMaxSize').value)||500,video_cache_max_size_mb:parseInt($('cfgVideoCacheMaxSize').value)||1000,image_mode:$('cfgImageMode').value,base_url:$('cfgBaseUrl').value};if($('cfgAdminPass').value)gc.admin_password=$('cfgAdminPass').value;try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:gc,grok_config:d.data.grok})});if(!s)return;const sd=await s.json();sd.success?(showToast('全局配置保存成功','success'),$('cfgAdminPass').value=''):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}},
565
- saveGrokSettings=async()=>{const pu=$('cfgProxyUrl').value.trim(),kc={api_key:$('cfgApiKey').value,proxy_url:pu,cache_proxy_url:pu?$('cfgCacheProxyUrl').value:'',cf_clearance:$('cfgCfClearance').value,x_statsig_id:$('cfgStatsigId').value,filtered_tags:$('cfgFilteredTags').value,temporary:$('cfgTemporary').value==='true',stream_chunk_timeout:parseInt($('cfgStreamChunkTimeout').value)||120,stream_first_response_timeout:parseInt($('cfgStreamFirstResponseTimeout').value)||30,stream_total_timeout:parseInt($('cfgStreamTotalTimeout').value)||600};try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:d.data.global,grok_config:kc})});if(!s)return;const sd=await s.json();sd.success?showToast('Grok配置保存成功','success'):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}};
566
- const updateHoverCardPosition=(cardElement)=>{const trigger=cardElement.querySelector('.hover-card-trigger'),content=cardElement.querySelector('.hover-card-content');if(!trigger||!content)return;console.log('更新悬浮卡片位置',cardElement,trigger,content);const rect=trigger.getBoundingClientRect(),windowHeight=window.innerHeight,spaceAbove=rect.top,spaceBelow=windowHeight-rect.bottom;content.classList.remove('top','bottom');const originalVisibility=getComputedStyle(content).visibility,originalOpacity=getComputedStyle(content).opacity;content.style.visibility='hidden';content.style.opacity='1';const contentHeight=content.offsetHeight;content.style.visibility=originalVisibility;content.style.opacity=originalOpacity;const position=spaceAbove>contentHeight+10?'top':spaceBelow>contentHeight+10?'bottom':'top';content.classList.add(position);console.log('悬浮卡片位置:',position,'高度:',contentHeight,'上方空间:',spaceAbove,'下方空间:',spaceBelow)},
567
- loadStorageMode=async()=>{try{const r=await apiRequest('/api/storage/mode');if(!r)return;const d=await r.json();if(d.success){const mode=d.data.mode;console.log('存储模式:',mode);$('storageModeText').textContent=mode;if(mode==='MYSQL'){$('storageMode').classList.add('bg-blue-50','text-blue-700','border-blue-200');$('storageModeTooltip').textContent='数据库连接模式 - 数据持久化存储,修改配置时可能稍慢但更安全'}else if(mode==='REDIS'){$('storageMode').classList.add('bg-purple-50','text-purple-700','border-purple-200');$('storageModeTooltip').textContent='Redis缓存模式 - 高速内存存储,数据持久化且读写性能极佳'}else{$('storageMode').classList.add('bg-green-50','text-green-700','border-green-200');$('storageModeTooltip').textContent='文件存储模式 - 本地文件存储,读写速度快'};updateHoverCardPosition($('storageMode').closest('.hover-card'))}}catch(e){console.error('加载存储模式失败:',e);$('storageModeText').textContent='FILE';$('storageMode').classList.add('bg-green-50','text-green-700','border-green-200');$('storageModeTooltip').textContent='文件存储模式 - 本地文件存储,读写速度快';updateHoverCardPosition($('storageMode').closest('.hover-card'))}};
568
- window.addEventListener('DOMContentLoaded',()=>{checkAuth();loadStorageMode();refreshTokens();setInterval(()=>{loadStats();updateRemaining()},30000);window.addEventListener('resize',()=>{const hoverCard=$('storageMode').closest('.hover-card');hoverCard&&updateHoverCardPosition(hoverCard)});const hoverCard=$('storageMode').closest('.hover-card'),trigger=hoverCard?.querySelector('.hover-card-trigger'),content=hoverCard?.querySelector('.hover-card-content');if(trigger&&content){trigger.addEventListener('mouseenter',()=>{content.style.opacity='1';content.style.visibility='visible'});trigger.addEventListener('mouseleave',()=>{content.style.opacity='0';content.style.visibility='hidden'})};$('cfgProxyUrl').addEventListener('input',updateCacheProxyReadonly)});
569
  </script>
570
  </body>
571
  </html>
 
20
  .hover-card-content.top::after{top:100%;border-top-color:hsl(0 0% 3.9%)}
21
  .hover-card-content.bottom::after{bottom:100%;border-bottom-color:hsl(0 0% 3.9%)}
22
  .hover-card-trigger:hover+.hover-card-content{opacity:1;visibility:visible}
23
+ .btn-icon{display:inline-flex;align-items:center;justify-content:center;border-radius:0.375rem;transition:color .2s,background-color .2s;height:2rem;width:2rem}
24
+ .btn-icon:focus-visible{outline:2px solid transparent;outline-offset:2px;box-shadow:0 0 0 1px hsl(0 0% 3.9%)}
25
+ .btn-icon:hover{background-color:hsl(0 0% 96.1%);color:hsl(0 0% 9%)}
26
+ .cfg-label{font-size:0.875rem;font-weight:500;color:hsl(0 0% 45.1%);margin-bottom:0.5rem;display:flex;align-items:center;gap:0.25rem}
27
+ .cfg-input{display:flex;height:2.25rem;width:100%;border-radius:0.375rem;border:1px solid hsl(0 0% 89%);background-color:hsl(0 0% 100%);padding:0.5rem 0.75rem;font-size:0.875rem}
28
+ .help-icon{display:inline-flex;align-items:center;justify-content:center;width:0.875rem;height:0.875rem;border-radius:9999px;border:1px solid hsl(0 0% 45.1%);color:hsl(0 0% 45.1%);cursor:help;font-size:10px;line-height:1}
29
  [title]{position:relative}
30
+ [title]:hover::after{content:attr(title);position:absolute;bottom:100%;left:50%;transform:translateX(-50%) translateY(-8px);background:hsl(0 0% 11%);color:hsl(0 0% 98%);padding:8px 12px;border-radius:6px;font-size:12px;font-weight:500;line-height:1.4;white-space:pre-line;max-width:350px;width:max-content;word-wrap:break-word;z-index:1000;pointer-events:none;box-shadow:0 8px 20px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.1);opacity:0;visibility:hidden;transition:all 0.2s ease;animation:tooltipFadeIn 0.2s ease forwards}
31
+ [title]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%) translateY(-4px);border:6px solid transparent;border-top-color:hsl(0 0% 11%);z-index:1000;opacity:0;visibility:hidden;transition:opacity 0.2s ease, visibility 0.2s ease}
32
+ [title]:hover::after,[title]:hover::before{opacity:1;visibility:visible}
33
+ @keyframes tooltipFadeIn{from{opacity:0;transform:translateX(-50%) translateY(-4px)}to{opacity:1;transform:translateX(-50%) translateY(-8px)}}
34
  </style>
35
  <script>
36
  tailwind.config={theme:{extend:{colors:{border:"hsl(0 0% 89%)",input:"hsl(0 0% 89%)",ring:"hsl(0 0% 3.9%)",background:"hsl(0 0% 100%)",foreground:"hsl(0 0% 3.9%)",primary:{DEFAULT:"hsl(0 0% 9%)",foreground:"hsl(0 0% 98%)"},secondary:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 9%)"},muted:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 45.1%)"},accent:{DEFAULT:"hsl(0 0% 96.1%)",foreground:"hsl(0 0% 9%)"},destructive:{DEFAULT:"hsl(0 84.2% 60.2%)",foreground:"hsl(0 0% 98%)"}}}}}
 
146
  <div class="flex items-center justify-between gap-4 p-4 border-b border-border">
147
  <div class="flex items-center gap-3 flex-1">
148
  <div class="flex items-center gap-2">
149
+ <select id="filterType" onchange="filterTokens()" class="h-8 px-1 text-sm rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring w-[90px]">
150
  <option value="all">全部类型</option>
151
  <option value="sso">SSO</option>
152
  <option value="ssoSuper">SuperSSO</option>
153
  </select>
154
+ <select id="filterStatus" onchange="filterTokens()" class="h-8 px-1 text-sm rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring w-[90px]">
155
  <option value="all">全部状态</option>
156
  <option value="未使用">未使用</option>
157
  <option value="限流中">限流中</option>
158
  <option value="失效">失效</option>
159
  <option value="正常">正常</option>
160
  </select>
161
+ <select id="filterTag" onchange="filterTokens()" class="h-8 px-1 text-sm rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring w-[90px]">
162
+ <option value="all">全部标签</option>
163
+ </select>
164
  </div>
165
  </div>
166
 
167
  <div class="flex items-center gap-2">
168
+ <button onclick="refreshTokens()" class="btn-icon" title="刷新列表">
 
 
 
 
169
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
170
+ <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
 
 
171
  </svg>
172
  </button>
173
  <div id="batchActions" class="hidden items-center gap-2">
174
+ <button onclick="exportSelected()" class="btn-icon" title="导出选中项">
 
 
 
 
175
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
176
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
 
 
177
  </svg>
178
  </button>
179
+ <button onclick="batchDelete()" class="btn-icon hover:bg-destructive/10 hover:text-destructive" title="批量删除">
 
 
 
 
180
  <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>
 
 
 
182
  </svg>
183
  </button>
184
  </div>
 
212
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-72">Token</th>
213
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">类型</th>
214
  <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">状态</th>
215
+ <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">普通</th>
216
+ <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">高级</th>
217
+ <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-32">标签</th>
218
+ <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-40">备注</th>
219
+ <th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-32">创建时间</th>
220
+ <th class="h-10 px-3 text-right align-middle text-sm font-medium text-muted-foreground w-28">操作</th>
221
  </tr>
222
  </thead>
223
  <tbody id="tokenTableBody" class="divide-y divide-border">
 
250
  <h3 class="text-sm font-semibold mb-4">系统设置</h3>
251
  <div class="space-y-4">
252
  <div>
253
+ <label class="cfg-label">登陆账户<span class="help-icon" title="登录管理后台的用户名">?</span></label>
254
+ <input id="cfgAdminUser" class="cfg-input" placeholder="admin">
 
 
 
255
  </div>
256
  <div>
257
+ <label class="cfg-label">登陆密码<span class="help-icon" title="登录管理后台的密码,留空表示不修改当前密码">?</span></label>
258
+ <input id="cfgAdminPass" type="password" class="cfg-input" placeholder="留空则不修改">
 
 
 
259
  </div>
260
  <div>
261
+ <label class="cfg-label">日志级别<span class="help-icon" title="日志详细程度。DEBUG:最详细 | INFO:一般信息 | WARNING:警告 | ERROR:仅错误">?</span></label>
262
+ <select id="cfgLogLevel" class="cfg-input"><option>DEBUG</option><option>INFO</option><option>WARNING</option><option>ERROR</option></select>
 
 
 
 
 
263
  </div>
264
  </div>
265
  </div>
 
269
  <h3 class="text-sm font-semibold mb-4">媒体设置</h3>
270
  <div class="space-y-4">
271
  <div>
272
+ <label class="cfg-label">
273
  图片模式
274
+ <span class="help-icon" title="返回图片的方式。URL:图片链接,支持图片缓存 | Base64:base64编码,不支持缓存">?</span>
275
  </label>
276
+ <select id="cfgImageMode" class="cfg-input">
277
+ <option value="url">URL链接</option>
278
+ <option value="base64">Base64</option>
279
  </select>
280
  </div>
281
  <div>
282
+ <label class="cfg-label">
283
  服务网址
284
+ <span class="help-icon" title="服务器的公网访问地址,用于构建图片URL链接(仅在图片模式为URL时需要)">?</span>
285
  </label>
286
+ <input id="cfgBaseUrl" class="cfg-input" placeholder="http://localhost:8000">
287
  </div>
288
  <div class="grid grid-cols-2 gap-3">
289
  <div>
290
+ <label class="cfg-label">
291
  图片缓存 (MB)
292
+ <span class="help-icon" title="图片缓存的最大容量(MB),超过后会自动清理旧缓存">?</span>
293
  </label>
294
+ <input id="cfgImageCacheMaxSize" type="number" class="cfg-input" placeholder="500">
295
  </div>
296
  <div>
297
+ <label class="cfg-label">
298
  视频缓存 (MB)
299
+ <span class="help-icon" title="视频缓存的最大容量(MB),超过后会自动清理旧缓存">?</span>
300
  </label>
301
+ <input id="cfgVideoCacheMaxSize" type="number" class="cfg-input" placeholder="1000">
302
  </div>
303
  </div>
304
  </div>
 
358
  <h3 class="text-sm font-semibold mb-4">基础设置</h3>
359
  <div class="space-y-4">
360
  <div>
361
+ <label class="cfg-label">
362
  API Key
363
+ <span class="help-icon" title="接口调用的身份验证密钥,用于保护API访问安全">?</span>
364
  </label>
365
+ <input id="cfgApiKey" class="cfg-input" placeholder="">
366
  </div>
367
  <div>
368
+ <label class="cfg-label">
369
+ 过滤标签
370
+ <span class="help-icon" title="需要过滤的响应标签多个标签逗号隔。如:xaiartifact,xai:tool_usage_card">?</span>
371
  </label>
372
+ <input id="cfgFilteredTags" class="cfg-input" placeholder="xaiartifact,xai:tool_usage_card">
373
  </div>
374
  <div>
375
+ <label class="cfg-label">
376
+ 显示思考
377
+ <span class="help-icon" title="开启后会显示模型思考过程(&lt;think&gt;标签内容);关闭后仅返回最终结果">?</span>
378
  </label>
379
+ <select id="cfgShowThinking" class="cfg-input">
380
+ <option value="true">开启</option>
381
+ <option value="false">关闭</option>
382
+ </select>
383
  </div>
384
  <div>
385
+ <label class="cfg-label">
386
  临时会话
387
+ <span class="help-icon" title="开启后每次对话都创建新会话,不保留历史;关闭后可以继续之前的对话">?</span>
388
  </label>
389
+ <select id="cfgTemporary" class="cfg-input">
390
  <option value="false">关闭</option>
391
  <option value="true">开启</option>
392
  </select>
 
399
  <h3 class="text-sm font-semibold mb-4">代理设置</h3>
400
  <div class="space-y-4">
401
  <div>
402
+ <label class="cfg-label">
403
+ X Statsig ID
404
+ <span class="help-icon" title="Statsig统计ID,用于功能实验和统计分析">?</span>
405
+ </label>
406
+ <div class="flex items-center gap-3">
407
+ <input id="cfgStatsigId" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="">
408
+ <label class="inline-flex items-center gap-2 cursor-pointer" title="开启后每次请求自动生成新的 x-statsig-id">
409
+ <span class="text-xs text-muted-foreground whitespace-nowrap">动态</span>
410
+ <div class="relative">
411
+ <input type="checkbox" id="cfgDynamicStatsig" class="sr-only peer">
412
+ <div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ring rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
413
+ </div>
414
+ </label>
415
+ </div>
416
+ </div>
417
+ <div>
418
+ <label class="cfg-label">
419
  CF Clearance
420
+ <span class="help-icon" title="Cloudflare验证cookie的值部分,用于绕过Cloudflare人机验证。只需输入cf_clearance=后面的值。">?</span>
421
  </label>
422
+ <input id="cfgCfClearance" class="cfg-input" placeholder="">
423
  </div>
424
  <div>
425
+ <label class="cfg-label">
426
  Proxy Url (服务代理)
427
+ <span class="help-icon" title="API请求和上传使用的代理。支持 http、https、socks5。格式:socks5://user:pass@host:port">?</span>
428
  </label>
429
+ <input id="cfgProxyUrl" class="cfg-input" placeholder="socks5://username:password@127.0.0.1:7890">
430
  </div>
431
  <div>
432
+ <label class="cfg-label">
433
  Cache Proxy Url (缓存代理)
434
+ <span class="help-icon" title="图片/视频缓存下载专用代理,不设置则使用服务代理。Grok的图片/视频获取接口对IP风控要求不高,可使用便宜的大流量节点">?</span>
435
  </label>
436
+ <input id="cfgCacheProxyUrl" class="cfg-input" placeholder="socks5://username:password@127.0.0.1:7890">
437
  </div>
438
  </div>
439
  </div>
 
443
  <h3 class="text-sm font-semibold mb-4">超时设置</h3>
444
  <div class="space-y-4">
445
  <div>
446
+ <label class="cfg-label">
447
  首次响应超时 (秒)
448
+ <span class="help-icon" title="等待API首次返回数据的最大时间(秒)。超时后会报错,建议30-60秒">?</span>
449
  </label>
450
+ <input id="cfgStreamFirstResponseTimeout" type="number" class="cfg-input" placeholder="30">
451
  </div>
452
  <div>
453
+ <label class="cfg-label">
454
  流式间隔超时 (秒)
455
+ <span class="help-icon" title="两次数据块之间的最大间隔时间(秒)。如果超过此时间没有收到新数据则断开,建议60-180秒">?</span>
456
  </label>
457
+ <input id="cfgStreamChunkTimeout" type="number" class="cfg-input" placeholder="120">
458
  </div>
459
  <div>
460
+ <label class="cfg-label">
461
  生成总过程超时 (秒)
462
+ <span class="help-icon" title="整个对话生成的最大总时长(秒)。适用于超长对话,建议300-900秒">?</span>
463
  </label>
464
+ <input id="cfgStreamTotalTimeout" type="number" class="cfg-input" placeholder="600">
465
  </div>
466
  </div>
467
  </div>
 
473
  <div class="text-xs text-gray-800 leading-relaxed">
474
  <div class="text-base font-medium text-gray-900 mb-2.5">部分说明</div>
475
  <div class="space-y-1.5">
476
+ <div><span class="font-medium">X Statsig ID:</span>反机器人验证参数。开启"动态 Statsig ID"后会自动生成,固定值将被忽略;关闭则使用上方设置的固定值</div>
477
+ <div><span class="font-medium">动态 Statsig:</span>开启后每次请求自动生成新的 x-statsig-id,增强请求多样性,推荐开启</div>
478
  <div><span class="font-medium">服务网址:</span>图片/视频链接返回时需要拼接您的服务网址(如 https://yourdomain.com),若您不使用视频功能且图片使用Base64模式则可留空</div>
479
  <div><span class="font-medium">代理设置:</span>服务代理用于访问Grok API和上传图片;缓存代理专门用于下载图片和视频缓存。若仅设置服务代理,缓存将使用相同的代理;若都设置,则分别使用不同的代理</div>
480
  <div><span class="font-medium">请求 403:</span>通常是被 CF 拦截了,可采用以下办法之一:1. 更换服务器IP | 2. 配置代理IP | 3.在服务器中访问 grok.com 通过 CF 验证后 F12 获取 cf_clearance</div>
 
484
  </div>
485
  </main>
486
 
487
+ <!-- 编辑信息模态框 -->
488
+ <div id="editTagsModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
489
+ <div class="bg-background rounded-lg border border-border w-full max-w-md shadow-xl">
490
+ <div class="flex items-center justify-between p-5 border-b border-border">
491
+ <h3 class="text-lg font-semibold">编辑信息</h3>
492
+ <button onclick="closeEditModal()" class="text-muted-foreground hover:text-foreground transition-colors">
493
+ <svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
494
+ <line x1="18" y1="6" x2="6" y2="18"/>
495
+ <line x1="6" y1="6" x2="18" y2="18"/>
496
+ </svg>
497
+ </button>
498
+ </div>
499
+ <div class="p-5 space-y-4">
500
+ <div class="space-y-2">
501
+ <label class="text-sm font-medium text-muted-foreground">Token</label>
502
+ <input id="editTokenInput" readonly class="flex h-9 w-full rounded-md border border-input bg-muted px-3 py-2 text-xs font-mono" placeholder="">
503
+ </div>
504
+ <div class="space-y-2">
505
+ <label class="text-sm font-medium text-muted-foreground">标签 <span class="text-muted-foreground">(多个标签用逗号分隔)</span></label>
506
+ <input
507
+ id="editTagsInput"
508
+ placeholder="例如: 生产环境, 测试用"
509
+ class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
510
+ />
511
+ <div id="suggestedTags" class="flex flex-wrap gap-2 mt-2"></div>
512
+ </div>
513
+ <div class="space-y-2">
514
+ <label class="text-sm font-medium text-muted-foreground">备注</label>
515
+ <textarea
516
+ id="editNoteInput"
517
+ rows="3"
518
+ placeholder="添加备注信息..."
519
+ class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-ring resize-none"
520
+ ></textarea>
521
+ </div>
522
+ </div>
523
+ <div class="flex items-center justify-end gap-3 p-5 border-t border-border bg-muted/30">
524
+ <button
525
+ onclick="closeEditModal()"
526
+ class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent h-9 px-5"
527
+ >
528
+ 取消
529
+ </button>
530
+ <button
531
+ onclick="submitEditInfo()"
532
+ class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-5"
533
+ >
534
+ 保存
535
+ </button>
536
+ </div>
537
+ </div>
538
+ </div>
539
+
540
  <!-- 添加 Token 模态框 -->
541
  <div id="addModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
542
  <div class="bg-background rounded-lg border border-border w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-xl">
 
585
  </div>
586
 
587
  <script>
588
+ let allTokens=[],filteredTokens=[],selectedTokens=new Set(),allTagsList=[];
589
  const $=(id)=>document.getElementById(id),
590
  checkAuth=()=>{const t=localStorage.getItem('adminToken');return t||(location.href='/login',null),t},
591
  apiRequest=async(url,opts={})=>{const t=checkAuth();if(!t)return null;const r=await fetch(url,{...opts,headers:{...opts.headers,Authorization:`Bearer ${t}`,'Content-Type':'application/json'}});return r.status===401?(localStorage.removeItem('adminToken'),location.href='/login',null):r},
592
  loadStats=async()=>{try{const r=await apiRequest('/api/stats');if(!r)return;const d=await r.json();if(d.success){const s=d.data;$('statTotal').textContent=s.total||0;['Unused','Limited','Expired','Active'].forEach((k,i)=>$(`stat${k}`).textContent=(s.normal?.[k.toLowerCase()]||0)+(s.super?.[k.toLowerCase()]||0))}}catch(e){console.error('加载统计失败:',e)}},
593
  calcRemaining=()=>{let n=0,h=0;allTokens.forEach(t=>{if(t.remaining_queries>0)n+=t.remaining_queries;if(t.heavy_remaining_queries>0)h+=t.heavy_remaining_queries});return{normal:n,heavy:h,total:n+h}},
594
+ loadTokens=async()=>{try{const r=await apiRequest('/api/tokens');if(!r)return;const d=await r.json();d.success&&(allTokens=d.data.map(t=>({...t,tags:t.tags||[],note:t.note||''})),filteredTokens=allTokens,selectedTokens.clear(),renderTokens(),updateRemaining(),await loadAllTags())}catch(e){console.error('加载列表失败:',e)}},
595
  updateRemaining=()=>{const r=calcRemaining();['Total','Normal','Heavy'].forEach(k=>$(`stat${k}Remaining`).textContent=r[k.toLowerCase()]===0?'-':r[k.toLowerCase()].toLocaleString())}
596
 
597
+ const renderTokens=()=>{const tb=$('tokenTableBody'),es=$('emptyState'),ss={'未使用':'bg-muted text-muted-foreground','限流中':'bg-orange-50 text-orange-700 border-orange-200','失效':'bg-destructive/10 text-destructive border-destructive/20','正常':'bg-green-50 text-green-700 border-green-200'},ts={sso:'bg-blue-50 text-blue-700 border-blue-200',ssoSuper:'bg-purple-50 text-purple-700 border-purple-200'},tl={sso:'SSO',ssoSuper:'SuperSSO'};if(!filteredTokens.length){tb.innerHTML='';es.classList.remove('hidden');$('selectAll').checked=false;return updateBatchActions()}es.classList.add('hidden');tb.innerHTML=filteredTokens.map(t=>{const tagsHtml=t.tags&&t.tags.length?t.tags.map(tag=>`<span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs bg-gray-100 text-gray-700">${tag}</span>`).join(' '):'<span class="text-xs text-muted-foreground">-</span>';const noteHtml=t.note&&t.note.length?`<span class="text-xs text-gray-700" title="${t.note}">${t.note.length>20?t.note.substring(0,20)+'...':t.note}</span>`:'<span class="text-xs text-muted-foreground">-</span>';return`<tr class="transition-colors"><td class="py-2.5 px-3 align-middle w-12"><input type="checkbox" class="token-checkbox h-3.5 w-3.5 rounded border border-input focus:ring-1 focus:ring-ring" data-token="${t.token}" data-type="${t.token_type}" ${selectedTokens.has(t.token)?'checked':''} onchange="toggleToken('${t.token}')"></td><td class="py-2.5 px-3 align-middle w-80"><div class="flex items-center gap-2"><span class="font-mono text-xs">${t.token.substring(0,30)}...</span><button onclick="copyToken('${t.token.replace(/'/g,"\\'")}',event)" class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent h-6 w-6" title="复制完整 Token"><svg class="h-3 w-3 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ts[t.token_type]}">${tl[t.token_type]}</span></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ss[t.status]}">${t.status}</span></td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.remaining_queries===-1?'-':t.remaining_queries}</td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.heavy_remaining_queries===-1?'-':t.heavy_remaining_queries}</td><td class="py-2.5 px-3 align-middle w-32"><div class="flex flex-wrap gap-1">${tagsHtml}</div></td><td class="py-2.5 px-3 align-middle w-40">${noteHtml}</td><td class="py-2.5 px-3 align-middle w-32 text-xs text-muted-foreground">${t.created_time?new Date(t.created_time).toLocaleString('zh-CN',{dateStyle:'short',timeStyle:'short'}):'-'}</td><td class="py-2.5 px-3 align-middle text-right w-28"><div class="flex items-center justify-end gap-1"><button onclick="testToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-blue-50 hover:text-blue-700 h-7 w-7" title="测试Token"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg></button><button onclick="editToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground h-7 w-7" title="编辑信息"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button><button onclick="deleteToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-destructive/10 hover:text-destructive h-7 w-7" title="删除"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></div></td></tr>`}).join('');updateBatchActions()},
598
  toggleToken=t=>selectedTokens[selectedTokens.has(t)?'delete':'add'](t)||updateBatchActions(),
599
  toggleSelectAll=()=>{const sa=$('selectAll');sa.checked?filteredTokens.forEach(t=>selectedTokens.add(t.token)):selectedTokens.clear();renderTokens()},
600
  updateBatchActions=()=>{const ba=$('batchActions'),sc=$('selectedCount'),c=selectedTokens.size;ba.classList[c>0?'add':'remove']('flex');ba.classList[c>0?'remove':'add']('hidden');c>0&&(sc.textContent=`已选择 ${c} 项`);$('selectAll').checked=filteredTokens.length>0&&c===filteredTokens.length},
601
+ filterTokens=()=>{const tf=$('filterType').value,sf=$('filterStatus').value,tagf=$('filterTag').value;filteredTokens=allTokens.filter(t=>(tf==='all'||t.token_type===tf)&&(sf==='all'||t.status===sf)&&(tagf==='all'||t.tags&&t.tags.includes(tagf)));selectedTokens.clear();renderTokens()},
602
+ loadAllTags=async()=>{try{const r=await apiRequest('/api/tokens/tags/all');if(!r)return;const d=await r.json();if(d.success){allTagsList=d.data;const tagFilter=$('filterTag');const currentValue=tagFilter.value;tagFilter.innerHTML='<option value="all">全部标签</option>'+allTagsList.map(tag=>`<option value="${tag}">${tag}</option>`).join('');tagFilter.value=currentValue}}catch(e){console.error('加载标签列表失败:',e)}},
603
  refreshTokens=async()=>{await loadTokens();await loadStats()},
604
  openAddModal=()=>$('addModal').classList.remove('hidden'),
605
  closeAddModal=()=>{$('addModal').classList.add('hidden');$('addTokenList').value=''},
606
+ deleteToken=async(t,tt)=>{if(!confirm('确定要删除这个 Token 吗?'))return;try{const r=await apiRequest('/api/tokens/delete',{method:'POST',body:JSON.stringify({tokens:[t],token_type:tt})});if(!r)return;const d=await r.json();d.success?await refreshTokens():showToast('删除失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('删除失败: '+e.message,'error')}},
607
+ batchDelete=async()=>{if(!selectedTokens.size||!confirm(`确定要删除选中的 ${selectedTokens.size} 个 Token 吗?此操作不可恢复!`))return;const tbt={sso:[],ssoSuper:[]};document.querySelectorAll('.token-checkbox:checked').forEach(cb=>tbt[cb.dataset.type].push(cb.dataset.token));try{const ps=[];['sso','ssoSuper'].forEach(k=>tbt[k].length&&ps.push(apiRequest('/api/tokens/delete',{method:'POST',body:JSON.stringify({tokens:tbt[k],token_type:k})})));await Promise.all(ps);await refreshTokens()}catch(e){showToast('批量删除失败: '+e.message,'error')}},
608
+ submitAddTokens=async()=>{const tt=$('addTokenType').value,tks=$('addTokenList').value.split('\n').map(t=>t.trim()).filter(t=>t);if(!tks.length)return showToast('请输入至少一个 Token','error');try{const r=await apiRequest('/api/tokens/add',{method:'POST',body:JSON.stringify({tokens:tks,token_type:tt})});if(!r)return;const d=await r.json();d.success?(closeAddModal(),await refreshTokens()):showToast('添加失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('添加失败: '+e.message,'error')}},
609
  copyToken=async(t,e)=>{e.stopPropagation();try{await navigator.clipboard.writeText(t);showToast('Token 已复制到剪贴板','success')}catch(err){console.error('复制失败:',err);showToast('复制失败,请手动复制','error')}}
610
+
611
+ let currentEditToken='',currentEditTokenType='';
612
+ editToken=(token,tokenType)=>{currentEditToken=token;currentEditTokenType=tokenType;const tokenData=allTokens.find(t=>t.token===token);const currentTags=tokenData?.tags||[];const currentNote=tokenData?.note||'';$('editTokenInput').value=token.substring(0,50)+'...';$('editTagsInput').value=currentTags.join(', ');$('editNoteInput').value=currentNote;const suggestedContainer=$('suggestedTags');suggestedContainer.innerHTML='';if(allTagsList.length>0){suggestedContainer.innerHTML='<div class="text-xs text-muted-foreground mb-1">常用标签:</div>'+allTagsList.map(tag=>`<button onclick="addTagToInput('${tag}')" class="inline-flex items-center rounded px-2 py-1 text-xs bg-muted hover:bg-accent transition-colors">${tag}</button>`).join('')}$('editTagsModal').classList.remove('hidden')},
613
+ closeEditModal=()=>{$('editTagsModal').classList.add('hidden');currentEditToken='';currentEditTokenType=''},
614
+ addTagToInput=(tag)=>{const input=$('editTagsInput');const currentValue=input.value.trim();const tags=currentValue?currentValue.split(',').map(t=>t.trim()):[];if(!tags.includes(tag)){tags.push(tag);input.value=tags.join(', ')}},
615
+ submitEditInfo=async()=>{if(!currentEditToken)return;const tagsInput=$('editTagsInput').value;const note=$('editNoteInput').value.trim();const tags=tagsInput.split(',').map(t=>t.trim()).filter(t=>t);const promises=[];promises.push(apiRequest('/api/tokens/tags',{method:'POST',body:JSON.stringify({token:currentEditToken,token_type:currentEditTokenType,tags})}));promises.push(apiRequest('/api/tokens/note',{method:'POST',body:JSON.stringify({token:currentEditToken,token_type:currentEditTokenType,note})}));try{const results=await Promise.all(promises);const allSuccess=results.every(r=>r&&r.ok);if(allSuccess){closeEditModal();await refreshTokens();showToast('信息更新成功','success')}else{showToast('部分更新失败,请重试','error')}}catch(e){showToast('更新失败: '+e.message,'error')}}
616
+
617
+ testToken=async(token,tokenType)=>{const btn=event.target.closest('button');const originalHtml=btn.innerHTML;btn.disabled=true;btn.innerHTML='<svg class="h-3.5 w-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';try{const r=await apiRequest('/api/tokens/test',{method:'POST',body:JSON.stringify({token,token_type:tokenType})});if(!r)return;const d=await r.json();if(d.success&&d.data.valid){showToast(`Token有效!剩余: ${d.data.remaining_queries===-1?'无限制':d.data.remaining_queries}次`,'success');await refreshTokens()}else{const errMsgs={expired:'Token已失效 (401错误)',blocked:'服务器被block,请稍后再试或更换IP',limited:'Token已被限流'};showToast(errMsgs[d.data?.error_type]||'Token无效或已失效','error')}}catch(e){showToast('测试失败: '+e.message,'error')}finally{btn.disabled=false;btn.innerHTML=originalHtml}}
618
 
619
  const exportSelected=()=>{if(!selectedTokens.size)return showToast('请先选择要导出的 Token','error');const sd=allTokens.filter(t=>selectedTokens.has(t.token)),csv=[['Token','类型','状态','普通调用剩余','高级调用剩余','创建时间'].join(','),...sd.map(t=>[`"${t.token}"`,t.token_type==='sso'?'SSO':'SuperSSO',t.status,t.remaining_queries===-1?'未使用':t.remaining_queries,t.heavy_remaining_queries===-1?'未使用':t.heavy_remaining_queries,`"${t.created_time?new Date(t.created_time).toLocaleString('zh-CN'):'-'}"`].join(','))].join('\n'),l=document.createElement('a');l.href=URL.createObjectURL(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}));l.download=`grok_tokens_${new Date().toISOString().slice(0,10)}.csv`;l.style.display='none';document.body.appendChild(l);l.click();document.body.removeChild(l);URL.revokeObjectURL(l.href);showToast(`已导出 ${selectedTokens.size} 个 Token`,'success')}
620
  showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)}
621
  logout=async()=>{if(!confirm('确定要退出登录吗?'))return;try{await apiRequest('/api/logout',{method:'POST'})}catch(e){console.error('登出失败:',e)}finally{localStorage.removeItem('adminToken');location.href='/login'}},
622
+ switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});t==='settings'&&loadSettings()},
623
+ updateCacheProxyReadonly=()=>{const proxyUrl=$('cfgProxyUrl').value.trim(),cacheProxyInput=$('cfgCacheProxyUrl');if(proxyUrl){cacheProxyInput.readOnly=false;cacheProxyInput.classList.remove('bg-muted');cacheProxyInput.placeholder='socks5://username:password@127.0.0.1:7890'}else{cacheProxyInput.readOnly=true;cacheProxyInput.classList.add('bg-muted');cacheProxyInput.value='';cacheProxyInput.placeholder='设置服务代理后自动启用'}},
624
+ updateStatsigIdState=()=>{const dynamicToggle=$('cfgDynamicStatsig'),statsigInput=$('cfgStatsigId');if(dynamicToggle.checked){statsigInput.disabled=true;statsigInput.classList.add('bg-muted','text-muted-foreground');statsigInput.placeholder='已启用动态生成'}else{statsigInput.disabled=false;statsigInput.classList.remove('bg-muted','text-muted-foreground');statsigInput.placeholder=''}};
625
+ loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;const cfClearance=k.cf_clearance||'';const cleanCfDisplay=cfClearance.startsWith('cf_clearance=')?cfClearance.split('cf_clearance=')[1]:cfClearance;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgImageMode').value=g.image_mode||'url';$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCacheProxyUrl').value=k.cache_proxy_url||'';$('cfgCfClearance').value=cleanCfDisplay;$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgDynamicStatsig').checked=k.dynamic_statsig!==false;updateStatsigIdState();$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgShowThinking').value=k.show_thinking!==false?'true':'false';$('cfgTemporary').value=k.temporary!==false?'true':'false';$('cfgStreamChunkTimeout').value=k.stream_chunk_timeout||120;$('cfgStreamFirstResponseTimeout').value=k.stream_first_response_timeout||30;$('cfgStreamTotalTimeout').value=k.stream_total_timeout||600;updateCacheProxyReadonly();await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}},
626
+ loadCacheSize=async()=>{try{const r=await apiRequest('/api/cache/size');if(!r)return;const d=await r.json();if(d.success){['image','video','total'].forEach(t=>$(`${t}CacheSize`).value=d.data[`${t}_size`]||'0 MB')}}catch(e){console.error('加载缓存大小失败:',e);['image','video','total'].forEach(t=>$(`${t}CacheSize`).value='0 MB')}},
627
+ clearCacheByType=async(type,url,msg)=>{if(!confirm(msg))return;try{const r=await apiRequest(url,{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`${type}缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'���知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
628
+ clearImageCache=()=>clearCacheByType('图片','/api/cache/clear/images','确定要清理图片缓存吗?此操作将删除所有图片缓存文件'),
629
+ clearVideoCache=()=>clearCacheByType('视频','/api/cache/clear/videos','确定要清理视频缓存吗?此操作将删除所有视频缓存文件!'),
630
+ clearCache=()=>clearCacheByType('','/api/cache/clear','确定要清理缓存吗?此操作将删除 /data/temp 目录中的所有文件!'),
631
  saveGlobalSettings=async()=>{const gc={admin_username:$('cfgAdminUser').value,log_level:$('cfgLogLevel').value,image_cache_max_size_mb:parseInt($('cfgImageCacheMaxSize').value)||500,video_cache_max_size_mb:parseInt($('cfgVideoCacheMaxSize').value)||1000,image_mode:$('cfgImageMode').value,base_url:$('cfgBaseUrl').value};if($('cfgAdminPass').value)gc.admin_password=$('cfgAdminPass').value;try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:gc,grok_config:d.data.grok})});if(!s)return;const sd=await s.json();sd.success?(showToast('全局配置保存成功','success'),$('cfgAdminPass').value=''):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}},
632
+ saveGrokSettings=async()=>{const pu=$('cfgProxyUrl').value.trim(),cf=$('cfgCfClearance').value.trim();const cleanCf=cf.startsWith('cf_clearance=')?cf.split('cf_clearance=')[1]:cf;const kc={api_key:$('cfgApiKey').value,proxy_url:pu,cache_proxy_url:pu?$('cfgCacheProxyUrl').value:'',cf_clearance:cleanCf,x_statsig_id:$('cfgStatsigId').value,dynamic_statsig:$('cfgDynamicStatsig').checked,filtered_tags:$('cfgFilteredTags').value,show_thinking:$('cfgShowThinking').value==='true',temporary:$('cfgTemporary').value==='true',stream_chunk_timeout:parseInt($('cfgStreamChunkTimeout').value)||120,stream_first_response_timeout:parseInt($('cfgStreamFirstResponseTimeout').value)||30,stream_total_timeout:parseInt($('cfgStreamTotalTimeout').value)||600};try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:d.data.global,grok_config:kc})});if(!s)return;const sd=await s.json();sd.success?showToast('Grok配置保存成功','success'):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}};
633
+ updateHoverCardPosition=c=>{const t=c.querySelector('.hover-card-trigger'),ct=c.querySelector('.hover-card-content');if(!t||!ct)return;const{top,bottom}=t.getBoundingClientRect(),h=window.innerHeight;ct.classList.remove('top','bottom');const{visibility:v,opacity:o}=getComputedStyle(ct);Object.assign(ct.style,{visibility:'hidden',opacity:'1'});const ch=ct.offsetHeight;Object.assign(ct.style,{visibility:v,opacity:o});ct.classList.add(top>ch+10?'top':h-bottom>ch+10?'bottom':'top')},
634
+ loadStorageMode=async()=>{const modeConfig={MYSQL:{classes:['bg-blue-50','text-blue-700','border-blue-200'],tooltip:'数据库连接模式 - 数据持久化存储,修改配置时可能稍慢但更安全'},REDIS:{classes:['bg-purple-50','text-purple-700','border-purple-200'],tooltip:'Redis缓存模式 - 高速内存存储,数据持久化且读写性能极佳'},FILE:{classes:['bg-green-50','text-green-700','border-green-200'],tooltip:'文件存储模式 - 本地文件存储,读写速度快'}};const applyMode=(mode)=>{$('storageModeText').textContent=mode;const config=modeConfig[mode]||modeConfig.FILE;$('storageMode').classList.add(...config.classes);$('storageModeTooltip').textContent=config.tooltip;updateHoverCardPosition($('storageMode').closest('.hover-card'))};try{const r=await apiRequest('/api/storage/mode');if(!r)return;const d=await r.json();d.success&&applyMode(d.data.mode)}catch(e){console.error('加载存储模式失败:',e);applyMode('FILE')}};
635
+ window.addEventListener('DOMContentLoaded',()=>{checkAuth();loadStorageMode();refreshTokens();setInterval(()=>{loadStats();updateRemaining()},30000);window.addEventListener('resize',()=>{const hoverCard=$('storageMode').closest('.hover-card');hoverCard&&updateHoverCardPosition(hoverCard)});const hoverCard=$('storageMode').closest('.hover-card'),trigger=hoverCard?.querySelector('.hover-card-trigger'),content=hoverCard?.querySelector('.hover-card-content');if(trigger&&content){trigger.addEventListener('mouseenter',()=>{content.style.opacity='1';content.style.visibility='visible'});trigger.addEventListener('mouseleave',()=>{content.style.opacity='0';content.style.visibility='hidden'})};$('cfgProxyUrl').addEventListener('input',updateCacheProxyReadonly);$('cfgDynamicStatsig').addEventListener('change',updateStatsigIdState)});
636
  </script>
637
  </body>
638
  </html>
data/setting.toml CHANGED
@@ -1,13 +1,15 @@
1
  [grok]
2
  api_key = ""
3
  proxy_url = ""
4
- temporary = true
5
  cf_clearance = ""
6
  x_statsig_id = "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk="
7
  filtered_tags = "xaiartifact,xai:tool_usage_card,grok:render"
8
  stream_chunk_timeout = 120
9
  stream_total_timeout = 600
10
  stream_first_response_timeout = 30
 
 
11
 
12
  [global]
13
  base_url = ""
 
1
  [grok]
2
  api_key = ""
3
  proxy_url = ""
4
+ cache_proxy_url = ""
5
  cf_clearance = ""
6
  x_statsig_id = "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk="
7
  filtered_tags = "xaiartifact,xai:tool_usage_card,grok:render"
8
  stream_chunk_timeout = 120
9
  stream_total_timeout = 600
10
  stream_first_response_timeout = 30
11
+ temporary = true
12
+ show_thinking = true
13
 
14
  [global]
15
  base_url = ""
main.py CHANGED
@@ -111,4 +111,4 @@ app.mount("", mcp_app)
111
 
112
  if __name__ == "__main__":
113
  import uvicorn
114
- uvicorn.run(app, host="0.0.0.0", port=8001)
 
111
 
112
  if __name__ == "__main__":
113
  import uvicorn
114
+ uvicorn.run(app, host="0.0.0.0", port=8001)