面板管理页通过“接入说明”查看完整引导,也可在“导入面板”弹窗直接下载模板。接入顺序为下载模板 → 本地开发与模拟 → 导入绑定 → 设备实际验证。从“下载模板”选择开关或空调,解压后在本地编辑器或 AI 工具中开发。模板包含完整源码和本说明的离线版 development.html,也可直接导入后台查看效果。面板管理负责导入、模拟、导出、删除和绑定,源码在本地维护。
在产品或设备详情查看真实物模型,核对属性标识符、类型、读写权限、单位、范围和枚举含义。不同产品只要满足同一面板的能力要求,就能共用面板。
power,类型 bool,可读写。power、targetTemperature、mode、fanSpeed;currentTemperature 是可选的室温。| 文件 | 用途 | 开发时怎么改 |
|---|---|---|
meta.json |
名称、版本、终端和物模型能力合同 | 按实际物模型声明 requirements,发布修改时更新 version |
index.html |
面板布局与控件 | 维护页面结构和控件的字段映射,保留 SDK 引用 |
panel.css |
共享样式、响应式布局与状态外观 | 优先调整统一变量,保持桌面双栏、手机单栏及状态样式 |
panel.js |
读取状态、响应操作和处理回执 | 通过 window.urPanel 调用宿主,按实际字段调整 |
panel-sdk.js |
与后台、H5 或 App 宿主通信 | 随模板使用,不写入账号令牌或设备密钥 |
development.html |
可离线阅读的开发说明 | 不参与设备控制,可保留在包内 |
仅接受ZIP导入;根目录必须同时有meta.json和index.html,禁止HTML内嵌清单或manifest.json。即使页面只有一个HTML,也要与meta.json一起压缩为ZIP。
模板无需 npm 安装,不依赖 CDN。需要使用框架时,先构建成静态 HTML/CSS/JS,再把构建产物按该目录结构打包;入口必须是根目录 index.html。图片、字体和脚本使用包内相对路径。
安装支持 things panel 命令的联犀 CLI 后,在解压目录的上一级运行:
# 下列 air-conditioner 是解压后的目录名,请按实际目录调整
ur things panel validate ./air-conditioner
ur things panel serve ./air-conditioner
# 外部编辑器修改后,重新运行模拟页面加载新内容
ur things panel pack ./air-conditioner --out ./air-conditioner.zip
validate、serve、pack 均可离线运行,无需登录;serve 的控制只改变模拟设备,不会发送到平台。直接双击 index.html 没有宿主桥,不能代替模拟运行。输出 ZIP 放在工程目录外,避免把旧包再次装入新包。CLI 不覆盖已有输出文件,重复打包时使用新文件名。
模拟设备若有已确认的布尔命令自动复位,可在 .data/reset-triggers.json 显式写入数组,如 [{"identifier":"clear_event","triggerValue":true,"resetValue":false}]。字段必须在清单中声明为可写布尔属性;模拟正常执行时才上报复位,失败、未同步及超时不伪造状态。该文件只影响模拟器,实际设备仍以协议和真实上报为准;禁止凭字段名称猜测复位行为。
也可以让 CLI 创建工程:
ur things panel init ./my-switch
ur things panel init ./my-aircon --template air-conditioner
CLI 与 Skills 安装步骤见 面板开发入口。v0.9.2 已提供面板命令,v0.9.3 补齐模拟数据、复位型命令说明与独立的 256 项能力声明上限。先安装完整 CLI,再执行 ur skills target detect 和 ur skills install --all,重开 AI 客户端会话;仅升级可执行文件不能证明客户端已经加载新技能。未安装 CLI 时仍可编辑源码并将根目录清单与入口打包为 ZIP,导入后台模拟。
面板只接受ZIP压缩包,meta.json和index.html必须直接位于根目录,不能多包一层文件夹。新导入、更新、模板下载和面板导出均使用这一格式。meta.json是机器读取的身份说明、运行条件与资料索引,不是UI组件树,也不包含设备绑定。
| 字段 | 必需 | 用途与限制 |
|---|---|---|
| formatVersion | 是 | 包格式版本,当前固定数字1,区别于面板内容版本 |
| name | 是 | 面板显示名称,非空、最多80字;后台填写的名称会写入此处 |
| desc | 否 | 备注和用途,最多2000字;后台备注可清空此字段 |
| version | 否 | 作者维护的内容版本,最多64字符,例如1.2.1;建议每次修改后递增,不用于自动覆盖资产 |
| entry | 是 | 固定为根入口index.html,不能是外部URL |
| sdkVersion | 是 | 宿主SDK合同版本,当前固定字符串1.0.0;不是CLI版本 |
| platforms | 是 | 非空、无重复数组:web、h5、appAndroid、appIos、mini;只填已适配的端,mini声明不代表宿主已经支持 |
| requirements | 是 | 物模型能力数组;无依赖也必须显式填写[]。用于绑定/运行兼容校验,不是完整物模型定义 |
| cover | 否 | 封面路径固定.data/cover.png;网页导入缺图时生成。PNG最多1MiB、单边1920px、总计200万像素 |
| readme | 否 | 包内Markdown说明路径,例如README.md,声明后文件必须存在 |
| skills | 否 | 包内Markdown技能文件路径数组,例如["skills/panel-development.md"];仅供开发Agent阅读,不会执行 |
| thingModel | 否 | 包内联犀物模型JSON路径,例如.data/thing-model.json;声明后必须存在且为有效JSON,导入面板不会自动导入模型 |
每项requirements必须声明kind、identifier和显式布尔required。kind为property时,access为read或write,define写实际字段类型及单位、枚举、范围等约束;kind为action时dir固定down,input写真实行为参数;必需事件当前不支持。identifier须与设备实际物模型完全一致;required=false表示缺少能力时按UI规格隐藏或禁用,不得补造模拟状态。访问类型、枚举及缩放须同时与业务JS映射一致。
所有资料路径相对ZIP根目录,不允许外部URL或../。只接受已定义字段;不要写入token、密码、企业归属、设备ID或绑定目标。归属取当前登录身份,绑定通过后台单独选择。导出及模板保留meta.json、README、skills、.data资料;包格式错误在上传或后端校验阶段明确拒绝。
下面是开关面板的最小清单;空调请从随包的完整清单修改。
{
"formatVersion": 1,
"name": "我的开关面板",
"version": "1.0.0",
"sdkVersion": "1.0.0",
"entry": "index.html",
"platforms": ["web", "h5", "appAndroid", "appIos"],
"requirements": [
{"kind": "property", "identifier": "power", "access": "write", "required": true, "define": {"type": "bool"}}
]
}
清单是物模型合同,不是页面组件配置。required: true 的能力缺失或不兼容时不能绑定;可选能力设为 false,并在界面中隐藏或禁用不可用控件。属性写入要求真实模型可读写且当前用户有控制权限。
平台数值范围使用数字字符串,例如 "min":"16","max":"30","step":"1"。枚举 mapping 的键使用数字字符串,不能用 cool、low 等英文语义键。空调样板模式为 0制冷、1制热、2除湿、3自动;风速为 0自动、1低风、2中风、3高风。必须以实际物模型为准,不能假设所有空调都相同。
在 index.html 中先加载 panel-sdk.js,再加载业务 JS。SDK 在 iframe 内提供 window.urPanel;账号、设备身份和控制权限由宿主限定。
const context = await window.urPanel.getContext();
const model = await window.urPanel.getThingModel();
// 先订阅,再读取初始快照,避免初始化期间漏掉新上报。
const unsubscribe = await window.urPanel.subscribe(
{ topics: ['property', 'connection', 'command'] },
(event) => {
if (event.topic === 'property') {
// event.data 是 identifier -> 属性快照的映射,更新界面。
console.log('属性更新', event.data);
}
if (event.topic === 'command') {
// 用 event.data.commandID 关联受理结果,展示最终执行状态。
console.log('执行结果', event.data);
}
},
);
const snapshot = await window.urPanel.readProperties({ identifiers: ['power'] });
// 请放在用户点击事件中,并先检查模型、当前权限和联网能力。
const accepted = await window.urPanel.writeProperties({ values: { power: true } });
// 有下行行为时,按实际行为模型调用:
// await window.urPanel.invokeAction({ identifier: 'restart', params: {} });
window.addEventListener('pagehide', () => unsubscribe(), { once: true });
| SDK 方法 | 参数与返回 | 界面应如何使用 |
|---|---|---|
getContext() |
返回当前设备上下文、mode、capabilities | 区分模拟、在线、离线,判断可用能力 |
openSettings() |
无参数,返回 {opened:true} |
按capabilities.settings显示齿轮,宿主固定当前设备;模拟仅预览设置入口 |
getThingModel() |
返回宿主限定的属性、行为及可用状态 | 按真实范围、单位、枚举和读写权限渲染控件 |
readProperties({identifiers}) |
返回 {identifier:{value,timestamp,receivedTime,quality,source}},时间单位毫秒 |
未知或无效质量显示未知,缓存不标成实时数据 |
writeProperties({values}) |
返回 commandID、accepted | 只表示受理,先显示等待,不能立刻提示设备执行成功 |
invokeAction({identifier,params}) |
返回 commandID、accepted;无参数也传 {} |
参数遵循实际行为输入模型,不能虚构行为 |
subscribe({topics},callback) |
返回取消订阅函数 | 监听属性、连接和命令;退出页面释放订阅 |
getThingModel()返回运行期SDK模型:properties中每项的identifier、type、mode、available以及存在的min/max/step/unit/mapping直接位于属性对象上,例如读取property.min,不是property.define.min。mode: "r"只读,mode: "rw"可写;仍需结合available和设备连接状态决定是否显示操作。约束缺失不能补成零值或虚构范围。
ZIP中的.data/thing-model.json是平台原生模型,约束位于properties[].define,另有userPerm等导入字段。它用于物模型导入和包结构校验,不能拿来代替宿主实际返回的SDK模型;SDK合同不保证返回userPerm。验证原生模型和真实宿主时应分别按这两种结构检查,不因SDK省略原生导入字段而推断设备能力缺失。
对于设备自动复位的触发字段,可在已确认其协议时使用 writeProperties({values: {clear_event: true}, completion: 'receipt'});宿主收到设备成功回执后立即发布命令结果,不等待属性等于下发值。返回 accepted 仍只是受理;面板必须独立等待设备新上报的复位值和成功回执,不能用回执伪造属性或宣称复位成功。省略 completion 的普通属性保留原有同步规则;无法识别该选项的旧宿主会拒绝请求,不会自动重发,须先升级宿主再发布使用该选项的面板。
命令最终状态为 succeeded、failed 或 timeout;超时表示结果未知,不自动重发。设备已确认但 stateSynchronized: false 时,应提示状态尚未同步并保留实际上报值,后续属性到达后再更新按钮。平台接口可通过request调用,凭据由getAuth运行期读取;不读取父窗口DOM或保存账号令牌。
开关和空调模板右上角的齿轮通过 await window.urPanel.openSettings() 打开宿主的当前设备设置。方法不接受参数,禁止拼接小程序路由、设备身份或任意URL;先读取 const context = await window.urPanel.getContext(),仅当 context.capabilities.settings.available === true 时显示入口。返回 {opened:true} 只表示设置已打开,不代表保存过设备配置。
/packageDevice/pages/settings/index,身份由已校验的当前设备快照提供并编码;离开时销毁旧桥,返回时重建并重新读状态。设备名称、位置、分享、设备版本和系统属性沿用原页面能力及权限。UNSUPPORTED_CAPABILITY,模板隐藏入口;导航失败显示原因并保留原面板。设置与设备控制锁相互独立,设备关机不影响可用的通用设置入口。新控制面板按正式物模型和现有接口开发,不迁移旧小程序路由、字段别名或旧UI行为。定时、日志、分享、支付等属于设备详情通用能力,不放入控制面板。
await urPanel.getAuth() 返回 {baseURL,headers}。headers.token 为当前登录token,同时包含宿主对应的应用、企业和项目请求头;不使用refresh token。每次调用读取实时会话,账号或企业改变后原面板失效。不得写入源码、ZIP、日志、URL或离线缓存。await urPanel.request({path,data}) 调用当前平台已有 POST 接口,path为 /api/v1/ 开头的纯相对路径;不传完整URL、query或自定义认证头。data为对象,结构遵循该接口,不受SDK标量属性声明格式约束。宿主自动携带当前认证上下文,后端执行原有鉴权;它不是设备限定令牌,接口权限与当前登录用户相同。data,不是 {code,msg,data} 外层包装;错误通过Promise拒绝,保留业务code/message。接口调用不自动跟踪设备回执;面板拿到msgToken后自行调用回执查询接口,或使用有现成跟踪的writeProperties/invokeAction。受理不等于执行成功。await urPanel.getAuth() 后用浏览器原生 fetch 请求平台同源接口,设置 credentials: 'omit' 并携带返回的headers;平台按当前用户权限校验。接口可以读取设备历史,也可以控制用户有权控制的设备。仅导入可信面板:脚本可读取当前用户令牌,权限不限于当前设备;关闭面板不会立即使已获取的令牌失效。不透明来源 null 不是面板专有来源,仍需依靠服务端鉴权。模拟宿主不提供真实令牌,不应把模拟当作平台验收。Android/iOS本地面板的原生fetch暂未验收;request仍通过宿主桥调用。原生 fetch 示例(只在可信面板运行时调用,不输出或保存认证头):
const { baseURL, headers } = await window.urPanel.getAuth();
const { productID, deviceName } = await window.urPanel.getContext();
const response = await fetch(`${baseURL}/api/v1/things/device/info/get-one`, {
method: 'POST', credentials: 'omit', headers,
body: JSON.stringify({ productID, deviceName }),
});
const result = await response.json();
if (!response.ok || result.code !== 200) throw new Error(result.msg || '请求失败');
const context = await urPanel.getContext();
// 需要交给自有接口客户端时按调用读取;不要持久化或输出headers。
const auth = await urPanel.getAuth();
const { msgToken } = await urPanel.request({
path: '/api/v1/things/device/interact/property-control-send',
data: {
productID: context.productID,
deviceName: context.deviceName,
data: JSON.stringify({ power: true }), // 改为正式模型的字段和值
isAsync: true,
shadowControl: 1, // 按接口合同选择,不从旧UI猜测
},
});
// 后续查询 /api/v1/things/device/interact/property-control-get-one,
// data传同一productID/deviceName和msgToken;200设备回执才是成功。
复杂索引属性、设备profile与控制策略直接采用对应平台接口的正式参数;不再为每一条接口创建SDK方法,不添加旧面板兼容转换层。requirements仅描述现有语义方法可校验的能力;接口型能力不能伪填为受支持的数组声明,应在开发说明列出实际接口、模型校验与依赖,运行前读取正式模型检查。
新模板以根目录 meta.json 为唯一清单。它包含 formatVersion: 1、name、desc、version、entry: "index.html"、sdkVersion: "1.0.0"、platforms 和 requirements。平台会用导入表单的名称和备注改写同一份清单,导出后与列表保持一致。
platforms 必须明确列出支持终端,只接受 web、h5、appAndroid、appIos、mini,不得重复或为空;声明不替代宿主和真机验证。现有 Web/H5/App 模板声明前四项,不因能加载 HTML 就自动增加小程序。
资料引用字段均为包内相对路径:cover: ".data/cover.png"、thingModel: ".data/thing-model.json"、readme: "README.md"、skills: ["skills/panel-development.md"]。除首次导入自动补齐的封面外,声明的文件必须存在;README/skills 只接受 Markdown,物模型只接受有效 JSON。真实模型还须用物模型校验命令及目标产品核对,不因面板包可导入就认为模型匹配。
.data/ 允许保存截图和配套物模型,README 面向人、skills 面向开发 Agent。CLI init、validate、pack 和网页下载均保留这些资料;不扫描工程其他隐藏目录,不携带 node_modules。它们只作为资料,不自动导入物模型、不执行技能内容,不包含账号、密钥或固定设备身份。
网页首次导入在无凭据、禁止网络的临时模拟框中生成 1200×720 画面,再导出 640×384 PNG 到 .data/cover.png;封面超过1 MiB、损坏或超200万像素会拒绝。已有封面回导保留原图和内容摘要。旧包无图显示“暂无封面”,不借用其他资产图片。导出ZIP包含封面及开发资料。ZIP仍限制10 MiB压缩、30 MiB展开、100文件,不新增导入接口。
新导入和更新仅接受ZIP,根目录必须有meta.json与index.html;拒绝manifest.json及HTML内嵌清单。存量有效面板在服务启动时自动迁移为根meta.json格式,保留面板ID、归属、创建时间及全部绑定版本,重新生成资源清单和内容摘要;新资源全部写成功后才切换资产指针,旧对象保留用于回滚。重复启动跳过已迁移资产,在.data/package-source.json记录原包摘要,避免不同来源的旧包归一后碰撞,不自动合并、删除或解绑;资源损坏或其他校验失败停止启动并记录具体面板ID。日常导入、更新、运行和导出不提供旧格式分支;导出直接返回当前新包,其packageHash与当前资产一致。
后台面板卡片点击“更新”,填写名称和备注并选择新 ZIP,点击“确认更新”。更新保留 panelID 及设备、产品、品类的全部绑定;用户在设备面板点击“刷新”后加载新内容。开发仍在本地完成,没有在线编辑。
CLI 先查询并核对 info --json '{"panelID":"<panel-id>"}' 返回的 data.panel.packageHash,再显式提交原版本摘要:
ur things panel update ./my-panel --panel-id <panel-id> --expected-hash <原版本完整packageHash>
调用 POST /api/v1/things/panel/package/update:panelID 为保持不变的资产身份;expectedPackageHash 为查询时的64字符完整摘要;filePath 为当前登录账号上传的临时ZIP路径;可选 name 非空时同时替换 desc(空值清除备注),省略名称则使用新包名称备注。成功返回 panelID/packageHash/manifest/reused,同内容不改写。权限为 things/panel/info:update,普通企业不能修改公共面板。
任何直接引用(包括其他企业不可列出的绑定)存在时,更新要求 requirements、platforms 的声明及 sdkVersion 与原版本保持一致;需要改变依赖时另行导入并重新核对绑定。无引用可更换依赖。并发摘要不匹配、非法包或另一个面板已使用相同内容均拒绝,原包和绑定保持不变;不要自动读取新摘要重试覆盖。旧资源授权在五分钟有效期内读取原包快照,新解析返回新内容摘要;已打开页面不会强制中断,须刷新加载新版本。回退使用上个版本完整包和当前摘要再次更新。更新不执行物模型导入。