dsh-pet-live2d 3.0.0 → 3.1.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/CHANGELOG.md +162 -0
- package/lib/client.js +72 -13
- package/lib/desktop.js +5 -1
- package/lib/display.js +39 -4
- package/package.json +2 -2
- package/pets/ds-whale-girl/pet.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,167 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 3.1.0(2026-09-29)
|
|
4
|
+
|
|
5
|
+
这一轮的主线是**桌面端**:托盘的"没有她"查到底了 —— 根因是本项目自己给产物贴错了
|
|
6
|
+
完整性标签(详见下面「完整性标签」那一节);同时修好了设置两边不同步、插件"以为已经有一只"、
|
|
7
|
+
独立模式页面把自己藏起来,并加上 macOS(arm64)支持。
|
|
8
|
+
|
|
9
|
+
### 修:桌面端的设置与 DSH 里的不一致(同一类"两份宿主分叉")
|
|
10
|
+
|
|
11
|
+
设置原来只走 `localStorage`(浏览器按 origin 隔离 ✗),所以共享存档必须走
|
|
12
|
+
`%DSH_HOME%\pet-settings.json` + `GET|POST /api/live2d-pet/settings` —— 而**桌面端的 Rust
|
|
13
|
+
宿主没有实现这条路由**(JS 侧 `lib/settings.js` 的注释里甚至写着"Rust 侧的对应实现见
|
|
14
|
+
`host/settings.rs`",而那个文件当时并不存在)。页面取不到就只能退回它自己那份 localStorage,
|
|
15
|
+
于是两个窗口各显示各的。
|
|
16
|
+
|
|
17
|
+
现在补上 `host/settings.rs`(与 `lib/settings.js` 逐字对应:三个键 `tuning`/`overrides`/
|
|
18
|
+
`outfit`、合并写、**空写不推进 `rev`**(防两端互推)、坏数据只取形状对的部分)与那条路由。
|
|
19
|
+
实测:GET 空存档 → POST `tuning`(rev 1)→ POST `outfit`(rev 2 且 tuning 不被擦掉)→ 落盘一致。
|
|
20
|
+
|
|
21
|
+
### 修:插件拉起的那只死了以后,插件一直以为"已经有一只了"
|
|
22
|
+
|
|
23
|
+
`lib/display.js` 只在 `stop()` 里清 `child`/`childPid`,**子进程自己退出(被杀 / 崩了)时不清**
|
|
24
|
+
—— 于是 `reconcile()` 永远认为"已经有一只了",既不重拉,设置里还显示"桌面已接管",而桌面上
|
|
25
|
+
什么都没有。现在给子进程挂了 `exit` 处理器,并在 `start()` 里加了一条自愈(记着的 pid 已经不在、
|
|
26
|
+
心跳也不新鲜 ⇒ 忘掉它再拉)。(用户 2026-09 报的"设置说已接管但看不到宠物"有一半是这个。)
|
|
27
|
+
|
|
28
|
+
### 新:托盘的**自证与诊断**(写给"托盘里没有她"这类问题)
|
|
29
|
+
|
|
30
|
+
`TrayIconBuilder::build()` 返回 Ok **不代表图标进了通知区域** —— `tray-icon` 在
|
|
31
|
+
`Shell_NotifyIcon(NIM_ADD)` 失败时是**静默忽略**的(源码注释说"等 Explorer 发 TaskbarCreated
|
|
32
|
+
再重注册",而那个广播只有资源管理器重启时才有)。现在启动后会自证一次
|
|
33
|
+
(`TrayIcon::rect()` = `Shell_NotifyIconGetRect`)并写日志;拿不到矩形时还会**绕开库手工调一次**
|
|
34
|
+
`Shell_NotifyIcon`(用她自己的窗口 + 系统图标),把返回值和 `GetLastError` 写进
|
|
35
|
+
`%DSH_HOME%\pet-desktop.log`。注意:**矩形拿不到不等于失败**(藏在溢出区里的图标也拿不到),
|
|
36
|
+
所以这条日志是"信息"而不是"失败"。
|
|
37
|
+
|
|
38
|
+
### 改:Windows 产物名改成 ASCII(`DSH-Pet.exe`)
|
|
39
|
+
|
|
40
|
+
与 Release 资产 / npm 子包那两份(本来都是 ASCII)保持一致,少一个变量。
|
|
41
|
+
|
|
42
|
+
### 修:手动启动的桌面端"设置里显示已接管,但看不到宠物"
|
|
43
|
+
|
|
44
|
+
**独立模式**(自己双击 exe / 不走插件)下,窗口起来了、canvas 也在画,但**页面把自己藏了**:
|
|
45
|
+
客户端的判据是"owner 不是我 ⇒ `visibility: hidden`",而它读的是 `GET /api/live2d-pet/layer`
|
|
46
|
+
—— 桌面端的 Rust 宿主**没有这个路由**(它只有自己的 `/__desktop/owner`),页面取不到就保持
|
|
47
|
+
默认 `owner: "inline"` ⇒ 桌面壳那一份按规则让位 ⇒ 什么都看不到(托盘图标又默认被 Win11 收进
|
|
48
|
+
溢出区,所以连"她还在"都看不出来)。
|
|
49
|
+
|
|
50
|
+
挂载模式(插件从设置里拉起,`--attach`)不受影响:那条路把整个 API 转发给了 DSH 里的插件,
|
|
51
|
+
而插件那份 JS 宿主是有 `/layer` 的 —— 这就是"插件起的桌面是好的、手动双击看不到"的全部原因。
|
|
52
|
+
|
|
53
|
+
修法:Rust 宿主补上 **`GET|POST /api/live2d-pet/layer`**(与 JS 宿主同形:`owner` / `mode` /
|
|
54
|
+
`desktopRunning` / `binary` / `download`;POST 支持改「显示位置」),`/__desktop/owner` 保留
|
|
55
|
+
成同形的别名(已有驱动在读它)。
|
|
56
|
+
|
|
57
|
+
> 这条属于项目最老的那条纪律:**宿主半区有两份实现,路由形状必须一起改**。客户端某一版
|
|
58
|
+
> 把读口从 `/__desktop/owner` 改成了 `{API}/layer`,Rust 那边没跟上。
|
|
59
|
+
|
|
60
|
+
### 修:**本地构建**出来的 exe 双击起不来(完整性标签,2026-09 用户报的第三次)
|
|
61
|
+
|
|
62
|
+
症状:双击 exe 弹一个错误框,里面是 `0x800700AA 请求的资源在使用中`(WebView2),
|
|
63
|
+
同时 `%DSH_HOME%` 里既没有 `pet-desktop.log`、偏好也没被改写 —— 而在 DSH 设置里
|
|
64
|
+
让插件拉起的那只一切正常。
|
|
65
|
+
|
|
66
|
+
根因是 **Windows 的完整性标签(integrity level)**:**在 DSH 会话里写出来的文件会带
|
|
67
|
+
`Low` 标签**(沙箱给工作区新文件打的就是它),而本地构建的那个 exe 正好产在那儿。
|
|
68
|
+
被打上 Low 标签的 exe,双击起来后进程就是 Low 完整性 —— 按 no-write-up 规则,
|
|
69
|
+
**它写不进任何 Medium 对象**:`%DSH_HOME%`(`拒绝访问 (os error 5)`)与
|
|
70
|
+
`%LOCALAPPDATA%\<id>\EBWebView`(WebView2 建不起来)全都中招。
|
|
71
|
+
插件从 npm 下载的那份(普通进程写的、没有标签)因此完全正常 —— 同一个版本、同一个
|
|
72
|
+
二进制,只差一个标签。
|
|
73
|
+
|
|
74
|
+
修法(构建期,两道):
|
|
75
|
+
|
|
76
|
+
- `tools/integrity.mjs`:检查产物的完整性标签,**带 Low/Medium 就摆成 `High`**(= 不设上限)
|
|
77
|
+
并复查,摆不正直接让构建失败;
|
|
78
|
+
- 接进 `build-portable.mjs`(产物出库)与 `npm-prepare-subpackage.mjs`(发 npm 前),
|
|
79
|
+
所以**发布物不可能再带压低进程的标签**。
|
|
80
|
+
|
|
81
|
+
⚠️ 这里有个**曾经的错误修法**:一开始是摆成 `Medium`,结果在"资源管理器跑在 High"的机器上
|
|
82
|
+
(UAC 关闭 ⇒ 整机 High)**连托盘都注册不上**(`Shell_NotifyIcon` 返回 FALSE、
|
|
83
|
+
`GetLastError=5`)—— 因为完整性标签是**上限**,Medium 会把进程压在 Medium,
|
|
84
|
+
它对 shell 的托盘消息被 UIPI 拦掉。**正确做法是没有标签,或者摆成 High。**
|
|
85
|
+
|
|
86
|
+
已经拿到带标签版本的用户:`icacls "<exe>" /setintegritylevel High` 一条命令即可修好
|
|
87
|
+
(或者重新下载一遍发布包)。
|
|
88
|
+
|
|
89
|
+
### 修:直接双击桌面端的 exe,她"没显示出来"(2026-09 用户报)
|
|
90
|
+
|
|
91
|
+
症状:双击 GitHub Release 里那个 exe,**什么都没发生**;但插件从设置里拉起来的那只一切正常。
|
|
92
|
+
|
|
93
|
+
根因不是 exe 坏了,是**显示层的偏好**:设置里选的是「页面内」时,桌面端按规则必须让位 ——
|
|
94
|
+
它起来、刷心跳、读到 `mode: "inline"`,然后**1 秒内把自己藏了**(进程其实活着,托盘图标也
|
|
95
|
+
在 Win11 的溢出区里)。而插件那条路会先把偏好写成「桌面」再拉起,所以它看着是好的。
|
|
96
|
+
|
|
97
|
+
现在:**手动启动(不是插件拉起的)遇到「页面内」偏好,就把它切成「桌面」** —— 双击这个动作
|
|
98
|
+
本身就是"我要她在桌面上"。写的是同一个共享文件,所以页面里那只立刻让位,仍然只有一只;
|
|
99
|
+
想改回来,去 DSH 设置里选「页面内」。
|
|
100
|
+
|
|
101
|
+
顺带把"什么都看不到"这件事本身修了(这才是真正难查的地方):
|
|
102
|
+
|
|
103
|
+
- **启动日志**:`%DSH_HOME%\pet-desktop.log`(同时照旧打 stderr),记下**是哪个 exe**、
|
|
104
|
+
显示层偏好、"显示 / 让位"的结论与原因;写不进 `%DSH_HOME%` 时**退到 exe 旁边**再写一份,
|
|
105
|
+
两处都写不进去就把原因带进弹框(以前这种写失败是静默的,用户第二次报的就是它:
|
|
106
|
+
弹框指的日志文件压根没生成);
|
|
107
|
+
- **起不来会弹框**:GUI 程序没有控制台,以前 panic 是彻底静默的。现在 Windows 上会弹一个
|
|
108
|
+
错误框,写明**程序路径** + 日志路径(macOS 上只有日志)。
|
|
109
|
+
|
|
110
|
+
### 修:已经有一只时,第二只不再报"起不来"
|
|
111
|
+
|
|
112
|
+
WebView2 的 user-data-dir 是**独占**的,第二个实例只会撞 `0x800700AA 请求的资源在使用中`,
|
|
113
|
+
而那个错误以前被 panic 成一个吓人的报错框。现在:启动时先看**心跳**(不是 pid —— 系统会重用
|
|
114
|
+
pid),已经有一只活着就**安静退出**并在手动双击时提示"她已经站在桌面上了(Win11 托盘图标
|
|
115
|
+
默认收在溢出区)"。
|
|
116
|
+
|
|
117
|
+
同时补了两条:
|
|
118
|
+
|
|
119
|
+
- **建窗失败会自动重试 3 次**(每次隔 1.5 秒):上一只刚退出、目录还没放开是常见情形,
|
|
120
|
+
等一下通常就好了,没必要让用户看报错;重试过程每次都写进日志。
|
|
121
|
+
- **插件收掉桌面端时连整棵进程树一起收**(Windows 上 `taskkill /T`):`child.kill()` 只杀壳,
|
|
122
|
+
WebView2 的 `msedgewebview2.exe` 子进程会留下来占着数据目录 —— 那正是"下一次启动撞
|
|
123
|
+
0x800700AA"的常见来源。
|
|
124
|
+
|
|
125
|
+
### 新:macOS(Apple Silicon)的桌面端构建
|
|
126
|
+
|
|
127
|
+
以前桌面端只有 Windows。现在 macOS arm64 也能出产物了 —— 由 GitHub Actions 构建
|
|
128
|
+
(`.github/workflows/desktop-mac.yml`,`macos-14` runner),一次产出两样:
|
|
129
|
+
|
|
130
|
+
| 产物 | 给谁用 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `DSH桌宠.app`(zip) | 人:双击就站出来 |
|
|
133
|
+
| `dsh-pet-live2d-desktop`(tar.gz,裸二进制) | 插件:npm 平台子包 `dsh-pet-live2d-desktop-darwin-arm64` 要的那份 |
|
|
134
|
+
|
|
135
|
+
为此改的东西(都在桌面端那一侧):macOS 上**读全局光标**改走 CoreGraphics(透明层"哪块
|
|
136
|
+
区域吃鼠标"的判定靠它,以前非 Windows 直接返回"读不到",等于她永远点不到);主目录按
|
|
137
|
+
平台取(macOS 是 `HOME`,以前只认 `USERPROFILE`,会退化成当前目录下的 `.dsh`);macOS 用
|
|
138
|
+
`ActivationPolicy::Accessory` 藏进"不进程序坞"(Windows 的 `skip_taskbar` 在 mac 上没有
|
|
139
|
+
实现);`.app` 里不再往包内写运行期数据(那会破坏代码签名)。
|
|
140
|
+
|
|
141
|
+
**没验过的地方,别当既成事实**:本项目所有驱动都跑在 Windows 上,macOS runner 又没有可
|
|
142
|
+
交互的窗口会话 —— "她能不能点、能不能穿透、跟不跟手"目前**只有代码层面的理由**,没有
|
|
143
|
+
实测。Intel Mac 与 Linux 都还没有构建。
|
|
144
|
+
|
|
145
|
+
### 注意:macOS 那份没有签名
|
|
146
|
+
|
|
147
|
+
第一次打开会被 Gatekeeper 拦("无法验证开发者"):右键→打开,或者
|
|
148
|
+
`xattr -dr com.apple.quarantine <路径>`。想双击就能开,需要 Apple Developer ID($99/年)
|
|
149
|
+
+ 公证 —— 那时 CI 里能一起做,但证书得先有。
|
|
150
|
+
|
|
151
|
+
## 3.0.1
|
|
152
|
+
|
|
153
|
+
### 修:「自拍」右手不抬(3.0.0 引入)
|
|
154
|
+
|
|
155
|
+
3.0.0 那一版把"保姿势录像"里的一道豁免撤掉了,于是自拍时抬手的那条参数每帧又被压回 0,
|
|
156
|
+
**手一直垂着**。3.0.1 把它修回来,并按"动作需要就每帧写值"的正解加固:
|
|
157
|
+
|
|
158
|
+
自拍的抬手参数(`phone5`)在动作文件里的曲线是 **0 → 9.7 → 0**,也就是说光靠曲线抬不起来;
|
|
159
|
+
现在由动作自己**每帧按住**一个明确的值(写在引擎更新之后,不会再被曲线或回放层盖掉),
|
|
160
|
+
动作一结束就松开,不会留下永久钉住的状态。
|
|
161
|
+
|
|
162
|
+
判据不再只看"参数值在不在跑"(那个骗过我三次),而是**像素级核对画面**:自拍峰值那一帧
|
|
163
|
+
与"掏出手机定格"必须有实质差异 —— 手机从桌面举到了脸边。
|
|
164
|
+
|
|
3
165
|
## 3.0.0
|
|
4
166
|
|
|
5
167
|
**桌面端正式随插件一起发布。** 装完插件、在设置里选「桌面」,她就在桌面上——
|
package/lib/client.js
CHANGED
|
@@ -260,6 +260,19 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
260
260
|
*/
|
|
261
261
|
let keptPoses = [];
|
|
262
262
|
const poseSnapshots = new Map();
|
|
263
|
+
/**
|
|
264
|
+
* **动作自己要的参数:每帧按住**(`motionOptions.<组>.holdParams = { id: 值 }`)。
|
|
265
|
+
*
|
|
266
|
+
* 和 `preset` 的区别就是"每帧":`preset` 只写一次,之后会被动作曲线与保姿势回放
|
|
267
|
+
* 盖掉(实测自拍播放中 `phone5` 恒为 0)。抬手这类"动作需要、但它自己又写不对"
|
|
268
|
+
* 的参数只能每帧按住。跟着 `currentEntry` 一起失效,不会留下永久钉住的状态。
|
|
269
|
+
*/
|
|
270
|
+
let actionHolds = null;
|
|
271
|
+
/**
|
|
272
|
+
* 诊断:"每帧按住"那次写入之后,`phone5` 在模型里是多少、写了多少次、有没有抛错。
|
|
273
|
+
* 用它区分"没执行"与"执行了但之后被覆盖"—— 我在这两者之间来回猜了两轮。
|
|
274
|
+
*/
|
|
275
|
+
let holdProbe = null;
|
|
263
276
|
/** 参数名 -> 下标 的缓存(见 parameterIndex)。 */
|
|
264
277
|
const paramIndexCache = new Map();
|
|
265
278
|
/** ~5 秒 @60fps,够盖住这只模型 4 秒的待机循环。 */
|
|
@@ -1050,30 +1063,29 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1050
1063
|
*
|
|
1051
1064
|
* 写在 applyRelease **之后**(否则会被还原表顶掉),表达式层之前(表情仍然最大)。
|
|
1052
1065
|
*
|
|
1053
|
-
* ⚠️
|
|
1054
|
-
* 自拍(`Selfie`)驱动 `phone5
|
|
1066
|
+
* ⚠️ **动作曲线碰过的参数不能被保姿势录像压住**(这一条修的是用户报的"自拍右手不抬"):
|
|
1067
|
+
* 自拍(`Selfie`)驱动 `phone5`(抬手,在干净试验台里确认过:推 `phone5` 会让
|
|
1055
1068
|
* `看手机/ArtMesh26` 那块几何变形),但「掏出手机」定格时录下的那一帧里 `phone5=0`,
|
|
1056
1069
|
* 于是这一层每帧把它写回 0 —— 写在动作更新之后,**曲线被压住**。干净环境实测:
|
|
1057
1070
|
* 自拍里 `phone5` 涨到 9.713,而手那块几何一动不动(盒完全相同)。
|
|
1058
1071
|
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1072
|
+
* 所以:录像里那些**当前动作曲线碰过**的参数跳过不写,让动作说了算;
|
|
1073
|
+
* 其余(掏出手机自己的 `phone`/`phone2`/`phone4`/`phone6` 之类)照旧保姿势。
|
|
1074
|
+
*
|
|
1075
|
+
* ⚠️ **别把它换成"宠物声明的名单"**:我在 3.0.0 那版换成了
|
|
1076
|
+
* `motionOptions.<组>.ignoreKeptParams`,结果用户立刻报"自拍又被修坏了" ——
|
|
1077
|
+
* 因为 `OpenCase` 的曲线清单本来就含 `phone5`,换名单等于**把这道豁免撤掉**。
|
|
1078
|
+
* 这条判据要看的是"动作碰过哪些参数",`currentEntry.params` 正是这个语义。
|
|
1065
1079
|
*/
|
|
1066
1080
|
const applyKeptPoses = (core, values) => {
|
|
1067
1081
|
if (keptPoses.length === 0 || values === null) return;
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
: optionsFor(currentEntry.group)?.ignoreKeptParams ?? null;
|
|
1071
|
-
const skipSet = Array.isArray(skip) ? new Set(skip) : null;
|
|
1082
|
+
const owned = currentEntry === null ? null : currentEntry.params;
|
|
1083
|
+
const ownedSet = owned === null || owned === undefined ? null : new Set(owned);
|
|
1072
1084
|
for (const group of keptPoses) {
|
|
1073
1085
|
const frame = poseSnapshots.get(group);
|
|
1074
1086
|
if (frame === undefined) continue;
|
|
1075
1087
|
for (const id of Object.keys(frame)) {
|
|
1076
|
-
if (
|
|
1088
|
+
if (ownedSet !== null && ownedSet.has(id)) continue;
|
|
1077
1089
|
const at = parameterIndex(core, id);
|
|
1078
1090
|
if (at >= 0) values[at] = frame[id];
|
|
1079
1091
|
}
|
|
@@ -1158,6 +1170,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1158
1170
|
if (Math.abs(mouthTargetFollow - mouthFollow) < 0.002) mouthFollow = mouthTargetFollow;
|
|
1159
1171
|
if (Math.abs(mouthTargetLean - mouthLean) < 0.002) mouthLean = mouthTargetLean;
|
|
1160
1172
|
}
|
|
1173
|
+
// **动作自己要的参数**(`motionOptions.<组>.holdParams`)**不在这里写** ——
|
|
1174
|
+
// 缝隙在 `update()` 之前,而曲线是 `update()` 里写的,写在这儿会被曲线盖掉。
|
|
1175
|
+
// 真正的实现在 `update()` 的钩子里(搜 `actionHolds`)。
|
|
1161
1176
|
// Blink. Runs before the early return because it is unconditional — it
|
|
1162
1177
|
// has nothing to do with what is pinned, and the engine's own blink is
|
|
1163
1178
|
// disabled precisely because its gate never opens for this model.
|
|
@@ -1397,6 +1412,31 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1397
1412
|
updateBase();
|
|
1398
1413
|
updateCalls += 1;
|
|
1399
1414
|
markSeam("U");
|
|
1415
|
+
// **每帧按住的值写在这里**(`update()` 之后 = 这一帧最后一步)。
|
|
1416
|
+
//
|
|
1417
|
+
// 为什么不在缝隙里(`loadParameters` 那条):缝隙在 `update()` **之前**,
|
|
1418
|
+
// 而动作曲线是 `update()` 里写的 ⇒ 死死被曲线盖住。实测:表装对了
|
|
1419
|
+
// (`holds={"phone5":10}`)而 `phone5` 读出来仍是曲线的形状
|
|
1420
|
+
// (1.78 → 9.87 → 0)。这正是"动作要写值"必须写在最后的原因。
|
|
1421
|
+
if (actionHolds !== null) {
|
|
1422
|
+
// ⚠️ 这个 try 是**静默**的:里面任何拼错的变量都会把"写入"一起吞掉,
|
|
1423
|
+
// 而外面的症状只是"值没生效"(我在这儿被坑了一轮:探针里写了个不存在的
|
|
1424
|
+
// `probeAt`,于是写入根本没跑,而 `holds` 表看起来完全正常)。
|
|
1425
|
+
try {
|
|
1426
|
+
const values = core._model.parameters.values;
|
|
1427
|
+
for (const id of Object.keys(actionHolds)) {
|
|
1428
|
+
const at = parameterIndex(core, id);
|
|
1429
|
+
if (at >= 0) values[at] = actionHolds[id];
|
|
1430
|
+
}
|
|
1431
|
+
const probeAt = parameterIndex(core, "phone5");
|
|
1432
|
+
holdProbe = {
|
|
1433
|
+
wrote: probeAt >= 0 ? values[probeAt] : null,
|
|
1434
|
+
calls: (holdProbe?.calls ?? 0) + 1,
|
|
1435
|
+
};
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
holdProbe = { error: String(error).slice(0, 120), calls: (holdProbe?.calls ?? 0) + 1 };
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1400
1440
|
updateSample = sample();
|
|
1401
1441
|
};
|
|
1402
1442
|
}
|
|
@@ -1516,6 +1556,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1516
1556
|
// supposed to do the spraying is a separate parameter (`jingyu`) that
|
|
1517
1557
|
// nothing in that motion touches — which is why it looked like a no-op.
|
|
1518
1558
|
const preset = opts.preset ?? null;
|
|
1559
|
+
// 每帧按住的参数(见 actionHolds 的注释)。读宠物声明的 `holdParams`。
|
|
1560
|
+
const holdParams = opts.holdParams ?? null;
|
|
1561
|
+
actionHolds = holdParams !== null && typeof holdParams === 'object' ? Object.assign({}, holdParams) : null;
|
|
1519
1562
|
// Stop ONLY when replaying the very same group+index, which is the one
|
|
1520
1563
|
// case the engine refuses on its own. Clearing the queue unconditionally
|
|
1521
1564
|
// removed the outgoing motion instantly, so there was nothing left to
|
|
@@ -1581,12 +1624,15 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1581
1624
|
restoreHeld();
|
|
1582
1625
|
playIdle();
|
|
1583
1626
|
};
|
|
1627
|
+
|
|
1584
1628
|
/** Return to the looping idle animation; the resting state of the pet. */
|
|
1585
1629
|
const playIdle = () => {
|
|
1586
1630
|
clearTimer();
|
|
1587
1631
|
token += 1;
|
|
1588
1632
|
kind = "idle";
|
|
1589
1633
|
currentEntry = null;
|
|
1634
|
+
// 回待机 = 动作结束 ⇒ 松开"每帧按住"的表(见 actionHolds)。
|
|
1635
|
+
actionHolds = null;
|
|
1590
1636
|
if (model === null || idleName === null) return;
|
|
1591
1637
|
// Coming back to rest retires the previous action's parameter pins, so
|
|
1592
1638
|
// the bubble-gum mouth (and anything else action-specific) is released
|
|
@@ -1955,6 +2001,19 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
|
|
|
1955
2001
|
}),
|
|
1956
2002
|
/** Diagnostic: how shut the eyes were on the last frame, 0..1. */
|
|
1957
2003
|
blinkAmount: () => blinkWrote,
|
|
2004
|
+
/**
|
|
2005
|
+
* Diagnostic: "每帧按住"那张表(`motionOptions.<组>.holdParams`)。
|
|
2006
|
+
*
|
|
2007
|
+
* 加它的原因:我按"抬手要每帧写值"实现了 `holdParams`,但实机读 `phone5` 仍是
|
|
2008
|
+
* 曲线值(8.88 而不是 10)—— 必须能直接看到"表到底有没有被装上、里面是什么",
|
|
2009
|
+
* 否则只能在外面猜是没传进来还是被覆盖了。
|
|
2010
|
+
*/
|
|
2011
|
+
holdDebug: () => ({
|
|
2012
|
+
holds: actionHolds === null ? null : Object.assign({}, actionHolds),
|
|
2013
|
+
group: currentEntry === null ? null : currentEntry.group,
|
|
2014
|
+
kind: kind,
|
|
2015
|
+
probe: holdProbe === null ? null : Object.assign({}, holdProbe),
|
|
2016
|
+
}),
|
|
1958
2017
|
/** Force a blink now, so a test does not have to wait for one. */
|
|
1959
2018
|
blinkNow: () => { blinkAt = 0; blinkStart = (typeof performance !== "undefined" ? performance.now() : Date.now()); },
|
|
1960
2019
|
/** Diagnostic: how many motions the engine is cross-fading right now. */
|
package/lib/desktop.js
CHANGED
|
@@ -26,7 +26,11 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
26
26
|
export function desktopPackageName(platform = process.platform, arch = process.arch) {
|
|
27
27
|
const table = {
|
|
28
28
|
'win32-x64': 'dsh-pet-live2d-desktop-win32-x64',
|
|
29
|
-
//
|
|
29
|
+
// macOS arm64(Apple Silicon)。**只有构建与代码就绪,npm 上还没有这个子包** ——
|
|
30
|
+
// 发布前要补两步,顺序不能反(否则用户拿到的主包会指向一个不存在的子包):
|
|
31
|
+
// 1. `node tools/npm-prepare-subpackage.mjs --sub darwin-arm64` 后把子包发上 npm;
|
|
32
|
+
// 2. 主包 `optionalDependencies` 加一行 `"dsh-pet-live2d-desktop-darwin-arm64": "<主包版本>"`。
|
|
33
|
+
'darwin-arm64': 'dsh-pet-live2d-desktop-darwin-arm64',
|
|
30
34
|
}
|
|
31
35
|
return table[platform + '-' + arch]
|
|
32
36
|
}
|
package/lib/display.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// 为什么不走 DSH 的进程内服务:桌面端是另一个进程,拿不到 `ctx.on(...)`。一个文件加一条
|
|
18
18
|
// 纯规则,是这两边唯一都能用的东西。
|
|
19
|
-
import { spawn } from 'node:child_process'
|
|
19
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { join } from 'node:path'
|
|
22
22
|
|
|
@@ -155,8 +155,20 @@ export function createDisplayLayer(options) {
|
|
|
155
155
|
function stop() {
|
|
156
156
|
if (child === null) return false
|
|
157
157
|
try {
|
|
158
|
-
child.kill()
|
|
159
|
-
|
|
158
|
+
// ⚠️ Windows 上 `child.kill()` 是 TerminateProcess:**只杀壳自己**。WebView2 的
|
|
159
|
+
// `msedgewebview2.exe` 子进程会留下来继续占着 `%LOCALAPPDATA%\<id>\EBWebView`
|
|
160
|
+
// (那是独占的),于是**下一次**启动撞 `0x800700AA 请求的资源在使用中`
|
|
161
|
+
// —— 用户 2026-09 报的那个报错框就是这么来的。
|
|
162
|
+
// 所以 Windows 上连整棵进程树一起收(`/T`),别留孤儿。
|
|
163
|
+
if (process.platform === 'win32') {
|
|
164
|
+
spawnSync('taskkill', ['/pid', String(childPid), '/T', '/F'], {
|
|
165
|
+
stdio: 'ignore',
|
|
166
|
+
windowsHide: true,
|
|
167
|
+
})
|
|
168
|
+
} else {
|
|
169
|
+
child.kill()
|
|
170
|
+
}
|
|
171
|
+
note('已收掉桌面端(pid ' + childPid + ',含子进程)')
|
|
160
172
|
} catch {
|
|
161
173
|
/* 已经退了 */
|
|
162
174
|
}
|
|
@@ -173,6 +185,15 @@ export function createDisplayLayer(options) {
|
|
|
173
185
|
note('找不到桌面端二进制,无法拉起(' + (options.hint ?? '') + ')')
|
|
174
186
|
return { ok: false, reason: 'binary-missing' }
|
|
175
187
|
}
|
|
188
|
+
// **先自愈**:我们记着的那个子进程可能早就没了(被杀 / 崩了 / 崩溃后没人收),
|
|
189
|
+
// 而 `exit` 事件也没到(比如进程被 `taskkill /T` 连带干掉)。不清理的话
|
|
190
|
+
// `childPid !== 0` 会让下面那条守卫永远返回"已经有一只了" —— 于是设置里显示
|
|
191
|
+
// **"桌面已接管"、桌面上却什么都没有**(用户 2026-09 报的现象)。
|
|
192
|
+
if (childPid !== 0 && !pidAlive(childPid) && !running()) {
|
|
193
|
+
note('记着的桌面端进程(pid ' + childPid + ')已经不在了 —— 忘掉它')
|
|
194
|
+
child = null
|
|
195
|
+
childPid = 0
|
|
196
|
+
}
|
|
176
197
|
if (childPid !== 0 || running()) return { ok: true, reason: 'already-running' }
|
|
177
198
|
if (Date.now() - lastAttempt < RETRY_MS && lastAttempt !== 0) {
|
|
178
199
|
return { ok: false, reason: 'retry-cooldown' }
|
|
@@ -180,13 +201,27 @@ export function createDisplayLayer(options) {
|
|
|
180
201
|
lastAttempt = Date.now()
|
|
181
202
|
try {
|
|
182
203
|
// `detached` + `unref`:她是**独立的**窗口进程,DSH 关掉她还得站着。
|
|
183
|
-
|
|
204
|
+
//
|
|
205
|
+
// `--from-plugin`:告诉壳"这一份是插件按设置拉起的" —— 壳据此**严格尊重**用户的
|
|
206
|
+
// 「页面内」选择。**手动双击**时没有这个参数,壳会把「页面内」改写成「桌面」:
|
|
207
|
+
// 双击的意图就是"我要她在桌面上",否则用户看到的是"双击了,什么都没发生"
|
|
208
|
+
// (2026-09 用户报的就是这个;见壳里 `host::display::manual_launch_overrides_inline`)。
|
|
209
|
+
child = spawn(binary.path, ['--attach', dshUrl, '--from-plugin'], {
|
|
184
210
|
detached: true,
|
|
185
211
|
stdio: 'ignore',
|
|
186
212
|
windowsHide: true,
|
|
187
213
|
})
|
|
188
214
|
childPid = child.pid ?? 0
|
|
189
215
|
child.unref()
|
|
216
|
+
// **子进程退出要当场忘掉它**:否则 `childPid` 永远非 0,reconcile 会一直认为
|
|
217
|
+
// "已经有一只了",她死了插件也不重拉(设置里还显示"桌面已接管")。
|
|
218
|
+
child.on('exit', () => {
|
|
219
|
+
if (childPid === (child?.pid ?? 0)) {
|
|
220
|
+
note('桌面端进程退出了(pid ' + childPid + ')')
|
|
221
|
+
child = null
|
|
222
|
+
childPid = 0
|
|
223
|
+
}
|
|
224
|
+
})
|
|
190
225
|
note('已拉起桌面端:' + binary.path + ' --attach ' + dshUrl + '(pid ' + childPid + ')')
|
|
191
226
|
return { ok: true, reason: 'spawned', pid: childPid }
|
|
192
227
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-pet-live2d",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Live2D 桌宠插件:为 DSH Web GUI 挂一只可拖动、会跟随鼠标、能换动作与表情的 Live2D 宠物。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"dsh-pet-live2d-desktop-win32-x64": "3.
|
|
52
|
+
"dsh-pet-live2d-desktop-win32-x64": "3.1.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"esbuild": "^0.25.10",
|