feat: add super refresh interval configuration and update token management logic
This commit is contained in:
1 parent
63137d6d30
commit
a2c2b1ba7f
14 files changed
+107
-28
No files matched your search
+5
-2
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 状态刷新间隔(秒)。" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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` |
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user