dsh-notify-yimit 0.2.1 → 0.2.3
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/README.en.md +4 -3
- package/README.md +12 -2
- package/lib/client.js +146 -0
- package/lib/index.js +225 -32
- package/lib/toast-host.ps1 +487 -16
- package/lib/typert.host.js +28 -0
- package/package.json +4 -2
- package/scripts/check-prepublish.mjs +60 -0
package/lib/index.js
CHANGED
|
@@ -96,11 +96,13 @@ const TOAST_LABELS = {
|
|
|
96
96
|
zh: {
|
|
97
97
|
ignore: '忽略',
|
|
98
98
|
jump: '跳转会话',
|
|
99
|
+
activate: '返回应用',
|
|
99
100
|
unnamed: '(未命名会话)'
|
|
100
101
|
},
|
|
101
102
|
en: {
|
|
102
103
|
ignore: 'Ignore',
|
|
103
104
|
jump: 'Open',
|
|
105
|
+
activate: 'Go to app',
|
|
104
106
|
unnamed: '(Unnamed session)'
|
|
105
107
|
}
|
|
106
108
|
}
|
|
@@ -114,6 +116,12 @@ const EDIT_TITLES = {
|
|
|
114
116
|
en: 'Notification preview'
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/** 编辑模式基准线说明(中/英):基准线下方画 ↑,表示通知从该位置往上堆叠。 */
|
|
120
|
+
const EDIT_ANCHOR_LABELS = {
|
|
121
|
+
zh: '通知以该位置为基准,均在此线上方',
|
|
122
|
+
en: 'Toasts anchor here and stack upward'
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
const HEX_RE = /^#[0-9a-fA-F]{3,8}$/
|
|
118
126
|
const isHex = (v) => typeof v === 'string' && HEX_RE.test(v)
|
|
119
127
|
|
|
@@ -154,6 +162,14 @@ function normalizeConfig(input) {
|
|
|
154
162
|
left: Math.round(pos.left),
|
|
155
163
|
top: Math.round(pos.top)
|
|
156
164
|
}
|
|
165
|
+
// 兼容旧版已保存的坐标。旧版语义混乱:position.top 被当作"样板窗窗口顶边",
|
|
166
|
+
// 而通知也按"窗口顶边 = pos.top"落位;新版统一为"可见卡片底边 = pos.top"。
|
|
167
|
+
// 两者相差(卡片高 + GLOW),故对没有新标记的旧数据整体下移这么多。
|
|
168
|
+
// (此处拿不到真实卡片高,用默认值估;用户重新拖动一次即精确。)
|
|
169
|
+
if (srcToast.posAtBaseline !== true) {
|
|
170
|
+
out.toast.position.top = Math.round(out.toast.position.top + DEFAULT_WINDOW_H - TOAST_BOTTOM_GAP)
|
|
171
|
+
}
|
|
172
|
+
out.toast.posAtBaseline = true
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
const srcColors = src.colors !== null && typeof src.colors === 'object' && !Array.isArray(src.colors) ? src.colors :
|
|
@@ -287,8 +303,45 @@ function openConfig() {
|
|
|
287
303
|
const MAX_QUEUE = 500
|
|
288
304
|
const RUNNING_THROTTLE_MS = 400
|
|
289
305
|
const KIND_DEBOUNCE_MS = 2000
|
|
290
|
-
/** 通知间距默认值(px);实际使用 config.toast.gap(0–60,可调)。 */
|
|
306
|
+
/** 通知间距默认值(px,指"可见卡片边到边"的间距);实际使用 config.toast.gap(0–60,可调)。 */
|
|
291
307
|
const TOAST_GAP = 12
|
|
308
|
+
/**
|
|
309
|
+
* 每个通知窗口四周的透明辉光边距(px),必须与 toast-host.ps1 里
|
|
310
|
+
* New-ToastWindow 的 `$border.Margin = Thickness(12)` 保持一致。
|
|
311
|
+
*
|
|
312
|
+
* 实测(两条通知同锚点堆叠):卡片可见矩形 = 窗口内缩 GLOW,
|
|
313
|
+
* 即 窗口高 = 可见卡片高 + 2×GLOW。因此
|
|
314
|
+
* · 某条的"可见卡片高" = 窗口高 − 2×GLOW;
|
|
315
|
+
* · 堆叠间距直接用用户设置的 gap 即可 —— 因为相邻两条的"窗口间距离"
|
|
316
|
+
* 里已经含了 2×GLOW 的透明边距,而它们是被计入 offsetY 的可见高度吸收的。
|
|
317
|
+
* 关键是 gap 绝不能再减去 2×GLOW:那样 gap ≤ 24 时会算出 0 间距,
|
|
318
|
+
* 用户看到的默认 12px 就变成"卡片贴在一起"(实测复现过)。
|
|
319
|
+
*/
|
|
320
|
+
const TOAST_GLOW = 12
|
|
321
|
+
/** 默认右下角排布时,最新那条"可见卡片底边"距工作区底边的留白(px);须与宿主一致。 */
|
|
322
|
+
const TOAST_EDGE_PAD = 20
|
|
323
|
+
/**
|
|
324
|
+
* 通知窗口内"可见卡片底边"到**窗口底边**的距离(px),须与 toast-host.ps1 的
|
|
325
|
+
* $script:TOAST_BOTTOM_GAP 一致(实测 = 12,即窗口四周的辉光边距)。
|
|
326
|
+
* 底部锚定公式(注意含"窗口高",不能用固定值):
|
|
327
|
+
* 窗口顶边 = 锚点Y − (窗口高 − TOAST_BOTTOM_GAP) − offsetY
|
|
328
|
+
* ⇒ 卡片可见底边 = 锚点Y − offsetY,与内容高度无关。
|
|
329
|
+
*/
|
|
330
|
+
const TOAST_BOTTOM_GAP = 12
|
|
331
|
+
/**
|
|
332
|
+
* 迁移旧配置时估算用的"通知窗口总高"(px),≈ 卡片高 110 + 2×辉光 24。
|
|
333
|
+
* 真实高度由宿主回传,这里只在读旧配置时用作回落值。
|
|
334
|
+
*/
|
|
335
|
+
const DEFAULT_WINDOW_H = 134
|
|
336
|
+
/**
|
|
337
|
+
* 堆叠时相邻两条之间的"窗口间"间距。
|
|
338
|
+
*
|
|
339
|
+
* 直接用用户设置的 gap —— 不要再减去 2×TOAST_GLOW:
|
|
340
|
+
* offsetY 累加的是"可见卡片高 + 间距",可见卡片高已经把两侧透明辉光吸收掉了,
|
|
341
|
+
* 所以 gap 就是肉眼看到的卡片间距。若在此处再减 2×GLOW,gap ≤ 24 时会被夹成 0,
|
|
342
|
+
* 默认的 12px 就变成"卡片严丝合缝贴在一起"(实测复现过)。
|
|
343
|
+
*/
|
|
344
|
+
const stackGap = (gap) => Math.max(0, gap)
|
|
292
345
|
const TEXT_MAX = 300
|
|
293
346
|
|
|
294
347
|
function clip(text) {
|
|
@@ -466,10 +519,41 @@ export function apply(ctx) {
|
|
|
466
519
|
let toastSeq = 1
|
|
467
520
|
let host = null
|
|
468
521
|
let hostAlive = false
|
|
469
|
-
let hostRestartAttempts = 0
|
|
470
522
|
let hostRestartUntil = 0
|
|
523
|
+
// ② 早退退避:spawn 成功 ≠ 进程活着。旧代码在 spawn 后同步置 hostAlive = true,
|
|
524
|
+
// 于是 ensureHost() 里的 `if (!hostAlive)` 永不成立、退避计数恒被清零,退避形同虚设。
|
|
525
|
+
// 宿主秒退时(例如 BOM 丢失),每条通知都会重拉一个 powershell.exe
|
|
526
|
+
// (冷启动 200–300ms + WPF 初始化),而 running 是高频事件 → 进程风暴。
|
|
527
|
+
// 因此把"早退"记在 exit 回调里(那里有真实存活时长),按连续早退次数退避。
|
|
528
|
+
let hostEarlyExits = 0
|
|
529
|
+
// ③ 一个故障周期内只告警一次,避免 600 字符 stderr 刷屏把关键信息淹掉。
|
|
530
|
+
let hostWarned = false
|
|
531
|
+
const HOST_EARLY_EXIT_MS = 3000
|
|
532
|
+
// ④ 设置页可见的宿主故障状态。null = 正常;否则描述最近一次故障。
|
|
533
|
+
// 客户端把它渲染成设置页上的横幅 —— 否则宿主失败时用户只看到"没有通知",
|
|
534
|
+
// 完全不知道发生了什么(报告者的核心抱怨就是"没有任何报错")。
|
|
535
|
+
// 形状:{ code, signal, at, count, retryAt }
|
|
536
|
+
let hostError = null
|
|
471
537
|
|
|
472
538
|
function spawnHost() {
|
|
539
|
+
// ① 运行时自检:toast-host.ps1 必须是「UTF-8 with BOM」。
|
|
540
|
+
// 宿主是 Windows PowerShell 5.1 拉起的,它对无 BOM 的 .ps1 会按系统 ANSI
|
|
541
|
+
// 代码页解码;在中文 Windows(936/932/950)下中文会吃掉字符串收尾引号 →
|
|
542
|
+
// 整段脚本语法错误 → 宿主秒退 → 所有自定义通知不显示且无任何报错。
|
|
543
|
+
// prepublishOnly 里也有同样的断言,但那只在发布时拦得住;这里是给
|
|
544
|
+
// "开发期误丢 BOM 后本地重载插件"这条路径兜底。
|
|
545
|
+
try {
|
|
546
|
+
const head = fs.readFileSync(TOAST_HOST_SCRIPT)
|
|
547
|
+
if (!(head[0] === 0xEF && head[1] === 0xBB && head[2] === 0xBF)) {
|
|
548
|
+
console.warn(
|
|
549
|
+
'[dsh-notify-yimit] lib/toast-host.ps1 缺少 UTF-8 BOM!中文 Windows 下 ' +
|
|
550
|
+
'PowerShell 5.1 会按 ANSI 解码导致脚本语法错误、宿主秒退,自定义通知将全部不显示。' +
|
|
551
|
+
'请把该文件另存为「UTF-8 with BOM」。'
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (process.env.DSH_DEBUG) console.warn(`[dsh-notify-yimit] BOM 自检失败: ${String(error)}`)
|
|
556
|
+
}
|
|
473
557
|
try {
|
|
474
558
|
const spawnFn = typeof globalThis.__DSH_NOTIFY_TEST_SPAWN__ === 'function' ? globalThis
|
|
475
559
|
.__DSH_NOTIFY_TEST_SPAWN__ : spawn
|
|
@@ -484,6 +568,9 @@ export function apply(ctx) {
|
|
|
484
568
|
return
|
|
485
569
|
}
|
|
486
570
|
hostAlive = true
|
|
571
|
+
// ④ 新宿主已拉起:清掉上一次的故障状态(真正的恢复以"活过阈值"为准,
|
|
572
|
+
// 但拉起成功即可先收起横幅,若又秒退会立刻重新置上)。
|
|
573
|
+
hostError = null
|
|
487
574
|
host.stdin.on('error', (err) => {
|
|
488
575
|
if (process.env.DSH_DEBUG) console.warn(`[dsh-notify-yimit] stdin error: ${err}`)
|
|
489
576
|
})
|
|
@@ -493,6 +580,64 @@ export function apply(ctx) {
|
|
|
493
580
|
})
|
|
494
581
|
host.stdout.setEncoding('utf8')
|
|
495
582
|
|
|
583
|
+
// ── 宿主 stderr 与早退诊断 ──────────────────────────────────────────
|
|
584
|
+
// toast-host.ps1 是「UTF-8 with BOM」文件:Windows PowerShell 5.1 对无 BOM 的
|
|
585
|
+
// .ps1 会按系统 ANSI 代码页解码,在中文 Windows(936/932/950)下中文会吃掉
|
|
586
|
+
// 字符串引号 → 整段脚本语法错误 → 宿主**秒退**。
|
|
587
|
+
// 而发通知失败原本是"静默入队",用户既看不到浮窗、也拿不到任何报错。
|
|
588
|
+
// 所以这里把宿主的 stderr 留住,并在它启动后很快以非 0 退出时明确告警。
|
|
589
|
+
let hostStderr = ''
|
|
590
|
+
let spawnedAt = Date.now()
|
|
591
|
+
host.stderr.on('data', (d) => {
|
|
592
|
+
if (hostStderr.length < 8192) hostStderr += String(d)
|
|
593
|
+
})
|
|
594
|
+
host.stderr.on('error', () => {})
|
|
595
|
+
// exit 报告里带上"存活时长"与 stderr,便于直接定位。
|
|
596
|
+
// ③ 条件用 `code !== 0 || signal != null`:被信号杀死时 code 为 null、signal 才有值,
|
|
597
|
+
// 旧写法 `code !== 0 && code !== null` 会正好漏掉这种情况而不告警。
|
|
598
|
+
host.on('exit', (code, signal) => {
|
|
599
|
+
const livedMs = Date.now() - spawnedAt
|
|
600
|
+
hostAlive = false
|
|
601
|
+
activeToasts.clear()
|
|
602
|
+
toastOrder.length = 0
|
|
603
|
+
pendingClose.clear()
|
|
604
|
+
if (livedMs < HOST_EARLY_EXIT_MS && (code !== 0 || signal !== null)) {
|
|
605
|
+
// ② 记一次早退,据此退避,避免进程风暴
|
|
606
|
+
hostEarlyExits++
|
|
607
|
+
hostRestartUntil = Date.now() + Math.min(30000, 1000 * (2 ** hostEarlyExits))
|
|
608
|
+
// ④ 暴露给设置页(客户端据此显示横幅)
|
|
609
|
+
hostError = {
|
|
610
|
+
code: code === null ? null : Number(code),
|
|
611
|
+
signal: signal === null ? null : String(signal),
|
|
612
|
+
at: Date.now(),
|
|
613
|
+
count: hostEarlyExits,
|
|
614
|
+
retryAt: hostRestartUntil,
|
|
615
|
+
}
|
|
616
|
+
// ③ 一个故障周期内只告警一次
|
|
617
|
+
if (!hostWarned) {
|
|
618
|
+
hostWarned = true
|
|
619
|
+
const detail = hostStderr.trim().slice(0, 600)
|
|
620
|
+
console.warn(
|
|
621
|
+
`[dsh-notify-yimit] 通知宿主进程启动后 ${livedMs}ms 即退出` +
|
|
622
|
+
`(code=${code}${signal !== null ? `, signal=${signal}` : ''}),自定义通知将无法显示。` +
|
|
623
|
+
`最可能的原因:lib/toast-host.ps1 丢失了 UTF-8 BOM` +
|
|
624
|
+
`(中文 Windows 下 PS 5.1 会按 ANSI 解码导致语法错误)。` +
|
|
625
|
+
`已进入退避,${Math.round((hostRestartUntil - Date.now()) / 1000)}s 内不再重启。` +
|
|
626
|
+
(detail.length > 0 ? `\n--- 宿主 stderr ---\n${detail}` : '\n(宿主未输出 stderr)')
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
} else if (livedMs >= HOST_EARLY_EXIT_MS) {
|
|
630
|
+
// ② 活过阈值 = 健康,清零计数与告警状态(并可提示已恢复)。
|
|
631
|
+
if (hostEarlyExits > 0) {
|
|
632
|
+
console.warn('[dsh-notify-yimit] 通知宿主已恢复正常运行。')
|
|
633
|
+
}
|
|
634
|
+
hostEarlyExits = 0
|
|
635
|
+
hostRestartUntil = 0
|
|
636
|
+
hostWarned = false
|
|
637
|
+
// ④ 清除故障状态,设置页横幅随之收起
|
|
638
|
+
hostError = null
|
|
639
|
+
}
|
|
640
|
+
})
|
|
496
641
|
let buf = ''
|
|
497
642
|
host.stdout.on('data', (d) => {
|
|
498
643
|
buf += String(d)
|
|
@@ -532,12 +677,21 @@ export function apply(ctx) {
|
|
|
532
677
|
} else if (msg.type === 'geometry' && typeof msg.left === 'number' && typeof msg.top ===
|
|
533
678
|
'number') {
|
|
534
679
|
// 编辑模式:样板窗拖拽/缩放结束 → 写回配置,并应用到在屏浮窗。
|
|
680
|
+
//
|
|
681
|
+
// 坐标口径:宿主回传的 msg.top 就是"基准线(卡片可见底边)的屏幕 Y"。
|
|
682
|
+
// 宿主定位时用的是同一个口径($win.Top = pos.top − lineInWin),
|
|
683
|
+
// 所以"存下的值"与"重开时的落点"必然一致,不会漂移。
|
|
684
|
+
// 注意不要再自己加 msg.baselineTop:那是"基准线从窗口顶边算起的偏移",
|
|
685
|
+
// 只为调试/换算提供,加进去会重复计入一个卡片高。
|
|
535
686
|
const width = Math.max(260, Math.min(520, Math.round(Number(msg.width) || 340)))
|
|
536
687
|
const left = Math.round(Number(msg.left))
|
|
688
|
+
// position.top 一律存"基准线的屏幕 Y";posAtBaseline 标记供
|
|
689
|
+
// normalizeConfig 区分新旧坐标(旧数据语义不同,需迁移)。
|
|
537
690
|
const top = Math.round(Number(msg.top))
|
|
538
691
|
store.set({
|
|
539
692
|
toast: {
|
|
540
693
|
width,
|
|
694
|
+
posAtBaseline: true,
|
|
541
695
|
position: {
|
|
542
696
|
left,
|
|
543
697
|
top
|
|
@@ -555,13 +709,6 @@ export function apply(ctx) {
|
|
|
555
709
|
}
|
|
556
710
|
}
|
|
557
711
|
})
|
|
558
|
-
|
|
559
|
-
host.on('exit', () => {
|
|
560
|
-
hostAlive = false
|
|
561
|
-
activeToasts.clear()
|
|
562
|
-
toastOrder.length = 0
|
|
563
|
-
pendingClose.clear()
|
|
564
|
-
})
|
|
565
712
|
}
|
|
566
713
|
|
|
567
714
|
function sendCmd(payload) {
|
|
@@ -576,14 +723,11 @@ export function apply(ctx) {
|
|
|
576
723
|
|
|
577
724
|
function ensureHost() {
|
|
578
725
|
if (host !== null && hostAlive) return true
|
|
726
|
+
// ② 退避窗口内直接放弃:否则宿主秒退时每条通知都会重拉一个 powershell.exe。
|
|
727
|
+
// 退避时长由 exit 回调按"连续早退次数"设置(那里才有真实存活时长),
|
|
728
|
+
// 不能像旧代码那样在 spawn 之后同步判断 hostAlive —— 那是恒为 true 的。
|
|
579
729
|
if (Date.now() < hostRestartUntil) return false
|
|
580
730
|
spawnHost()
|
|
581
|
-
if (!hostAlive) {
|
|
582
|
-
hostRestartAttempts++
|
|
583
|
-
hostRestartUntil = Date.now() + Math.min(30000, 1000 * (2 ** hostRestartAttempts))
|
|
584
|
-
} else {
|
|
585
|
-
hostRestartAttempts = 0
|
|
586
|
-
}
|
|
587
731
|
return host !== null && hostAlive
|
|
588
732
|
}
|
|
589
733
|
|
|
@@ -617,19 +761,27 @@ export function apply(ctx) {
|
|
|
617
761
|
const cfg = store.get()
|
|
618
762
|
const pos = cfg.toast?.position ?? null
|
|
619
763
|
const gap = typeof cfg.toast?.gap === 'number' ? cfg.toast.gap : TOAST_GAP
|
|
764
|
+
// 窗口排布间距:扣除两侧透明辉光边距,使"可见卡片间距"真正等于用户设置的 gap。
|
|
765
|
+
const boxGap = stackGap(gap)
|
|
620
766
|
if (pos !== null) {
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
767
|
+
// 自定义位置:锚点(pos.top,即编辑模式那条基准线)对应"最新那条的可见卡片底边"。
|
|
768
|
+
// 这是底部锚定 —— 无论通知内容多长、卡片多高,底边都恒定落在基准线上,
|
|
769
|
+
// 只有顶边随内容浮动。
|
|
770
|
+
//
|
|
771
|
+
// 定位公式必须与宿主 New-ToastWindow 的 Loaded 完全一致,否则宿主先落位、
|
|
772
|
+
// 这里再校正,两者会互相打架(表现为浮窗抖动或被推错位):
|
|
773
|
+
// 窗口顶边 = pos.top − (窗口高 − TOAST_BOTTOM_GAP) − offsetY
|
|
774
|
+
// "窗口高"用每条自己回传的 meta.height —— 卡片高度随内容变化,写死会让
|
|
775
|
+
// 长内容的底边偏离锚点。offsetY = 该条之上已叠的高度(第 0 位为 0)。
|
|
776
|
+
// 横向同步 position.left,编辑调整位置后,在屏浮窗立即左右移动到位。
|
|
777
|
+
let offset = 0
|
|
627
778
|
for (let i = 0; i < toastOrder.length; i += 1) {
|
|
628
779
|
const rec = activeToasts.get(toastOrder[i])
|
|
629
780
|
if (rec === undefined) continue
|
|
630
|
-
const height = rec.meta?.height ??
|
|
631
|
-
const newTop =
|
|
632
|
-
|
|
781
|
+
const height = rec.meta?.height ?? DEFAULT_WINDOW_H
|
|
782
|
+
const newTop = Math.round(pos.top - (height - TOAST_BOTTOM_GAP) - offset)
|
|
783
|
+
// 累积"本条之上"的高度:可见卡片高(= 窗口高 − 2×辉光)+ gap
|
|
784
|
+
offset += Math.max(0, height - 2 * TOAST_GLOW) + boxGap
|
|
633
785
|
const needsTop = rec.meta !== null && newTop !== rec.meta.top
|
|
634
786
|
const needsLeft = rec.meta !== null && rec.meta.left !== pos.left
|
|
635
787
|
if (needsTop || needsLeft) {
|
|
@@ -645,19 +797,19 @@ export function apply(ctx) {
|
|
|
645
797
|
}
|
|
646
798
|
sendCmd(move)
|
|
647
799
|
}
|
|
648
|
-
prevTop = newTop
|
|
649
800
|
}
|
|
650
801
|
return
|
|
651
802
|
}
|
|
652
|
-
//
|
|
653
|
-
//
|
|
803
|
+
// 默认右下:最新那条的"可见卡片底边"距工作区底边 TOAST_EDGE_PAD,向上堆叠;
|
|
804
|
+
// 横向回默认贴边(left 'default'),覆盖"恢复默认位置/改宽度后需回到右下角"的场景。
|
|
805
|
+
// 与宿主默认分支同式:窗口顶边 = 工作区底边 − TOAST_EDGE_PAD − (窗口高 − TOAST_BOTTOM_GAP) − offsetY。
|
|
654
806
|
if (globalWorkBottom === null) return
|
|
655
|
-
let
|
|
807
|
+
let offset = 0
|
|
656
808
|
for (let i = 0; i < toastOrder.length; i += 1) {
|
|
657
809
|
const rec = activeToasts.get(toastOrder[i])
|
|
658
810
|
if (rec === undefined) continue
|
|
659
|
-
const height = rec.meta?.height ??
|
|
660
|
-
const newTop = Math.round(
|
|
811
|
+
const height = rec.meta?.height ?? DEFAULT_WINDOW_H
|
|
812
|
+
const newTop = Math.round(globalWorkBottom - TOAST_EDGE_PAD - (height - TOAST_BOTTOM_GAP) - offset)
|
|
661
813
|
if (rec.meta !== null && newTop !== rec.meta.top) {
|
|
662
814
|
rec.meta.top = newTop
|
|
663
815
|
sendCmd({
|
|
@@ -667,7 +819,7 @@ export function apply(ctx) {
|
|
|
667
819
|
left: 'default'
|
|
668
820
|
})
|
|
669
821
|
}
|
|
670
|
-
|
|
822
|
+
offset += Math.max(0, height - 2 * TOAST_GLOW) + boxGap
|
|
671
823
|
}
|
|
672
824
|
}
|
|
673
825
|
|
|
@@ -745,10 +897,18 @@ export function apply(ctx) {
|
|
|
745
897
|
}
|
|
746
898
|
const insertIndex = replaceIndex !== -1 ? replaceIndex : 0
|
|
747
899
|
const gap = typeof cfg.toast?.gap === 'number' ? cfg.toast.gap : TOAST_GAP
|
|
900
|
+
const boxGap = stackGap(gap)
|
|
901
|
+
// 宿主首帧落位用的偏移量,语义 = "该条之上已经叠了多少高度(px)"。
|
|
902
|
+
// 宿主按底部锚定定位:窗口顶边 = 锚点Y − 卡片可见高 − offsetY,
|
|
903
|
+
// 于是该条"可见卡片底边 = 锚点Y − offsetY"。所以:
|
|
904
|
+
// · 第 0 位(最下方那条)上方没有任何东西 → offsetY 必须为 0;
|
|
905
|
+
// · 其余位置 = Σ(上方各条的可见卡片高 + gap)。
|
|
906
|
+
// 每条只吃自己的高度,永远不需要预测新条自己的高度。
|
|
748
907
|
let offsetY = 0
|
|
749
908
|
for (let i = 0; i < insertIndex; i += 1) {
|
|
750
909
|
const r = activeToasts.get(toastOrder[i])
|
|
751
|
-
|
|
910
|
+
// 上方每一条贡献"可见卡片高 + gap";可见卡片高 = 窗口高 − 2×辉光。
|
|
911
|
+
offsetY += Math.max(0, (r?.meta?.height ?? 130) - 2 * TOAST_GLOW) + boxGap
|
|
752
912
|
}
|
|
753
913
|
|
|
754
914
|
const lang = cfg.locale === 'zh' ? 'zh' : 'en'
|
|
@@ -774,6 +934,13 @@ export function apply(ctx) {
|
|
|
774
934
|
jumpLabel: l.jump,
|
|
775
935
|
// 桌面端隐藏「跳转会话」按钮(该按钮会打开浏览器,在桌面端内多余)
|
|
776
936
|
jumpEnabled: isDesktopRuntime !== true,
|
|
937
|
+
// 「返回应用」只在桌面端有意义:把已在运行的桌面窗口切到前台。
|
|
938
|
+
// 实现是宿主侧枚举窗口 + SetForegroundWindow —— 不能用 dsh://open:
|
|
939
|
+
// 应用只在 macOS 的 open-url 事件里处理该 URL,Windows 上协议 URL 是以
|
|
940
|
+
// argv 交给新进程的,而 second-instance 处理器不读 argv,那条路是死代码。
|
|
941
|
+
// 因此把应用可执行文件路径一并下发,供宿主按进程定位窗口。
|
|
942
|
+
activateLabel: isDesktopRuntime === true ? l.activate : '',
|
|
943
|
+
appPath: isDesktopRuntime === true ? String(process.execPath ?? '') : '',
|
|
777
944
|
unnamedLabel: l.unnamed,
|
|
778
945
|
// 通知窗口样式:宽度 + 自定义位置(null = 默认右下贴边)
|
|
779
946
|
width: cfg.toast?.width ?? 340,
|
|
@@ -1110,6 +1277,14 @@ export function apply(ctx) {
|
|
|
1110
1277
|
config: store.get(),
|
|
1111
1278
|
// 运行环境:桌面端(=Electron 宿主)还是 web 端。客户端据此隐藏只对浏览器有意义的设置项。
|
|
1112
1279
|
desktop: isDesktopRuntime === true,
|
|
1280
|
+
// ④ 宿主故障状态(null = 正常)。客户端在设置页渲染成横幅,让"通知不显示"
|
|
1281
|
+
// 这件事从"静默失败"变成可见、可解释。
|
|
1282
|
+
hostError: hostError === null ? null : {
|
|
1283
|
+
...hostError
|
|
1284
|
+
},
|
|
1285
|
+
// 宿主不可用时,通知会堆在插件侧的 queue 里(见 showDesktopToast 的失败分支)。
|
|
1286
|
+
// 给客户端一个条数,用于告诉用户"积压了多少条"。
|
|
1287
|
+
pendingCount: Math.max(0, queue.length - queueHead),
|
|
1113
1288
|
events: queue.slice(queueHead).map((item) => ({
|
|
1114
1289
|
...item
|
|
1115
1290
|
}))
|
|
@@ -1162,6 +1337,8 @@ export function apply(ctx) {
|
|
|
1162
1337
|
sendCmd({
|
|
1163
1338
|
cmd: 'edit',
|
|
1164
1339
|
title: EDIT_TITLES[lang],
|
|
1340
|
+
// 基准线提示文案:告诉用户"通知以该位置为基准、均在此线上方"
|
|
1341
|
+
anchorLabel: EDIT_ANCHOR_LABELS[lang],
|
|
1165
1342
|
width: cfg.toast?.width ?? 340,
|
|
1166
1343
|
position: cfg.toast?.position ?? null,
|
|
1167
1344
|
// 新增:类型色卡数组,样板窗内做预览
|
|
@@ -1185,6 +1362,22 @@ export function apply(ctx) {
|
|
|
1185
1362
|
ok: true
|
|
1186
1363
|
}
|
|
1187
1364
|
},
|
|
1365
|
+
/**
|
|
1366
|
+
* ④ 立即重试拉起通知宿主(设置页横幅上的「重新拉起宿主」)。
|
|
1367
|
+
* 清掉退避窗口与故障状态后立刻 spawn;若又秒退,exit 回调会重新置上
|
|
1368
|
+
* hostError 并按新的连续早退次数退避。
|
|
1369
|
+
*/
|
|
1370
|
+
retryHost() {
|
|
1371
|
+
hostEarlyExits = 0
|
|
1372
|
+
hostRestartUntil = 0
|
|
1373
|
+
hostWarned = false
|
|
1374
|
+
hostError = null
|
|
1375
|
+
hostAlive = false
|
|
1376
|
+
spawnHost()
|
|
1377
|
+
return {
|
|
1378
|
+
ok: hostAlive
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1188
1381
|
}
|
|
1189
1382
|
|
|
1190
1383
|
Object.defineProperty(service, 'typertRemote', {
|