Form-Page 配置指南
概述
form-page 是 AgentOS 中用于嵌入外部页面或技能的核心输出 UI 规范,通过 iframe 方式加载外部内容,支持丰富的配置选项和权限控制。本文档详细介绍了 form-page 代码块中 JSON 配置的各项参数及其作用。
基本语法
form-page 使用 Markdown 代码块语法,以 form-page 作为语言标识符:
```form-page
{
// JSON 配置内容
}
```完整配置示例
```form-page
{
"url": "http://10.1.5.65:5173/test-agentos",
"name": "知识引擎接口文档",
"pageType": "static",
"data": {},
"width": "100%",
"height": "400px",
"allow": "cross-origin-isolated; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; camera; microphone; geolocation; fullscreen",
"sandbox": "allow-same-origin allow-scripts allow-downloads",
"credentialless": false,
"allowModelPreview": true,
"config": {
"skill_config": {},
"info": {
"task_id": "",
"agent_id": "1955547108685766661",
"message_id": "7f9b76e9-4253-4ccf-8830-15d0f6e55df2",
"session_id": "4e0f95f1-5e19-43d0-b5b4-bb3b008bb044",
"skill_id": "1760544913533448192",
"skill_sort": 0,
"route_id": "1955842087039696897"
}
}
}
```配置参数详解
基础配置
url / code
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
url | string | 是* | - | 页面的 URL 地址,支持 占位符动态替换 |
code | string | 是* | - | Base64 编码的 HTML 代码,与 url 二选一 |
注意:
url和code必须提供其中一个。url用于加载外部页面,code用于直接渲染 HTML 内容。
URL 占位符示例:
{
"url": "http://example.com/user/{{user.id}}/profile",
"data": {
"user": {
"id": "12345"
}
}
}最终生成的 URL:http://example.com/user/12345/profile
name
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
name | string | 否 | - | 名称,用于标识和显示 |
pageType
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
pageType | string | 否 | "static" | 页面类型,控制与 AgentOS 的交互方式 |
页面类型说明:
| 值 | 描述 | 适用场景 | 特性 |
|---|---|---|---|
"static" | 纯展示页面 | 图片、PDF、文档预览等静态内容 | 无需与 AgentOS 交互,轻量级 |
"tudouApp" | 交互式应用 | 表单、数据处理、需要调用 AgentOS API 的技能 | 可访问 AgentOS 功能和数据 |
data
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
data | object | 否 | {} | 传递给技能的初始数据 |
使用示例:
{
"data": {
"formDefaults": {
"username": "admin",
"email": "admin@example.com"
},
"apiConfig": {
"baseUrl": "https://api.example.com"
}
}
}尺寸配置
width
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
width | string/number | 否 | "100%" | iframe 宽度 |
支持的值:
| 值类型 | 示例 | 描述 |
|---|---|---|
| 百分比 | "100%", "50%" | 相对于父容器的百分比 |
| 像素值 | "400px", 400 | 固定像素宽度 |
| 预设值 | "mini", "default", "middle" | 预定义的尺寸规格 |
预设值说明:
"mini": 小尺寸,适合简单组件和工具"default": 默认尺寸,适合通用场景"middle": 中等尺寸,适合表单和复杂组件
height
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
height | string/number | 否 | "400px" | iframe 高度 |
支持的值:
- 像素值:
"400px",400,"600px" - 百分比:
"100%","80%" - 自适应:
"auto"- 根据 iframe 内部内容自动调整高度
自适应高度说明:
当设置 height: "auto" 时,iframe 高度会根据内部页面的实际内容高度自动调整。这需要 iframe 内部页面配合实现高度通知机制。
外部监听高度变化:
// AgentOS 系统会自动监听 iframe 内部的高度变化通知
scp.get(target).$Bus.on("page-resize", params => {
const hVal = `${params[0]}px`;
const targetHeight = target.style.height;
if (hVal !== targetHeight) {
target.style.height = hVal;
}
});iframe 内部通知高度变化:
function handleResize() {
// 获取顶层容器元素的实际高度
const height = document.querySelector("#upload-form")?.scrollHeight;
// 通知外部调整 iframe 高度(+2px 用于避免滚动条)
scp.emit("page-resize", height + 2);
}
let observer;
onMounted(() => {
// 初始化时计算一次高度
handleResize();
// 使用 MutationObserver 监听 DOM 变化,自动调整高度
observer = new MutationObserver(() => {
handleResize();
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
});
onBeforeUnmount(() => {
observer?.disconnect();
});权限配置
allow - 浏览器功能权限
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
allow | string | 否 | 见下方默认值 | 控制 iframe 可访问的浏览器功能和 API 权限 |
默认权限:
"cross-origin-isolated; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; camera; microphone; geolocation; fullscreen"权限列表:
| 权限名称 | 描述 | 用途场景 |
|---|---|---|
cross-origin-isolated | 跨域隔离 | 启用高精度计时器、SharedArrayBuffer 等高级功能 |
accelerometer | 加速度传感器 | 游戏、运动应用、设备方向检测 |
autoplay | 自动播放 | 视频、音频自动播放 |
clipboard-write | 剪贴板写入 | 复制文本、图片到剪贴板 |
encrypted-media | 加密媒体 | 播放受保护的媒体内容(DRM) |
gyroscope | 陀螺仪 | 获取设备旋转信息,VR/AR 应用 |
picture-in-picture | 画中画 | 视频画中画模式 |
camera | 摄像头 | 视频通话、拍照、扫码等功能 |
microphone | 麦克风 | 语音输入、录音、语音通话 |
geolocation | 地理位置 | 地图服务、位置相关功能 |
fullscreen | 全屏 | 全屏显示内容 |
payment | 支付 | 在线支付功能 |
usb | USB 设备 | 访问 USB 设备 |
bluetooth | 蓝牙 | 蓝牙设备连接 |
magnetometer | 磁力计 | 指南针功能 |
ambient-light-sensor | 环境光传感器 | 自动调节屏幕亮度 |
display-capture | 屏幕捕获 | 屏幕录制、截图 |
midi | MIDI 设备 | 音乐制作、MIDI 设备控制 |
serial | 串口通信 | 硬件设备通信 |
hid | HID 设备 | 人机接口设备(键盘、鼠标等) |
配置格式:
// 单个权限
"allow": "camera"
// 多个权限(支持多种分隔符)
"allow": "camera; microphone; fullscreen"
"allow": "camera, microphone, fullscreen"
"allow": "camera microphone fullscreen"
"allow": "camera; microphone, fullscreen"权限合并规则:
系统会自动将用户配置的权限与默认权限进行合并去重:
// 用户配置
"allow": "payment; usb; camera"
// 最终生成的权限(合并去重后)
"allow": "cross-origin-isolated; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; camera; microphone; geolocation; fullscreen; payment; usb"sandbox - 安全沙箱权限
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
sandbox | string | 否 | 见下方默认值 | 控制 iframe 的安全沙箱权限,限制可执行操作 |
默认权限:
"allow-same-origin allow-scripts allow-downloads"权限列表:
| 权限名称 | 描述 | 用途场景 |
|---|---|---|
allow-same-origin | 同源访问 | 访问同源资源和 API |
allow-scripts | 脚本执行 | 执行 JavaScript 代码 |
allow-downloads | 文件下载 | 下载文件到本地 |
allow-forms | 表单提交 | 提交表单数据 |
allow-popups | 弹出窗口 | 打开新窗口或标签页 |
allow-modals | 模态框 | 显示 alert、confirm 等弹框 |
allow-orientation-lock | 屏幕方向锁定 | 锁定屏幕方向 |
allow-pointer-lock | 指针锁定 | 游戏中的鼠标锁定 |
allow-popups-to-escape-sandbox | 弹窗逃脱沙箱 | 弹出窗口不受沙箱限制 |
allow-presentation | 演示 API | 投屏、演示功能 |
allow-storage-access-by-user-activation | 用户激活的存储访问 | 用户操作后访问存储 |
allow-top-navigation | 顶层导航 | 导航到顶层窗口 |
allow-top-navigation-by-user-activation | 用户激活的顶层导航 | 用户操作后的顶层导航 |
allow-top-navigation-to-custom-protocols | 自定义协议导航 | 导航到自定义协议 |
配置格式:
// 单个权限
"sandbox": "allow-same-origin"
// 多个权限(空格分隔)
"sandbox": "allow-same-origin allow-scripts allow-forms"权限合并规则:
与 allow 属性类似,sandbox 权限也会自动合并去重:
// 用户配置
"sandbox": "allow-forms allow-popups allow-same-origin"
// 最终生成的权限(合并去重后)
"sandbox": "allow-same-origin allow-scripts allow-downloads allow-forms allow-popups"credentialless - 无凭据模式
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
credentialless | boolean | 否 | false | 控制 iframe 是否在无凭据模式下加载,用于跨域隔离兼容 |
说明:
- 当设置为
true时,iframe 将在无凭据模式下加载内容 - 有助于解决 Cross-Origin Isolation 相关的兼容性问题
- 只有明确设置为
true时才会添加credentialless属性到 iframe
功能配置
allowModelPreview - 模型预览
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
allowModelPreview | boolean | 否 | - | 是否显示全屏预览按钮 |
说明:
- 当设置为
true时,iframe 右下角会显示全屏预览按钮 - 用户可以点击按钮将技能内容全屏显示
系统配置
config - 系统配置对象
| 参数 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
config | object | 否 | {} | AgentOS 系统配置和业务流转数据 |
config 结构说明:
{
"config": {
"skill_config": {}, // 技能特定配置
"info": {
// 业务流转信息
"task_id": "",
"agent_id": "",
"message_id": "",
"session_id": "",
"skill_id": "",
"skill_sort": 0,
"route_id": ""
}
}
}info 字段说明:
| 字段名 | 类型 | 描述 |
|---|---|---|
task_id | string | 任务 ID |
agent_id | string | 智能体 ID |
message_id | string | 消息 ID |
session_id | string | 会话 ID |
skill_id | string | 技能 ID |
skill_sort | number | 技能排序 |
route_id | string | 路由 ID |
配置场景示例
场景 1:简单静态页面
适用于展示文档、图片等静态内容:
```form-page
{
"url": "https://example.com/docs/api.html",
"name": "API 文档",
"pageType": "static",
"width": "100%",
"height": "600px"
}
```场景 2:交互式表单
适用于需要与用户交互的表单页面:
```form-page
{
"url": "https://example.com/form/user-info",
"name": "用户信息表单",
"pageType": "tudouApp",
"width": "middle",
"height": "500px",
"data": {
"userId": "12345",
"defaultValues": {
"name": "张三",
"email": "zhangsan@example.com"
}
},
"allow": "camera; microphone; geolocation",
"sandbox": "allow-same-origin allow-scripts allow-forms allow-popups"
}
```场景 3:多媒体应用
适用于需要访问摄像头、麦克风等设备的应用:
```form-page
{
"url": "https://example.com/video-call",
"name": "视频通话",
"pageType": "tudouApp",
"width": "100%",
"height": "400px",
"allow": "camera; microphone; autoplay; fullscreen",
"sandbox": "allow-same-origin allow-scripts allow-modals",
"credentialless": true,
"allowModelPreview": true
}
```场景 4:跨域隔离应用
适用于需要高精度计时器或 SharedArrayBuffer 的应用:
```form-page
{
"url": "https://example.com/high-performance-app",
"name": "高性能计算应用",
"pageType": "tudouApp",
"width": "100%",
"height": "600px",
"allow": "cross-origin-isolated; clipboard-write",
"credentialless": true
}
```最佳实践
1. 权限最小化原则
只授予 Page 必需的权限,避免过度授权:
// ❌ 不推荐:授予所有权限
"allow": "camera; microphone; geolocation; usb; bluetooth; payment"
// ✅ 推荐:只授予必需权限
"allow": "camera; microphone" // 仅视频通话需要2. 合理设置页面类型
根据 Page 的实际需求选择合适的页面类型:
- 纯展示内容使用
"static" - 需要与 AgentOS 交互使用
"tudouApp"
3. 响应式尺寸设计
优先使用百分比或预设值,确保在不同屏幕尺寸下的良好显示:
// ✅ 推荐:响应式设计
"width": "100%",
"height": "400px"
// ✅ 推荐:使用预设值
"width": "middle"4. 数据传递规范
通过 data 字段传递结构化的初始数据:
{
"data": {
"config": {
"theme": "dark",
"language": "zh-CN"
},
"initialData": {
"user": { "id": "123", "name": "用户名" }
}
}
}5. 错误处理
确保 URL 有效性和数据格式正确:
// ✅ 确保 URL 可访问
"url": "https://valid-domain.com/path"
// ✅ 确保 JSON 格式正确
{
"url": "...",
"data": {} // 注意:最后一项不要有逗号
}常见问题
Q1: Page 页面无法加载?
可能原因:
- URL 不可访问
- 跨域策略限制
- 权限配置不正确
解决方案:
- 检查 URL 是否可以在浏览器中正常访问
- 确认目标页面支持 iframe 嵌入
- 调整
allow和sandbox权限配置
Q2: 无法访问摄像头/麦克风?
解决方案:
{
"allow": "camera; microphone",
"sandbox": "allow-same-origin allow-scripts"
}Q3: 跨域隔离功能不生效?
解决方案:
{
"allow": "cross-origin-isolated",
"credentialless": true
}Q4: 表单无法提交?
解决方案:
{
"sandbox": "allow-same-origin allow-scripts allow-forms"
}Q5: 弹窗被阻止?
解决方案:
{
"sandbox": "allow-same-origin allow-scripts allow-popups allow-modals"
}Page 内部开发指南
数据获取
获取外部传入的数据
在 form-page 内部,可以通过 scp.chat.context.get() 方法获取外部传入的配置数据:
// 获取外部传入的完整配置数据
const scpData = await scp?.chat?.context.get();
// 访问具体的配置信息
const messageId = scpData?.config?.info?.message_id;
const skillData = scpData?.data; // 对应配置中的 data 字段
const skillConfig = scpData?.config?.skill_config;获取用户 Token
Page 内部可以获取当前用户的认证 Token:
// 获取用户 Token
const token = await scp.env.token.get();
console.log("用户 Token:", token);应用初始化
Vue3 工程化应用初始化
以下是一个完整的 Vue3 应用初始化示例,展示了如何在 iframe 环境中正确初始化应用:
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import { getUserInfoByTokenApi, getUserInfo } from "./api/user";
import { useUserStore } from "./stores/user";
import storage from "./utils/storage";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
// 应用初始化函数
async function initializeApp() {
try {
// 获取用户 Token
const token = await scp.env.token.get();
console.log("Page 获取到 Token:", token);
if (token) {
// 通过 Token 获取用户信息
const { data: userInfoData } = await getUserInfoByTokenApi({ token });
const userInfo = {
tenantId: userInfoData.tenantId,
userId: userInfoData.id,
userName: userInfoData.username
};
// 存储 Token 和用户信息到本地存储
const t0 = Date.now();
storage.set({
expires_time: Date.now() + 8888888888888888 * 1000 - (Date.now() - t0),
token,
access_token: token, // 兼容旧版 SAAS 应用
userInfo: JSON.stringify(userInfo)
});
// 初始化并挂载 Vue 应用
app.use(router).mount("#app");
console.log("Vue 应用已成功初始化并挂载");
} else {
console.warn("未获取到 Token,使用默认配置初始化应用");
app.use(router).mount("#app");
}
} catch (error) {
console.error("初始化应用时发生错误:", error);
// 即使出错也要挂载应用,避免白屏
app.use(router).mount("#app");
}
}
// 检测运行环境并初始化
if (window.self !== window.top) {
// 当前页面被嵌入在 iframe 中( Page 环境)
console.log("检测到 iframe 环境,等待 scp 加载完成");
scp.onLoad(() => {
initializeApp();
});
} else {
// 当前页面是顶层页面(独立运行)
console.log("检测到独立运行环境");
try {
const { data } = await getUserInfo();
const { setUserInfo } = useUserStore();
setUserInfo(data);
app.use(router).mount("#app");
} catch (error) {
console.error("独立环境初始化失败:", error);
app.use(router).mount("#app");
}
}单页面应用初始化
对于简单的单页面应用,可以参考以下模式:
// 检测是否在 iframe 环境中
if (window.self !== window.top) {
// iframe 环境
scp.onLoad(async () => {
try {
// 获取外部传入的数据
const scpData = await scp?.chat?.context.get();
const token = await scp.env.token.get();
// 初始化页面数据
initializePage(scpData, token);
} catch (error) {
console.error("iframe 环境初始化失败:", error);
// 使用默认配置初始化
initializePage();
}
});
} else {
// 独立环境
initializePage();
}
function initializePage(scpData = null, token = null) {
// 页面初始化逻辑
console.log("页面初始化", { scpData, token });
// 如果有外部数据,使用外部数据初始化
if (scpData?.data) {
// 使用 scpData.data 中的数据初始化表单或页面状态
populateFormData(scpData.data);
}
// 如果有 token,设置 API 请求头
if (token) {
setApiToken(token);
}
}React 应用初始化
以下是在 form-page 中使用 React 开发应用的初始化示例:
import { useEffect, useState } from "react"; // 新增useState
import { useLocation } from "react-router";
import { storage } from "@pt/utils/modules/storage";
import { getLoginUrl, ROUTER_WHITE_LIST } from "@/config";
import { getUserInfoByTokenApi } from '@/api/modules/login'
function loadScpSkillScript(): Promise<void> {
return new Promise((resolve, reject) => {
// @ts-ignore
if (typeof scp !== 'undefined') {
console.log('[AuthGuard] scp 对象已存在,跳过加载');
resolve();
return;
}
const script = document.createElement('script');
script.src = '/scp.skill.js';
script.async = false;
script.onload = () => {
console.log('[AuthGuard] scp.skill.js 加载成功');
resolve();
};
script.onerror = () => {
console.error('[AuthGuard] scp.skill.js 加载失败');
reject(new Error('Failed to load scp.skill.js'));
};
document.head.appendChild(script);
});
}
interface AuthGuardProps {
children: React.ReactNode;
}
async function initConfig() {
// 安全检查:确保 scp 对象存在
// @ts-ignore
if (typeof scp === 'undefined' || !scp.env || !scp.env.token) {
console.warn('[AuthGuard] scp 对象未完全初始化,跳过 scp.env 配置');
return;
}
try {
// @ts-ignore
await scp.env.token.set(storage.get("token"));
// @ts-ignore
const token = await scp.env.token.get()
if (token) {
const { data: userInfoData } = await getUserInfoByTokenApi({
token,
})
let userInfo = {
tenantId: userInfoData.tenantId,
userId: userInfoData.id,
userName: userInfoData.username,
}
// 存储token
const t0 = Date.now()
storage.set({
expires_time: Date.now() + 8888888888888888 * 1000 - (Date.now() - t0),
token,
access_token: token, // 兼容 旧的 SAAS 应用,会使用 access_token
userInfo: JSON.stringify(userInfo),
})
} else {
// 如果没有 token,可以在这里做一些处理,比如跳转到登录页或者显示错误信息
console.warn('未获取到 Token,Vue 应用未初始化。')
}
} catch (error) {
console.error('[AuthGuard] scp.env 初始化失败:', error);
}
}
/**
* 路由守卫组件
* 用于验证用户登录状态和权限
*/
export default function AuthGuard({ children }: AuthGuardProps) {
const location = useLocation();
// 新增加载状态:true=还在初始化/校验,false=已完成
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let isCancelled = false;
const checkAuth = async () => {
try {
const currentPath = location.pathname;
const cookie_token = storage.get("token");
const cookie_userInfoStr = storage.get("userInfo");
console.log('[AuthGuard] 路由守卫检查:', {
currentPath,
hasToken: !!cookie_token,
hasUserInfo: !!cookie_userInfoStr,
token: cookie_token,
userInfo: cookie_userInfoStr,
});
// 先加载 scp.skill.js
try {
await loadScpSkillScript();
} catch (error) {
console.warn('[AuthGuard] 加载 scp.skill.js 失败,但继续执行', error);
}
// 先执行初始化,确保scp.env配置完成
if (window.self !== window.top) {
await initConfig();
}
if (isCancelled) return;
// 白名单路由直接放行
if (ROUTER_WHITE_LIST.includes(currentPath)) {
console.log('[AuthGuard] 白名单路由,直接放行');
setIsLoading(false); // 加载完成,允许渲染
return;
}
// 无认证信息则跳转登录
if (!cookie_token || !cookie_userInfoStr) {
console.error('[AuthGuard] 缺少认证信息,准备跳转登录页');
storage.clear();
window.location.href = getLoginUrl();
return;
}
// 认证通过,加载完成
console.log('[AuthGuard] 认证通过,允许访问');
setIsLoading(false);
} catch (error) {
console.error('[AuthGuard] 认证检查/初始化失败:', error);
if (!isCancelled) {
storage.clear();
window.location.href = getLoginUrl();
}
}
};
checkAuth();
return () => {
isCancelled = true;
setIsLoading(true); // 卸载时重置加载状态
console.log('[AuthGuard] 路由变化/组件卸载,终止后续操作');
};
}, [location.pathname]);
// 关键:加载中不渲染子组件,避免提前读取未初始化的scp.env
if (isLoading) {
// 可替换为项目的加载组件,比如 <Spin />
return <div>加载中...</div>;
}
return <>{children}</>;
}数据持久化
保存技能数据
技能可以将数据保存到 AgentOS 系统中,用于后续回填或恢复:
// 保存数据示例
async function saveSkillData(data) {
try {
const scpData = await scp?.chat?.context.get();
const messageId = scpData?.config?.info?.message_id;
if (messageId) {
// 保存数据到指定的命名空间和键
await saveDataByMessageId({
namespace: "action_ui",
key: messageId,
value: data
});
console.log("技能数据保存成功");
}
} catch (error) {
console.error("保存技能数据失败:", error);
}
}
// 获取已保存的数据
async function loadSkillData() {
try {
const scpData = await scp?.chat?.context.get();
const messageId = scpData?.config?.info?.message_id;
if (messageId) {
const { data } = await getSkillDataByMessageId({
namespace: "action_ui",
key: messageId
});
if (data?.value) {
console.log("加载已保存的数据:", data.value);
return data.value;
}
}
} catch (error) {
console.error("加载技能数据失败:", error);
}
return null;
}最佳实践
1. 环境检测
始终检测运行环境,确保代码在不同环境下都能正常工作:
// 推荐的环境检测方式
const isIframeEnvironment = window.self !== window.top;
const isScpAvailable = typeof scp !== "undefined";
if (isIframeEnvironment && isScpAvailable) {
// iframe + scp 环境
scp.onLoad(() => {
initializeApp();
});
} else if (isIframeEnvironment) {
// 仅 iframe 环境,无 scp
console.warn("iframe 环境但 scp 不可用");
initializeApp();
} else {
// 独立环境
initializeApp();
}2. 错误处理
始终包含错误处理逻辑,确保应用的健壮性:
async function safeApiCall(apiFunction, fallbackValue = null) {
try {
return await apiFunction();
} catch (error) {
console.error("API 调用失败:", error);
return fallbackValue;
}
}
// 使用示例
const scpData = await safeApiCall(() => scp?.chat?.context.get(), {});
const token = await safeApiCall(() => scp.env.token.get(), null);3. 数据验证
对外部传入的数据进行验证:
function validateScpData(scpData) {
if (!scpData || typeof scpData !== "object") {
return false;
}
// 验证必要字段
const requiredFields = ["config"];
return requiredFields.every(field => field in scpData);
}
// 使用示例
const scpData = await scp?.chat?.context.get();
if (validateScpData(scpData)) {
// 使用验证过的数据
initializeWithData(scpData);
} else {
// 使用默认配置
initializeWithDefaults();
}Cross-Origin Isolation 配置指南
什么是 Cross-Origin Isolation
Cross-Origin Isolation(跨域隔离)是一种 Web 安全机制,它允许网页访问某些强大的 Web API,如高精度计时器(performance.now())和 SharedArrayBuffer。当页面处于跨域隔离状态时,crossOriginIsolated 属性会返回 true。
启用 Cross-Origin Isolation 的方法
方法一:HTTP 响应头配置(推荐)
在服务器端设置以下 HTTP 响应头:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-originNginx 配置示例:
server {
listen 80;
server_name your-domain.com;
location / {
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# 其他配置...
}
}Apache 配置示例:
<VirtualHost *:80>
ServerName your-domain.com
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
# 其他配置...
</VirtualHost>Node.js Express 配置示例:
app.use((req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});方法二:Meta 标签配置
在 HTML 页面的 <head> 部分添加:
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp" />
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin" />Form-Page 中的 Cross-Origin Isolation
配置示例
{
"url": "https://your-domain.com/high-performance-app",
"name": "高性能计算应用",
"pageType": "tudouApp",
"allow": "cross-origin-isolated; clipboard-write",
"credentialless": true,
"height": "600px"
}关键配置项
- allow 权限:必须包含
cross-origin-isolated - credentialless 属性:设置为
true有助于跨域隔离兼容性 - 目标页面配置:iframe 内的页面必须正确设置 COEP 和 COOP 头
验证 Cross-Origin Isolation 状态
在浏览器控制台检查
// 检查当前页面是否处于跨域隔离状态
console.log("Cross-Origin Isolated:", crossOriginIsolated);
// 检查可用的高精度 API
if (crossOriginIsolated) {
console.log("高精度计时器可用:", performance.now());
console.log("SharedArrayBuffer 可用:", typeof SharedArrayBuffer !== "undefined");
} else {
console.log("跨域隔离未启用,某些 API 可能不可用");
}在技能内部检查
// 在 form-page 内部检查
scp.onLoad(async () => {
const isIsolated = self.crossOriginIsolated;
console.log("技能页面跨域隔离状态:", isIsolated);
if (isIsolated) {
// 可以安全使用高精度 API
const highResTime = performance.now();
console.log("高精度时间戳:", highResTime);
}
});常见问题和解决方案
Q1: crossOriginIsolated 返回 false
可能原因:
- 服务器未设置正确的 COEP 和 COOP 响应头
- 页面中包含不兼容的跨域资源
- iframe 权限配置不正确
解决方案:
- 确保服务器设置了正确的响应头
- 检查所有跨域资源是否设置了
Cross-Origin-Resource-Policy - 在 form-page 配置中添加
cross-origin-isolated权限
参考文档:Cross-Origin Isolation 配置指南
Q2: SharedArrayBuffer 不可用
解决方案:
{
"allow": "cross-origin-isolated",
"credentialless": true,
"sandbox": "allow-same-origin allow-scripts"
}Q3: 第三方资源加载失败
当启用 Cross-Origin Isolation 后,所有跨域资源都需要明确的 CORP 头。
解决方案:
# 对于图片、样式等资源
Cross-Origin-Resource-Policy: cross-origin
# 对于同源资源
Cross-Origin-Resource-Policy: same-origin
# 对于特定源的资源
Cross-Origin-Resource-Policy: same-site最佳实践
1. 渐进式启用
// 检测支持情况后再启用高级功能
if (crossOriginIsolated && typeof SharedArrayBuffer !== "undefined") {
// 使用高性能 API
initHighPerformanceFeatures();
} else {
// 降级到兼容模式
initCompatibilityMode();
}2. 资源预检
// 预检关键资源的可用性
async function checkResourceAvailability() {
const checks = {
sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
highResTimer: performance.now() !== performance.now(),
crossOriginIsolated: self.crossOriginIsolated
};
console.log("功能可用性检查:", checks);
return checks;
}3. 错误处理
// 优雅处理 Cross-Origin Isolation 相关错误
try {
const buffer = new SharedArrayBuffer(1024);
// 使用 SharedArrayBuffer
} catch (error) {
console.warn("SharedArrayBuffer 不可用,使用替代方案:", error);
// 使用 ArrayBuffer 替代
const buffer = new ArrayBuffer(1024);
}性能优化建议
1. 预加载关键资源
<!-- 预加载并设置正确的 CORP -->
<link rel="preload" href="/critical-resource.js" as="script" crossorigin="anonymous" />2. 资源缓存策略
// 利用 Cross-Origin Isolation 的缓存优势
if (crossOriginIsolated) {
// 可以使用更激进的缓存策略
cacheStrategy = "aggressive";
} else {
cacheStrategy = "conservative";
}兼容性说明
官方文档
权限策略
版本信息: v1.0 更新时间: 2025-04-12 适用版本: AgentOS v2.0+
本文档将随着 AgentOS 系统的更新而持续维护和完善。如有问题或建议,请联系开发团队。