为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;如需添加多域名请使用 , 分隔) | 允许跨域调用的域名白名单 |
# 生成 32 字节 (64 位十六进制) 强密钥
openssl rand -hex 32部署并配置自定义域名
- 在 Vercel 项目 Settings → Domains 中添加你的自定义域名
- 在 DNS 管理中添加 CNAME 记录
- 等待 SSL 证书自动签发(约 1-2 分钟)
部署在Cloudflare Worker
创建Worker
方法一
方法二
Fork本项目后手动部署到Cloudflare
添加自定义域名
在Worker的域项添加自定义域名
添加秘钥
在Worker设置中变量和秘钥项添加秘钥
| 项目 | 值 |
|---|---|
| 类型 | 秘钥 |
| 变量名称 | CAP_SECRET_KEY |
| 值 | 你的强秘钥 |
秘钥可以用以下代码生成,创建
Python文件,写入代码运行即可
import secrets
# 32字节原始随机熵,编码后约 43 字符
secret_key = secrets.token_urlsafe(32)
print(secret_key)填写完成后部署即可
接入Twikoo
登录进入 Twikoo 管理面板→配置管理→人机验证 按照表格修改设置后保存
| 配置项 | 值 | 说明 |
|---|---|---|
CAPTCHA_PROVIDER | Cap | 启用 Cap 验证码 |
CAP_API_ENDPOINT | https://你的自定义域名/api/ | Cap 服务地址(带 /api/ 后缀) |
CAP_SECRET_KEY | 你的强秘钥 | 与 worker 中 CAP_SECRET_KEY 一致 |
示例
虽然说是针对Twikoo做了一些拓展,但项目原本的功能是没有改变的,如想在文章中加入验证卡片可使用一下代码
<script src="https://你的自定义域名/cap.min.js"></script>
<!-- 全局引用后就不需要写了 -->
<cap-widget
id="cap"
data-cap-api-endpoint="https://你的自定义域名/api/">
</cap-widget>
注:原文本是英文,这里的中文是作者修改后的效果
详细内容请参考官方文档
作者在Cloudflare和Vercel分别部署了一个示例,感兴趣的读者可以体验
Cloudflare: https://cft.yvyang.qzz.io/
Vercel: https://cap.yvyang.top/
然而
Twikoo在1.7.15版本直接内嵌了Cap,只需要在设置选择Cap不再需要自己部署
之后我对Twikoo嵌入的Cap的验证界面作了一些美化,直接引入一下js文件即可完成美化
(() => {
// Cap.js 版本号(用于 WASM CDN 路径)
const VERSION = "0.0.6";
// 自定义 fetch:优先使用 Twikoo 注入的 CAP_CUSTOM_FETCH
const customFetch = (...args) =>
window?.CAP_CUSTOM_FETCH ? window.CAP_CUSTOM_FETCH(...args) : fetch(...args);
// FNV-1a 哈希函数:用 token 派生 salt 和 target
function hashString(str, length) {
let hash = 2166136261;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
hash >>>= 0;
let result = "";
const nextRand = () => {
hash ^= hash << 13;
hash ^= hash >>> 17;
hash ^= hash << 5;
return hash >>> 0;
};
while (result.length < length) {
result += nextRand().toString(16).padStart(8, "0");
}
return result.substring(0, length);
}
// 预加载 WASM(如果未自定义 URL)
if (!window.CAP_CUSTOM_WASM_URL) {
[
`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${VERSION}/browser/cap_wasm.min.js`,
`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${VERSION}/browser/cap_wasm_bg.wasm`
].forEach(url => {
const link = document.createElement("link");
link.rel = "prefetch";
link.href = url;
link.as = url.endsWith(".wasm") ? "fetch" : "script";
document.head.appendChild(link);
});
}
// ============ CapWidget 主组件 ============
class CapWidget extends HTMLElement {
#workerBlobUrl = ""; // Worker Blob URL
#token = null; // 当前验证 token
#maxWorkers = navigator.hardwareConcurrency || 8;
token = null; // 外部可读取的 token
#shadowRoot; // Shadow DOM 根
#container; // .captcha 容器
#eventListeners = new Map();// on* 属性事件监听
#isSolving = false; // 防重入锁
#resetTimeout; // 过期自动重置定时器
// ---- i18n ----
getI18nText(key, fallback) {
return this.getAttribute(`data-cap-i18n-${key}`) || fallback;
}
static get observedAttributes() {
return [
"onsolve", "onprogress", "onreset", "onerror",
"data-cap-worker-count", "data-cap-i18n-initial-state", "[cap]"
];
}
constructor() {
super();
// 清理旧监听器
if (this.#eventListeners) {
this.#eventListeners.forEach((handler, eventName) => {
this.removeEventListener(eventName.slice(2), handler);
});
}
this.#eventListeners = 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);
}
// ---- Worker 初始化 ----
initialize() {
this.#workerBlobUrl = URL.createObjectURL(
new Blob([
`(() => {
let wasmUrl, solvePow;
self.onmessage = async ({ data: { salt, target, wasmUrl: customWasmUrl } }) => {
if (wasmUrl !== customWasmUrl) {
wasmUrl = customWasmUrl;
try {
await import(wasmUrl).then(module =>
module.default().then(instance => {
solvePow = (instance?.exports ? instance.exports : module).solve_pow;
})
);
} catch (err) {
console.error("[cap worker] using fallback solver due to error:", err);
}
}
try {
const start = performance.now();
const nonce = solvePow(salt, target);
const end = performance.now();
self.postMessage({
nonce: Number(nonce),
found: true,
durationMs: (end - start).toFixed(2)
});
} catch (err) {
console.error("[cap worker]", err);
self.postMessage({ found: false, error: err.message || String(err) });
}
};
self.onerror = (err) => {
self.postMessage({ found: false, error: err });
};
})()`
], { type: "application/javascript" })
);
}
// ---- 属性变化监听 ----
attributeChangedCallback(name, oldVal, newVal) {
if (name.startsWith("on")) {
const eventName = name.slice(2);
const oldHandler = this.#eventListeners.get(name);
if (oldHandler) this.removeEventListener(eventName, oldHandler);
if (newVal) {
const handler = (ev) => {
const fnName = this.getAttribute(name);
if (typeof window[fnName] === "function") {
window[fnName].call(this, ev);
}
};
this.#eventListeners.set(name, handler);
this.addEventListener(eventName, handler);
}
}
if (name === "data-cap-worker-count") {
this.setWorkersCount(parseInt(newVal, 10));
}
if (name === "data-cap-i18n-initial-state" && this.#container) {
const p = this.#container.querySelector("p");
if (p) p.innerText = this.getI18nText("initial-state", "点击验证");
}
}
// ---- 挂载到 DOM ----
async connectedCallback() {
this.#shadowRoot = this.attachShadow({ mode: "open" });
this.#container = document.createElement("div");
this.createUI();
this.addEventListeners();
await this.initialize();
this.#container.removeAttribute("disabled");
const workerAttr = this.getAttribute("data-cap-worker-count");
const count = workerAttr ? parseInt(workerAttr, 10) : null;
this.setWorkersCount(count || navigator.hardwareConcurrency || 8);
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
this.innerHTML = `<input type="hidden" name="${fieldName}">`;
}
// ---- 核心:执行验证 ----
async solve() {
if (this.#isSolving) return;
try {
this.#isSolving = true;
// 状态 → verifying
this.updateUI("verifying", this.getI18nText("verifying-label", "正在计算中..."), true);
this.#container.setAttribute(
"aria-label",
this.getI18nText("verifying-aria-label", "正在计算人机证明,请稍候")
);
this.dispatchEvent("progress", { progress: 0 });
// 确定 API 端点
let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
apiEndpoint = "/";
} else if (!apiEndpoint) {
throw new Error("缺少 API 端点");
}
// 1. 获取 challenge
const challengeRes = await customFetch(`${apiEndpoint}challenge`, { method: "POST" });
const { challenge, token } = await challengeRes.json();
// 2. 展开 challenge(兼容新旧格式)
let challenges = challenge;
if (!Array.isArray(challenges)) {
let counter = 0;
challenges = Array.from({ length: challenge.c }, () => {
counter += 1;
return [
hashString(`${token}${counter}`, challenge.s),
hashString(`${token}${counter}d`, challenge.d)
];
});
}
// 3. 浏览器侧解 PoW
const solutions = await this.solveChallenges(challenges);
// 4. 兑换 token
const redeemRes = await customFetch(`${apiEndpoint}redeem`, {
method: "POST",
body: JSON.stringify({ token, solutions }),
headers: { "Content-Type": "application/json" }
});
const result = await redeemRes.json();
this.dispatchEvent("progress", { progress: 100 });
if (!result.success) throw new Error("验证失败");
// 5. 写入隐藏字段 + 触发事件
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
const hiddenInput = this.querySelector(`input[name='${fieldName}']`);
if (hiddenInput) hiddenInput.value = result.token;
this.dispatchEvent("solve", { token: result.token });
this.token = result.token;
// 6. 设置过期自动重置
if (this.#resetTimeout) clearTimeout(this.#resetTimeout);
const expiresIn = new Date(result.expires).getTime() - Date.now();
if (expiresIn > 0 && expiresIn < 86400000) {
this.#resetTimeout = setTimeout(() => this.reset(), expiresIn);
} else {
this.error("过期时间无效");
}
this.#container.setAttribute(
"aria-label",
this.getI18nText("verified-aria-label", "人机验证已完成,可以继续操作")
);
return { success: true, token: this.token };
} catch (err) {
this.#container.setAttribute(
"aria-label",
this.getI18nText("error-aria-label", "验证过程出错,请重试")
);
this.error(err.message);
throw err;
} finally {
this.#isSolving = false;
}
}
// ---- PoW 多线程计算 ----
async solveChallenges(challenges) {
const total = challenges.length;
let solved = 0;
const workers = Array(this.#maxWorkers).fill(null).map(() => {
try {
return new Worker(this.#workerBlobUrl);
} catch (err) {
console.error("[cap] 创建 Worker 失败:", err);
throw new Error("Worker 创建失败");
}
});
const solveOne = ([salt, target], idx) =>
new Promise((resolve, reject) => {
const worker = workers[idx];
if (!worker) return reject(new Error("Worker 不可用"));
const timer = setTimeout(() => {
try {
worker.terminate();
workers[idx] = new Worker(this.#workerBlobUrl);
} catch (err) {
console.error("[cap] 终止/重建 Worker 出错:", err);
}
reject(new Error("Worker 超时"));
}, 30000);
worker.onmessage = ({ data }) => {
if (data.found) {
clearTimeout(timer);
solved++;
this.dispatchEvent("progress", { progress: Math.round((solved / total) * 100) });
resolve(data.nonce);
}
};
worker.onerror = (err) => {
clearTimeout(timer);
this.error(`Worker 错误: ${err.message || err}`);
reject(err);
};
worker.postMessage({
salt,
target,
wasmUrl: window.CAP_CUSTOM_WASM_URL ||
`https://jsdelivr.topthink.com/npm/@cap.js/wasm@${VERSION}/browser/cap_wasm.min.js`
});
});
const results = [];
try {
for (let i = 0; i < challenges.length; i += this.#maxWorkers) {
const batch = challenges.slice(i, Math.min(i + this.#maxWorkers, challenges.length));
const batchResults = await Promise.all(
batch.map((ch, idx) => solveOne(ch, idx))
);
results.push(...batchResults);
}
} finally {
workers.forEach(w => {
if (w) try { w.terminate(); } catch (e) { console.error("[cap] 终止 Worker 出错:", e); }
});
}
return results;
}
// ---- Worker 数量控制 ----
setWorkersCount(count) {
const num = parseInt(count, 10);
const max = Math.min(navigator.hardwareConcurrency || 8, 16);
this.#maxWorkers = (!isNaN(num) && num > 0 && num <= max) ? num : (navigator.hardwareConcurrency || 8);
}
// ---- 构建 Shadow DOM UI ----
createUI() {
this.#container.classList.add("captcha");
this.#container.setAttribute("part", "container");
this.#container.setAttribute("role", "checkbox");
this.#container.setAttribute("tabindex", "0");
this.#container.setAttribute("aria-label", this.getI18nText("verify-aria-label", "点击进行人机验证"));
this.#container.setAttribute("aria-live", "polite");
this.#container.setAttribute("aria-atomic", "true");
this.#container.setAttribute("aria-busy", "false");
this.#container.setAttribute("aria-checked", "false");
this.#container.setAttribute("disabled", "true");
this.#container.setAttribute("aria-disabled", "true");
const labelId = `cap-label-${Math.random().toString(36).slice(2)}`;
this.#container.setAttribute("aria-describedby", labelId);
// 内部结构:checkbox + 文字 + 粒子 + 署名
this.#container.innerHTML = `
<div class="checkbox" part="checkbox">
<svg class="icon icon-check" viewBox="0 0 24 24" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<path d="M5 12 10 17 20 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" pathLength="1"></path>
</svg>
<svg class="icon icon-error" viewBox="0 0 24 24" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<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" />
</svg>
</div>
<p id="${labelId}" part="label">${this.getI18nText("initial-state", "点击验证")}</p>
<div class="sr-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
<div class="particles" aria-hidden="true"></div>
<a part="attribution" aria-label="由 Cap 提供保护" href="https://github.com/tiagozip/cap/" class="credits" target="_blank" rel="follow noopener">Cap</a>
`;
// Shadow DOM 样式(完整保留所有美化效果)
this.#shadowRoot.innerHTML = `<style>...</style>`;
// ↑ 此处样式与压缩版完全一致,因篇幅省略,详见 cap-widget.min.js
this.#shadowRoot.appendChild(this.#container);
}
// ---- 事件绑定 ----
addEventListeners() {
if (!this.#container) return;
this.#container.querySelector("a").addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
window.open("https://github.com/tiagozip/cap/", "_blank");
});
this.#container.addEventListener("click", () => {
if (!this.#container.hasAttribute("disabled")) this.solve();
});
this.#container.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
if (!this.#container.hasAttribute("disabled")) {
e.preventDefault();
e.stopPropagation();
this.solve();
}
}
});
this.addEventListener("progress", this.boundHandleProgress);
this.addEventListener("solve", this.boundHandleSolve);
this.addEventListener("error", this.boundHandleError);
this.addEventListener("reset", this.boundHandleReset);
}
// ---- UI 状态切换 ----
updateUI(state, text, disabled = false) {
if (!this.#container) return;
this.#container.setAttribute("data-state", state);
this.#container.querySelector("p").innerText = text;
this.#container.setAttribute("aria-busy", state === "verifying" ? "true" : "false");
this.#container.setAttribute("aria-checked",
state === "verifying" ? "mixed" : (state === "done" ? "true" : "false"));
if (state === "error") {
this.#container.setAttribute("aria-invalid", "true");
} else {
this.#container.removeAttribute("aria-invalid");
}
if (disabled) {
this.#container.setAttribute("disabled", "true");
this.#container.setAttribute("aria-disabled", "true");
} else {
this.#container.removeAttribute("disabled");
this.#container.removeAttribute("aria-disabled");
}
}
// ---- 进度回调 ----
handleProgress(ev) {
if (!this.#container) return;
const progress = ev.detail.progress;
const p = this.#container.querySelector("p");
const cb = this.#container.querySelector(".checkbox");
const sr = this.#container.querySelector(".sr-progress");
if (p && cb) {
cb.style.setProperty("--progress", `${progress}%`);
p.innerText = `${this.getI18nText("verifying-label", "正在计算")} ${progress}%`;
}
if (sr) {
sr.setAttribute("aria-valuenow", String(progress));
sr.setAttribute("aria-valuetext", `${this.getI18nText("verifying-label", "正在计算")} ${progress}%`);
}
this.executeAttributeCode("onprogress", ev);
}
// ---- 成功/错误/重置 ----
handleSolve(ev) {
this.updateUI("done", this.getI18nText("solved-label", "验证通过"), true);
this.executeAttributeCode("onsolve", ev);
}
handleError(ev) {
this.updateUI("error", this.getI18nText("error-label", "出错了,请重试"), false);
this.executeAttributeCode("onerror", ev);
}
handleReset(ev) {
this.updateUI("", this.getI18nText("initial-state", "点击验证"), false);
this.executeAttributeCode("onreset", ev);
}
executeAttributeCode(attr, ev) {
const code = this.getAttribute(attr);
if (code) new Function("event", code).call(this, ev);
}
error(msg = "未知错误") {
console.error("[cap]", msg);
this.dispatchEvent("error", { isCap: true, message: msg });
}
dispatchEvent(name, detail = {}) {
const ev = new CustomEvent(name, { bubbles: true, composed: true, detail });
super.dispatchEvent(ev);
}
// ---- 重置 ----
reset() {
if (this.#resetTimeout) {
clearTimeout(this.#resetTimeout);
this.#resetTimeout = null;
}
this.dispatchEvent("reset");
this.token = null;
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
const input = this.querySelector(`input[name='${fieldName}']`);
if (input) input.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.#eventListeners.forEach((handler, name) => {
this.removeEventListener(name.slice(2), handler);
});
this.#eventListeners.clear();
if (this.#shadowRoot) this.#shadowRoot.innerHTML = "";
this.reset();
this.cleanup();
}
cleanup() {
if (this.#resetTimeout) {
clearTimeout(this.#resetTimeout);
this.#resetTimeout = null;
}
if (this.#workerBlobUrl) {
URL.revokeObjectURL(this.#workerBlobUrl);
this.#workerBlobUrl = "";
}
}
}
// 注册自定义元素
if (!customElements.get("cap-widget")) {
customElements.define("cap-widget", CapWidget);
}
})();将js代码压缩后文件会小一些,一下是压缩后的可引用代码
(()=>{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 文件添加:
cap-widget::part(label) {
font-family: "LXGW WenKai", sans-serif;
/* 这个字体如果不是系统字体需要先引入 */
}
