返回官网

插件开发指南

开发自有插件接入 TrieCode:七类通用通道(cli/http/grpc/mcp/mqtt/internal/backend)、工具链检测、类型化能力、命令 / 视图 / 项目类型声明、manifest 契约与示例。

TrieCode 的插件系统面向第三方开放:任何开发者都可以编写插件,把自定义工具链接入 AI 智能体与 UI,无需修改 TrieCode 本体。插件通过 plugin.json 声明工具、类型化能力、命令、视图与项目类型,宿主统一分发渲染——UI 与 AI 同源获得插件能力。

插件结构

一个插件就是一个目录(安装后位于 %APPDATA%\TrieCode\plugins\{id}\)。目录按需存在,只有 plugin.json 是必填的:

my-toolchain/
├── plugin.json          # manifest(必填)
├── ui/                  # Web 视图页面(views.webview 的 entry,可选)
├── tools/               # cli 插件的可执行脚本 / 二进制(可选)
├── proto/               # gRPC 插件自带的 .proto(可选,复用宿主服务则不需要)
└── assets/              # 插件自带资源(如 lib/ 库文件;可选)

说明:

  • 运行时宿主还会创建 bin/dependencies 下载的二进制),不属于插件打包内容
  • 官方 arduino-cli-toolchain 插件只有 plugin.json + ui/:能力复用宿主 arduinoCli 服务注册表(无需自带 proto),arduino-cli 二进制由 dependencies 下载
  • browser-toolchain(MCP)只有 plugin.jsonespressif-idf-toolchain(ESP-IDF 工具链)为 plugin.json + ui/(status/config 两个 webview);openscad-toolchainplugin.json + lib/(内置 BOSL2 库)

plugin.json 最小结构:

{
  "id": "my-toolchain",          // kebab-case,全局唯一
  "name": "我的工具链",
  "version": "1.0.0",
  "developer": "你的名字",
  "icon": "🔧",
  "capabilities": {
    "compile": { "transport": "internal", "service": "arduinoCli", "method": "compile", "output": "avr-gcc" },
    "board-manage": { "transport": "internal", "service": "arduinoCli", "method": "board.search" }
  }
}

安装时宿主 validateManifest 校验结构:能力 keys、工具 transport、命令 kind/placement、项目类型 scaffold 路径安全(拒绝绝对路径 / .. 穿越)、视图 entry 等,非法 manifest 拒绝安装。

插件图标

plugin.jsonicon 字段声明插件图标,出现在活动栏、插件管理列表、市场卡片、新建项目页。支持三种形式:

形式说明示例
emoji直接写 emoji 字符"icon": "🔧"
lucide 图标名宿主渲染层映射 lucide 线条图标(小写 kebab)"icon": "box"
品牌标识品牌 LOGO:在宿主 packages/renderer/src/components/plugin/plugin-icon.tsx 注册 React 组件,icon 写标识字符串"icon": "wokwi"WokwiLogo"icon": "openscad"OpenscadLogo

品牌 LOGO 做法(官方插件 wokwi / esp-idf / easyeda / openscad 均如此):在宿主渲染层新建 {name}-logo.tsx 组件(SVG 内联用 currentColor 适配主题,位图 PNG 用 <img> import),在 plugin-icon.tsx 的标识分发里注册(如 if (s === 'openscad') return <OpenscadLogo size={size} />)。改品牌图标时按以下 6 处同步(迭代操作.md 有核对清单):

  1. 插件 manifest icon 标识串
  2. 市场元数据 backend/routes/plugins.jsPLUGIN_CATALOG 该插件 icon(与 manifest 必须一致)
  3. 官网插件广场 website/src/pages/plugins.astro 图标分支(标识 → SVG/图片)
  4. 官网首页 website/src/components/home/Plugins.astro 插件数据 icon(图片可用 /xxx.pngpublic/
  5. 宿主渲染层 plugin-icon.tsx 标识分发
  6. 本地已安装插件 registry(改图标后需「更新/重装」才刷新)

⚠️ 图标标识字符串在软件端、市场、官网三处必须一致,改一处漏一处就会”端上还是老图标”(Wokwi 曾踩过)。

七类通用通道

每个工具通过 transport 声明走哪类通道。所有通道都是宿主通用驱动,插件只需声明,无需宿主预注册。

transport含义
cli一次性命令行(spawn 数组参数防注入)
httpREST API(全局 fetch,尊重代理)
grpc插件自带 .proto,宿主动态加载连接插件 daemon
mcp调用插件声明的 MCP 服务器工具
mqtt通用 MQTT 设备通道(连接 broker / 订阅 / 发布 / 请求-响应)
internal宿主标准能力(串口 / 日志 / 状态 / arduino / toolchain)
backend调用插件自声明的 stdio 后端(JSON-RPC over stdin/stdout)

1. cli — 命令行工具(最常用)

一次性命令行工具。宿主用 spawn(数组参数,不经 shell,防注入)执行:

{
  "name": "flash",
  "description": "烧录固件到设备",
  "parameters": {
    "type": "object",
    "properties": {
      "hexPath": { "type": "string", "description": "固件文件路径" }
    },
    "required": ["hexPath"]
  },
  "transport": "cli",
  "category": "execute",
  "cli": {
    "command": ["esptool.py", "--port", "{port}", "write_flash", "0x0", "{hexPath}"],
    "parse": { "type": "json", "pick": "result" },
    "successCodes": [0],
    "timeoutMs": 30000
  }
}
  • command[0] 为可执行文件(插件依赖目录 / PATH / 绝对路径),其余为参数模板
  • {参数名} 占位符会被工具参数替换(值永远作为单个 argv,天然防 shell 注入)
  • {defaultPort} / {defaultFQBN} 特殊占位符 = 当前选中的串口 / 开发板;工具链插件还有 {settings.toolchain.active.*}(如 pythonExe / path / toolsPath / pythonEnvPath / exportPath)、{projectPath}{env.PATH} 等占位符
  • 必须声明 timeoutMs:cli 驱动默认 30s,长编译 / 烧录会误判超时(ESP-IDF 编译/烧录 600000、set-target 300000、--list-targets 60000)。能力绑定(capabilities)里的 cli 命令同样要声明
  • stream: true + streamChannel(如 compile:onOutput):逐行实时输出到编译面板;pty: true 在真终端打开交互命令(如 idf.py menuconfig);pauseSerialPort: true 烧录前暂停同端口后台串口日志

2. http — REST API

宿主用全局 fetch 执行(尊重网络代理设置):

{
  "name": "query_sensor",
  "description": "查询传感器数据",
  "transport": "http",
  "category": "query",
  "http": {
    "url": "https://api.example.com/sensors/{id}/data",
    "method": "GET",
    "query": { "key": "{apiKey}" },
    "headers": { "Authorization": "Bearer {token}" },
    "parse": { "type": "json", "pick": "data.value" },
    "timeoutMs": 10000
  }
}

3. grpc — 动态 proto(自带 .proto)

插件自带 .proto 文件,宿主动态加载并连接插件声明的后端 daemon

{
  "name": "compile",
  "description": "编译工程",
  "transport": "grpc",
  "category": "execute",
  "grpc": {
    "service": "mypkg.ToolService",
    "method": "Compile",
    "proto": "proto/tool.proto",
    "backendId": "default"
  },
  "backends": [
    {
      "type": "grpc",
      "command": "mytool-daemon",
      "port": 0,
      "args": ["daemon", "--port", "0"],
      "readiness": { "kind": "grpc", "timeoutMs": 20000 }
    }
  ]
}
  • service 用 proto 内全限定名(pkg.Service),method 为 RPC 方法名
  • proto 为插件目录内 .proto 相对路径;宿主只连接该插件自己声明的 backend 端口(安全边界)
  • 支持 unary 与 server-streaming;参数 camelCase 自动映射 proto 的 snake_case 字段
  • 需要下载的二进制在 dependencies 声明(如 arduino-cli 的做法)

4. mcp — 调用 MCP 服务器工具

插件声明 mcpServers + 用 mcp transport 认领其中工具:

{
  "mcpServers": [
    { "name": "browser", "command": "npx", "args": ["-y", "@playwright/mcp"], "autoApprove": false }
  ],
  "tools": [
    {
      "name": "browser_navigate",
      "description": "打开网页",
      "transport": "mcp",
      "category": "query",
      "mcp": { "server": "browser", "tool": "navigate" }
    }
  ]
}

5. internal — 宿主标准能力

宿主内置的标准能力(串口枚举 / 串口日志 / 本地状态 / Arduino 工具链),任何插件声明即可复用:

{
  "name": "list_ports",
  "description": "列出串口",
  "transport": "internal",
  "category": "query",
  "internal": { "service": "serialport", "method": "list" }
}

可用 internal 服务:serialport(串口列表/选择)、serialLog(后台串口日志)、state(板卡选项)、arduinoCli(Arduino 编译/烧录/库/板卡,仅嵌入式场景)、toolchain(工具链检测状态,如 ESP-IDF 的 get_status)。

6. backend — 调用插件自声明的 stdio 后端

backend transport 调用插件 backends 里声明的 stdio 长驻后端(JSON-RPC over stdin/stdout,如 ESP-IDF 的 idf.py confserver),适合「有状态」的工具:

{
  "name": "config_get",
  "description": "读取 Kconfig 符号当前值",
  "transport": "backend",
  "category": "query",
  "backend": { "backendId": "confserver", "method": "get_values", "timeoutMs": 20000 }
}

7. mqtt — 通用 MQTT 设备通道

mqtt transport 连接支持 MQTT 协议的设备(3D 打印机、传感器、ESP32 等),由宿主 mqtt-manager 统一管理连接 / 缓冲 / 安全。先在 mqttServers 声明连接,再让工具 transport: "mqtt" 引用它:

{
  "mqttServers": [
    {
      "name": "printer",
      "url": "mqtts://{settings.printer.ip}:8883",
      "username": "bblp",
      "password": "{settings.printer.accessCode}",
      "tls": { "rejectUnauthorized": false },
      "topics": ["device/{settings.printer.serial}/report"],
      "snapshotTopics": ["device/{settings.printer.serial}/report"],
      "allowedPublishTopics": ["device/{settings.printer.serial}/request"]
    }
  ],
  "tools": [
    {
      "name": "query_state",
      "description": "读取设备最新状态",
      "transport": "mqtt",
      "category": "query",
      "mqtt": { "server": "printer", "op": "read_state", "topic": "device/{settings.printer.serial}/report", "format": "json" }
    },
    {
      "name": "send_command",
      "description": "向设备发命令并等待响应",
      "transport": "mqtt",
      "category": "modify",
      "mqtt": {
        "server": "printer", "op": "request", "topic": "device/{settings.printer.serial}/request",
        "payloadArg": "command", "sequenceField": "sequence_id",
        "responseTopic": "device/{settings.printer.serial}/report", "responseMatchPath": "sequence_id",
        "responseMatchCommandPath": "command", "responseTimeoutMs": 8000, "format": "json"
      }
    }
  ]
}
  • op 五种publish(发命令)、request(发命令+等响应,sequence_id 双匹配)、collect(读消息/等事件)、read_state(最新快照)、status(连接状态)

  • 安全:非回环 broker 强制 TLS;LAN/回环明文放行(状态带警告);allowedPublishTopics 发布白名单 fail-closed;密码/访问码用 {settings.*} 占位符存插件设置(建议 type: "password"),永不进日志

  • 凭据:broker 地址 / 密码经 {settings.*} 从插件设置解析(见「设置」章节)

  • 设计文档docs/history/plan-mqtt-channel.md(连接声明 / 工具语义 / 缓冲 / 多连接隔离 / 安全清单)

  • 对应 backends 声明 type: "stdio" + rpc"jsonrpc"(缺省,标准 {id,result} 按 id 匹配)或 "kconfig"(ESP-IDF kconfserver 适配:启动横幅=整棵树、请求带顶层 version、响应无 id 按顺序匹配、方法为 set/save/load 字段)

  • method 为 JSON-RPC method,params = 工具 args

  • stdio 后端由宿主 stdioBridge 惰性管理(项目切换停旧起新、单活动实例/每插件),不进 BackendProcessManager;插件 webview 可经 backend:invoke IPC 调自己插件的 stdio 后端(插件作用域)

能力声明(UI 主按钮)

UI 主按钮(编译 / 上传 / 板卡 / 库管理 / 平台安装)不再固定绑定 Arduino 服务,而是按主动工具链路由到启用插件的类型化能力绑定。能力 id 枚举:

能力含义
compile编译(绑定可声明 output,如 avr-gcc,宿主据此显示编译仪表)
upload烧录
flash刷写
debug调试
board-manage板卡管理
board-select选择开发板
library-manage库管理
platform-install平台安装
{
  "capabilities": {
    "compile": { "transport": "internal", "service": "arduinoCli", "method": "compile", "output": "avr-gcc" },
    "upload": { "transport": "internal", "service": "arduinoCli", "method": "upload" }
  }
}

每个能力绑定一个工具后端调用(transport + service + method),UI 编译/上传按钮与 AI 工具经同一条能力路由执行。旧自由文本数组(["tools","skills"])兼容:宿主自动从绑定推导能力 id。

命令 / 视图 / 项目类型(UI 入口)

插件除工具外,还可声明三种 UI 入口:

命令(commands

{
  "id": "open-about",
  "title": "工具链信息",
  "icon": "🧛",
  "kind": "open-view",
  "view": "about",
  "placement": "sidebar.board.header"
}
  • kindaction(触发工具调用,需 transport 绑定)或 open-view(打开插件视图)
  • placementplugin.panel(插件聚合面板,插件按钮的唯一去处)/ sidebar.*.header(侧栏面板头部)/ panel.header / context.menu(右键菜单)/ device-panel.action(设备面板动作);同一槽位多个命令聚合为「更多 ▾」溢出菜单
    • ⚠️ 顶部工具栏不接受插件按钮(那是软件核心功能区),toolbar.action 已移除——manifest 里写它会被启动校验拒绝
  • when 最小 DSL:projectType=arduinoactiveToolchain!=x,支持 && / || / !;key 白名单(projectType / activeToolchain),未知 key 一律隐藏按钮(fail-safe)
  • 命令 id 由宿主加插件前缀:{pluginId}.{commandId},UI 经 invokeCommand 触发

项目类型(projectTypes

{
  "id": "my-firmware",
  "title": "我的固件项目",
  "icon": "🔧",
  "scaffold": [
    { "file": "main.cpp", "content": "// {name} 项目骨架" }
  ]
}

新建项目页展示插件贡献的项目类型卡片,选中后按 scaffold 模板生成骨架({name} 占位符替换为项目名,宿主做路径安全清洗)。

视图(views,Web 视图)

{
  "id": "about",
  "title": "工具链信息",
  "icon": "🧛",
  "slot": "sidebar",
  "type": "webview",
  "entry": "ui/index.html"
}
  • slotsidebar / activitybar / panel;活动栏/侧栏项图标取 icon(缺省回退)
  • 加载插件打包的 HTML(位于 ui/ 目录),走 triecode-plugin://{pluginId}/ui/... 协议——仅服务 active 插件、path 约束 + MIME 白名单
  • iframe 沙箱无 same-origin;与宿主通信走 postMessage(宿主校验消息来源与 schema)

插件左侧栏 UI 范式

插件在左侧栏呈现时遵循两段式布局(宿主已统一):

┌──────────────────────────────┐
│ [设备] [视图A] [视图B] [设置] │ ← 顶部选项卡(宿主按 views/devicePanel/settings 自动生成)
├──────────────────────────────┤
│ 具体内容(webview / 设备面板 / 设置面板) │
└──────────────────────────────┘
  • 顶部选项卡由宿主 PluginAggregatePanel 自动生成:views 的每个 webview 视图 + 设备面板(若声明 devicePanel)+ 设置面板(若声明 settings)。
  • 内容区:webview 视图渲染插件自带的 ui/*.html;设备面板 / 设置面板由宿主渲染。
  • ⚠️ 不要在 webview 页面内再加大标题 / 说明角标——宿主选项卡已是标题,页面内直接从内容开始(body padding 即可)。多余的标题 / 「?」会与宿主两层结构重复。

拖拽适配(必须)

左侧面板宽度可拖拽(200–480px),webview 内容必须响应式——任何固定宽度 / 不可收缩的元素都会在窄面板下变形(溢出成 iframe 水平滚动条、元素挤压消失)。推荐骨架(参考官方 esp-idf ui/config.html):

<style>
  body { padding: 12px; overflow-x: hidden; }       /* 残余溢出绝不变成水平滚动条 */
  /* 工具栏:输入框先收缩让位,按钮不压缩不换行 */
  .toolbar { display: flex; gap: 6px; align-items: center; min-width: 0; }
  .toolbar input { flex: 1 1 auto; min-width: 0; }  /* ⚠️ 输入框必须 min-width:0 */
  .toolbar button { flex-shrink: 0; white-space: nowrap; }
  /* 行布局:窄面板下 label/值换行,不挤压 */
  .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
  .row .val { flex: 1 1 auto; min-width: 0; word-break: break-all; }
  /* 控件固定宽 → 自适应 min(100%, x) */
  .ctrl { width: min(100%, 150px); }
</style>

自查清单:

  • 输入框 / 搜索框flex: 1 1 auto; min-width: 0(缺 min-width:0 会在 ~250px 以下溢出成水平滚动)
  • 按钮flex-shrink: 0; white-space: nowrap(不被压缩换行)
  • 行 / 表单flex-wrap: wrap(窄面板换行而非挤压)
  • 控件固定宽:改用 width: min(100%, x)
  • 长文本(路径 / URL / 报错):word-break: break-all / overflow-wrap: anywhere
  • body { overflow-x: hidden } 兜底,任何残余溢出不变成 iframe 滚动条

设备面板(devicePanel

插件面板的「设备」选项卡是数据驱动的通用设备壳:插件声明设备面板(选择器 + 配置选项 + 动作 + 可选 webview),宿主渲染通用控件——加新硬件不改宿主,通用元模型表达不了的形态用 detailView(webview)兜底。

{
  "devicePanel": {
    "id": "arduino-board",
    "title": "Arduino 设备",
    "selectors": [
      { "id": "board", "title": "开发板",
        "source": { "transport": "internal", "service": "arduinoCli", "method": "board.search" },
        "searchable": true, "key": "fqbn", "placeholder": "搜索开发板…" },
      { "id": "port", "title": "端口",
        "source": { "transport": "internal", "service": "serialport", "method": "list" },
        "key": "port" }
    ],
    "optionLoader": { "transport": "internal", "service": "arduinoCli", "method": "board.details" },
    "optionSet": { "transport": "internal", "service": "state", "method": "setBoardOption" },
    "actions": [
      { "id": "open-monitor", "title": "串口监视器", "kind": "action",
        "placement": "device-panel.action",
        "transport": "internal", "internal": { "service": "serialLog", "method": "open" } }
    ],
    "detailView": { "id": "dv", "title": "设备详情", "slot": "sidebar", "type": "webview", "entry": "ui/detail.html" }
  }
}
  • selectors:设备维度选择器。source 复用工具后端绑定(列表源推荐 internal 结构化 adapter——raw:true 返回 [{value,label}];cli/grpc/http 按行回退);searchable 开启搜索式下拉;选中结果按 key 写入设备上下文(约定 fqbn / board / port);onSelect:选中即自动执行设备面板动作(如 ESP-IDF target → set_target),省去「选完再点应用」一步
  • optionLoader / optionSet:设备配置选项的读取 / 写入(如 Arduino 的 CPU 频率);选项变更走 optionSet 写宿主状态,AI 与 UI 选项同源
  • actions:设备动作(复用命令机制,placement 限定 device-panel.action),触发时自动携带设备上下文(port / fqbn / device);hidden: true 隐藏按钮但保留命令(供 onSelect 自动触发 / AI 工具复用)
  • detailView:设备面板底部内嵌 webview 区块,与宿主双向通信(见下)

宿主按 selectors 的 keyfqbn/board/port)派生编译/上传前置门控 needsBoard / needsPort:Arduino 有 fqbn+port 选择器 → 都要先选;ESP-IDF 只有 target+port → 只需端口。

宿主维护「设备上下文」= 各选择器选中项 + 配置选项值;编译 / 上传等能力按钮从它组装参数(兼容 Arduino 的 fqbn / port)。

webview 双向通信

插件 webview(含 detailView 与活动栏视图)通过 postMessage 与宿主双向协作:

  • 加载后发 { type: 'ready' } → 宿主回 { type: 'host.ready' }
  • host.invoke{ type, id, method, args } 调用宿主白名单能力capability:run / command:invoke / devicePanel:* / toolchain:resolve / core:* 等),宿主回 { type: 'host.invoke.result', id, ok, data }
  • state.subscribe:订阅设备上下文,宿主在选中板 / 端口 / 选项变化时推 { type: 'state.push', key: 'deviceContext', value }
  • events:视图声明 events(如 ['core:installProgress'])接收宿主事件推送(平台安装进度等),以 state.push 到达

安全:iframe sandbox 无 same-origin + 消息源校验 + host.invoke 方法白名单 + 协议层 path / MIME 约束 + 仅服务 active 插件。

依赖与后端

  • 依赖下载dependencies 数组声明(URL 或本地路径,支持 zip 解压),安装时自动下载到 plugins/bin/
  • 长驻后端backends 声明随插件启停的服务进程(grpc / process / http / stdio 四型)。grpc/process/http 由宿主 BackendProcessManager 管理生命周期(自动启停、崩溃重启、端口分配);stdio 由 stdioBridge 惰性管理(rpc: 'jsonrpc' | 'kconfig'),见「七类通用通道 → backend」

工具链检测与一键安装(G7)

工具链插件可声明 detection(宿主通用检测引擎)与 installer(一键下载运行官方安装器)。检测结果存 pluginSettings.<pluginId>.toolchain,供 {settings.toolchain.active.*} 占位符使用:

{
  "detection": {
    "envVars": ["IDF_PATH"],
    "installerJson": {
      "paths": ["C:/Espressif/idf-env.json", "D:/Espressif/idf-env.json"],
      "selectedKey": "idfSelectedId",
      "entriesKey": "idfInstalled",
      "pathField": "path",
      "pythonField": "python",
      "toolsPathField": "espressif.idfToolsPath"
    },
    "pathGlobs": ["<toolsPath>/frameworks/esp-idf-v*", "C:/Espressif/frameworks/esp-idf-v*"],
    "validateFile": "tools/idf.py",
    "derive": {
      "toolsPath": "parent(frameworks)",
      "pythonEnv": "glob(python_env/idf*_py*_env)",
      "exportPathFrom": "tools/tools.json"
    }
  },
  "installer": {
    "url": "https://dl.espressif.com/dl/esp-idf/idf-installer.exe",
    "fileName": "idf-installer.exe"
  }
}
  • 检测源按序:手动路径 → 环境变量(envVars)→ 官方安装器 JSON(installerJson)→ 路径 glob(pathGlobs,支持 $USERPROFILE / ~ / <toolsPath> 展开)→ 校验文件(validateFile
  • derive 派生:toolsPath(如 parent(frameworks))、pythonEnvglob(...) 找 venv)、exportPathFrom(工具链自己的 tools.json 注册表 → 解析各工具 bin 目录合成 export PATH,复刻官方 export 脚本、<5ms 而无需跑慢命令;cli 命令 env.PATH 用 {settings.toolchain.active.exportPath}
  • installer:宿主下载并运行官方安装器(文件名白名单 [a-z0-9-]+.exe,如 idf-installer.exe

还可声明 projectDetection(文件标记 → 项目类型,声明式项目推断):如 CMakeLists.txt 含 project.cmakeesp-idf;配合 projectTypes 提供脚手架({name} 占位符 + 路径安全清洗)。

打包与发布

插件打包为 zip 上传到市场。zip 内可直接是 plugin.json 或包在一个子目录里。安装方式:左侧活动栏「插件」→「官方市场」一键安装,或「设置 → 集成 → 插件 → 从文件夹安装 / 从 ZIP 安装」。

安全提示:插件拥有执行命令的能力,请只从可信来源安装。TrieCode 对工具的参数注入有防护(cli 用数组参数、grpc 只连自身 backend),但插件作者声明的内容本身是受信任的。