@linxin666/dsh-pet 0.1.14 → 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 (90) 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 +296 -347
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +975 -317
  9. package/lib/invariant.js +1 -1
  10. package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
  11. package/lib/types/affinity.d.ts +15 -0
  12. package/lib/types/affinity.d.ts.map +1 -1
  13. package/lib/types/affinity.js +14 -0
  14. package/lib/types/client/PetDockEntry.d.ts +14 -12
  15. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  16. package/lib/types/client/PetDockEntry.js +13 -8
  17. package/lib/types/client/PetSettingsCard.d.ts +22 -9
  18. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/PetSettingsCard.js +42 -7
  20. package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
  21. package/lib/types/client/PetSprite.d.ts.map +1 -0
  22. package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
  23. package/lib/types/client/PluginSettingsCard.d.ts +26 -11
  24. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  25. package/lib/types/client/PluginSettingsCard.js +30 -8
  26. package/lib/types/client/index.d.ts +11 -8
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/index.js +32 -14
  29. package/lib/types/client/locales.d.ts +10 -10
  30. package/lib/types/client/locales.js +10 -10
  31. package/lib/types/client/pet-store.d.ts +5 -0
  32. package/lib/types/client/pet-store.d.ts.map +1 -1
  33. package/lib/types/client/pet-store.js +4 -0
  34. package/lib/types/client/settings-form.d.ts +76 -5
  35. package/lib/types/client/settings-form.d.ts.map +1 -1
  36. package/lib/types/client/settings-form.js +97 -20
  37. package/lib/types/client/spritesheet.d.ts +14 -48
  38. package/lib/types/client/spritesheet.d.ts.map +1 -1
  39. package/lib/types/client/spritesheet.js +13 -81
  40. package/lib/types/dsh-home.d.ts +17 -0
  41. package/lib/types/dsh-home.d.ts.map +1 -0
  42. package/lib/types/dsh-home.js +34 -0
  43. package/lib/types/event-projection.d.ts +36 -0
  44. package/lib/types/event-projection.d.ts.map +1 -0
  45. package/lib/types/event-projection.js +98 -0
  46. package/lib/types/index.d.ts +24 -11
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +53 -33
  49. package/lib/types/ledger.d.ts +79 -0
  50. package/lib/types/ledger.d.ts.map +1 -0
  51. package/lib/types/ledger.js +146 -0
  52. package/lib/types/persist.d.ts +15 -4
  53. package/lib/types/persist.d.ts.map +1 -1
  54. package/lib/types/persist.js +39 -8
  55. package/lib/types/registry.d.ts +158 -0
  56. package/lib/types/registry.d.ts.map +1 -0
  57. package/lib/types/registry.js +276 -0
  58. package/lib/types/routes.d.ts +10 -10
  59. package/lib/types/routes.d.ts.map +1 -1
  60. package/lib/types/routes.js +169 -51
  61. package/lib/types/service.d.ts +59 -57
  62. package/lib/types/service.d.ts.map +1 -1
  63. package/lib/types/service.js +111 -211
  64. package/package.json +2 -2
  65. package/src/affinity.ts +33 -0
  66. package/src/client/PetDockEntry.tsx +34 -26
  67. package/src/client/PetSettingsCard.tsx +67 -22
  68. package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
  69. package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
  70. package/src/client/PluginSettingsCard.tsx +90 -18
  71. package/src/client/index.ts +38 -16
  72. package/src/client/locales.ts +10 -10
  73. package/src/client/pet-store.ts +9 -0
  74. package/src/client/settings-card.module.css +9 -0
  75. package/src/client/settings-form.ts +153 -19
  76. package/src/client/spritesheet.ts +16 -92
  77. package/src/dsh-home.test.ts +35 -0
  78. package/src/dsh-home.ts +36 -0
  79. package/src/event-projection.ts +128 -0
  80. package/src/index.test.ts +23 -0
  81. package/src/index.ts +89 -45
  82. package/src/ledger.test.ts +69 -0
  83. package/src/ledger.ts +190 -0
  84. package/src/persist.test.ts +52 -3
  85. package/src/persist.ts +49 -11
  86. package/src/registry.test.ts +132 -0
  87. package/src/registry.ts +400 -0
  88. package/src/routes.ts +166 -53
  89. package/src/service.ts +153 -285
  90. package/lib/types/client/WhalePet.d.ts.map +0 -1
@@ -1,15 +1,18 @@
1
1
  /**
2
- * Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
3
- * (projects official session events and accepts legacy `activity/status`), the
4
- * affinity ledger, and the persisted display config. The API gateway maps
5
- * this service's methods onto `pet.state` / `pet.interact` /
6
- * `pet.setVisible` / `pet.setConfig` for browser consumers.
2
+ * Pet host service — the `pet.*` RPC domain. A composition facade: it wires
3
+ * the pure event projection (`event-projection`) onto the state machine,
4
+ * delegates the affinity economy to the ledger (`ledger`), and routes
5
+ * persistence through `persist`. The API gateway maps these methods onto
6
+ * `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
7
+ * `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
7
8
  * @module @linxin666/dsh-pet/service
8
9
  */
9
10
  import { Context, Service } from '@deepseek-ai/cordis';
10
- import { type AffinityConfig, type PetInteraction } from './affinity.ts';
11
+ import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity.ts';
12
+ import type { TreatConfig } from './treats.ts';
13
+ import { type LedgerInteractionResult } from './ledger.ts';
11
14
  import { type PetDisplayConfig } from './persist.ts';
12
- import { type TreatConfig } from './treats.ts';
15
+ import { type PetDefinition, type PetManifest, type PetRegistry } from './registry.ts';
13
16
  import { type PetStateConfig, type PetStateSnapshot } from './state.ts';
14
17
  /** Plugin configuration. */
15
18
  export interface PetConfig {
@@ -23,13 +26,21 @@ export interface PetConfig {
23
26
  persistDir?: string;
24
27
  /** Master switch for the plugin (browser half + host routes). */
25
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[];
26
33
  }
27
34
  /**
28
- * The pet's settings-namespace section: the display fields and name the web
29
- * 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
30
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).
31
40
  */
32
41
  export interface PetSettingsSection {
42
+ /** Selected pet id (a registry entry; the service clamps stale values). */
43
+ petId?: string;
33
44
  /** Master switch. */
34
45
  visible: boolean;
35
46
  /** Scale of the rendered pet in px (sprite cell height). */
@@ -38,8 +49,6 @@ export interface PetSettingsSection {
38
49
  right: number;
39
50
  /** Vertical inset from the viewport bottom edge, px. */
40
51
  bottom: number;
41
- /** User-customizable pet display name. */
42
- name: string;
43
52
  /** Master switch for the plugin (browser half + host routes). */
44
53
  enabled?: boolean;
45
54
  }
@@ -52,21 +61,19 @@ export interface PetStateView {
52
61
  phase: PetStateSnapshot['phase'];
53
62
  sessionActive: boolean;
54
63
  /** Affinity ledger snapshot. */
55
- affinity: {
56
- points: number;
57
- rank: string;
58
- rankEmoji: string;
59
- pets: number;
60
- feeds: number;
61
- turns: number;
62
- /** True while the pet interaction is inside its cooldown. */
63
- petCooldown: boolean;
64
- /** True while the feed is inside its cooldown. */
65
- feedCooldown: boolean;
66
- };
64
+ affinity: PetAffinityView;
67
65
  /** Display configuration. */
68
66
  display: PetDisplayConfig;
69
- /** 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). */
70
77
  name: string;
71
78
  /** Treat (小鱼干) stock snapshot. */
72
79
  treats: {
@@ -77,14 +84,7 @@ export interface PetStateView {
77
84
  };
78
85
  }
79
86
  /** Result of `pet.interact`. */
80
- export interface PetInteractResult {
81
- /** Reaction copy bubble. */
82
- reaction: string;
83
- /** Points gained (0 when inside the cooldown). */
84
- delta: number;
85
- /** Full affinity snapshot (same shape as state view). */
86
- affinity: PetStateView['affinity'];
87
- }
87
+ export type PetInteractResult = LedgerInteractionResult;
88
88
  declare module '@deepseek-ai/cordis' {
89
89
  interface Context {
90
90
  pet: PetService;
@@ -92,25 +92,21 @@ declare module '@deepseek-ai/cordis' {
92
92
  }
93
93
  /**
94
94
  * Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
95
- * written until a query or interaction arrives; event listeners update only
96
- * in-memory state, and persistence happens on interaction/config changes
97
- * plus every completed turn.
95
+ * written until an economic event or interaction arrives; event listeners
96
+ * update only in-memory state, and persistence happens on economic changes
97
+ * (turn rewards, feeds, config/name changes) — never on a read.
98
98
  */
99
99
  export declare class PetService extends Service {
100
100
  static inject: string[];
101
101
  private readonly machine;
102
- private readonly affinityConfig;
103
- private readonly treatConfig;
102
+ private readonly ledger;
103
+ private readonly registry;
104
104
  private readonly persistDir;
105
- private persist;
106
- /** Completed turns already rewarded, per session (turn numbers are per-session). */
107
- private rewardedTurns;
108
105
  private enabled;
109
106
  private disposeActivity;
110
107
  /** Session whose most recent meaningful event currently drives the global pet. */
111
108
  private displaySession;
112
109
  private readonly sessionActivity;
113
- private lastLegacyTurnRewardAt;
114
110
  constructor(ctx: Context, config?: PetConfig);
115
111
  /** Whether the pet service consumes session activity while enabled. */
116
112
  isEnabled(): boolean;
@@ -118,8 +114,24 @@ export declare class PetService extends Service {
118
114
  state(): Promise<PetStateView>;
119
115
  /** Current persisted display config (read-only view). */
120
116
  display(): PetDisplayConfig;
121
- /** Current persisted pet name (read-only view). */
122
- 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
+ }>;
123
135
  /** Start or stop the session-activity listeners that drive the pet. */
124
136
  setEnabled(enabled: boolean): void;
125
137
  private syncActivity;
@@ -139,7 +151,7 @@ export declare class PetService extends Service {
139
151
  ok: true;
140
152
  display: PetDisplayConfig;
141
153
  }>;
142
- /** RPC: rename the pet (trimmed, 1–20 chars). */
154
+ /** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
143
155
  setName(name: string): Promise<{
144
156
  ok: true;
145
157
  name: string;
@@ -148,9 +160,9 @@ export declare class PetService extends Service {
148
160
  error: string;
149
161
  }>;
150
162
  /**
151
- * Apply a committed settings section to the persisted display config. Called
152
- * by the settings surface on every change; values are clamped exactly like
153
- * 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.
154
166
  * @param section - the resolved settings section.
155
167
  */
156
168
  applySettingsSection(section: PetSettingsSection): void;
@@ -160,17 +172,7 @@ export declare class PetService extends Service {
160
172
  private rewardTurn;
161
173
  /** Preserve turn rewards for installations that only emit legacy activity. */
162
174
  private rewardLegacyTurn;
163
- /** Persist one accepted completed-turn reward. */
164
- private applyTurnReward;
165
- /**
166
- * Settle the treat economy (work + time output since the last settlement)
167
- * and persist whenever the ledger changed. A zero-gain first settlement
168
- * still starts the time clock (anchor write), which is what lets the
169
- * 30-minute time output ever accrue.
170
- */
171
- private settleTreats;
172
175
  private view;
173
- private affinityView;
174
176
  private flush;
175
177
  }
176
178
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,EAKL,KAAK,cAAc,EAEnB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAA;AACtB,OAAO,EAQL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;AAEnB,4BAA4B;AAC5B,MAAM,WAAW,SAAS;IACxB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAC/B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,2HAA2H;AAC3H,eAAO,MAAM,sBAAsB,QAAQ,CAAA;AAE3C,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChC,aAAa,EAAE,OAAO,CAAA;IACtB,gCAAgC;IAChC,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,SAAS,EAAE,MAAM,CAAA;QACjB,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,6DAA6D;QAC7D,WAAW,EAAE,OAAO,CAAA;QACpB,kDAAkD;QAClD,YAAY,EAAE,OAAO,CAAA;KACtB,CAAA;IACD,6BAA6B;IAC7B,OAAO,EAAE,gBAAgB,CAAA;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,MAAM,EAAE;QACN,0BAA0B;QAC1B,OAAO,EAAE,MAAM,CAAA;QACf,iBAAiB;QACjB,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;CACF;AAED,gCAAgC;AAChC,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,QAAQ,EAAE,YAAY,CAAC,UAAU,CAAC,CAAA;CACnC;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,GAAG,EAAE,UAAU,CAAA;KAChB;CACF;AAmHD;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,OAAO;IACrC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAK;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,OAAO,CAAY;IAC3B,oFAAoF;IACpF,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAA0B;IACjD,kFAAkF;IAClF,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiD;IACjF,OAAO,CAAC,sBAAsB,CAAI;gBAEtB,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc;IAehD,uEAAuE;IACvE,SAAS,IAAI,OAAO;IAIpB,uCAAuC;IACjC,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC;IAIpC,yDAAyD;IACzD,OAAO,IAAI,gBAAgB;IAI3B,mDAAmD;IACnD,OAAO,IAAI,MAAM;IAIjB,uEAAuE;IACvE,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKlC,OAAO,CAAC,YAAY;IAgDpB,oEAAoE;IACpE,OAAO,CAAC,eAAe;IAavB,8EAA8E;IAC9E,OAAO,CAAC,aAAa;IAMrB,gCAAgC;IAC1B,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA+BhE,iCAAiC;IAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAOpF,wFAAwF;IAClF,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAWnG,iDAAiD;IAC3C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAU/F;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAUvD,oFAAoF;IACpF,OAAO,CAAC,mBAAmB;IAc3B,qFAAqF;IACrF,OAAO,CAAC,UAAU;IAOlB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IAQxB,kDAAkD;IAClD,OAAO,CAAC,eAAe;IAKvB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,IAAI;IAmBZ,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,KAAK;CAOd"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
@@ -1,136 +1,60 @@
1
1
  /**
2
- * Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
3
- * (projects official session events and accepts legacy `activity/status`), the
4
- * affinity ledger, and the persisted display config. The API gateway maps
5
- * this service's methods onto `pet.state` / `pet.interact` /
6
- * `pet.setVisible` / `pet.setConfig` for browser consumers.
2
+ * Pet host service — the `pet.*` RPC domain. A composition facade: it wires
3
+ * the pure event projection (`event-projection`) onto the state machine,
4
+ * delegates the affinity economy to the ledger (`ledger`), and routes
5
+ * persistence through `persist`. The API gateway maps these methods onto
6
+ * `pet.state` / `pet.pets` / `pet.interact` / `pet.setVisible` /
7
+ * `pet.setConfig` / `pet.setName` / `pet.setPet` for browser consumers.
7
8
  * @module @linxin666/dsh-pet/service
8
9
  */
9
10
  import { Service } from '@deepseek-ai/cordis';
10
- import { applyInteraction, applyTurnReward, defaultAffinityConfig, rankOf, } from "./affinity.js";
11
- import { loadPetPersist, petHomeDir, savePetPersist, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, DISPLAY_INSET_MAX, PET_NAME_MAX_LENGTH, } from "./persist.js";
12
- import { defaultTreatConfig, settleTreatGrants, consumeTreat, } from "./treats.js";
11
+ import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
12
+ import { PetLedger } from "./ledger.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";
13
15
  import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
14
16
  /** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
15
17
  export const PET_SETTINGS_NAMESPACE = 'pet';
16
- /** Keep tool names readable inside the compact status bubble. */
17
- function displayToolName(name) {
18
- const compact = name.replace(/\s+/g, ' ').trim() || '工具';
19
- return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
20
- }
21
- /** Whether a legacy phase is part of the pet's supported vocabulary. */
22
- function isActivityPhase(phase) {
23
- return ['idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed'].includes(phase);
24
- }
25
- /**
26
- * Project the durable DSH session vocabulary into the pet's visual phases.
27
- * Unknown and log-only events do not disturb the last meaningful activity.
28
- */
29
- function projectOfficialEvent(event, runtime) {
30
- switch (event.type) {
31
- case 'turn/start':
32
- runtime.activeTools.clear();
33
- runtime.stepHadFailure = false;
34
- return { input: { phase: 'waiting', line: '准备开始' } };
35
- case 'step/start':
36
- runtime.activeTools.clear();
37
- runtime.stepHadFailure = false;
38
- return { input: { phase: 'waiting', line: '等待模型响应' } };
39
- case 'assistant/chunk': {
40
- const { chunk } = event.data;
41
- if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
42
- return { input: { phase: 'thinking', line: '正在思考' } };
43
- }
44
- if (chunk.type === 'text-delta' && chunk.text.length > 0) {
45
- return { input: { phase: 'review', line: '整理回复中' } };
46
- }
47
- return undefined;
48
- }
49
- case 'assistant/message':
50
- return { input: { phase: 'review', line: '整理回复中' } };
51
- case 'tool/call':
52
- runtime.activeTools.add(String(event.data.callId));
53
- return {
54
- input: {
55
- phase: 'tool',
56
- line: `正在使用 ${displayToolName(event.data.name)}`,
57
- },
58
- };
59
- case 'tool/result': {
60
- const block = event.data.message.content[0];
61
- runtime.activeTools.delete(String(event.data.message.source.callId));
62
- runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true;
63
- if (runtime.activeTools.size > 0) {
64
- return {
65
- input: {
66
- phase: 'tool',
67
- line: `还有 ${runtime.activeTools.size} 个工具运行中`,
68
- },
69
- };
70
- }
71
- return runtime.stepHadFailure
72
- ? { input: { phase: 'failed', line: '工具执行失败' } }
73
- : { input: { phase: 'thinking', line: '处理工具结果' } };
74
- }
75
- case 'turn/end': {
76
- runtime.activeTools.clear();
77
- switch (event.data.reason.kind) {
78
- case 'completed':
79
- return {
80
- input: { phase: 'done', line: '完成啦' },
81
- completedTurn: event.data.turn,
82
- };
83
- case 'error':
84
- return { input: { phase: 'failed', line: '执行失败' } };
85
- case 'max-tokens':
86
- return { input: { phase: 'failed', line: '达到输出上限' } };
87
- case 'interrupted':
88
- return { input: { phase: 'failed', line: '执行意外中断' } };
89
- case 'blocked':
90
- return { input: { phase: 'waiting', line: '等待继续' } };
91
- case 'aborted':
92
- return { input: { phase: 'idle', line: '已停止' } };
93
- default:
94
- // TurnEndReasonMap is merge-extensible; a newer ending must not
95
- // leave the pet showing stale in-progress work.
96
- return { input: { phase: 'idle' } };
97
- }
98
- }
99
- default:
100
- return undefined;
101
- }
102
- }
103
18
  /**
104
19
  * Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
105
- * written until a query or interaction arrives; event listeners update only
106
- * in-memory state, and persistence happens on interaction/config changes
107
- * plus every completed turn.
20
+ * written until an economic event or interaction arrives; event listeners
21
+ * update only in-memory state, and persistence happens on economic changes
22
+ * (turn rewards, feeds, config/name changes) — never on a read.
108
23
  */
109
24
  export class PetService extends Service {
110
25
  static inject = [];
111
26
  machine;
112
- affinityConfig;
113
- treatConfig;
27
+ ledger;
28
+ registry;
114
29
  persistDir;
115
- persist;
116
- /** Completed turns already rewarded, per session (turn numbers are per-session). */
117
- rewardedTurns = new Map();
118
30
  enabled;
119
31
  disposeActivity;
120
32
  /** Session whose most recent meaningful event currently drives the global pet. */
121
33
  displaySession;
122
34
  sessionActivity = new WeakMap();
123
- lastLegacyTurnRewardAt = 0;
124
35
  constructor(ctx, config = {}) {
125
36
  super(ctx, 'pet');
126
37
  this.persistDir = config.persistDir ?? petHomeDir();
127
- this.affinityConfig = { ...defaultAffinityConfig, ...(config.affinity ?? {}) };
128
- this.treatConfig = { ...defaultTreatConfig, ...(config.treats ?? {}) };
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
+ }
52
+ const ledgerConfig = { affinity: config.affinity, treats: config.treats };
53
+ this.ledger = new PetLedger(persist, ledgerConfig);
129
54
  this.machine = new PetStateMachine({
130
55
  ...defaultPetStateConfig,
131
56
  ...(config.state ?? {}),
132
57
  });
133
- this.persist = loadPetPersist(this.persistDir);
134
58
  this.enabled = config.enabled ?? true;
135
59
  this.syncActivity();
136
60
  }
@@ -144,11 +68,40 @@ export class PetService extends Service {
144
68
  }
145
69
  /** Current persisted display config (read-only view). */
146
70
  display() {
147
- return { ...this.persist.display };
71
+ return { ...this.ledger.snapshot.display };
72
+ }
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;
148
88
  }
149
- /** Current persisted pet name (read-only view). */
150
- petName() {
151
- return this.persist.name;
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 };
152
105
  }
153
106
  /** Start or stop the session-activity listeners that drive the pet. */
154
107
  setEnabled(enabled) {
@@ -210,11 +163,7 @@ export class PetService extends Service {
210
163
  activityRuntime(session) {
211
164
  let runtime = this.sessionActivity.get(session);
212
165
  if (runtime === undefined) {
213
- runtime = {
214
- activeTools: new Set(),
215
- officialEventsSeen: false,
216
- stepHadFailure: false,
217
- };
166
+ runtime = emptyProjectionRuntime();
218
167
  this.sessionActivity.set(session, runtime);
219
168
  }
220
169
  return runtime;
@@ -228,78 +177,61 @@ export class PetService extends Service {
228
177
  /** RPC: pet or feed the pet. */
229
178
  async interact(kind) {
230
179
  const nowMs = Date.now();
231
- // Feeding consumes a treat: settle the economy first (work + time
232
- // output since the last settlement), then gate on the feed cooldown
233
- // BEFORE spending stock — a feed inside the cooldown must not burn a
234
- // treat for nothing.
235
- if (kind === 'feed')
236
- this.settleTreats(nowMs);
237
- const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig);
238
- if (kind === 'feed' && !outcome.accepted) {
239
- return { reaction: outcome.reaction, delta: 0, affinity: this.affinityView(this.persist.affinity) };
240
- }
241
- if (kind === 'feed') {
242
- const consume = consumeTreat(this.persist.treats);
243
- if (!consume.ok) {
244
- const affinity = this.affinityView(this.persist.affinity);
245
- return {
246
- reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
247
- delta: 0,
248
- affinity,
249
- };
250
- }
251
- this.persist = { ...this.persist, treats: consume.ledger };
252
- }
253
- if (outcome.accepted) {
254
- this.persist = { ...this.persist, affinity: outcome.affinity };
180
+ const result = this.ledger.interact(kind, nowMs);
181
+ if (this.ledger.takeDirty())
255
182
  this.flush();
256
- }
257
- const affinity = this.affinityView(outcome.affinity);
258
- return { reaction: outcome.reaction, delta: outcome.delta, affinity };
183
+ return result;
259
184
  }
260
185
  /** RPC: show or hide the pet. */
261
186
  async setVisible(visible) {
262
- this.persist = { ...this.persist, display: { ...this.persist.display, visible } };
187
+ this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
263
188
  this.flush();
264
189
  this.syncSettingsFromPet();
265
- return { ok: true, display: this.persist.display };
190
+ return { ok: true, display: this.ledger.snapshot.display };
266
191
  }
267
192
  /** RPC: update display config (size / position). Values are clamped to whole pixels. */
268
193
  async setConfig(patch) {
269
- const next = { ...this.persist.display, ...patch };
194
+ const next = { ...this.ledger.snapshot.display, ...patch };
270
195
  next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)));
271
196
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
272
197
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
273
- this.persist = { ...this.persist, display: next };
198
+ this.ledger.setDisplay(next);
274
199
  this.flush();
275
200
  this.syncSettingsFromPet();
276
- return { ok: true, display: this.persist.display };
201
+ return { ok: true, display: this.ledger.snapshot.display };
277
202
  }
278
- /** RPC: rename the pet (trimmed, 1–20 chars). */
203
+ /** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
279
204
  async setName(name) {
280
205
  const trimmed = name.trim();
281
206
  if (trimmed === '')
282
207
  return { ok: false, error: 'name-empty' };
283
208
  if (trimmed.length > PET_NAME_MAX_LENGTH)
284
209
  return { ok: false, error: 'name-too-long' };
285
- this.persist = { ...this.persist, name: trimmed };
210
+ this.ledger.setPetName(this.selectedPetId(), trimmed);
286
211
  this.flush();
287
- this.syncSettingsFromPet();
288
212
  return { ok: true, name: trimmed };
289
213
  }
290
214
  /**
291
- * Apply a committed settings section to the persisted display config. Called
292
- * by the settings surface on every change; values are clamped exactly like
293
- * 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.
294
218
  * @param section - the resolved settings section.
295
219
  */
296
220
  applySettingsSection(section) {
297
- const next = { ...this.persist.display };
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
+ }
229
+ const next = { ...this.ledger.snapshot.display };
298
230
  next.visible = section.visible && (section.enabled ?? true);
299
231
  next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)));
300
232
  next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
301
233
  next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
302
- this.persist = { ...this.persist, display: next, name: section.name.trim() };
234
+ this.ledger.setDisplay(next);
303
235
  this.flush();
304
236
  }
305
237
  /** Mirror the persisted display config into the settings document (best-effort). */
@@ -307,86 +239,54 @@ export class PetService extends Service {
307
239
  const settings = this.ctx.get('settings', false);
308
240
  if (settings === undefined)
309
241
  return;
242
+ const snapshot = this.ledger.snapshot;
310
243
  void settings.update(PET_SETTINGS_NAMESPACE, {
311
- visible: this.persist.display.visible,
312
- size: this.persist.display.size,
313
- right: this.persist.display.right,
314
- bottom: this.persist.display.bottom,
315
- name: this.persist.name,
244
+ visible: snapshot.display.visible,
245
+ size: snapshot.display.size,
246
+ right: snapshot.display.right,
247
+ bottom: snapshot.display.bottom,
248
+ petId: snapshot.petId,
316
249
  }).catch(() => {
317
250
  // A settings write failure must not break the pet's own persistence.
318
251
  });
319
252
  }
320
253
  /** Award the turn reward once per completed turn (idempotent per session + turn). */
321
254
  rewardTurn(sessionId, turn) {
322
- const last = this.rewardedTurns.get(sessionId) ?? 0;
323
- if (turn <= last)
324
- return;
325
- this.rewardedTurns.set(sessionId, turn);
326
- this.applyTurnReward();
255
+ if (this.ledger.rewardTurn(sessionId, turn, Date.now()))
256
+ this.flush();
327
257
  }
328
258
  /** Preserve turn rewards for installations that only emit legacy activity. */
329
259
  rewardLegacyTurn() {
330
- const nowMs = Date.now();
331
- // A legacy `done` snapshot may repeat during the celebration window.
332
- if (nowMs - this.lastLegacyTurnRewardAt < 5_000)
333
- return;
334
- this.lastLegacyTurnRewardAt = nowMs;
335
- this.applyTurnReward();
336
- }
337
- /** Persist one accepted completed-turn reward. */
338
- applyTurnReward() {
339
- this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) };
340
- this.flush();
341
- }
342
- /**
343
- * Settle the treat economy (work + time output since the last settlement)
344
- * and persist whenever the ledger changed. A zero-gain first settlement
345
- * still starts the time clock (anchor write), which is what lets the
346
- * 30-minute time output ever accrue.
347
- */
348
- settleTreats(nowMs) {
349
- const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
350
- if (settlement.ledger !== this.persist.treats) {
351
- this.persist = { ...this.persist, treats: settlement.ledger };
260
+ if (this.ledger.rewardLegacyTurn(Date.now()))
352
261
  this.flush();
353
- }
354
262
  }
355
263
  view() {
356
264
  const snapshot = this.machine.render();
357
- // Time-output treats accrue while the host is idle too; settle on read.
358
- this.settleTreats(Date.now());
265
+ const entry = this.activeEntry();
266
+ // Read-only: the ledger settles on economic events only, never on a read,
267
+ // so polling the state cannot trigger pet.json writes.
359
268
  return {
360
269
  animation: snapshot.animation,
361
270
  ...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
362
271
  phase: snapshot.phase,
363
272
  sessionActive: snapshot.sessionActive,
364
- affinity: this.affinityView(this.persist.affinity),
365
- display: { ...this.persist.display },
366
- name: this.persist.name,
273
+ affinity: this.ledger.affinityView(Date.now()),
274
+ display: { ...this.ledger.snapshot.display },
275
+ pet: {
276
+ id: entry.id,
277
+ displayName: entry.displayName,
278
+ description: entry.description,
279
+ },
280
+ name: this.petName(),
367
281
  treats: {
368
- stocked: this.persist.treats.treats,
369
- max: this.treatConfig.maxTreats,
282
+ stocked: this.ledger.snapshot.treats.treats,
283
+ max: this.ledger.treatMax,
370
284
  },
371
285
  };
372
286
  }
373
- affinityView(affinity) {
374
- const nowMs = Date.now();
375
- const rank = rankOf(affinity.points);
376
- return {
377
- points: affinity.points,
378
- rank: rank.name,
379
- rankEmoji: rank.emoji,
380
- pets: affinity.pets,
381
- feeds: affinity.feeds,
382
- turns: affinity.turns,
383
- petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
384
- feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs,
385
- };
386
- }
387
287
  flush() {
388
288
  try {
389
- savePetPersist(this.persist, this.persistDir);
289
+ savePetPersist(this.ledger.snapshot, this.persistDir);
390
290
  }
391
291
  catch {
392
292
  // Persistence is best-effort; the in-memory ledger keeps working.