引擎與視窗
本頁介紹 Tauri SDK 的執行方式:為什麼要有引擎視窗、引擎頁的全部選項、引擎的啟動和重建、多個介面視窗、由誰來提醒、在引擎頁中處理提醒和來電、退出與托盤應用、單例項與多個 AppKey,以及令牌和本地資料儲存在哪裡。
| 要做什麼 | 看哪裡 |
|---|---|
| 寫引擎頁 | 引擎頁、引擎頁的選項 |
| 在介面視窗中使用、開啟多個視窗 | 介面視窗、多個視窗 |
| 顯示“正在重新連線”、處理引擎崩潰 | 引擎的狀態 |
| 決定由誰提醒,自己顯示通知或處理來電 | 由誰提醒、引擎頁中的提醒和來電 |
| 關閉視窗後縮到托盤 | 退出與托盤應用 |
| 令牌、聊天記錄儲存在哪裡,加密本地資料庫 | 令牌與本地資料 |
| 查 Rust 中的全部設定 | Rust 側的設定 |
為什麼要引擎視窗
桌面 IM 通常在使用者關閉主視窗後縮到托盤繼續執行,收到訊息時彈出通知。如果長連線放在介面視窗中,視窗一關就斷開了。所以 SDK 把連線放在一個引擎視窗中:
- 外掛在應用啟動後建立它,載入你提供的引擎頁。它不顯示,不出現在工作列、程式塢和視窗切換中,不獲取焦點,隨程序存在。
- 引擎持有長連線和登入令牌,負責同步、寫入本地資料、傳送佇列、令牌續期,以及沒有可見視窗時的系統通知和來電。
- 你的介面視窗各自建立客戶端,直接讀取本地資料;需要請求服務端的操作(發訊息、修改資料、上傳等)由 SDK 轉給引擎執行,結果原樣返回。事件在引擎寫入本地資料之後發出,每個視窗都會收到。
這和 Web SDK 中同一瀏覽器的多個標籤頁的方式相同,只是負責連線的固定是引擎視窗,不會在視窗之間切換。代價是應用多佔用一個 WebView 的記憶體,啟動時多建立一個 WebView。
引擎頁
引擎頁中只調用 createDRTauriEngine() 建立引擎客戶端,需要時再註冊提醒和來電的處理函式:
// src/engine.ts
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
const engine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { mode: 'auto' },
});
// 引擎客户端,可以像界面窗口中的客户端一样使用
engine.client.on('auth.stateChanged', (state) => console.log('登录状态', state.kind));- 必須是打包在應用中的頁面:外掛拒絕載入遠端地址,防止遠端內容拿到登入令牌。引擎頁和介面視窗的頁面來自同一次構建,SDK 的版本一致。
- 只做提醒相關的事:不顯示介面,不引入介面框架,不訪問你的業務資料。它與介面視窗共用同一份本地資料,但不共用記憶體。
- 一個 AppKey 一個引擎:同一 AppKey 已有引擎時再建立以
invalid_state(client_exists)拒絕。在介面視窗中呼叫createDRTauriEngine()丟擲unsupported(not_engine_window);反過來,在引擎頁中呼叫createDRTauriClient()丟擲unsupported(not_ui_window)。 - 引擎視窗的標籤固定為
deeprespond-im-engine,它的外掛許可權由外掛自動授予。
引擎頁的選項
| 選項 | 型別 | 必填 | 說明 |
|---|---|---|---|
appKey | string | 是 | 應用的 AppKey,格式為 org_name#app_name |
apiUrl | string | 是 | IM 服務的地址,如 https://im.example.com |
wsUrl | string | 否 | 長連線的地址,預設由 apiUrl 得出,同 Web SDK |
deviceName | string | 否 | 裝置名稱,顯示在登入裝置列表中。預設為計算機名,最長 64 個字元 |
appVersion | string | 否 | 你的應用的版本號,隨長連線上報,便於排查問題 |
ticketProvider | (ctx: TicketContext) => Promise<string> | 否 | 重新獲取登入憑證的函式,在引擎頁中執行,見 Web SDK 的自動重新獲取憑證 |
encryptDatabase | boolean | 否 | 加密本地資料庫,預設 false,需要外掛的 sqlcipher 特性,見加密本地資料庫 |
autoMarkRead | boolean | 否 | 使用者正在看一個會話時自動標記已讀,預設 false。“正在看”要求視窗可見且獲得焦點 |
conversationWindow | number | 否 | 會話列表在記憶體中最多保留的會話數,預設 1000 |
userCacheTtlMs | number | 否 | 其他使用者資料的快取時間(毫秒),預設 600000 |
outbox | { maxAgeHours?: number; persistAttachmentMaxBytes?: number } | 否 | 傳送佇列:訊息等待發送的最長時間,預設 48 小時;複製儲存的附件上限,預設 200 MB,更大的只記下檔案路徑,見傳送本地檔案 |
media | { compressImages?: boolean | { maxSide?: number; quality?: number }; preprocess?: (input: Blob, kind: FileKind) => Promise<Blob> } | 否 | 上傳前的處理:圖片壓縮的引數(預設長邊 2048、質量 0.85,false 按原圖),或你自己的處理函式,規則見 Web SDK 的上傳前的處理。設定了 preprocess 時,本地路徑的檔案要先讀進記憶體交給它 |
notifications.mode | 'auto' | 'manual' | 否 | 系統通知由 SDK 自動顯示(預設 auto),還是由你顯示(manual),見系統通知 |
notifications.sound | boolean | 否 | 自動顯示的通知是否帶系統的提示音,預設 true |
notifications.notices | boolean | 否 | 好友申請、入群邀請、入群申請是否提醒,預設 true |
notifications.calls | 'auto' | 'manual' | 否 | 沒有可見視窗時的來電由 SDK 顯示通知並響鈴(預設 auto),還是交給 onIncomingCall(manual) |
notifications.ringtone | string | 否 | 來電鈴聲檔案的地址(引擎頁能訪問的,如放在 Vite 的 public 目錄中的 /ringtone.mp3),迴圈播放。預設用 SDK 合成的鈴聲 |
notifications.format | (alert: EngineAlert) => { title: string; body: string } | null | 否 | 自定義自動顯示的通知的文字,返回 null 時這條不提醒,見通知的文字 |
badge | 'auto' | 'manual' | 否 | 未讀數由 SDK 顯示在程式塢、工作列或啟動器上(預設 auto),還是由你呼叫 desktop.setBadge(),見角標 |
badgeIncludesMuted | boolean | 否 | 角標是否計入免打擾的會話,預設 false |
attention | boolean | 否 | 自動提醒時是否同時請求系統引起注意(工作列閃爍、程式塢圖示跳動),預設 true |
logger | { level?: LogLevel; sink?: (entry: LogEntry) => void } | 否 | 日誌級別和輸出函式,同 Web SDK 的日誌 |
沒有 Web SDK 的 platform、storage、tokenStorage、persistStorage、sessionScope:平臺由外掛按系統給出,資料和令牌的儲存方式由外掛決定。
介面視窗
每個視窗用 createDRTauriClient() 建立自己的客戶端:
import { createDRTauriClient } from '@deeprespond/im-tauri';
const im = createDRTauriClient({
appKey: '1575529652#demo',
logger: { level: 'info' }, // 可选:本窗口的日志
});
console.log(im.engineState, im.desktop.info.os);- 只需要 AppKey:服務端地址等選項由引擎決定。
- 對你來說就是 Web SDK 的客戶端:各模組的 API、
LiveList、事件都相同,React、Vue 的繫結包可以直接使用。另外多了im.desktop(見桌面能力)和im.engineState。 - 一個視窗一個客戶端:同一視窗中每個 AppKey 只能有一個,已有時以
invalid_state(client_exists)拒絕。視窗關閉時外掛自動釋放它,不需要呼叫destroy()。 - 讀取不經過引擎:會話、訊息等本地資料由外掛直接返回,引擎還沒有就緒時也能顯示上次的內容。
- 需要服務端的呼叫都轉給引擎:包括 Web SDK 中由標籤頁自己完成的單獨上傳和換取下載地址。引擎還沒有就緒、或正在重建時,呼叫排隊等待,最長 10 秒,超時以
engine_unavailable失敗(details.reason為starting、restarting或failed)。 - 視窗中沒有令牌:
im.auth.currentUser中只有使用者名稱和會話 ID,access token 和 refresh token 只在引擎中。視窗即使載入了有問題的第三方指令碼,也偷不到令牌。
多個視窗
可以同時開啟多個視窗,如把一個會話單獨放在一個視窗中。新視窗的標籤要在 capabilities 中授權(見給視窗授權),開啟它的視窗要有建立視窗的許可權(core:webview:allow-create-webview-window):
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
function openInNewWindow(conversationKey: string) {
// 新窗口加载同一个页面,照常创建客户端,从地址中取出要打开的会话
new WebviewWindow(`chat-${Date.now()}`, {
url: `index.html?conversation=${encodeURIComponent(conversationKey)}`,
title: '会话',
width: 700,
height: 600,
});
}- 每個視窗讀取同一份本地資料,收到同樣的事件,列表各自自動更新。一個視窗登入、退出或切換使用者,其他視窗隨之改變。
- “正在看”:會話的訊息列表開啟、停在最新處、在介面上顯示,並且視窗可見、獲得焦點。比 Web SDK 多了焦點的要求:視窗被其他程式擋住時,使用者沒有在看。視窗最小化、隱藏、系統鎖屏時都按不可見處理。正在看的會話不提醒,開啟
autoMarkRead時自動標記已讀。 - 視窗關閉時:它的客戶端釋放列表、退訂線上狀態、退出在這個視窗中進入的聊天室;在這個視窗中進行的通話按結束通話處理。視窗崩潰時外掛替它通知引擎,效果相同。
引擎的狀態
介面視窗中的 im.engineState 是引擎當前的狀態,變化時發出 engine.stateChanged:
| 狀態 | 說明 |
|---|---|
starting | 引擎正在啟動,或引擎頁還沒有為這個 AppKey 建立引擎客戶端 |
ready | 引擎就緒 |
restarting | 引擎崩潰或卡住,外掛正在重建 |
failed | 引擎不可用:1 分鐘內連續崩潰 3 次後停止重建;引擎頁載入後 10 秒內沒有建立引擎客戶端(engine_url 寫錯、頁面指令碼出錯);另一個程序正在使用同一份資料(見單例項)。請提示使用者重新啟動應用 |
im.on('engine.stateChanged', ({ state }) => {
if (state === 'restarting') showToast('正在重新连接…');
if (state === 'failed') showToast('IM 服务不可用,请重新启动应用');
});im.connection.state 仍是連線的狀態,與 Web SDK 相同。引擎就緒不代表已經連上服務端。
崩潰與重建
引擎視窗的 WebView 程序崩潰、卡住(30 秒沒有響應),或頁面意外重新載入時,外掛在 1 秒後重建引擎視窗。重建後的引擎和普通啟動一樣恢復登入、重連、同步,你不需要處理:
- 重建期間視窗中需要服務端的呼叫排隊等待,規則同上;讀取本地資料不受影響。
- 沒有提交完成的本地寫入回滾,不會留下半條資料;傳送佇列中的訊息在重建後繼續傳送。
- 進行中的上傳由新的引擎從已上傳的部分繼續;下載由外掛進行,不受影響。
- 開發時熱更新重新載入引擎頁,也按重建處理。
1 分鐘內連續崩潰 3 次,或引擎頁載入失敗時,外掛不再重建,狀態為 failed,原因寫入日誌檔案(見診斷與日誌)。
由誰提醒
新訊息、來電這類提醒,每個視窗和引擎都會收到事件。為了不重複提醒,SDK 選出一個負責提醒的物件,規則與 Web SDK 的提醒標籤頁相同,“標籤頁”換為“視窗”:
- 有可見的介面視窗時,由最近獲得焦點的那個視窗負責;
- 沒有介面視窗,或都不可見(最小化、縮到托盤)時,由引擎負責。
message.received、call.incoming 的載荷中 alert 為 true 時,本視窗負責提醒。im.connection.isAlertTab 是本視窗當前是否負責提醒,變化時發出 window.roleChanged(代替 Web SDK 的 tabs.roleChanged,載荷相同,其中 isLeader 在介面視窗中總是 false)。
| 提醒 | 負責的是可見的視窗 | 負責的是引擎(沒有可見的視窗) |
|---|---|---|
新訊息(notifications.mode 為 auto) | 視窗沒有獲得焦點時,SDK 顯示系統通知;視窗獲得焦點時不顯示,你可以在介面中提示 | SDK 顯示系統通知 |
新訊息(manual) | 你在視窗中按 alert 呼叫 im.desktop.notify() | 交給引擎頁的 onAlert |
來電(notifications.calls 為 auto) | 你在視窗中按 call.incoming 顯示來電介面、播放鈴聲,同 Web SDK | SDK 顯示帶“接聽”“拒絕”的通知,在引擎頁中播放鈴聲 |
來電(manual) | 同上 | 交給引擎頁的 onIncomingCall |
自動模式下的通知一律由引擎顯示,不論負責提醒的是哪個視窗,所以不會重複。通知的內容、點選和許可權見系統通知。
引擎頁中的提醒和來電
沒有可見的視窗時,引擎頁是應用中唯一執行 JavaScript 的地方。需要自己決定提醒方式的,在引擎頁中註冊處理函式。它們只在負責提醒的是引擎時呼叫;有可見的視窗時,提醒由那個視窗中的程式碼處理。
自己顯示通知
notifications.mode 為 manual 時,SDK 不顯示新訊息和好友申請等通知,交給 onAlert:
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
const engine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { mode: 'manual' },
});
engine.onAlert(async (alert) => {
if (alert.kind === 'message') {
// notify 已按用户的推送设置和会话免打扰判断过,只有应该提醒的才会交来
const { message, notify } = alert;
await engine.client.desktop.notify({
id: `msg:${message.conversation_id}`, // 同一会话只保留一条
title: notify.title || '新消息',
body: notify.body ?? '',
conversation_id: message.conversation_id,
seq: message.seq ?? undefined,
});
} else {
// 好友申请、入群邀请、入群申请:标题和正文已经生成好
await engine.client.desktop.notify({ title: alert.title, body: alert.body });
}
});onAlert 收到的 EngineAlert 有兩種:
kind | 欄位 | 說明 |
|---|---|---|
message | message、notify | 新訊息和提醒的判斷(NotifyDecision) |
notice | type、title、body、user、group_id | type 為 friend_request(好友申請,user 為申請人)或 group_request(入群邀請或入群申請,group_id 為群) |
onAlert 返回取消註冊的函式。處理函式丟擲的異常寫入日誌,不影響 SDK。
通知的文字
自動模式的通知文字由 SDK 生成,只有中文:標題為傳送者或群名,正文按使用者的預覽方式生成(預覽方式為“不顯示”時只有“你收到了一條新訊息”),規則同 Web SDK 的提醒的判斷。需要其他語言或自己的文字時,提供 notifications.format:
import { createDRTauriEngine, type DREngine, type EngineAlert } from '@deeprespond/im-tauri/engine';
const text = { newMessage: 'New message', hidden: 'You have a new message', friendRequest: 'Friend request', groupRequest: 'Group request' };
const engine: DREngine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { format: (alert) => format(alert) },
});
function format(alert: EngineAlert): { title: string; body: string } | null {
if (alert.kind === 'notice') {
return { title: alert.type === 'friend_request' ? text.friendRequest : text.groupRequest, body: '' };
}
// 按用户的预览方式决定显示多少内容
const preview = engine.client.push.settings()?.effective_preview ?? 'full';
if (preview === 'none') return { title: '', body: text.hidden };
const title = alert.notify.title || text.newMessage; // 发送者或群名
if (preview === 'sender_only' || alert.message.type !== 'text') return { title, body: text.newMessage };
return { title, body: String(alert.message.body?.text ?? '') };
}format 丟擲異常時使用預設的文字。賬號在其他裝置登入、登入過期的通知(見會話結束時的通知)和來電通知不經過它。
自己處理來電
notifications.calls 為 manual 時,沒有可見視窗時的來電交給 onIncomingCall,引數是 Web SDK 的通話物件 CallHandle:
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
engine.onIncomingCall(async (call) => {
const snapshot = call.getSnapshot();
if (!engine.client.calls.supported) {
await engine.client.desktop.notify({ title: '来电', body: '请在手机上接听' });
return;
}
// 打开一个小窗口显示来电,在那个窗口中接听
new WebviewWindow(`call-${call.call_id}`, {
url: `index.html?call=${encodeURIComponent(call.call_id)}`,
title: snapshot.call.media === 'video' ? '视频来电' : '语音来电',
width: 360,
height: 480,
alwaysOnTop: true,
});
});引擎中不能接聽:引擎視窗不顯示,不執行媒體連線。可以在引擎中拒絕(
call.reject());接聽要在一個介面視窗中進行,視窗中的客戶端從im.calls.incoming中按call_id找到來電後呼叫join(),見 Web SDK 的接聽與拒絕。引擎視窗預設只有外掛的許可權:要在引擎頁中建立視窗的,另給引擎視窗授權:
json{ "identifier": "engine-windows", "windows": ["deeprespond-im-engine"], "permissions": ["core:webview:allow-create-webview-window"] }新視窗的標籤(如上例的
call-*)同樣要授予deeprespond-im:window。
退出與托盤應用
關閉最後一個視窗時退出
預設情況下(exit_when_last_window_closed(true)),除引擎視窗外的最後一個視窗被關閉時,外掛讓應用退出。這裡的視窗包括沒有建立客戶端的視窗(啟動頁、設定視窗);只是隱藏的視窗不算關閉。
退出前外掛先讓引擎收尾,最多等 2 秒:結束通話本人正在進行的通話(對方立即看到通話結束),以正常方式斷開長連線(聯絡人立即看到本人離線),關閉本地資料庫。2 秒內沒完成的直接退出;沒發出的訊息儲存在傳送佇列中,下次啟動後繼續傳送。
使用者按 Cmd+Q、系統關機或登出時同樣如此:外掛向系統申請延遲,在延遲內收尾。
縮到托盤
關閉視窗後留在托盤繼續收訊息的應用,把 exit_when_last_window_closed 設為 false,用托盤選單的“退出”結束應用:
// src-tauri/src/lib.rs
use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::Manager;
fn show_main<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(w) = app.get_webview_window("main") {
let _ = w.unminimize();
let _ = w.show();
let _ = w.set_focus();
}
}
pub fn run() {
let app = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
tauri_plugin_deeprespond_im::handle_args(app, &argv);
show_main(app);
}))
.plugin(
tauri_plugin_deeprespond_im::Builder::new()
.exit_when_last_window_closed(false)
// 托盘图标的提示文字显示未读数
.on_unread_changed(|app, unread| {
if let Some(tray) = app.tray_by_id("main") {
let tip = if unread.badge > 0 { format!("IM({} 条未读)", unread.badge) } else { "IM".to_string() };
let _ = tray.set_tooltip(Some(tip));
}
})
.build(),
)
// 关闭主窗口只是把它隐藏起来
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if window.label() == "main" {
api.prevent_close();
let _ = window.hide();
}
}
})
.setup(|app| {
let show = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
let mut tray = TrayIconBuilder::with_id("main").menu(&menu).tooltip("IM");
if let Some(icon) = app.default_window_icon() {
tray = tray.icon(icon.clone());
}
tray.on_menu_event(|app, event| match event.id.as_ref() {
"show" => show_main(app),
// 经插件退出:引擎先收尾,再退出
"quit" => tauri_plugin_deeprespond_im::exit(app, 0),
_ => {}
})
.build(app)?;
Ok(())
})
.build(tauri::generate_context!())
.expect("error while building tauri application");
app.run(|_app, _event| {
// macOS 的习惯:点击程序坞图标时重新显示窗口
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Reopen { has_visible_windows: false, .. } = _event {
show_main(_app);
}
});
}- 退出:呼叫
tauri_plugin_deeprespond_im::exit(app, code),或 Tauri 自己的app.exit(code),兩者都會先經外掛收尾。 - 不要在
RunEvent::ExitRequested中無條件地呼叫prevent_exit():外掛收尾後會再次請求退出,被你攔住後應用就退不出了。要留在托盤,用exit_when_last_window_closed(false)或像上例那樣隱藏視窗。 - 主視窗被關閉(銷燬)的:通知點選、來電接聽需要顯示主視窗時,外掛呼叫
on_main_window_missing,由你重新建立,見Rust 側的設定。 - macOS:習慣上關閉視窗不退出應用,點選程式塢圖示再開啟視窗,通常設為
false並像上例那樣處理Reopen。 - Windows:全部視窗都隱藏後,工作列上沒有角標和閃爍,可以用托盤圖示閃爍提醒,見托盤。
單例項與多個 AppKey
單例項
同一份本地資料只允許一個程序使用。第二個程序不建立引擎(im.engineState 為 failed),視窗中需要服務端的呼叫以 invalid_state(another_instance)失敗。請使用 Tauri 的單例項外掛,把第二次啟動的引數交給 tauri_plugin_deeprespond_im::handle_args(app, &argv) 並顯示已有的視窗,見只執行一個例項。
一個應用使用多個 AppKey
可以。引擎頁為每個 AppKey 各建立一個引擎客戶端,介面視窗按 AppKey 建立對應的客戶端。不同 AppKey 的本地資料、登入和連線互相獨立:
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
createDRTauriEngine({ appKey: '1575529652#work', apiUrl: 'https://im.example.com' });
createDRTauriEngine({ appKey: '1575529652#support', apiUrl: 'https://im.example.com' });同一個 AppKey 同一時刻只有一個登入使用者,切換賬號要先退出。
令牌與本地資料
儲存在哪裡
本地資料儲存在當前系統使用者的應用資料目錄中(按 tauri.conf.json 中的 identifier 區分),不隨 Windows 的漫遊配置檔案複製到其他電腦:
| 系統 | 目錄 |
|---|---|
| Windows | %LOCALAPPDATA%\{identifier}\deeprespond_im |
| macOS | ~/Library/Application Support/{identifier}/deeprespond_im |
| Linux | ~/.local/share/{identifier}/deeprespond_im |
其中有每個使用者一個 SQLite 資料庫、傳送佇列的附件、加密儲存的登入狀態和裝置標識。處理中的臨時檔案在應用的快取目錄中,啟動時刪除超過 1 天的。macOS、Linux 上這個目錄只有本使用者可以進入。
解除安裝時是否刪除這個目錄由你的安裝包決定,建議詢問使用者是否刪除本地聊天記錄。刪除指定使用者或全部使用者的本地資料用 im.auth.clearLocalData(),見 Web SDK 的介面參考。
登入令牌
登入狀態(含令牌)用一把主金鑰加密後儲存在資料目錄中,主金鑰儲存在系統的鑰匙串裡:macOS 的鑰匙串、Windows 的憑據管理器、Linux 的 Secret Service(GNOME 鑰匙環、KWallet)。只有引擎讀寫,介面視窗拿不到令牌。
鑰匙串不可用時(Linux 上沒有執行 Secret Service 的桌面環境比較常見),按 Rust 中的 secure_storage_fallback 處理:
| 取值 | 說明 |
|---|---|
Fallback::File(預設) | 主金鑰改存到資料目錄中只有本使用者可讀的檔案裡,並在日誌中警告。能讀取這個使用者檔案的程式都能解密出令牌,安全性低於鑰匙串。鑰匙串恢復可用後自動移入 |
Fallback::None | 主金鑰只在記憶體中,每次啟動應用都要重新登入。對安全要求高的應用請選這一項 |
use tauri_plugin_deeprespond_im::Fallback;
let im = tauri_plugin_deeprespond_im::Builder::<tauri::Wry>::new()
.secure_storage_fallback(Fallback::None)
.build();使用者拒絕訪問鑰匙串、或儲存的登入狀態無法解密時,按未登入處理。macOS 上應用的簽名變化後,系統會彈窗詢問是否允許訪問之前儲存的鑰匙串項,請用固定的證書籤名。
裝置標識與裝置名稱
- 裝置標識:
tauri_加 32 位十六進位制隨機數,第一次執行時生成,儲存在資料目錄中。同一臺電腦上,一個系統使用者、一個應用(identifier)對應一個裝置標識,同一應用的全部 AppKey 共用。同一臺電腦再次登入會替換原來的登入,不會多佔裝置數。 - 重新安裝:解除安裝時刪除了資料目錄的,重灌後是新裝置,要重新登入。
- 資料目錄被複制到另一臺電腦(如 macOS 的遷移助理、整機克隆):SDK 發現電腦變了,重新生成裝置標識,使用者要重新登入,本地聊天記錄保留。這樣兩臺電腦不會以同一臺裝置登入、互相擠下線。
- 裝置名稱:預設為計算機名,如“張三的 MacBook Pro”,顯示在本人的登入裝置列表中,其他使用者看不到。計算機名常含使用者的真實姓名,不想使用時用引擎選項
deviceName設定。
加密本地資料庫
資料庫預設不加密,靠系統的使用者隔離保護。需要加密的,分兩步:
在
src-tauri/Cargo.toml中開啟外掛的sqlcipher特性:tomltauri-plugin-deeprespond-im = { version = "=1.1.1", features = ["sqlcipher"] }建立引擎時傳
encryptDatabase: true。
資料庫的金鑰隨機生成,和登入令牌一樣加密儲存。沒有開啟特性卻傳了 true 的,建立引擎以 unsupported(sqlcipher_required)拒絕,不會悄悄地不加密。開啟或關閉加密後第一次啟動時,本地資料庫刪除重建、從服務端重新同步(發出 storage.reset,reason 為 encryption_changed),還沒發出的訊息隨之丟失。
磁碟已滿
本地資料庫寫入失敗時以 storage_error(quota_exceeded)拒絕,下載和儲存附件時為 storage_error(disk_full)。同時發出 storage.full({ freed_users },10 分鐘內最多一次),SDK 刪除這臺電腦上最久沒用過的其他使用者的本地資料騰出空間。收到後請提示使用者清理磁碟。
本地資料庫損壞、安裝了更舊版本的應用時,SDK 刪除資料庫並重新同步,發出 storage.reset(reason 分別為 corrupted、schema_downgrade)。
Rust 側的設定
外掛的 Builder 方法:
| 方法 | 預設 | 說明 |
|---|---|---|
engine_url(path) | "engine.html" | 引擎頁在前端資源中的路徑。遠端地址在啟動時報錯(開發構建允許本機開發伺服器的地址) |
main_window(label) | "main" | 通知點選、來電接聽沒有視窗處理時,顯示並聚焦的視窗 |
on_main_window_missing(f) | 無 | 上一項的視窗不存在時呼叫,由你重新建立:|app| { ... } |
exit_when_last_window_closed(bool) | true | 除引擎視窗外的最後一個視窗關閉時退出應用;縮到托盤的應用設為 false |
secure_storage_fallback(f) | Fallback::File | 鑰匙串不可用時的處理,見登入令牌 |
log_file(bool)、log_dir(path) | false、應用的日誌目錄 | 寫日誌檔案,見診斷與日誌 |
proxy(url) | 系統代理 | 外掛上傳、下載檔案使用的代理,見網路與代理 |
allow_read_dir(path)、allow_write_dir(path) | 無 | 額外允許讀取、寫入的目錄,可以多次呼叫,見檔案的來源 |
on_unread_changed(f) | 無 | 未讀數變化時呼叫:|app, unread| { ... },unread.total 為未讀總數,unread.badge 為角標數。用於更新托盤的提示文字或選單 |
on_notification_click(f) | 無 | 通知點選在 300 毫秒內沒有視窗認領時呼叫,見通知的點選 |
on_call_action(f) | 無 | 來電通知上的按鈕被點選時呼叫:|app, action| { ... },action.action 為 accept 或 decline,action.call_id 為通話 ID,見來電通知 |
disable_app_nap(bool) | true | macOS 上登入期間阻止 App Nap,避免系統拖慢心跳(不阻止系統休眠) |
只用預設設定時,可以寫 tauri_plugin_deeprespond_im::init()。
其他 Rust 介面:
| 介面 | 說明 |
|---|---|
tauri_plugin_deeprespond_im::exit(app, code) | 收尾後退出應用,見退出與托盤應用 |
tauri_plugin_deeprespond_im::handle_args(app, &argv) | 在單例項外掛的回撥中呼叫,交出第二次啟動的引數中的通知點選 |
app.deeprespond_im().unread() | 當前的未讀總數和角標數(Unread { total, badge }),需要 use tauri_plugin_deeprespond_im::DeeprespondImExt; |
app.deeprespond_im().engine_state() | 引擎狀態(EngineState::Starting、Ready、Restarting、Failed) |
介面參考
createDRTauriEngine()
在引擎頁中建立引擎客戶端。從 @deeprespond/im-tauri/engine 匯入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
options | DRTauriEngineOptions | 是 | 見引擎頁的選項 |
返回值:DREngine,見下文。
可能的錯誤(同步丟擲):unsupported(not_engine_window,不在引擎頁中;WebView 不滿足要求)、invalid_state(client_exists)、local_validation(appKey、apiUrl 格式不對)。外掛與 npm 包的版本不同(version_mismatch)、沒有 sqlcipher 特性卻要求加密(sqlcipher_required)、另一個程序正在使用同一份資料(another_instance)時,引擎客戶端不能工作,原因寫入日誌,介面視窗中的呼叫以對應的錯誤失敗。
DREngine
| 成員 | 說明 |
|---|---|
client | 引擎客戶端,與介面視窗中的客戶端用法相同,另有 desktop |
onAlert(handler) | notifications.mode 為 manual 時,處理負責提醒的是引擎時的新訊息和申請,見自己顯示通知。返回取消註冊的函式 |
onIncomingCall(handler) | notifications.calls 為 manual 時,處理負責提醒的是引擎時的來電,引數為 CallHandle,見自己處理來電。返回取消註冊的函式 |
createDRTauriClient()
在介面視窗中建立客戶端。從 @deeprespond/im-tauri 匯入。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
options.appKey | string | 是 | 應用的 AppKey,與引擎頁中的相同 |
options.logger | { level?: LogLevel; sink?: (entry: LogEntry) => void } | 否 | 本視窗的日誌;開啟了日誌檔案時同時寫入檔案 |
返回值:DRTauriClient,即 Web SDK 的 DRClient 加上 desktop 和 engineState。
可能的錯誤(同步丟擲):unsupported(not_tauri,不在 Tauri 應用中;not_ui_window,在引擎頁中;WebView 不滿足要求)、invalid_state(client_exists)、local_validation(appKey 格式不對)。
im.engineState
只讀屬性,'starting' | 'ready' | 'restarting' | 'failed',引擎當前的狀態,見引擎的狀態。變化時發出 engine.stateChanged。只在介面視窗中有。
獨立頻道視窗生命週期
獨立 RTC 頻道的媒體連線只在介面視窗中執行,引擎只負責轉發需要登入身份的請求,引擎視窗不能加入頻道。視窗關閉、被系統凍結、客戶端被銷燬或引擎重啟時,SDK 斷開媒體;程序被強制結束時不保證能通知服務端離開。視窗最小化不算離開。
銷燬客戶端時,SDK 最多等 5 秒讓頻道退出,這與應用退出時的收尾時間是兩回事。各系統的裝置許可權和 WebView 能力請在目標系統上實測,完整說明見獨立頻道。
