| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const LOGIN_PATH = "/admin/login" |
| const CHECK_ALL_PATH = "/admin/proxies/check-all" |
|
|
| |
| |
| const DEFAULT_HF_SPACE_REPO = "a3216/ds2api" |
| |
| const RESTART_COOLDOWN_MS = 5 * 60 * 1000 |
| |
| const LAST_RESTART_KV_KEY = "last_restart" |
|
|
| function vercelHeaders(env) { |
| const headers = {} |
| const bypass = env.VERCEL_BYPASS_TOKEN |
| if (bypass) { |
| headers["x-vercel-protection-bypass"] = bypass |
| } |
| return headers |
| } |
|
|
| async function login(baseURL, adminKey, extraHeaders) { |
| const res = await fetch(`${baseURL}${LOGIN_PATH}`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json", ...extraHeaders }, |
| body: JSON.stringify({ admin_key: adminKey, expire_hours: 1 }), |
| }) |
| if (!res.ok) { |
| const text = await res.text() |
| throw new Error(`login failed (${res.status}): ${text}`) |
| } |
| const data = await res.json() |
| return data.token |
| } |
|
|
| async function checkAll(baseURL, token, extraHeaders) { |
| const res = await fetch(`${baseURL}${CHECK_ALL_PATH}`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Authorization: `Bearer ${token}`, |
| ...extraHeaders, |
| }, |
| }) |
| if (!res.ok) { |
| const text = await res.text() |
| throw new Error(`check-all failed (${res.status}): ${text}`) |
| } |
| return res.json() |
| } |
|
|
| async function runCheck(env) { |
| const baseURL = (env.DS2API_BASE_URL || "").replace(/\/+$/, "") |
| const adminKey = env.ADMIN_KEY || "" |
|
|
| if (!baseURL || !adminKey) { |
| throw new Error("DS2API_BASE_URL and ADMIN_KEY secrets must be set") |
| } |
|
|
| const extraHeaders = vercelHeaders(env) |
| const token = await login(baseURL, adminKey, extraHeaders) |
| const result = await checkAll(baseURL, token, extraHeaders) |
| return result |
| } |
|
|
| |
|
|
| |
| async function fetchSpaceRuntime(repo, token) { |
| const url = `https://huggingface.co/api/spaces/${repo}/runtime` |
| const res = await fetch(url, { |
| headers: { Authorization: `Bearer ${token}` }, |
| }) |
| if (!res.ok) { |
| const text = await res.text() |
| throw new Error(`runtime query failed (${res.status}): ${text}`) |
| } |
| const data = await res.json() |
| return { |
| stage: data.stage, |
| hardwareCurrent: data.hardware ? data.hardware.current : null, |
| } |
| } |
|
|
| |
| function isSpaceStuck(runtime) { |
| if (!runtime) return true |
| if (runtime.stage !== "RUNNING") return true |
| if (!runtime.hardwareCurrent) return true |
| return false |
| } |
|
|
| |
| async function restartSpace(repo, token) { |
| const url = `https://huggingface.co/api/spaces/${repo}/restart` |
| const res = await fetch(url, { |
| method: "POST", |
| headers: { Authorization: `Bearer ${token}` }, |
| }) |
| if (!res.ok) { |
| const text = await res.text() |
| throw new Error(`restart failed (${res.status}): ${text}`) |
| } |
| return true |
| } |
|
|
| |
| async function getLastRestart(kv) { |
| if (!kv) return 0 |
| try { |
| const v = await kv.get(LAST_RESTART_KV_KEY) |
| const n = parseInt(v, 10) |
| return Number.isFinite(n) ? n : 0 |
| } catch { |
| return 0 |
| } |
| } |
|
|
| async function setLastRestart(kv) { |
| if (!kv) return |
| try { |
| await kv.put(LAST_RESTART_KV_KEY, String(Date.now())) |
| } catch { |
| |
| } |
| } |
|
|
| |
| async function runSpaceCheck(env) { |
| const token = env.HF_TOKEN || "" |
| if (!token) { |
| throw new Error("HF_TOKEN secret must be set for space self-healing") |
| } |
| const repo = (env.HF_SPACE_REPO || DEFAULT_HF_SPACE_REPO).trim() |
|
|
| const runtime = await fetchSpaceRuntime(repo, token) |
| if (!isSpaceStuck(runtime)) { |
| return { |
| ok: true, |
| action: "none", |
| repo, |
| stage: runtime.stage, |
| hardwareCurrent: runtime.hardwareCurrent, |
| message: "space healthy, no action", |
| } |
| } |
|
|
| |
| const last = await getLastRestart(env.SPACE_STATE) |
| const elapsed = Date.now() - last |
| if (elapsed < RESTART_COOLDOWN_MS) { |
| const waitMs = RESTART_COOLDOWN_MS - elapsed |
| return { |
| ok: true, |
| action: "cooldown", |
| repo, |
| stage: runtime.stage, |
| hardwareCurrent: runtime.hardwareCurrent, |
| message: `space stuck but in restart cooldown, wait ${Math.ceil(waitMs / 1000)}s`, |
| } |
| } |
|
|
| |
| await restartSpace(repo, token) |
| await setLastRestart(env.SPACE_STATE) |
| return { |
| ok: true, |
| action: "restarted", |
| repo, |
| stage: runtime.stage, |
| hardwareCurrent: runtime.hardwareCurrent, |
| message: "space was stuck, restart triggered", |
| } |
| } |
|
|
| |
| |
| function isSpaceCheckCron(cron) { |
| return cron === "*/10 * * * *" |
| } |
|
|
| export default { |
| async scheduled(event, env, ctx) { |
| if (isSpaceCheckCron(event.cron)) { |
| ctx.waitUntil( |
| runSpaceCheck(env) |
| .then((r) => { |
| if (r.action === "restarted") { |
| console.warn(`[space] ${r.message} repo=${r.repo} stage=${r.stage} hardware=${r.hardwareCurrent}`) |
| } else { |
| console.log(`[space] ${r.message} repo=${r.repo} stage=${r.stage} hardware=${r.hardwareCurrent}`) |
| } |
| }) |
| .catch((err) => { |
| console.error(`[space] check failed: ${err.message}`) |
| }) |
| ) |
| return |
| } |
|
|
| |
| ctx.waitUntil( |
| runCheck(env) |
| .then((result) => { |
| const items = result.items || [] |
| const healthy = items.filter((i) => i.healthy && !i.disabled).length |
| const banned = items.filter((i) => i.disabled).length |
| console.log( |
| `proxy health check done: ${healthy}/${items.length} healthy, ${banned} banned` |
| ) |
| }) |
| .catch((err) => { |
| console.error(`proxy health check failed: ${err.message}`) |
| }) |
| ) |
| }, |
|
|
| async fetch(request, env) { |
| const url = new URL(request.url) |
|
|
| if (url.pathname === "/check") { |
| try { |
| const result = await runCheck(env) |
| return Response.json(result) |
| } catch (err) { |
| return Response.json({ error: err.message }, { status: 500 }) |
| } |
| } |
|
|
| if (url.pathname === "/check-space") { |
| try { |
| const result = await runSpaceCheck(env) |
| return Response.json(result) |
| } catch (err) { |
| return Response.json({ error: err.message }, { status: 500 }) |
| } |
| } |
|
|
| return new Response("not found", { status: 404 }) |
| }, |
| } |
|
|