# 嵌入到网页 (/docs/developers/agent/embed/quickstart)





在你的网页中加入 PraxisKit，让登录用户与服务智能体对话。本页用本地订单助手跑通后端签发、前端挂载和身份隔离；其他形态见[选择接入方式](/docs/developers/agent/embed/overview)。

准备 Bun、一个可用的服务智能体及运行环境，以及同组织的 API Key。示例连接 Develop，API Key 仅存为后端环境变量 `PRAXIS_API_KEY`。浏览器登录宿主后，由宿主核验身份、向 Praxis 签发令牌，再把令牌交给 Kit。

## 1. 准备智能体 [#prepare]

在 Develop 控制台进入**服务智能体**，选择已有智能体。在**接入与配置 → 配置 → 指令**保存指令；在**配置 → 安全**选择运行环境，打开**允许 localhost**，点**保存安全设置**。本页宿主页为 `http://localhost:4242`。请使用专用测试智能体，来源白名单暂留空，仅启用 localhost；令牌继承非空 HTTPS 来源列表时，本地请求会被拒绝。上线到 HTTPS 网站时，添加该网站的来源。

嵌入策略目前只能在控制台配置。复制公开智能体 ID（`agent_…`）备用。页面显示“已保存，不产生新版本”后，继续实际签发；保存提示本身不能证明对话可用。

## 2. 创建签发端点 [#issue]

新建目录，把代码保存为 `server.ts`。这是仅绑定本机的演示服务器，用 HTTP Basic 验证两个测试账户。`DEMO_PASSWORD` 是你自行设置的测试密码，与 Praxis API Key 无关。服务器从验证成功的账户取用户 ID，不读取浏览器提交的用户 ID。

```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 = "http://localhost:4242";
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 (url.origin !== origin) return new Response("Use localhost:4242", { 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);
```

将三个环境变量放入服务端进程；`PRAXIS_AGENT_ID` 替换为实际 ID。启动前还需在下一步保存 `index.html`。

```bash
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts
```

期望：终端输出本地网址。未认证请求被宿主拒绝；已认证且同源的请求才会调用 Praxis。Praxis 签发成功返回 HTTP 201；宿主端点只返回 `token`、`expires_at`，并禁止缓存。

接入已有应用时，将 `authenticate` 替换为应用的会话验证，把路由接在登录与 CSRF 保护之后，保留每次签发的身份核验。不要将本地测试账户机制用于线上登录。有效期和刷新见[嵌入令牌与用户](/docs/developers/agent/embed/tokens)。

## 3. 挂载组件 [#mount]

选择一种前端实现。API 地址、CDN 和智能体必须属于同一环境；以下固定使用 Develop 的 Kit 0.2.8。

<Tabs items="['Web Component', 'React']">
  <Tab value="Web Component">
    保存为同目录的 `index.html`，然后运行上一步启动命令。

    ```html
    <!doctype html>
    <html lang="zh">
    <meta charset="utf-8">
    <title>Order assistant</title>
    <script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
    <praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
    <p id="status" role="status"></p>
    <script type="module">
      await customElements.whenDefined("praxis-agent");
      const agent = document.querySelector("praxis-agent");
      agent.addEventListener("praxis.error", () => {
        document.querySelector("#status").textContent = "Chat unavailable. Check your sign-in and configuration.";
      });
      agent.setOptions({
        api: {
          url: "https://api-develop.deepraxis.com",
          getToken: async () => {
            const response = await fetch("/api/praxis-token", {
              method: "POST", credentials: "same-origin", cache: "no-store",
            });
            if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
            const data = await response.json();
            if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
            return data.token;
          },
        },
        agent: { name: "Order assistant" },
      });
    </script>
    </html>
    ```
  </Tab>

  <Tab value="React">
    在已有 React 应用的 HTML 入口加入下面的模块脚本。将签发路由接入应用后端，确保 `/api/praxis-token` 与页面同源且受登录保护；React 页面无需由演示服务器提供。

    ```html
    <script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
    ```

    保存为 `Support.jsx`，在已登录的页面渲染 `<Support />`。组件使用 CDN 注册的 Web Component，并在卸载时移除它。

    ```jsx
    import { useEffect, useRef, useState } from "react";

    export default function Support() {
      const container = useRef(null);
      const [error, setError] = useState("");
      useEffect(() => {
        let cancelled = false;
        let agent;
        const onError = () => setError("Chat unavailable. Check your sign-in and configuration.");
        async function mount() {
          await customElements.whenDefined("praxis-agent");
          if (cancelled) return;
          agent = document.createElement("praxis-agent");
          agent.style.cssText = "display:block;height:600px;width:100%";
          agent.addEventListener("praxis.error", onError);
          agent.setOptions({
            api: {
              url: "https://api-develop.deepraxis.com",
              getToken: async () => {
                const response = await fetch("/api/praxis-token", {
                  method: "POST", credentials: "same-origin", cache: "no-store",
                });
                if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
                const data = await response.json();
                if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
                return data.token;
              },
            },
            agent: { name: "Order assistant" },
          });
          container.current.appendChild(agent);
        }
        mount().catch(onError);
        return () => { cancelled = true; agent?.removeEventListener("praxis.error", onError); agent?.remove(); };
      }, []);
      return <><div ref={container} /><p role="status">{error}</p></>;
    }
    ```
  </Tab>
</Tabs>

期望：页面出现高度为 600 像素的对话区。`api.url` 指定 API 地址，`getToken` 返回令牌字符串，`agent.name` 是界面名称。等待元素注册后再调用 `setOptions`。`praxis.ready` 只表示组件挂载；必须收到真实回复才能证明接入成功。

## 4. 验证接入 [#verify]

1. 打开 `http://localhost:4242`，用 `customer_123` 和所设测试密码登录。发送“你好，请介绍你能帮助处理的订单问题”，确认收到回复。
2. 在网络面板检查宿主令牌请求成功，响应没有 API Key；会话请求使用嵌入令牌。不要把令牌复制到日志或截图。
3. 用独立浏览器配置或无痕窗口登录 `customer_456`，期望看不到第一个账户的会话。不能用两个同登录态的标签页代替此检查。
4. 不带凭据请求本地签发端点，验证宿主返回 HTTP 401。已有应用还应验证退出登录后不能再签发。

```bash
curl --silent --output /dev/null --write-out "%{http_code}\n" \
  -X POST http://localhost:4242/api/praxis-token \
  -H "Origin: http://localhost:4242"
```

## 失败时检查 [#troubleshoot]

以下是接口定义的失败分支，按网络面板中的实际响应检查：

* 签发 HTTP 400：确认已保存嵌入策略，运行环境仍可用。
* 签发 HTTP 404：确认 API Key 与服务智能体属于同组织，ID 已替换正确。
* 对话返回 `EMBED_ORIGIN_NOT_ALLOWED`（HTTP 403）：核对实际页面来源；本地应启用 localhost，上线应保存 HTTPS 来源。

## 下一步 [#next]

* [传入上下文](/docs/developers/agent/embed/context)
* [自定义外观](/docs/developers/agent/embed/appearance)
* [智能体 API 参考](/docs/api/native/agent)
* [SaaS 嵌入示例](/docs/cookbook/scenarios/saas-embedded-assistant)
