{"id":24979,"date":"2026-08-06T10:17:40","date_gmt":"2026-08-06T10:17:40","guid":{"rendered":"https:\/\/capskip.com\/?p=24979"},"modified":"2026-08-06T10:17:40","modified_gmt":"2026-08-06T10:17:40","slug":"geetest-v3-nodejs","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/geetest-v3-nodejs\/","title":{"rendered":"\u5982\u4f55\u7528 Node.js \u8bc6\u522b\u6781\u9a8c v3 \u5e76\u56de\u4f20"},"content":{"rendered":"<p>Almost every CAPTCHA integration assumes one token goes in and one comes out. GeeTest does not work that way. A solve returns <strong>three<\/strong> values that must be submitted together, and the challenge you started from expires about a minute after you fetched it. Neither failure produces a useful error message, which is why GeeTest has a reputation for being fiddly.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Two inputs, one of them perishable<\/h2>\n<table>\n<thead>\n<tr>\n<th>Value<\/th>\n<th>Lifetime<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>gt<\/code><\/td>\n<td>Static per site. Cache it freely<\/td>\n<\/tr>\n<tr>\n<td><code>challenge<\/code><\/td>\n<td><strong>Single use, dead in roughly 60 seconds<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Both come from the site&#8217;s own GeeTest init endpoint. Treating the challenge as reusable is the reason integrations pass in testing and collapse in production, where queueing delay pushes the solve past the expiry.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Setup<\/h2>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">npm install capskip<\/pre>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">const { CapSkip } = require('capskip');\n\nconst solver = new CapSkip({\n  host: '127.0.0.1',\n  port: 8080,\n  recaptchaTimeout: 300,   \/\/ GeeTest uses this, not defaultTimeout\n});<\/pre>\n<p>That distinction matters. <code>defaultTimeout<\/code> only governs image CAPTCHAs; everything interactive uses <code>recaptchaTimeout<\/code>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Solving<\/h2>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">const result = await solver.geetest(\n  '81388ea1fc187e0c335c0a8907ff2625',   \/\/ gt, static per site\n  '7cf6a8b1a2c34d5e6f7089abcdef0123',   \/\/ challenge, fetched seconds ago\n  'https:\/\/example.com\/login',\n);\n\nconsole.log(result.challenge, result.validate, result.seccode);<\/pre>\n<p>Those three properties are the answer. <code>result.code<\/code> is populated too, but for GeeTest it holds the raw JSON string rather than anything submittable, so reaching for it out of habit produces a baffling failure.<\/p>\n<p>Use the <code>challenge<\/code> that comes back, not the one you passed in. They are not always identical.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The whole flow<\/h2>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">const { CapSkip } = require('capskip');\n\nconst solver = new CapSkip();\nconst LOGIN = 'https:\/\/example.com\/login';\n\n\/\/ 1. Fresh pair, cache-busted. Cached init responses return spent challenges.\nconst init = await (await fetch(\n  `https:\/\/example.com\/geetest\/init?t=${Date.now()}`,\n)).json();\n\n\/\/ 2. Solve immediately. Nothing slow between here and the previous step.\nconst result = await solver.geetest(init.gt, init.challenge, LOGIN);\n\n\/\/ 3. Post all three together with the real form fields.\nconst response = await fetch(LOGIN, {\n  method: 'POST',\n  body: new URLSearchParams({\n    geetest_challenge: result.challenge,\n    geetest_validate: result.validate,\n    geetest_seccode: result.seccode,\n    username: '...',\n    password: '...',\n  }),\n});<\/pre>\n<p>The cache-busting timestamp is doing real work. GeeTest init endpoints are frequently cached by a CDN or proxy, and a cached response hands you a challenge somebody already used.<\/p>\n<p>Those field names are the usual GeeTest v3 convention, though a site can rename them. Check the real form before assuming.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The batching trap<\/h2>\n<p>This is where Node&#8217;s ergonomics work against you. The obvious parallel version is wrong:<\/p>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ WRONG: every challenge is fetched up front, so the later solves\n\/\/ start against pairs that have already expired.\nconst pairs = await Promise.all(urls.map(fetchPair));\nconst results = await Promise.all(\n  pairs.map((p, i) =&gt; solver.geetest(p.gt, p.challenge, urls[i])));<\/pre>\n<p>Keep the fetch and the solve inside the same task so each pair is used within seconds of being issued:<\/p>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ RIGHT: each task fetches its own pair immediately before solving.\nconst results = await Promise.all(urls.map(async (url) =&gt; {\n  const pair = await fetchPair(url);\n  return solver.geetest(pair.gt, pair.challenge, url);\n}));<\/pre>\n<p><code>AsyncCapSkip<\/code> is exported as an alias of <code>CapSkip<\/code> here, so there is no separate async client to reach for. <code>Promise.all<\/code> is the whole story.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Errors<\/h2>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">const {\n  ValidationException, NetworkException, ApiException, TimeoutException,\n} = require('capskip');\n\ntry {\n  const result = await solver.geetest(gt, challenge, pageUrl);\n} catch (err) {\n  if (err instanceof ValidationException)   { \/* missing gt or challenge *\/ }\n  else if (err instanceof NetworkException) { \/* CapSkip not running *\/ }\n  else if (err instanceof ApiException)     { \/* usually an expired challenge *\/ }\n  else if (err instanceof TimeoutException) { \/* exceeded recaptchaTimeout *\/ }\n  else throw err;\n}<\/pre>\n<p>Most GeeTest failures arrive as <code>ApiException<\/code> and mean the challenge expired. Retrying with the same pair never works. Fetch a new one.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Frequently asked questions<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Why can I not just use result.code?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because GeeTest&#8217;s answer is three values. <code>code<\/code> holds the raw JSON for completeness while the SDK expands the useful parts into <code>challenge<\/code>, <code>validate<\/code> and <code>seccode<\/code>. Submit those.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I cache the gt value?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. The <code>gt<\/code> is a site identifier and rarely changes. The <code>challenge<\/code> is the perishable half and must be fetched fresh every time.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does this handle GeeTest v4?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The <code>geetest<\/code> method targets v3, the slide puzzle built on a <code>gt<\/code> and <code>challenge<\/code> pair. v4 changed the parameter model, so check the current <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a> before assuming the same call works.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Fetch a cache-busted pair, solve immediately, then post <code>challenge<\/code>, <code>validate<\/code> and <code>seccode<\/code> together. Use the returned challenge, cache only the <code>gt<\/code>, and keep fetch and solve inside the same task when running in parallel.<\/p>\n<p>Other languages are on the <a href=\"https:\/\/capskip.com\/geetest-solver\/\">GeeTest solver<\/a> page, the wider Node surface on the <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">Node.js CAPTCHA solver<\/a> page, and there is a live puzzle on our <a href=\"https:\/\/capskip.com\/captcha-demo\/geetest-v3\/\">GeeTest v3 demo<\/a>. CapSkip does <a href=\"https:\/\/capskip.com\/\">captcha bypass<\/a> locally, so solve volume costs nothing.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u6781\u9a8c\u8fd4\u56de\u4e09\u4e2a\u503c\u800c\u975e token\uff0c\u4e14\u6311\u6218\u7ea6\u4e00\u5206\u949f\u540e\u5931\u6548\u3002\u4e0b\u9762\u662f Node.js \u6d41\u7a0b\uff0c\u5305\u62ec\u4e3a\u4ec0\u4e48\u6279\u5904\u7406\u4f1a\u7834\u574f\u5b83\u3002<\/p>","protected":false},"author":1,"featured_media":24978,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve GeeTest v3 in Node.js | CapSkip","rank_math_description":"GeeTest returns three values in Node.js rather than one token, and the challenge expires in about a minute. Here is the full flow and the batching trap.","rank_math_focus_keyword":"solve geetest in node.js","footnotes":""},"categories":[70],"tags":[],"class_list":["post-24979","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24979","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=24979"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24979\/revisions"}],"predecessor-version":[{"id":24995,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24979\/revisions\/24995"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/24978"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=24979"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=24979"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=24979"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}