桌面能力
本页介绍 Tauri SDK 在 Web SDK 之外提供的桌面能力:系统通知与点击、角标、引起注意与托盘、以本地路径选择和发送文件、本地文件的预览、下载与打开文件、音视频通话在各系统上的差别、休眠与网络变化、诊断与日志,以及桌面相关的事件和错误。
这些能力在客户端的 desktop 属性上:界面窗口中为 im.desktop,引擎页中为 engine.client.desktop,用法相同。
系统通知
自动模式
引擎选项 notifications.mode 默认为 auto:SDK 自己显示系统通知,你不需要写代码。一条新消息在以下条件都满足时显示通知:
- 按用户的推送设置和会话免打扰应该提醒,即
im.push.shouldNotify()的结果,规则见 Web SDK 的提醒的判断; - 用户没有正在看这个会话(见多个窗口中的“正在看”);
- 负责提醒的窗口没有获得焦点,或者没有可见的窗口(见由谁提醒)。用户正在使用应用时不弹系统通知,你可以在界面中提示。
通知的内容和行为:
- 标题和正文:标题为发送者或群名,正文按用户的预览方式生成,预览方式为“不显示”时只有“你收到了一条新消息”。只有中文,需要其他语言时用引擎选项
notifications.format,见通知的文字。 - 好友申请、入群邀请、入群申请同样提醒,可以用
notifications.notices: false关闭。 - 同一会话只保留一条:新通知替换这个会话的旧通知,群里刷屏时不会堆满通知中心。
- 声音:使用系统默认的通知声音,
notifications.sound: false关闭。同一会话 3 秒内的新通知只替换内容、不再响,全部通知合计每秒最多响一次。 - 引起注意:显示通知的同时请求系统引起注意(Windows 任务栏按钮闪烁、macOS 程序坞图标跳动),
attention: false关闭。 - 退出登录、切换用户时:清除本应用已显示的通知,角标清零。
- 系统的勿扰模式(Windows 的专注助手、macOS 的专注模式)由系统处理,SDK 照常提交通知。
自动显示的通知都由引擎提交,不论负责提醒的是哪个窗口,所以不会重复。
会话结束时的通知
自动模式下,账号在其他设备登录被挤下线、或登录过期时,如果没有可见的窗口,引擎显示一条通知(如“你的账号已在其他设备登录”),点击后显示主窗口。不然缩在托盘里的应用不再收消息,用户也不会知道。有可见的窗口时,由你在 auth.stateChanged 中提示,同 Web SDK 的会话结束。
通知的点击
用户点击通知(或通知上的按钮)时,引擎页和全部界面窗口都收到 desktop.notificationClicked。处理了这次点击的窗口调用 claim() 认领:
import { getCurrentWindow } from '@tauri-apps/api/window';
im.on('desktop.notificationClicked', (e) => {
// 有多个窗口时只让主窗口处理,避免每个窗口都打开会话
if (getCurrentWindow().label !== 'main' || !e.conversation_id) return;
e.claim();
openConversation(e.conversation_id); // 正式会话的 ID 就是 conversation_key
const win = getCurrentWindow();
void win.unminimize().then(() => win.show()).then(() => win.setFocus());
});- 没有窗口认领时:300 毫秒内没有任何页面调用
claim()(如全部窗口都已关闭),插件调用你在 Rust 中注册的on_notification_click;没有注册的,显示并聚焦主窗口(main_window,窗口不存在时调用on_main_window_missing)。 - 把窗口显示到前面:隐藏和最小化的窗口照常收到事件。在窗口中调用
show()、setFocus()需要core:window:allow-show等权限,见给窗口授权。 - 点击的种类:
kind为message(新消息,带conversation_id、seq)、notice(好友申请、入群邀请或申请,data中有user或group_id)、call(来电,见来电通知)、session(会话结束)、custom(你用desktop.notify()显示的通知)。 - 应用被点击启动:macOS 的通知中心在应用退出后仍保留通知,点击会启动应用,第一个创建客户端的窗口收到这次点击,
launched为true。Windows 上应用退出后点击通知只会启动应用,不会交出点击的内容。
在 Rust 中处理没有窗口认领的点击,例如在主窗口已被关闭时重新打开它并带上会话:
let im = tauri_plugin_deeprespond_im::Builder::<tauri::Wry>::new()
.on_notification_click(|app, click| {
let url = match &click.conversation_id {
Some(id) => format!("index.html?conversation={id}"),
None => "index.html".to_string(),
};
let _ = tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App(url.into()))
.title("IM")
.inner_size(1000.0, 700.0)
.build();
})
.build();click 中有 id、kind、conversation_id、seq、action、data、launched,含义同上。
手动模式
notifications.mode 为 manual 时,SDK 不显示新消息和申请的通知,由你决定:
- 负责提醒的是界面窗口时,在
message.received中按alert和notify调用im.desktop.notify(); - 负责提醒的是引擎(没有可见的窗口)时,在引擎页的
onAlert中处理,见自己显示通知。
im.on('message.received', ({ message, notify, alert }) => {
const decision = notify as NotifyDecision | null;
if (!alert || !decision?.notify) return;
if (document.hasFocus() && message.conversation_id === currentConversation) return; // 正在看这个会话
void im.desktop.notify({
id: `msg:${message.conversation_id}`,
title: decision.title || '新消息',
body: decision.body ?? '',
conversation_id: message.conversation_id,
seq: message.seq ?? undefined,
});
});自动模式下也可以用 desktop.notify() 显示你自己的通知(如业务提醒),点击时同样发出 desktop.notificationClicked,kind 为 custom。
显示通知
desktop.notify() 显示一条系统通知:
try {
await im.desktop.notify({
id: 'order-1001', // 同一 id 的新通知替换旧的
title: '订单已发货',
body: '你的订单 1001 已发货',
data: { order_id: '1001' }, // 点击时原样交回
actions: [{ id: 'view', title: '查看' }], // 点击按钮时事件的 action 为 view
});
} catch (err) {
if (err instanceof DRError && err.code === 'permission_denied') showToast('请在系统设置中允许本应用的通知');
}
// 清除一个会话的通知;不传参数时清除本应用的全部通知
await im.desktop.clearNotifications({ conversation_id: '99584445836689408' });id不能以dr:开头(SDK 自己的通知使用),省略时每次都是新的一条。data的值都是字符串,合计最多 1 KB;actions最多 2 个。Linux 上按钮是否显示取决于桌面环境的通知服务。
通知权限
| 系统 | 说明 |
|---|---|
| macOS | 第一次显示通知前,SDK 请求通知权限;用户拒绝后,显示通知以 permission_denied(notification_denied)失败,不再弹出请求,需要用户在系统设置中打开。应用要从 .app 运行并签名,见打包与签名 |
| Windows | 应用要经安装包安装,见打包与签名。用户或组策略关闭了本应用的通知时,以 permission_denied(notification_disabled)失败 |
| Linux | 由桌面环境的通知服务显示,没有授权的步骤 |
im.desktop.notificationPermission() 读取当前的状态:granted、denied,还没有询问过(macOS)或读取不到时为 default;Linux 上总是 granted。im.desktop.requestNotificationPermission() 在 macOS 上请求权限,其他系统返回当前的状态。可以在设置页中据此提示用户。
来电通知
没有可见的窗口时来了电话,负责提醒的是引擎。notifications.calls 为 auto(默认)时,SDK 显示一条带“接听”“拒绝”按钮的通知,并在引擎页中循环播放铃声(notifications.ringtone 指定的文件,或 SDK 合成的铃声)。来电被接听、拒绝、取消,或在本人的其他设备上处理后,通知清除、铃声停止。
- 拒绝:由 SDK 直接拒绝,不需要窗口。
- 接听:要在一个界面窗口中接听。插件调用 Rust 中的
on_call_action(由你打开窗口),没有注册的显示主窗口;同时发出desktop.notificationClicked(kind为call,action为accept,data.call_id为通话 ID)。窗口收到后找到来电并接听:
im.on('desktop.notificationClicked', (e) => {
if (e.kind !== 'call' || e.action !== 'accept') return;
const call = im.calls.incoming.find((c) => c.call_id === e.data?.call_id);
if (!call) return;
e.claim();
void call.join();
});- 点击通知正文(不是按钮)时
action为空,按普通点击处理,通常显示来电界面让用户选择。 - Linux 上通知服务不支持按钮的,只显示通知,点击后打开窗口。
- 有可见的窗口时,来电由那个窗口按
call.incoming处理,同 Web SDK 的接收来电。 - 要自己处理没有窗口时的来电(如弹出一个置顶的小窗口),把
notifications.calls设为manual,见自己处理来电。
on_call_action 在来电通知的按钮被点击时调用,参数中 action 为 accept 或 decline,call_id 为通话 ID。“拒绝”已经由 SDK 执行,这里只是告诉你。
角标
引擎选项 badge 默认为 auto:SDK 把未读数显示在系统提供的位置。
| 系统 | 显示在哪里 | 说明 |
|---|---|---|
| macOS | 程序坞图标上的数字 | 属于整个应用,没有窗口时也显示 |
| Windows | 任务栏按钮上的数字图标,超过 99 显示“99+” | 依附在有任务栏按钮的窗口上。全部窗口都关闭或隐藏(缩到托盘)时不显示,只能靠托盘图标 |
| Linux | 启动器图标上的数字(Unity 启动器接口) | 需要桌面环境支持(如 Ubuntu 的 Dock、KDE Plasma),并且系统中装有应用的 .desktop 文件,文件名为应用的 identifier、productName 或可执行文件名之一。用 .deb 安装的应用会装上它;直接运行的 AppImage 没有时不显示 |
- 计入的未读:默认不计入免打扰的会话,引擎选项
badgeIncludesMuted: true时计入。 - 自己显示:
badge为manual时 SDK 不显示,你用im.desktop.setBadge(count)设置,setBadge(null)或0清除。 - 未读数变化:引擎和全部窗口都收到
desktop.unreadChanged({ total, badge },total为未读总数,badge为按上一条计算的角标数),多次变化在 500 毫秒内合并为一次。Rust 中可以用on_unread_changed或app.deeprespond_im().unread()取得同样的数。
引起注意
im.desktop.requestAttention() 请求系统引起用户注意:Windows 上任务栏按钮闪烁,直到窗口获得焦点;macOS 上程序坞图标跳动一次。传 { critical: true } 时 Windows 持续闪烁、macOS 持续跳动,直到用户切到应用。Linux 上为窗口的“需要注意”提示,Wayland 下由合成器决定,可能没有效果。
自动模式下 SDK 显示通知时已经做了这一步(attention 选项)。Windows 上没有带任务栏按钮的窗口时,没有可以闪烁的地方。
托盘
托盘图标和菜单由你用 Tauri 的托盘接口创建,SDK 不创建托盘(示例见缩到托盘)。SDK 提供两项辅助:
- 未读数:
desktop.unreadChanged事件,Rust 中的on_unread_changed,可以用来更新托盘的提示文字或菜单。 - 托盘图标闪烁:
im.desktop.trayBlink()让托盘图标在两个图标之间轮换,任一窗口获得焦点时自动停止并恢复第一个图标。
全部窗口都关闭后只有引擎页在运行,所以闪烁通常在引擎页中开始:
// src/engine.ts
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
const engine = createDRTauriEngine({ appKey: '1575529652#demo', apiUrl: 'https://im.example.com' });
// 未读数增加时开始闪烁,读完时停止
let lastBadge = 0;
engine.client.on('desktop.unreadChanged', ({ badge }) => {
if (badge > lastBadge) void engine.client.desktop.trayBlink({ trayId: 'main', icons: ['icons/tray.png', 'icons/tray-empty.png'] });
if (badge === 0) void engine.client.desktop.trayBlink(null);
lastBadge = badge;
});trayId是创建托盘时的 ID(如 Rust 中的TrayIconBuilder::with_id("main")),不存在时以local_validation(tray_not_found)失败。icons是两个 PNG 图标在应用资源目录中的相对路径,要在tauri.conf.json的bundle.resources中打包,如"resources": ["icons/tray.png", "icons/tray-empty.png"]。intervalMs为切换的间隔,默认 500 毫秒,可以设为 200 到 5000。- 需要插件的
tray特性(默认开启),关闭时以unsupported(tray_feature_disabled)失败。
文件
文件的来源
除了 Blob,凡是 Web SDK 中接受文件的地方都接受本地路径。为了不让窗口借 SDK 读取电脑上的任意文件,插件只接受以下路径:
| 来源 | 说明 |
|---|---|
im.desktop.pickFiles() | 系统的打开文件对话框,返回用户选择的路径;取消时为空数组 |
| 拖放到窗口上的文件 | 用 onFilesDropped() 接收,见下文 |
| 你在 Rust 中授权的目录 | 用 allow_read_dir(path) 授权,其中的文件都可以读取 |
| SDK 自己的目录 | 下载的文件、处理后的临时文件 |
其他路径以 permission_denied(path_not_allowed)拒绝。Tauri 的 dialog 插件返回的路径不被接受,选择文件请用 im.desktop.pickFiles()。选择和拖放得到的路径只在本次运行中有效;已经进入发送队列的附件在应用重启后仍可读取。
// 选择图片:filters 按扩展名过滤
const paths = await im.desktop.pickFiles({
multiple: true,
filters: [{ name: '图片', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic'] }],
});
for (const path of paths) {
await im.messages.send({ conversation_key: conversationKey }, { type: 'image', file: { path } });
}拖放用 @deeprespond/im-tauri 导出的 onFilesDropped() 接收,只交回拖入本窗口的、可读的普通文件,忽略文件夹:
import { onFilesDropped } from '@deeprespond/im-tauri';
const off = onFilesDropped((paths) => {
for (const path of paths) {
void im.messages.send({ conversation_key: conversationKey }, { type: 'file', file: { path } });
}
});
// 不再接收时
off();窗口要保持 Tauri 默认开启的拖放(窗口配置中的 dragDropEnabled),关闭后插件收不到拖放。用 Tauri 自己的拖放事件得到的路径同样被接受。
需要写入的位置同理:系统的“下载”目录、im.desktop.pickSavePath() 返回的路径、你用 allow_write_dir(path) 授权的目录。
let im = tauri_plugin_deeprespond_im::Builder::<tauri::Wry>::new()
.allow_read_dir("/opt/myapp/exports") // 你的应用自己生成文件的目录
.allow_write_dir("/opt/myapp/inbox")
.build();发送本地文件
文件参数写成 { path, name?, type? }:发送图片、语音、视频、文件消息,im.files.upload()、im.files.prepare(),以及上传头像都可以。
await im.messages.send(
{ conversation_key: conversationKey },
{ type: 'file', file: { path, name: '季度报告.pdf' } }, // name 省略时取文件名
);
const info = await im.files.upload({ path }, { purpose: 'attachment', kind: 'file' });- 文件内容不进入页面的内存:信息、类型由插件读取,内容在上传时由插件按片读出、直接传到文件存储,大文件也不会占用 WebView 的内存。
- 调用时的检查:只检查文件是否存在、能否读取、大小是否超过上限,不满足的以
local_validation拒绝,reason为file_not_found、file_unreadable或too_large。 name、type:省略时取文件名,类型按文件内容判断,不看扩展名。Blob照常可用:粘贴的图片、截图等Blob先由插件写入临时文件,再按路径处理。- 上传前的处理:规则与 Web SDK 的上传前的处理相同(压缩图片、去掉拍摄地点等元数据、读出视频的宽高和时长、截取封面),由插件完成。以下几点不同:
- HEIC 照片:只在 Linux 上打开插件的
heic特性(系统装有 libheif 1.17 以上)时转为图片发送,其他情况自动改为文件消息发送; - Linux 上本地路径的视频不截取封面,按没有封面发送,显示不受影响;
- 设置了引擎选项
media.preprocess时,本地文件要先读进内存交给它。
- HEIC 照片:只在 Linux 上打开插件的
im.files.prepare({ path }, kind):结果中没有blob,file.path是处理后的文件(可以用desktop.fileUrl()预览),其他字段同 Web SDK。
发送队列中的附件:消息进入发送队列后,SDK 保证应用重启后能继续发送:
- 不超过 200 MB(引擎选项
outbox.persistAttachmentMaxBytes)的附件,处理后复制一份保存在应用数据目录中; - 更大的只记下原文件的路径。发送前原文件被删除或移走的,消息以
send_abandoned(attachment_lost)失败;被修改了的(如用户又编辑了这个文件),已上传的部分作废,消息以send_abandoned(attachment_changed)失败,避免发出一个前后不一致的文件。请提示用户重新选择文件。
本地文件的预览
发送前显示所选图片、视频的预览,用 im.desktop.fileUrl(path) 取得地址,放进 <img>、<video>:
img.src = im.desktop.fileUrl(path); // 如 drim-file://localhost/...,Windows 上为 http://drim-file.localhost/...- 只能预览允许读取的路径,只响应授予了插件权限的窗口。
- 内容安全策略的
img-src、media-src要允许drim-file:和http://drim-file.localhost,见内容安全策略。 - Linux 上这个地址只能用于
<img>、<video>,不能用fetch读取。
收到的图片、视频照常用下载地址显示,同 Web SDK 的显示与下载文件。
下载与打开
im.desktop.download() 由插件把文件下载到磁盘,适合任何大小的文件:
const task = im.desktop.download({ message }, {
onProgress: (loaded, total) => {
if (total) progressBar.value = loaded / total;
},
});
cancelButton.onclick = () => task.cancel();
const { path, size } = await task; // 默认保存到系统的“下载”目录
console.log(path, size);
await im.desktop.revealPath(path); // 在文件管理器中显示- 来源:
{ message }取消息中的文件;也可以传文件地址(如消息body.url、群文件的地址),SDK 先换取下载地址;其他地址直接下载。 - 保存位置:默认是系统的“下载”目录,重名时自动改为“名称 (1).扩展名”(
conflict: 'overwrite'时覆盖)。saveTo可以是目录或完整路径,只能是“下载”目录、im.desktop.pickSavePath()返回的路径(直接覆盖,系统的保存对话框已经询问过)或你授权的目录,否则以permission_denied(path_not_allowed)拒绝。 - 文件名:取自消息中的文件名,去掉路径分隔符、
..和系统不允许的字符,不会写到目标目录以外。 - 续传:同一次运行中网络中断的从断点继续;下载地址过期的,SDK 重新换取后继续。应用重启后要重新开始。
- 取消:调用
task.cancel()或中止signal,任务以aborted拒绝,删除已下载的部分。 - 来源标记:下载的文件按系统的做法标记为来自网络,用户打开其中的程序时系统照常给出安全提示。
- 让用户选择保存位置时,先调用
pickSavePath():
const saveTo = await im.desktop.pickSavePath({ defaultName: String(message.body?.name ?? 'file') });
if (saveTo) await im.desktop.download({ message }, { saveTo });打开与定位:im.desktop.openPath(path) 用系统默认的程序打开文件,im.desktop.revealPath(path) 在文件管理器中显示它。只能用于 SDK 下载的文件和允许读取的路径。
可执行和脚本类文件(如 .exe、.msi、.bat、.ps1、.js、.lnk、.app、.pkg、.dmg、.sh、.desktop、.deb,以及 macOS、Linux 上带执行权限的程序)不能用 openPath() 打开,以 permission_denied(executable_file)拒绝。聊天中收到的文件被一键运行是常见的攻击方式,请改为定位文件并提示用户:
try {
await im.desktop.openPath(path);
} catch (err) {
if (err instanceof DRError && err.reason === 'executable_file') {
await im.desktop.revealPath(path);
showToast('这是一个程序文件,请确认来源可信后再打开');
} else {
throw err;
}
}im.files.fetchBlob() 在 Tauri 中仍可用,文件内容由窗口直接从下载地址取得,读进内存,只适合小文件;大文件请用 desktop.download()。
语音消息
录音用 Web SDK 的 im.files.recordVoice(),用法见 Web SDK 的录制语音。
- macOS:
Info.plist中必须有麦克风的用途说明,否则录音时应用会被系统终止;签名时的 entitlements 见打包与签名。 - Windows:用户在系统设置中关闭了“允许桌面应用访问麦克风”时,录音以
permission_required拒绝。 - Linux:依赖 GStreamer 的音频编码插件,不可用时以
unsupported拒绝,请隐藏录音按钮。
音视频通话
通话的用法与 Web SDK 的音视频通话相同。媒体连接在用户发起或接听通话的那个窗口中运行,这个窗口关闭时按挂断处理;应用退出时引擎先挂断进行中的通话。
| 系统 | 语音、视频通话 | 要做的配置 |
|---|---|---|
| Windows | 支持 | 不需要。插件对应用自己的页面自动允许使用摄像头和麦克风;系统设置中“允许桌面应用访问摄像头、麦克风”由用户决定 |
| macOS | 支持 | Info.plist 中的摄像头、麦克风用途说明,签名和沙盒的 entitlements,见打包与签名。第一次使用时系统询问用户,页面第一次使用设备时 WebView 也会询问一次 |
| Linux | 暂不支持 | — |
- 不支持的系统:
im.calls.supported(以及im.desktop.info.calls_supported)为false,发起和接听以unsupported(calls_unavailable)拒绝。来电时照常发出call.incoming,你可以提示“请在手机上接听”;用户的其他设备照常振铃。 - 自动播放:Tauri 的 WebView 默认允许没有用户点击时播放声音,铃声和对方的声音不会被拦住。你修改了 WebView2 的启动参数(窗口配置的
additionalBrowserArgs)的,要自己带上--autoplay-policy=no-user-gesture-required。 - 忙线:只按本应用中正在进行的通话判断,与 Web SDK 相同。
- 没有可见窗口时的来电见来电通知。
网络与代理
- 一直按前台处理:窗口最小化、缩到托盘不影响连接,桌面应用不登记离线推送。用户的手机在后台时照常收到推送,电脑上由 SDK 提醒,两边都会提醒;不想重复的,用户可以在手机上设置免打扰。
- 休眠与唤醒:系统从休眠中唤醒后,SDK 立即检查连接,5 秒内没有回应的重新连接,再补齐休眠期间的消息。同时发出
desktop.systemResumed({ slept_ms })。 - 网络变化:有线换无线、连上 VPN 等变化后,SDK 立即检查连接,发出
desktop.networkChanged({ online })。开机自启时还没有网络的,网络就绪后立即连接。 - 锁屏:只影响“正在看”,不断开连接。
- 代理:接口请求和长连接由 WebView 发出,使用系统的代理设置;文件的上传和下载由插件发出,默认同样使用系统代理,也可以在 Rust 中用
proxy(url)指定。两者不一致时可能出现“能收发消息但传不了文件”。 - 证书:插件使用系统的证书库,与 WebView 一致。企业网络安装了自己的根证书、私有化部署使用自签证书并已被系统信任的,文件的上传和下载同样可用。
诊断与日志
日志:引擎和每个窗口的日志与 Web SDK 相同,可以用选项 logger 交给你自己的日志系统,见 Web SDK 的日志。引擎窗口不显示,看不到它的控制台,建议在 Rust 中打开日志文件:
let im = tauri_plugin_deeprespond_im::Builder::<tauri::Wry>::new()
.log_file(true) // 默认关闭
.build();- 引擎、各窗口和插件的日志写入同一组文件,每行一条 JSON,带来源(
plugin、engine、window:{窗口标签})。 - 单个文件最大 10 MB,保留 5 个,最多 7 天。
- 默认位置在应用的日志目录下的
deeprespond_im中,可以用log_dir(path)修改:
| 系统 | 目录 |
|---|---|
| Windows | %LOCALAPPDATA%\{identifier}\logs\deeprespond_im |
| macOS | ~/Library/Logs/{identifier}/deeprespond_im |
| Linux | ~/.local/share/{identifier}/logs/deeprespond_im |
日志中不含令牌、密码和消息内容。
诊断包:用户反馈问题时,可以让他导出诊断包附上:
const zip = await im.desktop.exportDiagnostics(); // 返回压缩包的路径,默认在应用的缓存目录中
await im.desktop.revealPath(zip);压缩包中有最近的日志文件,SDK、系统和 WebView 的版本,引擎和连接的状态,代理设置(去掉了用户名和密码),本地数据库的大小,不含令牌和消息内容。saveTo 可以指定保存的目录或路径,规则同下载的 saveTo。
im.desktop.info 同步返回系统和版本信息:os、os_version、webview_version、sdk_version、calls_supported。
常见问题:
| 现象 | 先检查 |
|---|---|
| 能收发消息,但传不了文件 | WebView 和插件的代理设置是否一致(见网络与代理) |
| Windows 上不显示通知、点击通知没有反应 | 是否经安装包安装(见打包与签名) |
| macOS 上不显示通知 | 是否从 .app 运行、是否签名、用户是否允许了通知 |
| Linux 上每次启动都要重新登录 | Secret Service 是否在运行,secure_storage_fallback 是否为 Fallback::None(见登录令牌) |
im.engineState 一直是 starting 或变为 failed | 日志文件中引擎页的错误:engine_url 是否正确、引擎页是否加入了构建入口、是否用同一个 AppKey |
窗口中的调用都以 capability_missing 失败 | 窗口是否在 capabilities 中授予了 deeprespond-im:window |
| 心跳不准、提醒延迟 | 日志中的“唤醒检查落后”警告 |
事件
除 Web SDK 的全部事件外,Tauri SDK 另有以下事件,在 im.on()、engine.client.on() 中都有确切的载荷类型(@deeprespond/im-tauri 导出的 DesktopEvents)。
| 事件 | 载荷 | 说明 |
|---|---|---|
engine.stateChanged | { state: 'starting' | 'ready' | 'restarting' | 'failed' } | 引擎的状态变化,只在界面窗口中发出,见引擎的状态 |
desktop.notificationClicked | NotificationClickEvent | 通知或通知上的按钮被点击 |
desktop.unreadChanged | { total: number; badge: number } | 未读总数、角标数变化 |
desktop.systemResumed | { slept_ms: number } | 系统从休眠中唤醒 |
desktop.networkChanged | { online: boolean } | 网络变化 |
window.roleChanged | { isLeader: boolean; isAlertTab: boolean } | 本窗口开始或不再负责提醒,代替 Web SDK 的 tabs.roleChanged;isLeader 在界面窗口中总是 false |
storage.full | { freed_users: number } | 磁盘已满,见磁盘已满 |
storage.reset | reason 另有 corrupted、schema_downgrade、encryption_changed | 本地数据库损坏、应用降级、开启或关闭加密后重建 |
错误
除 Web SDK 的错误码外(见事件与错误处理),Tauri SDK 另有:
code | details.reason | 情况 |
|---|---|---|
engine_unavailable | starting、restarting、failed | 引擎 10 秒内没有就绪,或已经停止重建,见引擎的状态 |
invalid_state | another_instance | 另一个进程正在使用同一份本地数据,见单实例 |
unsupported | version_mismatch | npm 包与 Rust 插件的版本不同 |
unsupported | sqlcipher_required | 要求加密本地数据库,但插件没有打开 sqlcipher 特性 |
unsupported | not_engine_window、not_ui_window、not_tauri | 在错误的地方创建客户端 |
unsupported | calls_unavailable | 本系统不支持音视频通话(Linux) |
unsupported | tray_feature_disabled | 插件关闭了 tray 特性时调用 trayBlink() |
permission_denied | capability_missing | 窗口没有被授予 deeprespond-im:window |
permission_denied | path_not_allowed | 路径不在允许读取或写入的范围内 |
permission_denied | executable_file | 用 openPath() 打开可执行或脚本类文件 |
permission_denied | notification_denied | macOS 上用户拒绝了通知权限 |
permission_denied | notification_disabled | Windows 上用户或组策略关闭了本应用的通知 |
local_validation | file_not_found、file_unreadable、too_large | 本地文件不存在、不能读取或超过上传上限 |
local_validation | reserved_id、too_large、invalid_value | desktop.notify() 的 id 以 dr: 开头、data 超过 1 KB、按钮超过 2 个 |
local_validation | tray_not_found | trayBlink() 的 trayId 没有对应的托盘图标 |
send_abandoned | attachment_lost、attachment_changed | 只记了路径的大附件在发送前被删除或修改 |
storage_error | quota_exceeded、disk_full | 磁盘空间不足 |
接口参考
返回 Promise 的方法失败时以 DRError 拒绝。各方法都需要窗口有插件的权限,否则以 permission_denied(capability_missing)拒绝。
im.desktop.notify()
显示一条系统通知。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
n | DesktopNotification | 是 | 见 DesktopNotification |
返回值:Promise<void>。
可能的错误:local_validation(required,缺少标题或正文;reserved_id;too_large;invalid_value)、permission_denied(notification_denied、notification_disabled)。
im.desktop.clearNotifications()
清除本应用显示的通知。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.conversation_id | string | 否 | 只清除这个会话的通知;省略时清除全部 |
返回值:Promise<void>。
im.desktop.notificationPermission()
读取通知权限。返回值:Promise<'granted' | 'denied' | 'default'>,见通知权限。
im.desktop.requestNotificationPermission()
macOS 上请求通知权限,其他系统返回当前的状态。返回值:Promise<'granted' | 'denied'>。
im.desktop.setBadge()
badge 为 manual 时设置角标。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
count | number | null | 是 | 非负整数;null 或 0 清除 |
返回值:Promise<void>。可能的错误:local_validation(invalid_value)。
im.desktop.requestAttention()
请求系统引起用户注意,见引起注意。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.critical | boolean | 否 | 持续闪烁或跳动,直到用户切到应用。默认 false |
返回值:Promise<void>。
im.desktop.trayBlink()
开始或停止托盘图标闪烁,见托盘。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p | { trayId: string; icons: [string, string]; intervalMs?: number } | null | 是 | trayId 为托盘的 ID;icons 为两个图标在资源目录中的相对路径;intervalMs 为间隔,默认 500,范围 200~5000。null 停止 |
返回值:Promise<void>。可能的错误:local_validation(tray_not_found、file_not_found、invalid_path)、unsupported(tray_feature_disabled)。
im.desktop.pickFiles()
打开系统的选择文件对话框,返回的路径允许读取。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.multiple | boolean | 否 | 是否允许选择多个,默认 false |
p.filters | Array<{ name: string; extensions: string[] }> | 否 | 按扩展名过滤,扩展名不带点 |
返回值:Promise<string[]>,用户取消时为空数组。
im.desktop.pickSavePath()
打开系统的保存文件对话框,返回的路径允许写入。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.defaultName | string | 否 | 默认的文件名 |
返回值:Promise<string | null>,用户取消时为 null。
im.desktop.fileUrl()
同步返回本地文件的预览地址,见本地文件的预览。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
path | string | 是 | 允许读取的路径 |
返回值:string。
im.desktop.download()
把文件下载到磁盘,见下载与打开。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
source | string | { message: MessageView } | 是 | 文件地址或其他地址;或一条图片、语音、视频、文件消息 |
p | DownloadParams | 否 | 见 DownloadParams |
返回值:DownloadTask,见 DownloadTask。
可能的错误:aborted(已取消)、permission_denied(path_not_allowed)、not_found(文件不存在或已过期)、url_expired(直接给出的下载地址已过期)、storage_error(disk_full)、network_error。
im.desktop.openPath()
用系统默认的程序打开文件。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
path | string | 是 | SDK 下载的文件或允许读取的路径 |
返回值:Promise<void>。可能的错误:permission_denied(executable_file、path_not_allowed)、local_validation(file_not_found)。
im.desktop.revealPath()
在文件管理器中显示文件。参数和错误同 openPath(),没有 executable_file。
im.desktop.exportDiagnostics()
导出诊断包,见诊断与日志。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.saveTo | string | 否 | 保存的目录或完整路径;省略时保存在应用的缓存目录中 |
返回值:Promise<string>,压缩包的路径。
im.desktop.info
只读属性,DesktopInfo,见 DesktopInfo。
onFilesDropped()
接收拖入本窗口的文件,从 @deeprespond/im-tauri 导入。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
handler | (paths: string[]) => void | 是 | 拖入的可读的普通文件 |
返回值:() => void,取消接收。
数据结构
DesktopNotification
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 否 | 同一 id 的新通知替换旧的,不能以 dr: 开头;省略时每次都是新的一条 |
title | string | 是 | 标题 |
body | string | 是 | 正文 |
conversation_id | string | 否 | 点击时随事件交回;clearNotifications() 按它清除 |
seq | number | 否 | 点击时随事件交回 |
data | Record<string, string> | 否 | 点击时原样交回,合计最多 1 KB |
sound | boolean | 否 | 是否带系统的提示音,默认 true |
actions | Array<{ id: string; title: string }> | 否 | 按钮,最多 2 个;点击时事件的 action 为按钮的 id |
NotificationClickEvent
desktop.notificationClicked 的载荷。
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 通知的 id |
kind | 'message' | 'notice' | 'call' | 'session' | 'custom' | 新消息、申请、来电、会话结束、你自己的通知 |
conversation_id | string | 会话 ID(新消息和带了它的自定义通知) |
seq | number | 消息的序号 |
action | string | 点击的按钮的 id;点击正文时没有 |
data | Record<string, string> | 通知的 data;来电为 { call_id },申请为 { user } 或 { group_id } |
launched | boolean | 应用是否由这次点击启动 |
claim | () => void | 认领这次点击:插件不再调用 on_notification_click,也不显示主窗口 |
DownloadParams
| 字段 | 类型 | 说明 |
|---|---|---|
saveTo | string | 保存的目录或完整路径,默认系统的“下载”目录 |
conflict | 'rename' | 'overwrite' | 重名时改名(默认)或覆盖 |
onProgress | (loaded: number, total: number | null) => void | 下载进度,total 未知时为 null |
signal | AbortSignal | 中止下载 |
DownloadTask
兑现为 { path: string; size: number }(保存的路径和大小)的 Promise,另有 cancel() 方法取消下载。
DesktopInfo
| 字段 | 类型 | 说明 |
|---|---|---|
os | 'windows' | 'macos' | 'linux' | 系统 |
os_version | string | 系统版本 |
webview_version | string | WebView 的版本 |
sdk_version | string | Tauri SDK 的版本 |
calls_supported | boolean | 能否音视频通话,同 im.calls.supported |
独立频道窗口生命周期
独立频道的媒体只在界面窗口运行,引擎转发 HTTP 鉴权;隐藏引擎不支持媒体加入。窗口关闭、冻结、客户端销毁或引擎重启会清理媒体;不保证进程强杀时 HTTP 退出成功。普通最小化不等于离开。
频道主动销毁的退出尝试最多等待 5 秒,不替代应用整体退出的等待规则。各系统设备权限与 WebView 能力需验收,完整说明见独立频道。
