插件开发指南
开发自有插件接入 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.json;espressif-idf-toolchain(ESP-IDF 工具链)为plugin.json+ui/(status/config 两个 webview);openscad-toolchain 为plugin.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.json 的 icon 字段声明插件图标,出现在活动栏、插件管理列表、市场卡片、新建项目页。支持三种形式:
| 形式 | 说明 | 示例 |
|---|---|---|
| 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 有核对清单):
- 插件 manifest
icon标识串 - 市场元数据
backend/routes/plugins.js的PLUGIN_CATALOG该插件icon(与 manifest 必须一致) - 官网插件广场
website/src/pages/plugins.astro图标分支(标识 → SVG/图片) - 官网首页
website/src/components/home/Plugins.astro插件数据icon(图片可用/xxx.png走public/) - 宿主渲染层
plugin-icon.tsx标识分发 - 本地已安装插件 registry(改图标后需「更新/重装」才刷新)
⚠️ 图标标识字符串在软件端、市场、官网三处必须一致,改一处漏一处就会”端上还是老图标”(Wokwi 曾踩过)。
七类通用通道
每个工具通过 transport 声明走哪类通道。所有通道都是宿主通用驱动,插件只需声明,无需宿主预注册。
| transport | 含义 |
|---|---|
cli | 一次性命令行(spawn 数组参数防注入) |
http | REST 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-target300000、--list-targets60000)。能力绑定(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:invokeIPC 调自己插件的 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"
}
kind:action(触发工具调用,需 transport 绑定)或open-view(打开插件视图)placement:plugin.panel(插件聚合面板,插件按钮的唯一去处)/sidebar.*.header(侧栏面板头部)/panel.header/context.menu(右键菜单)/device-panel.action(设备面板动作);同一槽位多个命令聚合为「更多 ▾」溢出菜单- ⚠️ 顶部工具栏不接受插件按钮(那是软件核心功能区),
toolbar.action已移除——manifest 里写它会被启动校验拒绝
- ⚠️ 顶部工具栏不接受插件按钮(那是软件核心功能区),
when最小 DSL:projectType=arduino、activeToolchain!=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"
}
slot:sidebar/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 的 key(fqbn/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))、pythonEnv(glob(...)找 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.cmake → esp-idf;配合 projectTypes 提供脚手架({name} 占位符 + 路径安全清洗)。
打包与发布
插件打包为 zip 上传到市场。zip 内可直接是 plugin.json 或包在一个子目录里。安装方式:左侧活动栏「插件」→「官方市场」一键安装,或「设置 → 集成 → 插件 → 从文件夹安装 / 从 ZIP 安装」。
安全提示:插件拥有执行命令的能力,请只从可信来源安装。TrieCode 对工具的参数注入有防护(cli 用数组参数、grpc 只连自身 backend),但插件作者声明的内容本身是受信任的。