qwen2api commited on
Commit
f78edd7
·
verified ·
1 Parent(s): 861e8c7

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +24 -3
  2. index.js +314 -0
Dockerfile CHANGED
@@ -1,3 +1,24 @@
1
- FROM ekkoye8888/hermes-web-ui
2
- ENV PORT=7860
3
- ENV HERMES_ALLOW_ROOT_GATEWAY=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM docker:latest
2
+
3
+ # 安装 Node.js 和 curl
4
+ RUN apk add --no-cache nodejs npm curl
5
+
6
+ WORKDIR /app
7
+ COPY index.js .
8
+
9
+ # 默认环境变量
10
+ ENV TARGET_IMAGE="library/node, library/alpine"
11
+ # 格式: user1:pass1,user2:pass2
12
+ ENV DOCKER_ACCOUNTS=""
13
+ ENV REPORT_URL=""
14
+ # 间隔时间(秒)
15
+ ENV INTERVAL_SECONDS="60"
16
+ # 标记持续时间(秒),默认1小时
17
+ ENV MARK_DURATION_SECONDS="3600"
18
+
19
+ # 暴露端口
20
+ EXPOSE 7860
21
+
22
+ # 覆盖 docker 镜像的默认 ENTRYPOINT
23
+ ENTRYPOINT []
24
+ CMD ["node", "index.js"]
index.js ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require('http');
2
+ const { exec } = require('child_process');
3
+
4
+ // 公网 IP
5
+ let publicIP = 'unknown';
6
+
7
+ // 上报状态
8
+ let lastReportTime = null;
9
+ let lastReportSuccess = false;
10
+
11
+ // 获取环境变量
12
+ const TARGET_IMAGE = process.env.TARGET_IMAGE || '';
13
+ const DOCKER_ACCOUNTS = process.env.DOCKER_ACCOUNTS || ''; // 格式: user1:pass1,user2:pass2
14
+ const REPORT_URL = process.env.REPORT_URL || '';
15
+ const INTERVAL_SECONDS = parseInt(process.env.INTERVAL_SECONDS) || 60; // 间隔时间(秒)
16
+ const MARK_DURATION_SECONDS = parseInt(process.env.MARK_DURATION_SECONDS) || 3600; // 标记持续时间(秒),默认1小时
17
+
18
+ // 解析 Docker 账号列表
19
+ const accounts = DOCKER_ACCOUNTS.split(',')
20
+ .map(acc => {
21
+ const [user, pass] = acc.trim().split(':');
22
+ return user && pass ? { user, pass } : null;
23
+ })
24
+ .filter(acc => acc);
25
+
26
+ // 账号状态管理
27
+ const accountMarkedTime = new Map(); // 记录账号被标记的时间
28
+ let currentAccountIndex = 0;
29
+ let loggedInAccount = null; // 当前已登录的账号
30
+ let anonymousMarkedTime = null; // 匿名用户被标记的时间
31
+
32
+ // 获取当前可用账号(跳过被标记的)
33
+ function getAvailableAccount() {
34
+ if (accounts.length === 0) return null;
35
+
36
+ const now = Date.now();
37
+ const markDuration = MARK_DURATION_SECONDS * 1000;
38
+
39
+ // 清理已过期的标记
40
+ for (const [user, markedTime] of accountMarkedTime) {
41
+ if (now - markedTime >= markDuration) {
42
+ accountMarkedTime.delete(user);
43
+ console.log(`账号 ${user} 标记已移除`);
44
+ }
45
+ }
46
+
47
+ // 查找可用账号
48
+ for (let i = 0; i < accounts.length; i++) {
49
+ const idx = (currentAccountIndex + i) % accounts.length;
50
+ const acc = accounts[idx];
51
+ if (!accountMarkedTime.has(acc.user)) {
52
+ currentAccountIndex = idx;
53
+ return acc;
54
+ }
55
+ }
56
+
57
+ // 所有账号都被标记
58
+ return null;
59
+ }
60
+
61
+ // 获取离最近标记移除的时间(毫秒)
62
+ function getTimeUntilNextUnmark() {
63
+ if (accountMarkedTime.size === 0) return 0;
64
+
65
+ const now = Date.now();
66
+ const markDuration = MARK_DURATION_SECONDS * 1000;
67
+ let minWait = Infinity;
68
+
69
+ for (const markedTime of accountMarkedTime.values()) {
70
+ const wait = markDuration - (now - markedTime);
71
+ if (wait < minWait && wait > 0) minWait = wait;
72
+ }
73
+
74
+ return minWait === Infinity ? 0 : minWait;
75
+ }
76
+
77
+ // 获取公网 IP
78
+ async function fetchPublicIP() {
79
+ return new Promise((resolve) => {
80
+ exec('curl -s --connect-timeout 5 https://api.ipify.org', (error, stdout, stderr) => {
81
+ if (error || !stdout.trim()) {
82
+ console.error('获取公网 IP 失败:', error?.message || 'empty response');
83
+ resolve('unknown');
84
+ } else {
85
+ resolve(stdout.trim());
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ // 定期更新公网 IP
92
+ async function startIPUpdate() {
93
+ publicIP = await fetchPublicIP();
94
+ console.log(`公网 IP: ${publicIP}`);
95
+
96
+ // 每 5 分钟更新一次
97
+ setInterval(async () => {
98
+ publicIP = await fetchPublicIP();
99
+ }, 5 * 60 * 1000);
100
+ }
101
+
102
+ // 上报 URL
103
+ async function reportToURL() {
104
+ if (!REPORT_URL) return;
105
+
106
+ return new Promise((resolve) => {
107
+ exec(`curl -s --connect-timeout 10 "${REPORT_URL}"`, (error, stdout, stderr) => {
108
+ lastReportTime = new Date().toISOString();
109
+ if (error) {
110
+ lastReportSuccess = false;
111
+ console.error(`[${lastReportTime}] 上报失败:`, error.message);
112
+ } else {
113
+ lastReportSuccess = true;
114
+ console.log(`[${lastReportTime}] 上报成功`);
115
+ }
116
+ resolve();
117
+ });
118
+ });
119
+ }
120
+
121
+ // 定时上报(每 6 小时)
122
+ async function startReportTask() {
123
+ if (!REPORT_URL) {
124
+ console.log('REPORT_URL 未配置,跳过上报');
125
+ return;
126
+ }
127
+
128
+ // 立即执行一次
129
+ await reportToURL();
130
+
131
+ // 每 6 小时执行一次
132
+ setInterval(async () => {
133
+ await reportToURL();
134
+ }, 6 * 60 * 60 * 1000);
135
+ }
136
+
137
+ // 解析镜像名称列表
138
+ const images = TARGET_IMAGE.split(',').map(img => img.trim()).filter(img => img);
139
+
140
+ // 当前镜像索引(循环使用)
141
+ let currentImageIndex = 0;
142
+
143
+ console.log(`配置: ${images.length} 个镜像, ${accounts.length} 个账号, 间隔 ${INTERVAL_SECONDS} 秒`);
144
+
145
+ // Docker 登录
146
+ async function dockerLogin(account) {
147
+ return new Promise((resolve) => {
148
+ exec(`echo "${account.pass}" | docker login -u "${account.user}" --password-stdin`, (error, stdout, stderr) => {
149
+ if (error) {
150
+ console.error(`账号 ${account.user} 登录失败:`, stderr.trim());
151
+ resolve(false);
152
+ } else {
153
+ console.log(`账号 ${account.user} 登录成功`);
154
+ resolve(true);
155
+ }
156
+ });
157
+ });
158
+ }
159
+
160
+ // 运行一次 docker manifest inspect(只检查一个镜像)
161
+ async function runManifestInspect() {
162
+ if (images.length === 0) {
163
+ console.log('没有配置 TARGET_IMAGE');
164
+ return;
165
+ }
166
+
167
+ // 获取当前要检查的镜像
168
+ const image = images[currentImageIndex];
169
+
170
+ // 获取可用账号
171
+ const account = getAvailableAccount();
172
+
173
+ if (accounts.length > 0 && !account) {
174
+ // 所有账号都被标记,等待
175
+ const waitTime = getTimeUntilNextUnmark();
176
+ console.log(`所有账号都被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`);
177
+ setTimeout(() => runManifestInspect(), waitTime);
178
+ return;
179
+ }
180
+
181
+ // 检查匿名用户是否被标记
182
+ if (accounts.length === 0 && anonymousMarkedTime) {
183
+ const now = Date.now();
184
+ const markDuration = MARK_DURATION_SECONDS * 1000;
185
+ if (now - anonymousMarkedTime < markDuration) {
186
+ // 匿名用户仍在标记期内,等待
187
+ const waitTime = markDuration - (now - anonymousMarkedTime);
188
+ console.log(`匿名用户已被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`);
189
+ setTimeout(() => runManifestInspect(), waitTime);
190
+ return;
191
+ } else {
192
+ // 标记已过期,清除
193
+ anonymousMarkedTime = null;
194
+ console.log('匿名用户标记已移除');
195
+ }
196
+ }
197
+
198
+ // 如果有账号,检查是否需要登录
199
+ if (account) {
200
+ // 如果当前账号已登录,跳过登录
201
+ if (loggedInAccount?.user !== account.user) {
202
+ const loginSuccess = await dockerLogin(account);
203
+ if (!loginSuccess) {
204
+ // 登录失败,标记账号,尝试下一个
205
+ accountMarkedTime.set(account.user, Date.now());
206
+ console.log(`账号 ${account.user} 登录失败,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
207
+ loggedInAccount = null; // 重置登录状态
208
+ currentAccountIndex = (currentAccountIndex + 1) % accounts.length;
209
+ return;
210
+ }
211
+ loggedInAccount = account;
212
+ }
213
+ }
214
+
215
+ // 执行 manifest inspect
216
+ await new Promise((resolve) => {
217
+ exec(`docker manifest inspect ${image}`, (error, stdout, stderr) => {
218
+ const timestamp = new Date().toISOString();
219
+ if (error) {
220
+ const errorMsg = stderr.trim();
221
+ console.error(`[${timestamp}] 检查镜像 ${image} 失败:`, errorMsg);
222
+
223
+ // 检查是否是 rate limit 错误
224
+ if (errorMsg.includes('toomanyrequests') || errorMsg.includes('rate limit')) {
225
+ if (account) {
226
+ accountMarkedTime.set(account.user, Date.now());
227
+ console.log(`账号 ${account.user} 触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
228
+ currentAccountIndex = (currentAccountIndex + 1) % accounts.length;
229
+ loggedInAccount = null; // 重置登录状态
230
+ } else {
231
+ // 没有配置账号,标记匿名用户
232
+ anonymousMarkedTime = Date.now();
233
+ console.log(`匿名用户触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
234
+ }
235
+ }
236
+ } else {
237
+ console.log(`[${timestamp}] 检查镜像 ${image} 成功${account ? ` (账号: ${account.user})` : ' (匿名)'}`);
238
+ }
239
+ resolve();
240
+ });
241
+ });
242
+
243
+ // 更新索引,循环到下一个镜像
244
+ currentImageIndex = (currentImageIndex + 1) % images.length;
245
+ }
246
+
247
+ // 后台定时任务
248
+ async function startBackgroundTask() {
249
+ // 立即运行一次
250
+ await runManifestInspect();
251
+
252
+ // 使用递归 setTimeout 代替 setInterval,支持动态等待
253
+ function scheduleNext() {
254
+ setTimeout(async () => {
255
+ await runManifestInspect();
256
+ scheduleNext();
257
+ }, INTERVAL_SECONDS * 1000);
258
+ }
259
+ scheduleNext();
260
+ }
261
+
262
+ // HTTP 服务器
263
+ const server = http.createServer((req, res) => {
264
+ // 隐藏镜像名称:只显示前后一位字母
265
+ const hiddenImages = images.map(img => {
266
+ if (img.length <= 2) return img;
267
+ return img[0] + '...' + img[img.length - 1];
268
+ });
269
+
270
+ // 账号状态
271
+ const accountStatus = accounts.map(acc => ({
272
+ user: acc.user,
273
+ marked: accountMarkedTime.has(acc.user)
274
+ }));
275
+
276
+ // 匿名用户标记状态
277
+ let anonymousMarked = false;
278
+ let anonymousMarkedRemaining = 0;
279
+ if (anonymousMarkedTime) {
280
+ const now = Date.now();
281
+ const elapsed = now - anonymousMarkedTime;
282
+ const markDuration = MARK_DURATION_SECONDS * 1000;
283
+ if (elapsed >= markDuration) {
284
+ anonymousMarkedTime = null; // 标记已过期
285
+ } else {
286
+ anonymousMarked = true;
287
+ anonymousMarkedRemaining = Math.round((markDuration - elapsed) / 1000);
288
+ }
289
+ }
290
+
291
+ res.writeHead(200, { 'Content-Type': 'application/json' });
292
+ res.end(JSON.stringify({
293
+ status: 'running',
294
+ publicIP: publicIP,
295
+ imageCount: images.length,
296
+ images: hiddenImages,
297
+ intervalSeconds: INTERVAL_SECONDS,
298
+ accounts: accountStatus,
299
+ currentAccount: accounts[currentAccountIndex]?.user || null,
300
+ anonymousMarked: anonymousMarked,
301
+ anonymousMarkedRemaining: anonymousMarkedRemaining,
302
+ lastReportTime: lastReportTime,
303
+ lastReportSuccess: lastReportSuccess
304
+ }));
305
+ });
306
+
307
+ // 启动
308
+ const PORT = 7860;
309
+ server.listen(PORT, () => {
310
+ console.log(`HTTP 服务器运行在端口 ${PORT}`);
311
+ startIPUpdate();
312
+ startReportTask();
313
+ startBackgroundTask();
314
+ });