# whatevertools 文档全文 > 由 website/scripts/prepare.mjs 生成。仓库:https://github.com/mowtwo/whatevertools --- # 快速开始 ## 已经有一个 uTools 插件 不用改代码,先看它在各平台能跑到什么程度,再直接运行: ```bash npm i -D @whatevertools/cli electron npx whatevertools check ./dist # 用到的 API 在 Electron / 网页上的支持程度 npx whatevertools run ./dist # 在裸 Electron 启动器里运行,Alt+Shift+Space 打开主面板 ``` 打包成三种产物: ```bash npx whatevertools pack ./dist --target utools # 原样输出给 uTools 发布 npx whatevertools pack ./dist --target electron --product "我的工具" # Electron 独立应用 npx whatevertools pack ./dist --target web # 静态网页 ``` 需要长期维护时,用配置文件和 `build`: ```bash npx whatevertools init --ci github # 生成 whatevertools.config.ts 和 GitHub Actions npx whatevertools build # 按配置打包所有目标 npx whatevertools build --target electron --binary # 出 dmg / nsis / AppImage 安装包 ``` 下一步:[移植 uTools 插件](./porting) · [打包与签名](./packaging) ## 写一个新插件 继续用 uTools 的写法(plugin.json + preload + 网页)就能同时发布到 uTools 和其他平台。只想要类型提示: ```bash npm i -D @whatevertools/utools # 只有 .d.ts,零运行时,不进打包产物 ``` 也可以直接用原生 API,名字更中立,换成自定义注入名的宿主也能运行: ```ts import { getApi } from '@whatevertools/api' const wt = getApi() wt.lifecycle.onEnter((action) => { /* … */ }) const files = await wt.dialog.openAsync({ properties: ['openFile'] }) if (wt.supports('screen.captureRegion')) await wt.screen.captureRegion() ``` 用 Vite 写界面时,加上 [Vite 插件](./vite) 就能在 `vite dev` 里直接调试插件环境。 ## 想给自己的应用加插件 - Electron 应用:[给 Electron 应用加插件](./electron-host) - 网站:[给网站加插件](./web-host) --- # 核心概念 ## 分层 ``` 插件代码 ──► 兼容层(@whatevertools/compat-utools:window.utools、plugin.json、入口类型、唤起来源) │ ▼ 原生 API(@whatevertools/api:window.whatevertools,按命名空间组织) │ 只依赖能力接口 ▼ 能力接口(core)◄── Node / Electron 模块垫片(shim-node、shim-electron) │ ┌─────────┴──────────┐ Electron 平台 网页平台 (Tauri:规划中) │ main 进程宿主 │ 浏览器运行时 + 适配器包 ▼ ▼ 宿主核心引擎(core):插件表、生命周期、权限、入口与来源、定时任务、动态功能、快捷键、提供者、身份、同步 ``` - **平台无关的核心**:生命周期、权限、入口、调度、存储、同步都在 `@whatevertools/core`,平台只实现适配器接口。 - **兼容层**:uTools 特有的名字只出现在 `compat-utools`。宿主可以改注入名(`window.myhost`)或者不加载兼容层。 - **适配器包**:给某个平台补能力的实现(网页上的 sharp、文件对话框、虚拟文件系统……)都是独立的包,宿主可以替换。 ## 插件描述 | 文件 | 谁读 | 作用 | |---|---|---| | `plugin.json` | uTools、兼容层 | uTools 插件描述 | | `whatevertools.json` | 兼容层 | 补充身份、权限用途说明、`targets`、独立应用配置;带 `main` 字段时就是原生格式的插件描述。uTools 不读它 | ```json { "id": "quick-union-image", "name": "快拼图", "version": "1.2.0", "targets": ["utools", "electron", "web"], "permissions": { "fs.read": { "reason": "读取你选择、拖入或粘贴的图片" } }, "standalone": { "productName": "快拼图", "startFeature": "stitch" } } ``` ## targets 插件声明自己能在哪些目标上运行。只在桌面上成立的插件(透明置顶悬浮层、桌面挂件)写 `["utools", "electron"]`,打包到网页会直接报错,网页宿主也会拒绝安装。 ## 身份与版本 插件能拿到底座(运行时)版本、宿主名字和版本(任意字符串,插件自行解析)、伪装平台名和版本(旧插件 `getAppName` / `getAppVersion` 看到的值)、插件自己的名字和版本: ```ts whatevertools.identity // { runtime: { name, version }, host: { name, version }, mock: { platform, runtime }, plugin: { id, name, version }, env } ``` ## 支持程度 每个 API 在每个平台上标为:**原生**(行为一致)、**模拟**(换了实现、语义一致)、**部分**、**占位**(不报错但没效果)、**不支持**(抛 `EWTUNSUPPORTED`,宿主可配成警告)。完整表见 [API 支持程度](./api-support),运行时用 `supports(name)` 查询。 --- # 移植 / 多端兼容 目标:同一份插件代码继续发布到 uTools,同时能打包成 Electron 独立应用和网页。大多数 uTools 插件不用改代码,下面是需要注意的地方。 ## 1. 先跑兼容报告 ```bash npx whatevertools check ./dist ``` 按平台列出插件用到的每个 API 和支持程度。重点看网页列里的「部分 / 占位 / —」。 ## 2. 加 whatevertools.json(不影响 uTools) 放在插件构建产物目录(或上一级),给兼容层看: ```json { "id": "quick-union-image", "name": "快拼图", "version": "1.2.0", "targets": ["utools", "electron", "web"], "permissions": { "fs.read": { "reason": "读取你选择、拖入或粘贴的图片" }, "mcp": { "reason": "让 AI Agent 调用拼图工具" } }, "standalone": { "productName": "快拼图", "startFeature": "stitch" } } ``` - `permissions` 只是用途说明,显示在权限询问里,不会自动授予。权限 id 见 hosts.md。 - 只在桌面上成立的插件(透明置顶悬浮层、桌面挂件、模拟键鼠)把 `targets` 写成 `["utools", "electron"]`。 - Vite 项目可以用 `@whatevertools/vite` 的 `manifest` 选项自动写这个文件。 ## 3. 类型 ```bash npm i -D @whatevertools/utools # 只有 .d.ts,零运行时,不进打包产物 ``` ```jsonc // tsconfig.json { "compilerOptions": { "types": ["@whatevertools/utools"] } } // 或者 window.utools 声明成可选(在普通浏览器里调界面时): /// ``` ## 4. 网页平台的差异和改法 | 情况 | 网页上的行为 | 建议 | |---|---|---| | `showOpenDialog`(同步) | 返回 `undefined`,异步弹出选择器,选中的文件默认作为一次 `files` 进入(`onPluginEnter` 收到 `type: 'files'`、`from: 'dialog'`) | 改用 `dialog.openAsync`(见下方代码) | | `showOpenDialog` 选目录且带 `createDirectory` | 返回 `['/downloads']`,写进去的文件都会下载 | 不用改 | | `showSaveDialog` | 返回 `/downloads/<文件名>`,写入后浏览器下载 | 不用改 | | `shellShowItemInFolder` | 不报错、没效果 | 不用改 | | `utools.sharp` | `@whatevertools/sharp-canvas`(Canvas 实现):resize / extend / extract / rotate / flip / flop / composite / trim / flatten / png / jpeg / webp / metadata / stats 等常用操作 | 不支持 AVIF 编码、ICC、动图(只取第一帧);对结果要求严格时在桌面端处理 | | SVG 输入 | 自动补 `xmlns` 后用浏览器解码 | 不用改 | | `require('fs')` 等 | 虚拟文件系统(IndexedDB 持久化);`/drop` `/picked` `/downloads` `/tmp` 不落盘 | 别依赖真实路径;用户文件通过拖入 / 选择获得 | | `require('child_process')`、原生模块 | 默认不支持;宿主可以接本地 bridge 守护进程 | 用 `supports()` 判断后降级 | | 窗口置顶、鼠标穿透、全局快捷键、屏幕截图 | 浏览器做不到 | 声明 `targets` 排除 web | | `createBrowserWindow` | `window.open` 模拟(会被弹窗拦截影响) | 能在主界面完成的就别开新窗口 | 打开文件改成异步、同时保持 uTools 构建不变: ```ts async function openDialog(options) { // whatevertools 兼容层(独立应用、网页)提供原生 API;uTools 里没有 const native = globalThis[Symbol.for('whatevertools.api')] if (native?.dialog?.openAsync) return (await native.dialog.openAsync(options)) ?? [] return window.utools.showOpenDialog(options) ?? [] } ``` ## 5. Electron 平台的差异 - 每个插件一个独立的会话分区(localStorage / IndexedDB / cookie 互不可见)。 - preload 里的 `require` 拦截是纵深防御,不是安全边界。 - `pluginSetting.single: false`(多实例)暂时按单实例运行。 - 无界面测试(`WHATEVERTOOLS_HEADLESS=1`)时插件窗口固定在屏幕外。 ## 6. 打包 见 packaging.md。最小用法: ```bash npx whatevertools pack ./dist --target electron --name acme --product "Acme 工具" npx whatevertools pack ./dist --target web --name acme npx whatevertools pack ./dist --target utools ``` `--name` 替换注入给插件的全局变量名(`window.acme`)、底座名和包名;uTools 兼容层仍注入 `window.utools`,可以用 `--compat utools:myhost` 改名或 `--compat none` 关闭。 ## 7. 常见问题 - **网页上卡住没反应**:多半是权限询问用的 `confirm()` 被挡住了(测试环境),或者调用了需要用户手势的 API(文件选择要在点击事件里同步发起)。 - **`EWTUNSUPPORTED`**:这个 API 在当前平台不支持,查 api-support.md,用 `supports()` 判断后降级。 - **`EWTPERM`**:权限被拒绝;用户可以在宿主的设置页改。 - **`EWTENCRYPTED`**:给的是 `.upxs`,只能由 uTools 安装。 - **打包报「声明只支持 …」**:插件的 `targets` 不包含这个目标。 --- # @whatevertools/vite [whatevertools](https://github.com/mowtwo/whatevertools) 的 Vite 插件。普通的 Vite 项目(React、Vue、原生都行)加上它: - `vite build`:把 `whatevertools.json`(身份、权限说明、支持的目标、独立应用配置)写进产物,并按需打包成 uTools 插件、Electron 独立应用或网页; - `vite dev`:在普通浏览器里模拟插件环境——注入运行时、把 `public/preload.js` 打包成浏览器脚本、`window.utools` 直接可用,不用开 uTools 也能调界面。 ```ts // vite.config.ts import { defineConfig } from 'vite' import whatevertools from '@whatevertools/vite' export default defineConfig({ base: './', plugins: [ whatevertools({ manifest: { id: 'my-plugin', name: '我的插件', targets: ['utools', 'electron', 'web'] }, // 不写 targets 时读环境变量:WT_TARGET=web,electron vite build targets: ['utools'], name: 'acme', // electron / web 目标的注入名和包名 }), ], }) ``` uTools 的 `plugin.json` 和 `preload.js` 照常放在 `public/`。产物在 `release/<目标>/`。 | 选项 | 说明 | |---|---| | `targets` | `utools` / `electron` / `web`,可多个;默认读 `WT_TARGET` | | `manifest` | 写进 `dist/whatevertools.json`,`version` 默认取 package.json | | `out` | 打包输出目录,默认 `release` | | `name` / `packageName` / `productName` | 注入名、包名、应用名 | | `compat` | `utools`(默认)/ `utools:别名` / `none` | | `devRuntime` | `vite dev` 时是否模拟插件环境,默认 `true` | MIT --- # 打包与发布 `whatevertools`(简写 `wt`)把一个插件打成三种产物: | 目标 | 产物 | 说明 | |---|---|---| | `utools` | 原样的 uTools 插件目录 | 不带任何运行时,校验 plugin.json;`whatevertools.json` 不会进入产物 | | `electron` | 独立应用目录,可以继续出安装包(dmg / zip / nsis / AppImage…) | 多个插件时是一个启动器应用 | | `web` | 静态站点 | 多个插件时根目录是入口页,每个插件一个子目录 | 插件在 `whatevertools.json` 里写 `"targets": ["utools", "electron"]` 可以声明只支持哪些目标,打包到其他目标会直接报错(比如只在桌面上有意义的悬浮挂件)。 ## 配置文件 ```bash npx whatevertools init # 生成 whatevertools.config.ts npx whatevertools init --ci github # 同时生成 .github/workflows/build-apps.yml ``` ```ts // whatevertools.config.ts(也可以是 .mjs / .cjs / .json) import { defineConfig } from '@whatevertools/cli' export default defineConfig({ plugins: './dist', // 插件目录或插件包(.upx / .asar / .zip),可以多个 targets: ['utools', 'electron', 'web'], out: 'release', name: 'acme', // electron / web 的注入名(window.acme)和底座名 productName: '快拼图', electron: { install: { enabled: false }, // 打包后的应用能不能再装插件包(见 README「插件包」) binary: { formats: { mac: ['dmg', 'zip'], win: ['nsis'], linux: ['AppImage'] }, arch: ['arm64', 'x64'], // 默认当前机器架构 icon: 'build/icon.png', // 至少 512×512;不写用第一个插件的 logo builder: {}, // 原样合并进 electron-builder 配置 }, sign: { mac: { identity: undefined, notarize: undefined, entitlements: undefined }, win: { mode: 'azure', azure: { endpoint: '…', certificateProfileName: '…', codeSigningAccountName: '…' } }, }, }, }) ``` ## 命令 ```bash wt build # 按配置打包所有 targets wt build --target web,electron # 只打这几个(也可以用环境变量 WT_TARGET) wt build --target electron --binary # 继续出安装包(需要项目里 npm i -D electron-builder) wt build --binary --platform mac,win # 指定安装包平台(跨平台构建受 electron-builder 限制,建议在 CI 矩阵里各平台分别构建) wt doctor --target electron --binary # 检查 Node、Electron、electron-builder 和签名凭据,缺什么列出来 wt pack <插件目录或插件包...> --target web # 不用配置文件、直接打包 ``` 所有命令都是非交互的: - `--json`:stdout 只输出一行结果 JSON(`{"ok":true,...}` / `{"ok":false,"code":"…","error":"…"}`),日志都在 stderr; - 退出码:`0` 成功,`1` 失败(包括 `doctor` 有必需项没通过),`2` 用法或配置错误; - 环境变量:`WT_CONFIG` 指定配置文件,`WT_TARGET` 覆盖目标。 ### make ```makefile build: npx whatevertools build apps: npx whatevertools doctor --target electron --binary npx whatevertools build --target electron --binary --json > release/result.json ``` ## 签名和公证 证书、密码、API Key **只从环境变量读**,不要写进配置文件或提交到仓库。缺任何一项时 `wt build --binary` 直接失败,不会悄悄产出一个没签名的包;明确不想签名时在配置里关掉(`sign.mac.identity: false`、`sign.win.mode: 'none'`)。 ### macOS | 环境变量 | 说明 | |---|---| | `CSC_LINK` | Developer ID Application 证书(.p12 文件路径或 base64) | | `CSC_KEY_PASSWORD` | 证书密码 | | `APPLE_API_KEY` | App Store Connect API Key(**.p8 文件路径**) | | `APPLE_API_KEY_ID` / `APPLE_API_ISSUER` | Key ID / Issuer ID | | 或 `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID` | 用 Apple ID 公证 | | 或 `APPLE_KEYCHAIN_PROFILE` | `xcrun notarytool store-credentials` 存好的配置名 | 本机钥匙串里有 Developer ID Application 证书时不需要 `CSC_LINK`。开启 hardened runtime,默认 entitlements 是 Electron 需要的最小集合(JIT、可执行内存、加载插件自带的原生模块)。有公证凭据时自动公证。 ### Windows | 方式 | 环境变量 | |---|---| | pfx 证书 | `WIN_CSC_LINK`(或 `CSC_LINK`)+ `WIN_CSC_KEY_PASSWORD`(或 `CSC_KEY_PASSWORD`) | | Azure Trusted Signing | `AZURE_TENANT_ID` + `AZURE_CLIENT_ID` + `AZURE_CLIENT_SECRET`,配置里写 `sign.win.azure`;需要在 Windows 上构建 | ### Linux AppImage / deb / tar.gz 不需要代码签名。 ## CI `wt init --ci github` 生成的工作流:推送 `v*` 标签时在 macOS、Windows、Linux 上分别构建,先跑 `doctor`,再 `build --binary --json`,产物上传为 artifact 并挂到 GitHub Release。需要在仓库 Settings → Secrets 里配置: `MAC_CSC_LINK`、`MAC_CSC_KEY_PASSWORD`、`APPLE_API_KEY_CONTENT`(.p8 的文件内容,工作流会写成文件)、`APPLE_API_KEY_ID`、`APPLE_API_ISSUER`、`WIN_CSC_LINK`、`WIN_CSC_KEY_PASSWORD`,或 Azure 的三个变量。没配的平台就不签名(`doctor` 会提示)。 ## Vite 项目 普通 Vite 项目可以用 `@whatevertools/vite`,`vite build` 时直接产出各目标,`vite dev` 时在浏览器里模拟插件环境,见 [@whatevertools/vite](https://github.com/mowtwo/whatevertools/tree/main/packages/vite)。 --- # 给 Electron 应用加插件 `@whatevertools/electron` 是一个完整的插件宿主:插件表、生命周期、权限询问、入口与快捷键、窗口、MCP 工具、插件包安装。你的 Electron 应用调用 `runApp`(或者直接用 `WhateverToolsHost` 自己组装界面)就有了插件生态。 ```bash npm i @whatevertools/electron @whatevertools/compat-utools ``` ```ts import { runApp } from '@whatevertools/electron' runApp({ mode: 'launcher', // 'standalone':单个插件就是整个应用 plugins: ['/path/to/plugin', '/path/to/pack.upx'], compat: [{ type: 'utools' }], // 现有 uTools 插件直接跑 apiName: 'acme', // 原生 API 注入名:window.acme identity: { runtimeName: 'acme', host: { name: 'Acme Launcher', version: '2026.10-beta' }, mock: { platform: { name: 'uTools', version: '7.0.0' } }, }, install: { enabled: true, max: 20, deny: ['bad-*'] }, policy: { plugins: { 'quick-union-image': { 'fs.read': ['/Users/me/Pictures'] } } }, providers: { user: { get: () => ({ nickname: 'me', avatar: '', type: 'user' }) }, ai: { models: () => [/* … */], complete: async (ctx, req, io) => { /* 调你的模型;io.callTool 调插件工具 */ } }, }, mcp: { enabled: true }, timeouts: { idleExit: 3 * 60 * 1000 }, // 退到后台 3 分钟后结束插件(有窗口开着的除外) }) ``` ## 你得到了什么 - **启动器**:主面板(`Alt+Shift+Space`)、超级面板、指令快捷键、URL scheme、托盘。 - **设置页**:每个插件每项权限单独设置,安装 / 卸载插件包,复制 MCP 配置。 - **插件包安装**:`host.installer.install(file)`,详见 [插件包](./packages)。 - **安全**:每个插件独立的会话分区;远程网页窗口拿不到插件 API;插件页的摄像头、定位等浏览器权限映射成插件权限。 - **特殊接口提供者**:用户信息、AI 模型、付费、设备 id 由你提供,调用经过 `provider:` hook。 ## 只要宿主、不要启动器界面 ```ts import { WhateverToolsHost } from '@whatevertools/electron' const host = new WhateverToolsHost({ compat: [{ type: 'utools' }], install: { enabled: true } }) const plugin = host.loadPlugin('/path/to/plugin') await host.enter(plugin.id, { code: 'main', type: 'text', payload: '打开', from: 'main' }) ``` 入口类型、唤起来源、面板行为都可以替换,见 [hook 与权限](./hooks)。 --- # 给网站加插件 两种方式: 1. **固定插件**:`wt pack --target web` 把插件打成静态站点,多个插件打成带入口页的站点,部署到任何静态托管。 2. **动态安装**:网站引入 `@whatevertools/web-host`,用户在浏览器里安装 `.upx` 插件包,每个插件跑在自己的 iframe 里。 ## 动态安装 需要三个静态文件,页面必须是 https 或 localhost(Service Worker 的要求): 1. `@whatevertools/web-host/sw.js`:放在能覆盖作用域的位置,比如 `/__wt/sw.js`(作用域默认 `/__wt/`)。 2. 浏览器运行时:把 `@whatevertools/web/full` 打成 iife,全局名 `__whatevertoolsRuntime`: ```bash npx esbuild node_modules/@whatevertools/web/dist/full.js --bundle --format=iife --global-name=__whatevertoolsRuntime --minify --outfile=public/wt-runtime.js ``` 3. 你的页面(用打包工具引入本包)。 ## 用法 ```ts import { createWebHost } from '@whatevertools/web-host' const host = createWebHost({ swUrl: '/__wt/sw.js', runtimeUrl: '/wt-runtime.js', container: document.querySelector('#plugin-area')!, policy: { max: 5, deny: ['evil-*'] }, // 同 core 的 InstallPolicy prompt: async (c) => myDialog(`安装「${c.name}」${c.version}?`), // 默认浏览器 confirm apiName: 'acme', // 原生 API 注入名;uTools 兼容层默认照常注入 window.utools }) await host.ready input.onchange = async () => { try { const rec = await host.install(input.files![0]) // File / Blob / Uint8Array;文件夹传 FileList() host.open(rec.id, { feature: 'main' }) } catch (e: any) { alert(e.message) // e.code:EWTINSTALL(策略 / 取消 / 描述文件)、EWTENCRYPTED(upxs) } } host.list() // 已安装的插件 host.close('some-id') // 关掉它的 iframe await host.uninstall('some-id', { purgeData: true }) ``` - `host.hooks` 上可以挂 `plugin:install`(`apply`,`args[0]` 是 `InstallCandidate`,返回 `false` 拒绝),接自己的审核或签名校验。 - 描述文件按其他平台的规则读取(`whatevertools.json` 优先,再 `plugin.json`),插件 id 一致;声明了 `targets` 且不含 `web` 的插件会被拒绝。 - 同一个 id 再装一次就是升级:新版本文件写完后才切换记录,已打开的 iframe 自动重新加载。 - preload 不需要构建:运行时用 `preloadMode: 'cjs'`,在浏览器里按 Node 规则 `require` 插件里的相对文件、JSON 和 `node_modules`。 ## 限制 - iframe 和宿主页同源,插件之间、插件和宿主之间不是安全边界(存储按插件 id 分开,但恶意插件能访问 `parent`)。只装可信的插件;需要和主站隔离时,把整个宿主(页面 + 作用域)放到单独的子域名下。 - 插件文件在 IndexedDB 里,受浏览器存储配额限制。 - preload 里用到浏览器端没有的 Node 能力(原生模块、真实子进程)时会报错;需要的话接 `@whatevertools/bridge`(通过 `boot` 选项传启动配置)。 MIT ## 在自己的页面里启动运行时 ```ts import { boot } from '@whatevertools/web/full' // 带 uTools 兼容层和默认适配器 const rt = await boot({ manifestFiles: { 'plugin.json': pluginJson }, files: { 'index.html': 1234, 'preload.js': 567 }, // 插件文件清单(相对路径 → 字节数) base: '/plugins/my-plugin/', apiName: 'acme', compat: [{ type: 'utools' }], preloadMode: 'cjs', // preload 在浏览器里直接按 CommonJS 执行,不用预先打包 fileDialogs: { openDialog: 'enter' }, }) await rt.start() ``` --- # 插件包(upx) | 格式 | 是什么 | 支持 | |---|---|---| | `.upx` | gzip 压缩的 asar 归档(uTools 的离线插件包) | 读、写、安装、打包 | | `.asar` | Electron 归档(uTools 本地安装的插件就是这个格式) | 读、写、安装、打包 | | `.zip` | 普通压缩包(store / deflate) | 读、安装、打包 | | `.upxs` | uTools 加密并签名的安装包 | **不支持**:只能由 uTools 安装,请向作者索取 upx 或源码 | > 需要独立加载、调试一个已经打成 `.upxs` 的开发者包?另有一个与核心分开的调试工具 [whatevertools Inspector](https://github.com/mowtwo/whatevertools-inspector-dist/releases)(macOS,版本不稳定,仅针对离线开发者包,不公开解密细节,也不绕过任何付费校验)。whatevertools 核心本身仍不支持 `.upxs`。 ## 兼容安装模式 加了 SDK 的平台可以动态安装任意插件包,策略可配: ```ts install: { enabled: true, max: 10, // 最多装几个(升级不算新增) allow: ['acme-*'], // 只允许这些 id deny: ['bad-*'], // 禁止这些 id(优先于 allow) formats: ['upx', 'asar'], confirm: true, // 安装前让用户确认(显示名字、版本、插件声明的权限用途) } ``` - **Electron**:`host.installer.install(file)` / `uninstall(id)` / `list()`;装进数据目录,重启自动加载,同 id 再装就是升级。启动器默认允许安装,独立应用默认不允许。 - **网页**:`@whatevertools/web-host` 的 `install(file)`,文件存进 IndexedDB,由 Service Worker 提供,见 [给网站加插件](./web-host)。 - **自定义审核**:`plugin:install` hook 返回 `false` 拒绝安装,可以接签名校验、白名单服务或你自己的确认界面。 ## 固定打包模式 一个或几个插件直接打成一个产物,用户拿到就能用: ```bash # 一个 upx 打成 Electron 独立应用 / 网页 npx whatevertools pack ./translate.upx --target electron npx whatevertools pack ./translate.upx --target web # 几个插件打成一个「工具箱」应用(启动器模式)或一个带入口页的网站 npx whatevertools pack ocr.upx translate.upx ./my-plugin --target electron --product 工具箱 --package toolbox npx whatevertools pack ocr.upx translate.upx --target web --product 工具箱 --package toolbox # 打包后的应用还允许用户再装插件 npx whatevertools pack a.upx b.upx --target electron --product 工具箱 --allow-install --max-plugins 20 ``` ## 编程接口 ```ts import { openPackage, createUpx, readAsar, writeAsar } from '@whatevertools/upx' // 浏览器和 Node 都能用 import { extractPackage, packDirectory } from '@whatevertools/upx/node' const pkg = await openPackage(bytes, { name: 'x.upx' }) // { format, files, manifest } const upx = await packDirectory('./dist') // 插件目录 → .upx ``` --- # hook 与权限 --- # 网页适配器 网页平台没有 Node 和 Electron。缺的能力由一组**可替换的适配器包**补上:运行时默认接入,宿主可以换成自己的实现,也可以关掉。 | 包 | 补的能力 | 默认 | |---|---|---| | `@whatevertools/fs-virtual` | `fs` / `fs/promises`:内存常驻、IndexedDB 持久化的 POSIX 文件系统;`/drop` `/picked` `/downloads` `/tmp` 不落盘 | 是 | | `@whatevertools/db-indexeddb` | 插件数据库 `utools.db` / `dbStorage` | 是 | | `@whatevertools/sharp-canvas` | `utools.sharp`:Canvas 实现的常用子集 | full 入口默认 | | `@whatevertools/web-files` | `showOpenDialog` / `showSaveDialog` / `dialog.openAsync` / `shell.openPath`:浏览器文件选择器和下载 | 是(`fileDialogs: false` 关闭) | | `@whatevertools/shim-node` / `shim-electron` | `path` `os` `events` `util` `crypto` `buffer`……以及 `ipcRenderer` `clipboard` `nativeImage` | 是 | | `@whatevertools/bridge` + `bridge-server` | 真实文件系统、子进程、系统剪贴板、shell:通过本地守护进程,守护进程用系统对话框问权限 | 否(配置 `bridge`) | | `@whatevertools/sync-http` / `sync-folder` | 数据同步 | 否(配置 `sync`) | ## sharp-canvas Browser implementation of a practical subset of the sharp image API on top of HTML Canvas 在浏览器里用 Canvas 实现 [sharp](https://sharp.pixelplumbing.com/) 的常用子集,Web 运行时把它注入成 `utools.sharp` / `image.sharp`,让用了 sharp 的 uTools 插件在浏览器里照常工作。 这是 [whatevertools](https://github.com/mowtwo/whatevertools) 的一部分,用法和架构见仓库 README。 ## 用法 ```ts import { createCanvasSharp } from '@whatevertools/sharp-canvas' const sharp = createCanvasSharp({ readFile: (p) => fs.readFileSync(p), // 路径输入 writeFile: (p, d) => fs.writeFileSync(p, d), // toFile Buffer, // 有 Buffer shim 时输出用 Buffer.from 包装 }) const png = await sharp(input).resize(256, 256, { fit: 'contain', background: 'transparent' }).png().toBuffer() ``` 有 `document` 时用 ``,否则用 `OffscreenCanvas`(Worker 里也能跑,但 SVG 输入需要 `document`)。 ## 支持的内容 - 输入:文件路径(`io.readFile`)、Buffer / Uint8Array / ArrayBuffer(按魔数识别 png / jpeg / webp / gif / bmp / avif / svg / ico,能否解码取决于浏览器)、`{ create: { width, height, channels: 3|4, background } }`、`{ raw: { width, height, channels: 1~4 } }` 原始像素。`density` 对 SVG 生效;`failOn`、`limitInputPixels`、`animated` 等选项接受但忽略。 - 信息:`metadata()`(含 jpeg 的 EXIF `orientation`、`autoOrient`、PNG/JPEG 的 DPI)、`stats()`。 - 几何:`resize`(cover / contain / fill / inside / outside、position / gravity / strategy、`kernel: 'nearest'`、withoutEnlargement / withoutReduction)、`extend`、`extract`、`trim`(info 里带 trimOffsetLeft / trimOffsetTop)、`rotate`(90 的倍数无损;任意角度扩成外接矩形)、`autoOrient`、`flip`、`flop`。 - 通道:`flatten`、`ensureAlpha`、`removeAlpha`、`extractChannel`、`toColourspace('b-w')`。 - 合成:`composite`(输入同上,top/left 或 gravity、`tile`、各种 blend;in / dest-in 等模式覆盖层以外按透明处理,与 libvips 一致)。 - 像素:`blur`、`sharpen`、`median`、`grayscale` / `greyscale`、`negate`、`modulate`、`tint`、`gamma`、`normalise` / `normalize`、`threshold`、`linear`。 - 输出:`png`、`jpeg` / `jpg`、`webp`、`avif`、`heif`、`gif`、`tiff`、`raw`、`toFormat`;`toBuffer()`(支持 `resolveWithObject` 和 node 风格回调)、`toFile()`(按扩展名选格式,经 `io.writeFile` 写出)。默认输出格式跟随输入(raw / create 输入默认 raw,不能编码的格式退回 png)。 - 其他:`clone`;`withMetadata` / `keepMetadata` / `withIccProfile` / `timeout` 等为空操作;静态 `cache()`、`concurrency()`、`simd()`、`counters()`、`versions`、`format`、`gravity`、`strategy`、`position`、`fit`、`kernel`。 ## 与 sharp 的差异 - 操作严格按调用顺序执行(sharp 内部有固定顺序,比如 extract 写在 resize 前后含义不同、rotate 总在最前)。`rotate()` / `autoOrient()` 不论写在哪都在解码时生效。 - 没有 ICC / 色彩管理:全部按 sRGB 8 位处理,嵌入的 ICC、EXIF、XMP 都不会写入输出;16 位、CMYK、多页 / 动图(只取第一帧)不支持。 - 缩放用浏览器的插值(`imageSmoothingQuality: 'high'` + 大比例逐级减半),只有 `nearest` 和默认两档,与 lanczos3 结果有细微差别;JPEG / WebP 的压缩质量、文件体积和 libvips 不同,PNG 的 compressionLevel / palette 等选项忽略。 - Canvas 内部是预乘 alpha,完全透明像素的颜色会丢失,半透明像素有 ±1 的舍入误差。 - 能编码哪些格式取决于浏览器:png / jpeg / webp 普遍可用;AVIF、GIF、TIFF、HEIF 编码多数浏览器不支持,显式要求时抛 `code: 'EWTUNSUPPORTED'` 的错误。 - 输入 EXIF 方向只解析 jpeg;其他格式的 EXIF 方向交给浏览器。 - `stats().sharpness` 恒为 0;`normalise` 按每个颜色通道的百分位拉伸(sharp 按亮度);`withoutEnlargement` / `withoutReduction` 触发时整个 resize 跳过。 - 不支持:`text` 输入、多图 `join`、流式接口(pipe / stream)、`affine`、`convolve`、`recomb`、`boolean`、`joinChannel`、`bandbool`、`clahe`、`dilate`、`erode`、`unflatten`(这些方法调用时抛 `EWTUNSUPPORTED`)。 ## 测试 `pnpm test:sharp`:用 esbuild 打包,在无界面 Chrome 里跑真实断言。 MIT --- # API 支持程度(底座 v0.2.1) 由 `packages/core/src/apis.ts` 自动生成(`node scripts/gen-support.mjs`),不要手改。 支持程度:原生 = 行为一致;模拟 = 换了实现但语义一致;部分 = 只支持一部分参数或场景;占位 = 不报错但没有真实效果;— = 不支持(按宿主策略抛错或警告)。 运行时可以用 `whatevertools.supports(name)` 查询(会结合当前宿主实际配置,比如没有配置 AI 提供者时 `ai.chat` 返回 false)。 ## lifecycle | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `lifecycle.onEnter` | `onPluginEnter` | | 原生 | 模拟 | | | `lifecycle.onLeave` | `onPluginOut` | | 原生 | 模拟 | | | `lifecycle.onReady` | `onPluginReady` | | 原生 | 模拟 | | | `lifecycle.onDetach` | `onPluginDetach` | | 模拟 | 占位 | 启动器没有分离窗口时不会触发 | | `lifecycle.onSearchPush` | `onMainPush` | | 模拟 | — | | | `lifecycle.exit` | `outPlugin` | | 原生 | 模拟 | | ## events | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `schedule.onTrigger` | `onScheduleTrigger` | `schedule` | 模拟 | 部分 | | ## panel | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `panel.hide` | `hideMainWindow` | | 原生 | — | | | `panel.show` | `showMainWindow` | | 原生 | — | | | `panel.setHeight` | `setExpandHeight` | | 模拟 | 占位 | | | `panel.input.set` | `setSubInput` | | 模拟 | — | | | `panel.input.remove` | `removeSubInput` | | 模拟 | — | | | `panel.input.setValue` | `setSubInputValue` | | 模拟 | — | | | `panel.input.focus` | `subInputFocus` | | 模拟 | — | | | `panel.input.blur` | `subInputBlur` | | 模拟 | — | | | `panel.input.select` | `subInputSelect` | | 模拟 | — | | ## window | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `window.create` | `createBrowserWindow` | `window.create` | 原生 | 部分 | web 端用 window.open / iframe 模拟 | | `window.sendToParent` | `sendToParent` | | 原生 | 模拟 | | | `window.type` | `getWindowType` | | 原生 | 模拟 | | ## page | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `page.find` | `findInPage` | | 原生 | — | | | `page.stopFind` | `stopFindInPage` | | 原生 | — | | ## system | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `system.notify` | `showNotification` | `notification` | 原生 | 模拟 | | | `dialog.open` | `showOpenDialog` | `dialog` | 原生 | 部分 | web:选目录且带 createDirectory 时返回下载目录;其他情况浏览器没法同步选文件,返回 undefined 并异步弹出选择器,选中的文件按 WebBootConfig.fileDialogs.openDialog 交给插件(默认作为 files 进入)。新代码请用 dialog.openAsync | | `dialog.save` | `showSaveDialog` | `dialog` | 原生 | 模拟 | web:返回 /downloads 下的路径,写进这个路径的文件会由浏览器下载 | | `dialog.openAsync` | | `dialog` | 原生 | 模拟 | web:浏览器文件选择器,选中的文件放在虚拟文件系统 /picked 下(0.2.0 起) | | `dialog.saveAsync` | | `dialog` | 原生 | 模拟 | 同 dialog.save(0.2.0 起) | | `shell.openPath` | `shellOpenPath` | `shell.open` | 原生 | 部分 | web:只对 /downloads 下的文件有效(重新下载) | | `shell.trash` | `shellTrashItem` | `shell.trash` | 原生 | — | | | `shell.showInFolder` | `shellShowItemInFolder` | `shell.open` | 原生 | 占位 | web:浏览器管理下载位置,调用不报错也没有效果 | | `shell.openExternal` | `shellOpenExternal` | `shell.external` | 原生 | 模拟 | | | `shell.beep` | `shellBeep` | | 原生 | — | | | `system.appName` | `getAppName` | | 原生 | 模拟 | | | `system.appVersion` | `getAppVersion` | | 原生 | 模拟 | 返回 identity.mock.platform.version | | `paths.get` | `getPath` | | 原生 | 占位 | | | `system.fileIcon` | `getFileIcon` | | 原生 | — | | | `system.fileIconAsync` | `getFileIconAsync` | | 原生 | — | | | `paths.data` | `getPluginLocalDataPath` | `fs.plugin` | 原生 | 模拟 | | | `paths.temp` | `getPluginTempPath` | `fs.plugin` | 原生 | 模拟 | | | `system.isDev` | `isDev` | | 原生 | 模拟 | | | `system.isMac` | `isMacOS` | | 原生 | 模拟 | | | `system.isWindows` | `isWindows` | | 原生 | 模拟 | | | `system.isLinux` | `isLinux` | | 原生 | 模拟 | | | `system.isDark` | `isDarkColors` | | 原生 | 模拟 | | | `system.onThemeChange` | | | 原生 | 模拟 | 宿主主题变化(跟随系统或宿主设置) | ## device | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `device.id` | `getNativeId` | `system.identity` | 模拟 | 模拟 | | ## account | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `account.user` | `getUser` | `system.identity` | 模拟 | 模拟 | 宿主可通过 hook 提供用户信息,默认 null | ## context | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `context.activeFolder` | `readCurrentFolderPath` | `system.context` | 部分 | — | | | `context.activeBrowserUrl` | `readCurrentBrowserUrl` | `system.context` | 部分 | — | | | `context.activeWindow` | | `system.context` | 部分 | — | | ## clipboard | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `clipboard.writeText` | `copyText` | `clipboard.write` | 原生 | 模拟 | | | `clipboard.writeImage` | `copyImage` | `clipboard.write` | 原生 | 模拟 | | | `clipboard.writeFiles` | `copyFile` | `clipboard.write` | 原生 | — | | | `clipboard.readFiles` | `getCopiedFiles` | `clipboard.read` | 原生 | — | | ## input | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `input.sendText` | `hideMainWindowPasteText` | `input.paste` | 模拟 | — | | | `input.sendFiles` | `hideMainWindowPasteFile` | `input.paste` | 模拟 | — | | | `input.sendImage` | `hideMainWindowPasteImage` | `input.paste` | 模拟 | — | | | `input.typeText` | `hideMainWindowTypeString` | `input.paste` | 模拟 | — | | | `input.startDrag` | `startDrag` | `input.drag` | 原生 | — | | | `input.keyTap` | `simulateKeyboardTap` | `input.simulate` | 部分 | — | 系统级按键:macOS AppleScript / Windows SendKeys / Linux xdotool,或 providers.input | | `input.mouseMove` | `simulateMouseMove` | `input.simulate` | 占位 | — | | | `input.mouseClick` | `simulateMouseClick` | `input.simulate` | 占位 | — | | | `input.mouseDoubleClick` | `simulateMouseDoubleClick` | `input.simulate` | 占位 | — | | | `input.mouseRightClick` | `simulateMouseRightClick` | `input.simulate` | 占位 | — | | ## screen | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `screen.captureRegion` | `screenCapture` | `screen.captureDisplay` | 模拟 | — | | | `screen.captureDisplay` | `captureDisplay` | `screen.captureDisplay` | 原生 | — | | | `screen.pickColor` | `screenColorPick` | `screen.captureDisplay` | 模拟 | — | | | `screen.captureSources` | `desktopCaptureSources` | `screen.captureDisplay` | 原生 | — | | | `screen.displays` | `getAllDisplays` | `screen.read` | 原生 | 模拟 | | | `screen.primaryDisplay` | `getPrimaryDisplay` | `screen.read` | 原生 | 模拟 | | | `screen.cursor` | `getCursorScreenPoint` | `screen.read` | 原生 | 部分 | | | `screen.nearestDisplay` | `getDisplayNearestPoint` | `screen.read` | 原生 | 模拟 | | | `screen.matchingDisplay` | `getDisplayMatching` | `screen.read` | 原生 | 模拟 | | | `screen.toDipPoint` | `screenToDipPoint` | `screen.read` | 原生 | 模拟 | | | `screen.toScreenPoint` | `dipToScreenPoint` | `screen.read` | 原生 | 模拟 | | | `screen.toDipRect` | `screenToDipRect` | `screen.read` | 原生 | 模拟 | | | `screen.toScreenRect` | `dipToScreenRect` | `screen.read` | 原生 | 模拟 | | ## db | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `db.put` | `db.put` | `storage` | 原生 | 模拟 | | | `db.get` | `db.get` | `storage` | 原生 | 模拟 | | | `db.remove` | `db.remove` | `storage` | 原生 | 模拟 | | | `db.bulk` | `db.bulkDocs` | `storage` | 原生 | 模拟 | | | `db.all` | `db.allDocs` | `storage` | 原生 | 模拟 | | | `db.putAttachment` | `db.postAttachment` | `storage` | 原生 | 部分 | | | `db.getAttachment` | `db.getAttachment` | `storage` | 原生 | 模拟 | | | `db.getAttachmentType` | `db.getAttachmentType` | `storage` | 原生 | 模拟 | | | `db.syncState` | `db.replicateStateFromCloud` | `storage` | 模拟 | 模拟 | 配置了同步适配器才有意义 | | `db.onPull` | `onDbPull` | `storage` | 模拟 | 模拟 | 配置了同步适配器才会触发 | | `kv.set` | `dbStorage.setItem` | `storage` | 原生 | 模拟 | | | `kv.get` | `dbStorage.getItem` | `storage` | 原生 | 模拟 | | | `kv.remove` | `dbStorage.removeItem` | `storage` | 原生 | 模拟 | | ## features | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `features.list` | `getFeatures` | | 原生 | 模拟 | | | `features.set` | `setFeature` | `features` | 原生 | 模拟 | | | `features.remove` | `removeFeature` | `features` | 原生 | 模拟 | | ## launch | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `launch.open` | `redirect` | `redirect` | 模拟 | — | | ## hotkeys | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `hotkeys.openSettings` | `redirectHotKeySetting` | | 模拟 | — | | | `hotkeys.get` | `getCmdHotKey` | | 模拟 | — | | ## schedule | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `schedule.request` | `requestSchedule` | `schedule` | 模拟 | 部分 | | | `schedule.list` | `getSchedules` | `schedule` | 模拟 | 部分 | | | `schedule.remove` | `removeSchedule` | `schedule` | 模拟 | 部分 | | ## ai | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `ai.chat` | `ai` | `ai` | 模拟 | 模拟 | 需要宿主配置模型提供方(hook: service:ai) | | `ai.models` | `allAiModels` | `ai` | 模拟 | 模拟 | | | `tools.register` | `registerTool` | | 模拟 | — | 注册不需要权限;外部 Agent 第一次调用时按 mcp 权限询问 | ## media | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `media.ffmpeg` | `runFFmpeg` | `media.ffmpeg` | 模拟 | 部分 | 使用宿主配置或 PATH 中的 ffmpeg;web 端可接 ffmpeg.wasm | | `image.sharp` | `sharp` | `image.sharp` | 模拟 | 部分 | electron:宿主安装的 sharp;web:@whatevertools/sharp-canvas(Canvas 实现的常用子集,不支持 AVIF 编码、ICC、动图),也可以换成 wasm 版 | ## payment | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `billing.isPurchased` | `isPurchasedUser` | `payment` | 占位 | 占位 | 默认 true,宿主可 hook | | `billing.purchase` | `openPurchase` | `payment` | 占位 | 占位 | | ## ubrowser | API | uTools 旧名 | 权限 | Electron | Web | 说明 | |---|---|---|---|---|---| | `browser.automate` | `ubrowser` | `browser` | 部分 | — | 用隐藏的 BrowserWindow 实现常用链式指令 | | `browser.idle` | `getIdleUBrowsers` | `browser` | 模拟 | — | | | `browser.setProxy` | `setUBrowserProxy` | `browser` | 模拟 | — | | | `browser.clearCache` | `clearUBrowserCache` | `browser` | 模拟 | — | | ## 权限 默认询问;只有插件自己的沙盒(数据库、私有目录)默认放行。每一项都可以按插件单独设置。 | 权限 | 名称 | 说明 | 风险 | 默认 | |---|---|---|---|---| | `storage` | 插件数据库 | db / kv,只能访问自己的数据 | low | 允许 | | `fs.plugin` | 插件私有目录 | 读写插件自己的数据目录和临时目录 | low | 允许 | | `fs.read` | 读取文件 | 读取任意(或指定范围内的)本地文件 | medium | 询问 | | `fs.write` | 写入文件 | 创建、修改、删除任意(或指定范围内的)本地文件 | high | 询问 | | `clipboard.write` | 写入剪贴板 | clipboard.writeText / writeImage / writeFiles | low | 询问 | | `clipboard.read` | 读取剪贴板 | 读取剪贴板里的文字、图片和文件 | medium | 询问 | | `notification` | 系统通知 | system.notify | low | 询问 | | `dialog` | 文件对话框 | 弹出打开/保存对话框,用户选中的路径会临时授予读写 | low | 询问 | | `shell.open` | 打开文件 | 用系统默认程序打开文件、在访达/资源管理器中显示 | medium | 询问 | | `shell.external` | 打开链接 | 用浏览器打开网址 | low | 询问 | | `shell.trash` | 移到废纸篓 | shell.trash | high | 询问 | | `screen.read` | 屏幕信息 | 显示器列表、光标位置 | low | 询问 | | `screen.capture` | 截屏 | screen.selectRegion / capture / sources / pickColor | high | 询问 | | `window.create` | 独立窗口 | window.create | low | 询问 | | `window.overlay` | 覆盖层窗口 | 置顶、鼠标穿透、全屏透明、所有桌面可见 | medium | 询问 | | `input.paste` | 粘贴到其他应用 | input.pasteText / pasteFiles / pasteImage / typeText | medium | 询问 | | `input.simulate` | 模拟键鼠 | input.keyTap / mouseMove / mouseClick | high | 询问 | | `input.drag` | 拖出文件 | input.startDrag | low | 询问 | | `network` | 网络 | Node http/https/net/dgram/tls 模块(页面里的 fetch 不受影响) | medium | 询问 | | `process.spawn` | 运行程序 | child_process / worker_threads | high | 询问 | | `process.native` | 原生模块 | 加载 .node 原生扩展 | high | 询问 | | `electron.raw` | 未过滤的 Electron API | require("electron") 拿到完整对象,而不是兼容白名单;只能在权限设置里显式授予,不会弹窗询问 | high | 禁止 | | `system.identity` | 用户与设备信息 | system.user / deviceId | medium | 询问 | | `system.context` | 前台应用上下文 | system.activeFolder / activeBrowserUrl | medium | 询问 | | `schedule` | 定时任务 | schedule.request | low | 询问 | | `features` | 动态功能 | features.set / remove | low | 询问 | | `redirect` | 跳转其他插件 | launch.open,向其他插件传数据 | low | 询问 | | `ai` | AI 模型 | ai.chat / models,消耗宿主配置的模型额度 | medium | 询问 | | `mcp` | MCP 工具 | 把注册的工具暴露给外部 AI Agent(MCP) | medium | 询问 | | `browser` | 自动化浏览器 | browser.automate | high | 询问 | | `media.ffmpeg` | FFmpeg | media.ffmpeg | low | 询问 | | `image.sharp` | Sharp | image.sharp 图像处理 | low | 询问 | | `payment` | 付费 | billing.isPurchased / purchase | low | 询问 | | `media.camera` | 摄像头 | 页面通过 getUserMedia 使用摄像头 | high | 询问 | | `media.microphone` | 麦克风 | 页面通过 getUserMedia 使用麦克风 | high | 询问 | | `geolocation` | 位置 | 页面读取地理位置 | medium | 询问 | | `device.hardware` | 硬件设备 | WebHID / WebSerial / WebUSB / MIDI | high | 询问 | --- # 给 AI Agent whatevertools 为 AI 编程 Agent(Claude Code 等)准备了两样东西: ## Agent skill 一份 skill:什么时候该看哪份参考、动手前要知道的核心概念、工作规则(先查支持程度、不编造 API、凭据只走环境变量……),以及移植、宿主、打包、API 支持表四份参考。 ```bash npx whatevertools skill # 装进当前项目的 .claude/skills/whatevertools npx whatevertools skill --dir ~/.claude/skills # 装成全局 skill ``` 也可以直接下载:[SKILL.md](/skills/whatevertools/SKILL.md) · [porting.md](/skills/whatevertools/references/porting.md) · [hosts.md](/skills/whatevertools/references/hosts.md) · [packaging.md](/skills/whatevertools/references/packaging.md) · [api-support.md](/skills/whatevertools/references/api-support.md) ## llms.txt - [/llms.txt](/llms.txt):站点索引,按 [llms.txt 约定](https://llmstxt.org/) 列出每篇文档的地址和一句话说明。 - [/llms-full.txt](/llms-full.txt):所有文档合成的一个纯文本文件,直接塞进 Agent 的上下文。 ## 给 Agent 的提示 > 用 whatevertools 把 ./dist 里的 uTools 插件打成 Electron 安装包和网页版。先运行 `npx whatevertools check ./dist` 看兼容报告,网页不支持的 API 用 `supports()` 降级;只在桌面成立的功能在 whatevertools.json 里用 targets 排除 web。 --- # 快拼图 · 网页 图片拼接工具:等高 / 等宽拼接、宫格、自由网格、长图与瀑布流,图片编辑、网格预设、批量工作流。原本是 uTools 插件,**同一份代码**打包成网页后直接在浏览器里运行。
[全屏打开在线试用 ↗](/demo/quick-union-image/){target="_blank"} · [插件源码](https://github.com/mowtwo/utools-quick-union-image){target="_blank"} > 在线试用的数据只存在你的浏览器里(IndexedDB)。保存图片会触发浏览器下载。 ## 怎么做到的 ```bash npx whatevertools check ./dist # 用到 9 个 API:网页上 6 个完整支持,sharp 部分支持,2 个不支持(插件里都有判断) npx whatevertools pack ./dist --target web # 产出静态站点,部署到任何静态托管 ``` | uTools 能力 | 网页上由谁补 | |---|---| | `utools.sharp`(拼接、缩放、圆角遮罩、编辑) | `@whatevertools/sharp-canvas`:浏览器 Canvas 实现 | | `showSaveDialog` + 写文件 | `@whatevertools/web-files`:返回 `/downloads/…`,写完自动下载 | | `showOpenDialog` 选图片 | `dialog.openAsync`:浏览器文件选择器(插件里加了 5 行适配,uTools 构建不受影响) | | `require('fs')` / `path` | 虚拟文件系统 + Node 模块垫片 | | `utools.db` / `dbStorage` | IndexedDB | 插件只改了一处:打开文件时优先用异步 API。 ```ts async function openDialog(options) { const native = globalThis[Symbol.for('whatevertools.api')] // uTools 里没有,照旧走 showOpenDialog if (native?.dialog?.openAsync) return (await native.dialog.openAsync(options)) ?? [] return window.utools.showOpenDialog(options) ?? [] } ``` ## 网页版的限制 `check` 报告里网页不支持的两个 API:`registerTool`(MCP 工具,插件先判断再注册)和 `subInputBlur`(uTools 子输入框失焦,插件包在 try 里)。网页上没有这两个概念,不影响使用。 - **AI 编排**需要宿主配置 AI 模型提供者,在线试用没有配置。 - **MCP 工具**(给 AI Agent 调用拼图)只在桌面上有意义,网页版不注册。 - 浏览器不支持编码 **AVIF**,导出 AVIF 时会提示改用 png / jpeg / webp。 --- # 桌面晾衣绳 · Electron 在桌面顶部拉一根晾衣绳:挂便签、待办、时钟、计时器、文件夹和截图,还有桌面地毯和信号轮盘。原本是 uTools 插件,用 whatevertools 打包成**不依赖 uTools 的 Electron 独立应用**。 [插件源码](https://github.com/mowtwo/utools-desktop-widgets){target="_blank"} ## 为什么只出桌面版 这个插件的核心是**透明、置顶、鼠标穿透的全屏悬浮层**:绳子和挂件浮在所有窗口上面,只有挂件本身能点,其他地方点下去落到桌面。再加上桌面地毯、按显示器摆放、鼠标顶到屏幕边缘的检测、截图挂到绳上——这些浏览器都做不到。 所以插件在 `whatevertools.json` 里声明: ```json { "targets": ["utools", "electron"] } ``` `wt pack --target web` 会直接拒绝,网页宿主也不会安装它。 ## 打包 ```bash npx whatevertools pack ./dist --target electron --product 桌面晾衣绳 # 或者用配置文件出签名安装包 npx whatevertools build --target electron --binary ``` 在 Electron 宿主里,悬浮层窗口、权限询问(覆盖层、截屏、剪贴板……按插件单独授权)、定时任务、MCP 工具(让 AI Agent 挂便签、加待办、发信号)都能工作。 ## 致谢 晾衣绳造型参考 [Tendedero](https://github.com/alejandrobujan/tendedero)(MIT);桌面地毯移植自 [desktop-rug](https://github.com/josssphzhou/desktop-rug)(MIT,创意与视觉设计来自 Terkel);信号轮盘与特效移植自 [lolPing](https://github.com/darrenprx/lolPing)(MIT);信号图标来自 [game-icons.net](https://game-icons.net)(CC BY 3.0),音效来自 [Kenney](https://kenney.nl)(CC0)。