如何开发一个 Raycast 扩展?

原文链接

前言

是的,我已经将 Alfred 换为 Raycast 了,后者的免费功能足够好用。

但也没有完全丢掉 Alfred,还在用它的剪贴板历史功能。

此前看到一个帖子说:Alfred 用户一定要试试 Raycast。当时我不以为意。

用了七、八年的 Alfred 几乎没怎么"变过",有种不思进取的感觉。特别是近几年 AI 的发展,它好像还没反应过来一样。

对比

在使用 Alfred 时,用得最多的功能是:

  • Web Search - 快速跳转,通常用于搜索文档等。
  • Clipboard History - 剪贴板历史,没什么好说的,用过都说好用。
  • Workflow - 工作流,比如翻译、打开项目等,写过一两个插件,开发体验不好,能做的有限。

这些在 Raycast 都有替代方案,甚至更好。除此之外,Raycast 的界面更现代一些,跟最新的 macOS 风格更加契合。并且可以完全通过键盘完成一系列操作。

最重要的是,Raycast Extension 的开发体验比 Alfred 好太多了。官方有提供很多了 API,统一的 UI 风格,有统一的分发 Store,不用网上到处搜索。活跃度很高,目前扩展数量 2200+

比较麻烦的是,在 Raycast 发布扩展,需要经过官方审核,通过后才会上传到 Store,审核周期较慢。好处是能在一定程度上把控整体的风格,避免各扩展之间外观上参差不齐,能从官方角度提一些更好的交互建议。

开始之前

技术栈

Node.js + TypeScript + React

虽然使用 React 构建,但只能使用官方提供的一些 API 或内置组件,不能过多地自定义(可能是 React to macOS Native 的缘故吧),好处是 UI 风格较为统一。

安装扩展

  • 键入 Store 搜索安装
  • 键入 Import Extension 在本地导入

键入 Manage Extensions 可以管理本地扩展。

卸载扩展

  • 键入关键词,选中要卸载的扩展,在 Action Panel 选择 Uninstall Extension 操作即可,快捷键「⌃ + X」。
  • 键入 Manage Extension 可管理本地扩展。

在 Raycast 中卸载本地扩展(即非通过 Store 安装的扩展)时,本地扩展文件不会被删除。
扩展路径 ~/Library/Application Support/com.raycast.macos/extensions

创建扩展

键入 Create Extension 回车输入必要信息,便创建好了。

官方提供了多个模板,可以选择简单的 Show Detail 模板。

接着:

shell 复制代码
$ cd <your-extension-path>
$ npm install
$ npm run dev

调试扩展

当我们执行 npm run dev(即 ray develop)时,它会自动安装扩展,并以开发模式启动 Raycast 应用。

为了方便调试,在 Preferences - Advanced - Developer Tools 下勾选几个选项:

  • Auto-reload on Save
  • Open Raycast in development mode
  • Keep window always visible during development

这对开发体验尤为重要。一是开发过程中文件修改可以自动重载,二是使 Raycast 始终显示,以避免切换其他应用时窗口关闭。

在开发模式下,使用 console.log() 可以在终端上显示打印信息。

如果没有正确使用 API、组件或快捷键冲突,此处也会有 Warning 显示。

发布扩展

如果想要将你的扩展发布到 Store 上,是需要官方审核的。

为避免审核不通过,你的扩展应满足 Prepare an Extension for Store 要求。

二选一:

  • Fork 官方仓库,将你的扩展放在仓库 extensions 目录下,提交 PR。
  • 创建独立仓库,然后通过 ray publish 提交。

提交 PR 时,建议附上视频,或许可以更快地审核。我之前写了一个微信开发工具的扩展,由于他们没有小程序项目,流程上它们跑不通,可能会要求附上视频辅助审核,例如这个

If you add a new extension or command, include a screencast (or screenshot for straightforward changes). A good screencast will make the review much faster - especially if your extension requires registration in other services.

提交 PR 之后,机器人 greptile-apps 可能会提一些 Review 意见,按需调整即可。

想要吐槽的是,审核很慢,看 commit 记录一天只处理几个。

开发

Raycast Extension 官方开发文档

命名规范

遵循 Apple Style Guide,详见这里

示例:

  • 扩展标题:尽量使用名词,而不是动词,比如 WeChat DevTool。
  • 扩展描述:一句话准确、简洁地描述扩展程序的功能。
  • 命令标题:动名词组合,尽可能具体地描述命令的作用,比如 Open Project。
  • 命令副标题:为标题提供上下文信息。如果副标题几乎与命令标题重复,那么你可能不需要它。不指定时,该位置会显示扩展标题。
    • 示例:命令标题 Search Package,命令副标题 NPM,
    • 示例:命令标题 Search NPM Package,无命名副标题。
    • 以上示例,对于使用者来说,都是非常清晰明确的。
  • 命令描述:一句话准确、简洁地描述命令的功能。

本地化

很遗憾,目前 Raycast 扩展仅支持美式英文,不支持本地化语言,更多请看这里

不上传到 Store 可忽略。

扩展信息

基本上都在 package.json 声明,列举一些基本字段:

json 复制代码
{
  "name": "wechat-devtool", // 包名
  "title": "WeChat DevTool", // 扩展标题
  "description": "Quickly open WeChat Mini Program projects via official CLI.", // 扩展描述
  "icon": "icon.png", // 扩展图标 512 × 512 的 PNG 图片
  "author": "tofrankie", // Raycast 账户用户名
  "contributors": ["someone"], // 贡献者 Raycast 用户名
  "license": "MIT", // 协议
  "categories": ["Developer Tools"], // 扩展分类
  "commands": [ // 命令列表
    {
      "name": "open-project", // 名称,跟入口文件同名
      "title": "Open Project", // 标题
      "description": "Open Mini Program projects via WeChat DevTool.", // 描述
      "mode": "view" // 模式
    },
    {
      "name": "configure-projects",
      "title": "Configure Projects",
      "description": "Configure Mini Program projects.",
      "mode": "view"
    }
  ],
  "preferences": [ // 扩展偏好设置
    {
      "name": "wechat-devtool-path", // 名称
      "type": "textfield", // 类型
      "title": "WeChat DevTool Path", // 标题
      "description": "The path to the WeChat DevTool executable.", // 描述
      "required": false // 是否必需
    }
  ],
  "keywords": [ // 关键词,可供 Store 搜索使用
    "Developer Tools",
    "WeChat",
    "WeChat DevTool",
    "WeChat Mini Program"
  ],
  // ...
}

目录结构:

txt 复制代码
.
├── assets                      # 静态资源目录
│   └── icon.png
├── CHANGELOG.md                # 变更日志
├── metadata                    # 扩展截图
│   ├── wechat-devtool-1.png
│   └── wechat-devtool-2.png
├── package.json                # 扩展信息等
├── README.md                   # About This Extension 会显示这个
├── src
│   ├── configure-projects.tsx  # 入口文件(跟 commands name 同名)
│   └── open-project.tsx        # 入口文件
└── tsconfig.json

一些注意点:

  • name :不能与 Store 已有扩展重复,可以在这里搜索一下。将用于发布后的扩展链接,比如: https://www.raycast.com/tofrankie/wechat-devtool
  • icon :图标放在 /assets 目录。其他需要跟扩展一同打包的静态资源都要放在此目录下。
  • author:是 Raycast 用户名,不是 GitHub 用户名。
  • license:若要发布到 Store,只能是 MIT 协议。
  • categories :至少选择一个,可选分类看这里
  • commands
    • mode :可选值 viewno-viewmenu-bar。比如,打开链接等不需要界面的命令操作,可以选择 no-view
  • preferences :偏好设置时可以同步的。
    • required :当设为 true 时,用户设置后才能使用其他命令。

更多请看 Extension PropertiesExtension Schemas

命令入口文件

入口文件放在 /src 目录下,文件名要与 commands[].name 保持一致。

命令为 view 模式,可以用官方提供的组件来构建界面,详见 User Interface

  • List 列表类型
  • Grid 网格类型
  • Detail 渲染 Markdown 内容、展示图片
  • Form 表单类型

虽然是用 React 编写的界面,但不能自由使用类似 div 或第三方组件库来构建复杂界面。

Action Panel

当我们使用 List、Detail、Form 等构建命令界面时,通常需要声明 Action Panel 来提供更多选项。

jsx 复制代码
<List.Item
  // others...
  actions={
    <ActionPanel>
      <Action
        title="Open Project"
        icon={Icon.Terminal}
        onAction={() => {
          // do something...
        }}
      />
      <Action.Push
        title="Go to Configuration"
        icon={Icon.Gear}
        target={<ConfigureProjects />}
      />
      <Action.CopyToClipboard
        title="Copy Project Path"
        content={project.path}
        shortcut={{ modifiers: ["cmd", "shift"], key: "," }}
      />
      <Action
        title="Delete Project"
        icon={Icon.Trash}
        style={Action.Style.Destructive}
        onAction={() => {
          // do something...
        }}
      />
    </ActionPanel>
  }
/>

Raycast 提供了 Action.OpenAction.PushAction.CopyToClipboardAction.OpenInBrowser 等一系列内置命令以轻松完成常用操作,更多请看这里

对于危险操作,可以声明为 Action.Style.Destructive 以高亮显示,可以配合 confirmAlert 二次确认。

Action 快捷键

在 Action Panel 中,将第一、第二个操作作为 Primary Action 和 Secondary Action,它们会自动分配快捷键。

  • 在 List、Grid、Detail 页面分别 Enter、⌘ + Enter
  • 在 Form 页面分别是 ⌘ + Enter、⌘ + ⇧ + Enter

设定 Action 快捷键时,可以参考 Raycast 官方推荐的常用快捷键,以便使用各插件有一致的用户体验,更多请看这里

Name macOS Windows
Copy ⌘ + ⇧ + C ctrl + shift + C
CopyDeeplink ⌘ + ⇧ + C ctrl + shift + C
CopyName ⌘ + ⇧ + . ctrl + alt + C
CopyPath ⌘ + ⇧ + , alt + shift + C
Save ⌘ + S ctrl + S
Duplicate ⌘ + D ctrl + shift + S
Edit ⌘ + E ctrl + E
MoveDown ⌘ + ⇧ + ↓ ctrl + shift + ↓
MoveUp ⌘ + ⇧ + ↑ ctrl + shift + ↑
New ⌘ + N ctrl + N
Open ⌘ + O ctrl + O
OpenWith ⌘ + ⇧ + O ctrl + shift + O
Pin ⌘ + ⇧ + P ctrl + .
Refresh ⌘ + R ctrl + R
Remove ⌃ + X ctrl + D
RemoveAll ⌃ + ⇧ + X ctrl + shift + D
ToggleQuickLook ⌘ + Y ctrl + Y

图标、图片

开发扩展免不了使用图片,Raycast 已经内置了很多图标可直接使用,详见这里

还可以指定远程图片、本地文件等。

ts 复制代码
type ImageLike = URL | Asset | Icon | FileIcon | Image

type ImageSource = URL | Asset | Icon | { light: URL | Asset; dark: URL | Asset }
  • URL:如 HTTP 链接
  • Assetassets 目录的图片文件
  • Icon:Raycast 内置的图标
  • FileIcon:该文件/目录在 Finder 所显示的图标
  • Image :类型如 ImageSource,还可以指定浅色、深色主题的图片,命名形式 icon.pngicon@dark.png

图片着色:

tsx 复制代码
import { Color, Icon, List } from "@raycast/api"

const tintedIcon = { source: Icon.RaycastLogoPos, tintColor: Color.Blue }

export default function Example() {
  return (
    <List>
      <List.Item title="Blue" icon={tintedIcon} />
    </List>
  )
}

本地 svg 图片同样也是可以着色的。

导航

  • 在 Action Panel 可以用 Action.Push 跳转目标页
  • 在页面可以使用 const { push, pop } = useNavigation() 跳转下一页或返回上一页
  • 使用 popToRoot() 可以返回 Raycast 根界面
  • 使用 closeMainWindow() 可以主动关闭 Raycast 窗口

搜索

使用 List 构建的页面带有一个搜索栏用于筛选,来源是 List.Item 的 titlekeywords 字段。

我发现,它没有根页面输入框那么智能,对筛选中文或拼音不太灵光的样子。

这种情况下,有两种解决方法:

  • 自定义搜索规则:searchText + onSearchTextChange
  • 丰富 keywords 内容

List Props

已后者为例,可以将项目名称、项目路径、项目名称拼音(若有中文)添加到 keywords 字段,参考 pinyin.ts

图片展示

若要展示一张图片,似乎只有 Detail + Markdown 的方式了。

tsx 复制代码
export default function ImageView({ url }: { url: string }) {
  // 可通过 example.png?raycast-width=250&raycast-height=250 指定宽高
  const markdown = `![](${url})`;
  return <Detail markdown={markdown} />;
}

表单

没什么好说的,文档很详尽了,请看这里

Toast、Loading、Alert

提供的 API 有:

  • showToast 有 Success、Failure、Animated 三种,后者为 Loading 圈圈。如果 Raycast 窗口关闭了,会回退到 showHUD
  • showFailureToast 显示错误信息优先选择这个
  • showHUD Raycast 窗口时在屏幕下方显示一条信息
  • confirmAlert 用于危险操作的二次确认弹窗

环境信息

可以在 environment 获取:

js 复制代码
import { useNavigation, showToast, Toast, showHUD, environment } from "@raycast/api";

environment.raycastVersion // 1.102.5
environment.ownerOrAuthorName // tofrankie
environment.extensionName // wechat-devtool
environment.commandName // open-project
environment.commandMode // view
environment.assetsPath // /Users/frankie/.config/raycast/extensions/wechat-devtool/assets
environment.supportPath // /Users/frankie/Library/Application Support/com.raycast.macos/extensions/wechat-devtool
environment.isDevelopment // true
environment.appearance // light
environment.textSize // medium
environment.launchType // userInitiated

扩展相关文件

  • assetsPath 扩展安装到 Raycast 的产物路径
  • supportPath 扩展相关文件,比如扩展记录一个配置文件,可以放在该目录下

需要注意的是,Raycast 不会同步 supportPath 目录的文件。

之前我想着把一些配置写入 supportPath 的文件,使其在 Raycast 中进行同步。但这是不行的,对扩展来说,只同步 preferences 的配置,而 preferences 又没办法动态更新。

Shell 环境

有时我们要在 Raycast 扩展中执行一些 Shell 命令。

下面是默认情况下的一些变量值。

shell 复制代码
$ echo $SHELL
/bin/zsh
shell 复制代码
$ echo $PATH
/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin:.
shell 复制代码
$ printenv
SUPPORT_PATH=/Users/frankie/Library/Application Support/com.raycast.macos/extensions/wechat-devtool
TMPDIR=/var/folders/z4/nxcp1z415jgff4rwrrf8v3y00000gn/T
ASSETS_PATH=/Users/frankie/.config/raycast/extensions/wechat-devtool/assets
LC_ALL=en_CN-u-hc-h23-u-ca-gregory-u-nu-latn
__CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34
EXTENSION_NAME=wechat-devtool
RAYCAST_VERSION=1.102.5
PWD=/
FAVICON_PROVIDER=legacy
NODE_PATH=/Applications/Raycast.app/Contents/Resources/RaycastNodeExtensions_RaycastNodeExtensions.bundle/Contents/Resources/api/node_modules
NODE_ENV=development
SHLVL=1
HOME=/Users/frankie
COMMAND_NAME=open-project
RAYCAST_BUNDLE_ID=com.raycast.macos
_=/usr/bin/printenv

通常我们会在 ~/.zshrc 中配置 export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" 以使用 Homebrew 安装的命令,但 Raycast 扩展执行 Shell 不会加载 ~/.zshrc 因此无法直接使用里面的命令。

可以这样处理(仅供参考):

ts 复制代码
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

function execCommand(command: string, cwd?: sting) {
  const env = getEnv();
  await execAsync(command, { cwd, env });
  // do something...
}

function getEnv() {
  return { ...process.env, PATH: joinHomebrewPath() };
}

function joinHomebrewPath() {
  return [process.env.PATH, "/opt/homebrew/bin", "/opt/homebrew/sbin"].filter(Boolean).join(":");
}

Change Log

开发完成后,需要在 CHANGELOG.md 补充一条变更记录。

遵循 Keep a Changelog 写法,要求如下:

  • 格式必须是 ## [xxx] - {PR_MERGE_DATE}
  • [] 内的为本次变更的标题
  • {PR_MERGE_DATE} 为合并日期。因为从提交 PR 到审核通过可能要好几天,被合并时会自动替换

更多请看这里

text 复制代码
# WeChat DevTool Changelog

## [New Version] - {PR_MERGE_DATE}

- Add something
- Improve something
- Fix something

## [First Release] - 2025-07-11

- **Open Project** - Open configured mini program project via WeChat DevTool CLI.
- **Graphical Configuration** - Complete graphical interface for dynamic project management.

截图

存放在 metadata 目录下,可提供 1 ~ 6 张截图。

Raycast 内置提供了截屏功能,在 Raycast Settings - Advanced - Window Capture 中设置快捷键(如 ⌥ + ⇧ + ⌘ + M)。

npm run dev 模式下启动扩展,按下快捷键,便可调出截屏窗口,并勾选上「Save to Metadata」,就会自动保存到扩展的 metadata 目录,更多请看这里

Raycast 提供了一些高清壁纸,请看这里

贡献

如果想对已发布的扩展作出贡献:

  1. fork raycast/extensions
  2. 功能调整...
  3. 将你的 Raycast 用户名添加到 package.json 的 contributors 字段
  4. 提交 PR,等待审核发布

由于审核较慢,如果同时多个扩展做出贡献,可以创建不同的分支去处理,如 ext/extension-a、ext/extension-b。

相关推荐
Delroy5 分钟前
CSS Grid布局:从魔方拼图到网页设计大师 🎯
前端·css
拜晨12 分钟前
类型体操的实践与总结: 从useInfiniteScroll 到 InfiniteList
前端·typescript
月弦笙音16 分钟前
【XSS】后端服务已经加了放xss攻击,前端还需要加么?
前端·javascript·xss
天下琴川17 分钟前
Dify智能体平台二次开发笔记(10):企业微信5.0 智能机器人对接 Dify 智能体
笔记·机器人·企业微信
code_Bo19 分钟前
基于vueflow实现动态添加标记的装置图
前端·javascript·vue.js
njsgcs29 分钟前
部署网页在服务器(公网)上笔记 infinityfree 写一个找工作单html文件的网站
笔记
传奇开心果编程1 小时前
【传奇开心果系列】Flet框架实现的图形化界面的PDF转word转换器办公小工具自定义模板
前端·python·学习·ui·前端框架·pdf·word
IT_陈寒2 小时前
Python开发者必知的5个高效技巧,让你的代码速度提升50%!
前端·人工智能·后端
zm4352 小时前
浅记Monaco-editor 初体验
前端