feat: add super refresh interval configuration and update token management logic

This commit is contained in:
Chenyme committed 2026-02-04 23:34:58 +08:00
1 parent 63137d6d30
commit a2c2b1ba7f
14 files changed
+107 -28

No files matched your search

+5 -2
View File
@@ -131,8 +131,11 @@ async def create_image(request: ImageGenerationRequest):
try:
token_mgr = await get_token_manager()
await token_mgr.reload_if_stale()
pool_name = ModelService.pool_for_model(request.model)
token = token_mgr.get_token(pool_name)
token = None
for pool_name in ModelService.pool_candidates_for_model(request.model):
token = token_mgr.get_token(pool_name)
if token:
break
except Exception as e:
logger.error(f"Failed to get token: {e}")
raise AppException(
+5 -2
View File
@@ -520,8 +520,11 @@ class ChatService:
try:
token_mgr = await get_token_manager()
await token_mgr.reload_if_stale()
pool_name = ModelService.pool_for_model(model)
token = token_mgr.get_token(pool_name)
token = None
for pool_name in ModelService.pool_candidates_for_model(model):
token = token_mgr.get_token(pool_name)
if token:
break
except Exception as e:
logger.error(f"Failed to get token: {e}")
raise AppException(
+5 -2
View File
@@ -441,8 +441,11 @@ class VideoService:
try:
token_mgr = await get_token_manager()
await token_mgr.reload_if_stale()
pool_name = ModelService.pool_for_model(model)
token = token_mgr.get_token(pool_name)
token = None
for pool_name in ModelService.pool_candidates_for_model(model):
token = token_mgr.get_token(pool_name)
if token:
break
except Exception as e:
logger.error(f"Failed to get token: {e}")
raise AppException(
+10 -1
View File
@@ -3,7 +3,7 @@ Grok 模型管理服务
"""
from enum import Enum
from typing import Optional, Tuple
from typing import Optional, Tuple, List
from pydantic import BaseModel, Field
from app.core.exceptions import ValidationException
@@ -158,5 +158,14 @@ class ModelService:
return "ssoSuper"
return "ssoBasic"
@classmethod
def pool_candidates_for_model(cls, model_id: str) -> List[str]:
"""按优先级返回可用 Token 池列表"""
model = cls.get(model_id)
if model and model.tier == Tier.SUPER:
return ["ssoSuper"]
# 基础模型优先使用 basic 池,缺失时可回退到 super 池
return ["ssoBasic", "ssoSuper"]
__all__ = ["ModelService"]
+4 -2
View File
@@ -5,7 +5,8 @@ from app.services.token.models import (
TokenStatus,
TokenPoolStats,
EffortType,
DEFAULT_QUOTA,
BASIC__DEFAULT_QUOTA,
SUPER_DEFAULT_QUOTA,
EFFORT_COST,
)
from app.services.token.pool import TokenPool
@@ -19,7 +20,8 @@ __all__ = [
"TokenStatus",
"TokenPoolStats",
"EffortType",
"DEFAULT_QUOTA",
"BASIC__DEFAULT_QUOTA",
"SUPER_DEFAULT_QUOTA",
"EFFORT_COST",
# Core
"TokenPool",
+36 -7
View File
@@ -6,16 +6,37 @@ from datetime import datetime
from typing import Dict, List, Optional
from app.core.logger import logger
from app.services.token.models import TokenInfo, EffortType, FAIL_THRESHOLD, TokenStatus
from app.services.token.models import (
TokenInfo,
EffortType,
FAIL_THRESHOLD,
TokenStatus,
BASIC__DEFAULT_QUOTA,
SUPER_DEFAULT_QUOTA,
)
from app.core.storage import get_storage
from app.core.config import get_config
from app.services.token.pool import TokenPool
# 批量刷新配置
REFRESH_INTERVAL_HOURS = 8
REFRESH_BATCH_SIZE = 10
REFRESH_CONCURRENCY = 5
SUPER_POOL_NAME = "ssoSuper"
BASIC_POOL_NAME = "ssoBasic"
def _default_quota_for_pool(pool_name: str) -> int:
if pool_name == SUPER_POOL_NAME:
return SUPER_DEFAULT_QUOTA
return BASIC__DEFAULT_QUOTA
def _refresh_interval_hours_for_pool(pool_name: str) -> float:
if pool_name == SUPER_POOL_NAME:
return get_config("token.super_refresh_interval_hours", 2)
return get_config("token.refresh_interval_hours", 8)
class TokenManager:
"""管理 Token 的增删改查和配额同步"""
@@ -68,6 +89,9 @@ class TokenManager:
for pool_name, tokens in data.items():
pool = TokenPool(pool_name)
for token_data in tokens:
quota_missing = not (
isinstance(token_data, dict) and "quota" in token_data
)
try:
# 统一存储裸 token
if isinstance(token_data, dict):
@@ -77,6 +101,8 @@ class TokenManager:
):
token_data["token"] = raw_token[4:]
token_info = TokenInfo(**token_data)
if quota_missing and pool_name == SUPER_POOL_NAME:
token_info.quota = SUPER_DEFAULT_QUOTA
pool.add(token_info)
except Exception as e:
logger.warning(
@@ -347,7 +373,7 @@ class TokenManager:
logger.warning(f"Pool '{pool_name}': token already exists")
return False
pool.add(TokenInfo(token=token))
pool.add(TokenInfo(token=token, quota=_default_quota_for_pool(pool_name)))
await self._save()
logger.info(f"Pool '{pool_name}': token added")
return True
@@ -429,9 +455,10 @@ class TokenManager:
async def reset_all(self):
"""重置所有 Token 配额"""
count = 0
for pool in self.pools.values():
for pool_name, pool in self.pools.items():
default_quota = _default_quota_for_pool(pool_name)
for token in pool:
token.reset()
token.reset(default_quota)
count += 1
await self._save()
@@ -452,7 +479,8 @@ class TokenManager:
for pool in self.pools.values():
token = pool.get(raw_token)
if token:
token.reset()
default_quota = _default_quota_for_pool(pool.name)
token.reset(default_quota)
await self._save()
logger.info(f"Token {raw_token[:10]}...: reset completed")
return True
@@ -495,8 +523,9 @@ class TokenManager:
# 收集需要刷新的 token
to_refresh: List[TokenInfo] = []
for pool in self.pools.values():
interval_hours = _refresh_interval_hours_for_pool(pool.name)
for token in pool:
if token.need_refresh(REFRESH_INTERVAL_HOURS):
if token.need_refresh(interval_hours):
to_refresh.append(token)
if not to_refresh:
+11 -7
View File
@@ -2,8 +2,9 @@
Token 数据模型
额度规则:
- 新号默认 80 配额
- 重置后恢复 80
- Basic 新号默认 80 配额
- Super 新号默认 140 配额
- 重置后恢复默认值
- lowEffort 扣 1,highEffort 扣 4
"""
@@ -14,7 +15,8 @@ from datetime import datetime
# 默认配额
DEFAULT_QUOTA = 80
BASIC__DEFAULT_QUOTA = 80
SUPER_DEFAULT_QUOTA = 140
# 失败阈值
FAIL_THRESHOLD = 5
@@ -47,7 +49,7 @@ class TokenInfo(BaseModel):
token: str
status: TokenStatus = TokenStatus.ACTIVE
quota: int = DEFAULT_QUOTA
quota: int = BASIC__DEFAULT_QUOTA
# 统计
created_at: int = Field(
@@ -118,9 +120,10 @@ class TokenInfo(BaseModel):
]:
self.status = TokenStatus.ACTIVE
def reset(self):
def reset(self, default_quota: Optional[int] = None):
"""重置配额到默认值"""
self.quota = DEFAULT_QUOTA
quota = BASIC__DEFAULT_QUOTA if default_quota is None else default_quota
self.quota = max(0, int(quota))
self.status = TokenStatus.ACTIVE
self.fail_count = 0
self.last_fail_reason = None
@@ -188,6 +191,7 @@ __all__ = [
"TokenPoolStats",
"EffortType",
"EFFORT_COST",
"DEFAULT_QUOTA",
"BASIC__DEFAULT_QUOTA",
"SUPER_DEFAULT_QUOTA",
"FAIL_THRESHOLD",
]
+2
View File
@@ -9,6 +9,7 @@ const NUMERIC_FIELDS = new Set([
'retry_backoff_max',
'retry_budget',
'refresh_interval_hours',
'super_refresh_interval_hours',
'fail_threshold',
'limit_mb',
'save_delay_ms',
@@ -61,6 +62,7 @@ const LOCALE_MAP = {
"label": "Token 池设置",
"auto_refresh": { title: "自动刷新", desc: "是否开启 Token 自动刷新机制。" },
"refresh_interval_hours": { title: "刷新间隔", desc: "Token 刷新的时间间隔(小时)。" },
"super_refresh_interval_hours": { title: "Super 刷新间隔", desc: "Super Token 刷新的时间间隔(小时)。" },
"fail_threshold": { title: "失败阈值", desc: "单个 Token 连续失败多少次后被标记为不可用。" },
"save_delay_ms": { title: "保存延迟", desc: "Token 变更合并写入的延迟(毫秒)。" },
"reload_interval_sec": { title: "一致性刷新", desc: "多 worker 场景下 Token 状态刷新间隔(秒)。" }
+20 -2
View File
@@ -15,6 +15,12 @@ let pageSize = 50;
const byId = (id) => document.getElementById(id);
const qsa = (selector) => document.querySelectorAll(selector);
const DEFAULT_QUOTA_BASIC = 80;
const DEFAULT_QUOTA_SUPER = 140;
function getDefaultQuotaForPool(pool) {
return pool === 'ssoSuper' ? DEFAULT_QUOTA_SUPER : DEFAULT_QUOTA_BASIC;
}
function setText(id, text) {
const el = byId(id);
@@ -93,6 +99,7 @@ function getPaginationData() {
async function init() {
apiKey = await ensureApiKey();
if (apiKey === null) return;
setupEditPoolDefaults();
setupConfirmDialog();
loadData();
}
@@ -405,7 +412,7 @@ function openEditModal(index) {
byId('edit-original-token').value = '';
byId('edit-original-pool').value = '';
byId('edit-pool').value = 'ssoBasic';
byId('edit-quota').value = 80;
byId('edit-quota').value = getDefaultQuotaForPool('ssoBasic');
byId('edit-note').value = '';
document.querySelector('#edit-modal h3').innerText = '添加 Token';
}
@@ -413,6 +420,16 @@ function openEditModal(index) {
openModal('edit-modal');
}
function setupEditPoolDefaults() {
const poolSelect = byId('edit-pool');
const quotaInput = byId('edit-quota');
if (!poolSelect || !quotaInput) return;
poolSelect.addEventListener('change', () => {
if (currentEditIndex >= 0) return;
quotaInput.value = getDefaultQuotaForPool(poolSelect.value);
});
}
function closeEditModal() {
closeModal('edit-modal', () => {
// reset styles for token input
@@ -525,6 +542,7 @@ async function submitImport() {
const pool = byId('import-pool').value.trim() || 'ssoBasic';
const text = byId('import-text').value;
const lines = text.split('\n');
const defaultQuota = getDefaultQuotaForPool(pool);
lines.forEach(line => {
const t = line.trim();
@@ -533,7 +551,7 @@ async function submitImport() {
token: t,
pool: pool,
status: 'active',
quota: 80,
quota: defaultQuota,
note: '',
use_count: 0,
_selected: false
+1
View File
@@ -29,6 +29,7 @@ video_format = "html"
[token]
auto_refresh = true
refresh_interval_hours = 8
super_refresh_interval_hours = 2
fail_threshold = 5
save_delay_ms = 500
reload_interval_sec = 30
+1
View File
@@ -27,6 +27,7 @@ video_format = "html"
[token]
auto_refresh = true
refresh_interval_hours = 8
super_refresh_interval_hours = 2
fail_threshold = 5
save_delay_ms = 500
reload_interval_sec = 30
+2 -1
View File
@@ -52,7 +52,7 @@ Default password: `grok2api` (config key `app.app_key`, change it in production)
### Usage limits
- Basic account: 80 requests / 20h
- Super account: not tested by the author
- Super account: 140 requests / 2h
### Models
@@ -177,6 +177,7 @@ Config file: `data/config.toml`
| | `video_idle_timeout` | Video idle timeout | Idle timeout for video generation (seconds). | `90.0` |
| **token** | `auto_refresh` | Auto refresh | Enable automatic token refresh. | `true` |
| | `refresh_interval_hours` | Refresh interval | Token refresh interval (hours). | `8` |
| | `super_refresh_interval_hours` | Super refresh interval | Super token refresh interval (hours). | `2` |
| | `fail_threshold` | Failure threshold | Consecutive failures before a token is disabled. | `5` |
| | `save_delay_ms` | Save delay | Debounced save delay for token changes (ms). | `500` |
| | `reload_interval_sec` | Consistency refresh | Token state refresh interval in multi-worker setups (sec). | `30` |
+3 -1
View File
@@ -54,7 +54,9 @@ async def lifespan(app: FastAPI):
# 3. 启动 Token 刷新调度器
refresh_enabled = get_config("token.auto_refresh", True)
if refresh_enabled:
interval = get_config("token.refresh_interval_hours", 8)
basic_interval = get_config("token.refresh_interval_hours", 8)
super_interval = get_config("token.super_refresh_interval_hours", 2)
interval = min(basic_interval, super_interval)
scheduler = get_scheduler(interval)
scheduler.start()
+2 -1
View File
@@ -64,7 +64,7 @@ docker compose up -d
### 可用次数
- Basic 账号:80 次 / 20h
- Super 账号:无账号,作者未测试
- Super 账号:140 次 / 2h
### 可用模型
@@ -210,6 +210,7 @@ curl http://localhost:8000/v1/images/generations \
| | `video_idle_timeout` | 视频空闲超时 | 视频生成空闲超时(秒),超过将断开。 | `90.0` |
| **token** | `auto_refresh` | 自动刷新 | 是否开启 Token 自动刷新机制。 | `true` |
| | `refresh_interval_hours` | 刷新间隔 | Token 刷新的时间间隔(小时)。 | `8` |
| | `super_refresh_interval_hours` | Super 刷新间隔 | Super Token 刷新的时间间隔(小时)。 | `2` |
| | `fail_threshold` | 失败阈值 | 单个 Token 连续失败多少次后被标记为不可用。 | `5` |
| | `save_delay_ms` | 保存延迟 | Token 变更合并写入的延迟(毫秒)。 | `500` |
| | `reload_interval_sec` | 一致性刷新 | 多 worker 场景下 Token 状态刷新间隔(秒)。 | `30` |