智能体

打开托管对话

由后端签发一次性链接,把已登录用户跳转到独立的 Hosted 对话页。

给你的网站添加一个按钮,让已登录用户进入 Praxis 的独立 Hosted 对话页。宿主后端验证用户、签发链接并立即跳转;不需要在宿主页安装聊天组件。

准备智能体

准备 Bun、同组织的 API Key,以及一个已配置指令、运行环境和嵌入策略的服务智能体。智能体必须有短号;在 Develop 控制台的服务智能体中打开它,确认顶部显示短号,并复制公开 ID agent_…。

尚未保存嵌入策略时,进入接入与配置 → 配置 → 安全,选择运行环境,点保存安全设置。嵌入策略目前只能在控制台配置。以下示例使用 Develop API 和本地宿主 http://localhost:4242,对话则在托管域名中打开。

Hosted 是独立页面,不能作为 iframe 嵌进宿主。需要留在当前页面时,选择一行嵌入或 Kit 组件。

签发链接并跳转

把以下完整服务器保存为 server.ts。它只绑定本机,用 HTTP Basic 验证两个测试账户,并检查表单请求来源。PRAXIS_API_KEY 只存在后端;external_user_id 来自通过验证的账户,而不是表单字段。

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-hosted") {
      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, hosted: true }),
        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.hosted_url !== "string") throw new Error("Missing URL");
      return new Response(null, {
        status: 302, headers: { ...noStore, Location: data.hosted_url },
      });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);

hosted: true 让签发响应额外包含 hosted_url。成功签发的 HTTP 状态为 201;宿主返回 HTTP 302,并把该 URL 放进 Location,浏览器随即进入对话页。宿主不把 API Key 或签发响应中的 token 发给表单页面。

把实际智能体 ID、API Key 和你设置的本地测试密码放入服务端环境。下一节保存页面后,从同目录启动:

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

期望:终端显示本地地址。未经认证或来源不匹配的请求无法签发。用于已有应用时,将测试账户验证换成你的登录会话验证,把路由置于登录与 CSRF 保护之后;不要把这套测试账户机制用于线上登录。

添加打开按钮

保存为同目录的 index.html。普通表单提交可以直接跟随后端的跳转;不要通过后台 fetch 请求后就假设浏览器已经导航。

<!doctype html>
<html lang="zh">
<meta charset="utf-8">
<title>Order support</title>
<form action="/api/praxis-hosted" method="post">
  <button type="submit">Open order assistant</button>
</form>
</html>

打开 http://localhost:4242,以 customer_123 和所设测试密码登录,然后点按钮。期望:浏览器进入 chat-develop.praxisusercontent.com 下的对话页。每次点击都向宿主重新请求链接,不缓存上一次的结果。浏览器地址变化后,再检查是否出现可输入的对话区。

理解链接寿命

hosted_url 的 #c=… 是一次性兑换凭证,不是嵌入令牌。它在签发后 60 秒内有效,只能成功兑换一次。不要记录、收藏或分享带凭证的链接;每个用户应从宿主验证身份后进入。

托管页读取凭证后先移除 URL 片段,再自动向 POST /agent/v1/embed/exchange 提交 code,取得 token 和 expires_at。你的宿主不需要手动兑换,否则浏览器拿到的链接已被消费。

链接的 60 秒期限与对话令牌有效期是两件事。兑换成功后,页面在令牌到期前使用该令牌;Hosted 不自动刷新令牌。到期或收到吊销错误后,页面会提示回到原网站重新进入。刷新当前托管页也不能重新使用已经移除的一次性凭证,应回宿主重新点击按钮。

验证接入

在服务端可以用下面的请求检查签发形状。它只输出记录 ID、到期时间和是否存在链接,不输出凭证;这是独立的接口检查,不会替你导航浏览器。测试用户 ID 只用于这次受控检查,应用必须使用已验证身份。

set -o pipefail
curl --fail-with-body --silent --show-error \
  -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"agent":"agent_REPLACE_ME","external_user_id":"customer_123","hosted":true}' \
  | jq '{id, expires_at, has_hosted_url: (.hosted_url | type == "string")}'

再从按钮执行完整路径:网络面板应看到宿主 POST 返回 302,随后托管页兑换成功并显示对话。发送一条订单问题,确认收到真实回复;只看到加载界面不算成功。

用独立浏览器以 customer_456 进入,确认无法读取第一个账户的会话。无凭据访问宿主端点应返回 HTTP 401。验收链接寿命时,使用单独的测试签发,让链接超过 60 秒后再打开,期望要求回到宿主重新进入,不在原链接上反复重试。

失败处理与外观

兑换接口对过期、已用和未知凭证均返回 NOT_FOUND(HTTP 404)。这些情况都应重新从宿主进入。缺少短号或嵌入策略会使签发返回 HTTP 400;在控制台补齐前提后再试。出现 HTTP 429 时按 Retry-After 等待,再由宿主签发新链接。

外观在接入与配置 → 接入方式与外观 → iframe保存,Hosted 共享名称、主题和欢迎语等设置。持久化外观目前只能在控制台配置。Hosted 始终全页显示,不使用浮窗启动按钮。控制台的 Hosted → 打开预览用于预览,不替代宿主对真实登录用户的签发。

下一步

导出

本页内容