@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.
- package/README.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +296 -347
- package/lib/client.js.map +1 -1
- package/lib/index.js +975 -317
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +32 -14
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +79 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +146 -0
- package/lib/types/persist.d.ts +15 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +39 -8
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +59 -57
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +111 -211
- package/package.json +2 -2
- package/src/affinity.ts +33 -0
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/index.ts +38 -16
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/client/spritesheet.ts +16 -92
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +69 -0
- package/src/ledger.ts +190 -0
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +49 -11
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +153 -285
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/lib/types/service.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet host service — the `pet.*` RPC domain.
|
|
3
|
-
*
|
|
4
|
-
* affinity
|
|
5
|
-
*
|
|
6
|
-
* `pet.
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
96
|
-
* in-memory state, and persistence happens on
|
|
97
|
-
*
|
|
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
|
|
103
|
-
private readonly
|
|
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
|
-
/**
|
|
122
|
-
|
|
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
|
|
152
|
-
* by the settings surface on every change; values are
|
|
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
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -1,136 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet host service — the `pet.*` RPC domain.
|
|
3
|
-
*
|
|
4
|
-
* affinity
|
|
5
|
-
*
|
|
6
|
-
* `pet.
|
|
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
106
|
-
* in-memory state, and persistence happens on
|
|
107
|
-
*
|
|
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
|
-
|
|
113
|
-
|
|
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.
|
|
128
|
-
|
|
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.
|
|
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
|
-
/**
|
|
150
|
-
petName() {
|
|
151
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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.
|
|
187
|
+
this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
|
|
263
188
|
this.flush();
|
|
264
189
|
this.syncSettingsFromPet();
|
|
265
|
-
return { ok: true, display: this.
|
|
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.
|
|
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.
|
|
198
|
+
this.ledger.setDisplay(next);
|
|
274
199
|
this.flush();
|
|
275
200
|
this.syncSettingsFromPet();
|
|
276
|
-
return { ok: true, display: this.
|
|
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.
|
|
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
|
|
292
|
-
* by the settings surface on every change; values are
|
|
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
|
-
|
|
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.
|
|
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:
|
|
312
|
-
size:
|
|
313
|
-
right:
|
|
314
|
-
bottom:
|
|
315
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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(
|
|
365
|
-
display: { ...this.
|
|
366
|
-
|
|
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.
|
|
369
|
-
max: this.
|
|
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.
|
|
289
|
+
savePetPersist(this.ledger.snapshot, this.persistDir);
|
|
390
290
|
}
|
|
391
291
|
catch {
|
|
392
292
|
// Persistence is best-effort; the in-memory ledger keeps working.
|