YvYang的AI摘要
Spark-Lite

前言

近来许多朋友们包括作者的网站都受到了一些恶意和推广评论的骚扰,于是作者决定在评论区添加人机验证的功能。
作者使用的评论系统是Twikoo,在更新了编写本文时的最新版本1.7.14后发现Twikoo不知道什么时候加入了人机验证,这可省了大功夫,然后便是选择验证系统了,目前有三种选择Cloudflare Turnstile 极验 GeetestCap,作者看上了开源的 Cap ,由于没有服务器所以作者找到了社区的CloudFlare版本,进行了一些适配的修改后接入了Twikoo。

教程

那就让我们开始吧,本次教程分为VercelCloudflare版本

部署在Vercel

创建Vercel项目

方法一

点击按钮一键部署
Deploy with Vercel

方法二

Fork本项目后手动部署到Vercel

创建 Neon 数据库

Vercel 项目 Dashboard 中:

  1. 进入 Storage 选项卡
  2. 点击 Create → Neon Postgres
  3. 选择区域(建议选 US East,离 Vercel 函数最近)
  4. 创建完成后,连接到刚刚创建的项目

设置环境变量

进入 Environment Variables 添加环境变量

Name Value 说明
CAP_SECRET_KEY 你的强密钥 Twikoo 服务端验证密钥
CAP_ALLOWED_ORIGINS(可选) 安全域名(格式为https://cap.example.com;如需添加多域名请使用 , 分隔) 允许跨域调用的域名白名单
1
2
# 生成 32 字节 (64 位十六进制) 强密钥
openssl rand -hex 32

部署并配置自定义域名

  1. 在 Vercel 项目 Settings → Domains 中添加你的自定义域名
  2. 在 DNS 管理中添加 CNAME 记录
  3. 等待 SSL 证书自动签发(约 1-2 分钟)

部署在Cloudflare Worker

创建Worker

方法一

使用下面链接一键部署
Deploy to Cloudflare

方法二

Fork本项目后手动部署到Cloudflare

添加自定义域名

在Worker的项添加自定义域名

添加秘钥

在Worker设置变量和秘钥项添加秘钥

项目
类型 秘钥
变量名称 CAP_SECRET_KEY
你的强秘钥

秘钥可以用以下代码生成,创建Python文件,写入代码运行即可

1
2
3
4
5
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做了一些拓展,但项目原本的功能是没有改变的,如想在文章中加入验证卡片可使用一下代码

1
2
3
4
5
6
7
<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文件即可完成美化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
(() => {
// 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代码压缩后文件会小一些,一下是压缩后的可引用代码

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
2
3
4
cap-widget::part(label) {
font-family: "LXGW WenKai", sans-serif;
/* 这个字体如果不是系统字体需要先引入 */
}

相关链接