@linxin666/dsh-pet 0.2.4 → 0.2.6
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.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +110 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +164 -34
- package/lib/types/access.d.ts +16 -0
- package/lib/types/access.d.ts.map +1 -0
- package/lib/types/access.js +20 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +8 -2
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +10 -2
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -1
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +1 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts +13 -1
- package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -1
- package/lib/types/client/renderers/live2d.d.ts.map +1 -1
- package/lib/types/client/renderers/live2d.js +120 -24
- package/lib/types/image-dimensions.d.ts +26 -0
- package/lib/types/image-dimensions.d.ts.map +1 -0
- package/lib/types/image-dimensions.js +77 -0
- package/lib/types/index.js +1 -1
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +43 -1
- package/lib/types/routes.d.ts +6 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +36 -33
- package/package.json +1 -1
- package/src/access.ts +40 -0
- package/src/client/PetSettingsCard.tsx +8 -2
- package/src/client/PetSprite.test.tsx +71 -0
- package/src/client/PetSprite.tsx +10 -2
- package/src/client/renderers/live2d/Live2dVisualMount.test.tsx +72 -0
- package/src/client/renderers/live2d/Live2dVisualMount.tsx +1 -0
- package/src/client/renderers/live2d/runtime.ts +8 -2
- package/src/client/renderers/live2d.test.ts +211 -8
- package/src/client/renderers/live2d.ts +129 -29
- package/src/client/settings-card.module.css +2 -0
- package/src/image-dimensions.test.ts +85 -0
- package/src/image-dimensions.ts +76 -0
- package/src/index.ts +1 -1
- package/src/registry.test.ts +58 -5
- package/src/registry.ts +40 -1
- package/src/routes.ts +38 -34
|
@@ -32,6 +32,26 @@ const TAP_GROUP = 'TapBody';
|
|
|
32
32
|
* downsampled atlas only when the effective on-screen scale warrants it.
|
|
33
33
|
*/
|
|
34
34
|
const TEXTURE_OPTIONS = { lod: 'single-auto' };
|
|
35
|
+
/** Recursively release the activation without invalidating shared texture caches. */
|
|
36
|
+
const DESTROY_OPTIONS = { children: true };
|
|
37
|
+
/** Remove only this activation's canvas; `true` would release Pixi globals. */
|
|
38
|
+
const RENDERER_DESTROY_OPTIONS = { removeView: true };
|
|
39
|
+
/** Ignore hidden/zero boxes and keep Pixi dimensions stable and integral. */
|
|
40
|
+
function normalizeRendererSize(width, height) {
|
|
41
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return {
|
|
44
|
+
width: Math.max(1, Math.round(width)),
|
|
45
|
+
height: Math.max(1, Math.round(height)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Fit the model from its unscaled dimensions into the current Pixi screen. */
|
|
49
|
+
function layoutModel(app, model, sourceSize, config) {
|
|
50
|
+
const fit = Math.min(app.renderer.width / sourceSize.width, app.renderer.height / sourceSize.height) * 0.92;
|
|
51
|
+
model.scale.set(fit * (config.scale ?? 1));
|
|
52
|
+
model.anchor.set(0.5);
|
|
53
|
+
model.position.set(app.renderer.width / 2 + (config.translate?.x ?? 0), app.renderer.height / 2 + (config.translate?.y ?? 0));
|
|
54
|
+
}
|
|
35
55
|
let vendorConfigured = false;
|
|
36
56
|
/** Configure pixi extensions + the Cubism SDK once per page. */
|
|
37
57
|
function configureOnce(vendor) {
|
|
@@ -67,11 +87,65 @@ export const live2dRenderer = {
|
|
|
67
87
|
let disposed = false;
|
|
68
88
|
let app;
|
|
69
89
|
let model;
|
|
90
|
+
let modelAttached = false;
|
|
91
|
+
let modelSourceSize;
|
|
92
|
+
let resizeObserver;
|
|
93
|
+
let resizeTracking = false;
|
|
70
94
|
let errorListener;
|
|
71
95
|
let unsubscribe;
|
|
72
96
|
/** The motion group the current phase maps to (resume target after taps). */
|
|
73
97
|
let phaseGroup = config.motions.idle;
|
|
74
98
|
let tapPlaying = false;
|
|
99
|
+
const stopResizeTracking = () => {
|
|
100
|
+
resizeTracking = false;
|
|
101
|
+
resizeObserver?.disconnect();
|
|
102
|
+
resizeObserver = undefined;
|
|
103
|
+
};
|
|
104
|
+
const resizeRenderer = (pixiApp, width, height) => {
|
|
105
|
+
const next = normalizeRendererSize(width, height);
|
|
106
|
+
if (disposed || !resizeTracking || next === undefined)
|
|
107
|
+
return;
|
|
108
|
+
if (pixiApp.renderer.width === next.width && pixiApp.renderer.height === next.height)
|
|
109
|
+
return;
|
|
110
|
+
pixiApp.renderer.resize(next.width, next.height);
|
|
111
|
+
if (model !== undefined && modelSourceSize !== undefined) {
|
|
112
|
+
layoutModel(pixiApp, model, modelSourceSize, config);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const trackContainerSize = (pixiApp) => {
|
|
116
|
+
if (typeof ResizeObserver === 'undefined')
|
|
117
|
+
return;
|
|
118
|
+
resizeTracking = true;
|
|
119
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
120
|
+
const entry = entries.find(candidate => candidate.target === ctx.container);
|
|
121
|
+
if (entry === undefined)
|
|
122
|
+
return;
|
|
123
|
+
resizeRenderer(pixiApp, entry.contentRect.width, entry.contentRect.height);
|
|
124
|
+
});
|
|
125
|
+
resizeObserver.observe(ctx.container);
|
|
126
|
+
// Catch a synchronous layout change between init() and observe().
|
|
127
|
+
resizeRenderer(pixiApp, ctx.container.clientWidth, ctx.container.clientHeight);
|
|
128
|
+
};
|
|
129
|
+
/** Release every resource currently owned by this activation exactly once. */
|
|
130
|
+
const destroyResources = () => {
|
|
131
|
+
stopResizeTracking();
|
|
132
|
+
unsubscribe?.();
|
|
133
|
+
unsubscribe = undefined;
|
|
134
|
+
const currentApp = app;
|
|
135
|
+
const currentModel = model;
|
|
136
|
+
const modelOwnedByApp = currentApp !== undefined && modelAttached;
|
|
137
|
+
app = undefined;
|
|
138
|
+
model = undefined;
|
|
139
|
+
modelSourceSize = undefined;
|
|
140
|
+
modelAttached = false;
|
|
141
|
+
try {
|
|
142
|
+
if (currentModel !== undefined && !modelOwnedByApp)
|
|
143
|
+
currentModel.destroy(DESTROY_OPTIONS);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
currentApp?.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
75
149
|
const playGroup = (group) => {
|
|
76
150
|
if (model === undefined)
|
|
77
151
|
return;
|
|
@@ -107,40 +181,62 @@ export const live2dRenderer = {
|
|
|
107
181
|
}
|
|
108
182
|
configureOnce(vendor);
|
|
109
183
|
const pixiApp = new vendor.Application();
|
|
110
|
-
|
|
111
|
-
width:
|
|
112
|
-
height:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
184
|
+
const initialSize = normalizeRendererSize(ctx.container.clientWidth, ctx.container.clientHeight) ?? {
|
|
185
|
+
width: 160,
|
|
186
|
+
height: 174,
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
await pixiApp.init({
|
|
190
|
+
width: initialSize.width,
|
|
191
|
+
height: initialSize.height,
|
|
192
|
+
backgroundAlpha: 0,
|
|
193
|
+
antialias: true,
|
|
194
|
+
autoDensity: true,
|
|
195
|
+
preference: 'webgl',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// init() can fail after allocating a partial renderer; cleanup is
|
|
200
|
+
// best-effort because Pixi may not consider that partial app ready.
|
|
201
|
+
try {
|
|
202
|
+
pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
118
207
|
if (disposed) {
|
|
119
|
-
pixiApp.destroy(
|
|
208
|
+
pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
|
|
120
209
|
return;
|
|
121
210
|
}
|
|
211
|
+
app = pixiApp;
|
|
122
212
|
pixiApp.canvas.style.display = 'block';
|
|
123
213
|
pixiApp.canvas.style.width = '100%';
|
|
124
214
|
pixiApp.canvas.style.height = '100%';
|
|
125
215
|
ctx.container.appendChild(pixiApp.canvas);
|
|
216
|
+
// Keep a model that rejects during setup off Ticker.shared; from()
|
|
217
|
+
// does not expose that partial instance to callers for disposal.
|
|
218
|
+
trackContainerSize(pixiApp);
|
|
126
219
|
const loaded = await vendor.Live2DModel.from(config.modelUrl, {
|
|
220
|
+
autoUpdate: false,
|
|
127
221
|
autoHitTest: false,
|
|
128
222
|
autoFocus: false,
|
|
129
223
|
textureOptions: TEXTURE_OPTIONS,
|
|
130
224
|
});
|
|
225
|
+
model = loaded;
|
|
131
226
|
if (disposed) {
|
|
132
|
-
|
|
227
|
+
destroyResources();
|
|
133
228
|
return;
|
|
134
229
|
}
|
|
135
|
-
|
|
136
|
-
|
|
230
|
+
modelSourceSize = {
|
|
231
|
+
width: Math.max(1, loaded.width),
|
|
232
|
+
height: Math.max(1, loaded.height),
|
|
233
|
+
};
|
|
137
234
|
// Auto-fit the model into the container; the manifest scale multiplies
|
|
138
235
|
// the fit and translate offsets from the center anchor.
|
|
139
|
-
|
|
140
|
-
loaded.scale.set(fit * (config.scale ?? 1));
|
|
141
|
-
loaded.anchor.set(0.5);
|
|
142
|
-
loaded.position.set(pixiApp.renderer.width / 2 + (config.translate?.x ?? 0), pixiApp.renderer.height / 2 + (config.translate?.y ?? 0));
|
|
236
|
+
layoutModel(pixiApp, loaded, modelSourceSize, config);
|
|
143
237
|
pixiApp.stage.addChild(loaded);
|
|
238
|
+
modelAttached = true;
|
|
239
|
+
loaded.automator.autoUpdate = true;
|
|
144
240
|
// Resume the phase group once a tap motion finishes playing.
|
|
145
241
|
loaded.on('motionFinish', () => {
|
|
146
242
|
if (tapPlaying) {
|
|
@@ -152,20 +248,20 @@ export const live2dRenderer = {
|
|
|
152
248
|
unsubscribe = ctx.phase.subscribe(applyPhase);
|
|
153
249
|
};
|
|
154
250
|
void boot().catch(() => {
|
|
155
|
-
|
|
156
|
-
|
|
251
|
+
try {
|
|
252
|
+
destroyResources();
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
if (!disposed)
|
|
256
|
+
errorListener?.('load-failed');
|
|
257
|
+
}
|
|
157
258
|
});
|
|
158
259
|
return {
|
|
159
260
|
dispose() {
|
|
160
261
|
if (disposed)
|
|
161
262
|
return;
|
|
162
263
|
disposed = true;
|
|
163
|
-
|
|
164
|
-
model = undefined;
|
|
165
|
-
if (app !== undefined) {
|
|
166
|
-
app.destroy(true, { children: true });
|
|
167
|
-
app = undefined;
|
|
168
|
-
}
|
|
264
|
+
destroyResources();
|
|
169
265
|
},
|
|
170
266
|
tap(x, y) {
|
|
171
267
|
const current = model;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal PNG/WebP dimension reader — header-only, no decoding, no
|
|
3
|
+
* dependencies. Used by the decoration registry to verify a strip's actual
|
|
4
|
+
* pixel geometry matches its descriptor (single-row sprite strip; the client
|
|
5
|
+
* renders by frame-column offsets, so a mismatched strip silently shows the
|
|
6
|
+
* wrong frames). Parsing is best-effort: an unrecognized or truncated header
|
|
7
|
+
* returns undefined (the caller decides whether to warn).
|
|
8
|
+
*
|
|
9
|
+
* PNG: signature (8) + IHDR chunk — length (4) + 'IHDR' (4) + width (4) +
|
|
10
|
+
* height (4), both big-endian uint32 at fixed offsets 16/20.
|
|
11
|
+
* WebP: RIFF header (12) + chunk — 'VP8X' extended (width-1/height-1 as
|
|
12
|
+
* little-endian uint24 at 24/27), 'VP8L' lossless (packed 14-bit dims at
|
|
13
|
+
* 21), or 'VP8 ' lossy (frame header, low 14 bits of the uint16 at 26/28).
|
|
14
|
+
* @module @linxin666/dsh-pet/image-dimensions
|
|
15
|
+
*/
|
|
16
|
+
export interface ImageDimensions {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read image pixel dimensions from a PNG or WebP buffer. Returns undefined
|
|
22
|
+
* for formats this reader does not recognize (never throws). Callers treat
|
|
23
|
+
* undefined as "cannot verify", not as an error.
|
|
24
|
+
*/
|
|
25
|
+
export declare function imageDimensions(buf: Buffer): ImageDimensions | undefined;
|
|
26
|
+
//# sourceMappingURL=image-dimensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-dimensions.d.ts","sourceRoot":"","sources":["../../src/image-dimensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AA8CD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAGxE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal PNG/WebP dimension reader — header-only, no decoding, no
|
|
3
|
+
* dependencies. Used by the decoration registry to verify a strip's actual
|
|
4
|
+
* pixel geometry matches its descriptor (single-row sprite strip; the client
|
|
5
|
+
* renders by frame-column offsets, so a mismatched strip silently shows the
|
|
6
|
+
* wrong frames). Parsing is best-effort: an unrecognized or truncated header
|
|
7
|
+
* returns undefined (the caller decides whether to warn).
|
|
8
|
+
*
|
|
9
|
+
* PNG: signature (8) + IHDR chunk — length (4) + 'IHDR' (4) + width (4) +
|
|
10
|
+
* height (4), both big-endian uint32 at fixed offsets 16/20.
|
|
11
|
+
* WebP: RIFF header (12) + chunk — 'VP8X' extended (width-1/height-1 as
|
|
12
|
+
* little-endian uint24 at 24/27), 'VP8L' lossless (packed 14-bit dims at
|
|
13
|
+
* 21), or 'VP8 ' lossy (frame header, low 14 bits of the uint16 at 26/28).
|
|
14
|
+
* @module @linxin666/dsh-pet/image-dimensions
|
|
15
|
+
*/
|
|
16
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
17
|
+
/** Read the pixel size of a PNG buffer, or undefined when unrecognized. */
|
|
18
|
+
function pngDimensions(buf) {
|
|
19
|
+
if (buf.length < 24)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE))
|
|
22
|
+
return undefined;
|
|
23
|
+
if (buf.toString('ascii', 12, 16) !== 'IHDR')
|
|
24
|
+
return undefined;
|
|
25
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
26
|
+
}
|
|
27
|
+
/** Read the pixel size of a WebP buffer, or undefined when unrecognized. */
|
|
28
|
+
function webpDimensions(buf) {
|
|
29
|
+
if (buf.length < 21)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (buf.toString('ascii', 0, 4) !== 'RIFF')
|
|
32
|
+
return undefined;
|
|
33
|
+
if (buf.toString('ascii', 8, 12) !== 'WEBP')
|
|
34
|
+
return undefined;
|
|
35
|
+
const fourcc = buf.toString('ascii', 12, 16);
|
|
36
|
+
if (fourcc === 'VP8X') {
|
|
37
|
+
// Extended header: 1-byte flags at 20, then width-1/height-1 uint24 LE.
|
|
38
|
+
if (buf.length < 30)
|
|
39
|
+
return undefined;
|
|
40
|
+
return {
|
|
41
|
+
width: 1 + buf.readUIntLE(24, 3),
|
|
42
|
+
height: 1 + buf.readUIntLE(27, 3),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (fourcc === 'VP8L') {
|
|
46
|
+
// Lossless header: 0x2f marker at 20, then 14-bit width / 14-bit height
|
|
47
|
+
// packed into the little-endian uint32 at 21.
|
|
48
|
+
if (buf.length < 25)
|
|
49
|
+
return undefined;
|
|
50
|
+
const bits = buf.readUInt32LE(21);
|
|
51
|
+
return {
|
|
52
|
+
width: 1 + (bits & 0x3fff),
|
|
53
|
+
height: 1 + ((bits >>> 14) & 0x3fff),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (fourcc === 'VP8 ') {
|
|
57
|
+
// Lossy frame header: 3-byte tag + 3-byte start code, then width/height
|
|
58
|
+
// as uint16 LE whose low 14 bits carry the dimension.
|
|
59
|
+
if (buf.length < 30)
|
|
60
|
+
return undefined;
|
|
61
|
+
return {
|
|
62
|
+
width: buf.readUInt16LE(26) & 0x3fff,
|
|
63
|
+
height: buf.readUInt16LE(28) & 0x3fff,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read image pixel dimensions from a PNG or WebP buffer. Returns undefined
|
|
70
|
+
* for formats this reader does not recognize (never throws). Callers treat
|
|
71
|
+
* undefined as "cannot verify", not as an error.
|
|
72
|
+
*/
|
|
73
|
+
export function imageDimensions(buf) {
|
|
74
|
+
if (buf.length >= 12 && buf.toString('ascii', 0, 4) === 'RIFF')
|
|
75
|
+
return webpDimensions(buf);
|
|
76
|
+
return pngDimensions(buf);
|
|
77
|
+
}
|
package/lib/types/index.js
CHANGED
|
@@ -80,7 +80,7 @@ function applyImpl(ctx, config = {}) {
|
|
|
80
80
|
// pattern as dsh-remote-web-ui's /api/pair family). The routes are
|
|
81
81
|
// registered while the plugin is enabled; toggling the setting off makes
|
|
82
82
|
// the pet API disappear until it is re-enabled.
|
|
83
|
-
const routes = makePetRoutes({ service });
|
|
83
|
+
const routes = makePetRoutes({ service, ctx });
|
|
84
84
|
let disposeRoutes;
|
|
85
85
|
const syncRoutes = () => {
|
|
86
86
|
const enabled = current().enabled ?? true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC7D,OAAO,EAAuB,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC5F,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
|
package/lib/types/registry.js
CHANGED
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
* whale-girl manifest overrides its own durations.
|
|
27
27
|
* @module @linxin666/dsh-pet/registry
|
|
28
28
|
*/
|
|
29
|
-
import { existsSync,
|
|
29
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
30
30
|
import { homedir } from 'node:os';
|
|
31
31
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
33
|
import { normalizePetRemarks } from './remarks.js';
|
|
34
34
|
import { mergeVoicePacks, normalizeVoicePack } from './voice-pack.js';
|
|
35
35
|
import { parseDecorationManifest } from './decoration.js';
|
|
36
|
+
import { imageDimensions } from './image-dimensions.js';
|
|
36
37
|
import { PET_DECORATION_API_VERSION } from './contracts/status-decoration.js';
|
|
37
38
|
import { dshHome } from './dsh-home.js';
|
|
38
39
|
import { parsePetManifest } from './manifest-v2.js';
|
|
@@ -473,6 +474,29 @@ function loadVoicePackFile(file, options) {
|
|
|
473
474
|
}
|
|
474
475
|
/** Decoration asset URL prefix (served by the decoration route, M5). */
|
|
475
476
|
export const DECORATION_ASSET_PREFIX = '/api/pet/decoration';
|
|
477
|
+
/** Read the pixel dimensions of a decoration strip (PNG/WebP), if decodable. */
|
|
478
|
+
function readImageDimensions(file) {
|
|
479
|
+
let header;
|
|
480
|
+
try {
|
|
481
|
+
// Only the header is needed for dimensions; cap the read so a huge or
|
|
482
|
+
// corrupt strip cannot balloon memory during the registry scan.
|
|
483
|
+
const fd = openSync(file, 'r');
|
|
484
|
+
try {
|
|
485
|
+
header = Buffer.alloc(64);
|
|
486
|
+
const read = readSync(fd, header, 0, header.length, 0);
|
|
487
|
+
if (read < 0)
|
|
488
|
+
return undefined;
|
|
489
|
+
header = header.subarray(0, read);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
closeSync(fd);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
return imageDimensions(header);
|
|
499
|
+
}
|
|
476
500
|
/**
|
|
477
501
|
* Scan one directory of decoration folders ('decoration.json' + strip).
|
|
478
502
|
* Later scans override earlier ones on id collision; a bad descriptor warns
|
|
@@ -523,6 +547,24 @@ function scanDecorationDir(dir, options) {
|
|
|
523
547
|
options.warnings?.push(message);
|
|
524
548
|
options.diagnostics?.push({ level: 'warning', source: entryDir, message });
|
|
525
549
|
}
|
|
550
|
+
else {
|
|
551
|
+
// Geometry check: the client renders the strip as a single row of
|
|
552
|
+
// 'columns' frames (background-position advances by frame width only),
|
|
553
|
+
// so the strip must be exactly cell.width * columns wide and cell.height
|
|
554
|
+
// tall. A mismatched strip silently shows the wrong/partial frames —
|
|
555
|
+
// warn-and-keep, mirroring the missing-strip discipline (never throw).
|
|
556
|
+
const actual = readImageDimensions(join(entryDir, manifest.entry));
|
|
557
|
+
if (actual !== undefined) {
|
|
558
|
+
const expectedWidth = manifest.cell.width * manifest.columns;
|
|
559
|
+
if (actual.width !== expectedWidth || actual.height !== manifest.cell.height) {
|
|
560
|
+
const message = 'decoration ' + manifest.id + ': strip ' + actual.width + 'x' + actual.height
|
|
561
|
+
+ ' does not match cell ' + manifest.cell.width + 'x' + manifest.cell.height + ' x ' + manifest.columns
|
|
562
|
+
+ ' columns (expected ' + expectedWidth + 'x' + manifest.cell.height + '); frames will render wrong';
|
|
563
|
+
options.warnings?.push(message);
|
|
564
|
+
options.diagnostics?.push({ level: 'warning', source: entryDir, message });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
526
568
|
entries.push({
|
|
527
569
|
apiVersion: PET_DECORATION_API_VERSION,
|
|
528
570
|
id: manifest.id,
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* domains are platform-registered, so the pet serves its own API and media —
|
|
6
6
|
* the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
|
|
7
7
|
* is one prefix registration serving every registry entry (manifest, atlas,
|
|
8
|
-
* optional previews), so adding a pet never touches route wiring.
|
|
8
|
+
* optional previews), so adding a pet never touches route wiring. Both the
|
|
9
|
+
* JSON API, the asset prefix, and the Live2D runtime prefix are loopback-only
|
|
10
|
+
* by default; a live paired-device cookie is an extra allow path when
|
|
11
|
+
* remote-web-ui is loaded.
|
|
9
12
|
* @module @linxin666/dsh-pet/routes
|
|
10
13
|
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
15
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
12
16
|
import type { PetService } from './service.ts';
|
|
13
17
|
/** Browser-facing base path of the pet API. */
|
|
@@ -53,6 +57,7 @@ export interface PetRuntimeRoots {
|
|
|
53
57
|
/** Build the full route family (API + assets + runtime) for one service. */
|
|
54
58
|
export declare function makePetRoutes(deps: {
|
|
55
59
|
service: PetService;
|
|
60
|
+
ctx: Context;
|
|
56
61
|
assetCaps?: PetAssetCaps;
|
|
57
62
|
} & PetRuntimeRoots): WebRoute[];
|
|
58
63
|
export { petPackageRoot } from './registry.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAM9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;aACzB,QAAQ;IACR,iDAAiD;aACjD,KAAK;IACL,8EAA8E;aAC9E,KAAK;CACG,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAsPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAwDjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
package/lib/types/routes.js
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* domains are platform-registered, so the pet serves its own API and media —
|
|
6
6
|
* the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
|
|
7
7
|
* is one prefix registration serving every registry entry (manifest, atlas,
|
|
8
|
-
* optional previews), so adding a pet never touches route wiring.
|
|
8
|
+
* optional previews), so adding a pet never touches route wiring. Both the
|
|
9
|
+
* JSON API, the asset prefix, and the Live2D runtime prefix are loopback-only
|
|
10
|
+
* by default; a live paired-device cookie is an extra allow path when
|
|
11
|
+
* remote-web-ui is loaded.
|
|
9
12
|
* @module @linxin666/dsh-pet/routes
|
|
10
13
|
*/
|
|
11
14
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
12
15
|
import { readFile } from 'node:fs/promises';
|
|
13
16
|
import { join, sep } from 'node:path';
|
|
14
17
|
import { DECORATION_ASSET_PREFIX, petEntryView, petPackageRoot } from './registry.js';
|
|
15
|
-
import {
|
|
18
|
+
import { isPetAllowed } from './access.js';
|
|
16
19
|
import { dshHome } from './dsh-home.js';
|
|
17
20
|
/** Browser-facing base path of the pet API. */
|
|
18
21
|
export const PET_API_PREFIX = '/api/pet';
|
|
@@ -114,20 +117,20 @@ function readJsonBody(req) {
|
|
|
114
117
|
req.on('error', reject);
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
|
-
/** Shared route fence:
|
|
118
|
-
function guard(req, res) {
|
|
119
|
-
if (
|
|
120
|
+
/** Shared route fence: loopback always passes; a live paired-device cookie is an extra allow path. */
|
|
121
|
+
function guard(ctx, req, res) {
|
|
122
|
+
if (isPetAllowed(ctx, req))
|
|
120
123
|
return true;
|
|
121
124
|
json(res, 403, { ok: false, error: 'forbidden: loopback-only' });
|
|
122
125
|
return false;
|
|
123
126
|
}
|
|
124
127
|
/** Wrap one async service call as a GET JSON route. */
|
|
125
|
-
function getRoute(path, run) {
|
|
128
|
+
function getRoute(ctx, path, run) {
|
|
126
129
|
return {
|
|
127
130
|
kind: 'exact',
|
|
128
131
|
path,
|
|
129
132
|
handler: (req, res) => {
|
|
130
|
-
if (!guard(req, res))
|
|
133
|
+
if (!guard(ctx, req, res))
|
|
131
134
|
return;
|
|
132
135
|
if (!requireMethod(req, res, 'GET'))
|
|
133
136
|
return;
|
|
@@ -138,12 +141,12 @@ function getRoute(path, run) {
|
|
|
138
141
|
};
|
|
139
142
|
}
|
|
140
143
|
/** Wrap one async service call as a POST JSON route (body passed through). */
|
|
141
|
-
function postRoute(path, run) {
|
|
144
|
+
function postRoute(ctx, path, run) {
|
|
142
145
|
return {
|
|
143
146
|
kind: 'exact',
|
|
144
147
|
path,
|
|
145
148
|
handler: (req, res) => {
|
|
146
|
-
if (!guard(req, res))
|
|
149
|
+
if (!guard(ctx, req, res))
|
|
147
150
|
return Promise.resolve();
|
|
148
151
|
if (!requireMethod(req, res, 'POST'))
|
|
149
152
|
return Promise.resolve();
|
|
@@ -178,10 +181,10 @@ function dirAliases(registry) {
|
|
|
178
181
|
* match; containedRealpath stays as the second layer. Composed pets without
|
|
179
182
|
* a manifest file get a synthesized pet.json.
|
|
180
183
|
*/
|
|
181
|
-
function assetHandler(registry, caps) {
|
|
184
|
+
function assetHandler(ctx, registry, caps) {
|
|
182
185
|
const aliases = dirAliases(registry);
|
|
183
|
-
return (req, res) => {
|
|
184
|
-
if (!guard(req, res))
|
|
186
|
+
return ((req, res) => {
|
|
187
|
+
if (!guard(ctx, req, res))
|
|
185
188
|
return;
|
|
186
189
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
187
190
|
res.writeHead(405);
|
|
@@ -289,7 +292,7 @@ function assetHandler(registry, caps) {
|
|
|
289
292
|
res.end();
|
|
290
293
|
return;
|
|
291
294
|
}
|
|
292
|
-
readFile(resolved).then((body) => {
|
|
295
|
+
return readFile(resolved).then((body) => {
|
|
293
296
|
res.writeHead(200, {
|
|
294
297
|
'content-type': mimeFor(resolved),
|
|
295
298
|
'content-length': String(body.byteLength),
|
|
@@ -304,7 +307,7 @@ function assetHandler(registry, caps) {
|
|
|
304
307
|
res.writeHead(404);
|
|
305
308
|
res.end();
|
|
306
309
|
});
|
|
307
|
-
};
|
|
310
|
+
});
|
|
308
311
|
}
|
|
309
312
|
/** Browser-facing base path of the plugin runtime files (pet-center M3). */
|
|
310
313
|
export const PET_RUNTIME_PREFIX = PET_API_PREFIX + '/runtime';
|
|
@@ -328,9 +331,9 @@ export const PET_RUNTIME_CAP = 16 * 1024 * 1024;
|
|
|
328
331
|
* guidance (the Cubism Core is user-supplied, so its absence is a normal
|
|
329
332
|
* state, not an error).
|
|
330
333
|
*/
|
|
331
|
-
function runtimeHandler(roots) {
|
|
332
|
-
return (req, res) => {
|
|
333
|
-
if (!guard(req, res))
|
|
334
|
+
function runtimeHandler(ctx, roots) {
|
|
335
|
+
return ((req, res) => {
|
|
336
|
+
if (!guard(ctx, req, res))
|
|
334
337
|
return;
|
|
335
338
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
336
339
|
res.writeHead(405);
|
|
@@ -387,7 +390,7 @@ function runtimeHandler(roots) {
|
|
|
387
390
|
res.end();
|
|
388
391
|
return;
|
|
389
392
|
}
|
|
390
|
-
readFile(resolved).then((body) => {
|
|
393
|
+
return readFile(resolved).then((body) => {
|
|
391
394
|
res.writeHead(200, {
|
|
392
395
|
'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
|
|
393
396
|
'content-length': String(body.byteLength),
|
|
@@ -402,7 +405,7 @@ function runtimeHandler(roots) {
|
|
|
402
405
|
res.writeHead(404);
|
|
403
406
|
res.end();
|
|
404
407
|
});
|
|
405
|
-
};
|
|
408
|
+
});
|
|
406
409
|
}
|
|
407
410
|
/**
|
|
408
411
|
* The decoration asset handler behind '/api/pet/decoration/<id>/<file>'
|
|
@@ -411,9 +414,9 @@ function runtimeHandler(roots) {
|
|
|
411
414
|
* match, with realpath containment and the same size ceilings as pet
|
|
412
415
|
* assets. Crafted '..' or '.' segments never match the normalized closure.
|
|
413
416
|
*/
|
|
414
|
-
function decorationHandler(registry, caps) {
|
|
417
|
+
function decorationHandler(ctx, registry, caps) {
|
|
415
418
|
return (req, res) => {
|
|
416
|
-
if (!guard(req, res))
|
|
419
|
+
if (!guard(ctx, req, res))
|
|
417
420
|
return;
|
|
418
421
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
419
422
|
res.writeHead(405);
|
|
@@ -529,36 +532,36 @@ function decorationHandler(registry, caps) {
|
|
|
529
532
|
}
|
|
530
533
|
/** Build the full route family (API + assets + runtime) for one service. */
|
|
531
534
|
export function makePetRoutes(deps) {
|
|
532
|
-
const { service } = deps;
|
|
535
|
+
const { service, ctx } = deps;
|
|
533
536
|
const apiRoutes = [
|
|
534
|
-
getRoute(PET_API_PREFIX + '/state', () => service.state()),
|
|
535
|
-
getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
|
|
536
|
-
getRoute(PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
|
|
537
|
-
postRoute(PET_API_PREFIX + '/interact', (body) => {
|
|
537
|
+
getRoute(ctx, PET_API_PREFIX + '/state', () => service.state()),
|
|
538
|
+
getRoute(ctx, PET_API_PREFIX + '/pets', () => service.pets()),
|
|
539
|
+
getRoute(ctx, PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
|
|
540
|
+
postRoute(ctx, PET_API_PREFIX + '/interact', (body) => {
|
|
538
541
|
const kind = body.kind;
|
|
539
542
|
if (kind !== 'pet' && kind !== 'feed')
|
|
540
543
|
return Promise.reject(new Error('invalid-kind'));
|
|
541
544
|
return service.interact(kind);
|
|
542
545
|
}),
|
|
543
|
-
postRoute(PET_API_PREFIX + '/set-visible', (body) => {
|
|
546
|
+
postRoute(ctx, PET_API_PREFIX + '/set-visible', (body) => {
|
|
544
547
|
const visible = body.visible;
|
|
545
548
|
if (typeof visible !== 'boolean')
|
|
546
549
|
return Promise.reject(new Error('invalid-visible'));
|
|
547
550
|
return service.setVisible(visible);
|
|
548
551
|
}),
|
|
549
|
-
postRoute(PET_API_PREFIX + '/set-config', (body) => service.setConfig({
|
|
552
|
+
postRoute(ctx, PET_API_PREFIX + '/set-config', (body) => service.setConfig({
|
|
550
553
|
...(typeof body.size === 'number' ? { size: body.size } : {}),
|
|
551
554
|
...(typeof body.right === 'number' ? { right: body.right } : {}),
|
|
552
555
|
...(typeof body.bottom === 'number' ? { bottom: body.bottom } : {}),
|
|
553
556
|
...(typeof body.visible === 'boolean' ? { visible: body.visible } : {}),
|
|
554
557
|
})),
|
|
555
|
-
postRoute(PET_API_PREFIX + '/set-name', (body) => {
|
|
558
|
+
postRoute(ctx, PET_API_PREFIX + '/set-name', (body) => {
|
|
556
559
|
const name = body.name;
|
|
557
560
|
if (typeof name !== 'string')
|
|
558
561
|
return Promise.reject(new Error('invalid-name'));
|
|
559
562
|
return service.setName(name);
|
|
560
563
|
}),
|
|
561
|
-
postRoute(PET_API_PREFIX + '/set-pet', (body) => {
|
|
564
|
+
postRoute(ctx, PET_API_PREFIX + '/set-pet', (body) => {
|
|
562
565
|
const petId = body.petId;
|
|
563
566
|
if (typeof petId !== 'string')
|
|
564
567
|
return Promise.reject(new Error('invalid-pet'));
|
|
@@ -568,12 +571,12 @@ export function makePetRoutes(deps) {
|
|
|
568
571
|
const assetRoute = {
|
|
569
572
|
kind: 'prefix',
|
|
570
573
|
path: PET_ASSET_PREFIX,
|
|
571
|
-
handler: assetHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
574
|
+
handler: assetHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
572
575
|
};
|
|
573
576
|
const runtimeRoute = {
|
|
574
577
|
kind: 'prefix',
|
|
575
578
|
path: PET_RUNTIME_PREFIX,
|
|
576
|
-
handler: runtimeHandler({
|
|
579
|
+
handler: runtimeHandler(ctx, {
|
|
577
580
|
runtimeDir: deps.runtimeDir ?? join(dshHome(), 'pets', '.runtime'),
|
|
578
581
|
vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
|
|
579
582
|
}),
|
|
@@ -581,7 +584,7 @@ export function makePetRoutes(deps) {
|
|
|
581
584
|
const decorationRoute = {
|
|
582
585
|
kind: 'prefix',
|
|
583
586
|
path: DECORATION_ASSET_PREFIX,
|
|
584
|
-
handler: decorationHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
587
|
+
handler: decorationHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
585
588
|
};
|
|
586
589
|
return [...apiRoutes, assetRoute, runtimeRoute, decorationRoute];
|
|
587
590
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|