@linxin666/dsh-pet 0.2.2 → 0.2.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/README.i18n.yaml +2 -2
- package/README.md +129 -3
- package/README.zh.md +129 -3
- package/assets/decorations/whale/decoration.json +20 -0
- package/assets/decorations/whale/whale-frames.png +0 -0
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/contracts/pet-manifest-v2.schema.json +281 -0
- package/contracts/status-decoration-v1.schema.json +144 -0
- package/contracts/voice-pack-v1.schema.json +234 -0
- package/lib/client.js +581 -38
- package/lib/client.js.map +1 -1
- package/lib/index.js +1684 -59
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- package/lib/types/chatter.d.ts +61 -3
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +71 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -1
- package/lib/types/client/PetSettingsCard.d.ts +14 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +24 -1
- package/lib/types/client/PetSprite.d.ts +7 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +120 -12
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -0
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +14 -0
- package/lib/types/client/phase-stream.d.ts +22 -0
- package/lib/types/client/phase-stream.d.ts.map +1 -0
- package/lib/types/client/phase-stream.js +28 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
- package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/runtime.js +63 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
- package/lib/types/client/renderers/live2d.d.ts +52 -0
- package/lib/types/client/renderers/live2d.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d.js +192 -0
- package/lib/types/client/renderers/registry.d.ts +32 -0
- package/lib/types/client/renderers/registry.d.ts.map +1 -0
- package/lib/types/client/renderers/registry.js +49 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- package/lib/types/contracts/renderer.d.ts +50 -0
- package/lib/types/contracts/renderer.d.ts.map +1 -0
- package/lib/types/contracts/renderer.js +15 -0
- package/lib/types/contracts/status-decoration.d.ts +85 -0
- package/lib/types/contracts/status-decoration.d.ts.map +1 -0
- package/lib/types/contracts/status-decoration.js +21 -0
- package/lib/types/decoration.d.ts +39 -0
- package/lib/types/decoration.d.ts.map +1 -0
- package/lib/types/decoration.js +210 -0
- package/lib/types/event-projection.d.ts +8 -3
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +9 -4
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -0
- package/lib/types/manifest-v2.d.ts +129 -0
- package/lib/types/manifest-v2.d.ts.map +1 -0
- package/lib/types/manifest-v2.js +387 -0
- package/lib/types/model3.d.ts +26 -0
- package/lib/types/model3.d.ts.map +1 -0
- package/lib/types/model3.js +96 -0
- package/lib/types/registry.d.ts +112 -5
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +395 -38
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +312 -13
- package/lib/types/service.d.ts +38 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +46 -3
- package/lib/types/voice-pack.d.ts +98 -0
- package/lib/types/voice-pack.d.ts.map +1 -0
- package/lib/types/voice-pack.js +384 -0
- package/package.json +13 -10
- package/src/chatter.test.ts +89 -2
- package/src/chatter.ts +120 -14
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +57 -0
- package/src/client/PetSprite.test.tsx +265 -12
- package/src/client/PetSprite.tsx +165 -33
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +14 -0
- package/src/client/phase-stream.test.ts +76 -0
- package/src/client/phase-stream.ts +39 -0
- package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
- package/src/client/renderers/PetRendererSwitch.tsx +48 -0
- package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
- package/src/client/renderers/live2d/runtime.test.ts +61 -0
- package/src/client/renderers/live2d/runtime.ts +119 -0
- package/src/client/renderers/live2d/vendor-entry.ts +14 -0
- package/src/client/renderers/live2d.test.ts +263 -0
- package/src/client/renderers/live2d.ts +231 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-form.ts +8 -6
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -0
- package/src/contracts/status-decoration.ts +78 -0
- package/src/decoration.test.ts +178 -0
- package/src/decoration.ts +220 -0
- package/src/event-projection.ts +10 -5
- package/src/index.ts +2 -0
- package/src/manifest-v2.test.ts +251 -0
- package/src/manifest-v2.ts +414 -0
- package/src/model3.test.ts +79 -0
- package/src/model3.ts +91 -0
- package/src/registry.test.ts +438 -2
- package/src/registry.ts +470 -38
- package/src/routes.ts +328 -14
- package/src/service.ts +65 -3
- package/src/voice-pack.test.ts +216 -0
- package/src/voice-pack.ts +413 -0
package/lib/types/routes.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* optional previews), so adding a pet never touches route wiring.
|
|
9
9
|
* @module @linxin666/dsh-pet/routes
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
11
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { petEntryView } from './registry.js';
|
|
13
|
+
import { join, sep } from 'node:path';
|
|
14
|
+
import { DECORATION_ASSET_PREFIX, petEntryView, petPackageRoot } from './registry.js';
|
|
15
15
|
import { isLoopbackRequest } from './loopback.js';
|
|
16
|
+
import { dshHome } from './dsh-home.js';
|
|
16
17
|
/** Browser-facing base path of the pet API. */
|
|
17
18
|
export const PET_API_PREFIX = '/api/pet';
|
|
18
19
|
/** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
|
|
@@ -20,6 +21,43 @@ export const PET_ASSET_PREFIX = '/pet';
|
|
|
20
21
|
const MANIFEST_FILE = 'pet.json';
|
|
21
22
|
const PREVIEW_DIR = 'previews';
|
|
22
23
|
const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
24
|
+
/**
|
|
25
|
+
* Per-class size ceilings for served pet assets, in bytes (pet-center M2 P3,
|
|
26
|
+
* issue #623). Constants are tested directly; makePetRoutes accepts an
|
|
27
|
+
* override so tests can exercise the 413 path with tiny caps.
|
|
28
|
+
*/
|
|
29
|
+
export const PET_ASSET_CAPS = {
|
|
30
|
+
/** pet.json manifest. */
|
|
31
|
+
manifest: 64 * 1024,
|
|
32
|
+
/** Atlas, preview and Live2D texture imagery. */
|
|
33
|
+
image: 20 * 1024 * 1024,
|
|
34
|
+
/** Live2D model closure files (.moc3, motion/physics/expression JSON; M3). */
|
|
35
|
+
model: 32 * 1024 * 1024,
|
|
36
|
+
};
|
|
37
|
+
/** Imagery extensions classify into the image cap; everything else served from a closure is model-class. */
|
|
38
|
+
const IMAGE_EXTENSIONS = new Set(['.webp', '.png', '.gif', '.jpg', '.jpeg']);
|
|
39
|
+
/** Lowercased file extension ('' when none). */
|
|
40
|
+
function extensionOf(file) {
|
|
41
|
+
const dot = file.lastIndexOf('.');
|
|
42
|
+
return dot < 0 ? '' : file.slice(dot).toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* realpath containment: resolve both sides and require the candidate to stay
|
|
46
|
+
* inside the base directory. A pet directory (or an atlas/preview inside it)
|
|
47
|
+
* that is a symlink escaping its root is rejected, never followed.
|
|
48
|
+
*/
|
|
49
|
+
export function containedRealpath(base, candidate) {
|
|
50
|
+
try {
|
|
51
|
+
const realBase = realpathSync(base);
|
|
52
|
+
const realCandidate = realpathSync(candidate);
|
|
53
|
+
return realCandidate === realBase || realCandidate.startsWith(realBase + sep)
|
|
54
|
+
? realCandidate
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
23
61
|
const MIME_BY_EXT = {
|
|
24
62
|
'.webp': 'image/webp',
|
|
25
63
|
'.png': 'image/png',
|
|
@@ -133,10 +171,14 @@ function dirAliases(registry) {
|
|
|
133
171
|
/**
|
|
134
172
|
* The one asset handler behind the '/pet' prefix. Resolves the pet by id (or
|
|
135
173
|
* legacy directory alias), then serves exactly the files a manifest declares:
|
|
136
|
-
* pet.json, the
|
|
137
|
-
*
|
|
174
|
+
* pet.json, the entry's servable set (the sprite2d atlas, or the live2d
|
|
175
|
+
* model3.json plus its reference closure — pet-center M3), and optional
|
|
176
|
+
* 'previews/<name>' media. The servable match is an exact string comparison
|
|
177
|
+
* against scan-time normalized paths, so crafted '..' or '.' segments never
|
|
178
|
+
* match; containedRealpath stays as the second layer. Composed pets without
|
|
179
|
+
* a manifest file get a synthesized pet.json.
|
|
138
180
|
*/
|
|
139
|
-
function assetHandler(registry) {
|
|
181
|
+
function assetHandler(registry, caps) {
|
|
140
182
|
const aliases = dirAliases(registry);
|
|
141
183
|
return (req, res) => {
|
|
142
184
|
if (!guard(req, res))
|
|
@@ -198,15 +240,15 @@ function assetHandler(registry) {
|
|
|
198
240
|
if (file === undefined)
|
|
199
241
|
synthesized = true;
|
|
200
242
|
}
|
|
201
|
-
else if (rest.length > 0 &&
|
|
202
|
-
file = join(entry.dir,
|
|
243
|
+
else if (rest.length > 0 && entry.servable.includes(rel)) {
|
|
244
|
+
file = join(entry.dir, rel);
|
|
203
245
|
}
|
|
204
246
|
else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
|
|
205
247
|
const preview = join(entry.dir, PREVIEW_DIR, rest[1]);
|
|
206
248
|
file = existsSync(preview) ? preview : undefined;
|
|
207
249
|
}
|
|
208
250
|
if (synthesized) {
|
|
209
|
-
const body = Buffer.from(JSON.stringify(petEntryView(entry), null, 2), 'utf8');
|
|
251
|
+
const body = Buffer.from(JSON.stringify(petEntryView(entry, registry.globalVoice), null, 2), 'utf8');
|
|
210
252
|
res.writeHead(200, {
|
|
211
253
|
'content-type': 'application/json; charset=utf-8',
|
|
212
254
|
'content-length': String(body.byteLength),
|
|
@@ -224,7 +266,29 @@ function assetHandler(registry) {
|
|
|
224
266
|
res.end();
|
|
225
267
|
return;
|
|
226
268
|
}
|
|
227
|
-
|
|
269
|
+
// realpath containment: a symlink escaping the pet directory is refused.
|
|
270
|
+
const resolved = containedRealpath(entry.dir, file);
|
|
271
|
+
if (resolved === undefined) {
|
|
272
|
+
res.writeHead(403);
|
|
273
|
+
res.end();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Enforce the size ceiling before the file is read into memory.
|
|
277
|
+
const cap = rest.length === 1 && rest[0] === MANIFEST_FILE
|
|
278
|
+
? caps.manifest
|
|
279
|
+
: IMAGE_EXTENSIONS.has(extensionOf(rel)) ? caps.image : caps.model;
|
|
280
|
+
try {
|
|
281
|
+
if (statSync(resolved).size > cap) {
|
|
282
|
+
res.writeHead(413);
|
|
283
|
+
res.end();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
res.writeHead(404);
|
|
289
|
+
res.end();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
228
292
|
readFile(resolved).then((body) => {
|
|
229
293
|
res.writeHead(200, {
|
|
230
294
|
'content-type': mimeFor(resolved),
|
|
@@ -242,12 +306,234 @@ function assetHandler(registry) {
|
|
|
242
306
|
});
|
|
243
307
|
};
|
|
244
308
|
}
|
|
245
|
-
/**
|
|
309
|
+
/** Browser-facing base path of the plugin runtime files (pet-center M3). */
|
|
310
|
+
export const PET_RUNTIME_PREFIX = PET_API_PREFIX + '/runtime';
|
|
311
|
+
/**
|
|
312
|
+
* The runtime files the route may serve, by exact name (no slashes, no
|
|
313
|
+
* user-controlled path segments, so traversal is structurally impossible):
|
|
314
|
+
* the user-supplied Cubism Core from the pet runtime directory (the plugin
|
|
315
|
+
* never bundles or downloads it — issue #623 M1 §0) and the plugin-shipped
|
|
316
|
+
* MIT vendor bundle from the package lib directory.
|
|
317
|
+
*/
|
|
318
|
+
const RUNTIME_FILES = {
|
|
319
|
+
'live2dcubismcore.min.js': { root: 'runtimeDir' },
|
|
320
|
+
'live2d-vendor.js': { root: 'vendorDir' },
|
|
321
|
+
'live2d-vendor.js.map': { root: 'vendorDir' },
|
|
322
|
+
};
|
|
323
|
+
/** Size ceiling for one runtime file (the Cubism Core is ~200 KB today). */
|
|
324
|
+
export const PET_RUNTIME_CAP = 16 * 1024 * 1024;
|
|
325
|
+
/**
|
|
326
|
+
* The runtime handler behind '/api/pet/runtime/<name>'. A missing file
|
|
327
|
+
* answers 404 with a JSON marker the client renderer turns into install
|
|
328
|
+
* guidance (the Cubism Core is user-supplied, so its absence is a normal
|
|
329
|
+
* state, not an error).
|
|
330
|
+
*/
|
|
331
|
+
function runtimeHandler(roots) {
|
|
332
|
+
return (req, res) => {
|
|
333
|
+
if (!guard(req, res))
|
|
334
|
+
return;
|
|
335
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
336
|
+
res.writeHead(405);
|
|
337
|
+
res.end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let pathname;
|
|
341
|
+
try {
|
|
342
|
+
pathname = new URL(req.url ?? '/', 'http://pet.local').pathname;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
res.writeHead(400);
|
|
346
|
+
res.end();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const rest = pathname.slice(PET_RUNTIME_PREFIX.length).replace(/^\/+/, '');
|
|
350
|
+
let name;
|
|
351
|
+
try {
|
|
352
|
+
name = decodeURIComponent(rest);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
res.writeHead(400);
|
|
356
|
+
res.end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const spec = RUNTIME_FILES[name];
|
|
360
|
+
// Exact-name allow-list: anything with a path separator never matches.
|
|
361
|
+
if (spec === undefined) {
|
|
362
|
+
res.writeHead(404);
|
|
363
|
+
res.end();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const base = spec.root === 'runtimeDir' ? roots.runtimeDir : roots.vendorDir;
|
|
367
|
+
const file = join(base, name);
|
|
368
|
+
if (!existsSync(file)) {
|
|
369
|
+
json(res, 404, { ok: false, error: 'runtime-file-missing', file: name });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const resolved = containedRealpath(base, file);
|
|
373
|
+
if (resolved === undefined) {
|
|
374
|
+
res.writeHead(403);
|
|
375
|
+
res.end();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
if (statSync(resolved).size > PET_RUNTIME_CAP) {
|
|
380
|
+
res.writeHead(413);
|
|
381
|
+
res.end();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
res.writeHead(404);
|
|
387
|
+
res.end();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
readFile(resolved).then((body) => {
|
|
391
|
+
res.writeHead(200, {
|
|
392
|
+
'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
|
|
393
|
+
'content-length': String(body.byteLength),
|
|
394
|
+
'cache-control': 'no-cache',
|
|
395
|
+
});
|
|
396
|
+
if (req.method === 'HEAD') {
|
|
397
|
+
res.end();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
res.end(body);
|
|
401
|
+
}, () => {
|
|
402
|
+
res.writeHead(404);
|
|
403
|
+
res.end();
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The decoration asset handler behind '/api/pet/decoration/<id>/<file>'
|
|
409
|
+
* (pet-center M5, #567). Serves exactly the files a decoration descriptor
|
|
410
|
+
* declares — decoration.json and the PNG/WebP strip — by exact allow-list
|
|
411
|
+
* match, with realpath containment and the same size ceilings as pet
|
|
412
|
+
* assets. Crafted '..' or '.' segments never match the normalized closure.
|
|
413
|
+
*/
|
|
414
|
+
function decorationHandler(registry, caps) {
|
|
415
|
+
return (req, res) => {
|
|
416
|
+
if (!guard(req, res))
|
|
417
|
+
return;
|
|
418
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
419
|
+
res.writeHead(405);
|
|
420
|
+
res.end();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
let pathname;
|
|
424
|
+
try {
|
|
425
|
+
pathname = new URL(req.url ?? '/', 'http://pet.local').pathname;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
res.writeHead(400);
|
|
429
|
+
res.end();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const segments = pathname.split('/').filter(segment => segment !== '');
|
|
433
|
+
const prefixSegments = DECORATION_ASSET_PREFIX.split('/').filter(segment => segment !== '');
|
|
434
|
+
if (segments.length < prefixSegments.length + 2) {
|
|
435
|
+
res.writeHead(404);
|
|
436
|
+
res.end();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
for (let i = 0; i < prefixSegments.length; i += 1) {
|
|
440
|
+
if (segments[i] !== prefixSegments[i]) {
|
|
441
|
+
res.writeHead(404);
|
|
442
|
+
res.end();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
let id;
|
|
447
|
+
try {
|
|
448
|
+
id = decodeURIComponent(segments[prefixSegments.length]);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
res.writeHead(400);
|
|
452
|
+
res.end();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const entry = registry.decorationById?.(id);
|
|
456
|
+
if (entry === undefined) {
|
|
457
|
+
res.writeHead(404);
|
|
458
|
+
res.end();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const rest = [];
|
|
462
|
+
for (const segment of segments.slice(prefixSegments.length + 1)) {
|
|
463
|
+
let decoded;
|
|
464
|
+
try {
|
|
465
|
+
decoded = decodeURIComponent(segment);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
res.writeHead(400);
|
|
469
|
+
res.end();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
rest.push(decoded);
|
|
473
|
+
}
|
|
474
|
+
const rel = rest.join('/');
|
|
475
|
+
if (!entry.servable.includes(rel)) {
|
|
476
|
+
res.writeHead(404);
|
|
477
|
+
res.end();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const file = join(entry.dir, rel);
|
|
481
|
+
const resolved = containedRealpath(entry.dir, file);
|
|
482
|
+
if (resolved === undefined) {
|
|
483
|
+
res.writeHead(403);
|
|
484
|
+
res.end();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const cap = rel === 'decoration.json' ? caps.manifest : caps.image;
|
|
488
|
+
let stat;
|
|
489
|
+
try {
|
|
490
|
+
stat = statSync(resolved);
|
|
491
|
+
if (stat.size > cap) {
|
|
492
|
+
res.writeHead(413);
|
|
493
|
+
res.end();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
res.writeHead(404);
|
|
499
|
+
res.end();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// Weak ETag from size + mtime: 'no-cache' forces revalidation, and the
|
|
503
|
+
// validator lets repeat requests settle as 304 — the ornament remounts
|
|
504
|
+
// on whisper and display-session flips, and without a validator each
|
|
505
|
+
// remount would re-download the full strip body.
|
|
506
|
+
const etag = '"' + stat.size.toString(16) + '-' + Math.round(stat.mtimeMs).toString(16) + '"';
|
|
507
|
+
if (req.headers['if-none-match'] === etag) {
|
|
508
|
+
res.writeHead(304, { etag, 'cache-control': 'no-cache' });
|
|
509
|
+
res.end();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
readFile(resolved).then((body) => {
|
|
513
|
+
res.writeHead(200, {
|
|
514
|
+
'content-type': mimeFor(resolved),
|
|
515
|
+
'content-length': String(body.byteLength),
|
|
516
|
+
'cache-control': 'no-cache',
|
|
517
|
+
etag,
|
|
518
|
+
});
|
|
519
|
+
if (req.method === 'HEAD') {
|
|
520
|
+
res.end();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
res.end(body);
|
|
524
|
+
}, () => {
|
|
525
|
+
res.writeHead(404);
|
|
526
|
+
res.end();
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/** Build the full route family (API + assets + runtime) for one service. */
|
|
246
531
|
export function makePetRoutes(deps) {
|
|
247
532
|
const { service } = deps;
|
|
248
533
|
const apiRoutes = [
|
|
249
534
|
getRoute(PET_API_PREFIX + '/state', () => service.state()),
|
|
250
535
|
getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
|
|
536
|
+
getRoute(PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
|
|
251
537
|
postRoute(PET_API_PREFIX + '/interact', (body) => {
|
|
252
538
|
const kind = body.kind;
|
|
253
539
|
if (kind !== 'pet' && kind !== 'feed')
|
|
@@ -282,9 +568,22 @@ export function makePetRoutes(deps) {
|
|
|
282
568
|
const assetRoute = {
|
|
283
569
|
kind: 'prefix',
|
|
284
570
|
path: PET_ASSET_PREFIX,
|
|
285
|
-
handler: assetHandler(service.registrySnapshot()),
|
|
571
|
+
handler: assetHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
572
|
+
};
|
|
573
|
+
const runtimeRoute = {
|
|
574
|
+
kind: 'prefix',
|
|
575
|
+
path: PET_RUNTIME_PREFIX,
|
|
576
|
+
handler: runtimeHandler({
|
|
577
|
+
runtimeDir: deps.runtimeDir ?? join(dshHome(), 'pets', '.runtime'),
|
|
578
|
+
vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
|
|
579
|
+
}),
|
|
580
|
+
};
|
|
581
|
+
const decorationRoute = {
|
|
582
|
+
kind: 'prefix',
|
|
583
|
+
path: DECORATION_ASSET_PREFIX,
|
|
584
|
+
handler: decorationHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
286
585
|
};
|
|
287
|
-
return [...apiRoutes, assetRoute];
|
|
586
|
+
return [...apiRoutes, assetRoute, runtimeRoute, decorationRoute];
|
|
288
587
|
}
|
|
289
588
|
// Re-exported for the package surface (the registry owns the definition now).
|
|
290
589
|
export { petPackageRoot } from './registry.js';
|
package/lib/types/service.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity
|
|
|
16
16
|
import type { TreatConfig } from './treats.ts';
|
|
17
17
|
import { type LedgerInteractionResult } from './ledger.ts';
|
|
18
18
|
import { type PetDisplayConfig } from './persist.ts';
|
|
19
|
-
import { type PetDefinition, type PetManifest, type PetRegistry } from './registry.ts';
|
|
19
|
+
import { type PetDefinition, type PetManifest, type PetRegistry, type PetRegistryDiagnostic } from './registry.ts';
|
|
20
|
+
import type { DecorationView } from './contracts/status-decoration.ts';
|
|
20
21
|
import { type PetStateConfig, type PetStateSnapshot } from './state.ts';
|
|
21
22
|
/** Plugin configuration. */
|
|
22
23
|
export interface PetConfig {
|
|
@@ -30,6 +31,8 @@ export interface PetConfig {
|
|
|
30
31
|
persistDir?: string;
|
|
31
32
|
/** Master switch for the plugin (browser half + host routes). */
|
|
32
33
|
enabled?: boolean;
|
|
34
|
+
/** Status-decoration master switch (pet-center M5, #567); defaults to on. */
|
|
35
|
+
decorationEnabled?: boolean;
|
|
33
36
|
/** Prebuilt registry (tests); defaults to scanning the package + user dirs. */
|
|
34
37
|
registry?: PetRegistry;
|
|
35
38
|
/** Extra manifest entries composed by the embedding application. */
|
|
@@ -55,6 +58,11 @@ export interface PetSettingsSection {
|
|
|
55
58
|
bottom: number;
|
|
56
59
|
/** Master switch for the plugin (browser half + host routes). */
|
|
57
60
|
enabled?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Status-decoration master switch (pet-center M5, #567). Defaults to on;
|
|
63
|
+
* the settings surface mirrors this field and can turn it off.
|
|
64
|
+
*/
|
|
65
|
+
decorationEnabled?: boolean;
|
|
58
66
|
}
|
|
59
67
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
60
68
|
export declare const PET_SETTINGS_NAMESPACE = "pet";
|
|
@@ -119,6 +127,12 @@ export interface PetStateView {
|
|
|
119
127
|
* rendered by the client as a distinct whisper bubble.
|
|
120
128
|
*/
|
|
121
129
|
whisper?: string;
|
|
130
|
+
/**
|
|
131
|
+
* The active status decoration (pet-center M5, #567), when the master
|
|
132
|
+
* switch is on and the default decoration entry exists. Absent means the
|
|
133
|
+
* browser half renders no ornament.
|
|
134
|
+
*/
|
|
135
|
+
decoration?: DecorationView;
|
|
122
136
|
}
|
|
123
137
|
/** Result of `pet.interact`. */
|
|
124
138
|
export type PetInteractResult = LedgerInteractionResult;
|
|
@@ -141,9 +155,17 @@ export declare class PetService extends Service {
|
|
|
141
155
|
private readonly registry;
|
|
142
156
|
private readonly persistDir;
|
|
143
157
|
private enabled;
|
|
158
|
+
/** Status-decoration master switch (M5, #567); mirrored from settings. */
|
|
159
|
+
private decorationEnabled;
|
|
144
160
|
private disposeActivity;
|
|
145
161
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
146
162
|
private displaySession;
|
|
163
|
+
/**
|
|
164
|
+
* Effective voice-pack overrides for the currently selected pet (M4,
|
|
165
|
+
* #677). Cached per pet id; the registry is an immutable snapshot, so the
|
|
166
|
+
* global pack and each entry's pack cannot change behind the cache.
|
|
167
|
+
*/
|
|
168
|
+
private voiceCache;
|
|
147
169
|
/**
|
|
148
170
|
* Per-session activity, most recent last (Map insertion order). Bounded by
|
|
149
171
|
* MAX_SESSION_BUBBLES so a burst of sessions cannot grow it without bound;
|
|
@@ -151,6 +173,12 @@ export declare class PetService extends Service {
|
|
|
151
173
|
*/
|
|
152
174
|
private readonly sessionActivity;
|
|
153
175
|
constructor(ctx: Context, config?: PetConfig);
|
|
176
|
+
/**
|
|
177
|
+
* The draw-time voice-pool provider handed to every projection runtime.
|
|
178
|
+
* It re-resolves when the selected pet changes, so live engines re-voice
|
|
179
|
+
* on the next draw without being rebuilt (M4, #677).
|
|
180
|
+
*/
|
|
181
|
+
private voicePools;
|
|
154
182
|
/** Whether the pet service consumes session activity while enabled. */
|
|
155
183
|
isEnabled(): boolean;
|
|
156
184
|
/** RPC: current pet state snapshot. */
|
|
@@ -161,6 +189,15 @@ export declare class PetService extends Service {
|
|
|
161
189
|
pets(): Promise<PetDefinition[]>;
|
|
162
190
|
/** The loaded registry (the asset routes serve its entries). */
|
|
163
191
|
registrySnapshot(): PetRegistry;
|
|
192
|
+
/** RPC: structured registry diagnostics (pet-center M2, issue #623). */
|
|
193
|
+
diagnostics(): Promise<{
|
|
194
|
+
diagnostics: PetRegistryDiagnostic[];
|
|
195
|
+
}>;
|
|
196
|
+
/**
|
|
197
|
+
* The active status decoration view (M5, #567): the default 'whale' entry
|
|
198
|
+
* (user directories override built-ins by id), gated by the master switch.
|
|
199
|
+
*/
|
|
200
|
+
private activeDecoration;
|
|
164
201
|
/** The selected pet's registry entry. */
|
|
165
202
|
activeEntry(): NonNullable<PetRegistry['entries'][number]>;
|
|
166
203
|
/** Currently selected pet id (persisted). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,EAAgC,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EASL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAA;AACrB,OAAO,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -15,8 +15,9 @@ import { Service } from '@deepseek-ai/cordis';
|
|
|
15
15
|
import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from './event-projection.js';
|
|
16
16
|
import { PetLedger } from './ledger.js';
|
|
17
17
|
import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from './persist.js';
|
|
18
|
-
import { loadPetRegistry, petEntryView, petPackageRoot, } from './registry.js';
|
|
18
|
+
import { DEFAULT_DECORATION_ID, decorationView, loadPetRegistry, petEntryView, petPackageRoot, } from './registry.js';
|
|
19
19
|
import { WHISPER_TTL_MS } from './chatter.js';
|
|
20
|
+
import { mergeVoicePacks } from './voice-pack.js';
|
|
20
21
|
import { defaultPetStateConfig, PetStateMachine, } from './state.js';
|
|
21
22
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
22
23
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
@@ -36,9 +37,17 @@ export class PetService extends Service {
|
|
|
36
37
|
registry;
|
|
37
38
|
persistDir;
|
|
38
39
|
enabled;
|
|
40
|
+
/** Status-decoration master switch (M5, #567); mirrored from settings. */
|
|
41
|
+
decorationEnabled;
|
|
39
42
|
disposeActivity;
|
|
40
43
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
41
44
|
displaySession;
|
|
45
|
+
/**
|
|
46
|
+
* Effective voice-pack overrides for the currently selected pet (M4,
|
|
47
|
+
* #677). Cached per pet id; the registry is an immutable snapshot, so the
|
|
48
|
+
* global pack and each entry's pack cannot change behind the cache.
|
|
49
|
+
*/
|
|
50
|
+
voiceCache;
|
|
42
51
|
/**
|
|
43
52
|
* Per-session activity, most recent last (Map insertion order). Bounded by
|
|
44
53
|
* MAX_SESSION_BUBBLES so a burst of sessions cannot grow it without bound;
|
|
@@ -72,8 +81,25 @@ export class PetService extends Service {
|
|
|
72
81
|
this.stateConfig = { ...defaultPetStateConfig, ...(config.state ?? {}) };
|
|
73
82
|
this.machine = new PetStateMachine(this.stateConfig);
|
|
74
83
|
this.enabled = config.enabled ?? true;
|
|
84
|
+
this.decorationEnabled = config.decorationEnabled ?? true;
|
|
75
85
|
this.syncActivity();
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The draw-time voice-pool provider handed to every projection runtime.
|
|
89
|
+
* It re-resolves when the selected pet changes, so live engines re-voice
|
|
90
|
+
* on the next draw without being rebuilt (M4, #677).
|
|
91
|
+
*/
|
|
92
|
+
voicePools() {
|
|
93
|
+
return () => {
|
|
94
|
+
const entry = this.activeEntry();
|
|
95
|
+
if (this.voiceCache !== undefined && this.voiceCache.petId === entry.id) {
|
|
96
|
+
return this.voiceCache.overrides;
|
|
97
|
+
}
|
|
98
|
+
const overrides = mergeVoicePacks(this.registry.globalVoice, entry.voice)?.overrides ?? {};
|
|
99
|
+
this.voiceCache = { petId: entry.id, overrides };
|
|
100
|
+
return overrides;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
77
103
|
/** Whether the pet service consumes session activity while enabled. */
|
|
78
104
|
isEnabled() {
|
|
79
105
|
return this.enabled;
|
|
@@ -88,12 +114,26 @@ export class PetService extends Service {
|
|
|
88
114
|
}
|
|
89
115
|
/** RPC: the registry entries the browser half renders and selects from. */
|
|
90
116
|
async pets() {
|
|
91
|
-
return this.registry.entries.map(petEntryView);
|
|
117
|
+
return this.registry.entries.map(entry => petEntryView(entry, this.registry.globalVoice));
|
|
92
118
|
}
|
|
93
119
|
/** The loaded registry (the asset routes serve its entries). */
|
|
94
120
|
registrySnapshot() {
|
|
95
121
|
return this.registry;
|
|
96
122
|
}
|
|
123
|
+
/** RPC: structured registry diagnostics (pet-center M2, issue #623). */
|
|
124
|
+
async diagnostics() {
|
|
125
|
+
return { diagnostics: this.registry.diagnostics };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The active status decoration view (M5, #567): the default 'whale' entry
|
|
129
|
+
* (user directories override built-ins by id), gated by the master switch.
|
|
130
|
+
*/
|
|
131
|
+
activeDecoration() {
|
|
132
|
+
if (!this.decorationEnabled)
|
|
133
|
+
return undefined;
|
|
134
|
+
const entry = this.registry.decorationById?.(DEFAULT_DECORATION_ID);
|
|
135
|
+
return entry === undefined ? undefined : decorationView(entry);
|
|
136
|
+
}
|
|
97
137
|
/** The selected pet's registry entry. */
|
|
98
138
|
activeEntry() {
|
|
99
139
|
return this.registry.byId(this.selectedPetId()) ?? this.registry.defaultEntry();
|
|
@@ -195,7 +235,7 @@ export class PetService extends Service {
|
|
|
195
235
|
let activity = this.sessionActivity.get(session);
|
|
196
236
|
if (activity === undefined) {
|
|
197
237
|
activity = {
|
|
198
|
-
runtime: emptyProjectionRuntime(),
|
|
238
|
+
runtime: emptyProjectionRuntime(this.voicePools()),
|
|
199
239
|
machine: new PetStateMachine(this.stateConfig),
|
|
200
240
|
};
|
|
201
241
|
this.sessionActivity.set(session, activity);
|
|
@@ -273,6 +313,7 @@ export class PetService extends Service {
|
|
|
273
313
|
* @param section - the resolved settings section.
|
|
274
314
|
*/
|
|
275
315
|
applySettingsSection(section) {
|
|
316
|
+
this.decorationEnabled = section.decorationEnabled ?? true;
|
|
276
317
|
const selected = typeof section.petId === 'string' ? this.registry.byId(section.petId) : undefined;
|
|
277
318
|
if (selected !== undefined) {
|
|
278
319
|
this.ledger.setPetId(selected.id);
|
|
@@ -351,6 +392,7 @@ export class PetService extends Service {
|
|
|
351
392
|
const freshWhisper = whisper !== undefined && Date.now() - whisper.at < WHISPER_TTL_MS
|
|
352
393
|
? whisper.text
|
|
353
394
|
: undefined;
|
|
395
|
+
const decoration = this.activeDecoration();
|
|
354
396
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
355
397
|
// so polling the state cannot trigger pet.json writes.
|
|
356
398
|
return {
|
|
@@ -360,6 +402,7 @@ export class PetService extends Service {
|
|
|
360
402
|
sessionActive: snapshot.sessionActive,
|
|
361
403
|
sessions,
|
|
362
404
|
...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
|
|
405
|
+
...(decoration === undefined ? {} : { decoration }),
|
|
363
406
|
affinity: this.ledger.affinityView(Date.now()),
|
|
364
407
|
display: { ...this.ledger.snapshot.display },
|
|
365
408
|
pet: {
|