heiyuheiyu commited on
Commit
8581e8a
ยท
verified ยท
1 Parent(s): 13832dc

Upload Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +119 -684
Dockerfile CHANGED
@@ -1,684 +1,119 @@
1
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2
- # Chat2API โ€” HuggingFace Space ็‰ˆ
3
- # ๅŸบไบŽ Node.js๏ผŒ้›†ๆˆ Chat2API ้กน็›ฎ + code-server ๆŒ‰้œ€ๅฏๅœ IDE
4
- # ๆŒไน…ๅŒ–ๅญ˜ๅ‚จ๏ผšHuggingFace Dataset๏ผˆๆฏๅฐๆ—ถ่‡ชๅŠจๅค‡ไปฝ๏ผŒ้‡ๅฏ่‡ชๅŠจๆขๅค๏ผ‰
5
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
6
-
7
- FROM node:24-slim
8
-
9
- # โ”€โ”€ 1. ๅŸบ็ก€็ณป็ปŸไพ่ต– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
- RUN apt-get update && apt-get install -y --no-install-recommends \
11
- git openssh-client build-essential python3 python3-pip \
12
- g++ make ca-certificates curl wget nginx \
13
- && rm -rf /var/lib/apt/lists/*
14
-
15
- # โ”€โ”€ 1.1. ๅฎ‰่ฃ… code-server๏ผˆๆต่งˆๅ™จ็‰ˆ VS Code๏ผŒ่‡ชๅธฆ terminal๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
- RUN curl -fsSL https://code-server.dev/install.sh | sh
17
-
18
- # โ”€โ”€ 2. ๅฎ‰่ฃ… GitHub CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
19
- RUN mkdir -p -m 755 /etc/apt/keyrings \
20
- && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
21
- && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
22
- && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
23
- && mkdir -p -m 755 /etc/apt/sources.list.d \
24
- && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
25
- | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
26
- && apt-get update \
27
- && apt-get install -y --no-install-recommends gh \
28
- && rm -rf /var/lib/apt/lists/* \
29
- && gh --version
30
-
31
- # โ”€โ”€ 3. ๅฎ‰่ฃ… HuggingFace Hub Python ๅฎขๆˆท็ซฏ๏ผˆ็”จไบŽ Dataset ๆŒไน…ๅŒ–๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
- RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages
33
-
34
- # โ”€โ”€ 4. ็ŽฏๅขƒไธŽ Git ้…็ฝฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
- RUN update-ca-certificates && \
36
- git config --global http.sslVerify false && \
37
- git config --global url."https://github.com/".insteadOf ssh://git@github.com/
38
-
39
- # โ”€โ”€ 5. ๅ…‹้š†ๅนถๆž„ๅปบ Chat2API ้กน็›ฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
40
- WORKDIR /app/chat2api
41
- RUN git clone --depth=1 https://github.com/xiaoY233/Chat2API.git . && \
42
- npm install && \
43
- npm run build:linux 2>/dev/null || true
44
- # ๆณจ๏ผšbuild:linux ไบงๅ‡บ AppImage/deb๏ผŒไฝ†ๅœจๅฎนๅ™จๅ†…ๆˆ‘ไปฌ็›ดๆŽฅ็”จๅผ€ๅ‘ๆจกๅผ่ฟ่กŒๆœๅŠก็ซฏ่ฟ›็จ‹
45
-
46
- # โ”€โ”€ 6. ็Žฏๅขƒๅ˜้‡้ป˜่ฎคๅ€ผ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
47
- ENV PORT=7860 \
48
- HOME=/root \
49
- PYTHONUNBUFFERED=1
50
-
51
- # โ”€โ”€ 7. ๅ†™ๅ…ฅ Dataset ๆŒไน…ๅŒ–ๅŒๆญฅๅผ•ๆ“Ž๏ผˆsync.py๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
52
- RUN cat <<'EOF' > /usr/local/bin/sync.py
53
- import os, sys, tarfile, shutil
54
- from huggingface_hub import HfApi, hf_hub_download
55
- from datetime import datetime, timedelta
56
-
57
- api = HfApi()
58
- repo_id = os.getenv("HF_DATASET")
59
- token = os.getenv("HF_TOKEN")
60
-
61
- DATA_DIR = "/root/.chat2api"
62
-
63
- # โ”€โ”€ ๅทฅๅ…ทๅ‡ฝๆ•ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
64
-
65
- def _parse_skip_list(env_var):
66
- raw = os.getenv(env_var, "").strip()
67
- if not raw:
68
- return set()
69
- return {s.strip().strip("/") for s in raw.split(",") if s.strip()}
70
-
71
- def _is_skipped(rel_path, skip_set):
72
- rel = rel_path.strip("/")
73
- for skip in skip_set:
74
- if rel == skip or rel.startswith(skip + "/"):
75
- return True
76
- return False
77
-
78
- def _walk_local(base_dir, skip_set=None):
79
- results = []
80
- if not os.path.isdir(base_dir):
81
- return results
82
- for dirpath, dirnames, filenames in os.walk(base_dir):
83
- for fname in filenames:
84
- abs_path = os.path.join(dirpath, fname)
85
- rel_to_base = os.path.relpath(abs_path, base_dir)
86
- if skip_set is not None:
87
- rel_to_data = os.path.relpath(abs_path, DATA_DIR)
88
- if _is_skipped(rel_to_data, skip_set):
89
- continue
90
- results.append((abs_path, rel_to_base))
91
- return results
92
-
93
- # โ”€โ”€ restore() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
-
95
- def restore():
96
- if not repo_id or not token:
97
- print("Skip Restore: HF_DATASET or HF_TOKEN not set")
98
- return
99
-
100
- restore_skip_raw = os.getenv("RESTORE_SKIP", "").strip()
101
- if restore_skip_raw == "all":
102
- print("Restore skip: RESTORE_SKIP=all, skipping all restore.")
103
- return
104
-
105
- INIT_FLAG = "initialized.flag"
106
- force_restore = os.getenv("FORCE_RESTORE", "").strip().lower() in ("true", "1", "yes")
107
-
108
- try:
109
- all_files = list(api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token))
110
- except Exception as e:
111
- print(f"Restore Error (list_repo_files): {e}")
112
- return
113
-
114
- flag_exists = INIT_FLAG in all_files
115
-
116
- if not flag_exists and not force_restore:
117
- print("Restore skip: initialized.flag not found, first deploy.")
118
- import io
119
- api.upload_file(
120
- path_or_fileobj=io.BytesIO(b"initialized\n"),
121
- path_in_repo=INIT_FLAG,
122
- repo_id=repo_id,
123
- repo_type="dataset",
124
- token=token,
125
- commit_message="Create initialized.flag on first deploy",
126
- )
127
- print("initialized.flag created in Dataset.")
128
- return
129
-
130
- if force_restore:
131
- print("Restore: FORCE_RESTORE=true, ignoring initialized.flag.")
132
- else:
133
- print("Restore: initialized.flag found, normal restart.")
134
-
135
- skip_set = _parse_skip_list("RESTORE_SKIP")
136
-
137
- try:
138
- now = datetime.now()
139
- for i in range(5):
140
- day = (now - timedelta(days=i)).strftime("%Y-%m-%d")
141
- name = f"backup_{day}.tar.gz"
142
- if name in all_files:
143
- print(f"Downloading {name}...")
144
- path = hf_hub_download(repo_id=repo_id, filename=name, repo_type="dataset", token=token)
145
- os.makedirs(DATA_DIR, exist_ok=True)
146
- with tarfile.open(path, "r:gz") as tar:
147
- for member in tar.getmembers():
148
- if _is_skipped(member.name, skip_set):
149
- print(f"Restore skip: {member.name}")
150
- continue
151
- tar.extract(member, path=DATA_DIR)
152
- print(f"Restored from {name}")
153
- break
154
-
155
- import io
156
- api.upload_file(
157
- path_or_fileobj=io.BytesIO(b"initialized at container startup\n"),
158
- path_in_repo=INIT_FLAG,
159
- repo_id=repo_id,
160
- repo_type="dataset",
161
- token=token,
162
- commit_message="Set initialized.flag",
163
- )
164
- print("initialized.flag updated.")
165
- except Exception as e:
166
- print(f"Restore Error: {e}")
167
-
168
-
169
- # โ”€โ”€ backup() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
170
-
171
- def backup():
172
- if not repo_id or not token:
173
- print("Skip Backup: HF_DATASET or HF_TOKEN not set")
174
- return
175
-
176
- backup_tar_skip_raw = os.getenv("BACKUP_TAR_SKIP", "").strip()
177
- if backup_tar_skip_raw == "all":
178
- print("Backup skip: BACKUP_TAR_SKIP=all")
179
- return
180
- try:
181
- tar_skip_set = _parse_skip_list("BACKUP_TAR_SKIP")
182
- day = datetime.now().strftime("%Y-%m-%d")
183
- name = f"backup_{day}.tar.gz"
184
- with tarfile.open(name, "w:gz") as tar:
185
- for abs_path, rel_to_base in _walk_local(DATA_DIR, skip_set=tar_skip_set):
186
- tar.add(abs_path, arcname=rel_to_base)
187
- api.upload_file(path_or_fileobj=name, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token)
188
- print(f"Backup {name} done.")
189
- except Exception as e:
190
- print(f"Backup Error: {e}")
191
-
192
-
193
- if __name__ == "__main__":
194
- if len(sys.argv) > 1 and sys.argv[1] == "backup":
195
- backup()
196
- else:
197
- restore()
198
- EOF
199
-
200
- # โ”€โ”€ 8. ๅ†™ๅ…ฅ cs-manager ๅฎˆๆŠค่ฟ›็จ‹๏ผˆๆฅ่‡ชๅŽŸ็‰ˆ Dockerfile๏ผŒๅŽŸๆ ทๅคๅˆถ๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
201
- RUN cat <<'PYEOF' > /usr/local/bin/cs-manager
202
- #!/usr/bin/env python3
203
- """
204
- cs-manager: code-server ๆŒ‰้œ€ๅฏๅœๅฎˆๆŠค่ฟ›็จ‹
205
-
206
- ็›‘ๅฌ Unix socket๏ผŒๆไพ›ไธคไธช HTTP ็ซฏ็‚น๏ผš
207
- GET /wakeup - ่งฆๅ‘ๅฏๅŠจ code-server๏ผˆ่‹ฅๆœช่ฟ่กŒ๏ผ‰๏ผŒ่ฟ”ๅ›ž"ๅฏๅŠจไธญ"็ญ‰ๅพ…้กต
208
- GET /heartbeat - ๆ›ดๆ–ฐๆœ€ๅŽๆดป่ทƒๆ—ถ้—ด๏ผˆnginx ๆฏๆฌกๆˆๅŠŸไปฃ็† /ide/ ๅŽ่ฐƒ็”จ๏ผ‰
209
-
210
- ๅŽๅฐๅฎšๆ—ถไปปๅŠก๏ผš
211
- ๆฏ 60 ็ง’ๆฃ€ๆŸฅไธ€ๆฌก๏ผŒ่‹ฅ่ถ…่ฟ‡ IDE_IDLE_MINUTES ๅˆ†้’Ÿๆ—  heartbeat๏ผŒkill code-server
212
- """
213
-
214
- import os, sys, time, signal, subprocess, threading, socket, re
215
- from http.server import HTTPServer, BaseHTTPRequestHandler
216
-
217
- # โ”€โ”€ ้…็ฝฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
218
- SOCK_PATH = "/tmp/cs-manager.sock"
219
- CS_PORT = int(os.environ.get("CODE_SERVER_PORT", "13337"))
220
- CS_PASSWORD = os.environ.get("CODE_SERVER_PASSWORD", "changeme123!")
221
- CS_USER_DATA_DIR = "/root/.code-server"
222
- CS_EXTENSIONS_DIR= "/root/.code-server/extensions"
223
- CS_WORKSPACE = "/root/.chat2api"
224
- CS_LOG = "/root/.chat2api/logs/code-server.log"
225
- IDLE_MINUTES = int(os.environ.get("IDE_IDLE_MINUTES", "30"))
226
- LAST_ACCESS_FILE = "/tmp/cs-last-access"
227
- CS_PID_FILE = "/tmp/cs-server.pid"
228
- CHECK_INTERVAL = 60 # ็ง’
229
-
230
- # โ”€โ”€ ๅ…จๅฑ€็Šถๆ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
231
- _lock = threading.Lock()
232
- _starting = False # ๆญฃๅœจๅฏๅŠจไธญ๏ผŒ้˜ฒๆญขๅนถๅ‘้‡ๅคๅฏๅŠจ
233
-
234
- # โ”€โ”€ ๅทฅๅ…ทๅ‡ฝๆ•ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
235
-
236
- def log(msg):
237
- ts = time.strftime("%Y-%m-%d %H:%M:%S")
238
- print(f"[cs-manager {ts}] {msg}", flush=True)
239
-
240
- def touch_last_access():
241
- try:
242
- with open(LAST_ACCESS_FILE, "w") as f:
243
- f.write(str(time.time()))
244
- os.utime(LAST_ACCESS_FILE, None)
245
- except Exception as e:
246
- log(f"touch_last_access error: {e}")
247
-
248
- def get_cs_pid():
249
- """่ฏปๅ– PID ๆ–‡ไปถ๏ผŒ่ฟ”ๅ›ž code-server PID๏ผˆ่‹ฅ่ฟ›็จ‹ๅญ˜ๅœจ๏ผ‰๏ผŒๅฆๅˆ™่ฟ”ๅ›ž None"""
250
- try:
251
- with open(CS_PID_FILE) as f:
252
- pid = int(f.read().strip())
253
- os.kill(pid, 0) # ๆŽขๆต‹่ฟ›็จ‹ๆ˜ฏๅฆๅญ˜ๅœจ
254
- return pid
255
- except Exception:
256
- return None
257
-
258
- def is_cs_running():
259
- return get_cs_pid() is not None
260
-
261
- def is_cs_port_ready():
262
- """ๅฐ่ฏ• TCP ่ฟžๆŽฅ code-server ็ซฏๅฃ๏ผŒ็กฎ่ฎคๆœๅŠก็œŸๆญฃๅฏ็”จ"""
263
- try:
264
- s = socket.create_connection(("127.0.0.1", CS_PORT), timeout=1)
265
- s.close()
266
- return True
267
- except Exception:
268
- return False
269
-
270
- def start_cs():
271
- """ๅฏๅŠจ code-server๏ผŒ้ž้˜ปๅกž๏ผŒ่ฟ”ๅ›žๅŽ่ฟ›็จ‹ๅœจๅŽๅฐ่ฟ่กŒ"""
272
- global _starting
273
- with _lock:
274
- if _starting or is_cs_running():
275
- return
276
- _starting = True
277
-
278
- try:
279
- log(f"Starting code-server on port {CS_PORT}...")
280
- os.makedirs(os.path.dirname(CS_LOG), exist_ok=True)
281
- os.makedirs(CS_USER_DATA_DIR, exist_ok=True)
282
-
283
- # ๅ†™ๅ…ฅ code-server config.yaml
284
- cfg_dir = "/root/.config/code-server"
285
- os.makedirs(cfg_dir, exist_ok=True)
286
- with open(os.path.join(cfg_dir, "config.yaml"), "w") as f:
287
- f.write(f"bind-addr: 127.0.0.1:{CS_PORT}\n")
288
- f.write(f"auth: password\n")
289
- f.write(f"password: {CS_PASSWORD}\n")
290
- f.write(f"cert: false\n")
291
-
292
- env = os.environ.copy()
293
- env.pop("PORT", None) # ้˜ฒๆญข code-server ่ฏปๅ– PORT=7860 ่ฆ†็›–็ซฏๅฃ
294
-
295
- log_file = open(CS_LOG, "a")
296
- proc = subprocess.Popen(
297
- [
298
- "code-server",
299
- "--disable-telemetry",
300
- "--disable-update-check",
301
- f"--user-data-dir={CS_USER_DATA_DIR}",
302
- f"--extensions-dir={CS_EXTENSIONS_DIR}",
303
- CS_WORKSPACE,
304
- ],
305
- stdout=log_file,
306
- stderr=log_file,
307
- env=env,
308
- start_new_session=True, # ่„ฑ็ฆปๅฝ“ๅ‰่ฟ›็จ‹็ป„๏ผŒ้ฟๅ…้š็ˆถ่ฟ›็จ‹็ปˆๆญข
309
- )
310
-
311
- # ๅ†™ๅ…ฅ PID ๆ–‡ไปถ
312
- with open(CS_PID_FILE, "w") as f:
313
- f.write(str(proc.pid))
314
- log(f"code-server started, PID={proc.pid}")
315
- touch_last_access()
316
- except Exception as e:
317
- log(f"start_cs error: {e}")
318
- finally:
319
- with _lock:
320
- _starting = False
321
-
322
- def stop_cs():
323
- """็ปˆๆญข code-server ่ฟ›็จ‹"""
324
- pid = get_cs_pid()
325
- if pid is None:
326
- log("stop_cs: code-server not running, skip")
327
- return
328
- try:
329
- os.kill(pid, signal.SIGTERM)
330
- log(f"code-server (PID={pid}) terminated (SIGTERM)")
331
- except Exception as e:
332
- log(f"stop_cs error: {e}")
333
- try:
334
- os.remove(CS_PID_FILE)
335
- except Exception:
336
- pass
337
-
338
- # โ”€โ”€ ้—ฒ็ฝฎๆฃ€ๆต‹ๅฎšๆ—ถๅ™จ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
339
-
340
- def idle_checker():
341
- while True:
342
- time.sleep(CHECK_INTERVAL)
343
- try:
344
- if not is_cs_running():
345
- continue
346
- try:
347
- mtime = os.path.getmtime(LAST_ACCESS_FILE)
348
- except FileNotFoundError:
349
- mtime = 0
350
- idle_secs = time.time() - mtime
351
- idle_mins = idle_secs / 60
352
- if idle_mins >= IDLE_MINUTES:
353
- log(f"Idle for {idle_mins:.1f} min (threshold={IDLE_MINUTES} min), stopping code-server...")
354
- stop_cs()
355
- else:
356
- remaining = IDLE_MINUTES - idle_mins
357
- log(f"code-server running, idle {idle_mins:.1f}/{IDLE_MINUTES} min (auto-stop in {remaining:.1f} min)")
358
- except Exception as e:
359
- log(f"idle_checker error: {e}")
360
-
361
- # โ”€โ”€ HTTP ่ฏทๆฑ‚ๅค„็† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
362
-
363
- WAKEUP_HTML = """\
364
- <!DOCTYPE html>
365
- <html lang="zh">
366
- <head>
367
- <meta charset="utf-8">
368
- <meta http-equiv="refresh" content="5;url=/ide/">
369
- <title>IDE ๅฏๅŠจไธญ...</title>
370
- <style>
371
- body {{ font-family: sans-serif; display:flex; align-items:center;
372
- justify-content:center; height:100vh; margin:0; background:#1e1e1e; color:#ccc; }}
373
- .box {{ text-align:center; }}
374
- .spinner {{ width:48px; height:48px; border:5px solid #555;
375
- border-top-color:#0078d4; border-radius:50%;
376
- animation:spin 1s linear infinite; margin:0 auto 20px; }}
377
- @keyframes spin {{ to {{ transform:rotate(360deg) }} }}
378
- p {{ margin:6px 0; }}
379
- small {{ color:#888; }}
380
- </style>
381
- </head>
382
- <body>
383
- <div class="box">
384
- <div class="spinner"></div>
385
- <p>VS Code IDE ๆญฃๅœจๅฏๅŠจ๏ผŒ่ฏท็จๅ€™...</p>
386
- <p><small>้กต้ขๅฐ†ๅœจ 5 ็ง’ๅŽ่‡ชๅŠจ้‡่ฏ•๏ผŒๆˆ–ๆ‰‹ๅŠจ <a href="/ide/" style="color:#0078d4">ๅˆทๆ–ฐ</a></small></p>
387
- </div>
388
- </body>
389
- </html>
390
- """
391
-
392
- class CSManagerHandler(BaseHTTPRequestHandler):
393
- def log_message(self, fmt, *args):
394
- pass # ้™้ป˜ access log๏ผŒ้ฟๅ…ๅˆทๆ—ฅๅฟ—
395
-
396
- def do_GET(self):
397
- if self.path.startswith("/wakeup"):
398
- self._handle_wakeup()
399
- elif self.path.startswith("/heartbeat"):
400
- self._handle_heartbeat()
401
- else:
402
- self.send_response(404)
403
- self.end_headers()
404
-
405
- def _handle_wakeup(self):
406
- """
407
- nginx @ide_wakeup ่ฐƒ็”จๆญค็ซฏ็‚นใ€‚
408
- ่‹ฅ code-server ๅทฒๅœจ่ฟ่กŒ๏ผˆ็ซฏๅฃๅฏ่พพ๏ผ‰๏ผŒ่ฟ”ๅ›ž 302 ้‡ๅฎšๅ‘ๅ›ž /ide/ใ€‚
409
- ่‹ฅๆœช่ฟ่กŒ๏ผŒ่งฆๅ‘ๅŽๅฐๅฏๅŠจ๏ผŒ่ฟ”ๅ›ž 200 "ๅฏๅŠจไธญ"็ญ‰ๅพ…้กตใ€‚
410
- """
411
- if is_cs_port_ready():
412
- touch_last_access()
413
- self.send_response(302)
414
- self.send_header("Location", "/ide/")
415
- self.end_headers()
416
- else:
417
- if not is_cs_running():
418
- t = threading.Thread(target=start_cs, daemon=True)
419
- t.start()
420
- html = WAKEUP_HTML.encode()
421
- self.send_response(200)
422
- self.send_header("Content-Type", "text/html; charset=utf-8")
423
- self.send_header("Content-Length", str(len(html)))
424
- self.end_headers()
425
- self.wfile.write(html)
426
-
427
- def _handle_heartbeat(self):
428
- """nginx ๆˆๅŠŸ่ฝฌๅ‘ /ide/ ่ฏทๆฑ‚ๅŽ่ฐƒ็”จ๏ผŒๆ›ดๆ–ฐๆดป่ทƒๆ—ถ้—ด"""
429
- touch_last_access()
430
- self.send_response(204)
431
- self.end_headers()
432
-
433
-
434
- class UnixSocketHTTPServer(HTTPServer):
435
- """ๅœจ Unix domain socket ไธŠ็›‘ๅฌ็š„ HTTPServer"""
436
- address_family = socket.AF_UNIX
437
-
438
- def server_bind(self):
439
- try:
440
- os.unlink(self.server_address)
441
- except FileNotFoundError:
442
- pass
443
- super().server_bind()
444
- os.chmod(self.server_address, 0o666)
445
-
446
-
447
- # โ”€โ”€ ไธปๅ…ฅๅฃ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
448
-
449
- if __name__ == "__main__":
450
- log(f"cs-manager starting (idle_timeout={IDLE_MINUTES} min, cs_port={CS_PORT})")
451
- log(f"Listening on Unix socket: {SOCK_PATH}")
452
-
453
- # ๅฏๅŠจ้—ฒ็ฝฎๆฃ€ๆต‹ๅŽๅฐ็บฟ็จ‹
454
- t = threading.Thread(target=idle_checker, daemon=True)
455
- t.start()
456
-
457
- # ๅฏๅŠจ HTTP ๆœๅŠก๏ผˆUnix socket๏ผ‰
458
- server = UnixSocketHTTPServer(SOCK_PATH, CSManagerHandler)
459
- try:
460
- server.serve_forever()
461
- except KeyboardInterrupt:
462
- log("cs-manager shutting down")
463
- server.server_close()
464
- PYEOF
465
-
466
- RUN chmod +x /usr/local/bin/cs-manager
467
-
468
- # โ”€โ”€ 9. ๅ†™ๅ…ฅไธปๅฏๅŠจ่„šๆœฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
469
- RUN cat <<'EOF' > /usr/local/bin/start-chat2api
470
- #!/bin/bash
471
- set -e
472
-
473
- # โ”€โ”€ ็Žฏๅขƒๅ˜้‡ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
474
- LISTEN_PORT="${PORT:-7860}"
475
- CHAT2API_PORT=7862 # Chat2API ๆœๅŠกๅ†…้ƒจ็ซฏๅฃ
476
- CODE_SERVER_PORT="${CODE_SERVER_PORT:-13337}"
477
- IDE_IDLE_MINUTES="${IDE_IDLE_MINUTES:-30}"
478
-
479
- export CODE_SERVER_PORT IDE_IDLE_MINUTES
480
- export CODE_SERVER_PASSWORD="${CODE_SERVER_PASSWORD:-changeme123!}"
481
- export CHAT2API_DATA_DIR="/root/.chat2api"
482
-
483
- echo "=== Chat2API HuggingFace Space ๅฏๅŠจ ==="
484
- echo "ๅค–้ƒจ็›‘ๅฌ็ซฏๅฃ : ${LISTEN_PORT}"
485
- echo "Chat2APIๅ†…้ƒจ็ซฏๅฃ: ${CHAT2API_PORT}"
486
- echo "IDEๅ†…้ƒจ็ซฏๅฃ : ${CODE_SERVER_PORT}"
487
- echo "IDE้—ฒ็ฝฎ่‡ชๅŠจๅ…ณ้—ญ : ${IDE_IDLE_MINUTES} min"
488
-
489
- # โ”€โ”€ ๆญฅ้ชค 1๏ผšไปŽ HF Dataset ๆขๅคๆŒไน…ๅŒ–ๆ•ฐๆฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
490
- echo "--- ๆขๅคๆŒไน…ๅŒ–้…็ฝฎๆ•ฐๆฎ ---"
491
- python3 /usr/local/bin/sync.py restore
492
-
493
- # โ”€โ”€ ๆญฅ้ชค 2๏ผšๅ‡†ๅค‡ Chat2API ๆ•ฐๆฎ็›ฎๅฝ• โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
494
- mkdir -p "${CHAT2API_DATA_DIR}/logs" "${CHAT2API_DATA_DIR}/sessions"
495
-
496
- # ่‹ฅๆ•ฐๆฎ็›ฎๅฝ•ไธๅญ˜ๅœจ้…็ฝฎ๏ผŒไปŽ HF Space ๅ˜้‡็”Ÿๆˆๅˆๅง‹้…็ฝฎ
497
- CONFIG_FILE="${CHAT2API_DATA_DIR}/config.json"
498
- if [ ! -f "$CONFIG_FILE" ]; then
499
- echo "็”Ÿๆˆๅˆๅง‹ config.json..."
500
- python3 - << 'PYEOF'
501
- import os, json
502
-
503
- config = {
504
- "port": int(os.getenv("CHAT2API_PORT", "7862")),
505
- "apiKey": os.getenv("CHAT2API_API_KEY", ""),
506
- "loadBalance": {
507
- "strategy": os.getenv("LB_STRATEGY", "round-robin")
508
- }
509
- }
510
- os.makedirs(os.getenv("CHAT2API_DATA_DIR", "/root/.chat2api"), exist_ok=True)
511
- with open(os.path.join(os.getenv("CHAT2API_DATA_DIR", "/root/.chat2api"), "config.json"), "w") as f:
512
- json.dump(config, f, indent=2)
513
- print("config.json initialized.")
514
- PYEOF
515
- fi
516
-
517
- # ่‹ฅๆ•ฐๆฎ็›ฎๅฝ•ไธๅญ˜ๅœจ่ดฆๆˆทๅˆ—่กจ๏ผŒไปŽ HF Space ๅ˜้‡ๆณจๅ…ฅๅˆๅง‹่ดฆๆˆท
518
- ACCOUNTS_FILE="${CHAT2API_DATA_DIR}/accounts.json"
519
- if [ ! -f "$ACCOUNTS_FILE" ]; then
520
- echo "็”Ÿๆˆๅˆๅง‹ accounts.json..."
521
- python3 - << 'PYEOF'
522
- import os, json
523
-
524
- accounts = []
525
- # ไปŽ็Žฏๅขƒๅ˜้‡ PROVIDER_x_TOKEN ๆ‰น้‡ๆณจๅ…ฅ่ดฆๆˆท
526
- # ๆ ผๅผ: PROVIDER_1_TYPE=deepseek, PROVIDER_1_TOKEN=xxx
527
- for i in range(1, 11):
528
- ptype = os.getenv(f"PROVIDER_{i}_TYPE", "").strip()
529
- token = os.getenv(f"PROVIDER_{i}_TOKEN", "").strip()
530
- if ptype and token:
531
- accounts.append({"provider": ptype, "token": token, "enabled": True})
532
-
533
- data_dir = os.getenv("CHAT2API_DATA_DIR", "/root/.chat2api")
534
- with open(os.path.join(data_dir, "accounts.json"), "w") as f:
535
- json.dump(accounts, f, indent=2)
536
- print(f"accounts.json initialized with {len(accounts)} account(s).")
537
- PYEOF
538
- fi
539
-
540
- # โ”€โ”€ ๆญฅ้ชค 3๏ผšๅฏๅŠจ Chat2API ๆœๅŠก๏ผˆๅŽๅฐ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
541
- echo "--- ๅฏๅŠจ Chat2API ๆœๅŠก (ๅ†…้ƒจ็ซฏๅฃ ${CHAT2API_PORT}) ---"
542
- cd /app/chat2api
543
- PORT=${CHAT2API_PORT} \
544
- CHAT2API_DATA="${CHAT2API_DATA_DIR}" \
545
- npx electron-vite dev 2>&1 | tee "${CHAT2API_DATA_DIR}/logs/chat2api.log" &
546
- echo "Chat2API ๅทฒๅœจๅŽๅฐๅฏๅŠจ"
547
-
548
- # โ”€โ”€ ๆญฅ้ชค 4๏ผšๅฏๅŠจ cs-manager ๅฎˆๆŠค่ฟ›็จ‹๏ผˆๅŽๅฐ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
549
- echo "--- ๅฏๅŠจ IDE ๆŒ‰้œ€็ฎก็†ๅ™จ ---"
550
- python3 /usr/local/bin/cs-manager 2>&1 | tee "${CHAT2API_DATA_DIR}/logs/cs-manager.log" &
551
-
552
- # ็ญ‰ๅพ… cs-manager socket ๅฐฑ็ปช๏ผˆๆœ€ๅคš 10 ็ง’๏ผ‰
553
- for i in $(seq 1 20); do
554
- if [ -S /tmp/cs-manager.sock ]; then
555
- echo "cs-manager socket ๅฐฑ็ปช"
556
- break
557
- fi
558
- sleep 0.5
559
- done
560
-
561
- # โ”€โ”€ ๆญฅ้ชค 5๏ผš็ญ‰ๅพ… Chat2API ๆœๅŠกๅฐฑ็ปช๏ผˆๆœ€ๅคš 120 ็ง’๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
562
- echo "็ญ‰ๅพ… Chat2API ๆœๅŠกๅฏๅŠจ..."
563
- for i in $(seq 1 60); do
564
- if curl -fsS http://127.0.0.1:${CHAT2API_PORT}/ >/dev/null 2>&1; then
565
- echo "Chat2API ๆœๅŠกๅทฒๅฐฑ็ปช๏ผˆ${i}*2s๏ผ‰"
566
- break
567
- fi
568
- sleep 2
569
- done
570
-
571
- # โ”€โ”€ ๆญฅ้ชค 6๏ผšๅฏๅŠจๅฎšๆ—ถๅค‡ไปฝๅพช็Žฏ๏ผˆๆฏ 60 ๅˆ†้’Ÿ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
572
- (while true; do sleep 3600; python3 /usr/local/bin/sync.py backup; done) &
573
- echo "ๆŒไน…ๅŒ–ๅค‡ไปฝๅพช็ŽฏๅทฒๅฏๅŠจ๏ผˆๆฏๅฐๆ—ถๅค‡ไปฝไธ€ๆฌก๏ผ‰"
574
-
575
- # โ”€โ”€ ๆญฅ้ชค 7๏ผš็”Ÿๆˆ nginx ้…็ฝฎๅนถๅฏๅŠจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
576
- rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
577
-
578
- cat > /etc/nginx/conf.d/chat2api.conf <<NGINX
579
- # cs-manager Unix socket upstream๏ผˆIDE ๆŒ‰้œ€ๅฏๅœๆŽงๅˆถๅ™จ๏ผ‰
580
- upstream cs_manager {
581
- server unix:/tmp/cs-manager.sock;
582
- }
583
-
584
- server {
585
- listen PLACEHOLDER_LISTEN_PORT;
586
- server_name _;
587
- client_max_body_size 100M;
588
-
589
- access_log /dev/stdout;
590
- error_log /dev/stderr warn;
591
-
592
- # HF Space ็”ฑ Cloudflare ๅš SSL ็ปˆ็ป“๏ผŒ้ฟๅ…็ซฏๅฃๅทๅ‡บ็Žฐๅœจ้‡ๅฎšๅ‘ URL ไธญ
593
- absolute_redirect off;
594
- port_in_redirect off;
595
-
596
- # โ”€โ”€ /ide/ ไธป location๏ผˆIDE ๆŒ‰้œ€ๅฏๅœ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
597
- location /ide/ {
598
- proxy_pass http://127.0.0.1:PLACEHOLDER_CODE_SERVER_PORT/;
599
- proxy_http_version 1.1;
600
- proxy_set_header Host \$host;
601
- proxy_set_header Upgrade \$http_upgrade;
602
- proxy_set_header Connection "upgrade";
603
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
604
- proxy_set_header X-Forwarded-Proto \$scheme;
605
- proxy_redirect / /ide/;
606
- proxy_read_timeout 86400;
607
- proxy_connect_timeout 2s;
608
- error_page 502 504 @ide_wakeup;
609
- post_action /ide-heartbeat/;
610
- }
611
-
612
- # โ”€โ”€ ๅฟƒ่ทณ็ซฏ็‚น โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
613
- location /ide-heartbeat/ {
614
- internal;
615
- rewrite ^ /heartbeat break;
616
- proxy_pass http://cs_manager;
617
- proxy_connect_timeout 1s;
618
- proxy_read_timeout 2s;
619
- }
620
-
621
- # โ”€โ”€ IDE ๅ”ค้†’ fallback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
622
- location @ide_wakeup {
623
- rewrite ^ /wakeup break;
624
- proxy_pass http://cs_manager;
625
- proxy_http_version 1.1;
626
- proxy_set_header Host \$host;
627
- proxy_connect_timeout 5s;
628
- proxy_read_timeout 30s;
629
- }
630
-
631
- # โ”€โ”€ ๆ‰€ๆœ‰ๅ…ถไป–่ฏทๆฑ‚๏ผš่ฝฌๅ‘ๅˆฐ Chat2API ๆœๅŠก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
632
- location / {
633
- proxy_pass http://127.0.0.1:PLACEHOLDER_CHAT2API_PORT/;
634
- proxy_http_version 1.1;
635
- proxy_set_header Host \$host;
636
- proxy_set_header Upgrade \$http_upgrade;
637
- proxy_set_header Connection "upgrade";
638
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
639
- proxy_set_header X-Forwarded-Proto \$scheme;
640
- proxy_read_timeout 86400;
641
- }
642
- }
643
- NGINX
644
-
645
- sed -i \
646
- "s/PLACEHOLDER_LISTEN_PORT/${LISTEN_PORT}/g;
647
- s/PLACEHOLDER_CODE_SERVER_PORT/${CODE_SERVER_PORT}/g;
648
- s/PLACEHOLDER_CHAT2API_PORT/${CHAT2API_PORT}/g" \
649
- /etc/nginx/conf.d/chat2api.conf
650
-
651
- echo "nginx ้…็ฝฎๅทฒ็”Ÿๆˆ๏ผš"
652
- cat /etc/nginx/conf.d/chat2api.conf
653
-
654
- nginx -t
655
- echo "ๅฏๅŠจ nginx๏ผˆๅ‰ๅฐ่ฟ่กŒ๏ผŒไฟๆŒๅฎนๅ™จๅญ˜ๆดป๏ผ‰..."
656
- exec nginx -g 'daemon off; error_log /dev/stderr warn;'
657
- EOF
658
-
659
- RUN chmod +x /usr/local/bin/start-chat2api
660
-
661
- # โ”€โ”€ 10. ๆšด้œฒ็ซฏๅฃ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
662
- EXPOSE 7860
663
-
664
- # โ”€โ”€ ่ฎฟ้—ฎ่ฏดๆ˜Ž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
665
- # Space ๅฏๅŠจๅŽ่ฎฟ้—ฎๆ–นๅผ๏ผš
666
- # ไธป็•Œ้ข (Chat2API ็ฎก็†้ขๆฟ) : https://<your-space>.hf.space/
667
- # API ๆŽฅๅ…ฅ็ซฏ็‚น : https://<your-space>.hf.space/v1/
668
- # VS Code IDE : https://<your-space>.hf.space/ide/
669
- #
670
- # ๅฟ…ๅกซ HF Space Secrets๏ผˆๅ˜้‡๏ผ‰๏ผš
671
- # HF_TOKEN โ€”โ€” HuggingFace ่ฎฟ้—ฎไปค็‰Œ๏ผˆ็”จไบŽ Dataset ่ฏปๅ†™๏ผŒ้œ€ๆœ‰ write ๆƒ้™๏ผ‰
672
- # HF_DATASET โ€”โ€” Dataset repo ID๏ผŒๆ ผๅผ๏ผšyour-username/your-dataset-name
673
- #
674
- # ๅฏ้€‰ Secrets๏ผˆๅฏๅœจ้ขๆฟไธญ้…็ฝฎ๏ผŒไนŸๅฏ้€š่ฟ‡ๅ˜้‡้ข„ๆณจๅ…ฅ๏ผ‰๏ผš
675
- # CHAT2API_API_KEY โ€”โ€” ไธบ Chat2API ่ฎพ็ฝฎ่ฎฟ้—ฎๅฏ†้’ฅ๏ผˆ็•™็ฉบๅˆ™ๆ— ้œ€่ฎค่ฏ๏ผ‰
676
- # CODE_SERVER_PASSWORDโ€”โ€” VS Code IDE ็™ปๅฝ•ๅฏ†็ ๏ผˆ้ป˜่ฎค๏ผšchangeme123!๏ผ‰
677
- # IDE_IDLE_MINUTES โ€”โ€” IDE ้—ฒ็ฝฎ่‡ชๅŠจๅ…ณ้—ญๆ—ถ้—ด๏ผˆๅˆ†้’Ÿ๏ผ‰๏ผŒ้ป˜่ฎค 30
678
- # PROVIDER_1_TYPE โ€”โ€” ็ฌฌไธ€ไธชๆœๅŠกๅ•†็ฑปๅž‹๏ผŒๅฆ‚ deepseek / kimi / qwen
679
- # PROVIDER_1_TOKEN โ€”โ€” ็ฌฌไธ€ไธชๆœๅŠกๅ•†็š„่ฎค่ฏ Token
680
- # PROVIDER_2_TYPE / PROVIDER_2_TOKEN โ€”โ€” ็ฌฌไบŒไธชๆœๅŠกๅ•†๏ผˆไปฅๆญค็ฑปๆŽจ๏ผŒๆœ€ๅคš 10 ไธช๏ผ‰
681
- # LB_STRATEGY โ€”โ€” ่ดŸ่ฝฝๅ‡่กก็ญ–็•ฅ๏ผšround-robin / fill-first / failover
682
- # FORCE_RESTORE โ€”โ€” ่ฎพไธบ true ๅผบๅˆถไปŽ Dataset ่ฆ†็›–ๆขๅค๏ผˆFactory Reset ็”จ๏ผ‰
683
-
684
- CMD ["/usr/local/bin/start-chat2api"]
 
1
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2
+ # Chat2API โ€” HuggingFace Space ็‰ˆ๏ผˆๅ•ๆ–‡ไปถ้ƒจ็ฝฒ็‰ˆ๏ผ‰
3
+ # ๅŸบไบŽ Node.js๏ผŒ้›†ๆˆ Chat2API ้กน็›ฎ + code-server ๆŒ‰้œ€ๅฏๅœ IDE
4
+ # ๆŒไน…ๅŒ–ๅญ˜ๅ‚จ๏ผšHuggingFace Dataset๏ผˆๆฏๅฐๆ—ถ่‡ชๅŠจๅค‡ไปฝ๏ผŒ้‡ๅฏ่‡ชๅŠจๆขๅค๏ผ‰
5
+ #
6
+ # ไฟฎๅค่ฏดๆ˜Ž๏ผš
7
+ # ๅŽŸ็‰ˆ Dockerfile ็”จ RUN cat <<'EOF'> ๅ†™่„šๆœฌ๏ผŒๅ†…้ƒจๅตŒๅฅ— heredoc ๆ—ถ
8
+ # BuildKit ไผšๆๅ‰ๆˆชๆ–ญ๏ผŒๅฏผ่‡ด่„šๆœฌไธบ็ฉบๆ–‡ไปถ๏ผŒ่ฟ่กŒๆ—ถๆŠฅ exit 127ใ€‚
9
+ # ๆœฌ็‰ˆๆ”น็”จ RUN python3 -c "import base64; open(...).write(base64.b64decode(...))"
10
+ # ๅฐ†ๆ‰€ๆœ‰่„šๆœฌไปฅ base64 ๅฝขๅผๅ†…ๅตŒ๏ผŒๅฝปๅบ•็ป•ๅผ€ heredoc ๅตŒๅฅ—้—ฎ้ข˜ใ€‚
11
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
12
+
13
+ FROM node:24-slim
14
+
15
+ # โ”€โ”€ 1. ๅŸบ็ก€็ณป็ปŸไพ่ต– + Electron/Chromium ่ฟ่กŒๅบ“ + Xvfb โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
+ RUN apt-get update && apt-get install -y --no-install-recommends \
17
+ git openssh-client build-essential python3 python3-pip \
18
+ g++ make ca-certificates curl wget nginx \
19
+ xvfb \
20
+ libgbm1 libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 \
21
+ libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
22
+ libxfixes3 libxrandr2 libpangocairo-1.0-0 libcairo2 \
23
+ libasound2 libxtst6 libx11-xcb1 libxcb-dri3-0 \
24
+ fonts-liberation libappindicator3-1 xdg-utils \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # โ”€โ”€ 1.1. ๅฎ‰่ฃ… code-server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
+ RUN curl -fsSL https://code-server.dev/install.sh | sh
29
+
30
+ # โ”€โ”€ 2. ๅฎ‰่ฃ… GitHub CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ RUN mkdir -p -m 755 /etc/apt/keyrings \
32
+ && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
33
+ && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
34
+ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
35
+ && mkdir -p -m 755 /etc/apt/sources.list.d \
36
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
37
+ | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
38
+ && apt-get update \
39
+ && apt-get install -y --no-install-recommends gh \
40
+ && rm -rf /var/lib/apt/lists/* \
41
+ && gh --version
42
+
43
+ # โ”€โ”€ 3. ๅฎ‰่ฃ… HuggingFace Hub โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
44
+ RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages
45
+
46
+ # โ”€โ”€ 4. ็ŽฏๅขƒไธŽ Git ้…็ฝฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
47
+ RUN update-ca-certificates && \
48
+ git config --global http.sslVerify false && \
49
+ git config --global url."https://github.com/".insteadOf ssh://git@github.com/
50
+
51
+ # โ”€โ”€ 5. ๅ…‹้š†ๅนถๆž„ๅปบ Chat2API ้กน็›ฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
52
+ WORKDIR /app/chat2api
53
+ RUN git clone --depth=1 https://github.com/xiaoY233/Chat2API.git . && \
54
+ npm install && \
55
+ npm run build:linux 2>/dev/null || true
56
+
57
+ # โ”€โ”€ 6. ็Žฏๅขƒๅ˜้‡้ป˜่ฎคๅ€ผ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
58
+ ENV PORT=7860 \
59
+ HOME=/root \
60
+ PYTHONUNBUFFERED=1 \
61
+ DISPLAY=:99
62
+
63
+ # โ”€โ”€ 7. ๅ†…ๅตŒๆ‰€ๆœ‰่„šๆœฌ๏ผˆbase64 ็ผ–็ ๏ผŒๅฝปๅบ•่ง„้ฟ BuildKit heredoc ๅตŒๅฅ—ๆˆชๆ–ญ้—ฎ้ข˜๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€
64
+ # ๆฏไธช RUN ๆŒ‡ไปค็”จ python3 ไธ€่กŒๅฎŒๆˆ๏ผšdecode โ†’ ๅ†™ๆ–‡ไปถ โ†’ ่ฎพๆƒ้™
65
+ # ๅŽŸ็†๏ผšbase64 ๅญ—็ฌฆไธฒไธญไธๅซไปปไฝ• shell ็‰นๆฎŠๅญ—็ฌฆ๏ผŒไธๅ— heredoc ๅฝฑๅ“ใ€‚
66
+
67
+ RUN python3 -c "import base64,os; d=base64.b64decode('aW1wb3J0IG9zLCBzeXMsIHRhcmZpbGUsIHNodXRpbApmcm9tIGh1Z2dpbmdmYWNlX2h1YiBpbXBvcnQgSGZBcGksIGhmX2h1Yl9kb3dubG9hZApmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZWRlbHRhCgphcGkgPSBIZkFwaSgpCnJlcG9faWQgPSBvcy5nZXRlbnYoIkhGX0RBVEFTRVQiKQp0b2tlbiAgID0gb3MuZ2V0ZW52KCJIRl9UT0tFTiIpCgpEQVRBX0RJUiA9ICIvcm9vdC8uY2hhdDJhcGkiCgojIOKUgOKUgCDlt6Xlhbflh73mlbAg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpkZWYgX3BhcnNlX3NraXBfbGlzdChlbnZfdmFyKToKICAgIHJhdyA9IG9zLmdldGVudihlbnZfdmFyLCAiIikuc3RyaXAoKQogICAgaWYgbm90IHJhdzoKICAgICAgICByZXR1cm4gc2V0KCkKICAgIHJldHVybiB7cy5zdHJpcCgpLnN0cmlwKCIvIikgZm9yIHMgaW4gcmF3LnNwbGl0KCIsIikgaWYgcy5zdHJpcCgpfQoKZGVmIF9pc19za2lwcGVkKHJlbF9wYXRoLCBza2lwX3NldCk6CiAgICByZWwgPSByZWxfcGF0aC5zdHJpcCgiLyIpCiAgICBmb3Igc2tpcCBpbiBza2lwX3NldDoKICAgICAgICBpZiByZWwgPT0gc2tpcCBvciByZWwuc3RhcnRzd2l0aChza2lwICsgIi8iKToKICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgIHJldHVybiBGYWxzZQoKZGVmIF93YWxrX2xvY2FsKGJhc2VfZGlyLCBza2lwX3NldD1Ob25lKToKICAgIHJlc3VsdHMgPSBbXQogICAgaWYgbm90IG9zLnBhdGguaXNkaXIoYmFzZV9kaXIpOgogICAgICAgIHJldHVybiByZXN1bHRzCiAgICBmb3IgZGlycGF0aCwgZGlybmFtZXMsIGZpbGVuYW1lcyBpbiBvcy53YWxrKGJhc2VfZGlyKToKICAgICAgICBmb3IgZm5hbWUgaW4gZmlsZW5hbWVzOgogICAgICAgICAgICBhYnNfcGF0aCA9IG9zLnBhdGguam9pbihkaXJwYXRoLCBmbmFtZSkKICAgICAgICAgICAgcmVsX3RvX2Jhc2UgPSBvcy5wYXRoLnJlbHBhdGgoYWJzX3BhdGgsIGJhc2VfZGlyKQogICAgICAgICAgICBpZiBza2lwX3NldCBpcyBub3QgTm9uZToKICAgICAgICAgICAgICAgIHJlbF90b19kYXRhID0gb3MucGF0aC5yZWxwYXRoKGFic19wYXRoLCBEQVRBX0RJUikKICAgICAgICAgICAgICAgIGlmIF9pc19za2lwcGVkKHJlbF90b19kYXRhLCBza2lwX3NldCk6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgcmVzdWx0cy5hcHBlbmQoKGFic19wYXRoLCByZWxfdG9fYmFzZSkpCiAgICByZXR1cm4gcmVzdWx0cwoKIyDilIDilIAgcmVzdG9yZSgpIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKZGVmIHJlc3RvcmUoKToKICAgIGlmIG5vdCByZXBvX2lkIG9yIG5vdCB0b2tlbjoKICAgICAgICBwcmludCgiU2tpcCBSZXN0b3JlOiBIRl9EQVRBU0VUIG9yIEhGX1RPS0VOIG5vdCBzZXQiKQogICAgICAgIHJldHVybgoKICAgIHJlc3RvcmVfc2tpcF9yYXcgPSBvcy5nZXRlbnYoIlJFU1RPUkVfU0tJUCIsICIiKS5zdHJpcCgpCiAgICBpZiByZXN0b3JlX3NraXBfcmF3ID09ICJhbGwiOgogICAgICAgIHByaW50KCJSZXN0b3JlIHNraXA6IFJFU1RPUkVfU0tJUD1hbGwsIHNraXBwaW5nIGFsbCByZXN0b3JlLiIpCiAgICAgICAgcmV0dXJuCgogICAgSU5JVF9GTEFHID0gImluaXRpYWxpemVkLmZsYWciCiAgICBmb3JjZV9yZXN0b3JlID0gb3MuZ2V0ZW52KCJGT1JDRV9SRVNUT1JFIiwgIiIpLnN0cmlwKCkubG93ZXIoKSBpbiAoInRydWUiLCAiMSIsICJ5ZXMiKQoKICAgIHRyeToKICAgICAgICBhbGxfZmlsZXMgPSBsaXN0KGFwaS5saXN0X3JlcG9fZmlsZXMocmVwb19pZD1yZXBvX2lkLCByZXBvX3R5cGU9ImRhdGFzZXQiLCB0b2tlbj10b2tlbikpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcHJpbnQoZiJSZXN0b3JlIEVycm9yIChsaXN0X3JlcG9fZmlsZXMpOiB7ZX0iKQogICAgICAgIHJldHVybgoKICAgIGZsYWdfZXhpc3RzID0gSU5JVF9GTEFHIGluIGFsbF9maWxlcwoKICAgIGlmIG5vdCBmbGFnX2V4aXN0cyBhbmQgbm90IGZvcmNlX3Jlc3RvcmU6CiAgICAgICAgcHJpbnQoIlJlc3RvcmUgc2tpcDogaW5pdGlhbGl6ZWQuZmxhZyBub3QgZm91bmQsIGZpcnN0IGRlcGxveS4iKQogICAgICAgIGltcG9ydCBpbwogICAgICAgIGFwaS51cGxvYWRfZmlsZSgKICAgICAgICAgICAgcGF0aF9vcl9maWxlb2JqPWlvLkJ5dGVzSU8oYiJpbml0aWFsaXplZFxuIiksCiAgICAgICAgICAgIHBhdGhfaW5fcmVwbz1JTklUX0ZMQUcsCiAgICAgICAgICAgIHJlcG9faWQ9cmVwb19pZCwKICAgICAgICAgICAgcmVwb190eXBlPSJkYXRhc2V0IiwKICAgICAgICAgICAgdG9rZW49dG9rZW4sCiAgICAgICAgICAgIGNvbW1pdF9tZXNzYWdlPSJDcmVhdGUgaW5pdGlhbGl6ZWQuZmxhZyBvbiBmaXJzdCBkZXBsb3kiLAogICAgICAgICkKICAgICAgICBwcmludCgiaW5pdGlhbGl6ZWQuZmxhZyBjcmVhdGVkIGluIERhdGFzZXQuIikKICAgICAgICByZXR1cm4KCiAgICBpZiBmb3JjZV9yZXN0b3JlOgogICAgICAgIHByaW50KCJSZXN0b3JlOiBGT1JDRV9SRVNUT1JFPXRydWUsIGlnbm9yaW5nIGluaXRpYWxpemVkLmZsYWcuIikKICAgIGVsc2U6CiAgICAgICAgcHJpbnQoIlJlc3RvcmU6IGluaXRpYWxpemVkLmZsYWcgZm91bmQsIG5vcm1hbCByZXN0YXJ0LiIpCgogICAgc2tpcF9zZXQgPSBfcGFyc2Vfc2tpcF9saXN0KCJSRVNUT1JFX1NLSVAiKQoKICAgIHRyeToKICAgICAgICBub3cgPSBkYXRldGltZS5ub3coKQogICAgICAgIGZvciBpIGluIHJhbmdlKDUpOgogICAgICAgICAgICBkYXkgID0gKG5vdyAtIHRpbWVkZWx0YShkYXlzPWkpKS5zdHJmdGltZSgiJVktJW0tJWQiKQogICAgICAgICAgICBuYW1lID0gZiJiYWNrdXBfe2RheX0udGFyLmd6IgogICAgICAgICAgICBpZiBuYW1lIGluIGFsbF9maWxlczoKICAgICAgICAgICAgICAgIHByaW50KGYiRG93bmxvYWRpbmcge25hbWV9Li4uIikKICAgICAgICAgICAgICAgIHBhdGggPSBoZl9odWJfZG93bmxvYWQocmVwb19pZD1yZXBvX2lkLCBmaWxlbmFtZT1uYW1lLCByZXBvX3R5cGU9ImRhdGFzZXQiLCB0b2tlbj10b2tlbikKICAgICAgICAgICAgICAgIG9zLm1ha2VkaXJzKERBVEFfRElSLCBleGlzdF9vaz1UcnVlKQogICAgICAgICAgICAgICAgd2l0aCB0YXJmaWxlLm9wZW4ocGF0aCwgInI6Z3oiKSBhcyB0YXI6CiAgICAgICAgICAgICAgICAgICAgZm9yIG1lbWJlciBpbiB0YXIuZ2V0bWVtYmVycygpOgogICAgICAgICAgICAgICAgICAgICAgICBpZiBfaXNfc2tpcHBlZChtZW1iZXIubmFtZSwgc2tpcF9zZXQpOgogICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJpbnQoZiJSZXN0b3JlIHNraXA6IHttZW1iZXIubmFtZX0iKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgICAgICAgICAgdGFyLmV4dHJhY3QobWVtYmVyLCBwYXRoPURBVEFfRElSKQogICAgICAgICAgICAgICAgcHJpbnQoZiJSZXN0b3JlZCBmcm9tIHtuYW1lfSIpCiAgICAgICAgICAgICAgICBicmVhawoKICAgICAgICBpbXBvcnQgaW8KICAgICAgICBhcGkudXBsb2FkX2ZpbGUoCiAgICAgICAgICAgIHBhdGhfb3JfZmlsZW9iaj1pby5CeXRlc0lPKGIiaW5pdGlhbGl6ZWQgYXQgY29udGFpbmVyIHN0YXJ0dXBcbiIpLAogICAgICAgICAgICBwYXRoX2luX3JlcG89SU5JVF9GTEFHLAogICAgICAgICAgICByZXBvX2lkPXJlcG9faWQsCiAgICAgICAgICAgIHJlcG9fdHlwZT0iZGF0YXNldCIsCiAgICAgICAgICAgIHRva2VuPXRva2VuLAogICAgICAgICAgICBjb21taXRfbWVzc2FnZT0iU2V0IGluaXRpYWxpemVkLmZsYWciLAogICAgICAgICkKICAgICAgICBwcmludCgiaW5pdGlhbGl6ZWQuZmxhZyB1cGRhdGVkLiIpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcHJpbnQoZiJSZXN0b3JlIEVycm9yOiB7ZX0iKQoKCiMg4pSA4pSAIGJhY2t1cCgpIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKZGVmIGJhY2t1cCgpOgogICAgaWYgbm90IHJlcG9faWQgb3Igbm90IHRva2VuOgogICAgICAgIHByaW50KCJTa2lwIEJhY2t1cDogSEZfREFUQVNFVCBvciBIRl9UT0tFTiBub3Qgc2V0IikKICAgICAgICByZXR1cm4KCiAgICBiYWNrdXBfdGFyX3NraXBfcmF3ID0gb3MuZ2V0ZW52KCJCQUNLVVBfVEFSX1NLSVAiLCAiIikuc3RyaXAoKQogICAgaWYgYmFja3VwX3Rhcl9za2lwX3JhdyA9PSAiYWxsIjoKICAgICAgICBwcmludCgiQmFja3VwIHNraXA6IEJBQ0tVUF9UQVJfU0tJUD1hbGwiKQogICAgICAgIHJldHVybgogICAgdHJ5OgogICAgICAgIHRhcl9za2lwX3NldCA9IF9wYXJzZV9za2lwX2xpc3QoIkJBQ0tVUF9UQVJfU0tJUCIpCiAgICAgICAgZGF5ICA9IGRhdGV0aW1lLm5vdygpLnN0cmZ0aW1lKCIlWS0lbS0lZCIpCiAgICAgICAgbmFtZSA9IGYiYmFja3VwX3tkYXl9LnRhci5neiIKICAgICAgICB3aXRoIHRhcmZpbGUub3BlbihuYW1lLCAidzpneiIpIGFzIHRhcjoKICAgICAgICAgICAgZm9yIGFic19wYXRoLCByZWxfdG9fYmFzZSBpbiBfd2Fsa19sb2NhbChEQVRBX0RJUiwgc2tpcF9zZXQ9dGFyX3NraXBfc2V0KToKICAgICAgICAgICAgICAgIHRhci5hZGQoYWJzX3BhdGgsIGFyY25hbWU9cmVsX3RvX2Jhc2UpCiAgICAgICAgYXBpLnVwbG9hZF9maWxlKHBhdGhfb3JfZmlsZW9iaj1uYW1lLCBwYXRoX2luX3JlcG89bmFtZSwgcmVwb19pZD1yZXBvX2lkLCByZXBvX3R5cGU9ImRhdGFzZXQiLCB0b2tlbj10b2tlbikKICAgICAgICBwcmludChmIkJhY2t1cCB7bmFtZX0gZG9uZS4iKQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHByaW50KGYiQmFja3VwIEVycm9yOiB7ZX0iKQoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBpZiBsZW4oc3lzLmFyZ3YpID4gMSBhbmQgc3lzLmFyZ3ZbMV0gPT0gImJhY2t1cCI6CiAgICAgICAgYmFja3VwKCkKICAgIGVsc2U6CiAgICAgICAgcmVzdG9yZSgpCg=='); open('/usr/local/bin/sync.py','wb').write(d)"
68
+
69
+ RUN python3 -c "import base64,os,stat; d=base64.b64decode('IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKY3MtbWFuYWdlcjogY29kZS1zZXJ2ZXIg5oyJ6ZyA5ZCv5YGc5a6I5oqk6L+b56iLCgrnm5HlkKwgVW5peCBzb2NrZXTvvIzmj5DkvpvkuKTkuKogSFRUUCDnq6/ngrnvvJoKICBHRVQgL3dha2V1cCAgICAtIOinpuWPkeWQr+WKqCBjb2RlLXNlcnZlcu+8iOiLpeacqui/kOihjO+8ie+8jOi/lOWbniLlkK/liqjkuK0i562J5b6F6aG1CiAgR0VUIC9oZWFydGJlYXQgLSDmm7TmlrDmnIDlkI7mtLvot4Pml7bpl7TvvIhuZ2lueCDmr4/mrKHmiJDlip/ku6PnkIYgL2lkZS8g5ZCO6LCD55So77yJCgrlkI7lj7Dlrprml7bku7vliqHvvJoKICDmr48gNjAg56eS5qOA5p+l5LiA5qyh77yM6Iul6LaF6L+HIElERV9JRExFX01JTlVURVMg5YiG6ZKf5pegIGhlYXJ0YmVhdO+8jGtpbGwgY29kZS1zZXJ2ZXIKIiIiCgppbXBvcnQgb3MsIHN5cywgdGltZSwgc2lnbmFsLCBzdWJwcm9jZXNzLCB0aHJlYWRpbmcsIHNvY2tldCwgcmUKZnJvbSBodHRwLnNlcnZlciBpbXBvcnQgSFRUUFNlcnZlciwgQmFzZUhUVFBSZXF1ZXN0SGFuZGxlcgoKIyDilIDilIAg6YWN572uIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApTT0NLX1BBVEggICAgICAgID0gIi90bXAvY3MtbWFuYWdlci5zb2NrIgpDU19QT1JUICAgICAgICAgID0gaW50KG9zLmVudmlyb24uZ2V0KCJDT0RFX1NFUlZFUl9QT1JUIiwgIjEzMzM3IikpCkNTX1BBU1NXT1JEICAgICAgPSBvcy5lbnZpcm9uLmdldCgiQ09ERV9TRVJWRVJfUEFTU1dPUkQiLCAiY2hhbmdlbWUxMjMhIikKSURMRV9NSU5VVEVTICAgICA9IGludChvcy5lbnZpcm9uLmdldCgiSURFX0lETEVfTUlOVVRFUyIsICIzMCIpKQpDSEVDS19JTlRFUlZBTCAgID0gNjAgICAjIOenkgoKIyDilIDilIAg5YWo5bGA54q25oCBIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApjc19wcm9jICAgICAgICAgID0gTm9uZQpsYXN0X2hlYXJ0YmVhdCAgID0gMC4wCmxvY2sgICAgICAgICAgICAgPSB0aHJlYWRpbmcuTG9jaygpCgojIOKUgOKUgCBjb2RlLXNlcnZlciDlkK/lgZwg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpkZWYgc3RhcnRfY3MoKToKICAgIGdsb2JhbCBjc19wcm9jLCBsYXN0X2hlYXJ0YmVhdAogICAgd2l0aCBsb2NrOgogICAgICAgIGlmIGNzX3Byb2MgaXMgbm90IE5vbmUgYW5kIGNzX3Byb2MucG9sbCgpIGlzIE5vbmU6CiAgICAgICAgICAgIHJldHVybiAgIyBhbHJlYWR5IHJ1bm5pbmcKICAgICAgICBwcmludChmIltjcy1tYW5hZ2VyXSBTdGFydGluZyBjb2RlLXNlcnZlciBvbiBwb3J0IHtDU19QT1JUfSIsIGZsdXNoPVRydWUpCiAgICAgICAgZW52ID0gb3MuZW52aXJvbi5jb3B5KCkKICAgICAgICBlbnZbIlBBU1NXT1JEIl0gPSBDU19QQVNTV09SRAogICAgICAgIGNzX3Byb2MgPSBzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICBbImNvZGUtc2VydmVyIiwKICAgICAgICAgICAgICItLWJpbmQtYWRkciIsIGYiMTI3LjAuMC4xOntDU19QT1JUfSIsCiAgICAgICAgICAgICAiLS1hdXRoIiwgInBhc3N3b3JkIiwKICAgICAgICAgICAgICItLWRpc2FibGUtdGVsZW1ldHJ5IiwKICAgICAgICAgICAgICIvYXBwL2NoYXQyYXBpIl0sCiAgICAgICAgICAgIGVudj1lbnYsCiAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgKQogICAgICAgIGxhc3RfaGVhcnRiZWF0ID0gdGltZS50aW1lKCkKICAgICAgICBwcmludChmIltjcy1tYW5hZ2VyXSBjb2RlLXNlcnZlciBwaWQ9e2NzX3Byb2MucGlkfSIsIGZsdXNoPVRydWUpCgpkZWYgc3RvcF9jcygpOgogICAgZ2xvYmFsIGNzX3Byb2MKICAgIHdpdGggbG9jazoKICAgICAgICBpZiBjc19wcm9jIGlzIE5vbmUgb3IgY3NfcHJvYy5wb2xsKCkgaXMgbm90IE5vbmU6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIHByaW50KCJbY3MtbWFuYWdlcl0gU3RvcHBpbmcgaWRsZSBjb2RlLXNlcnZlciIsIGZsdXNoPVRydWUpCiAgICAgICAgY3NfcHJvYy50ZXJtaW5hdGUoKQogICAgICAgIHRyeToKICAgICAgICAgICAgY3NfcHJvYy53YWl0KHRpbWVvdXQ9MTApCiAgICAgICAgZXhjZXB0IHN1YnByb2Nlc3MuVGltZW91dEV4cGlyZWQ6CiAgICAgICAgICAgIGNzX3Byb2Mua2lsbCgpCiAgICAgICAgY3NfcHJvYyA9IE5vbmUKCmRlZiBpc19jc19ydW5uaW5nKCk6CiAgICB3aXRoIGxvY2s6CiAgICAgICAgcmV0dXJuIGNzX3Byb2MgaXMgbm90IE5vbmUgYW5kIGNzX3Byb2MucG9sbCgpIGlzIE5vbmUKCiMg4pSA4pSAIEhUVFAgaGFuZGxlciDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCldBS0VVUF9IVE1MID0gIiIiXAo8IURPQ1RZUEUgaHRtbD48aHRtbD48aGVhZD4KPG1ldGEgY2hhcnNldD0idXRmLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJyZWZyZXNoIiBjb250ZW50PSIzIj4KPHRpdGxlPklERSDlkK/liqjkuK3igKY8L3RpdGxlPgo8c3R5bGU+CmJvZHl7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtkaXNwbGF5OmZsZXg7YWxpZ24taXRlbXM6Y2VudGVyO2p1c3RpZnktY29udGVudDpjZW50ZXI7CiAgICAgaGVpZ2h0OjEwMHZoO21hcmdpbjowO2JhY2tncm91bmQ6IzFlMWUyZTtjb2xvcjojY2RkNmY0fQouYm94e3RleHQtYWxpZ246Y2VudGVyfS5zcGlubmVye3dpZHRoOjQ4cHg7aGVpZ2h0OjQ4cHg7Ym9yZGVyOjVweCBzb2xpZCAjMzEzMjQ0OwogICAgIGJvcmRlci10b3AtY29sb3I6Izg5YjRmYTtib3JkZXItcmFkaXVzOjUwJTthbmltYXRpb246c3BpbiAxcyBsaW5lYXIgaW5maW5pdGU7CiAgICAgbWFyZ2luOjAgYXV0byAyMHB4fQpAa2V5ZnJhbWVzIHNwaW57dG97dHJhbnNmb3JtOnJvdGF0ZSgzNjBkZWcpfX0KPC9zdHlsZT48L2hlYWQ+PGJvZHk+CjxkaXYgY2xhc3M9ImJveCI+CiAgPGRpdiBjbGFzcz0ic3Bpbm5lciI+PC9kaXY+CiAgPGgyPlZTIENvZGUgSURFIOWQr+WKqOS4reKApjwvaDI+CiAgPHA+6aG16Z2i5bCG5ZyoIDMg56eS5ZCO6Ieq5Yqo5Yi35pawPC9wPgo8L2Rpdj48L2JvZHk+PC9odG1sPgoiIiIKCmNsYXNzIEhhbmRsZXIoQmFzZUhUVFBSZXF1ZXN0SGFuZGxlcik6CiAgICBkZWYgbG9nX21lc3NhZ2Uoc2VsZiwgZm10LCAqYXJncyk6CiAgICAgICAgcGFzcyAgIyDpnZnpu5jorr/pl67ml6Xlv5cKCiAgICBkZWYgZG9fR0VUKHNlbGYpOgogICAgICAgIGdsb2JhbCBsYXN0X2hlYXJ0YmVhdAogICAgICAgIHBhdGggPSBzZWxmLnBhdGguc3BsaXQoIj8iKVswXQoKICAgICAgICBpZiBwYXRoID09ICIvaGVhcnRiZWF0IjoKICAgICAgICAgICAgbGFzdF9oZWFydGJlYXQgPSB0aW1lLnRpbWUoKQogICAgICAgICAgICBzZWxmLl9yZXNwb25kKDIwMCwgInRleHQvcGxhaW4iLCBiIm9rIikKCiAgICAgICAgZWxpZiBwYXRoID09ICIvd2FrZXVwIjoKICAgICAgICAgICAgaWYgbm90IGlzX2NzX3J1bm5pbmcoKToKICAgICAgICAgICAgICAgIHN0YXJ0X2NzKCkKICAgICAgICAgICAgICAgICMg562J5b6FIGNvZGUtc2VydmVyIOWwsee7qu+8iOacgOWkmiAzMCDnp5LvvIkKICAgICAgICAgICAgICAgIGZvciBfIGluIHJhbmdlKDMwKToKICAgICAgICAgICAgICAgICAgICB0aW1lLnNsZWVwKDEpCiAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgICAgICBpbXBvcnQgdXJsbGliLnJlcXVlc3QKICAgICAgICAgICAgICAgICAgICAgICAgdXJsbGliLnJlcXVlc3QudXJsb3BlbihmImh0dHA6Ly8xMjcuMC4wLjE6e0NTX1BPUlR9LyIsIHRpbWVvdXQ9MSkKICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgIHNlbGYuX3Jlc3BvbmQoMjAwLCAidGV4dC9odG1sIiwgV0FLRVVQX0hUTUwuZW5jb2RlKCkpCgogICAgICAgIGVsc2U6CiAgICAgICAgICAgIHNlbGYuX3Jlc3BvbmQoNDA0LCAidGV4dC9wbGFpbiIsIGIibm90IGZvdW5kIikKCiAgICBkZWYgX3Jlc3BvbmQoc2VsZiwgY29kZSwgY3R5cGUsIGJvZHkpOgogICAgICAgIHNlbGYuc2VuZF9yZXNwb25zZShjb2RlKQogICAgICAgIHNlbGYuc2VuZF9oZWFkZXIoIkNvbnRlbnQtVHlwZSIsIGN0eXBlKQogICAgICAgIHNlbGYuc2VuZF9oZWFkZXIoIkNvbnRlbnQtTGVuZ3RoIiwgc3RyKGxlbihib2R5KSkpCiAgICAgICAgc2VsZi5lbmRfaGVhZGVycygpCiAgICAgICAgc2VsZi53ZmlsZS53cml0ZShib2R5KQoKIyDilIDilIAgVW5peCBzb2NrZXQgc2VydmVyIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKY2xhc3MgVW5peEhUVFBTZXJ2ZXIoSFRUUFNlcnZlcik6CiAgICBhZGRyZXNzX2ZhbWlseSA9IHNvY2tldC5BRl9VTklYCgogICAgZGVmIHNlcnZlcl9iaW5kKHNlbGYpOgogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKFNPQ0tfUEFUSCk6CiAgICAgICAgICAgIG9zLnJlbW92ZShTT0NLX1BBVEgpCiAgICAgICAgc3VwZXIoKS5zZXJ2ZXJfYmluZCgpCiAgICAgICAgb3MuY2htb2QoU09DS19QQVRILCAwbzY2NikKCiMg4pSA4pSAIOepuumXsuajgOafpeW+queOryDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCmRlZiBpZGxlX3dhdGNoZXIoKToKICAgIHdoaWxlIFRydWU6CiAgICAgICAgdGltZS5zbGVlcChDSEVDS19JTlRFUlZBTCkKICAgICAgICBpZiBpc19jc19ydW5uaW5nKCk6CiAgICAgICAgICAgIGlkbGVfc2VjcyA9IHRpbWUudGltZSgpIC0gbGFzdF9oZWFydGJlYXQKICAgICAgICAgICAgaWYgaWRsZV9zZWNzID4gSURMRV9NSU5VVEVTICogNjA6CiAgICAgICAgICAgICAgICBwcmludChmIltjcy1tYW5hZ2VyXSBJZGxlIHtpZGxlX3NlY3MvNjA6LjFmfSBtaW4gPiB7SURMRV9NSU5VVEVTfSBtaW4sIHN0b3BwaW5nLiIsIGZsdXNoPVRydWUpCiAgICAgICAgICAgICAgICBzdG9wX2NzKCkKCiMg4pSA4pSAIOS4u+WFpeWPoyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICB3YXRjaGVyID0gdGhyZWFkaW5nLlRocmVhZCh0YXJnZXQ9aWRsZV93YXRjaGVyLCBkYWVtb249VHJ1ZSkKICAgIHdhdGNoZXIuc3RhcnQoKQoKICAgIHNlcnZlciA9IFVuaXhIVFRQU2VydmVyKFNPQ0tfUEFUSCwgSGFuZGxlcikKICAgIHByaW50KGYiW2NzLW1hbmFnZXJdIExpc3RlbmluZyBvbiB7U09DS19QQVRIfSIsIGZsdXNoPVRydWUpCiAgICB0cnk6CiAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQogICAgZXhjZXB0IEtleWJvYXJkSW50ZXJydXB0OgogICAgICAgIHBhc3MK'); p='/usr/local/bin/cs-manager'; open(p,'wb').write(d); os.chmod(p, os.stat(p).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)"
70
+
71
+ RUN python3 -c "import base64,os,stat; d=base64.b64decode('IyEvYmluL2Jhc2gKc2V0IC1lCgojIOKUgOKUgCDnjq/looPlj5jph48g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkxJU1RFTl9QT1JUPSIke1BPUlQ6LTc4NjB9IgpDSEFUMkFQSV9QT1JUPTc4NjIKQ09ERV9TRVJWRVJfUE9SVD0iJHtDT0RFX1NFUlZFUl9QT1JUOi0xMzMzN30iCklERV9JRExFX01JTlVURVM9IiR7SURFX0lETEVfTUlOVVRFUzotMzB9IgoKZXhwb3J0IENPREVfU0VSVkVSX1BPUlQgSURFX0lETEVfTUlOVVRFUwpleHBvcnQgQ09ERV9TRVJWRVJfUEFTU1dPUkQ9IiR7Q09ERV9TRVJWRVJfUEFTU1dPUkQ6LWNoYW5nZW1lMTIzIX0iCmV4cG9ydCBDSEFUMkFQSV9EQVRBX0RJUj0iL3Jvb3QvLmNoYXQyYXBpIgoKZWNobyAiPT09IENoYXQyQVBJIEh1Z2dpbmdGYWNlIFNwYWNlIOWQr+WKqCA9PT0iCmVjaG8gIuWklumDqOebkeWQrOerr+WPoyAgICA6ICR7TElTVEVOX1BPUlR9IgplY2hvICJDaGF0MkFQSeWGhemDqOerr+WPozogJHtDSEFUMkFQSV9QT1JUfSIKZWNobyAiSURF5YaF6YOo56uv5Y+jICAgICA6ICR7Q09ERV9TRVJWRVJfUE9SVH0iCmVjaG8gIklERemXsue9ruiHquWKqOWFs+mXrSA6ICR7SURFX0lETEVfTUlOVVRFU30gbWluIgoKIyDilIDilIAg5q2l6aqkIDHvvJrku44gSEYgRGF0YXNldCDmgaLlpI3mjIHkuYXljJbmlbDmja4g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACmVjaG8gIi0tLSDmgaLlpI3mjIHkuYXljJbphY3nva7mlbDmja4gLS0tIgpweXRob24zIC91c3IvbG9jYWwvYmluL3N5bmMucHkgcmVzdG9yZSB8fCB0cnVlCgojIOKUgOKUgCDmraXpqqQgMu+8muWHhuWkhyBDaGF0MkFQSSDmlbDmja7nm67lvZUg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACm1rZGlyIC1wICIke0NIQVQyQVBJX0RBVEFfRElSfS9sb2dzIiAiJHtDSEFUMkFQSV9EQVRBX0RJUn0vc2Vzc2lvbnMiCgojIOiLpeaVsOaNruebruW9leS4jeWtmOWcqOmFjee9ru+8jOS7jiBIRiBTcGFjZSDlj5jph4/nlJ/miJDliJ3lp4vphY3nva4KQ09ORklHX0ZJTEU9IiR7Q0hBVDJBUElfREFUQV9ESVJ9L2NvbmZpZy5qc29uIgppZiBbICEgLWYgIiRDT05GSUdfRklMRSIgXTsgdGhlbgogICAgZWNobyAi55Sf5oiQ5Yid5aeLIGNvbmZpZy5qc29uLi4uIgogICAgcHl0aG9uMyAvdXNyL2xvY2FsL2Jpbi9pbml0LWNvbmZpZy5weQpmaQoKIyDoi6XmlbDmja7nm67lvZXkuI3lrZjlnKjotKbmiLfliJfooajvvIzku44gSEYgU3BhY2Ug5Y+Y6YeP5rOo5YWl5Yid5aeL6LSm5oi3CkFDQ09VTlRTX0ZJTEU9IiR7Q0hBVDJBUElfREFUQV9ESVJ9L2FjY291bnRzLmpzb24iCmlmIFsgISAtZiAiJEFDQ09VTlRTX0ZJTEUiIF07IHRoZW4KICAgIGVjaG8gIueUn+aIkOWIneWniyBhY2NvdW50cy5qc29uLi4uIgogICAgcHl0aG9uMyAvdXNyL2xvY2FsL2Jpbi9pbml0LWFjY291bnRzLnB5CmZpCgojIOKUgOKUgCDmraXpqqQgM++8muWQr+WKqOiZmuaLn+aYvuekuu+8iEVsZWN0cm9uIOmcgOimgSBYIGRpc3BsYXnvvInilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKZWNobyAiLS0tIOWQr+WKqCBYdmZiIOiZmuaLn+aYvuekuiAtLS0iClh2ZmIgOjk5IC1zY3JlZW4gMCAxMjgweDgwMHgyNCAtYWMgK2V4dGVuc2lvbiBHTFggK3JlbmRlciAtbm9yZXNldCAmClhWRkJfUElEPSQhCmV4cG9ydCBESVNQTEFZPTo5OQplY2hvICLnrYnlvoUgWHZmYiDlsLHnu6ouLi4iCnNsZWVwIDIKCiMg4pSA4pSAIOatpemqpCA077ya5ZCv5YqoIENoYXQyQVBJ77yIRWxlY3Ryb24gQXBwSW1hZ2XvvIzlkI7lj7Dov5DooYzvvInilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKZWNobyAiLS0tIOWQr+WKqCBDaGF0MkFQSSDmnI3liqEgKOWGhemDqOerr+WPoyAke0NIQVQyQVBJX1BPUlR9KSAtLS0iCmNkIC9hcHAvY2hhdDJhcGkKCiMg5p+l5om+5p6E5bu65Lqn54mp77ya5LyY5YWI55SoIEFwcEltYWdl77yM5YW25qyh55SoIHVucGFja2VkIOebruW9leWGheeahCBlbGVjdHJvbiDlj6/miafooYzmlofku7YKQVBQSU1BR0U9JChmaW5kIGRpc3QgLW5hbWUgIiouQXBwSW1hZ2UiIC1tYXhkZXB0aCAxIDI+L2Rldi9udWxsIHwgaGVhZCAtMSkKCmlmIFsgLW4gIiRBUFBJTUFHRSIgXTsgdGhlbgogICAgZWNobyAi5L2/55SoIEFwcEltYWdlOiAkQVBQSU1BR0UiCiAgICBDSEFUMkFQSV9QT1JUPSIke0NIQVQyQVBJX1BPUlR9IiBcCiAgICBDSEFUMkFQSV9EQVRBPSIke0NIQVQyQVBJX0RBVEFfRElSfSIgXAogICAgIiRBUFBJTUFHRSIgLS1uby1zYW5kYm94IC0tZGlzYWJsZS1ncHUgMj4mMSBcCiAgICAgICAgfCB0ZWUgIiR7Q0hBVDJBUElfREFUQV9ESVJ9L2xvZ3MvY2hhdDJhcGkubG9nIiAmCmVsc2UKICAgICMg5Zue6YCA77ya5L2/55SoIGVsZWN0cm9uLXZpdGUgZGV2IOaooeW8j++8iOS7jemcgCBESVNQTEFZ77yJCiAgICBlY2hvICJBcHBJbWFnZSDmnKrmib7liLDvvIzlm57pgIDliLAgZWxlY3Ryb24tdml0ZSBkZXYg5qih5byPIgogICAgQ0hBVDJBUElfUE9SVD0iJHtDSEFUMkFQSV9QT1JUfSIgXAogICAgQ0hBVDJBUElfREFUQT0iJHtDSEFUMkFQSV9EQVRBX0RJUn0iIFwKICAgIG5weCBlbGVjdHJvbiAuIC0tbm8tc2FuZGJveCAtLWRpc2FibGUtZ3B1IDI+JjEgXAogICAgICAgIHwgdGVlICIke0NIQVQyQVBJX0RBVEFfRElSfS9sb2dzL2NoYXQyYXBpLmxvZyIgJgpmaQplY2hvICJDaGF0MkFQSSDlt7LlnKjlkI7lj7DlkK/liqgiCgojIOKUgOKUgCDmraXpqqQgNe+8muWQr+WKqCBjcy1tYW5hZ2VyIOWuiOaKpOi/m+eoi++8iOWQjuWPsO+8ieKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAplY2hvICItLS0g5ZCv5YqoIElERSDmjInpnIDnrqHnkIblmaggLS0tIgpweXRob24zIC91c3IvbG9jYWwvYmluL2NzLW1hbmFnZXIgMj4mMSB8IHRlZSAiJHtDSEFUMkFQSV9EQVRBX0RJUn0vbG9ncy9jcy1tYW5hZ2VyLmxvZyIgJgoKIyDnrYnlvoUgY3MtbWFuYWdlciBzb2NrZXQg5bCx57uq77yI5pyA5aSaIDEwIOenku+8iQpmb3IgaSBpbiAkKHNlcSAxIDIwKTsgZG8KICBpZiBbIC1TIC90bXAvY3MtbWFuYWdlci5zb2NrIF07IHRoZW4KICAgIGVjaG8gImNzLW1hbmFnZXIgc29ja2V0IOWwsee7qiIKICAgIGJyZWFrCiAgZmkKICBzbGVlcCAwLjUKZG9uZQoKIyDilIDilIAg5q2l6aqkIDbvvJrnrYnlvoUgQ2hhdDJBUEkg5pyN5Yqh5bCx57uq77yI5pyA5aSaIDEyMCDnp5LvvInilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKZWNobyAi562J5b6FIENoYXQyQVBJIOacjeWKoeWQr+WKqC4uLiIKZm9yIGkgaW4gJChzZXEgMSA2MCk7IGRvCiAgaWYgY3VybCAtZnNTICJodHRwOi8vMTI3LjAuMC4xOiR7Q0hBVDJBUElfUE9SVH0vIiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkNoYXQyQVBJIOacjeWKoeW3suWwsee7qu+8iCR7aX0qMnPvvIkiCiAgICBicmVhawogIGZpCiAgc2xlZXAgMgpkb25lCgojIOKUgOKUgCDmraXpqqQgN++8muWQr+WKqOWumuaXtuWkh+S7veW+queOr++8iOavjyA2MCDliIbpkp/vvInilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKKHdoaWxlIHRydWU7IGRvIHNsZWVwIDM2MDA7IHB5dGhvbjMgL3Vzci9sb2NhbC9iaW4vc3luYy5weSBiYWNrdXAgfHwgdHJ1ZTsgZG9uZSkgJgplY2hvICLmjIHkuYXljJblpIfku73lvqrnjq/lt7LlkK/liqjvvIjmr4/lsI/ml7blpIfku73kuIDmrKHvvIkiCgojIOKUgOKUgCDmraXpqqQgOO+8mueUn+aIkCBuZ2lueCDphY3nva7lubblkK/liqgg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACnJtIC1mIC9ldGMvbmdpbngvc2l0ZXMtZW5hYmxlZC9kZWZhdWx0IC9ldGMvbmdpbngvY29uZi5kL2RlZmF1bHQuY29uZgoKTkdJTlhfQ09ORj0iL2V0Yy9uZ2lueC9jb25mLmQvY2hhdDJhcGkuY29uZiIKCnB5dGhvbjMgLSA8PFBZRU9GCmltcG9ydCBvcwpsaXN0ZW4gICA9IG9zLmVudmlyb24uZ2V0KCJQT1JUIiwgIjc4NjAiKQpjc19wb3J0ICA9IG9zLmVudmlyb24uZ2V0KCJDT0RFX1NFUlZFUl9QT1JUIiwgIjEzMzM3IikKYXBpX3BvcnQgPSAiNzg2MiIKCmNvbmYgPSBmIiIiCnVwc3RyZWFtIGNzX21hbmFnZXIge3sKICAgIHNlcnZlciB1bml4Oi90bXAvY3MtbWFuYWdlci5zb2NrOwp9fQoKc2VydmVyIHt7CiAgICBsaXN0ZW4ge2xpc3Rlbn07CiAgICBzZXJ2ZXJfbmFtZSBfOwogICAgY2xpZW50X21heF9ib2R5X3NpemUgMTAwTTsKCiAgICBhY2Nlc3NfbG9nIC9kZXYvc3Rkb3V0OwogICAgZXJyb3JfbG9nICAvZGV2L3N0ZGVyciB3YXJuOwoKICAgIGFic29sdXRlX3JlZGlyZWN0IG9mZjsKICAgIHBvcnRfaW5fcmVkaXJlY3Qgb2ZmOwoKICAgIGxvY2F0aW9uIC9pZGUvIHt7CiAgICAgICAgcHJveHlfcGFzcyBodHRwOi8vMTI3LjAuMC4xOntjc19wb3J0fS87CiAgICAgICAgcHJveHlfaHR0cF92ZXJzaW9uIDEuMTsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIEhvc3QgXCRob3N0OwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgVXBncmFkZSBcJGh0dHBfdXBncmFkZTsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIENvbm5lY3Rpb24gInVwZ3JhZGUiOwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgWC1Gb3J3YXJkZWQtRm9yIFwkcHJveHlfYWRkX3hfZm9yd2FyZGVkX2ZvcjsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIFgtRm9yd2FyZGVkLVByb3RvIFwkc2NoZW1lOwogICAgICAgIHByb3h5X3JlZGlyZWN0IC8gL2lkZS87CiAgICAgICAgcHJveHlfcmVhZF90aW1lb3V0IDg2NDAwOwogICAgICAgIHByb3h5X2Nvbm5lY3RfdGltZW91dCAyczsKICAgICAgICBlcnJvcl9wYWdlIDUwMiA1MDQgQGlkZV93YWtldXA7CiAgICAgICAgcG9zdF9hY3Rpb24gL2lkZS1oZWFydGJlYXQvOwogICAgfX0KCiAgICBsb2NhdGlvbiAvaWRlLWhlYXJ0YmVhdC8ge3sKICAgICAgICBpbnRlcm5hbDsKICAgICAgICByZXdyaXRlIF4gL2hlYXJ0YmVhdCBicmVhazsKICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly9jc19tYW5hZ2VyOwogICAgICAgIHByb3h5X2Nvbm5lY3RfdGltZW91dCAxczsKICAgICAgICBwcm94eV9yZWFkX3RpbWVvdXQgMnM7CiAgICB9fQoKICAgIGxvY2F0aW9uIEBpZGVfd2FrZXVwIHt7CiAgICAgICAgcmV3cml0ZSBeIC93YWtldXAgYnJlYWs7CiAgICAgICAgcHJveHlfcGFzcyBodHRwOi8vY3NfbWFuYWdlcjsKICAgICAgICBwcm94eV9odHRwX3ZlcnNpb24gMS4xOwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgSG9zdCBcJGhvc3Q7CiAgICAgICAgcHJveHlfY29ubmVjdF90aW1lb3V0IDVzOwogICAgICAgIHByb3h5X3JlYWRfdGltZW91dCAzMHM7CiAgICB9fQoKICAgIGxvY2F0aW9uIC8ge3sKICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly8xMjcuMC4wLjE6e2FwaV9wb3J0fS87CiAgICAgICAgcHJveHlfaHR0cF92ZXJzaW9uIDEuMTsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIEhvc3QgXCRob3N0OwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgVXBncmFkZSBcJGh0dHBfdXBncmFkZTsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIENvbm5lY3Rpb24gInVwZ3JhZGUiOwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgWC1Gb3J3YXJkZWQtRm9yIFwkcHJveHlfYWRkX3hfZm9yd2FyZGVkX2ZvcjsKICAgICAgICBwcm94eV9zZXRfaGVhZGVyIFgtRm9yd2FyZGVkLVByb3RvIFwkc2NoZW1lOwogICAgICAgIHByb3h5X3JlYWRfdGltZW91dCA4NjQwMDsKICAgIH19Cn19CiIiIgoKd2l0aCBvcGVuKCIvZXRjL25naW54L2NvbmYuZC9jaGF0MmFwaS5jb25mIiwgInciKSBhcyBmOgogICAgZi53cml0ZShjb25mKQpwcmludCgibmdpbnggY29uZmlnIHdyaXR0ZW4uIikKUFlFT0YKCmVjaG8gIm5naW54IOmFjee9ruW3sueUn+aIkCIKbmdpbnggLXQKZWNobyAi5ZCv5YqoIG5naW5477yI5YmN5Y+w6L+Q6KGM77yM5L+d5oyB5a655Zmo5a2Y5rS777yJLi4uIgpleGVjIG5naW54IC1nICdkYWVtb24gb2ZmOyBlcnJvcl9sb2cgL2Rldi9zdGRlcnIgd2FybjsnCg=='); p='/usr/local/bin/start-chat2api'; open(p,'wb').write(d); os.chmod(p, os.stat(p).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)"
72
+
73
+ RUN python3 -c "import base64,os; d=base64.b64decode('aW1wb3J0IG9zLCBqc29uCgpjb25maWcgPSB7CiAgICAicG9ydCI6IGludChvcy5nZXRlbnYoIkNIQVQyQVBJX1BPUlQiLCAiNzg2MiIpKSwKICAgICJhcGlLZXkiOiBvcy5nZXRlbnYoIkNIQVQyQVBJX0FQSV9LRVkiLCAiIiksCiAgICAibG9hZEJhbGFuY2UiOiB7CiAgICAgICAgInN0cmF0ZWd5Ijogb3MuZ2V0ZW52KCJMQl9TVFJBVEVHWSIsICJyb3VuZC1yb2JpbiIpCiAgICB9Cn0KZGF0YV9kaXIgPSBvcy5nZXRlbnYoIkNIQVQyQVBJX0RBVEFfRElSIiwgIi9yb290Ly5jaGF0MmFwaSIpCm9zLm1ha2VkaXJzKGRhdGFfZGlyLCBleGlzdF9vaz1UcnVlKQp3aXRoIG9wZW4ob3MucGF0aC5qb2luKGRhdGFfZGlyLCAiY29uZmlnLmpzb24iKSwgInciKSBhcyBmOgogICAganNvbi5kdW1wKGNvbmZpZywgZiwgaW5kZW50PTIpCnByaW50KCJjb25maWcuanNvbiBpbml0aWFsaXplZC4iKQo='); open('/usr/local/bin/init-config.py','wb').write(d)"
74
+
75
+ RUN python3 -c "import base64,os; d=base64.b64decode('aW1wb3J0IG9zLCBqc29uCgphY2NvdW50cyA9IFtdCmZvciBpIGluIHJhbmdlKDEsIDExKToKICAgIHB0eXBlID0gb3MuZ2V0ZW52KGYiUFJPVklERVJfe2l9X1RZUEUiLCAiIikuc3RyaXAoKQogICAgdG9rZW4gPSBvcy5nZXRlbnYoZiJQUk9WSURFUl97aX1fVE9LRU4iLCAiIikuc3RyaXAoKQogICAgaWYgcHR5cGUgYW5kIHRva2VuOgogICAgICAgIGFjY291bnRzLmFwcGVuZCh7InByb3ZpZGVyIjogcHR5cGUsICJ0b2tlbiI6IHRva2VuLCAiZW5hYmxlZCI6IFRydWV9KQoKZGF0YV9kaXIgPSBvcy5nZXRlbnYoIkNIQVQyQVBJX0RBVEFfRElSIiwgIi9yb290Ly5jaGF0MmFwaSIpCm9zLm1ha2VkaXJzKGRhdGFfZGlyLCBleGlzdF9vaz1UcnVlKQp3aXRoIG9wZW4ob3MucGF0aC5qb2luKGRhdGFfZGlyLCAiYWNjb3VudHMuanNvbiIpLCAidyIpIGFzIGY6CiAgICBqc29uLmR1bXAoYWNjb3VudHMsIGYsIGluZGVudD0yKQpwcmludChmImFjY291bnRzLmpzb24gaW5pdGlhbGl6ZWQgd2l0aCB7bGVuKGFjY291bnRzKX0gYWNjb3VudChzKS4iKQo='); open('/usr/local/bin/init-accounts.py','wb').write(d)"
76
+
77
+ # โ”€โ”€ 8. ้ชŒ่ฏ่„šๆœฌๅ†™ๅ…ฅๅฎŒๆ•ด๏ผˆๆž„ๅปบๆ—ถๆ ก้ชŒ๏ผŒๆœ‰้—ฎ้ข˜็ซ‹ๅณๆŠฅ้”™๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
78
+ RUN python3 -c "
79
+ import os, stat
80
+ scripts = {
81
+ '/usr/local/bin/sync.py': False,
82
+ '/usr/local/bin/cs-manager': True,
83
+ '/usr/local/bin/start-chat2api': True,
84
+ '/usr/local/bin/init-config.py': False,
85
+ '/usr/local/bin/init-accounts.py': False,
86
+ }
87
+ for path, need_exec in scripts.items():
88
+ assert os.path.exists(path), f'MISSING: {path}'
89
+ size = os.path.getsize(path)
90
+ assert size > 100, f'TOO SMALL ({size}B): {path}'
91
+ if need_exec:
92
+ mode = os.stat(path).st_mode
93
+ assert mode & stat.S_IEXEC, f'NOT EXECUTABLE: {path}'
94
+ print(f'OK {size:6d}B {path}')
95
+ print('All scripts verified.')
96
+ "
97
+
98
+ # โ”€โ”€ 9. ๆšด้œฒ็ซฏๅฃ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
99
+ EXPOSE 7860
100
+
101
+ # โ”€โ”€ ่ฎฟ้—ฎ่ฏดๆ˜Ž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
102
+ # Space ๅฏๅŠจๅŽ่ฎฟ้—ฎๆ–นๅผ๏ผš
103
+ # ไธป็•Œ้ข (Chat2API ็ฎก็†้ขๆฟ) : https://<your-space>.hf.space/
104
+ # API ๆŽฅๅ…ฅ็ซฏ็‚น : https://<your-space>.hf.space/v1/
105
+ # VS Code IDE : https://<your-space>.hf.space/ide/
106
+ #
107
+ # ๅฟ…ๅกซ HF Space Secrets๏ผš
108
+ # HF_TOKEN โ€”โ€” HuggingFace ่ฎฟ้—ฎไปค็‰Œ๏ผˆwrite ๆƒ้™๏ผ‰
109
+ # HF_DATASET โ€”โ€” Dataset repo ID๏ผŒๆ ผๅผ๏ผšusername/dataset-name
110
+ #
111
+ # ๅฏ้€‰ Secrets๏ผš
112
+ # CHAT2API_API_KEY โ€”โ€” Chat2API ่ฎฟ้—ฎๅฏ†้’ฅ๏ผˆ็•™็ฉบๅˆ™ๆ— ้œ€่ฎค่ฏ๏ผ‰
113
+ # CODE_SERVER_PASSWORD โ€”โ€” VS Code IDE ็™ปๅฝ•ๅฏ†็ ๏ผˆ้ป˜่ฎค๏ผšchangeme123!๏ผ‰
114
+ # IDE_IDLE_MINUTES โ€”โ€” IDE ้—ฒ็ฝฎ่‡ชๅŠจๅ…ณ้—ญๅˆ†้’Ÿๆ•ฐ๏ผˆ้ป˜่ฎค๏ผš30๏ผ‰
115
+ # PROVIDER_1_TYPE / PROVIDER_1_TOKEN โ€”โ€” ้ข„ๆณจๅ…ฅๆœๅŠกๅ•†๏ผˆๆœ€ๅคš 10 ไธช๏ผ‰
116
+ # LB_STRATEGY โ€”โ€” ่ดŸ่ฝฝๅ‡่กก็ญ–็•ฅ๏ผšround-robin / fill-first / failover
117
+ # FORCE_RESTORE โ€”โ€” ่ฎพไธบ true ๅผบๅˆถไปŽ Dataset ่ฆ†็›–ๆขๅค
118
+
119
+ CMD ["/usr/local/bin/start-chat2api"]