为twikoo接入Cap人机验证
前言
近来许多朋友们包括作者的网站都受到了一些恶意和推广评论的骚扰,于是作者决定在评论区添加人机验证的功能。
作者使用的评论系统是Twikoo,在更新了编写本文时的最新版本1.7.14后发现Twikoo不知道什么时候加入了人机验证,这可省了大功夫,然后便是选择验证系统了,目前有三种选择Cloudflare Turnstile 极验 Geetest 和 Cap,作者看上了开源的 Cap ,由于没有服务器所以作者找到了社区的CloudFlare版本,进行了一些适配的修改后接入了Twikoo。
教程
那就让我们开始吧,本次教程分为Vercel和Cloudflare版本
部署在Vercel
创建Vercel项目
方法一
方法二
Fork本项目后手动部署到Vercel
创建 Neon 数据库
在 Vercel 项目 Dashboard 中:
- 进入 Storage 选项卡
- 点击 Create → Neon Postgres
- 选择区域(建议选 US East,离 Vercel 函数最近)
- 创建完成后,连接到刚刚创建的项目
设置环境变量
进入 Environment Variables 添加环境变量
| Name | Value | 说明 |
|---|---|---|
CAP_SECRET_KEY |
你的强密钥 | Twikoo 服务端验证密钥 |
CAP_ALLOWED_ORIGINS(可选) |
安全域名(格式为https://cap.example.com;如需添加多域名请使用 , 分隔) |
允许跨域调用的域名白名单 |
1 | # 生成 32 字节 (64 位十六进制) 强密钥 |
部署并配置自定义域名
- 在 Vercel 项目 Settings → Domains 中添加你的自定义域名
- 在 DNS 管理中添加 CNAME 记录
- 等待 SSL 证书自动签发(约 1-2 分钟)
部署在Cloudflare Worker
创建Worker
方法一
方法二
Fork本项目后手动部署到Cloudflare
添加自定义域名
在Worker的域项添加自定义域名
添加秘钥
在Worker设置中变量和秘钥项添加秘钥
| 项目 | 值 |
|---|---|
| 类型 | 秘钥 |
| 变量名称 | CAP_SECRET_KEY |
| 值 | 你的强秘钥 |
秘钥可以用以下代码生成,创建
Python文件,写入代码运行即可
1 | import secrets |
填写完成后部署即可
接入Twikoo
登录进入 Twikoo 管理面板→配置管理→人机验证 按照表格修改设置后保存
| 配置项 | 值 | 说明 |
|---|---|---|
CAPTCHA_PROVIDER |
Cap |
启用 Cap 验证码 |
CAP_API_ENDPOINT |
https://你的自定义域名/api/ |
Cap 服务地址(带 /api/ 后缀) |
CAP_SECRET_KEY |
你的强秘钥 |
与 worker 中 CAP_SECRET_KEY 一致 |
示例
虽然说是针对Twikoo做了一些拓展,但项目原本的功能是没有改变的,如想在文章中加入验证卡片可使用一下代码
1 | <script src="https://你的自定义域名/cap.min.js"></script> |
注:原文本是英文,这里的中文是作者修改后的效果
详细内容请参考官方文档
作者在Cloudflare和Vercel分别部署了一个示例,感兴趣的读者可以体验
Cloudflare: https://cft.yvyang.qzz.io/
Vercel: https://cap.yvyang.top/
然而
Twikoo在1.7.15版本直接内嵌了Cap,只需要在设置选择Cap不再需要自己部署
之后我对Twikoo嵌入的Cap的验证界面作了一些美化,直接引入一下js文件即可完成美化
1 | (() => { |
将js代码压缩后文件会小一些,一下是压缩后的可引用代码
1 | (()=>{const a="0.0.6",t=(...a)=>window?.CAP_CUSTOM_FETCH?window.CAP_CUSTOM_FETCH(...a):fetch(...a);function e(a,t){let e=2166136261;for(let t=0;t<a.length;t++)e^=a.charCodeAt(t),e+=(e<<1)+(e<<4)+(e<<7)+(e<<8)+(e<<24);e>>>=0;let r="";const o=()=>(e^=e<<13,e^=e>>>17,e^=e<<5,e>>>0);for(;r.length<t;)r+=o().toString(16).padStart(8,"0");return r.substring(0,t)}window.CAP_CUSTOM_WASM_URL||[`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${a}/browser/cap_wasm.min.js`,`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${a}/browser/cap_wasm_bg.wasm`].forEach(a=>{const t=document.createElement("link");t.rel="prefetch",t.href=a,t.as=a.endsWith(".wasm")?"fetch":"script",document.head.appendChild(t)});class r extends HTMLElement{#a="";#t=null;#e=navigator.hardwareConcurrency||8;token=null;#r;#o;#n=new Map;#c=!1;#i;getI18nText(a,t){return this.getAttribute(`data-cap-i18n-${a}`)||t}static get observedAttributes(){return["onsolve","onprogress","onreset","onerror","data-cap-worker-count","data-cap-i18n-initial-state","[cap]"]}constructor(){super(),this.#n&&this.#n.forEach((a,t)=>{this.removeEventListener(t.slice(2),a)}),this.#n=new Map,this.boundHandleProgress=this.handleProgress.bind(this),this.boundHandleSolve=this.handleSolve.bind(this),this.boundHandleError=this.handleError.bind(this),this.boundHandleReset=this.handleReset.bind(this)}initialize(){this.#a=URL.createObjectURL(new Blob(['(() => {\n let wasmUrl,solvePow;\n self.onmessage = async ({data:{salt,target,wasmUrl:customWasmUrl}}) => {\n if (wasmUrl !== customWasmUrl) {\n wasmUrl = customWasmUrl;\n try {\n await import(wasmUrl).then(module =>\n module.default().then(instance => {\n solvePow = (instance?.exports ? instance.exports :module).solve_pow;\n})\n );\n} catch (err) {\n console.error("[cap worker] using fallback solver due to error:",err);\n}\n}\n try {\n const start = performance.now();\n const nonce = solvePow(salt,target);\n const end = performance.now();\n self.postMessage({\n nonce:Number(nonce),\n found:true,\n durationMs:(end - start).toFixed(2)\n});\n} catch (err) {\n console.error("[cap worker]",err);\n self.postMessage({found:false,error:err.message || String(err)});\n}\n};\n self.onerror = (err) => {\n self.postMessage({found:false,error:err});\n};\n})()'],{type:"application/javascript"}))}attributeChangedCallback(a,t,e){if(a.startsWith("on")){const t=a.slice(2),r=this.#n.get(a);if(r&&this.removeEventListener(t,r),e){const e=t=>{const e=this.getAttribute(a);"function"==typeof window[e]&&window[e].call(this,t)};this.#n.set(a,e),this.addEventListener(t,e)}}if("data-cap-worker-count"===a&&this.setWorkersCount(parseInt(e,10)),"data-cap-i18n-initial-state"===a&&this.#o){const a=this.#o.querySelector("p");a&&(a.innerText=this.getI18nText("initial-state","点击验证"))}}async connectedCallback(){this.#r=this.attachShadow({mode:"open"}),this.#o=document.createElement("div"),this.createUI(),this.addEventListeners(),await this.initialize(),this.#o.removeAttribute("disabled");const a=this.getAttribute("data-cap-worker-count"),t=a?parseInt(a,10):null;this.setWorkersCount(t||navigator.hardwareConcurrency||8);const e=this.getAttribute("data-cap-hidden-field-name")||"cap-token";this.innerHTML=`<input type="hidden" name="${e}">`}async solve(){if(!this.#c)try{this.#c=!0,this.updateUI("verifying",this.getI18nText("verifying-label","正在计算中..."),!0),this.#o.setAttribute("aria-label",this.getI18nText("verifying-aria-label","正在计算人机证明,请稍候")),this.dispatchEvent("progress",{progress:0});let a=this.getAttribute("data-cap-api-endpoint");if(!a&&window?.CAP_CUSTOM_FETCH)a="/";else if(!a)throw new Error("缺少 API 端点");const r=await t(`${a}challenge`,{method:"POST"}),{challenge:o,token:n}=await r.json();let c=o;if(!Array.isArray(c)){let a=0;c=Array.from({length:o.c},()=>(a+=1,[e(`${n}${a}`,o.s),e(`${n}${a}d`,o.d)]))}const i=await this.solveChallenges(c),s=await t(`${a}redeem`,{method:"POST",body:JSON.stringify({token:n,solutions:i}),headers:{"Content-Type":"application/json"}}),p=await s.json();if(this.dispatchEvent("progress",{progress:100}),!p.success)throw new Error("验证失败");const d=this.getAttribute("data-cap-hidden-field-name")||"cap-token",h=this.querySelector(`input[name='${d}']`);h&&(h.value=p.token),this.dispatchEvent("solve",{token:p.token}),this.token=p.token,this.#i&&clearTimeout(this.#i);const l=new Date(p.expires).getTime()-Date.now();return l>0&&l<864e5?this.#i=setTimeout(()=>this.reset(),l):this.error("过期时间无效"),this.#o.setAttribute("aria-label",this.getI18nText("verified-aria-label","人机验证已完成,可以继续操作")),{success:!0,token:this.token}}catch(a){throw this.#o.setAttribute("aria-label",this.getI18nText("error-aria-label","验证过程出错,请重试")),this.error(a.message),a}finally{this.#c=!1}}async solveChallenges(t){const e=t.length;let r=0;const o=Array(this.#e).fill(null).map(()=>{try{return new Worker(this.#a)}catch(a){throw new Error("Worker 创建失败")}}),n=([t,n],c)=>new Promise((i,s)=>{const p=o[c];if(!p)return s(new Error("Worker 不可用"));const d=setTimeout(()=>{try{p.terminate(),o[c]=new Worker(this.#a)}catch(a){}s(new Error("Worker 超时"))},3e4);p.onmessage=({data:a})=>{a.found&&(clearTimeout(d),r++,this.dispatchEvent("progress",{progress:Math.round(r/e*100)}),i(a.nonce))},p.onerror=a=>{clearTimeout(d),this.error(`Worker 错误:${a.message||a}`),s(a)},p.postMessage({salt:t,target:n,wasmUrl:window.CAP_CUSTOM_WASM_URL||`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${a}/browser/cap_wasm.min.js`})}),c=[];try{for(let a=0;a<t.length;a+=this.#e){const e=t.slice(a,Math.min(a+this.#e,t.length)),r=await Promise.all(e.map((a,t)=>n(a,t)));c.push(...r)}}finally{o.forEach(a=>{if(a)try{a.terminate()}catch(a){}})}return c}setWorkersCount(a){const t=parseInt(a,10),e=Math.min(navigator.hardwareConcurrency||8,16);this.#e=!isNaN(t)&&t>0&&t<=e?t:navigator.hardwareConcurrency||8}createUI(){this.#o.classList.add("captcha"),this.#o.setAttribute("part","container"),this.#o.setAttribute("role","checkbox"),this.#o.setAttribute("tabindex","0"),this.#o.setAttribute("aria-label",this.getI18nText("verify-aria-label","点击进行人机验证")),this.#o.setAttribute("aria-live","polite"),this.#o.setAttribute("aria-atomic","true"),this.#o.setAttribute("aria-busy","false"),this.#o.setAttribute("aria-checked","false"),this.#o.setAttribute("disabled","true"),this.#o.setAttribute("aria-disabled","true");const a=`cap-label-${Math.random().toString(36).slice(2)}`;this.#o.setAttribute("aria-describedby",a),this.#o.innerHTML=`\n <div class="checkbox" part="checkbox">\n <svg class="icon icon-check" viewBox="0 0 24 24" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">\n <path d="M5 12 10 17 20 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" pathLength="1"></path>\n </svg>\n <svg class="icon icon-error" viewBox="0 0 24 24" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">\n <path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8" />\n </svg>\n </div>\n <p id="${a}" part="label">${this.getI18nText("initial-state","点击验证")}</p>\n <div class="sr-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>\n <div class="particles" aria-hidden="true"></div>\n <a part="attribution" aria-label="由 Cap 提供保护" href="https://github.com/tiagozip/cap/" class="credits" target="_blank" rel="follow noopener">Cap</a>\n `,this.#r.innerHTML='\n <style>\n .captcha,.captcha * {box-sizing:border-box;}\n .captcha{\n background-color:var(--cap-background,#ffffff);\n border:1px solid var(--cap-border-color,rgba(0,0,0,0.08));\n border-radius:var(--cap-border-radius,12px);\n user-select:none;\n height:var(--cap-widget-height,56px);\n width:var(--cap-widget-width,280px);\n display:flex;\n align-items:center;\n padding:var(--cap-widget-padding,16px);\n gap:var(--cap-gap,12px);\n cursor:pointer;\n transition:border-color 0.3s cubic-bezier(0.4,0,0.2,1);\n position:relative;\n -webkit-tap-highlight-color:rgba(255,255,255,0);\n overflow:hidden;\n color:var(--cap-color,#1d1d1f);\n box-shadow:var(--cap-shadow,0 1px 3px rgba(0,0,0,0.04),0 1px 2px rgba(0,0,0,0.06));\n backdrop-filter:blur(10px);\n -webkit-backdrop-filter:blur(10px);\n --cap-skeleton-base:var(--cap-skeleton-base-light,#4b5563);\n --cap-skeleton-sheen:var(--cap-skeleton-sheen-light,#e5e7eb);\n}\n :host(:not([data-cap-skeleton="false"])) .captcha:not([data-state]) p,\n :host(:not([data-cap-skeleton="false"])) .captcha[data-state=""] p {\n position:relative;\n background:linear-gradient(90deg,\n var(--cap-skeleton-base,rgb(114,118,126)) 25%,\n var(--cap-skeleton-sheen,#e5e7eb) 50%,\n var(--cap-skeleton-base,rgb(114,118,126)) 75%\n );\n background-size:400% 100%;\n -webkit-background-clip:text;\n background-clip:text;\n color:var(--cap-color-initial,transparent);\n animation:cap-sheen var(--cap-skeleton-intro-speed,6s) ease-in-out infinite;\n}\n :host([data-cap-size="compact"]) .captcha {height:44px;width:240px;padding:12px;gap:10px;}\n :host([data-cap-size="compact"]) .captcha p {font-size:14px;}\n :host([data-cap-size="compact"]) .checkbox {width:20px;height:20px;}\n :host([data-cap-size="large"]) .captcha {height:64px;width:320px;padding:18px;gap:14px;}\n :host([data-cap-size="large"]) .captcha p {font-size:16px;}\n :host([data-cap-size="large"]) .checkbox {width:28px;height:28px;}\n :host([data-cap-shape="pill"]) .captcha {border-radius:9999px;}\n :host([data-cap-shape="pill"]) .checkbox {border-radius:9999px;}\n :host([data-cap-shape="square"]) .captcha {border-radius:8px;}\n :host([data-cap-elevation="flat"]) .captcha {box-shadow:none;}\n :host([data-cap-hide-attribution="true"]) .credits {display:none;}\n :host([data-cap-fullwidth="true"]) .captcha {width:100%;}\n :host([dir="rtl"]) .captcha {direction:rtl;}\n :host([data-cap-credits-position="top-right"]) .credits {top:8px;bottom:auto;right:10px;left:auto;}\n :host([data-cap-credits-position="top-left"]) .credits {top:8px;bottom:auto;left:10px;right:auto;}\n :host([data-cap-credits-position="bottom-left"]) .credits {bottom:8px;top:auto;left:10px;right:auto;}\n .particles{\n position:absolute;inset:0;pointer-events:none;opacity:0;\n background-image:radial-gradient(circle,var(--cap-particle-color,rgba(14,165,233,0.20)) 1px,transparent 1.5px);\n background-size:var(--cap-particle-gap,22px) var(--cap-particle-gap,22px);\n will-change:background-position,opacity;mix-blend-mode:var(--cap-particle-blend,overlay);\n filter:blur(var(--cap-particle-blur,0px));\n}\n .captcha[data-state=verifying] .particles{\n opacity:var(--cap-particle-opacity,.4);\n animation:cap-particles var(--cap-particle-speed,28s) linear infinite;\n}\n :host([data-cap-particles="false"]) .particles {display:none;}\n :host([data-cap-skeleton="false"]) .particles {display:none;}\n @keyframes cap-particles {to{background-position:200% 200%;}}\n .captcha:hover{\n background-color:var(--cap-background-hover,#f8f9fa);\n border-color:var(--cap-border-color-hover,rgba(0,0,0,0.12));\n box-shadow:var(--cap-shadow-hover,0 4px 12px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06));\n transform:translateY(-1px);\n}\n .captcha:active{transform:translateY(0);box-shadow:var(--cap-shadow-active,0 1px 2px rgba(0,0,0,0.06));}\n .checkbox{\n width:var(--cap-checkbox-size,24px);height:var(--cap-checkbox-size,24px);\n border:var(--cap-checkbox-border,2px solid #d1d5db);border-radius:var(--cap-checkbox-border-radius,6px);\n background-color:var(--cap-checkbox-background,#ffffff);\n transition:all 0.2s cubic-bezier(0.4,0,0.2,1);\n display:flex;align-items:center;justify-content:center;position:relative;\n}\n .checkbox .icon{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;pointer-events:none;}\n .checkbox .icon-check path{stroke-dasharray:1;stroke-dashoffset:1;}\n .captcha[data-state=done] .checkbox .icon-check{opacity:1;transition:opacity 0.2s ease;color:var(--cap-checkmark-color,#00a67d);}\n .captcha[data-state=done] .checkbox .icon-check path{stroke-dashoffset:0;transition:stroke-dashoffset 0.45s ease;}\n .captcha[data-state=error] .checkbox .icon-error{opacity:1;transition:opacity 0.2s ease;color:var(--cap-error-icon-color,#f55b50);}\n .sr-progress{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}\n .captcha *{font-family:var(--cap-font,-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display",system-ui,"Helvetica Neue",Arial,sans-serif);}\n .captcha p{margin:0;font-weight:500;font-size:15px;line-height:1.4;user-select:none;transition:all 0.2s cubic-bezier(0.4,0,0.2,1);letter-spacing:-0.01em;display:inline-block;}\n .captcha[data-state=verifying]{border-color:var(--cap-border-color-verifying,var(--cap-accent,#0ea5e9));transition:border-color 0.3s cubic-bezier(0.4,0,0.2,1);color:var(--cap-color-verifying,#0c4a6e);}\n .captcha[data-state=verifying] .checkbox{\n background:none;display:flex;align-items:center;justify-content:center;transform:scale(1.1);border:none;border-radius:50%;\n background:conic-gradient(var(--cap-spinner-color,var(--cap-accent,#0ea5e9)) 0%,var(--cap-spinner-color,var(--cap-accent,#0ea5e9)) var(--progress,0%),var(--cap-spinner-background-color,#eee) var(--progress,0%),var(--cap-spinner-background-color,#eee) 100%);\n position:relative;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);\n}\n .captcha[data-state=verifying] .checkbox::after{content:"";background-color:var(--cap-background,#fdfdfd);width:calc(100% - var(--cap-spinner-thickness,5px));height:calc(100% - var(--cap-spinner-thickness,5px));border-radius:50%;margin:calc(var(--cap-spinner-thickness,5px)/2)}\n .captcha[data-state=verifying] p{\n position:relative;\n background:linear-gradient(90deg,var(--cap-skeleton-base,#9ca3af) 25%,var(--cap-skeleton-sheen,#e5e7eb) 37%,var(--cap-skeleton-base,#9ca3af) 63%);\n background-size:400% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;\n animation:cap-sheen var(--cap-skeleton-speed,3.6s) ease-in-out infinite;\n}\n .captcha[data-state=verifying]::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(120deg,transparent 0%,var(--cap-ambient-color,rgba(14,165,233,0.08)) 20%,var(--cap-ambient-peak,rgba(14,165,233,0.18)) 35%,var(--cap-ambient-color,rgba(14,165,233,0.08)) 50%,transparent 60%);background-size:200% 100%;animation:cap-ambient var(--cap-ambient-speed,2.2s) ease-in-out infinite;}\n .captcha[data-state=verifying]::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;box-shadow:0 0 0 0 var(--cap-glow-color,rgba(14,165,233,0.0));animation:cap-glow var(--cap-glow-speed,1.8s) ease-in-out infinite;}\n :host([data-cap-skeleton="false"]) .captcha[data-state=verifying] p{animation:none;background:none;color:inherit;}\n :host([data-cap-skeleton="false"]) .captcha[data-state=verifying]::before,\n :host([data-cap-skeleton="false"]) .captcha[data-state=verifying]::after{display:none;}\n @keyframes cap-sheen{0%{background-position:0% 0%;}100%{background-position:-400% 0%;}}\n @keyframes cap-ambient{0%{background-position:200% 0;}100%{background-position:-200% 0;}}\n @keyframes cap-glow{0%,100%{box-shadow:0 0 0 0 var(--cap-glow-color,rgba(14,165,233,0.0));}50%{box-shadow:0 0 0 var(--cap-glow-spread,6px) var(--cap-glow-color,rgba(14,165,233,0.10));}}\n @supports not (background:conic-gradient(red,blue)){\n .captcha[data-state=verifying] .checkbox{border:2px solid var(--cap-spinner-background-color,#eee);border-top-color:var(--cap-spinner-color,var(--cap-accent,#0ea5e9));background:none;animation:cap-spin 0.8s linear infinite;}\n .captcha[data-state=verifying] .checkbox::after{content:none;}\n}\n @keyframes cap-spin{to{transform:rotate(360deg);}}\n .captcha[data-state=done]{border-color:var(--cap-border-color-success,#22c55e);color:var(--cap-color-success,#0d7332);}\n .captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:none;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);}\n .captcha[data-state=error]{border-color:var(--cap-border-color-error,#ef4444);color:var(--cap-color-error,#b91c1c);}\n .captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:none;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);}\n .captcha[disabled]{cursor:not-allowed;opacity:0.6;}\n .captcha[disabled][data-state=verifying]{cursor:progress;opacity:1;}\n .captcha[disabled][data-state=done]{cursor:default;opacity:1;}\n .captcha .credits{position:absolute;bottom:8px;right:10px;font-size:var(--cap-credits-font-size,11px);color:var(--cap-credits-color,#6b7280);opacity:var(--cap-credits-opacity,0.7);text-decoration:none;font-weight:500;transition:all 0.2s cubic-bezier(0.4,0,0.2,1);letter-spacing:0.01em;}\n .captcha .credits:hover{opacity:1;color:var(--cap-credits-color-hover,rgba(100,107,120,0.84));}\n @media (prefers-color-scheme:dark){\n .captcha{background-color:var(--cap-background-dark,rgba(28,28,30,0.8));border-color:var(--cap-border-color-dark,rgba(255,255,255,0.1));color:var(--cap-color-dark,#f2f2f7);box-shadow:var(--cap-shadow-dark,0 1px 3px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2));transition:border-color 0.3s cubic-bezier(0.4,0,0.2,1);--cap-skeleton-base:var(--cap-skeleton-base-dark,#b7b8b9);--cap-skeleton-sheen:var(--cap-skeleton-sheen-dark,rgb(236,236,236));}\n .captcha:hover{background-color:var(--cap-background-hover-dark,rgba(44,44,46,0.8));border-color:var(--cap-border-color-hover-dark,rgba(255,255,255,0.15));}\n .checkbox{background-color:var(--cap-checkbox-background-dark,rgba(44,44,46,0.8));border-color:var(--cap-checkbox-border-dark,rgba(255,255,255,0.2));}\n .captcha[data-state=verifying]{background-color:var(--cap-background-dark,rgba(28,28,30,0.8));border-color:var(--cap-border-color-verifying-dark,rgba(14,165,233,0.8));color:var(--cap-color-verifying-dark,#60a5fa);box-shadow:0 0 0 1px rgba(14,165,233,0.2),var(--cap-shadow-dark,0 1px 3px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2));}\n .captcha[data-state=verifying] .checkbox::after{background-color:var(--cap-background-dark,rgba(28,28,30,0.8));}\n .captcha[data-state=verifying]::after{background:linear-gradient(120deg,transparent 0%,var(--cap-ambient-color-dark,rgba(59,130,246,0.06)) 28%,var(--cap-ambient-peak-dark,rgba(59,130,246,0.12)) 42%,var(--cap-ambient-color-dark,rgba(59,130,246,0.06)) 56%,transparent 70%);opacity:var(--cap-ambient-opacity-dark,1);}\n .captcha[data-state=verifying]::before{--cap-glow-color:var(--cap-glow-color-dark,rgba(59,130,246,0.08));--cap-glow-spread:var(--cap-glow-spread-dark,5px);}\n .captcha[data-state=done]{background-color:var(--cap-background-dark,rgba(28,28,30,0.8));border-color:var(--cap-border-color-success-dark,rgba(34,197,94,0.8));color:var(--cap-color-success-dark,#4ade80);box-shadow:0 0 0 1px rgba(34,197,94,0.2),var(--cap-shadow-dark,0 1px 3px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2));}\n .captcha[data-state=error]{background-color:var(--cap-background-dark,rgba(28,28,30,0.8));border-color:var(--cap-border-color-error-dark,rgba(239,68,68,0.8));color:var(--cap-color-error-dark,#f87171);box-shadow:0 0 0 1px rgba(239,68,68,0.2),var(--cap-shadow-dark,0 1px 3px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2));}\n .credits{color:var(--cap-credits-color-dark,#8e8e93);transition:color 0.3s cubic-bezier(0.4,0,0.2,1);}\n .credits:hover{color:var(--cap-credits-color-hover-dark,#aeaeb2);}\n :host(:not([data-cap-skeleton="false"])) .captcha:not([data-state]) p,\n :host(:not([data-cap-skeleton="false"])) .captcha[data-state=""] p{color:transparent;}\n}\n @media (prefers-contrast:more){.captcha{border-color:currentColor!important;box-shadow:none!important}.captcha:hover{border-color:currentColor!important}.checkbox{border-color:currentColor!important}}\n @media (forced-colors:active){.captcha{forced-color-adjust:none;background:Canvas;border-color:ButtonText;color:ButtonText;box-shadow:none}.checkbox{border-color:ButtonText;background:Canvas}.checkbox .icon{color:ButtonText!important}.captcha p{background:none!important;color:ButtonText!important}.captcha[data-state=done] .checkbox,.captcha[data-state=error] .checkbox{background-image:none}.captcha .particles,.captcha:not([data-state]) p::after,.captcha[data-state=""] p::after{display:none}}\n .captcha:focus-visible{outline:2px solid var(--cap-focus-color,#007aff);outline-offset:2px;box-shadow:0 0 0 3px var(--cap-focus-ring,rgba(0,122,255,0.25)),var(--cap-shadow,0 1px 3px rgba(0,0,0,0.04),0 1px 2px rgba(0,0,0,0.06));border-color:var(--cap-focus-border,var(--cap-focus-color,#007aff));}\n @media (prefers-reduced-motion:reduce){.captcha,.checkbox,.captcha p,.credits{transition:none!important}.captcha[data-state=done],.captcha[data-state=error]{animation:none!important;transition:background-color 0.2s ease!important}.captcha[data-state=done]{background-color:var(--cap-background-success,#f0fdf4)!important}.captcha[data-state=error]{background-color:var(--cap-background-error,#fef2f2)!important}.captcha[data-state=verifying] .checkbox,.captcha[data-state=done] .checkbox,.captcha[data-state=error] .checkbox{transition:none!important}.captcha[data-state=verifying] .checkbox{animation:none!important}.captcha:not([data-state]) p,.captcha[data-state=""] p{animation:none!important;background:none!important;color:inherit!important}.captcha:not([data-state]) p::after,.captcha[data-state=""] p::after{display:none!important}.particles,.captcha[data-state=verifying]::before,.captcha[data-state=verifying]::after{display:none!important}.checkbox .icon,.checkbox .icon-check path{transition:none!important}.captcha[data-state=done] .checkbox .icon-check path{stroke-dashoffset:0!important}}\n @media (pointer:coarse){.captcha{height:var(--cap-widget-height-touch,60px);}}\n </style>\n ',this.#r.appendChild(this.#o)}addEventListeners(){this.#o&&(this.#o.querySelector("a").addEventListener("click",a=>{a.stopPropagation(),a.preventDefault(),window.open("https://github.com/tiagozip/cap/","_blank")}),this.#o.addEventListener("click",()=>{this.#o.hasAttribute("disabled")||this.solve()}),this.#o.addEventListener("keydown",a=>{"Enter"!==a.key&&" "!==a.key||this.#o.hasAttribute("disabled")||(a.preventDefault(),a.stopPropagation(),this.solve())}),this.addEventListener("progress",this.boundHandleProgress),this.addEventListener("solve",this.boundHandleSolve),this.addEventListener("error",this.boundHandleError),this.addEventListener("reset",this.boundHandleReset))}updateUI(a,t,e=!1){this.#o&&(this.#o.setAttribute("data-state",a),this.#o.querySelector("p").innerText=t,this.#o.setAttribute("aria-busy","verifying"===a?"true":"false"),this.#o.setAttribute("aria-checked","verifying"===a?"mixed":"done"===a?"true":"false"),"error"===a?this.#o.setAttribute("aria-invalid","true"):this.#o.removeAttribute("aria-invalid"),e?(this.#o.setAttribute("disabled","true"),this.#o.setAttribute("aria-disabled","true")):(this.#o.removeAttribute("disabled"),this.#o.removeAttribute("aria-disabled")))}handleProgress(a){if(!this.#o)return;const t=a.detail.progress,e=this.#o.querySelector("p"),r=this.#o.querySelector(".checkbox"),o=this.#o.querySelector(".sr-progress");e&&r&&(r.style.setProperty("--progress",`${t}%`),e.innerText=`${this.getI18nText("verifying-label","正在计算")} ${t}%`),o&&(o.setAttribute("aria-valuenow",String(t)),o.setAttribute("aria-valuetext",`${this.getI18nText("verifying-label","正在计算")} ${t}%`)),this.executeAttributeCode("onprogress",a)}handleSolve(a){this.updateUI("done",this.getI18nText("solved-label","验证通过"),!0),this.executeAttributeCode("onsolve",a)}handleError(a){this.updateUI("error",this.getI18nText("error-label","出错了,请重试"),!1),this.executeAttributeCode("onerror",a)}handleReset(a){this.updateUI("",this.getI18nText("initial-state","点击验证"),!1),this.executeAttributeCode("onreset",a)}executeAttributeCode(a,t){const e=this.getAttribute(a);e&&new Function("event",e).call(this,t)}error(a="未知错误"){this.dispatchEvent("error",{isCap:!0,message:a})}dispatchEvent(a,t={}){const e=new CustomEvent(a,{bubbles:!0,composed:!0,detail:t});super.dispatchEvent(e)}reset(){this.#i&&(clearTimeout(this.#i),this.#i=null),this.dispatchEvent("reset"),this.token=null;const a=this.getAttribute("data-cap-hidden-field-name")||"cap-token",t=this.querySelector(`input[name='${a}']`);t&&(t.value="")}get tokenValue(){return this.token}disconnectedCallback(){this.removeEventListener("progress",this.boundHandleProgress),this.removeEventListener("solve",this.boundHandleSolve),this.removeEventListener("error",this.boundHandleError),this.removeEventListener("reset",this.boundHandleReset),this.#n.forEach((a,t)=>{this.removeEventListener(t.slice(2),a)}),this.#n.clear(),this.#r&&(this.#r.innerHTML=""),this.reset(),this.cleanup()}cleanup(){this.#i&&(clearTimeout(this.#i),this.#i=null),this.#a&&(URL.revokeObjectURL(this.#a),this.#a="")}}customElements.get("cap-widget")||customElements.define("cap-widget",r)})(); |
如果要修改卡片的字体,就在任意 CSS 文件添加:
1 | cap-widget::part(label) { |










