7 files changed
+249
-388
No files matched your search
+60
-100
@@ -1,6 +1,7 @@
|
||||
"""缓存服务模块"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from curl_cffi.requests import AsyncSession
|
||||
@@ -19,42 +20,20 @@ class CacheService:
|
||||
self.cache_dir = Path(f"data/temp/{cache_type}")
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_cache_filename(file_path: str) -> str:
|
||||
"""将文件路径转换为缓存文件名"""
|
||||
filename = file_path.lstrip('/').replace('/', '-')
|
||||
return filename
|
||||
|
||||
def _get_cache_path(self, file_path: str) -> Path:
|
||||
def _cache_path(self, file_path: str) -> Path:
|
||||
"""获取缓存文件的完整路径"""
|
||||
filename = self._get_cache_filename(file_path)
|
||||
filename = file_path.lstrip('/').replace('/', '-')
|
||||
return self.cache_dir / filename
|
||||
|
||||
async def download_file(self, file_path: str, auth_token: str, timeout: float = 30.0) -> Optional[Path]:
|
||||
"""下载并缓存文件
|
||||
|
||||
Args:
|
||||
file_path: 文件路径,如 /users/xxx/generated/xxx/file.jpg
|
||||
auth_token: 认证令牌
|
||||
timeout: 下载超时时间(秒)
|
||||
|
||||
Returns:
|
||||
缓存文件路径,下载失败返回 None
|
||||
"""
|
||||
cache_path = self._get_cache_path(file_path)
|
||||
|
||||
"""下载并缓存文件"""
|
||||
cache_path = self._cache_path(file_path)
|
||||
if cache_path.exists():
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 文件已缓存: {cache_path}")
|
||||
return cache_path
|
||||
|
||||
file_url = f"https://assets.grok.com{file_path}"
|
||||
|
||||
try:
|
||||
# 构建 Cookie
|
||||
cf_clearance = setting.grok_config.get("cf_clearance", "")
|
||||
cookie = f"{auth_token};{cf_clearance}" if cf_clearance else auth_token
|
||||
|
||||
# 构建请求头
|
||||
headers = {
|
||||
**get_dynamic_headers(pathname=file_path),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
@@ -64,17 +43,16 @@ class CacheService:
|
||||
"Sec-Fetch-User": "?1",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Referer": "https://grok.com/",
|
||||
"Cookie": cookie
|
||||
"Cookie": f"{auth_token};{cf_clearance}" if cf_clearance else auth_token
|
||||
}
|
||||
|
||||
# 代理配置
|
||||
proxy_url = setting.grok_config.get("proxy_url")
|
||||
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else {}
|
||||
|
||||
async with AsyncSession() as session:
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: {file_url}")
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 开始下载: https://assets.grok.com{file_path}")
|
||||
response = await session.get(
|
||||
file_url,
|
||||
f"https://assets.grok.com{file_path}",
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
@@ -82,70 +60,44 @@ class CacheService:
|
||||
impersonate="chrome133a"
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
cache_path.write_bytes(response.content)
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 文件已缓存: {cache_path} ({len(response.content)} bytes)")
|
||||
|
||||
asyncio.create_task(self.cleanup_cache())
|
||||
|
||||
return cache_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.cache_type.upper()}Cache] 下载文件失败: {e}")
|
||||
return None
|
||||
|
||||
def get_cached_file(self, file_path: str) -> Optional[Path]:
|
||||
"""获取缓存的文件路径
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
缓存文件路径,不存在返回 None
|
||||
"""
|
||||
cache_path = self._get_cache_path(file_path)
|
||||
def get_cached(self, file_path: str) -> Optional[Path]:
|
||||
"""获取缓存的文件路径"""
|
||||
cache_path = self._cache_path(file_path)
|
||||
return cache_path if cache_path.exists() else None
|
||||
|
||||
async def cleanup_cache(self):
|
||||
"""清理缓存目录,确保不超过配置的大小限制"""
|
||||
try:
|
||||
# 获取配置的最大缓存大小
|
||||
config_key = f"{self.cache_type}_cache_max_size_mb"
|
||||
max_size_mb = setting.global_config.get(config_key, 500)
|
||||
max_size_mb = setting.global_config.get(f"{self.cache_type}_cache_max_size_mb", 500)
|
||||
max_size_bytes = max_size_mb * 1024 * 1024
|
||||
|
||||
# 获取缓存大小和修改时间
|
||||
files = []
|
||||
total_size = 0
|
||||
files = [(fp, (stat := fp.stat()).st_size, stat.st_mtime)
|
||||
for fp in self.cache_dir.glob("*") if fp.is_file()]
|
||||
total_size = sum(size for _, size, _ in files)
|
||||
|
||||
for file_path in self.cache_dir.glob("*"):
|
||||
if file_path.is_file():
|
||||
size = file_path.stat().st_size
|
||||
mtime = file_path.stat().st_mtime
|
||||
files.append((file_path, size, mtime))
|
||||
total_size += size
|
||||
|
||||
# 如果总大小未超限,无需清理
|
||||
if total_size <= max_size_bytes:
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB,未超限")
|
||||
return
|
||||
|
||||
logger.info(f"[{self.cache_type.upper()}Cache] 缓存大小 {total_size / 1024 / 1024:.2f}MB 超过限制 {max_size_mb}MB,开始清理")
|
||||
|
||||
# 按修改时间排序
|
||||
files.sort(key=lambda x: x[2])
|
||||
|
||||
# 删除文件直到总大小低于限制
|
||||
for file_path, size, _ in files:
|
||||
if total_size <= max_size_bytes:
|
||||
break
|
||||
|
||||
file_path.unlink()
|
||||
total_size -= size
|
||||
logger.debug(f"[{self.cache_type.upper()}Cache] 已删除缓存文件: {file_path}")
|
||||
|
||||
logger.info(f"[{self.cache_type.upper()}Cache] 缓存清理完成,当前大小 {total_size / 1024 / 1024:.2f}MB")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.cache_type.upper()}Cache] 清理缓存失败: {e}")
|
||||
|
||||
@@ -157,27 +109,51 @@ class ImageCacheService(CacheService):
|
||||
super().__init__("image")
|
||||
|
||||
async def download_image(self, image_path: str, auth_token: str) -> Optional[Path]:
|
||||
"""下载并缓存图片
|
||||
|
||||
Args:
|
||||
image_path: 图片路径,如 /users/xxx/generated/xxx/image.jpg
|
||||
auth_token: 认证令牌
|
||||
|
||||
Returns:
|
||||
缓存文件路径,下载失败返回 None`
|
||||
"""
|
||||
"""下载并缓存图片"""
|
||||
return await self.download_file(image_path, auth_token, timeout=30.0)
|
||||
|
||||
def get_cached_image(self, image_path: str) -> Optional[Path]:
|
||||
"""获取缓存的图片路径
|
||||
def get_cached(self, image_path: str) -> Optional[Path]:
|
||||
"""获取缓存的图片路径"""
|
||||
return super().get_cached(image_path)
|
||||
|
||||
Args:
|
||||
image_path: 图片路径
|
||||
@staticmethod
|
||||
def to_base64(image_path: Path) -> Optional[str]:
|
||||
"""将图片转换为 base64 编码"""
|
||||
try:
|
||||
if not image_path.exists():
|
||||
logger.error(f"[ImageCache] 图片文件不存在: {image_path}")
|
||||
return None
|
||||
|
||||
Returns:
|
||||
缓存文件路径,不存在返回 None
|
||||
"""
|
||||
return self.get_cached_file(image_path)
|
||||
with open(image_path, "rb") as f:
|
||||
base64_data = base64.b64encode(f.read()).decode('utf-8')
|
||||
|
||||
mime_type = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
||||
'.gif': 'image/gif', '.webp': 'image/webp'}.get(image_path.suffix.lower(), 'image/jpeg')
|
||||
|
||||
return f"data:{mime_type};base64,{base64_data}"
|
||||
except Exception as e:
|
||||
logger.error(f"[ImageCache] 图片转 base64 失败: {e}")
|
||||
return None
|
||||
|
||||
async def download_base64(self, image_path: str, auth_token: str) -> Optional[str]:
|
||||
"""下载图片并转换为 base64 编码(转换后立即删除缓存文件)"""
|
||||
try:
|
||||
cache_path = await self.download_file(image_path, auth_token, timeout=30.0)
|
||||
if not cache_path:
|
||||
return None
|
||||
|
||||
base64_str = self.to_base64(cache_path)
|
||||
|
||||
try:
|
||||
cache_path.unlink()
|
||||
logger.debug(f"[ImageCache] 已删除临时文件: {cache_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[ImageCache] 删除临时文件失败: {e}")
|
||||
|
||||
return base64_str
|
||||
except Exception as e:
|
||||
logger.error(f"[ImageCache] 下载并转换 base64 失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class VideoCacheService(CacheService):
|
||||
@@ -187,30 +163,14 @@ class VideoCacheService(CacheService):
|
||||
super().__init__("video")
|
||||
|
||||
async def download_video(self, video_path: str, auth_token: str) -> Optional[Path]:
|
||||
"""下载并缓存视频
|
||||
|
||||
Args:
|
||||
video_path: 视频路径,如 /users/xxx/generated/xxx/video.mp4
|
||||
auth_token: 认证令牌
|
||||
|
||||
Returns:
|
||||
缓存文件路径,下载失败返回 None
|
||||
"""
|
||||
"""下载并缓存视频"""
|
||||
return await self.download_file(video_path, auth_token, timeout=60.0)
|
||||
|
||||
def get_cached_video(self, video_path: str) -> Optional[Path]:
|
||||
"""获取缓存的视频路径
|
||||
|
||||
Args:
|
||||
video_path: 视频路径
|
||||
|
||||
Returns:
|
||||
缓存文件路径,不存在返回 None
|
||||
"""
|
||||
return self.get_cached_file(video_path)
|
||||
def get_cached(self, video_path: str) -> Optional[Path]:
|
||||
"""获取缓存的视频路径"""
|
||||
return super().get_cached(video_path)
|
||||
|
||||
|
||||
# 全局实例
|
||||
image_cache_service = ImageCacheService()
|
||||
video_cache_service = VideoCacheService()
|
||||
|
||||
@@ -157,18 +157,30 @@ class GrokResponseProcessor:
|
||||
|
||||
# 提取图片数据
|
||||
if images := model_response.get("generatedImageUrls"):
|
||||
# 获取图片返回模式
|
||||
image_mode = setting.global_config.get("image_mode", "url")
|
||||
|
||||
for img in images:
|
||||
try:
|
||||
cache_path = await image_cache_service.download_image(f"/{img}", auth_token)
|
||||
if cache_path:
|
||||
img_path = img.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
|
||||
content += f"\n"
|
||||
if image_mode == "base64":
|
||||
# base64 模式:下载并转换为 base64
|
||||
base64_str = await image_cache_service.download_base64(f"/{img}", auth_token)
|
||||
if base64_str:
|
||||
content += f"\n"
|
||||
else:
|
||||
content += f"\n"
|
||||
else:
|
||||
content += f"\n"
|
||||
# url 模式:缓存并返回链接
|
||||
cache_path = await image_cache_service.download_image(f"/{img}", auth_token)
|
||||
if cache_path:
|
||||
img_path = img.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
|
||||
content += f"\n"
|
||||
else:
|
||||
content += f"\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"[Processor] 缓存图片失败: {e}")
|
||||
logger.warning(f"[Processor] 处理图片失败: {e}")
|
||||
content += f"\n"
|
||||
|
||||
# 返回 OpenAI 响应格式
|
||||
@@ -333,19 +345,66 @@ class GrokResponseProcessor:
|
||||
# 提取图片数据
|
||||
if is_image:
|
||||
if model_resp := grok_resp.get("modelResponse"):
|
||||
# 获取图片返回模式
|
||||
image_mode = setting.global_config.get("image_mode", "url")
|
||||
|
||||
# 生成图片链接并缓存
|
||||
content = ""
|
||||
for img in model_resp.get("generatedImageUrls", []):
|
||||
try:
|
||||
# 缓存图片
|
||||
await image_cache_service.download_image(f"/{img}", auth_token)
|
||||
# 本地图片路径
|
||||
img_path = img.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
|
||||
content += f"\n"
|
||||
if image_mode == "base64":
|
||||
# base64 模式:下载并转换为 base64
|
||||
base64_str = await image_cache_service.download_base64(f"/{img}", auth_token)
|
||||
if base64_str:
|
||||
# 分块发送 base64 数据,每 8KB 一个 chunk
|
||||
markdown_prefix = "\n"
|
||||
|
||||
# 提取 data URL 的 mime 和 base64 部分
|
||||
if base64_str.startswith("data:"):
|
||||
parts = base64_str.split(",", 1)
|
||||
if len(parts) == 2:
|
||||
mime_part = parts[0] + ","
|
||||
b64_data = parts[1]
|
||||
|
||||
# 发送前缀
|
||||
yield make_chunk(markdown_prefix + mime_part)
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
|
||||
# 分块发送 base64 数据
|
||||
chunk_size = 8192
|
||||
for i in range(0, len(b64_data), chunk_size):
|
||||
chunk_data = b64_data[i:i + chunk_size]
|
||||
yield make_chunk(chunk_data)
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
|
||||
# 发送后缀
|
||||
yield make_chunk(markdown_suffix)
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
else:
|
||||
yield make_chunk(f"\n")
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
else:
|
||||
yield make_chunk(f"\n")
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
else:
|
||||
yield make_chunk(f"\n")
|
||||
timeout_manager.mark_chunk_received()
|
||||
chunk_index += 1
|
||||
else:
|
||||
# url 模式:缓存并返回链接
|
||||
await image_cache_service.download_image(f"/{img}", auth_token)
|
||||
# 本地图片路径
|
||||
img_path = img.replace('/', '-')
|
||||
base_url = setting.global_config.get("base_url", "")
|
||||
img_url = f"{base_url}/images/{img_path}" if base_url else f"/images/{img_path}"
|
||||
content += f"\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"[Processor] 缓存图片失败: {e}")
|
||||
logger.warning(f"[Processor] 处理图片失败: {e}")
|
||||
content += f"\n"
|
||||
yield make_chunk(content.strip(), "stop")
|
||||
timeout_manager.mark_chunk_received()
|
||||
|
||||
+80
-123
@@ -4,103 +4,49 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理控制台 - Grok2API</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
.tab-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
[title] {
|
||||
position: relative;
|
||||
}
|
||||
[title]:hover::after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
background: hsl(0 0% 3.9%);
|
||||
color: hsl(0 0% 98%);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
[title]:hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: hsl(0 0% 3.9%);
|
||||
z-index: 1000;
|
||||
}
|
||||
@keyframes slide-up{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
.animate-slide-up{animation:slide-up .3s ease-out}
|
||||
.tab-btn{transition:all .2s ease}
|
||||
[title]{position:relative}
|
||||
[title]:hover::after{content:attr(title);position:absolute;bottom:100%;left:50%;transform:translateX(-50%) translateY(-4px);background:hsl(0 0% 3.9%);color:hsl(0 0% 98%);padding:4px 8px;border-radius:4px;font-size:11px;white-space:nowrap;z-index:1000;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}
|
||||
[title]:hover::before{content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:hsl(0 0% 3.9%);z-index:1000}
|
||||
</style>
|
||||
<script>
|
||||
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%)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
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%)"}}}}}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-background text-foreground antialiased">
|
||||
<!-- 导航栏 -->
|
||||
<header class="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="mx-auto flex h-14 max-w-7xl items-center px-6">
|
||||
<div class="mr-4 flex">
|
||||
<span class="font-bold text-lg">Grok2API</span>
|
||||
<div class="mr-4 flex items-baseline gap-3">
|
||||
<span class="font-bold text-xl">Grok2API</span>
|
||||
<span class="text-xs text-gray-400">by @Chenyme</span>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-between space-x-2 md:justify-end">
|
||||
<nav class="flex items-center space-x-2">
|
||||
<button
|
||||
onclick="logout()"
|
||||
class="inline-flex items-center justify-center 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 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
|
||||
<nav class="flex items-center gap-1">
|
||||
<a
|
||||
href="https://github.com/chenyme/grok2api/issues"
|
||||
target="_blank"
|
||||
class="inline-flex items-center justify-center text-xs transition-colors hover:bg-accent hover:text-accent-foreground h-7 px-2.5 gap-1"
|
||||
>
|
||||
<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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
反馈
|
||||
</a>
|
||||
<button
|
||||
onclick="logout()"
|
||||
class="inline-flex items-center justify-center text-xs transition-colors hover:bg-accent hover:text-accent-foreground h-7 px-2.5 gap-1"
|
||||
>
|
||||
<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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
退出
|
||||
</button>
|
||||
</nav>
|
||||
@@ -274,6 +220,18 @@
|
||||
|
||||
<!-- 全局设置面板 -->
|
||||
<div id="panelSettings" class="hidden">
|
||||
<!-- 配置提示 -->
|
||||
<div class="mb-6 rounded border border-blue-300 bg-blue-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-800 leading-relaxed">
|
||||
<div class="text-base font-medium text-gray-900 mb-2.5">部分说明</div>
|
||||
<div class="space-y-1.5">
|
||||
<div><span class="font-medium">服务网址:</span>图片/视频链接返回时需要拼接您的服务网址(如 https://yourdomain.com),若您不使用视频功能且图片使用Base64模式则可留空</div>
|
||||
<div><span class="font-medium">日志报错 403:</span>通常是被 CF 拦截了,可配置代理IP、更换服务器IP或者在服务器中访问 grok.com 通过 CF 验证后 F12 获取 cf_clearance</div>
|
||||
<div><span class="font-medium">X Statsig ID:</span>反机器人验证参数,勿随意修改,一般 403 是被盾了,并不是需要更新 X Statsig ID</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- 全局配置 -->
|
||||
<div class="rounded-lg border border-border bg-background p-6">
|
||||
@@ -302,6 +260,23 @@
|
||||
<option>DEBUG</option><option>INFO</option><option>WARNING</option><option>ERROR</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
图片模式
|
||||
<span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="选择图片返回方式:URL模式返回图片链接(需缓存);Base64模式返回base64编码(无需缓存,直接嵌入响应)">?</span>
|
||||
</label>
|
||||
<select id="cfgImageMode" 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">
|
||||
<option value="url">URL (图片链接)</option>
|
||||
<option value="base64">Base64 (base64编码)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
服务网址
|
||||
<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="用于生成图片链接的服务地址。建议使用域名以保护服务器IP,如无图片需求可留空。注意:Base64模式下无需填写此项">?</span>
|
||||
</label>
|
||||
<input id="cfgBaseUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://localhost:8000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
图片缓存上限 (MB)
|
||||
@@ -318,8 +293,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
缓存大小
|
||||
<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>
|
||||
项目缓存清理
|
||||
<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>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
@@ -347,24 +322,16 @@
|
||||
<span class="text-sm text-muted-foreground w-12">总计:</span>
|
||||
<input id="totalCacheSize" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm font-medium" placeholder="0 MB">
|
||||
<button onclick="clearCache()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-3 transition-colors" title="清除所有缓存(图片+视频)">
|
||||
<svg class="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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">
|
||||
<path d="M3 6h18"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
清除全部
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
服务网址
|
||||
<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="用于生成图片链接的服务地址。建议使用域名以保护服务器IP,如无图片需求可留空">?</span>
|
||||
</label>
|
||||
<input id="cfgBaseUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://localhost:8000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button onclick="saveGlobalSettings()" class="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-6 transition-colors">保存配置</button>
|
||||
@@ -405,14 +372,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
Filtered_tags
|
||||
Filtered Tags
|
||||
<span class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-muted-foreground text-muted-foreground cursor-help" style="font-size:10px;line-height:1" title="过滤Grok响应中的指定标签,多个标签用逗号分隔">?</span>
|
||||
</label>
|
||||
<input id="cfgFilteredTags" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="xaiartifact,xai:tool_usage_card">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
临时会话
|
||||
临时会话模式
|
||||
<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>
|
||||
</label>
|
||||
<select id="cfgTemporary" 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">
|
||||
@@ -420,13 +387,6 @@
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
数据块间隔超时 (秒)
|
||||
<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>
|
||||
</label>
|
||||
<input id="cfgStreamChunkTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="120">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
首次响应超时 (秒)
|
||||
@@ -436,7 +396,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
总超时限制 (秒)
|
||||
流式间隔超时 (秒)
|
||||
<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>
|
||||
</label>
|
||||
<input id="cfgStreamChunkTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="120">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium flex items-center gap-1 mb-2">
|
||||
生成总过程超时 (秒)
|
||||
<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="流式响应的总时长限制。设置为0表示不限制总时长">?</span>
|
||||
</label>
|
||||
<input id="cfgStreamTotalTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="600">
|
||||
@@ -507,17 +474,7 @@
|
||||
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)}},
|
||||
updateRemaining=()=>{const r=calcRemaining();['Total','Normal','Heavy'].forEach(k=>$(`stat${k}Remaining`).textContent=r[k.toLowerCase()]===0?'-':r[k.toLowerCase()].toLocaleString())}
|
||||
|
||||
const renderTokens=()=>{const tb=$('tokenTableBody'),es=$('emptyState'),ss={'未使用':'bg-muted text-muted-foreground','限流中':'bg-orange-50 text-orange-700 border-orange-200','失效':'bg-destructive/10 text-destructive border-destructive/20','正常':'bg-green-50 text-green-700 border-green-200'},ts={sso:'bg-blue-50 text-blue-700 border-blue-200',ssoSuper:'bg-purple-50 text-purple-700 border-purple-200'},tl={sso:'SSO',ssoSuper:'SuperSSO'};if(!filteredTokens.length){tb.innerHTML='';es.classList.remove('hidden');$('selectAll').checked=false;return updateBatchActions()}es.classList.add('hidden');tb.innerHTML=filteredTokens.map(t=>`
|
||||
<tr class="transition-colors">
|
||||
<td class="py-2.5 px-3 align-middle w-12"><input type="checkbox" class="token-checkbox h-3.5 w-3.5 rounded border border-input focus:ring-1 focus:ring-ring" data-token="${t.token}" data-type="${t.token_type}" ${selectedTokens.has(t.token)?'checked':''} onchange="toggleToken('${t.token}')"></td>
|
||||
<td class="py-2.5 px-3 align-middle w-80"><div class="flex items-center gap-2"><span class="font-mono text-xs">${t.token.substring(0,30)}...</span><button onclick="copyToken('${t.token.replace(/'/g,"\\'")}',event)" class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent h-6 w-6" title="复制完整 Token"><svg class="h-3 w-3 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></td>
|
||||
<td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ts[t.token_type]}">${tl[t.token_type]}</span></td>
|
||||
<td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ss[t.status]}">${t.status}</span></td>
|
||||
<td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.remaining_queries===-1?'-':t.remaining_queries}</td>
|
||||
<td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.heavy_remaining_queries===-1?'-':t.heavy_remaining_queries}</td>
|
||||
<td class="py-2.5 px-3 align-middle w-32 text-xs text-muted-foreground">${t.created_time?new Date(t.created_time).toLocaleString('zh-CN',{dateStyle:'short',timeStyle:'short'}):'-'}</td>
|
||||
<td class="py-2.5 px-3 align-middle text-right w-16"><button onclick="deleteToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-destructive/10 hover:text-destructive h-7 w-7"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></td>
|
||||
</tr>`).join('');updateBatchActions()},
|
||||
const renderTokens=()=>{const tb=$('tokenTableBody'),es=$('emptyState'),ss={'未使用':'bg-muted text-muted-foreground','限流中':'bg-orange-50 text-orange-700 border-orange-200','失效':'bg-destructive/10 text-destructive border-destructive/20','正常':'bg-green-50 text-green-700 border-green-200'},ts={sso:'bg-blue-50 text-blue-700 border-blue-200',ssoSuper:'bg-purple-50 text-purple-700 border-purple-200'},tl={sso:'SSO',ssoSuper:'SuperSSO'};if(!filteredTokens.length){tb.innerHTML='';es.classList.remove('hidden');$('selectAll').checked=false;return updateBatchActions()}es.classList.add('hidden');tb.innerHTML=filteredTokens.map(t=>`<tr class="transition-colors"><td class="py-2.5 px-3 align-middle w-12"><input type="checkbox" class="token-checkbox h-3.5 w-3.5 rounded border border-input focus:ring-1 focus:ring-ring" data-token="${t.token}" data-type="${t.token_type}" ${selectedTokens.has(t.token)?'checked':''} onchange="toggleToken('${t.token}')"></td><td class="py-2.5 px-3 align-middle w-80"><div class="flex items-center gap-2"><span class="font-mono text-xs">${t.token.substring(0,30)}...</span><button onclick="copyToken('${t.token.replace(/'/g,"\\'")}',event)" class="inline-flex items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-accent h-6 w-6" title="复制完整 Token"><svg class="h-3 w-3 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ts[t.token_type]}">${tl[t.token_type]}</span></td><td class="py-2.5 px-3 align-middle w-20"><span class="inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium border ${ss[t.status]}">${t.status}</span></td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.remaining_queries===-1?'-':t.remaining_queries}</td><td class="py-2.5 px-3 align-middle w-20 text-xs tabular-nums">${t.heavy_remaining_queries===-1?'-':t.heavy_remaining_queries}</td><td class="py-2.5 px-3 align-middle w-32 text-xs text-muted-foreground">${t.created_time?new Date(t.created_time).toLocaleString('zh-CN',{dateStyle:'short',timeStyle:'short'}):'-'}</td><td class="py-2.5 px-3 align-middle text-right w-16"><button onclick="deleteToken('${t.token}','${t.token_type}')" class="inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring hover:bg-destructive/10 hover:text-destructive h-7 w-7"><svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></td></tr>`).join('');updateBatchActions()},
|
||||
toggleToken=t=>selectedTokens[selectedTokens.has(t)?'delete':'add'](t)||updateBatchActions(),
|
||||
toggleSelectAll=()=>{const sa=$('selectAll');sa.checked?filteredTokens.forEach(t=>selectedTokens.add(t.token)):selectedTokens.clear();renderTokens()},
|
||||
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},
|
||||
@@ -530,16 +487,16 @@
|
||||
submitAddTokens=async()=>{const tt=$('addTokenType').value,tks=$('addTokenList').value.split('\n').map(t=>t.trim()).filter(t=>t);if(!tks.length)return alert('请输入至少一个 Token');try{const r=await apiRequest('/api/tokens/add',{method:'POST',body:JSON.stringify({tokens:tks,token_type:tt})});if(!r)return;const d=await r.json();d.success?(closeAddModal(),await refreshTokens()):alert('添加失败: '+(d.error||'未知错误'))}catch(e){alert('添加失败: '+e.message)}},
|
||||
copyToken=async(t,e)=>{e.stopPropagation();try{await navigator.clipboard.writeText(t);showToast('Token 已复制到剪贴板','success')}catch(err){console.error('复制失败:',err);showToast('复制失败,请手动复制','error')}}
|
||||
|
||||
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.visibility='hidden';document.body.appendChild(l);l.click();document.body.removeChild(l);URL.revokeObjectURL(l.href);showToast(`已导出 ${selectedTokens.size} 个 Token`,'success')},
|
||||
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 0.3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
||||
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')}
|
||||
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)}
|
||||
logout=async()=>{if(!confirm('确定要退出登录吗?'))return;try{await apiRequest('/api/logout',{method:'POST'})}catch(e){console.error('登出失败:',e)}finally{localStorage.removeItem('adminToken');location.href='/login'}},
|
||||
switchTab=t=>{['tokens','settings'].forEach(n=>{$(`panel${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('hidden');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'add':'remove']('border-primary','text-primary');$(`tab${n.charAt(0).toUpperCase()+n.slice(1)}`).classList[n===t?'remove':'add']('border-transparent','text-muted-foreground')});t==='settings'&&loadSettings()},
|
||||
loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCfClearance').value=k.cf_clearance||'';$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgTemporary').value=k.temporary!==false?'true':'false';$('cfgStreamChunkTimeout').value=k.stream_chunk_timeout||120;$('cfgStreamFirstResponseTimeout').value=k.stream_first_response_timeout||30;$('cfgStreamTotalTimeout').value=k.stream_total_timeout||600;await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}},
|
||||
loadSettings=async()=>{try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(d.success){const g=d.data.global,k=d.data.grok;$('cfgAdminUser').value=g.admin_username||'';$('cfgAdminPass').value='';$('cfgLogLevel').value=g.log_level||'DEBUG';$('cfgImageCacheMaxSize').value=g.image_cache_max_size_mb||500;$('cfgVideoCacheMaxSize').value=g.video_cache_max_size_mb||1000;$('cfgImageMode').value=g.image_mode||'url';$('cfgBaseUrl').value=g.base_url||'';$('cfgApiKey').value=k.api_key||'';$('cfgProxyUrl').value=k.proxy_url||'';$('cfgCfClearance').value=k.cf_clearance||'';$('cfgStatsigId').value=k.x_statsig_id||'';$('cfgFilteredTags').value=k.filtered_tags||'';$('cfgTemporary').value=k.temporary!==false?'true':'false';$('cfgStreamChunkTimeout').value=k.stream_chunk_timeout||120;$('cfgStreamFirstResponseTimeout').value=k.stream_first_response_timeout||30;$('cfgStreamTotalTimeout').value=k.stream_total_timeout||600;await loadCacheSize()}}catch(e){console.error('加载配置失败:',e);showToast('加载配置失败','error')}},
|
||||
loadCacheSize=async()=>{try{const r=await apiRequest('/api/cache/size');if(!r)return;const d=await r.json();if(d.success){$('imageCacheSize').value=d.data.image_size||'0 MB';$('videoCacheSize').value=d.data.video_size||'0 MB';$('totalCacheSize').value=d.data.total_size||'0 MB'}}catch(e){console.error('加载缓存大小失败:',e);$('imageCacheSize').value='0 MB';$('videoCacheSize').value='0 MB';$('totalCacheSize').value='0 MB'}},
|
||||
clearImageCache=async()=>{if(!confirm('确定要清理图片缓存吗?此操作将删除所有图片缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/images',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`图片缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
clearVideoCache=async()=>{if(!confirm('确定要清理视频缓存吗?此操作将删除所有视频缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/videos',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`视频缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
clearCache=async()=>{if(!confirm('确定要清理缓存吗?此操作将删除 /data/temp 目录中的所有文件!'))return;try{const r=await apiRequest('/api/cache/clear',{method:'POST'});if(!r)return;const d=await r.json();if(d.success){showToast(`缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success');await loadCacheSize()}else{showToast('清理失败: '+(d.error||'未知错误'),'error')}}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
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,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')}},
|
||||
clearImageCache=async()=>{if(!confirm('确定要清理图片缓存吗?此操作将删除所有图片缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/images',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`图片缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
clearVideoCache=async()=>{if(!confirm('确定要清理视频缓存吗?此操作将删除所有视频缓存文件!'))return;try{const r=await apiRequest('/api/cache/clear/videos',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`视频缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
clearCache=async()=>{if(!confirm('确定要清理缓存吗?此操作将删除 /data/temp 目录中的所有文件!'))return;try{const r=await apiRequest('/api/cache/clear',{method:'POST'});if(!r)return;const d=await r.json();d.success?(showToast(`缓存清理完成,已删除 ${d.data.deleted_count||0} 个文件`,'success'),await loadCacheSize()):showToast('清理失败: '+(d.error||'未知错误'),'error')}catch(e){showToast('清理失败: '+e.message,'error')}},
|
||||
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')}},
|
||||
saveGrokSettings=async()=>{const kc={api_key:$('cfgApiKey').value,proxy_url:$('cfgProxyUrl').value,cf_clearance:$('cfgCfClearance').value,x_statsig_id:$('cfgStatsigId').value,filtered_tags:$('cfgFilteredTags').value,temporary:$('cfgTemporary').value==='true',stream_chunk_timeout:parseInt($('cfgStreamChunkTimeout').value)||120,stream_first_response_timeout:parseInt($('cfgStreamFirstResponseTimeout').value)||30,stream_total_timeout:parseInt($('cfgStreamTotalTimeout').value)||600};try{const r=await apiRequest('/api/settings');if(!r)return;const d=await r.json();if(!d.success)return showToast('加载配置失败','error');const s=await apiRequest('/api/settings',{method:'POST',body:JSON.stringify({global_config:d.data.global,grok_config:kc})});if(!s)return;const sd=await s.json();sd.success?showToast('Grok配置保存成功','success'):showToast('保存失败: '+(sd.error||'未知错误'),'error')}catch(e){showToast('保存失败: '+e.message,'error')}};
|
||||
window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();setInterval(()=>{loadStats();updateRemaining()},30000)});
|
||||
</script>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
+13
-139
@@ -4,52 +4,14 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - Grok2API</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
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%)",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(0 84.2% 60.2%)",
|
||||
foreground: "hsl(0 0% 98%)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
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%)"},destructive:{DEFAULT:"hsl(0 84.2% 60.2%)",foreground:"hsl(0 0% 98%)"}}}}}
|
||||
</script>
|
||||
<style>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
@keyframes slide-up{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
.animate-slide-up{animation:slide-up .3s ease-out}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full bg-background text-foreground antialiased">
|
||||
@@ -64,44 +26,15 @@
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div class="bg-background py-8 px-4 sm:px-10 rounded-lg">
|
||||
<form id="loginForm" class="space-y-6">
|
||||
<!-- 账户 -->
|
||||
<div class="space-y-2">
|
||||
<label for="username" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
账户
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
|
||||
placeholder="请输入用户名"
|
||||
>
|
||||
<label for="username" class="text-sm font-medium">账户</label>
|
||||
<input type="text" id="username" name="username" required class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50" placeholder="请输入账户">
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div class="space-y-2">
|
||||
<label for="password" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
|
||||
placeholder="请输入密码"
|
||||
>
|
||||
<label for="password" class="text-sm font-medium">密码</label>
|
||||
<input type="password" id="password" name="password" required class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50" placeholder="请输入密码">
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
id="loginButton"
|
||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full text-sm"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button type="submit" id="loginButton" class="inline-flex items-center justify-center rounded-md font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-10 w-full disabled:opacity-50">登录</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-xs text-muted-foreground">
|
||||
@@ -112,69 +45,10 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const loginButton = document.getElementById('loginButton');
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
loginButton.disabled = true;
|
||||
loginButton.textContent = '登录中...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(loginForm);
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: formData.get('username'),
|
||||
password: formData.get('password')
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('adminToken', data.token);
|
||||
window.location.href = '/manage';
|
||||
} else {
|
||||
showToast(data.message || '登录失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('网络错误,请稍后重试', 'error');
|
||||
} finally {
|
||||
loginButton.disabled = false;
|
||||
loginButton.textContent = '登录';
|
||||
}
|
||||
});
|
||||
|
||||
function showToast(message, type = 'error') {
|
||||
const toast = document.createElement('div');
|
||||
const bgColors = {
|
||||
success: 'bg-green-600',
|
||||
error: 'bg-destructive',
|
||||
info: 'bg-primary'
|
||||
};
|
||||
toast.className = `fixed bottom-4 right-4 ${bgColors[type] || bgColors.error} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transition = 'opacity 0.3s';
|
||||
setTimeout(() => toast.parentNode && document.body.removeChild(toast), 300);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
if (token) {
|
||||
fetch('/api/stats', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}).then(response => {
|
||||
if (response.ok) window.location.href = '/manage';
|
||||
});
|
||||
}
|
||||
});
|
||||
const form=document.getElementById('loginForm'),btn=document.getElementById('loginButton');
|
||||
form.addEventListener('submit',async(e)=>{e.preventDefault();btn.disabled=true;btn.textContent='登录中...';try{const fd=new FormData(form),r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:fd.get('username'),password:fd.get('password')})});const d=await r.json();d.success?(localStorage.setItem('adminToken',d.token),location.href='/manage'):showToast(d.message||'登录失败','error')}catch(e){showToast('网络错误,请稍后重试','error')}finally{btn.disabled=false;btn.textContent='登录'}});
|
||||
function showToast(m,t='error'){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.error} 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)}
|
||||
window.addEventListener('DOMContentLoaded',()=>{const t=localStorage.getItem('adminToken');t&&fetch('/api/stats',{headers:{Authorization:`Bearer ${t}`}}).then(r=>{if(r.ok)location.href='/manage'})});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -2,12 +2,12 @@
|
||||
admin_username = "admin"
|
||||
admin_password = "admin"
|
||||
log_level = "INFO"
|
||||
image_mode = "url"
|
||||
image_cache_max_size_mb = 500
|
||||
video_cache_max_size_mb = 1000
|
||||
base_url = ""
|
||||
|
||||
|
||||
|
||||
[grok]
|
||||
api_key = ""
|
||||
proxy_url = ""
|
||||
|
||||
@@ -14,19 +14,19 @@ from app.api.admin.manage import router as admin_router
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
logger.debug("[Web2API] 应用启动成功")
|
||||
logger.debug("[Grok2API] 应用启动成功")
|
||||
yield
|
||||
logger.info("[Web2API] 应用关闭成功")
|
||||
logger.info("[Grok2API] 应用关闭成功")
|
||||
|
||||
|
||||
# 初始化日志
|
||||
logger.info("[Web2API] 应用正在启动...")
|
||||
logger.info("[Grok2API] 应用正在启动...")
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="Web2API",
|
||||
description="Web服务API",
|
||||
version="1.0.0",
|
||||
title="Grok2API",
|
||||
description="Grok API 转换服务",
|
||||
version="1.0.3",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
@@ -44,8 +44,19 @@ app.mount("/static", StaticFiles(directory="app/template"), name="template")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径"""
|
||||
return {"message": "Welcome to Web2API"}
|
||||
"""根路径 - 重定向到登录页"""
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "Grok2API",
|
||||
"version": "1.0.3"
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user