diff --git a/app/api/v1/image.py b/app/api/v1/image.py index 28f8ec9..7c08430 100644 --- a/app/api/v1/image.py +++ b/app/api/v1/image.py @@ -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( diff --git a/app/services/grok/chat.py b/app/services/grok/chat.py index d1141af..e2dda63 100644 --- a/app/services/grok/chat.py +++ b/app/services/grok/chat.py @@ -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( diff --git a/app/services/grok/media.py b/app/services/grok/media.py index e6c6e1f..70b1854 100644 --- a/app/services/grok/media.py +++ b/app/services/grok/media.py @@ -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( diff --git a/app/services/grok/model.py b/app/services/grok/model.py index 76a9664..ced2c9d 100644 --- a/app/services/grok/model.py +++ b/app/services/grok/model.py @@ -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"] diff --git a/app/services/token/__init__.py b/app/services/token/__init__.py index 945e8f1..ef0d6ea 100644 --- a/app/services/token/__init__.py +++ b/app/services/token/__init__.py @@ -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", diff --git a/app/services/token/manager.py b/app/services/token/manager.py index e266fe5..be51058 100644 --- a/app/services/token/manager.py +++ b/app/services/token/manager.py @@ -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: diff --git a/app/services/token/models.py b/app/services/token/models.py index 137f509..0701ab7 100644 --- a/app/services/token/models.py +++ b/app/services/token/models.py @@ -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", ] diff --git a/app/static/config/config.js b/app/static/config/config.js index bab540c..5e62dab 100644 --- a/app/static/config/config.js +++ b/app/static/config/config.js @@ -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 状态刷新间隔(秒)。" } diff --git a/app/static/token/token.js b/app/static/token/token.js index dda2152..1a63ff5 100644 --- a/app/static/token/token.js +++ b/app/static/token/token.js @@ -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 diff --git a/config.defaults.toml b/config.defaults.toml index d23d030..ac7224c 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -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 diff --git a/data/config.toml b/data/config.toml index ccaa534..b4a33a3 100644 --- a/data/config.toml +++ b/data/config.toml @@ -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 diff --git a/docs/README.en.md b/docs/README.en.md index 7dc41ac..f750aa6 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -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` | diff --git a/main.py b/main.py index 7d8d19d..bd81ff2 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/readme.md b/readme.md index ac0dfa9..b08b02c 100644 --- a/readme.md +++ b/readme.md @@ -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` |