# 嵌入式对话与页面动作 > 状态:SOURCE_VERIFIED / DEPLOYED_BUSINESS_E2E_PENDING(2026-08-24) 本文描述 ReachAI 当前 Embed Chat、交互组件和 Page Bridge 的现行契约。它不是早期方案或 SDK 设计草案;精确前端类型以 `ai-admin-front/src/sdk/` 为准,服务端行为以 Control `platform` package、Runtime 交互实现和当前测试为准。 ## 当前结论 - 业务页面通过 `@reachai/embed-chat` 的 `createEafChat()` 嵌入对话;`Eaf*` 是兼容敏感的 SDK 技术标识,不代表产品仍叫 EAF。 - 浏览器只持有短期 Embed Token,不持有项目 `appSecret`。Token 必须由业务后端根据本系统登录态换取。 - Control 终止 Embed 身份、持久化 session/chat/page-action 事实并把可信执行委托 Runtime。 - Runtime 的 `uiRequest` 用统一交互组件渲染;Page Action 只能由已注册的当前页面 Bridge 执行,不能从模型文本直接调用任意 JavaScript。 - Page Action 使用 server request、claim、result 和 `requestId` 去重,目标是把副作用限定在唯一页面实例并避免重复执行。 - 源码契约已核对;是否能称为接入完成仍取决于目标服务、数据库、业务登录、Token broker、真实浏览器对话和页面动作证据。 ## 组件与所有权 | 组件 | 所在位置 | 责任 | | --- | --- | --- | | Embed Chat SDK | `ai-admin-front/src/sdk/`,发布为 `@reachai/embed-chat` | 对话壳、Token 刷新、SSE、统一交互、Page Bridge 和公开事件 | | 业务 Token broker | 接入方业务后端;可使用 `ReachAiEmbedTokenClient` | 解析当前业务用户,服务端换取短期 Embed Token | | Embed public API | `reachai-control-service` `PlatformEmbedPublicController` | Token、session、消息、交互恢复、Page Action 和页面重绑定 | | Embed 目录/治理 | Control `PlatformEmbedCatalogController` 等 | Session、页面、动作、Renderer 和调试目录 | | Agent/Workflow 执行 | `reachai-runtime-service` | Supervisor、GraphSpec、交互暂停/恢复、Trace/RunOps | | 项目凭据与 Embed policy | `reachai-capability-service` | 验证项目签名、allowed origin/agent 和 Token TTL | | 可选知识检索 | `reachai-knowledge-service` | 仅在被选 Workflow 使用 Knowledge 节点时参与检索,不终止 Embed 身份 | | 可选模型调用 | `reachai-model-service` | 为 Runtime/Knowledge 提供 Chat、Embedding、Rerank;业务浏览器不直连 | ## 端到端链路 ```text 1. 业务页面创建唯一 pageInstanceId 和 Page Bridge 2. SDK 调用业务后端 Token broker 3. 业务后端从本系统登录态解析用户 4. ReachAiEmbedTokenClient 以项目签名调用 Control /api/embed/token/exchange 5. Control 委托 Capability 验证项目/策略,并复核 Runtime Agent 6. Control 返回短期 Embed JWT 7. SDK 创建 /api/embed/chat/sessions 8. 用户消息通过同步或 SSE 入口进入 Control -> Runtime 9. Runtime 返回公开文本、uiRequest 和/或 page.action.requested 10. SDK 在当前 Page Bridge 中确认、claim、执行并回传结果 11. Control/Runtime 继续执行或完成 Run,并留下 Trace/审计证据 ``` ## 业务后端 Token broker Token broker 是强制安全边界,不是可选代理: 1. 读取业务系统自己的 Cookie/Bearer/session; 2. 解析当前用户、tenant、roles 和允许公开的 attributes; 3. 使用 SDK 传来的 `pageKey`、`pageInstanceId`、`route`、`origin`; 4. 调用 `ReachAiEmbedTokenClient.exchange(...)`; 5. 只把短期 token、`expiresIn`/`expiresAt` 返回浏览器。 浏览器传来的 `externalUserId`、roles 或 tenant 不能代替业务后端登录态。完整 Java 示例和签名配置见 [Spring Boot Starter README](../../reachai-spring-boot2-starter/README.md)。 推荐 Enrollment 配置使用 Starter credential store;不要为了前端接入把 `appKey/appSecret` 写入 Vite 变量、Local Storage、页面源码或 SDK `pageRegistry` 选项。 ## 前端 SDK 主契约 ### `createEafChat()` 核心选项: | 选项 | 说明 | | --- | --- | | `agentId` | 要调用的稳定 Agent key/id | | `mount` | CSS selector 或 HTMLElement | | `tokenProvider(context)` | 从业务后端获取短期 Token;应处理 `signal` 和刷新原因 | | `bridge` | 当前页面的 `EafPageBridge` | | `page` | `pageKey`、route 等页面目录描述 | | `apiBase` / `embedPathPrefix` | ReachAI 公共地址和 Embed 路径;默认走同源 `/api/embed` | | `stream` | 是否使用 SSE 消息入口 | | `theme`、`position`、`resizable` | 外观与布局,不改变安全边界 | | `onEvent`、`onError`、`onStateChange` | 公开事件、错误和 Token 状态 | `tokenProvider` 会收到 `reason`、`attempt`、`signal`、`sessionId`、`pageKey`、`pageInstanceId`、`route` 和 `origin`。业务前端应把页面身份原样交给自己的 broker,但 broker 仍必须独立解析用户。 `EafChatClient` 当前提供 `open()`、`close()`、`toggle()`、`send()`、`retry()`、`rebindPage()`、`setContext()` 和 `destroy()`。`registerPageCatalog()` 仅用于兼容路径;浏览器侧 `pageRegistry.appSecret` 已 deprecated,不应在新接入中使用。 最小安装与调用示例见 [SDK 接入与 Embed Chat 快速参考](./SDK接入与EmbedChat快速参考.md)。 ### `createEafPageBridge()` 每个实际页面实例创建自己的 Bridge: ```ts const bridge = createEafPageBridge({ route: location.pathname, confirmAction: async (request) => window.confirm(request.title || '确认执行?'), }) bridge.registerAction('order.refresh', async () => { await reloadOrder() return { status: 'SUCCESS', message: '订单已刷新' } }, { title: '刷新订单', confirmRequired: false, }) ``` `pageInstanceId` 由 Bridge 生成或显式传入。同一用户打开两个相同路由的标签页时也必须不同;服务器事件和结果都以目标页面实例约束。 ## Embed API | 方法与路径 | 用途 | | --- | --- | | `POST /api/embed/token/exchange` | 项目签名换取短期 Embed JWT | | `POST /api/embed/chat/sessions` | 校验 JWT 并创建绑定页面/用户/Agent 的 session | | `POST /api/embed/chat/sessions/{sessionId}/messages` | 同步消息 | | `POST /api/embed/chat/sessions/{sessionId}/messages/stream` | SSE 消息 | | `POST /api/embed/chat/sessions/{sessionId}/interactions/{interactionId}/submit` | 同步提交/取消交互 | | `POST /api/embed/chat/sessions/{sessionId}/interactions/{interactionId}/submit/stream` | SSE 恢复交互 | | `GET /api/embed/chat/sessions/{sessionId}/page-actions/pending` | 恢复尚未处理的页面动作 | | `POST /api/embed/chat/sessions/{sessionId}/page-actions/{requestId}/claim` | 执行副作用前原子 claim | | `POST /api/embed/chat/sessions/{sessionId}/page-actions/{requestId}/result` | 回传业务结果 | | `POST /api/embed/chat/sessions/{sessionId}/page-bridge/rebind` | SPA 跨页面导航后绑定新 Bridge | 所有 session 后续请求都要带 Embed Bearer token。Control 会复核 session 与 token 的 project、Agent、用户和页面身份;不能拿一个页面的 token 操作另一个页面实例。 ## Token 与 Session 绑定 Embed JWT 当前使用 HS256 与 `kid`,包含: - `tenantId`、`appId/projectCode`、`agentId` - `externalUserId`、`globalUserId`、`userName`、roles/attributes - `pageKey`、`pageInstanceId`、`route`、`origin` - `iat`、`exp`、`jti` Control 使用 `control_embed_session` 保存服务端 session 事实。Token 过期后 SDK 可刷新,但不会自动重发上一条用户消息;401 重试必须保持显式,避免重复业务副作用。 ## 对话与交互事件 Embed SDK 对业务页面公开的主要事件为: - `message.delta` - `ui.requested` - `page.action.requested` - `turn.progress` - `message.completed` - `interaction.submitted` - `interaction.cancelled` - `error` SDK 默认不把 `supervisor.step` 或 reasoning 作为公共业务事件。公开文本只来自后端显式 `message.delta` / completion,不从 Workflow 节点调试事件推导第二份答案。 推荐顺序是: ```text message.delta -> ui.requested -> page.action.requested -> message.completed ``` 同一个 `requestId` 的 Page Action 即使同时出现在 SSE 和 completion queue 中也只执行一次;断线后 pending poll 负责恢复未完成请求。 `ui.requested` 使用 Runtime 统一交互协议,支持确认、表单等结构化输入。提交时必须带 `interactionId` 和幂等键;取消是显式业务选择,不应伪装成网络错误或成功。 ## Page Action 协议 ### 请求 `page.action.requested` 至少包含: - `requestId` - `target.pageInstanceId` - `actionKey` - `args` - 可选 `title`、`confirm`、`nodeId`、metadata SDK 只查找当前 Bridge 已注册的 `actionKey`。不存在、被禁止、超时或用户取消时返回结构化结果,绝不使用 `eval()`、字符串函数名或任意 DOM 脚本执行。 ### 结果 公开 `PageActionStatus`: - `SUCCESS` - `NO_DATA` - `PRECONDITION_FAILED` - `USER_CANCELLED` - `FAILED` - `ACTION_NOT_FOUND` - `FORBIDDEN` - `TIMEOUT` - `CANCELLED`(旧调用兼容;新接入使用 `USER_CANCELLED`) 业务无数据、前置条件不满足和用户取消是可解释的业务终态,不应全部折叠成异常。返回的 `message` 必须可安全展示,`data` 只包含当前用户有权看到的结果。 ### 副作用与 exactly-once 边界 执行顺序固定为: 1. 校验 request/session/pageInstance; 2. 如需确认,先取得用户同意; 3. SDK 调用 server claim; 4. claim 成功后才执行页面副作用; 5. 上报结构化 result; 6. Runtime 根据结果继续或结束。 claim 和 `requestId` 只能降低重复执行概率,不能替代业务后端幂等。不可逆动作仍应在业务 API 层使用幂等键、权限校验和审计。 ## 跨页面导航 保留动作 `__reachai.navigate` 只用于平台已批准的 SPA 导航。业务应用不注册该 key,而是在 `createEafPageBridge({ onNavigate })` 提供自己的路由适配器: 1. 平台返回目标 `pageKey/route`; 2. host 校验目标是否在自身路由目录; 3. SPA 完成导航并挂载目标页面动作; 4. 调用 `chat.rebindPage({ bridge, page })`; 5. SDK 获取目标页面的新 Token,并复用活动 session 完成服务端 rebind。 模型文本、任意 URL 或未注册页面都不能直接触发导航。 ## 页面与动作目录 Control 使用 `control_project_page`、`control_project_page_resource`、`control_page_action` 保存页面和动作目录。业务页面注册入口为 `POST /api/registry/projects/{projectCode}/pages/register`;注册必须来自业务后端、Starter/接入工具或受控 AI Coding 流程,不能把项目密钥放入浏览器自动注册。 页面目录是设计/治理事实,运行期可执行动作仍以当前 session 上报并绑定的 Bridge action definitions 为准。目录里存在某个 action 不代表当前标签页一定已经挂载 handler。 ## 数据与服务边界 | 数据 | owning service | | --- | --- | | `control_embed_session`、`control_embed_chat_event` | Control | | `control_page_action_event`、`control_page_action` | Control | | `control_project_page`、`control_project_page_resource` | Control | | `control_embed_renderer` | Control | | `runtime_interaction_session`、Workflow checkpoint、Run/Trace | Runtime | | 项目凭据和 allowed origin/agent policy | Capability | 同库阶段也不得跨服务直接操作这些表。Control 通过 Runtime API 恢复交互,Runtime 通过 Control Page Bridge contract 发起动作。 ## 安全要求 - 浏览器永不保存项目 `appSecret`、Enrollment Token、平台管理员 session 或内部 service secret。 - Token broker 从业务登录态确定用户;不接受浏览器自报 principal。 - allowed origin、allowed Agent、Token TTL 和项目状态由 Capability credential owner 验证。 - Page Action handler 仍要调用带本地鉴权的业务 API;前端 handler 不是权限边界。 - 不在 Trace、chat event、错误响应或 analytics 中保存 Token、Cookie、原始秘密和不必要的业务正文。 - Embed CORS、反向代理 SSE 缓冲、HTTPS、CSP、iframe/宿主页策略需要在目标环境单独验证。 ## 验收门禁 以下证据都具备,才可称某个业务系统完成 Embed 接入: 1. 独立业务用户能用本系统登录态从 broker 获取 Token,浏览器无项目密钥; 2. Token 中 project、Agent、user、page、origin 绑定正确,过期/篡改/跨页使用被拒绝; 3. 创建真实 `control_embed_session`,消息进入 Runtime 并形成 Run/Trace; 4. SSE 文本、交互确认/表单和取消在真实 Network 链路工作; 5. 当前页面动作成功,另一个标签页/用户/项目无法 claim 或回传; 6. action timeout、用户取消、无数据、业务失败和断线恢复产生正确结构化结果; 7. 业务后端再次执行 tenant、role 和 row-level 授权; 8. 服务重启、Token 刷新和重复 `requestId` 不造成重复不可逆副作用。 只有 TypeScript/Maven 测试或 SDK 打包通过时,状态仍应是源码/制品验证,不是业务浏览器 E2E。 ## 验证入口 ```powershell Set-Location ai-admin-front npm run test:sdk npm run test:conversation npm run build:sdk:pack npm run verify:sdk-pack ``` 后端还应运行 Control/Runtime/Capability 目标测试和边界检查;服务启动后再使用真实业务页面、真实用户和当前部署制品完成上述验收矩阵。 ## 进一步阅读 - [平台身份与授权模型](./平台身份与授权模型.md) - [SDK 接入与 Embed Chat 快速参考](./SDK接入与EmbedChat快速参考.md) - [Spring Boot Starter README](../../reachai-spring-boot2-starter/README.md) - [Workflow INTERACTION Runtime](../architecture/workflow-interaction-runtime.md) - [业务页面工作台](../architecture/business-page-workbench.md) - [Control API 认证边界矩阵](../architecture/platform-api-auth-matrix.md)