dsh-retrace 0.4.30 → 0.4.31
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/lib/client.bundle.js +505 -52
- package/lib/client.js +180 -21
- package/lib/close-guard-client.js +381 -2
- package/lib/close-guard.js +57 -0
- package/lib/dynamic-client.js +505 -52
- package/lib/dynamic-host.js +7 -7
- package/lib/http.js +20 -2
- package/lib/identity/shortcode.js +49 -10
- package/lib/index.js +191 -74
- package/lib/llm-summary.js +1 -1
- package/lib/marker-carrier.js +7 -7
- package/package.json +1 -1
package/lib/client.bundle.js
CHANGED
|
@@ -37,42 +37,6 @@ __export(client_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(client_exports);
|
|
38
38
|
var import_react = require("react");
|
|
39
39
|
|
|
40
|
-
// lib/badge.js
|
|
41
|
-
function uuidOf(id) {
|
|
42
|
-
const text = String(id ?? "");
|
|
43
|
-
const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
44
|
-
if (m) return m[0].replace(/-/g, "").toLowerCase();
|
|
45
|
-
const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
|
|
46
|
-
return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
|
|
47
|
-
}
|
|
48
|
-
function fnv1a64(text) {
|
|
49
|
-
let hash = 0xcbf29ce484222325n;
|
|
50
|
-
const prime = 0x100000001b3n;
|
|
51
|
-
const mask = 0xffffffffffffffffn;
|
|
52
|
-
for (let i = 0; i < text.length; i++) {
|
|
53
|
-
hash ^= BigInt(text.charCodeAt(i));
|
|
54
|
-
hash = hash * prime & mask;
|
|
55
|
-
}
|
|
56
|
-
return hash;
|
|
57
|
-
}
|
|
58
|
-
var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
59
|
-
function toBase36(value) {
|
|
60
|
-
if (value < 0n) value = -value;
|
|
61
|
-
if (value === 0n) return "0";
|
|
62
|
-
let out = "";
|
|
63
|
-
let v = value;
|
|
64
|
-
while (v > 0n) {
|
|
65
|
-
out = BASE36[Number(v % 36n)] + out;
|
|
66
|
-
v /= 36n;
|
|
67
|
-
}
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
function sessionBadge(sessionId) {
|
|
71
|
-
const uuid = uuidOf(sessionId);
|
|
72
|
-
if (uuid.length === 0) return "";
|
|
73
|
-
return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
40
|
// lib/marker-carrier.js
|
|
77
41
|
var MARKER_ID_PREFIXES = ["retrace", "message-editor"];
|
|
78
42
|
var AUDIT_EVENT_TYPE = "compaction/prune";
|
|
@@ -192,6 +156,42 @@ function carrierShadowedSeqs(event, eventAt) {
|
|
|
192
156
|
return Array.isArray(inner) ? inner.filter((s) => Number.isSafeInteger(s)) : [];
|
|
193
157
|
}
|
|
194
158
|
|
|
159
|
+
// lib/badge.js
|
|
160
|
+
function uuidOf(id) {
|
|
161
|
+
const text = String(id ?? "");
|
|
162
|
+
const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
163
|
+
if (m) return m[0].replace(/-/g, "").toLowerCase();
|
|
164
|
+
const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
|
|
165
|
+
return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
|
|
166
|
+
}
|
|
167
|
+
function fnv1a64(text) {
|
|
168
|
+
let hash = 0xcbf29ce484222325n;
|
|
169
|
+
const prime = 0x100000001b3n;
|
|
170
|
+
const mask = 0xffffffffffffffffn;
|
|
171
|
+
for (let i = 0; i < text.length; i++) {
|
|
172
|
+
hash ^= BigInt(text.charCodeAt(i));
|
|
173
|
+
hash = hash * prime & mask;
|
|
174
|
+
}
|
|
175
|
+
return hash;
|
|
176
|
+
}
|
|
177
|
+
var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
178
|
+
function toBase36(value) {
|
|
179
|
+
if (value < 0n) value = -value;
|
|
180
|
+
if (value === 0n) return "0";
|
|
181
|
+
let out = "";
|
|
182
|
+
let v = value;
|
|
183
|
+
while (v > 0n) {
|
|
184
|
+
out = BASE36[Number(v % 36n)] + out;
|
|
185
|
+
v /= 36n;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function sessionBadge(sessionId) {
|
|
190
|
+
const uuid = uuidOf(sessionId);
|
|
191
|
+
if (uuid.length === 0) return "";
|
|
192
|
+
return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
195
|
// lib/close-guard-client.js
|
|
196
196
|
var GUARD_POLL_MS = 5e3;
|
|
197
197
|
var GUARD_TTL_MS = 3e4;
|
|
@@ -287,7 +287,7 @@ function shouldArmNativeGate(snapshot, page = pageEnvironment()) {
|
|
|
287
287
|
function createGuardStore({ now = Date.now, armTtlMs = GUARD_ARM_TTL_MS } = {}) {
|
|
288
288
|
let snapshot = null;
|
|
289
289
|
let armedAt = 0;
|
|
290
|
-
|
|
290
|
+
const store = {
|
|
291
291
|
/** 设置最新 host 快照(runningSessions 形状 { running: [...] })。 */
|
|
292
292
|
set(next) {
|
|
293
293
|
snapshot = next && typeof next === "object" ? { ...next, at: next.at ?? now() } : null;
|
|
@@ -308,6 +308,343 @@ function createGuardStore({ now = Date.now, armTtlMs = GUARD_ARM_TTL_MS } = {})
|
|
|
308
308
|
armedAt = 0;
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
+
try {
|
|
312
|
+
const win = globalThis?.window;
|
|
313
|
+
const doc = globalThis?.document;
|
|
314
|
+
if (win && doc) {
|
|
315
|
+
queueMicrotask(() => {
|
|
316
|
+
try {
|
|
317
|
+
installDesktopGate({ win, doc, store });
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
return store;
|
|
325
|
+
}
|
|
326
|
+
var GATE_WATCHDOG_MS = 1500;
|
|
327
|
+
var GATE_MODAL_ID = "dsh-rt-guard-modal";
|
|
328
|
+
var GATE_HINT_ID = "dsh-rt-guard-toast";
|
|
329
|
+
var GATE_CONFIG_KEY = "dsh-retrace:config";
|
|
330
|
+
var GATE_ROUTE = "/api/plugins/retrace/runningState";
|
|
331
|
+
var GATE_QUERY = "closeGuardEvent";
|
|
332
|
+
function runningCountOf(snapshot) {
|
|
333
|
+
return Array.isArray(snapshot?.running) ? snapshot.running.length : 0;
|
|
334
|
+
}
|
|
335
|
+
function gateEnabled(storage = globalThis?.localStorage) {
|
|
336
|
+
try {
|
|
337
|
+
const raw = storage?.getItem?.(GATE_CONFIG_KEY);
|
|
338
|
+
if (raw === null || raw === void 0 || raw === "") return true;
|
|
339
|
+
const parsed = JSON.parse(raw);
|
|
340
|
+
return parsed?.closeGuard !== false;
|
|
341
|
+
} catch {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function planBeforeUnload(snapshot, { armed = false, enabled = true, desktop = false, visible = true } = {}) {
|
|
346
|
+
if (!enabled) return { action: "allow", reason: "disabled" };
|
|
347
|
+
if (armed) return { action: "allow", reason: "armed" };
|
|
348
|
+
const kind = classifySnapshot(snapshot);
|
|
349
|
+
if (kind === "unknown") return { action: "allow", reason: "state-unknown" };
|
|
350
|
+
if (kind === "idle") return { action: "allow", reason: "no-running" };
|
|
351
|
+
if (visible === false) return { action: "allow", reason: "hidden" };
|
|
352
|
+
if (!desktop) return { action: "allow", reason: "browser-native-gate" };
|
|
353
|
+
return { action: "gate", reason: "running-desktop" };
|
|
354
|
+
}
|
|
355
|
+
function reportGateEvent(event, extra = {}, fetchImpl = globalThis?.fetch) {
|
|
356
|
+
try {
|
|
357
|
+
if (typeof fetchImpl !== "function") return;
|
|
358
|
+
const params = new URLSearchParams({ [GATE_QUERY]: String(event) });
|
|
359
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
360
|
+
if (value !== void 0 && value !== null && value !== "") params.set(key, String(value));
|
|
361
|
+
}
|
|
362
|
+
const done = fetchImpl(`${GATE_ROUTE}?${params.toString()}`, { method: "GET", cache: "no-store", keepalive: true });
|
|
363
|
+
if (done && typeof done.catch === "function") done.catch(() => {
|
|
364
|
+
});
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function installDesktopGate({
|
|
369
|
+
win,
|
|
370
|
+
doc,
|
|
371
|
+
store,
|
|
372
|
+
page = pageEnvironment,
|
|
373
|
+
enabled = gateEnabled,
|
|
374
|
+
report = reportGateEvent,
|
|
375
|
+
timers = {}
|
|
376
|
+
} = {}) {
|
|
377
|
+
if (!win || !doc || !store || win.__dshRetraceCloseGate) return () => {
|
|
378
|
+
};
|
|
379
|
+
win.__dshRetraceCloseGate = true;
|
|
380
|
+
const setT = timers.setTimeout ?? win.setTimeout?.bind(win) ?? setTimeout;
|
|
381
|
+
const clearT = timers.clearTimeout ?? win.clearTimeout?.bind(win) ?? clearTimeout;
|
|
382
|
+
let modal = null;
|
|
383
|
+
let attempt = 0;
|
|
384
|
+
let secondary = null;
|
|
385
|
+
const labelOf = (sessionId) => {
|
|
386
|
+
try {
|
|
387
|
+
return sessionBadge(sessionId) || String(sessionId);
|
|
388
|
+
} catch {
|
|
389
|
+
return String(sessionId);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const langOf = () => String(win.navigator?.language ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
|
|
393
|
+
const el = (tag, css, text) => {
|
|
394
|
+
const node = doc.createElement(tag);
|
|
395
|
+
if (css) node.style.cssText = css;
|
|
396
|
+
if (text !== void 0) node.textContent = text;
|
|
397
|
+
return node;
|
|
398
|
+
};
|
|
399
|
+
const removeModal = () => {
|
|
400
|
+
try {
|
|
401
|
+
modal?.remove?.();
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
modal = null;
|
|
405
|
+
};
|
|
406
|
+
const modalVisible = () => {
|
|
407
|
+
try {
|
|
408
|
+
if (!modal || modal.isConnected === false) return false;
|
|
409
|
+
const height = modal.offsetHeight;
|
|
410
|
+
if (typeof height === "number") return height > 0;
|
|
411
|
+
const rect = modal.getBoundingClientRect?.();
|
|
412
|
+
return rect === void 0 ? false : rect.width > 0;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const eventExtra = (extra = {}) => ({ surface: "desktop-renderer", ...extra });
|
|
418
|
+
function createTimer() {
|
|
419
|
+
try {
|
|
420
|
+
const g = win;
|
|
421
|
+
const canWorker = typeof g.Worker === "function" && typeof g.Blob === "function" && typeof g.URL?.createObjectURL === "function";
|
|
422
|
+
if (canWorker) {
|
|
423
|
+
const url = g.URL.createObjectURL(new g.Blob(["onmessage=function(e){setTimeout(function(){postMessage(1)},e.data)}"], { type: "text/javascript" }));
|
|
424
|
+
const worker = new g.Worker(url);
|
|
425
|
+
let done = false;
|
|
426
|
+
const stop = () => {
|
|
427
|
+
if (done) return;
|
|
428
|
+
done = true;
|
|
429
|
+
try {
|
|
430
|
+
worker.terminate();
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
g.URL.revokeObjectURL(url);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
kind: "worker",
|
|
440
|
+
arm(ms, cb) {
|
|
441
|
+
worker.onmessage = () => {
|
|
442
|
+
if (done) return;
|
|
443
|
+
stop();
|
|
444
|
+
cb();
|
|
445
|
+
};
|
|
446
|
+
worker.postMessage(ms);
|
|
447
|
+
},
|
|
448
|
+
dispose: stop
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
let handle;
|
|
454
|
+
return {
|
|
455
|
+
kind: "timeout",
|
|
456
|
+
arm(ms, cb) {
|
|
457
|
+
handle = setT(cb, ms);
|
|
458
|
+
},
|
|
459
|
+
dispose() {
|
|
460
|
+
try {
|
|
461
|
+
clearT(handle);
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function clearSecondary() {
|
|
468
|
+
const current = secondary;
|
|
469
|
+
secondary = null;
|
|
470
|
+
if (!current) return;
|
|
471
|
+
try {
|
|
472
|
+
current.timer?.dispose?.();
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
doc.removeEventListener?.("visibilitychange", current.onHidden);
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
doc.removeEventListener?.("keydown", current.onKeydown);
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function showHint(text) {
|
|
485
|
+
try {
|
|
486
|
+
const tip = el("div", "position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483002;background:#262626;color:#eee;border:1px solid #555;border-radius:8px;padding:6px 12px;font:12px/18px -apple-system,system-ui,sans-serif;max-width:90vw;", text);
|
|
487
|
+
tip.id = GATE_HINT_ID;
|
|
488
|
+
tip.className = "dsh-rt-guard-toast";
|
|
489
|
+
doc.body.appendChild(tip);
|
|
490
|
+
setT(() => {
|
|
491
|
+
try {
|
|
492
|
+
tip.remove?.();
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}, 8e3);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function release(reason, id) {
|
|
500
|
+
if (id !== void 0 && id !== attempt) return;
|
|
501
|
+
attempt += 1;
|
|
502
|
+
clearSecondary();
|
|
503
|
+
report(reason, eventExtra({ running: runningCountOf(store.get()) }));
|
|
504
|
+
try {
|
|
505
|
+
store.arm();
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
removeModal();
|
|
509
|
+
try {
|
|
510
|
+
win.close?.();
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
setT(() => showHint(reason.startsWith("fail-soft") ? "\u786E\u8BA4\u6846\u672A\u80FD\u663E\u793A\uFF1A\u5DF2\u6309\u5B98\u65B9\u884C\u4E3A\u653E\u884C\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u5173\u95ED" : "\u5DF2\u653E\u884C\uFF1A\u82E5\u7A97\u53E3\u672A\u5173\u95ED\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u5173\u95ED"), 300);
|
|
514
|
+
}
|
|
515
|
+
function showModal(copy, lang, id) {
|
|
516
|
+
const overlay = el("div", "position:fixed;inset:0;z-index:2147483001;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:16px;");
|
|
517
|
+
overlay.id = GATE_MODAL_ID;
|
|
518
|
+
overlay.className = "dsh-rt-guard-overlay";
|
|
519
|
+
overlay.setAttribute?.("role", "dialog");
|
|
520
|
+
const box = el("div", "background:#202020;color:#eee;border:1px solid #b8860b;border-radius:12px;max-width:min(520px,92vw);padding:14px 16px;font:13px/20px -apple-system,system-ui,sans-serif;display:flex;flex-direction:column;gap:10px;box-sizing:border-box;");
|
|
521
|
+
box.className = "dsh-rt-guard-modal";
|
|
522
|
+
const title = el("div", "font-weight:700;color:#f0c674;font-size:14px;", copy.head);
|
|
523
|
+
title.className = "dsh-rt-guard-modal-title";
|
|
524
|
+
const body = el("div", "white-space:pre-line;font:12px/19px ui-monospace,SFMono-Regular,monospace;", copy.lines.join("\n"));
|
|
525
|
+
body.className = "dsh-rt-guard-modal-lines";
|
|
526
|
+
box.appendChild(title);
|
|
527
|
+
box.appendChild(body);
|
|
528
|
+
if (copy.hint) {
|
|
529
|
+
const note = el("div", "color:#aaa;font-size:11px;", copy.hint);
|
|
530
|
+
note.className = "dsh-rt-guard-modal-hint";
|
|
531
|
+
box.appendChild(note);
|
|
532
|
+
}
|
|
533
|
+
const waiting = el("div", "color:#aaa;font-size:11px;", lang === "zh" ? "\u7B49\u5F85\u4F60\u7684\u9009\u62E9\uFF08\u4E0D\u81EA\u52A8\u5173\u95ED\uFF1BEsc \u7B49\u540C\u53D6\u6D88\uFF09" : "Waiting for your choice (no auto-close; Esc cancels)");
|
|
534
|
+
waiting.className = "dsh-rt-guard-modal-wait";
|
|
535
|
+
box.appendChild(waiting);
|
|
536
|
+
const actions = el("div", "display:flex;justify-content:flex-end;gap:8px;");
|
|
537
|
+
actions.className = "dsh-rt-guard-modal-actions";
|
|
538
|
+
const cancel = el("button", "border:1px solid #555;background:transparent;color:#eee;border-radius:6px;padding:4px 12px;font-size:12px;line-height:20px;cursor:pointer;", lang === "zh" ? "\u53D6\u6D88" : "Cancel");
|
|
539
|
+
cancel.type = "button";
|
|
540
|
+
cancel.className = "dsh-rt-guard-btn";
|
|
541
|
+
cancel.onclick = () => {
|
|
542
|
+
if (id !== attempt) return;
|
|
543
|
+
attempt += 1;
|
|
544
|
+
clearSecondary();
|
|
545
|
+
report("cancel", eventExtra());
|
|
546
|
+
removeModal();
|
|
547
|
+
};
|
|
548
|
+
const proceed = el("button", "border:1px solid #a33;background:#8b1f1f;color:#fff;font-weight:600;border-radius:6px;padding:4px 12px;font-size:12px;line-height:20px;cursor:pointer;", lang === "zh" ? "\u4ECD\u8981\u5173\u95ED" : "Close anyway");
|
|
549
|
+
proceed.type = "button";
|
|
550
|
+
proceed.className = "dsh-rt-guard-btn dsh-rt-guard-btn-primary";
|
|
551
|
+
proceed.onclick = () => release("release", id);
|
|
552
|
+
actions.appendChild(cancel);
|
|
553
|
+
actions.appendChild(proceed);
|
|
554
|
+
box.appendChild(actions);
|
|
555
|
+
overlay.appendChild(box);
|
|
556
|
+
try {
|
|
557
|
+
doc.body.appendChild(overlay);
|
|
558
|
+
modal = overlay;
|
|
559
|
+
} catch {
|
|
560
|
+
modal = null;
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
(cancel.focus ?? proceed.focus)?.call(cancel);
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
return modalVisible();
|
|
568
|
+
}
|
|
569
|
+
function armSecondary(id) {
|
|
570
|
+
clearSecondary();
|
|
571
|
+
const onHidden = () => {
|
|
572
|
+
if (doc.visibilityState === "hidden") release("fail-soft-hidden", id);
|
|
573
|
+
};
|
|
574
|
+
const onKeydown = (event) => {
|
|
575
|
+
if (event?.key !== "Escape" || id !== attempt) return;
|
|
576
|
+
attempt += 1;
|
|
577
|
+
clearSecondary();
|
|
578
|
+
report("cancel", eventExtra());
|
|
579
|
+
removeModal();
|
|
580
|
+
};
|
|
581
|
+
const timer = createTimer();
|
|
582
|
+
const state = { id, timer, onHidden, onKeydown };
|
|
583
|
+
secondary = state;
|
|
584
|
+
try {
|
|
585
|
+
doc.addEventListener?.("visibilitychange", onHidden);
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
doc.addEventListener?.("keydown", onKeydown);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
timer.arm(GATE_WATCHDOG_MS, () => {
|
|
593
|
+
if (id !== attempt || secondary !== state) return;
|
|
594
|
+
if (modalVisible()) report("waiting", eventExtra({ running: runningCountOf(store.get()) }));
|
|
595
|
+
else release("fail-soft-ui-gone", id);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function onBeforeUnload(event) {
|
|
599
|
+
const snapshot = store.get();
|
|
600
|
+
const plan = planBeforeUnload(snapshot, {
|
|
601
|
+
armed: store.isArmed?.() === true,
|
|
602
|
+
enabled: enabled() === true,
|
|
603
|
+
desktop: clientDesktopEvidence(page()).desktop === true,
|
|
604
|
+
visible: doc.visibilityState !== "hidden"
|
|
605
|
+
});
|
|
606
|
+
if (plan.action !== "gate") {
|
|
607
|
+
if (plan.reason === "no-running" || plan.reason === "hidden") report(`allow-${plan.reason}`, eventExtra({ running: runningCountOf(snapshot) }));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const id = attempt + 1;
|
|
611
|
+
attempt = id;
|
|
612
|
+
let shown = false;
|
|
613
|
+
try {
|
|
614
|
+
shown = showModal(buildRunningCopy(snapshot, { locale: langOf(), labelOf }), langOf(), id) === true;
|
|
615
|
+
} catch {
|
|
616
|
+
shown = false;
|
|
617
|
+
}
|
|
618
|
+
if (!shown) {
|
|
619
|
+
removeModal();
|
|
620
|
+
release("fail-soft-ui-not-rendered", id);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
event?.preventDefault?.();
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
if (event) event.returnValue = "";
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
report("intercept", eventExtra({ running: runningCountOf(snapshot) }));
|
|
632
|
+
armSecondary(id);
|
|
633
|
+
}
|
|
634
|
+
win.addEventListener("beforeunload", onBeforeUnload);
|
|
635
|
+
if (clientDesktopEvidence(page()).desktop === true) report("gate-ready", eventExtra({ running: runningCountOf(store.get()) }));
|
|
636
|
+
return () => {
|
|
637
|
+
clearSecondary();
|
|
638
|
+
try {
|
|
639
|
+
win.removeEventListener("beforeunload", onBeforeUnload);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
removeModal();
|
|
643
|
+
try {
|
|
644
|
+
delete win.__dshRetraceCloseGate;
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
};
|
|
311
648
|
}
|
|
312
649
|
|
|
313
650
|
// lib/client.js
|
|
@@ -495,7 +832,7 @@ var zh = {
|
|
|
495
832
|
"path.round": "\u7B2C {n} \u8F6E",
|
|
496
833
|
"path.empty": "\uFF08\u5F53\u524D\u6CA1\u6709\u5BF9\u8BDD\u8F6E\u6B21\uFF09",
|
|
497
834
|
"options.closeGuard": "\u9000\u51FA\u786E\u8BA4\uFF08\u5173\u95ED\u5B88\u536B\uFF09",
|
|
498
|
-
"options.closeGuardDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u9875\u9762\u5173\u95ED\u524D\u786E\u8BA4\u9000\u51FA\u2014\u2014\u6709\u8FD0\u884C\u4E2D\u4EFB\u52A1\u6216\u672A\u5B8C\u6210\u5BF9\u8BDD\u65F6\
|
|
835
|
+
"options.closeGuardDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u9875\u9762\u5173\u95ED\u524D\u786E\u8BA4\u9000\u51FA\u2014\u2014\u6709\u8FD0\u884C\u4E2D\u4EFB\u52A1\u6216\u672A\u5B8C\u6210\u5BF9\u8BDD\u65F6\uFF0C\u5F39**\u672C\u63D2\u4EF6\u81EA\u7ED8**\u7684\u4E2D\u6587\u786E\u8BA4\u6846\uFF08[\u53D6\u6D88] / [\u4ECD\u8981\u5173\u95ED]\uFF09\uFF0C\u70B9[\u4ECD\u8981\u5173\u95ED]\u771F\u7684\u9000\u51FA\uFF08\u786E\u8BA4\u6846\u4E00\u76F4\u7B49\u4F60\u7684\u9009\u62E9\uFF0CEsc \u7B49\u540C\u53D6\u6D88\uFF09\uFF1B\u65E0\u4EFB\u52A1\u76F4\u63A5\u5173\uFF0C\u4E0D\u6253\u6270\u3002\u684C\u9762\u7AEF\uFF08DSH Desktop\uFF09\u4E0D\u6B66\u88C5**\u5BBF\u4E3B\u539F\u751F\u786E\u8BA4\u6846**\u90A3\u6761\u8DEF\uFF08\u684C\u9762\u58F3\u672A\u5904\u7406 will-prevent-unload\uFF0C\u5BBF\u4E3B\u6846\u4F1A\u88AB\u9759\u9ED8\u541E\u6389\uFF09\uFF0C\u6539\u7528**\u9875\u9762\u81EA\u7ED8\u786E\u8BA4\u95E8**\uFF1A\u5148\u753B\u6846\u5E76\u786E\u8BA4\u770B\u5F97\u89C1\uFF0C\u624D\u62E6\u4E0B\u5173\u95ED\uFF1B\u6846\u753B\u4E0D\u51FA\u6765\u3001\u4E0D\u53EF\u89C1\u6216\u9875\u9762\u4E0D\u53EF\u89C1 \u21D2 \u5F53\u573A\u653E\u884C\uFF0C\u770B\u95E8\u72D7\uFF08Web Worker \u8BA1\u65F6\u5668\uFF0C\u4E0D\u53D7\u540E\u53F0\u8282\u6D41\uFF09\u53EA\u4F5C\u6700\u540E\u4E00\u9053\u3002\u6B66\u88C5**\u539F\u751F\u95E8**\u7684\u5224\u636E\u662F\u4E24\u9053\uFF1A\u5BBF\u4E3B\u8BF4\u4F1A\u5F39\u539F\u751F\u6846\uFF08quitVeto\uFF09\uFF0C\u4E14\u672C\u9875\u81EA\u5DF1\u770B\u4E0D\u51FA\u684C\u9762\u75D5\u8FF9\uFF08UA \u65E0 Electron\u3001URL \u65E0 dsh-desktop-\uFF09\u2014\u2014\u4EFB\u4E00\u6761\u4E0D\u6210\u7ACB\u5C31\u4E0D\u6B66\u88C5\uFF08\u4E00\u7968\u5426\u51B3\uFF09\u3002\u9000\u51FA\u5165\u53E3\u968F\u7248\u672C/\u5E73\u53F0\u800C\u53D8\u2014\u2014\u5916\u90E8\u62A5\u544A\u7684 0.9.0 \u4E00\u7C7B\u5165\u53E3\u4F1A\u8D70\u5230\u9875\u9762 beforeunload\uFF0C2.0.9 \u7684\u6258\u76D8/\u2318Q \u8D70 requestQuit\u2192window.destroy\u2192app.exit \u4E0D\u7ECF\u8FC7\u672C\u95E8\u3002\u684C\u9762\u7AEF\u9000\u4E0D\u6389\u65F6\u5148\u5173\u8FD9\u4E00\u9879\uFF08\u7ACB\u5373\u6062\u590D\uFF0C\u4E0D\u7528\u91CD\u542F\uFF09\u3002"
|
|
499
836
|
};
|
|
500
837
|
var en = {
|
|
501
838
|
"action.edit": "Edit",
|
|
@@ -642,7 +979,7 @@ var en = {
|
|
|
642
979
|
"path.round": "Round {n}",
|
|
643
980
|
"path.empty": "(no conversation rounds yet)",
|
|
644
981
|
"options.closeGuard": "Exit confirmation (close guard)",
|
|
645
|
-
"options.closeGuardDesc": "On (default): confirm before the page closes \u2014 when sessions have running or unfinished work
|
|
982
|
+
"options.closeGuardDesc": "On (default): confirm before the page closes \u2014 when sessions have running or unfinished work this plugin shows its own Chinese confirm dialog ([Cancel] / [Close anyway]), and [Close anyway] really exits (the dialog waits for your choice; Esc cancels); with no running work the page closes directly, without interruption. Desktop (DSH Desktop) never arms the host native dialog path (the desktop shell has no will-prevent-unload handler, so that dialog is swallowed silently); it uses the page-drawn confirm gate instead: draw the dialog and verify it is visible first, only then block the close; if it cannot be drawn or shown, or the page is not visible, the close passes immediately, with a watchdog (Web Worker timer, immune to background throttling) as the last resort. Arming the native gate takes both criteria: the host says a native dialog appears (quitVeto) AND this page shows no desktop evidence itself (no Electron in the UA, no dsh-desktop- in the URL) \u2014 if either fails it stays unarmed (a client-side veto). Quit entries differ by version/platform \u2014 the 0.9.0-class entry in the external report does reach page beforeunload, while the 2.0.9 tray/\u2318Q goes requestQuit\u2192window.destroy\u2192app.exit and never reaches this gate. If Desktop cannot quit, turn this off first (recovers immediately, no restart)."
|
|
646
983
|
};
|
|
647
984
|
function opFailureText(code, message, t) {
|
|
648
985
|
if (code === "agent-busy") return t("error.busy");
|
|
@@ -658,6 +995,26 @@ function isReplacementSurfaceEvent(event) {
|
|
|
658
995
|
return SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0 && event.surfaceOp !== "append";
|
|
659
996
|
}
|
|
660
997
|
var wire = null;
|
|
998
|
+
var badgeBySession = /* @__PURE__ */ new Map();
|
|
999
|
+
var normalizeBadgeKey = (id) => String(id ?? "").replace(/^session-/, "").toLowerCase();
|
|
1000
|
+
function rememberBadge(sessionId, badge) {
|
|
1001
|
+
if (typeof badge !== "string" || badge === "") return;
|
|
1002
|
+
badgeBySession.set(normalizeBadgeKey(sessionId), badge);
|
|
1003
|
+
}
|
|
1004
|
+
function canonicalBadgeOf(sessionId) {
|
|
1005
|
+
const hit = badgeBySession.get(normalizeBadgeKey(sessionId));
|
|
1006
|
+
return typeof hit === "string" ? hit : "";
|
|
1007
|
+
}
|
|
1008
|
+
function rememberBadgeMap(badges) {
|
|
1009
|
+
if (!badges || typeof badges !== "object") return 0;
|
|
1010
|
+
let added = 0;
|
|
1011
|
+
for (const [id, code] of Object.entries(badges)) {
|
|
1012
|
+
const before = canonicalBadgeOf(id);
|
|
1013
|
+
rememberBadge(id, code);
|
|
1014
|
+
if (before === "" && canonicalBadgeOf(id) !== "") added += 1;
|
|
1015
|
+
}
|
|
1016
|
+
return added;
|
|
1017
|
+
}
|
|
661
1018
|
function __setMessageEditorWire(fn) {
|
|
662
1019
|
wire = fn;
|
|
663
1020
|
}
|
|
@@ -1613,6 +1970,93 @@ function reportJumpUnavailable(reason, detail = {}) {
|
|
|
1613
1970
|
} catch {
|
|
1614
1971
|
}
|
|
1615
1972
|
}
|
|
1973
|
+
function reportBadgeFailure(op, detail) {
|
|
1974
|
+
const message = detail instanceof Error ? detail.message : String(detail ?? "unknown");
|
|
1975
|
+
console.warn(`[dsh-retrace] ${op} failed: ${message}`);
|
|
1976
|
+
try {
|
|
1977
|
+
const sent = callOp("clientReport", { id: name, source: `${op}-failed: ${message}`.slice(0, 200) });
|
|
1978
|
+
if (sent && typeof sent.catch === "function") sent.catch(() => {
|
|
1979
|
+
});
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
var badgeInfoSent = /* @__PURE__ */ new Set();
|
|
1984
|
+
function reportBadgeInfo(op, detail) {
|
|
1985
|
+
const key = `${op}:${detail}`;
|
|
1986
|
+
if (badgeInfoSent.has(key)) return;
|
|
1987
|
+
badgeInfoSent.add(key);
|
|
1988
|
+
console.warn(`[dsh-retrace] ${op}: ${detail}`);
|
|
1989
|
+
try {
|
|
1990
|
+
const sent = callOp("clientReport", { id: name, source: `${op}: ${String(detail).slice(0, 180)}` });
|
|
1991
|
+
if (sent && typeof sent.catch === "function") sent.catch(() => {
|
|
1992
|
+
});
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function safeSchedule(fn, delayMs) {
|
|
1997
|
+
try {
|
|
1998
|
+
const schedule = typeof window === "undefined" ? void 0 : window.setTimeout;
|
|
1999
|
+
if (typeof schedule === "function") {
|
|
2000
|
+
const handle = schedule(fn, delayMs);
|
|
2001
|
+
return () => {
|
|
2002
|
+
try {
|
|
2003
|
+
window.clearTimeout(handle);
|
|
2004
|
+
} catch {
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
if (typeof queueMicrotask === "function") {
|
|
2009
|
+
queueMicrotask(fn);
|
|
2010
|
+
return () => {
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
return () => {
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function bootstrapBadgeTitles({ attempts = 5, delayMs = 3e3 } = {}) {
|
|
2019
|
+
if (globalThis.__DSH_RETRACE_BADGE_BOOTSTRAP !== true) return () => {
|
|
2020
|
+
};
|
|
2021
|
+
let mapPending;
|
|
2022
|
+
try {
|
|
2023
|
+
mapPending = callOp("badgeMap", {});
|
|
2024
|
+
} catch (cause) {
|
|
2025
|
+
mapPending = Promise.reject(cause);
|
|
2026
|
+
}
|
|
2027
|
+
Promise.resolve(mapPending).then((result) => {
|
|
2028
|
+
if (result && result.ok === true) reportBadgeInfo("badgeMap", `loaded ${rememberBadgeMap(result.value?.badges)}`);
|
|
2029
|
+
else reportBadgeFailure("badgeMap", result?.error?.message ?? "host returned ok:false");
|
|
2030
|
+
}).catch((cause) => reportBadgeFailure("badgeMap", cause));
|
|
2031
|
+
let cancelTimer = () => {
|
|
2032
|
+
};
|
|
2033
|
+
let cancelled = false;
|
|
2034
|
+
let left = attempts;
|
|
2035
|
+
const attempt = () => {
|
|
2036
|
+
if (cancelled) return;
|
|
2037
|
+
let pending;
|
|
2038
|
+
try {
|
|
2039
|
+
pending = callOp("initBadgeTitles", {});
|
|
2040
|
+
} catch (cause) {
|
|
2041
|
+
pending = Promise.reject(cause);
|
|
2042
|
+
}
|
|
2043
|
+
Promise.resolve(pending).then((result) => {
|
|
2044
|
+
if (result && result.ok === true) return;
|
|
2045
|
+
throw new Error(result?.error?.message ?? "host returned ok:false");
|
|
2046
|
+
}).catch((cause) => {
|
|
2047
|
+
if (cancelled) return;
|
|
2048
|
+
left -= 1;
|
|
2049
|
+
const suffix = left > 0 ? `(retry, ${left} left)` : "(retries exhausted)";
|
|
2050
|
+
reportBadgeFailure("initBadgeTitles", `${cause?.message ?? cause} ${suffix}`);
|
|
2051
|
+
if (left > 0) cancelTimer = safeSchedule(attempt, delayMs);
|
|
2052
|
+
});
|
|
2053
|
+
};
|
|
2054
|
+
cancelTimer = safeSchedule(attempt, 0);
|
|
2055
|
+
return () => {
|
|
2056
|
+
cancelled = true;
|
|
2057
|
+
if (cancelTimer) cancelTimer();
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
1616
2060
|
async function jumpToAnchor(store, anchorSeq, readNodes) {
|
|
1617
2061
|
const { key, reason, pages } = await resolveAnchorKey({
|
|
1618
2062
|
anchorSeq,
|
|
@@ -2044,14 +2488,21 @@ function RetraceView({ sessionId, useChat, useProjection, t, actions, store }) {
|
|
|
2044
2488
|
refreshGit();
|
|
2045
2489
|
refreshDigests();
|
|
2046
2490
|
timelineGet(`/doctor?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setDoctor(result?.ok === true ? result.value : null)).catch(() => setDoctor(null));
|
|
2047
|
-
callOp("setBadgeTitle", { sessionId }).
|
|
2048
|
-
|
|
2491
|
+
callOp("setBadgeTitle", { sessionId }).then((result) => {
|
|
2492
|
+
if (!result || result.ok !== true) {
|
|
2493
|
+
reportBadgeFailure("setBadgeTitle", result?.error?.message ?? "host returned ok:false");
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
reportBadgeInfo("setBadgeTitle", `${sessionId} \u2192 ${result.value?.title ?? ""}${result.value?.alreadyTagged === true ? "(alreadyTagged)" : ""}`);
|
|
2497
|
+
}).catch((cause) => reportBadgeFailure("setBadgeTitle", cause));
|
|
2049
2498
|
callOp("sessionBadge", { sessionId }).then((result) => {
|
|
2050
2499
|
if (result && result.ok === true && typeof result.value?.badge === "string") {
|
|
2051
2500
|
setBadge(result.value.badge);
|
|
2501
|
+
rememberBadge(sessionId, result.value.badge);
|
|
2502
|
+
} else {
|
|
2503
|
+
reportBadgeFailure("sessionBadge", result?.error?.message ?? "response has no badge");
|
|
2052
2504
|
}
|
|
2053
|
-
}).catch(() =>
|
|
2054
|
-
});
|
|
2505
|
+
}).catch((cause) => reportBadgeFailure("sessionBadge", cause));
|
|
2055
2506
|
}, []);
|
|
2056
2507
|
const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq, () => chatNodesRef.current);
|
|
2057
2508
|
const jumpRow = (row) => jump(jumpTargetOf(row));
|
|
@@ -2721,14 +3172,7 @@ function installCloseGuard(t) {
|
|
|
2721
3172
|
const ui = { banner: null, modal: null, toast: null, toastTimer: 0, dismissed: null, expanded: false, lastSig: "" };
|
|
2722
3173
|
let supported = true;
|
|
2723
3174
|
let failures = 0;
|
|
2724
|
-
const labelOf = (sessionId) =>
|
|
2725
|
-
try {
|
|
2726
|
-
const badge = sessionBadge(sessionId);
|
|
2727
|
-
return badge || String(sessionId);
|
|
2728
|
-
} catch {
|
|
2729
|
-
return String(sessionId);
|
|
2730
|
-
}
|
|
2731
|
-
};
|
|
3175
|
+
const labelOf = (sessionId) => canonicalBadgeOf(sessionId) || String(sessionId);
|
|
2732
3176
|
const runningSig = (snapshot) => {
|
|
2733
3177
|
const running = snapshot?.running;
|
|
2734
3178
|
if (!Array.isArray(running)) return "";
|
|
@@ -2771,7 +3215,7 @@ function installCloseGuard(t) {
|
|
|
2771
3215
|
}
|
|
2772
3216
|
}
|
|
2773
3217
|
function bannerNote(lang) {
|
|
2774
|
-
return lang === "zh" ? "\u5173\u95ED\u9875\u9762/\u9000\u51FA\u524D\u8BF7\u5148\u786E\u8BA4\u2014\u2014\u4EFB\u52A1\u4E2D\u65AD\u53EF\u80FD\u4E22\u5931\u8FDB\u5EA6(\u684C\u9762\u7AEF\
|
|
3218
|
+
return lang === "zh" ? "\u5173\u95ED\u9875\u9762/\u9000\u51FA\u524D\u8BF7\u5148\u786E\u8BA4\u2014\u2014\u4EFB\u52A1\u4E2D\u65AD\u53EF\u80FD\u4E22\u5931\u8FDB\u5EA6(\u684C\u9762\u7AEF\u7528\u9875\u9762\u81EA\u7ED8\u786E\u8BA4\u6846;\u82E5\u672A\u5F39\u51FA,\u4EE5\u6B64\u6A2A\u5E45\u4E3A\u51C6)" : "Confirm before closing/exiting \u2014 interrupting work may lose progress (Desktop uses a page-drawn confirm dialog; if it does not appear, rely on this banner)";
|
|
2775
3219
|
}
|
|
2776
3220
|
function renderBanner() {
|
|
2777
3221
|
const cfg = getConfig();
|
|
@@ -2968,6 +3412,15 @@ function apply(ctx) {
|
|
|
2968
3412
|
const disposeStyle = ensureStyle();
|
|
2969
3413
|
ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
|
|
2970
3414
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
|
|
3415
|
+
ctx.effect(() => {
|
|
3416
|
+
try {
|
|
3417
|
+
return bootstrapBadgeTitles();
|
|
3418
|
+
} catch (error) {
|
|
3419
|
+
reportBadgeFailure("initBadgeTitles", error);
|
|
3420
|
+
return () => {
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
}, "dsh-retrace: init badge titles");
|
|
2971
3424
|
const t = ctx.locale.bind(NS);
|
|
2972
3425
|
let disposeCloseGuard = () => {
|
|
2973
3426
|
};
|