@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/src/service.ts
CHANGED
|
@@ -1,40 +1,45 @@
|
|
|
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
|
|
|
10
11
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
11
12
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
13
|
+
import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity.ts'
|
|
14
|
+
import type { TreatConfig } from './treats.ts'
|
|
12
15
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './affinity.ts'
|
|
16
|
+
emptyProjectionRuntime,
|
|
17
|
+
isActivityPhase,
|
|
18
|
+
projectOfficialEvent,
|
|
19
|
+
type ActivityStatusEventLike,
|
|
20
|
+
type ProjectionRuntime,
|
|
21
|
+
} from './event-projection.ts'
|
|
22
|
+
import { PetLedger, type LedgerConfig, type LedgerInteractionResult } from './ledger.ts'
|
|
21
23
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
savePetPersist,
|
|
24
|
+
DEFAULT_PET_NAME,
|
|
25
|
+
DISPLAY_INSET_MAX,
|
|
25
26
|
DISPLAY_SIZE_MAX,
|
|
26
27
|
DISPLAY_SIZE_MIN,
|
|
27
|
-
DISPLAY_INSET_MAX,
|
|
28
28
|
PET_NAME_MAX_LENGTH,
|
|
29
|
+
loadPetPersist,
|
|
30
|
+
petHomeDir,
|
|
31
|
+
savePetPersist,
|
|
29
32
|
type PetDisplayConfig,
|
|
30
33
|
type PetPersist,
|
|
31
34
|
} from './persist.ts'
|
|
32
35
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type
|
|
37
|
-
|
|
36
|
+
loadPetRegistry,
|
|
37
|
+
petEntryView,
|
|
38
|
+
petPackageRoot,
|
|
39
|
+
type PetDefinition,
|
|
40
|
+
type PetManifest,
|
|
41
|
+
type PetRegistry,
|
|
42
|
+
} from './registry.ts'
|
|
38
43
|
import {
|
|
39
44
|
defaultPetStateConfig,
|
|
40
45
|
PetStateMachine,
|
|
@@ -55,14 +60,22 @@ export interface PetConfig {
|
|
|
55
60
|
persistDir?: string
|
|
56
61
|
/** Master switch for the plugin (browser half + host routes). */
|
|
57
62
|
enabled?: boolean
|
|
63
|
+
/** Prebuilt registry (tests); defaults to scanning the package + user dirs. */
|
|
64
|
+
registry?: PetRegistry
|
|
65
|
+
/** Extra manifest entries composed by the embedding application. */
|
|
66
|
+
pets?: readonly PetManifest[]
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/**
|
|
61
|
-
* The pet's settings-namespace section: the
|
|
62
|
-
* settings surface edits. `right`/`bottom` are also updated by drag
|
|
70
|
+
* The pet's settings-namespace section: the pet selection and display fields
|
|
71
|
+
* the web settings surface edits. `right`/`bottom` are also updated by drag
|
|
63
72
|
* interactions, which keep the settings document in sync through the service.
|
|
73
|
+
* Naming is per pet and lives outside the settings document (the hover-panel
|
|
74
|
+
* rename targets the selected pet).
|
|
64
75
|
*/
|
|
65
76
|
export interface PetSettingsSection {
|
|
77
|
+
/** Selected pet id (a registry entry; the service clamps stale values). */
|
|
78
|
+
petId?: string
|
|
66
79
|
/** Master switch. */
|
|
67
80
|
visible: boolean
|
|
68
81
|
/** Scale of the rendered pet in px (sprite cell height). */
|
|
@@ -71,8 +84,6 @@ export interface PetSettingsSection {
|
|
|
71
84
|
right: number
|
|
72
85
|
/** Vertical inset from the viewport bottom edge, px. */
|
|
73
86
|
bottom: number
|
|
74
|
-
/** User-customizable pet display name. */
|
|
75
|
-
name: string
|
|
76
87
|
/** Master switch for the plugin (browser half + host routes). */
|
|
77
88
|
enabled?: boolean
|
|
78
89
|
}
|
|
@@ -87,21 +98,19 @@ export interface PetStateView {
|
|
|
87
98
|
phase: PetStateSnapshot['phase']
|
|
88
99
|
sessionActive: boolean
|
|
89
100
|
/** Affinity ledger snapshot. */
|
|
90
|
-
affinity:
|
|
91
|
-
points: number
|
|
92
|
-
rank: string
|
|
93
|
-
rankEmoji: string
|
|
94
|
-
pets: number
|
|
95
|
-
feeds: number
|
|
96
|
-
turns: number
|
|
97
|
-
/** True while the pet interaction is inside its cooldown. */
|
|
98
|
-
petCooldown: boolean
|
|
99
|
-
/** True while the feed is inside its cooldown. */
|
|
100
|
-
feedCooldown: boolean
|
|
101
|
-
}
|
|
101
|
+
affinity: PetAffinityView
|
|
102
102
|
/** Display configuration. */
|
|
103
103
|
display: PetDisplayConfig
|
|
104
|
-
/**
|
|
104
|
+
/** The selected pet's registry identity. */
|
|
105
|
+
pet: {
|
|
106
|
+
/** Registry id. */
|
|
107
|
+
id: string
|
|
108
|
+
/** Manifest display name (unrenamed default). */
|
|
109
|
+
displayName: string
|
|
110
|
+
/** Manifest description. */
|
|
111
|
+
description: string
|
|
112
|
+
}
|
|
113
|
+
/** The selected pet's display name (user rename or manifest default). */
|
|
105
114
|
name: string
|
|
106
115
|
/** Treat (小鱼干) stock snapshot. */
|
|
107
116
|
treats: {
|
|
@@ -113,14 +122,7 @@ export interface PetStateView {
|
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
/** Result of `pet.interact`. */
|
|
116
|
-
export
|
|
117
|
-
/** Reaction copy bubble. */
|
|
118
|
-
reaction: string
|
|
119
|
-
/** Points gained (0 when inside the cooldown). */
|
|
120
|
-
delta: number
|
|
121
|
-
/** Full affinity snapshot (same shape as state view). */
|
|
122
|
-
affinity: PetStateView['affinity']
|
|
123
|
-
}
|
|
125
|
+
export type PetInteractResult = LedgerInteractionResult
|
|
124
126
|
|
|
125
127
|
declare module '@deepseek-ai/cordis' {
|
|
126
128
|
interface Context {
|
|
@@ -128,152 +130,48 @@ declare module '@deepseek-ai/cordis' {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
/** Runtime shape of the optional legacy activity event. */
|
|
132
|
-
interface ActivityStatusEventLike {
|
|
133
|
-
phase?: string
|
|
134
|
-
line?: string
|
|
135
|
-
phrase?: string
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Per-session facts needed to project the official event stream. */
|
|
139
|
-
interface SessionActivityRuntime {
|
|
140
|
-
activeTools: Set<string>
|
|
141
|
-
officialEventsSeen: boolean
|
|
142
|
-
stepHadFailure: boolean
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** One official event projection, optionally carrying a completed turn reward. */
|
|
146
|
-
interface PetActivityTransition {
|
|
147
|
-
input: PetStateInput
|
|
148
|
-
completedTurn?: number
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Keep tool names readable inside the compact status bubble. */
|
|
152
|
-
function displayToolName(name: string): string {
|
|
153
|
-
const compact = name.replace(/\s+/g, ' ').trim() || '工具'
|
|
154
|
-
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
158
|
-
function isActivityPhase(phase: string): phase is PetStateInput['phase'] {
|
|
159
|
-
return ['idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed'].includes(phase)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
164
|
-
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
165
|
-
*/
|
|
166
|
-
function projectOfficialEvent(
|
|
167
|
-
event: SessionEvent,
|
|
168
|
-
runtime: SessionActivityRuntime,
|
|
169
|
-
): PetActivityTransition | undefined {
|
|
170
|
-
switch (event.type) {
|
|
171
|
-
case 'turn/start':
|
|
172
|
-
runtime.activeTools.clear()
|
|
173
|
-
runtime.stepHadFailure = false
|
|
174
|
-
return { input: { phase: 'waiting', line: '准备开始' } }
|
|
175
|
-
case 'step/start':
|
|
176
|
-
runtime.activeTools.clear()
|
|
177
|
-
runtime.stepHadFailure = false
|
|
178
|
-
return { input: { phase: 'waiting', line: '等待模型响应' } }
|
|
179
|
-
case 'assistant/chunk': {
|
|
180
|
-
const { chunk } = event.data
|
|
181
|
-
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
182
|
-
return { input: { phase: 'thinking', line: '正在思考' } }
|
|
183
|
-
}
|
|
184
|
-
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
185
|
-
return { input: { phase: 'review', line: '整理回复中' } }
|
|
186
|
-
}
|
|
187
|
-
return undefined
|
|
188
|
-
}
|
|
189
|
-
case 'assistant/message':
|
|
190
|
-
return { input: { phase: 'review', line: '整理回复中' } }
|
|
191
|
-
case 'tool/call':
|
|
192
|
-
runtime.activeTools.add(String(event.data.callId))
|
|
193
|
-
return {
|
|
194
|
-
input: {
|
|
195
|
-
phase: 'tool',
|
|
196
|
-
line: `正在使用 ${displayToolName(event.data.name)}`,
|
|
197
|
-
},
|
|
198
|
-
}
|
|
199
|
-
case 'tool/result': {
|
|
200
|
-
const block = event.data.message.content[0]
|
|
201
|
-
runtime.activeTools.delete(String(event.data.message.source.callId))
|
|
202
|
-
runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true
|
|
203
|
-
if (runtime.activeTools.size > 0) {
|
|
204
|
-
return {
|
|
205
|
-
input: {
|
|
206
|
-
phase: 'tool',
|
|
207
|
-
line: `还有 ${runtime.activeTools.size} 个工具运行中`,
|
|
208
|
-
},
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return runtime.stepHadFailure
|
|
212
|
-
? { input: { phase: 'failed', line: '工具执行失败' } }
|
|
213
|
-
: { input: { phase: 'thinking', line: '处理工具结果' } }
|
|
214
|
-
}
|
|
215
|
-
case 'turn/end': {
|
|
216
|
-
runtime.activeTools.clear()
|
|
217
|
-
switch (event.data.reason.kind) {
|
|
218
|
-
case 'completed':
|
|
219
|
-
return {
|
|
220
|
-
input: { phase: 'done', line: '完成啦' },
|
|
221
|
-
completedTurn: event.data.turn,
|
|
222
|
-
}
|
|
223
|
-
case 'error':
|
|
224
|
-
return { input: { phase: 'failed', line: '执行失败' } }
|
|
225
|
-
case 'max-tokens':
|
|
226
|
-
return { input: { phase: 'failed', line: '达到输出上限' } }
|
|
227
|
-
case 'interrupted':
|
|
228
|
-
return { input: { phase: 'failed', line: '执行意外中断' } }
|
|
229
|
-
case 'blocked':
|
|
230
|
-
return { input: { phase: 'waiting', line: '等待继续' } }
|
|
231
|
-
case 'aborted':
|
|
232
|
-
return { input: { phase: 'idle', line: '已停止' } }
|
|
233
|
-
default:
|
|
234
|
-
// TurnEndReasonMap is merge-extensible; a newer ending must not
|
|
235
|
-
// leave the pet showing stale in-progress work.
|
|
236
|
-
return { input: { phase: 'idle' } }
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
default:
|
|
240
|
-
return undefined
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
133
|
/**
|
|
245
134
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
246
|
-
* written until
|
|
247
|
-
* in-memory state, and persistence happens on
|
|
248
|
-
*
|
|
135
|
+
* written until an economic event or interaction arrives; event listeners
|
|
136
|
+
* update only in-memory state, and persistence happens on economic changes
|
|
137
|
+
* (turn rewards, feeds, config/name changes) — never on a read.
|
|
249
138
|
*/
|
|
250
139
|
export class PetService extends Service {
|
|
251
140
|
static inject: string[] = []
|
|
252
141
|
|
|
253
142
|
private readonly machine: PetStateMachine
|
|
254
|
-
private readonly
|
|
255
|
-
private readonly
|
|
143
|
+
private readonly ledger: PetLedger
|
|
144
|
+
private readonly registry: PetRegistry
|
|
256
145
|
private readonly persistDir: string
|
|
257
|
-
private persist: PetPersist
|
|
258
|
-
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
259
|
-
private rewardedTurns = new Map<string, number>()
|
|
260
146
|
private enabled: boolean
|
|
261
147
|
private disposeActivity: (() => void) | undefined
|
|
262
148
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
263
149
|
private displaySession: Session | undefined
|
|
264
|
-
private readonly sessionActivity = new WeakMap<Session,
|
|
265
|
-
private lastLegacyTurnRewardAt = 0
|
|
150
|
+
private readonly sessionActivity = new WeakMap<Session, ProjectionRuntime>()
|
|
266
151
|
|
|
267
152
|
constructor(ctx: Context, config: PetConfig = {}) {
|
|
268
153
|
super(ctx, 'pet')
|
|
269
154
|
this.persistDir = config.persistDir ?? petHomeDir()
|
|
270
|
-
this.
|
|
271
|
-
|
|
155
|
+
this.registry = config.registry
|
|
156
|
+
?? loadPetRegistry({
|
|
157
|
+
packageRoot: petPackageRoot(import.meta.url),
|
|
158
|
+
...(config.pets === undefined ? {} : { extra: config.pets }),
|
|
159
|
+
})
|
|
160
|
+
if (this.registry.entries.length === 0) {
|
|
161
|
+
throw new Error('[dsh-pet] no valid pet manifests found; nothing to render')
|
|
162
|
+
}
|
|
163
|
+
let persist = loadPetPersist(this.persistDir)
|
|
164
|
+
if (this.registry.byId(persist.petId) === undefined) {
|
|
165
|
+
// The selected pet no longer exists (removed or a fresh install with a
|
|
166
|
+
// copied pet.json): fall back to the registry default.
|
|
167
|
+
persist = { ...persist, petId: this.registry.defaultEntry().id }
|
|
168
|
+
}
|
|
169
|
+
const ledgerConfig: LedgerConfig = { affinity: config.affinity, treats: config.treats }
|
|
170
|
+
this.ledger = new PetLedger(persist, ledgerConfig)
|
|
272
171
|
this.machine = new PetStateMachine({
|
|
273
172
|
...defaultPetStateConfig,
|
|
274
173
|
...(config.state ?? {}),
|
|
275
174
|
})
|
|
276
|
-
this.persist = loadPetPersist(this.persistDir)
|
|
277
175
|
this.enabled = config.enabled ?? true
|
|
278
176
|
|
|
279
177
|
this.syncActivity()
|
|
@@ -291,12 +189,44 @@ export class PetService extends Service {
|
|
|
291
189
|
|
|
292
190
|
/** Current persisted display config (read-only view). */
|
|
293
191
|
display(): PetDisplayConfig {
|
|
294
|
-
return { ...this.
|
|
192
|
+
return { ...this.ledger.snapshot.display }
|
|
295
193
|
}
|
|
296
194
|
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
return this.
|
|
195
|
+
/** RPC: the registry entries the browser half renders and selects from. */
|
|
196
|
+
async pets(): Promise<PetDefinition[]> {
|
|
197
|
+
return this.registry.entries.map(petEntryView)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The loaded registry (the asset routes serve its entries). */
|
|
201
|
+
registrySnapshot(): PetRegistry {
|
|
202
|
+
return this.registry
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The selected pet's registry entry. */
|
|
206
|
+
activeEntry(): NonNullable<PetRegistry['entries'][number]> {
|
|
207
|
+
return this.registry.byId(this.selectedPetId()) ?? this.registry.defaultEntry()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Currently selected pet id (persisted). */
|
|
211
|
+
selectedPetId(): string {
|
|
212
|
+
return this.ledger.snapshot.petId
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The display name of one pet (user rename or manifest displayName). */
|
|
216
|
+
petName(petId: string = this.selectedPetId()): string {
|
|
217
|
+
const stored = this.ledger.snapshot.names[petId]
|
|
218
|
+
if (stored !== undefined && stored.trim() !== '') return stored
|
|
219
|
+
return this.registry.byId(petId)?.displayName ?? DEFAULT_PET_NAME
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** RPC: switch the selected pet (persisted, settings document mirrored). */
|
|
223
|
+
async setPetId(petId: string): Promise<{ ok: true; petId: string } | { ok: false; error: string }> {
|
|
224
|
+
const entry = this.registry.byId(petId)
|
|
225
|
+
if (entry === undefined) return { ok: false, error: 'unknown-pet' }
|
|
226
|
+
this.ledger.setPetId(entry.id)
|
|
227
|
+
this.flush()
|
|
228
|
+
this.syncSettingsFromPet()
|
|
229
|
+
return { ok: true, petId: entry.id }
|
|
300
230
|
}
|
|
301
231
|
|
|
302
232
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
@@ -354,14 +284,10 @@ export class PetService extends Service {
|
|
|
354
284
|
}
|
|
355
285
|
|
|
356
286
|
/** Return the projection state associated with one live session. */
|
|
357
|
-
private activityRuntime(session: Session):
|
|
287
|
+
private activityRuntime(session: Session): ProjectionRuntime {
|
|
358
288
|
let runtime = this.sessionActivity.get(session)
|
|
359
289
|
if (runtime === undefined) {
|
|
360
|
-
runtime =
|
|
361
|
-
activeTools: new Set(),
|
|
362
|
-
officialEventsSeen: false,
|
|
363
|
-
stepHadFailure: false,
|
|
364
|
-
}
|
|
290
|
+
runtime = emptyProjectionRuntime()
|
|
365
291
|
this.sessionActivity.set(session, runtime)
|
|
366
292
|
}
|
|
367
293
|
return runtime
|
|
@@ -377,79 +303,61 @@ export class PetService extends Service {
|
|
|
377
303
|
/** RPC: pet or feed the pet. */
|
|
378
304
|
async interact(kind: PetInteraction): Promise<PetInteractResult> {
|
|
379
305
|
const nowMs = Date.now()
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// treat for nothing.
|
|
384
|
-
if (kind === 'feed') this.settleTreats(nowMs)
|
|
385
|
-
const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig)
|
|
386
|
-
if (kind === 'feed' && !outcome.accepted) {
|
|
387
|
-
return { reaction: outcome.reaction, delta: 0, affinity: this.affinityView(this.persist.affinity) }
|
|
388
|
-
}
|
|
389
|
-
if (kind === 'feed') {
|
|
390
|
-
const consume = consumeTreat(this.persist.treats)
|
|
391
|
-
if (!consume.ok) {
|
|
392
|
-
const affinity = this.affinityView(this.persist.affinity)
|
|
393
|
-
return {
|
|
394
|
-
reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
|
|
395
|
-
delta: 0,
|
|
396
|
-
affinity,
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
this.persist = { ...this.persist, treats: consume.ledger }
|
|
400
|
-
}
|
|
401
|
-
if (outcome.accepted) {
|
|
402
|
-
this.persist = { ...this.persist, affinity: outcome.affinity }
|
|
403
|
-
this.flush()
|
|
404
|
-
}
|
|
405
|
-
const affinity = this.affinityView(outcome.affinity)
|
|
406
|
-
return { reaction: outcome.reaction, delta: outcome.delta, affinity }
|
|
306
|
+
const result = this.ledger.interact(kind, nowMs)
|
|
307
|
+
if (this.ledger.takeDirty()) this.flush()
|
|
308
|
+
return result
|
|
407
309
|
}
|
|
408
310
|
|
|
409
311
|
/** RPC: show or hide the pet. */
|
|
410
312
|
async setVisible(visible: boolean): Promise<{ ok: true; display: PetDisplayConfig }> {
|
|
411
|
-
this.
|
|
313
|
+
this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible })
|
|
412
314
|
this.flush()
|
|
413
315
|
this.syncSettingsFromPet()
|
|
414
|
-
return { ok: true, display: this.
|
|
316
|
+
return { ok: true, display: this.ledger.snapshot.display }
|
|
415
317
|
}
|
|
416
318
|
|
|
417
319
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
418
320
|
async setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }> {
|
|
419
|
-
const next = { ...this.
|
|
321
|
+
const next = { ...this.ledger.snapshot.display, ...patch }
|
|
420
322
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)))
|
|
421
323
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)))
|
|
422
324
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)))
|
|
423
|
-
this.
|
|
325
|
+
this.ledger.setDisplay(next)
|
|
424
326
|
this.flush()
|
|
425
327
|
this.syncSettingsFromPet()
|
|
426
|
-
return { ok: true, display: this.
|
|
328
|
+
return { ok: true, display: this.ledger.snapshot.display }
|
|
427
329
|
}
|
|
428
330
|
|
|
429
|
-
/** RPC: rename the pet (trimmed, 1–20 chars). */
|
|
331
|
+
/** RPC: rename the selected pet (trimmed, 1–20 chars, per-pet storage). */
|
|
430
332
|
async setName(name: string): Promise<{ ok: true; name: string } | { ok: false; error: string }> {
|
|
431
333
|
const trimmed = name.trim()
|
|
432
334
|
if (trimmed === '') return { ok: false, error: 'name-empty' }
|
|
433
335
|
if (trimmed.length > PET_NAME_MAX_LENGTH) return { ok: false, error: 'name-too-long' }
|
|
434
|
-
this.
|
|
336
|
+
this.ledger.setPetName(this.selectedPetId(), trimmed)
|
|
435
337
|
this.flush()
|
|
436
|
-
this.syncSettingsFromPet()
|
|
437
338
|
return { ok: true, name: trimmed }
|
|
438
339
|
}
|
|
439
340
|
|
|
440
341
|
/**
|
|
441
|
-
* Apply a committed settings section to the persisted
|
|
442
|
-
* by the settings surface on every change; values are
|
|
443
|
-
* the setConfig RPC so both write paths converge.
|
|
342
|
+
* Apply a committed settings section to the persisted selection and display
|
|
343
|
+
* config. Called by the settings surface on every change; values are
|
|
344
|
+
* clamped exactly like the setConfig RPC so both write paths converge.
|
|
444
345
|
* @param section - the resolved settings section.
|
|
445
346
|
*/
|
|
446
347
|
applySettingsSection(section: PetSettingsSection): void {
|
|
447
|
-
|
|
348
|
+
if (typeof section.petId === 'string' && this.registry.byId(section.petId) !== undefined) {
|
|
349
|
+
this.ledger.setPetId(section.petId)
|
|
350
|
+
} else if (section.petId !== undefined) {
|
|
351
|
+
// The stored selection names a pet the registry no longer has: keep the
|
|
352
|
+
// current selection and repair the settings document.
|
|
353
|
+
this.syncSettingsFromPet()
|
|
354
|
+
}
|
|
355
|
+
const next = { ...this.ledger.snapshot.display }
|
|
448
356
|
next.visible = section.visible && (section.enabled ?? true)
|
|
449
357
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)))
|
|
450
358
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)))
|
|
451
359
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)))
|
|
452
|
-
this.
|
|
360
|
+
this.ledger.setDisplay(next)
|
|
453
361
|
this.flush()
|
|
454
362
|
}
|
|
455
363
|
|
|
@@ -457,12 +365,13 @@ export class PetService extends Service {
|
|
|
457
365
|
private syncSettingsFromPet(): void {
|
|
458
366
|
const settings = this.ctx.get('settings', false) as { update(ns: string, patch: object): Promise<void> } | undefined
|
|
459
367
|
if (settings === undefined) return
|
|
368
|
+
const snapshot = this.ledger.snapshot
|
|
460
369
|
void settings.update(PET_SETTINGS_NAMESPACE, {
|
|
461
|
-
visible:
|
|
462
|
-
size:
|
|
463
|
-
right:
|
|
464
|
-
bottom:
|
|
465
|
-
|
|
370
|
+
visible: snapshot.display.visible,
|
|
371
|
+
size: snapshot.display.size,
|
|
372
|
+
right: snapshot.display.right,
|
|
373
|
+
bottom: snapshot.display.bottom,
|
|
374
|
+
petId: snapshot.petId,
|
|
466
375
|
}).catch(() => {
|
|
467
376
|
// A settings write failure must not break the pet's own persistence.
|
|
468
377
|
})
|
|
@@ -470,83 +379,42 @@ export class PetService extends Service {
|
|
|
470
379
|
|
|
471
380
|
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
472
381
|
private rewardTurn(sessionId: string, turn: number): void {
|
|
473
|
-
|
|
474
|
-
if (turn <= last) return
|
|
475
|
-
this.rewardedTurns.set(sessionId, turn)
|
|
476
|
-
this.applyTurnReward()
|
|
382
|
+
if (this.ledger.rewardTurn(sessionId, turn, Date.now())) this.flush()
|
|
477
383
|
}
|
|
478
384
|
|
|
479
385
|
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
480
386
|
private rewardLegacyTurn(): void {
|
|
481
|
-
|
|
482
|
-
// A legacy `done` snapshot may repeat during the celebration window.
|
|
483
|
-
if (nowMs - this.lastLegacyTurnRewardAt < 5_000) return
|
|
484
|
-
this.lastLegacyTurnRewardAt = nowMs
|
|
485
|
-
this.applyTurnReward()
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Persist one accepted completed-turn reward. */
|
|
489
|
-
private applyTurnReward(): void {
|
|
490
|
-
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) }
|
|
491
|
-
this.flush()
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Settle the treat economy (work + time output since the last settlement)
|
|
496
|
-
* and persist whenever the ledger changed. A zero-gain first settlement
|
|
497
|
-
* still starts the time clock (anchor write), which is what lets the
|
|
498
|
-
* 30-minute time output ever accrue.
|
|
499
|
-
*/
|
|
500
|
-
private settleTreats(nowMs: number): void {
|
|
501
|
-
const settlement = settleTreatGrants(
|
|
502
|
-
this.persist.treats,
|
|
503
|
-
this.persist.affinity.turns,
|
|
504
|
-
nowMs,
|
|
505
|
-
this.treatConfig,
|
|
506
|
-
)
|
|
507
|
-
if (settlement.ledger !== this.persist.treats) {
|
|
508
|
-
this.persist = { ...this.persist, treats: settlement.ledger }
|
|
509
|
-
this.flush()
|
|
510
|
-
}
|
|
387
|
+
if (this.ledger.rewardLegacyTurn(Date.now())) this.flush()
|
|
511
388
|
}
|
|
512
389
|
|
|
513
390
|
private view(): PetStateView {
|
|
514
391
|
const snapshot = this.machine.render()
|
|
515
|
-
|
|
516
|
-
|
|
392
|
+
const entry = this.activeEntry()
|
|
393
|
+
// Read-only: the ledger settles on economic events only, never on a read,
|
|
394
|
+
// so polling the state cannot trigger pet.json writes.
|
|
517
395
|
return {
|
|
518
396
|
animation: snapshot.animation,
|
|
519
397
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
520
398
|
phase: snapshot.phase,
|
|
521
399
|
sessionActive: snapshot.sessionActive,
|
|
522
|
-
affinity: this.affinityView(
|
|
523
|
-
display: { ...this.
|
|
524
|
-
|
|
400
|
+
affinity: this.ledger.affinityView(Date.now()),
|
|
401
|
+
display: { ...this.ledger.snapshot.display },
|
|
402
|
+
pet: {
|
|
403
|
+
id: entry.id,
|
|
404
|
+
displayName: entry.displayName,
|
|
405
|
+
description: entry.description,
|
|
406
|
+
},
|
|
407
|
+
name: this.petName(),
|
|
525
408
|
treats: {
|
|
526
|
-
stocked: this.
|
|
527
|
-
max: this.
|
|
409
|
+
stocked: this.ledger.snapshot.treats.treats,
|
|
410
|
+
max: this.ledger.treatMax,
|
|
528
411
|
},
|
|
529
412
|
}
|
|
530
413
|
}
|
|
531
414
|
|
|
532
|
-
private affinityView(affinity: AffinityState): PetStateView['affinity'] {
|
|
533
|
-
const nowMs = Date.now()
|
|
534
|
-
const rank = rankOf(affinity.points)
|
|
535
|
-
return {
|
|
536
|
-
points: affinity.points,
|
|
537
|
-
rank: rank.name,
|
|
538
|
-
rankEmoji: rank.emoji,
|
|
539
|
-
pets: affinity.pets,
|
|
540
|
-
feeds: affinity.feeds,
|
|
541
|
-
turns: affinity.turns,
|
|
542
|
-
petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
|
|
543
|
-
feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs,
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
415
|
private flush(): void {
|
|
548
416
|
try {
|
|
549
|
-
savePetPersist(this.
|
|
417
|
+
savePetPersist(this.ledger.snapshot, this.persistDir)
|
|
550
418
|
} catch {
|
|
551
419
|
// Persistence is best-effort; the in-memory ledger keeps working.
|
|
552
420
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WhalePet.d.ts","sourceRoot":"","sources":["../../../src/client/WhalePet.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAqC,WAAW,EAAE,MAAM,OAAO,CAAA;AAE3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,gCAAgC,CAAA;AAEhE,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,wBAAwB,CAAA;AAErD,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,kCAAkC;IAClC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,8CAA8C;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAOD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,WAAW,CAyT1D"}
|