bg5 commited on
Commit
bbe37cc
·
verified ·
1 Parent(s): 8084313

部署 zai2api 到 HF Space

Browse files
Files changed (6) hide show
  1. Dockerfile +17 -16
  2. captcha-provider/server.js +10 -169
  3. deploy_hf_space.py +17 -8
  4. init.sh +61 -0
  5. internal/captcha.go +5 -11
  6. internal/chat.go +1 -1
Dockerfile CHANGED
@@ -12,12 +12,12 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /zai2api ./cmd/main.go
12
 
13
  FROM node:20-bookworm-slim
14
 
15
- ENV DEBIAN_FRONTEND=noninteractive \
16
  BROWSER_BACKEND=cloak \
17
- TOKEN_BOUND_CAPTCHA=true \
18
  CHROMIUM_PATH=/usr/bin/chromium \
19
- CAPTCHA_PROVIDER_URL=http://127.0.0.1:9876 \
20
- LOG_LEVEL=info \
21
  PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
22
 
23
  RUN apt-get update \
@@ -36,15 +36,16 @@ RUN cd /app/captcha-provider \
36
  && npm ci --omit=dev \
37
  && npm cache clean --force
38
 
39
- COPY --chown=node:node captcha-provider /app/captcha-provider
40
- COPY --chown=node:node start-space.sh /app/start-space.sh
41
-
42
- RUN mkdir -p /app/data \
43
- && chown -R node:node /app \
44
- && chmod +x /app/start-space.sh
45
-
46
- USER node
47
-
48
- EXPOSE 7860
49
- ENTRYPOINT ["tini", "--"]
50
- CMD ["/app/start-space.sh"]
 
 
12
 
13
  FROM node:20-bookworm-slim
14
 
15
+ ENV DEBIAN_FRONTEND=noninteractive \
16
  BROWSER_BACKEND=cloak \
17
+ POOL_SIZE=0 \
18
  CHROMIUM_PATH=/usr/bin/chromium \
19
+ CAPTCHA_PROVIDER_URL=http://127.0.0.1:9876 \
20
+ LOG_LEVEL=info \
21
  PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
22
 
23
  RUN apt-get update \
 
36
  && npm ci --omit=dev \
37
  && npm cache clean --force
38
 
39
+ COPY --chown=node:node captcha-provider /app/captcha-provider
40
+ COPY --chown=node:node .env /app/.env
41
+ COPY --chown=node:node init.sh /app/init.sh
42
+
43
+ RUN mkdir -p /app/data \
44
+ && chown -R node:node /app \
45
+ && chmod +x /app/init.sh
46
+
47
+ USER node
48
+
49
+ EXPOSE 7860
50
+ ENTRYPOINT ["tini", "--"]
51
+ CMD ["/app/init.sh"]
captcha-provider/server.js CHANGED
@@ -20,7 +20,6 @@ const TOKEN_TTL = Number(process.env.TOKEN_TTL || 240000); // token 有效期 4
20
  const REFILL_INTERVAL = Number(process.env.REFILL_INTERVAL || 3000); // 补充间隔 3s
21
  const FALLBACK_CAPTCHA_PROVIDER_URL = process.env.FALLBACK_CAPTCHA_PROVIDER_URL || '';
22
  const FALLBACK_CAPTCHA_TIMEOUT = Number(process.env.FALLBACK_CAPTCHA_TIMEOUT || 60000);
23
- const TOKEN_BOUND_CAPTCHA = process.env.TOKEN_BOUND_CAPTCHA !== 'false' && BROWSER_BACKEND === 'cloak';
24
 
25
  // ─── State ───
26
  let browser = null;
@@ -186,21 +185,21 @@ async function acquireToken() {
186
 
187
  try {
188
  return await new Promise((resolve, reject) => {
189
- const timeout = setTimeout(() => reject(new Error('captcha timeout')), 25000);
190
  window.initAliyunCaptcha({
191
  SceneId: sceneId,
192
  mode: 'popup',
193
  element: `#${id}`,
194
  button: `#${triggerId}`,
195
  language: 'cn',
196
- timeout: 10000,
197
  delayBeforeSuccess: false,
198
  success: (token) => { clearTimeout(timeout); resolve(token); },
199
  fail: () => {},
200
  onError: (err) => { clearTimeout(timeout); reject(new Error('captcha error: ' + JSON.stringify(err))); },
201
  onClose: () => {},
202
  });
203
- setTimeout(() => trigger.click(), 200);
204
  });
205
  } finally {
206
  try { container.remove(); } catch {}
@@ -213,131 +212,6 @@ async function acquireToken() {
213
  }
214
  }
215
 
216
- function decodeJwtPayload(token) {
217
- const parts = String(token || '').split('.');
218
- if (parts.length < 2) throw new Error('invalid jwt');
219
- const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
220
- const padded = payload + '='.repeat((4 - payload.length % 4) % 4);
221
- return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
222
- }
223
-
224
- function extractUserId(jwtToken) {
225
- const payload = decodeJwtPayload(jwtToken);
226
- if (!payload.id) throw new Error('jwt missing id');
227
- return payload.id;
228
- }
229
-
230
- async function launchTokenBrowser() {
231
- const proxyURL = process.env.PROXY_SERVER || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
232
-
233
- if (BROWSER_BACKEND === 'cloak') {
234
- const opts = {
235
- headless: true,
236
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
237
- };
238
- if (proxyURL) opts.proxy = { server: proxyURL };
239
- return cloak.launch(opts);
240
- }
241
-
242
- const launchOpts = {
243
- executablePath: CHROMIUM_PATH,
244
- headless: true,
245
- args: [
246
- '--disable-blink-features=AutomationControlled',
247
- '--disable-dev-shm-usage',
248
- '--no-sandbox',
249
- '--disable-gpu',
250
- '--disable-extensions',
251
- ],
252
- };
253
- if (proxyURL) launchOpts.proxy = { server: proxyURL };
254
- return chromium.launch(launchOpts);
255
- }
256
-
257
- async function acquireTokenFromChat(jwtToken) {
258
- const userId = extractUserId(jwtToken);
259
- console.log(`[provider] Captcha requested for user ${String(userId).slice(0, 12)}...`);
260
-
261
- const localBrowser = await launchTokenBrowser();
262
- try {
263
- const localCtx = await localBrowser.newContext({
264
- viewport: { width: 1265, height: 1281 },
265
- locale: 'zh-CN',
266
- timezoneId: 'Asia/Shanghai',
267
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
268
- });
269
- await localCtx.addInitScript(() => {
270
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
271
- Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
272
- Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en-US', 'en'] });
273
- window.chrome = { runtime: {}, loadTimes: () => ({}) };
274
- });
275
-
276
- const freshPage = await localCtx.newPage();
277
- await freshPage.goto(ZAI_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
278
- await freshPage.waitForSelector('textarea, [contenteditable="true"], [role="textbox"]', { timeout: 15000 });
279
- await freshPage.evaluate((token) => {
280
- localStorage.setItem('token', token);
281
- }, jwtToken);
282
- await freshPage.reload({ waitUntil: 'domcontentloaded', timeout: 30000 });
283
- await freshPage.waitForSelector('textarea, [contenteditable="true"], [role="textbox"]', { timeout: 15000 });
284
- await freshPage.waitForTimeout(1000);
285
-
286
- let capturedToken = '';
287
- let resolveToken;
288
- let rejectToken;
289
- const tokenPromise = new Promise((resolve, reject) => {
290
- resolveToken = resolve;
291
- rejectToken = reject;
292
- });
293
- const captureTimeout = setTimeout(() => rejectToken(new Error('chat captcha timeout')), 25000);
294
-
295
- await freshPage.route('**/api/v2/chat/completions**', async (route) => {
296
- try {
297
- const body = route.request().postDataJSON();
298
- if (body && body.captcha_verify_param) {
299
- capturedToken = body.captcha_verify_param;
300
- clearTimeout(captureTimeout);
301
- await route.abort('blockedbyclient');
302
- resolveToken(capturedToken);
303
- return;
304
- }
305
- } catch (err) {
306
- console.warn(`[provider] Route parse error: ${err.message}`);
307
- }
308
- try {
309
- await route.continue();
310
- } catch {}
311
- });
312
-
313
- const inputSelector = 'textarea, [contenteditable="true"], [role="textbox"]';
314
- await freshPage.fill(inputSelector, 'hi');
315
- const buttonState = await freshPage.evaluate(() => {
316
- const btn = document.querySelector('#send-message-button') || document.querySelector('button[type="submit"]');
317
- if (!btn) return 'not_found';
318
- if (btn.disabled) btn.disabled = false;
319
- return 'ready';
320
- });
321
- if (buttonState === 'not_found') {
322
- throw new Error('send button not found');
323
- }
324
- await freshPage.click('#send-message-button, button[type="submit"]', { timeout: 5000 });
325
-
326
- return await tokenPromise;
327
- } finally {
328
- try { await localBrowser.close(); } catch {}
329
- }
330
- }
331
-
332
- async function readJsonBody(req) {
333
- let raw = '';
334
- for await (const chunk of req) {
335
- raw += chunk;
336
- if (raw.length > 1024 * 1024) throw new Error('request body too large');
337
- }
338
- return raw ? JSON.parse(raw) : {};
339
- }
340
-
341
  async function acquireFallbackToken() {
342
  if (!FALLBACK_CAPTCHA_PROVIDER_URL) return null;
343
  if (!process.env.BACKUP_TOKEN) {
@@ -440,39 +314,10 @@ const server = http.createServer(async (req, res) => {
440
  });
441
  }
442
 
443
- if (req.url === '/token' && req.method === 'POST') {
444
- if (!ready) {
445
- return sendJson(res, 503, { ok: false, error: 'not ready', lastError });
446
- }
447
-
448
- try {
449
- const body = await readJsonBody(req);
450
- const jwtToken = body.jwt_token || body.jwtToken || '';
451
- if (!jwtToken) {
452
- return sendJson(res, 400, { ok: false, error: 'jwt_token required' });
453
- }
454
- const started = Date.now();
455
- const token = await acquireTokenFromChat(jwtToken);
456
- const elapsed = Date.now() - started;
457
- stats.served++;
458
- console.log(`[provider] Served chat captcha token in ${elapsed}ms`);
459
- return sendJson(res, 200, { ok: true, token, captcha_verify_param: token, cached: false, elapsed_ms: elapsed });
460
- } catch (err) {
461
- lastError = err.message;
462
- stats.errors++;
463
- console.error(`[provider] Chat captcha error: ${err.message}`);
464
- return sendJson(res, 500, { ok: false, error: err.message });
465
- }
466
- }
467
-
468
  if (req.method === 'GET' && req.url === '/token') {
469
  if (!ready) {
470
  return sendJson(res, 503, { error: 'not ready', lastError });
471
  }
472
-
473
- if (TOKEN_BOUND_CAPTCHA) {
474
- return sendJson(res, 400, { ok: false, error: 'jwt_token required' });
475
- }
476
 
477
  // 先从池里取
478
  const cached = getValidToken();
@@ -515,25 +360,21 @@ const server = http.createServer(async (req, res) => {
515
  }
516
  }
517
 
518
- sendJson(res, 404, { error: 'Use POST /token or GET /health' });
519
  });
520
 
521
  // ─── Start ───
522
 
523
  server.listen(PORT, HOST, async () => {
524
  console.log(`[provider] zai-captcha-provider listening on http://${HOST}:${PORT}`);
525
- console.log(`[provider] Pool size: ${POOL_SIZE}, TTL: ${TOKEN_TTL}ms`);
526
  try {
527
  await launchBrowser();
528
- if (TOKEN_BOUND_CAPTCHA) {
529
- console.log('[provider] Token-bound captcha mode enabled; skipping anonymous prefill');
530
- } else {
531
- // 初始填充池
532
- await refillPool();
533
- console.log(`[provider] Initial pool filled: ${tokenPool.length} tokens`);
534
- // 定期补充
535
- setInterval(refillPool, REFILL_INTERVAL);
536
- }
537
  } catch (err) {
538
  console.error('[provider] Startup error:', err.message);
539
  lastError = err.message;
 
20
  const REFILL_INTERVAL = Number(process.env.REFILL_INTERVAL || 3000); // 补充间隔 3s
21
  const FALLBACK_CAPTCHA_PROVIDER_URL = process.env.FALLBACK_CAPTCHA_PROVIDER_URL || '';
22
  const FALLBACK_CAPTCHA_TIMEOUT = Number(process.env.FALLBACK_CAPTCHA_TIMEOUT || 60000);
 
23
 
24
  // ─── State ───
25
  let browser = null;
 
185
 
186
  try {
187
  return await new Promise((resolve, reject) => {
188
+ const timeout = setTimeout(() => reject(new Error('captcha timeout')), 60000);
189
  window.initAliyunCaptcha({
190
  SceneId: sceneId,
191
  mode: 'popup',
192
  element: `#${id}`,
193
  button: `#${triggerId}`,
194
  language: 'cn',
195
+ timeout: 30000,
196
  delayBeforeSuccess: false,
197
  success: (token) => { clearTimeout(timeout); resolve(token); },
198
  fail: () => {},
199
  onError: (err) => { clearTimeout(timeout); reject(new Error('captcha error: ' + JSON.stringify(err))); },
200
  onClose: () => {},
201
  });
202
+ setTimeout(() => trigger.click(), 500);
203
  });
204
  } finally {
205
  try { container.remove(); } catch {}
 
212
  }
213
  }
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  async function acquireFallbackToken() {
216
  if (!FALLBACK_CAPTCHA_PROVIDER_URL) return null;
217
  if (!process.env.BACKUP_TOKEN) {
 
314
  });
315
  }
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  if (req.method === 'GET' && req.url === '/token') {
318
  if (!ready) {
319
  return sendJson(res, 503, { error: 'not ready', lastError });
320
  }
 
 
 
 
321
 
322
  // 先从池里取
323
  const cached = getValidToken();
 
360
  }
361
  }
362
 
363
+ sendJson(res, 404, { error: 'Use GET /token or GET /health' });
364
  });
365
 
366
  // ─── Start ───
367
 
368
  server.listen(PORT, HOST, async () => {
369
  console.log(`[provider] zai-captcha-provider listening on http://${HOST}:${PORT}`);
370
+ console.log(`[provider] Pool size: ${POOL_SIZE}, TTL: ${TOKEN_TTL}ms`);
371
  try {
372
  await launchBrowser();
373
+ // 初始填充池
374
+ await refillPool();
375
+ console.log(`[provider] Initial pool filled: ${tokenPool.length} tokens`);
376
+ // 定期补充
377
+ setInterval(refillPool, REFILL_INTERVAL);
 
 
 
 
378
  } catch (err) {
379
  console.error('[provider] Startup error:', err.message);
380
  lastError = err.message;
deploy_hf_space.py CHANGED
@@ -43,11 +43,12 @@ def main() -> None:
43
  "node_modules/*",
44
  "captcha-provider/node_modules/*",
45
  "data/*",
46
- "*.log",
47
- ".generated-auth-token.txt",
48
- ],
49
- commit_message="部署 zai2api 到 HF Space",
50
- )
 
51
 
52
  api.add_space_secret(
53
  repo_id=space_id,
@@ -67,11 +68,19 @@ def main() -> None:
67
  repo_id=space_id,
68
  key="CAPTCHA_PROVIDER_URL",
69
  value="http://127.0.0.1:9876",
70
- )
71
  api.add_space_variable(repo_id=space_id, key="BROWSER_BACKEND", value="cloak")
72
- api.add_space_variable(repo_id=space_id, key="TOKEN_BOUND_CAPTCHA", value="true")
73
  api.add_space_variable(repo_id=space_id, key="CHROMIUM_PATH", value="/usr/bin/chromium")
74
-
 
 
 
 
 
 
 
 
75
  print(f"Space: https://huggingface.co/spaces/{space_id}")
76
  print(f"App: https://{space_id.replace('/', '-')}.hf.space")
77
  if generated_auth_token:
 
43
  "node_modules/*",
44
  "captcha-provider/node_modules/*",
45
  "data/*",
46
+ "*.log",
47
+ ".generated-auth-token.txt",
48
+ "proxy-3000.js",
49
+ ],
50
+ commit_message="部署 zai2api 到 HF Space",
51
+ )
52
 
53
  api.add_space_secret(
54
  repo_id=space_id,
 
68
  repo_id=space_id,
69
  key="CAPTCHA_PROVIDER_URL",
70
  value="http://127.0.0.1:9876",
71
+ )
72
  api.add_space_variable(repo_id=space_id, key="BROWSER_BACKEND", value="cloak")
73
+ api.add_space_variable(repo_id=space_id, key="POOL_SIZE", value="0")
74
  api.add_space_variable(repo_id=space_id, key="CHROMIUM_PATH", value="/usr/bin/chromium")
75
+ api.add_space_variable(repo_id=space_id, key="LOG_LEVEL", value="info")
76
+ api.add_space_variable(repo_id=space_id, key="DEBUG_LOGGING", value="false")
77
+
78
+ for stale_key in ("TOKEN_BOUND_CAPTCHA", "FALLBACK_CAPTCHA_PROVIDER_URL"):
79
+ try:
80
+ api.delete_space_variable(repo_id=space_id, key=stale_key)
81
+ except Exception:
82
+ pass
83
+
84
  print(f"Space: https://huggingface.co/spaces/{space_id}")
85
  print(f"App: https://{space_id.replace('/', '-')}.hf.space")
86
  if generated_auth_token:
init.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ if [ -f /app/.env ]; then
5
+ set -a
6
+ . /app/.env
7
+ set +a
8
+ fi
9
+
10
+ : "${PORT:=7860}"
11
+ : "${CAPTCHA_INTERNAL_HOST:=127.0.0.1}"
12
+ : "${CAPTCHA_INTERNAL_PORT:=9876}"
13
+ : "${CAPTCHA_PROVIDER_URL:=http://${CAPTCHA_INTERNAL_HOST}:${CAPTCHA_INTERNAL_PORT}}"
14
+ : "${BROWSER_BACKEND:=cloak}"
15
+ : "${POOL_SIZE:=0}"
16
+
17
+ export PORT CAPTCHA_PROVIDER_URL BROWSER_BACKEND POOL_SIZE
18
+
19
+ mkdir -p /app/data
20
+
21
+ supervise_captcha() {
22
+ while true; do
23
+ echo "[space] starting captcha provider on ${CAPTCHA_INTERNAL_HOST}:${CAPTCHA_INTERNAL_PORT}"
24
+ set +e
25
+ (
26
+ cd /app/captcha-provider
27
+ HOST="${CAPTCHA_INTERNAL_HOST}" PORT="${CAPTCHA_INTERNAL_PORT}" node server.js
28
+ )
29
+ code="$?"
30
+ set -e
31
+ echo "[space] captcha provider exited with ${code}, restarting in 2s"
32
+ sleep 2
33
+ done
34
+ }
35
+
36
+ supervise_proxy() {
37
+ while true; do
38
+ echo "[space] starting zai2api proxy on 0.0.0.0:${PORT}"
39
+ set +e
40
+ /app/zai2api
41
+ code="$?"
42
+ set -e
43
+ echo "[space] zai2api proxy exited with ${code}, restarting in 2s"
44
+ sleep 2
45
+ done
46
+ }
47
+
48
+ cleanup() {
49
+ kill "${CAPTCHA_SUP_PID:-}" "${PROXY_SUP_PID:-}" 2>/dev/null || true
50
+ wait 2>/dev/null || true
51
+ }
52
+
53
+ trap 'cleanup; exit 0' INT TERM EXIT
54
+
55
+ supervise_captcha &
56
+ CAPTCHA_SUP_PID="$!"
57
+
58
+ supervise_proxy &
59
+ PROXY_SUP_PID="$!"
60
+
61
+ wait
internal/captcha.go CHANGED
@@ -1,7 +1,6 @@
1
  package internal
2
 
3
  import (
4
- "bytes"
5
  "encoding/json"
6
  "fmt"
7
  "io"
@@ -11,14 +10,13 @@ import (
11
  )
12
 
13
  type captchaProviderResponse struct {
14
- OK bool `json:"ok"`
15
- Token string `json:"token"`
16
- CaptchaVerifyParam string `json:"captcha_verify_param"`
17
- Error string `json:"error"`
18
  }
19
 
20
  // fetchCaptchaToken 从 captcha provider 服务获取验证码 token
21
- func fetchCaptchaToken(upstreamToken string) (string, error) {
22
  if Cfg.CaptchaProviderURL == "" {
23
  return "", nil
24
  }
@@ -26,8 +24,7 @@ func fetchCaptchaToken(upstreamToken string) (string, error) {
26
  url := strings.TrimRight(Cfg.CaptchaProviderURL, "/") + "/token"
27
 
28
  client := &http.Client{Timeout: 35 * time.Second}
29
- payload, _ := json.Marshal(map[string]string{"jwt_token": upstreamToken})
30
- resp, err := client.Post(url, "application/json", bytes.NewReader(payload))
31
  if err != nil {
32
  return "", fmt.Errorf("captcha provider request failed: %w", err)
33
  }
@@ -50,9 +47,6 @@ func fetchCaptchaToken(upstreamToken string) (string, error) {
50
  if !result.OK {
51
  return "", fmt.Errorf("captcha provider error: %s", result.Error)
52
  }
53
- if result.Token == "" {
54
- result.Token = result.CaptchaVerifyParam
55
- }
56
 
57
  return result.Token, nil
58
  }
 
1
  package internal
2
 
3
  import (
 
4
  "encoding/json"
5
  "fmt"
6
  "io"
 
10
  )
11
 
12
  type captchaProviderResponse struct {
13
+ OK bool `json:"ok"`
14
+ Token string `json:"token"`
15
+ Error string `json:"error"`
 
16
  }
17
 
18
  // fetchCaptchaToken 从 captcha provider 服务获取验证码 token
19
+ func fetchCaptchaToken() (string, error) {
20
  if Cfg.CaptchaProviderURL == "" {
21
  return "", nil
22
  }
 
24
  url := strings.TrimRight(Cfg.CaptchaProviderURL, "/") + "/token"
25
 
26
  client := &http.Client{Timeout: 35 * time.Second}
27
+ resp, err := client.Get(url)
 
28
  if err != nil {
29
  return "", fmt.Errorf("captcha provider request failed: %w", err)
30
  }
 
47
  if !result.OK {
48
  return "", fmt.Errorf("captcha provider error: %s", result.Error)
49
  }
 
 
 
50
 
51
  return result.Token, nil
52
  }
internal/chat.go CHANGED
@@ -415,7 +415,7 @@ func makeUpstreamRequest(token string, messages []Message, model string, imageUR
415
 
416
  // 注入 captcha_verify_param(如果配置了 captcha provider)
417
  if Cfg.CaptchaProviderURL != "" {
418
- captchaToken, err := fetchCaptchaToken(token)
419
  if err != nil {
420
  LogWarn("[Captcha] Failed to get captcha token: %v", err)
421
  } else if captchaToken != "" {
 
415
 
416
  // 注入 captcha_verify_param(如果配置了 captcha provider)
417
  if Cfg.CaptchaProviderURL != "" {
418
+ captchaToken, err := fetchCaptchaToken()
419
  if err != nil {
420
  LogWarn("[Captcha] Failed to get captcha token: %v", err)
421
  } else if captchaToken != "" {