dsh-wisp 1.34.0 → 1.35.0
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.md +68 -0
- package/lib/client.js +156 -43
- package/lib/client.template.js +156 -43
- package/package.json +1 -1
- package/verify-wisp.mjs +55 -2
package/README.md
CHANGED
|
@@ -448,6 +448,74 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
448
448
|
|
|
449
449
|
## 变更
|
|
450
450
|
|
|
451
|
+
### 1.35.0
|
|
452
|
+
|
|
453
|
+
**她开始替你"值班"。** 在这之前,你走开时她只是**安静下来**;回来只说一句"你走了好久"。
|
|
454
|
+
可那段时间里跑完的轮次、出的错,她本来就看得到 —— 以前只是没人把它们留下来。
|
|
455
|
+
|
|
456
|
+
#### ① 值班
|
|
457
|
+
|
|
458
|
+
- **走开期间**:她放慢轮询(**1.2s → 5s**)但**不停** —— 值班的前提是还看得见。
|
|
459
|
+
絮叨和踱步照旧停掉(那些只给看得见的人看)。
|
|
460
|
+
- **只记账,不表演**:藏起来时她不换表情、不说话。否则"只说一次"的状态会被白白用掉
|
|
461
|
+
(里程碑在没人看的时候被说掉,你回来就不会再提了)。
|
|
462
|
+
- **回来报账**:*"你走的这段时间,跑完 3 轮了。出了 2 次错。"* ——
|
|
463
|
+
没有账的时候才是原来那句问候。
|
|
464
|
+
- **单轮跑得异常久**(超过 **10 分钟**)她会中途说一声:*"这轮跑了 10 分钟了,还在跑。"*
|
|
465
|
+
同一轮只说一次,而且不是催促 —— 是"我还在看着"。
|
|
466
|
+
|
|
467
|
+
#### ② 今日小结
|
|
468
|
+
|
|
469
|
+
菜单「行为」里多了一项 **今天做了什么**(也可以 `__wisp.saySummary()`):
|
|
470
|
+
*"今天:7 轮、2 次报错、专注 50 分钟、起来活动 3 次。记着呢。"*
|
|
471
|
+
|
|
472
|
+
她记的是:**轮次 / 报错次数 / 最长一轮 / 专注总时长 / 提醒你起来的次数**。
|
|
473
|
+
存在记忆里、**按天重置** —— 刷新页面不会丢,第二天重新算。
|
|
474
|
+
|
|
475
|
+
#### 可观测性
|
|
476
|
+
|
|
477
|
+
`doctor()` 里多了 `today`(今天的账)、`watch`(走开期间的值班记录)、
|
|
478
|
+
`runInFlightMs` 与 `longRunSaid`(当前这轮跑了多久、有没有已经提过)。
|
|
479
|
+
最后两个是**被测试逼出来的**:一条断言失败时,诊断直接说出 `runInFlight=599800` ——
|
|
480
|
+
差 200ms 没到阈值,问题在测试的时间轴而不是功能。
|
|
481
|
+
|
|
482
|
+
预检 529 → **539 项**;冒烟 57 项。
|
|
483
|
+
|
|
484
|
+
### 1.34.1
|
|
485
|
+
|
|
486
|
+
### 1.34.1
|
|
487
|
+
|
|
488
|
+
**菜单改成紧凑刻度。** 上一版样式好看但**占地太大** —— 对陪伴插件的浮层来说不合适,
|
|
489
|
+
它不该占掉屏幕的观感重心。
|
|
490
|
+
|
|
491
|
+
压缩是分两轮做的,因为**高度**才是占屏幕的主因(宽度只是一部分):
|
|
492
|
+
|
|
493
|
+
| | 1.34.0 | 1.34.1 |
|
|
494
|
+
|---|---|---|
|
|
495
|
+
| 面板宽 | 188px | **174px** |
|
|
496
|
+
| 行高 | 8×10 padding | **4×6** |
|
|
497
|
+
| 字号 | 12.5px | **11px** |
|
|
498
|
+
| 圆角 | 14px | **9px** |
|
|
499
|
+
| 分隔线外边距 | 6px 8px | **3px 4px** |
|
|
500
|
+
| 摘要胶囊 | 9.5px / 15px 行高 | **9px / 13px** |
|
|
501
|
+
| 开关 | 30×16 | **22×12** |
|
|
502
|
+
| 滑块轨 / 旋钮 | 18 / 13px | **12 / 9px** |
|
|
503
|
+
| 角落键帽 | 7×4 padding | **3×3** |
|
|
504
|
+
| 子面板宽 | 164px | **150px** |
|
|
505
|
+
| 字形槽 | 14px | **12px** |
|
|
506
|
+
|
|
507
|
+
实测(真浏览器截图裁剪框):**452×500 → 363×334**,即**宽 −20%、高 −33%**。
|
|
508
|
+
字号到 11px 就停 —— 再小在这个尺寸的浮层上就不叫可读了。
|
|
509
|
+
|
|
510
|
+
**顺带一个副作用**:面板变窄之后,她在屏幕**右下角**时子面板已经能放在右侧
|
|
511
|
+
(以前放不下要翻到左边)。定位算式用的是"菜单宽度 + 面板宽度",所以宽度一变它自己就跟上了 ——
|
|
512
|
+
这也是那条"四角几何"检查的意义:它每次都验一遍真实关系。
|
|
513
|
+
|
|
514
|
+
`MENU_W` / `SUBMENU_W` 两个估算常量跟着更新(前者用于菜单夹取,后者必须等于 CSS 宽度 ——
|
|
515
|
+
定位算式的正确性依赖这一点)。
|
|
516
|
+
|
|
517
|
+
### 1.34.0
|
|
518
|
+
|
|
451
519
|
### 1.34.0
|
|
452
520
|
|
|
453
521
|
**菜单样式重做。** 这一轮先把"菜单拍下来"这一步补上 —— 样式改动光看代码看不出来。
|
package/lib/client.js
CHANGED
|
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
var exports = module.exports
|
|
52
52
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
53
53
|
|
|
54
|
-
const VERSION = '1.
|
|
54
|
+
const VERSION = '1.35.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -77,6 +77,8 @@ const WHATS_NEW = {
|
|
|
77
77
|
'1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
|
|
78
78
|
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
79
|
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
80
|
+
'1.34.1': '菜单改成紧凑刻度,占屏幕小了三成。',
|
|
81
|
+
'1.35.0': '她开始替你值班:你走开时看着,回来报账;还能说今日小结。',
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -332,6 +334,22 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
332
334
|
'算力不够了,得补点碳水。', '电量告急,切待机。', '你要不要也来一口?',
|
|
333
335
|
'这碗不算加餐,算基础设施。', '吃饱了才有力气摸鱼。', '饭碗见底了,该续命了。', '我数着米粒呢。',
|
|
334
336
|
],
|
|
337
|
+
/* 值班:你走开的时候她替你看着,回来汇报。分片拼装(跑了多少轮 / 有没有报错),
|
|
338
|
+
因为三种组合(只有轮次、只有报错、两者都有)都该读起来像人话。 */
|
|
339
|
+
awayOpen: ['你走的这段时间,', '刚才那会儿,', '我没走开 —— '],
|
|
340
|
+
awayRuns: ['跑完了 {n} 轮。', '跑完 {n} 轮了。', '有 {n} 轮跑完了。'],
|
|
341
|
+
awayErrors: ['有一次报错,我看过了。', '出了 {n} 次错。', '有 {n} 次报错,你回来看一眼。'],
|
|
342
|
+
/* 单轮跑得异常久:不是催促,是"我还在看着" */
|
|
343
|
+
longRun: [
|
|
344
|
+
'这轮跑了 {n} 分钟了,还在跑。', '{n} 分钟了,我一直看着。', '还在跑,已经 {n} 分钟。',
|
|
345
|
+
'{n} 分钟 —— 要不要我替你盯着,你先去忙?',
|
|
346
|
+
],
|
|
347
|
+
/* 今日小结:把她记着的东西一次说完 */
|
|
348
|
+
summary: [
|
|
349
|
+
'今天跑了 {runs} 轮,报错 {errors} 次,专注 {focus} 分钟 —— 我提醒你起来 {care} 次。',
|
|
350
|
+
'今天:{runs} 轮、{errors} 次报错、专注 {focus} 分钟、起来活动 {care} 次。记着呢。',
|
|
351
|
+
'这一天跑完 {runs} 轮,出错 {errors} 次,专注 {focus} 分钟。我提醒过你 {care} 次别久坐。',
|
|
352
|
+
],
|
|
335
353
|
/* 专注计时器。她是陪着你干活的,所以"替你画出一段时间的边界"比再多说几句更实在。 */
|
|
336
354
|
focusStart: [
|
|
337
355
|
'{n} 分钟。开始吧。', '好,{n} 分钟,我盯着你。', '计时开始,{n} 分钟。', '{n} 分钟不看别的。',
|
|
@@ -695,49 +713,50 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
695
713
|
.wisp-root[data-mood="attn"] .wisp-body{animation-name:wisp-nudge;animation-duration:1.5s;animation-iteration-count:4}
|
|
696
714
|
@keyframes wisp-nudge{0%,100%{transform:translate3d(0,0,0)}25%{transform:translate3d(-7px,0,0)}75%{transform:translate3d(7px,0,0)}}
|
|
697
715
|
@keyframes wisp-hop{0%,100%{transform:translate3d(0,0,0)}45%{transform:translate3d(0,-16px,0)}}
|
|
698
|
-
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。
|
|
699
|
-
|
|
700
|
-
.wisp-menu-
|
|
716
|
+
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。
|
|
717
|
+
尺寸走**紧凑刻度**:这是陪伴插件的浮层,不该占掉屏幕的观感重心。 */
|
|
718
|
+
.wisp-menu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;min-width:164px;max-width:196px;padding:4px;border-radius:9px;font-size:11px;line-height:1.2;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--wisp-surface);border:1px solid var(--wisp-line);box-shadow:0 10px 28px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(16px) saturate(160%);backdrop-filter:blur(16px) saturate(160%);pointer-events:auto;animation:wisp-menu .13s ease}
|
|
719
|
+
.wisp-menu-item{display:block;width:100%;padding:4px 6px;border:0;border-radius:6px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;white-space:nowrap;transition:background .1s ease,box-shadow .1s ease}
|
|
701
720
|
.wisp-menu-item:hover{background:var(--wisp-hover,rgba(120,170,255,.2))}
|
|
702
721
|
.wisp-menu-item:active{background:var(--wisp-hover,rgba(120,170,255,.2));box-shadow:inset 0 1px 2px rgba(0,0,0,.12)}
|
|
703
722
|
/* 键盘焦点必须是**看得见**的:这些行都是 border:0 的按钮,浏览器默认轮廓很弱。 */
|
|
704
|
-
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-
|
|
705
|
-
.wisp-menu-head{padding:
|
|
706
|
-
.wisp-menu-title{font-size:
|
|
707
|
-
.wisp-menu-sub{
|
|
708
|
-
.wisp-menu-
|
|
709
|
-
.wisp-menu-
|
|
710
|
-
.wisp-menu-
|
|
711
|
-
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
723
|
+
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-slider:focus-visible{outline:2px solid var(--wisp-accent,#4a7cf7);outline-offset:-2px}
|
|
724
|
+
.wisp-menu-head{padding:1px 6px 4px}
|
|
725
|
+
.wisp-menu-title{font-size:11.5px;font-weight:600;letter-spacing:.01em}
|
|
726
|
+
.wisp-menu-sub{font-size:9px;opacity:.6;margin-top:0}
|
|
727
|
+
.wisp-menu-sep{height:1px;margin:3px 4px;background:var(--wisp-line,var(--wisp-say-line));opacity:.5}
|
|
728
|
+
.wisp-menu-hint{padding:4px 6px 0;font-size:9px;opacity:.5;border-top:1px solid var(--wisp-line,var(--wisp-say-line));margin-top:3px}
|
|
729
|
+
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
712
730
|
/* 摘要做成胶囊:收起状态也要能一眼扫到"现在是什么",纯淡字太弱。 */
|
|
713
|
-
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:
|
|
731
|
+
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:0 4px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:9px;line-height:13px;opacity:.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
714
732
|
/* 主操作(专注):淡色底 + 左侧强调条 + 字形,一眼看出"这是主要动作"。 */
|
|
715
733
|
.wisp-menu-item[data-accent="true"]{position:relative;font-weight:600;color:var(--wisp-accent,inherit);background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.2)),transparent 88%)}
|
|
716
|
-
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:
|
|
734
|
+
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:2px;top:50%;width:2px;height:11px;margin-top:-5.5px;border-radius:1px;background:var(--wisp-accent,#4a7cf7)}
|
|
717
735
|
.wisp-menu-item[data-accent="true"]:hover{background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.32)),transparent 92%)}
|
|
718
736
|
.wisp-menu-item[data-muted="true"]{opacity:.8}
|
|
719
|
-
|
|
737
|
+
/* 字形槽:每行都留等宽位,标签左边线才对得齐 */
|
|
738
|
+
.wisp-menu-glyph{display:inline-block;width:12px;margin-right:4px;text-align:center;font-size:10.5px;opacity:.72;font-weight:400}
|
|
720
739
|
.wisp-menu-item[data-accent="true"] .wisp-menu-glyph{opacity:1}
|
|
721
|
-
.wisp-menu-caret{opacity:.5;font-size:
|
|
722
|
-
.wisp-switch{flex:0 0 auto;width:
|
|
723
|
-
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0
|
|
724
|
-
.wisp-switch i{position:absolute;top:2px;left:2px;width:
|
|
725
|
-
.wisp-switch[data-on="true"] i{transform:translateX(
|
|
726
|
-
.wisp-slider-row{padding:
|
|
727
|
-
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:
|
|
740
|
+
.wisp-menu-caret{opacity:.5;font-size:8px;flex:0 0 auto}
|
|
741
|
+
.wisp-switch{flex:0 0 auto;width:22px;height:12px;border-radius:6px;background:rgba(140,160,190,.4);position:relative;transition:background .16s ease}
|
|
742
|
+
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0 2px rgba(74,124,247,.14)}
|
|
743
|
+
.wisp-switch i{position:absolute;top:2px;left:2px;width:8px;height:8px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .16s ease}
|
|
744
|
+
.wisp-switch[data-on="true"] i{transform:translateX(10px)}
|
|
745
|
+
.wisp-slider-row{padding:3px 6px 5px}
|
|
746
|
+
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:9.5px;opacity:.85;margin-bottom:1px}
|
|
728
747
|
.wisp-slider-value{font-variant-numeric:tabular-nums;opacity:.75}
|
|
729
|
-
.wisp-slider{position:relative;height:
|
|
730
|
-
.wisp-slider-track{position:absolute;left:0;right:0;top:
|
|
731
|
-
.wisp-slider-fill{position:absolute;left:0;top:
|
|
732
|
-
.wisp-slider-knob{position:absolute;top:
|
|
733
|
-
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.
|
|
734
|
-
.wisp-slider:active .wisp-slider-knob{transform:scale(1.
|
|
748
|
+
.wisp-slider{position:relative;height:12px;cursor:pointer;outline:none;touch-action:none}
|
|
749
|
+
.wisp-slider-track{position:absolute;left:0;right:0;top:5px;height:2px;border-radius:1px;background:rgba(140,160,190,.4)}
|
|
750
|
+
.wisp-slider-fill{position:absolute;left:0;top:5px;height:2px;border-radius:1px;background:var(--wisp-accent,#4a7cf7)}
|
|
751
|
+
.wisp-slider-knob{position:absolute;top:1px;width:9px;height:9px;margin-left:-5px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:transform .1s ease}
|
|
752
|
+
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.15)}
|
|
753
|
+
.wisp-slider:active .wisp-slider-knob{transform:scale(1.25)}
|
|
735
754
|
/* 四个角落做成"键帽":方位是空间的,控件也该长得像方位键。 */
|
|
736
|
-
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:
|
|
737
|
-
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:
|
|
755
|
+
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:3px;padding:1px 4px 3px}
|
|
756
|
+
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:3px;padding:3px 3px;border:1px solid var(--wisp-line,var(--wisp-say-line));border-radius:5px;background:var(--wisp-hover,rgba(120,170,255,.12));color:inherit;font:inherit;font-size:9.5px;cursor:pointer;transition:background .1s ease,border-color .1s ease}
|
|
738
757
|
.wisp-grid2 button:hover,.wisp-corner:hover{background:var(--wisp-hover,rgba(120,170,255,.26))}
|
|
739
758
|
.wisp-grid2 button[data-current="true"],.wisp-corner[data-current="true"]{border-color:var(--wisp-accent,#4a7cf7);color:var(--wisp-accent,inherit);font-weight:600;box-shadow:0 0 0 1px var(--wisp-accent,#4a7cf7) inset}
|
|
740
|
-
.wisp-corner-glyph{font-size:
|
|
759
|
+
.wisp-corner-glyph{font-size:10px;opacity:.7}
|
|
741
760
|
.wisp-root[data-hidden="true"]{display:none}
|
|
742
761
|
.wisp-tab{position:absolute;width:56px;height:56px;padding:0;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:16px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));box-shadow:0 6px 18px rgba(0,0,0,.22);cursor:pointer;pointer-events:auto;animation:wisp-menu .16s ease;transition:transform .16s ease}
|
|
743
762
|
.wisp-tab:hover{transform:translateY(-2px)}
|
|
@@ -748,7 +767,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
748
767
|
.wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
|
|
749
768
|
/* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
|
|
750
769
|
与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
|
|
751
|
-
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:
|
|
770
|
+
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:150px;padding:4px;border-radius:9px;font-size:11px;line-height:1.2;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 16px 40px rgba(0,0,0,.34),0 2px 6px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(18px) saturate(170%);backdrop-filter:blur(18px) saturate(170%);pointer-events:auto;animation:wisp-fade .12s ease}
|
|
752
771
|
@keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
|
|
753
772
|
.wisp-menu-sub{padding-left:10px}
|
|
754
773
|
.wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
|
|
@@ -1513,6 +1532,47 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1513
1532
|
return null
|
|
1514
1533
|
}
|
|
1515
1534
|
|
|
1535
|
+
/* ---- 今天记了些什么 -----------------------------------------------------
|
|
1536
|
+
轮次、报错、专注时长、提醒次数 —— 这些数字本来就在手上,以前只是没留下来。
|
|
1537
|
+
存在记忆里(按天重置),刷新页面也不会丢,这样"今日小结"才说得准。 */
|
|
1538
|
+
const watchToday = () => {
|
|
1539
|
+
const today = dayKey()
|
|
1540
|
+
if (!memory.stats || memory.statsDay !== today) {
|
|
1541
|
+
memory.stats = { runs: 0, errors: 0, longestMs: 0, focusMs: 0, care: 0 }
|
|
1542
|
+
memory.statsDay = today
|
|
1543
|
+
saveMemory()
|
|
1544
|
+
}
|
|
1545
|
+
return memory.stats
|
|
1546
|
+
}
|
|
1547
|
+
const bumpStats = (patchStats) => {
|
|
1548
|
+
const stats = watchToday()
|
|
1549
|
+
for (const key of Object.keys(patchStats)) {
|
|
1550
|
+
if (typeof stats[key] !== 'number') stats[key] = 0
|
|
1551
|
+
stats[key] += patchStats[key]
|
|
1552
|
+
}
|
|
1553
|
+
saveMemory()
|
|
1554
|
+
return stats
|
|
1555
|
+
}
|
|
1556
|
+
const saySummary = () => {
|
|
1557
|
+
const stats = watchToday()
|
|
1558
|
+
const text = pick(LINES.summary)
|
|
1559
|
+
.split('{runs}').join(String(stats.runs))
|
|
1560
|
+
.split('{errors}').join(String(stats.errors))
|
|
1561
|
+
.split('{focus}').join(String(Math.round(stats.focusMs / 60000)))
|
|
1562
|
+
.split('{care}').join(String(stats.care))
|
|
1563
|
+
wake(true)
|
|
1564
|
+
flashHappy(text)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/* ---- 值班 ---------------------------------------------------------------
|
|
1568
|
+
你在的时候她陪你;你走开的时候她替你看着:跑完了几轮、有没有报错。
|
|
1569
|
+
藏起来时**放慢**轮询而不是停掉 —— 值班的前提是还看得见(5 秒的延迟对这件事足够)。
|
|
1570
|
+
回来时把那段时间的账报给你。 */
|
|
1571
|
+
const HIDDEN_POLL_MS = 5000
|
|
1572
|
+
const LONG_RUN_MS = 600000
|
|
1573
|
+
let longRunSaidAt = 0
|
|
1574
|
+
let awayWatch = null
|
|
1575
|
+
|
|
1516
1576
|
/* ---- react to what the app is doing ---- */
|
|
1517
1577
|
let lastBusy = false
|
|
1518
1578
|
let lastPending = null
|
|
@@ -1616,10 +1676,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1616
1676
|
/* 一轮刚跑完的那一刻看一次错误。同一批错误只说一次;错误清掉之后再出现新的
|
|
1617
1677
|
才会再说 —— 否则每轮结束都会重复念同一句。 */
|
|
1618
1678
|
const checkErrors = () => {
|
|
1619
|
-
if (!cfg.reactions) return
|
|
1679
|
+
if (!cfg.reactions) return 0
|
|
1620
1680
|
const count = countErrors()
|
|
1621
|
-
|
|
1681
|
+
/* 只有"新出现"的才算一次:同一批错误不该被数很多遍(值班汇报要的是新账) */
|
|
1682
|
+
const fresh = count > lastErrorCount ? count - lastErrorCount : 0
|
|
1683
|
+
if (count > 0 && count !== lastErrorCount && !paused) flashWorried(pick(LINES.error))
|
|
1622
1684
|
lastErrorCount = count
|
|
1685
|
+
return fresh
|
|
1623
1686
|
}
|
|
1624
1687
|
|
|
1625
1688
|
const poll = () => {
|
|
@@ -1630,16 +1693,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1630
1693
|
if (!busy && lastBusy) {
|
|
1631
1694
|
/* 里程碑优先于普通庆祝:同一次跑完只该说一句,而"第 25 轮"比"跑完了"更值得说 */
|
|
1632
1695
|
const hit = cfg.reactions && cfg.milestones ? recordRun() : null
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1696
|
+
/* 藏起来时**只记账不表演**:没人看得见她,而且"只说一次"的状态会被白白用掉
|
|
1697
|
+
(里程碑在没人看的时候被说掉,你回来就不会再提了)。回来有专门的汇报。 */
|
|
1698
|
+
if (!paused) {
|
|
1699
|
+
if (hit !== null) {
|
|
1700
|
+
flashProud(pick(LINES.milestone).split('{n}').join(String(hit)))
|
|
1701
|
+
} else if (cfg.celebrate && nowMs() - busySince >= cfg.celebrateAfterMs) {
|
|
1702
|
+
flashHappy(pick(LINES.done))
|
|
1703
|
+
}
|
|
1637
1704
|
}
|
|
1638
1705
|
// 一轮结束 = 错误唯一会出现的时机,所以错误检查挂在这个边上而不是轮询里
|
|
1639
|
-
checkErrors()
|
|
1706
|
+
const fresh = checkErrors()
|
|
1707
|
+
/* 记账:今天跑了多少轮、最长一轮多久、有没有新报错;走开期间的另记一份 */
|
|
1708
|
+
const ranMs = busySince > 0 ? nowMs() - busySince : 0
|
|
1709
|
+
bumpStats({ runs: 1, errors: fresh })
|
|
1710
|
+
const stats = watchToday()
|
|
1711
|
+
if (ranMs > stats.longestMs) { stats.longestMs = ranMs; saveMemory() }
|
|
1712
|
+
if (awayWatch !== null) {
|
|
1713
|
+
awayWatch.runs++
|
|
1714
|
+
awayWatch.errors += fresh
|
|
1715
|
+
if (ranMs > awayWatch.longestMs) awayWatch.longestMs = ranMs
|
|
1716
|
+
}
|
|
1640
1717
|
}
|
|
1641
1718
|
lastBusy = busy
|
|
1642
1719
|
|
|
1720
|
+
/* 单轮跑得异常久:她中途说一声(同一轮只说一次)。不是催促,是"我还在看着"。 */
|
|
1721
|
+
if (busy && busySince > 0) {
|
|
1722
|
+
const ran = nowMs() - busySince
|
|
1723
|
+
if (!paused && ran >= LONG_RUN_MS && longRunSaidAt < busySince) {
|
|
1724
|
+
longRunSaidAt = nowMs()
|
|
1725
|
+
say(pick(LINES.longRun).split('{n}').join(String(Math.round(ran / 60000))))
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1643
1729
|
/* ---- 久坐追踪 ---------------------------------------------------------
|
|
1644
1730
|
"连着坐了多久"没有直接信号,用"最近有没有活动"近似:跑着任务或输入框里有字
|
|
1645
1731
|
就算还在动。中间短暂停一下(不到 CARE_ACTIVE_GAP_MS)不算离开 —— 人是会停下来
|
|
@@ -1664,6 +1750,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1664
1750
|
/* 提醒你的时候她自己得先醒着 —— 打盹着喊人起来活动,读起来像 bug。
|
|
1665
1751
|
pending 面板那条路也是这么处理的(先 wake 再说话)。 */
|
|
1666
1752
|
if (mood === 'sleep') wake(true)
|
|
1753
|
+
bumpStats({ care: 1 })
|
|
1667
1754
|
flashCare(pick(LINES.care))
|
|
1668
1755
|
}
|
|
1669
1756
|
|
|
@@ -1758,6 +1845,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1758
1845
|
focusHandle = null
|
|
1759
1846
|
focusMinutes = 0
|
|
1760
1847
|
focusDone++
|
|
1848
|
+
bumpStats({ focusMs: minutes * 60000 })
|
|
1761
1849
|
wake(true)
|
|
1762
1850
|
flashProud(pick(LINES.focusDone).split('{n}').join(String(minutes)))
|
|
1763
1851
|
}
|
|
@@ -2030,7 +2118,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2030
2118
|
/* ---- 右键 = 叫回原位 ----
|
|
2031
2119
|
她大了之后很容易被拖到看不见的地方,而"恢复位置"原先只能开 Console。
|
|
2032
2120
|
现在右键她本体弹出菜单,归位只是其中一项。 */
|
|
2033
|
-
const MENU_W =
|
|
2121
|
+
const MENU_W = 164
|
|
2034
2122
|
const MENU_ITEM_H = 30
|
|
2035
2123
|
const MENU_PAD = 10
|
|
2036
2124
|
let menuEl = null
|
|
@@ -2231,7 +2319,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2231
2319
|
延迟是 hover intent:鼠标只是路过一行,不该弹出东西。 */
|
|
2232
2320
|
const HOVER_OPEN_MS = 140
|
|
2233
2321
|
const HOVER_CLOSE_MS = 260
|
|
2234
|
-
const SUBMENU_W =
|
|
2322
|
+
const SUBMENU_W = 150
|
|
2235
2323
|
let flyoutName = ''
|
|
2236
2324
|
let flyoutEl = null
|
|
2237
2325
|
let flyoutRow = null
|
|
@@ -2644,6 +2732,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2644
2732
|
children: [
|
|
2645
2733
|
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2646
2734
|
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2735
|
+
{ label: '今天做了什么', glyph: '✎', run: () => saySummary() },
|
|
2647
2736
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2648
2737
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2649
2738
|
],
|
|
@@ -2929,11 +3018,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2929
3018
|
try { state = document.visibilityState === 'hidden' ? 'hidden' : 'visible' } catch (e) { state = 'visible' }
|
|
2930
3019
|
if (state === 'hidden') {
|
|
2931
3020
|
awayAt = nowMs()
|
|
2932
|
-
/*
|
|
3021
|
+
/* 絮叨和踱步停掉(它们本来就只给看得见的人看),但**轮询放慢而不是停掉** ——
|
|
3022
|
+
值班的前提是还看得见:你走开时跑完的那几轮、出的错,都得记下来。 */
|
|
2933
3023
|
paused = true
|
|
3024
|
+
awayWatch = { runs: 0, errors: 0, longestMs: 0, at: awayAt }
|
|
2934
3025
|
syncPoll()
|
|
2935
3026
|
syncChatter()
|
|
2936
3027
|
syncWander()
|
|
3028
|
+
if (pollHandle) { clock.cancel(pollHandle); pollHandle = null }
|
|
3029
|
+
pollHandle = clock.every(HIDDEN_POLL_MS, poll)
|
|
2937
3030
|
return
|
|
2938
3031
|
}
|
|
2939
3032
|
/* 回来了:先恢复再算问候 —— 恢复是必须的,问候才是锦上添花 */
|
|
@@ -2945,9 +3038,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2945
3038
|
}
|
|
2946
3039
|
if (awayAt === 0) return
|
|
2947
3040
|
const away = nowMs() - awayAt
|
|
3041
|
+
const watch = awayWatch
|
|
3042
|
+
awayWatch = null
|
|
2948
3043
|
awayAt = 0
|
|
2949
3044
|
if (!(cfg.backAfterMs > 0) || away < cfg.backAfterMs) return
|
|
2950
3045
|
wake(true)
|
|
3046
|
+
/* 有账就报账(这才是"值班"的意义);没账才是普通的那句问候。 */
|
|
3047
|
+
if (watch !== null && (watch.runs > 0 || watch.errors > 0)) {
|
|
3048
|
+
const bits = []
|
|
3049
|
+
if (watch.runs > 0) bits.push(pick(LINES.awayRuns).split('{n}').join(String(watch.runs)))
|
|
3050
|
+
if (watch.errors > 0) bits.push(pick(LINES.awayErrors).split('{n}').join(String(watch.errors)))
|
|
3051
|
+
say(pick(LINES.awayOpen) + bits.join(''))
|
|
3052
|
+
return
|
|
3053
|
+
}
|
|
2951
3054
|
say(pick(away >= BACK_LONG_MS ? LINES.backLong : LINES.backSoon))
|
|
2952
3055
|
}
|
|
2953
3056
|
if (typeof document.addEventListener === 'function') {
|
|
@@ -3179,6 +3282,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3179
3282
|
/* 专注计时器:脚本与测试都能驱动 */
|
|
3180
3283
|
startFocus,
|
|
3181
3284
|
stopFocus,
|
|
3285
|
+
saySummary,
|
|
3286
|
+
get today() { return { ...watchToday() } },
|
|
3182
3287
|
get focus() { return focusState() },
|
|
3183
3288
|
openAbout: () => aboutDialog(),
|
|
3184
3289
|
openActions: () => actionsDialog(),
|
|
@@ -3309,11 +3414,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3309
3414
|
},
|
|
3310
3415
|
streak: streakOf(memory.days),
|
|
3311
3416
|
awaySince: Math.round(awayAt),
|
|
3417
|
+
/* 今天记了什么 —— 不用猜"她为什么说今天跑了 7 轮" */
|
|
3418
|
+
today: { ...watchToday() },
|
|
3419
|
+
/* 走开期间的值班记录(不在值班时为 null) */
|
|
3420
|
+
watch: awayWatch === null ? null : { ...awayWatch, forMs: Math.round(nowMs() - awayWatch.at) },
|
|
3421
|
+
/* 单轮跑了多久、有没有已经提过 —— 断言失败时能自己说出原因,不用猜 */
|
|
3422
|
+
runInFlightMs: busySince > 0 && lastBusy ? Math.round(nowMs() - busySince) : 0,
|
|
3423
|
+
longRunSaid: longRunSaidAt > 0,
|
|
3312
3424
|
/* 专注:不用猜"她为什么一直站在干活中" */
|
|
3313
3425
|
focus: focusState(),
|
|
3314
3426
|
/* 定时器是否挂着;页面藏起来时三个都该是 false */
|
|
3315
3427
|
timers: {
|
|
3316
|
-
poll: pollHandle !== null,
|
|
3428
|
+
poll: pollHandle !== null,
|
|
3429
|
+
pollMs: paused ? HIDDEN_POLL_MS : cfg.pollMs, chatter: chatterHandle !== null,
|
|
3317
3430
|
wander: wanderHandle !== null, paused,
|
|
3318
3431
|
},
|
|
3319
3432
|
/* 今日轮次:不用猜"她为什么突然得意",数字就在这里 */
|
package/lib/client.template.js
CHANGED
|
@@ -51,7 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
var exports = module.exports
|
|
52
52
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
53
53
|
|
|
54
|
-
const VERSION = '1.
|
|
54
|
+
const VERSION = '1.35.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -77,6 +77,8 @@ const WHATS_NEW = {
|
|
|
77
77
|
'1.33.1': '修子菜单位置算错、会压住主菜单的问题。',
|
|
78
78
|
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
79
|
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
80
|
+
'1.34.1': '菜单改成紧凑刻度,占屏幕小了三成。',
|
|
81
|
+
'1.35.0': '她开始替你值班:你走开时看着,回来报账;还能说今日小结。',
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -281,6 +283,22 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
281
283
|
'算力不够了,得补点碳水。', '电量告急,切待机。', '你要不要也来一口?',
|
|
282
284
|
'这碗不算加餐,算基础设施。', '吃饱了才有力气摸鱼。', '饭碗见底了,该续命了。', '我数着米粒呢。',
|
|
283
285
|
],
|
|
286
|
+
/* 值班:你走开的时候她替你看着,回来汇报。分片拼装(跑了多少轮 / 有没有报错),
|
|
287
|
+
因为三种组合(只有轮次、只有报错、两者都有)都该读起来像人话。 */
|
|
288
|
+
awayOpen: ['你走的这段时间,', '刚才那会儿,', '我没走开 —— '],
|
|
289
|
+
awayRuns: ['跑完了 {n} 轮。', '跑完 {n} 轮了。', '有 {n} 轮跑完了。'],
|
|
290
|
+
awayErrors: ['有一次报错,我看过了。', '出了 {n} 次错。', '有 {n} 次报错,你回来看一眼。'],
|
|
291
|
+
/* 单轮跑得异常久:不是催促,是"我还在看着" */
|
|
292
|
+
longRun: [
|
|
293
|
+
'这轮跑了 {n} 分钟了,还在跑。', '{n} 分钟了,我一直看着。', '还在跑,已经 {n} 分钟。',
|
|
294
|
+
'{n} 分钟 —— 要不要我替你盯着,你先去忙?',
|
|
295
|
+
],
|
|
296
|
+
/* 今日小结:把她记着的东西一次说完 */
|
|
297
|
+
summary: [
|
|
298
|
+
'今天跑了 {runs} 轮,报错 {errors} 次,专注 {focus} 分钟 —— 我提醒你起来 {care} 次。',
|
|
299
|
+
'今天:{runs} 轮、{errors} 次报错、专注 {focus} 分钟、起来活动 {care} 次。记着呢。',
|
|
300
|
+
'这一天跑完 {runs} 轮,出错 {errors} 次,专注 {focus} 分钟。我提醒过你 {care} 次别久坐。',
|
|
301
|
+
],
|
|
284
302
|
/* 专注计时器。她是陪着你干活的,所以"替你画出一段时间的边界"比再多说几句更实在。 */
|
|
285
303
|
focusStart: [
|
|
286
304
|
'{n} 分钟。开始吧。', '好,{n} 分钟,我盯着你。', '计时开始,{n} 分钟。', '{n} 分钟不看别的。',
|
|
@@ -644,49 +662,50 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
644
662
|
.wisp-root[data-mood="attn"] .wisp-body{animation-name:wisp-nudge;animation-duration:1.5s;animation-iteration-count:4}
|
|
645
663
|
@keyframes wisp-nudge{0%,100%{transform:translate3d(0,0,0)}25%{transform:translate3d(-7px,0,0)}75%{transform:translate3d(7px,0,0)}}
|
|
646
664
|
@keyframes wisp-hop{0%,100%{transform:translate3d(0,0,0)}45%{transform:translate3d(0,-16px,0)}}
|
|
647
|
-
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。
|
|
648
|
-
|
|
649
|
-
.wisp-menu-
|
|
665
|
+
/* 菜单与级联子面板共用一套外观变量 —— 面板是菜单的"上一层",只改阴影与浮起量。
|
|
666
|
+
尺寸走**紧凑刻度**:这是陪伴插件的浮层,不该占掉屏幕的观感重心。 */
|
|
667
|
+
.wisp-menu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;min-width:164px;max-width:196px;padding:4px;border-radius:9px;font-size:11px;line-height:1.2;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--wisp-surface);border:1px solid var(--wisp-line);box-shadow:0 10px 28px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(16px) saturate(160%);backdrop-filter:blur(16px) saturate(160%);pointer-events:auto;animation:wisp-menu .13s ease}
|
|
668
|
+
.wisp-menu-item{display:block;width:100%;padding:4px 6px;border:0;border-radius:6px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;white-space:nowrap;transition:background .1s ease,box-shadow .1s ease}
|
|
650
669
|
.wisp-menu-item:hover{background:var(--wisp-hover,rgba(120,170,255,.2))}
|
|
651
670
|
.wisp-menu-item:active{background:var(--wisp-hover,rgba(120,170,255,.2));box-shadow:inset 0 1px 2px rgba(0,0,0,.12)}
|
|
652
671
|
/* 键盘焦点必须是**看得见**的:这些行都是 border:0 的按钮,浏览器默认轮廓很弱。 */
|
|
653
|
-
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-
|
|
654
|
-
.wisp-menu-head{padding:
|
|
655
|
-
.wisp-menu-title{font-size:
|
|
656
|
-
.wisp-menu-sub{
|
|
657
|
-
.wisp-menu-
|
|
658
|
-
.wisp-menu-
|
|
659
|
-
.wisp-menu-
|
|
660
|
-
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
672
|
+
.wisp-menu-item:focus-visible,.wisp-corner:focus-visible,.wisp-slider:focus-visible{outline:2px solid var(--wisp-accent,#4a7cf7);outline-offset:-2px}
|
|
673
|
+
.wisp-menu-head{padding:1px 6px 4px}
|
|
674
|
+
.wisp-menu-title{font-size:11.5px;font-weight:600;letter-spacing:.01em}
|
|
675
|
+
.wisp-menu-sub{font-size:9px;opacity:.6;margin-top:0}
|
|
676
|
+
.wisp-menu-sep{height:1px;margin:3px 4px;background:var(--wisp-line,var(--wisp-say-line));opacity:.5}
|
|
677
|
+
.wisp-menu-hint{padding:4px 6px 0;font-size:9px;opacity:.5;border-top:1px solid var(--wisp-line,var(--wisp-say-line));margin-top:3px}
|
|
678
|
+
.wisp-menu-row{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
661
679
|
/* 摘要做成胶囊:收起状态也要能一眼扫到"现在是什么",纯淡字太弱。 */
|
|
662
|
-
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:
|
|
680
|
+
.wisp-menu-sum{flex:0 1 auto;min-width:0;padding:0 4px;border-radius:999px;background:var(--wisp-hover,rgba(120,170,255,.2));font-size:9px;line-height:13px;opacity:.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
663
681
|
/* 主操作(专注):淡色底 + 左侧强调条 + 字形,一眼看出"这是主要动作"。 */
|
|
664
682
|
.wisp-menu-item[data-accent="true"]{position:relative;font-weight:600;color:var(--wisp-accent,inherit);background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.2)),transparent 88%)}
|
|
665
|
-
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:
|
|
683
|
+
.wisp-menu-item[data-accent="true"]::before{content:"";position:absolute;left:2px;top:50%;width:2px;height:11px;margin-top:-5.5px;border-radius:1px;background:var(--wisp-accent,#4a7cf7)}
|
|
666
684
|
.wisp-menu-item[data-accent="true"]:hover{background:linear-gradient(90deg,var(--wisp-hover,rgba(120,170,255,.32)),transparent 92%)}
|
|
667
685
|
.wisp-menu-item[data-muted="true"]{opacity:.8}
|
|
668
|
-
|
|
686
|
+
/* 字形槽:每行都留等宽位,标签左边线才对得齐 */
|
|
687
|
+
.wisp-menu-glyph{display:inline-block;width:12px;margin-right:4px;text-align:center;font-size:10.5px;opacity:.72;font-weight:400}
|
|
669
688
|
.wisp-menu-item[data-accent="true"] .wisp-menu-glyph{opacity:1}
|
|
670
|
-
.wisp-menu-caret{opacity:.5;font-size:
|
|
671
|
-
.wisp-switch{flex:0 0 auto;width:
|
|
672
|
-
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0
|
|
673
|
-
.wisp-switch i{position:absolute;top:2px;left:2px;width:
|
|
674
|
-
.wisp-switch[data-on="true"] i{transform:translateX(
|
|
675
|
-
.wisp-slider-row{padding:
|
|
676
|
-
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:
|
|
689
|
+
.wisp-menu-caret{opacity:.5;font-size:8px;flex:0 0 auto}
|
|
690
|
+
.wisp-switch{flex:0 0 auto;width:22px;height:12px;border-radius:6px;background:rgba(140,160,190,.4);position:relative;transition:background .16s ease}
|
|
691
|
+
.wisp-switch[data-on="true"]{background:var(--wisp-accent,#4a7cf7);box-shadow:0 0 0 2px rgba(74,124,247,.14)}
|
|
692
|
+
.wisp-switch i{position:absolute;top:2px;left:2px;width:8px;height:8px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .16s ease}
|
|
693
|
+
.wisp-switch[data-on="true"] i{transform:translateX(10px)}
|
|
694
|
+
.wisp-slider-row{padding:3px 6px 5px}
|
|
695
|
+
.wisp-slider-top{display:flex;justify-content:space-between;align-items:baseline;font-size:9.5px;opacity:.85;margin-bottom:1px}
|
|
677
696
|
.wisp-slider-value{font-variant-numeric:tabular-nums;opacity:.75}
|
|
678
|
-
.wisp-slider{position:relative;height:
|
|
679
|
-
.wisp-slider-track{position:absolute;left:0;right:0;top:
|
|
680
|
-
.wisp-slider-fill{position:absolute;left:0;top:
|
|
681
|
-
.wisp-slider-knob{position:absolute;top:
|
|
682
|
-
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.
|
|
683
|
-
.wisp-slider:active .wisp-slider-knob{transform:scale(1.
|
|
697
|
+
.wisp-slider{position:relative;height:12px;cursor:pointer;outline:none;touch-action:none}
|
|
698
|
+
.wisp-slider-track{position:absolute;left:0;right:0;top:5px;height:2px;border-radius:1px;background:rgba(140,160,190,.4)}
|
|
699
|
+
.wisp-slider-fill{position:absolute;left:0;top:5px;height:2px;border-radius:1px;background:var(--wisp-accent,#4a7cf7)}
|
|
700
|
+
.wisp-slider-knob{position:absolute;top:1px;width:9px;height:9px;margin-left:-5px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.35);transition:transform .1s ease}
|
|
701
|
+
.wisp-slider:hover .wisp-slider-knob{transform:scale(1.15)}
|
|
702
|
+
.wisp-slider:active .wisp-slider-knob{transform:scale(1.25)}
|
|
684
703
|
/* 四个角落做成"键帽":方位是空间的,控件也该长得像方位键。 */
|
|
685
|
-
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:
|
|
686
|
-
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:
|
|
704
|
+
.wisp-grid2{display:grid;grid-template-columns:1fr 1fr;gap:3px;padding:1px 4px 3px}
|
|
705
|
+
.wisp-grid2 button,.wisp-corner{display:flex;align-items:center;justify-content:center;gap:3px;padding:3px 3px;border:1px solid var(--wisp-line,var(--wisp-say-line));border-radius:5px;background:var(--wisp-hover,rgba(120,170,255,.12));color:inherit;font:inherit;font-size:9.5px;cursor:pointer;transition:background .1s ease,border-color .1s ease}
|
|
687
706
|
.wisp-grid2 button:hover,.wisp-corner:hover{background:var(--wisp-hover,rgba(120,170,255,.26))}
|
|
688
707
|
.wisp-grid2 button[data-current="true"],.wisp-corner[data-current="true"]{border-color:var(--wisp-accent,#4a7cf7);color:var(--wisp-accent,inherit);font-weight:600;box-shadow:0 0 0 1px var(--wisp-accent,#4a7cf7) inset}
|
|
689
|
-
.wisp-corner-glyph{font-size:
|
|
708
|
+
.wisp-corner-glyph{font-size:10px;opacity:.7}
|
|
690
709
|
.wisp-root[data-hidden="true"]{display:none}
|
|
691
710
|
.wisp-tab{position:absolute;width:56px;height:56px;padding:0;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:16px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));box-shadow:0 6px 18px rgba(0,0,0,.22);cursor:pointer;pointer-events:auto;animation:wisp-menu .16s ease;transition:transform .16s ease}
|
|
692
711
|
.wisp-tab:hover{transform:translateY(-2px)}
|
|
@@ -697,7 +716,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
697
716
|
.wisp-menu-caret{opacity:.55;font-size:10px;flex:0 0 auto}
|
|
698
717
|
/* 级联子菜单:浮在行的**右侧**(放不下时翻到左侧),不把下面的内容推下去。
|
|
699
718
|
与父行顶部对齐、允许轻微重叠 —— 指针从行斜着移到面板上时不会穿出"同一区域"。 */
|
|
700
|
-
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:
|
|
719
|
+
.wisp-submenu{--wisp-surface:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));--wisp-line:var(--dsw-alias-border-l1,var(--wisp-say-line));--wisp-hover:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2));--wisp-accent:var(--dsw-alias-brand-primary,#4a7cf7);position:absolute;box-sizing:border-box;width:150px;padding:4px;border-radius:9px;font-size:11px;line-height:1.2;color:var(--dsw-alias-label-primary,var(--wisp-say-fg));background:var(--dsw-alias-bg-overlay,var(--wisp-say-bg));border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));box-shadow:0 16px 40px rgba(0,0,0,.34),0 2px 6px rgba(0,0,0,.18);-webkit-backdrop-filter:blur(18px) saturate(170%);backdrop-filter:blur(18px) saturate(170%);pointer-events:auto;animation:wisp-fade .12s ease}
|
|
701
720
|
@keyframes wisp-fade{0%{opacity:0}100%{opacity:1}}
|
|
702
721
|
.wisp-menu-sub{padding-left:10px}
|
|
703
722
|
.wisp-menu-sub .wisp-menu-item{padding-left:12px;opacity:.94}
|
|
@@ -1462,6 +1481,47 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1462
1481
|
return null
|
|
1463
1482
|
}
|
|
1464
1483
|
|
|
1484
|
+
/* ---- 今天记了些什么 -----------------------------------------------------
|
|
1485
|
+
轮次、报错、专注时长、提醒次数 —— 这些数字本来就在手上,以前只是没留下来。
|
|
1486
|
+
存在记忆里(按天重置),刷新页面也不会丢,这样"今日小结"才说得准。 */
|
|
1487
|
+
const watchToday = () => {
|
|
1488
|
+
const today = dayKey()
|
|
1489
|
+
if (!memory.stats || memory.statsDay !== today) {
|
|
1490
|
+
memory.stats = { runs: 0, errors: 0, longestMs: 0, focusMs: 0, care: 0 }
|
|
1491
|
+
memory.statsDay = today
|
|
1492
|
+
saveMemory()
|
|
1493
|
+
}
|
|
1494
|
+
return memory.stats
|
|
1495
|
+
}
|
|
1496
|
+
const bumpStats = (patchStats) => {
|
|
1497
|
+
const stats = watchToday()
|
|
1498
|
+
for (const key of Object.keys(patchStats)) {
|
|
1499
|
+
if (typeof stats[key] !== 'number') stats[key] = 0
|
|
1500
|
+
stats[key] += patchStats[key]
|
|
1501
|
+
}
|
|
1502
|
+
saveMemory()
|
|
1503
|
+
return stats
|
|
1504
|
+
}
|
|
1505
|
+
const saySummary = () => {
|
|
1506
|
+
const stats = watchToday()
|
|
1507
|
+
const text = pick(LINES.summary)
|
|
1508
|
+
.split('{runs}').join(String(stats.runs))
|
|
1509
|
+
.split('{errors}').join(String(stats.errors))
|
|
1510
|
+
.split('{focus}').join(String(Math.round(stats.focusMs / 60000)))
|
|
1511
|
+
.split('{care}').join(String(stats.care))
|
|
1512
|
+
wake(true)
|
|
1513
|
+
flashHappy(text)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/* ---- 值班 ---------------------------------------------------------------
|
|
1517
|
+
你在的时候她陪你;你走开的时候她替你看着:跑完了几轮、有没有报错。
|
|
1518
|
+
藏起来时**放慢**轮询而不是停掉 —— 值班的前提是还看得见(5 秒的延迟对这件事足够)。
|
|
1519
|
+
回来时把那段时间的账报给你。 */
|
|
1520
|
+
const HIDDEN_POLL_MS = 5000
|
|
1521
|
+
const LONG_RUN_MS = 600000
|
|
1522
|
+
let longRunSaidAt = 0
|
|
1523
|
+
let awayWatch = null
|
|
1524
|
+
|
|
1465
1525
|
/* ---- react to what the app is doing ---- */
|
|
1466
1526
|
let lastBusy = false
|
|
1467
1527
|
let lastPending = null
|
|
@@ -1565,10 +1625,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1565
1625
|
/* 一轮刚跑完的那一刻看一次错误。同一批错误只说一次;错误清掉之后再出现新的
|
|
1566
1626
|
才会再说 —— 否则每轮结束都会重复念同一句。 */
|
|
1567
1627
|
const checkErrors = () => {
|
|
1568
|
-
if (!cfg.reactions) return
|
|
1628
|
+
if (!cfg.reactions) return 0
|
|
1569
1629
|
const count = countErrors()
|
|
1570
|
-
|
|
1630
|
+
/* 只有"新出现"的才算一次:同一批错误不该被数很多遍(值班汇报要的是新账) */
|
|
1631
|
+
const fresh = count > lastErrorCount ? count - lastErrorCount : 0
|
|
1632
|
+
if (count > 0 && count !== lastErrorCount && !paused) flashWorried(pick(LINES.error))
|
|
1571
1633
|
lastErrorCount = count
|
|
1634
|
+
return fresh
|
|
1572
1635
|
}
|
|
1573
1636
|
|
|
1574
1637
|
const poll = () => {
|
|
@@ -1579,16 +1642,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1579
1642
|
if (!busy && lastBusy) {
|
|
1580
1643
|
/* 里程碑优先于普通庆祝:同一次跑完只该说一句,而"第 25 轮"比"跑完了"更值得说 */
|
|
1581
1644
|
const hit = cfg.reactions && cfg.milestones ? recordRun() : null
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1645
|
+
/* 藏起来时**只记账不表演**:没人看得见她,而且"只说一次"的状态会被白白用掉
|
|
1646
|
+
(里程碑在没人看的时候被说掉,你回来就不会再提了)。回来有专门的汇报。 */
|
|
1647
|
+
if (!paused) {
|
|
1648
|
+
if (hit !== null) {
|
|
1649
|
+
flashProud(pick(LINES.milestone).split('{n}').join(String(hit)))
|
|
1650
|
+
} else if (cfg.celebrate && nowMs() - busySince >= cfg.celebrateAfterMs) {
|
|
1651
|
+
flashHappy(pick(LINES.done))
|
|
1652
|
+
}
|
|
1586
1653
|
}
|
|
1587
1654
|
// 一轮结束 = 错误唯一会出现的时机,所以错误检查挂在这个边上而不是轮询里
|
|
1588
|
-
checkErrors()
|
|
1655
|
+
const fresh = checkErrors()
|
|
1656
|
+
/* 记账:今天跑了多少轮、最长一轮多久、有没有新报错;走开期间的另记一份 */
|
|
1657
|
+
const ranMs = busySince > 0 ? nowMs() - busySince : 0
|
|
1658
|
+
bumpStats({ runs: 1, errors: fresh })
|
|
1659
|
+
const stats = watchToday()
|
|
1660
|
+
if (ranMs > stats.longestMs) { stats.longestMs = ranMs; saveMemory() }
|
|
1661
|
+
if (awayWatch !== null) {
|
|
1662
|
+
awayWatch.runs++
|
|
1663
|
+
awayWatch.errors += fresh
|
|
1664
|
+
if (ranMs > awayWatch.longestMs) awayWatch.longestMs = ranMs
|
|
1665
|
+
}
|
|
1589
1666
|
}
|
|
1590
1667
|
lastBusy = busy
|
|
1591
1668
|
|
|
1669
|
+
/* 单轮跑得异常久:她中途说一声(同一轮只说一次)。不是催促,是"我还在看着"。 */
|
|
1670
|
+
if (busy && busySince > 0) {
|
|
1671
|
+
const ran = nowMs() - busySince
|
|
1672
|
+
if (!paused && ran >= LONG_RUN_MS && longRunSaidAt < busySince) {
|
|
1673
|
+
longRunSaidAt = nowMs()
|
|
1674
|
+
say(pick(LINES.longRun).split('{n}').join(String(Math.round(ran / 60000))))
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1592
1678
|
/* ---- 久坐追踪 ---------------------------------------------------------
|
|
1593
1679
|
"连着坐了多久"没有直接信号,用"最近有没有活动"近似:跑着任务或输入框里有字
|
|
1594
1680
|
就算还在动。中间短暂停一下(不到 CARE_ACTIVE_GAP_MS)不算离开 —— 人是会停下来
|
|
@@ -1613,6 +1699,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1613
1699
|
/* 提醒你的时候她自己得先醒着 —— 打盹着喊人起来活动,读起来像 bug。
|
|
1614
1700
|
pending 面板那条路也是这么处理的(先 wake 再说话)。 */
|
|
1615
1701
|
if (mood === 'sleep') wake(true)
|
|
1702
|
+
bumpStats({ care: 1 })
|
|
1616
1703
|
flashCare(pick(LINES.care))
|
|
1617
1704
|
}
|
|
1618
1705
|
|
|
@@ -1707,6 +1794,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1707
1794
|
focusHandle = null
|
|
1708
1795
|
focusMinutes = 0
|
|
1709
1796
|
focusDone++
|
|
1797
|
+
bumpStats({ focusMs: minutes * 60000 })
|
|
1710
1798
|
wake(true)
|
|
1711
1799
|
flashProud(pick(LINES.focusDone).split('{n}').join(String(minutes)))
|
|
1712
1800
|
}
|
|
@@ -1979,7 +2067,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1979
2067
|
/* ---- 右键 = 叫回原位 ----
|
|
1980
2068
|
她大了之后很容易被拖到看不见的地方,而"恢复位置"原先只能开 Console。
|
|
1981
2069
|
现在右键她本体弹出菜单,归位只是其中一项。 */
|
|
1982
|
-
const MENU_W =
|
|
2070
|
+
const MENU_W = 164
|
|
1983
2071
|
const MENU_ITEM_H = 30
|
|
1984
2072
|
const MENU_PAD = 10
|
|
1985
2073
|
let menuEl = null
|
|
@@ -2180,7 +2268,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2180
2268
|
延迟是 hover intent:鼠标只是路过一行,不该弹出东西。 */
|
|
2181
2269
|
const HOVER_OPEN_MS = 140
|
|
2182
2270
|
const HOVER_CLOSE_MS = 260
|
|
2183
|
-
const SUBMENU_W =
|
|
2271
|
+
const SUBMENU_W = 150
|
|
2184
2272
|
let flyoutName = ''
|
|
2185
2273
|
let flyoutEl = null
|
|
2186
2274
|
let flyoutRow = null
|
|
@@ -2593,6 +2681,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2593
2681
|
children: [
|
|
2594
2682
|
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2595
2683
|
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2684
|
+
{ label: '今天做了什么', glyph: '✎', run: () => saySummary() },
|
|
2596
2685
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2597
2686
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2598
2687
|
],
|
|
@@ -2878,11 +2967,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2878
2967
|
try { state = document.visibilityState === 'hidden' ? 'hidden' : 'visible' } catch (e) { state = 'visible' }
|
|
2879
2968
|
if (state === 'hidden') {
|
|
2880
2969
|
awayAt = nowMs()
|
|
2881
|
-
/*
|
|
2970
|
+
/* 絮叨和踱步停掉(它们本来就只给看得见的人看),但**轮询放慢而不是停掉** ——
|
|
2971
|
+
值班的前提是还看得见:你走开时跑完的那几轮、出的错,都得记下来。 */
|
|
2882
2972
|
paused = true
|
|
2973
|
+
awayWatch = { runs: 0, errors: 0, longestMs: 0, at: awayAt }
|
|
2883
2974
|
syncPoll()
|
|
2884
2975
|
syncChatter()
|
|
2885
2976
|
syncWander()
|
|
2977
|
+
if (pollHandle) { clock.cancel(pollHandle); pollHandle = null }
|
|
2978
|
+
pollHandle = clock.every(HIDDEN_POLL_MS, poll)
|
|
2886
2979
|
return
|
|
2887
2980
|
}
|
|
2888
2981
|
/* 回来了:先恢复再算问候 —— 恢复是必须的,问候才是锦上添花 */
|
|
@@ -2894,9 +2987,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2894
2987
|
}
|
|
2895
2988
|
if (awayAt === 0) return
|
|
2896
2989
|
const away = nowMs() - awayAt
|
|
2990
|
+
const watch = awayWatch
|
|
2991
|
+
awayWatch = null
|
|
2897
2992
|
awayAt = 0
|
|
2898
2993
|
if (!(cfg.backAfterMs > 0) || away < cfg.backAfterMs) return
|
|
2899
2994
|
wake(true)
|
|
2995
|
+
/* 有账就报账(这才是"值班"的意义);没账才是普通的那句问候。 */
|
|
2996
|
+
if (watch !== null && (watch.runs > 0 || watch.errors > 0)) {
|
|
2997
|
+
const bits = []
|
|
2998
|
+
if (watch.runs > 0) bits.push(pick(LINES.awayRuns).split('{n}').join(String(watch.runs)))
|
|
2999
|
+
if (watch.errors > 0) bits.push(pick(LINES.awayErrors).split('{n}').join(String(watch.errors)))
|
|
3000
|
+
say(pick(LINES.awayOpen) + bits.join(''))
|
|
3001
|
+
return
|
|
3002
|
+
}
|
|
2900
3003
|
say(pick(away >= BACK_LONG_MS ? LINES.backLong : LINES.backSoon))
|
|
2901
3004
|
}
|
|
2902
3005
|
if (typeof document.addEventListener === 'function') {
|
|
@@ -3128,6 +3231,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3128
3231
|
/* 专注计时器:脚本与测试都能驱动 */
|
|
3129
3232
|
startFocus,
|
|
3130
3233
|
stopFocus,
|
|
3234
|
+
saySummary,
|
|
3235
|
+
get today() { return { ...watchToday() } },
|
|
3131
3236
|
get focus() { return focusState() },
|
|
3132
3237
|
openAbout: () => aboutDialog(),
|
|
3133
3238
|
openActions: () => actionsDialog(),
|
|
@@ -3258,11 +3363,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3258
3363
|
},
|
|
3259
3364
|
streak: streakOf(memory.days),
|
|
3260
3365
|
awaySince: Math.round(awayAt),
|
|
3366
|
+
/* 今天记了什么 —— 不用猜"她为什么说今天跑了 7 轮" */
|
|
3367
|
+
today: { ...watchToday() },
|
|
3368
|
+
/* 走开期间的值班记录(不在值班时为 null) */
|
|
3369
|
+
watch: awayWatch === null ? null : { ...awayWatch, forMs: Math.round(nowMs() - awayWatch.at) },
|
|
3370
|
+
/* 单轮跑了多久、有没有已经提过 —— 断言失败时能自己说出原因,不用猜 */
|
|
3371
|
+
runInFlightMs: busySince > 0 && lastBusy ? Math.round(nowMs() - busySince) : 0,
|
|
3372
|
+
longRunSaid: longRunSaidAt > 0,
|
|
3261
3373
|
/* 专注:不用猜"她为什么一直站在干活中" */
|
|
3262
3374
|
focus: focusState(),
|
|
3263
3375
|
/* 定时器是否挂着;页面藏起来时三个都该是 false */
|
|
3264
3376
|
timers: {
|
|
3265
|
-
poll: pollHandle !== null,
|
|
3377
|
+
poll: pollHandle !== null,
|
|
3378
|
+
pollMs: paused ? HIDDEN_POLL_MS : cfg.pollMs, chatter: chatterHandle !== null,
|
|
3266
3379
|
wander: wanderHandle !== null, paused,
|
|
3267
3380
|
},
|
|
3268
3381
|
/* 今日轮次:不用猜"她为什么突然得意",数字就在这里 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "DeepSeek娘 — an unofficial floating desktop companion for the DeepSeek Harness UI. Zero dependencies; sprites inlined as data URIs; schedules on the Client timer service instead of the trapped browser timer globals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/verify-wisp.mjs
CHANGED
|
@@ -3430,8 +3430,12 @@ head('3y-3. the focus timer, and going quiet while the page is hidden')
|
|
|
3430
3430
|
quiet.document.visibilityState = 'hidden'
|
|
3431
3431
|
quiet.document.dispatch('visibilitychange', {})
|
|
3432
3432
|
const timers = quietApi.doctor().timers
|
|
3433
|
-
|
|
3434
|
-
|
|
3433
|
+
/* 絮叨与踱步停掉(只给看得见的人看),但**轮询放慢而不是停掉** ——
|
|
3434
|
+
值班的前提是还看得见:你走开时跑完的轮次和出的错,都要被记下来。 */
|
|
3435
|
+
check(timers.paused === true && timers.chatter === false && timers.wander === false,
|
|
3436
|
+
'hiding the page stops the chatter and the wander', JSON.stringify(timers))
|
|
3437
|
+
check(timers.poll === true && timers.pollMs === 5000,
|
|
3438
|
+
'but the poll keeps a slow watch instead of stopping (she is on duty)', JSON.stringify(timers))
|
|
3435
3439
|
|
|
3436
3440
|
/* 藏起来期间:来了一堆报错,她也不该有任何反应(轮询停了) */
|
|
3437
3441
|
quiet.state.errors = 3
|
|
@@ -3454,6 +3458,55 @@ head('3y-3. the focus timer, and going quiet while the page is hidden')
|
|
|
3454
3458
|
quiet.state.busy = false
|
|
3455
3459
|
quiet.advance(3000, 200)
|
|
3456
3460
|
check(moodOf(quiet) === 'worried', 'and now the same errors do get a reaction', moodOf(quiet))
|
|
3461
|
+
/* ---- 值班:走开期间跑完的轮次与报错,回来要报账 ------------------------------ */
|
|
3462
|
+
const saidNow = () => quiet.all('wisp-say').filter((el) => el.removed !== true)
|
|
3463
|
+
.map((el) => el.textContent).join(' ')
|
|
3464
|
+
quiet.document.visibilityState = 'hidden'
|
|
3465
|
+
quiet.document.dispatch('visibilitychange', {})
|
|
3466
|
+
quiet.state.busy = true
|
|
3467
|
+
quiet.advance(60000, 5000) // 你走开之后,她看着一轮跑了一分钟
|
|
3468
|
+
quiet.state.errors = 5
|
|
3469
|
+
quiet.state.busy = false
|
|
3470
|
+
quiet.advance(180000, 5000) // 一共走开三分钟(超过"回来问候"的门槛)
|
|
3471
|
+
check(saidNow() === '', 'while you are away she takes notes but says nothing', saidNow())
|
|
3472
|
+
|
|
3473
|
+
quiet.document.visibilityState = 'visible'
|
|
3474
|
+
quiet.document.dispatch('visibilitychange', {})
|
|
3475
|
+
quiet.advance(1000, 200)
|
|
3476
|
+
const report = saidNow()
|
|
3477
|
+
check(/轮/.test(report) && /跑完/.test(report),
|
|
3478
|
+
'coming back, she reports how many runs finished while you were away', report)
|
|
3479
|
+
check(/错/.test(report), 'and she mentions the errors she saw', report)
|
|
3480
|
+
const book = quietApi.doctor().today
|
|
3481
|
+
check(book.runs >= 1 && book.errors >= 1 && book.longestMs > 0,
|
|
3482
|
+
'and the day book recorded the runs, the errors and the longest one',
|
|
3483
|
+
JSON.stringify(book))
|
|
3484
|
+
|
|
3485
|
+
/* ---- 单轮跑得异常久:她会中途说一声,而且同一轮只说一次 ---------------------- */
|
|
3486
|
+
quiet.state.busy = true
|
|
3487
|
+
/* 刚好推过"单轮跑太久"的阈值。推 11 分钟是不行的:她的话早就过期了(气泡有存活时间),
|
|
3488
|
+
于是断言会看到空字符串 —— 断言本身没错,是喂给它的时间不对。 */
|
|
3489
|
+
/* 多推几步:busySince 是在推进开始后的第一个轮询才记下的,卡在 601000 会差 200ms。 */
|
|
3490
|
+
quiet.advance(604800, 1200)
|
|
3491
|
+
const longLine = saidNow()
|
|
3492
|
+
const during = quietApi.doctor()
|
|
3493
|
+
check(/分钟/.test(longLine), 'a single run that drags on gets a check-in',
|
|
3494
|
+
`她说的是「${longLine}」;runInFlight=${during.runInFlightMs} longRunSaid=${during.longRunSaid} paused=${during.timers.paused}`)
|
|
3495
|
+
quiet.state.busy = false
|
|
3496
|
+
quiet.advance(3000, 500)
|
|
3497
|
+
|
|
3498
|
+
/* ---- 今日小结:把她记着的东西一次说完 ---------------------------------------- */
|
|
3499
|
+
quietApi.saySummary()
|
|
3500
|
+
quiet.advance(200, 50)
|
|
3501
|
+
const summaryLine = saidNow()
|
|
3502
|
+
/* 词池里有三个变体(今天…/今天:…/这一天…),所以断言**内容**而不是某一个措辞 */
|
|
3503
|
+
check(/轮/.test(summaryLine) && /错/.test(summaryLine) && /分钟/.test(summaryLine),
|
|
3504
|
+
'the daily summary reads back what she has been tracking', summaryLine)
|
|
3505
|
+
check(/\d/.test(summaryLine), 'with real numbers', summaryLine)
|
|
3506
|
+
check(summaryLine.indexOf('{') < 0, 'and no placeholder is left unfilled', summaryLine)
|
|
3507
|
+
check(typeof quietApi.today.runs === 'number' && quietApi.today.runs >= 1,
|
|
3508
|
+
'the day book is also readable from the api', JSON.stringify(quietApi.today))
|
|
3509
|
+
|
|
3457
3510
|
quiet.win.__wisp.destroy()
|
|
3458
3511
|
active = keepQuiet
|
|
3459
3512
|
}
|