引擎与窗口
本页介绍 Tauri SDK 的运行方式:为什么要有引擎窗口、引擎页的全部选项、引擎的启动和重建、多个界面窗口、由谁来提醒、在引擎页中处理提醒和来电、退出与托盘应用、单实例与多个 AppKey,以及令牌和本地数据保存在哪里。
为什么要引擎窗口
桌面 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 = "=2.0.0", 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。只在界面窗口中有。
独立频道窗口生命周期
独立频道的媒体只在界面窗口运行,引擎转发 HTTP 鉴权;隐藏引擎不支持媒体加入。窗口关闭、冻结、客户端销毁或引擎重启会清理媒体;不保证进程强杀时 HTTP 退出成功。普通最小化不等于离开。
频道主动销毁的退出尝试最多等待 5 秒,不替代应用整体退出的等待规则。各系统设备权限与 WebView 能力需验收,完整说明见独立频道。
