@linxin666/dsh-pet 0.1.13 → 0.1.15
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 +11 -7
- package/README.zh.md +11 -7
- package/lib/client.js +38 -17
- package/lib/client.js.map +1 -1
- package/lib/index.js +393 -150
- package/lib/invariant.js +1 -1
- package/lib/{state-C9EyycwI.js → state-CFyJv0sQ.js} +22 -7
- 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/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +27 -7
- package/lib/types/client/WhalePet.d.ts.map +1 -1
- package/lib/types/client/WhalePet.js +2 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/settings-form.d.ts +14 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +38 -10
- 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/ledger.d.ts +77 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +139 -0
- package/lib/types/persist.d.ts +5 -1
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +7 -3
- package/lib/types/service.d.ts +24 -44
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +98 -131
- package/lib/types/state.d.ts +10 -12
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +14 -10
- package/package.json +1 -1
- package/src/affinity.ts +33 -0
- package/src/client/PluginSettingsCard.tsx +83 -17
- package/src/client/WhalePet.test.tsx +25 -1
- package/src/client/WhalePet.tsx +6 -0
- package/src/client/index.ts +3 -3
- package/src/client/pet.module.css +9 -0
- package/src/client/settings-card.module.css +6 -0
- package/src/client/settings-form.ts +41 -9
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/ledger.test.ts +67 -0
- package/src/ledger.ts +183 -0
- package/src/persist.ts +7 -3
- package/src/service.ts +110 -171
- package/src/state.test.ts +4 -1
- package/src/state.ts +17 -13
package/lib/types/service.js
CHANGED
|
@@ -1,46 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet host service — the `pet.*` RPC domain.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig`
|
|
7
|
-
*
|
|
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.interact` / `pet.setVisible` / `pet.setConfig` for
|
|
7
|
+
* browser consumers.
|
|
8
8
|
* @module @linxin666/dsh-pet/service
|
|
9
9
|
*/
|
|
10
10
|
import { Service } from '@deepseek-ai/cordis';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
|
|
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";
|
|
14
14
|
import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
|
|
15
15
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
16
16
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
17
17
|
/**
|
|
18
18
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
19
|
-
* written until
|
|
20
|
-
* in-memory state, and persistence happens on
|
|
21
|
-
*
|
|
19
|
+
* written until an economic event or interaction arrives; event listeners
|
|
20
|
+
* update only in-memory state, and persistence happens on economic changes
|
|
21
|
+
* (turn rewards, feeds, config/name changes) — never on a read.
|
|
22
22
|
*/
|
|
23
23
|
export class PetService extends Service {
|
|
24
24
|
static inject = [];
|
|
25
25
|
machine;
|
|
26
|
-
|
|
27
|
-
treatConfig;
|
|
26
|
+
ledger;
|
|
28
27
|
persistDir;
|
|
29
|
-
persist;
|
|
30
|
-
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
31
|
-
rewardedTurns = new Map();
|
|
32
28
|
enabled;
|
|
33
29
|
disposeActivity;
|
|
30
|
+
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
31
|
+
displaySession;
|
|
32
|
+
sessionActivity = new WeakMap();
|
|
34
33
|
constructor(ctx, config = {}) {
|
|
35
34
|
super(ctx, 'pet');
|
|
36
35
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
37
|
-
|
|
38
|
-
this.
|
|
36
|
+
const ledgerConfig = { affinity: config.affinity, treats: config.treats };
|
|
37
|
+
this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
|
|
39
38
|
this.machine = new PetStateMachine({
|
|
40
39
|
...defaultPetStateConfig,
|
|
41
40
|
...(config.state ?? {}),
|
|
42
41
|
});
|
|
43
|
-
this.persist = loadPetPersist(this.persistDir);
|
|
44
42
|
this.enabled = config.enabled ?? true;
|
|
45
43
|
this.syncActivity();
|
|
46
44
|
}
|
|
@@ -54,11 +52,11 @@ export class PetService extends Service {
|
|
|
54
52
|
}
|
|
55
53
|
/** Current persisted display config (read-only view). */
|
|
56
54
|
display() {
|
|
57
|
-
return { ...this.
|
|
55
|
+
return { ...this.ledger.snapshot.display };
|
|
58
56
|
}
|
|
59
57
|
/** Current persisted pet name (read-only view). */
|
|
60
58
|
petName() {
|
|
61
|
-
return this.
|
|
59
|
+
return this.ledger.snapshot.name;
|
|
62
60
|
}
|
|
63
61
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
64
62
|
setEnabled(enabled) {
|
|
@@ -75,38 +73,40 @@ export class PetService extends Service {
|
|
|
75
73
|
this.disposeActivity = (() => {
|
|
76
74
|
const disposers = [
|
|
77
75
|
this.ctx.on('session/event', (session, event) => {
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
break;
|
|
76
|
+
const runtime = this.activityRuntime(session);
|
|
77
|
+
// `activity/status` is an optional compatibility input. It is not
|
|
78
|
+
// declared as a durable event type by this package because current
|
|
79
|
+
// Harness installations publish the official session vocabulary.
|
|
80
|
+
if (event.type === 'activity/status') {
|
|
81
|
+
const payload = (event.data ?? {});
|
|
82
|
+
if (typeof payload.phase !== 'string' || !isActivityPhase(payload.phase))
|
|
83
|
+
return;
|
|
84
|
+
this.applyActivity(session, {
|
|
85
|
+
phase: payload.phase,
|
|
86
|
+
...(typeof payload.line === 'string' ? { line: payload.line } : {}),
|
|
87
|
+
...(typeof payload.phrase === 'string' ? { phrase: payload.phrase } : {}),
|
|
88
|
+
});
|
|
89
|
+
// On a legacy-only stream the compatibility event owns turn
|
|
90
|
+
// rewards. Once any official activity is observed, turn/end owns
|
|
91
|
+
// them and a derived legacy `done` cannot double-count.
|
|
92
|
+
if (payload.phase === 'done' && !runtime.officialEventsSeen) {
|
|
93
|
+
this.rewardLegacyTurn();
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const transition = projectOfficialEvent(event, runtime);
|
|
98
|
+
if (transition === undefined)
|
|
99
|
+
return;
|
|
100
|
+
runtime.officialEventsSeen = true;
|
|
101
|
+
this.applyActivity(session, transition.input);
|
|
102
|
+
if (transition.completedTurn !== undefined) {
|
|
103
|
+
this.rewardTurn(String(session.id), transition.completedTurn);
|
|
107
104
|
}
|
|
108
105
|
}),
|
|
109
|
-
this.ctx.on('session/disposed', () => {
|
|
106
|
+
this.ctx.on('session/disposed', (session) => {
|
|
107
|
+
if (session !== this.displaySession)
|
|
108
|
+
return;
|
|
109
|
+
this.displaySession = undefined;
|
|
110
110
|
this.machine.onSessionDisposed();
|
|
111
111
|
}),
|
|
112
112
|
];
|
|
@@ -114,55 +114,46 @@ export class PetService extends Service {
|
|
|
114
114
|
dispose(); };
|
|
115
115
|
})();
|
|
116
116
|
}
|
|
117
|
+
/** Return the projection state associated with one live session. */
|
|
118
|
+
activityRuntime(session) {
|
|
119
|
+
let runtime = this.sessionActivity.get(session);
|
|
120
|
+
if (runtime === undefined) {
|
|
121
|
+
runtime = emptyProjectionRuntime();
|
|
122
|
+
this.sessionActivity.set(session, runtime);
|
|
123
|
+
}
|
|
124
|
+
return runtime;
|
|
125
|
+
}
|
|
126
|
+
/** Commit one activity as the host-global pet's most recent display state. */
|
|
127
|
+
applyActivity(session, input) {
|
|
128
|
+
this.displaySession = session;
|
|
129
|
+
this.machine.onActivityStatus(input);
|
|
130
|
+
this.machine.onSessionActive();
|
|
131
|
+
}
|
|
117
132
|
/** RPC: pet or feed the pet. */
|
|
118
133
|
async interact(kind) {
|
|
119
134
|
const nowMs = Date.now();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// BEFORE spending stock — a feed inside the cooldown must not burn a
|
|
123
|
-
// treat for nothing.
|
|
124
|
-
if (kind === 'feed')
|
|
125
|
-
this.settleTreats(nowMs);
|
|
126
|
-
const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig);
|
|
127
|
-
if (kind === 'feed' && !outcome.accepted) {
|
|
128
|
-
return { reaction: outcome.reaction, delta: 0, affinity: this.affinityView(this.persist.affinity) };
|
|
129
|
-
}
|
|
130
|
-
if (kind === 'feed') {
|
|
131
|
-
const consume = consumeTreat(this.persist.treats);
|
|
132
|
-
if (!consume.ok) {
|
|
133
|
-
const affinity = this.affinityView(this.persist.affinity);
|
|
134
|
-
return {
|
|
135
|
-
reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
|
|
136
|
-
delta: 0,
|
|
137
|
-
affinity,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
this.persist = { ...this.persist, treats: consume.ledger };
|
|
141
|
-
}
|
|
142
|
-
if (outcome.accepted) {
|
|
143
|
-
this.persist = { ...this.persist, affinity: outcome.affinity };
|
|
135
|
+
const result = this.ledger.interact(kind, nowMs);
|
|
136
|
+
if (this.ledger.takeDirty())
|
|
144
137
|
this.flush();
|
|
145
|
-
|
|
146
|
-
const affinity = this.affinityView(outcome.affinity);
|
|
147
|
-
return { reaction: outcome.reaction, delta: outcome.delta, affinity };
|
|
138
|
+
return result;
|
|
148
139
|
}
|
|
149
140
|
/** RPC: show or hide the pet. */
|
|
150
141
|
async setVisible(visible) {
|
|
151
|
-
this.
|
|
142
|
+
this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
|
|
152
143
|
this.flush();
|
|
153
144
|
this.syncSettingsFromPet();
|
|
154
|
-
return { ok: true, display: this.
|
|
145
|
+
return { ok: true, display: this.ledger.snapshot.display };
|
|
155
146
|
}
|
|
156
147
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
157
148
|
async setConfig(patch) {
|
|
158
|
-
const next = { ...this.
|
|
149
|
+
const next = { ...this.ledger.snapshot.display, ...patch };
|
|
159
150
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)));
|
|
160
151
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
|
|
161
152
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
|
|
162
|
-
this.
|
|
153
|
+
this.ledger.setDisplay(next);
|
|
163
154
|
this.flush();
|
|
164
155
|
this.syncSettingsFromPet();
|
|
165
|
-
return { ok: true, display: this.
|
|
156
|
+
return { ok: true, display: this.ledger.snapshot.display };
|
|
166
157
|
}
|
|
167
158
|
/** RPC: rename the pet (trimmed, 1–20 chars). */
|
|
168
159
|
async setName(name) {
|
|
@@ -171,7 +162,7 @@ export class PetService extends Service {
|
|
|
171
162
|
return { ok: false, error: 'name-empty' };
|
|
172
163
|
if (trimmed.length > PET_NAME_MAX_LENGTH)
|
|
173
164
|
return { ok: false, error: 'name-too-long' };
|
|
174
|
-
this.
|
|
165
|
+
this.ledger.setName(trimmed);
|
|
175
166
|
this.flush();
|
|
176
167
|
this.syncSettingsFromPet();
|
|
177
168
|
return { ok: true, name: trimmed };
|
|
@@ -183,12 +174,13 @@ export class PetService extends Service {
|
|
|
183
174
|
* @param section - the resolved settings section.
|
|
184
175
|
*/
|
|
185
176
|
applySettingsSection(section) {
|
|
186
|
-
const next = { ...this.
|
|
177
|
+
const next = { ...this.ledger.snapshot.display };
|
|
187
178
|
next.visible = section.visible && (section.enabled ?? true);
|
|
188
179
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)));
|
|
189
180
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
|
|
190
181
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
|
|
191
|
-
this.
|
|
182
|
+
this.ledger.setDisplay(next);
|
|
183
|
+
this.ledger.setName(section.name.trim());
|
|
192
184
|
this.flush();
|
|
193
185
|
}
|
|
194
186
|
/** Mirror the persisted display config into the settings document (best-effort). */
|
|
@@ -196,73 +188,48 @@ export class PetService extends Service {
|
|
|
196
188
|
const settings = this.ctx.get('settings', false);
|
|
197
189
|
if (settings === undefined)
|
|
198
190
|
return;
|
|
191
|
+
const snapshot = this.ledger.snapshot;
|
|
199
192
|
void settings.update(PET_SETTINGS_NAMESPACE, {
|
|
200
|
-
visible:
|
|
201
|
-
size:
|
|
202
|
-
right:
|
|
203
|
-
bottom:
|
|
204
|
-
name:
|
|
193
|
+
visible: snapshot.display.visible,
|
|
194
|
+
size: snapshot.display.size,
|
|
195
|
+
right: snapshot.display.right,
|
|
196
|
+
bottom: snapshot.display.bottom,
|
|
197
|
+
name: snapshot.name,
|
|
205
198
|
}).catch(() => {
|
|
206
199
|
// A settings write failure must not break the pet's own persistence.
|
|
207
200
|
});
|
|
208
201
|
}
|
|
209
202
|
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
210
203
|
rewardTurn(sessionId, turn) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return;
|
|
214
|
-
this.rewardedTurns.set(sessionId, turn);
|
|
215
|
-
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) };
|
|
216
|
-
this.flush();
|
|
204
|
+
if (this.ledger.rewardTurn(sessionId, turn, Date.now()))
|
|
205
|
+
this.flush();
|
|
217
206
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
* still starts the time clock (anchor write), which is what lets the
|
|
222
|
-
* 30-minute time output ever accrue.
|
|
223
|
-
*/
|
|
224
|
-
settleTreats(nowMs) {
|
|
225
|
-
const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
|
|
226
|
-
if (settlement.ledger !== this.persist.treats) {
|
|
227
|
-
this.persist = { ...this.persist, treats: settlement.ledger };
|
|
207
|
+
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
208
|
+
rewardLegacyTurn() {
|
|
209
|
+
if (this.ledger.rewardLegacyTurn(Date.now()))
|
|
228
210
|
this.flush();
|
|
229
|
-
}
|
|
230
211
|
}
|
|
231
212
|
view() {
|
|
232
213
|
const snapshot = this.machine.render();
|
|
233
|
-
//
|
|
234
|
-
|
|
214
|
+
// Read-only: the ledger settles on economic events only, never on a read,
|
|
215
|
+
// so polling the state cannot trigger pet.json writes.
|
|
235
216
|
return {
|
|
236
217
|
animation: snapshot.animation,
|
|
237
218
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
238
219
|
phase: snapshot.phase,
|
|
239
220
|
sessionActive: snapshot.sessionActive,
|
|
240
|
-
affinity: this.affinityView(
|
|
241
|
-
display: { ...this.
|
|
242
|
-
name: this.
|
|
221
|
+
affinity: this.ledger.affinityView(Date.now()),
|
|
222
|
+
display: { ...this.ledger.snapshot.display },
|
|
223
|
+
name: this.ledger.snapshot.name,
|
|
243
224
|
treats: {
|
|
244
|
-
stocked: this.
|
|
245
|
-
max: this.
|
|
225
|
+
stocked: this.ledger.snapshot.treats.treats,
|
|
226
|
+
max: this.ledger.treatMax,
|
|
246
227
|
},
|
|
247
228
|
};
|
|
248
229
|
}
|
|
249
|
-
affinityView(affinity) {
|
|
250
|
-
const nowMs = Date.now();
|
|
251
|
-
const rank = rankOf(affinity.points);
|
|
252
|
-
return {
|
|
253
|
-
points: affinity.points,
|
|
254
|
-
rank: rank.name,
|
|
255
|
-
rankEmoji: rank.emoji,
|
|
256
|
-
pets: affinity.pets,
|
|
257
|
-
feeds: affinity.feeds,
|
|
258
|
-
turns: affinity.turns,
|
|
259
|
-
petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
|
|
260
|
-
feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
230
|
flush() {
|
|
264
231
|
try {
|
|
265
|
-
savePetPersist(this.
|
|
232
|
+
savePetPersist(this.ledger.snapshot, this.persistDir);
|
|
266
233
|
}
|
|
267
234
|
catch {
|
|
268
235
|
// Persistence is best-effort; the in-memory ledger keeps working.
|
package/lib/types/state.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps official DSH session activity
|
|
3
|
+
* and the legacy `activity/status` vocabulary onto the 9-state Codex pet
|
|
4
|
+
* animation contract, plus turn-end celebration and no-session idle.
|
|
6
5
|
*
|
|
7
6
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
7
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -10,13 +9,13 @@
|
|
|
10
9
|
* function of (input, nowMs); persistence and RPC live in the service.
|
|
11
10
|
* @module @linxin666/dsh-pet/state
|
|
12
11
|
*/
|
|
13
|
-
/**
|
|
14
|
-
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done';
|
|
12
|
+
/** Activity phases understood by the pet host. */
|
|
13
|
+
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'review' | 'done' | 'failed';
|
|
15
14
|
/** The Codex-compatible 9-state animation contract (spritesheet rows). */
|
|
16
15
|
export type PetAnimation = 'idle' | 'running-right' | 'running-left' | 'waving' | 'jumping' | 'failed' | 'waiting' | 'running' | 'review';
|
|
17
16
|
/** One input snapshot consumed by the machine. */
|
|
18
17
|
export interface PetStateInput {
|
|
19
|
-
/** Current
|
|
18
|
+
/** Current activity phase of the active session. */
|
|
20
19
|
phase: ActivityPhase;
|
|
21
20
|
/** Human-readable status line (plain text). */
|
|
22
21
|
line?: string;
|
|
@@ -44,13 +43,12 @@ export interface PetStateConfig {
|
|
|
44
43
|
export declare const defaultPetStateConfig: PetStateConfig;
|
|
45
44
|
/**
|
|
46
45
|
* Map one activity phase onto the animation contract.
|
|
47
|
-
* - thinking
|
|
48
|
-
*
|
|
46
|
+
* - thinking → `running` and tool → `running-right` (focused work).
|
|
47
|
+
* - review → `review` while answer text is streaming.
|
|
49
48
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
50
49
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
50
|
+
* - failed → `failed` until another activity event arrives.
|
|
51
51
|
* - idle → `idle` (calm breathing loop).
|
|
52
|
-
* `failed` has no DSH phase source yet; the machine keeps the mapping table
|
|
53
|
-
* so a future error event can light it up.
|
|
54
52
|
*/
|
|
55
53
|
export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
|
|
56
54
|
/** The spritesheet row index for one animation track. */
|
|
@@ -68,7 +66,7 @@ export declare class PetStateMachine {
|
|
|
68
66
|
private sessionActive;
|
|
69
67
|
private doneAt;
|
|
70
68
|
constructor(config?: PetStateConfig, now?: () => number);
|
|
71
|
-
/** Consume one
|
|
69
|
+
/** Consume one projected activity update. */
|
|
72
70
|
onActivityStatus(input: PetStateInput): void;
|
|
73
71
|
/** A session became the active one (or a fresh session started). */
|
|
74
72
|
onSessionActive(): void;
|
package/lib/types/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEnG,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,eAAe,GACf,cAAc,GACd,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,SAAS,EAAE,YAAY,CAAA;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAA;IACpB,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAsC,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAUpE;AAED,yDAAyD;AACzD,wBAAgB,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAarD;AAED;;;GAGG;AACH,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;gBAGf,MAAM,GAAE,cAAsC,EAC9C,GAAG,GAAE,MAAM,MAAiB;IAG/C,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAO5C,oEAAoE;IACpE,eAAe,IAAI,IAAI;IAIvB,sDAAsD;IACtD,iBAAiB,IAAI,IAAI;IAQzB,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB;CAwB3B"}
|
package/lib/types/state.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps official DSH session activity
|
|
3
|
+
* and the legacy `activity/status` vocabulary onto the 9-state Codex pet
|
|
4
|
+
* animation contract, plus turn-end celebration and no-session idle.
|
|
6
5
|
*
|
|
7
6
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
7
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -13,20 +12,21 @@
|
|
|
13
12
|
export const defaultPetStateConfig = { celebrateMs: 2400 };
|
|
14
13
|
/**
|
|
15
14
|
* Map one activity phase onto the animation contract.
|
|
16
|
-
* - thinking
|
|
17
|
-
*
|
|
15
|
+
* - thinking → `running` and tool → `running-right` (focused work).
|
|
16
|
+
* - review → `review` while answer text is streaming.
|
|
18
17
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
19
18
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
19
|
+
* - failed → `failed` until another activity event arrives.
|
|
20
20
|
* - idle → `idle` (calm breathing loop).
|
|
21
|
-
* `failed` has no DSH phase source yet; the machine keeps the mapping table
|
|
22
|
-
* so a future error event can light it up.
|
|
23
21
|
*/
|
|
24
22
|
export function animationForPhase(phase) {
|
|
25
23
|
switch (phase) {
|
|
26
24
|
case 'thinking': return 'running';
|
|
27
25
|
case 'tool': return 'running-right';
|
|
26
|
+
case 'review': return 'review';
|
|
28
27
|
case 'waiting': return 'waiting';
|
|
29
28
|
case 'done': return 'jumping';
|
|
29
|
+
case 'failed': return 'failed';
|
|
30
30
|
case 'idle': return 'idle';
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -61,7 +61,7 @@ export class PetStateMachine {
|
|
|
61
61
|
this.config = config;
|
|
62
62
|
this.now = now;
|
|
63
63
|
}
|
|
64
|
-
/** Consume one
|
|
64
|
+
/** Consume one projected activity update. */
|
|
65
65
|
onActivityStatus(input) {
|
|
66
66
|
this.phase = input.phase;
|
|
67
67
|
this.line = input.line;
|
|
@@ -94,7 +94,11 @@ export class PetStateMachine {
|
|
|
94
94
|
animation = 'idle';
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
const bubble = this.
|
|
97
|
+
const bubble = this.phase === 'done'
|
|
98
|
+
&& this.doneAt !== undefined
|
|
99
|
+
&& nowMs - this.doneAt >= this.config.celebrateMs
|
|
100
|
+
? undefined
|
|
101
|
+
: this.phrase ?? this.line;
|
|
98
102
|
return {
|
|
99
103
|
animation,
|
|
100
104
|
...(bubble === undefined ? {} : { bubble }),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
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.
|
|
4
|
+
"version": "0.1.15",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/affinity.ts
CHANGED
|
@@ -84,6 +84,39 @@ export function rankOf(points: number): (typeof AFFINITY_RANKS)[number] {
|
|
|
84
84
|
return rank
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Read-only affinity snapshot suited for the RPC view shape. */
|
|
88
|
+
export interface PetAffinityView {
|
|
89
|
+
points: number
|
|
90
|
+
rank: string
|
|
91
|
+
rankEmoji: string
|
|
92
|
+
pets: number
|
|
93
|
+
feeds: number
|
|
94
|
+
turns: number
|
|
95
|
+
/** True while the pet interaction is inside its cooldown. */
|
|
96
|
+
petCooldown: boolean
|
|
97
|
+
/** True while the feed is inside its cooldown. */
|
|
98
|
+
feedCooldown: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Derive the read-only view of one affinity state at a wall-clock instant. */
|
|
102
|
+
export function affinityViewOf(
|
|
103
|
+
state: AffinityState,
|
|
104
|
+
nowMs: number,
|
|
105
|
+
config: AffinityConfig = defaultAffinityConfig,
|
|
106
|
+
): PetAffinityView {
|
|
107
|
+
const rank = rankOf(state.points)
|
|
108
|
+
return {
|
|
109
|
+
points: state.points,
|
|
110
|
+
rank: rank.name,
|
|
111
|
+
rankEmoji: rank.emoji,
|
|
112
|
+
pets: state.pets,
|
|
113
|
+
feeds: state.feeds,
|
|
114
|
+
turns: state.turns,
|
|
115
|
+
petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
|
|
116
|
+
feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
function clamp(points: number): number {
|
|
88
121
|
return Math.min(AFFINITY_MAX, Math.max(0, points))
|
|
89
122
|
}
|