@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.
Files changed (125) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +129 -3
  3. package/README.zh.md +129 -3
  4. package/assets/decorations/whale/decoration.json +20 -0
  5. package/assets/decorations/whale/whale-frames.png +0 -0
  6. package/assets/whale/pet.json +169 -20
  7. package/assets/whale-refined/pet.json +17 -2
  8. package/contracts/pet-manifest-v2.schema.json +281 -0
  9. package/contracts/status-decoration-v1.schema.json +144 -0
  10. package/contracts/voice-pack-v1.schema.json +234 -0
  11. package/lib/client.js +581 -38
  12. package/lib/client.js.map +1 -1
  13. package/lib/index.js +1684 -59
  14. package/lib/live2d-vendor.js +995 -0
  15. package/lib/live2d-vendor.js.map +1 -0
  16. package/lib/types/chatter.d.ts +61 -3
  17. package/lib/types/chatter.d.ts.map +1 -1
  18. package/lib/types/chatter.js +71 -12
  19. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  20. package/lib/types/client/PetDockEntry.js +2 -1
  21. package/lib/types/client/PetSettingsCard.d.ts +14 -0
  22. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  23. package/lib/types/client/PetSettingsCard.js +24 -1
  24. package/lib/types/client/PetSprite.d.ts +7 -1
  25. package/lib/types/client/PetSprite.d.ts.map +1 -1
  26. package/lib/types/client/PetSprite.js +120 -12
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/index.js +5 -0
  29. package/lib/types/client/locales.d.ts +14 -0
  30. package/lib/types/client/locales.d.ts.map +1 -1
  31. package/lib/types/client/locales.js +14 -0
  32. package/lib/types/client/phase-stream.d.ts +22 -0
  33. package/lib/types/client/phase-stream.d.ts.map +1 -0
  34. package/lib/types/client/phase-stream.js +28 -0
  35. package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
  36. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
  37. package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
  38. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
  39. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
  40. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
  41. package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
  42. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
  43. package/lib/types/client/renderers/live2d/runtime.js +63 -0
  44. package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
  45. package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
  46. package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
  47. package/lib/types/client/renderers/live2d.d.ts +52 -0
  48. package/lib/types/client/renderers/live2d.d.ts.map +1 -0
  49. package/lib/types/client/renderers/live2d.js +192 -0
  50. package/lib/types/client/renderers/registry.d.ts +32 -0
  51. package/lib/types/client/renderers/registry.d.ts.map +1 -0
  52. package/lib/types/client/renderers/registry.js +49 -0
  53. package/lib/types/client/settings-form.d.ts.map +1 -1
  54. package/lib/types/client/settings-form.js +9 -6
  55. package/lib/types/contracts/renderer.d.ts +50 -0
  56. package/lib/types/contracts/renderer.d.ts.map +1 -0
  57. package/lib/types/contracts/renderer.js +15 -0
  58. package/lib/types/contracts/status-decoration.d.ts +85 -0
  59. package/lib/types/contracts/status-decoration.d.ts.map +1 -0
  60. package/lib/types/contracts/status-decoration.js +21 -0
  61. package/lib/types/decoration.d.ts +39 -0
  62. package/lib/types/decoration.d.ts.map +1 -0
  63. package/lib/types/decoration.js +210 -0
  64. package/lib/types/event-projection.d.ts +8 -3
  65. package/lib/types/event-projection.d.ts.map +1 -1
  66. package/lib/types/event-projection.js +9 -4
  67. package/lib/types/index.d.ts +2 -0
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/index.js +2 -0
  70. package/lib/types/manifest-v2.d.ts +129 -0
  71. package/lib/types/manifest-v2.d.ts.map +1 -0
  72. package/lib/types/manifest-v2.js +387 -0
  73. package/lib/types/model3.d.ts +26 -0
  74. package/lib/types/model3.d.ts.map +1 -0
  75. package/lib/types/model3.js +96 -0
  76. package/lib/types/registry.d.ts +112 -5
  77. package/lib/types/registry.d.ts.map +1 -1
  78. package/lib/types/registry.js +395 -38
  79. package/lib/types/routes.d.ts +39 -2
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +312 -13
  82. package/lib/types/service.d.ts +38 -1
  83. package/lib/types/service.d.ts.map +1 -1
  84. package/lib/types/service.js +46 -3
  85. package/lib/types/voice-pack.d.ts +98 -0
  86. package/lib/types/voice-pack.d.ts.map +1 -0
  87. package/lib/types/voice-pack.js +384 -0
  88. package/package.json +13 -10
  89. package/src/chatter.test.ts +89 -2
  90. package/src/chatter.ts +120 -14
  91. package/src/client/PetDockEntry.tsx +19 -11
  92. package/src/client/PetSettingsCard.tsx +57 -0
  93. package/src/client/PetSprite.test.tsx +265 -12
  94. package/src/client/PetSprite.tsx +165 -33
  95. package/src/client/index.ts +6 -0
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/phase-stream.test.ts +76 -0
  98. package/src/client/phase-stream.ts +39 -0
  99. package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
  100. package/src/client/renderers/PetRendererSwitch.tsx +48 -0
  101. package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
  102. package/src/client/renderers/live2d/runtime.test.ts +61 -0
  103. package/src/client/renderers/live2d/runtime.ts +119 -0
  104. package/src/client/renderers/live2d/vendor-entry.ts +14 -0
  105. package/src/client/renderers/live2d.test.ts +263 -0
  106. package/src/client/renderers/live2d.ts +231 -0
  107. package/src/client/renderers/registry.ts +58 -0
  108. package/src/client/settings-form.ts +8 -6
  109. package/src/client/settings-section.module.css +23 -0
  110. package/src/contracts/renderer.ts +54 -0
  111. package/src/contracts/status-decoration.ts +78 -0
  112. package/src/decoration.test.ts +178 -0
  113. package/src/decoration.ts +220 -0
  114. package/src/event-projection.ts +10 -5
  115. package/src/index.ts +2 -0
  116. package/src/manifest-v2.test.ts +251 -0
  117. package/src/manifest-v2.ts +414 -0
  118. package/src/model3.test.ts +79 -0
  119. package/src/model3.ts +91 -0
  120. package/src/registry.test.ts +438 -2
  121. package/src/registry.ts +470 -38
  122. package/src/routes.ts +328 -14
  123. package/src/service.ts +65 -3
  124. package/src/voice-pack.test.ts +216 -0
  125. package/src/voice-pack.ts +413 -0
@@ -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 declared spritesheet path, and optional 'previews/<name>'
137
- * media. Composed pets without a manifest file get a synthesized pet.json.
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 && rel === entry.spritesheetPath) {
202
- file = join(entry.dir, entry.spritesheetPath);
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
- const resolved = file;
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
- /** Build the full route family (API + assets) for one service. */
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';
@@ -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":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
@@ -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: {