HTML 设备面板开发说明

面板管理页通过“接入说明”查看完整引导,也可在“导入面板”弹窗直接下载模板。接入顺序为下载模板 → 本地开发与模拟 → 导入绑定 → 设备实际验证。从“下载模板”选择开关或空调,解压后在本地编辑器或 AI 工具中开发。模板包含完整源码和本说明的离线版 development.html,也可直接导入后台查看效果。面板管理负责导入、模拟、导出、删除和绑定,源码在本地维护。

1. 先确认设备物模型

在产品或设备详情查看真实物模型,核对属性标识符、类型、读写权限、单位、范围和枚举含义。不同产品只要满足同一面板的能力要求,就能共用面板。

2. 文件各自负责什么

文件 用途 开发时怎么改
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。图片、字体和脚本使用包内相对路径。

3. 本地运行、校验和打包

安装支持 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,导入后台模拟。

4. 声明清单和能力合同

面板只接受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高风。必须以实际物模型为准,不能假设所有空调都相同。

5. 通过 SDK 读取状态和控制设备

在 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} 只表示设置已打开,不代表保存过设备配置。

平台接口与运行期认证

新控制面板按正式物模型和现有接口开发,不迁移旧小程序路由、字段别名或旧UI行为。定时、日志、分享、支付等属于设备详情通用能力,不放入控制面板。

原生 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仅描述现有语义方法可校验的能力;接口型能力不能伪填为受支持的数组声明,应在开发说明列出实际接口、模型校验与依赖,运行前读取正式模型检查。

6. 在后台模拟、导入和绑定

  1. 在“面板管理 → 导入面板”中填写名称、备注,拖拽或点击选择 ZIP,再确认导入。平台导入自动归为公共面板,其他企业导入自动归为当前企业,不需要选择归属。名称和备注写入导入清单并参与内容摘要,导出时保持一致。导入成功后进入可选绑定步骤,可以直接绑定设备、产品或品类,也可以点“稍后绑定”;绑定失败不会撤销导入。
  2. 打开“查看 / 模拟”,测试控制、状态、失败反馈和 Web / 手机尺寸。这里使用模拟设备,不能作为真实设备控制验收。
  3. 在导入成功后的可选步骤中选择目标并绑定,设备需先选所属产品;也可稍后到设备、产品或品类详情点击“绑定面板”。优先级是设备 > 产品 > 品类(含最近上级品类);全部未绑定时后台显示空状态。
  4. 从真实设备详情打开设备面板,核对初始状态、下发回执和后续属性上报。只看到页面或 accepted 都不算控制成功。
  5. 修改后更新meta.json的version并重新打包,在原卡片点“更新”导入新ZIP,保留原ID及绑定。需要改变已绑定能力合同则另行导入。列表中的“导出”下载当前完整包,“下载模板”获取开发起点。

7. 交付前检查

包元数据与资料目录

新模板以根目录 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 与原版本保持一致;需要改变依赖时另行导入并重新核对绑定。无引用可更换依赖。并发摘要不匹配、非法包或另一个面板已使用相同内容均拒绝,原包和绑定保持不变;不要自动读取新摘要重试覆盖。旧资源授权在五分钟有效期内读取原包快照,新解析返回新内容摘要;已打开页面不会强制中断,须刷新加载新版本。回退使用上个版本完整包和当前摘要再次更新。更新不执行物模型导入。