# 一行嵌入 (/docs/developers/agent/embed/iframe)



在网页中插入一行加载脚本，显示 Praxis 托管的 iframe 对话界面。外观在控制台保存，用户留在你的网页中对话；“一行”只指前端安装，后端仍需为登录用户签发令牌。

## 准备 HTTPS 宿主 [#prepare]

准备一个可修改页面和后端的 HTTPS 网站、Bun、同组织的 API Key，以及有短号的服务智能体。以下连接 Develop。用于演示的 HTTPS 入口应转发到本机 `127.0.0.1:4242`，并保留原始 `Host` 请求头；证书和域名由你的宿主提供。

在 Develop 控制台打开**服务智能体 → 你的智能体 → 接入与配置 → 配置 → 安全**，选择运行环境，在**来源白名单**添加实际 HTTPS 来源，例如 `https://support.example.com`，点**保存安全设置**。来源不包含路径。嵌入策略目前只能在控制台配置。

仅打开**允许 localhost**不能授权 iframe 嵌入；本页需要已保存的 HTTPS 来源。若只做本机 HTTP 调试，先使用 [Kit 快速开始](/docs/developers/agent/embed/quickstart)。

记录两个标识：签发请求使用公开 ID `agent_…`；脚本的 `data-agent` 使用控制台顶部可复制的短号，格式为 5–8 位字母或数字。不要把完整 ID 填进短号位置。

## 提供签发端点 [#issue]

把以下代码保存为 `server.ts`，放在 HTTPS 入口后。示例使用两个受密码保护的测试账户；上线时将 `authenticate` 换成宿主登录会话验证，并保留 CSRF 保护。`HOST_ORIGIN` 填实际 HTTPS 来源，不带末尾斜线。所有环境变量只配置在后端。

```ts
import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = process.env.HOST_ORIGIN;
if (!origin || new URL(origin).protocol !== "https:") throw new Error("Set HOST_ORIGIN to your HTTPS origin");
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (request.headers.get("Host") !== new URL(origin).host) return new Response("Host rejected", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);
```

在下一节保存 `index.html` 后启动。`DEMO_PASSWORD` 是你自己设置的测试账户密码，不是 API Key；浏览器用 `customer_123` 或 `customer_456` 登录。

```bash
export HOST_ORIGIN="https://support.example.com"
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in the server process.
bun server.ts
```

加载器向 `data-token-endpoint` 发 POST 请求，并携带浏览器凭据。端点必须从已验证登录态取得用户 ID，每次签发都重新核验。Praxis 返回 HTTP 201 后，宿主向加载器返回 `token` 和 `expires_at`。不要在 HTML 中写入 API Key，也不要让令牌进入页面 URL。

## 插入加载脚本 [#install]

保存为 `index.html`。把 `ABC123` 替换为实际短号；若已有宿主页，只需将其中的脚本标签放到需要出现对话的位置。

```html
<!doctype html>
<html lang="zh">
<meta charset="utf-8">
<title>Order assistant</title>
<body>
  <h1>Order support</h1>
  <script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" async></script>
</body>
</html>
```

加载器自动创建 iframe 并完成令牌传递。直接打开或粘贴一个 iframe 地址不会自动获得宿主登录用户的身份。

| 脚本属性                  | 用途                                      |
| --------------------- | --------------------------------------- |
| `data-agent`          | 必填，智能体短号                                |
| `data-token-endpoint` | 必填，宿主签发端点；示例与页面同源                       |
| `data-layout`         | 可选，`inline`、`popup`、`sidebar` 或 `modal` |
| `data-launcher`       | 可选，`bubble`、`none` 或已有按钮的 CSS 选择器       |

未指定布局和启动按钮时使用控制台保存的外观；还没有保存布局时，加载器采用 `popup`。显式设置脚本属性会覆盖对应选项。如果使用 `inline`，脚本所在位置就是插入位置，给外层容器预留足够高度。

## 保存外观 [#appearance]

在**接入与配置 → 接入方式与外观 → iframe**调整名称、欢迎语和布局，点**保存**。期望显示“已保存，嵌入页刷新后生效”；刷新你的宿主页验证。持久化外观目前只能在控制台配置。

如果选择 `data-launcher="none"`，浮层没有默认打开按钮。为已有按钮设置 CSS 选择器，或在宿主代码中控制打开，示例如下。按钮需在加载器执行前存在。

```html
<button id="open-support" type="button">Open support</button>
<script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" data-layout="popup" data-launcher="#open-support" async></script>
```

这是上一节脚本的替代写法，同一智能体在页面中只保留一份加载脚本。

## 验证与排错 [#verify]

打开 HTTPS 宿主页并登录，确认对话界面或启动按钮出现；发送问题并收到回复。再用独立浏览器登录第二个测试账户，确认无法看到第一个账户的会话。

在网络面板确认加载了 `loader.js`，iframe 来自 `chat-develop.praxisusercontent.com`，宿主令牌端点成功返回。API Key 只能出现在宿主到 Praxis 的服务端请求中，不应出现在浏览器网络记录里。

* 框架被浏览器阻止：检查允许来源是否与页面来源一致；宿主 CSP 的 `frame-src` 要允许 `https://chat-develop.praxisusercontent.com`，`script-src` 要允许脚本 CDN。不要禁用整站 CSP 来排错。
* 控制台提示 `Invalid Praxis loader configuration`：检查短号格式、脚本域名以及签发端点是否填写。
* 宿主端点返回 HTTP 401 或 403：先恢复宿主登录或修正同源检查；不要把重试当成重新登录。

验证来源拒绝时，从另一个不在白名单中的 HTTPS 站点加载同一脚本，期望浏览器阻止嵌入。验证签发保护时，在退出宿主登录后再次访问签发端点，期望宿主拒绝请求；不要只检查按钮是否隐藏。

用户退出或切换账户时，重新加载宿主页，让后端按新登录态签发，避免沿用原账户界面。加载器的打开和关闭只影响界面显示，不是账户退出，也不会替你的宿主结束登录会话。

## 下一步 [#next]

* [自定义外观](/docs/developers/agent/embed/appearance)
* [保护嵌入接入](/docs/developers/agent/embed/security)
* [智能体 API 参考](/docs/api/native/agent)
* [Cookbook](/docs/cookbook)
