From 94582265c1b91d582305030439e2f3c8de33fcfe Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:16:50 +0800 Subject: [PATCH] feat: optimize initialization services and configurations --- .gitignore | 1 + Dockerfile | 1 + app/core/config.py | 82 +++++++++++++++--- config.defaults.toml | 37 +++++++++ docker-compose.yml | 13 ++- README.en.md => docs/README.en.md | 11 +++ env.example | 19 ----- readme.md | 133 +++++++++++++++++------------- scripts/init_storage.sh | 41 +-------- 9 files changed, 208 insertions(+), 130 deletions(-) create mode 100644 config.defaults.toml rename README.en.md => docs/README.en.md (92%) delete mode 100644 env.example diff --git a/.gitignore b/.gitignore index 874c029..c711d97 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ logs/ # Data data/*.json data/tmp/ +data/.locks/ # Testing .pytest_cache/ diff --git a/Dockerfile b/Dockerfile index da179a2..ba133ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ COPY pyproject.toml uv.lock /app/ RUN uv sync --frozen --no-dev --no-install-project --active +COPY config.defaults.toml /app/config.defaults.toml COPY app /app/app COPY main.py /app/main.py COPY scripts /app/scripts diff --git a/app/core/config.py b/app/core/config.py index d248c69..401d512 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -2,10 +2,46 @@ 配置管理 - config.toml: 运行时配置 +- config.defaults.toml: 默认配置基线 """ -from typing import Any -from app.core.storage import get_storage +from copy import deepcopy +from pathlib import Path +from typing import Any, Dict +import tomllib + +from app.core.logger import logger + +DEFAULT_CONFIG_FILE = Path(__file__).parent.parent.parent / "config.defaults.toml" + + +def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: + """深度合并字典: override 覆盖 base.""" + if not isinstance(base, dict): + return deepcopy(override) if isinstance(override, dict) else deepcopy(base) + + result = deepcopy(base) + if not isinstance(override, dict): + return result + + for key, val in override.items(): + if isinstance(val, dict) and isinstance(result.get(key), dict): + result[key] = _deep_merge(result[key], val) + else: + result[key] = val + return result + + +def _load_defaults() -> Dict[str, Any]: + """加载默认配置文件""" + if not DEFAULT_CONFIG_FILE.exists(): + return {} + try: + with DEFAULT_CONFIG_FILE.open("rb") as f: + return tomllib.load(f) + except Exception as e: + logger.warning(f"Failed to load defaults from {DEFAULT_CONFIG_FILE}: {e}") + return {} class Config: @@ -16,32 +52,53 @@ class Config: def __init__(self): self._config = {} + self._defaults = {} + self._defaults_loaded = False + + def _ensure_defaults(self): + if self._defaults_loaded: + return + self._defaults = _load_defaults() + self._defaults_loaded = True async def load(self): """显式加载配置""" try: from app.core.storage import get_storage, LocalStorage - import asyncio + self._ensure_defaults() + storage = get_storage() config_data = await storage.load_config() + from_remote = True # 从本地 data/config.toml 初始化后端 if config_data is None: local_storage = LocalStorage() + from_remote = False try: # 尝试读取本地配置 config_data = await local_storage.load_config() - # 初始化后端 - await storage.save_config(config_data) - logger.info(f"Initialized remote storage ({storage.__class__.__name__}) with local config.") except Exception as e: logger.info(f"Failed to auto-init config from local: {e}") config_data = {} - - self._config = config_data or {} + + config_data = config_data or {} + merged = _deep_merge(self._defaults, config_data) + + # 自动回填缺失配置到存储 + should_persist = (not from_remote) or (merged != config_data) + 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." + ) + + self._config = merged except Exception as e: - print(f"Error loading config: {e}") + logger.error(f"Error loading config: {e}") self._config = {} def get(self, key: str, default: Any = None) -> Any: @@ -66,8 +123,11 @@ class Config: from app.core.storage import get_storage storage = get_storage() async with storage.acquire_lock("config_save", timeout=10): - await storage.save_config(new_config) - await self.load() + self._ensure_defaults() + base = _deep_merge(self._defaults, self._config or {}) + merged = _deep_merge(base, new_config or {}) + await storage.save_config(merged) + self._config = merged # 全局配置实例 diff --git a/config.defaults.toml b/config.defaults.toml new file mode 100644 index 0000000..58cb2cf --- /dev/null +++ b/config.defaults.toml @@ -0,0 +1,37 @@ +[grok] +temporary = true +stream = true +thinking = true +dynamic_statsig = true +filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] +timeout = 120 +base_proxy_url = "" +asset_proxy_url = "" +cf_clearance = "" +max_retry = 3 +retry_status_codes = [401,429,403] + +[app] +app_url = "http://127.0.0.1:8000" +app_key = "grok2api" +api_key = "" +image_format = "url" +video_format = "url" + +[token] +auto_refresh = true +refresh_interval_hours = 8 +fail_threshold = 5 +save_delay_ms = 500 +reload_interval_sec = 30 + +[cache] +enable_auto_clean = true +limit_mb = 1024 + +[performance] +assets_max_concurrent = 25 +media_max_concurrent = 50 +usage_max_concurrent = 25 +assets_delete_batch_size = 10 +admin_assets_batch_size = 10 diff --git a/docker-compose.yml b/docker-compose.yml index 1e5edbb..0e3dac6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,22 @@ services: grok2api: - build: . container_name: grok2api + image: ghcr.io/chenyme/grok2api:latest ports: - "8000:8000" environment: TZ: Asia/Shanghai - SERVER_HOST: 0.0.0.0 + LOG_LEVEL: INFO SERVER_PORT: 8000 + SERVER_WORKERS: 1 + SERVER_STORAGE_TYPE: local + # SERVER_STORAGE_URL: + + # SERVER_STORAGE_TYPE: (local, redis, mysql, pgsql) default: local + # SERVER_STORAGE_URL: (local mode is empty) default: empty + # Redis: redis://localhost:6379/0 or redis://:password@localhost:6379/0 + # MySQL: mysql+aiomysql://user:pass@localhost/db + # PgSQL: postgresql+asyncpg://user:pass@localhost/db volumes: - ./data:/app/data - ./logs:/app/logs diff --git a/README.en.md b/docs/README.en.md similarity index 92% rename from README.en.md rename to docs/README.en.md index 4ad70d6..865adee 100644 --- a/README.en.md +++ b/docs/README.en.md @@ -31,6 +31,17 @@ git clone https://github.com/chenyme/grok2api docker compose up -d ``` +### Environment variables + +| Variable | Description | Default | Example | +| :--- | :--- | :--- | :--- | +| `LOG_LEVEL` | Log level | `INFO` | `DEBUG` | +| `SERVER_HOST` | Bind address | `0.0.0.0` | `0.0.0.0` | +| `SERVER_PORT` | Service port | `8000` | `8000` | +| `SERVER_WORKERS` | Uvicorn worker count | `1` | `2` | +| `SERVER_STORAGE_TYPE` | Storage type (`local`/`redis`/`mysql`/`pgsql`) | `local` | `pgsql` | +| `SERVER_STORAGE_URL` | Storage URL (empty for local) | `""` | `postgresql+asyncpg://user:password@host:5432/db` | + ### Usage limits - Basic account: 80 requests / 20h diff --git a/env.example b/env.example deleted file mode 100644 index 29f7e7f..0000000 --- a/env.example +++ /dev/null @@ -1,19 +0,0 @@ -# ============================================ -# 启动配置 -# ============================================ -SERVER_HOST=0.0.0.0 -SERVER_PORT=8000 -SERVER_WORKERS=1 -LOG_LEVEL=INFO - -# ============================================ -# 存储配置 -# ============================================ -# 存储类型: local, redis, mysql, pgsql -SERVER_STORAGE_TYPE=local - -# 存储连接地址 (local 模式下留空) -# Redis: redis://localhost:6379/0 or redis://:password@localhost:6379/0 -# MySQL: mysql+aiomysql://user:pass@localhost/db -# PgSQL: postgresql+asyncpg://user:pass@localhost/db -SERVER_STORAGE_URL= \ No newline at end of file diff --git a/readme.md b/readme.md index 701655c..45728dd 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,19 @@ git clone https://github.com/chenyme/grok2api docker compose up -d ``` +### 环境变量 + +> 配置 `.env` 文件 + +| 变量名 | 说明 | 默认值 | 示例 | +| :---------------------- | :-------------------------------------------------- | :---------- | :-------------------------------------------------- | +| `LOG_LEVEL` | 日志级别 | `INFO` | `DEBUG` | +| `SERVER_HOST` | 服务监听地址 | `0.0.0.0` | `0.0.0.0` | +| `SERVER_PORT` | 服务端口 | `8000` | `8000` | +| `SERVER_WORKERS` | Uvicorn worker 数量 | `1` | `2` | +| `SERVER_STORAGE_TYPE` | 存储类型(`local`/`redis`/`mysql`/`pgsql`) | `local` | `pgsql` | +| `SERVER_STORAGE_URL` | 存储连接串(local 时可为空) | `""` | `postgresql+asyncpg://user:password@host:5432/db` | + ### 可用次数 - Basic 账号:80 次 / 20h @@ -38,24 +51,25 @@ docker compose up -d ### 可用模型 -| 模型名 | 计次 | 可用账号 | 对话功能 | 图像功能 | 视频功能 | -| :--- | :---: | :--- | :---: | :---: | :---: | -| `grok-3` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-3-fast` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-4` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-4-mini` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-4-fast` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-4-heavy` | 4 | Super | 支持 | 支持 | - | -| `grok-4.1` | 1 | Basic/Super | 支持 | 支持 | - | -| `grok-4.1-thinking` | 4 | Basic/Super | 支持 | 支持 | - | -| `grok-imagine-1.0` | 4 | Basic/Super | - | 支持 | - | -| `grok-imagine-1.0-video` | - | Basic/Super | - | - | 支持 | +| 模型名 | 计次 | 可用账号 | 对话功能 | 图像功能 | 视频功能 | +| :------------------------- | :--: | :---------- | :------: | :------: | :------: | +| `grok-3` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-3-fast` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-4` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-4-mini` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-4-fast` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-4-heavy` | 4 | Super | 支持 | 支持 | - | +| `grok-4.1` | 1 | Basic/Super | 支持 | 支持 | - | +| `grok-4.1-thinking` | 4 | Basic/Super | 支持 | 支持 | - | +| `grok-imagine-1.0` | 4 | Basic/Super | - | 支持 | - | +| `grok-imagine-1.0-video` | - | Basic/Super | - | - | 支持 |
## 接口说明 ### `POST /v1/chat/completions` + > 通用接口,支持对话聊天、图像生成、图像编辑、视频生成、视频超分 ```bash @@ -73,17 +87,17 @@ curl http://localhost:8000/v1/chat/completions \
-| 字段 | 类型 | 说明 | 可用参数 | -| :--- | :--- | :--- | :--- | -| `model` | string | 模型名称 | - | -| `messages` | array | 消息列表 | `developer`, `system`, `user`, `assistant` | -| `stream` | boolean | 是否开启流式输出 | `true`, `false` | -| `thinking` | string | 思维链模式 | `enabled`, `disabled`, `null` | -| `video_config` | object | **视频模型专用配置对象** | - | -| └─ `aspect_ratio` | string | 视频宽高比 | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` | -| └─ `video_length` | integer | 视频时长 (秒) | `5` - `15` | -| └─ `resolution` | string | 分辨率 | `SD`, `HD` | -| └─ `preset` | string | 风格预设 | `fun`, `normal`, `spicy` | +| 字段 | 类型 | 说明 | 可用参数 | +| :------------------- | :------ | :----------------------------- | :------------------------------------------------- | +| `model` | string | 模型名称 | - | +| `messages` | array | 消息列表 | `developer`, `system`, `user`, `assistant` | +| `stream` | boolean | 是否开启流式输出 | `true`, `false` | +| `thinking` | string | 思维链模式 | `enabled`, `disabled`, `null` | +| `video_config` | object | **视频模型专用配置对象** | - | +| └─`aspect_ratio` | string | 视频宽高比 | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` | +| └─`video_length` | integer | 视频时长 (秒) | `5` - `15` | +| └─`resolution` | string | 分辨率 | `SD`, `HD` | +| └─`preset` | string | 风格预设 | `fun`, `normal`, `spicy` | 注:除上述外的其他参数将自动丢弃并忽略 @@ -92,6 +106,7 @@ curl http://localhost:8000/v1/chat/completions \ ### `POST /v1/images/generations` + > 图像接口,支持图像生成、图像编辑 ```bash @@ -110,12 +125,12 @@ curl http://localhost:8000/v1/images/generations \
-| 字段 | 类型 | 说明 | 可用参数 | -| :--- | :--- | :--- | :--- | -| `model` | string | 图像模型名 | `grok-imagine-1.0` | -| `prompt` | string | 图像描述提示词 | - | -| `n` | integer | 生成数量 | `1` - `10` (流式模式仅限 `1` 或 `2`) | -| `stream` | boolean | 是否开启流式输出 | `true`, `false` | +| 字段 | 类型 | 说明 | 可用参数 | +| :--------- | :------ | :--------------- | :------------------------------------------- | +| `model` | string | 图像模型名 | `grok-imagine-1.0` | +| `prompt` | string | 图像描述提示词 | - | +| `n` | integer | 生成数量 | `1` - `10` (流式模式仅限 `1` 或 `2`) | +| `stream` | boolean | 是否开启流式输出 | `true`, `false` | 注:除上述外的其他参数将自动丢弃并忽略 @@ -129,36 +144,36 @@ curl http://localhost:8000/v1/images/generations \ 配置文件:`data/config.toml` -| 模块 | 字段 | 配置名 | 说明 | 默认值 | -| :--- | :--- | :--- | :--- | :--- | -| **app** | `app_url` | 应用地址 | 当前 Grok2API 服务的外部访问 URL,用于文件链接访问。 | `http://127.0.0.1:8000` | -| | `app_key` | 后台密码 | 登录 Grok2API 服务管理后台的密码,请妥善保管。 | `grok2api` | -| | `api_key` | API 密钥 | 调用 Grok2API 服务所需的 Bearer Token,请妥善保管。 | `""` | -| | `image_format` | 图片格式 | 生成的图片格式(url 或 base64)。 | `url` | -| | `video_format` | 视频格式 | 生成的视频格式(仅支持 url)。 | `url` | -| **grok** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` | -| | `stream` | 流式响应 | 是否默认启用流式输出。 | `true` | -| | `thinking` | 思维链 | 是否启用模型思维链输出。 | `true` | -| | `dynamic_statsig` | 动态指纹 | 是否启用动态生成 Statsig 值。 | `true` | -| | `filter_tags` | 过滤标签 | 自动过滤 Grok 响应中的特殊标签。 | `["xaiartifact", "xai:tool_usage_card", "grok:render"]` | -| | `timeout` | 超时时间 | 请求 Grok 服务的超时时间(秒)。 | `120` | -| | `base_proxy_url` | 基础代理 URL | 代理请求到 Grok 官网的基础服务地址。 | `""` | -| | `asset_proxy_url` | 资源代理 URL | 代理请求到 Grok 官网的静态资源(图片/视频)地址。 | `""` | -| | `cf_clearance` | CF Clearance | Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。 | `""` | -| | `max_retry` | 最大重试 | 请求 Grok 服务失败时的最大重试次数。 | `3` | -| | `retry_status_codes` | 重试状态码 | 触发重试的 HTTP 状态码列表。 | `[401, 429, 403]` | -| **token** | `auto_refresh` | 自动刷新 | 是否开启 Token 自动刷新机制。 | `true` | -| | `refresh_interval_hours` | 刷新间隔 | Token 刷新的时间间隔(小时)。 | `8` | -| | `fail_threshold` | 失败阈值 | 单个 Token 连续失败多少次后被标记为不可用。 | `5` | -| | `save_delay_ms` | 保存延迟 | Token 变更合并写入的延迟(毫秒)。 | `500` | -| | `reload_interval_sec` | 一致性刷新 | 多 worker 场景下 Token 状态刷新间隔(秒)。 | `30` | -| **cache** | `enable_auto_clean` | 自动清理 | 是否启用缓存自动清理,开启后按上限自动回收。 | `true` | -| | `limit_mb` | 清理阈值 | 缓存大小阈值(MB),超过阈值会触发清理。 | `1024` | -| **performance** | `assets_max_concurrent` | 资产并发上限 | 资源上传/下载/列表的并发上限。推荐 25。 | `25` | -| | `media_max_concurrent` | 媒体并发上限 | 视频/媒体生成请求的并发上限。推荐 50。 | `50` | -| | `usage_max_concurrent` | 用量并发上限 | 用量查询请求的并发上限。推荐 25。 | `25` | -| | `assets_delete_batch_size` | 资产清理批量 | 在线资产删除单批并发数量。推荐 10。 | `10` | -| | `admin_assets_batch_size` | 管理端批量 | 管理端在线资产统计/清理批量并发数量。推荐 10。 | `10` | +| 模块 | 字段 | 配置名 | 说明 | 默认值 | +| :-------------------- | :--------------------------- | :----------- | :--------------------------------------------------- | :-------------------------------------------------------- | +| **app** | `app_url` | 应用地址 | 当前 Grok2API 服务的外部访问 URL,用于文件链接访问。 | `http://127.0.0.1:8000` | +| | `app_key` | 后台密码 | 登录 Grok2API 服务管理后台的密码,请妥善保管。 | `grok2api` | +| | `api_key` | API 密钥 | 调用 Grok2API 服务所需的 Bearer Token,请妥善保管。 | `""` | +| | `image_format` | 图片格式 | 生成的图片格式(url 或 base64)。 | `url` | +| | `video_format` | 视频格式 | 生成的视频格式(仅支持 url)。 | `url` | +| **grok** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` | +| | `stream` | 流式响应 | 是否默认启用流式输出。 | `true` | +| | `thinking` | 思维链 | 是否启用模型思维链输出。 | `true` | +| | `dynamic_statsig` | 动态指纹 | 是否启用动态生成 Statsig 值。 | `true` | +| | `filter_tags` | 过滤标签 | 自动过滤 Grok 响应中的特殊标签。 | `["xaiartifact", "xai:tool_usage_card", "grok:render"]` | +| | `timeout` | 超时时间 | 请求 Grok 服务的超时时间(秒)。 | `120` | +| | `base_proxy_url` | 基础代理 URL | 代理请求到 Grok 官网的基础服务地址。 | `""` | +| | `asset_proxy_url` | 资源代理 URL | 代理请求到 Grok 官网的静态资源(图片/视频)地址。 | `""` | +| | `cf_clearance` | CF Clearance | Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。 | `""` | +| | `max_retry` | 最大重试 | 请求 Grok 服务失败时的最大重试次数。 | `3` | +| | `retry_status_codes` | 重试状态码 | 触发重试的 HTTP 状态码列表。 | `[401, 429, 403]` | +| **token** | `auto_refresh` | 自动刷新 | 是否开启 Token 自动刷新机制。 | `true` | +| | `refresh_interval_hours` | 刷新间隔 | Token 刷新的时间间隔(小时)。 | `8` | +| | `fail_threshold` | 失败阈值 | 单个 Token 连续失败多少次后被标记为不可用。 | `5` | +| | `save_delay_ms` | 保存延迟 | Token 变更合并写入的延迟(毫秒)。 | `500` | +| | `reload_interval_sec` | 一致性刷新 | 多 worker 场景下 Token 状态刷新间隔(秒)。 | `30` | +| **cache** | `enable_auto_clean` | 自动清理 | 是否启用缓存自动清理,开启后按上限自动回收。 | `true` | +| | `limit_mb` | 清理阈值 | 缓存大小阈值(MB),超过阈值会触发清理。 | `1024` | +| **performance** | `assets_max_concurrent` | 资产并发上限 | 资源上传/下载/列表的并发上限。推荐 25。 | `25` | +| | `media_max_concurrent` | 媒体并发上限 | 视频/媒体生成请求的并发上限。推荐 50。 | `50` | +| | `usage_max_concurrent` | 用量并发上限 | 用量查询请求的并发上限。推荐 25。 | `25` | +| | `assets_delete_batch_size` | 资产清理批量 | 在线资产删除单批并发数量。推荐 10。 | `10` | +| | `admin_assets_batch_size` | 管理端批量 | 管理端在线资产统计/清理批量并发数量。推荐 10。 | `10` |
diff --git a/scripts/init_storage.sh b/scripts/init_storage.sh index e6c0e9b..0c61ac2 100755 --- a/scripts/init_storage.sh +++ b/scripts/init_storage.sh @@ -5,49 +5,12 @@ ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" DATA_DIR="$ROOT_DIR/data" LOG_DIR="$ROOT_DIR/logs" TMP_DIR="$DATA_DIR/tmp" +DEFAULT_CONFIG="$ROOT_DIR/config.defaults.toml" mkdir -p "$DATA_DIR" "$LOG_DIR" "$TMP_DIR" if [ ! -f "$DATA_DIR/config.toml" ]; then - cat > "$DATA_DIR/config.toml" <<'TOML' -[grok] -temporary = true -stream = true -thinking = true -dynamic_statsig = true -filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] -timeout = 120 -base_proxy_url = "" -asset_proxy_url = "" -cf_clearance = "" -max_retry = 3 -retry_status_codes = [401,429,403] - -[app] -app_url = "http://127.0.0.1:8000" -app_key = "grok2api" -api_key = "" -image_format = "url" -video_format = "url" - -[token] -auto_refresh = true -refresh_interval_hours = 8 -fail_threshold = 5 -save_delay_ms = 500 -reload_interval_sec = 30 - -[cache] -enable_auto_clean = true -limit_mb = 1024 - -[performance] -assets_max_concurrent = 25 # 推荐 25 -media_max_concurrent = 50 # 推荐 50 -usage_max_concurrent = 25 # 推荐 25 -assets_delete_batch_size = 10 # 推荐 10 -admin_assets_batch_size = 10 # 推荐 10 -TOML + cp "$DEFAULT_CONFIG" "$DATA_DIR/config.toml" fi if [ ! -f "$DATA_DIR/token.json" ]; then