桌面能力
本頁介紹 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 |
獨立頻道視窗生命週期
獨立 RTC 頻道的媒體連線只在介面視窗中執行,引擎只負責轉發需要登入身份的請求,引擎視窗不能加入頻道。視窗關閉、被系統凍結、客戶端被銷燬或引擎重啟時,SDK 斷開媒體;程序被強制結束時不保證能通知服務端離開。視窗最小化不算離開。
銷燬客戶端時,SDK 最多等 5 秒讓頻道退出,這與應用退出時的收尾時間是兩回事。各系統的裝置許可權和 WebView 能力請在目標系統上實測,完整說明見獨立頻道。
