别高兴太早:147 个 Raycast 命令里 33 个在 Tinycast 上跑不起来
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
当一款开源启动器宣布"原生运行 Raycast 扩展"时,第一批冲上去试用的人往往会得到一个让人心情复杂的结果:扩展装上了、命令列出来了、一部分跑得又快又顺,另一部分却直接甩给你一行报错。Tinycast 的官方文档给出了一个罕见的、可以自行复现的诚实数字:在开发机上实测 37 个真实安装的扩展中,32 个能完整启动并渲染,147 个 view 命令中有 114 个正常运行——换句话说,有整整 33 个命令是跑不起来的。
这篇文章不打算替这 33 个失败辩护,而是想把"哪些能跑、哪些不能跑、为什么不能跑、升级前怎么预检"这四件事讲清楚。结论全部来自仓库源码(docs/features/extensions.md的实测数据、Scripts/raycast-runtime/的运行时实现),你可以拿着这些方法在自己机器上复现,不用信任何二手结论。
数字是诚实的:32/37 个扩展、114/147 个命令能跑
这个数字不是营销话术,而是刻在源码里的测试基线。在 docs/features/extensions.md 的"What's supported"一节末尾,原文写着:
Measured against the 37 extensions installed in a real Raycast on the development machine:32 extensions / 114 of 147 view commandsboot and render.
更关键的是这句话的后半段:Scripts/raycast-runtime/test.mjs <dir>和Scripts/run-tests.sh ext-test可以原样复现这个测量。也就是说,兼容性边界不是"开发者说能跑",而是一套任何人可跑的测试资产。
Tinycast 运行 Raycast 扩展的方式决定了这个边界的性质。它不内置 Electron、不依赖浏览器、也不需要 Node.js——扩展在 Raycast 里构建出来的是一个预编译 CommonJS bundle,Tinycast 用 macOS 自带的 JavaScriptCore(JSContext)执行这个 bundle,再通过自研的@raycast/apishim 和 Node polyfill 补齐外部依赖,最后把 React 渲染出的组件树翻译成原生界面。架构图在 docs/features/extensions.md 的"How it works"一节,简化为:
<command>.js (esbuild output, deps inlined) │ require("@raycast/api"), require("react"), require("node:fs"), … ▼ RaycastRuntime.generated.js ← React 19 + react-reconciler + @raycast/api shim + polyfills │ render tree as JSON ▲ dispatch(handlerId, args) ▼ │ ExtensionRuntime (JavaScriptCore) │ host calls ▼ │ ExtensionManager ── ExtensionHostBridge ── Clipboard / storage / toasts / fetch / exec运行时源码全部放在 Scripts/raycast-runtime/src/:api/components.js是组件面、api/system.js是剪贴板/存储/偏好等系统 API、node-shims.js是 Node 内建模块 shim、polyfills.js补 JavaScriptCore 缺失的全局对象(含 WebAssembly 的 promise 形式修补)。这套"零二进制成本"的路线换来的是极低的常驻资源占用——一个运行中的命令持有一个 JS 引擎,仅此而已,这也是 Tinycast 敢自称"tiny"的原因之一。
需要泼一盆冷水的是:"32/37"并不等于"能跑的 32 个扩展体验完全一致"。能启动和能完整交互之间,还有大量细节差异,后面会专门讲。
哪些扩展能完整渲染:UI 面和 Node 面都比想象中全
先说好消息。Tinycast 对 Raycast 组件面的覆盖几乎是"全组件"级别的。以 Scripts/raycast-runtime/src/api/components.js 和官方文档的"What's supported"为准:
- 列表与网格:
List(含Item、Section、EmptyView、Item.Detail、Dropdown)、Grid(含磁贴布局、Dropdown); - 详情与表单:
Detail(含Metadata的Label/Link/TagList/Separator)、Form全家桶(TextField、PasswordField、TextArea、Checkbox、Dropdown、TagPicker、DatePicker、FilePicker); - 动作系统:
ActionPanel(含Section、Submenu)与全部Action便捷变体(CopyToClipboard、Paste、Open、Push、SubmitForm、PickDate等),连废弃别名(ActionPanel.Item、CopyToClipboardAction…)都保留了,因为线上 bundle 还在用; - API 面:
Clipboard、LocalStorage、Cache、environment、getPreferenceValues、showToast、showHUD、confirmAlert、open、getSelectedText、launchCommand、useNavigation等。
Node 内建模块的覆盖同样深:path、fs(含fs/promises、流式createReadStream)、os、child_process(exec/spawn/同步四件套)、crypto(哈希/HMAC/PBKDF2/AES)、zlib、http/https(request/get/Agent)、stream全家、url、buffer、events、timers,以及WebAssembly和WebSocket全局。注意这不是简单的"能用就行"——文档里记录了三个被当作基准案例的硬核场景:
- Homebrew 扩展:用
stream-chain+stream-json构建 object-mode 管道解析包索引,再走fetch→pipeThrough→pipeline→fs.createWriteStream全链路; - Zotero 扩展:搜索数据库依赖 sql.js,即WebAssembly——JavaScriptCore 的 promise 形式在私有队列上永不 settle,Scripts/raycast-runtime/src/polyfills.js 里用同步构造器包了一层才救活;
- Home Assistant 扩展:
homeassistant.local的 mDNS 解析由 Swift 的getaddrinfo代为回答(见 Scripts/raycast-runtime/src/dgram.js 的"造假 UDP 包"实现),WebSocket 走URLSessionWebSocketTask。
这些都是文档中白纸黑字、可在Scripts/raycast-runtime/test.mjs里逐步复现的"完整渲染"案例。
33 个失败命令的共性:都断在四个缺口上
那 33 个跑不起来的命令,不是随机散布在各扩展里的"bug",而是集中在四个明确的技术缺口上。这是好消息:缺口是可枚举、可预判的,而不是薛定谔的兼容性。
缺口一:Raycast 专属服务没有本地对应物。这是最大的一类。打开 Scripts/raycast-runtime/src/api/index.js,你会看到一个清晰的取舍:
const AI = { ...rejectingNamespace("AI", ["ask"]), Model: Object.freeze({}), Creativity: Object.freeze({}), }; const BrowserExtension = rejectingNamespace("BrowserExtension", ["getContent", "getTabs"]); const WindowManagement = { DesktopType: nestedEnums.WindowManagement.DesktopType, ...rejectingNamespace("WindowManagement", ["getWindowsOnActiveDesktop", "getActiveWindow", "setWindowBounds", "getDesktops"]), };rejectingNamespace的实现很直白:命名空间照常导出,import不报错,但一旦调用就 reject 并给出明确原因。这意味着所有依赖AI.ask的 AI 类命令、依赖BrowserExtension读标签页的命令、依赖WindowManagement做窗口布局的命令,全部落在 33 个失败名单里。注意一个微妙点:Tinycast 自己内置了 34 个原生窗口管理命令(见 docs/features/window-management.md),但那是 Swift 直调 macOS Accessibility 的实现,跟"运行 Raycast 的窗口管理扩展"是两条完全不同的路——后者在 Tinycast 的扩展沙箱里就是调用WindowManagement.getWindowsOnActiveDesktop()并收到not supported。
缺口二:裸 socket 与流式 HTTP。Scripts/raycast-runtime/src/node-shims.js 顶部维护了一张UNSUPPORTED_EXPORTS表,net、tls、dns、vm、http2、domain等模块"resolve but throw on use"——bundle 只是require它们能正常加载,一旦真的用就抛错。文档的 gap 表里说得很清楚:net/tls没有任何 bridge 原始 socket;http.request是一次性返回整个 body,所以Server-Sent Events(SSE)、网络级进度、socket 背压全部不可达。依赖 SSE 的实时流类扩展、依赖tls做自签名证书客户端校验的扩展,都会在这一类里阵亡。fetch的AbortSignal倒是完整的,但信号不跨 bridge——调用方能拿到AbortError,底层的URLSessionTask还是会跑完,所以"超时"只约束调用方,不约束网络。
缺口三:交互式子进程。spawn的 stdout/stderr 是流式的,但 stdin 只在子进程启动的同一 tick 发送一次,之后的stdin.write会被丢弃。任何需要"启动一个交互式 CLI 并持续喂输入"的命令(REPL 类、密码交互类)都会卡在这一条。
缺口四:tools/入口未暴露。Raycast 生态里 AI 工具扩展(声明tools/目录、被 Raycast 的 AI 功能调用)在 Tinycast 里完全不上层,文档原话是 "Not surfaced"。
把这些缺口套回 33 这个数字,就能解释为什么它不是一个常数:你装了什么扩展,就有对应的失败命令,但失败的模式只有这几种。更重要的设计取向在 Scripts/raycast-runtime/src/api/system.js 的unsupported()里:
export function unsupported(what) { return Promise.reject( new Error(`${what} is not supported in Tinycast extensions yet. See docs/extensions.md.`), ); }所有缺口都是显式报错,而不是静默降级。一个命令不会"假装跑起来了但结果不对",它会直接告诉你断在哪。这在工程上是兼容层最难能可贵的性质——可验证、可诊断,用户不会在不知情的情况下拿到错误数据。
别被"能跑"骗了:三个最容易踩的隐形坑
33 个明确失败之外,114 个"能跑"的命令里还藏着三类只有真上手才会撞见的问题,它们比显式报错更难排查。
坑一:空参数有语义。文档里记录了一个很精彩的案例:Coffee 扩展的 "Caffeinate for…" 命令曾执行caffeinate -t NaN然后瞬间退出。根因是 Raycast 的契约——每个声明的 argument 都会被发送,未填写时是空字符串而不是 undefined。Number("")是0,Number(undefined)是NaN,Tinycast 忠实实现了空串契约(ExtensionCommand.completeArguments),反而是那些习惯性省略空参数的扩展自己写出了 bug。这类问题从 Tinycast/Features/Extensions/Model/ExtensionManifest.swift 的参数解析一路牵扯到具体命令的逻辑,是兼容性之外最常见的真实故障源。
坑二:JavaScriptCore 不是 V8。运行时两次栽过的差异都被记在文档里:Error.stack只含帧、不重复消息;MessageChannel缺失导致 React scheduler 回退到setTimeout。还有react-dom被显式置为"调用即抛错"(Scripts/raycast-runtime/src/index.js),因为 Tinycast 渲染扩展不走 DOM。依赖这些环境细节的扩展,即使逻辑正确也可能在 JSC 下表现异常。
坑三:OAuth 与第三方代码信任。社区里流传的"OAuth PKCE 全链路实现"需要打一个问号:检索整个 Tinycast/Features/Extensions/ 的 Swift 源码,扩展宿主侧(ExtensionHostBridge)的 dispatch 面只有 clipboard / storage / cache / window / feedback / system / fetch / websocket / dns / proc,没有任何 OAuth 流程;OAuth PKCE 基础设施(MCPOAuth*.swift一整套)属于 MCP 模块,服务于 AI 聊天连接工具服务器,与 Raycast 扩展的授权流程无关。一个在 Raycast 里通过 OAuth 登录的扩展,迁移到 Tinycast 后那条登录路径是不存在的。此外,扩展开关本身就是"运行第三方代码的同意",首次开启需要确认,且不会跟着设置备份自动迁移——这是刻在 docs/features/extensions.md 的 "Off means off" 不变量。
升级前用兼容性清单预检工作流
所以,正确的姿势不是"装完再撞",而是在把工作流迁移过来之前做一次预检。Tinycast 仓库提供了一条完整的验证链路,从上到下越来越接近真实环境:
第一步:看 manifest,过滤已知缺口。打开每个扩展的package.json,按命令逐个检查 mode 和代码里用到的 API。凡是在源码里能搜到AI.、WindowManagement.、BrowserExtension.、net.、tls.、http2.调用的命令,直接判为"迁移后有风险"。这类静态检查不需要跑任何代码。
第二步:JS 层渲染树验证。对任意一个已构建的扩展 bundle,运行:
node Scripts/raycast-runtime/test.mjs ~/.config/raycast/extensions/<uuid> [command]这个 harness(Scripts/raycast-runtime/test.mjs)在一个裸vm上下文里跑真实运行时,打印 Swift 端会收到的完整渲染树,任何unsupported调用都会以 failure 形式暴露出来。它还能用EXT_TEST_PREFS='{"version":"v8"}'模拟用户在设置里配的偏好值——很多扩展的代码路径被一个没有 manifest 默认值的偏好挡着,不喂这个值根本走不到。
第三步:真实引擎验证。JS 层通过不代表 JavaScriptCore 下也通过,用:
Scripts/run-tests.sh ext-testext-test编译的是真实引擎源码(没有第二份拷贝需要同步),EXT_TEST_VERBOSE=1能看到扩展自己的console.error。对于"装完第一次能跑、第二次卡在 Starting…"这类诡异问题,文档还专门解释过:那是复用 JSContext 导致 React scheduler 的 timer 被连带取消的历史 bug,现在的修复是"每命令一 context,跑完即弃"。
第四步:对着 gap 表做兜底方案。预检出失败的命令,先别急着弃用整个扩展——Tinycast 的定位是"启动器本身带原生功能",大量 Raycast 工作流在它身上有原生替代:窗口布局用内置的 34 个原生窗口命令,系统操作用内置的 31 个系统级命令,文件搜索走 Spotlight 白名单式 scopes(见 docs/features/file-search.md)。如果某个第三方扩展的独有能力正好踩在 AI / 浏览器 / 窗口管理的缺口中,那要么接受"这部分暂时没有",要么把它留在 Raycast 里做双轨。
最后记住一个时间维度:这 33 个缺口不是静态的。运行时源码就摆在 Scripts/raycast-runtime/src/,README级的构建命令写在文档末尾(node gen-enums.mjs→node build.mjs→ 提交Resources/RaycastRuntime.generated.js)。今天的 33 个失败,是明天某个 commit 就能缩小的数字——这正是"兼容性有明确边界、边界可测量"比"宣称完全兼容"更值得信任的地方。
别高兴太早:147 个 Raycast 命令里 33 个在 Tinycast 上跑不起来
当一款开源启动器宣布"原生运行 Raycast 扩展"时,第一批冲上去试用的人往往会得到一个让人心情复杂的结果:扩展装上了、命令列出来了、一部分跑得又快又顺,另一部分却直接甩给你一行报错。Tinycast 的官方文档给出了一个罕见的、可以自行复现的诚实数字:在开发机上实测 37 个真实安装的扩展中,32 个能完整启动并渲染,147 个 view 命令中有 114 个正常运行——换句话说,有整整 33 个命令是跑不起来的。
这篇文章不打算替这 33 个失败辩护,而是想把"哪些能跑、哪些不能跑、为什么不能跑、升级前怎么预检"这四件事讲清楚。结论全部来自仓库源码(docs/features/extensions.md 的实测数据、Scripts/raycast-runtime/ 的运行时实现),你可以拿着这些方法在自己机器上复现,不用信任何二手结论。
数字是诚实的:32/37 个扩展、114/147 个命令能跑
这个数字不是营销话术,而是刻在源码里的测试基线。在 docs/features/extensions.md 的"What's supported"一节末尾,原文写着:
Measured against the 37 extensions installed in a real Raycast on the development machine:32 extensions / 114 of 147 view commandsboot and render.
更关键的是这句话的后半段:Scripts/raycast-runtime/test.mjs <dir>和Scripts/run-tests.sh ext-test可以原样复现这个测量。也就是说,兼容性边界不是"开发者说能跑",而是一套任何人可跑的测试资产。
Tinycast 运行 Raycast 扩展的方式决定了这个边界的性质。它不内置 Electron、不依赖浏览器、也不需要 Node.js——扩展在 Raycast 里构建出来的是一个预编译 CommonJS bundle,Tinycast 用 macOS 自带的 JavaScriptCore(JSContext)执行这个 bundle,再通过自研的@raycast/apishim 和 Node polyfill 补齐外部依赖,最后把 React 渲染出的组件树翻译成原生界面。架构图在 docs/features/extensions.md 的"How it works"一节,简化为:
<command>.js (esbuild output, deps inlined) │ require("@raycast/api"), require("react"), require("node:fs"), … ▼ RaycastRuntime.generated.js ← React 19 + react-reconciler + @raycast/api shim + polyfills │ render tree as JSON ▲ dispatch(handlerId, args) ▼ │ ExtensionRuntime (JavaScriptCore) │ host calls ▼ │ ExtensionManager ── ExtensionHostBridge ── Clipboard / storage / toasts / fetch / exec运行时源码全部放在 Scripts/raycast-runtime/src/:api/components.js是组件面、api/system.js是剪贴板/存储/偏好等系统 API、node-shims.js是 Node 内建模块 shim、polyfills.js补 JavaScriptCore 缺失的全局对象(含 WebAssembly 的 promise 形式修补)。这套"零二进制成本"的路线换来的是极低的常驻资源占用——一个运行中的命令持有一个 JS 引擎,仅此而已,这也是 Tinycast 敢自称"tiny"的原因之一。
需要泼一盆冷水的是:"32/37"并不等于"能跑的 32 个扩展体验完全一致"。能启动和能完整交互之间,还有大量细节差异,后面会专门讲。
哪些扩展能完整渲染:UI 面和 Node 面都比想象中全
先说好消息。Tinycast 对 Raycast 组件面的覆盖几乎是"全组件"级别的。以 Scripts/raycast-runtime/src/api/components.js 和官方文档的"What's supported"为准:
- 列表与网格:
List(含Item、Section、EmptyView、Item.Detail、Dropdown)、Grid(含磁贴布局、Dropdown); - 详情与表单:
Detail(含Metadata的Label/Link/TagList/Separator)、Form全家桶(TextField、PasswordField、TextArea、Checkbox、Dropdown、TagPicker、DatePicker、FilePicker); - 动作系统:
ActionPanel(含Section、Submenu)与全部Action便捷变体(CopyToClipboard、Paste、Open、Push、SubmitForm、PickDate等),连废弃别名(ActionPanel.Item、CopyToClipboardAction…)都保留了,因为线上 bundle 还在用; - API 面:
Clipboard、LocalStorage、Cache、environment、getPreferenceValues、showToast、showHUD、confirmAlert、open、getSelectedText、launchCommand、useNavigation等。
Node 内建模块的覆盖同样深:path、fs(含fs/promises、流式createReadStream)、os、child_process(exec/spawn/同步四件套)、crypto(哈希/HMAC/PBKDF2/AES)、zlib、http/https(request/get/Agent)、stream全家、url、buffer、events、timers,以及WebAssembly和WebSocket全局。注意这不是简单的"能用就行"——文档里记录了三个被当作基准案例的硬核场景:
- Homebrew 扩展:用
stream-chain+stream-json构建 object-mode 管道解析包索引,再走fetch→pipeThrough→pipeline→fs.createWriteStream全链路; - Zotero 扩展:搜索数据库依赖 sql.js,即WebAssembly——JavaScriptCore 的 promise 形式在私有队列上永不 settle,Scripts/raycast-runtime/src/polyfills.js 里用同步构造器包了一层才救活;
- Home Assistant 扩展:
homeassistant.local的 mDNS 解析由 Swift 的getaddrinfo代为回答(见 Scripts/raycast-runtime/src/dgram.js 的"造假 UDP 包"实现),WebSocket 走URLSessionWebSocketTask。
这些都是文档中白纸黑字、可在Scripts/raycast-runtime/test.mjs里逐步复现的"完整渲染"案例。
33 个失败命令的共性:都断在四个缺口上
那 33 个跑不起来的命令,不是随机散布在各扩展里的"bug",而是集中在四个明确的技术缺口上。这是好消息:缺口是可枚举、可预判的,而不是薛定谔的兼容性。
缺口一:Raycast 专属服务没有本地对应物。这是最大的一类。打开 Scripts/raycast-runtime/src/api/index.js,你会看到一个清晰的取舍:
const AI = { ...rejectingNamespace("AI", ["ask"]), Model: Object.freeze({}), Creativity: Object.freeze({}), }; const BrowserExtension = rejectingNamespace("BrowserExtension", ["getContent", "getTabs"]); const WindowManagement = { DesktopType: nestedEnums.WindowManagement.DesktopType, ...rejectingNamespace("WindowManagement", ["getWindowsOnActiveDesktop", "getActiveWindow", "setWindowBounds", "getDesktops"]), };rejectingNamespace的实现很直白:命名空间照常导出,import不报错,但一旦调用就 reject 并给出明确原因。这意味着所有依赖AI.ask的 AI 类命令、依赖BrowserExtension读标签页的命令、依赖WindowManagement做窗口布局的命令,全部落在 33 个失败名单里。注意一个微妙点:Tinycast 自己内置了 34 个原生窗口管理命令(见 docs/features/window-management.md),但那是 Swift 直调 macOS Accessibility 的实现,跟"运行 Raycast 的窗口管理扩展"是两条完全不同的路——后者在 Tinycast 的扩展沙箱里就是调用WindowManagement.getWindowsOnActiveDesktop()并收到not supported。
缺口二:裸 socket 与流式 HTTP。Scripts/raycast-runtime/src/node-shims.js 顶部维护了一张UNSUPPORTED_EXPORTS表,net、tls、dns、vm、http2、domain等模块"resolve but throw on use"——bundle 只是require它们能正常加载,一旦真的用就抛错。文档的 gap 表里说得很清楚:net/tls没有任何 bridge 原始 socket;http.request是一次性返回整个 body,所以Server-Sent Events(SSE)、网络级进度、socket 背压全部不可达。依赖 SSE 的实时流类扩展、依赖tls做自签名证书客户端校验的扩展,都会在这一类里阵亡。fetch的AbortSignal倒是完整的,但信号不跨 bridge——调用方能拿到AbortError,底层的URLSessionTask还是会跑完,所以"超时"只约束调用方,不约束网络。
缺口三:交互式子进程。spawn的 stdout/stderr 是流式的,但 stdin 只在子进程启动的同一 tick 发送一次,之后的stdin.write会被丢弃。任何需要"启动一个交互式 CLI 并持续喂输入"的命令(REPL 类、密码交互类)都会卡在这一条。
缺口四:tools/入口未暴露。Raycast 生态里 AI 工具扩展(声明tools/目录、被 Raycast 的 AI 功能调用)在 Tinycast 里完全不上层,文档原话是 "Not surfaced"。
把这些缺口套回 33 这个数字,就能解释为什么它不是一个常数:你装了什么扩展,就有对应的失败命令,但失败的模式只有这几种。更重要的设计取向在 Scripts/raycast-runtime/src/api/system.js 的unsupported()里:
export function unsupported(what) { return Promise.reject( new Error(`${what} is not supported in Tinycast extensions yet. See docs/extensions.md.`), ); }所有缺口都是显式报错,而不是静默降级。一个命令不会"假装跑起来了但结果不对",它会直接告诉你断在哪。这在工程上是兼容层最难能可贵的性质——可验证、可诊断,用户不会在不知情的情况下拿到错误数据。
别被"能跑"骗了:三个最容易踩的隐形坑
33 个明确失败之外,114 个"能跑"的命令里还藏着三类只有真上手才会撞见的问题,它们比显式报错更难排查。
坑一:空参数有语义。文档里记录了一个很精彩的案例:Coffee 扩展的 "Caffeinate for…" 命令曾执行caffeinate -t NaN然后瞬间退出。根因是 Raycast 的契约——每个声明的 argument 都会被发送,未填写时是空字符串而不是 undefined。Number("")是0,Number(undefined)是NaN,Tinycast 忠实实现了空串契约(ExtensionCommand.completeArguments),反而是那些习惯性省略空参数的扩展自己写出了 bug。这类问题从 Tinycast/Features/Extensions/Model/ExtensionManifest.swift 的参数解析一路牵扯到具体命令的逻辑,是兼容性之外最常见的真实故障源。
坑二:JavaScriptCore 不是 V8。运行时两次栽过的差异都被记在文档里:Error.stack只含帧、不重复消息;MessageChannel缺失导致 React scheduler 回退到setTimeout。还有react-dom被显式置为"调用即抛错"(Scripts/raycast-runtime/src/index.js),因为 Tinycast 渲染扩展不走 DOM。依赖这些环境细节的扩展,即使逻辑正确也可能在 JSC 下表现异常。
坑三:OAuth 与第三方代码信任。社区里流传的"OAuth PKCE 全链路实现"需要打一个问号:检索整个 Tinycast/Features/Extensions/ 的 Swift 源码,扩展宿主侧(ExtensionHostBridge)的 dispatch 面只有 clipboard / storage / cache / window / feedback / system / fetch / websocket / dns / proc,没有任何 OAuth 流程;OAuth PKCE 基础设施(MCPOAuth*.swift一整套)属于 MCP 模块,服务于 AI 聊天连接工具服务器,与 Raycast 扩展的授权流程无关。一个在 Raycast 里通过 OAuth 登录的扩展,迁移到 Tinycast 后那条登录路径是不存在的。此外,扩展开关本身就是"运行第三方代码的同意",首次开启需要确认,且不会跟着设置备份自动迁移——这是刻在 docs/features/extensions.md 的 "Off means off" 不变量。
升级前用兼容性清单预检工作流
所以,正确的姿势不是"装完再撞",而是在把工作流迁移过来之前做一次预检。Tinycast 仓库提供了一条完整的验证链路,从上到下越来越接近真实环境:
第一步:看 manifest,过滤已知缺口。打开每个扩展的package.json,按命令逐个检查 mode 和代码里用到的 API。凡是在源码里能搜到AI.、WindowManagement.、BrowserExtension.、net.、tls.、http2.调用的命令,直接判为"迁移后有风险"。这类静态检查不需要跑任何代码。
第二步:JS 层渲染树验证。对任意一个已构建的扩展 bundle,运行:
node Scripts/raycast-runtime/test.mjs ~/.config/raycast/extensions/<uuid> [command]这个 harness(Scripts/raycast-runtime/test.mjs)在一个裸vm上下文里跑真实运行时,打印 Swift 端会收到的完整渲染树,任何unsupported调用都会以 failure 形式暴露出来。它还能用EXT_TEST_PREFS='{"version":"v8"}'模拟用户在设置里配的偏好值——很多扩展的代码路径被一个没有 manifest 默认值的偏好挡着,不喂这个值根本走不到。
第三步:真实引擎验证。JS 层通过不代表 JavaScriptCore 下也通过,用:
Scripts/run-tests.sh ext-testext-test编译的是真实引擎源码(没有第二份拷贝需要同步),EXT_TEST_VERBOSE=1能看到扩展自己的console.error。对于"装完第一次能跑、第二次卡在 Starting…"这类诡异问题,文档还专门解释过:那是复用 JSContext 导致 React scheduler 的 timer 被连带取消的历史 bug,现在的修复是"每命令一 context,跑完即弃"。
第四步:对着 gap 表做兜底方案。预检出失败的命令,先别急着弃用整个扩展——Tinycast 的定位是"启动器本身带原生功能",大量 Raycast 工作流在它身上有原生替代:窗口布局用内置的 34 个原生窗口命令,系统操作用内置的 31 个系统级命令,文件搜索走 Spotlight 白名单式 scopes(见 docs/features/file-search.md)。如果某个第三方扩展的独有能力正好踩在 AI / 浏览器 / 窗口管理的缺口中,那要么接受"这部分暂时没有",要么把它留在 Raycast 里做双轨。
最后记住一个时间维度:这 33 个缺口不是静态的。运行时源码就摆在 Scripts/raycast-runtime/src/,构建命令写在文档末尾(node gen-enums.mjs→node build.mjs→ 提交Resources/RaycastRuntime.generated.js)。今天的 33 个失败,是明天某个 commit 就能缩小的数字——这正是"兼容性有明确边界、边界可测量"比"宣称完全兼容"更值得信任的地方。
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考