Spaces:
Running
Running
Add HF Space keepalive workflow
Browse files- .github/workflows/hf-space-keepalive.yml +30 -0
- README.md +3 -0
- huggingface-space-free.md +171 -0
- package.json +1 -0
- scripts/keepalive-hf-space.mjs +99 -0
.github/workflows/hf-space-keepalive.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Hugging Face Space Keepalive
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
- cron: '17 */6 * * *'
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
permissions:
|
| 9 |
+
contents: read
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
keepalive:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
timeout-minutes: 5
|
| 15 |
+
env:
|
| 16 |
+
HF_SPACE_KEEPALIVE_URL: ${{ vars.HF_SPACE_KEEPALIVE_URL || 'https://misonl-gpt-image-playground-customer.hf.space' }}
|
| 17 |
+
HF_SPACE_KEEPALIVE_PATH: /api/auth-status
|
| 18 |
+
HF_SPACE_KEEPALIVE_TIMEOUT_MS: '30000'
|
| 19 |
+
HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED: 'true'
|
| 20 |
+
steps:
|
| 21 |
+
- name: Checkout repository
|
| 22 |
+
uses: actions/checkout@v4
|
| 23 |
+
|
| 24 |
+
- name: Setup Node.js
|
| 25 |
+
uses: actions/setup-node@v4
|
| 26 |
+
with:
|
| 27 |
+
node-version: 20
|
| 28 |
+
|
| 29 |
+
- name: Ping Space keepalive endpoint
|
| 30 |
+
run: node scripts/keepalive-hf-space.mjs
|
README.md
CHANGED
|
@@ -441,6 +441,8 @@ NEXT_PUBLIC_IMAGE_STORAGE_MODE=indexeddb
|
|
| 441 |
|
| 442 |
本仓库也提供 `docker-compose.memory.yml` 作为本地模拟模板;Hugging Face Docker Space 通常直接通过 Space Variables 和 Secrets 设置环境变量,不需要提交 `.env.local`。完整部署步骤见 [Hugging Face Space 免费层部署](./docs/deployment/huggingface-space-free.md)。
|
| 443 |
|
|
|
|
|
|
|
| 444 |
`memory` 模式不创建 SQLite 文件,也不连接 PostgreSQL。它只适合无持久化演示、短会话调试或可接受重启丢失 Agent 幂等状态的环境;容器重启后请求记录、artifact 元数据和 replay 状态都会清空。Web 图片二进制按 `NEXT_PUBLIC_IMAGE_STORAGE_MODE` 保存;HF 免费层推荐 `indexeddb`,让网页结果保存在浏览器侧。Agent API 产物仍写入容器临时文件系统,以便提供 `content_url` 下载。
|
| 445 |
|
| 446 |
如果把当前 Dockerfile 直接部署到 Hugging Face Docker Space,Space README 顶部 YAML 需要使用 Docker SDK,并把应用端口指向本项目默认端口:
|
|
@@ -490,6 +492,7 @@ docker logs -f gpt-image-playground-customer
|
|
| 490 |
| `npm run dev` | 启动本地开发服务。 |
|
| 491 |
| `npm run build` | 执行生产构建。 |
|
| 492 |
| `npm run start` | 启动生产模式服务。 |
|
|
|
|
| 493 |
| `npm run smoke:hf-space` | 构建并启动 HF 免费层近似容器,验证 memory 状态后端和 Agent API 契约。 |
|
| 494 |
| `npm run lint` | 检查 `src/` 代码。 |
|
| 495 |
| `npm run format` | 格式化 `src/` 下的 TypeScript 和 React 文件。 |
|
|
|
|
| 441 |
|
| 442 |
本仓库也提供 `docker-compose.memory.yml` 作为本地模拟模板;Hugging Face Docker Space 通常直接通过 Space Variables 和 Secrets 设置环境变量,不需要提交 `.env.local`。完整部署步骤见 [Hugging Face Space 免费层部署](./docs/deployment/huggingface-space-free.md)。
|
| 443 |
|
| 444 |
+
免费 CPU Basic 会在长时间无访问后休眠。本仓库提供 `.github/workflows/hf-space-keepalive.yml`,默认每 6 小时访问一次 `/api/auth-status`,只做只读 keepalive,不携带密码或 token,不触发生图。若 Space 地址变化,在 GitHub 仓库 Variables 中设置 `HF_SPACE_KEEPALIVE_URL`。
|
| 445 |
+
|
| 446 |
`memory` 模式不创建 SQLite 文件,也不连接 PostgreSQL。它只适合无持久化演示、短会话调试或可接受重启丢失 Agent 幂等状态的环境;容器重启后请求记录、artifact 元数据和 replay 状态都会清空。Web 图片二进制按 `NEXT_PUBLIC_IMAGE_STORAGE_MODE` 保存;HF 免费层推荐 `indexeddb`,让网页结果保存在浏览器侧。Agent API 产物仍写入容器临时文件系统,以便提供 `content_url` 下载。
|
| 447 |
|
| 448 |
如果把当前 Dockerfile 直接部署到 Hugging Face Docker Space,Space README 顶部 YAML 需要使用 Docker SDK,并把应用端口指向本项目默认端口:
|
|
|
|
| 492 |
| `npm run dev` | 启动本地开发服务。 |
|
| 493 |
| `npm run build` | 执行生产构建。 |
|
| 494 |
| `npm run start` | 启动生产模式服务。 |
|
| 495 |
+
| `npm run keepalive:hf-space` | 访问 HF Space 只读状态端点,用于 keepalive 验证。 |
|
| 496 |
| `npm run smoke:hf-space` | 构建并启动 HF 免费层近似容器,验证 memory 状态后端和 Agent API 契约。 |
|
| 497 |
| `npm run lint` | 检查 `src/` 代码。 |
|
| 498 |
| `npm run format` | 格式化 `src/` 下的 TypeScript 和 React 文件。 |
|
huggingface-space-free.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Space 免费层部署
|
| 2 |
+
|
| 3 |
+
本文档描述如何把本项目部署到 Hugging Face Docker Space 免费层,用作公网图片生成服务。
|
| 4 |
+
|
| 5 |
+
## 目标形态
|
| 6 |
+
|
| 7 |
+
- 手机浏览器可以访问 Space 网页并正常生图。
|
| 8 |
+
- 电脑上的 Agent 可以通过 `/api/agent/*` 调用同一个 Space 生图。
|
| 9 |
+
- 状态后端使用 `memory`,不依赖 SQLite、PostgreSQL 或外部数据库。
|
| 10 |
+
- 图片 Web 结果优先保存在浏览器 IndexedDB,减少服务端临时盘依赖。
|
| 11 |
+
|
| 12 |
+
## Space README YAML
|
| 13 |
+
|
| 14 |
+
本仓库顶层 `README.md` 已包含 Hugging Face Space metadata。如果你维护的是单独的 Space 仓库,确认它的 `README.md` 顶部使用 Docker SDK,并暴露本项目端口:
|
| 15 |
+
|
| 16 |
+
```yaml
|
| 17 |
+
---
|
| 18 |
+
sdk: docker
|
| 19 |
+
app_port: 4783
|
| 20 |
+
---
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
官方依据:
|
| 24 |
+
|
| 25 |
+
- Docker Space 配置、Variables/Secrets 和权限说明:https://huggingface.co/docs/hub/main/spaces-sdks-docker
|
| 26 |
+
- 免费 CPU Basic 规格说明:https://huggingface.co/docs/hub/main/spaces-gpus
|
| 27 |
+
|
| 28 |
+
## Space Variables
|
| 29 |
+
|
| 30 |
+
在 Space Settings 中添加这些 Variables:
|
| 31 |
+
|
| 32 |
+
```dotenv
|
| 33 |
+
AGENT_STATE_BACKEND=memory
|
| 34 |
+
AGENT_SQLITE_PATH=
|
| 35 |
+
AGENT_DATABASE_URL=
|
| 36 |
+
AGENT_DB_PASSWORD=
|
| 37 |
+
AGENT_DB_PASSWORD_FILE=
|
| 38 |
+
NEXT_PUBLIC_IMAGE_STORAGE_MODE=indexeddb
|
| 39 |
+
APP_LOG_LEVEL=warn
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
`NEXT_PUBLIC_IMAGE_STORAGE_MODE` 是构建期和运行期都需要的值。Dockerfile 已声明 build arg,Hugging Face Docker Space 会把同名 Variable 作为 build arg 传入构建,并在运行期注入环境变量。
|
| 43 |
+
|
| 44 |
+
可选:
|
| 45 |
+
|
| 46 |
+
```dotenv
|
| 47 |
+
AGENT_PUBLIC_BASE_URL=https://<user>-<space>.hf.space
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
`AGENT_PUBLIC_BASE_URL` 只影响 OpenAPI `servers[0].url`,Agent skill 仍应以 `GPT_IMAGE_PLAYGROUND_URL` 指向实际 Space 地址。
|
| 51 |
+
|
| 52 |
+
## Space Secrets
|
| 53 |
+
|
| 54 |
+
在 Space Settings 中添加 Secrets,不要写入仓库文件:
|
| 55 |
+
|
| 56 |
+
```dotenv
|
| 57 |
+
OPENAI_API_KEY=<your-api-key>
|
| 58 |
+
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 59 |
+
APP_PASSWORD=<page-password>
|
| 60 |
+
AGENT_API_TOKEN=<long-random-agent-token>
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
公网部署建议至少设置 `APP_PASSWORD` 和 `AGENT_API_TOKEN`。如果不设置 `APP_PASSWORD`,任何人都可以打开网页并消耗服务端 API Key。
|
| 64 |
+
|
| 65 |
+
如果使用服务端渠道池,改用 `OPENAI_CHANNEL_N_*` Secrets:
|
| 66 |
+
|
| 67 |
+
```dotenv
|
| 68 |
+
OPENAI_ROUTING_STRATEGY=round_robin
|
| 69 |
+
OPENAI_CHANNEL_1_ID=official
|
| 70 |
+
OPENAI_CHANNEL_1_BASE_URL=https://api.openai.com/v1
|
| 71 |
+
OPENAI_CHANNEL_1_API_KEYS=<key-a>,<key-b>
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 手机网页使用
|
| 75 |
+
|
| 76 |
+
1. 打开 Space 地址,例如 `https://<user>-<space>.hf.space`。
|
| 77 |
+
2. 如果配置了 `APP_PASSWORD`,输入页面访问密码。
|
| 78 |
+
3. 直接填写提示词并生图。若 Space 没有配置服务端 API Key,也可以在右上角 `API 设置` 中填写自己的 API Key 和 API URL。
|
| 79 |
+
4. `NEXT_PUBLIC_IMAGE_STORAGE_MODE=indexeddb` 时,图片结果保存在当前浏览器 IndexedDB。换设备、清理浏览器数据或隐私模式退出后,本地历史可能消失。
|
| 80 |
+
|
| 81 |
+
## 电脑 Agent API 使用
|
| 82 |
+
|
| 83 |
+
先做只读契约检查,不触发真实生图:
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
GPT_IMAGE_PLAYGROUND_URL=https://<user>-<space>.hf.space \
|
| 87 |
+
GPT_IMAGE_AGENT_TOKEN=<agent-token> \
|
| 88 |
+
GPT_IMAGE_AGENT_CONTRACT_CHECK=1 \
|
| 89 |
+
node skills/gpt-image-playground-agent/scripts/generate-image.mjs
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
真实文生图:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
GPT_IMAGE_PLAYGROUND_URL=https://<user>-<space>.hf.space \
|
| 96 |
+
GPT_IMAGE_AGENT_TOKEN=<agent-token> \
|
| 97 |
+
node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
|
| 98 |
+
"a product photo of a ceramic mug on a wooden table"
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
脚本会先读取 `GET /api/agent/capabilities`,再调用 Agent API。成功响应会保留相对 `content_url`,同时补充 `absolute_content_url` 和 `absolute_metadata_url`,便于在桌面环境直接下载产物。
|
| 102 |
+
|
| 103 |
+
## 本地 HF 近似 smoke
|
| 104 |
+
|
| 105 |
+
提交前运行:
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
npm run smoke:hf-space
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
该命令会:
|
| 112 |
+
|
| 113 |
+
- 使用 `NEXT_PUBLIC_IMAGE_STORAGE_MODE=indexeddb` 构建 Docker 镜像。
|
| 114 |
+
- 以 `AGENT_STATE_BACKEND=memory` 启动临时容器。
|
| 115 |
+
- 用手机 User-Agent 检查首页可访问。
|
| 116 |
+
- 检查 `/api/agent/capabilities` 返回 `state_backend=memory` 和 `image_storage_mode=indexeddb`。
|
| 117 |
+
- 执行 Agent 生成和编辑脚本的契约检查,不触发真实上游生图。
|
| 118 |
+
|
| 119 |
+
## 免费层限制
|
| 120 |
+
|
| 121 |
+
- Hugging Face 免费 CPU Basic 适合公开演示和轻量使用,不适合长期高并发。
|
| 122 |
+
- CPU Basic 免费层在长时间无访问后会休眠;需要真正永不休眠或自定义 sleep time 时,应升级到付费硬件。
|
| 123 |
+
- Docker Space 重启后容器磁盘写入会丢失。`memory` 状态后端的 Agent 幂等记录、replay 状态和分享元数据也会丢失。
|
| 124 |
+
- Agent API 仍会把产物图片写入容器临时文件系统,以便提供 `content_url` 下载。重启后这些链接不保证继续有效。
|
| 125 |
+
- 需要长期保存图片、分享链接或 Agent replay 状态时,不应使用免费层纯内存模式。应切换到 PostgreSQL 加持久卷或外部对象存储。
|
| 126 |
+
|
| 127 |
+
## 免费层 Keepalive
|
| 128 |
+
|
| 129 |
+
本仓库提供 GitHub Actions 定时 keepalive,降低 CPU Basic 因长时间无访问进入休眠的概率:
|
| 130 |
+
|
| 131 |
+
- 工作流文件:`.github/workflows/hf-space-keepalive.yml`
|
| 132 |
+
- 默认频率:每 6 小时一次,可手动触发 `workflow_dispatch`
|
| 133 |
+
- 默认目标:`https://misonl-gpt-image-playground-customer.hf.space/api/auth-status`
|
| 134 |
+
- 行为边界:只访问只读鉴权状态端点,不携带 `APP_PASSWORD`、`AGENT_API_TOKEN` 或 OpenAI Key,不触发生图、不访问 Agent 生成接口。
|
| 135 |
+
|
| 136 |
+
如果 Space 地址变化,在 GitHub 仓库 Variables 中设置:
|
| 137 |
+
|
| 138 |
+
```text
|
| 139 |
+
HF_SPACE_KEEPALIVE_URL=https://<user>-<space>.hf.space
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
本地手动验证:
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
HF_SPACE_KEEPALIVE_URL=https://<user>-<space>.hf.space \
|
| 146 |
+
HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED=true \
|
| 147 |
+
npm run keepalive:hf-space
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
注意:keepalive 是免费层的 best-effort 机制,不能保证绕过 Hugging Face 平台维护、重启或政策限制。若需要平台级保证,应升级到付费硬件并设置永不休眠。
|
| 151 |
+
|
| 152 |
+
## 验证门禁
|
| 153 |
+
|
| 154 |
+
最小验证:
|
| 155 |
+
|
| 156 |
+
```bash
|
| 157 |
+
npm test
|
| 158 |
+
npm run lint
|
| 159 |
+
npm run build
|
| 160 |
+
npm run keepalive:hf-space
|
| 161 |
+
npm run smoke:hf-space
|
| 162 |
+
git diff --check
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
真实 Hugging Face gate:
|
| 166 |
+
|
| 167 |
+
1. 推送到 Space 仓库后等待构建完成。
|
| 168 |
+
2. 手机打开 Space 页面,确认能进入页面并发起一次真实生成。
|
| 169 |
+
3. 电脑执行 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 契约检查。
|
| 170 |
+
4. 如有可用测试额度,再执行一次真实 Agent 生成。
|
| 171 |
+
5. 重启 Space 后确认旧 Agent replay 和旧临时产物丢失符合预期。
|
package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
"postbuild": "node scripts/patch-standalone-runtime.mjs",
|
| 10 |
"test": "node --test --import tsx \"src/**/*.test.ts\"",
|
| 11 |
"test:postgres": "node scripts/test-postgres-live.mjs",
|
|
|
|
| 12 |
"smoke:hf-space": "node scripts/smoke-hf-space-memory.mjs",
|
| 13 |
"start": "node scripts/start-standalone.mjs",
|
| 14 |
"lint": "eslint src",
|
|
|
|
| 9 |
"postbuild": "node scripts/patch-standalone-runtime.mjs",
|
| 10 |
"test": "node --test --import tsx \"src/**/*.test.ts\"",
|
| 11 |
"test:postgres": "node scripts/test-postgres-live.mjs",
|
| 12 |
+
"keepalive:hf-space": "node scripts/keepalive-hf-space.mjs",
|
| 13 |
"smoke:hf-space": "node scripts/smoke-hf-space-memory.mjs",
|
| 14 |
"start": "node scripts/start-standalone.mjs",
|
| 15 |
"lint": "eslint src",
|
scripts/keepalive-hf-space.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
const DEFAULT_SPACE_URL = 'https://misonl-gpt-image-playground-customer.hf.space';
|
| 4 |
+
const DEFAULT_KEEPALIVE_PATH = '/api/auth-status';
|
| 5 |
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
| 6 |
+
const MIN_TIMEOUT_MS = 1_000;
|
| 7 |
+
|
| 8 |
+
function readPositiveIntegerEnv(name, fallback) {
|
| 9 |
+
const rawValue = process.env[name]?.trim();
|
| 10 |
+
if (!rawValue) return fallback;
|
| 11 |
+
const value = Number.parseInt(rawValue, 10);
|
| 12 |
+
if (!Number.isSafeInteger(value) || value < MIN_TIMEOUT_MS) {
|
| 13 |
+
throw new Error(`${name} must be an integer greater than or equal to ${MIN_TIMEOUT_MS}`);
|
| 14 |
+
}
|
| 15 |
+
return value;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function normalizeUrl(rawUrl, path) {
|
| 19 |
+
const baseUrl = new URL(rawUrl);
|
| 20 |
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
| 21 |
+
return new URL(normalizedPath, baseUrl).toString();
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function readExpectedPasswordRequired() {
|
| 25 |
+
const rawValue = process.env.HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED?.trim();
|
| 26 |
+
if (!rawValue) return undefined;
|
| 27 |
+
if (rawValue === 'true') return true;
|
| 28 |
+
if (rawValue === 'false') return false;
|
| 29 |
+
throw new Error('HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED must be true or false');
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
async function readJsonResponse(response) {
|
| 33 |
+
const text = await response.text();
|
| 34 |
+
if (!text) return undefined;
|
| 35 |
+
try {
|
| 36 |
+
return JSON.parse(text);
|
| 37 |
+
} catch {
|
| 38 |
+
throw new Error(`Keepalive endpoint returned non-JSON body: ${text.slice(0, 200)}`);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
async function pingKeepaliveEndpoint() {
|
| 43 |
+
const spaceUrl = process.env.HF_SPACE_KEEPALIVE_URL?.trim() || DEFAULT_SPACE_URL;
|
| 44 |
+
const path = process.env.HF_SPACE_KEEPALIVE_PATH?.trim() || DEFAULT_KEEPALIVE_PATH;
|
| 45 |
+
const timeoutMs = readPositiveIntegerEnv('HF_SPACE_KEEPALIVE_TIMEOUT_MS', DEFAULT_TIMEOUT_MS);
|
| 46 |
+
const expectedPasswordRequired = readExpectedPasswordRequired();
|
| 47 |
+
const url = normalizeUrl(spaceUrl, path);
|
| 48 |
+
const controller = new AbortController();
|
| 49 |
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
| 50 |
+
const startedAt = Date.now();
|
| 51 |
+
|
| 52 |
+
try {
|
| 53 |
+
const response = await fetch(url, {
|
| 54 |
+
headers: {
|
| 55 |
+
'User-Agent': 'gpt-image-playground-keepalive/1.0'
|
| 56 |
+
},
|
| 57 |
+
signal: controller.signal
|
| 58 |
+
});
|
| 59 |
+
const elapsedMs = Date.now() - startedAt;
|
| 60 |
+
const body = await readJsonResponse(response);
|
| 61 |
+
|
| 62 |
+
if (!response.ok) {
|
| 63 |
+
throw new Error(`Keepalive endpoint failed with HTTP ${response.status}`);
|
| 64 |
+
}
|
| 65 |
+
if (expectedPasswordRequired !== undefined && body?.passwordRequired !== expectedPasswordRequired) {
|
| 66 |
+
throw new Error(`passwordRequired expected ${expectedPasswordRequired}, got ${body?.passwordRequired}`);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
console.log(
|
| 70 |
+
JSON.stringify(
|
| 71 |
+
{
|
| 72 |
+
ok: true,
|
| 73 |
+
url,
|
| 74 |
+
status: response.status,
|
| 75 |
+
elapsedMs,
|
| 76 |
+
passwordRequired: body?.passwordRequired
|
| 77 |
+
},
|
| 78 |
+
null,
|
| 79 |
+
2
|
| 80 |
+
)
|
| 81 |
+
);
|
| 82 |
+
} finally {
|
| 83 |
+
clearTimeout(timeout);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
pingKeepaliveEndpoint().catch((error) => {
|
| 88 |
+
console.error(
|
| 89 |
+
JSON.stringify(
|
| 90 |
+
{
|
| 91 |
+
ok: false,
|
| 92 |
+
error: error instanceof Error ? error.message : String(error)
|
| 93 |
+
},
|
| 94 |
+
null,
|
| 95 |
+
2
|
| 96 |
+
)
|
| 97 |
+
);
|
| 98 |
+
process.exit(1);
|
| 99 |
+
});
|