@deeprespond/im-tauri 0.0.0-stage → 1.0.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/chunk-DMEEI2YZ.js +1161 -0
- package/dist/chunk-DMEEI2YZ.js.map +1 -0
- package/dist/engine.d.ts +21 -0
- package/dist/engine.js +536 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/types-CVlFmFSn.d.ts +277 -0
- package/package.json +52 -4
- package/src/templates/engine.html +20 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { currentLabel, ENGINE_LABEL, unsupported, Bridge, makeLogger, Desktop, createTauriPlatform, invalidState, osFromUserAgent, clickEvent } from './chunk-DMEEI2YZ.js';
|
|
2
|
+
import { DRError, createDRClient } from '@deeprespond/im-web';
|
|
3
|
+
import { displayName, conversationTitle } from '@deeprespond/im-web/render';
|
|
4
|
+
|
|
5
|
+
// src/desktop/ringtone.ts
|
|
6
|
+
var Ringtone = class {
|
|
7
|
+
constructor(src) {
|
|
8
|
+
this.src = src;
|
|
9
|
+
}
|
|
10
|
+
src;
|
|
11
|
+
#audio = null;
|
|
12
|
+
#ctx = null;
|
|
13
|
+
#timer = null;
|
|
14
|
+
get playing() {
|
|
15
|
+
return this.#audio !== null || this.#timer !== null;
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
if (this.playing) return;
|
|
19
|
+
if (this.src && typeof Audio !== "undefined") {
|
|
20
|
+
const audio = new Audio(this.src);
|
|
21
|
+
audio.loop = true;
|
|
22
|
+
this.#audio = audio;
|
|
23
|
+
void audio.play().catch(() => {
|
|
24
|
+
this.#audio = null;
|
|
25
|
+
this.#synth();
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.#synth();
|
|
30
|
+
}
|
|
31
|
+
stop() {
|
|
32
|
+
if (this.#audio) {
|
|
33
|
+
this.#audio.pause();
|
|
34
|
+
this.#audio.src = "";
|
|
35
|
+
this.#audio = null;
|
|
36
|
+
}
|
|
37
|
+
if (this.#timer !== null) {
|
|
38
|
+
clearInterval(this.#timer);
|
|
39
|
+
this.#timer = null;
|
|
40
|
+
}
|
|
41
|
+
if (this.#ctx) {
|
|
42
|
+
void this.#ctx.close().catch(() => {
|
|
43
|
+
});
|
|
44
|
+
this.#ctx = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 每 3 秒响一次:两段 0.4 秒的双音(440 Hz 与 480 Hz 叠加,接近电话铃声)。 */
|
|
48
|
+
#synth() {
|
|
49
|
+
const Ctx = globalThis.AudioContext;
|
|
50
|
+
if (!Ctx) return;
|
|
51
|
+
const ctx = new Ctx();
|
|
52
|
+
this.#ctx = ctx;
|
|
53
|
+
const ring = () => {
|
|
54
|
+
const now = ctx.currentTime;
|
|
55
|
+
for (const start of [0, 0.6]) {
|
|
56
|
+
const gain = ctx.createGain();
|
|
57
|
+
gain.gain.setValueAtTime(0, now + start);
|
|
58
|
+
gain.gain.linearRampToValueAtTime(0.2, now + start + 0.02);
|
|
59
|
+
gain.gain.setValueAtTime(0.2, now + start + 0.38);
|
|
60
|
+
gain.gain.linearRampToValueAtTime(0, now + start + 0.4);
|
|
61
|
+
gain.connect(ctx.destination);
|
|
62
|
+
for (const freq of [440, 480]) {
|
|
63
|
+
const osc = ctx.createOscillator();
|
|
64
|
+
osc.frequency.value = freq;
|
|
65
|
+
osc.connect(gain);
|
|
66
|
+
osc.start(now + start);
|
|
67
|
+
osc.stop(now + start + 0.4);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
void ctx.resume?.().catch(() => {
|
|
72
|
+
});
|
|
73
|
+
ring();
|
|
74
|
+
this.#timer = setInterval(ring, 3e3);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/desktop/texts.ts
|
|
79
|
+
var texts = {
|
|
80
|
+
friendRequest: { title: "新的好友申请", body: (name, message) => message ? `${name}:${message}` : `${name} 请求添加你为好友` },
|
|
81
|
+
groupInvitation: { title: "入群邀请", body: (group) => `邀请你加入“${group}”` },
|
|
82
|
+
groupApplication: { title: "入群申请", body: (group) => `“${group}”有新的入群申请` },
|
|
83
|
+
incomingCall: { title: (name) => name, body: (video) => video ? "邀请你视频通话" : "邀请你语音通话", accept: "接听", decline: "拒绝" },
|
|
84
|
+
sessionEnded: {
|
|
85
|
+
replaced: { title: "已在其他设备登录", body: "你的账号已在其他设备登录,点击打开应用" },
|
|
86
|
+
expired: { title: "登录已失效", body: "请打开应用重新登录" }
|
|
87
|
+
},
|
|
88
|
+
unnamedGroup: "群聊"
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/desktop/alerts.ts
|
|
92
|
+
var SAME_CONVERSATION_QUIET_MS = 3e3;
|
|
93
|
+
var GLOBAL_SOUND_GAP_MS = 1e3;
|
|
94
|
+
var EngineAlerts = class {
|
|
95
|
+
constructor(bridge, client, desktop, host, opts, onWarn) {
|
|
96
|
+
this.bridge = bridge;
|
|
97
|
+
this.client = client;
|
|
98
|
+
this.desktop = desktop;
|
|
99
|
+
this.host = host;
|
|
100
|
+
this.opts = opts;
|
|
101
|
+
this.onWarn = onWarn;
|
|
102
|
+
this.#ringtone = new Ringtone(opts.notifications?.ringtone);
|
|
103
|
+
}
|
|
104
|
+
bridge;
|
|
105
|
+
client;
|
|
106
|
+
desktop;
|
|
107
|
+
host;
|
|
108
|
+
opts;
|
|
109
|
+
onWarn;
|
|
110
|
+
alertHandlers = /* @__PURE__ */ new Set();
|
|
111
|
+
callHandlers = /* @__PURE__ */ new Set();
|
|
112
|
+
#lastSound = /* @__PURE__ */ new Map();
|
|
113
|
+
#lastAnySound = 0;
|
|
114
|
+
#ringtone;
|
|
115
|
+
#ringing = /* @__PURE__ */ new Map();
|
|
116
|
+
#friendUnread = -1;
|
|
117
|
+
#invitations = /* @__PURE__ */ new Set();
|
|
118
|
+
#offs = [];
|
|
119
|
+
get #mode() {
|
|
120
|
+
return this.opts.notifications?.mode ?? "auto";
|
|
121
|
+
}
|
|
122
|
+
start() {
|
|
123
|
+
const c = this.client;
|
|
124
|
+
this.#offs.push(
|
|
125
|
+
c.on("message.received", (p) => void this.#message(p.message, p.notify)),
|
|
126
|
+
c.on("friendRequests.changed", (p) => void this.#friendRequests(p.lists, p.unread_count)),
|
|
127
|
+
c.on("groupRequests.changed", (p) => void this.#groupRequests(p)),
|
|
128
|
+
c.on("call.incoming", (p) => void this.#incoming(p.call, p.alert)),
|
|
129
|
+
c.on("auth.stateChanged", (state) => {
|
|
130
|
+
if (state.kind !== "signed_out" && state.kind !== "ended") return;
|
|
131
|
+
const cleared = this.#signedOut();
|
|
132
|
+
if (state.kind === "ended") void cleared.then(() => this.#sessionEnded(state.code, state.reason));
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
stop() {
|
|
137
|
+
for (const off of this.#offs.splice(0)) off();
|
|
138
|
+
for (const stop of this.#ringing.values()) stop();
|
|
139
|
+
this.#ringing.clear();
|
|
140
|
+
this.#ringtone.stop();
|
|
141
|
+
}
|
|
142
|
+
/** 负责提醒的是不是引擎(没有可见的界面窗口)。 */
|
|
143
|
+
#engineAlerts() {
|
|
144
|
+
try {
|
|
145
|
+
return this.host().alertTab().self;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async #emitAlert(alert) {
|
|
151
|
+
for (const handler of [...this.alertHandlers]) {
|
|
152
|
+
try {
|
|
153
|
+
await handler(alert);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
this.onWarn("App 的 onAlert 出错", err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
#text(alert, fallback) {
|
|
160
|
+
const format = this.opts.notifications?.format;
|
|
161
|
+
if (!format) return fallback;
|
|
162
|
+
try {
|
|
163
|
+
return format(alert);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.onWarn("notifications.format 出错,使用默认文字", err);
|
|
166
|
+
return fallback;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** 同一会话 3 秒内不再响;全部通知每秒最多响一次。 */
|
|
170
|
+
#sound(key) {
|
|
171
|
+
if (this.opts.notifications?.sound === false) return false;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const last = this.#lastSound.get(key) ?? 0;
|
|
174
|
+
const allowed = now - last >= SAME_CONVERSATION_QUIET_MS && now - this.#lastAnySound >= GLOBAL_SOUND_GAP_MS;
|
|
175
|
+
this.#lastSound.set(key, now);
|
|
176
|
+
if (allowed) this.#lastAnySound = now;
|
|
177
|
+
if (this.#lastSound.size > 500) this.#lastSound.clear();
|
|
178
|
+
return allowed;
|
|
179
|
+
}
|
|
180
|
+
#attention() {
|
|
181
|
+
if (this.opts.attention === false) return;
|
|
182
|
+
void this.bridge.call("attention_request", { critical: false, label: null }).catch(() => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async #message(message, decision) {
|
|
186
|
+
if (!decision?.notify) return;
|
|
187
|
+
const host = this.host();
|
|
188
|
+
const alertTab = host.alertTab();
|
|
189
|
+
if (this.#mode === "manual") {
|
|
190
|
+
if (alertTab.self) await this.#emitAlert({ kind: "message", message, notify: decision });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (host.viewing(message.conversation_id)) return;
|
|
194
|
+
if (!alertTab.self && alertTab.focused) return;
|
|
195
|
+
const text = this.#text({ kind: "message", message, notify: decision }, { title: decision.title ?? "", body: decision.body ?? "" });
|
|
196
|
+
if (!text) return;
|
|
197
|
+
await this.desktop.show({ id: `dr:msg:${message.conversation_id}`, kind: "message", title: text.title, body: text.body, conversation_id: message.conversation_id, seq: message.seq ?? void 0, sound: this.#sound(message.conversation_id) }).catch((err) => this.onWarn("显示消息通知失败", err));
|
|
198
|
+
this.#attention();
|
|
199
|
+
}
|
|
200
|
+
async #notice(alert, id) {
|
|
201
|
+
if (this.opts.notifications?.notices === false) return;
|
|
202
|
+
if (this.#mode === "manual") {
|
|
203
|
+
if (this.#engineAlerts()) await this.#emitAlert(alert);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const alertTab = this.host().alertTab();
|
|
207
|
+
if (!alertTab.self && alertTab.focused) return;
|
|
208
|
+
const text = this.#text(alert, { title: alert.title, body: alert.body });
|
|
209
|
+
if (!text) return;
|
|
210
|
+
const data = {};
|
|
211
|
+
if (alert.user) data.user = alert.user;
|
|
212
|
+
if (alert.group_id) data.group_id = alert.group_id;
|
|
213
|
+
await this.desktop.show({ id, kind: "notice", title: text.title, body: text.body, data, sound: this.#sound(id) }).catch((err) => this.onWarn("显示通知失败", err));
|
|
214
|
+
this.#attention();
|
|
215
|
+
}
|
|
216
|
+
async #friendRequests(lists, unread) {
|
|
217
|
+
const previous = this.#friendUnread;
|
|
218
|
+
this.#friendUnread = unread;
|
|
219
|
+
if (!lists.includes("received") || previous < 0 || unread <= previous) return;
|
|
220
|
+
let latest;
|
|
221
|
+
try {
|
|
222
|
+
latest = (await this.client.friends.requests.received({ status: "pending", limit: 1 })).items[0];
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!latest) return;
|
|
227
|
+
const name = latest.nickname || latest.username;
|
|
228
|
+
await this.#notice({ kind: "notice", type: "friend_request", title: texts.friendRequest.title, body: texts.friendRequest.body(name, latest.message), user: latest.username }, `dr:notice:friend:${latest.username}`);
|
|
229
|
+
}
|
|
230
|
+
async #groupRequests(p) {
|
|
231
|
+
if (p.list === "invitations" && Array.isArray(p.items)) {
|
|
232
|
+
const fresh = [];
|
|
233
|
+
const seen = /* @__PURE__ */ new Set();
|
|
234
|
+
for (const item of p.items) {
|
|
235
|
+
const id = item.group_id ?? item.group?.group_id;
|
|
236
|
+
if (!id || item.status && item.status !== "pending") continue;
|
|
237
|
+
seen.add(id);
|
|
238
|
+
if (!this.#invitations.has(id)) fresh.push({ group_id: id, name: item.group?.name });
|
|
239
|
+
}
|
|
240
|
+
const first = this.#invitations.size === 0 && fresh.length === seen.size && seen.size > 0 && !this.#primed;
|
|
241
|
+
this.#invitations = seen;
|
|
242
|
+
this.#primed = true;
|
|
243
|
+
if (first) return;
|
|
244
|
+
for (const inv of fresh) {
|
|
245
|
+
const group = inv.name || texts.unnamedGroup;
|
|
246
|
+
await this.#notice({ kind: "notice", type: "group_request", title: texts.groupInvitation.title, body: texts.groupInvitation.body(group), group_id: inv.group_id }, `dr:notice:group:${inv.group_id}`);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (p.list === "pending" && p.group_id) {
|
|
251
|
+
const group = await this.client.groups.get(p.group_id).catch(() => void 0);
|
|
252
|
+
await this.#notice({ kind: "notice", type: "group_request", title: texts.groupApplication.title, body: texts.groupApplication.body(group?.name || texts.unnamedGroup), group_id: p.group_id }, `dr:notice:group:${p.group_id}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
#primed = false;
|
|
256
|
+
async #incoming(call, alert) {
|
|
257
|
+
if (!alert) return;
|
|
258
|
+
if ((this.opts.notifications?.calls ?? "auto") === "manual") {
|
|
259
|
+
for (const handler of [...this.callHandlers]) {
|
|
260
|
+
try {
|
|
261
|
+
await handler(call);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
this.onWarn("App 的 onIncomingCall 出错", err);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const id = `dr:call:${call.call_id}`;
|
|
269
|
+
if (this.#ringing.has(id)) return;
|
|
270
|
+
const snapshot = call.getSnapshot();
|
|
271
|
+
const inviter = snapshot.inviter?.nickname || snapshot.inviter?.username || (snapshot.call.initiator ? displayName(this.client, snapshot.call.initiator) : "");
|
|
272
|
+
const title = snapshot.group?.name ? `${inviter}(${snapshot.group.name})` : inviter;
|
|
273
|
+
await this.desktop.show({
|
|
274
|
+
id,
|
|
275
|
+
kind: "call",
|
|
276
|
+
title: texts.incomingCall.title(title),
|
|
277
|
+
body: texts.incomingCall.body(snapshot.call.media === "video"),
|
|
278
|
+
data: { call_id: call.call_id },
|
|
279
|
+
sound: false,
|
|
280
|
+
actions: [
|
|
281
|
+
{ id: "accept", title: texts.incomingCall.accept },
|
|
282
|
+
{ id: "decline", title: texts.incomingCall.decline }
|
|
283
|
+
]
|
|
284
|
+
}).catch((err) => this.onWarn("显示来电通知失败", err));
|
|
285
|
+
this.#ringtone.start();
|
|
286
|
+
this.#attention();
|
|
287
|
+
const stop = () => {
|
|
288
|
+
unsubscribe();
|
|
289
|
+
this.#ringing.delete(id);
|
|
290
|
+
void this.desktop.clear(id).catch(() => {
|
|
291
|
+
});
|
|
292
|
+
if (this.#ringing.size === 0) this.#ringtone.stop();
|
|
293
|
+
};
|
|
294
|
+
const unsubscribe = call.subscribe(() => {
|
|
295
|
+
const s = call.getSnapshot();
|
|
296
|
+
if (!s.ringing || s.call.status === "ended" || s.self?.state !== "ringing") stop();
|
|
297
|
+
});
|
|
298
|
+
this.#ringing.set(id, stop);
|
|
299
|
+
}
|
|
300
|
+
/** 来电通知上的“拒绝”(插件直接交给引擎)。 */
|
|
301
|
+
async declineCall(callId) {
|
|
302
|
+
const call = this.client.calls.incoming.find((c) => c.call_id === callId);
|
|
303
|
+
if (!call) return;
|
|
304
|
+
await call.reject().catch((err) => this.onWarn("拒绝来电失败", err));
|
|
305
|
+
}
|
|
306
|
+
async #sessionEnded(code, reason) {
|
|
307
|
+
if (this.#mode !== "auto" || !this.#engineAlerts()) return;
|
|
308
|
+
const replaced = code === "session_revoked" && (reason === "replaced" || reason === "device_limit" || reason === "kicked");
|
|
309
|
+
const t = replaced ? texts.sessionEnded.replaced : texts.sessionEnded.expired;
|
|
310
|
+
await this.desktop.show({ id: "dr:session", kind: "session", title: t.title, body: t.body }).catch((err) => this.onWarn("显示通知失败", err));
|
|
311
|
+
}
|
|
312
|
+
/** 退出登录、会话结束:清除本应用已显示的通知,停止铃声(与 Flutter SDK 相同)。 */
|
|
313
|
+
#signedOut() {
|
|
314
|
+
for (const stop of [...this.#ringing.values()]) stop();
|
|
315
|
+
this.#friendUnread = -1;
|
|
316
|
+
this.#invitations.clear();
|
|
317
|
+
this.#primed = false;
|
|
318
|
+
return this.bridge.call("notify_clear", { all: true }).catch(() => {
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** 会话标题(App 的 format 中可以使用)。 */
|
|
322
|
+
title(view) {
|
|
323
|
+
return conversationTitle(this.client, view);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/desktop/badge.ts
|
|
328
|
+
var MERGE_MS = 500;
|
|
329
|
+
var UnreadBadge = class {
|
|
330
|
+
constructor(bridge, client, host, opts) {
|
|
331
|
+
this.bridge = bridge;
|
|
332
|
+
this.client = client;
|
|
333
|
+
this.host = host;
|
|
334
|
+
this.opts = opts;
|
|
335
|
+
}
|
|
336
|
+
bridge;
|
|
337
|
+
client;
|
|
338
|
+
host;
|
|
339
|
+
opts;
|
|
340
|
+
#timer = null;
|
|
341
|
+
#last = null;
|
|
342
|
+
#offs = [];
|
|
343
|
+
start() {
|
|
344
|
+
this.#offs.push(
|
|
345
|
+
this.client.on("conversations.unreadChanged", () => this.schedule()),
|
|
346
|
+
this.client.on("push.changed", () => this.schedule()),
|
|
347
|
+
this.client.on("auth.stateChanged", (state) => {
|
|
348
|
+
if (state.kind !== "signed_in" && state.kind !== "suspended") this.#apply({ total: 0, badge: 0 });
|
|
349
|
+
else this.schedule();
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
this.schedule();
|
|
353
|
+
}
|
|
354
|
+
stop() {
|
|
355
|
+
for (const off of this.#offs.splice(0)) off();
|
|
356
|
+
if (this.#timer !== null) clearTimeout(this.#timer);
|
|
357
|
+
this.#timer = null;
|
|
358
|
+
}
|
|
359
|
+
schedule() {
|
|
360
|
+
if (this.#timer !== null) return;
|
|
361
|
+
this.#timer = setTimeout(() => {
|
|
362
|
+
this.#timer = null;
|
|
363
|
+
let totals;
|
|
364
|
+
try {
|
|
365
|
+
totals = this.host().unreadTotals();
|
|
366
|
+
} catch {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const signedIn = this.client.auth.state.kind === "signed_in" || this.client.auth.state.kind === "suspended";
|
|
370
|
+
const total = signedIn ? totals.total : 0;
|
|
371
|
+
const badge = signedIn ? this.opts.includeMuted ? totals.total : totals.unmuted : 0;
|
|
372
|
+
this.#apply({ total, badge });
|
|
373
|
+
}, MERGE_MS);
|
|
374
|
+
}
|
|
375
|
+
#apply(next) {
|
|
376
|
+
if (this.#last && this.#last.total === next.total && this.#last.badge === next.badge) return;
|
|
377
|
+
this.#last = next;
|
|
378
|
+
if (this.opts.auto) void this.bridge.call("badge_set", { count: next.badge > 0 ? next.badge : null }).catch((err) => this.opts.onWarn("设置角标失败", err));
|
|
379
|
+
void this.bridge.call("unread_report", next).catch(() => {
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
this.host().publish("desktop.unreadChanged", next);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/engine.ts
|
|
389
|
+
var ALIVE_MS = 3e3;
|
|
390
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
391
|
+
var WAKE_LAG_MS = 5e3;
|
|
392
|
+
var DEFAULT_PERSIST_MAX = 200 * 1024 * 1024;
|
|
393
|
+
var pages = /* @__PURE__ */ new WeakMap();
|
|
394
|
+
function createDRTauriEngine(options, env) {
|
|
395
|
+
if ((env ? env.bridge.label : currentLabel()) !== ENGINE_LABEL) throw unsupported("not_engine_window", "createDRTauriEngine 只能在引擎页中调用");
|
|
396
|
+
if (!options || typeof options.appKey !== "string") throw new DRError({ code: "local_validation", message: "appKey", details: { reason: "required", field: "appKey" }, source: "local" });
|
|
397
|
+
const bridge = env?.bridge ?? Bridge.get();
|
|
398
|
+
let engines = pages.get(bridge);
|
|
399
|
+
if (!engines) {
|
|
400
|
+
engines = /* @__PURE__ */ new Map();
|
|
401
|
+
pages.set(bridge, engines);
|
|
402
|
+
wirePage(bridge, engines);
|
|
403
|
+
}
|
|
404
|
+
const logger = makeLogger(bridge, options.logger);
|
|
405
|
+
const warn = (message, err) => logger.sink({ level: "warn", time: Date.now(), module: "tauri", message, code: err instanceof DRError ? err.code : void 0, reason: err instanceof DRError ? err.reason : void 0 });
|
|
406
|
+
let client;
|
|
407
|
+
const desktop = new Desktop(bridge, () => client);
|
|
408
|
+
const tp = createTauriPlatform({
|
|
409
|
+
role: "leader",
|
|
410
|
+
appKey: options.appKey,
|
|
411
|
+
bridge,
|
|
412
|
+
encrypt: options.encryptDatabase === true,
|
|
413
|
+
onWarn: warn,
|
|
414
|
+
extend: (h) => {
|
|
415
|
+
env?.onHost?.(h);
|
|
416
|
+
return { desktop: { value: desktop, enumerable: true } };
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
if (env?.overrides) Object.assign(tp.platform, env.overrides);
|
|
420
|
+
tp.platform.ready = bridge.ready.then(async (info) => {
|
|
421
|
+
if (info.another_instance) throw invalidState("another_instance");
|
|
422
|
+
await tp.engineKv.load();
|
|
423
|
+
});
|
|
424
|
+
client = createDRClient({
|
|
425
|
+
appKey: options.appKey,
|
|
426
|
+
apiUrl: options.apiUrl,
|
|
427
|
+
wsUrl: options.wsUrl,
|
|
428
|
+
platform: osFromUserAgent(typeof navigator !== "undefined" ? navigator.userAgent : ""),
|
|
429
|
+
deviceName: options.deviceName,
|
|
430
|
+
appVersion: options.appVersion,
|
|
431
|
+
ticketProvider: options.ticketProvider,
|
|
432
|
+
autoMarkRead: options.autoMarkRead,
|
|
433
|
+
conversationWindow: options.conversationWindow,
|
|
434
|
+
userCacheTtlMs: options.userCacheTtlMs,
|
|
435
|
+
outbox: { maxAgeHours: options.outbox?.maxAgeHours, persistAttachmentMaxBytes: options.outbox?.persistAttachmentMaxBytes ?? DEFAULT_PERSIST_MAX },
|
|
436
|
+
media: options.media,
|
|
437
|
+
logger,
|
|
438
|
+
platformAdapter: tp.platform
|
|
439
|
+
});
|
|
440
|
+
const host = tp.host;
|
|
441
|
+
const alerts = new EngineAlerts(bridge, client, desktop, host, options, warn);
|
|
442
|
+
const badge = new UnreadBadge(bridge, client, host, { auto: (options.badge ?? "auto") === "auto", includeMuted: options.badgeIncludesMuted === true, onWarn: warn });
|
|
443
|
+
const entry = { client, host, alerts, badge, platform: tp };
|
|
444
|
+
engines.get(options.appKey)?.alerts.stop();
|
|
445
|
+
engines.get(options.appKey)?.badge.stop();
|
|
446
|
+
engines.set(options.appKey, entry);
|
|
447
|
+
void tp.platform.ready.then(
|
|
448
|
+
() => {
|
|
449
|
+
const signedIn = () => client.auth.state.kind === "signed_in" || client.auth.state.kind === "suspended";
|
|
450
|
+
const report = () => void bridge.call("engine_ready", { app_key: options.appKey, signed_in: signedIn() }).catch((err) => warn("报告引擎就绪失败", err));
|
|
451
|
+
report();
|
|
452
|
+
client.on("auth.stateChanged", report);
|
|
453
|
+
alerts.start();
|
|
454
|
+
badge.start();
|
|
455
|
+
},
|
|
456
|
+
(err) => warn("引擎客户端没有创建", err)
|
|
457
|
+
);
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
client,
|
|
460
|
+
onAlert: (handler) => {
|
|
461
|
+
alerts.alertHandlers.add(handler);
|
|
462
|
+
return () => alerts.alertHandlers.delete(handler);
|
|
463
|
+
},
|
|
464
|
+
onIncomingCall: (handler) => {
|
|
465
|
+
alerts.callHandlers.add(handler);
|
|
466
|
+
return () => alerts.callHandlers.delete(handler);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function wirePage(bridge, engines) {
|
|
471
|
+
const each = (fn) => {
|
|
472
|
+
for (const e of engines.values()) {
|
|
473
|
+
try {
|
|
474
|
+
fn(e);
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
setInterval(() => void bridge.call("engine_alive").catch(() => {
|
|
480
|
+
}), ALIVE_MS);
|
|
481
|
+
bridge.on(
|
|
482
|
+
"wake",
|
|
483
|
+
() => each((e) => {
|
|
484
|
+
const host = e.host();
|
|
485
|
+
const lag = host.heartbeatLag();
|
|
486
|
+
if (lag > WAKE_LAG_MS) {
|
|
487
|
+
host.log("warn", "tauri", `唤醒检查落后 ${Math.round(lag)} 毫秒,计时器可能被系统节流`);
|
|
488
|
+
host.checkConnection("wake_check", PROBE_TIMEOUT_MS);
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
);
|
|
492
|
+
bridge.on(
|
|
493
|
+
"resume",
|
|
494
|
+
(event) => each((e) => {
|
|
495
|
+
const host = e.host();
|
|
496
|
+
host.checkConnection("resume", PROBE_TIMEOUT_MS);
|
|
497
|
+
host.publish("desktop.systemResumed", { slept_ms: Number(event.slept_ms) || 0 });
|
|
498
|
+
})
|
|
499
|
+
);
|
|
500
|
+
bridge.on(
|
|
501
|
+
"network",
|
|
502
|
+
(event) => each((e) => {
|
|
503
|
+
const host = e.host();
|
|
504
|
+
if (event.online !== false) host.checkConnection("network", PROBE_TIMEOUT_MS);
|
|
505
|
+
host.publish("desktop.networkChanged", { online: event.online !== false });
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
bridge.on("screen_lock", () => {
|
|
509
|
+
});
|
|
510
|
+
bridge.on("storage_full", (event) => {
|
|
511
|
+
const e = engines.get(String(event.app_key));
|
|
512
|
+
e?.host().publish("storage.full", { freed_users: Number(event.freed_users) || 0 });
|
|
513
|
+
});
|
|
514
|
+
bridge.on("call_action", (event) => {
|
|
515
|
+
if (event.action !== "decline") return;
|
|
516
|
+
each((e) => void e.alerts.declineCall(String(event.call_id)));
|
|
517
|
+
});
|
|
518
|
+
bridge.on("notification_click", (event) => {
|
|
519
|
+
const payload = clickEvent(bridge, event);
|
|
520
|
+
each((e) => e.host().emit("desktop.notificationClicked", payload));
|
|
521
|
+
});
|
|
522
|
+
bridge.on("exit", () => {
|
|
523
|
+
const work = [...engines.values()].map(async (e) => {
|
|
524
|
+
e.alerts.stop();
|
|
525
|
+
e.badge.stop();
|
|
526
|
+
await e.platform.engineKv?.flush();
|
|
527
|
+
await e.host().shutdown();
|
|
528
|
+
});
|
|
529
|
+
void Promise.allSettled(work).then(() => bridge.call("engine_exit_done").catch(() => {
|
|
530
|
+
}));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export { createDRTauriEngine };
|
|
535
|
+
//# sourceMappingURL=engine.js.map
|
|
536
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/desktop/ringtone.ts","../src/desktop/texts.ts","../src/desktop/alerts.ts","../src/desktop/badge.ts","../src/engine.ts"],"names":[],"mappings":"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","file":"engine.js","sourcesContent":["/**\n * 没有界面窗口时的来电铃声(《2.功能概述文档》第 10 节):在引擎页中播放。给了资源路径的循环播放那个文件,\n * 否则用 Web Audio 合成的双音铃声。Tauri 的 WebView 默认不要求用户手势就能播放(wry 的 autoplay 默认开启)。\n */\nexport class Ringtone {\n #audio: HTMLAudioElement | null = null;\n #ctx: AudioContext | null = null;\n #timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(private readonly src?: string) {}\n\n get playing(): boolean {\n return this.#audio !== null || this.#timer !== null;\n }\n\n start(): void {\n if (this.playing) return;\n if (this.src && typeof Audio !== 'undefined') {\n const audio = new Audio(this.src);\n audio.loop = true;\n this.#audio = audio;\n void audio.play().catch(() => {\n this.#audio = null;\n this.#synth();\n });\n return;\n }\n this.#synth();\n }\n\n stop(): void {\n if (this.#audio) {\n this.#audio.pause();\n this.#audio.src = '';\n this.#audio = null;\n }\n if (this.#timer !== null) {\n clearInterval(this.#timer);\n this.#timer = null;\n }\n if (this.#ctx) {\n void this.#ctx.close().catch(() => {});\n this.#ctx = null;\n }\n }\n\n /** 每 3 秒响一次:两段 0.4 秒的双音(440 Hz 与 480 Hz 叠加,接近电话铃声)。 */\n #synth(): void {\n const Ctx = (globalThis as { AudioContext?: typeof AudioContext }).AudioContext;\n if (!Ctx) return;\n const ctx = new Ctx();\n this.#ctx = ctx;\n const ring = () => {\n const now = ctx.currentTime;\n for (const start of [0, 0.6]) {\n const gain = ctx.createGain();\n gain.gain.setValueAtTime(0, now + start);\n gain.gain.linearRampToValueAtTime(0.2, now + start + 0.02);\n gain.gain.setValueAtTime(0.2, now + start + 0.38);\n gain.gain.linearRampToValueAtTime(0, now + start + 0.4);\n gain.connect(ctx.destination);\n for (const freq of [440, 480]) {\n const osc = ctx.createOscillator();\n osc.frequency.value = freq;\n osc.connect(gain);\n osc.start(now + start);\n osc.stop(now + start + 0.4);\n }\n }\n };\n void ctx.resume?.().catch(() => {});\n ring();\n this.#timer = setInterval(ring, 3000);\n }\n}\n","/** 引擎自动显示的通知中 SDK 自己的文字(消息通知的标题和正文由推送设置的预览方式生成,见 push.shouldNotify)。 */\nexport const texts = {\n friendRequest: { title: '新的好友申请', body: (name: string, message: string) => (message ? `${name}:${message}` : `${name} 请求添加你为好友`) },\n groupInvitation: { title: '入群邀请', body: (group: string) => `邀请你加入“${group}”` },\n groupApplication: { title: '入群申请', body: (group: string) => `“${group}”有新的入群申请` },\n incomingCall: { title: (name: string) => name, body: (video: boolean) => (video ? '邀请你视频通话' : '邀请你语音通话'), accept: '接听', decline: '拒绝' },\n sessionEnded: {\n replaced: { title: '已在其他设备登录', body: '你的账号已在其他设备登录,点击打开应用' },\n expired: { title: '登录已失效', body: '请打开应用重新登录' },\n },\n unnamedGroup: '群聊',\n};\n","import type { CallHandle, DRClient, FriendRequest, MessageView, NotifyDecision } from '@deeprespond/im-web';\nimport { conversationTitle, displayName } from '@deeprespond/im-web/render';\nimport type { PlatformHost } from '@deeprespond/im-web/platform';\nimport type { Bridge } from '../adapter/ipc';\nimport type { DRTauriEngineOptions, EngineAlert } from '../types';\nimport type { Desktop } from './module';\nimport { Ringtone } from './ringtone';\nimport { texts } from './texts';\n\nconst SAME_CONVERSATION_QUIET_MS = 3000;\nconst GLOBAL_SOUND_GAP_MS = 1000;\n\ntype Handler<T> = (value: T) => void | Promise<void>;\n\n/**\n * 引擎的提醒(《2.功能概述文档》第 8 节、第 10 节):\n * - auto:新消息经 push.shouldNotify 判断后,会话不是正被看、负责提醒的窗口没有焦点(或负责提醒的是引擎)时由引擎显示系统通知;\n * 同一会话只保留一条,3 秒内不再响,全部通知每秒最多响一次。好友申请、入群申请同样提醒。\n * - manual:负责提醒的是引擎时交给 App 的 onAlert。\n * - 来电:负责提醒的是引擎时,auto 显示带“接听”“拒绝”的通知并响铃,manual 交给 onIncomingCall。\n * - 账号被挤下线、会话过期:没有可见的界面窗口时显示一条通知。\n */\nexport class EngineAlerts {\n readonly alertHandlers = new Set<Handler<EngineAlert>>();\n readonly callHandlers = new Set<Handler<CallHandle>>();\n #lastSound = new Map<string, number>();\n #lastAnySound = 0;\n #ringtone: Ringtone;\n #ringing = new Map<string, () => void>();\n #friendUnread = -1;\n #invitations = new Set<string>();\n #offs: Array<() => void> = [];\n\n constructor(\n private readonly bridge: Bridge,\n private readonly client: DRClient,\n private readonly desktop: Desktop,\n private readonly host: () => PlatformHost,\n private readonly opts: DRTauriEngineOptions,\n private readonly onWarn: (message: string, err?: unknown) => void,\n ) {\n this.#ringtone = new Ringtone(opts.notifications?.ringtone);\n }\n\n get #mode(): 'auto' | 'manual' {\n return this.opts.notifications?.mode ?? 'auto';\n }\n\n start(): void {\n const c = this.client;\n this.#offs.push(\n c.on('message.received', (p) => void this.#message(p.message, p.notify as NotifyDecision | null)),\n c.on('friendRequests.changed', (p) => void this.#friendRequests(p.lists, p.unread_count)),\n c.on('groupRequests.changed', (p) => void this.#groupRequests(p as { list: string; group_id: string | null; items?: unknown[] })),\n c.on('call.incoming', (p) => void this.#incoming(p.call, p.alert)),\n c.on('auth.stateChanged', (state) => {\n if (state.kind !== 'signed_out' && state.kind !== 'ended') return;\n // 先清除本应用的通知,再显示会话结束的那一条\n const cleared = this.#signedOut();\n if (state.kind === 'ended') void cleared.then(() => this.#sessionEnded(state.code, state.reason));\n }),\n );\n }\n\n stop(): void {\n for (const off of this.#offs.splice(0)) off();\n for (const stop of this.#ringing.values()) stop();\n this.#ringing.clear();\n this.#ringtone.stop();\n }\n\n /** 负责提醒的是不是引擎(没有可见的界面窗口)。 */\n #engineAlerts(): boolean {\n try {\n return this.host().alertTab().self;\n } catch {\n return false;\n }\n }\n\n async #emitAlert(alert: EngineAlert): Promise<void> {\n for (const handler of [...this.alertHandlers]) {\n try {\n await handler(alert);\n } catch (err) {\n this.onWarn('App 的 onAlert 出错', err);\n }\n }\n }\n\n #text(alert: EngineAlert, fallback: { title: string; body: string }): { title: string; body: string } | null {\n const format = this.opts.notifications?.format;\n if (!format) return fallback;\n try {\n return format(alert);\n } catch (err) {\n this.onWarn('notifications.format 出错,使用默认文字', err);\n return fallback;\n }\n }\n\n /** 同一会话 3 秒内不再响;全部通知每秒最多响一次。 */\n #sound(key: string): boolean {\n if (this.opts.notifications?.sound === false) return false;\n const now = Date.now();\n const last = this.#lastSound.get(key) ?? 0;\n const allowed = now - last >= SAME_CONVERSATION_QUIET_MS && now - this.#lastAnySound >= GLOBAL_SOUND_GAP_MS;\n this.#lastSound.set(key, now);\n if (allowed) this.#lastAnySound = now;\n if (this.#lastSound.size > 500) this.#lastSound.clear();\n return allowed;\n }\n\n #attention(): void {\n if (this.opts.attention === false) return;\n void this.bridge.call('attention_request', { critical: false, label: null }).catch(() => {});\n }\n\n async #message(message: MessageView, decision: NotifyDecision | null): Promise<void> {\n if (!decision?.notify) return;\n const host = this.host();\n const alertTab = host.alertTab();\n if (this.#mode === 'manual') {\n if (alertTab.self) await this.#emitAlert({ kind: 'message', message, notify: decision });\n return;\n }\n // 正被看的会话不提醒;负责提醒的窗口正获得焦点时由它的界面提示(《2.功能概述文档》第 8.2 节)\n if (host.viewing(message.conversation_id)) return;\n if (!alertTab.self && alertTab.focused) return;\n const text = this.#text({ kind: 'message', message, notify: decision }, { title: decision.title ?? '', body: decision.body ?? '' });\n if (!text) return;\n await this.desktop\n .show({ id: `dr:msg:${message.conversation_id}`, kind: 'message', title: text.title, body: text.body, conversation_id: message.conversation_id, seq: message.seq ?? undefined, sound: this.#sound(message.conversation_id) })\n .catch((err) => this.onWarn('显示消息通知失败', err));\n this.#attention();\n }\n\n async #notice(alert: Extract<EngineAlert, { kind: 'notice' }>, id: string): Promise<void> {\n if (this.opts.notifications?.notices === false) return;\n if (this.#mode === 'manual') {\n if (this.#engineAlerts()) await this.#emitAlert(alert);\n return;\n }\n const alertTab = this.host().alertTab();\n if (!alertTab.self && alertTab.focused) return;\n const text = this.#text(alert, { title: alert.title, body: alert.body });\n if (!text) return;\n const data: Record<string, string> = {};\n if (alert.user) data.user = alert.user;\n if (alert.group_id) data.group_id = alert.group_id;\n await this.desktop.show({ id, kind: 'notice', title: text.title, body: text.body, data, sound: this.#sound(id) }).catch((err) => this.onWarn('显示通知失败', err));\n this.#attention();\n }\n\n async #friendRequests(lists: string[], unread: number): Promise<void> {\n const previous = this.#friendUnread;\n this.#friendUnread = unread;\n if (!lists.includes('received') || previous < 0 || unread <= previous) return;\n let latest: FriendRequest | undefined;\n try {\n latest = (await this.client.friends.requests.received({ status: 'pending', limit: 1 } as never)).items[0];\n } catch {\n return;\n }\n if (!latest) return;\n const name = latest.nickname || latest.username;\n await this.#notice({ kind: 'notice', type: 'friend_request', title: texts.friendRequest.title, body: texts.friendRequest.body(name, latest.message), user: latest.username }, `dr:notice:friend:${latest.username}`);\n }\n\n async #groupRequests(p: { list: string; group_id: string | null; items?: unknown[] }): Promise<void> {\n if (p.list === 'invitations' && Array.isArray(p.items)) {\n const fresh: Array<{ group_id: string; name?: string }> = [];\n const seen = new Set<string>();\n for (const item of p.items as Array<{ group_id?: string; group?: { group_id?: string; name?: string }; status?: string }>) {\n const id = item.group_id ?? item.group?.group_id;\n if (!id || (item.status && item.status !== 'pending')) continue;\n seen.add(id);\n if (!this.#invitations.has(id)) fresh.push({ group_id: id, name: item.group?.name });\n }\n const first = this.#invitations.size === 0 && fresh.length === seen.size && seen.size > 0 && !this.#primed;\n this.#invitations = seen;\n this.#primed = true;\n // 第一次加载时已有的邀请不提醒\n if (first) return;\n for (const inv of fresh) {\n const group = inv.name || texts.unnamedGroup;\n await this.#notice({ kind: 'notice', type: 'group_request', title: texts.groupInvitation.title, body: texts.groupInvitation.body(group), group_id: inv.group_id }, `dr:notice:group:${inv.group_id}`);\n }\n return;\n }\n if (p.list === 'pending' && p.group_id) {\n const group = (await this.client.groups.get(p.group_id).catch(() => undefined)) as { name?: string } | undefined;\n await this.#notice({ kind: 'notice', type: 'group_request', title: texts.groupApplication.title, body: texts.groupApplication.body(group?.name || texts.unnamedGroup), group_id: p.group_id }, `dr:notice:group:${p.group_id}`);\n }\n }\n\n #primed = false;\n\n async #incoming(call: CallHandle, alert: boolean): Promise<void> {\n // 有可见的界面窗口时由那个窗口的 App 代码处理\n if (!alert) return;\n if ((this.opts.notifications?.calls ?? 'auto') === 'manual') {\n for (const handler of [...this.callHandlers]) {\n try {\n await handler(call);\n } catch (err) {\n this.onWarn('App 的 onIncomingCall 出错', err);\n }\n }\n return;\n }\n const id = `dr:call:${call.call_id}`;\n if (this.#ringing.has(id)) return;\n const snapshot = call.getSnapshot();\n const inviter = snapshot.inviter?.nickname || snapshot.inviter?.username || (snapshot.call.initiator ? displayName(this.client, snapshot.call.initiator) : '');\n const title = snapshot.group?.name ? `${inviter}(${snapshot.group.name})` : inviter;\n await this.desktop\n .show({\n id,\n kind: 'call',\n title: texts.incomingCall.title(title),\n body: texts.incomingCall.body(snapshot.call.media === 'video'),\n data: { call_id: call.call_id },\n sound: false,\n actions: [\n { id: 'accept', title: texts.incomingCall.accept },\n { id: 'decline', title: texts.incomingCall.decline },\n ],\n })\n .catch((err) => this.onWarn('显示来电通知失败', err));\n this.#ringtone.start();\n this.#attention();\n // 来电被接听、拒绝、取消,或在本人的其他设备上处理后,清除通知并停止铃声(《2.功能概述文档》第 8.3 节)\n const stop = () => {\n unsubscribe();\n this.#ringing.delete(id);\n void this.desktop.clear(id).catch(() => {});\n if (this.#ringing.size === 0) this.#ringtone.stop();\n };\n const unsubscribe = call.subscribe(() => {\n const s = call.getSnapshot();\n if (!s.ringing || s.call.status === 'ended' || s.self?.state !== 'ringing') stop();\n });\n this.#ringing.set(id, stop);\n }\n\n /** 来电通知上的“拒绝”(插件直接交给引擎)。 */\n async declineCall(callId: string): Promise<void> {\n const call = this.client.calls.incoming.find((c) => c.call_id === callId);\n if (!call) return;\n await call.reject().catch((err) => this.onWarn('拒绝来电失败', err));\n }\n\n async #sessionEnded(code: string, reason: string | undefined): Promise<void> {\n if (this.#mode !== 'auto' || !this.#engineAlerts()) return;\n // 在其他设备登录、超过设备数被挤下线、被本人在其他设备上踢下线(《sdk-web/2.功能概述文档》第 3.5 节的吊销原因)\n const replaced = code === 'session_revoked' && (reason === 'replaced' || reason === 'device_limit' || reason === 'kicked');\n const t = replaced ? texts.sessionEnded.replaced : texts.sessionEnded.expired;\n await this.desktop.show({ id: 'dr:session', kind: 'session', title: t.title, body: t.body }).catch((err) => this.onWarn('显示通知失败', err));\n }\n\n /** 退出登录、会话结束:清除本应用已显示的通知,停止铃声(与 Flutter SDK 相同)。 */\n #signedOut(): Promise<void> {\n for (const stop of [...this.#ringing.values()]) stop();\n this.#friendUnread = -1;\n this.#invitations.clear();\n this.#primed = false;\n return this.bridge.call<void>('notify_clear', { all: true }).catch(() => {});\n }\n\n /** 会话标题(App 的 format 中可以使用)。 */\n title(view: Parameters<typeof conversationTitle>[1]): string {\n return conversationTitle(this.client, view);\n }\n}\n","import type { DRClient } from '@deeprespond/im-web';\nimport type { PlatformHost } from '@deeprespond/im-web/platform';\nimport type { Bridge } from '../adapter/ipc';\n\nconst MERGE_MS = 500;\n\n/**\n * 角标与未读(《2.功能概述文档》第 8.4 节):引擎在未读合计变化后 500 毫秒内合并一次,\n * auto 模式下显示在系统的位置,并向 Rust(on_unread_changed)和各窗口(desktop.unreadChanged)报告。\n */\nexport class UnreadBadge {\n #timer: ReturnType<typeof setTimeout> | null = null;\n #last: { total: number; badge: number } | null = null;\n #offs: Array<() => void> = [];\n\n constructor(\n private readonly bridge: Bridge,\n private readonly client: DRClient,\n private readonly host: () => PlatformHost,\n private readonly opts: { auto: boolean; includeMuted: boolean; onWarn: (message: string, err?: unknown) => void },\n ) {}\n\n start(): void {\n this.#offs.push(\n this.client.on('conversations.unreadChanged', () => this.schedule()),\n this.client.on('push.changed', () => this.schedule()),\n this.client.on('auth.stateChanged', (state) => {\n if (state.kind !== 'signed_in' && state.kind !== 'suspended') this.#apply({ total: 0, badge: 0 });\n else this.schedule();\n }),\n );\n this.schedule();\n }\n\n stop(): void {\n for (const off of this.#offs.splice(0)) off();\n if (this.#timer !== null) clearTimeout(this.#timer);\n this.#timer = null;\n }\n\n schedule(): void {\n if (this.#timer !== null) return;\n this.#timer = setTimeout(() => {\n this.#timer = null;\n let totals: { total: number; unmuted: number };\n try {\n totals = this.host().unreadTotals();\n } catch {\n return;\n }\n const signedIn = this.client.auth.state.kind === 'signed_in' || this.client.auth.state.kind === 'suspended';\n const total = signedIn ? totals.total : 0;\n const badge = signedIn ? (this.opts.includeMuted ? totals.total : totals.unmuted) : 0;\n this.#apply({ total, badge });\n }, MERGE_MS);\n }\n\n #apply(next: { total: number; badge: number }): void {\n if (this.#last && this.#last.total === next.total && this.#last.badge === next.badge) return;\n this.#last = next;\n if (this.opts.auto) void this.bridge.call('badge_set', { count: next.badge > 0 ? next.badge : null }).catch((err) => this.opts.onWarn('设置角标失败', err));\n void this.bridge.call('unread_report', next).catch(() => {});\n try {\n this.host().publish('desktop.unreadChanged', next);\n } catch {\n // 客户端已关闭\n }\n }\n}\n","import { DRError, createDRClient, type CallHandle, type DRClient } from '@deeprespond/im-web';\nimport type { PlatformHost } from '@deeprespond/im-web/platform';\nimport { createTauriPlatform, osFromUserAgent, type TauriPlatform } from './adapter';\nimport { Bridge, ENGINE_LABEL, currentLabel } from './adapter/ipc';\nimport { clickEvent, invalidState, makeLogger, unsupported } from './common';\nimport { EngineAlerts } from './desktop/alerts';\nimport { UnreadBadge } from './desktop/badge';\nimport { Desktop } from './desktop/module';\nimport type { DREngine, DRTauriEngineClient, DRTauriEngineOptions, EngineAlert } from './types';\n\nexport type { DREngine, DRTauriEngineClient, DRTauriEngineOptions, EngineAlert, DesktopModule, DesktopEvents } from './types';\n\nconst ALIVE_MS = 3000;\nconst PROBE_TIMEOUT_MS = 5000;\nconst WAKE_LAG_MS = 5000;\nconst DEFAULT_PERSIST_MAX = 200 * 1024 * 1024;\n\ninterface EngineEntry {\n client: DRClient;\n host: () => PlatformHost;\n alerts: EngineAlerts;\n badge: UnreadBadge;\n platform: TauriPlatform;\n}\n\n/** 引擎页中的引擎客户端(每个 AppKey 一个,《3.接口设计》第 3 节),按页面的插件连接分开(测试中一个进程有多个页面)。 */\nconst pages = new WeakMap<Bridge, Map<string, EngineEntry>>();\n\n/** 测试用:指定页面的插件连接(不对 App 公开)。 */\nexport interface PageEnv {\n bridge: Bridge;\n /** 替换平台的部分成员(测试中的假服务端)。 */\n overrides?: Partial<import('@deeprespond/im-web/platform').PlatformAdapter>;\n /** 取得核心接口(测试中模拟事件)。 */\n onHost?: (host: PlatformHost) => void;\n}\n\n/**\n * 创建引擎客户端(引擎页中调用,《3.接口设计》第 3 节):Web SDK 的核心作为固定的“主标签页”,持有长连接和令牌、写本地存储、\n * 处理界面窗口转来的调用;没有界面窗口时负责提醒和来电。\n */\nexport function createDRTauriEngine(options: DRTauriEngineOptions, env?: PageEnv): DREngine {\n if ((env ? env.bridge.label : currentLabel()) !== ENGINE_LABEL) throw unsupported('not_engine_window', 'createDRTauriEngine 只能在引擎页中调用');\n if (!options || typeof options.appKey !== 'string') throw new DRError({ code: 'local_validation', message: 'appKey', details: { reason: 'required', field: 'appKey' }, source: 'local' });\n const bridge = env?.bridge ?? Bridge.get();\n let engines = pages.get(bridge);\n if (!engines) {\n engines = new Map();\n pages.set(bridge, engines);\n wirePage(bridge, engines);\n }\n const logger = makeLogger(bridge, options.logger);\n const warn = (message: string, err?: unknown) =>\n logger.sink({ level: 'warn', time: Date.now(), module: 'tauri', message, code: err instanceof DRError ? err.code : undefined, reason: err instanceof DRError ? err.reason : undefined });\n let client!: DRClient;\n const desktop = new Desktop(bridge, () => client);\n const tp = createTauriPlatform({\n role: 'leader',\n appKey: options.appKey,\n bridge,\n encrypt: options.encryptDatabase === true,\n onWarn: warn,\n extend: (h) => {\n env?.onHost?.(h);\n return { desktop: { value: desktop, enumerable: true } };\n },\n });\n if (env?.overrides) Object.assign(tp.platform, env.overrides);\n // 读入保存的会话之后客户端才读取登录状态;另一个进程正在使用同一份数据时不创建(《2.功能概述文档》第 4.6 节)\n tp.platform.ready = bridge.ready.then(async (info) => {\n if (info.another_instance) throw invalidState('another_instance');\n await tp.engineKv!.load();\n });\n client = createDRClient({\n appKey: options.appKey,\n apiUrl: options.apiUrl,\n wsUrl: options.wsUrl,\n platform: osFromUserAgent(typeof navigator !== 'undefined' ? navigator.userAgent : ''),\n deviceName: options.deviceName,\n appVersion: options.appVersion,\n ticketProvider: options.ticketProvider,\n autoMarkRead: options.autoMarkRead,\n conversationWindow: options.conversationWindow,\n userCacheTtlMs: options.userCacheTtlMs,\n outbox: { maxAgeHours: options.outbox?.maxAgeHours, persistAttachmentMaxBytes: options.outbox?.persistAttachmentMaxBytes ?? DEFAULT_PERSIST_MAX },\n media: options.media,\n logger,\n platformAdapter: tp.platform,\n });\n const host = tp.host;\n const alerts = new EngineAlerts(bridge, client, desktop, host, options, warn);\n const badge = new UnreadBadge(bridge, client, host, { auto: (options.badge ?? 'auto') === 'auto', includeMuted: options.badgeIncludesMuted === true, onWarn: warn });\n const entry: EngineEntry = { client, host, alerts, badge, platform: tp };\n // 同一 AppKey 已有客户端时 createDRClient 以 invalid_state(client_exists)拒绝;destroy 之后再创建的替换旧的\n engines.get(options.appKey)?.alerts.stop();\n engines.get(options.appKey)?.badge.stop();\n engines.set(options.appKey, entry);\n void tp.platform.ready!.then(\n () => {\n const signedIn = () => client.auth.state.kind === 'signed_in' || client.auth.state.kind === 'suspended';\n const report = () => void bridge.call('engine_ready', { app_key: options.appKey, signed_in: signedIn() }).catch((err) => warn('报告引擎就绪失败', err));\n report();\n client.on('auth.stateChanged', report);\n alerts.start();\n badge.start();\n },\n (err) => warn('引擎客户端没有创建', err),\n );\n return Object.freeze({\n client: client as unknown as DRTauriEngineClient,\n onAlert: (handler: (alert: EngineAlert) => void | Promise<void>) => {\n alerts.alertHandlers.add(handler);\n return () => alerts.alertHandlers.delete(handler);\n },\n onIncomingCall: (handler: (call: CallHandle) => void | Promise<void>) => {\n alerts.callHandlers.add(handler);\n return () => alerts.callHandlers.delete(handler);\n },\n });\n}\n\n/** 引擎页一次性的接线:存活报告、唤醒检查、系统事件、退出收尾、通知点击。 */\nfunction wirePage(bridge: Bridge, engines: Map<string, EngineEntry>): void {\n const each = (fn: (e: EngineEntry) => void) => {\n for (const e of engines.values()) {\n try {\n fn(e);\n } catch {\n // 客户端已关闭\n }\n }\n };\n setInterval(() => void bridge.call('engine_alive').catch(() => {}), ALIVE_MS);\n // 唤醒检查(《2.功能概述文档》第 4.5 节):心跳计划落后 5 秒以上的立即补发并记警告\n bridge.on('wake', () =>\n each((e) => {\n const host = e.host();\n const lag = host.heartbeatLag();\n if (lag > WAKE_LAG_MS) {\n host.log('warn', 'tauri', `唤醒检查落后 ${Math.round(lag)} 毫秒,计时器可能被系统节流`);\n host.checkConnection('wake_check', PROBE_TIMEOUT_MS);\n }\n }),\n );\n // 系统唤醒、网络变化后立即检查连接(《2.功能概述文档》第 7 节)\n bridge.on('resume', (event) =>\n each((e) => {\n const host = e.host();\n host.checkConnection('resume', PROBE_TIMEOUT_MS);\n host.publish('desktop.systemResumed', { slept_ms: Number(event.slept_ms) || 0 });\n }),\n );\n bridge.on('network', (event) =>\n each((e) => {\n const host = e.host();\n if (event.online !== false) host.checkConnection('network', PROBE_TIMEOUT_MS);\n host.publish('desktop.networkChanged', { online: event.online !== false });\n }),\n );\n bridge.on('screen_lock', () => {\n // 锁屏只影响“正被看”:界面窗口自己重新报告可见性\n });\n bridge.on('storage_full', (event) => {\n const e = engines.get(String(event.app_key));\n e?.host().publish('storage.full', { freed_users: Number(event.freed_users) || 0 });\n });\n bridge.on('call_action', (event) => {\n if (event.action !== 'decline') return;\n each((e) => void e.alerts.declineCall(String(event.call_id)));\n });\n bridge.on('notification_click', (event) => {\n const payload = clickEvent(bridge, event);\n each((e) => e.host().emit('desktop.notificationClicked', payload));\n });\n // 退出前的收尾(《2.功能概述文档》第 4.3 节):停止接收调用、挂断通话、正常关闭长连接、关闭数据库,最多 2 秒\n bridge.on('exit', () => {\n const work = [...engines.values()].map(async (e) => {\n e.alerts.stop();\n e.badge.stop();\n await e.platform.engineKv?.flush();\n await e.host().shutdown();\n });\n void Promise.allSettled(work).then(() => bridge.call('engine_exit_done').catch(() => {}));\n });\n}\n"]}
|