File size: 4,532 Bytes
71912ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""七个空间共用的可恢复任务客户端;仅 URL 和请求 JSON 随空间变化。"""

import argparse
import json
import os
import time
import uuid
from pathlib import Path

import httpx


def main():
    """提交或恢复任务,轮询终态后通过 result_url 保存结果。

    Returns:
        下载成功后结束;状态文件保留幂等键和任务 ID,便于断线后继续。
    """
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--url", default="https://idlecloudx-moss-image-base.hf.space")
    parser.add_argument("--request", type=Path, required=True)
    parser.add_argument("--state", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--timeout", type=int, default=900)
    args = parser.parse_args()
    api_key = os.environ.get("JOB_API_KEY")
    if not api_key:
        raise SystemExit("请设置 JOB_API_KEY;HF_TOKEN 可选,游客无需设置。")
    payload = json.loads(args.request.read_text(encoding="utf-8-sig"))
    base_url = args.url.rstrip("/")
    if args.state.exists():
        state = json.loads(args.state.read_text(encoding="utf-8"))
        if state["payload"] != payload or state["base_url"] != base_url:
            raise SystemExit("状态文件属于另一请求,请为新任务使用新的 --state 路径。")
    else:
        state = {"base_url": base_url, "payload": payload, "idempotency_key": uuid.uuid4().hex}
        # 先保存幂等键,提交响应丢失时仍能安全恢复同一个任务。
        args.state.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
    headers = {"X-API-Key": api_key, "Idempotency-Key": state["idempotency_key"]}
    hf_token = os.environ.get("HF_TOKEN", "").strip()
    if hf_token:
        headers["Authorization"] = f"Bearer {hf_token}"
    deadline = time.monotonic() + args.timeout
    # 下载只使用 URL 自带的令牌,不把任务密钥或 HF_TOKEN 发送到结果地址。
    with httpx.Client(headers=headers, timeout=30) as client, httpx.Client(timeout=120) as downloader:
        while time.monotonic() < deadline:
            downloading = "result_url" in state
            try:
                if downloading:
                    response = downloader.get(state["result_url"])
                elif "job_id" not in state:
                    response = client.post(f"{base_url}/api/jobs", json=payload)
                else:
                    response = client.get(f"{base_url}/api/jobs/{state['job_id']}")
                is_json = "application/json" in response.headers.get("content-type", "")
                # 业务失败是终态;只对限流、重启或传输故障重试。
                if not downloading and is_json and response.status_code == 500:
                    failure = response.json()
                    if failure.get("status") == "failed":
                        raise SystemExit(json.dumps(failure["error"], ensure_ascii=False))
                if response.status_code in {408, 429, 500, 502, 503, 504}:
                    time.sleep(min(30, max(2, int(response.headers.get("Retry-After", "5")))))
                    continue
                response.raise_for_status()
                # HF 启动等待页可能以 HTTP 200 返回 HTML。
                if "text/html" in response.headers.get("content-type", ""):
                    time.sleep(5)
                    continue
            except httpx.TransportError:
                time.sleep(3)
                continue
            if downloading:
                args.output.write_bytes(response.content)
                print(f"已保存:{args.output.resolve()}")
                return
            body = response.json()
            if "job_id" not in state:
                state["job_id"] = body["job_id"]
                print(f"任务已提交:{state['job_id']}", flush=True)
            elif body["status"] in {"queued", "running"}:
                time.sleep(int(response.headers.get("Retry-After", "2")))
                continue
            elif body["status"] == "failed":
                raise SystemExit(json.dumps(body["error"], ensure_ascii=False))
            else:
                state["result_url"] = body["result_url"]
            args.state.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
    raise SystemExit("等待超时;用相同命令和状态文件继续,不会重复生成。")


if __name__ == "__main__":
    main()