@linxin666/dsh-pet 0.2.3 → 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 +64 -1
- package/README.zh.md +64 -1
- package/assets/decorations/whale/decoration.json +20 -0
- package/assets/decorations/whale/whale-frames.png +0 -0
- 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 +144 -18
- package/lib/client.js.map +1 -1
- package/lib/index.js +979 -70
- 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/PetSettingsCard.d.ts +4 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +3 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +103 -4
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/renderers/live2d.d.ts.map +1 -1
- package/lib/types/client/renderers/live2d.js +13 -1
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- 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/registry.d.ts +55 -2
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +198 -16
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +131 -3
- package/lib/types/service.d.ts +33 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +42 -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 +11 -10
- package/src/chatter.test.ts +89 -2
- package/src/chatter.ts +120 -14
- package/src/client/PetSettingsCard.tsx +18 -0
- package/src/client/PetSprite.test.tsx +254 -12
- package/src/client/PetSprite.tsx +142 -25
- package/src/client/locales.ts +4 -0
- package/src/client/renderers/live2d.test.ts +23 -2
- package/src/client/renderers/live2d.ts +14 -1
- package/src/client/settings-form.ts +8 -6
- 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/registry.test.ts +223 -0
- package/src/registry.ts +235 -15
- package/src/routes.ts +128 -3
- package/src/service.ts +59 -3
- package/src/voice-pack.test.ts +216 -0
- package/src/voice-pack.ts +413 -0
package/lib/types/registry.js
CHANGED
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
* whale-girl manifest overrides its own durations.
|
|
27
27
|
* @module @linxin666/dsh-pet/registry
|
|
28
28
|
*/
|
|
29
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
29
|
+
import { existsSync, readdirSync, readFileSync, 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
|
+
import { mergeVoicePacks, normalizeVoicePack } from './voice-pack.js';
|
|
35
|
+
import { parseDecorationManifest } from './decoration.js';
|
|
36
|
+
import { PET_DECORATION_API_VERSION } from './contracts/status-decoration.js';
|
|
34
37
|
import { dshHome } from './dsh-home.js';
|
|
35
38
|
import { parsePetManifest } from './manifest-v2.js';
|
|
36
39
|
import { collectModel3References } from './model3.js';
|
|
@@ -376,22 +379,25 @@ function scanPetDir(dir, options) {
|
|
|
376
379
|
}
|
|
377
380
|
if (!verdict.ok)
|
|
378
381
|
continue;
|
|
382
|
+
let entry;
|
|
379
383
|
if (verdict.manifest.renderer === 'live2d') {
|
|
380
|
-
|
|
381
|
-
if (entry !== undefined)
|
|
382
|
-
entries.push(entry);
|
|
383
|
-
continue;
|
|
384
|
+
entry = resolveLive2dEntry(verdict.manifest, entryDir, options);
|
|
384
385
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
else {
|
|
387
|
+
const legacy = flattenV2Sprite2d(verdict.manifest);
|
|
388
|
+
if (legacy === undefined) {
|
|
389
|
+
const note = 'pet ' + verdict.manifest.id + ': sprite2d.atlasRows only supports 9 or 11 under the v1 compat resolver';
|
|
390
|
+
options.diagnostics?.push({ level: 'error', source: entryDir, message: note });
|
|
391
|
+
options.warnings?.push(note);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
entry = resolvePetManifest(legacy, entryDir, options);
|
|
391
395
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
if (entry === undefined)
|
|
397
|
+
continue;
|
|
398
|
+
// Optional voice pack (voice.json) — pure content, warn-and-drop (M4).
|
|
399
|
+
const voice = loadVoicePackFile(join(entryDir, 'voice.json'), options);
|
|
400
|
+
entries.push({ ...entry, ...(voice === undefined ? {} : { voice }) });
|
|
395
401
|
}
|
|
396
402
|
return entries;
|
|
397
403
|
}
|
|
@@ -405,6 +411,136 @@ function readPetJson(file, warnings) {
|
|
|
405
411
|
return undefined;
|
|
406
412
|
}
|
|
407
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Scan-time read ceiling for user-authored JSON descriptors (voice.json,
|
|
416
|
+
* .voice.json, decoration.json): the registry reads these synchronously at
|
|
417
|
+
* plugin startup, and a pathological file — multi-GB, or a FIFO/device
|
|
418
|
+
* symlink — must not hang or exhaust the host before the warn-and-drop
|
|
419
|
+
* discipline can apply (review-spd follow-up, pet-center M4/M5).
|
|
420
|
+
*/
|
|
421
|
+
export const PET_SCAN_JSON_CAP = 64 * 1024;
|
|
422
|
+
/**
|
|
423
|
+
* Stat one scanned JSON descriptor with a regular-file + size guard, so a
|
|
424
|
+
* pathological user file is skipped with a warning instead of stalling or
|
|
425
|
+
* OOM-ing the host at startup. Returns the Stats, or undefined when the
|
|
426
|
+
* caller must skip the file (a warning was recorded).
|
|
427
|
+
*/
|
|
428
|
+
function guardedScannedJsonStat(file, options, what) {
|
|
429
|
+
let st;
|
|
430
|
+
try {
|
|
431
|
+
st = statSync(file);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
const warn = (message) => {
|
|
437
|
+
options.warnings?.push(file + ': ' + message);
|
|
438
|
+
options.diagnostics?.push({ level: 'warning', source: file, message: file + ': ' + message });
|
|
439
|
+
};
|
|
440
|
+
if (!st.isFile()) {
|
|
441
|
+
warn(what + ' is not a regular file; ignored');
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
if (st.size > PET_SCAN_JSON_CAP) {
|
|
445
|
+
warn(what + ' exceeds the ' + PET_SCAN_JSON_CAP + '-byte scan ceiling; ignored');
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
return st;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Load and normalize one optional voice.json (pet-center M4). A missing
|
|
452
|
+
* file is silent; a broken file warns and drops. The pack is pure content,
|
|
453
|
+
* so every issue stays a warning — a bad voice.json never rejects a pet.
|
|
454
|
+
*/
|
|
455
|
+
function loadVoicePackFile(file, options) {
|
|
456
|
+
if (!existsSync(file))
|
|
457
|
+
return undefined;
|
|
458
|
+
if (guardedScannedJsonStat(file, options, 'voice pack') === undefined)
|
|
459
|
+
return undefined;
|
|
460
|
+
const warn = (message) => {
|
|
461
|
+
options.warnings?.push(file + ': ' + message);
|
|
462
|
+
options.diagnostics?.push({ level: 'warning', source: file, message: file + ': ' + message });
|
|
463
|
+
};
|
|
464
|
+
let raw;
|
|
465
|
+
try {
|
|
466
|
+
raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
warn('voice pack is not valid JSON; ignored: ' + (error instanceof Error ? error.message : String(error)));
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
return normalizeVoicePack(raw, warn);
|
|
473
|
+
}
|
|
474
|
+
/** Decoration asset URL prefix (served by the decoration route, M5). */
|
|
475
|
+
export const DECORATION_ASSET_PREFIX = '/api/pet/decoration';
|
|
476
|
+
/**
|
|
477
|
+
* Scan one directory of decoration folders ('decoration.json' + strip).
|
|
478
|
+
* Later scans override earlier ones on id collision; a bad descriptor warns
|
|
479
|
+
* and skips — the never-throw philosophy holds for decorations too (M5).
|
|
480
|
+
*/
|
|
481
|
+
function scanDecorationDir(dir, options) {
|
|
482
|
+
if (!existsSync(dir))
|
|
483
|
+
return [];
|
|
484
|
+
let names = [];
|
|
485
|
+
try {
|
|
486
|
+
names = readdirSync(dir).filter(name => !name.startsWith('.'));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
names.sort();
|
|
492
|
+
const entries = [];
|
|
493
|
+
for (const name of names) {
|
|
494
|
+
const entryDir = join(dir, name);
|
|
495
|
+
const manifestFile = join(entryDir, 'decoration.json');
|
|
496
|
+
if (!existsSync(manifestFile))
|
|
497
|
+
continue;
|
|
498
|
+
if (guardedScannedJsonStat(manifestFile, options, 'decoration descriptor') === undefined)
|
|
499
|
+
continue;
|
|
500
|
+
let raw;
|
|
501
|
+
try {
|
|
502
|
+
raw = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = 'skipping ' + manifestFile + ': ' + (error instanceof Error ? error.message : String(error));
|
|
506
|
+
options.warnings?.push(message);
|
|
507
|
+
options.diagnostics?.push({ level: 'error', source: entryDir, message });
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const verdict = parseDecorationManifest(raw, manifestFile);
|
|
511
|
+
for (const diagnostic of verdict.diagnostics) {
|
|
512
|
+
options.diagnostics?.push({ level: diagnostic.level, source: entryDir, message: diagnostic.message });
|
|
513
|
+
options.warnings?.push(diagnostic.message);
|
|
514
|
+
}
|
|
515
|
+
if (!verdict.ok)
|
|
516
|
+
continue;
|
|
517
|
+
const manifest = verdict.manifest;
|
|
518
|
+
// A missing strip keeps the entry listed (mirroring the live2d closure
|
|
519
|
+
// discipline) but earns a diagnostic: the ornament will silently render
|
|
520
|
+
// nothing, and the warning names the file to fix.
|
|
521
|
+
if (!existsSync(join(entryDir, manifest.entry))) {
|
|
522
|
+
const message = 'decoration ' + manifest.id + ': strip file missing: ' + manifest.entry;
|
|
523
|
+
options.warnings?.push(message);
|
|
524
|
+
options.diagnostics?.push({ level: 'warning', source: entryDir, message });
|
|
525
|
+
}
|
|
526
|
+
entries.push({
|
|
527
|
+
apiVersion: PET_DECORATION_API_VERSION,
|
|
528
|
+
id: manifest.id,
|
|
529
|
+
dir: entryDir,
|
|
530
|
+
entryPath: manifest.entry,
|
|
531
|
+
servable: ['decoration.json', manifest.entry],
|
|
532
|
+
license: manifest.license,
|
|
533
|
+
assetBase: DECORATION_ASSET_PREFIX + '/' + encodeURIComponent(manifest.id),
|
|
534
|
+
entryUrl: DECORATION_ASSET_PREFIX + '/' + encodeURIComponent(manifest.id) + '/' + manifest.entry,
|
|
535
|
+
cell: manifest.cell,
|
|
536
|
+
columns: manifest.columns,
|
|
537
|
+
durations: manifest.durations,
|
|
538
|
+
loop: manifest.loop,
|
|
539
|
+
phases: manifest.phases,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return entries;
|
|
543
|
+
}
|
|
408
544
|
/**
|
|
409
545
|
* Load the pet registry: built-in 'assets/*' first, then the hatch-pet
|
|
410
546
|
* custom pets directory, then composed 'extra' manifests (each later source
|
|
@@ -435,12 +571,15 @@ export function loadPetRegistry(options) {
|
|
|
435
571
|
}
|
|
436
572
|
// The pet-center user directory ranks above the legacy hatch-pet source.
|
|
437
573
|
const dshPetsDir = options.dshPetsDir ?? join(dshHome(), 'pets');
|
|
574
|
+
let globalVoice;
|
|
438
575
|
if (dshPetsDir !== '') {
|
|
439
576
|
for (const entry of scanPetDir(dshPetsDir, { assetPrefix, warnings, diagnostics })) {
|
|
440
577
|
if (byId.has(entry.id))
|
|
441
578
|
warnings.push('user pet ' + entry.id + ' overrides an earlier registration');
|
|
442
579
|
byId.set(entry.id, entry);
|
|
443
580
|
}
|
|
581
|
+
// The global voice override layers under every per-pet pack (M4, #677).
|
|
582
|
+
globalVoice = loadVoicePackFile(join(dshPetsDir, '.voice.json'), { warnings, diagnostics });
|
|
444
583
|
}
|
|
445
584
|
for (const manifest of options.extra ?? []) {
|
|
446
585
|
const raw = manifest.spritesheetPath;
|
|
@@ -461,17 +600,59 @@ export function loadPetRegistry(options) {
|
|
|
461
600
|
warnings.push('composed pet ' + entry.id + ' overrides an earlier registration');
|
|
462
601
|
byId.set(entry.id, entry);
|
|
463
602
|
}
|
|
603
|
+
// Status decorations (pet-center M5, #567): built-in entries first,
|
|
604
|
+
// then same-id user entries under '$DSH_HOME/pets/decorations' override.
|
|
605
|
+
const decorationById = new Map();
|
|
606
|
+
for (const entry of scanDecorationDir(join(packageRoot, 'assets', 'decorations'), { warnings, diagnostics })) {
|
|
607
|
+
decorationById.set(entry.id, entry);
|
|
608
|
+
}
|
|
609
|
+
if (dshPetsDir !== '') {
|
|
610
|
+
for (const entry of scanDecorationDir(join(dshPetsDir, 'decorations'), { warnings, diagnostics })) {
|
|
611
|
+
if (decorationById.has(entry.id)) {
|
|
612
|
+
warnings.push('user decoration ' + entry.id + ' overrides the built-in one');
|
|
613
|
+
}
|
|
614
|
+
decorationById.set(entry.id, entry);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
464
617
|
const entries = [...byId.values()];
|
|
618
|
+
const decorations = [...decorationById.values()];
|
|
465
619
|
return {
|
|
466
620
|
entries,
|
|
467
621
|
warnings,
|
|
468
622
|
diagnostics,
|
|
469
623
|
byId: (id) => byId.get(id),
|
|
470
624
|
defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
|
|
625
|
+
...(globalVoice === undefined ? {} : { globalVoice }),
|
|
626
|
+
decorations,
|
|
627
|
+
decorationById: (id) => decorationById.get(id),
|
|
471
628
|
};
|
|
472
629
|
}
|
|
473
|
-
/**
|
|
474
|
-
export
|
|
630
|
+
/** The built-in default decoration id (M5): the first reference ornament. */
|
|
631
|
+
export const DEFAULT_DECORATION_ID = 'whale';
|
|
632
|
+
/** Strip host-only fields, leaving the browser-visible decoration view. */
|
|
633
|
+
export function decorationView(entry) {
|
|
634
|
+
return {
|
|
635
|
+
apiVersion: PET_DECORATION_API_VERSION,
|
|
636
|
+
id: entry.id,
|
|
637
|
+
assetBase: entry.assetBase,
|
|
638
|
+
entryUrl: entry.entryUrl,
|
|
639
|
+
cell: entry.cell,
|
|
640
|
+
columns: entry.columns,
|
|
641
|
+
durations: entry.durations,
|
|
642
|
+
loop: entry.loop,
|
|
643
|
+
phases: entry.phases,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Strip host-only fields, leaving the client-visible definition. When the
|
|
648
|
+
* registry carries a global voice pack, its panel chrome layers under the
|
|
649
|
+
* entry's own pack (per-slot merge, pet > global), mirroring the voice-pool
|
|
650
|
+
* layering (pet-center M4, issue #677).
|
|
651
|
+
*/
|
|
652
|
+
export function petEntryView(entry, globalVoice) {
|
|
653
|
+
const panel = globalVoice === undefined
|
|
654
|
+
? entry.voice?.panel
|
|
655
|
+
: mergeVoicePacks(globalVoice, entry.voice)?.panel;
|
|
475
656
|
return {
|
|
476
657
|
id: entry.id,
|
|
477
658
|
displayName: entry.displayName,
|
|
@@ -486,6 +667,7 @@ export function petEntryView(entry) {
|
|
|
486
667
|
...(entry.sequences === undefined ? {} : { sequences: entry.sequences }),
|
|
487
668
|
atlasUrl: entry.atlasUrl,
|
|
488
669
|
manifestUrl: entry.manifestUrl,
|
|
670
|
+
...(panel === undefined ? {} : { panel }),
|
|
489
671
|
};
|
|
490
672
|
}
|
|
491
673
|
/** The absolute file a pet's atlas resolves to (host asset route). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,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;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,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,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAwDnH;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
package/lib/types/routes.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { join, sep } from 'node:path';
|
|
14
|
-
import { petEntryView, petPackageRoot } from './registry.js';
|
|
14
|
+
import { DECORATION_ASSET_PREFIX, petEntryView, petPackageRoot } from './registry.js';
|
|
15
15
|
import { isLoopbackRequest } from './loopback.js';
|
|
16
16
|
import { dshHome } from './dsh-home.js';
|
|
17
17
|
/** Browser-facing base path of the pet API. */
|
|
@@ -248,7 +248,7 @@ function assetHandler(registry, caps) {
|
|
|
248
248
|
file = existsSync(preview) ? preview : undefined;
|
|
249
249
|
}
|
|
250
250
|
if (synthesized) {
|
|
251
|
-
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');
|
|
252
252
|
res.writeHead(200, {
|
|
253
253
|
'content-type': 'application/json; charset=utf-8',
|
|
254
254
|
'content-length': String(body.byteLength),
|
|
@@ -404,6 +404,129 @@ function runtimeHandler(roots) {
|
|
|
404
404
|
});
|
|
405
405
|
};
|
|
406
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
|
+
}
|
|
407
530
|
/** Build the full route family (API + assets + runtime) for one service. */
|
|
408
531
|
export function makePetRoutes(deps) {
|
|
409
532
|
const { service } = deps;
|
|
@@ -455,7 +578,12 @@ export function makePetRoutes(deps) {
|
|
|
455
578
|
vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
|
|
456
579
|
}),
|
|
457
580
|
};
|
|
458
|
-
|
|
581
|
+
const decorationRoute = {
|
|
582
|
+
kind: 'prefix',
|
|
583
|
+
path: DECORATION_ASSET_PREFIX,
|
|
584
|
+
handler: decorationHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
585
|
+
};
|
|
586
|
+
return [...apiRoutes, assetRoute, runtimeRoute, decorationRoute];
|
|
459
587
|
}
|
|
460
588
|
// Re-exported for the package surface (the registry owns the definition now).
|
|
461
589
|
export { petPackageRoot } from './registry.js';
|
package/lib/types/service.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { TreatConfig } from './treats.ts';
|
|
|
17
17
|
import { type LedgerInteractionResult } from './ledger.ts';
|
|
18
18
|
import { type PetDisplayConfig } from './persist.ts';
|
|
19
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. */
|
|
@@ -165,6 +193,11 @@ export declare class PetService extends Service {
|
|
|
165
193
|
diagnostics(): Promise<{
|
|
166
194
|
diagnostics: PetRegistryDiagnostic[];
|
|
167
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;
|
|
168
201
|
/** The selected pet's registry entry. */
|
|
169
202
|
activeEntry(): NonNullable<PetRegistry['entries'][number]>;
|
|
170
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,7 +114,7 @@ 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() {
|
|
@@ -98,6 +124,16 @@ export class PetService extends Service {
|
|
|
98
124
|
async diagnostics() {
|
|
99
125
|
return { diagnostics: this.registry.diagnostics };
|
|
100
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
|
+
}
|
|
101
137
|
/** The selected pet's registry entry. */
|
|
102
138
|
activeEntry() {
|
|
103
139
|
return this.registry.byId(this.selectedPetId()) ?? this.registry.defaultEntry();
|
|
@@ -199,7 +235,7 @@ export class PetService extends Service {
|
|
|
199
235
|
let activity = this.sessionActivity.get(session);
|
|
200
236
|
if (activity === undefined) {
|
|
201
237
|
activity = {
|
|
202
|
-
runtime: emptyProjectionRuntime(),
|
|
238
|
+
runtime: emptyProjectionRuntime(this.voicePools()),
|
|
203
239
|
machine: new PetStateMachine(this.stateConfig),
|
|
204
240
|
};
|
|
205
241
|
this.sessionActivity.set(session, activity);
|
|
@@ -277,6 +313,7 @@ export class PetService extends Service {
|
|
|
277
313
|
* @param section - the resolved settings section.
|
|
278
314
|
*/
|
|
279
315
|
applySettingsSection(section) {
|
|
316
|
+
this.decorationEnabled = section.decorationEnabled ?? true;
|
|
280
317
|
const selected = typeof section.petId === 'string' ? this.registry.byId(section.petId) : undefined;
|
|
281
318
|
if (selected !== undefined) {
|
|
282
319
|
this.ledger.setPetId(selected.id);
|
|
@@ -355,6 +392,7 @@ export class PetService extends Service {
|
|
|
355
392
|
const freshWhisper = whisper !== undefined && Date.now() - whisper.at < WHISPER_TTL_MS
|
|
356
393
|
? whisper.text
|
|
357
394
|
: undefined;
|
|
395
|
+
const decoration = this.activeDecoration();
|
|
358
396
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
359
397
|
// so polling the state cannot trigger pet.json writes.
|
|
360
398
|
return {
|
|
@@ -364,6 +402,7 @@ export class PetService extends Service {
|
|
|
364
402
|
sessionActive: snapshot.sessionActive,
|
|
365
403
|
sessions,
|
|
366
404
|
...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
|
|
405
|
+
...(decoration === undefined ? {} : { decoration }),
|
|
367
406
|
affinity: this.ledger.affinityView(Date.now()),
|
|
368
407
|
display: { ...this.ledger.snapshot.display },
|
|
369
408
|
pet: {
|