Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 107 additions & 193 deletions app/api/v1/admin.py

Large diffs are not rendered by default.

261 changes: 89 additions & 172 deletions app/api/v1/image.py

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions app/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

from app.core.config import get_config

DEFAULT_API_KEY = ""
DEFAULT_APP_KEY = "grok2api"

# 定义 Bearer Scheme
security = HTTPBearer(
auto_error=False,
Expand All @@ -22,7 +25,7 @@ def get_admin_api_key() -> str:

为空时表示不启用后台接口认证。
"""
api_key = get_config("app.api_key", "")
api_key = get_config("app.api_key", DEFAULT_API_KEY)
return api_key or ""


Expand Down Expand Up @@ -63,7 +66,7 @@ async def verify_app_key(

app_key 必须配置,否则拒绝登录。
"""
app_key = get_config("app.app_key", "")
app_key = get_config("app.app_key", DEFAULT_APP_KEY)

if not app_key:
raise HTTPException(
Expand Down
106 changes: 103 additions & 3 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,78 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any
return result


def _migrate_deprecated_config(
config: Dict[str, Any], valid_sections: set
) -> tuple[Dict[str, Any], set]:
"""
迁移废弃的配置节到新配置结构
Returns:
(迁移后的配置, 废弃的配置节集合)
"""
# 配置映射规则:旧配置 -> 新配置
MIGRATION_MAP = {
# grok.* -> 对应的新配置节
"grok.temporary": "chat.temporary",
"grok.disable_memory": "chat.disable_memory",
"grok.stream": "chat.stream",
"grok.thinking": "chat.thinking",
"grok.dynamic_statsig": "chat.dynamic_statsig",
"grok.filter_tags": "chat.filter_tags",
"grok.timeout": "network.timeout",
"grok.base_proxy_url": "network.base_proxy_url",
"grok.asset_proxy_url": "network.asset_proxy_url",
"grok.cf_clearance": "security.cf_clearance",
"grok.browser": "security.browser",
"grok.user_agent": "security.user_agent",
"grok.max_retry": "retry.max_retry",
"grok.retry_status_codes": "retry.retry_status_codes",
"grok.retry_backoff_base": "retry.retry_backoff_base",
"grok.retry_backoff_factor": "retry.retry_backoff_factor",
"grok.retry_backoff_max": "retry.retry_backoff_max",
"grok.retry_budget": "retry.retry_budget",
"grok.stream_idle_timeout": "timeout.stream_idle_timeout",
"grok.video_idle_timeout": "timeout.video_idle_timeout",
"grok.image_ws": "image.image_ws",
"grok.image_ws_nsfw": "image.image_ws_nsfw",
"grok.image_ws_blocked_seconds": "image.image_ws_blocked_seconds",
"grok.image_ws_final_min_bytes": "image.image_ws_final_min_bytes",
"grok.image_ws_medium_min_bytes": "image.image_ws_medium_min_bytes",
}

deprecated_sections = set(config.keys()) - valid_sections
if not deprecated_sections:
return config, set()

result = {k: deepcopy(v) for k, v in config.items() if k in valid_sections}
migrated_count = 0

# 处理废弃配置节中的配置项
for old_section in deprecated_sections:
if old_section not in config or not isinstance(config[old_section], dict):
continue

for old_key, old_value in config[old_section].items():
# 查找映射规则
old_path = f"{old_section}.{old_key}"
new_path = MIGRATION_MAP.get(old_path)

if new_path:
new_section, new_key = new_path.split(".", 1)
# 确保新配置节存在
if new_section not in result:
result[new_section] = {}
# 迁移配置项(保留用户的自定义值)
result[new_section][new_key] = old_value
migrated_count += 1
logger.debug(f"Migrated config: {old_path} -> {new_path} = {old_value}")

if migrated_count > 0:
logger.info(f"Migrated {migrated_count} config items from deprecated sections")

return result, deprecated_sections


def _load_defaults() -> Dict[str, Any]:
"""加载默认配置文件"""
if not DEFAULT_CONFIG_FILE.exists():
Expand All @@ -53,12 +125,19 @@ class Config:
def __init__(self):
self._config = {}
self._defaults = {}
self._code_defaults = {}
self._defaults_loaded = False

def register_defaults(self, defaults: Dict[str, Any]):
"""注册代码中定义的默认值"""
self._code_defaults = _deep_merge(self._code_defaults, defaults)

def _ensure_defaults(self):
if self._defaults_loaded:
return
self._defaults = _load_defaults()
file_defaults = _load_defaults()
# 合并文件默认值和代码默认值(代码默认值优先级更低)
self._defaults = _deep_merge(self._code_defaults, file_defaults)
self._defaults_loaded = True

async def load(self):
Expand All @@ -84,17 +163,33 @@ async def load(self):
config_data = {}

config_data = config_data or {}

# 检查是否有废弃的配置节
valid_sections = set(self._defaults.keys())
config_data, deprecated_sections = _migrate_deprecated_config(
config_data, valid_sections
)
if deprecated_sections:
logger.info(
f"Cleaned deprecated config sections: {deprecated_sections}"
)

merged = _deep_merge(self._defaults, config_data)

# 自动回填缺失配置到存储
should_persist = (not from_remote) or (merged != config_data)
# 或迁移了配置后需要更新
should_persist = (
(not from_remote) or (merged != config_data) or deprecated_sections
)
if should_persist:
async with storage.acquire_lock("config_save", timeout=10):
await storage.save_config(merged)
if not from_remote:
logger.info(
f"Initialized remote storage ({storage.__class__.__name__}) with config baseline."
)
if deprecated_sections:
logger.info("Configuration automatically migrated and cleaned.")

self._config = merged
except Exception as e:
Expand Down Expand Up @@ -140,4 +235,9 @@ def get_config(key: str, default: Any = None) -> Any:
return config.get(key, default)


__all__ = ["Config", "config", "get_config"]
def register_defaults(defaults: Dict[str, Any]):
"""注册默认配置"""
config.register_defaults(defaults)


__all__ = ["Config", "config", "get_config", "register_defaults"]
86 changes: 86 additions & 0 deletions app/services/grok/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Grok 服务默认配置

此文件定义所有 Grok 相关服务的默认值,会在应用启动时注册到配置系统中。
"""

# Grok 服务默认配置
GROK_DEFAULTS = {
"app": {
"app_url": "",
"app_key": "grok2api",
"api_key": "",
"image_format": "url",
"video_format": "html",
},
"network": {
"timeout": 120,
"base_proxy_url": "",
"asset_proxy_url": "",
},
"security": {
"cf_clearance": "",
"browser": "chrome136",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
},
"chat": {
"temporary": True,
"disable_memory": True,
"stream": True,
"thinking": False,
"dynamic_statsig": True,
"filter_tags": ["grok:render", "xaiartifact", "xai:tool_usage_card"],
},
"retry": {
"max_retry": 3,
"retry_status_codes": [401, 429, 403],
"retry_backoff_base": 0.5,
"retry_backoff_factor": 2.0,
"retry_backoff_max": 30.0,
"retry_budget": 90.0,
},
"timeout": {
"stream_idle_timeout": 45.0,
"video_idle_timeout": 90.0,
},
"image": {
"image_ws": True,
"image_ws_nsfw": True,
"image_ws_blocked_seconds": 15,
"image_ws_final_min_bytes": 100000,
"image_ws_medium_min_bytes": 30000,
},
"token": {
"auto_refresh": True,
"refresh_interval_hours": 8,
"super_refresh_interval_hours": 2,
"fail_threshold": 5,
"save_delay_ms": 500,
"reload_interval_sec": 30,
},
"cache": {
"enable_auto_clean": True,
"limit_mb": 1024,
},
"performance": {
"assets_max_concurrent": 25,
"assets_delete_batch_size": 10,
"assets_batch_size": 10,
"assets_max_tokens": 1000,
"media_max_concurrent": 50,
"usage_max_concurrent": 25,
"usage_batch_size": 50,
"usage_max_tokens": 1000,
"nsfw_max_concurrent": 10,
"nsfw_batch_size": 50,
"nsfw_max_tokens": 1000,
},
}


def get_grok_defaults():
"""获取 Grok 默认配置"""
return GROK_DEFAULTS


__all__ = ["GROK_DEFAULTS", "get_grok_defaults"]
1 change: 0 additions & 1 deletion app/services/grok/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class ModelService:
cost=Cost.HIGH,
display_name="GROK-4.1-EXPERT",
),

ModelInfo(
model_id="grok-4.1-thinking",
grok_model="grok-4-1-thinking-1129",
Expand Down
22 changes: 22 additions & 0 deletions app/services/grok/processors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
OpenAI 响应格式处理器
"""

from .base import BaseProcessor, StreamIdleTimeoutError
from .chat_processors import StreamProcessor, CollectProcessor
from .video_processors import VideoStreamProcessor, VideoCollectProcessor
from .image_processors import ImageStreamProcessor, ImageCollectProcessor
from .image_ws_processors import ImageWSStreamProcessor, ImageWSCollectProcessor

__all__ = [
"BaseProcessor",
"StreamIdleTimeoutError",
"StreamProcessor",
"CollectProcessor",
"VideoStreamProcessor",
"VideoCollectProcessor",
"ImageStreamProcessor",
"ImageCollectProcessor",
"ImageWSStreamProcessor",
"ImageWSCollectProcessor",
]
Loading
Loading