打开托管对话
由后端签发一次性链接,把已登录用户跳转到独立的 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 → 打开预览用于预览,不替代宿主对真实登录用户的签发。