Tauri v2 在 macOS 上如何打开自定义 slack:// URL?
结论
在 Tauri v2 中,slack:// 属于 URL scheme,应使用 Opener plugin 的 openUrl()。openPath() 用于处理文件路径。
如果当前版本的 Opener plugin 不允许使用自定义 scheme,可以选择以下方案:
- 在 Shell plugin 中为
/usr/bin/open配置shell:allow-execute。 - 在 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。
建议按以下顺序处理:
- 修正 URL,确保参数格式为
team=...&id=...。 - 使用 Opener plugin 的
openUrl(),并为slack://配置最小权限。 - 如果当前插件版本不接受自定义 scheme,改用带有严格参数限制的
shell:allow-execute。 - 如果对安全性和错误处理要求较高,将调用放入 Rust command。
- 准备一个经过验证的 HTTPS Slack 链接作为回退。
备注:内容仅供参考。
