- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
本篇技术指南以 Kun 开源仓库中create-kun-extension脚手架自带的framework-neutral Webview 模板(templates/webview/README.md)为核心,讲解如何用纯 TypeScript(无 React、无框架)构建一个运行在 Kun 沙箱 Webview 中的侧栏扩展:包括目录结构、Manifest 权限、CSP 基线、View Bridge 状态读写、Vite 打包约束以及validate/pack发布链路。读完本文,你将能独立完成一个 Kun Webview 扩展的创建、测试、验证与打包全流程,并理解为什么这类模板必须用 Vite 而不是裸tsc构建。
一、模板定位:框架中立的沙箱 Webview 扩展
Kun 的扩展体系将扩展 UI 分为三层:声明式贡献(按钮、菜单、设置项)、Webview(自定义列表、图表、表单、仪表盘)和 Direct DOM(高风险、不稳定)。复杂 UI 必须使用宿主创建的沙箱 Webview(详见 docs/extensions/webview-and-dom.md 的"选择方式"一节)。
webview模板是create-kun-extension提供的三种模板之一(另外两种是node与react,见 scaffold.mjs 中的SCAFFOLD_TEMPLATES)。它与react模板的关键区别在于:
- 不依赖任何 UI 框架,直接用原生 DOM API 编写视图逻辑;
- 只使用沙箱 Host transport(
window.kunExtension)与公开的@kun/extension-api客户端; - 通过 CSP 的
connect-src 'none'从源头禁止 Webview 直连网络,一切网络请求都必须走 Kun 的 Network Broker 授权。
从源码结构看,模板刻意保持"最小可运行":一个 View(侧栏计数视图)、一份不可变状态、一个针对状态 reducer 的单元测试,构成了理解 Kun Webview 扩展运行模型的最小闭环。
二、模板目录结构与职责
模板文件位于 packages/create-kun-extension/templates/webview/(脚手架生成后会替换其中的{{...}}占位符):
webview/ ├── src/webview/ │ ├── index.html # 沙箱入口 HTML,内置 CSP │ ├── main.ts # View 主逻辑:Bridge 客户端 + 状态读写 + 主题订阅 │ ├── state.ts # 纯函数状态 reducer(immutable) │ └── state.test.ts # 针对 reducer 的 node:test 单元测试 ├── kun-extension.json # Extension API v1 Manifest ├── package.json # 构建/测试/验证/打包脚本与依赖 ├── tsconfig.json # 源码 TypeScript 配置(noEmit,供 typecheck) ├── tsconfig.test.json # 测试编译配置(输出到 dist/test) ├── vite.config.ts # 浏览器资源打包配置 ├── _gitignore # 生成后重命名为 .gitignore └── LICENSE各文件职责如下:
| 文件 | 职责 |
|---|---|
index.html | 声明 CSP、挂载按钮,引用./main.js(Vite 产物) |
main.ts | 创建ExtensionHostClient,读取/写回 View State,订阅主题变化 |
state.ts | 导出具名类型ViewState与不可变 reducerincrement |
state.test.ts | 用 Node 内置测试运行器验证increment的不可变性 |
kun-extension.json | 声明身份、入口、激活事件、贡献点与最小权限 |
vite.config.ts | 把 npm 依赖打进浏览器可解析的静态资源 |
三、Manifest:一个最小但完整的沙箱 View 声明
kun-extension.json 展示了 browser-only View 扩展的完整声明(字段级约束见 docs/extensions/manifest.md):
{ "$schema": "https://kun.dev/schemas/extensions/manifest/v1.json", "manifestVersion": 1, "apiVersion": "1.0.0", "name": "{{NAME}}", "publisher": "{{PUBLISHER}}", "version": "0.1.0", "displayName": "{{DISPLAY_NAME_JSON}}", "description": "A framework-neutral Kun sidebar Webview", "license": "MIT", "engines": { "kun": ">=0.1.0" }, "browser": "dist/webview/index.html", "activationEvents": [ "onView:main" ], "contributes": { "views.rightSidebar": [ { "id": "main", "title": "{{DISPLAY_NAME_JSON}}", "entry": "dist/webview/index.html", "localResourceRoots": ["dist/webview"] } ] }, "permissions": [ "ui.views", "webview", "storage.workspace" ], "stateSchemaVersion": 1 }要点解读:
browser入口而非main:模板是纯浏览器扩展,没有 Node Host。Kun 的 Manifest 规则要求main与browser至少存在其一;且browser-only Manifest 不能声明commands、agentProfiles、tools、modelProviders或authentication,这些都需要 Node handler(见 manifest.md 的"入口"一节)。onView:main激活事件:用户打开右侧栏的mainView 时触发。Validator 会双向校验:每个 View 贡献必须声明对应事件,每个非 startup 事件也必须指向真实贡献。- 隐含权限推导:任意
browser入口强制要求webview权限;任意views.*View 要求ui.views与webview。模板显式声明的三份权限恰好满足这一最小集,并额外申请了storage.workspace用于持久化 View 数据。 localResourceRoots:限定该 View 可加载的本地资源根目录,这里是dist/webview,与协议层的资源校验配合(见下文"本地资源协议")。
四、沙箱基线:CSP、无 Node、无直连网络
模板入口 index.html 的<head>中内置了完整的 Content Security Policy:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'none'; img-src 'self' data:" />这是 Kun Webview 强制沙箱基线的一部分(详见 webview-and-dom.md 的"强制沙箱基线"与"Content Security Policy"两节):
- 扩展不能关闭
nodeIntegration: false、contextIsolation: true、Chromiumsandbox: true,只能使用 Kun-owned preload; - Guest 拿不到 Node global、Electron module、Extension Host IPC handle、Kun runtime token、账号秘密或完整
window.kunGui; connect-src 'none'意味着浏览器fetch、WebSocket 等直连全部被 CSP 阻断——即使 Manifest 声明了network:<hostname>权限,网络请求也只能通过 Kun Network Broker 发出(security-and-resources.md 的"Network Broker"一节);- navigation、popup、任意 download 默认拒绝;camera、microphone、geolocation 等设备权限默认全部拒绝。
模板还演示了img-src 'self' data:的写法,允许视图使用data:URI 图片;如果你的扩展需要更多字体资源,可按文档中的最低策略意图补充font-src 'self'。
五、View Bridge 与状态读写:main.ts 逐行拆解
模板核心 main.ts 只有 29 行,完整展示了沙箱 Webview 的三种能力:初始化 Bridge 客户端、读写 Schema-versioned View State、订阅主题变化。
import { ExtensionHostClient, type HostTransport } from '@kun/extension-api' import { increment, type ViewState } from './state.js' declare global { interface Window { readonly kunExtension: HostTransport } } const client = new ExtensionHostClient(window.kunExtension) const button = document.querySelector<HTMLButtonElement>('#increment') let state: ViewState = (await client.ui.getViewState<ViewState>()) ?? { count: 0 }关键点:
window.kunExtension是 Kun-owned preload 暴露的窄 transport,不是window.kunGui。官方客户端ExtensionHostClient只接受这个 transport,业务代码不要直接发私有 method(见 webview-and-dom.md 的"窄 View Bridge"一节)。getViewState/setViewState是 schema-versioned 的持久状态通道。在 client-ui-apis.ts 中可以看到其底层实现:getViewState调用ui.getViewState并在found时按泛型返回,setViewState调用ui.setViewState。状态 scope 固定为 extension + contribution + workspace,不能存放 credential、token、cookie、secret 或私有 prompt,也不接受任意 binary(webview-and-dom.md 的"View State"一节)。getTheme返回已解析的工作台主题(含实际 zoom、reduced-motion 状态与background、sidebarBackground、surface、foreground、border、accent等稳定公开 token),模板把它写入document.documentElement.dataset.theme,再通过onDidChangeTheme订阅实时切换。
function render(): void { if (button) button.textContent = `Count: ${state.count}` } button?.addEventListener('click', () => { state = increment(state) render() void client.ui.setViewState(state) }) const theme = await client.ui.getTheme() document.documentElement.dataset.theme = theme.kind client.ui.onDidChangeTheme((next) => { document.documentElement.dataset.theme = next.kind }) render()交互逻辑保持"不可变状态 + 渲染函数"的朴素模式:点击 →increment生成新状态 → 重渲染 →setViewState持久化。主题订阅在 View teardown 时应释放,业务组件不要直接发私有 method。
六、纯函数状态与测试:state.ts 与 state.test.ts
state.ts 把状态模型与 DOM 完全解耦:
export type ViewState = { count: number } export function increment(state: ViewState): ViewState { return { count: state.count + 1 } }对应的 state.test.ts 使用 Node 内置的node:test与node:assert/strict,无需额外测试框架:
import assert from 'node:assert/strict' import test from 'node:test' import { increment } from './state.js' test('increments immutable state', () => { assert.deepEqual(increment({ count: 1 }), { count: 2 }) })模板的test脚本先执行vite build,再用tsc -p tsconfig.test.json把测试编译到dist/test,最后用node --test运行——这正是"View 逻辑与 DOM 分离、可在 Node 环境单测"的架构红利。
七、为什么必须用 Vite 打包:裸 tsc 的致命缺陷
模板 package.json 的构建链是:
"scripts": { "build": "npm run typecheck && vite build", "typecheck": "tsc --noEmit -p tsconfig.json", "test": "npm run build && tsc -p tsconfig.test.json && node --test dist/test/state.test.js", "validate": "kun extension validate .", "pack": "npm run build && kun extension pack ." }依赖只有运行时@kun/extension-api(^1.0.0)与开发依赖typescript、vite、@types/node。README 明确警告:不要用裸tsc输出替换 Webview 构建。原因在 vite.config.ts 与 webview-and-dom.md 中均有印证:
export default defineConfig({ root: 'src/webview', base: './', css: { postcss: { plugins: [] } }, build: { target: 'es2022', outDir: '../../dist/webview', emptyOutDir: true } })- Chromium 不像 Node 那样解析
@kun/extension-api这类裸模块说明符;tsc只是类型检查 + 转译,会原样保留该 import,浏览器加载页面时会直接失败; - Vite 把
@kun/extension-api打进浏览器 bundle,并以base: './'生成受localResourceRoots约束的相对 URL; - 因此发布前要检查最终 HTML/JavaScript,而不是只检查 TypeScript 源码——模板的
pack脚本强制先vite build再打包,正是为杜绝这种"tsc 产物上线即 404"的问题。
八、验证与打包:kun extension validate / pack 全流程
模板的标准工作流是 README 中给出的四步命令:
npm install npm test npm run validate npm run pack各步骤说明:
npm install:安装@kun/extension-api与构建工具。注意 Kun 官方脚手架生成的独立项目按名称安装已发布的@kun/extension-api(可选@kun/extension-react、@kun/extension-test);kunCLI 来自 Kun 本体安装,与 npm 上同名的无关包不是一回事(见 create-kun-extension/README.md)。npm test:typecheck → vite build → 编译测试 → node --test,一次跑通类型、构建与单元测试。npm run validate:等价于kun extension validate .,对开发目录做 Manifest、版本、入口、贡献、权限与资源引用校验。Kun 在任何代码或远程内容被读取之前完成验证;未知字段、无效引用与不兼容版本必须按同版本 Schema 处理,不能靠忽略字段"猜测兼容"(manifest.md)。对打包产物还可使用kun extension validate ./dist/<publisher>.<name>-<version>.kunx。npm run pack:等价于npm run build && kun extension pack .,产出.kunx包。发布包根必须包含README.md、LICENSE、kun-extension.integrity.json(记录包文件 SHA-256 的完整性清单)及所有入口引用的本地资源;不要往包里打进秘密、令牌、私钥、开发.env或用户数据。
模板的.gitignore(生成前为_gitignore)已排除node_modules/、dist/与*.kunx,避免构建产物进入版本控制。
九、从脚手架创建 Webview 扩展
除了手工拷贝模板,更推荐用官方脚手架 create-kun-extension 生成(支持node、webview、react三种模板):
npx create-kun-extension my-extension \ --template webview \ --publisher acme \ --name issue-assistant \ --display-name "Issue Assistant"建议先执行预检npm view create-kun-extension version;若配置的 registry 返回E404(说明未发布该脚手架),可改用仓库内的本地实现:
node ./packages/create-kun-extension/src/cli.mjs my-extension \ --template webview \ --publisher acme \ --name issue-assistant脚手架参数与校验规则见 cli.mjs 与 scaffold.mjs:
| 参数 | 说明 |
|---|---|
<directory> | 目标目录(必填,且必须尚不存在) |
--template | node(默认)/webview/react |
--publisher | 发布者 ID:小写字母/数字/连字符,最长 64;builtin、kun、openai、system为保留值 |
--name | 扩展名 ID:小写字母开头,仅小写字母/数字/连字符,最长 64 |
--display-name | 面向用户的名称(1–128 字符,无控制字符);缺省时由 name 自动 title-case |
--json | 输出机器可读结果(含生成的files列表) |
脚手架会先把模板复制到临时 staging 目录,完成{{EXTENSION_ID}}、{{PUBLISHER}}、{{NAME}}、{{DISPLAY_NAME}}等占位符替换(DISPLAY_NAME还会分别做 JSON 转义与 HTML 转义),再原子重命名到目标目录;任何失败都会清理 staging。生成后执行npm install && npm test && npm run validate && npm run pack即可完成从代码到.kunx的闭环。
十、进阶:从模板到生产级 Webview 的检查清单
模板是一个刻意最小化的起点。从 webview-and-dom.md 的"发布前检查"与 security-and-resources.md 的"扩展作者安全清单"可以归纳出生产化时需要注意的约束:
- 只从
kun-extension://与声明 roots 加载资源:Kun 通过kun-extension://<publisher.name>/<package-relative-path>协议提供资源,handler 会拒绝路径穿越、链接逃逸、跨扩展读取与远程 redirect。不要拼接用户输入构造资源 URL;静态 asset 在构建期生成已知路径,动态数据走 bridge。 - 消息与状态要有 Schema、size/rate limit 与 disposal:View 不要使用浏览器 local/session storage 承载持久数据(partition 默认非持久,清理/重建 View 时会丢失);大文件数据应使用扩展 Storage API 或文件型公开能力,状态版本变化走明确迁移。
- React 视图可选:若你愿意引入框架,
react模板(templates/react/src/webview/main.tsx)提供了ExtensionViewProvider、useTheme、useViewState、useHostMessage等 Hooks,底层仍是通过同一个window.kunExtensiontransport 创建的ExtensionHostClient,两种模板共享同一套沙箱契约。 - 无 Node、无 custom preload、无 direct network、无 remote code:Webview 无法声明自定义 preload;需要网络时在 Manifest 声明精确的
network:<hostname>,通过 Network Broker 发起请求,并只传 account reference(providers-and-accounts.md)。 - View teardown 时 dispose:关闭、disable/uninstall、workspace 切换或 guest 终止会取消 pending call 与事件订阅,旧 guest 的晚到消息会被拒绝——组件 unmount 时应释放 subscription 与 pending work。
模板提供的storage.workspace权限只覆盖 View State 的最小持久化需求;如果你还要保存扩展级全局状态或敏感数据,需要按 security-and-resources.md 的存储类型表补充storage.global/storage.secrets权限——新增权限不会继承旧同意,用户必须在受保护窗口重新确认。
结语
create-kun-extension的 webview 模板是理解 Kun Extension API v1 沙箱模型的最佳起点:它用不到 30 行的main.ts串起了 Bridge 初始化、状态持久化与主题订阅三条核心链路,用 Vite 配置解释了"浏览器无法解析 npm 裸说明符"这一关键构建约束,并用四步命令覆盖了从安装、测试、验证到打包的完整发布流程。以此为骨架,配合仓库中的 Manifest 参考、Webview 与 Direct DOM 与 权限与资源 文档,你就能在此基础上扩展出符合最小权限原则的生产级 Kun 侧栏扩展。
- 人工智能
- AI Agent
- 自主智能体
- 桌面应用
- MCP Clients
【免费下载链接】Kun
Local-first AI agent workspace for coding, writing, design, research, and automation — one runtime for desktop GUI and TUI.
相关推荐
Kun React 扩展开发实战:基于 Node Host + 沙箱 Webview 的官方模板全解析
Kun React 扩展开发实战:基于 Node Host + 沙箱 Webview 的官方模板全解析 导读 本文以 Kun 开源仓库中 create kun
人工智能AI Agent自主智能体桌面应用MCP Clients如何快速入门Play框架:5分钟搭建你的第一个Java Web应用
如何快速入门Play框架:5分钟搭建你的第一个Java Web应用 Play框架是一个轻量级的Java Web开发框架,它采用了MVC架构模式,提供了快速开发、
Material Shell扩展发布流程:从打包到上架extensions.gnome.org
Material Shell扩展发布流程:从打包到上架extensions.gnome.org 你还在为GNOME Shell扩展发布流程感到困惑?本文将带你一
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考