From f0f499bbca06f914483dc5ddb22711ece7517765 Mon Sep 17 00:00:00 2001 From: XianYuDaXian Date: Sat, 14 Mar 2026 11:07:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E4=BA=91=E7=AB=AF?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E5=85=BC=E5=AE=B9=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20Vercel=20=E5=AD=98=E5=82=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- app/api/pages/admin.py | 12 +++++++----- app/api/pages/asset_response.py | 17 +++++++++++++++++ app/api/pages/embedded_assets.py | 17 +++++++++++++++++ app/api/pages/public.py | 27 +++++++++++---------------- app/core/logger.py | 14 +++++++++++--- readme.md | 24 +++++++++++++++++++++++- scripts/entrypoint.sh | 2 +- 8 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 app/api/pages/asset_response.py create mode 100644 app/api/pages/embedded_assets.py diff --git a/Dockerfile b/Dockerfile index 526fc3c..ccdf947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,10 +28,10 @@ COPY main.py ./ COPY scripts ./scripts RUN mkdir -p /app/data /app/data/tmp /app/logs \ - && chmod +x /app/scripts/entrypoint.sh + && chmod +x /app/scripts/entrypoint.sh /app/scripts/init_storage.sh EXPOSE 8000 ENTRYPOINT ["/app/scripts/entrypoint.sh"] -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/api/pages/admin.py b/app/api/pages/admin.py index bb581e8..b4d6be4 100644 --- a/app/api/pages/admin.py +++ b/app/api/pages/admin.py @@ -1,7 +1,9 @@ from pathlib import Path from fastapi import APIRouter -from fastapi.responses import FileResponse, RedirectResponse +from fastapi.responses import RedirectResponse + +from app.api.pages.asset_response import serve_embedded_asset router = APIRouter() STATIC_DIR = Path(__file__).resolve().parents[2] / "static" @@ -14,19 +16,19 @@ async def admin_root(): @router.get("/admin/login", include_in_schema=False) async def admin_login(): - return FileResponse(STATIC_DIR / "admin/pages/login.html") + return serve_embedded_asset(STATIC_DIR, "admin/pages/login.html", "text/html; charset=utf-8") @router.get("/admin/config", include_in_schema=False) async def admin_config(): - return FileResponse(STATIC_DIR / "admin/pages/config.html") + return serve_embedded_asset(STATIC_DIR, "admin/pages/config.html", "text/html; charset=utf-8") @router.get("/admin/cache", include_in_schema=False) async def admin_cache(): - return FileResponse(STATIC_DIR / "admin/pages/cache.html") + return serve_embedded_asset(STATIC_DIR, "admin/pages/cache.html", "text/html; charset=utf-8") @router.get("/admin/token", include_in_schema=False) async def admin_token(): - return FileResponse(STATIC_DIR / "admin/pages/token.html") + return serve_embedded_asset(STATIC_DIR, "admin/pages/token.html", "text/html; charset=utf-8") diff --git a/app/api/pages/asset_response.py b/app/api/pages/asset_response.py new file mode 100644 index 0000000..665fb24 --- /dev/null +++ b/app/api/pages/asset_response.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from fastapi.responses import FileResponse, Response + +from app.api.pages.embedded_assets import EMBEDDED_ASSETS + + +def serve_embedded_asset(static_dir: Path, relative_path: str, media_type: str) -> Response: + """优先返回磁盘文件,不存在时回退到内嵌资源。""" + file_path = static_dir / relative_path + if file_path.exists(): + return FileResponse(file_path, media_type=media_type) + + content = EMBEDDED_ASSETS.get(relative_path) + if content is None: + raise RuntimeError(f"Embedded asset not found: {relative_path}") + return Response(content=content, media_type=media_type) diff --git a/app/api/pages/embedded_assets.py b/app/api/pages/embedded_assets.py new file mode 100644 index 0000000..644998a --- /dev/null +++ b/app/api/pages/embedded_assets.py @@ -0,0 +1,17 @@ +"""内嵌页面资源,用于 Serverless 环境回退。""" + +EMBEDDED_ASSETS = { + "public/pages/login.html": "\n\n\n\n \n \n Grok2API - Public\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n\n
\n
\n \n
Public Access
\n
请输入 Public Key(如未配置可直接进入)
\n\n
\n \n \n
\n\n \n
\n
\n\n
\n\n \n \n \n \n \n\n\n\n", + "public/pages/imagine.html": "\n\n\n\n \n \n Grok2API - Imagine 瀑布流\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n
\n

Imagine 瀑布流

\n

通过 WebSocket 持续生成图片,实时展示 base64 瀑布流。

\n
\n\n
\n\n
\n
\n
\n
生成设置
\n
\n
\n
\n \n \n
\n
\n
\n
\n
\n
自动跟随
\n
滚动到最新
\n
\n \n
\n
\n
\n
自动保存
\n
自动保存图片
\n
\n \n
\n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n
\n
\n
自动过滤
\n
过滤不达标图片
\n
\n \n
\n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n
\n
\n
反向新增
\n
最新显示在上方
\n
\n \n
\n
\n
\n
\n
\n\n
\n
\n
连接状态
\n 未连接\n \n \n \n
\n
\n
\n
\n
连接模式
\n
\n \n \n \n
\n
\n
\n
图片数量
\n
0
\n
\n
\n
活跃任务
\n
0
\n
\n
\n
平均耗时
\n
-
\n
\n
\n
\n
\n
\n\n
\n
\n
瀑布流
\n
\n
\n
\n
\n
等待连接中
\n
启动任务后,生成的图片会自动汇聚到这里
\n
\n
\n
\n
\n\n \n
\n \n \n \n \"\"\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n
编辑中 0%
\n
\n \n
\n
\n
\n\n
\n \n \n \n \n \n \n
\n \n \n
\n
\n\n \n
\n\n \n
\n
\n \n \n
\n \n
\n \n \n \n \n \n \n \n \n
\n \n \n
\n
\n\n
\n\n \n \n \n \n \n \n \n \n \n\n\n", + "public/pages/voice.html": "\n\n\n\n \n \n Grok2API - LiveKit 陪聊\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n
\n
\n

LiveKit 陪聊

\n

LiveKit 语音会话,连接 Grok Voice。

\n
\n
\n \n \n
\n
\n\n
\n\n
\n
\n
\n
连接设置
\n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n
\n \n 1.0x\n
\n \n
\n
\n
提示:Voice / Personality 会在建立连接前发送到服务端。
\n
\n\n
\n
\n
会话日志
\n
\n \n \n
\n
\n
\n
\n
\n\n \n
\n
\n
\n\n
\n
\n
\n \n \n \n \n
\n
\n \n
\n\n
\n\n \n \n \n \n \n \n \n\n\n\n", + "public/pages/video.html": "\n\n\n\n \n \n Grok2API - Video 视频工作台\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n
\n
\n

Video 视频工作台

\n

生成短视频,支持参考图、多种预设风格与视频拼接。

\n
\n
\n\n
\n\n
\n
\n
\n
\n
\n
\n
当前画面
\n
\n \n 已添加 0/7 张\n
\n \n \n
\n
可上传 / 粘贴 / 拖拽参考图(最多 7 张)
\n
\n
\n \n
\n \n \n
\n
\n
\n \n
请先添加参考图
\n
支持拖拽图片到此处,或按 Ctrl+V 粘贴图片
\n
\n
\n
\n
当前 parentPostId
\n
-
\n
\n
\n
当前模式
\n
upload
\n
\n
\n
\n \n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
视频延长工作区
\n
\n \n \n \n \n
\n
\n
请先选择或生成一个视频,工作区会始终展示当前视频。
\n
\n
\n
\n
当前工作视频
\n
-
\n
\n
\n
历史数量
\n
0
\n
\n
\n
\n \n
\n
\n \n
\n 00:00.000\n 总时长 -\n
\n
\n
\n
\n
frame_index
\n
-
\n
\n
\n
timestamp_ms
\n
-
\n
\n
\n
extend_post_id
\n
-
\n
\n
\n
\n \n \n
\n
\n \n
\n
\n
\n
\n\n \n
\n\n
\n
\n
\n

选择缓存视频

\n \n
\n
\n
\n
\n
\n
\n\n
\n \n
\n
\n \"参考图放大预览\"\n
\n
\n\n
\n\n \n
\n
\n \n \n \n
\n \n
\n \n
\n \n
\n \n
\n
\n
\n
\n \n
\n\n \n \n \n \n \n \n \n \n\n\n\n", + "public/pages/chat.html": "\n\n\n\n \n \n Grok2API - Chat 聊天\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n \n
\n \n
\n
\n
\n \n
\n

Chat 聊天

\n

通过 chat 接口与 Grok 进行会话聊天

\n
\n
\n 就绪\n
\n\n
\n
输入一条消息开始对话。
\n
\n\n
\n \n
\n
\n
\n \n
\n \n
\n
\n
\n
\n \n \n
\n
\n \n
\n
\n
\n \n \n
\n
\n \n
\n \n 0.8\n
\n
\n
\n \n
\n \n 0.95\n
\n
\n
\n \n \n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n\n
\n\n \n \n \n \n \n\n\n\n\n", + "public/pages/nsfw.html": "\n\n\n\n \n \n Grok2API - NSFW 工作台\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n
\n
\n

NSFW 工作台

\n

先生成 6 张候选图,再选中 1 张按 parentPostId 并行生成视频。

\n
\n
\n
候选图:未开始
\n
视频:未开始
\n
\n
\n\n
\n\n
\n
\n
生成参数
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n
\n
操作
\n
\n \n \n \n \n \n \n \n
\n
未选择候选图
\n
\n
\n\n
\n
\n
候选图瀑布流
\n
0 张
\n
\n
点击“生成 6 张”开始
\n
\n
\n\n
\n
\n
视频结果
\n
0 个任务
\n
\n
选中候选图后可并行生成 1~4 个视频
\n
\n
\n
\n
\n\n \n
\n\n \n
\n \n
\n \n \n
\n \n \n \n
\n
\n\n \n
\n\n \n
\n \n
未选择候选图
\n \n
\n \n \n \n \n
\n
\n
\n\n\n
\n \n \n \n \"\"\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n
编辑中 0%
\n
\n \n
\n
\n
\n\n
\n\n \n \n \n \n \n \n \n \n\n\n\n", + "public/pages/imagine_workbench.html": "\n\n\n\n \n \n Grok2API - 图片编辑工作台\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n
\n
\n\n
\n
\n
\n
\n

图片编辑工作台

\n

\n 支持上传/粘贴/拖拽最多 5 张参考图进行编辑;编辑后沿用上一次输出的 parentPostId 继续链路。\n

\n
\n
未开始
\n
\n\n
\n\n
\n
\n
当前画面
\n\n
\n \n \n 已添加 0/5 张\n
\n\n
\n
可上传 / 粘贴 / 拖拽参考图(最多 5 张)
\n
\n\n
\n \n
\n \n \n
\n
\n\n
\n \n
请先添加参考图
\n
支持拖拽图片到此处,或按 Ctrl+V 粘贴图片
\n
\n\n
\n
\n
当前 parentPostId
\n
-
\n
\n
\n
当前模式
\n
upload
\n
\n
\n\n
\n \n
\n
\n
\n \n
\n
\n\n
\n \n \n \n
\n\n
\n
\n
\n
编辑中 0%
\n
\n\n \n
\n
\n
\n\n \n
\n
\n \n
\n \n \n \n \n \n \n \n \n
\n \n \n
\n
\n\n
\n\n \n \n \n \n \n \n \n \n\n\n\n", + "public/manifest.webmanifest": "{\n \"name\": \"Grok2API\",\n \"short_name\": \"Grok2API\",\n \"description\": \"Grok2API 功能工作台\",\n \"start_url\": \"/login\",\n \"scope\": \"/\",\n \"display\": \"standalone\",\n \"background_color\": \"#fafafa\",\n \"theme_color\": \"#fafafa\",\n \"icons\": [\n {\n \"src\": \"/static/public/pwa/icon-192.png\",\n \"sizes\": \"192x192\",\n \"type\": \"image/png\",\n \"purpose\": \"any\"\n },\n {\n \"src\": \"/static/public/pwa/icon-512.png\",\n \"sizes\": \"512x512\",\n \"type\": \"image/png\",\n \"purpose\": \"any maskable\"\n }\n ]\n}\n", + "public/sw.js": "const CACHE_PREFIX = \"grok2api-pwa-\";\nconst CACHE_NAME = `${CACHE_PREFIX}v2`;\n\nself.addEventListener(\"install\", (event) => {\n event.waitUntil(self.skipWaiting());\n});\n\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME)\n .map((key) => caches.delete(key))\n );\n await self.clients.claim();\n })()\n );\n});\n\nself.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== self.location.origin) return;\n\n const cacheablePathPrefixes = [\n \"/static/\",\n \"/login\",\n \"/imagine\",\n \"/video\",\n \"/voice\",\n \"/nsfw\",\n \"/imagine-workbench\",\n \"/manifest.webmanifest\"\n ];\n const isCacheable = cacheablePathPrefixes.some((prefix) => url.pathname.startsWith(prefix));\n if (!isCacheable) return;\n\n event.respondWith(\n (async () => {\n try {\n const response = await fetch(request);\n const cache = await caches.open(CACHE_NAME);\n cache.put(request, response.clone());\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n })()\n );\n});\n", + "admin/pages/login.html": "\n\n\n\n \n \n Grok2API - 登录\n \n \n \n \n \n \n \n \n \n\n\n\n
\n\n
\n
\n \n
管理后台
\n
请输入后台密码以继续
\n\n
\n \n \n
\n
\n
\n\n
\n\n \n \n \n \n\n\n\n", + "admin/pages/config.html": "\n\n\n\n \n \n Grok2API - 配置管理\n \n \n \n \n \n \n \n \n\n\n\n\n
\n\n
\n
\n
\n
\n

配置管理

\n

管理 API 密钥及系统参数设置。

\n
\n \n
\n\n
\n\n
\n
加载中...
\n
\n\n
\n
\n
\n\n \n \n \n \n \n\n\n\n", + "admin/pages/cache.html": "\n\n\n\n \n \n Grok2API - 缓存管理\n \n \n \n \n \n \n \n \n\n\n\n
\n\n
\n
\n
\n
\n

缓存管理

\n

管理本地资源与在线资产缓存。

\n
\n
\n\n
\n\n
\n
\n
\n
\n
本地图片
\n
\n 0\n 个文件\n
\n
\n
\n
0 MB
\n \n
\n
\n
\n\n
\n
\n
\n
本地视频
\n
\n 0\n 个文件\n
\n
\n
\n
0 MB
\n \n
\n
\n
\n\n
\n
\n
\n
在线资产
\n
\n 0\n 个文件\n
\n
连接正常
\n
\n
\n
\n
\n
\n\n
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n
\n \n 文件大小时间操作
\n
\n
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n
\n \n 文件大小时间操作
\n
\n
\n
\n\n
\n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n Token类型资产数上次清空时间操作
\n
暂无可用账号
\n
\n\n
\n \n
\n
\n\n
\n
\n 已选择\n 0\n 项\n
\n \n
\n \n \n
\n
\n \n \n \n \n
\n
\n\n \n
\n
请确认
\n
\n
\n \n \n
\n
\n
\n\n \n
\n
失败详情
\n
\n
\n \n \n
\n
\n
\n\n \n \n \n \n \n \n \n\n\n\n", + "admin/pages/token.html": "\n\n\n\n \n \n Grok2API - Token 管理\n \n \n \n \n \n \n \n \n\n\n\n\n \n
\n
\n\n \n
\n\n
\n
\n
\n

Token 列表

\n

管理 Grok2API 的 Token 服务号池。

\n
\n
\n \n \n
\n
\n\n
\n\n \n
\n \n
\n
-
\n
Token 总数
\n
\n
\n
-
\n
Token 正常
\n
\n
\n
-
\n
Token 限流
\n
\n
\n
-
\n
Token 失效
\n
\n\n \n
\n
-
\n
Chat 剩余
\n
\n
\n
-
\n
Image 剩余
\n
\n
\n
-
\n
Video 剩余
\n
\n
\n
-
\n
总调用次数
\n
\n
\n\n \n
\n \n \n \n \n \n \n \n
\n\n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Token类型状态额度备注操作
\n
加载中...
\n
\n 暂无 Token,请点击右上角导入或添加。\n
\n
\n\n \n
\n
第 0 / 0 页 · 共 0 条
\n
\n \n \n \n \n \n \n
\n
\n\n \n\n
\n
\n
\n\n \n
\n
\n 已选择\n 0\n 项\n
\n \n
\n \n \n \n \n \n \n
\n
\n \n \n \n \n
\n
\n\n\n \n
\n
\n
\n

批量导入 Token

\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n\n \n
\n
\n
\n

编辑 Token

\n \n
\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n\n
\n
\n
\n
请确认
\n
\n
\n \n \n
\n
\n
\n
\n\n \n \n \n \n \n \n \n\n\n\n" +} diff --git a/app/api/pages/public.py b/app/api/pages/public.py index 2c9c971..f394014 100644 --- a/app/api/pages/public.py +++ b/app/api/pages/public.py @@ -1,8 +1,9 @@ from pathlib import Path from fastapi import APIRouter, HTTPException -from fastapi.responses import FileResponse, RedirectResponse +from fastapi.responses import RedirectResponse +from app.api.pages.asset_response import serve_embedded_asset from app.core.auth import is_public_enabled router = APIRouter() @@ -20,66 +21,60 @@ async def root(): async def public_login(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/login.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/login.html", "text/html; charset=utf-8") @router.get("/imagine", include_in_schema=False) async def public_imagine(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/imagine.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/imagine.html", "text/html; charset=utf-8") @router.get("/voice", include_in_schema=False) async def public_voice(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/voice.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/voice.html", "text/html; charset=utf-8") @router.get("/video", include_in_schema=False) async def public_video(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/video.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/video.html", "text/html; charset=utf-8") @router.get("/chat", include_in_schema=False) async def public_chat(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/chat.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/chat.html", "text/html; charset=utf-8") @router.get("/nsfw", include_in_schema=False) async def public_nsfw(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/nsfw.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/nsfw.html", "text/html; charset=utf-8") @router.get("/imagine-workbench", include_in_schema=False) async def public_imagine_workbench(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse(STATIC_DIR / "public/pages/imagine_workbench.html") + return serve_embedded_asset(STATIC_DIR, "public/pages/imagine_workbench.html", "text/html; charset=utf-8") @router.get("/manifest.webmanifest", include_in_schema=False) async def public_manifest(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse( - STATIC_DIR / "public/manifest.webmanifest", - media_type="application/manifest+json", - ) + return serve_embedded_asset(STATIC_DIR, "public/manifest.webmanifest", "application/manifest+json") @router.get("/sw.js", include_in_schema=False) async def public_service_worker(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") - return FileResponse( - STATIC_DIR / "public/sw.js", - media_type="application/javascript", - ) + return serve_embedded_asset(STATIC_DIR, "public/sw.js", "application/javascript") diff --git a/app/core/logger.py b/app/core/logger.py index 879fc59..f12207c 100644 --- a/app/core/logger.py +++ b/app/core/logger.py @@ -79,6 +79,13 @@ def _env_flag(name: str, default: bool) -> bool: return raw.strip().lower() in ("1", "true", "yes", "on", "y") +def _should_enqueue_logs() -> bool: + """Serverless 环境禁用 multiprocessing 队列日志""" + if "VERCEL" in os.environ or "AWS_LAMBDA_FUNCTION_NAME" in os.environ: + return False + return _env_flag("LOG_ENQUEUE", True) + + def _make_json_sink(output): """创建 JSON sink""" @@ -106,6 +113,7 @@ def setup_logging( """设置日志配置""" logger.remove() file_logging = _env_flag("LOG_FILE_ENABLED", file_logging) + enqueue_logs = _should_enqueue_logs() # 控制台输出 if json_console: @@ -114,7 +122,7 @@ def setup_logging( level=level, format="{message}", colorize=False, - enqueue=True, + enqueue=enqueue_logs, ) else: logger.add( @@ -122,7 +130,7 @@ def setup_logging( level=level, format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {file.name}:{line} - {message}", colorize=True, - enqueue=True, + enqueue=enqueue_logs, ) # 文件输出 @@ -132,7 +140,7 @@ def setup_logging( _file_json_sink, level=level, format="{message}", - enqueue=True, + enqueue=enqueue_logs, ) else: logger.warning("File logging disabled: no writable log directory.") diff --git a/readme.md b/readme.md index f253c12..7ba9d92 100644 --- a/readme.md +++ b/readme.md @@ -216,9 +216,29 @@ Vercel 部署建议: - 必设 `DATA_DIR=/tmp/data` - 建议设 `LOG_FILE_ENABLED=false` -- 如需持久化数据,不要使用 `local`,改为外部 `redis/mysql/pgsql` +- 不建议使用 `local` 保存长期状态 +- 推荐直接使用外部 `redis/mysql/pgsql` 保存配置、Token 和运行状态 - 如需前端工作台,建议同时设置 `PUBLIC_ENABLED=true` 和 `PUBLIC_KEY` +推荐环境变量: + +```env +LOG_LEVEL=INFO +LOG_FILE_ENABLED=false +DATA_DIR=/tmp/data +SERVER_STORAGE_TYPE=redis +SERVER_STORAGE_URL=redis://:password@host:6379/0 +PUBLIC_ENABLED=true +PUBLIC_KEY=your-own-long-random-key +``` + +如果你使用 PostgreSQL,也可以改成: + +```env +SERVER_STORAGE_TYPE=pgsql +SERVER_STORAGE_URL=postgresql+asyncpg://user:password@host:5432/db +``` + 适用场景: - 轻量 API 转发 @@ -228,7 +248,9 @@ Vercel 部署建议: 注意: - Vercel 的本地磁盘是临时的 +- 后台保存的配置如果依赖 `local`,在冷启动或实例切换后可能恢复默认值 - 大量视频/图片缓存不适合长期放在 `local` +- 如果你要长期使用管理后台、Public 页面和工作台,建议从一开始就配置远程存储 ## Render 部署 diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index fafc9e4..96f32f5 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh set -eu -/app/scripts/init_storage.sh +sh /app/scripts/init_storage.sh exec "$@"