扩展侧:processingExtensionId 通用 AI 处理契约
本节是 ne101_camera 案例的扩展侧契约参考页,覆盖
processingExtensionId通用 AI 处理契约:白名单校验(AI_EXT_IDS)、模式映射(EXT_MODES)、__imageData注入机制和降级策略。
processingExtensionId 通用契约
ne101_camera 组件最容易被误解的一点是:它看起来在做「AI 物体检测」,但翻遍 1972 行 bundle.js,你找不到一行 YOLO 推理、一行 ONNX runtime、一行模型权重加载。
组件本身不做任何 AI。 所有 AI 推理都被外包给了用户通过 processingExtensionId 配置字段指定的扩展。这个字段位于 manifest.json L23-L24 的 default_config 块里:
"processingEnabled": false,
"processingExtensionId": "",
processingEnabled 是总开关(默认 false,开箱即用只是一个纯展示摄像头画面的组件),processingExtensionId 是扩展 ID 槽位(默认空字符串 = 未选中任何扩展 = 不做处理)。
当用户在 AdvancedPanel 里把总开关打开并从下拉框选择了一个扩展(例如 locate-anything-v2)后,组件的 generateTransformJsCode 会把扩展 ID 写进生成的 Transform 代码的 extensions.invoke() 调用里,Transform 在主控沙箱执行时由平台负责把调用路由到对应扩展的 HTTP/RPC 端点。
这个「组件 + 可插拔扩展」契约是 NeoMind 生态 AI 复用的模板:一个组件,N 个推理后端。同一个 ne101_camera 组件,搭配 locate-anything-v2 就是「开放词汇目标检测」,搭配 ocr-device-inference 就是「OCR 文字识别」,搭配 yolo-device-inference 就是「边缘设备端 YOLOv8 推理」。
组件本身不需要知道这些扩展的内部实现,只需要知道如何调用它们、如何归一化它们的响应(见 4.3)。
为什么选择「可插拔扩展」而不是「内置 AI」:如果组件自己打包了一个 YOLO 模型(例如把 onnxruntime-web + yolov8n.weights 嵌进 bundle),会有三个严重后果。
- bundle 体积爆炸——一个量化后的 YOLOv8n 权重就有 12MB,加上 onnxruntime-web 的 WASM 大约 12MB,整个 bundle 从 80KB 暴涨到 25MB 以上,平台加载时间从毫秒级变成秒级。
- 模型选择被锁死——用户想要 OCR 就得换一个「内置 OCR 模型」版本的组件,组件市场的 SKU 数量成倍增长。
- 模型更新和组件更新强耦合——YOLO 模型每迭代一版就要发一个新组件版本,而扩展是独立部署的(由用户或平台运维单独升级),扩展更新不需要触碰组件代码。
「可插拔扩展」把这三个问题全部解开了:组件保持 80KB,用户自己选模型,扩展可以独立更新。
组件零 AI、推理外包给扩展是 NeoMind 组件市场的核心范式。同一个组件搭配不同扩展就能做不同任务(检测 / OCR / 描述),组件保持 80KB 轻量,扩展可独立升级。这就是「一组件多用途」范式的根基。
下图展示了「组件 → processingExtensionId → N 个候选扩展」的扇出关系。组件只暴露一个槽位,由用户的下拉选择决定实际调用哪一个扩展,扩展之间互不感知。
设计决策:可插拔扩展 vs 内置 AI 模型
- 选择:组件零 AI,推理外包给
processingExtensionId指定的扩展(manifest.json L24)。 - 备选方案 A:组件打包自己的 YOLO 模型(onnxruntime-web + 权重文件)。否决理由:bundle 体积从 80KB 暴涨到 25MB+,加载时间从毫秒变秒,且模型选择被锁死——想要 OCR 必须换组件版本。
- 备选方案 B:组件打包多个模型(一个检测 + 一个 OCR),按用户配置在运行时切换。否决理由:体积问题更严重(25MB x 2),且模型之间会有 GPU/WASM 内存竞争。
- 理由:可插拔扩展让组件保持轻量(80KB),模型选择权交给用户(按场景挑扩展),扩展可独立升级(不影响组件版本)。这是 NeoMind 组件市场「一组件多用途」范式的根基。
- 代价:组件无法在「没有安装任何 AI 扩展」的环境下做检测——但这正是
processingExtensionId默认空字符串的语义(纯展示模式)。
AI_EXT_IDS 白名单
平台上有许多扩展(天气、ONVIF 桥接、各种 AI 推理),但 ne101_camera 只关心能消费图像输入并返回检测结果的 AI 扩展。组件用一个硬编码的白名单来过滤,定义在 bundle.js L144:
var AI_EXT_IDS = ['locate-anything-v2', 'image-analyzer-v2', 'yolo-device-inference', 'ocr-device-inference'];
这四个扩展的职责分别是:
locate-anything-v2—— 基于 Grounding DINO 风格的开放词汇目标检测,支持「找一只猫」「找红色汽车」这类自由文本描述(phrase),是四个扩展里模式最多(5 个)、能力最广的一个。image-analyzer-v2—— 服务端 YOLOv8 目标检测,固定类别集(COCO 80 类),不需要 phrase 输入,适合「数人头」「数车辆」这类标准检测场景。yolo-device-inference—— 边缘设备端 YOLOv8 推理,与image-analyzer-v2功能类似但推理发生在 NE101 设备本身(而非服务端),延迟更低、不占服务端 GPU。ocr-device-inference—— PaddleOCR 文字识别,返回文本块及其多边形包围盒,用于「车牌识别」「告示牌文字提取」场景。
这个白名单在 AdvancedPanel 的扩展加载逻辑里被使用,位于 bundle.js L1488-L1491。组件调用 window.neomind.listExtensions() 拿到平台所有已安装扩展的列表,然后用白名单做过滤:
var arr = Array.isArray(exts) ? exts : [];
var filtered = [];
for (var i = 0; i < arr.length; i++) {
if (AI_EXT_IDS.indexOf(arr[i].id) >= 0) filtered.push(arr[i]);
}
过滤后的 filtered 数组才会传给 ExtDropdown 渲染成下拉选项。这意味着即使平台上装了 weather-forecast-v2、onvif-bridge、uink-rms-bridge 等非 AI 扩展,它们也不会出现在 ne101_camera 的扩展选择下拉框里——因为这些扩展无法消费图像输入,选中了也只是 Transform 调用失败。
为什 么用硬编码白名单而不是「显示所有扩展」:用户体验是核心理由。如果下拉框里混入 weather-forecast-v2,用户可能会选中它,然后困惑于「为什么摄像头画面上没有检测框」——天气扩展根本不接受图像参数。白名单把「能用的」和「不能用的」在 UI 层就分开了,避免用户进入「选错了但不知道为什么」的死胡同。
设计决策:硬编码白名单 vs 元数据驱动 vs 显示全部
- 选择:在
bundle.js里硬编码AI_EXT_IDS = ['locate-anything-v2', ...]四元素数组(L144),用indexOf做过滤(L1488-L1491)。 - 备选方案 A:元数据驱动——扩展在自己的 manifest 里声明
"supports_image": true,组件读这个字段做过滤。否决理由:这要求所有扩展作者遵守一个「声明能力」的契约,而 NeoMind 当前的扩展 manifest 没有这个字段。引入它需要平台层面的标准化,短期内不会发生。 - 备选方案 B:显示所有已安装扩展。否决理由:非 AI 扩展(天气、ONVIF 桥)混入下拉框,用户选错后 Transform 调用失败,体验极差,且排错困难(错误可能延迟到运行 Transform 时才暴露)。
- 理由:硬编码白名单是最简单的方案——四个扩展是已知的、稳定的 AI 集合,新增一个 AI 扩展时只需要往数组里加一个字符串。在没有扩展元数据标准的当下,这是务实的选择。