> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kazzle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 工具

> 应用如何声明工具以及其处理程序如何返回结果。

# 工具

**工具**是 AI 可以调用的具名操作，具有类型化输入——例如 `send_email(to, subject, body)`。工具在技能的 `tools.ts` 中声明，对于应用提供的工具，由其中一个组件中的 HTTP 路由支持。本页是工具契约的权威来源；[技能](/apps/skills)涵盖 `tools.ts` 的位置。

## 声明工具

`tools.ts` 中的每个条目都有一个提供商安全的 `name`、一个友好的 `displayName`、一个 `description`、一个 Zod `input` 模式和一个 `target`，用于指定 Kazzle 调用的内容。

```ts theme={"theme":"material-theme-darker"}
import { z } from 'zod';
import type { KazzleTool } from '@kazzle/app/tools';

export const SaveBookmarkInput = z.object({
  url: z.string().url().describe('URL to save.'),
  title: z.string().optional().describe('Optional human-readable title.'),
}).strict();

const saveBookmarkTarget = {
  type: 'app',
  component: 'api',
  path: '/tools/save-bookmark',
  method: 'POST',
  body: '${input}',
} as const;

export const tools = [
  {
    name: 'save_bookmark',
    displayName: 'Save bookmark',
    description: 'Save a bookmark URL and return a confirmation.',
    input: SaveBookmarkInput,
    target: saveBookmarkTarget,
  },
] as const satisfies readonly KazzleTool[];
```

## 目标

`target` 是工具或必需操作按钮的可复用地址。直接 AI 工具调用和按钮点击可以使用同一个目标对象，因此都运行相同的代码。

| 目标       | 字段                                                       | 运行位置                                   |
| -------- | -------------------------------------------------------- | -------------------------------------- |
| `app`    | `component`、`path`、`method`、可选的 `query`、`headers`、`body` | 你自己的某个组件。Kazzle 解析组件 URL 并添加签名的应用身份令牌。 |
| `url`    | `url`、`method`、可选的 `query`、`headers`、`body`              | 外部端点。URL 源和方法是字面的作者值。                  |
| `kazzle` | `name`                                                   | 内置的客户端 Kazzle 处理程序。                    |

HTTP 目标使用相同的请求字段：

```ts theme={"theme":"material-theme-darker"}
type RequestSpec = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  query?: Record<string, string>;
  headers?: Record<string, string>;
  body?: unknown;
};
```

没有隐式请求默认值。如果你的处理程序应该接收原始工具输入作为 JSON，请写 `body: '${input}'`。

## 引用

Kazzle 在分发前解析 HTTP 目标 `query`、`headers`、`body` 和 `url` 字段中的引用：

* `${input}` — 整个工具输入，或整个按钮输入。
* `${input.path}` — 输入中的嵌套值。
* `${env.NAME}` — 来自拥有应用组件声明的 `env.collection` + `env.environment` 的具名环境变量。

解析是单遍的。如果引用缺失或未知，工具失败而不是替换为空值。当你需要字面的 `${input.name}` 文本时，使用 `$${input.name}`。

## 处理程序请求

对于 `app` 目标，在目标组件中添加匹配的路由。使用 `body: '${input}'`，Kazzle 将类型化输入作为 JSON 正文发送：

```json theme={"theme":"material-theme-darker"}
{ "...": "the typed input" }
```

`app` 目标还接收：

| 标头                    | 值                                                           |
| --------------------- | ----------------------------------------------------------- |
| `Authorization`       | `Bearer <identity token>` 标识用户 + 安装                         |
| `Kazzle-Tool-Context` | `{"source":"thread","threadId":"..."}` 或 `{"source":"api"}` |

应用目标无法通过 `headers` 设置 `Authorization`；Kazzle 拥有该标头。当处理程序必须区分线程与工具 API 时，从 `@kazzle/app/tools` 使用 `toolContext(req)` 读取上下文。

声明的工具如果没有匹配的路由则无用——同时添加两者。不支持 `tools.json`；如果应用编译器找到它会失败。

## 处理程序响应

返回纯文本或最多三个通道的 JSON：

```json theme={"theme":"material-theme-darker"}
{ "content": "...", "markdown": "...", "embedUrl": "https://..." }
```

* **`content`** — AI 读取的纯结果（馈送到模型）。必需。
* **`markdown`** — 可选。在工具卡中呈现的简短富文本摘要（react-markdown；原始 HTML 被转义）。适合一个句子、一个小列表、一个内联链接。裸相对路径呈现为死预格式化文本——链接必须是**绝对的**。
* **`embedUrl`** — 可选。你的应用提供的**绝对** URL；卡在沙箱 iframe 中呈现它。这是你展示真实、全宽 UI 的方式——连接屏幕、仪表板、图表。你的应用托管并拥有该页面，因此它可以完全针对你自己的后端、cookie 和 OAuth 进行交互。没有任何内容写入驱动器。

当两者都设置时，`embedUrl` 优先于 `markdown`。始终保持有意义的 `content` ——那是 AI 读取的内容。

```ts theme={"theme":"material-theme-darker"}
if (req.method === 'POST' && new URL(req.url).pathname === '/tools/save-bookmark') {
  const input = await req.json();
  // ...do the work...
  return Response.json({ content: `Saved ${input.title}` });
}
```

## 必需操作

应用工具可以在需要用户（或设备）操作时暂停线程。返回 `type: 'action_required'` 以及卡标题和按钮。仅在 `toolContext(req).source === 'thread'` 时有效——通过工具 API（`source: 'api'`），改为返回正常的域错误。

```ts theme={"theme":"material-theme-darker"}
import { toolContext, isThreadToolInvocation } from '@kazzle/app/tools';

const context = toolContext(req);
if (!isThreadToolInvocation(context)) {
  return Response.json(
    { code: 'connect_required', error: 'Gmail must be connected before this tool can run.' },
    { status: 409 },
  );
}

return Response.json({
  type: 'action_required',
  title: 'Connect Gmail',
  description: 'Connect Gmail before Kazzle can search messages.',
  // Optional: only this device may run the gated buttons
  // assignee: { computerId: '<from computer { list: {} }>' },
  elements: [
    {
      type: 'button',
      id: 'connect',
      label: 'Connect Gmail',
      variant: 'primary',
      input: { scope: 'gmail.readonly' },
      target: {
        type: 'app',
        component: 'api',
        path: '/oauth/start',
        method: 'POST',
        body: '${input}',
      },
    },
  ],
});
```

Kazzle 在工具调用时保存卡，并在按钮被按下后通过 `/chat/resume` 恢复。匹配的客户端看到作者按钮；不匹配的客户端仍然看到跳过/取消加上"在 {device} 上继续"。没有 `target` 的按钮将其 `input` 作为工具结果提交。有目标的按钮先运行该目标，然后使用目标结果作为工具结果。

### 富 UI——嵌入应用托管的页面

当工具的结果是视觉或交互式的（连接屏幕、图表、摘要仪表板）时，从你的某个组件提供该页面，并将其**绝对** URL 作为 `embedUrl` 返回。从注入的组件 URL 构建 URL——永远不要使用相对路径。

```ts theme={"theme":"material-theme-darker"}
// A tool that needs the user to connect their account:
return Response.json({
  content: "Gmail isn't connected yet — the user needs to connect it.",
  embedUrl: `${process.env.KAZZLE_APP_COMPONENT_URL}/connect?token=${identity}`,
});
```

该页面在跨源 iframe 中运行（其自己的源、cookie 和脚本）。要调整卡的大小，将其高度发布到父级——卡侦听此操作并调整大小（上限为视口的 70%）：

```ts theme={"theme":"material-theme-darker"}
// inside the embedded page
parent.postMessage({ __kazzle_height: document.body.scrollHeight }, '*');
```

优先使用 `embedUrl` 而不是发出 HTML 字符串：你的应用已经提供页面，UI 保持交互式并与你的代码一起版本化，并且没有按调用工件写入任何地方。
