@linxin666/dsh-pet 0.1.15 → 0.1.16

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 (74) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +73 -38
  3. package/README.zh.md +91 -55
  4. package/assets/whale/pet.json +12 -1
  5. package/cordis.patch.yml +4 -4
  6. package/lib/client.js +270 -334
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +641 -87
  9. package/lib/types/client/PetDockEntry.d.ts +14 -12
  10. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  11. package/lib/types/client/PetDockEntry.js +13 -8
  12. package/lib/types/client/PetSettingsCard.d.ts +22 -9
  13. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PetSettingsCard.js +42 -7
  15. package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
  16. package/lib/types/client/PetSprite.d.ts.map +1 -0
  17. package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
  18. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/PluginSettingsCard.js +3 -1
  20. package/lib/types/client/index.d.ts +11 -8
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/index.js +30 -12
  23. package/lib/types/client/locales.d.ts +10 -10
  24. package/lib/types/client/locales.js +10 -10
  25. package/lib/types/client/pet-store.d.ts +5 -0
  26. package/lib/types/client/pet-store.d.ts.map +1 -1
  27. package/lib/types/client/pet-store.js +4 -0
  28. package/lib/types/client/settings-form.d.ts +62 -0
  29. package/lib/types/client/settings-form.d.ts.map +1 -1
  30. package/lib/types/client/settings-form.js +61 -12
  31. package/lib/types/client/spritesheet.d.ts +14 -48
  32. package/lib/types/client/spritesheet.d.ts.map +1 -1
  33. package/lib/types/client/spritesheet.js +13 -81
  34. package/lib/types/index.d.ts +24 -11
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/lib/types/index.js +53 -33
  37. package/lib/types/ledger.d.ts +4 -2
  38. package/lib/types/ledger.d.ts.map +1 -1
  39. package/lib/types/ledger.js +11 -4
  40. package/lib/types/persist.d.ts +10 -3
  41. package/lib/types/persist.d.ts.map +1 -1
  42. package/lib/types/persist.js +32 -5
  43. package/lib/types/registry.d.ts +158 -0
  44. package/lib/types/registry.d.ts.map +1 -0
  45. package/lib/types/registry.js +276 -0
  46. package/lib/types/routes.d.ts +10 -10
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +169 -51
  49. package/lib/types/service.d.ts +46 -13
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +73 -16
  52. package/package.json +2 -2
  53. package/src/client/PetDockEntry.tsx +34 -26
  54. package/src/client/PetSettingsCard.tsx +67 -22
  55. package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
  56. package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
  57. package/src/client/PluginSettingsCard.tsx +7 -1
  58. package/src/client/index.ts +36 -14
  59. package/src/client/locales.ts +10 -10
  60. package/src/client/pet-store.ts +9 -0
  61. package/src/client/settings-card.module.css +3 -0
  62. package/src/client/settings-form.ts +114 -12
  63. package/src/client/spritesheet.ts +16 -92
  64. package/src/index.test.ts +23 -0
  65. package/src/index.ts +89 -45
  66. package/src/ledger.test.ts +5 -3
  67. package/src/ledger.ts +11 -4
  68. package/src/persist.test.ts +52 -3
  69. package/src/persist.ts +42 -8
  70. package/src/registry.test.ts +132 -0
  71. package/src/registry.ts +400 -0
  72. package/src/routes.ts +166 -53
  73. package/src/service.ts +102 -20
  74. package/lib/types/client/WhalePet.d.ts.map +0 -1
@@ -1,26 +1,38 @@
1
1
  /**
2
2
  * Pet HTTP routes — the browser half talks to the host through plain
3
- * same-origin JSON endpoints (`/api/pet/*`) and loads the whale-girl atlas
4
- * from `/pet/whale/*`. The `/plugins/` endpoint only serves client bundles
5
- * and RPC domains are platform-registered, so the pet serves its own API
6
- * and media — the same pattern as dsh-remote-web-ui's `/api/pair` family.
3
+ * same-origin JSON endpoints ('/api/pet/*') and loads pet assets from
4
+ * '/pet/<id>/*'. The '/plugins/' endpoint only serves client bundles and RPC
5
+ * domains are platform-registered, so the pet serves its own API and media —
6
+ * the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
7
+ * is one prefix registration serving every registry entry (manifest, atlas,
8
+ * optional previews), so adding a pet never touches route wiring.
7
9
  * @module @linxin666/dsh-pet/routes
8
10
  */
11
+ import { existsSync } from 'node:fs';
9
12
  import { readFile } from 'node:fs/promises';
10
13
  import { join } from 'node:path';
11
- import { fileURLToPath } from 'node:url';
14
+ import { petEntryView } from "./registry.js";
12
15
  /** Browser-facing base path of the pet API. */
13
16
  export const PET_API_PREFIX = '/api/pet';
14
- /** Browser-facing base path of the pet asset routes. */
15
- export const PET_ASSET_PREFIX = '/pet/whale';
16
- /** Relative (to package root) asset files exposed under the prefix. */
17
- const ASSET_FILES = [
18
- { name: 'spritesheet.webp', mime: 'image/webp' },
19
- { name: 'pet.json', mime: 'application/json' },
20
- ];
21
- /** Absolute package root, resolved from this module's own location (lib/). */
22
- export function petPackageRoot(importMetaUrl) {
23
- return fileURLToPath(new URL('../', importMetaUrl));
17
+ /** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
18
+ export const PET_ASSET_PREFIX = '/pet';
19
+ const MANIFEST_FILE = 'pet.json';
20
+ const PREVIEW_DIR = 'previews';
21
+ const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/;
22
+ const MIME_BY_EXT = {
23
+ '.webp': 'image/webp',
24
+ '.png': 'image/png',
25
+ '.gif': 'image/gif',
26
+ '.jpg': 'image/jpeg',
27
+ '.jpeg': 'image/jpeg',
28
+ '.json': 'application/json',
29
+ };
30
+ /** Content type by file extension (safe fallback: octet-stream). */
31
+ function mimeFor(file) {
32
+ const dot = file.lastIndexOf('.');
33
+ if (dot < 0)
34
+ return 'application/octet-stream';
35
+ return MIME_BY_EXT[file.slice(dot).toLowerCase()] ?? 'application/octet-stream';
24
36
  }
25
37
  /** Write one JSON response. */
26
38
  function json(res, status, body) {
@@ -42,8 +54,6 @@ function readJsonBody(req) {
42
54
  req.on('data', (chunk) => {
43
55
  size += chunk.length;
44
56
  if (size > 64 * 1024) {
45
- // Reject first so the error handler can write the 400 response,
46
- // then close the connection once the response is flushed.
47
57
  reject(new Error('body-too-large'));
48
58
  queueMicrotask(() => req.destroy());
49
59
  return;
@@ -98,61 +108,169 @@ function postRoute(path, run) {
98
108
  },
99
109
  };
100
110
  }
101
- /** Build the full route family (API + assets) for one service + package root. */
111
+ /** Legacy URL aliases: each entry's directory basename (e.g. 'whale'). */
112
+ function dirAliases(registry) {
113
+ const aliases = new Map();
114
+ for (const entry of registry.entries) {
115
+ const alias = entry.dir.split(/[\\/]/).pop() ?? '';
116
+ if (alias !== '' && !aliases.has(alias))
117
+ aliases.set(alias, entry);
118
+ }
119
+ return aliases;
120
+ }
121
+ /**
122
+ * The one asset handler behind the '/pet' prefix. Resolves the pet by id (or
123
+ * legacy directory alias), then serves exactly the files a manifest declares:
124
+ * pet.json, the declared spritesheet path, and optional 'previews/<name>'
125
+ * media. Composed pets without a manifest file get a synthesized pet.json.
126
+ */
127
+ function assetHandler(registry) {
128
+ const aliases = dirAliases(registry);
129
+ return (req, res) => {
130
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
131
+ res.writeHead(405);
132
+ res.end();
133
+ return;
134
+ }
135
+ let pathname;
136
+ try {
137
+ pathname = new URL(req.url ?? '/', 'http://pet.local').pathname;
138
+ }
139
+ catch {
140
+ res.writeHead(400);
141
+ res.end();
142
+ return;
143
+ }
144
+ const segments = pathname.split('/').filter(segment => segment !== '');
145
+ if (segments[0] !== 'pet' || segments[1] === undefined) {
146
+ res.writeHead(404);
147
+ res.end();
148
+ return;
149
+ }
150
+ let id;
151
+ try {
152
+ id = decodeURIComponent(segments[1]);
153
+ }
154
+ catch {
155
+ res.writeHead(400);
156
+ res.end();
157
+ return;
158
+ }
159
+ const entry = registry.byId(id) ?? aliases.get(id);
160
+ if (entry === undefined) {
161
+ res.writeHead(404);
162
+ res.end();
163
+ return;
164
+ }
165
+ const rest = [];
166
+ for (const segment of segments.slice(2)) {
167
+ let decoded;
168
+ try {
169
+ decoded = decodeURIComponent(segment);
170
+ }
171
+ catch {
172
+ res.writeHead(400);
173
+ res.end();
174
+ return;
175
+ }
176
+ rest.push(decoded);
177
+ }
178
+ const rel = rest.join('/');
179
+ let file;
180
+ let synthesized = false;
181
+ if (rest.length === 1 && rest[0] === MANIFEST_FILE) {
182
+ const manifestFile = join(entry.dir, MANIFEST_FILE);
183
+ file = existsSync(manifestFile) ? manifestFile : undefined;
184
+ if (file === undefined)
185
+ synthesized = true;
186
+ }
187
+ else if (rest.length > 0 && rel === entry.spritesheetPath) {
188
+ file = join(entry.dir, entry.spritesheetPath);
189
+ }
190
+ else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
191
+ const preview = join(entry.dir, PREVIEW_DIR, rest[1]);
192
+ file = existsSync(preview) ? preview : undefined;
193
+ }
194
+ if (synthesized) {
195
+ const body = Buffer.from(JSON.stringify(petEntryView(entry), null, 2), 'utf8');
196
+ res.writeHead(200, {
197
+ 'content-type': 'application/json; charset=utf-8',
198
+ 'content-length': String(body.byteLength),
199
+ 'cache-control': 'no-cache',
200
+ });
201
+ if (req.method === 'HEAD') {
202
+ res.end();
203
+ return;
204
+ }
205
+ res.end(body);
206
+ return;
207
+ }
208
+ if (file === undefined) {
209
+ res.writeHead(404);
210
+ res.end();
211
+ return;
212
+ }
213
+ const resolved = file;
214
+ readFile(resolved).then((body) => {
215
+ res.writeHead(200, {
216
+ 'content-type': mimeFor(resolved),
217
+ 'content-length': String(body.byteLength),
218
+ 'cache-control': 'no-cache',
219
+ });
220
+ if (req.method === 'HEAD') {
221
+ res.end();
222
+ return;
223
+ }
224
+ res.end(body);
225
+ }, () => {
226
+ res.writeHead(404);
227
+ res.end();
228
+ });
229
+ };
230
+ }
231
+ /** Build the full route family (API + assets) for one service. */
102
232
  export function makePetRoutes(deps) {
103
- const { service, packageRoot } = deps;
233
+ const { service } = deps;
104
234
  const apiRoutes = [
105
- getRoute(`${PET_API_PREFIX}/state`, () => service.state()),
106
- postRoute(`${PET_API_PREFIX}/interact`, (body) => {
235
+ getRoute(PET_API_PREFIX + '/state', () => service.state()),
236
+ getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
237
+ postRoute(PET_API_PREFIX + '/interact', (body) => {
107
238
  const kind = body.kind;
108
239
  if (kind !== 'pet' && kind !== 'feed')
109
240
  return Promise.reject(new Error('invalid-kind'));
110
241
  return service.interact(kind);
111
242
  }),
112
- postRoute(`${PET_API_PREFIX}/set-visible`, (body) => {
243
+ postRoute(PET_API_PREFIX + '/set-visible', (body) => {
113
244
  const visible = body.visible;
114
245
  if (typeof visible !== 'boolean')
115
246
  return Promise.reject(new Error('invalid-visible'));
116
247
  return service.setVisible(visible);
117
248
  }),
118
- postRoute(`${PET_API_PREFIX}/set-config`, (body) => service.setConfig({
249
+ postRoute(PET_API_PREFIX + '/set-config', (body) => service.setConfig({
119
250
  ...(typeof body.size === 'number' ? { size: body.size } : {}),
120
251
  ...(typeof body.right === 'number' ? { right: body.right } : {}),
121
252
  ...(typeof body.bottom === 'number' ? { bottom: body.bottom } : {}),
122
253
  ...(typeof body.visible === 'boolean' ? { visible: body.visible } : {}),
123
254
  })),
124
- postRoute(`${PET_API_PREFIX}/set-name`, (body) => {
255
+ postRoute(PET_API_PREFIX + '/set-name', (body) => {
125
256
  const name = body.name;
126
257
  if (typeof name !== 'string')
127
258
  return Promise.reject(new Error('invalid-name'));
128
259
  return service.setName(name);
129
260
  }),
261
+ postRoute(PET_API_PREFIX + '/set-pet', (body) => {
262
+ const petId = body.petId;
263
+ if (typeof petId !== 'string')
264
+ return Promise.reject(new Error('invalid-pet'));
265
+ return service.setPetId(petId);
266
+ }),
130
267
  ];
131
- const assetRoutes = ASSET_FILES.map((file) => ({
132
- kind: 'exact',
133
- path: `${PET_ASSET_PREFIX}/${file.name}`,
134
- handler: (req, res) => {
135
- if (req.method !== 'GET' && req.method !== 'HEAD') {
136
- res.writeHead(405);
137
- res.end();
138
- return;
139
- }
140
- return readFile(join(packageRoot, 'assets', 'whale', file.name)).then((body) => {
141
- res.writeHead(200, {
142
- 'content-type': file.mime,
143
- 'content-length': String(body.byteLength),
144
- 'cache-control': 'no-cache',
145
- });
146
- if (req.method === 'HEAD') {
147
- res.end();
148
- return;
149
- }
150
- res.end(body);
151
- }, () => {
152
- res.writeHead(404);
153
- res.end();
154
- });
155
- },
156
- }));
157
- return [...apiRoutes, ...assetRoutes];
268
+ const assetRoute = {
269
+ kind: 'prefix',
270
+ path: PET_ASSET_PREFIX,
271
+ handler: assetHandler(service.registrySnapshot()),
272
+ };
273
+ return [...apiRoutes, assetRoute];
158
274
  }
275
+ // Re-exported for the package surface (the registry owns the definition now).
276
+ export { petPackageRoot } from "./registry.js";
@@ -3,8 +3,8 @@
3
3
  * the pure event projection (`event-projection`) onto the state machine,
4
4
  * delegates the affinity economy to the ledger (`ledger`), and routes
5
5
  * persistence through `persist`. The API gateway maps these methods onto
6
- * `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig` for
7
- * browser consumers.
6
+ * `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
7
+ * `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
8
8
  * @module @linxin666/dsh-pet/service
9
9
  */
10
10
  import { Context, Service } from '@deepseek-ai/cordis';
@@ -12,6 +12,7 @@ import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity
12
12
  import type { TreatConfig } from './treats.ts';
13
13
  import { type LedgerInteractionResult } from './ledger.ts';
14
14
  import { type PetDisplayConfig } from './persist.ts';
15
+ import { type PetDefinition, type PetManifest, type PetRegistry } from './registry.ts';
15
16
  import { type PetStateConfig, type PetStateSnapshot } from './state.ts';
16
17
  /** Plugin configuration. */
17
18
  export interface PetConfig {
@@ -25,13 +26,21 @@ export interface PetConfig {
25
26
  persistDir?: string;
26
27
  /** Master switch for the plugin (browser half + host routes). */
27
28
  enabled?: boolean;
29
+ /** Prebuilt registry (tests); defaults to scanning the package + user dirs. */
30
+ registry?: PetRegistry;
31
+ /** Extra manifest entries composed by the embedding application. */
32
+ pets?: readonly PetManifest[];
28
33
  }
29
34
  /**
30
- * The pet's settings-namespace section: the display fields and name the web
31
- * settings surface edits. `right`/`bottom` are also updated by drag
35
+ * The pet's settings-namespace section: the pet selection and display fields
36
+ * the web settings surface edits. `right`/`bottom` are also updated by drag
32
37
  * interactions, which keep the settings document in sync through the service.
38
+ * Naming is per pet and lives outside the settings document (the hover-panel
39
+ * rename targets the selected pet).
33
40
  */
34
41
  export interface PetSettingsSection {
42
+ /** Selected pet id (a registry entry; the service clamps stale values). */
43
+ petId?: string;
35
44
  /** Master switch. */
36
45
  visible: boolean;
37
46
  /** Scale of the rendered pet in px (sprite cell height). */
@@ -40,8 +49,6 @@ export interface PetSettingsSection {
40
49
  right: number;
41
50
  /** Vertical inset from the viewport bottom edge, px. */
42
51
  bottom: number;
43
- /** User-customizable pet display name. */
44
- name: string;
45
52
  /** Master switch for the plugin (browser half + host routes). */
46
53
  enabled?: boolean;
47
54
  }
@@ -57,7 +64,16 @@ export interface PetStateView {
57
64
  affinity: PetAffinityView;
58
65
  /** Display configuration. */
59
66
  display: PetDisplayConfig;
60
- /** User-customizable pet display name. */
67
+ /** The selected pet's registry identity. */
68
+ pet: {
69
+ /** Registry id. */
70
+ id: string;
71
+ /** Manifest display name (unrenamed default). */
72
+ displayName: string;
73
+ /** Manifest description. */
74
+ description: string;
75
+ };
76
+ /** The selected pet's display name (user rename or manifest default). */
61
77
  name: string;
62
78
  /** Treat (小鱼干) stock snapshot. */
63
79
  treats: {
@@ -84,6 +100,7 @@ export declare class PetService extends Service {
84
100
  static inject: string[];
85
101
  private readonly machine;
86
102
  private readonly ledger;
103
+ private readonly registry;
87
104
  private readonly persistDir;
88
105
  private enabled;
89
106
  private disposeActivity;
@@ -97,8 +114,24 @@ export declare class PetService extends Service {
97
114
  state(): Promise<PetStateView>;
98
115
  /** Current persisted display config (read-only view). */
99
116
  display(): PetDisplayConfig;
100
- /** Current persisted pet name (read-only view). */
101
- petName(): string;
117
+ /** RPC: the registry entries the browser half renders and selects from. */
118
+ pets(): Promise<PetDefinition[]>;
119
+ /** The loaded registry (the asset routes serve its entries). */
120
+ registrySnapshot(): PetRegistry;
121
+ /** The selected pet's registry entry. */
122
+ activeEntry(): NonNullable<PetRegistry['entries'][number]>;
123
+ /** Currently selected pet id (persisted). */
124
+ selectedPetId(): string;
125
+ /** The display name of one pet (user rename or manifest displayName). */
126
+ petName(petId?: string): string;
127
+ /** RPC: switch the selected pet (persisted, settings document mirrored). */
128
+ setPetId(petId: string): Promise<{
129
+ ok: true;
130
+ petId: string;
131
+ } | {
132
+ ok: false;
133
+ error: string;
134
+ }>;
102
135
  /** Start or stop the session-activity listeners that drive the pet. */
103
136
  setEnabled(enabled: boolean): void;
104
137
  private syncActivity;
@@ -118,7 +151,7 @@ export declare class PetService extends Service {
118
151
  ok: true;
119
152
  display: PetDisplayConfig;
120
153
  }>;
121
- /** RPC: rename the pet (trimmed, 1–20 chars). */
154
+ /** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
122
155
  setName(name: string): Promise<{
123
156
  ok: true;
124
157
  name: string;
@@ -127,9 +160,9 @@ export declare class PetService extends Service {
127
160
  error: string;
128
161
  }>;
129
162
  /**
130
- * Apply a committed settings section to the persisted display config. Called
131
- * by the settings surface on every change; values are clamped exactly like
132
- * the setConfig RPC so both write paths converge.
163
+ * Apply a committed settings section to the persisted selection and display
164
+ * config. Called by the settings surface on every change; values are
165
+ * clamped exactly like the setConfig RPC so both write paths converge.
133
166
  * @param section - the resolved settings section.
134
167
  */
135
168
  applySettingsSection(section: PetSettingsSection): void;
@@ -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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"}
@@ -3,14 +3,15 @@
3
3
  * the pure event projection (`event-projection`) onto the state machine,
4
4
  * delegates the affinity economy to the ledger (`ledger`), and routes
5
5
  * persistence through `persist`. The API gateway maps these methods onto
6
- * `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig` for
7
- * browser consumers.
6
+ * `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
7
+ * `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
8
8
  * @module @linxin666/dsh-pet/service
9
9
  */
10
10
  import { Service } from '@deepseek-ai/cordis';
11
11
  import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
12
12
  import { PetLedger } from "./ledger.js";
13
- import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
13
+ import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
14
+ import { loadPetRegistry, petEntryView, petPackageRoot, } from "./registry.js";
14
15
  import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
15
16
  /** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
16
17
  export const PET_SETTINGS_NAMESPACE = 'pet';
@@ -24,6 +25,7 @@ export class PetService extends Service {
24
25
  static inject = [];
25
26
  machine;
26
27
  ledger;
28
+ registry;
27
29
  persistDir;
28
30
  enabled;
29
31
  disposeActivity;
@@ -33,8 +35,22 @@ export class PetService extends Service {
33
35
  constructor(ctx, config = {}) {
34
36
  super(ctx, 'pet');
35
37
  this.persistDir = config.persistDir ?? petHomeDir();
38
+ this.registry = config.registry
39
+ ?? loadPetRegistry({
40
+ packageRoot: petPackageRoot(import.meta.url),
41
+ ...(config.pets === undefined ? {} : { extra: config.pets }),
42
+ });
43
+ if (this.registry.entries.length === 0) {
44
+ throw new Error('[dsh-pet] no valid pet manifests found; nothing to render');
45
+ }
46
+ let persist = loadPetPersist(this.persistDir);
47
+ if (this.registry.byId(persist.petId) === undefined) {
48
+ // The selected pet no longer exists (removed or a fresh install with a
49
+ // copied pet.json): fall back to the registry default.
50
+ persist = { ...persist, petId: this.registry.defaultEntry().id };
51
+ }
36
52
  const ledgerConfig = { affinity: config.affinity, treats: config.treats };
37
- this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
53
+ this.ledger = new PetLedger(persist, ledgerConfig);
38
54
  this.machine = new PetStateMachine({
39
55
  ...defaultPetStateConfig,
40
56
  ...(config.state ?? {}),
@@ -54,9 +70,38 @@ export class PetService extends Service {
54
70
  display() {
55
71
  return { ...this.ledger.snapshot.display };
56
72
  }
57
- /** Current persisted pet name (read-only view). */
58
- petName() {
59
- return this.ledger.snapshot.name;
73
+ /** RPC: the registry entries the browser half renders and selects from. */
74
+ async pets() {
75
+ return this.registry.entries.map(petEntryView);
76
+ }
77
+ /** The loaded registry (the asset routes serve its entries). */
78
+ registrySnapshot() {
79
+ return this.registry;
80
+ }
81
+ /** The selected pet's registry entry. */
82
+ activeEntry() {
83
+ return this.registry.byId(this.selectedPetId()) ?? this.registry.defaultEntry();
84
+ }
85
+ /** Currently selected pet id (persisted). */
86
+ selectedPetId() {
87
+ return this.ledger.snapshot.petId;
88
+ }
89
+ /** The display name of one pet (user rename or manifest displayName). */
90
+ petName(petId = this.selectedPetId()) {
91
+ const stored = this.ledger.snapshot.names[petId];
92
+ if (stored !== undefined && stored.trim() !== '')
93
+ return stored;
94
+ return this.registry.byId(petId)?.displayName ?? DEFAULT_PET_NAME;
95
+ }
96
+ /** RPC: switch the selected pet (persisted, settings document mirrored). */
97
+ async setPetId(petId) {
98
+ const entry = this.registry.byId(petId);
99
+ if (entry === undefined)
100
+ return { ok: false, error: 'unknown-pet' };
101
+ this.ledger.setPetId(entry.id);
102
+ this.flush();
103
+ this.syncSettingsFromPet();
104
+ return { ok: true, petId: entry.id };
60
105
  }
61
106
  /** Start or stop the session-activity listeners that drive the pet. */
62
107
  setEnabled(enabled) {
@@ -155,32 +200,38 @@ export class PetService extends Service {
155
200
  this.syncSettingsFromPet();
156
201
  return { ok: true, display: this.ledger.snapshot.display };
157
202
  }
158
- /** RPC: rename the pet (trimmed, 1–20 chars). */
203
+ /** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
159
204
  async setName(name) {
160
205
  const trimmed = name.trim();
161
206
  if (trimmed === '')
162
207
  return { ok: false, error: 'name-empty' };
163
208
  if (trimmed.length > PET_NAME_MAX_LENGTH)
164
209
  return { ok: false, error: 'name-too-long' };
165
- this.ledger.setName(trimmed);
210
+ this.ledger.setPetName(this.selectedPetId(), trimmed);
166
211
  this.flush();
167
- this.syncSettingsFromPet();
168
212
  return { ok: true, name: trimmed };
169
213
  }
170
214
  /**
171
- * Apply a committed settings section to the persisted display config. Called
172
- * by the settings surface on every change; values are clamped exactly like
173
- * the setConfig RPC so both write paths converge.
215
+ * Apply a committed settings section to the persisted selection and display
216
+ * config. Called by the settings surface on every change; values are
217
+ * clamped exactly like the setConfig RPC so both write paths converge.
174
218
  * @param section - the resolved settings section.
175
219
  */
176
220
  applySettingsSection(section) {
221
+ if (typeof section.petId === 'string' && this.registry.byId(section.petId) !== undefined) {
222
+ this.ledger.setPetId(section.petId);
223
+ }
224
+ else if (section.petId !== undefined) {
225
+ // The stored selection names a pet the registry no longer has: keep the
226
+ // current selection and repair the settings document.
227
+ this.syncSettingsFromPet();
228
+ }
177
229
  const next = { ...this.ledger.snapshot.display };
178
230
  next.visible = section.visible && (section.enabled ?? true);
179
231
  next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)));
180
232
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
181
233
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
182
234
  this.ledger.setDisplay(next);
183
- this.ledger.setName(section.name.trim());
184
235
  this.flush();
185
236
  }
186
237
  /** Mirror the persisted display config into the settings document (best-effort). */
@@ -194,7 +245,7 @@ export class PetService extends Service {
194
245
  size: snapshot.display.size,
195
246
  right: snapshot.display.right,
196
247
  bottom: snapshot.display.bottom,
197
- name: snapshot.name,
248
+ petId: snapshot.petId,
198
249
  }).catch(() => {
199
250
  // A settings write failure must not break the pet's own persistence.
200
251
  });
@@ -211,6 +262,7 @@ export class PetService extends Service {
211
262
  }
212
263
  view() {
213
264
  const snapshot = this.machine.render();
265
+ const entry = this.activeEntry();
214
266
  // Read-only: the ledger settles on economic events only, never on a read,
215
267
  // so polling the state cannot trigger pet.json writes.
216
268
  return {
@@ -220,7 +272,12 @@ export class PetService extends Service {
220
272
  sessionActive: snapshot.sessionActive,
221
273
  affinity: this.ledger.affinityView(Date.now()),
222
274
  display: { ...this.ledger.snapshot.display },
223
- name: this.ledger.snapshot.name,
275
+ pet: {
276
+ id: entry.id,
277
+ displayName: entry.displayName,
278
+ description: entry.description,
279
+ },
280
+ name: this.petName(),
224
281
  treats: {
225
282
  stocked: this.ledger.snapshot.treats.treats,
226
283
  max: this.ledger.treatMax,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
- "description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
4
- "version": "0.1.15",
3
+ "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
+ "version": "0.1.16",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"