feat: support the mysql

This commit is contained in:
Chenyme committed 2025-10-15 14:50:56 +08:00
1 parent 7c13fda11c
commit f612156e8e
10 files changed
+516 -47

No files matched your search

+33 -20
View File
@@ -439,28 +439,13 @@ class StreamTimeoutSettings(BaseModel):
async def update_settings(request: UpdateSettingsRequest, _: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""更新全局配置"""
try:
import toml
import aiofiles
logger.debug("[Admin] 更新全局配置")
# 异步读取现有配置
async with aiofiles.open(setting.config_path, "r", encoding="utf-8") as f:
content = await f.read()
config = toml.loads(content)
# 更新配置
if request.global_config:
config["global"].update(request.global_config)
if request.grok_config:
config["grok"].update(request.grok_config)
# 异步写回配置文件
async with aiofiles.open(setting.config_path, "w", encoding="utf-8") as f:
await f.write(toml.dumps(config))
# 重新加载配置
setting.global_config = setting.load("global")
setting.grok_config = setting.load("grok")
# 使用ConfigManager的save方法(支持存储抽象层)
await setting.save(
global_config=request.global_config,
grok_config=request.grok_config
)
logger.debug("[Admin] 配置更新成功")
return {"success": True, "message": "配置更新成功"}
@@ -706,3 +691,31 @@ async def get_stats(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
status_code=500,
detail={"error": f"获取统计信息失败: {str(e)}", "code": "STATS_ERROR"}
)
@router.get("/api/storage/mode")
async def get_storage_mode(_: bool = Depends(verify_admin_session)) -> Dict[str, Any]:
"""
获取当前存储模式
返回当前的存储模式(file/mysql)。
"""
try:
logger.debug("[Admin] 获取存储模式")
import os
storage_mode = os.getenv("STORAGE_MODE", "file").upper()
return {
"success": True,
"data": {
"mode": storage_mode
}
}
except Exception as e:
logger.error(f"[Admin] 获取存储模式异常 - 错误: {str(e)}")
raise HTTPException(
status_code=500,
detail={"error": f"获取存储模式失败: {str(e)}", "code": "STORAGE_MODE_ERROR"}
)
+40
View File
@@ -15,6 +15,11 @@ class ConfigManager:
self.config_path: Path = Path(__file__).parents[2] / "data" / "setting.toml"
self.global_config: Dict[str, Any] = self.load("global")
self.grok_config: Dict[str, Any] = self.load("grok")
self._storage = None
def set_storage(self, storage) -> None:
"""设置存储实例"""
self._storage = storage
def load(self, section: str) -> Dict[str, Any]:
"""配置加载器"""
@@ -23,6 +28,41 @@ class ConfigManager:
return toml.load(f)[section]
except Exception as e:
raise Exception(f"[Setting] 配置加载失败: {e}")
async def reload(self) -> None:
"""重新加载配置(用于从存储同步后)"""
self.global_config = self.load("global")
self.grok_config = self.load("grok")
async def save(self, global_config: Dict[str, Any] = None, grok_config: Dict[str, Any] = None) -> None:
"""保存配置到存储"""
if not self._storage:
# 如果没有设置存储,使用传统文件保存方式(向后兼容)
import aiofiles
async with aiofiles.open(self.config_path, "r", encoding="utf-8") as f:
content = await f.read()
config = toml.loads(content)
if global_config:
config["global"].update(global_config)
if grok_config:
config["grok"].update(grok_config)
async with aiofiles.open(self.config_path, "w", encoding="utf-8") as f:
await f.write(toml.dumps(config))
else:
# 使用存储抽象层
config_data = await self._storage.load_config()
if global_config:
config_data["global"].update(global_config)
if grok_config:
config_data["grok"].update(grok_config)
await self._storage.save_config(config_data)
# 重新加载配置
await self.reload()
# 全局设置
setting = ConfigManager()
+357
View File
@@ -0,0 +1,357 @@
"""存储抽象层 - 支持文件和MySQL两种存储模式"""
import os
import json
import toml
import asyncio
import aiofiles
from pathlib import Path
from typing import Dict, Any, Optional, Literal
from abc import ABC, abstractmethod
from app.core.logger import logger
StorageMode = Literal["file", "mysql"]
class BaseStorage(ABC):
"""存储基类"""
@abstractmethod
async def init_db(self) -> None:
"""初始化数据库"""
pass
@abstractmethod
async def load_tokens(self) -> Dict[str, Any]:
"""加载token数据"""
pass
@abstractmethod
async def save_tokens(self, data: Dict[str, Any]) -> None:
"""保存token数据"""
pass
@abstractmethod
async def load_config(self) -> Dict[str, Any]:
"""加载配置数据"""
pass
@abstractmethod
async def save_config(self, data: Dict[str, Any]) -> None:
"""保存配置数据"""
pass
class FileStorage(BaseStorage):
"""文件存储实现"""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.token_file = data_dir / "token.json"
self.config_file = data_dir / "setting.toml"
self._token_lock = asyncio.Lock()
self._config_lock = asyncio.Lock()
async def init_db(self) -> None:
"""初始化文件存储"""
self.data_dir.mkdir(parents=True, exist_ok=True)
# 初始化token文件
if not self.token_file.exists():
default_tokens = {"sso": {}, "ssoSuper": {}}
async with aiofiles.open(self.token_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(default_tokens, indent=2, ensure_ascii=False))
logger.info("[Storage] 创建新的token文件")
# 初始化配置文件
if not self.config_file.exists():
default_config = {
"global": {
"api_keys": [],
"admin_username": "admin",
"admin_password": "admin"
},
"grok": {
"proxy_url": "",
"cf_clearance": "",
"x_statsig_id": ""
}
}
async with aiofiles.open(self.config_file, "w", encoding="utf-8") as f:
await f.write(toml.dumps(default_config))
logger.info("[Storage] 创建新的配置文件")
async def load_tokens(self) -> Dict[str, Any]:
"""从文件加载token数据"""
try:
async with self._token_lock:
if not self.token_file.exists():
return {"sso": {}, "ssoSuper": {}}
async with aiofiles.open(self.token_file, "r", encoding="utf-8") as f:
content = await f.read()
return json.loads(content)
except Exception as e:
logger.error(f"[Storage] 加载token失败: {e}")
return {"sso": {}, "ssoSuper": {}}
async def save_tokens(self, data: Dict[str, Any]) -> None:
"""保存token数据到文件"""
try:
async with self._token_lock:
async with aiofiles.open(self.token_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(data, indent=2, ensure_ascii=False))
except Exception as e:
logger.error(f"[Storage] 保存token失败: {e}")
raise
async def load_config(self) -> Dict[str, Any]:
"""从文件加载配置数据"""
try:
async with self._config_lock:
if not self.config_file.exists():
return {"global": {}, "grok": {}}
async with aiofiles.open(self.config_file, "r", encoding="utf-8") as f:
content = await f.read()
return toml.loads(content)
except Exception as e:
logger.error(f"[Storage] 加载配置失败: {e}")
return {"global": {}, "grok": {}}
async def save_config(self, data: Dict[str, Any]) -> None:
"""保存配置数据到文件"""
try:
async with self._config_lock:
async with aiofiles.open(self.config_file, "w", encoding="utf-8") as f:
await f.write(toml.dumps(data))
except Exception as e:
logger.error(f"[Storage] 保存配置失败: {e}")
raise
class MysqlStorage(BaseStorage):
"""MySQL存储实现"""
def __init__(self, database_url: str, data_dir: Path):
self.database_url = database_url
self.data_dir = data_dir
self.token_file = data_dir / "token.json"
self.config_file = data_dir / "setting.toml"
self._pool = None
self._file_storage = FileStorage(data_dir)
async def init_db(self) -> None:
"""初始化MySQL数据库和连接池"""
try:
import aiomysql
from urllib.parse import urlparse, unquote
# 使用标准库解析URL
parsed = urlparse(self.database_url)
# 解码用户名和密码(处理URL编码)
user = unquote(parsed.username) if parsed.username else ""
password = unquote(parsed.password) if parsed.password else ""
host = parsed.hostname
port = parsed.port if parsed.port else 3306
database = parsed.path[1:] if parsed.path else "grok2api" # 去掉开头的 '/'
logger.info(f"[Storage] 解析数据库连接: {user}@{host}:{port}/{database}")
# 创建连接池
self._pool = await aiomysql.create_pool(
host=host,
port=port,
user=user,
password=password,
db=database,
charset="utf8mb4",
autocommit=True,
maxsize=10
)
logger.info(f"[Storage] MySQL连接池创建成功: {host}:{port}/{database}")
# 创建表
async with self._pool.acquire() as conn:
async with conn.cursor() as cursor:
# 创建tokens表
await cursor.execute("""
CREATE TABLE IF NOT EXISTS grok_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
data JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
# 创建settings表
await cursor.execute("""
CREATE TABLE IF NOT EXISTS grok_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
data JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info("[Storage] MySQL表创建/验证成功")
# 确保文件存储目录存在
await self._file_storage.init_db()
# 从数据库同步数据到文件
await self._sync_from_db()
except ImportError:
logger.error("[Storage] aiomysql未安装,请运行: pip install aiomysql")
raise Exception("aiomysql未安装")
except Exception as e:
logger.error(f"[Storage] MySQL初始化失败: {e}")
raise
async def _sync_from_db(self) -> None:
"""从数据库同步数据到文件"""
try:
# 同步tokens
tokens = await self._load_from_db("grok_tokens")
if tokens:
await self._file_storage.save_tokens(tokens)
logger.info("[Storage] Token数据已从数据库同步到文件")
# 同步settings
settings = await self._load_from_db("grok_settings")
if settings:
await self._file_storage.save_config(settings)
logger.info("[Storage] 配置数据已从数据库同步到文件")
except Exception as e:
logger.warning(f"[Storage] 从数据库同步数据失败: {e}")
async def _load_from_db(self, table: str) -> Optional[Dict[str, Any]]:
"""从数据库表加载最新数据"""
try:
async with self._pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(f"SELECT data FROM {table} ORDER BY id DESC LIMIT 1")
result = await cursor.fetchone()
if result:
return json.loads(result[0])
return None
except Exception as e:
logger.error(f"[Storage] 从数据库加载{table}失败: {e}")
return None
async def _save_to_db(self, table: str, data: Dict[str, Any]) -> None:
"""保存数据到数据库表"""
try:
async with self._pool.acquire() as conn:
async with conn.cursor() as cursor:
json_data = json.dumps(data, ensure_ascii=False)
# 检查是否已有数据
await cursor.execute(f"SELECT id FROM {table} ORDER BY id DESC LIMIT 1")
result = await cursor.fetchone()
if result:
# 更新现有数据
await cursor.execute(
f"UPDATE {table} SET data = %s WHERE id = %s",
(json_data, result[0])
)
else:
# 插入新数据
await cursor.execute(
f"INSERT INTO {table} (data) VALUES (%s)",
(json_data,)
)
except Exception as e:
logger.error(f"[Storage] 保存数据到{table}失败: {e}")
raise
async def load_tokens(self) -> Dict[str, Any]:
"""从文件加载token数据(已从数据库同步)"""
return await self._file_storage.load_tokens()
async def save_tokens(self, data: Dict[str, Any]) -> None:
"""保存token数据到文件和数据库"""
# 先保存到文件
await self._file_storage.save_tokens(data)
# 再保存到数据库
await self._save_to_db("grok_tokens", data)
async def load_config(self) -> Dict[str, Any]:
"""从文件加载配置数据(已从数据库同步)"""
return await self._file_storage.load_config()
async def save_config(self, data: Dict[str, Any]) -> None:
"""保存配置数据到文件和数据库"""
# 先保存到文件
await self._file_storage.save_config(data)
# 再保存到数据库
await self._save_to_db("grok_settings", data)
async def close(self) -> None:
"""关闭数据库连接池"""
if self._pool:
self._pool.close()
await self._pool.wait_closed()
logger.info("[Storage] MySQL连接池已关闭")
class StorageManager:
"""存储管理器 - 单例模式"""
_instance: Optional['StorageManager'] = None
_storage: Optional[BaseStorage] = None
_initialized: bool = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
async def init(self) -> None:
"""初始化存储管理器"""
if self._initialized:
return
# 读取环境变量
storage_mode = os.getenv("STORAGE_MODE", "file").lower()
database_url = os.getenv("DATABASE_URL", "")
data_dir = Path(__file__).parents[2] / "data"
if storage_mode == "mysql":
if not database_url:
logger.error("[Storage] MySQL模式需要DATABASE_URL环境变量")
raise ValueError("MySQL模式需要DATABASE_URL环境变量")
logger.info(f"[Storage] 使用MySQL存储模式")
self._storage = MysqlStorage(database_url, data_dir)
else:
logger.info(f"[Storage] 使用文件存储模式")
self._storage = FileStorage(data_dir)
await self._storage.init_db()
self._initialized = True
logger.info("[Storage] 存储管理器初始化完成")
def get_storage(self) -> BaseStorage:
"""获取存储实例"""
if not self._initialized or not self._storage:
raise RuntimeError("StorageManager未初始化,请先调用init()")
return self._storage
async def close(self) -> None:
"""关闭存储"""
if self._storage and isinstance(self._storage, MysqlStorage):
await self._storage.close()
# 全局存储管理器实例
storage_manager = StorageManager()
+14 -4
View File
@@ -51,6 +51,7 @@ class GrokTokenManager:
self.token_file = Path(__file__).parents[3] / "data" / "token.json"
self._file_lock = asyncio.Lock()
self.token_file.parent.mkdir(parents=True, exist_ok=True)
self._storage = None
# 同步加载初始数据
self._load_data()
@@ -58,6 +59,10 @@ class GrokTokenManager:
logger.debug(f"[Token] 管理器初始化完成,文件: {self.token_file}")
def set_storage(self, storage) -> None:
"""设置存储实例"""
self._storage = storage
def _load_data(self) -> None:
"""同步加载Token数据(仅用于初始化)"""
default_data = {
@@ -77,11 +82,16 @@ class GrokTokenManager:
self.token_data = default_data
async def _save_data(self) -> None:
"""异步保存Token数据到文件"""
"""异步保存Token数据到存储"""
try:
async with self._file_lock:
async with aiofiles.open(self.token_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(self.token_data, indent=2, ensure_ascii=False))
if not self._storage:
# 如果没有设置存储,使用传统文件保存方式(向后兼容)
async with self._file_lock:
async with aiofiles.open(self.token_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(self.token_data, indent=2, ensure_ascii=False))
else:
# 使用存储抽象层
await self._storage.save_tokens(self.token_data)
except IOError as e:
logger.error(f"[Token] 保存Token数据失败: {str(e)}")
raise GrokApiException(
+29 -3
View File
@@ -10,6 +10,16 @@
@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}
.hover-card{position:relative;display:inline-block}
.hover-card-trigger{cursor:pointer}
.hover-card-content{position:absolute;left:50%;transform:translateX(-50%);background:hsl(0 0% 3.9%);color:hsl(0 0% 98%);padding:8px 12px;border-radius:6px;font-size:12px;font-weight:500;white-space:nowrap;z-index:9999;pointer-events:none;opacity:0;visibility:hidden;transition:opacity .2s ease,transform .2s ease;box-shadow:0 4px 12px rgba(0,0,0,.25);border:1px solid hsl(0 0% 14.9%)}
.hover-card:hover .hover-card-content{opacity:1;visibility:visible}
.hover-card-content.top{bottom:100%;transform:translateX(-50%) translateY(-8px)}
.hover-card-content.bottom{top:100%;transform:translateX(-50%) translateY(8px)}
.hover-card-content::after{content:'';position:absolute;left:50%;transform:translateX(-50%);border:6px solid transparent;z-index:1000}
.hover-card-content.top::after{top:100%;border-top-color:hsl(0 0% 3.9%)}
.hover-card-content.bottom::after{bottom:100%;border-bottom-color:hsl(0 0% 3.9%)}
.hover-card-trigger:hover+.hover-card-content{opacity:1;visibility:visible}
[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}
@@ -26,8 +36,22 @@
<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 gap-1">
<div class="flex flex-1 items-center justify-end">
<div class="flex items-center gap-2">
<div class="hover-card">
<span id="storageMode" class="hover-card-trigger inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground border">
<svg class="h-3 w-3 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="m21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
<path d="m3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
</svg>
<span id="storageModeText">FILE</span>
</span>
<div id="storageModeTooltip" class="hover-card-content top">
加载中...
</div>
</div>
<nav class="flex items-center gap-1">
<a
href="https://github.com/chenyme/grok2api/issues"
target="_blank"
@@ -498,7 +522,9 @@
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)});
const updateHoverCardPosition=(cardElement)=>{const trigger=cardElement.querySelector('.hover-card-trigger'),content=cardElement.querySelector('.hover-card-content');if(!trigger||!content)return;console.log('更新悬浮卡片位置',cardElement,trigger,content);const rect=trigger.getBoundingClientRect(),windowHeight=window.innerHeight,spaceAbove=rect.top,spaceBelow=windowHeight-rect.bottom;content.classList.remove('top','bottom');const originalVisibility=getComputedStyle(content).visibility,originalOpacity=getComputedStyle(content).opacity;content.style.visibility='hidden';content.style.opacity='1';const contentHeight=content.offsetHeight;content.style.visibility=originalVisibility;content.style.opacity=originalOpacity;const position=spaceAbove>contentHeight+10?'top':spaceBelow>contentHeight+10?'bottom':'top';content.classList.add(position);console.log('悬浮卡片位置:',position,'高度:',contentHeight,'上方空间:',spaceAbove,'下方空间:',spaceBelow)},
loadStorageMode=async()=>{try{const r=await apiRequest('/api/storage/mode');if(!r)return;const d=await r.json();if(d.success){const mode=d.data.mode;console.log('存储模式:',mode);$('storageModeText').textContent=mode;if(mode==='MYSQL'){$('storageMode').classList.add('bg-blue-50','text-blue-700','border-blue-200');$('storageModeTooltip').textContent='数据库连接模式 - 数据持久化存储,修改配置时可能稍慢但更安全'}else{$('storageMode').classList.add('bg-green-50','text-green-700','border-green-200');$('storageModeTooltip').textContent='文件存储模式 - 本地文件存储,读写速度快'};updateHoverCardPosition($('storageMode').closest('.hover-card'))}}catch(e){console.error('加载存储模式失败:',e);$('storageModeText').textContent='FILE';$('storageMode').classList.add('bg-green-50','text-green-700','border-green-200');$('storageModeTooltip').textContent='文件存储模式 - 本地文件存储,读写速度快';updateHoverCardPosition($('storageMode').closest('.hover-card'))}};
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'})}});
</script>
</body>
</html>
+14 -15
View File
@@ -1,20 +1,19 @@
[global]
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 = ""
stream_chunk_timeout = 120
stream_first_response_timeout = 30
stream_total_timeout = 600
cf_clearance = ""
temporary = true
cf_clearance = ""
x_statsig_id = "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk="
filtered_tags = "xaiartifact,xai:tool_usage_card,grok:render"
x_statsig_id = "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk="
stream_chunk_timeout = 120
stream_total_timeout = 600
stream_first_response_timeout = 30
[global]
base_url = ""
log_level = "DEBUG"
image_mode = "url"
admin_password = "admin"
admin_username = "admin"
image_cache_max_size_mb = 500
video_cache_max_size_mb = 1000
+2 -2
View File
@@ -1,4 +1,4 @@
{
"ssoNormal": {},
"ssoSuper": {}
"ssoSuper": {},
"ssoNormal": {}
}
+6 -1
View File
@@ -6,6 +6,11 @@ services:
volumes:
- grok_data:/app/data
- ./logs:/app/logs
environment:
# 数据存储模式( file/mysql 二选一 )
- STORAGE_MODE=file
# MySQL数据库 URL (仅在STORAGE_MODE=mysql时需要 示例:mysql://user:password@host:port/database)
# - DATABASE_URL=mysql://
volumes:
grok_data:
grok_data:
+19 -1
View File
@@ -5,6 +5,9 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.core.logger import logger
from app.core.exception import register_exception_handlers
from app.core.storage import storage_manager
from app.core.config import setting
from app.services.grok.token import token_manager
from app.api.v1.chat import router as chat_router
from app.api.v1.models import router as models_router
from app.api.v1.images import router as images_router
@@ -14,8 +17,23 @@ from app.api.admin.manage import router as admin_router
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
logger.debug("[Grok2API] 应用启动成功")
# 初始化存储管理器
await storage_manager.init()
# 设置存储到配置和token管理器
storage = storage_manager.get_storage()
setting.set_storage(storage)
token_manager.set_storage(storage)
# 重新加载配置和token数据(从存储同步)
await setting.reload()
token_manager._load_data()
logger.info("[Grok2API] 应用启动成功")
yield
# 关闭存储连接
await storage_manager.close()
logger.info("[Grok2API] 应用关闭成功")
+2 -1
View File
@@ -6,4 +6,5 @@ curl_cffi==0.13.0
requests==2.32.5
starlette==0.48.0
pydantic==2.12.0
aiofiles==24.1.0
aiofiles==24.1.0
aiomysql==0.2.0