Upload 31 files
Browse files- Dockerfile +36 -26
- app/api/admin/manage.py +240 -2
- app/api/v1/chat.py +1 -1
- app/core/auth.py +1 -1
- app/services/grok/cache.py +7 -7
- app/services/grok/client.py +64 -32
- app/services/grok/create.py +103 -0
- app/services/grok/processer.py +73 -54
- app/services/grok/statsig.py +65 -5
- app/services/grok/token.py +52 -9
- app/services/grok/upload.py +3 -5
- app/template/admin.html +182 -115
- data/setting.toml +3 -1
- main.py +1 -1
Dockerfile
CHANGED
|
@@ -1,39 +1,49 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
|
| 3 |
-
WORKDIR /
|
| 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 --
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
WORKDIR /app
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
EXPOSE
|
| 38 |
|
| 39 |
-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "
|
|
|
|
| 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] 聊天请求
|
| 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] 文件
|
| 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]
|
| 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] 文件
|
| 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]
|
| 103 |
|
| 104 |
-
logger.info(f"[{self.cache_type.upper()}Cache]
|
| 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,
|
| 123 |
-
if isinstance(
|
| 124 |
-
logger.warning(f"[Client] 图片上传失败: {url}, 错误: {
|
| 125 |
-
elif
|
| 126 |
-
image_attachments.append(
|
|
|
|
| 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 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 238 |
-
|
| 239 |
-
error_message =
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 305 |
-
if
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 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 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"\n")
|
| 391 |
-
timeout_manager.mark_chunk_received()
|
| 392 |
chunk_index += 1
|
| 393 |
else:
|
| 394 |
yield make_chunk(f"\n")
|
| 395 |
-
timeout_manager.mark_chunk_received()
|
| 396 |
chunk_index += 1
|
| 397 |
else:
|
| 398 |
yield make_chunk(f"\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 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
elif is_thinking and not current_is_thinking:
|
| 470 |
-
|
|
|
|
|
|
|
| 471 |
thinking_finished = True
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"\n")
|
|
|
|
| 398 |
chunk_index += 1
|
| 399 |
else:
|
| 400 |
yield make_chunk(f"\n")
|
|
|
|
| 401 |
chunk_index += 1
|
| 402 |
else:
|
| 403 |
yield make_chunk(f"\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 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}
|
| 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]
|
| 286 |
else:
|
| 287 |
await self.update_limits(sso_value, normal=rate_limit_data.get("remainingTokens", -1), heavy=None)
|
| 288 |
-
logger.info(f"[Token]
|
| 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(-
|
| 25 |
-
[title]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:
|
|
|
|
|
|
|
| 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-
|
| 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-
|
| 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">普通
|
| 224 |
-
<th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-20">高级
|
| 225 |
-
<th class="h-10 px-3 text-left align-middle text-sm font-medium text-muted-foreground w-
|
| 226 |
-
<th class="h-10 px-3 text-
|
|
|
|
|
|
|
| 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="
|
| 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="
|
| 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="
|
| 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="
|
| 290 |
图片模式
|
| 291 |
-
<span class="
|
| 292 |
</label>
|
| 293 |
-
<select id="cfgImageMode" class="
|
| 294 |
-
<option value="url">URL
|
| 295 |
-
<option value="base64">Base64
|
| 296 |
</select>
|
| 297 |
</div>
|
| 298 |
<div>
|
| 299 |
-
<label class="
|
| 300 |
服务网址
|
| 301 |
-
<span class="
|
| 302 |
</label>
|
| 303 |
-
<input id="cfgBaseUrl" class="
|
| 304 |
</div>
|
| 305 |
<div class="grid grid-cols-2 gap-3">
|
| 306 |
<div>
|
| 307 |
-
<label class="
|
| 308 |
图片缓存 (MB)
|
| 309 |
-
<span class="
|
| 310 |
</label>
|
| 311 |
-
<input id="cfgImageCacheMaxSize" type="number" class="
|
| 312 |
</div>
|
| 313 |
<div>
|
| 314 |
-
<label class="
|
| 315 |
视频缓存 (MB)
|
| 316 |
-
<span class="
|
| 317 |
</label>
|
| 318 |
-
<input id="cfgVideoCacheMaxSize" type="number" class="
|
| 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="
|
| 379 |
API Key
|
| 380 |
-
<span class="
|
| 381 |
</label>
|
| 382 |
-
<input id="cfgApiKey" class="
|
| 383 |
</div>
|
| 384 |
<div>
|
| 385 |
-
<label class="
|
| 386 |
-
|
| 387 |
-
<span class="
|
| 388 |
</label>
|
| 389 |
-
<input id="
|
| 390 |
</div>
|
| 391 |
<div>
|
| 392 |
-
<label class="
|
| 393 |
-
|
| 394 |
-
<span class="
|
| 395 |
</label>
|
| 396 |
-
<
|
|
|
|
|
|
|
|
|
|
| 397 |
</div>
|
| 398 |
<div>
|
| 399 |
-
<label class="
|
| 400 |
临时会话
|
| 401 |
-
<span class="
|
| 402 |
</label>
|
| 403 |
-
<select id="cfgTemporary" class="
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
CF Clearance
|
| 418 |
-
<span class="
|
| 419 |
</label>
|
| 420 |
-
<input id="cfgCfClearance" class="
|
| 421 |
</div>
|
| 422 |
<div>
|
| 423 |
-
<label class="
|
| 424 |
Proxy Url (服务代理)
|
| 425 |
-
<span class="
|
| 426 |
</label>
|
| 427 |
-
<input id="cfgProxyUrl" class="
|
| 428 |
</div>
|
| 429 |
<div>
|
| 430 |
-
<label class="
|
| 431 |
Cache Proxy Url (缓存代理)
|
| 432 |
-
<span class="
|
| 433 |
</label>
|
| 434 |
-
<input id="cfgCacheProxyUrl" class="
|
| 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="
|
| 445 |
首次响应超时 (秒)
|
| 446 |
-
<span class="
|
| 447 |
</label>
|
| 448 |
-
<input id="cfgStreamFirstResponseTimeout" type="number" class="
|
| 449 |
</div>
|
| 450 |
<div>
|
| 451 |
-
<label class="
|
| 452 |
流式间隔超时 (秒)
|
| 453 |
-
<span class="
|
| 454 |
</label>
|
| 455 |
-
<input id="cfgStreamChunkTimeout" type="number" class="
|
| 456 |
</div>
|
| 457 |
<div>
|
| 458 |
-
<label class="
|
| 459 |
生成总过程超时 (秒)
|
| 460 |
-
<span class="
|
| 461 |
</label>
|
| 462 |
-
<input id="cfgStreamTotalTimeout" type="number" class="
|
| 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>反机器人验证参数
|
|
|
|
| 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-
|
| 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():
|
| 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){
|
| 551 |
-
submitAddTokens=async()=>{const tt=$('addTokenType').value,tks=$('addTokenList').value.split('\n').map(t=>t.trim()).filter(t=>t);if(!tks.length)return
|
| 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${
|
| 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 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
| 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:$('
|
| 566 |
-
|
| 567 |
-
loadStorageMode=async()=>{
|
| 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="开启后会显示模型的思考过程(<think>标签内容);关闭后仅返回最终结果">?</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 |
-
|
| 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)
|