快速上手
这一章是一条完整的开发流水线:环境 → 创建工程 → 运行调试 → 调用能力 → 打包 → 发布。跟着做,20 分钟跑起你的第一个插件。
第 1 步:准备环境
| 依赖 | 要求 | 用途 |
|---|---|---|
| UDhold 桌面端 | 最新版,下载安装 | 调试容器与打包发布都在宿主内完成 |
| Node.js | 20 LTS 或更高 | 工程型插件的构建工具链 |
| pnpm | 9+(npm i -g pnpm) | 官方插件工程的包管理器 |
| UDhold 账号 | 已登录 | 向导建项目、发布市场需要 |
装好宿主后,呼出主搜索框(默认 Alt+Z)输入「插件开发者」回车——这是 UDhold 内置的插件开发工作台,接下来的创建、调试、发布都在它里面完成。
安装 Node.js 和 npm
npm 不用单独装——它随 Node.js 安装包一起分发,装好 Node.js 就有了 npm。步骤:
- 打开 Node.js 官网下载页:https://nodejs.org/zh-cn/download
- 选择 LTS 版本(长期支持版,标绿的按钮),按操作系统下载安装包:
下载 *.msi 安装包(如 node-v22.x-x64.msi),双击安装:
- 一路「下一步」,默认会勾选 Add to PATH(自动配置环境变量)和 npm components,保持默认即可
- 「Tools for Native Modules」页面可以不勾(插件开发用不到原生编译工具链)
- 装完重开一个终端窗口(让 PATH 生效)
两种方式任选:
- 官网安装包:下载
*.pkg双击,按提示安装 - Homebrew(已装 brew 的话更省事):
brew install node@22
推荐用 nvm 管理版本(免 sudo、可多版本切换):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
# 重开终端后:
nvm install --lts
- 验证安装——打开终端(Windows 用 PowerShell),能打印出版本号即成功:
node -v # v22.x.x 这样的输出(≥ v20 即满足要求)
npm -v # 10.x 这样的输出
npm config set registry https://registry.npmmirror.com
安装 pnpm
工程型插件(见下面「方式三」)用 pnpm 管理依赖,任选一种方式安装:
npm install -g pnpm
Node.js 自带 corepack(v16.13+),启用即得 pnpm:
corepack enable pnpm
brew install pnpm
验证:
pnpm -v # 9.x 或更高
第 2 步:创建工程
三种方式任选:CLI 脚手架一键创建最快;向导创建适合快速起步和纯服务/静态插件;手动搭建适合理解每个文件、向官方插件仓库贡献插件。
方式一:CLI 脚手架一键创建(推荐)
装好 Node.js 后,用官方脚手架交互式生成完整插件框架:
pnpm add -g @udhold/plugin-cli
udhold create my-plugin
npm install -g @udhold/plugin-cli
udhold create my-plugin
按提示完成四步:项目名 → 选模板(Vue 3 + 设计系统(推荐)/ React + Vite / 免构建静态 HTML)→ 填插件信息(ID、标题、描述、作者)→ 自动生成工程。生成后进入工程目录 pnpm install && pnpm dev(静态模板免安装),直接跳到第 3 步调试;dev 端口等细节在生成工程的 plugin.json 与 vite.config 中调整。
@udhold/udhold-ui 等依赖来自 npm)。想向官方插件仓库贡献插件,把生成目录挪进 udhold-plugin/plugins/ 下并把依赖改成 workspace:* 即可。方式二:向导创建(推荐起步)
- 在「插件开发者」的开发页点「新建项目」。
- 填写插件标题、描述、简介,选择工程存放目录。
- 选择插件形态,向导会生成对应的最小工程:
形态 生成内容 静态 HTML index.html+plugin.json,免构建、零依赖页面 + Node 服务 页面 + server.js后端(声明式services)页面 + Python 服务 页面 + Python 后端 纯 Node 服务 只有后端,无界面(纯服务插件) 纯 Python 服务 同上 - 需要登录:向导会先在市场登记插件条目(拿到全局唯一的插件
name),再落盘工程文件。 - 完成后工程自动出现在工作台列表里,点「运行调试」即可看到插件跑起来。
方式三:手动搭建(Vue 3 + 官方设计系统)
官方插件(计算器、JSON Tools……)都在官方插件工程仓库里开发:仓库内置 @udhold/udhold-ui 组件库与 @udhold/udhold-api-types 类型包(见开发者资源),把你的插件放在 udhold-plugin/plugins/ 下即可直接以 workspace:* 引用它们。
2.1 目录结构
在 udhold-plugin/plugins/ 下新建 my-plugin/:
my-plugin/
├── src/
│ ├── main.ts # 入口
│ ├── App.vue # 根组件
│ └── ... # 你的页面与组件
├── dist/ # 构建产物(pnpm build 生成)
├── index.html # Vite HTML 模板
├── logo.png # 插件图标
├── plugin.json # 插件清单(放插件根目录,与官方插件一致)
├── package.json
├── tsconfig.json
├── uno.config.ts
└── vite.config.ts
plugin.json 和 logo.png 放在插件根目录,不要放进 public/。打包时打包器会把 plugin.json 原文和 logo 固定收进安装包根目录,不依赖构建产物;放进 public/ 只会让构建在 dist 里多出一份冗余副本。2.2 plugin.json
{
"name": "my-plugin",
"title": "我的插件",
"description": "一句话描述插件做什么",
"author": "你的名字",
"version": "1.0.0",
"logo": "logo.png",
"loading": { "variant": "Drive", "label": "加载中" },
"permissions": [],
"pluginSetting": {
"slot": "workspace",
"Detach": true,
"Menu": true,
"DesktopShortcut": true
},
"development": { "main": "http://localhost:5261" },
"features": [
{
"code": "main",
"explain": "我的插件主功能",
"icon": "logo.png",
"cmds": ["我的插件", "mypl"]
}
],
"dist": "dist/index.html",
"build": ["dist"],
"readme": "这里是插件详情页展示的完整介绍,支持 Markdown。"
}
常用字段速查(完整契约见类型包内的 schema):
| 字段 | 说明 |
|---|---|
name / title / version | 全局唯一标识 / 显示名 / semver 版本(更新的唯一判定来源) |
dist | 构建产物入口;免构建插件直接写 index.html |
build | 打包收录清单:["dist"] 只收 dist 目录;["."] 收整个插件根(自动排除 node_modules 等) |
development.main | 开发态 dev server 地址,调试时宿主直连它(仅允许 localhost) |
pluginSetting.slot | 面板槽位:workspace(工作区)/ leftPanel / rightPanel |
pluginSetting.Detach | true 时打开即分离为独立窗口 |
pluginSetting.Menu / MenuBottom | 进活动栏顶部 / 底部菜单 |
pluginSetting.SubSearch | 分离窗口标题栏带搜索框(配合 SubSearchPlaceholder) |
features[].code | 功能唯一标识,打开时作为启动参数回传给插件 |
features[].cmds | 主搜索框关键词:字符串直接匹配(支持拼音、首字母);对象可声明 regex / over / img / files / window 五种匹配规则 |
permissions | 平台能力路径,如 ["files.list", "ai.*"],见介绍的权限模型 |
tauri | 原生能力标识,如 ["dialog:default", "notification:default"] |
plugins | 跨插件调用白名单 |
services[] | 声明式服务进程(node / python / exe),配 `startup: "open" |
startup | "background" 随宿主后台启动 |
exports / events | 对外暴露接口与事件的声明(同时会作为 MCP 工具暴露) |
contextMenu[] | Windows 资源管理器右键菜单注册 |
2.3 package.json
{
"name": "@udhold-plugin/my-plugin",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.5.13",
"@udhold/udhold-ui": "workspace:*",
"@udhold/udhold-api-types": "workspace:*"
},
"devDependencies": {
"@iconify/utils": "^3.1.0",
"@unocss/preset-attributify": "^66.6.6",
"@unocss/preset-icons": "^66.6.6",
"@unocss/transformer-directives": "^66.6.6",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"unocss": "^66.6.6",
"vite": "^6.0.3",
"vue-tsc": "^2.1.10"
}
}
tsconfig.json 里加上类型包,window.udhold.* 就有完整代码提示:
{
"compilerOptions": {
"types": ["@udhold/udhold-api-types"]
}
}
2.4 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
base: './',
// 端口自定义,避开常用端口即可;strictPort 防止 vite 自行换端口导致宿主连不上
server: { port: 5261, strictPort: true, open: false },
plugins: [
vue(),
UnoCSS(),
{
// 与内置插件对齐:允许 file:/udhold-icon: 资源源(指令图标协议)
name: 'configure-response-headers',
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src * 'unsafe-inline' 'unsafe-eval' data: blob: udhold-icon: file:; img-src * data: blob: udhold-icon: file:;"
)
next()
})
}
}
],
resolve: {
optimizeDeps: { exclude: ['@udhold/udhold-ui'] },
ssr: { noExternal: ['@udhold/udhold-ui'] },
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
@udhold/udhold-ui是源码直引包(无预构建产物),必须保留optimizeDeps.exclude与ssr.noExternal两项配置,否则 Vite 依赖预扫描会报错。
2.5 src/main.ts 与 src/App.vue
import { createApp } from 'vue'
import App from './App.vue'
import 'virtual:uno.css'
import '@udhold/udhold-ui/styles/global.css'
import { setupThemeSync } from '@udhold/udhold-ui/utils'
// 应用宿主主题(明暗 + 主题色),并注册后续变更监听
setupThemeSync()
createApp(App).mount('#app')
<script setup lang="ts">
import { ref } from 'vue'
import { Message, ConfirmDialog, useMessage } from '@udhold/udhold-ui/components/common'
const { messageState, handleClose, handleAction, confirmState, handleConfirm, handleCancel } = useMessage()
const count = ref(0)
</script>
<template>
<div class="p-4">
<h1 class="text-base text-highlighted">我的插件</h1>
<button class="btn-solid" @click="count++">点了 {{ count }} 次</button>
</div>
<!-- 全局消息与确认弹窗,挂一次即可全局使用 -->
<Message :items="messageState" @close="handleClose" @action-click="handleAction" />
<ConfirmDialog
v-model:visible="confirmState.visible" v-model:extra-values="confirmState.extraValues"
:title="confirmState.title" :message="confirmState.message" :type="confirmState.type"
:confirm-text="confirmState.confirmText" :cancel-text="confirmState.cancelText" :extra="confirmState.extra"
@confirm="handleConfirm" @cancel="handleCancel"
/>
</template>
样式与组件的完整用法见设计系统系列文档。
第 3 步:运行调试
- 工程目录里启动 dev server:
pnpm dev(向导创建的工程在「插件开发者」里点「运行调试」会自动完成安装依赖、启动、登记全流程)。 - 在「插件开发者」工作台选中工程,点「运行调试」。宿主会登记该工程并以
development.main的地址直连 dev server——改代码即时热更新(HMR),不用重启宿主。 - 呼出主搜索框,输入你在
cmds里声明的关键词(如「我的插件」),回车打开插件。
调试要点:
development.main只允许http://localhost:*/127.0.0.1地址(安全限制,防远程页面注入)。- dev server 没启动时,宿主探测不到端口会回退加载本地 dist 产物——如果你看到的是旧界面,先确认 dev server 是否在跑、端口是否与
development.main一致。 - 「运行调试」的登记关系存在内存里,宿主重启后失效,重新点一次即可。
- 免构建工程(无
development.main)以「链接」语义直接加载工程目录里的文件。
第 4 步:调用宿主能力
大多数宿主方法返回统一信封 IpcResult<T>:{ code, success, data, msg }。code !== 0 即失败(403 = 权限未声明),失败不会抛异常,要靠检查 code:
// 基础能力(免权限声明)
udhold.dbStorage.setItem('launchCount', String((Number(udhold.dbStorage.getItem('launchCount')) || 0) + 1))
udhold.shellOpenExternal('https://udhold.cn')
await udhold.copyText('已复制的内容')
// 敏感能力:先在 plugin.json 声明 permissions,再按信封处理结果
const res = await udhold.clipboard.getHistory(1, 20)
if (res.code !== 0) {
console.error('调用失败:', res.msg) // 403 → 检查 permissions 声明
} else {
console.log('历史条目:', res.data)
}
启动参数:用户从搜索框打开插件时,命中的 feature 的 code 会回传;跳转方通过 router.push 传的 query 也在其中。在生命周期钩子里读取:
udhold.plugin.onMounted((action) => {
// action.code:命中的 feature 标识(缺省 'main')
// action.payload:启动参数(即 router.push 的 query)
// action.from:唤起来源 main / panel / hotkey / redirect / workflow / api
if (action.code === 'convert') {
const input = action.payload?.text ?? ''
// ...
}
})
清理时机:定时器、文件监听、服务进程一律在 onBeforeUnmount 里释放(宿主销毁前给 200ms 宽限):
const off = udhold.plugin.onBeforeUnmount(() => {
clearInterval(timer)
})
第 5 步:打包与本地验证
- 构建产物。在插件工程目录执行:
pnpm build
产出dist/目录。 - 打包规则由
plugin.json的build字段决定:["dist"]只收录 dist 目录;缺省时收录整个插件根(自动排除node_modules、.git等)。plugin.json与logo会固定打进包根。 - 在「插件开发者」的发布页选中工程,一键打包为
my-plugin-v1.0.0.zip。 - 本地验证:把 zip 发给测试者,在插件中心选「本地安装」导入(也支持双击安装),检查真实安装态下的表现。
build 声明 ["."] 或留空即可,发布页会直接按目录打包。第 6 步:发布市场
- 确保已登录 UDhold 账号,且插件已在市场登记(向导创建建项目时自动完成;CLI 与手动搭建在发布页先「登记」)。
- 发布页上传 zip 包,填写版本说明,选择上架状态(先「测试」再转「正式」是常见的灰度节奏)。
- 等待审核:审核重点包括
permissions与实际功能是否匹配、plugins: ['*']等高风险声明、导出接口是否构成权限泄漏。 - 审核通过即上架,全平台用户可在插件市场搜到。
- 发新版本:改
plugin.json的version→ 重新构建打包 → 上传。已安装用户打开插件时宿主自动检查并静默更新,无需手动催更。
接下来
功能逻辑跑通了,就该打磨界面了。去看设计系统:一套设计令牌 + 现成组件,让你的插件和 UDhold 原生功能长得像一家人。想用 Cursor、Claude 等 AI 工具辅助编码,看 LLMs.txt;卡住了先翻常见问题。