# Corptcha API 参考（AI Agent 用）

## 核心概念

- **Site ID**（`cpt_` 开头）：站点公开标识，出现在前端初始化与后端校验请求体。
- **Secret**：站点密钥，仅用于后端 `/v1/verify` 的 `Authorization: Bearer` 头，绝不出现在前端。
- **token**：用户完成验证后由 `onSuccess` 回调返回的一次性令牌，只能成功核验一次。

## 前端 SDK

- 引入：`<script src="https://res.25y.cn/corptcha/corptcha.iife.js"></script>`
- 初始化：`const widget = Corptcha.render(container, options)`

### 渲染参数 options

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `apiBaseUrl` | string | 验证服务 API 地址，`https://cpt-api.25y.cn` |
| `siteKey` | string | 站点 Site ID |
| `purpose` | string | 场景标识（login/register/comment 等），与后端一致 |
| `language` | 'zh-CN' \| 'en-US' | 界面语言 |
| `appearance` | 'compact' \| 'normal' | 外观形态 |
| `theme` | object | `{ mode: 'light'\|'dark'\|'auto', accentColor, borderRadius, fontFamily }` |
| `autoExecute` | boolean | 挂载后自动开始验证 |
| `onSuccess` | (token: string) => void | 验证成功，返回一次性 token |
| `onError` | (error: { errorCode, message }) => void | 验证失败 |
| `onStateChange` | (state: string) => void | 状态变化 |
| `onExpired` | () => void | token 过期 |

### 实例方法

| 方法 | 说明 |
| --- | --- |
| `execute()` | 手动开始验证 |
| `reset()` | 重置，允许重新验证 |
| `destroy()` | 销毁组件 |

## 后端校验接口

### POST /v1/verify

**请求**

```json
POST https://cpt-api.25y.cn/v1/verify
Authorization: Bearer {Secret}
Content-Type: application/json

{
  "token": "前端 onSuccess 拿到的 token",
  "purpose": "login",
  "siteKey": "cpt_xxxxxxxxxxxx"
}
```

**成功响应**（HTTP 200）

```json
{
  "success": true,
  "siteId": "cpt_xxxxxxxxxxxx",
  "purpose": "login",
  "issuedAt": "2026-08-04T10:00:00.000Z",
  "riskLevel": "low",
  "requestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

`riskLevel` 取值：`low` / `medium` / `high` / `critical`。

**失败响应**（HTTP 4xx）

```json
{
  "errorCode": "TOKEN_NOT_FOUND",
  "message": "验证令牌无效",
  "requestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

### 错误码

| errorCode | HTTP | 含义 |
| --- | --- | --- |
| `UNAUTHORIZED` | 401 | Secret 缺失或错误 |
| `TOKEN_NOT_FOUND` | 400 | 令牌无效或已使用 |
| `TOKEN_EXPIRED` | 400 | 令牌已过期 |
| `TOKEN_SITE_MISMATCH` | 400 | 站点不匹配 |
| `TOKEN_PURPOSE_MISMATCH` | 400 | purpose 与签发时不符 |
| `REQUEST_VALIDATION_FAILED` | 400 | 请求体缺字段或格式错误 |

## 多语言校验示例

以下示例均为「服务端」代码，需传入用户项目的 Secret。

### Node.js / TypeScript（Node 18+）

```js
const VERIFY_URL = 'https://cpt-api.25y.cn/v1/verify';

async function verifyCaptcha(token, purpose, siteKey, secret) {
  const response = await fetch(VERIFY_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json', authorization: `Bearer ${secret}` },
    body: JSON.stringify({ token, purpose, siteKey }),
  });
  const result = await response.json();
  if (!response.ok || result.success !== true) {
    throw new Error(`人机验证未通过：${result.errorCode ?? 'UNKNOWN'}`);
  }
  return result;
}
```

### Python（requests）

```python
import requests

def verify_captcha(token, purpose, site_key, secret):
    resp = requests.post(
        "https://cpt-api.25y.cn/v1/verify",
        headers={"content-type": "application/json", "authorization": f"Bearer {secret}"},
        json={"token": token, "purpose": purpose, "siteKey": site_key},
        timeout=5,
    )
    result = resp.json()
    if resp.status_code != 200 or result.get("success") is not True:
        raise ValueError(f"人机验证未通过：{result.get('errorCode', 'UNKNOWN')}")
    return result
```

### Java（okhttp4 + gson）

```java
public static JsonObject verifyCaptcha(String token, String purpose, String siteKey, String secret) throws Exception {
    OkHttpClient client = new OkHttpClient();
    JsonObject body = new JsonObject();
    body.addProperty("token", token);
    body.addProperty("purpose", purpose);
    body.addProperty("siteKey", siteKey);
    Request request = new Request.Builder()
            .url("https://cpt-api.25y.cn/v1/verify")
            .post(RequestBody.create(body.toString(), MediaType.get("application/json")))
            .header("authorization", "Bearer " + secret)
            .build();
    try (Response response = client.newCall(request).execute()) {
        JsonObject result = JsonParser.parseString(response.body().string()).getAsJsonObject();
        if (!response.isSuccessful() || !result.get("success").getAsBoolean()) {
            throw new IllegalStateException("人机验证未通过：" + result.get("errorCode").getAsString());
        }
        return result;
    }
}
```

### Go

```go
func verifyCaptcha(token, purpose, siteKey, secret string) error {
	payload, _ := json.Marshal(map[string]string{"token": token, "purpose": purpose, "siteKey": siteKey})
	req, _ := http.NewRequest(http.MethodPost, "https://cpt-api.25y.cn/v1/verify", bytes.NewReader(payload))
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", "Bearer "+secret)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("人机验证未通过")
	}
	return nil
}
```

### PHP（cURL）

```php
function verifyCaptcha(string $token, string $purpose, string $siteKey, string $secret): array {
    $ch = curl_init('https://cpt-api.25y.cn/v1/verify');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode(['token' => $token, 'purpose' => $purpose, 'siteKey' => $siteKey]),
        CURLOPT_HTTPHEADER => ['content-type: application/json', 'authorization: Bearer ' . $secret],
        CURLOPT_TIMEOUT => 5,
    ]);
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $result = json_decode($response, true);
    if ($status !== 200 || ($result['success'] ?? false) !== true) {
        throw new RuntimeException('人机验证未通过');
    }
    return $result;
}
```

## 常见排错

| 现象 | 处理 |
| --- | --- |
| `TOKEN_NOT_FOUND` | token 已用过或无效，重新验证 |
| `TOKEN_PURPOSE_MISMATCH` | 前后端 purpose 不一致 |
| `UNAUTHORIZED` | Secret 错误或未传 Bearer 头 |
| `Corptcha is not defined` | SDK 未加载完，加 `defer` |
| 前端跨域 | 走官方 `https://cpt-api.25y.cn`，widget 接口已开放任意来源；自建部署需自行放行 |
