dsh-wisp 1.42.0 → 1.48.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 +666 -29
- package/assets/canon/attn.webp +0 -0
- package/assets/canon/eat.webp +0 -0
- package/assets/canon/happy.webp +0 -0
- package/assets/canon/idle.webp +0 -0
- package/assets/canon/poked.webp +0 -0
- package/assets/canon/proud.webp +0 -0
- package/assets/canon/sleepy.webp +0 -0
- package/assets/canon/work.webp +0 -0
- package/assets/classic/attn.webp +0 -0
- package/assets/classic/eat.webp +0 -0
- package/assets/classic/happy.webp +0 -0
- package/assets/classic/idle.webp +0 -0
- package/assets/classic/poked.webp +0 -0
- package/assets/classic/proud.webp +0 -0
- package/assets/classic/work.webp +0 -0
- package/assets/deepsea/attn.webp +0 -0
- package/assets/deepsea/eat.webp +0 -0
- package/assets/deepsea/happy.webp +0 -0
- package/assets/deepsea/idle.webp +0 -0
- package/assets/deepsea/poked.webp +0 -0
- package/assets/deepsea/proud.webp +0 -0
- package/assets/deepsea/sleepy.webp +0 -0
- package/assets/deepsea/work.webp +0 -0
- package/assets/motion/idle.webp +0 -0
- package/assets/motion/sleepy.webm +0 -0
- package/assets/motion/sleepy.webp +0 -0
- package/assets/motion/swim_attn.webp +0 -0
- package/assets/motion/swim_eat.webp +0 -0
- package/assets/motion/swim_happy.webp +0 -0
- package/assets/motion/swim_idle.webp +0 -0
- package/assets/motion/swim_poked.webp +0 -0
- package/assets/motion/swim_proud.webp +0 -0
- package/assets/motion/swim_sleepy.webp +0 -0
- package/assets/motion/swim_work.webp +0 -0
- package/assets/night/attn.webp +0 -0
- package/assets/night/eat.webp +0 -0
- package/assets/night/happy.webp +0 -0
- package/assets/night/idle.webp +0 -0
- package/assets/night/poked.webp +0 -0
- package/assets/night/proud.webp +0 -0
- package/assets/night/sleepy.webp +0 -0
- package/assets/night/work.webp +0 -0
- package/assets/pajama/attn.webp +0 -0
- package/assets/pajama/eat.webp +0 -0
- package/assets/pajama/happy.webp +0 -0
- package/assets/pajama/idle.webp +0 -0
- package/assets/pajama/poked.webp +0 -0
- package/assets/pajama/proud.webp +0 -0
- package/assets/pajama/sleepy.webp +0 -0
- package/assets/pajama/work.webp +0 -0
- package/assets/swim/attn.webp +0 -0
- package/assets/swim/eat.webp +0 -0
- package/assets/swim/happy.webp +0 -0
- package/assets/swim/idle.webp +0 -0
- package/assets/swim/poked.webp +0 -0
- package/assets/swim/proud.webp +0 -0
- package/assets/swim/sleepy.webp +0 -0
- package/assets/swim/work.webp +0 -0
- package/audit/generation-audit.jsonl +57 -0
- package/build.mjs +130 -8
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1014 -104
- package/lib/client.template.js +953 -65
- package/lib/index.js +311 -16
- package/package.json +1 -1
- package/tools/CHARACTER-PROMPT.md +65 -9
- package/tools/audit-log.mjs +11 -2
- package/tools/engine-probe.mjs +500 -2
- package/verify-wisp.mjs +1576 -51
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
4
4
|
|
|
5
|
-
**当前版本 `1.
|
|
5
|
+
**当前版本 `1.48.0`** · 零依赖 · 单文件客户端半包(精灵图内嵌为 data URI;帧动画素材自 1.47.0 起由宿主半包按 URL 发,客户端里只留一张文件名清单)
|
|
6
6
|
|
|
7
7
|
> 非官方插件,与 DeepSeek(深度求索)官方无关。角色形象与图片许可见 [NOTICE.md](NOTICE.md)。
|
|
8
8
|
|
|
@@ -12,12 +12,12 @@ DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
|
12
12
|
|
|
13
13
|
| 行为 | 触发条件 |
|
|
14
14
|
|---|---|
|
|
15
|
-
|
|
|
15
|
+
| 待机 | 站着时放 **`idle` 那一段帧动画**(`<img>` 动图 WebP,站着时那点极小的起伏),静态立绘这时退场。**「静止」档与系统的「减少动态效果」会把它藏起来、把画面交回静态立绘**(v1.46.5 起;在那之前站立是一张一动不动的立绘) |
|
|
16
16
|
| `idle` 青色 | 默认 |
|
|
17
17
|
| `alert` 琥珀色 | Agent 正在跑(出现「停止生成」按钮)**或** 输入框里有字 |
|
|
18
18
|
| `happy` 粉色 | 戳她一下 / 双击 / **一轮跑完** |
|
|
19
19
|
| `sleep` 灰色冒 `z` | 静置 `sleepAfterMs`(默认 90 秒);任意输入唤醒 |
|
|
20
|
-
| `attn` 举手招你过来 +
|
|
20
|
+
| `attn` 举手招你过来 + 暖红光晕 | **有东西在等你处理**:Agent 提问 / 计划待确认 / 工具授权待批准。优先于其余一切状态,睡着了也会被叫起来 |
|
|
21
21
|
| 她记着你 | 她会记住**一起多少天**、你常把她放在**哪个角**、常让她**穿哪套** —— 够格的时候说出来(第 2/7/14/30/100 天;某个角用过 ≥8 次且占多数;某套皮肤换过 ≥6 次)。**同一条一辈子只说一次、一天最多一句** |
|
|
22
22
|
| 今日里程碑 | 她数着**今天跑完多少轮**(跨刷新记着,按天归零):到 **10 / 25 / 50 / 100 / 200 / 500** 时会用得意的表情报出数字。同一个里程碑一天只说一次 |
|
|
23
23
|
| 久坐提醒 | 连续活动到 `careAfterMs`(默认 **90 分钟**)她会抬手指你一下、说一句叫你起来动动。中间停顿不到 90 秒不算离开;睡着时会被叫醒再提醒;`0` = 关 |
|
|
@@ -31,13 +31,15 @@ DeepSeek Harness Web 界面的浮动陪伴插件:**DeepSeek娘** 桌宠。
|
|
|
31
31
|
| 拖动 | 按住本体拖动,带边界约束;**松手即记住位置**,下次刷新回到原处 |
|
|
32
32
|
| 说话 | 挂载问候、点击、睡着、醒来、跑完各有一组台词 |
|
|
33
33
|
| 按轮廓命中 | 只有**她自己的像素**可点:命中层带一层由 alpha 蒙版生成的 `clip-path`,透明处整层穿透到下面的应用(hover / 点击 / 右键 / 滚轮全都不受影响)。抓着光标也只在她的轮廓上出现 |
|
|
34
|
-
| 看余额 | 菜单「行为 → **看看余额**」(或 `__wisp.checkBalance()`)。**按需查,不轮询** ——
|
|
34
|
+
| 看余额 | 菜单「行为 → **看看余额**」(或 `__wisp.checkBalance()`)。**按需查,不轮询** —— 稳态开销那条承诺不破。数字来自平台自己的账户服务(客户端走 `remote.account` Remote,动态插件形态走 host 座位),**插件从头到尾不碰凭据**;未登录 / 刚没读到 / 这个壳里没有该服务,是三句不同的话 |
|
|
35
35
|
| 按下有反馈 | 按住她 → 身体压一下再弹回来(**按下那一刻**就有反应,不用等松手) |
|
|
36
36
|
| 拖着有重量 | 拖动中她朝**运动的反方向**侧倾(最多 8°),松手时顿一下再站稳。无过渡跟手,松手才走弹簧 |
|
|
37
37
|
| 她会注意你的指针 | 指针进到 **170px** 以内,她朝指针方向侧身(最多 3.5°,越近越多),出圈回正。睡着时不跟 |
|
|
38
38
|
| 换表情会动 | `happy` / `poked` / `attn` 等有表情的状态切换时弹一下;**同一个表情再来一次也弹**(第二下不出声会被读成卡住) |
|
|
39
39
|
| 溜达有方向 | 自己踱步时朝行进方向微微前倾(最多 2.5°),像"往那边去"而不是"被平移过去" |
|
|
40
|
-
| 动作幅度可调 | 菜单「行为 → 动作幅度」三选一:**灵动 / 克制(半幅)/ 静止**。`静止`
|
|
40
|
+
| 动作幅度可调 | 菜单「行为 → 动作幅度」三选一:**灵动 / 克制(半幅)/ 静止**。`静止` 连呼吸、`z` 与帧动画一起停 |
|
|
41
|
+
| 帧动画开关 | 菜单「行为 → **帧动画**」一个开关:关掉后画面永远交给静态立绘,**其余动作照旧**(这是它和 `静止` 档的分工)。默认开;配置键 `frame` |
|
|
42
|
+
| 睡着时会动 | `sleep` 上叠另一段 **动图 WebP**(`<img>`,她睡着时被子那点极小的起伏)。两段素材(`idle` / `sleepy`)都是 720×1280、24fps、4.04 秒、97 帧一帧不抽;**换状态时同一个 `<img>` 换源**,不重建元素。**「静止」档与系统的「减少动态效果」会把动图藏起来、把画面交回静态立绘**,躲起来时也一样(动图没有 `pause()` 可喊) |
|
|
41
43
|
|
|
42
44
|
外层 `pointer-events:none`,只有**她自己的轮廓**可点:命中层带一层由 alpha 蒙版生成的
|
|
43
45
|
`clip-path`,所以透明处是真的穿透 —— 她站在哪个按钮上都不影响你点它(1.40.0 之前
|
|
@@ -52,11 +54,13 @@ dsh-wisp-plugin/
|
|
|
52
54
|
├── package.json dsh.bundle.patch + dsh.client.platform,零依赖
|
|
53
55
|
├── cordis.patch.yml 插入一行 host 入口(指向 dsh-wisp,绝不能是 dsh-wisp/client)
|
|
54
56
|
├── lib/
|
|
55
|
-
│ ├── index.js host
|
|
57
|
+
│ ├── index.js host 半包:检查更新 / 查余额两个 handler + 帧动画素材路由 /wisp-motion/
|
|
56
58
|
│ ├── client.template.js 浏览器半包源码(唯一需要编辑的文件)
|
|
57
59
|
│ └── client.js 由 build.mjs 生成,勿手改
|
|
58
|
-
├── assets/
|
|
59
|
-
├──
|
|
60
|
+
├── assets/ 六套皮肤 × 8 张 1024x1536 透明 WebP
|
|
61
|
+
│ ├── motion/ 帧动画素材(动图 WebP;由宿主半包的路由发给页面,不进客户端包体)
|
|
62
|
+
│ └── audio/ 音效(可选,内联)
|
|
63
|
+
├── build.mjs 注入精灵图 + 帧动画**清单** + 建造防线
|
|
60
64
|
├── verify-wisp.mjs 预检:两套契约 + 真实执行
|
|
61
65
|
├── NOTICE.md 角色来源与许可
|
|
62
66
|
└── README.md 本文件
|
|
@@ -65,20 +69,22 @@ dsh-wisp-plugin/
|
|
|
65
69
|
## 构建与预检
|
|
66
70
|
|
|
67
71
|
```bash
|
|
68
|
-
node build.mjs # 把 assets/*.webp
|
|
72
|
+
node build.mjs # 把 assets/**/*.webp(+ assets/motion/*.webp)注入 lib/client.template.js → lib/client.js
|
|
69
73
|
node verify-wisp.mjs # 预检;exit 0 = 两半包都符合契约
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
**改了 `client.template.js` 就必须重新 `build.mjs`** —— `client.js` 是生成物。
|
|
73
77
|
|
|
74
|
-
`build.mjs`
|
|
78
|
+
`build.mjs` 现在有五道防线,任何一道失败都拒绝写出文件:
|
|
75
79
|
|
|
76
|
-
1. 占位符 `__SPRITES_LITERAL__`
|
|
77
|
-
2. 注入后的 `SPRITES`
|
|
78
|
-
3.
|
|
79
|
-
|
|
80
|
+
1. 占位符 `__SPRITES_LITERAL__` / `__MOTION_LITERAL__` 必须各出现一次,且替换后都不得残留;
|
|
81
|
+
2. 注入后的 `SPRITES` 表要**真的被求值**,每个 key 都必须是 `data:image/` 开头;
|
|
82
|
+
3. 注入后的 `MOTION` 表同样要**真的被求值**,每个值都必须是 `data:image/webp;base64,` 开头
|
|
83
|
+
(写错一个 key,客户端就会建一个永远不动、一加载就报错的 `<img>`,而那是**静默**失败);
|
|
84
|
+
4. 产物**不得调用被陷阱的全局**(`setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `fetch` / `require`);
|
|
85
|
+
5. `const VERSION` 必须与 `package.json` 的 `version` 一致(防止版本漂移)。
|
|
80
86
|
|
|
81
|
-
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **
|
|
87
|
+
`verify-wisp.mjs` 在真实契约下执行浏览器半包:六个被陷阱的全局以**抛异常**的形式注入,虚拟时钟同时驱动两条调度路径,假 DOM / Blob / localStorage(含"抛异常的存储"这一档)齐全。当前 **800 项全 PASS,exit 0**(**这一行由预检自己核对** —— 数字对不上就红;顶部的版本号同样由它核对)。
|
|
82
88
|
|
|
83
89
|
---
|
|
84
90
|
|
|
@@ -144,19 +150,21 @@ node tools/pack-check.mjs
|
|
|
144
150
|
**为什么不能靠"我看过 files[] 了"**:包里的文档指向包外、`tools/` 少声明一个、素材没打进去 ——
|
|
145
151
|
这些都要真的解出副本才暴露。这个检查第一次跑就抓到了 README 里指向开发机绝对路径的死引用。
|
|
146
152
|
|
|
147
|
-
当前包:**
|
|
153
|
+
当前包:**79 个文件 / 41.2 MB**(tarball 41.2 MB —— 动图与立绘本来就压不动,所以两个数一样),其中 `assets/` 占 58 个文件、35.0 MB —— 但**运行时只会读全部 58 个**:
|
|
148
154
|
|
|
149
155
|
| 素材 | 数量 | 体积 | 运行时 |
|
|
150
156
|
|---|---|---|---|
|
|
151
|
-
| 默认档 1024×1536 |
|
|
152
|
-
| `
|
|
157
|
+
| 默认档 1024×1536(6 套皮肤 × 8 情绪) | 48 | 5.9 MB | ✅ 全都被 base64 进 JS(精灵图 8.0 MB) |
|
|
158
|
+
| `motion/*.webp`(通用两条 + 泳装八条) | 10 | 29.1 MB(每条 2.6~3.3 MB) | ✅ **1.47.0 起不再内联**:由宿主半包 `/wisp-motion/` 路由按 URL 发(客户端里只有一张文件名清单,十条合计 302 字节) |
|
|
159
|
+
| `_hi` 2048×3072 | 35 | 8.1 MB | ❌ 已从仓库删除(2026-09-30),要重新生成 |
|
|
153
160
|
| `_md` 512×768 | 35 | 1.5 MB | ❌ 从不读取 |
|
|
154
161
|
| `_sm` 256×384 | 35 | 0.6 MB | ❌ 从不读取 |
|
|
155
162
|
|
|
156
163
|
后三档加起来 **105 个文件 / 9.9 MB**,唯一用途是「哪天想用 `node build.mjs --tier=hi # 需要先 --all-tiers 生成 _hi;缺档会自动回退到默认档` 换个档重新打包」。
|
|
157
164
|
而且 `_hi` 连**画质收益都测不出来**(见下面性能小节:DPR 2 下高频能量差 0.3%,DPR 1/3 下 1024 档反而高 4~5%)。
|
|
158
|
-
**这 9.9 MB 已经省掉了**(2026-09-30):`assets/` 现在只有默认档
|
|
159
|
-
|
|
165
|
+
**这 9.9 MB 已经省掉了**(2026-09-30):`assets/` 现在只有默认档 48 个文件 / 5.9 MB,外加十条合计 29.1 MB 的动图帧动画
|
|
166
|
+
(1.47.1 起泳装那八条也落盘了),包是 **41.2 MB**(tarball 41.2 MB)—— 其中 `lib/client.js` 一个文件 8.25 MB
|
|
167
|
+
(只有精灵图;动图自 1.47.0 起外置投递,不再进单文件)。
|
|
160
168
|
|
|
161
169
|
要生成别的档位(例如想用 `--tier=hi` 重打包)先跑:
|
|
162
170
|
|
|
@@ -170,6 +178,134 @@ node tools/assets.mjs --from <绿幕母版目录> --skin <皮肤> --all-tiers
|
|
|
170
178
|
classic 皮肤的绿幕原图已经不在了,那些分档是**唯一**的重建来源,所以 `build.mjs --tier=…` 在
|
|
171
179
|
别人的机器上仍然可用。
|
|
172
180
|
|
|
181
|
+
**帧动画素材(1.45.0 起;1.46.1 换成动图 WebP;1.46.3 换 720p;1.46.4 修锯齿;1.46.5 增到两段;1.47.0 起改成外置投递、并给泳装接上八个状态;1.47.1 泳装那八条素材落盘)**:
|
|
182
|
+
`assets/motion/` 下每一个动图 WebP 对应**一个「皮肤+状态」** —— 通用两条(所有皮肤共用的
|
|
183
|
+
`idle` / `sleepy`),泳装八条(`swim_<立绘名>`)。
|
|
184
|
+
|
|
185
|
+
| 素材 | 谁在播 | 体积(磁盘) | 幕布 | 不透明 / 半透明 / 幕布残留 |
|
|
186
|
+
|---|---|---|---|---|
|
|
187
|
+
| `idle.webp` | 各皮肤的 `idle` | 2693.3 KB | 品红 `#FE01FF` | **27.016%** / 0.284% / **0.006%** |
|
|
188
|
+
| `sleepy.webp` | 各皮肤的 `sleep` | 3318.6 KB | 绿 `#A1E47B` | **17.379%** / 1.310% / **0.0017%** |
|
|
189
|
+
| `swim_idle.webp` | 泳装的 `idle` | 2756.9 KB | 品红 `#FF00FC` | **30.249%** / 0.256% / **0.0000%** |
|
|
190
|
+
| `swim_attn.webp` | 泳装的 `attn` | 3298.2 KB | 品红 `#FE00FC` | **29.753%** / 0.396% / **0.0000%** |
|
|
191
|
+
| `swim_happy.webp` | 泳装的 `happy` | 3229.7 KB | 品红 `#FF00FD` | **33.966%** / 0.365% / **0.0000%** |
|
|
192
|
+
| `swim_sleepy.webp` | 泳装的 `sleep` | 2888.3 KB | 品红 `#FF00FE` | **30.303%** / 0.321% / **0.0000%** |
|
|
193
|
+
| `swim_work.webp` | 泳装的 `alert` | 2617.0 KB | 品红 `#FD01FC` | **27.880%** / 0.264% / **0.0000%** |
|
|
194
|
+
| `swim_proud.webp` | 泳装的 `proud` | 2808.6 KB | 品红 `#FD01FC` | **27.456%** / 0.322% / **0.0000%** |
|
|
195
|
+
| `swim_eat.webp` | 泳装的 `eat` | 3095.0 KB | 品红 `#FD01FC` | **29.644%** / 0.432% / **0.0000%** |
|
|
196
|
+
| `swim_poked.webp` | 泳装的 `poked` | 3094.5 KB | 品红 `#FF00FE` | **30.089%** / 0.308% / **0.0000%** |
|
|
197
|
+
|
|
198
|
+
十条都是**动图 WebP**(RIFF/WEBP + 一串 ANMF 帧块,alpha 写在格式里)、**720×1280**、
|
|
199
|
+
24 fps、97 帧、4.04 秒循环(`-loop 0`)。
|
|
200
|
+
|
|
201
|
+
**投递方式在 1.47.0 换了,这是那一版的重点**:素材**不再内联**。客户端半包里的 `MOTION`
|
|
202
|
+
表存的是**文件名**(`{ idle: 'idle.webp', swim_idle: 'swim_idle.webp', … }`,十条合计 **302 字节**),
|
|
203
|
+
字节留在 `assets/motion/` 里,由**宿主半包**(`lib/index.js`)注册的
|
|
204
|
+
`/wisp-motion/<文件>` 路由发给页面;客户端只做
|
|
205
|
+
`new URL('wisp-motion/<文件>', document.baseURI)`。
|
|
206
|
+
|
|
207
|
+
为什么必须这样:720p 的动图是 MB 级素材 —— 通用那两条内联就是 7.83 MB,八条泳装再内联
|
|
208
|
+
会到 ~45 MB。换外置之后 `lib/client.js` 从 **15.3 MB 回到 7.8 MB**(1.47.1 实测 **8.25 MB**,
|
|
209
|
+
十条素材全在磁盘上)。预算因此分成两个数:
|
|
210
|
+
**内联**(清单,≤ 2048 字节,实测 302 B)与**磁盘**(素材仍然跟着包走,涨了要有人重新量一次;
|
|
211
|
+
1.47.1 实测 **29800.0 KB / 29.1 MB**,上界随之收到 **34000 KB**)。
|
|
212
|
+
|
|
213
|
+
为什么是它、不是什么:`@deepseek-ai/dsh-client-resources` **不是**一条素材路 —— 它是
|
|
214
|
+
`dsh-resource://` 的**取值**模型(要有 provider、挂在 slot 上、给的是值不是图片 URL)。
|
|
215
|
+
可行的是平台自己的 HTTP 载体:`ctx.webServer.register` 是文档明写的插件扩展点
|
|
216
|
+
("let the feature plugins claim their routes"),`import.meta.url` 给包目录。
|
|
217
|
+
|
|
218
|
+
**失败是静默的、而且是有意的**:路由不在(Desktop 载体没有 HTTP 服务器)、404、解码失败
|
|
219
|
+
—— `<img>` 的 `error` 一到,动图藏起来、画面交回静态立绘,她还是好好地站在那儿。
|
|
220
|
+
`__wisp.doctor().motion.frame` 会报出 `asset` / `src` / `failed`,一眼看得出是哪一种。
|
|
221
|
+
|
|
222
|
+
编码命令(原素材 720×1280 / 24fps,一帧不抽;每条素材一条命令):
|
|
223
|
+
|
|
224
|
+
**泳装那八条(1.47.0 写下配方,1.47.1 按它生成;品红幕,全部同一条)**:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
# 0) 首帧 = 尾帧:品红画布 720x1280 + 立绘裁到包围盒(+12px) 后 scale=-1:1266,落底留 14px
|
|
228
|
+
ffmpeg -f lavfi -i "color=c=0xFF00FF:s=720x1280" -i assets/swim/<状态>.webp \
|
|
229
|
+
-filter_complex "[1:v]crop=W:H:X:Y,scale=-1:1266:flags=lanczos[fg];[0:v][fg]overlay=(W-w)/2:H-h-14,format=rgb24" \
|
|
230
|
+
-frames:v 1 _first_<状态>.png # 复制一份作 _last_<状态>.png
|
|
231
|
+
|
|
232
|
+
# 1) 生成:mode=first_last_frame / model=2.5 / duration=4 / 720p / ratio=adaptive / generate_audio=false
|
|
233
|
+
# (首尾同一张 ⇒ 身份、画风、起手姿态被钉死,而且天然无缝循环)
|
|
234
|
+
|
|
235
|
+
# 2) 抠像:幕布色**从生成片里采样**(crop=8:8:10:10 后读那一个像素),不是写死的
|
|
236
|
+
ffmpeg -i <源>.mp4 -vf "fps=24,colorkey=#<采样色>:0.33:0.05,format=rgba,\
|
|
237
|
+
geq=r='r(X,Y)-max(0,min(r(X,Y),b(X,Y))-g(X,Y))':g='g(X,Y)':\
|
|
238
|
+
b='b(X,Y)-max(0,min(r(X,Y),b(X,Y))-g(X,Y))':a='alpha(X,Y)',\
|
|
239
|
+
scale=720:-1:flags=lanczos" \
|
|
240
|
+
-c:v libwebp_anim -loop 0 -an -q:v 40 assets/motion/swim_<立绘名>.webp
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
采样而不是写死是有理由的:生成片每次的"品红"都稍有不同(实测 `#FE01FF` / `#FF00FF` 都出现过),
|
|
244
|
+
`colorkey` 的距离阈值按采样值算才是"离幕布多远",按别人那一版的值算就是随缘。
|
|
245
|
+
1.47.1 那八条实测采到**五种**幕布色:`#FD01FC`(work / proud / eat)、`#FE00FC`(attn)、
|
|
246
|
+
`#FF00FC`(idle)、`#FF00FD`(happy)、`#FF00FE`(sleepy / poked)——
|
|
247
|
+
同一批请求、同一套提示词,幕布色仍然每条都不一样,这就是"必须采样"的现场证据。
|
|
248
|
+
|
|
249
|
+
三个指标(每条素材都要量,**不透明 ≥ 15% 是硬门槛** —— 低于它说明她被她自己抠掉了):
|
|
250
|
+
|
|
251
|
+
* **不透明%**:`alpha == 255` 的像素占全片比例;
|
|
252
|
+
* **半透明%**:`0 < alpha < 255`(平滑带,越高边缘越柔);
|
|
253
|
+
* **幕布残留%**:`alpha > 127` 且 RGB 与采样幕布色的三通道 L1 距离 / 765 ≤ 0.05 的像素比例。
|
|
254
|
+
|
|
255
|
+
**两条通用素材的旧命令**(历史,配方没变):
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
# sleepy:绿幕 #A1E47B + RGB 去绿边 G = min(G, max(R,B))
|
|
259
|
+
ffmpeg -i <源>.mp4 -vf "fps=24,colorkey=#A1E47B:0.19:0.05,format=rgba,\
|
|
260
|
+
geq=r='r(X,Y)':g='min(g(X,Y),max(r(X,Y),b(X,Y)))':b='b(X,Y)':a='alpha(X,Y)'" \
|
|
261
|
+
-c:v libwebp_anim -loop 0 -an -q:v 40 assets/motion/sleepy.webp
|
|
262
|
+
|
|
263
|
+
# idle:品红幕 #FE01FF + RGB 去品红边(把"R、B 同时高于 G"的那一份减掉)
|
|
264
|
+
ffmpeg -i <源>.mp4 -vf "fps=24,colorkey=#FE01FF:0.33:0.05,format=rgba,\
|
|
265
|
+
geq=r='r(X,Y)-max(0,min(r(X,Y),b(X,Y))-g(X,Y))':g='g(X,Y)':\
|
|
266
|
+
b='b(X,Y)-max(0,min(r(X,Y),b(X,Y))-g(X,Y))':a='alpha(X,Y)'" \
|
|
267
|
+
-c:v libwebp_anim -loop 0 -an -q:v 40 assets/motion/idle.webp
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
参数各管一件事,都别乱动:
|
|
271
|
+
|
|
272
|
+
- **`colorkey` 的 `similarity + blend` 就是"全不透明"的阈值**,两条都保持原样
|
|
273
|
+
(sleepy `0.24` / idle `0.38`)—— 所以"她自己"一个像素都没少:不透明占比
|
|
274
|
+
**17.379% / 27.016%**,和上一版**逐位相同**。
|
|
275
|
+
- **`blend` 收到 0.05**(1.46.4 是 0.06 / 0.08,下限是 0.05):它是边缘过渡带的半宽,
|
|
276
|
+
而**这条带是动画体积的大头** —— 实测 sleepy 在 `-q:v 40` 下把 blend 归零,
|
|
277
|
+
3405.7 KB → **1867.8 KB**(省 45%)。收到下限(而不是归零)是刻意的:0.02 那条带
|
|
278
|
+
只有 0.04 个色距宽,等于一刀切,而 1 bit 的 alpha 在 560px 的显示尺寸上就是一排台阶。
|
|
279
|
+
- **去边那一步是配套的,不是可选项**:过渡带一放宽,幕布的边就被一起放出来。
|
|
280
|
+
绿幕那条是 `G = min(G, max(R, B))`(与 `tools/keyout.mjs` 给立绘用的**同一条规则**);
|
|
281
|
+
**品红不能照搬** —— `B = min(B, max(G, R))` 会把"蓝色比红绿都强"的**头发本身**削掉。
|
|
282
|
+
品红这条减的是"R、B **同时**高于 G"的那一份(`R -= min(R,B)-G`,B 同理):她的蓝
|
|
283
|
+
(`min(R,B) < G`)一个像素都不动。实测 idle 的品红残留 **1.09% → 0.006%**,
|
|
284
|
+
头发边上那圈紫边没了。别改用 `despill`:它在 YUV 里做,会把整张画面一起脱色。
|
|
285
|
+
- **`-q:v 40`**:1.46.4 用的 60 是给"只有一段素材"的预算留的;两段同时进包时 60 装不下
|
|
286
|
+
(合计内联 9.7 MB)。40 与 60 在**这个内容上**的差别很小(sleepy 4440 → 3406 KB,
|
|
287
|
+
而画面在 560px 的显示尺寸下肉眼看不出块效应),省下的余量花在了去幕布色上 ——
|
|
288
|
+
那才是眼睛真能看见的地方。**fps / 分辨率 / 帧数一个都不能降**:预检里有断言钉着
|
|
289
|
+
720×1280 / 97 帧 / 41ms(24fps),两条都查。
|
|
290
|
+
- **为什么 idle 那条是品红幕布**:它是另一条生成线出来的,幕布色由那一步决定。
|
|
291
|
+
品红对她是**更远**的底色 —— 按 colorkey 那把尺子(三通道 L1 / 765)量:她的青色发梢
|
|
292
|
+
到绿幕 0.24、到品红 **0.51**;肤色到绿幕 0.24、到品红 0.35。
|
|
293
|
+
|
|
294
|
+
`colorkey` 的 similarity 曾经是 0.26(绿幕那版)—— 那会把她的皮肤和浅色衣服一起抠掉
|
|
295
|
+
(她自己变成"空心的",暗色主题下看起来就是一块黑色剪影),实测只有 5.5% 的像素还是
|
|
296
|
+
全不透明的;0.10 时是 18.1%,也就是整个她。**动作层的尺寸**由 CSS 补:sleepy 那段她只占
|
|
297
|
+
83.5% 盒高(立绘 98.8%),要 `scale(1.186) translateY(7.84%)`;idle 那段占 97.2%,
|
|
298
|
+
差 1.0~1.6% ⇒ **不补**(见 `lib/client.template.js` 里 `.wisp-video` 上那段推导与 1.46.5 小节)。
|
|
299
|
+
1.47.1 把同一把尺子用在泳装那八条上:素材 0.9515~0.9858 个盒高、立绘 0.9674~0.9967,
|
|
300
|
+
差 **0.2%~1.6%**(最大 `swim_proud` 1.6%)⇒ 八条**全都不补**,`MOTION_FIT` 一个字没改;
|
|
301
|
+
样式表那边补了一条 `.wisp-video[data-clip^="swim_"]{transform:none}`,把"不补"写成显式规则。
|
|
302
|
+
|
|
303
|
+
它和精灵图一样**按需解码**:挂载时建一个 `<img>` 给当前皮肤当前状态的那一段
|
|
304
|
+
(默认皮肤是 `idle`),换状态**和换皮肤**时**同一个元素换源**(不重建),
|
|
305
|
+
`prefers-reduced-motion` / 静止档 / 「帧动画」开关 / 躲起来时**把它藏起来、把画面交回
|
|
306
|
+
静态立绘**(见变更日志:动图没有 `pause()` 可喊)。1.47.0 起这一段素材是从页面自己
|
|
307
|
+
origin 的 `/wisp-motion/` 路由上取的 —— 路由不在就静默降级,见上文「投递方式」。
|
|
308
|
+
|
|
173
309
|
## 自检:`__wisp.doctor()`
|
|
174
310
|
|
|
175
311
|
她依赖的每一个选择器都是**读 DSH 源码推出来的**,而它们错了不会有任何报错 —— 对应的功能只是
|
|
@@ -228,6 +364,7 @@ node verify-wisp.mjs
|
|
|
228
364
|
| 指标 | 数值 |
|
|
229
365
|
|---|---|
|
|
230
366
|
| 动画帧使用量 | **0**(走 `timer` 服务,根本不挂帧循环) |
|
|
367
|
+
| 帧动画(v1.45.0 新增;v1.46.1 换成动图 `<img>`;v1.46.5 起两段) | **0 帧**:挂载时(`idle`)就建一个 `<img>`(动图 WebP),换状态只换 `src`,解码与循环都在浏览器的图片通道上,JS 这边一次回调都不挂。静止档 / `prefers-reduced-motion` / 躲起来时把它 `display:none`(动图没有 `pause()` 可喊),**不显示也就不解码** |
|
|
231
368
|
| 动作层(v1.38 新增) | **0 帧**:姿势与一次性动作都是"事件写一次属性,浏览器自己算"。指针靠近只做算术,出圈即归零,变化不到 0.05° 不写样式 |
|
|
232
369
|
| 定时器 | 1 个 `interval`(反应轮询)+ 3 个 `timeout` |
|
|
233
370
|
| 反应轮询成本 | 每轮 3 次 `querySelector`,10 s 内共 **7.2–8.3 ms**(≈ **0.86 ms/轮**) |
|
|
@@ -271,7 +408,8 @@ node verify-wisp.mjs
|
|
|
271
408
|
| — | — | — | 菜单里调过的 `size` 会**记在本地**并活过刷新;但配置里显式写了 `size` 时以配置为准 |
|
|
272
409
|
| `sleepAfterMs` | number | `90000` | 静置多久打盹,限制 `5000–3600000` |
|
|
273
410
|
| `reactions` | boolean | `true` | 是否跟随 Agent 忙碌 / 输入框内容变情绪 |
|
|
274
|
-
| `motion` | string | `'full'` | 动作幅度:`full` 灵动 / `subtle` 克制(半幅)/ `off`
|
|
411
|
+
| `motion` | string | `'full'` | 动作幅度:`full` 灵动 / `subtle` 克制(半幅)/ `off` 静止(连呼吸、`z` 与帧动画一起停 —— 动图被藏起来、画面交回静态立绘)。认不出的值退回 `full` |
|
|
412
|
+
| `frame` | boolean | `true` | **帧动画总开关**(`idle` / `sleep` 那两段动图)。关掉 = 画面永远交给静态立绘;与 `motion: 'off'` 不同,它**只收走动图**,呼吸、气泡、一次性动作照旧 |
|
|
275
413
|
| `persist` | boolean | `true` | 是否跨刷新记住位置 |
|
|
276
414
|
| `celebrate` | boolean | `true` | 一轮跑完是否庆祝一下 |
|
|
277
415
|
| `celebrateAfterMs` | number | `2500` | 只庆祝跑够这么久的轮次(避免每次小工具调用都跳) |
|
|
@@ -322,7 +460,7 @@ window.__wisp.position // { x, y }
|
|
|
322
460
|
window.__wisp.resetPosition() // 清掉记忆位置,回到右下角
|
|
323
461
|
window.__wisp.clock // 'timer-service' | 'animation-frame'
|
|
324
462
|
window.__wisp.checkBalance() // 查一次余额并说出来;返回 { state: 'ready' | 'signed-out' | 'unavailable' | 'failed' | 'unsupported', wallets, … }
|
|
325
|
-
window.__wisp.doctor().balance // 上一次余额查询的结果 +
|
|
463
|
+
window.__wisp.doctor().balance // 上一次余额查询的结果 + 两条通道在不在 + 这次走了哪条(**从不含凭据**)
|
|
326
464
|
window.__wisp.doctor().motion // { level, amp, reduced, tilt, tiltLocal, accent, pressed }
|
|
327
465
|
window.__wisp.destroy()
|
|
328
466
|
```
|
|
@@ -452,6 +590,10 @@ node tools/audit-log.mjs --declare <manifest> # 生成前:登记将要发
|
|
|
452
590
|
node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登记表 + 记产物哈希
|
|
453
591
|
```
|
|
454
592
|
|
|
593
|
+
**同一个入包文件重出过时**(例如某张表情先出坏了、改提示词再来一版),日志里会有**多条** `confirm`:
|
|
594
|
+
历史一条不删,但哈希比对只认**最新**那一条 —— 旧条目会被报成「同一文件的更早版本(重出记录)」并跳过比对。
|
|
595
|
+
不这么做的话,任何一次重出都会让「产物哈希与记录不符」永久变红(本项目第一次重出就踩到了)。
|
|
596
|
+
|
|
455
597
|
预检里已有两条常驻断言(`3z` 段):链校验通过且覆盖全部入包素材、且**审计中不得出现 `edit_image`**。
|
|
456
598
|
|
|
457
599
|
### 它证明了什么、没证明什么
|
|
@@ -464,13 +606,16 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
464
606
|
- 另外:即使某次真的传了参考图,代理也不会在登记表里留下痕迹(它不记参数)——
|
|
465
607
|
所以"没喂参考图"最终依赖的是**自我声明 + 声明不可事后篡改**,而不是第三方取证。这一条必须说清楚。
|
|
466
608
|
|
|
467
|
-
##
|
|
609
|
+
## 更新:能做什么
|
|
468
610
|
|
|
469
611
|
### 先说结论
|
|
470
612
|
|
|
471
|
-
- **检查更新:能。**
|
|
472
|
-
|
|
473
|
-
|
|
613
|
+
- **检查更新:能。** 客户端半包自己用 `window.fetch` 读 npm / GitHub 两个源,取版本更高的那个
|
|
614
|
+
(宿主半包那条 `ctx.web.fetch` 的路仍在,动态插件形态下才有座位)。
|
|
615
|
+
- **安装更新:能(v1.48.0 起)。** 平台把**插件管理器本身**做成了客户端可用的 Remote 命名空间:
|
|
616
|
+
`ctx.remote.pluginManager.installBundle(spec)` —— 平台自己的「插件」设置页走的就是它。
|
|
617
|
+
查到新版之后,「关于她」弹窗里会出现 **「安装 vX」**,点一下就装;装完该刷新还是该重启由平台的
|
|
618
|
+
`ChangeResult.application` 说了算(`applied` / `restart-required`),她照实转述,不自作聪明。
|
|
474
619
|
|
|
475
620
|
为什么必须绕这么一圈 —— 两边的限制都查到源码级:
|
|
476
621
|
|
|
@@ -507,7 +652,8 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
507
652
|
| **报版本** | 菜单「版本与更新」说一句「我是 1.24.0。<这一版做了什么>」;`__wisp.version` / `__wisp.doctor().update` 也能读 |
|
|
508
653
|
| **更新感知** | 她记住上次见到的版本(localStorage)。**版本变了就说一句「我更新到 … 了」**;第一次安装不吭声(那时她不是更新,是刚来) |
|
|
509
654
|
| **复制包名** | 同一项会把 `dsh-wisp` 复制到剪贴板,方便在 DSH 的插件列表里粘贴安装。**没有剪贴板权限时静默降级**成只说话,不报错 |
|
|
510
|
-
|
|
|
655
|
+
| **直接安装** | 查到新版后「关于她」里出现「安装 vX」→ `remote.pluginManager.installBundle('dsh-wisp@X')`。装的就是**刚才查到的那一版**(数字与装下去的东西一致);失败按原因说人话(`no-matching-version` = npm 还没这一版) |
|
|
656
|
+
| **能力边界如实上报** | `doctor().update` 里有 `canSelfUpdate: true`、`install`(走哪条通道 + 最后一次装的结果)与 `why`(她自己仍然不碰网络与文件) |
|
|
511
657
|
|
|
512
658
|
### 真正更新它的路径
|
|
513
659
|
|
|
@@ -515,13 +661,504 @@ node tools/audit-log.mjs --confirm <manifest> # 生成后:并入代理登
|
|
|
515
661
|
2. 或对本包目录重新 `npm i` / `npm pack` 后安装;
|
|
516
662
|
3. 更新后重载页面 —— 她会自己发现版本变了并说一句。
|
|
517
663
|
|
|
664
|
+
**两个源都可以换**(1.43.0 起写进文档):宿主半包读 `config.urls`,所以镜像或私有
|
|
665
|
+
registry 只要在 patch 行里指过去就行。国内常见配置:
|
|
666
|
+
|
|
667
|
+
```yaml
|
|
668
|
+
- insert:
|
|
669
|
+
- id: wisp
|
|
670
|
+
name: "dsh-wisp"
|
|
671
|
+
config:
|
|
672
|
+
urls:
|
|
673
|
+
npm: "https://registry.npmmirror.com/dsh-wisp/latest"
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
只给 `npm` 一个键也可以:没给的源仍用内置默认值(`registry.npmjs.org` 和
|
|
677
|
+
`raw.githubusercontent.com/.../main/package.json`)。预检里有一条断言盯着"换了源
|
|
678
|
+
就真的只去问新地址",免得"支持镜像"只是文档里的一句话。
|
|
679
|
+
|
|
680
|
+
**她仍然不自己碰网络与文件**:检查走客户端 `fetch`,安装走平台的插件管理器 ——
|
|
681
|
+
两件事都不是她做的。她自己既没有 Node(宿主半包在 vm 沙箱里)也没有包管理器;
|
|
682
|
+
`doctor().update.install.via` 会告诉你这次走的是哪条通道(`remote.pluginManager` 或 `null`),
|
|
683
|
+
`install.state` 是最后一次的结果(`installed` / `failed` / `unsupported` / `pending`)。
|
|
684
|
+
|
|
518
685
|
### 预检里的守卫
|
|
519
686
|
|
|
520
687
|
`WHATS_NEW` 必须写着**当前版本**,否则预检红 —— 这样"这一版做了什么"不会静默过期。
|
|
521
|
-
另外 `
|
|
688
|
+
另外 `hint` 里必须含包名或仓库地址、`why` 里必须说明沙箱原因、`canSelfUpdate` 必须如实,
|
|
689
|
+
都有断言盯着;安装那条路另有六条断言:装的版本号是否就是提示的那一版、`applied` 与
|
|
690
|
+
`restart-required` 是否说了两句不同的话、`no-matching-version` 是否有自己的句子、
|
|
691
|
+
抛错是否被接住、没有插件管理器时是否明说、以及英文覆盖层齐全且无汉字。
|
|
522
692
|
|
|
523
693
|
## 变更
|
|
524
694
|
|
|
695
|
+
### 1.48.0
|
|
696
|
+
|
|
697
|
+
**查到新版可以直接装了 —— 不再只是"告诉你,去插件列表里自己装"。**
|
|
698
|
+
|
|
699
|
+
以前这一步卡在一个真实结论上:客户端半包**没有 host RPC 座位**(1.44.0 那条教训),
|
|
700
|
+
而"装"又需要文件系统和包管理器 —— 所以插件只能报版本号,让用户自己去 DSH 的插件列表粘贴包名。
|
|
701
|
+
|
|
702
|
+
漏掉的是:**平台把插件管理器自己也做成了客户端可用的 Remote 命名空间**。
|
|
703
|
+
|
|
704
|
+
```js
|
|
705
|
+
ctx.remote.pluginManager.installBundle('dsh-wisp@1.48.0') // → ChangeResult
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
平台自己的「插件」设置页(安装 / 更新 / 停用 / 卸载)走的就是这个命名空间
|
|
709
|
+
(`installBundle` / `waitForInstall` / `cancelInstall` / `listPlugins` …)。
|
|
710
|
+
它**不需要 host 座位**,也不需要插件碰网络或凭据 —— 装这一步由平台带着 pnpm 与 profile 的
|
|
711
|
+
写权限去做。这也正是"dsh 里万物皆插件"该有的样子:要用什么能力,先看平台有没有把它开成服务。
|
|
712
|
+
|
|
713
|
+
现在的流程:菜单「关于她…」→ **检查更新** → 查到新版时弹窗里直接出现 **「安装 v1.48.0」**
|
|
714
|
+
→ 点一下就开始装,她说一句进度,装完照平台的结论说话:
|
|
715
|
+
|
|
716
|
+
| 平台说 | 她说 |
|
|
717
|
+
|---|---|
|
|
718
|
+
| `application: 'applied'` | 装好了 —— 刷新页面就是新版 |
|
|
719
|
+
| `application: 'restart-required'` | 装好了 —— 重启 DSH 后生效 |
|
|
720
|
+
| 信封 `ok: false` + `no-matching-version` | npm 上还没有这一版(GitHub 先更新了)—— 等一会儿再装 |
|
|
721
|
+
| 抛错 / 其他失败 | 没装成(原因进 `doctor().update.install`) |
|
|
722
|
+
| 这个壳里没有插件管理器 | 明说"我装不了",而不是装作做了 |
|
|
723
|
+
|
|
724
|
+
装的是**刚才查到的那一版**(`dsh-wisp@<version>`),不是 `latest` —— 提示里的数字和装下去的
|
|
725
|
+
东西必须是同一个。安装有 3 分钟的宽松截止时间;到点了不谎报失败,只说"动作已经发出去了、还没回来"。
|
|
726
|
+
|
|
727
|
+
`doctor().update.canSelfUpdate` 从 `false` 改成 `true`(它现在真的能触发安装),
|
|
728
|
+
同时新增 `install.via` / `install.state` 可查。
|
|
729
|
+
|
|
730
|
+
### 1.47.1
|
|
731
|
+
|
|
732
|
+
**泳装八个状态真的有动图了 —— 八条素材落盘,而接线早就在那儿等着。**
|
|
733
|
+
|
|
734
|
+
1.47.0 把**投递**(宿主的 `/wisp-motion/` 路由 + 客户端那张文件名清单)和**接线**
|
|
735
|
+
(`MOTION_OF['swim:<状态>']` 按皮肤+状态点素材)都做完了,差的只有素材本身;这一版把八条补齐:
|
|
736
|
+
`assets/motion/` 下多了 `swim_idle` / `swim_attn` / `swim_happy` / `swim_sleepy` /
|
|
737
|
+
`swim_work` / `swim_proud` / `swim_eat` / `swim_poked`,每条 **720×1280 / 24 fps / 97 帧 /
|
|
738
|
+
4.04 秒循环**(和通用那两条同一把尺子,预检逐条断言),由宿主半包按 URL 发给页面。
|
|
739
|
+
**一条素材对应一张立绘**:`sleep` 那一档播的是 `swim_sleepy`(睡着那张立绘),`alert`
|
|
740
|
+
那一档播的是 `swim_work`(敲键盘那张)—— 素材按"哪张立绘"点,几何也按素材名查,两边不会各说各话。
|
|
741
|
+
|
|
742
|
+
配方照抄上面「帧动画素材」那一节写下的那一条(首帧=尾帧 + 品红幕**采样** + colorkey +
|
|
743
|
+
去品红边的 RGB 那一步),八条一条没改。三条门槛全过:
|
|
744
|
+
|
|
745
|
+
| 状态 | 素材 | 体积 | 不透明% | 半透明% | 品红残留% |
|
|
746
|
+
|---|---|---|---|---|---|
|
|
747
|
+
| `idle` | `swim_idle.webp` | 2756.9 KB | **30.249** | 0.256 | **0.0000** |
|
|
748
|
+
| `attn` | `swim_attn.webp` | 3298.2 KB | **29.753** | 0.396 | **0.0000** |
|
|
749
|
+
| `happy` | `swim_happy.webp` | 3229.7 KB | **33.966** | 0.365 | **0.0000** |
|
|
750
|
+
| `sleep` | `swim_sleepy.webp` | 2888.3 KB | **30.303** | 0.321 | **0.0000** |
|
|
751
|
+
| `alert` | `swim_work.webp` | 2617.0 KB | **27.880** | 0.264 | **0.0000** |
|
|
752
|
+
| `proud` | `swim_proud.webp` | 2808.6 KB | **27.456** | 0.322 | **0.0000** |
|
|
753
|
+
| `eat` | `swim_eat.webp` | 3095.0 KB | **29.644** | 0.432 | **0.0000** |
|
|
754
|
+
| `poked` | `swim_poked.webp` | 3094.5 KB | **30.089** | 0.308 | **0.0000** |
|
|
755
|
+
|
|
756
|
+
不透明 ≥15%(实测 27.5~34.0%)、品红残留 ≤0.05%(实测 0.0000%)、平滑带 0.256~0.432%。
|
|
757
|
+
八条合计 **23788.2 KB**;加上通用那两条,`assets/motion/` 现在 **10 条 / 29800.0 KB(29.1 MB)**。
|
|
758
|
+
|
|
759
|
+
**尺寸校正:八条都不用补。** 按包围盒(A>24)逐条量"她占盒子高多少":素材 0.9515~0.9858,
|
|
760
|
+
各自对应的泳装立绘 0.9674~0.9967,差 **0.2%~1.6%**,八条全在"小于 2% 就不补"那条线以内
|
|
761
|
+
(最大的是 `swim_proud` 的 1.6%,在 210px 的盒子里 3px 量级)。所以 `MOTION_FIT`
|
|
762
|
+
**一个字都没变**,但样式表多了一条 `.wisp-video[data-clip^="swim_"]{transform:none}` ——
|
|
763
|
+
不补也要**显式地**不补,否则 base 那条给通用 `sleepy` 的 `scale(1.186)` 会落到这八条头上
|
|
764
|
+
(凭空放大 18.6%)。sleepy 那条 1.186 与 idle 那条"不补"都原样保留。
|
|
765
|
+
|
|
766
|
+
**体积上界跟着实测改了**:预检里的 `MOTION_DISK_BUDGET_KB` 从 39000 收到 **34000**
|
|
767
|
+
(实测十条 29800.0 KB;上界按"每条不超过实测最大 3.3 MB"估,再留一点重编码余量),
|
|
768
|
+
内联那份仍是 2048 B 的清单、实测 **302 B**。
|
|
769
|
+
|
|
770
|
+
**这一轮一共提交了 13 次 `create_video_task`**(本轮要生成的 7 条里,4 条一次过:
|
|
771
|
+
`happy` / `sleepy` / `work` / `poked`):
|
|
772
|
+
`attn` 提交 3 次、`eat` 提交 3 次 —— 前两次调用都正常发出、却在客户端 60 秒超时之后
|
|
773
|
+
**既没有产物落地、也没有任何错误**(同参重提第三次就成了);`proud` 提交 3 次 ——
|
|
774
|
+
前两次返回的是内容安全拒绝(`输入图像可能包含敏感信息,请更换后重试`),
|
|
775
|
+
**立绘本身一个像素没改**,换的只是首帧的取景余量(裁切 12→28px、缩放 1266→1252)后才通过。
|
|
776
|
+
这 13 次里 7 次产出素材、2 次内容安全拒绝、4 次无产物;`swim/idle` 那一条用的是上一轮
|
|
777
|
+
已经生成好的源片,**没有**计入这 13 次。
|
|
778
|
+
|
|
779
|
+
**动作一览不再点一下就关。**
|
|
780
|
+
|
|
781
|
+
以前点一个动作预览,弹窗立刻消失(当时的理由是"关掉才看得见她")。实际用起来是:想连看三个动作,
|
|
782
|
+
就得重开三次窗,每次还要重新找那一格。现在预览**不关窗** —— 点一个演一个,可以连着点;
|
|
783
|
+
关窗仍有三条路:**Esc / 右上角 × / 点背景**。
|
|
784
|
+
|
|
785
|
+
焦点也不再交还给她:弹窗还在,被点的那个格子就还在,焦点留在原地(以前必须交接,
|
|
786
|
+
是因为那个格子会随窗一起消失)。弹窗里的说明也跟着改了:**"可以连着点,窗不会关。"**
|
|
787
|
+
|
|
788
|
+
### 1.47.0
|
|
789
|
+
|
|
790
|
+
**新增「帧动画」独立开关。**
|
|
791
|
+
|
|
792
|
+
动图化之后(1.46.5 起 `idle` 与 `sleep` 各有一段),"我不想让她一直动"只有一个粗开关:
|
|
793
|
+
把「动作幅度」整个调到 `静止` —— 那是连呼吸、`z`、一次性动作一起停。现在多了一个**只管画面**的开关:
|
|
794
|
+
右键 → 行为 → **帧动画**。关掉之后画面永远交给静态立绘,其余动作照旧。
|
|
795
|
+
|
|
796
|
+
它走的是和"减少动态效果 / 静止档 / 躲起来"**同一条闸门** `motionFrozen()` ——
|
|
797
|
+
一处判断,`syncMotion()` 用它决定画面归谁,`doctor()` 用它汇报,
|
|
798
|
+
所以不会出现"报告说在动、画面上停着"这种分歧。配置键 `frame`(默认 `true`,也能在 profile 里写死)。
|
|
799
|
+
|
|
800
|
+
`doctor().motion.frame` 多了一个 `enabled` 字段:一眼分清"她不动"是哪个原因(开关 / 幅度 / 系统设置 / 躲起来)。
|
|
801
|
+
|
|
802
|
+
**帧动画素材改成外置投递:`lib/client.js` 15.3 MB → 7.8 MB。**
|
|
803
|
+
|
|
804
|
+
720p 的动图是 MB 级素材:通用那两条内联就是 7.83 MB,八条泳装再内联会到 ~45 MB。
|
|
805
|
+
所以 1.47.0 把**投递**换了一条路 —— 客户端半包里的 `MOTION` 表只留**文件名**,
|
|
806
|
+
字节留在 `assets/motion/` 里,由**宿主半包**注册的 `/wisp-motion/<文件>` 路由发给页面
|
|
807
|
+
(`ctx.webServer.register`),客户端只做 `new URL('wisp-motion/<文件>', document.baseURI)`。
|
|
808
|
+
|
|
809
|
+
* **为什么是这条路**:`@deepseek-ai/dsh-client-resources` 查过了,**不是**素材路 ——
|
|
810
|
+
它是 `dsh-resource://<协议>` 的**取值**模型(要有 provider、挂在 slot 上、给的是值);
|
|
811
|
+
平台给插件用的 HTTP 载体才是(`dsh-host-webserver` 明写"let the feature plugins claim
|
|
812
|
+
their routes"),而宿主半包是 Loader 按 ES module 导入的,`import.meta.url` 就是包目录。
|
|
813
|
+
* **失败是静默的、有意的**:路由不在 / 404 / 解码失败 ⇒ `<img>` 的 `error` ⇒ 动图收起来、
|
|
814
|
+
画面交回静态立绘。`doctor().motion.frame` 报 `asset` / `src` / `failed`。
|
|
815
|
+
* **体积实测**:内联部分 = 一张文件名清单(十条合计 **30 字节**量级,预检上界 2048 B);
|
|
816
|
+
磁盘部分是十条素材本身(预检按实测设上限)。
|
|
817
|
+
* **接线**:`MOTION_OF` 从"按状态"改成"按**皮肤+状态**"(`'swim:sleep' → 'swim_sleepy'`),
|
|
818
|
+
通用那一对(`idle` / `sleepy`)留给其它皮肤。换皮肤与换状态一样会**同时**换图与几何
|
|
819
|
+
(`applySkin()` 里那一句 `syncMotion()` 就是为它写的)。
|
|
820
|
+
|
|
821
|
+
**泳装八个状态的素材:本轮没有生成出来。** 生成配方(首帧=尾帧 + 品红幕采样 + 抠像)
|
|
822
|
+
写在上面「帧动画素材」那一节,接线、预检与真引擎探针都已就位:**把
|
|
823
|
+
`assets/motion/swim_<立绘名>.webp` 放进去,再跑一次 `node build.mjs` 就自动生效** ——
|
|
824
|
+
在那之前这八个状态走静默降级(连元素都不建,**不会**退回去播通用那段循环,因为那是
|
|
825
|
+
穿着泳装播别的皮肤的动作)。预检里那条"八条要么都在、要么都不在"盯的就是这个:
|
|
826
|
+
半套素材是坏的,整套和没有都是好的。
|
|
827
|
+
|
|
828
|
+
### 1.46.5
|
|
829
|
+
|
|
830
|
+
**站着时她也在动了:`idle` 接上第二条循环,两段素材一起塞进 8 MB。**
|
|
831
|
+
|
|
832
|
+
`assets/motion/idle.webp`(720×1280 / 24fps / 4.04 秒 / 97 帧,一帧没抽)接进 `idle` 状态 ——
|
|
833
|
+
挂载即播,换状态时**同一个 `<img>` 换源**。动作层那一套规则一条没动:`pointer-events:none`、
|
|
834
|
+
与立绘同盒、冻结(静止档 / `prefers-reduced-motion` / 躲起来 / 加载失败)=藏起动图 + 交回
|
|
835
|
+
静态立绘、**动作层不许有 CSS filter**、动图显示时静态立绘隐藏。
|
|
836
|
+
|
|
837
|
+
**体积:两段合计内联 7.83 MB(目标 ≤ 8 MB)。** 上一版一段素材就是 5031.3 KB,两段按老参数
|
|
838
|
+
(`-q:v 60`,blend 0.06 / 0.08)要 **9.69 MB**。三个候选档都量了同样的三个指标:
|
|
839
|
+
|
|
840
|
+
| 档位 | sleepy / idle 原始 | 合计内联 | idle 不透明 / 半透明 / 品红残留 | sleepy 不透明 / 半透明 / 绿残留 |
|
|
841
|
+
|---|---|---|---|---|
|
|
842
|
+
| 老参数(q:v 60,blend 0.06 / 0.08) | 3773.4 / 3665.3 KB | 9918 KB(9.69 MB)✗ | 27.016% / 0.437% / 1.09% | 17.379% / 1.396% / 0.0012% |
|
|
843
|
+
| **采用(q:v 40,blend 0.05 两条)** | 3318.6 / 2693.3 KB | **8015.8 KB(7.83 MB)** ✓ | 27.016% / 0.284% / **0.006%** | 17.379% / 1.310% / **0.0017%** |
|
|
844
|
+
| 备份(q:v 35,blend 0.06 / 0.08) | 3310.2 / 2828.8 KB | 8185.3 KB(7.99 MB)✓ | 27.016% / 0.437% / 0.008% | 17.379% / 1.396% / 0.0021% |
|
|
845
|
+
|
|
846
|
+
- **没有降 fps、没有抽帧、没有降分辨率**:两条都还是 720×1280 / 24fps / 97 帧(预检钉着)。
|
|
847
|
+
- **也没有缩短时长 —— 量过才敢说不行**:这段动作**不是 2 秒周期的**。帧 0 与帧 48 的差是
|
|
848
|
+
噪声底的 **15.2 倍**,而整段 4.04 秒的接缝只有 **2.0 倍**(也就是说帧 96 几乎就是帧 0)。
|
|
849
|
+
砍成 2 秒会在循环点上"啪"地弹回另一个姿势;48/49 帧的各种窗口里最好的也有 **7.3 倍**。
|
|
850
|
+
4.04 秒是这段素材唯一能无缝接上的长度。
|
|
851
|
+
- **blend 0.06 / 0.08 → 0.05**(预检钉住的下限):这条过渡带是体积的大头 —— sleepy 在
|
|
852
|
+
q:v 40 下把 blend 归零,3405.7 KB → **1867.8 KB**(省 45%)。全不透明阈值
|
|
853
|
+
(`similarity + blend`)一点没动,所以**她自己一个像素都没少**(两条不透明占比与上一版
|
|
854
|
+
逐位相同)。代价是半透明边缘 0.44% → 0.28%(idle):备份档那个 0.44% 更"柔",
|
|
855
|
+
但它只剩 0.1% 的余量,而 0.05 是项目自己定的下限。
|
|
856
|
+
- **`-q:v` 60 → 40**:两段同时进包时 60 装不下(9.69 MB)。这是这一版唯一降质的地方,
|
|
857
|
+
而在这个内容上 40 与 60 的差别很小(sleepy 4440 → 3406 KB,560px 的显示尺寸下看不出
|
|
858
|
+
块效应)。1.46.4 之所以从 40 提到 60,理由是"块效应叠在**硬边**上更显锯齿"—— 现在边是软的。
|
|
859
|
+
- **idle 那段的幕布是品红(`#FE01FF`),并补了一条去品红边**:源片就是这样生成的,品红对她
|
|
860
|
+
其实**更远**(按 colorkey 那把尺子量色距:青色发梢到绿幕 0.24、到品红 **0.51**;肤色
|
|
861
|
+
0.24 对 0.35)。但**绿幕那条去边规则不能照搬**:`B = min(B, max(G,R))` 会把"蓝色比红绿
|
|
862
|
+
都强"的**头发本身**削掉。新规则只减"R、B **同时**高于 G"的那一份 —— 她的蓝
|
|
863
|
+
(`min(R,B) < G`)一个像素不动。实测品红残留 **1.09% → 0.006%**:头发边上那圈紫边没了
|
|
864
|
+
(2× 放大对比过:没去边的版本沿发梢有一整条粉紫线)。
|
|
865
|
+
- **动作层的几何按素材取**:新增 `MOTION_FIT` 表 + 元素上的 `data-clip`,样式表用
|
|
866
|
+
`.wisp-video[data-clip="idle"]{transform:none}` 把"不校正"写成显式规则(否则 base 那条
|
|
867
|
+
1.186 会落到 idle 头上 —— 放大 18.6%)。**idle 不补尺寸校正**:她在那张里占 97.2% 盒高、
|
|
868
|
+
立绘 98.2~98.8%,差 1.0~1.6% < 2%,在 210px 的盒子里是 2~3px —— 不值得为它多一条能和
|
|
869
|
+
"两层同格"对不上的变换。**sleepy 的 `scale(1.186) translateY(7.84%)` 原样不动**(它的
|
|
870
|
+
包围盒实测仍是 83.3% 高 / 8.83% 底边距,与上一版逐位相同)。
|
|
871
|
+
- **失败按素材记**:一段素材加载失败后不再重指源(重试坏源只会每次换表情烧一次解码),
|
|
872
|
+
但**另一段照常播**;`doctor().motion.frame.asset` 也改成"查得到才算数"(表里没有这一段时
|
|
873
|
+
报 `null`,而不是报一个根本不存在的素材名)。这两条都是新断言逼出来的。
|
|
874
|
+
- **预检新增**:bundle 里**同时**有 `idle` 与 `sleepy`(且没有"没有状态会播"的白带素材);
|
|
875
|
+
`idle` 在挂载时就建动图元素并接管画面;换状态时**同一个元素换源**、几何跟着换(`data-clip`
|
|
876
|
+
一起变);idle 的行内 `transform` 是 `none`;坏掉一段之后另一段仍然会播。**真实引擎探针**
|
|
877
|
+
多读三件事:idle 动图的 `naturalWidth` = **素材自己的**画布宽度(探针直接从
|
|
878
|
+
`assets/motion/idle.webp` 读 VP8X,不写死 720)、变换前与立绘盒子逐边一致、计算后
|
|
879
|
+
`transform` 是 `none`。体积预算 5540 → **8820 KB**(两段实测内联 + 约 10%)。
|
|
880
|
+
|
|
881
|
+
### 1.46.4
|
|
882
|
+
|
|
883
|
+
**睡着时她不再缩小一圈;锯齿也没了 —— 两个都是量出来的。**
|
|
884
|
+
|
|
885
|
+
**① 她比立绘小 15.7%。** 两层共用同一个盒子(`140×210`、`object-fit:contain`、
|
|
886
|
+
`object-position:bottom center`),但两张素材里她占画面的比例不同。实测包围盒(阈值 A>127):
|
|
887
|
+
|
|
888
|
+
| 素材 | 画面 | 她占画面高 | 底边距 |
|
|
889
|
+
|---|---|---|---|
|
|
890
|
+
| 立绘 `deepsea/sleepy.webp` | 1024×1536(2:3,与盒子**同比例** → contain 正好铺满盒高) | **98.8%** | 1.17%(18px) |
|
|
891
|
+
| 动图 `motion/sleepy.webp` | 720×1280(0.5625 **窄于**盒子 → contain 按高贴合) | **83.5%** | 8.83%(113px) |
|
|
892
|
+
|
|
893
|
+
于是"画面交给她"的那一刻她**缩了 15.7%**。修法**不重做素材**,只给动作层一条变换:
|
|
894
|
+
|
|
895
|
+
```
|
|
896
|
+
transform: scale(1.186) translateY(7.84%) /* transform-origin: 50% 100% */
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
- 放大倍率 = 0.9883 / 0.8352 = **1.1834**,取 **1.186**(97 帧之间包围盒自己还在 ±0.2% 抖);
|
|
900
|
+
- `origin` 放**脚底**,缩放不动底边 —— 但底边距会跟着放大成 `0.0883 × 1.186 = 0.1047`,
|
|
901
|
+
要收回立绘那 `0.0117` 就得让底边下移 `0.0930` 个盒高;
|
|
902
|
+
- 而 `scale(s) translateY(t)` 是**右乘先生效**:translate 先作用在元素自身坐标里、再被 s 缩放
|
|
903
|
+
一次,真正下移的是 `s × t` —— 所以 `t = 0.0930 / 1.186 = 7.84%`。
|
|
904
|
+
- 验算:`0.8352 × 1.186 = 0.9905` 高(立绘 0.9883,差 0.2% ≈ 1px),底边距回到 `0.0117` ✓。
|
|
905
|
+
|
|
906
|
+
两个落点都写了:样式表 + 行内(项目惯例:样式表缺失的壳里画面也得是对的)。
|
|
907
|
+
|
|
908
|
+
**② 锯齿:不是分辨率的问题,是 `blend` 的问题。** `colorkey` 的 `blend` 原先只有 **0.02**,
|
|
909
|
+
过渡带宽度 = `2 × blend = 0.04` 个色距 —— 边缘几乎是**一刀切**,1 bit 的 alpha 在 560px 的
|
|
910
|
+
显示尺寸上就是一排台阶。放宽到 **0.06**(`similarity` 0.22 → 0.18,两者之和仍是 0.24,
|
|
911
|
+
所以不透明占比**一个像素都没少**:17.38%),半透明边缘 **0.80% → 1.40%**(+74%)。
|
|
912
|
+
|
|
913
|
+
代价是绿幕的边也被一起放出来(绿边 0.078% → 0.119%),所以配套加了一步 **RGB 去绿边**
|
|
914
|
+
`G = min(G, max(R, B))` —— 与 `tools/keyout.mjs` 给立绘用的**同一条规则**:绿边回到
|
|
915
|
+
**0.001%**,比修之前还干净,而她的颜色几乎没变(165,514 个不透明像素里只改了 1,176 个)。
|
|
916
|
+
WebP 质量同时从 `-q:v 40` 提到 **60**(块效应叠在硬边上只会更显锯齿)。
|
|
917
|
+
|
|
918
|
+
| | 修前 | 修后 |
|
|
919
|
+
|---|---|---|
|
|
920
|
+
| 素材 | 2866.7 KB(q:v 40 / blend 0.02) | **3773.4 KB**(q:v 60 / blend 0.06 + 去绿边) |
|
|
921
|
+
| 内联 | 3822.5 KB | **5031.3 KB** |
|
|
922
|
+
| 预检预算 | 4200 KB | **5540 KB**(= 实测内联 + 10%) |
|
|
923
|
+
| 不透明 / 半透明 / 绿边 | 17.38% / 0.80% / 0.078% | **17.38% / 1.40% / 0.001%** |
|
|
924
|
+
|
|
925
|
+
fps / 分辨率 / 帧数一个没动:**720×1280 / 24fps / 97 帧**(预检里新增断言钉着)。
|
|
926
|
+
|
|
927
|
+
**预检新增**:动作层的 CSS 必须含 `scale(1.186)` 与底部对齐的 `transform-origin`(样式表与
|
|
928
|
+
行内**两份都点名**);文档里那条编码命令的 `blend ≥ 0.05` 且必须带 `geq`/`despill` 去绿边;
|
|
929
|
+
素材几何 `720×1280 / 97 帧 / 41ms`。**真实引擎探针**多读两件事:动作层**计算后**的 `transform`
|
|
930
|
+
真的是 `matrix(1.186, …)`,以及渲染出来的底边真的比立绘那条落地线低 9.3% 个盒高。
|
|
931
|
+
|
|
932
|
+
### 1.46.3
|
|
933
|
+
|
|
934
|
+
**睡眠循环换成 720p 重生成。**
|
|
935
|
+
|
|
936
|
+
原来生成 480×854,而她在屏幕上约 560 px 宽 —— 放大后发虚。改用 720p(720×1280,24fps,97 帧,首帧用她 1024×1536 原立绘合成,源不被放大)重生成,绿边 0.01%、不透明 18.1%。
|
|
937
|
+
|
|
938
|
+
代价是体积:素材 1718 → 2867 KB,内联 2.4 → 3.8 MB,客户端包约 6.4 → 10.2 MB。**这条是用户明确选择的**(问过:清晰度 vs 体积)。预检的体积预算随之从 2472 KB 提到 4200 KB。
|
|
939
|
+
|
|
940
|
+
### 1.46.2
|
|
941
|
+
|
|
942
|
+
**把绿边抠干净。**
|
|
943
|
+
|
|
944
|
+
上一版 colorkey 用 0.10/0.10:边缘残留绿色像素占 0.43%(放大后就是一圈绿光)。实测三档后改成 **0.22/0.02** —— 绿边降到 **0.01%**,她自己从 18.0% 变成 16.8%(只削掉一点边 ✓),体积反而从 1854 KB 降到 **1718 KB**。
|
|
945
|
+
|
|
946
|
+
### 1.46.1
|
|
947
|
+
|
|
948
|
+
**帧动画从 alpha 视频换成动图 WebP(`<video>` → `<img>`,480 宽 / 24fps / 97 帧一帧不抽),
|
|
949
|
+
理由是这个壳不还原 VP9 的 alpha 平面** —— 同一段素材她在暗色主题下渲染成黑色剪影、亮色主题
|
|
950
|
+
下白色剪影(用户确认过),而图片路径的 alpha 是硬的。
|
|
951
|
+
|
|
952
|
+
「冻结」跟着改语义:动图没有 `pause()` 可喊,所以静止档 / `prefers-reduced-motion` /
|
|
953
|
+
躲起来时不再"暂停并回到首帧",而是**把动图藏起来、把画面交回静态立绘**(v1.45.2 那条
|
|
954
|
+
"画面同时只归一个人"照旧)。动作层**不许有 CSS filter** 这条规矩保留。
|
|
955
|
+
|
|
956
|
+
同时修掉一个更早的错:`colorkey` 的 similarity 从 **0.26 收紧到 0.10**。0.26 会把她的皮肤与
|
|
957
|
+
浅色衣服一起抠掉 —— 实测只有 **5.5%** 的像素还是全不透明的(她自己变成空心的,暗色主题下
|
|
958
|
+
看起来就是"一块黑色剪影"),0.10 时是 **18.1%**,也就是整个她。
|
|
959
|
+
|
|
960
|
+
### 1.46.0
|
|
961
|
+
|
|
962
|
+
**查余额终于真的能用了 —— 根因和 1.44.0 那次一模一样:座位不对。**
|
|
963
|
+
|
|
964
|
+
旧实现是「客户端 `host.call('checkBalance')` → 宿主半包 inject `deepseekAccount`」。
|
|
965
|
+
宿主半包那份代码写得没错,服务也真的存在(`@deepseek-ai/dsh-deepseek-account`,
|
|
966
|
+
由 `dsh-base` 注册),但**bundle 形态的客户端半包根本没有 host RPC 座位** ——
|
|
967
|
+
那个座位只发给动态插件。于是真机上它永远走"这个壳里查不了余额"那一句,
|
|
968
|
+
而预检里那条路是拿假座位测的,全绿。
|
|
969
|
+
|
|
970
|
+
现在客户端直接调平台自己的账户 Remote(平台自己的「账户」设置页用的就是它):
|
|
971
|
+
|
|
972
|
+
```js
|
|
973
|
+
ctx.remote.account.getBalance({ version, locale, timezoneOffsetSeconds })
|
|
974
|
+
-> { ok: true, value } | { ok: false, reason }
|
|
975
|
+
value = { status:'ready', value:[wallets], bonusWallets:[…] } | { status:'failed' } | null
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
`value` 与宿主半包那条路的载荷**完全同形**,所以只在取数那一层分叉,归一化与台词完全共用。
|
|
979
|
+
`null` 依然是"没登录"与"刚才没读到"共用的一个值,照旧**再问一次 `getState()`** 才敢说话。
|
|
980
|
+
|
|
981
|
+
| | 什么时候用 | 凭据在哪 |
|
|
982
|
+
|---|---|---|
|
|
983
|
+
| `remote.account` Remote | **默认**(bundle 安装) | 宿主,插件只能拿到归一化结果 |
|
|
984
|
+
| host 座位(`host.call`) | 动态插件形态(那里才有座位) | 同上 |
|
|
985
|
+
| 两条都没有 | 如实说"这个壳里我查不了余额" | —— |
|
|
986
|
+
|
|
987
|
+
`doctor().balance` 现在报 `hostSeat` / `remoteSeat` / `via`(这次真的走了哪条),
|
|
988
|
+
所以"为什么查不到"在 Console 里一眼能定位。
|
|
989
|
+
|
|
990
|
+
### 1.45.3
|
|
991
|
+
|
|
992
|
+
**删掉两条身体层的程序动画:`attn` 的左右轻推(`wisp-nudge`)与 `happy` 的上下跳(`wisp-hop`)。**
|
|
993
|
+
|
|
994
|
+
两条都靠 `translate` 挪整个 `.wisp-body`,而这一层同时承载帧动画与交叉淡入 —— 位移叠上去
|
|
995
|
+
不像"她在动",更像素材在抖。1.45.0 的帧动画、1.45.2 的"画面同时只归一个人",说的都是同一件事。
|
|
996
|
+
|
|
997
|
+
| 层 | 之前动什么 | 现在动什么 |
|
|
998
|
+
|---|---|---|
|
|
999
|
+
| `.wisp-body` | `happy` 跳 / `attn` 左右推 / `worried` 抖 | **只剩 `worried` 那一下**(`wisp-worry`,上下 5px、三下) |
|
|
1000
|
+
| `.wisp-motion` | 按下 / 落地 / 换表情弹一下 | 不变 |
|
|
1001
|
+
| `.wisp-lean` | 拖动侧倾 / 朝指针侧身 / 溜达前倾 | 不变 |
|
|
1002
|
+
|
|
1003
|
+
`worried` 留着不是漏删:它带着"出事了"的语义,和这两条装饰性的晃动不是一回事。
|
|
1004
|
+
|
|
1005
|
+
同时清掉两条早已失效的 `animation-duration`(`sleep` 9s / `alert` 4.6s)—— 它们是给
|
|
1006
|
+
1.45.1 删掉的常驻浮动调速度用的,没有动画名可调,留着只会让人以为身体层还在动。
|
|
1007
|
+
|
|
1008
|
+
换表情**仍然会弹一下**(`.wisp-motion` 层,`data-accent`),所以 `happy` / `attn` 进来时
|
|
1009
|
+
她照样有回应;少的是那两段持续的晃动。
|
|
1010
|
+
|
|
1011
|
+
### 1.45.2
|
|
1012
|
+
|
|
1013
|
+
**修掉睡觉时的重影:帧动画一播,静态立绘就退场。**
|
|
1014
|
+
|
|
1015
|
+
1.45.0 把那段 alpha 视频**叠在**立绘上,并且刻意不隐藏立绘(当时的理由:"万一下视频不播还有兜底")。
|
|
1016
|
+
实际效果是**两层同时可见**:视频的透明处挡不住下面那张静帧,被子那点起伏被看成两张错位的画
|
|
1017
|
+
(用户确认看到了重影)。
|
|
1018
|
+
|
|
1019
|
+
现在画面在**同一时刻只属于一个人**:
|
|
1020
|
+
|
|
1021
|
+
| 情形 | 视频 | 静态立绘 |
|
|
1022
|
+
|---|---|---|
|
|
1023
|
+
| 正常播放(`sleep` 且未冻结) | 显示、播放 | **隐藏** |
|
|
1024
|
+
| 冻结(`motion:'off'` / `prefers-reduced-motion` / 被躲起来) | 隐藏(沿用 `pause()` + `currentTime = 0` + `display:none`) | **显示**(这时它才是画面里那个人) |
|
|
1025
|
+
| 视频加载/解码失败(`error`) | 隐藏 | **显示**(静默兜底) |
|
|
1026
|
+
| 该状态没有动作素材 | 不存在 | 显示(不变) |
|
|
1027
|
+
|
|
1028
|
+
实现只有**一个闸门**:`syncMotion()` 判完"这一刻该不该动"之后,顺手决定画面归谁 ——
|
|
1029
|
+
视频真的上了画面就写 `root.dataset.frame = "on"`,其余每一种情况都把这个属性删掉。
|
|
1030
|
+
样式表里一条规则把属性翻译成画面:
|
|
1031
|
+
|
|
1032
|
+
```css
|
|
1033
|
+
.wisp-root[data-frame="on"] .wisp-img{visibility:hidden}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
用 `visibility` 而不是 `display:none`:盒子照旧占位,**布局一点不动**,"退场/回场"不会让她跳一下
|
|
1037
|
+
(视频那一层正是靠同一套盒模型对齐的,动布局就是动对齐)。属性走 CSS,是因为它盖得住**交叉淡入**
|
|
1038
|
+
留下的那张旧图;行内再镜像一份,给样式表缺失的壳兜底。`doctor().motion.frame.spriteHidden`
|
|
1039
|
+
会报这一刻画面归谁 —— 重影就是"两个都可见",所以这件事必须能被问一次。
|
|
1040
|
+
|
|
1041
|
+
**验证**:预检 713 → **723 项**,其中新增的 **10 条**盯这一版("在播时立绘 `visibility:hidden`"、
|
|
1042
|
+
"冻结时它**可见**,不只是还在 DOM 里"、"`error` 之后恢复可见"、"躲起来时画面交还立绘"、
|
|
1043
|
+
"播放 → 冻结 → 播放 跟着来回切"、"没有素材时属性不许挂着",以及样式表那条属性 → 画面的映射)。
|
|
1044
|
+
真引擎探针补了 3 条:真 Chromium 里读**计算后**的 `visibility`(在播 `hidden` / 冻结 `visible` /
|
|
1045
|
+
`error` 注入后 `visible`)—— 假 DOM 证明不了级联。
|
|
1046
|
+
|
|
1047
|
+
### 1.45.1
|
|
1048
|
+
|
|
1049
|
+
**删掉 wisp-bob(站立时的上下浮动)。**
|
|
1050
|
+
|
|
1051
|
+
原来的实现是给 .wisp-body 挂一条 5.6 秒的 CSS 浮动动画 —— 它会和新的帧动画叠在一起动(两层各自上下起伏,看起来很飘)。现在身体层不再有任何动画:站立时是静止的立绘,动起来交给帧动画(本期只有 sleepy)。
|
|
1052
|
+
|
|
1053
|
+
引擎探针里那条断言也随之改成「身体层不再有动画」(odyAnim === 'none')。
|
|
1054
|
+
|
|
1055
|
+
### 1.45.0
|
|
1056
|
+
|
|
1057
|
+
**她动起来了:第一条真正的帧动画 —— 睡着时那段 alpha 视频循环。**
|
|
1058
|
+
|
|
1059
|
+
> **这一段已被 1.46.1 取代**(下面保留为当时的记录):素材从 VP9 alpha 视频换成**动图 WebP**,
|
|
1060
|
+
> 元素从 `<video>` 换成 `<img>`,"冻结"从 `pause() + currentTime = 0` 改成**藏起动图、把画面
|
|
1061
|
+
> 交回立绘**,`colorkey` 的 similarity 从 0.26 收紧到 0.10。数字(315.8 KB / 421.1 KB / 800 KB
|
|
1062
|
+
> 预算 / `playing` 字段)也都是那一版的。
|
|
1063
|
+
|
|
1064
|
+
到这一版之前,她的"动作"全是**位移/缩放/旋转**:立绘本身一动不动。这一版给 `sleep`
|
|
1065
|
+
状态叠了一段 `<video>`:她睡着时被子那点极小的起伏,4.04 秒无缝循环。
|
|
1066
|
+
|
|
1067
|
+
| 事项 | 实情 |
|
|
1068
|
+
|---|---|
|
|
1069
|
+
| 素材 | `assets/motion/sleepy.webm` —— VP9 + **alpha 平面**(`AlphaMode=1`)、480×854、24 fps、4.04 秒循环。同一套立绘的睡眠姿做的图生视频,角落是**透**的 |
|
|
1070
|
+
| 体积 | 原始 **315.8 KB** → 内联成 data URI **421.1 KB**。预检里有一条 **800 KB 预算**盯着它(超了就在详情里报出实际大小) |
|
|
1071
|
+
| 为什么是视频 | 同一段动作,视频比逐帧序列少一个数量级的字节,而 alpha 视频在 Chromium 里是原生解码。**代价是它必须有一条会动的播放通道** —— 所以冻结规则不是可选项 |
|
|
1072
|
+
|
|
1073
|
+
**1. 叠加,不是替换。** `<video>` 和立绘在**同一个盒子**里(`.wisp-lean` 层,逐条复用
|
|
1074
|
+
`.wisp-img` 的 `inset:0 / 100% / contain / bottom center`),`pointer-events:none` ——
|
|
1075
|
+
她能不能被点到,仍然只由那张 alpha 蒙版生成的剪影命中层决定,透明处照旧整层穿透。
|
|
1076
|
+
字节走的是**和立绘同一条** data URI → Blob → object URL 通道(这个壳把 `data:` 源当坏图
|
|
1077
|
+
处理,视频没有理由不一样),失败时静默降级:藏掉视频、继续用静态立绘,不抛不冒泡。
|
|
1078
|
+
|
|
1079
|
+
**2. 冻结规则(硬要求):"停住"不是"慢一点"。**
|
|
1080
|
+
`cfg.motion === 'off'`、`prefers-reduced-motion: reduce` 命中,或者她被**躲起来**时:
|
|
1081
|
+
|
|
1082
|
+
```
|
|
1083
|
+
pause() → currentTime = 0 → display: none
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
三件事一件都不能少。少了 `currentTime = 0`,静止档里的她会定格在循环的半途 ——
|
|
1087
|
+
同一个"静止",每次按下开关停的地方都不一样。恢复时同一个元素接着播(不重建)。
|
|
1088
|
+
系统设置**在会话中途**改了也当场生效:CSS 的媒体查询是实时的,而播放状态是 JS 的,
|
|
1089
|
+
所以插件订阅了 `matchMedia` 的 change。
|
|
1090
|
+
|
|
1091
|
+
**3. 不白解码。** 没有动作素材的状态**连 `<video>` 都不建**(挂载时不碰这 316 KB),
|
|
1092
|
+
`mood('sleep')` 时才建一个;`doctor().motion.frame` 会报 `asset / built / playing / frozen / failed`
|
|
1093
|
+
—— 帧动画的失败是**静默**的(画面回到静态立绘),所以它必须能被一眼看出来。
|
|
1094
|
+
|
|
1095
|
+
**验证**:预检 685 → **713 项**,其中新增的 **28 条**盯这一版(含"静止档真的 `pause()` 且 `currentTime` 为 0"、
|
|
1096
|
+
"reduced-motion 下同样冻结"、"视频带 `pointer-events:none`"、"和立绘的行内盒模型逐条相同"、
|
|
1097
|
+
"800 KB 预算"、"内联的字节和磁盘上那份一样长")。真引擎探针也补了一节 ——
|
|
1098
|
+
**假 DOM 证明不了这段视频解得开**:实测 `480x854 readyState=4`、四角 alpha `0/0/0/0`
|
|
1099
|
+
(alpha 平面真的出画了)、与立绘的盒子偏差 `0.00,0.00`、会话中途打开 reduced-motion 当场停住。
|
|
1100
|
+
|
|
1101
|
+
### 1.43.2
|
|
1102
|
+
|
|
1103
|
+
**台词只说人话;"为什么查不成"排进弹窗。**
|
|
1104
|
+
|
|
1105
|
+
1.43.1 把诊断直接拼进了台词 —— 真机上的效果是一句话后面挂一串
|
|
1106
|
+
`ctx.get("web")→undefined; ctx.web→undefined`,读起来像乱码(用户的原话:
|
|
1107
|
+
"还说一串英文")。现在:
|
|
1108
|
+
|
|
1109
|
+
- **台词回到一句人话**:「这个壳里我没有联网的通道,查不了更新。」
|
|
1110
|
+
- **技术细节排进「关于她」弹窗**:取服务的方式、拿到的东西是什么形状、兜底通道
|
|
1111
|
+
为什么也不行 —— 查失败时自动展开,可以看、可以复制、可以追问。
|
|
1112
|
+
|
|
1113
|
+
预检 680 → **682 项**,其中一条断言"台词里不许出现英文碎片"(失败台词与弹窗分工,
|
|
1114
|
+
以后再有人往句子里塞 `ctx.get(...)` 就会红)。
|
|
1115
|
+
|
|
1116
|
+
### 1.43.1
|
|
1117
|
+
|
|
1118
|
+
**检查更新在"真的拿不到 web 服务"的壳里也能用了 —— 而且失败时说得清为什么。**
|
|
1119
|
+
|
|
1120
|
+
背景:1.43.0 装好、宿主半包也重启之后,她的原话仍是
|
|
1121
|
+
「我是 1.43.0。这个壳里我没有联网的通道,查不了更新。」—— 也就是说
|
|
1122
|
+
`ctx.get('web')` 与 `ctx.web` **两条路都拿不到可用的服务**。于是做了两件事:
|
|
1123
|
+
|
|
1124
|
+
1. **加一条兜底通道**:`pluginManager.inspect('dsh-wisp')`。插件管理器本来就是
|
|
1125
|
+
"安装之前去 registry 问这个包是什么版本"的那个人 —— 所以这条路走的是**应用自己**
|
|
1126
|
+
的网络出口,还尊重用户配的镜像源,而且它是平台文档里写明的宿主服务
|
|
1127
|
+
(`ctx.get('pluginManager')`)。web 服务不可用时自动走它,`diag.via` 会写
|
|
1128
|
+
`pluginManager`。**它比插件自己去抓 packument 更接近"装的时候会装到哪一版"。**
|
|
1129
|
+
2. **失败必须能追问**:`resolveWebService()` 现在返回 `{ service, how, why }`,
|
|
1130
|
+
"为什么拿不到"会跟着失败台词一起说出来:
|
|
1131
|
+
|
|
1132
|
+
> 这个壳里我没有联网的通道,查不了更新。(诊断:通道=none;ctx.get("web")→undefined; ctx.web→undefined)
|
|
1133
|
+
|
|
1134
|
+
一句话就能定案,不需要谁再去开控制台。顺带把"有服务但没有 `fetch`"
|
|
1135
|
+
(`web-has-no-fetch`)从"没有通道"里分出来 —— "平台没给服务"和"服务没暴露这个方法"
|
|
1136
|
+
是两件事,下一步动作也不同。
|
|
1137
|
+
|
|
1138
|
+
预检 673 → **680 项**,其中一条是端到端的:**没有 web 服务、只有插件管理器时,
|
|
1139
|
+
检查更新照样给出答案,并注明走的是哪条通道。**
|
|
1140
|
+
|
|
1141
|
+
### 1.43.0
|
|
1142
|
+
|
|
1143
|
+
**一批"真的用鼠标从头点了一遍"才发现的问题。**
|
|
1144
|
+
|
|
1145
|
+
| # | 问题 | 修法 |
|
|
1146
|
+
|---|---|---|
|
|
1147
|
+
| 1 | **拨完开关,子面板消失** —— 而代码注释里明确写着"拨个开关面板就消失是最烦人的交互之一" | `refreshSurfaces()` 重画前先记下 `pinned` 再原样传下去(重画的 DOM 里鼠标并没有真的悬停过,"悬停态"重开必然被宽限期关掉) |
|
|
1148
|
+
| 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 走整数步到达** |
|
|
1149
|
+
| 3 | **更新提示语写死"按包名安装"** —— 新版只在 GitHub 上时那句话会把人送回旧版 | 她按 `from`/`sources` 判断:只有 GitHub 更新时改口,并把**仓库地址**放进剪贴板(`doctor().update.hint` 能看到用了哪条) |
|
|
1150
|
+
| 4 | npm 源写死 `registry.npmjs.org`,镜像用户可能两个源一起废 | 宿主半包的 `config.urls` 本来就支持换源 —— 现在**写进文档**并加断言:换镜像真的只会去问新地址 |
|
|
1151
|
+
| 5 | 触摸屏上手指划过也在驱动力姿势,抬手后姿势冻住 | 触摸一律归零(手指既不是"靠近"也不是"路过";该有的反馈是"按下压一下") |
|
|
1152
|
+
| 6 | 指针移出窗口后她一直朝最后那个方向歪着 | `blur` 只覆盖"切走应用",补一个 document 级 `mouseleave` |
|
|
1153
|
+
| 7 | 被边界夹住的拖动仍在"原地歪" | 位置没变就把姿势归零(她其实没动) |
|
|
1154
|
+
| 8 | `motion: off` 与 `prefers-reduced-motion` **挡不住溜达** | CSS 关不掉 JS 写进去的坐标 —— 溜达调度器一起停 |
|
|
1155
|
+
| 9 | 静止档下 "z" 停成满不透明,像一个卡住的字 | 静止版本给 0.5 透明度;减弱动画那档同样处理 |
|
|
1156
|
+
| 10 | 脚底光晕挂在姿势层,她一倾斜连"地板"一起歪 | 移到 motion 层:跟着压扁(那是她压在地上),不跟着侧身 |
|
|
1157
|
+
|
|
1158
|
+
**评估后决定不改的一条**:`.wisp-tab` 那颗迷你按钮是 56×56 的**可见胶囊**(有边框、底色、阴影),整块就是按钮本身 —— 命中区和用户看到的一致,不属于"透明边距吃点击"那一类。
|
|
1159
|
+
|
|
1160
|
+
预检 654 → **673 项**:上面每一条都配了"再犯就会露出来"的断言,包括"拨开关后面板还在""默认值可达""静止档真的不动"这三条行为级的。
|
|
1161
|
+
|
|
525
1162
|
### 1.42.0
|
|
526
1163
|
|
|
527
1164
|
**查更新失败时,她会直接说清是哪一种失败。**
|
|
@@ -1234,7 +1871,7 @@ v1.31.0 · 深海女仆
|
|
|
1234
1871
|
```
|
|
1235
1872
|
关于她…
|
|
1236
1873
|
动作一览…
|
|
1237
|
-
▸ 外观
|
|
1874
|
+
▸ 外观 6 套皮肤 + 放大 / 缩小
|
|
1238
1875
|
▸ 行为 说句话 / 睡一会儿 / 自己溜达 / 跟随状态 / 跑完撒花 / 饿了会说话
|
|
1239
1876
|
▸ 位置 四个角落
|
|
1240
1877
|
版本与更新
|