面板是包里的 panel.html,以标签页打开(ctx.ui.openPanel(),或用户在插件库 / ⌘K 里点开)。
它运行在一个沙箱 iframe 里:opaque origin、CSP 禁止一切网络请求与外部资源。面板只能做这些事:
run(action, args):执行本插件的命令(界面来源:写命令不弹确认,不进撤销栈);openCourse(courseId)、openCalendarAt(isoDate):打开应用的课程页 / 日历某一天;notify(message, kind):顶部通知(第三方插件的通知前面会被加上插件名);askUniMind(prompt):带着一句话打开 UniMind 并发送(第三方插件会先显示「插件想让 UniMind 回答:…」由用户点发送)。
读写数据一律走 run():面板和 UniMind 调的是同一批命令,逻辑只写一份。
两种写法
不用框架
import '@uniflow/plugin-sdk/styles.css' // 可选:基础样式 + uf-* 组件类
import { connectPanel, unwrap } from '@uniflow/plugin-sdk/panel'
async function main() {
const panel = await connectPanel()
const render = async () => {
const stats = await unwrap(panel.run('get_stats', { days: 7 }))
document.getElementById('root')!.textContent = panel.t('panel.today', { minutes: stats.todayMinutes })
}
panel.on('data', render) // 本插件的数据变了(UniMind 写的、后台任务写的、别的设备同步来的)
panel.on('locale', render) // 用户切换了界面语言
await render()
}
void main()React
import { mountReactPanel, type PanelProps } from '@uniflow/plugin-sdk/react'
function Panel({ run, t, subscribe, courses, askUniMind }: PanelProps) { … }
void mountReactPanel(Panel)语言、主题、参数、课程列表变化时组件自动重渲染;数据变化不自动重渲染,用 subscribe 自己决定怎么刷新
(一次批量写入会触发很多次 data,合并成一次刷新)。
PanelApi
| 字段 | 说明 |
|---|---|
pluginId、locale |
|
params |
打开面板时带的参数(ctx.ui.openPanel({ deckId }));再次打开会带新的参数(_open 递增) |
run(action, args) |
返回 ExecResult:{ ok: true, result } 或 { ok: false, code, error };unwrap() 把失败变成异常 |
t(key, opts) |
本插件文案 |
subscribe(listener) / on('data' | 'locale' | 'theme' | 'params' | 'courses' | 'change', fn) |
事件 |
courses |
课程列表(只有声明了 context.read:courses 才有内容) |
openCourse、openCalendarAt、notify、askUniMind? |
宿主动作;askUniMind 为 undefined = 宿主没有提供这个动作(应用里的插件面板总会提供),隐藏相关入口 |
theme |
{ scheme: 'light' | 'dark', vars } |
embedded |
面板嵌在设置页里(高度随内容,SDK 自动上报高度) |
样式与主题
宿主把应用的设计令牌写进面板的 :root(--surface-raised、--text-primary、--accent、--danger …),
并在 <html> 上设置 data-theme="light|dark",用户切换主题时实时更新。
- Tailwind:
tailwind.config.cjs里presets: [require('@uniflow/plugin-sdk/tailwind-preset')],panel/styles.css写@tailwind base; @tailwind components; @tailwind utilities;—— 应用的类名(bg-surface-raised、text-content-secondary、bg-brand、ring-line-subtle、animate-fade-in…)全部可用,深浅色自动正确。 - 不用 Tailwind:
import '@uniflow/plugin-sdk/styles.css',直接写var(--accent),或用uf-card、uf-btn uf-btn-primary、uf-input、uf-badge、uf-empty等组件类。
布局
- 标签页里:面板占满标签页,自己负责滚动(根元素
h-screen overflow-y-auto,或.uf-page)。左右留白 32px,顶部 24px。 - 嵌入模式(
embedded):去掉外层留白与固定高度,高度随内容。 - 窄到约 560px 也要能用(用户会把窗口拉窄、分屏)。
沙箱里不能做的事
- 任何网络请求(fetch、XHR、WebSocket、EventSource)、外部
<script>/<img src=https://…>/ 字体 / iframe —— 都被 CSP 拒绝。 图片与字体用data:/blob:(构建时.svg/.png/.woff2自动内联成 data URL)。 window.open、弹窗、alert/confirm/prompt、页面跳转 —— 都被沙箱拦截。用行内确认代替confirm()。<form>的提交事件不会触发(沙箱没有 allow-forms):不要依赖onSubmit/ 回车提交表单,用按钮的onClick与输入框的onKeyDown(Enter)自己处理。- 访问
parent、应用的 localStorage / cookie —— opaque origin,碰不到。面板自己的 localStorage 也不可用:状态放进命令与存储。 eval/new Function。
键盘
面板是独立文档:快捷键挂在 window 上即可,不会影响应用别处。带 ⌘ / Ctrl 的组合键 SDK 会转给应用(⌘K 照常可用);
你自己处理了的组合键请 preventDefault(),就不会再转发。