# 表单

> 将 Kanorio 表单放置在您自己的前端，并为您处理机器人防护、文件上传、同意与联系人收集。

> Markdown version of https://kanorio.com/zh-CN/developers/guides/forms. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp
>
> Translated from the English original: https://kanorio.com/en/developers/guides/forms.md

请在后台的 **营销 › 在线表单** 建立表单，然后将其显示在您的前端。Kanorio 会检查机器人与垃圾信息、保存提交内容、创建或更新联系人、发送确认与团队邮件，并触发 `customers.form.submitted` Webhook。

开始之前，请从后台复制表单 ID。如果浏览器从您自己的域名调用 API，请将该域名新增至 [网站设置 › 开发者](https://app.kanorio.com/settings/developer) 下的 **允许的来源**。`localhost` 始终是被允许的。

## 选项 1：表单元素

`<KanorioForm>` 可渲染所有问题类型、文件上传与同意复选框，并为您执行机器人防护。这是最快的方法，且无需升级即可获得新的防护规则。

```tsx
import { KanorioProvider, KanorioForm } from "@kanorio/elements";
import "@kanorio/elements/elements.css";

export function ContactSection() {
  return (
    <KanorioProvider siteUrl="https://s-xxxxxxxxxxxx.kanorio.com">
      <KanorioForm formId="cform12345678901234567890" siteName="Sourdough Studio" />
    </KanorioProvider>
  );
}
```

若未使用 React，请使用 `<kanorio-form site-url="…" form-id="…">`。请参阅 [Elements](https://kanorio.com/zh-CN/developers/elements)。

## 选项 2：您自己的标记

当您想要完全控制 HTML 时，请直接使用 Content Delivery API。

### 1. 加载表单

`GET /api/v1/forms/{id}` 会返回问题、同意设置以及表单是否正在接受回答。如果 `availability.accepting` 为 `false`，请显示关闭消息而非输入字段。

### 2. 在显示表单时获取机器人检查

`GET /api/v1/forms/{id}/challenge` 会返回此提交的机器人检查。其 `provider` 由网站拥有者在 **网站设置 › 机器人防护** 中设置：

- `kanorio` (默认)：`altcha` 中的内置工作量证明挑战。SDK 会为您解决（在中端手机上约需半秒），并将结果作为 `_tk` 发送。
- `turnstile`、`recaptcha` 或 `hcaptcha`：拥有者自己的密钥。请使用 `siteKey` 和 `action`（Turnstile 亦需 `cData`）渲染该提供者的组件，并将其令牌作为 `captchaToken` 传递。服务器会根据网站设置决定验证哪个提供者，因此来自任何其他提供者的令牌将被拒绝。

每次提交请获取新的挑战：挑战与组件令牌皆为单次使用。

### 3. 提交

发送以问题 ID 为键的答案，并附上防护字段。蜜罐 (Honeypot) 字段必须为空。

```ts
import { forms } from "@kanorio/sdk/forms";

const SITE_URL = "https://s-xxxxxxxxxxxx.kanorio.com";
const FORM_ID = "cform12345678901234567890";
const client = forms.client({ siteUrl: SITE_URL });

// when the form is shown
const renderedAt = Date.now();
const challenge = await client.challenge(FORM_ID);

// when the visitor submits
// built-in: the SDK solves the challenge; third-party: pass the widget token
const protection = await forms.protectionFromChallenge(FORM_ID, challenge, renderedAt, {
  captchaToken: challenge.provider === "kanorio" ? undefined : widgetToken,
});

const result = await client.submit(FORM_ID, {
  answers: { name: "Ada", email: "ada@example.com", message: "Do you ship?" },
  consent: "checked",
  pagePath: window.location.pathname,
  protection,
});

if (!result.ok) {
  // result.error.code, and result.error.issues for INVALID_ANSWERS
}
```

若未使用 SDK，请使用此内容 `POST /api/v1/forms/{id}/submit`（显示内置检查；若使用第三方提供者，请发送 `captchaToken` 取代 `_tk`）：

```json
{
  "answers": { "name": "Ada", "email": "ada@example.com" },
  "consent": "checked",
  "pagePath": "/contact",
  "_hp": "",
  "_ts": 1793520000000,
  "_tk": "<solved ALTCHA payload>",
  "_ctx": "cform12345678901234567890"
}
```

成功的提交会返回 `200` 与 `{}`。如果表单在此期间关闭，内容则为 `{ "state": "closed" }`、`"not_open"` 或 `"full"`。

| 错误代码 | 处理方式 |
|---|---|
| `INVALID_ANSWERS` | 显示 `details.issues` 中的消息，并以问题 ID 作为键值 |
| `VERIFICATION_EXPIRED` | 获取新的验证挑战（并重置小工具）后重新提交 |
| `DUPLICATE_SUBMISSION` | 告知访客表单已提交 |
| `RATE_LIMITED` | 等待 `Retry-After` 指定的时间后重试 |

### 文件上传

对于文件问题，请先使用 `POST /api/v1/forms/{id}/upload-request` 请求上传票据，并附上问题 ID、文件名、类型与大小，以及来自 `forms.uploadProtectionFromChallenge(FORM_ID, challenge, renderedAt, solved)` 的防护字段：使用内置检查时，这与您提交时使用的已解决挑战相同；使用第三方提供者时，它会使用挑战的 `uploadToken`，因为组件令牌为单次使用且会在提交时检查。使用返回的 `headers` 将文件 `PUT` 至 `uploadUrl`，然后将 `uploadId` 作为该问题的答案发送。超过问题限制的文件会失败并返回 `413 FILE_TOO_LARGE`。

## 提交后

- 提交内容会出现在后台以及 Management API 的 `submissions` 下。
- 联系人会根据电子邮件或电话答案进行创建或更新。
- 确认与团队邮件会按照表单设置发送。
- Webhook 订阅者会收到 `customers.form.submitted`。

## 从您的服务器转发

您可以从自己的服务器提交，例如 Next.js 服务器动作。速率限制适用于每位访客，因此直接从浏览器提交最能保护您的配额。如果您进行转发，请保持浏览器的防护字段不变。
