hern0425 commited on
Commit
f4dff88
·
verified ·
1 Parent(s): 40fb5dc

Deploy from App

Browse files
Files changed (6) hide show
  1. .dockerignore +14 -0
  2. CONFIG.md +116 -0
  3. Dockerfile +22 -0
  4. README.md +3 -3
  5. bootstrap.py +198 -0
  6. requirements.txt +4 -0
.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .venv
4
+ __pycache__/
5
+ *.pyc
6
+ *.pyo
7
+ *.pyd
8
+ *.log
9
+ llmdoc/
10
+ __MACOSX/
11
+ dist/
12
+ .tmp-runtime-stage/
13
+ .tmp-runtime/
14
+ .tmp-bootstrap/
CONFIG.md ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HF Space 配置说明
2
+
3
+ ## 1. 部署模式
4
+
5
+ 当前 `_hf` 目录采用以下模式:
6
+
7
+ - Docker 镜像只包含运行环境和 `bootstrap.py`
8
+ - 不内嵌业务脚本(`openai.py` / `main.py` / `claude_compat.py`)
9
+ - 容器启动时通过环境变量下载 zip 脚本包并运行
10
+
11
+ 如果未配置下载地址,启动会直接失败(这是预期行为)。
12
+
13
+ ## 2. 必填环境变量
14
+
15
+ - `SCRIPT_ZIP_URL`
16
+ - 脚本 zip 下载地址
17
+ - 例如对象存储直链、带鉴权的下载地址
18
+
19
+ - `SCRIPT_ZIP_SHA256`(推荐)
20
+ - zip 文件的 SHA256 校验值
21
+ - 与 `build_runtime_zip.py` 输出一致
22
+
23
+ 说明:
24
+
25
+ - 若不使用 `SCRIPT_ZIP_SHA256`,必须设置 `ALLOW_UNVERIFIED_ZIP=1`
26
+ - 生产环境强烈建议始终校验 SHA256
27
+
28
+ ## 3. 入口与解压相关变量
29
+
30
+ - `SCRIPT_EXTRACT_DIR`(默认 `/opt/runtime`)
31
+ - zip 解压后的运行根目录
32
+
33
+ - `SCRIPT_TMP_DIR`(默认 `/opt/runtime/.tmp`)
34
+ - 下载与中间临时文件目录
35
+
36
+ - `SCRIPT_WORKDIR`(默认 `app`)
37
+ - 入口脚本所在目录(相对于解压根目录)
38
+
39
+ - `SCRIPT_ENTRY`(默认 `openai.py`)
40
+ - 入口脚本文件名
41
+
42
+ 默认入口等价于:
43
+
44
+ - `/opt/runtime/current/app/openai.py`
45
+
46
+ 如果你的 zip 结构不同,请调整 `SCRIPT_WORKDIR` 和 `SCRIPT_ENTRY`。
47
+
48
+ ## 4. 鉴权相关变量(按需)
49
+
50
+ - `SCRIPT_ZIP_TOKEN`
51
+ - 下载时自动添加 `Authorization: Bearer <token>`
52
+
53
+ - `SCRIPT_ZIP_HEADER`
54
+ - 自定义单个请求头,格式:`Key: Value`
55
+
56
+ ## 5. 应用运行变量(建议)
57
+
58
+ - `PORT=7860`
59
+ - `POOL_MIN_SIZE=1`
60
+ - `POOL_MAX_SIZE=4`
61
+ - `ALLOW_UNVERIFIED_ZIP=0`
62
+
63
+ 可选:
64
+
65
+ - `LOG_LEVEL=INFO`
66
+ - `HTTP_DEBUG=0`
67
+ - `POOL_INIT_BLOCKING=0`
68
+
69
+ ## 6. zip 包内容要求
70
+
71
+ 默认需要包含以下路径:
72
+
73
+ - `app/openai.py`
74
+ - `app/main.py`
75
+ - `app/claude_compat.py`
76
+
77
+ 你也可以自定义结构,但必须与 `SCRIPT_WORKDIR` + `SCRIPT_ENTRY` 对齐。
78
+
79
+ ## 7. 生成 zip 包
80
+
81
+ 在项目根目录执行:
82
+
83
+ ```bash
84
+ python build_runtime_zip.py
85
+ ```
86
+
87
+ 输出:
88
+
89
+ - `dist/runtime-payload.zip`
90
+ - `sha256=<...>`
91
+
92
+ 将 zip 上传到可下载地址后,把对应 URL 和 SHA256 填到 HF Space Variables。
93
+
94
+ ## 8. HF Space 最小配置清单
95
+
96
+ 建议至少配置:
97
+
98
+ - `SCRIPT_ZIP_URL=<your zip url>`
99
+ - `SCRIPT_ZIP_SHA256=<sha256>`
100
+ - `PORT=7860`
101
+ - `POOL_MIN_SIZE=1`
102
+ - `POOL_MAX_SIZE=4`
103
+
104
+ ## 9. 常见问题排查
105
+
106
+ - 启动报 `SCRIPT_ZIP_URL is required`
107
+ - 未配置 `SCRIPT_ZIP_URL`
108
+
109
+ - 启动报 `SHA256 mismatch`
110
+ - `SCRIPT_ZIP_SHA256` 与实际 zip 不一致,重新计算并更新
111
+
112
+ - 启动报 `Entry script not found`
113
+ - zip 内路径与 `SCRIPT_WORKDIR` / `SCRIPT_ENTRY` 不匹配
114
+
115
+ - 下载 401/403
116
+ - 检查 `SCRIPT_ZIP_TOKEN` 或 `SCRIPT_ZIP_HEADER` 是否正确
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PORT=7860 \
6
+ SCRIPT_ENTRY=openai.py \
7
+ SCRIPT_WORKDIR=app \
8
+ SCRIPT_EXTRACT_DIR=/opt/runtime \
9
+ SCRIPT_TMP_DIR=/opt/runtime/.tmp
10
+
11
+ WORKDIR /app
12
+
13
+ COPY requirements.txt /app/requirements.txt
14
+
15
+ RUN pip install --no-cache-dir --upgrade pip \
16
+ && pip install --no-cache-dir -r /app/requirements.txt
17
+
18
+ COPY . /app
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["python", "bootstrap.py"]
README.md CHANGED
@@ -1,7 +1,7 @@
1
  ---
2
- title: Zai
3
- emoji: 🏢
4
- colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
 
1
  ---
2
+ title: Zv2
3
+ emoji: 🌖
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
bootstrap.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Runtime bootstrapper.
3
+
4
+ Goal:
5
+ - Keep the Docker image stable (only runtime deps + this bootstrapper).
6
+ - At container startup, optionally download a zip payload, extract it, then run the entry script.
7
+
8
+ Security notes:
9
+ - Running remote code is risky. Prefer pinning with SCRIPT_ZIP_SHA256.
10
+ - Extraction is protected against ZipSlip path traversal.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import time
21
+ import urllib.request
22
+ import uuid
23
+ import zipfile
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+
28
+ def _e(name: str, default: str | None = None) -> str | None:
29
+ v = os.getenv(name)
30
+ if v is None or v == "":
31
+ return default
32
+ return v
33
+
34
+
35
+ def _sha256_file(path: Path) -> str:
36
+ h = hashlib.sha256()
37
+ with path.open("rb") as f:
38
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
39
+ h.update(chunk)
40
+ return h.hexdigest()
41
+
42
+
43
+ def _safe_extract_zip(zip_path: Path, dest_dir: Path) -> None:
44
+ dest_dir.mkdir(parents=True, exist_ok=True)
45
+ dest_real = dest_dir.resolve()
46
+
47
+ with zipfile.ZipFile(zip_path) as zf:
48
+ for info in zf.infolist():
49
+ # Guard against ZipSlip: normalize and ensure within dest.
50
+ target = dest_dir / info.filename
51
+ try:
52
+ target_real = target.resolve()
53
+ except FileNotFoundError:
54
+ # Parent may not exist yet; resolve the parent.
55
+ target_real = (dest_dir / Path(info.filename).as_posix()).resolve()
56
+
57
+ if dest_real not in target_real.parents and target_real != dest_real:
58
+ raise RuntimeError(f"Unsafe zip entry path: {info.filename}")
59
+
60
+ zf.extractall(dest_dir)
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class BootstrapConfig:
65
+ zip_url: str | None
66
+ zip_sha256: str | None
67
+ allow_unverified: bool
68
+ extract_base: Path
69
+ tmp_dir: Path
70
+ entry_relpath: str
71
+ download_timeout_s: float
72
+ bearer_token: str | None
73
+ header_kv: str | None
74
+
75
+
76
+ def _load_config() -> BootstrapConfig:
77
+ zip_url = _e("SCRIPT_ZIP_URL")
78
+ zip_sha256 = _e("SCRIPT_ZIP_SHA256")
79
+ allow_unverified = _e("ALLOW_UNVERIFIED_ZIP", "0") == "1"
80
+ extract_base = Path(_e("SCRIPT_EXTRACT_DIR", "/opt/runtime") or "/opt/runtime")
81
+ tmp_dir = Path(_e("SCRIPT_TMP_DIR", str(extract_base / ".tmp")) or str(extract_base / ".tmp"))
82
+
83
+ # Where to run inside the extracted payload.
84
+ # Recommended: zip contains folder "app/" with openai.py inside.
85
+ workdir = _e("SCRIPT_WORKDIR", "app") or ""
86
+ entry = _e("SCRIPT_ENTRY", "openai.py") or "openai.py"
87
+ entry_relpath = str(Path(workdir) / entry) if workdir else entry
88
+
89
+ download_timeout_s = float(_e("SCRIPT_DOWNLOAD_TIMEOUT", "60") or "60")
90
+
91
+ # Auth/header options:
92
+ # - SCRIPT_ZIP_TOKEN: bearer token
93
+ # - HF_TOKEN: commonly present on Spaces; used as fallback
94
+ bearer_token = _e("SCRIPT_ZIP_TOKEN") or _e("HF_TOKEN")
95
+ # - SCRIPT_ZIP_HEADER: raw "Key: Value" header line, optional
96
+ header_kv = _e("SCRIPT_ZIP_HEADER")
97
+
98
+ return BootstrapConfig(
99
+ zip_url=zip_url,
100
+ zip_sha256=zip_sha256,
101
+ allow_unverified=allow_unverified,
102
+ extract_base=extract_base,
103
+ tmp_dir=tmp_dir,
104
+ entry_relpath=entry_relpath,
105
+ download_timeout_s=download_timeout_s,
106
+ bearer_token=bearer_token,
107
+ header_kv=header_kv,
108
+ )
109
+
110
+
111
+ def _download_zip(cfg: BootstrapConfig, out_path: Path) -> None:
112
+ assert cfg.zip_url
113
+ req = urllib.request.Request(cfg.zip_url, method="GET")
114
+ if cfg.bearer_token:
115
+ req.add_header("Authorization", f"Bearer {cfg.bearer_token}")
116
+ if cfg.header_kv and ":" in cfg.header_kv:
117
+ k, v = cfg.header_kv.split(":", 1)
118
+ req.add_header(k.strip(), v.strip())
119
+
120
+ with urllib.request.urlopen(req, timeout=cfg.download_timeout_s) as resp:
121
+ status = getattr(resp, "status", None)
122
+ if status is None and hasattr(resp, "getcode"):
123
+ status = resp.getcode()
124
+ if isinstance(status, int) and status >= 400:
125
+ raise RuntimeError(f"Download failed: HTTP {status}")
126
+ out_path.parent.mkdir(parents=True, exist_ok=True)
127
+ with out_path.open("wb") as f:
128
+ shutil.copyfileobj(resp, f)
129
+
130
+
131
+ def _atomic_replace_dir(tmp_dir: Path, target_dir: Path) -> None:
132
+ if target_dir.exists():
133
+ shutil.rmtree(target_dir, ignore_errors=True)
134
+ target_dir.parent.mkdir(parents=True, exist_ok=True)
135
+ tmp_dir.rename(target_dir)
136
+
137
+
138
+ def main() -> int:
139
+ cfg = _load_config()
140
+
141
+ # Where the extracted payload will live.
142
+ current_dir = cfg.extract_base / "current"
143
+
144
+ if cfg.zip_url:
145
+ if not cfg.zip_sha256 and not cfg.allow_unverified:
146
+ raise RuntimeError(
147
+ "SCRIPT_ZIP_URL is set but SCRIPT_ZIP_SHA256 is missing. "
148
+ "Set SCRIPT_ZIP_SHA256 (recommended) or ALLOW_UNVERIFIED_ZIP=1."
149
+ )
150
+
151
+ cfg.tmp_dir.mkdir(parents=True, exist_ok=True)
152
+ td_path = cfg.tmp_dir / f"bootstrap_{uuid.uuid4().hex}"
153
+ td_path.mkdir(parents=True, exist_ok=True)
154
+ try:
155
+ zip_path = td_path / "payload.zip"
156
+
157
+ t0 = time.time()
158
+ print(f"[bootstrap] downloading zip from SCRIPT_ZIP_URL to {zip_path}", flush=True)
159
+ _download_zip(cfg, zip_path)
160
+ print(f"[bootstrap] download done in {time.time() - t0:.2f}s size={zip_path.stat().st_size}", flush=True)
161
+
162
+ if cfg.zip_sha256:
163
+ got = _sha256_file(zip_path)
164
+ want = cfg.zip_sha256.lower().strip()
165
+ if got != want:
166
+ raise RuntimeError(f"SHA256 mismatch: got={got} want={want}")
167
+ print("[bootstrap] sha256 verified", flush=True)
168
+ else:
169
+ print("[bootstrap] sha256 not provided; running unverified payload", flush=True)
170
+
171
+ tmp_extract = cfg.extract_base / f".extract_tmp_{uuid.uuid4().hex}"
172
+ if tmp_extract.exists():
173
+ shutil.rmtree(tmp_extract, ignore_errors=True)
174
+
175
+ print(f"[bootstrap] extracting zip to {tmp_extract}", flush=True)
176
+ _safe_extract_zip(zip_path, tmp_extract)
177
+ _atomic_replace_dir(tmp_extract, current_dir)
178
+ print(f"[bootstrap] extracted to {current_dir}", flush=True)
179
+ finally:
180
+ shutil.rmtree(td_path, ignore_errors=True)
181
+
182
+ else:
183
+ raise RuntimeError(
184
+ "SCRIPT_ZIP_URL is required in this HF deployment. "
185
+ "Set SCRIPT_ZIP_URL and SCRIPT_ZIP_SHA256 (recommended)."
186
+ )
187
+
188
+ entry_path = (current_dir / cfg.entry_relpath).resolve()
189
+ if not entry_path.exists():
190
+ raise RuntimeError(f"Entry script not found: {entry_path} (SCRIPT_WORKDIR/SCRIPT_ENTRY)")
191
+
192
+ print(f"[bootstrap] starting: {sys.executable} {entry_path}", flush=True)
193
+ proc = subprocess.run([sys.executable, str(entry_path)], cwd=str(entry_path.parent))
194
+ return int(proc.returncode)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ raise SystemExit(main())
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ httpx
4
+ httpcore