Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """一键将当前前端发布到 Hugging Face Space(static/gradio/docker).""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from pathlib import Path | |
| from typing import Iterable | |
| HF_ENDPOINT = "https://huggingface.co" | |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| DEFAULT_PUBLISH_FILES = ["index.html", "styles.css", "app.js", "README.md"] | |
| class DeployError(RuntimeError): | |
| """部署失败错误。""" | |
| def api_request(path: str, token: str, method: str = "GET", payload: dict | None = None) -> dict: | |
| url = f"{HF_ENDPOINT}{path}" | |
| data = None | |
| headers = {"Authorization": f"Bearer {token}"} | |
| if payload is not None: | |
| headers["Content-Type"] = "application/json" | |
| data = json.dumps(payload).encode("utf-8") | |
| req = urllib.request.Request(url, data=data, headers=headers, method=method) | |
| try: | |
| with urllib.request.urlopen(req, timeout=30) as resp: | |
| body = resp.read().decode("utf-8") | |
| return json.loads(body) if body else {} | |
| except urllib.error.HTTPError as exc: | |
| body = exc.read().decode("utf-8", errors="ignore") | |
| raise DeployError(f"{method} {path} 失败: {exc.code} {body}") from exc | |
| def run(cmd: list[str], cwd: str | None = None) -> None: | |
| subprocess.run(cmd, check=True, cwd=cwd) | |
| def get_namespace(token: str) -> str: | |
| whoami = api_request("/api/whoami-v2", token) | |
| name = whoami.get("name") | |
| if not name: | |
| raise DeployError("无法从 /api/whoami-v2 获取用户名") | |
| return name | |
| def build_create_repo_payload(space_name: str, organization: str | None, private: bool, sdk: str) -> dict: | |
| return { | |
| "type": "space", | |
| "name": space_name, | |
| "organization": organization, | |
| "private": private, | |
| "sdk": sdk, | |
| } | |
| def resolve_repo_id(token: str, space_name: str, organization: str | None, private: bool, sdk: str) -> str: | |
| namespace = organization or get_namespace(token) | |
| payload = build_create_repo_payload( | |
| space_name=space_name, | |
| organization=organization, | |
| private=private, | |
| sdk=sdk, | |
| ) | |
| try: | |
| api_request("/api/repos/create", token, method="POST", payload=payload) | |
| print(f"✅ 已创建 Space: {namespace}/{space_name}") | |
| except DeployError as exc: | |
| msg = str(exc).lower() | |
| if "409" in msg or "already exists" in msg: | |
| print(f"ℹ️ Space 已存在,继续发布: {namespace}/{space_name}") | |
| else: | |
| raise | |
| return f"{namespace}/{space_name}" | |
| def resolve_publish_files(extra_files: Iterable[str] | None = None) -> list[str]: | |
| files = list(DEFAULT_PUBLISH_FILES) | |
| if extra_files: | |
| for item in extra_files: | |
| if item and item not in files: | |
| files.append(item) | |
| return files | |
| def publish( | |
| repo_id: str, | |
| token: str, | |
| publish_files: list[str], | |
| message: str, | |
| force_push: bool, | |
| branch: str, | |
| ) -> None: | |
| with tempfile.TemporaryDirectory(prefix="hf-space-") as tmp: | |
| for rel in publish_files: | |
| src = PROJECT_ROOT / rel | |
| if not src.exists(): | |
| raise FileNotFoundError(f"缺少发布文件: {src}") | |
| dst = Path(tmp) / rel | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, dst) | |
| run(["git", "init"], cwd=tmp) | |
| run(["git", "checkout", "-b", branch], cwd=tmp) | |
| run(["git", "config", "user.name", "my-ai-coder"], cwd=tmp) | |
| run(["git", "config", "user.email", "bot@local"], cwd=tmp) | |
| run(["git", "add", "."], cwd=tmp) | |
| run(["git", "commit", "-m", message], cwd=tmp) | |
| safe_token = urllib.parse.quote(token, safe="") | |
| remote = f"https://user:{safe_token}@huggingface.co/spaces/{repo_id}" | |
| run(["git", "remote", "add", "origin", remote], cwd=tmp) | |
| push_cmd = ["git", "push", "-u", "origin", branch] | |
| if force_push: | |
| push_cmd.append("--force") | |
| run(push_cmd, cwd=tmp) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="一键发布到 Hugging Face Space") | |
| parser.add_argument("--space", required=True, help="Space 名称,例如 my-omni-ai-studio") | |
| parser.add_argument("--token", default=None, help="HF Token,默认读取 HF_TOKEN 或 HUGGING_FACE_HUB_TOKEN") | |
| parser.add_argument("--org", default=None, help="组织名(可选),不填则发布到个人账号") | |
| parser.add_argument("--private", action="store_true", help="创建私有 Space") | |
| parser.add_argument("--sdk", default="static", choices=["static", "gradio", "docker"], help="Space SDK 类型") | |
| parser.add_argument("--branch", default="main", help="目标分支名,默认 main") | |
| parser.add_argument("--message", default="Deploy My Omni AI Studio", help="部署提交信息") | |
| parser.add_argument("--include", action="append", default=[], help="额外发布文件(可多次传入)") | |
| parser.add_argument("--no-force", action="store_true", help="关闭默认 --force 推送") | |
| return parser.parse_args() | |
| def resolve_token(token_arg: str | None) -> str | None: | |
| if token_arg: | |
| return token_arg | |
| return os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN") | |
| def main() -> int: | |
| args = parse_args() | |
| token = resolve_token(args.token) | |
| if not token: | |
| print("❌ 缺少 Token,请通过 --token 或 HF_TOKEN/HUGGING_FACE_HUB_TOKEN 提供。", file=sys.stderr) | |
| return 1 | |
| publish_files = resolve_publish_files(args.include) | |
| repo_id = resolve_repo_id(token, args.space, args.org, args.private, args.sdk) | |
| publish( | |
| repo_id=repo_id, | |
| token=token, | |
| publish_files=publish_files, | |
| message=args.message, | |
| force_push=not args.no_force, | |
| branch=args.branch, | |
| ) | |
| print(f"🚀 发布完成: https://huggingface.co/spaces/{repo_id}") | |
| print(f"📦 发布文件: {', '.join(publish_files)}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |