PAYATHON 2026

Tauri v2 在 macOS 上如何打开自定义 slack:// URL?

排障赵师傅

结论

在 Tauri v2 中,slack:// 属于 URL scheme,应使用 Opener plugin 的 openUrl()。openPath() 用于处理文件路径。

如果当前版本的 Opener plugin 不允许使用自定义 scheme,可以选择以下方案:

  1. 在 Shell plugin 中为 /usr/bin/open 配置 shell:allow-execute。
  2. 在 Rust 端实现经过严格校验的 Tauri command,再调用 macOS 的 /usr/bin/open。这种方案可以更细致地限制协议和参数,适合作为稳定的兜底方式。

Slack URL 的参数之间必须有 &:

slack://user?team=TEAM_ID&id=USER_ID

下面这种写法是错误的:

slack://user?team=TEAM_IDid=USER_ID

优先使用 Opener plugin 的 openUrl()

前端代码可以改为:

import { openUrl } from '@tauri-apps/plugin-opener';

const params = new URLSearchParams({
  team: 'EDUA2R4KG',
  id: 'WR9RY8KMF',
});

await openUrl(`slack://user?${params.toString()}`);

URLSearchParams 会正确编码参数,也能避免漏写 &。

接着,在 src-tauri/capabilities/default.json 中允许 Opener plugin 打开 Slack scheme:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "opener:allow-open-url",
      "allow": [
        {
          "url": "slack://*"
        }
      ]
    }
  ]
}

不同版本的 Opener plugin 对 URL scope pattern 的校验可能不同。如果 slack://* 无法通过构建时的 schema 校验,或运行时仍拒绝该协议,请以项目生成的 capability schema 为准,并改用后面的 Shell 或 Rust 方案。

还需确认 Rust 端已经注册 Opener plugin。手动配置项目时通常需要:

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .run(tauri::generate_context!())
        .expect("error while running Tauri application");
}

通过 Tauri CLI 添加插件时,注册代码可能已经自动生成。

原来的 Shell 配置为什么没有生效

原代码调用了 Command.create():

Command.create('open', [url])

Command.create() 执行外部程序,需要的权限是 shell:allow-execute。原配置使用的是:

"identifier": "shell:allow-open"

Shell:allow-open 不会授权 Command.create() 执行名为 open 的程序,因此会报错:

program not allowed on the configured shell scope: open

权限名中的 open 和 macOS 的 /usr/bin/open 命令含义不同。

正确配置 Shell plugin

可以将权限限制为 macOS 自带的 /usr/bin/open,再用正则约束参数:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [
        {
          "name": "open",
          "cmd": "/usr/bin/open",
          "args": [
            {
              "validator": "^slack://user\\?team=[A-Za-z0-9]+&id=[A-Za-z0-9]+$"
            }
          ]
        }
      ]
    }
  ]
}

前端继续这样调用:

import { Command } from '@tauri-apps/plugin-shell';

async function openSlackUser(teamId, userId) {
  const params = new URLSearchParams({
    team: teamId,
    id: userId,
  });

  const url = `slack://user?${params.toString()}`;
  const command = Command.create('open', [url]);
  const result = await command.execute();

  if (result.code !== 0) {
    throw new Error(result.stderr || `open exited with code ${result.code}`);
  }
}

await openSlackUser('EDUA2R4KG', 'WR9RY8KMF');

这里无需通过 shell 拼接命令。URL 会作为一个独立参数传给 /usr/bin/open,其中的 & 不会被解释为 shell 操作符。

不要使用 "args": true 或能匹配任意内容的规则。前端数据可能来自表格、接口或用户输入。权限过宽时,WebView 可以调用 /usr/bin/open 打开其他路径和协议。

使用 Rust 作为兜底方案

如果 Opener plugin 受版本或 scope pattern 限制,无法打开自定义协议,可以把这项操作放到 Rust 端。Rust command 先解析 URL,并且只接受预期的 slack://user 格式。

在 Cargo.toml 中加入 URL 解析依赖:

[dependencies]
url = "2"

然后定义命令:

use std::process::Command;
use url::Url;

#[tauri::command]
fn open_slack_url(raw_url: String) -> Result<(), String> {
    let url = Url::parse(&raw_url)
        .map_err(|error| format!("Invalid Slack URL: {error}"))?;

    if url.scheme() != "slack" || url.host_str() != Some("user") {
        return Err("Only slack://user URLs are allowed".into());
    }

    let mut team = None;
    let mut user_id = None;

    for (key, value) in url.query_pairs() {
        match key.as_ref() {
            "team" => team = Some(value.into_owned()),
            "id" => user_id = Some(value.into_owned()),
            _ => {}
        }
    }

    if team.as_deref().is_none_or(str::is_empty)
        || user_id.as_deref().is_none_or(str::is_empty)
    {
        return Err("The Slack URL must contain team and id parameters".into());
    }

    let status = Command::new("/usr/bin/open")
        .arg(url.as_str())
        .status()
        .map_err(|error| format!("Failed to start /usr/bin/open: {error}"))?;

    if !status.success() {
        return Err(format!("/usr/bin/open exited with status {status}"));
    }

    Ok(())
}

将命令注册到 Tauri:

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![open_slack_url])
        .run(tauri::generate_context!())
        .expect("error while running Tauri application");
}

前端调用:

import { invoke } from '@tauri-apps/api/core';

async function openSlackUser(teamId, userId) {
  const params = new URLSearchParams({
    team: teamId,
    id: userId,
  });

  await invoke('open_slack_url', {
    rawUrl: `slack://user?${params.toString()}`,
  });
}

如果项目所用的 Rust 版本不支持 Option::is_none_or,可以换成兼容性更好的判断:

if team.as_deref().map_or(true, str::is_empty)
    || user_id.as_deref().map_or(true, str::is_empty)
{
    return Err("The Slack URL must contain team and id parameters".into());
}

是否改用 HTTPS URL

有效的 Slack 网页链接可以作为回退方案。不能直接把 slack:// 替换为 https://,因为两种 URL 的路径和参数格式未必对应。

使用 HTTPS 链接有两个好处:

  • 用户没有安装 Slack 客户端时,仍可能通过浏览器继续操作。
  • https:// 通常更容易通过 Opener plugin 的默认 URL 限制。

浏览器最终会打开桌面版 Slack、要求用户登录,还是只显示网页版,取决于 Slack、浏览器和用户的系统设置。因此,HTTPS 链接无法保证启动原生应用。

可以先打开自定义协议,失败后再使用经过验证的 HTTPS 链接:

import { openUrl } from '@tauri-apps/plugin-opener';

async function openSlackDestination(slackUrl, webUrl) {
  try {
    await openUrl(slackUrl);
  } catch (error) {
    console.warn('Unable to open Slack protocol URL:', error);
    await openUrl(webUrl);
  }
}

运行时检查

Tauri 权限配置正确后,macOS 还必须已经注册 slack scheme。这个注册通常由 Slack 桌面应用在安装时完成。如果 Slack 未安装、安装损坏,或 Launch Services 尚未正确注册,/usr/bin/open 仍可能失败。

可以先在终端中单独测试:

/usr/bin/open 'slack://user?team=EDUA2R4KG&id=WR9RY8KMF'

如果这条命令也无法打开 Slack,通常需要检查 URL、Slack 的安装状态、ID 是否有效,以及 macOS 的协议注册状态,而不是继续排查 Tauri。

建议按以下顺序处理:

  1. 修正 URL,确保参数格式为 team=...&id=...。
  2. 使用 Opener plugin 的 openUrl(),并为 slack:// 配置最小权限。
  3. 如果当前插件版本不接受自定义 scheme,改用带有严格参数限制的 shell:allow-execute。
  4. 如果对安全性和错误处理要求较高,将调用放入 Rust command。
  5. 准备一个经过验证的 HTTPS Slack 链接作为回退。

备注:内容仅供参考。