misonL commited on
Commit
78948ff
·
verified ·
1 Parent(s): 9030f57

Add HF Space keepalive workflow

Browse files
.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
+ });