跳转至

嵌入式 QuickJS 中的个人命令

状态: 已实现。Bun 已从 Hex 中移除:用户的配置现在在 QuickJS 中进程内运行,没有外部运行时,也没有子进程。

Bun 此前承载的内容

在此变更之前,personal_commands 会在一个预置的 ~/.config/hex 工作区(package.json、bun.lock、node_modules、.hex-sdk、Effect 固定到 SDK 版本、SDK 变更时执行 bun update)中,于受管的 @hex/commands SDK(sdk/commands:macOS 捆绑包中构建的 dist,Windows 和 Linux 上嵌入的 TypeScript src)上运行 Bun。

Rust 通过 NDJSON 与宿主通信:一次注册,然后是 invoke 和 transform 请求、每个原生动作的 toolCall 帧和 toolResult 回复、世代、退役以及重启退避。

配置此前可以做到的事情:

  • 命令: phrases、group、description、when(应用程序或浏览器宿主)、类型化的 captures(digit、letter、choice、union、text),以及固定的 action 或 run 处理器。处理器接收 { hex, context, captures },其中 hex.openUrl/openApplication/openPath/press/typeText 会发送原生工具调用并以其结果解析。Effect 处理器使用 @hex/commands/effect。
  • 变换: (text, context) => string | Promise<string>,作为一个阶段链接。
  • 听写控制: 声明式的 start/stop/send/cancel 短语。
  • Bun 提供的任何能力: npm 包、fetch、文件系统、子进程。

Rust 此前已经负责验证:捕获编译、重叠检测、受保护短语、目录以及动作验证。内置变换本来就是原生的。

决策

  • 引擎: rquickjs 0.14(QuickJS-ng 0.16.2),仅启用 std 和 loader 特性。它为每个 Hex 目标(Linux x64/ARM64、macOS ARM64、Windows x64/ARM64)提供预生成的绑定,因此构建无需 bindgen。
  • 单个工作线程 拥有运行时。一次加载会构建全新的运行时和上下文,对配置模块图求值一次(QuickJS 此时将其编译为字节码;调用永远不会重新编译),并保留处理器和变换函数,直到下次加载将其替换。每次加载全新的运行时意味着旧配置的任何内容都不会在重载后存留,并且内存限制按配置生效。加载大约需要三分之一毫秒,因此将字节码缓存到磁盘没有任何收益。
  • 限制: 中断处理器会在 1 秒后停止任何调用,2 秒后停止加载;运行时上限为 64 MiB,JS 栈为 1 MiB。
  • 无环境权限: 上下文仅包含标准 JavaScript,外加一个写入 Hex 日志的 console。没有文件系统、网络、进程、环境或定时器。导入解析到 @hex/commands 或配置文件夹内的文件(.ts、.js、.mjs、.json);其他任何内容都会被拒绝。
  • 命令返回动作。 处理器返回一个动作、一个列表或什么都不返回;hex.* 仍然存在并排队动作(立即解析),因此大多数现有处理器可以继续工作。Hex 验证每个动作,然后在处理器返回后于单独线程上的原生执行器上按顺序运行它们。工具调用协议,以及随之而来的延迟回复间隙(#20),已经消失。
  • 变换 是 (text, context) => string。接受在没有 I/O 的情况下落定的 Promise,因此 async 函数仍然可用;任何东西都不能等待任何东西。
  • 语言: 以 ES2023 模块为基线,TypeScript 通过擦除方式支持。

TypeScript:使用 oxc 的解析器进行擦除

在 apollo(x86_64,探测 crate 的剥离发布二进制,单独 rquickjs 对比添加每个选项)上测量:

rquickjs (QuickJS-ng)                  +1.30 MiB
oxc_parser + oxc_ast_visit (erasure)   +0.79 MiB
oxc_transformer + codegen + semantic   +2.63 MiB

擦除使用 oxc_parser 解析,并用空格覆盖每个仅类型(type-only)的跨度(即 ts-blank-space 方法),因此 QuickJS 运行的 JavaScript 保留了用户的行和列,其错误在没有源映射的情况下指向他们的文件。其体积是 transformer 的三分之一,且速度更快(starter 配置约 6 µs)。它只拒绝需要生成代码的语法:枚举、命名空间、构造函数参数属性和 import =;随附的 tsconfig.json 设置了 erasableSyntaxOnly,因此编辑器会标记相同的内容。带 JSDoc 的 JavaScript 可以节省 0.79 MiB,但现有配置和类型化的 defineHexConfig 捕获推断都是 TypeScript,因此擦除让它们保持原样加载。

对用户而言的变更

  • 无需安装。hex commands init(或 Create Config / Set up)会在缺失时写入 hex.config.ts、tsconfig.json,以及 Hex 拥有的 hex.d.ts(@hex/commands 的类型)和 AGENTS.md。
  • 现有的 hex.config.ts 文件只要仅使用 @hex/commands、本地文件和普通函数,就能按原样加载:模板、类型化捕获、hex.* 调用和变换都能工作。
  • 破坏性变更: Effect 处理器(@hex/commands/effect)、npm 包、Node/Bun API(fetch、fs、process、定时器)、读取动作结果或在动作之间穿插工作的处理器,以及 TypeScript 枚举或命名空间。旧的工作区文件(package.json、bun.lock、node_modules、.hex-sdk、.agents)会被忽略,可以删除。
  • hex commands check(macOS)会像 Hex 一样加载配置,并打印其注册的内容或失败原因,取代 bun run check。

测量

apollo、Linux x86_64、发布构建、starter 配置外加一个智能标点变换(五次正则替换),作用于 50 词的听写文本:

binary (stripped)          47,038,240 -> 49,029,408 bytes  (+1.90 MiB, +4.2%)
config load                p50 0.33 ms, p99 0.82 ms   // runtime + context + erase + evaluate + compile registry
memory per loaded config   about 0.5 MiB RSS
transformation, in engine  p50 20 µs, p99 32 µs
transformation, via worker p50 24 µs, p99 40 µs      // what dictation sees
built-in transformation    p50 40 ns                  // native, unchanged

它所取代的 Bun 宿主,在同一台机器上以相同方式测量:

host start to registration   87-98 ms (a child process)
host memory                  about 90 MiB RSS
transformation round trip    p50 23 µs, p99 209 µs   // stdin/stdout only, before Hex's own queueing

QuickJS 对 Bun 用 JIT 编译的代码采用解释执行,因此正则密集的转换中位耗时与 Bun 大致相同,但尾部分布要紧凑得多,而且没有进程启动延迟,也不会常驻数十 MB 内存。

验证

  • personal_commands.rs 中的测试用真实脚本驱动 worker:启动配置、通过编译后的注册表从语音文本解析出的捕获、返回并排队的行为、无效行为、发生失败的转换、时间和内存限制、带位置信息的语法和加载错误、保留最后有效配置的重载、重叠拒绝、过期的代次、导入,以及 Node API 的缺失。typescript.rs 测试类型擦除和被拒绝的语法。
  • Linux 测试和 clippy 在 apollo 上运行;Windows x64 和 ARM64 的 clippy 通过 apollo 上的 cargo xwin 运行;macOS 通过 PR CI 运行。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex