一行嵌入
在已有 HTTPS 网站中插入加载脚本,让登录用户使用 iframe 对话界面。
在网页中插入一行加载脚本,显示 Praxis 托管的 iframe 对话界面。外观在控制台保存,用户留在你的网页中对话;“一行”只指前端安装,后端仍需为登录用户签发令牌。
准备 HTTPS 宿主
准备一个可修改页面和后端的 HTTPS 网站、Bun、同组织的 API Key,以及有短号的服务智能体。以下连接 Develop。用于演示的 HTTPS 入口应转发到本机 127.0.0.1:4242,并保留原始 Host 请求头;证书和域名由你的宿主提供。
在 Develop 控制台打开服务智能体 → 你的智能体 → 接入与配置 → 配置 → 安全,选择运行环境,在来源白名单添加实际 HTTPS 来源,例如 https://support.example.com,点保存安全设置。来源不包含路径。嵌入策略目前只能在控制台配置。
仅打开允许 localhost不能授权 iframe 嵌入;本页需要已保存的 HTTPS 来源。若只做本机 HTTP 调试,先使用 Kit 快速开始。
记录两个标识:签发请求使用公开 ID agent_…;脚本的 data-agent 使用控制台顶部可复制的短号,格式为 5–8 位字母或数字。不要把完整 ID 填进短号位置。
提供签发端点
把以下代码保存为 server.ts,放在 HTTPS 入口后。示例使用两个受密码保护的测试账户;上线时将 authenticate 换成宿主登录会话验证,并保留 CSRF 保护。HOST_ORIGIN 填实际 HTTPS 来源,不带末尾斜线。所有环境变量只配置在后端。
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 登录。
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。
插入加载脚本
保存为 index.html。把 ABC123 替换为实际短号;若已有宿主页,只需将其中的脚本标签放到需要出现对话的位置。
<!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,脚本所在位置就是插入位置,给外层容器预留足够高度。
保存外观
在接入与配置 → 接入方式与外观 → iframe调整名称、欢迎语和布局,点保存。期望显示“已保存,嵌入页刷新后生效”;刷新你的宿主页验证。持久化外观目前只能在控制台配置。
如果选择 data-launcher="none",浮层没有默认打开按钮。为已有按钮设置 CSS 选择器,或在宿主代码中控制打开,示例如下。按钮需在加载器执行前存在。
<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>这是上一节脚本的替代写法,同一智能体在页面中只保留一份加载脚本。
验证与排错
打开 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 站点加载同一脚本,期望浏览器阻止嵌入。验证签发保护时,在退出宿主登录后再次访问签发端点,期望宿主拒绝请求;不要只检查按钮是否隐藏。
用户退出或切换账户时,重新加载宿主页,让后端按新登录态签发,避免沿用原账户界面。加载器的打开和关闭只影响界面显示,不是账户退出,也不会替你的宿主结束登录会话。