@linxin666/dsh-pet 0.2.4 → 0.2.5

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.
Files changed (44) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +109 -27
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +164 -34
  7. package/lib/types/access.d.ts +16 -0
  8. package/lib/types/access.d.ts.map +1 -0
  9. package/lib/types/access.js +20 -0
  10. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PetSettingsCard.js +8 -2
  12. package/lib/types/client/PetSprite.d.ts.map +1 -1
  13. package/lib/types/client/PetSprite.js +10 -2
  14. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -1
  15. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +1 -0
  16. package/lib/types/client/renderers/live2d/runtime.d.ts +13 -1
  17. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -1
  18. package/lib/types/client/renderers/live2d.d.ts.map +1 -1
  19. package/lib/types/client/renderers/live2d.js +120 -24
  20. package/lib/types/image-dimensions.d.ts +26 -0
  21. package/lib/types/image-dimensions.d.ts.map +1 -0
  22. package/lib/types/image-dimensions.js +77 -0
  23. package/lib/types/index.js +1 -1
  24. package/lib/types/registry.d.ts.map +1 -1
  25. package/lib/types/registry.js +43 -1
  26. package/lib/types/routes.d.ts +6 -1
  27. package/lib/types/routes.d.ts.map +1 -1
  28. package/lib/types/routes.js +36 -33
  29. package/package.json +1 -1
  30. package/src/access.ts +40 -0
  31. package/src/client/PetSettingsCard.tsx +8 -2
  32. package/src/client/PetSprite.test.tsx +71 -0
  33. package/src/client/PetSprite.tsx +10 -2
  34. package/src/client/renderers/live2d/Live2dVisualMount.test.tsx +72 -0
  35. package/src/client/renderers/live2d/Live2dVisualMount.tsx +1 -0
  36. package/src/client/renderers/live2d/runtime.ts +8 -2
  37. package/src/client/renderers/live2d.test.ts +211 -8
  38. package/src/client/renderers/live2d.ts +129 -29
  39. package/src/image-dimensions.test.ts +85 -0
  40. package/src/image-dimensions.ts +76 -0
  41. package/src/index.ts +1 -1
  42. package/src/registry.test.ts +58 -5
  43. package/src/registry.ts +40 -1
  44. package/src/routes.ts +38 -34
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: a0adc3fe2cb2767a8e372ed32cec6e8f7bb74660
6
- README.zh.md: e048ab6aece36283648b95ceddf705b3c6783f7e
5
+ README.md: 5b33a914b3a373385771a4526c5129ca91dd71db
6
+ README.zh.md: 4ce92ac1d90ca1a667edc058996ddc9dd7504584
package/README.md CHANGED
@@ -288,7 +288,7 @@ The two built-in whale-girl atlases use the same 9-state × 8-column contract: `
288
288
 
289
289
  ## Security model
290
290
 
291
- - All pet API and asset routes are fenced to loopback clients.
291
+ - Every `/api/pet/*` and `/pet/<id>/*` route is loopback-only by default (the shared plugin-family fence: loopback socket + Host header + browser same-origin markers): unpaired LAN clients get `403 forbidden: loopback-only` before any pet state or atlas is served. When `dsh-remote-web-ui` is also loaded, a live paired-device cookie is an additional allow path (the same cookie `api/gate` already checks); unpaired and revoked devices stay 403. The pet does not depend on the remote plugin.
292
292
  - Asset serving resolves both the pet directory and the candidate file through `realpath`; symlink escapes are refused (403). Files are size-capped before being read into memory (manifest 64 KB, imagery 20 MB; over-cap answers 413).
293
293
  - Live2D models are served by closure: only the manifest, the declared primary assets, and the files the `.model3.json` references (each screened against traversal, absolute and URL forms).
294
294
  - The plugin never downloads executables and never bundles the Live2D Cubism Core.
package/README.zh.md CHANGED
@@ -288,7 +288,7 @@ pnpm typecheck # 仅类型检查
288
288
 
289
289
  ## 安全模型
290
290
 
291
- - 全部宠物 API 与资产路由仅服务回环客户端。
291
+ - 全部 `/api/pet/*` 与 `/pet/<id>/*` 路由默认仅限 loopback(插件家族共享围栏:loopback 套接字 + Host 头 + 浏览器同源标记):未配对的局域网客户端在任何宠物状态或图集下发前即收到 `403 forbidden: loopback-only`。同时装了 `dsh-remote-web-ui` 时,有效的已配对设备 cookie 是额外放行路径(与 `api/gate` 检查同一枚 cookie);未配对与已撤销设备仍 403。宠物插件不硬依赖远程插件。
292
292
  - 资产服务对宠物目录与目标文件双双做 `realpath` 解析;symlink 越界一律拒绝(403)。文件读入内存前按类限大小(清单 64 KB、图像 20 MB;超限 413)。
293
293
  - Live2D 模型按闭包放行:仅清单、声明的主资产与 `.model3.json` 引用到的文件(引用先经穿越/绝对路径/URL 形态筛查)。
294
294
  - 插件从不下载可执行文件,也从不内置 Live2D Cubism Core。
package/lib/client.js CHANGED
@@ -244,7 +244,7 @@ window.__ModuleLoader__.load({
244
244
  const position = (index) => -index * frameWidth + "px 0px";
245
245
  const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
246
246
  el.style.backgroundPosition = position(segment.from);
247
- if (reduceMotion) return;
247
+ if (reduceMotion || segment.from === segment.to) return;
248
248
  let raf = 0;
249
249
  let index = segment.from;
250
250
  let elapsed = 0;
@@ -258,8 +258,8 @@ window.__ModuleLoader__.load({
258
258
  elapsed = 0;
259
259
  if (index < segment.to) index += 1;
260
260
  else if (decoration.loop) index = segment.from;
261
+ el.style.backgroundPosition = position(index);
261
262
  }
262
- el.style.backgroundPosition = position(index);
263
263
  if (!decoration.loop && index === segment.to) return;
264
264
  raf = requestAnimationFrame(tick);
265
265
  };
@@ -789,6 +789,7 @@ window.__ModuleLoader__.load({
789
789
  const downRef = (0, react.useRef)(null);
790
790
  const [error, setError] = (0, react.useState)(null);
791
791
  (0, react.useEffect)(() => {
792
+ setError(null);
792
793
  const container = containerRef.current;
793
794
  const live2d = props.definition.live2d;
794
795
  if (container === null || live2d === void 0) return void 0;
@@ -1019,6 +1020,25 @@ window.__ModuleLoader__.load({
1019
1020
  * downsampled atlas only when the effective on-screen scale warrants it.
1020
1021
  */
1021
1022
  const TEXTURE_OPTIONS = { lod: "single-auto" };
1023
+ /** Recursively release the activation without invalidating shared texture caches. */
1024
+ const DESTROY_OPTIONS = { children: true };
1025
+ /** Remove only this activation's canvas; `true` would release Pixi globals. */
1026
+ const RENDERER_DESTROY_OPTIONS = { removeView: true };
1027
+ /** Ignore hidden/zero boxes and keep Pixi dimensions stable and integral. */
1028
+ function normalizeRendererSize(width, height) {
1029
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return void 0;
1030
+ return {
1031
+ width: Math.max(1, Math.round(width)),
1032
+ height: Math.max(1, Math.round(height))
1033
+ };
1034
+ }
1035
+ /** Fit the model from its unscaled dimensions into the current Pixi screen. */
1036
+ function layoutModel(app, model, sourceSize, config) {
1037
+ const fit = Math.min(app.renderer.width / sourceSize.width, app.renderer.height / sourceSize.height) * .92;
1038
+ model.scale.set(fit * (config.scale ?? 1));
1039
+ model.anchor.set(.5);
1040
+ model.position.set(app.renderer.width / 2 + (config.translate?.x ?? 0), app.renderer.height / 2 + (config.translate?.y ?? 0));
1041
+ }
1022
1042
  let vendorConfigured = false;
1023
1043
  /** Configure pixi extensions + the Cubism SDK once per page. */
1024
1044
  function configureOnce(vendor) {
@@ -1045,11 +1065,56 @@ window.__ModuleLoader__.load({
1045
1065
  let disposed = false;
1046
1066
  let app;
1047
1067
  let model;
1068
+ let modelAttached = false;
1069
+ let modelSourceSize;
1070
+ let resizeObserver;
1071
+ let resizeTracking = false;
1048
1072
  let errorListener;
1049
1073
  let unsubscribe;
1050
1074
  /** The motion group the current phase maps to (resume target after taps). */
1051
1075
  let phaseGroup = config.motions.idle;
1052
1076
  let tapPlaying = false;
1077
+ const stopResizeTracking = () => {
1078
+ resizeTracking = false;
1079
+ resizeObserver?.disconnect();
1080
+ resizeObserver = void 0;
1081
+ };
1082
+ const resizeRenderer = (pixiApp, width, height) => {
1083
+ const next = normalizeRendererSize(width, height);
1084
+ if (disposed || !resizeTracking || next === void 0) return;
1085
+ if (pixiApp.renderer.width === next.width && pixiApp.renderer.height === next.height) return;
1086
+ pixiApp.renderer.resize(next.width, next.height);
1087
+ if (model !== void 0 && modelSourceSize !== void 0) layoutModel(pixiApp, model, modelSourceSize, config);
1088
+ };
1089
+ const trackContainerSize = (pixiApp) => {
1090
+ if (typeof ResizeObserver === "undefined") return;
1091
+ resizeTracking = true;
1092
+ resizeObserver = new ResizeObserver((entries) => {
1093
+ const entry = entries.find((candidate) => candidate.target === ctx.container);
1094
+ if (entry === void 0) return;
1095
+ resizeRenderer(pixiApp, entry.contentRect.width, entry.contentRect.height);
1096
+ });
1097
+ resizeObserver.observe(ctx.container);
1098
+ resizeRenderer(pixiApp, ctx.container.clientWidth, ctx.container.clientHeight);
1099
+ };
1100
+ /** Release every resource currently owned by this activation exactly once. */
1101
+ const destroyResources = () => {
1102
+ stopResizeTracking();
1103
+ unsubscribe?.();
1104
+ unsubscribe = void 0;
1105
+ const currentApp = app;
1106
+ const currentModel = model;
1107
+ const modelOwnedByApp = currentApp !== void 0 && modelAttached;
1108
+ app = void 0;
1109
+ model = void 0;
1110
+ modelSourceSize = void 0;
1111
+ modelAttached = false;
1112
+ try {
1113
+ if (currentModel !== void 0 && !modelOwnedByApp) currentModel.destroy(DESTROY_OPTIONS);
1114
+ } finally {
1115
+ currentApp?.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
1116
+ }
1117
+ };
1053
1118
  const playGroup = (group) => {
1054
1119
  if (model === void 0) return;
1055
1120
  const groups = model.internalModel.settings.motions ?? {};
@@ -1079,38 +1144,54 @@ window.__ModuleLoader__.load({
1079
1144
  }
1080
1145
  configureOnce(vendor);
1081
1146
  const pixiApp = new vendor.Application();
1082
- await pixiApp.init({
1083
- width: Math.max(1, ctx.container.clientWidth || 160),
1084
- height: Math.max(1, ctx.container.clientHeight || 174),
1085
- backgroundAlpha: 0,
1086
- antialias: true,
1087
- autoDensity: true,
1088
- preference: "webgl"
1089
- });
1147
+ const initialSize = normalizeRendererSize(ctx.container.clientWidth, ctx.container.clientHeight) ?? {
1148
+ width: 160,
1149
+ height: 174
1150
+ };
1151
+ try {
1152
+ await pixiApp.init({
1153
+ width: initialSize.width,
1154
+ height: initialSize.height,
1155
+ backgroundAlpha: 0,
1156
+ antialias: true,
1157
+ autoDensity: true,
1158
+ preference: "webgl"
1159
+ });
1160
+ } catch (error) {
1161
+ try {
1162
+ pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
1163
+ } catch {}
1164
+ throw error;
1165
+ }
1090
1166
  if (disposed) {
1091
- pixiApp.destroy(true, { children: true });
1167
+ pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
1092
1168
  return;
1093
1169
  }
1170
+ app = pixiApp;
1094
1171
  pixiApp.canvas.style.display = "block";
1095
1172
  pixiApp.canvas.style.width = "100%";
1096
1173
  pixiApp.canvas.style.height = "100%";
1097
1174
  ctx.container.appendChild(pixiApp.canvas);
1175
+ trackContainerSize(pixiApp);
1098
1176
  const loaded = await vendor.Live2DModel.from(config.modelUrl, {
1177
+ autoUpdate: false,
1099
1178
  autoHitTest: false,
1100
1179
  autoFocus: false,
1101
1180
  textureOptions: TEXTURE_OPTIONS
1102
1181
  });
1182
+ model = loaded;
1103
1183
  if (disposed) {
1104
- pixiApp.destroy(true, { children: true });
1184
+ destroyResources();
1105
1185
  return;
1106
1186
  }
1107
- app = pixiApp;
1108
- model = loaded;
1109
- const fit = Math.min(pixiApp.renderer.width / Math.max(1, loaded.width), pixiApp.renderer.height / Math.max(1, loaded.height)) * .92;
1110
- loaded.scale.set(fit * (config.scale ?? 1));
1111
- loaded.anchor.set(.5);
1112
- loaded.position.set(pixiApp.renderer.width / 2 + (config.translate?.x ?? 0), pixiApp.renderer.height / 2 + (config.translate?.y ?? 0));
1187
+ modelSourceSize = {
1188
+ width: Math.max(1, loaded.width),
1189
+ height: Math.max(1, loaded.height)
1190
+ };
1191
+ layoutModel(pixiApp, loaded, modelSourceSize, config);
1113
1192
  pixiApp.stage.addChild(loaded);
1193
+ modelAttached = true;
1194
+ loaded.automator.autoUpdate = true;
1114
1195
  loaded.on("motionFinish", () => {
1115
1196
  if (tapPlaying) {
1116
1197
  tapPlaying = false;
@@ -1121,18 +1202,17 @@ window.__ModuleLoader__.load({
1121
1202
  unsubscribe = ctx.phase.subscribe(applyPhase);
1122
1203
  };
1123
1204
  boot().catch(() => {
1124
- if (!disposed) errorListener?.("load-failed");
1205
+ try {
1206
+ destroyResources();
1207
+ } finally {
1208
+ if (!disposed) errorListener?.("load-failed");
1209
+ }
1125
1210
  });
1126
1211
  return {
1127
1212
  dispose() {
1128
1213
  if (disposed) return;
1129
1214
  disposed = true;
1130
- unsubscribe?.();
1131
- model = void 0;
1132
- if (app !== void 0) {
1133
- app.destroy(true, { children: true });
1134
- app = void 0;
1135
- }
1215
+ destroyResources();
1136
1216
  },
1137
1217
  tap(x, y) {
1138
1218
  const current = model;
@@ -1987,8 +2067,10 @@ window.__ModuleLoader__.load({
1987
2067
  choiceField("petId", this.petChoices)
1988
2068
  ]);
1989
2069
  this.store = this.form.bind(() => this.projection());
1990
- this.loadPets();
1991
- this.loadDiagnostics();
2070
+ window.setTimeout(() => {
2071
+ this.loadPets();
2072
+ this.loadDiagnostics();
2073
+ }, 0);
1992
2074
  }
1993
2075
  /** Fetch registry diagnostics once (soft-fail: an empty list on error). */
1994
2076
  async loadDiagnostics() {