1. 完整示例
插件化手册
  • FH2 插件开发手册
    • 产品简介
    • 典型场景
    • 发版说明
    • 快速开始
      • 快速开始总览
      • 第一个插件
      • 后端插件
      • 全栈闭环
    • AI辅助开发
      • AI 开发 Skill
      • 安装与复验
      • 第一个工程
      • 真实需求示例:现场问题上报
      • 改造已有工程
      • 调试与交付
    • 开发指南
      • 开发指南
      • 基础概念
        • 基础概念总览
        • 什么是插件开发
        • 平台架构
        • 前端宿主、扩展点与能力
        • 后端运行时、入口与权限
        • 开发流程
        • 安全与隔离
        • 支持范围与问题反馈
      • 开发指南
        • 开发指南总览
        • 前端插件
          • 前端插件深度指南
          • 前端工程结构
          • 插件清单(fh2-plugin.json)
          • 入口与生命周期
          • 界面与状态
          • 多宿主插件
          • 跨宿主通信服务
          • HTTP、WebSocket 与事件
          • 存储、Blob 与定时器
          • 工具链与本地调试页面
        • 后端插件
          • 后端开发深度指南
          • 后端插件的结构
          • 后端源清单 fh2-plugin.json
          • HTTP 接口处理函数
          • 消息订阅与事件处理函数
          • ctx 与能力注入
          • 数据存储与缓存
          • 流程编排
          • 触发方式
          • 连接外部系统
          • 流程编排练习(DAG)
        • 前后端协作
          • 前后端协作总览
          • 前后端共享契约
          • 端到端幂等
          • 事件、通知与状态
          • 统一错误模型
          • 链路标识与诊断日志
        • 完整练习
          • 环境准备
          • 第一个前端插件
          • 第一个后端插件
          • 第一个全栈插件
      • 能力总览
        • 能力地图
        • 能力可用性矩阵
        • 前端能力
          • 平台通用能力
            • 平台通用能力总览
            • 基础运行时:HTTP、事件、WebSocket 与存储
            • 导航、位置与通知
            • 弹窗、音频与剪贴板
            • 地图能力
            • 项目设备能力
            • WPMZ 与航线预览
            • 受控对象上传
          • 项目宿主
            • 项目宿主总览
            • 项目上下文
            • 项目侧边栏入口
            • 项目插件路由
            • 项目工作区
          • 消息宿主
            • 消息宿主总览
            • 消息告警弹层
            • 消息导航与通知
          • 计划库宿主
            • 计划库宿主总览
            • 计划库表单上下文
            • 计划库表单扩展
            • 校验、提交与 `biz_ext`
          • 驾驶舱
            • 驾驶舱插件能力总览
            • 驾驶舱扩展点
            • 上下文与遥测
            • 相机、检测与 AI
            • 场景几何与直播指针
            • 飞控、云台与任务
            • 驾驶舱平台事件
        • 后端能力
          • 后端能力地图
          • 后端能力总览
          • 配置、受管出网与集成连接
          • 事件与流程
          • 飞行任务与违停巡逻
          • 数据存储
          • 媒体与前端通知
          • 资源租约
          • 作业、目标轨迹与视频处理
      • 调试与测试
        • 调试与测试总览
        • 快速入门排错
        • 本地开发
        • Web IDE:打开工程与保存改动
        • 工作流编辑器:节点、接线与交付
        • 知识与 MCP:先判断需要哪一层事实
        • 前端本地调试
        • 后端运行时与流程调试
        • 模拟数据与测试数据
        • 集成测试与 E2E
        • 日志与链路追踪
        • 常见问题排查
      • 完整示例
        • 示例与案例总览
        • 项目侧边栏与工作区
        • 消息告警卡片
        • 计划表单任务清单
        • 驾驶舱动作与左侧面板
        • 最小后端插件
        • 最小全栈插件
        • 多宿主插件实战
        • 事故快处
        • 交通抓拍
        • 违停巡逻完整案例
      • 构建发布与安全
        • 构建、发布与安全
        • 质量门禁
        • 行业包构建与打包
        • 能力版本发布
        • 安装、升级与回滚
        • 安全基线
      • API参考
        • API 参考总览
        • 前端
          • 前端 API 参考
          • 前端插件清单结构
          • 前端 SDK API
          • 前端扩展点目录
          • 前端能力目录
          • 前端平台事件目录
          • 跨宿主通信服务 API
          • 工具链命令行参考(`fh2-plugin`)
        • 后端
          • 后端 API 参考
          • 后端插件源清单
          • 后端 HTTP 接口参考
          • 后端事件消费参考
          • 后端脚本上下文(`ctx`)
          • 后端数据存储
          • 流程编排语言
          • 流程触发器参考
          • 后端能力目录
          • 后端 HTTP API
        • 共享契约
          • 共享契约参考
          • 类型与契约目录
          • 错误模型
          • 版本兼容性
      • 附录
        • 错误码
        • 术语表
        • 常见问题
  1. 完整示例

驾驶舱动作与左侧面板

这个示例在驾驶舱工具栏加一个按钮,点击后打开一个带 Vue 界面的左侧面板。它是"工具栏按钮 → 面板贡献 → Vue 界面"的最短闭环,也是大部分驾驶舱插件的起点。
fh2-plugin-sdk 是插件开发 SDK;下文模块名用于说明,实际导入路径保留生成工程配置。模块名不表示已发布的 npm 包名。

这个示例做什么#

工具栏出现"最小示例"按钮。
点击后左侧面板打开,显示图标、标题和一个计数按钮。
按钮本身不拥有界面容器,它调用面板贡献句柄的 reveal() 来展示并聚焦面板。
图标用 ctx.blobs 管理:写一次 Blob,拿受管的 URL 给界面用。

关键代码讲解#

清单:声明按钮、面板和界面#

fh2-plugin.json:
{
  "id": "exp_minimal",
  "name": "最小示例",
  "version": "0.1.0",
  "entry": "./plugin.js",
  "permissions": ["platform:storage"],
  "extensions": [
    { "id": "open-minimal-panel", "point": "cockpit.ui.toolbar-actions" },
    { "id": "minimal-panel", "point": "cockpit.ui.left-panel" }
  ],
  "contributes": {
    "icons": [{ "id": "plugin", "src": "./icons/plugin.svg" }]
  },
  "views": {
    "minimal-panel": { "entry": "./views/minimal-panel.js" }
  }
}
清单里只声明扩展 ID 和扩展点;label / icon / render / onTrigger 这些运行时字段在激活时用 contribute 提交。platform:storage 权限用于 ctx.blobs。

图标文件:public/icons/plugin.svg#

新建图标,清单的 ./icons/plugin.svg 才能被构建找到(界面里的 Blob 图与工具栏图标是两份资源):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2563eb" d="M4 4h16v16H4z"/></svg>

入口:两个贡献,一个句柄#

// src/plugin.ts
import { createViewState, definePlugin } from 'fh2-plugin-sdk'
import type { AppCockpitContext } from './generated/plugin-types'
import { minimalPanelView } from './views/minimal-panel/contract'

export default definePlugin({
  async activate (rawCtx) {
    if (rawCtx.host.id !== 'cockpit')
      return
    const ctx = rawCtx as AppCockpitContext

    // 图标 Blob:写入一次,之后直接取受管 URL
    let logo = await ctx.blobs.get('minimal-logo')
    if (!logo) {
      logo = new Blob([
        '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
        + '<rect width="64" height="64" rx="12" fill="#2563eb"/>'
        + '<text x="32" y="38" text-anchor="middle" font-size="20" fill="white">FH2</text>'
        + '</svg>',
      ], { type: 'image/svg+xml' })
      await ctx.blobs.put('minimal-logo', logo)
    }
    const blobUrl = (await ctx.blobs.url('minimal-logo')) ?? ''

    // 注册界面会话:这里用 handle 的工厂形态——
    // get(ctx, initialState) 传入的初始状态会作为工厂函数的入参,用于按内容创建状态
    await minimalPanelView.handle(ctx, initialState => ({
      state: createViewState(initialState),
      methods: {},
    }))
    // 常规场景(状态固定、不按实例区分)直接用对象形态:
    // await minimalPanelView.handle(ctx, { state: createViewState(...), methods: {...} })
    // 两种形态的说明见「界面与状态」一页。

    // 左侧面板:初始隐藏,等待 reveal()
    const panel = await ctx.extensions.contribute('minimal-panel', {
      visible: false,
      label: '最小示例',
      icon: 'plugin', // 图标 ID 需在清单 contributes.icons 声明
      render: () => minimalPanelView.get(ctx, { blobUrl }),
    })

    // 工具栏按钮:点击打开面板
    await ctx.extensions.contribute('open-minimal-panel', {
      icon: 'plugin', // 图标 ID 需在清单 contributes.icons 声明
      label: '最小示例',
      active: false,
      disabled: false,
      async onTrigger () {
        await panel.reveal()
      },
    })
  },
})
要点:
panel 是贡献句柄,reveal() 同时完成"展示 + 聚焦",不需要自己找容器。
重复点击同一个按钮复用同一份贡献,不会叠出多个面板。
blobUrl 来自受管的 ctx.blobs。不要自己调 URL.createObjectURL():自建对象 URL 不会进入引擎资源台账,卸载后可能泄漏。

界面:契约与挂载#

// src/views/minimal-panel/contract.ts
import { definePluginView } from 'fh2-plugin-sdk'

export interface MinimalPanelState {
  readonly blobUrl: string;
}

export const minimalPanelView = definePluginView<{
  state: MinimalPanelState;
  initialState: MinimalPanelState;
  methods: Record<string, never>;
}>('minimal-panel')
// src/views/minimal-panel/index.ts
import { createApp, reactive } from 'vue'
import { defineView } from 'fh2-plugin-sdk'
import { minimalPanelView } from './contract'
import MinimalPanelView from './MinimalPanelView.vue'

export default defineView({
  mount (ctx) {
    const state = reactive({ blobUrl: '', count: 0 })
    const client = minimalPanelView.client(ctx.port)
    const subscription = client.state.subscribe((next) => {
      state.blobUrl = next.blobUrl
    })
    const app = createApp(MinimalPanelView, { state })
    app.mount(ctx.mountElement)
    return {
      dispose: () => {
        subscription.dispose()
        client.dispose()
        app.unmount()
      },
    }
  },
})
<!-- src/views/minimal-panel/MinimalPanelView.vue -->
<script setup lang="ts">
import { reactive } from 'vue'

const props = defineProps<{ state: { blobUrl: string; count: number } }>()
const local = reactive({ count: 0 }) // 点击计数是界面局部状态,关闭后重置
</script>

<template>
  <div class="minimal-panel">
    <img v-if="props.state.blobUrl" :src="props.state.blobUrl" alt="logo" width="32">
    <h3>最小示例</h3>
    <button @click="local.count++">
      点击了 {{ local.count }} 次
    </button>
  </div>
</template>

文件修改清单#

以下只针对刚创建的新工程,所有路径相对 exp_minimal/。已有业务工程不要照此删除文件:
文件或目录操作
fh2-plugin.json、src/plugin.ts用本页完整内容整文件替换
src/views/minimal-panel/ 中的契约、界面入口和 Vue 组件逐个新建,复制对应内容
src/hosts/cockpit/、src/views/hello-panel/、src/views/issue-panel/、src/views/issue-dialog/删除原模板演示;它们引用旧清单
原 tests/contract.test.ts、tests/issue-activate.test.ts删除原模板测试,由下方本例测试覆盖新功能
tests/example.test.ts新建下面的聚焦测试
依赖、构建配置、tsconfig.json、vitest.config.ts保留原样;生成类型通过 generate 更新,不手改
另新建上方 public/icons/plugin.svg。

聚焦单测:tests/example.test.ts#

新建测试文件。它验证入口方法和状态,不替代浏览器或目标环境验收:
import { describe, expect, it, vi } from 'vitest'
import type { PluginContext } from 'fh2-plugin-sdk'
import plugin from '../src/plugin'

const captured = vi.hoisted(() => ({ factory: undefined as any }))
vi.mock('../src/views/minimal-panel/contract', () => ({ minimalPanelView: {
  handle: async (_ctx: unknown, factory: unknown) => {
    captured.factory = factory
  },
  get: () => ({}),
} }))

describe('驾驶舱面板', () => {
  it('只注册一份面板,用 reveal 展示,并用受管 Blob URL', async () => {
    const reveal = vi.fn().mockResolvedValue(undefined)
    const contributions = new Map<string, any>()
    const contribute = vi.fn(async (id, value) => {
      contributions.set(id, value)
      return { reveal, dispose: vi.fn() }
    })
    const put = vi.fn().mockResolvedValue(undefined)
    await plugin.activate({ host: { id: 'cockpit' }, blobs: { get: vi.fn().mockResolvedValue(undefined), put, url: vi.fn().mockResolvedValue('blob:example') }, extensions: { contribute } } as unknown as PluginContext)
    expect(put).toHaveBeenCalledOnce()
    expect(contribute).toHaveBeenCalledTimes(2)
    await contributions.get('open-minimal-panel').onTrigger()
    expect(reveal).toHaveBeenCalledOnce()
    expect(captured.factory({ blobUrl: 'blob:example' }).state.get()).toEqual({ blobUrl: 'blob:example' })
  })
})

怎么在本地跑起来#

先完成环境准备,再创建工程。保留生成工程的依赖、构建配置和实际 SDK 导入路径,按本页合并清单并修改业务实现;新增文件的 SDK 导入路径沿用模板。本例的按钮、面板、计数与 Blob 预览可在驾驶舱调试页面验证,不需要发送设备命令。
在确认的业务父目录运行创建向导,选择「1=前端插件」,Plugin ID 输入 exp_minimal,其余项按默认值确认并选择立即安装。命令只要求终端能直接运行 fh2-plugin;未注册全局 CLI 时,把它替换为开发者工具目录中 packages/fh2-plugin-toolchain/bin/fh2-plugin.mjs 的实际路径:
dev cockpit 会启动高保真驾驶舱调试页面。验证步骤:
1.
工具栏出现"最小示例"按钮。
2.
点击后面板打开,显示图标、标题和计数按钮;重复点击不会出现第二个面板。
3.
界面里的点击计数是局部状态,关闭再打开面板后归零。
4.
卸载驾驶舱后,诊断面板里界面、通信端口、Blob、订阅等资源计数应全部归零。

常见问题#

现象原因与处理
工具栏没有按钮当前宿主不是 cockpit,或 open-minimal-panel 没提交成功
点击无面板出现面板贡献没提交成功、render 抛错,或 reveal() 句柄失效
图标不显示Blob 写入或 URL 生成失败;检查 platform:storage 权限和诊断面板
重复点击出现多个界面复用了不同的贡献;必须对同一扩展 ID 只 contribute 一次并用它的句柄 reveal
卸载后资源计数非零dispose 漏释放,或代码绕过了 ctx.timers / ctx.blobs 自己管理资源

延伸阅读#

驾驶舱扩展点:工具栏、面板等位置完整说明。
存储与 Blob:ctx.blobs 与定时器管理。
界面与状态:通信通道详解。
多宿主插件:扩展到多个宿主。
修改于 2026-10-22 03:40:00
上一页
计划表单任务清单
下一页
最小后端插件
Built with