快速上手

从环境准备到发布市场的完整实操:创建工程、运行调试、调用宿主能力、打包发布。

这一章是一条完整的开发流水线:环境 → 创建工程 → 运行调试 → 调用能力 → 打包 → 发布。跟着做,20 分钟跑起你的第一个插件。

第 1 步:准备环境

依赖要求用途
UDhold 桌面端最新版,下载安装调试容器与打包发布都在宿主内完成
Node.js20 LTS 或更高工程型插件的构建工具链
pnpm9+(npm i -g pnpm)官方插件工程的包管理器
UDhold 账号已登录向导建项目、发布市场需要
只做免构建静态插件(纯 HTML)或全程用「插件开发者」向导的话,Node.js 可以先不装——UDhold 会用内置运行时代劳。要做工程型插件(Vue/React),再按下面步骤装。

装好宿主后,呼出主搜索框(默认 Alt+Z)输入「插件开发者」回车——这是 UDhold 内置的插件开发工作台,接下来的创建、调试、发布都在它里面完成。

安装 Node.js 和 npm

npm 不用单独装——它随 Node.js 安装包一起分发,装好 Node.js 就有了 npm。步骤:

  1. 打开 Node.js 官网下载页:https://nodejs.org/zh-cn/download
  2. 选择 LTS 版本(长期支持版,标绿的按钮),按操作系统下载安装包:

下载 *.msi 安装包(如 node-v22.x-x64.msi),双击安装:

  1. 一路「下一步」,默认会勾选 Add to PATH(自动配置环境变量)和 npm components,保持默认即可
  2. 「Tools for Native Modules」页面可以不勾(插件开发用不到原生编译工具链)
  3. 装完重开一个终端窗口(让 PATH 生效)
  1. 验证安装——打开终端(Windows 用 PowerShell),能打印出版本号即成功:
node -v   # v22.x.x 这样的输出(≥ v20 即满足要求)
npm -v    # 10.x 这样的输出
国内网络拉取 npm 包慢或超时,可切换国内镜像源(可选,仅影响下载速度):
npm config set registry https://registry.npmmirror.com

安装 pnpm

工程型插件(见下面「方式三」)用 pnpm 管理依赖,任选一种方式安装:

npm install -g pnpm

验证:

pnpm -v   # 9.x 或更高

第 2 步:创建工程

三种方式任选:CLI 脚手架一键创建最快;向导创建适合快速起步和纯服务/静态插件;手动搭建适合理解每个文件、向官方插件仓库贡献插件。

方式一:CLI 脚手架一键创建(推荐)

装好 Node.js 后,用官方脚手架交互式生成完整插件框架:

pnpm add -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:* 即可。

方式二:向导创建(推荐起步)

  1. 在「插件开发者」的开发页点「新建项目」。
  2. 填写插件标题、描述、简介,选择工程存放目录。
  3. 选择插件形态,向导会生成对应的最小工程:
    形态生成内容
    静态 HTMLindex.html + plugin.json,免构建、零依赖
    页面 + Node 服务页面 + server.js 后端(声明式 services)
    页面 + Python 服务页面 + Python 后端
    纯 Node 服务只有后端,无界面(纯服务插件)
    纯 Python 服务同上
  4. 需要登录:向导会先在市场登记插件条目(拿到全局唯一的插件 name),再落盘工程文件。
  5. 完成后工程自动出现在工作台列表里,点「运行调试」即可看到插件跑起来。
向导生成的工程是独立目录,不依赖任何仓库。形态选错了不要紧——工程文件都是普通文件,随时可以改造成工程化开发。

方式三:手动搭建(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.Detachtrue 时打开即分离为独立窗口
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 步:运行调试

  1. 工程目录里启动 dev server:pnpm dev(向导创建的工程在「插件开发者」里点「运行调试」会自动完成安装依赖、启动、登记全流程)。
  2. 在「插件开发者」工作台选中工程,点「运行调试」。宿主会登记该工程并以 development.main 的地址直连 dev server——改代码即时热更新(HMR),不用重启宿主。
  3. 呼出主搜索框,输入你在 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 步:打包与本地验证

  1. 构建产物。在插件工程目录执行:
    pnpm build
    

    产出 dist/ 目录。
  2. 打包规则由 plugin.json 的 build 字段决定:["dist"] 只收录 dist 目录;缺省时收录整个插件根(自动排除 node_modules、.git 等)。plugin.json 与 logo 会固定打进包根。
  3. 在「插件开发者」的发布页选中工程,一键打包为 my-plugin-v1.0.0.zip。
  4. 本地验证:把 zip 发给测试者,在插件中心选「本地安装」导入(也支持双击安装),检查真实安装态下的表现。
免构建静态插件没有构建环节,build 声明 ["."] 或留空即可,发布页会直接按目录打包。

第 6 步:发布市场

  1. 确保已登录 UDhold 账号,且插件已在市场登记(向导创建建项目时自动完成;CLI 与手动搭建在发布页先「登记」)。
  2. 发布页上传 zip 包,填写版本说明,选择上架状态(先「测试」再转「正式」是常见的灰度节奏)。
  3. 等待审核:审核重点包括 permissions 与实际功能是否匹配、plugins: ['*'] 等高风险声明、导出接口是否构成权限泄漏。
  4. 审核通过即上架,全平台用户可在插件市场搜到。
  5. 发新版本:改 plugin.json 的 version → 重新构建打包 → 上传。已安装用户打开插件时宿主自动检查并静默更新,无需手动催更。

接下来

功能逻辑跑通了,就该打磨界面了。去看设计系统:一套设计令牌 + 现成组件,让你的插件和 UDhold 原生功能长得像一家人。想用 Cursor、Claude 等 AI 工具辅助编码,看 LLMs.txt;卡住了先翻常见问题。