Kunkun
构建扩展

Worker 扩展

宿主渲染的 Raycast 风格 React 扩展,在 Web Worker 或 Node.js 进程中运行。

Worker 扩展在沙箱(Web Worker 或 Node.js/Deno 进程)中运行 React 组件。你使用 @kunkunsh/sdk/ui 中的原语编写 React;宿主将其渲染为原生组件——一种具有一致样式的 Raycast 式体验。运行机制参见扩展如何运行

模式

模式运行时UINode.js
worker-viewWeb Worker宿主渲染
node-viewNode.js / Deno 进程宿主渲染
worker-headlessWeb Worker
node-headlessNode.js / Deno 进程

项目结构

package.json
build.ts

设置

mkdir my-worker-ext && cd my-worker-ext
pnpm init && pnpm add @kunkunsh/sdk react valibot
pnpm add -D @types/react @types/bun

构建脚本

// build.ts
import { dedupeReact, kunkunCommandPlugin } from "@kunkunsh/sdk/build";
import type { BunPlugin } from "bun";

// 浏览器目标 → worker-view
await Bun.build({
  entrypoints: ["./src/App.tsx"],
  outdir: "./dist",
  target: "browser", format: "esm", minify: true,
  plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});

// Node 目标 → node-view
await Bun.build({
  entrypoints: ["./src/App.tsx"],
  outdir: "./dist/node",
  target: "node", format: "esm", minify: true,
  plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});

kunkunCommandPlugin 注入引导代码和 RPC 连接。dedupeReact 必须使用——两套 React 会破坏协调器。

dist 必须自包含:Bun 默认会打包 JS 依赖,因此除非宿主提供该依赖,否则避免使用 external 条目。原生 .node 插件不能内联——请打包其加载程序 + 平台文件,或惰性加载。

组件

import { useState } from "react";
import { Button, Div, H2, H3, P, Hr } from "@kunkunsh/sdk/ui";
import { popToRoot, showToast, Toast, Clipboard, LocalStorage } from "@kunkunsh/sdk";

export default function App() {
  const [text, setText] = useState("");
  return (
    <Div className="p-6" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <H2>My Worker Extension</H2>
      <Hr />
      <Button title="读取剪贴板" variant="outline" onClick={async () => setText(await Clipboard.readText())} />
      <Button title="提示" variant="primary" onClick={() => showToast({ title: "Hi", style: Toast.Style.Success })} />
      <Button title="返回" variant="outline" onClick={() => popToRoot()} />
      {text && <P>{text}</P>}
    </Div>
  );
}

@kunkunsh/sdk/ui 导入 UI 原语:

import { Button, Div, H2, H3, P, Code, Pre, Hr, Image, List, Grid, Form, TextField, TextArea, Select, Checkbox } from "@kunkunsh/sdk/ui";
<List>
  <List.Item title="项目" subtitle="副标题" icon="mdi:file" onClick={() => {}} />
</List>

<Form>
  <TextField title="名称" value={name} onChange={setName} />
  <Select title="选择" value={choice} onChange={setChoice}
    options={[{ label: "A", value: "a" }, { label: "B", value: "b" }]} />
</Form>

清单

{
  "kunkun": {
    "identifier": "com.you.my-worker-ext",
    "name": "My Worker Extension",
    "permissions": ["clipboard-read", "clipboard-write", "storage"],
    "commands": [
      { "name": "main", "title": "My Command", "mode": "worker-view", "main": "dist/App.js" },
      { "name": "with-fs", "title": "Node 版本", "mode": "node-view", "main": "dist/node/App.js" }
    ]
  }
}

可用 API

Worker-view 和 node-view 都能通过 RPC 获得完整的宿主 API——ClipboardLocalStoragedbshowToastshowHUDpopToRootgetEnvironmentnetwork.fetchpath,以及 fsshelldialogpermissionsspawnBackend。每个调用都受权限门控:在 manifest 中声明你要用的能力(例如 scoped shell 权限列出要运行的程序)。

Node-view 额外提供的是运行时本身:你的 bundle 跑在 Node.js/Deno 进程里,可以直接使用 Node 内置模块(child_processfs、原生 .node 插件),而不必经过宿主 API。除非需要这些,优先用 worker-view。

完整接口请参阅 SDK 参考

实时更新的列表

Worker 和 node 视图可以直接绑定记录存储并使用 useQuery,这样列表在数据变化时会自动重新渲染——无需手动重新获取:

import { useQuery } from "@kunkunsh/sdk/utils";
import { List } from "@kunkunsh/sdk/ui";

function Bookmarks() {
  const { data } = useQuery<{ title: string; url: string }>("bookmarks", { orderBy: [["createdAt", "desc"]] });
  return <List>{data?.map((r) => <List.Item key={r.id} title={r.data.title} subtitle={r.data.url} />)}</List>;
}

实际示例

  • youtube-tools(service + worker)和 youtube-podcast-generator(worker-view 消费者)
  • port-manager——一个扩展中包含 worker-view、无视图命令和菜单栏命令
  • git-repos——扫描本地仓库的 React 视图

On this page