dlaoshi-outfit 2.1.3 → 2.1.4

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.4 — 2026-10-22
4
+
5
+ 按 DSH 的「可逆副作用」规范重写挂载流程,支持热重组。
6
+
7
+ - 所有副作用都成对:样式表、`html` 皮肤类、主题 token、槽位占用全部由 `ctx.effect` 托管,
8
+ 卸载时逐项回滚,不再依赖页面刷新
9
+ - 二次 `apply()` 幂等:先释放上一批槽位占用再挂载,避免热重组后出现两只桌宠
10
+ - 新增自愈:槽位容器还在、本插件节点消失时(HMR 驱逐),约 5 秒内自动重新挂载
11
+ - 测试新增「重复 apply 不重复占用槽位」断言
12
+
13
+ > 注意:全新安装插件仍需重启一次 Host——DSH 的插件图在启动时组合,这不是插件能绕过的。
14
+
3
15
  ## 2.1.3 — 2026-10-22
4
16
 
5
17
  修复:网页端加载失败(`__ModuleLoader__` 未注册)。
package/README.md CHANGED
@@ -123,6 +123,36 @@ Copy-Item "C:\Users\你\Documents\dlaoshi-outfit\*" `
123
123
  └── examples/ # profile 配置示例
124
124
  ```
125
125
 
126
+ ## 常见问题
127
+
128
+ ### 装完之后没反应 / 第一次启动不生效
129
+
130
+ **这是 DSH 的加载机制,不是插件的问题。** 本插件已按「可逆副作用」实现:卸载即回滚、重复挂载不会叠加、被 HMR 驱逐后会自动重挂,但**全新安装仍需重启一次 Host**(插件图只在启动时组合)。 DSH 的插件图(loader entries)在 **Host 启动时组合**;热更新只对已经加载过的客户端 bundle 生效。全新安装一个插件等于往插件图里加一个新 entry,**必须完整重启一次 DSH** 才会生效。
131
+
132
+ 排查顺序:
133
+
134
+ 1. 完全退出 DSH(不是关窗口,要从托盘/任务管理器退出)
135
+ 2. 重新启动
136
+ 3. 设置 → 插件,确认 `dlaoshi-outfit` 存在且已启用
137
+ 4. 仍不出现 → 再重启一次(首次安装有时需要两轮:第一轮装包、第二轮组合)
138
+ 5. 打开浏览器控制台(F12),看有没有 `__ModuleLoader__` 相关报错
139
+
140
+ ### 桌宠突然消失
141
+
142
+ 通常是客户端热更新(HMR)把插件卸载后没有重新挂载。按 `Ctrl+R` 刷新页面即可;刷新后仍不在就重启 DSH。
143
+
144
+ ### 网页端提示「加载插件失败 / 未注册」
145
+
146
+ 确认安装的是 **2.1.3 或更高版本**。`2.1.0` 和 `2.1.1` 的客户端注册 id 与包名不一致,已被标记 deprecated:
147
+
148
+ ```powershell
149
+ dsh plugin add dlaoshi-outfit@2.1.3
150
+ ```
151
+
152
+ ### 开发时想热更新
153
+
154
+ 客户端代码改动只有在 `pnpm run dev:web` 同时运行时才会自动重建并热替换;否则改完 `client.js` 需要重启 DSH。
155
+
126
156
  ## 开发
127
157
 
128
158
  ```powershell
package/client.js CHANGED
@@ -78,6 +78,10 @@ window.__ModuleLoader__.load({
78
78
  listener(outfitEnabled)
79
79
  return () => { outfitListeners.delete(listener) }
80
80
  }
81
+
82
+ /** Disposer of the currently mounted occupant set, shared across apply()
83
+ * calls so a second mount replaces the first instead of stacking on it. */
84
+ let mountedOccupants = null
81
85
  /** Host route that reports the live turn's tokens and think latency. */
82
86
  const STATS_PATH = '/fatfish/stats'
83
87
 
@@ -1152,18 +1156,34 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
1152
1156
  window.__petDebug = { starts: 0, ends: 0, happy: 0, sad: 0, balance: null }
1153
1157
  }
1154
1158
 
1159
+ // Reversible, idempotent mount. Every side effect below is paired with a
1160
+ // disposer owned by this plugin's fiber, so the loader can apply this
1161
+ // module again (hot recompose) without a restart and without leaving a
1162
+ // duplicate stylesheet, class or slot occupant behind.
1163
+ if (typeof document.querySelectorAll === 'function') {
1164
+ for (const stale of document.querySelectorAll('style[data-plugin="dlaoshi-outfit"]')) {
1165
+ try { stale.remove() } catch (err) { /* already detached */ }
1166
+ }
1167
+ }
1168
+
1155
1169
  // Apply the remembered outfit state before the stylesheet mounts so the
1156
1170
  // page never flashes the beautified look on a "switched off" session.
1157
1171
  outfitEnabled = readOutfitSetting()
1158
- applyOutfitClass(outfitEnabled)
1172
+ ctx.effect(() => {
1173
+ applyOutfitClass(outfitEnabled)
1174
+ return () => {
1175
+ try { document.documentElement.classList.remove(SKIN_CLASS) } catch (err) { /* no document */ }
1176
+ }
1177
+ }, 'dlaoshi-pet: skin class')
1159
1178
 
1160
- const tag = document.createElement('style')
1161
- tag.dataset.plugin = 'dlaoshi-outfit'
1162
- tag.textContent = CSS
1163
- document.head.appendChild(tag)
1164
- ctx.effect(() => () => {
1165
- tag.remove()
1166
- document.documentElement.classList.remove(SKIN_CLASS)
1179
+ ctx.effect(() => {
1180
+ const tag = document.createElement('style')
1181
+ tag.dataset.plugin = 'dlaoshi-outfit'
1182
+ tag.textContent = CSS
1183
+ document.head.appendChild(tag)
1184
+ return () => {
1185
+ try { tag.remove() } catch (err) { /* already detached */ }
1186
+ }
1167
1187
  }, 'dlaoshi-pet: stylesheet')
1168
1188
 
1169
1189
  ctx.effect(() => ctx.theme.overrideTokens('dlaoshi-outfit', TOKENS),
@@ -1172,7 +1192,14 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
1172
1192
  const occupy = (name, id, order, component) => ctx.slots.inject(name, () =>
1173
1193
  ctx.slots.register({ name, id, order }, component))
1174
1194
 
1175
- ctx.effect(() => {
1195
+ // A second mount must replace the first, never stack on top of it: the
1196
+ // loader may re-apply this module while a previous occupant set is still
1197
+ // registered (hot recompose), and two pets would otherwise fight.
1198
+ const mountOccupants = () => {
1199
+ if (typeof mountedOccupants === 'function') {
1200
+ try { mountedOccupants() } catch (err) { /* already gone */ }
1201
+ mountedOccupants = null
1202
+ }
1176
1203
  const stop = [
1177
1204
  occupy('shell.overlay', 'dlaoshi-companion', 20, Companion),
1178
1205
  occupy('sidebar.brand.mark', 'dlaoshi-sidebar', 5, SidebarMark),
@@ -1181,7 +1208,36 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
1181
1208
  occupy('settings.general.item', 'dlaoshi-theme', 40, ThemeSettingsRow),
1182
1209
  ]
1183
1210
  return () => { for (const fn of stop) if (typeof fn === 'function') fn() }
1211
+ }
1212
+
1213
+ mountedOccupants = mountOccupants()
1214
+ ctx.effect(() => () => {
1215
+ if (typeof mountedOccupants === 'function') mountedOccupants()
1216
+ mountedOccupants = null
1184
1217
  }, 'dlaoshi-pet: slot occupants')
1218
+
1219
+ // Self heal for hot recomposition: when the loader evicts this module's
1220
+ // occupants but the overlay slot itself stays mounted, remount instead of
1221
+ // leaving the user with a vanished pet until they refresh.
1222
+ ctx.effect(() => {
1223
+ if (typeof window === 'undefined' || typeof document === 'undefined') return () => {}
1224
+ let misses = 0
1225
+ const timer = setInterval(() => {
1226
+ try {
1227
+ const slot = document.querySelector('[data-slot="shell.overlay"]')
1228
+ if (!slot || typeof slot.querySelector !== 'function') { misses = 0; return }
1229
+ if (slot.querySelector(`.${PREFIX}-pet`)) { misses = 0; return }
1230
+ misses += 1
1231
+ if (misses < 2) return
1232
+ misses = 0
1233
+ mountedOccupants = mountOccupants()
1234
+ } catch (err) {
1235
+ misses = 0
1236
+ }
1237
+ }, 2500)
1238
+ if (timer && typeof timer.unref === 'function') timer.unref()
1239
+ return () => clearInterval(timer)
1240
+ }, 'dlaoshi-pet: hot-recompose self heal')
1185
1241
  },
1186
1242
  }
1187
1243
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlaoshi-outfit",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "type": "module",
5
5
  "description": "Anime whale-boy maid-outfit desktop pet, token/think-time standee and liquid-glass theme for DeepSeek Harness",
6
6
  "exports": {