dsh-wisp 1.42.0 → 1.44.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 +84 -3
- package/cordis.patch.yml +5 -0
- package/lib/client.js +192 -15
- package/lib/client.template.js +192 -15
- package/lib/index.js +118 -16
- package/package.json +1 -1
- package/verify-wisp.mjs +313 -19
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
4
4
|
|
|
5
|
-
**当前版本 `1.
|
|
5
|
+
**当前版本 `1.44.0`** · 零依赖 · 单文件客户端半包(精灵图内嵌为 data URI)
|
|
6
6
|
|
|
7
7
|
> 非官方插件,与 DeepSeek(深度求索)官方无关。角色形象与图片许可见 [NOTICE.md](NOTICE.md)。
|
|
8
8
|
|
|
@@ -78,7 +78,7 @@ node verify-wisp.mjs # 预检;exit 0 = 两半包都符合契约
|
|
|
78
78
|
3. 产物**不得调用被陷阱的全局**(`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require`);
|
|
79
79
|
4. `const VERSION` 必须与 `package.json` 的 `version` 一致(防止版本漂移)。
|
|
80
80
|
|
|
81
|
-
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **
|
|
81
|
+
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **685 项全 PASS,exit 0**(**这一行由预检自己核对** —— 数字对不上就红;顶部的版本号同样由它核对)。
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -515,13 +515,94 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
515
515
|
2. 或对本包目录重新 `npm i` / `npm pack` 后安装;
|
|
516
516
|
3. 更新后重载页面 —— 她会自己发现版本变了并说一句。
|
|
517
517
|
|
|
518
|
+
**两个源都可以换**(1.43.0 起写进文档):宿主半包读 `config.urls`,所以镜像或私有
|
|
519
|
+
registry 只要在 patch 行里指过去就行。国内常见配置:
|
|
520
|
+
|
|
521
|
+
```yaml
|
|
522
|
+
- insert:
|
|
523
|
+
- id: wisp
|
|
524
|
+
name: "dsh-wisp"
|
|
525
|
+
config:
|
|
526
|
+
urls:
|
|
527
|
+
npm: "https://registry.npmmirror.com/dsh-wisp/latest"
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
只给 `npm` 一个键也可以:没给的源仍用内置默认值(`registry.npmjs.org` 和
|
|
531
|
+
`raw.githubusercontent.com/.../main/package.json`)。预检里有一条断言盯着"换了源
|
|
532
|
+
就真的只去问新地址",免得"支持镜像"只是文档里的一句话。
|
|
533
|
+
|
|
534
|
+
**她只报告,不安装**:宿主半包在 vm 沙箱里,没有网络、没有文件系统,"装"这件事
|
|
535
|
+
做不了 —— 所以 `canSelfUpdate` 恒为 `false`,`why` 里写明原因。
|
|
536
|
+
|
|
518
537
|
### 预检里的守卫
|
|
519
538
|
|
|
520
539
|
`WHATS_NEW` 必须写着**当前版本**,否则预检红 —— 这样"这一版做了什么"不会静默过期。
|
|
521
|
-
另外 `canSelfUpdate === false`、`hint`
|
|
540
|
+
另外 `canSelfUpdate === false`、`hint` 里必须含包名或仓库地址、`why` 里必须说明沙箱原因,
|
|
541
|
+
都有断言盯着。
|
|
522
542
|
|
|
523
543
|
## 变更
|
|
524
544
|
|
|
545
|
+
### 1.43.2
|
|
546
|
+
|
|
547
|
+
**台词只说人话;"为什么查不成"排进弹窗。**
|
|
548
|
+
|
|
549
|
+
1.43.1 把诊断直接拼进了台词 —— 真机上的效果是一句话后面挂一串
|
|
550
|
+
`ctx.get("web")→undefined; ctx.web→undefined`,读起来像乱码(用户的原话:
|
|
551
|
+
"还说一串英文")。现在:
|
|
552
|
+
|
|
553
|
+
- **台词回到一句人话**:「这个壳里我没有联网的通道,查不了更新。」
|
|
554
|
+
- **技术细节排进「关于她」弹窗**:取服务的方式、拿到的东西是什么形状、兜底通道
|
|
555
|
+
为什么也不行 —— 查失败时自动展开,可以看、可以复制、可以追问。
|
|
556
|
+
|
|
557
|
+
预检 680 → **682 项**,其中一条断言"台词里不许出现英文碎片"(失败台词与弹窗分工,
|
|
558
|
+
以后再有人往句子里塞 `ctx.get(...)` 就会红)。
|
|
559
|
+
|
|
560
|
+
### 1.43.1
|
|
561
|
+
|
|
562
|
+
**检查更新在"真的拿不到 web 服务"的壳里也能用了 —— 而且失败时说得清为什么。**
|
|
563
|
+
|
|
564
|
+
背景:1.43.0 装好、宿主半包也重启之后,她的原话仍是
|
|
565
|
+
「我是 1.43.0。这个壳里我没有联网的通道,查不了更新。」—— 也就是说
|
|
566
|
+
`ctx.get('web')` 与 `ctx.web` **两条路都拿不到可用的服务**。于是做了两件事:
|
|
567
|
+
|
|
568
|
+
1. **加一条兜底通道**:`pluginManager.inspect('dsh-wisp')`。插件管理器本来就是
|
|
569
|
+
"安装之前去 registry 问这个包是什么版本"的那个人 —— 所以这条路走的是**应用自己**
|
|
570
|
+
的网络出口,还尊重用户配的镜像源,而且它是平台文档里写明的宿主服务
|
|
571
|
+
(`ctx.get('pluginManager')`)。web 服务不可用时自动走它,`diag.via` 会写
|
|
572
|
+
`pluginManager`。**它比插件自己去抓 packument 更接近"装的时候会装到哪一版"。**
|
|
573
|
+
2. **失败必须能追问**:`resolveWebService()` 现在返回 `{ service, how, why }`,
|
|
574
|
+
"为什么拿不到"会跟着失败台词一起说出来:
|
|
575
|
+
|
|
576
|
+
> 这个壳里我没有联网的通道,查不了更新。(诊断:通道=none;ctx.get("web")→undefined; ctx.web→undefined)
|
|
577
|
+
|
|
578
|
+
一句话就能定案,不需要谁再去开控制台。顺带把"有服务但没有 `fetch`"
|
|
579
|
+
(`web-has-no-fetch`)从"没有通道"里分出来 —— "平台没给服务"和"服务没暴露这个方法"
|
|
580
|
+
是两件事,下一步动作也不同。
|
|
581
|
+
|
|
582
|
+
预检 673 → **680 项**,其中一条是端到端的:**没有 web 服务、只有插件管理器时,
|
|
583
|
+
检查更新照样给出答案,并注明走的是哪条通道。**
|
|
584
|
+
|
|
585
|
+
### 1.43.0
|
|
586
|
+
|
|
587
|
+
**一批"真的用鼠标从头点了一遍"才发现的问题。**
|
|
588
|
+
|
|
589
|
+
| # | 问题 | 修法 |
|
|
590
|
+
|---|---|---|
|
|
591
|
+
| 1 | **拨完开关,子面板消失** —— 而代码注释里明确写着"拨个开关面板就消失是最烦人的交互之一" | `refreshSurfaces()` 重画前先记下 `pinned` 再原样传下去(重画的 DOM 里鼠标并没有真的悬停过,"悬停态"重开必然被宽限期关掉) |
|
|
592
|
+
| 2 | **大小回不到出厂 250%** —— 区间 0.4–8、步长 0.25,`2.5` 落在 `0.4+0.25k` 格点外(最近 2.4 / 2.65) | 步长改 0.1(`0.4 + 21×0.1 = 2.5` ✓),并加断言:**默认值必须能从 lo 走整数步到达** |
|
|
593
|
+
| 3 | **更新提示语写死"按包名安装"** —— 新版只在 GitHub 上时那句话会把人送回旧版 | 她按 `from`/`sources` 判断:只有 GitHub 更新时改口,并把**仓库地址**放进剪贴板(`doctor().update.hint` 能看到用了哪条) |
|
|
594
|
+
| 4 | npm 源写死 `registry.npmjs.org`,镜像用户可能两个源一起废 | 宿主半包的 `config.urls` 本来就支持换源 —— 现在**写进文档**并加断言:换镜像真的只会去问新地址 |
|
|
595
|
+
| 5 | 触摸屏上手指划过也在驱动力姿势,抬手后姿势冻住 | 触摸一律归零(手指既不是"靠近"也不是"路过";该有的反馈是"按下压一下") |
|
|
596
|
+
| 6 | 指针移出窗口后她一直朝最后那个方向歪着 | `blur` 只覆盖"切走应用",补一个 document 级 `mouseleave` |
|
|
597
|
+
| 7 | 被边界夹住的拖动仍在"原地歪" | 位置没变就把姿势归零(她其实没动) |
|
|
598
|
+
| 8 | `motion: off` 与 `prefers-reduced-motion` **挡不住溜达** | CSS 关不掉 JS 写进去的坐标 —— 溜达调度器一起停 |
|
|
599
|
+
| 9 | 静止档下 "z" 停成满不透明,像一个卡住的字 | 静止版本给 0.5 透明度;减弱动画那档同样处理 |
|
|
600
|
+
| 10 | 脚底光晕挂在姿势层,她一倾斜连"地板"一起歪 | 移到 motion 层:跟着压扁(那是她压在地上),不跟着侧身 |
|
|
601
|
+
|
|
602
|
+
**评估后决定不改的一条**:`.wisp-tab` 那颗迷你按钮是 56×56 的**可见胶囊**(有边框、底色、阴影),整块就是按钮本身 —— 命中区和用户看到的一致,不属于"透明边距吃点击"那一类。
|
|
603
|
+
|
|
604
|
+
预检 654 → **673 项**:上面每一条都配了"再犯就会露出来"的断言,包括"拨开关后面板还在""默认值可达""静止档真的不动"这三条行为级的。
|
|
605
|
+
|
|
525
606
|
### 1.42.0
|
|
526
607
|
|
|
527
608
|
**查更新失败时,她会直接说清是哪一种失败。**
|
package/cordis.patch.yml
CHANGED
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
# size render scale, 0.4–8 (默认 4 → 560x840 CSS px)
|
|
31
31
|
# motion 'full' | 'subtle' | 'off' (默认 'full')
|
|
32
32
|
#
|
|
33
|
+
# 更新检查的两个源也可以换(宿主半包读 config.urls)—— 国内镜像:
|
|
34
|
+
# urls:
|
|
35
|
+
# npm: "https://registry.npmmirror.com/dsh-wisp/latest"
|
|
36
|
+
# 没给的源仍用内置默认值;只给 npm 一个键也是合法的。
|
|
37
|
+
#
|
|
33
38
|
# 菜单里的「看看余额」**不需要任何配置或 API key**:余额由平台自己的账户服务
|
|
34
39
|
# 提供(宿主半包的 optional inject: deepseekAccount),凭据留在平台手里。
|
|
35
40
|
#
|
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.44.0'
|
|
55
55
|
|
|
56
56
|
/* 每一版一句话。**必须写当前版本** —— 预检里有一条断言盯着它,
|
|
57
57
|
忘了更新就会红,不会静默过期。 */
|
|
@@ -86,6 +86,12 @@ const WHATS_NEW = {
|
|
|
86
86
|
'1.40.0': '她的透明处真的点不到了:按轮廓命中,站在按钮上也不挡你点它。',
|
|
87
87
|
'1.41.0': '检查更新更耐得住:一个源连不上不再把另一个拖死,失败时 doctor() 会说清是哪个源、为什么。',
|
|
88
88
|
'1.42.0': '查更新失败时她会直接说清是哪一种:没通道 / 连不上 / 宿主没应答。',
|
|
89
|
+
'1.43.0': '一批"真的用鼠标点过一遍"才发现的问题:拨完开关面板不再消失、大小能调回出厂值、触摸与指针离开不再留姿势、静止档真的会停。',
|
|
90
|
+
'1.43.1': '检查更新多了一条兜底通道(问插件管理器),并且失败时会直接说出"为什么拿不到"。',
|
|
91
|
+
'1.43.2': '台词只说人话;查不成的原因排进「关于她」弹窗,不再往句子里塞英文碎片。',
|
|
92
|
+
'1.44.0': '查更新改成客户端自己抓(bundle 插件没有 host RPC 座位)—— 这才是它一直缺的那条路。',
|
|
93
|
+
'1.43.4': '把查更新失败时的诊断临时写进窗口标题,供定位用。',
|
|
94
|
+
'1.43.3': '把"没接上宿主"和"宿主没有网络通道"分成两句 —— 断在哪一环,一句话就看得出来。',
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
/* 更新这件事**这个插件自己做不到**,如实写在代码里,免得以后有人以为能:
|
|
@@ -95,6 +101,11 @@ const WHATS_NEW = {
|
|
|
95
101
|
所以这里只做三件真能做的事:报版本、说她更新了、把包名复制给你去 DSH 里装。 */
|
|
96
102
|
const SEEN_VERSION_KEY = 'dsh-wisp:seen-version:v1'
|
|
97
103
|
const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
104
|
+
/* 但"按包名安装"只在 npm 上已经有这一版时成立。npm 暂存(或还没发)而 GitHub
|
|
105
|
+
已经更新时,照那句话装回来的是**旧版** —— 她得改口,并复制仓库地址而不是包名。
|
|
106
|
+
她本来就知道这个版本号是从哪个源读到的(updateCheck.from / sources)。 */
|
|
107
|
+
const UPDATE_REPO = 'https://github.com/969246694/dsh-wisp'
|
|
108
|
+
const UPDATE_HINT_GITHUB = 'npm 上还是旧版,用这个地址装 dsh-wisp:' + UPDATE_REPO
|
|
98
109
|
|
|
99
110
|
/* ---- sprites: 256x384 WebP, transparent, inlined by build.mjs ---- */
|
|
100
111
|
const SPRITES = {
|
|
@@ -467,6 +478,11 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
467
478
|
「宿主那边没应答」是三件事,含糊地说成"没查到"就没人能追下去 ——
|
|
468
479
|
而用户看到的恰恰只有那一句。 */
|
|
469
480
|
updateNoChannel: ['这个壳里我没有联网的通道,查不了更新。', '这里没有能出去的网络通道。'],
|
|
481
|
+
/* 两种"没通道"必须分开说 —— 它们是链路上**不同的一环**断了:
|
|
482
|
+
· 没接上宿主半包(客户端这一侧连 `host.call` 都没有)
|
|
483
|
+
· 宿主跑了,但它拿不到 `web` 服务
|
|
484
|
+
合并成一句话的代价是:谁也说不清断在哪一环(这个坑真踩过)。 */
|
|
485
|
+
updateNoSeat: ['我没接上宿主半包,查不了更新。', '宿主那边没接上,查不了更新。'],
|
|
470
486
|
updateNoHost: ['宿主那边没应答,更新没查成。', '问不到宿主,更新没查成。'],
|
|
471
487
|
milestone: [
|
|
472
488
|
'今天第 {n} 轮了。', '{n} 轮了,还不错。', '已经 {n} 轮了,你挺能撑。',
|
|
@@ -526,6 +542,7 @@ const UPDATE_HINT = '在 DSH 的插件列表里按包名安装:dsh-wisp'
|
|
|
526
542
|
streak: ['{n} days in a row.', 'Day {n} together.'],
|
|
527
543
|
updateFailed: ["The update check didn't work.", "Couldn't reach the update sources."],
|
|
528
544
|
updateNoChannel: ["This shell gives me no network channel, so I can't check for updates.", 'No way out to the network from here.'],
|
|
545
|
+
updateNoSeat: ["I'm not connected to my host half, so I can't check for updates.", 'No host half attached.'],
|
|
529
546
|
updateNoHost: ["The host side didn't answer — the update check failed.", 'No answer from the host.'],
|
|
530
547
|
balanceChecking: ['One moment…', 'Let me check.'],
|
|
531
548
|
balance: ['Balance: {n}.', "You've got {n}.", '{n} on the account.'],
|
|
@@ -948,6 +965,8 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
948
965
|
.wisp-root[data-motion="off"]{--wisp-amp:0}
|
|
949
966
|
/* 静止档:连呼吸和 z 都停 —— 她说"我不想让她动"时,是最彻底的静止。 */
|
|
950
967
|
.wisp-root[data-motion="off"] .wisp-body,.wisp-root[data-motion="off"] .wisp-zzz{animation:none}
|
|
968
|
+
/* 但"不动"不等于"冻住":动画一关,z 会停在满不透明的初始态,看起来像一个卡住的字 */
|
|
969
|
+
.wisp-root[data-motion="off"] .wisp-zzz{opacity:.5}
|
|
951
970
|
.wisp-root[data-accent="press"] .wisp-motion{animation:wisp-press .2s ease-out}
|
|
952
971
|
.wisp-root[data-accent="land"] .wisp-motion{animation:wisp-land .56s cubic-bezier(.2,1.15,.35,1)}
|
|
953
972
|
.wisp-root[data-accent="pop-a"] .wisp-motion{animation:wisp-pop-a .38s ease-out}
|
|
@@ -1033,7 +1052,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1033
1052
|
.wisp-credits{font-size:11px;opacity:.8;margin:6px 0 0}
|
|
1034
1053
|
.wisp-dialog-action{display:block;width:100%;margin:4px 0 2px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l1,var(--wisp-say-line));border-radius:8px;background:none;color:inherit;font:inherit;cursor:pointer}
|
|
1035
1054
|
.wisp-dialog-action:hover{background:var(--dsw-alias-bg-layer-2,rgba(120,170,255,.2))}
|
|
1036
|
-
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}.wisp-motion{animation:none!important}.wisp-lean{transform:none!important;transition:none!important}.wisp-menu,.wisp-submenu,.wisp-switch,.wisp-switch i,.wisp-slider-knob,.wisp-menu-item,.wisp-corner{animation:none!important;transition:none!important}}`
|
|
1055
|
+
@media (prefers-reduced-motion:reduce){.wisp-body{animation:none!important}.wisp-motion{animation:none!important}.wisp-zzz{animation:none!important;opacity:.5}.wisp-lean{transform:none!important;transition:none!important}.wisp-menu,.wisp-submenu,.wisp-switch,.wisp-switch i,.wisp-slider-knob,.wisp-menu-item,.wisp-corner{animation:none!important;transition:none!important}}`
|
|
1037
1056
|
|
|
1038
1057
|
const installStyles = (styles) => {
|
|
1039
1058
|
if (styles && typeof styles.insert === 'function') {
|
|
@@ -1658,7 +1677,11 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
1658
1677
|
|
|
1659
1678
|
/* 结构:body(情绪动画)> motion(一次性动作)> lean(姿势)> shine/img,
|
|
1660
1679
|
外加一个兄弟节点 hit(命中层)。 */
|
|
1661
|
-
|
|
1680
|
+
/* 光晕(脚底那圈光)挂在 motion 层、**不在 lean 层**:它是地面上的东西,
|
|
1681
|
+
不该跟着她的侧身一起歪 —— 挂在 lean 里的时候,她一倾斜光圈也跟着转,
|
|
1682
|
+
看起来像地板歪了。但按下/落地那一下的压扁它该跟着(那是"她压在地上"),
|
|
1683
|
+
所以留在 motion 层而不是提到 body 层。 */
|
|
1684
|
+
motionEl.appendChild(shine)
|
|
1662
1685
|
leanEl.appendChild(img)
|
|
1663
1686
|
motionEl.appendChild(leanEl)
|
|
1664
1687
|
bodyEl.appendChild(motionEl)
|
|
@@ -2373,6 +2396,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2373
2396
|
let wanderHandle = null
|
|
2374
2397
|
const syncWander = () => {
|
|
2375
2398
|
if (wanderHandle) { clock.cancel(wanderHandle); wanderHandle = null }
|
|
2399
|
+
/* 溜达也是"动作":`motion: off` 和 prefers-reduced-motion 都该让她待在原地。
|
|
2400
|
+
之前只关了 CSS 动画,她照样会自己滑走 —— 那是最容易被忽略的一种"没停下来",
|
|
2401
|
+
因为看起来只是一次正常的挪动。(CSS 关不掉 JS 写进去的坐标。) */
|
|
2402
|
+
if (cfg.motion === 'off' || reduced()) return
|
|
2376
2403
|
if (!cfg.wander || cfg.wanderRange <= 0 || paused) return
|
|
2377
2404
|
wanderHandle = clock.every(cfg.wanderMs, () => {
|
|
2378
2405
|
if (destroyed || hidden || mood !== 'idle' || start !== null) return
|
|
@@ -2481,8 +2508,17 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2481
2508
|
const dx = e.clientX - start.x, dy = e.clientY - start.y
|
|
2482
2509
|
if (!moved && Math.sqrt(dx * dx + dy * dy) > 4) moved = true
|
|
2483
2510
|
if (!moved) return
|
|
2511
|
+
const beforeX = coords.x, beforeY = coords.y
|
|
2484
2512
|
coords.x = clamp(origin.x + dx, 0, maxX())
|
|
2485
2513
|
coords.y = clamp(origin.y + dy, 0, maxY())
|
|
2514
|
+
/* 被边界夹住时她其实**没有动** —— 这时候再按指针位移去倾,就是"原地歪",
|
|
2515
|
+
看起来像坏了。位置没变就把姿势归零(指针继续拖也不影响,因为下一帧
|
|
2516
|
+
只要真的动了就会重新算)。 */
|
|
2517
|
+
if (coords.x === beforeX && coords.y === beforeY) {
|
|
2518
|
+
lastDragX = e.clientX
|
|
2519
|
+
setTilt(0)
|
|
2520
|
+
return
|
|
2521
|
+
}
|
|
2486
2522
|
/* 拖动时她朝**运动的反方向**倾:被拎着走的东西,顶是往后倒的。
|
|
2487
2523
|
只看"这一次事件走了多远"就等价于速度,不用存历史、不用积分 ——
|
|
2488
2524
|
而且它天然跟着指针事件的频率走,慢拖就是小角度。 */
|
|
@@ -2569,6 +2605,10 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2569
2605
|
if (destroyed || paused || hidden || start !== null) return
|
|
2570
2606
|
if (mood === 'sleep') return
|
|
2571
2607
|
if (cfg.motion === 'off' || reduced()) return
|
|
2608
|
+
/* 触摸屏上"指针"就是手指:手指划过时让她整只歪过去是错的 ——
|
|
2609
|
+
那既不是"靠近"也不是"路过",而且手指一抬起姿势就冻在那儿。
|
|
2610
|
+
所以触摸一律归零(真正该有的反馈是"按下压一下",那是另一条通道)。 */
|
|
2611
|
+
if (e.pointerType === 'touch') { setTilt(0); return }
|
|
2572
2612
|
if (typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return
|
|
2573
2613
|
const dx = e.clientX - (coords.x + boxW() / 2)
|
|
2574
2614
|
const dy = e.clientY - (coords.y + boxH() / 2)
|
|
@@ -2579,9 +2619,16 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2579
2619
|
}
|
|
2580
2620
|
window.addEventListener('pointermove', leanToPointer, { passive: true })
|
|
2581
2621
|
/* 指针移出窗口、或者你切走应用之后,不会再有点事件来把它归零 ——
|
|
2582
|
-
没有这一下,她会一直朝着最后那个位置歪着。
|
|
2622
|
+
没有这一下,她会一直朝着最后那个位置歪着。
|
|
2623
|
+
`blur` 只覆盖"切走应用",**指针移出窗口不会触发它** —— 补一个文档级
|
|
2624
|
+
mouseleave(指针离开文档时才发),两条一起才真的覆盖到。 */
|
|
2583
2625
|
const resetTilt = () => setTilt(0)
|
|
2584
2626
|
window.addEventListener('blur', resetTilt)
|
|
2627
|
+
try {
|
|
2628
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
2629
|
+
document.addEventListener('mouseleave', resetTilt)
|
|
2630
|
+
}
|
|
2631
|
+
} catch (e) { /* 没有 document:她最多保留最后一次侧身,不影响其它功能 */ }
|
|
2585
2632
|
|
|
2586
2633
|
/* 键盘入口:Enter / Space / 菜单键 / Shift+F10 打开菜单。
|
|
2587
2634
|
Space 默认会滚动页面,Enter 在表单里会提交 —— 都要拦。 */
|
|
@@ -2621,7 +2668,11 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2621
2668
|
只认**带修饰键**的滚轮:她占地不小,直接吞掉普通滚轮等于在她身上把页面滚动禁掉。
|
|
2622
2669
|
带修饰键是「缩放」的通行手势,此时 preventDefault 才是帮忙而不是捣乱。
|
|
2623
2670
|
步长 0.25,上下限由 CONFIG_SPEC 统一夹紧(0.4–8),不在这里再写一遍。 */
|
|
2624
|
-
|
|
2671
|
+
/* 步长必须是"默认值可达"的:size 的区间是 0.4–8,默认 2.5。
|
|
2672
|
+
原来键盘/滚轮步长 0.25,可达值只有 0.4 + 0.25k —— **2.5 不在格点上**
|
|
2673
|
+
(最近的是 2.4 / 2.65),于是用户只要动过一次滑块,就再也调不回出厂大小。
|
|
2674
|
+
0.1 让 2.5 正好落在格点上(0.4 + 21×0.1)。预检里有一条守着这个不变式。 */
|
|
2675
|
+
const WHEEL_STEP = 0.1
|
|
2625
2676
|
const onWheel = (ev) => {
|
|
2626
2677
|
if (!ev.ctrlKey && !ev.metaKey) return // 普通滚轮原样放行
|
|
2627
2678
|
if (typeof ev.preventDefault === 'function') ev.preventDefault()
|
|
@@ -2932,12 +2983,17 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
2932
2983
|
子面板里的开关点完必须留在原地 —— 拨个开关面板就消失是最烦人的交互之一。 */
|
|
2933
2984
|
const refreshSurfaces = () => {
|
|
2934
2985
|
const keep = flyoutName
|
|
2986
|
+
/* 重画之前先记住"这个面板是点开的还是悬停开的":openMenu 里的 closeFlyout()
|
|
2987
|
+
会把标记清掉,不记下来,拨一次开关后面板就从"钉住"降级成"悬停开启" ——
|
|
2988
|
+
而重画出来的 DOM 里鼠标并没有真的悬停过,宽限期一到面板就自己关了。
|
|
2989
|
+
用户看到的现象就是:拨个开关,面板消失(而这正是这段注释上面说不能有的)。 */
|
|
2990
|
+
const keepPinned = flyoutPinned
|
|
2935
2991
|
const keepIndex = flyoutRow && flyoutRow.dataset ? Number(flyoutRow.dataset.rowIndex) : -1
|
|
2936
2992
|
openMenu(menuAtX, menuAtY, false) // 重画菜单(会先关掉面板)
|
|
2937
2993
|
if (!keep || !menuEl) return
|
|
2938
2994
|
const row = menuEl.querySelectorAll('.wisp-menu-item')
|
|
2939
2995
|
.find((el) => el.dataset && el.dataset.group === keep)
|
|
2940
|
-
if (row) openFlyout(keep, row, Number.isFinite(keepIndex) && keepIndex >= 0 ? keepIndex : Number(row.dataset.rowIndex))
|
|
2996
|
+
if (row) openFlyout(keep, row, Number.isFinite(keepIndex) && keepIndex >= 0 ? keepIndex : Number(row.dataset.rowIndex), keepPinned)
|
|
2941
2997
|
}
|
|
2942
2998
|
|
|
2943
2999
|
const openFlyout = (name, rowEl, index, pinned) => {
|
|
@@ -3217,7 +3273,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3217
3273
|
])
|
|
3218
3274
|
}
|
|
3219
3275
|
|
|
3220
|
-
const aboutDialog = () => {
|
|
3276
|
+
const aboutDialog = (diagRes) => {
|
|
3221
3277
|
const note = WHATS_NEW[VERSION]
|
|
3222
3278
|
/* 菜单里那条「版本与更新」已经收掉了(版本号现在直接显示在菜单头部),
|
|
3223
3279
|
所以检查更新必须有新的入口 —— 放在这里,上下文正合适。 */
|
|
@@ -3237,9 +3293,12 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3237
3293
|
para('她还能陪你专注计时:菜单里开一段 25 或 45 分钟,期间她保持"干活中",戳她一下是问还剩多久,到点她替你收工。'),
|
|
3238
3294
|
para('这一版:' + (note || VERSION)),
|
|
3239
3295
|
para('形象与设定参考社区项目 Neko3000/deepseek-whalechan(设定文档 CC BY-NC-SA 4.0,原作者 B站 ZipZipPipe、上善无形)。'),
|
|
3296
|
+
/* 上一次检查更新失败时,把宿主看到的东西摆在最显眼的位置 —— 台词只有一句,
|
|
3297
|
+
而"为什么拿不到"必须能被看到、被复制、被追问。 */
|
|
3298
|
+
diagRes ? para('上次查更新为什么没成:\n' + describeDiag(diagRes)) : null,
|
|
3240
3299
|
checkBtn,
|
|
3241
3300
|
dialogFoot('非官方同人作品,与 DeepSeek 官方无关', 'v' + VERSION),
|
|
3242
|
-
])
|
|
3301
|
+
].filter(Boolean))
|
|
3243
3302
|
}
|
|
3244
3303
|
|
|
3245
3304
|
/* 菜单的结构 ------------------------------------------------------------
|
|
@@ -3532,7 +3591,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3532
3591
|
if (focused && focused.dataset && focused.dataset.slider === '1'
|
|
3533
3592
|
&& (ev.key === 'ArrowLeft' || ev.key === 'ArrowRight')) {
|
|
3534
3593
|
if (typeof ev.preventDefault === 'function') ev.preventDefault()
|
|
3535
|
-
const step = ev.shiftKey ? 1 : 0.
|
|
3594
|
+
const step = ev.shiftKey ? 1 : 0.1
|
|
3536
3595
|
applyConfig({ size: cfg.size + (ev.key === 'ArrowRight' ? step : -step) })
|
|
3537
3596
|
/* 只更新滑块自身,不重画整张菜单 —— 焦点和展开状态都不动 */
|
|
3538
3597
|
try {
|
|
@@ -3749,6 +3808,9 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3749
3808
|
|
|
3750
3809
|
host 是 shell 注入的名字,不是全局:拿不到就如实说"查不了",不许抛。 */
|
|
3751
3810
|
let updateCheck = { state: 'idle', at: 0 }
|
|
3811
|
+
/* 上一次真正复制给用户的提示语(doctor 里能看到)—— "她让你装的是哪个" 必须可查,
|
|
3812
|
+
否则"按包名安装"和"用 GitHub 地址装"这两条路到底走了哪条就没人说得清。 */
|
|
3813
|
+
let lastUpdateHint = UPDATE_HINT
|
|
3752
3814
|
|
|
3753
3815
|
const compareVersions = (a, b) => {
|
|
3754
3816
|
const parse = (v) => String(v).split('-')[0].split('.').map((n) => {
|
|
@@ -3774,12 +3836,58 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3774
3836
|
return null
|
|
3775
3837
|
}
|
|
3776
3838
|
|
|
3839
|
+
/* 客户端**自己的**通道 —— 这才是这个插件真正的联网方式。
|
|
3840
|
+
|
|
3841
|
+
事实(从平台的 dsh-client-modules 加载器读出来的):bundle 插件的客户端半包
|
|
3842
|
+
是用 <script> 加载的**普通页面脚本**,平台不注入陷阱、不替换全局 —— 所以
|
|
3843
|
+
`window.fetch` 对它可用。而 `host.call` 那个 RPC 座位只发给**动态插件**
|
|
3844
|
+
(runner 里 `hasHostHalf: definition.hostCode !== undefined`),我们这个形态
|
|
3845
|
+
从来没有过它。
|
|
3846
|
+
|
|
3847
|
+
以前代码里写着"浏览器半包的 fetch 是陷阱",那是把**预检替身**的契约当成了
|
|
3848
|
+
平台契约 —— 整个更新检查因此在错误的路上走了很久(真机上永远"没接上宿主")。 */
|
|
3849
|
+
const NPM_LATEST_URL = 'https://registry.npmjs.org/dsh-wisp/latest'
|
|
3850
|
+
const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/969246694/dsh-wisp/main/package.json'
|
|
3851
|
+
const clientVersionAt = (url) => {
|
|
3852
|
+
try {
|
|
3853
|
+
const fetcher = window.fetch
|
|
3854
|
+
if (typeof fetcher !== 'function') return Promise.resolve(null)
|
|
3855
|
+
return fetcher.call(window, url, { cache: 'no-store' })
|
|
3856
|
+
.then((res) => (res && res.ok === true ? res.json() : null))
|
|
3857
|
+
.then((data) => (data && typeof data.version === 'string' ? data.version : null))
|
|
3858
|
+
.catch(() => null)
|
|
3859
|
+
} catch (e) { return Promise.resolve(null) }
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
/* 客户端侧的完整检查:两个源各取一次,取更高的那个(与宿主侧同一套规则)。 */
|
|
3863
|
+
const clientSideCheck = () => {
|
|
3864
|
+
updateCheck = { state: 'checking', at: nowMs() }
|
|
3865
|
+
return Promise.all([clientVersionAt(NPM_LATEST_URL), clientVersionAt(GITHUB_RAW_URL)])
|
|
3866
|
+
.then(([npmVersion, githubVersion]) => {
|
|
3867
|
+
const sources = {
|
|
3868
|
+
npm: npmVersion ? { ok: true, version: npmVersion } : { ok: false, reason: 'fetch-failed' },
|
|
3869
|
+
github: githubVersion ? { ok: true, version: githubVersion } : { ok: false, reason: 'fetch-failed' },
|
|
3870
|
+
}
|
|
3871
|
+
const cands = [['npm', npmVersion], ['github', githubVersion]].filter((p) => typeof p[1] === 'string')
|
|
3872
|
+
if (cands.length === 0) {
|
|
3873
|
+
updateCheck = { state: 'failed', reason: 'fetch-failed', sources, diag: null, at: nowMs() }
|
|
3874
|
+
publishDiagToTitle(updateCheck)
|
|
3875
|
+
return updateCheck
|
|
3876
|
+
}
|
|
3877
|
+
let best = cands[0]
|
|
3878
|
+
for (const c of cands) if (compareVersions(best[1], c[1]) < 0) best = c
|
|
3879
|
+
updateCheck = compareVersions(VERSION, best[1]) < 0
|
|
3880
|
+
? { state: 'available', latest: best[1], from: best[0], sources, at: nowMs() }
|
|
3881
|
+
: { state: 'current', latest: best[1], from: best[0], sources, at: nowMs() }
|
|
3882
|
+
return updateCheck
|
|
3883
|
+
})
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3777
3886
|
const checkForUpdate = () => {
|
|
3778
3887
|
const seat = hostSeat()
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
}
|
|
3888
|
+
/* 没有 host 座位不是失败 —— 那是**这个形态的插件的常态**(bundle 插件没有 host RPC)。
|
|
3889
|
+
客户端自己抓:这才是它本来该走的路。 */
|
|
3890
|
+
if (seat === null) return clientSideCheck()
|
|
3783
3891
|
updateCheck = { state: 'checking', at: nowMs() }
|
|
3784
3892
|
return Promise.resolve()
|
|
3785
3893
|
.then(() => seat.call('checkUpdate', { current: VERSION }))
|
|
@@ -3792,8 +3900,11 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3792
3900
|
state: 'failed',
|
|
3793
3901
|
reason: (res && res.reason) || 'no-result',
|
|
3794
3902
|
sources: (res && res.sources) || null,
|
|
3903
|
+
/* 宿主看到的诊断也要留下来 —— 失败台词会把它念出来,doctor 里也能查。 */
|
|
3904
|
+
diag: (res && res.diag) || null,
|
|
3795
3905
|
at: nowMs(),
|
|
3796
3906
|
}
|
|
3907
|
+
publishDiagToTitle(updateCheck)
|
|
3797
3908
|
} else if (compareVersions(VERSION, res.latest) < 0) {
|
|
3798
3909
|
/* from / sources 一起留着:两个源不一致时(npm 暂存、GitHub 已更新),
|
|
3799
3910
|
"这个数字是哪儿来的"比数字本身更需要能查。 */
|
|
@@ -3895,17 +4006,73 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3895
4006
|
})
|
|
3896
4007
|
}
|
|
3897
4008
|
|
|
4009
|
+
/* 版本号来自 GitHub,而 npm 上那份**不是同一个号**(通常是 npm 暂存或还没发)——
|
|
4010
|
+
这时候"按包名安装"会把人送回旧版。 */
|
|
4011
|
+
const githubOnlyNewer = (res) => {
|
|
4012
|
+
if (!res || res.state !== 'available' || res.from !== 'github') return false
|
|
4013
|
+
const npm = res.sources && res.sources.npm
|
|
4014
|
+
return !!(npm && npm.ok === true && npm.version !== res.latest)
|
|
4015
|
+
}
|
|
4016
|
+
|
|
3898
4017
|
const updateLine = (res) => {
|
|
3899
4018
|
if (res.state === 'available') return pick(LINES.updateAvailable) + res.latest
|
|
3900
4019
|
if (res.state === 'current') return pick(LINES.updateCurrent)
|
|
3901
4020
|
/* 失败要说清是哪一种:没有通道 / 连不上 / 宿主没应答。
|
|
3902
4021
|
`no-web-service` 是"这个壳根本没有联网入口",不是"网络不好"——
|
|
3903
4022
|
以前两者共用一句"没查到更新",于是诊断只能靠猜。 */
|
|
3904
|
-
|
|
4023
|
+
/* 失败要说清是哪一种 —— 它们是链路上**不同的一环**断了:
|
|
4024
|
+
`unsupported`:客户端这侧连 `host.call` 都没有(第一环);
|
|
4025
|
+
`no-web-service`:宿主跑了,但它拿不到 `web` 服务(第三环);
|
|
4026
|
+
`fetch-failed`:服务拿到了,两个源都连不上(第四环)。 */
|
|
4027
|
+
if (res.state === 'unsupported') return pick(LINES.updateNoSeat)
|
|
4028
|
+
if (res.reason === 'no-web-service') return pick(LINES.updateNoChannel)
|
|
3905
4029
|
if (res.reason === 'call-failed') return pick(LINES.updateNoHost)
|
|
3906
4030
|
return pick(LINES.updateFailed)
|
|
3907
4031
|
}
|
|
3908
4032
|
|
|
4033
|
+
/* 技术的部分**不进台词**(台词是给人读的一句话,塞进去就是一串英文碎片),
|
|
4034
|
+
但必须看得见 —— 失败时把宿主实际看到的排进「关于她」弹窗:
|
|
4035
|
+
取服务的方式、拿到的东西是什么形状、兜底通道为什么也不行。
|
|
4036
|
+
这是"她说查不了"和"我知道为什么"之间的那一步。 */
|
|
4037
|
+
/* 诊断的**临时出口**:把"宿主看见了什么"写进窗口标题。
|
|
4038
|
+
Agent 侧读一下窗口标题就能拿到(不用截图、不用控制台、不用用户复制)——
|
|
4039
|
+
这台机器上的断点已经卡了两轮,值得开一个临时通道把它钉死。
|
|
4040
|
+
120 秒后自动还原;定位到问题后这一条会撤掉。 */
|
|
4041
|
+
let diagTitleTimer = null
|
|
4042
|
+
const publishDiagToTitle = (res) => {
|
|
4043
|
+
try {
|
|
4044
|
+
if (typeof document === 'undefined' || !document || typeof document.title !== 'string') return
|
|
4045
|
+
const diag = (res && res.diag) || {}
|
|
4046
|
+
const short = [
|
|
4047
|
+
'wisp-diag',
|
|
4048
|
+
String((res && res.state) || '?'),
|
|
4049
|
+
String((res && res.reason) || '-'),
|
|
4050
|
+
'via=' + String(diag.via || '-'),
|
|
4051
|
+
'how=' + String(diag.how || '-'),
|
|
4052
|
+
'why=' + String(diag.why || '-'),
|
|
4053
|
+
'mgr=' + String(diag.manager || '-'),
|
|
4054
|
+
].join(' ')
|
|
4055
|
+
const original = document.title
|
|
4056
|
+
document.title = short
|
|
4057
|
+
if (diagTitleTimer !== null) clock.cancel(diagTitleTimer)
|
|
4058
|
+
diagTitleTimer = clock.timeout(() => {
|
|
4059
|
+
try { document.title = original } catch (e) { /* ignore */ }
|
|
4060
|
+
diagTitleTimer = null
|
|
4061
|
+
}, 120000)
|
|
4062
|
+
} catch (e) { /* 标题写不了不该影响任何别的功能 */ }
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
const describeDiag = (res) => {
|
|
4066
|
+
const diag = res && res.diag
|
|
4067
|
+
if (!diag || typeof diag !== 'object') return '这次连诊断信息都没有拿到。'
|
|
4068
|
+
const lines = ['通道:' + String(diag.via || '未知')]
|
|
4069
|
+
if (diag.how) lines.push('取服务的方式:' + String(diag.how))
|
|
4070
|
+
if (diag.why) lines.push('看到的东西:' + String(diag.why))
|
|
4071
|
+
if (diag.registry) lines.push('插件管理器的 registry:' + String(diag.registry))
|
|
4072
|
+
if (diag.manager) lines.push('兜底通道:' + String(diag.manager))
|
|
4073
|
+
return lines.join('\n')
|
|
4074
|
+
}
|
|
4075
|
+
|
|
3909
4076
|
/* 把包名复制到剪贴板,便于在 DSH 的「按插件包名安装」里粘贴。
|
|
3910
4077
|
没有剪贴板权限(或不是安全上下文)时**静默降级**成只说话 —— 不该因此报错。 */
|
|
3911
4078
|
const copyText = (text) => {
|
|
@@ -3925,6 +4092,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3925
4092
|
用户点这一项想知道的就是这两件事。 */
|
|
3926
4093
|
const showUpdate = () => {
|
|
3927
4094
|
const note = WHATS_NEW[VERSION]
|
|
4095
|
+
let hint = UPDATE_HINT
|
|
3928
4096
|
const copied = copyText('dsh-wisp')
|
|
3929
4097
|
/* 一步到位:先把"我是谁、这一版做了什么"说出来,同时去查有没有新版;
|
|
3930
4098
|
查回来再用一句话覆盖掉 —— 一次只冒一个气泡,所以是"说完结果就结束"。 */
|
|
@@ -3935,9 +4103,18 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
3935
4103
|
started.then((res) => {
|
|
3936
4104
|
if (destroyed) return
|
|
3937
4105
|
if (res.state === 'checking') return
|
|
4106
|
+
/* 新版只在 GitHub 上 → 改口 + 把仓库地址放进剪贴板(包名会装回旧版)。 */
|
|
4107
|
+
if (githubOnlyNewer(res)) {
|
|
4108
|
+
hint = UPDATE_HINT_GITHUB
|
|
4109
|
+
lastUpdateHint = hint
|
|
4110
|
+
copyText(UPDATE_REPO)
|
|
4111
|
+
}
|
|
3938
4112
|
flashHappy(base + updateLine(res))
|
|
4113
|
+
/* 查不成的时候:把"为什么"摆到弹窗里(台词只留一句人话)。 */
|
|
4114
|
+
if (res.state === 'failed') aboutDialog(res)
|
|
3939
4115
|
})
|
|
3940
4116
|
}
|
|
4117
|
+
lastUpdateHint = hint
|
|
3941
4118
|
return { version: VERSION, note: note || null, copied, checking: true }
|
|
3942
4119
|
}
|
|
3943
4120
|
|
|
@@ -4097,7 +4274,7 @@ body[data-ds-dark-theme] .wisp-layer{--wisp-say-fg:#eaf2f8;--wisp-say-bg:rgba(20
|
|
|
4097
4274
|
version: VERSION,
|
|
4098
4275
|
whatsNew: WHATS_NEW[VERSION] ?? null,
|
|
4099
4276
|
seenBefore: readStored(SEEN_VERSION_KEY),
|
|
4100
|
-
hint:
|
|
4277
|
+
hint: lastUpdateHint,
|
|
4101
4278
|
canSelfUpdate: false,
|
|
4102
4279
|
/* 上一次检查的结果(idle / checking / current / available / failed / unsupported) */
|
|
4103
4280
|
lastCheck: { ...updateCheck },
|