dsh-wisp 1.34.1 → 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 +35 -0
- package/lib/client.js +121 -10
- package/lib/client.template.js +121 -10
- package/package.json +1 -1
- package/verify-wisp.mjs +55 -2
package/README.md
CHANGED
|
@@ -448,6 +448,41 @@ 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
|
+
|
|
451
486
|
### 1.34.1
|
|
452
487
|
|
|
453
488
|
**菜单改成紧凑刻度。** 上一版样式好看但**占地太大** —— 对陪伴插件的浮层来说不合适,
|
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
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -78,6 +78,7 @@ const WHATS_NEW = {
|
|
|
78
78
|
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
79
|
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
80
80
|
'1.34.1': '菜单改成紧凑刻度,占屏幕小了三成。',
|
|
81
|
+
'1.35.0': '她开始替你值班:你走开时看着,回来报账;还能说今日小结。',
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -333,6 +334,22 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
333
334
|
'算力不够了,得补点碳水。', '电量告急,切待机。', '你要不要也来一口?',
|
|
334
335
|
'这碗不算加餐,算基础设施。', '吃饱了才有力气摸鱼。', '饭碗见底了,该续命了。', '我数着米粒呢。',
|
|
335
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
|
+
],
|
|
336
353
|
/* 专注计时器。她是陪着你干活的,所以"替你画出一段时间的边界"比再多说几句更实在。 */
|
|
337
354
|
focusStart: [
|
|
338
355
|
'{n} 分钟。开始吧。', '好,{n} 分钟,我盯着你。', '计时开始,{n} 分钟。', '{n} 分钟不看别的。',
|
|
@@ -1515,6 +1532,47 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1515
1532
|
return null
|
|
1516
1533
|
}
|
|
1517
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
|
+
|
|
1518
1576
|
/* ---- react to what the app is doing ---- */
|
|
1519
1577
|
let lastBusy = false
|
|
1520
1578
|
let lastPending = null
|
|
@@ -1618,10 +1676,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1618
1676
|
/* 一轮刚跑完的那一刻看一次错误。同一批错误只说一次;错误清掉之后再出现新的
|
|
1619
1677
|
才会再说 —— 否则每轮结束都会重复念同一句。 */
|
|
1620
1678
|
const checkErrors = () => {
|
|
1621
|
-
if (!cfg.reactions) return
|
|
1679
|
+
if (!cfg.reactions) return 0
|
|
1622
1680
|
const count = countErrors()
|
|
1623
|
-
|
|
1681
|
+
/* 只有"新出现"的才算一次:同一批错误不该被数很多遍(值班汇报要的是新账) */
|
|
1682
|
+
const fresh = count > lastErrorCount ? count - lastErrorCount : 0
|
|
1683
|
+
if (count > 0 && count !== lastErrorCount && !paused) flashWorried(pick(LINES.error))
|
|
1624
1684
|
lastErrorCount = count
|
|
1685
|
+
return fresh
|
|
1625
1686
|
}
|
|
1626
1687
|
|
|
1627
1688
|
const poll = () => {
|
|
@@ -1632,16 +1693,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1632
1693
|
if (!busy && lastBusy) {
|
|
1633
1694
|
/* 里程碑优先于普通庆祝:同一次跑完只该说一句,而"第 25 轮"比"跑完了"更值得说 */
|
|
1634
1695
|
const hit = cfg.reactions && cfg.milestones ? recordRun() : null
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
+
}
|
|
1639
1704
|
}
|
|
1640
1705
|
// 一轮结束 = 错误唯一会出现的时机,所以错误检查挂在这个边上而不是轮询里
|
|
1641
|
-
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
|
+
}
|
|
1642
1717
|
}
|
|
1643
1718
|
lastBusy = busy
|
|
1644
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
|
+
|
|
1645
1729
|
/* ---- 久坐追踪 ---------------------------------------------------------
|
|
1646
1730
|
"连着坐了多久"没有直接信号,用"最近有没有活动"近似:跑着任务或输入框里有字
|
|
1647
1731
|
就算还在动。中间短暂停一下(不到 CARE_ACTIVE_GAP_MS)不算离开 —— 人是会停下来
|
|
@@ -1666,6 +1750,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1666
1750
|
/* 提醒你的时候她自己得先醒着 —— 打盹着喊人起来活动,读起来像 bug。
|
|
1667
1751
|
pending 面板那条路也是这么处理的(先 wake 再说话)。 */
|
|
1668
1752
|
if (mood === 'sleep') wake(true)
|
|
1753
|
+
bumpStats({ care: 1 })
|
|
1669
1754
|
flashCare(pick(LINES.care))
|
|
1670
1755
|
}
|
|
1671
1756
|
|
|
@@ -1760,6 +1845,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1760
1845
|
focusHandle = null
|
|
1761
1846
|
focusMinutes = 0
|
|
1762
1847
|
focusDone++
|
|
1848
|
+
bumpStats({ focusMs: minutes * 60000 })
|
|
1763
1849
|
wake(true)
|
|
1764
1850
|
flashProud(pick(LINES.focusDone).split('{n}').join(String(minutes)))
|
|
1765
1851
|
}
|
|
@@ -2646,6 +2732,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2646
2732
|
children: [
|
|
2647
2733
|
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2648
2734
|
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2735
|
+
{ label: '今天做了什么', glyph: '✎', run: () => saySummary() },
|
|
2649
2736
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2650
2737
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2651
2738
|
],
|
|
@@ -2931,11 +3018,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2931
3018
|
try { state = document.visibilityState === 'hidden' ? 'hidden' : 'visible' } catch (e) { state = 'visible' }
|
|
2932
3019
|
if (state === 'hidden') {
|
|
2933
3020
|
awayAt = nowMs()
|
|
2934
|
-
/*
|
|
3021
|
+
/* 絮叨和踱步停掉(它们本来就只给看得见的人看),但**轮询放慢而不是停掉** ——
|
|
3022
|
+
值班的前提是还看得见:你走开时跑完的那几轮、出的错,都得记下来。 */
|
|
2935
3023
|
paused = true
|
|
3024
|
+
awayWatch = { runs: 0, errors: 0, longestMs: 0, at: awayAt }
|
|
2936
3025
|
syncPoll()
|
|
2937
3026
|
syncChatter()
|
|
2938
3027
|
syncWander()
|
|
3028
|
+
if (pollHandle) { clock.cancel(pollHandle); pollHandle = null }
|
|
3029
|
+
pollHandle = clock.every(HIDDEN_POLL_MS, poll)
|
|
2939
3030
|
return
|
|
2940
3031
|
}
|
|
2941
3032
|
/* 回来了:先恢复再算问候 —— 恢复是必须的,问候才是锦上添花 */
|
|
@@ -2947,9 +3038,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2947
3038
|
}
|
|
2948
3039
|
if (awayAt === 0) return
|
|
2949
3040
|
const away = nowMs() - awayAt
|
|
3041
|
+
const watch = awayWatch
|
|
3042
|
+
awayWatch = null
|
|
2950
3043
|
awayAt = 0
|
|
2951
3044
|
if (!(cfg.backAfterMs > 0) || away < cfg.backAfterMs) return
|
|
2952
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
|
+
}
|
|
2953
3054
|
say(pick(away >= BACK_LONG_MS ? LINES.backLong : LINES.backSoon))
|
|
2954
3055
|
}
|
|
2955
3056
|
if (typeof document.addEventListener === 'function') {
|
|
@@ -3181,6 +3282,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3181
3282
|
/* 专注计时器:脚本与测试都能驱动 */
|
|
3182
3283
|
startFocus,
|
|
3183
3284
|
stopFocus,
|
|
3285
|
+
saySummary,
|
|
3286
|
+
get today() { return { ...watchToday() } },
|
|
3184
3287
|
get focus() { return focusState() },
|
|
3185
3288
|
openAbout: () => aboutDialog(),
|
|
3186
3289
|
openActions: () => actionsDialog(),
|
|
@@ -3311,11 +3414,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3311
3414
|
},
|
|
3312
3415
|
streak: streakOf(memory.days),
|
|
3313
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,
|
|
3314
3424
|
/* 专注:不用猜"她为什么一直站在干活中" */
|
|
3315
3425
|
focus: focusState(),
|
|
3316
3426
|
/* 定时器是否挂着;页面藏起来时三个都该是 false */
|
|
3317
3427
|
timers: {
|
|
3318
|
-
poll: pollHandle !== null,
|
|
3428
|
+
poll: pollHandle !== null,
|
|
3429
|
+
pollMs: paused ? HIDDEN_POLL_MS : cfg.pollMs, chatter: chatterHandle !== null,
|
|
3319
3430
|
wander: wanderHandle !== null, paused,
|
|
3320
3431
|
},
|
|
3321
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
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -78,6 +78,7 @@ const WHATS_NEW = {
|
|
|
78
78
|
'1.33.2': '修她在屏幕四角时子菜单跟着偏的问题。',
|
|
79
79
|
'1.34.0': '菜单样式重做:摘要做成胶囊、每行都有图标位、键盘焦点看得见。',
|
|
80
80
|
'1.34.1': '菜单改成紧凑刻度,占屏幕小了三成。',
|
|
81
|
+
'1.35.0': '她开始替你值班:你走开时看着,回来报账;还能说今日小结。',
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -282,6 +283,22 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
282
283
|
'算力不够了,得补点碳水。', '电量告急,切待机。', '你要不要也来一口?',
|
|
283
284
|
'这碗不算加餐,算基础设施。', '吃饱了才有力气摸鱼。', '饭碗见底了,该续命了。', '我数着米粒呢。',
|
|
284
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
|
+
],
|
|
285
302
|
/* 专注计时器。她是陪着你干活的,所以"替你画出一段时间的边界"比再多说几句更实在。 */
|
|
286
303
|
focusStart: [
|
|
287
304
|
'{n} 分钟。开始吧。', '好,{n} 分钟,我盯着你。', '计时开始,{n} 分钟。', '{n} 分钟不看别的。',
|
|
@@ -1464,6 +1481,47 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1464
1481
|
return null
|
|
1465
1482
|
}
|
|
1466
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
|
+
|
|
1467
1525
|
/* ---- react to what the app is doing ---- */
|
|
1468
1526
|
let lastBusy = false
|
|
1469
1527
|
let lastPending = null
|
|
@@ -1567,10 +1625,13 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1567
1625
|
/* 一轮刚跑完的那一刻看一次错误。同一批错误只说一次;错误清掉之后再出现新的
|
|
1568
1626
|
才会再说 —— 否则每轮结束都会重复念同一句。 */
|
|
1569
1627
|
const checkErrors = () => {
|
|
1570
|
-
if (!cfg.reactions) return
|
|
1628
|
+
if (!cfg.reactions) return 0
|
|
1571
1629
|
const count = countErrors()
|
|
1572
|
-
|
|
1630
|
+
/* 只有"新出现"的才算一次:同一批错误不该被数很多遍(值班汇报要的是新账) */
|
|
1631
|
+
const fresh = count > lastErrorCount ? count - lastErrorCount : 0
|
|
1632
|
+
if (count > 0 && count !== lastErrorCount && !paused) flashWorried(pick(LINES.error))
|
|
1573
1633
|
lastErrorCount = count
|
|
1634
|
+
return fresh
|
|
1574
1635
|
}
|
|
1575
1636
|
|
|
1576
1637
|
const poll = () => {
|
|
@@ -1581,16 +1642,39 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1581
1642
|
if (!busy && lastBusy) {
|
|
1582
1643
|
/* 里程碑优先于普通庆祝:同一次跑完只该说一句,而"第 25 轮"比"跑完了"更值得说 */
|
|
1583
1644
|
const hit = cfg.reactions && cfg.milestones ? recordRun() : null
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
+
}
|
|
1588
1653
|
}
|
|
1589
1654
|
// 一轮结束 = 错误唯一会出现的时机,所以错误检查挂在这个边上而不是轮询里
|
|
1590
|
-
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
|
+
}
|
|
1591
1666
|
}
|
|
1592
1667
|
lastBusy = busy
|
|
1593
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
|
+
|
|
1594
1678
|
/* ---- 久坐追踪 ---------------------------------------------------------
|
|
1595
1679
|
"连着坐了多久"没有直接信号,用"最近有没有活动"近似:跑着任务或输入框里有字
|
|
1596
1680
|
就算还在动。中间短暂停一下(不到 CARE_ACTIVE_GAP_MS)不算离开 —— 人是会停下来
|
|
@@ -1615,6 +1699,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1615
1699
|
/* 提醒你的时候她自己得先醒着 —— 打盹着喊人起来活动,读起来像 bug。
|
|
1616
1700
|
pending 面板那条路也是这么处理的(先 wake 再说话)。 */
|
|
1617
1701
|
if (mood === 'sleep') wake(true)
|
|
1702
|
+
bumpStats({ care: 1 })
|
|
1618
1703
|
flashCare(pick(LINES.care))
|
|
1619
1704
|
}
|
|
1620
1705
|
|
|
@@ -1709,6 +1794,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1709
1794
|
focusHandle = null
|
|
1710
1795
|
focusMinutes = 0
|
|
1711
1796
|
focusDone++
|
|
1797
|
+
bumpStats({ focusMs: minutes * 60000 })
|
|
1712
1798
|
wake(true)
|
|
1713
1799
|
flashProud(pick(LINES.focusDone).split('{n}').join(String(minutes)))
|
|
1714
1800
|
}
|
|
@@ -2595,6 +2681,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2595
2681
|
children: [
|
|
2596
2682
|
{ label: '说句话', glyph: '✦', run: () => { wake(true); say(pick(LINES.click)) } },
|
|
2597
2683
|
{ label: '睡一会儿', glyph: '☾', run: () => setMood('sleep') },
|
|
2684
|
+
{ label: '今天做了什么', glyph: '✎', run: () => saySummary() },
|
|
2598
2685
|
/* 开关是**真正的开关行**:带 aria-checked,点完菜单不关(就在原地更新) */
|
|
2599
2686
|
...TOGGLES.map((t) => ({ kind: 'switch', key: t.key, label: t.label })),
|
|
2600
2687
|
],
|
|
@@ -2880,11 +2967,15 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2880
2967
|
try { state = document.visibilityState === 'hidden' ? 'hidden' : 'visible' } catch (e) { state = 'visible' }
|
|
2881
2968
|
if (state === 'hidden') {
|
|
2882
2969
|
awayAt = nowMs()
|
|
2883
|
-
/*
|
|
2970
|
+
/* 絮叨和踱步停掉(它们本来就只给看得见的人看),但**轮询放慢而不是停掉** ——
|
|
2971
|
+
值班的前提是还看得见:你走开时跑完的那几轮、出的错,都得记下来。 */
|
|
2884
2972
|
paused = true
|
|
2973
|
+
awayWatch = { runs: 0, errors: 0, longestMs: 0, at: awayAt }
|
|
2885
2974
|
syncPoll()
|
|
2886
2975
|
syncChatter()
|
|
2887
2976
|
syncWander()
|
|
2977
|
+
if (pollHandle) { clock.cancel(pollHandle); pollHandle = null }
|
|
2978
|
+
pollHandle = clock.every(HIDDEN_POLL_MS, poll)
|
|
2888
2979
|
return
|
|
2889
2980
|
}
|
|
2890
2981
|
/* 回来了:先恢复再算问候 —— 恢复是必须的,问候才是锦上添花 */
|
|
@@ -2896,9 +2987,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2896
2987
|
}
|
|
2897
2988
|
if (awayAt === 0) return
|
|
2898
2989
|
const away = nowMs() - awayAt
|
|
2990
|
+
const watch = awayWatch
|
|
2991
|
+
awayWatch = null
|
|
2899
2992
|
awayAt = 0
|
|
2900
2993
|
if (!(cfg.backAfterMs > 0) || away < cfg.backAfterMs) return
|
|
2901
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
|
+
}
|
|
2902
3003
|
say(pick(away >= BACK_LONG_MS ? LINES.backLong : LINES.backSoon))
|
|
2903
3004
|
}
|
|
2904
3005
|
if (typeof document.addEventListener === 'function') {
|
|
@@ -3130,6 +3231,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3130
3231
|
/* 专注计时器:脚本与测试都能驱动 */
|
|
3131
3232
|
startFocus,
|
|
3132
3233
|
stopFocus,
|
|
3234
|
+
saySummary,
|
|
3235
|
+
get today() { return { ...watchToday() } },
|
|
3133
3236
|
get focus() { return focusState() },
|
|
3134
3237
|
openAbout: () => aboutDialog(),
|
|
3135
3238
|
openActions: () => actionsDialog(),
|
|
@@ -3260,11 +3363,19 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3260
3363
|
},
|
|
3261
3364
|
streak: streakOf(memory.days),
|
|
3262
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,
|
|
3263
3373
|
/* 专注:不用猜"她为什么一直站在干活中" */
|
|
3264
3374
|
focus: focusState(),
|
|
3265
3375
|
/* 定时器是否挂着;页面藏起来时三个都该是 false */
|
|
3266
3376
|
timers: {
|
|
3267
|
-
poll: pollHandle !== null,
|
|
3377
|
+
poll: pollHandle !== null,
|
|
3378
|
+
pollMs: paused ? HIDDEN_POLL_MS : cfg.pollMs, chatter: chatterHandle !== null,
|
|
3268
3379
|
wander: wanderHandle !== null, paused,
|
|
3269
3380
|
},
|
|
3270
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
|
}
|