跳转到内容

做一个面板

面板是最常见的插件形态:一块 iframe,挂在宿主声明好的位置。本篇从快速开始的静态面板往前走三步:挂载位置、宿主上下文、连接后端。

先分清两个”视图”:manifest 的 views 声明的是UI 挂载位置(抽屉/活动栏/设置页里的面板,与具体某张表无关)——就是本篇。产品里表格顶部那排”视图” tab(网格/看板/甘特…)是表的投影,那个位置也能自定义,但走的是另一个贡献点 tableViews → 见自定义表格视图。

视图声明的 location 决定它出现在哪:

"contributes": {
"views": [
{ "id": "panel", "title": "工具", "icon": "clock",
"location": "right-drawer", "entry": "ui/index.html" },
{ "id": "workbench", "title": "工作台", "icon": "bot",
"location": "activity-bar", "entry": "ui/index.html#/workbench" }
]
}
location出现在适合
right-drawer右侧工具栏图标 → 抽屉面板常驻小工具,与表格并排用
activity-bar左侧活动栏图标 → 占满编辑区的全屏页工作台式独立界面(Agent 工作台就是它)
settings偏好设置页的一个分区复杂配置 UI(简单配置用 settings 贡献的自动表单更省)
fullscreen / floating / bottom-panel全屏 / 浮窗 / 底部面板特定形态需要时

entry 支持 hash 路由(ui/index.html#/workbench)—— 一份 UI 构建产物服务多个视图是内置插件的通行做法。

活动栏全屏页天然有路由深链:打开后 URL 是 /projects/<项目>/page/<插件id>/<视图id>,刷新、分享、浏览器前进后退都由宿主承载,插件不用做任何事。

面板 iframe 启动时并不知道自己在哪个项目里。宿主会通过 postMessage 把上下文推进来,SDK 封装好了:

<div id="app"></div>
<script type="module">
import { installContextBridge, getContext, onContextChange } from '@xinghan/plugin-sdk/ui'
installContextBridge() // 幂等,装一次消息桥
function render() {
const ctx = getContext()
// ctx.projectId 当前项目(打开面板必然有项目)
// ctx.activeNodeId 编辑器当前激活的节点(用户正在看哪张表/哪个文件)
// ctx.activeFileType / ctx.activeNodeName / ctx.locale
document.getElementById('app').textContent = ctx.activeNodeName
? `正在看:${ctx.activeNodeName}(${ctx.activeFileType})`
: '还没有打开任何表或文件'
}
onContextChange(render) // 用户切换表/文件、切语言时宿主会推 update
render()
</script>

不同挂载形态下上下文字段有别(表视图会带 tableNodeId/viewId,文件渲染器会带 nodeId),完整字段见 UI 桥参考。

需要读写数据、调 LLM、存配置时,把插件升级成 node 运行时,UI 与后端之间就是普通 HTTP:

{
"runtime": "node",
"entry": "dist/server.js",
"devEntry": "src/server.ts",
"capabilities": ["dataEngine", "storage"]
}

后端是一个普通 HTTP 服务(监听宿主经环境变量 PORT 指定的端口),宿主把 /plugins/<你的id>/* 反代过来;UI 里用相对路径请求,天然免配置:

// ui 侧:请求会被宿主反代到你的后端进程
const res = await fetch('../api/summary', { method: 'POST' })
// server 侧(任意 node HTTP 框架;内置插件多用 koa)
import { CapabilityClient } from '@xinghan/plugin-sdk/server'
const cap = new CapabilityClient({
baseUrl: process.env.CAPABILITY_BASE_URL,
token: process.env.CAPABILITY_TOKEN,
})
router.post('/api/summary', async (ctx) => {
const { projectId, tableNodeId } = ctx.request.body
const actor = ctx.headers['x-actor'] as string // 宿主注入的用户身份
const fields = await cap.dataEngine.getFields({ tableNodeId }, { actor, projectId })
ctx.body = { ok: true, data: fields }
})

数据读写的完整讲解(查询、批量写入、治理与撤销)见读写表格数据。

  • UI 热更新:视图声明加 devUiUrl: "http://localhost:3001" 指向你的 vite dev server,宿主会反代 iframe 到它;
  • 后端免构建:devEntry 指向 TS 源码,宿主 dev 模式用 tsx 直接跑。
  • 最小静态面板:plugins/marketplace/pomodoro/(一个 right-drawer 视图,capabilities: [])
  • 带后端 + 活动栏工作台:plugins/builtin/agent-anthropic/(views 三种 location 全用上了)