dsh-pet-live2d 3.0.1 → 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 CHANGED
@@ -1,5 +1,153 @@
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
+
3
151
  ## 3.0.1
4
152
 
5
153
  ### 修:「自拍」右手不抬(3.0.0 引入)
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
- // 将来加构建时在这里补:'darwin-arm64' / 'linux-x64' …
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
- note('已收掉桌面端(pid ' + childPid + ')')
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
- child = spawn(binary.path, ['--attach', dshUrl], {
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.0.1",
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.0.1"
52
+ "dsh-pet-live2d-desktop-win32-x64": "3.1.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "esbuild": "^0.25.10",