@linxin666/dsh-pet 0.1.14 → 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/lib/client.js +29 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +355 -251
- 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/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/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 +15 -46
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +47 -204
- package/package.json +1 -1
- package/src/affinity.ts +33 -0
- package/src/client/PluginSettingsCard.tsx +83 -17
- package/src/client/index.ts +3 -3
- 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 +60 -274
package/lib/index.js
CHANGED
|
@@ -1,12 +1,132 @@
|
|
|
1
|
-
import { a as AFFINITY_MAX, c as
|
|
1
|
+
import { a as AFFINITY_MAX, c as applyInteraction, d as emptyAffinity, f as rankOf, i as rowOf, l as applyTurnReward, n as animationForPhase, o as AFFINITY_RANKS, r as defaultPetStateConfig, s as affinityViewOf, t as PetStateMachine, u as defaultAffinityConfig } from "./state-CFyJv0sQ.js";
|
|
2
2
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import z from "schemastery";
|
|
4
4
|
import { Service } from "@deepseek-ai/cordis";
|
|
5
5
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { isAbsolute, join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { readFile } from "node:fs/promises";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
//#region src/event-projection.ts
|
|
11
|
+
/** Fresh projection runtime for a newly seen session. */
|
|
12
|
+
function emptyProjectionRuntime() {
|
|
13
|
+
return {
|
|
14
|
+
activeTools: /* @__PURE__ */ new Set(),
|
|
15
|
+
officialEventsSeen: false,
|
|
16
|
+
stepHadFailure: false
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Keep tool names readable inside the compact status bubble. */
|
|
20
|
+
function displayToolName(name) {
|
|
21
|
+
const compact = name.replace(/\s+/g, " ").trim() || "工具";
|
|
22
|
+
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
|
|
23
|
+
}
|
|
24
|
+
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
25
|
+
function isActivityPhase(phase) {
|
|
26
|
+
return [
|
|
27
|
+
"idle",
|
|
28
|
+
"waiting",
|
|
29
|
+
"thinking",
|
|
30
|
+
"tool",
|
|
31
|
+
"review",
|
|
32
|
+
"done",
|
|
33
|
+
"failed"
|
|
34
|
+
].includes(phase);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
38
|
+
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
39
|
+
*/
|
|
40
|
+
function projectOfficialEvent(event, runtime) {
|
|
41
|
+
switch (event.type) {
|
|
42
|
+
case "turn/start":
|
|
43
|
+
runtime.activeTools.clear();
|
|
44
|
+
runtime.stepHadFailure = false;
|
|
45
|
+
return { input: {
|
|
46
|
+
phase: "waiting",
|
|
47
|
+
line: "准备开始"
|
|
48
|
+
} };
|
|
49
|
+
case "step/start":
|
|
50
|
+
runtime.activeTools.clear();
|
|
51
|
+
runtime.stepHadFailure = false;
|
|
52
|
+
return { input: {
|
|
53
|
+
phase: "waiting",
|
|
54
|
+
line: "等待模型响应"
|
|
55
|
+
} };
|
|
56
|
+
case "assistant/chunk": {
|
|
57
|
+
const { chunk } = event.data;
|
|
58
|
+
if (chunk.type === "reasoning-delta" && chunk.text.length > 0) return { input: {
|
|
59
|
+
phase: "thinking",
|
|
60
|
+
line: "正在思考"
|
|
61
|
+
} };
|
|
62
|
+
if (chunk.type === "text-delta" && chunk.text.length > 0) return { input: {
|
|
63
|
+
phase: "review",
|
|
64
|
+
line: "整理回复中"
|
|
65
|
+
} };
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
case "assistant/message": return { input: {
|
|
69
|
+
phase: "review",
|
|
70
|
+
line: "整理回复中"
|
|
71
|
+
} };
|
|
72
|
+
case "tool/call":
|
|
73
|
+
runtime.activeTools.add(String(event.data.callId));
|
|
74
|
+
return { input: {
|
|
75
|
+
phase: "tool",
|
|
76
|
+
line: `正在使用 ${displayToolName(event.data.name)}`
|
|
77
|
+
} };
|
|
78
|
+
case "tool/result": {
|
|
79
|
+
const block = event.data.message.content[0];
|
|
80
|
+
runtime.activeTools.delete(String(event.data.message.source.callId));
|
|
81
|
+
runtime.stepHadFailure ||= event.data.error !== void 0 || block.isError === true;
|
|
82
|
+
if (runtime.activeTools.size > 0) return { input: {
|
|
83
|
+
phase: "tool",
|
|
84
|
+
line: `还有 ${runtime.activeTools.size} 个工具运行中`
|
|
85
|
+
} };
|
|
86
|
+
return runtime.stepHadFailure ? { input: {
|
|
87
|
+
phase: "failed",
|
|
88
|
+
line: "工具执行失败"
|
|
89
|
+
} } : { input: {
|
|
90
|
+
phase: "thinking",
|
|
91
|
+
line: "处理工具结果"
|
|
92
|
+
} };
|
|
93
|
+
}
|
|
94
|
+
case "turn/end":
|
|
95
|
+
runtime.activeTools.clear();
|
|
96
|
+
switch (event.data.reason.kind) {
|
|
97
|
+
case "completed": return {
|
|
98
|
+
input: {
|
|
99
|
+
phase: "done",
|
|
100
|
+
line: "完成啦"
|
|
101
|
+
},
|
|
102
|
+
completedTurn: event.data.turn
|
|
103
|
+
};
|
|
104
|
+
case "error": return { input: {
|
|
105
|
+
phase: "failed",
|
|
106
|
+
line: "执行失败"
|
|
107
|
+
} };
|
|
108
|
+
case "max-tokens": return { input: {
|
|
109
|
+
phase: "failed",
|
|
110
|
+
line: "达到输出上限"
|
|
111
|
+
} };
|
|
112
|
+
case "interrupted": return { input: {
|
|
113
|
+
phase: "failed",
|
|
114
|
+
line: "执行意外中断"
|
|
115
|
+
} };
|
|
116
|
+
case "blocked": return { input: {
|
|
117
|
+
phase: "waiting",
|
|
118
|
+
line: "等待继续"
|
|
119
|
+
} };
|
|
120
|
+
case "aborted": return { input: {
|
|
121
|
+
phase: "idle",
|
|
122
|
+
line: "已停止"
|
|
123
|
+
} };
|
|
124
|
+
default: return { input: { phase: "idle" } };
|
|
125
|
+
}
|
|
126
|
+
default: return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
10
130
|
//#region src/treats.ts
|
|
11
131
|
const defaultTreatConfig = {
|
|
12
132
|
turnsPerTreat: 3,
|
|
@@ -80,6 +200,193 @@ function consumeTreat(ledger) {
|
|
|
80
200
|
};
|
|
81
201
|
}
|
|
82
202
|
//#endregion
|
|
203
|
+
//#region src/ledger.ts
|
|
204
|
+
/**
|
|
205
|
+
* Pet affinity economy (ledger) — composes the pure affinity and treats
|
|
206
|
+
* modules with the cooldown/dedup bookkeeping and emits updated persistence
|
|
207
|
+
* snapshots, marking dirty so the owning facade decides when to flush. Read
|
|
208
|
+
* paths (view) no longer settle the economy; settlements happen on explicit
|
|
209
|
+
* economic events: completed-turn rewards (official or legacy) and feeds.
|
|
210
|
+
* @module @linxin666/dsh-pet/ledger
|
|
211
|
+
*/
|
|
212
|
+
/**
|
|
213
|
+
* Holds the current persistence snapshot and all economy bookkeeping. Every
|
|
214
|
+
* mutating call flags takeDirty so the facade persists exactly once per
|
|
215
|
+
* batch of changes; read methods (snapshot, affinityView) never write.
|
|
216
|
+
*/
|
|
217
|
+
var PetLedger = class {
|
|
218
|
+
affinityConfig;
|
|
219
|
+
treatConfig;
|
|
220
|
+
current;
|
|
221
|
+
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
222
|
+
rewardedTurns = /* @__PURE__ */ new Map();
|
|
223
|
+
lastLegacyTurnRewardAt = 0;
|
|
224
|
+
dirty = false;
|
|
225
|
+
constructor(persist, config = {}) {
|
|
226
|
+
this.affinityConfig = {
|
|
227
|
+
...defaultAffinityConfig,
|
|
228
|
+
...config.affinity ?? {}
|
|
229
|
+
};
|
|
230
|
+
this.treatConfig = {
|
|
231
|
+
...defaultTreatConfig,
|
|
232
|
+
...config.treats ?? {}
|
|
233
|
+
};
|
|
234
|
+
this.current = persist;
|
|
235
|
+
}
|
|
236
|
+
/** Affinity cooldown/rank tuning (read-only). */
|
|
237
|
+
get affinity() {
|
|
238
|
+
return this.affinityConfig;
|
|
239
|
+
}
|
|
240
|
+
/** The current persistence snapshot (trade a copy when mutating). */
|
|
241
|
+
get snapshot() {
|
|
242
|
+
return this.current;
|
|
243
|
+
}
|
|
244
|
+
/** Stock cap reported to clients. */
|
|
245
|
+
get treatMax() {
|
|
246
|
+
return this.treatConfig.maxTreats;
|
|
247
|
+
}
|
|
248
|
+
/** Consume the pending-write flag if any mutation occurred. */
|
|
249
|
+
takeDirty() {
|
|
250
|
+
const was = this.dirty;
|
|
251
|
+
this.dirty = false;
|
|
252
|
+
return was;
|
|
253
|
+
}
|
|
254
|
+
/** Replace the display block (clamping stays a caller concern). */
|
|
255
|
+
setDisplay(display) {
|
|
256
|
+
this.current = {
|
|
257
|
+
...this.current,
|
|
258
|
+
display
|
|
259
|
+
};
|
|
260
|
+
this.dirty = true;
|
|
261
|
+
}
|
|
262
|
+
/** Replace the pet display name (validation stays a caller concern). */
|
|
263
|
+
setName(name) {
|
|
264
|
+
this.current = {
|
|
265
|
+
...this.current,
|
|
266
|
+
name
|
|
267
|
+
};
|
|
268
|
+
this.dirty = true;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Settle the treat economy (work + time output since the last settlement).
|
|
272
|
+
* A zero-gain first settlement still starts the time clock (anchor write),
|
|
273
|
+
* which is how the 30-minute time output can ever accrue. Returns true when
|
|
274
|
+
* the in-memory ledger changed and should be persisted.
|
|
275
|
+
*/
|
|
276
|
+
settleTreats(nowMs) {
|
|
277
|
+
const settlement = settleTreatGrants(this.current.treats, this.current.affinity.turns, nowMs, this.treatConfig);
|
|
278
|
+
if (settlement.ledger === this.current.treats) return false;
|
|
279
|
+
this.current = {
|
|
280
|
+
...this.current,
|
|
281
|
+
treats: settlement.ledger
|
|
282
|
+
};
|
|
283
|
+
this.dirty = true;
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Award the completed-turn reward once per session+turn (idempotent) and
|
|
288
|
+
* run the treat settlement that work output feeds. Returns true when the
|
|
289
|
+
* snapshot changed.
|
|
290
|
+
*/
|
|
291
|
+
rewardTurn(sessionId, turn, nowMs) {
|
|
292
|
+
if (turn <= (this.rewardedTurns.get(sessionId) ?? 0)) return false;
|
|
293
|
+
this.rewardedTurns.set(sessionId, turn);
|
|
294
|
+
let changed = this.applyTurnReward();
|
|
295
|
+
if (this.settleTreats(nowMs)) changed = true;
|
|
296
|
+
return changed;
|
|
297
|
+
}
|
|
298
|
+
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
299
|
+
rewardLegacyTurn(nowMs) {
|
|
300
|
+
if (nowMs - this.lastLegacyTurnRewardAt < 5e3) return false;
|
|
301
|
+
this.lastLegacyTurnRewardAt = nowMs;
|
|
302
|
+
let changed = this.applyTurnReward();
|
|
303
|
+
if (this.settleTreats(nowMs)) changed = true;
|
|
304
|
+
return changed;
|
|
305
|
+
}
|
|
306
|
+
applyTurnReward() {
|
|
307
|
+
this.current = {
|
|
308
|
+
...this.current,
|
|
309
|
+
affinity: applyTurnReward(this.current.affinity, this.affinityConfig)
|
|
310
|
+
};
|
|
311
|
+
this.dirty = true;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Pet or feed the pet. Feeding settles first, then gates on the feed
|
|
316
|
+
* cooldown before spending stock — a feed inside the cooldown must not burn
|
|
317
|
+
* a treat for nothing.
|
|
318
|
+
*/
|
|
319
|
+
interact(kind, nowMs) {
|
|
320
|
+
if (kind === "feed") this.settleTreats(nowMs);
|
|
321
|
+
const outcome = applyInteraction(this.current.affinity, kind, nowMs, this.affinityConfig);
|
|
322
|
+
if (kind === "feed" && !outcome.accepted) return {
|
|
323
|
+
reaction: outcome.reaction,
|
|
324
|
+
delta: 0,
|
|
325
|
+
affinity: this.affinityView(nowMs)
|
|
326
|
+
};
|
|
327
|
+
if (kind === "feed") {
|
|
328
|
+
const consume = consumeTreat(this.current.treats);
|
|
329
|
+
if (!consume.ok) return {
|
|
330
|
+
reaction: "没有小鱼干了,多陪鲸鱼娘工作一会儿吧~",
|
|
331
|
+
delta: 0,
|
|
332
|
+
affinity: this.affinityView(nowMs)
|
|
333
|
+
};
|
|
334
|
+
this.current = {
|
|
335
|
+
...this.current,
|
|
336
|
+
treats: consume.ledger
|
|
337
|
+
};
|
|
338
|
+
this.dirty = true;
|
|
339
|
+
}
|
|
340
|
+
if (outcome.accepted) {
|
|
341
|
+
this.current = {
|
|
342
|
+
...this.current,
|
|
343
|
+
affinity: outcome.affinity
|
|
344
|
+
};
|
|
345
|
+
this.dirty = true;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
reaction: outcome.reaction,
|
|
349
|
+
delta: outcome.delta,
|
|
350
|
+
affinity: this.affinityView(nowMs)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** Current affinity view for the RPC snapshot. */
|
|
354
|
+
affinityView(nowMs) {
|
|
355
|
+
return affinityViewOf(this.current.affinity, nowMs, this.affinityConfig);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/dsh-home.ts
|
|
360
|
+
/**
|
|
361
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
362
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
363
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
364
|
+
*/
|
|
365
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
366
|
+
function expandHome(path, home = homedir()) {
|
|
367
|
+
if (path === "~") return home;
|
|
368
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
369
|
+
return path;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Resolve the DSH home directory.
|
|
373
|
+
* @param env - process environment to read DSH_HOME from.
|
|
374
|
+
* @param home - platform home directory fallback (test seam).
|
|
375
|
+
* @returns the absolute DSH home path.
|
|
376
|
+
*/
|
|
377
|
+
function resolveDshHome(env = process.env, home = homedir()) {
|
|
378
|
+
const raw = env.DSH_HOME;
|
|
379
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
380
|
+
const expanded = expandHome(raw.trim(), home);
|
|
381
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
|
|
382
|
+
}
|
|
383
|
+
return join(home, ".dsh");
|
|
384
|
+
}
|
|
385
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
386
|
+
function dshHome() {
|
|
387
|
+
return resolveDshHome();
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
83
390
|
//#region src/persist.ts
|
|
84
391
|
/**
|
|
85
392
|
* Pet persistence — tiny JSON store for affinity + display config, written
|
|
@@ -104,9 +411,13 @@ function emptyPersist() {
|
|
|
104
411
|
display: { ...defaultDisplayConfig }
|
|
105
412
|
};
|
|
106
413
|
}
|
|
107
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Resolve the persistence directory ($DSH_HOME or ~/.dsh). Delegates to the
|
|
416
|
+
* shared {@link dshHome} resolution so the plugin family keeps one DSH_HOME
|
|
417
|
+
* definition (env override, ~ expansion, cwd-joined relative values).
|
|
418
|
+
*/
|
|
108
419
|
function petHomeDir() {
|
|
109
|
-
return
|
|
420
|
+
return dshHome();
|
|
110
421
|
}
|
|
111
422
|
/** Numeric field guard: finite numbers only, else the fallback. */
|
|
112
423
|
function finiteNum(value, fallback) {
|
|
@@ -162,153 +473,34 @@ function savePetPersist(data, dir = petHomeDir()) {
|
|
|
162
473
|
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
163
474
|
renameSync(tmp, target);
|
|
164
475
|
}
|
|
165
|
-
/** Keep tool names readable inside the compact status bubble. */
|
|
166
|
-
function displayToolName(name) {
|
|
167
|
-
const compact = name.replace(/\s+/g, " ").trim() || "工具";
|
|
168
|
-
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
|
|
169
|
-
}
|
|
170
|
-
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
171
|
-
function isActivityPhase(phase) {
|
|
172
|
-
return [
|
|
173
|
-
"idle",
|
|
174
|
-
"waiting",
|
|
175
|
-
"thinking",
|
|
176
|
-
"tool",
|
|
177
|
-
"review",
|
|
178
|
-
"done",
|
|
179
|
-
"failed"
|
|
180
|
-
].includes(phase);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
184
|
-
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
185
|
-
*/
|
|
186
|
-
function projectOfficialEvent(event, runtime) {
|
|
187
|
-
switch (event.type) {
|
|
188
|
-
case "turn/start":
|
|
189
|
-
runtime.activeTools.clear();
|
|
190
|
-
runtime.stepHadFailure = false;
|
|
191
|
-
return { input: {
|
|
192
|
-
phase: "waiting",
|
|
193
|
-
line: "准备开始"
|
|
194
|
-
} };
|
|
195
|
-
case "step/start":
|
|
196
|
-
runtime.activeTools.clear();
|
|
197
|
-
runtime.stepHadFailure = false;
|
|
198
|
-
return { input: {
|
|
199
|
-
phase: "waiting",
|
|
200
|
-
line: "等待模型响应"
|
|
201
|
-
} };
|
|
202
|
-
case "assistant/chunk": {
|
|
203
|
-
const { chunk } = event.data;
|
|
204
|
-
if (chunk.type === "reasoning-delta" && chunk.text.length > 0) return { input: {
|
|
205
|
-
phase: "thinking",
|
|
206
|
-
line: "正在思考"
|
|
207
|
-
} };
|
|
208
|
-
if (chunk.type === "text-delta" && chunk.text.length > 0) return { input: {
|
|
209
|
-
phase: "review",
|
|
210
|
-
line: "整理回复中"
|
|
211
|
-
} };
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
case "assistant/message": return { input: {
|
|
215
|
-
phase: "review",
|
|
216
|
-
line: "整理回复中"
|
|
217
|
-
} };
|
|
218
|
-
case "tool/call":
|
|
219
|
-
runtime.activeTools.add(String(event.data.callId));
|
|
220
|
-
return { input: {
|
|
221
|
-
phase: "tool",
|
|
222
|
-
line: `正在使用 ${displayToolName(event.data.name)}`
|
|
223
|
-
} };
|
|
224
|
-
case "tool/result": {
|
|
225
|
-
const block = event.data.message.content[0];
|
|
226
|
-
runtime.activeTools.delete(String(event.data.message.source.callId));
|
|
227
|
-
runtime.stepHadFailure ||= event.data.error !== void 0 || block.isError === true;
|
|
228
|
-
if (runtime.activeTools.size > 0) return { input: {
|
|
229
|
-
phase: "tool",
|
|
230
|
-
line: `还有 ${runtime.activeTools.size} 个工具运行中`
|
|
231
|
-
} };
|
|
232
|
-
return runtime.stepHadFailure ? { input: {
|
|
233
|
-
phase: "failed",
|
|
234
|
-
line: "工具执行失败"
|
|
235
|
-
} } : { input: {
|
|
236
|
-
phase: "thinking",
|
|
237
|
-
line: "处理工具结果"
|
|
238
|
-
} };
|
|
239
|
-
}
|
|
240
|
-
case "turn/end":
|
|
241
|
-
runtime.activeTools.clear();
|
|
242
|
-
switch (event.data.reason.kind) {
|
|
243
|
-
case "completed": return {
|
|
244
|
-
input: {
|
|
245
|
-
phase: "done",
|
|
246
|
-
line: "完成啦"
|
|
247
|
-
},
|
|
248
|
-
completedTurn: event.data.turn
|
|
249
|
-
};
|
|
250
|
-
case "error": return { input: {
|
|
251
|
-
phase: "failed",
|
|
252
|
-
line: "执行失败"
|
|
253
|
-
} };
|
|
254
|
-
case "max-tokens": return { input: {
|
|
255
|
-
phase: "failed",
|
|
256
|
-
line: "达到输出上限"
|
|
257
|
-
} };
|
|
258
|
-
case "interrupted": return { input: {
|
|
259
|
-
phase: "failed",
|
|
260
|
-
line: "执行意外中断"
|
|
261
|
-
} };
|
|
262
|
-
case "blocked": return { input: {
|
|
263
|
-
phase: "waiting",
|
|
264
|
-
line: "等待继续"
|
|
265
|
-
} };
|
|
266
|
-
case "aborted": return { input: {
|
|
267
|
-
phase: "idle",
|
|
268
|
-
line: "已停止"
|
|
269
|
-
} };
|
|
270
|
-
default: return { input: { phase: "idle" } };
|
|
271
|
-
}
|
|
272
|
-
default: return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
476
|
/**
|
|
276
477
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
277
|
-
* written until
|
|
278
|
-
* in-memory state, and persistence happens on
|
|
279
|
-
*
|
|
478
|
+
* written until an economic event or interaction arrives; event listeners
|
|
479
|
+
* update only in-memory state, and persistence happens on economic changes
|
|
480
|
+
* (turn rewards, feeds, config/name changes) — never on a read.
|
|
280
481
|
*/
|
|
281
482
|
var PetService = class extends Service {
|
|
282
483
|
static inject = [];
|
|
283
484
|
machine;
|
|
284
|
-
|
|
285
|
-
treatConfig;
|
|
485
|
+
ledger;
|
|
286
486
|
persistDir;
|
|
287
|
-
persist;
|
|
288
|
-
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
289
|
-
rewardedTurns = /* @__PURE__ */ new Map();
|
|
290
487
|
enabled;
|
|
291
488
|
disposeActivity;
|
|
292
489
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
293
490
|
displaySession;
|
|
294
491
|
sessionActivity = /* @__PURE__ */ new WeakMap();
|
|
295
|
-
lastLegacyTurnRewardAt = 0;
|
|
296
492
|
constructor(ctx, config = {}) {
|
|
297
493
|
super(ctx, "pet");
|
|
298
494
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
};
|
|
303
|
-
this.treatConfig = {
|
|
304
|
-
...defaultTreatConfig,
|
|
305
|
-
...config.treats ?? {}
|
|
495
|
+
const ledgerConfig = {
|
|
496
|
+
affinity: config.affinity,
|
|
497
|
+
treats: config.treats
|
|
306
498
|
};
|
|
499
|
+
this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
|
|
307
500
|
this.machine = new PetStateMachine({
|
|
308
501
|
...defaultPetStateConfig,
|
|
309
502
|
...config.state ?? {}
|
|
310
503
|
});
|
|
311
|
-
this.persist = loadPetPersist(this.persistDir);
|
|
312
504
|
this.enabled = config.enabled ?? true;
|
|
313
505
|
this.syncActivity();
|
|
314
506
|
}
|
|
@@ -322,11 +514,11 @@ var PetService = class extends Service {
|
|
|
322
514
|
}
|
|
323
515
|
/** Current persisted display config (read-only view). */
|
|
324
516
|
display() {
|
|
325
|
-
return { ...this.
|
|
517
|
+
return { ...this.ledger.snapshot.display };
|
|
326
518
|
}
|
|
327
519
|
/** Current persisted pet name (read-only view). */
|
|
328
520
|
petName() {
|
|
329
|
-
return this.
|
|
521
|
+
return this.ledger.snapshot.name;
|
|
330
522
|
}
|
|
331
523
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
332
524
|
setEnabled(enabled) {
|
|
@@ -372,11 +564,7 @@ var PetService = class extends Service {
|
|
|
372
564
|
activityRuntime(session) {
|
|
373
565
|
let runtime = this.sessionActivity.get(session);
|
|
374
566
|
if (runtime === void 0) {
|
|
375
|
-
runtime =
|
|
376
|
-
activeTools: /* @__PURE__ */ new Set(),
|
|
377
|
-
officialEventsSeen: false,
|
|
378
|
-
stepHadFailure: false
|
|
379
|
-
};
|
|
567
|
+
runtime = emptyProjectionRuntime();
|
|
380
568
|
this.sessionActivity.set(session, runtime);
|
|
381
569
|
}
|
|
382
570
|
return runtime;
|
|
@@ -390,73 +578,38 @@ var PetService = class extends Service {
|
|
|
390
578
|
/** RPC: pet or feed the pet. */
|
|
391
579
|
async interact(kind) {
|
|
392
580
|
const nowMs = Date.now();
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
reaction: outcome.reaction,
|
|
397
|
-
delta: 0,
|
|
398
|
-
affinity: this.affinityView(this.persist.affinity)
|
|
399
|
-
};
|
|
400
|
-
if (kind === "feed") {
|
|
401
|
-
const consume = consumeTreat(this.persist.treats);
|
|
402
|
-
if (!consume.ok) return {
|
|
403
|
-
reaction: "没有小鱼干了,多陪鲸鱼娘工作一会儿吧~",
|
|
404
|
-
delta: 0,
|
|
405
|
-
affinity: this.affinityView(this.persist.affinity)
|
|
406
|
-
};
|
|
407
|
-
this.persist = {
|
|
408
|
-
...this.persist,
|
|
409
|
-
treats: consume.ledger
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
if (outcome.accepted) {
|
|
413
|
-
this.persist = {
|
|
414
|
-
...this.persist,
|
|
415
|
-
affinity: outcome.affinity
|
|
416
|
-
};
|
|
417
|
-
this.flush();
|
|
418
|
-
}
|
|
419
|
-
const affinity = this.affinityView(outcome.affinity);
|
|
420
|
-
return {
|
|
421
|
-
reaction: outcome.reaction,
|
|
422
|
-
delta: outcome.delta,
|
|
423
|
-
affinity
|
|
424
|
-
};
|
|
581
|
+
const result = this.ledger.interact(kind, nowMs);
|
|
582
|
+
if (this.ledger.takeDirty()) this.flush();
|
|
583
|
+
return result;
|
|
425
584
|
}
|
|
426
585
|
/** RPC: show or hide the pet. */
|
|
427
586
|
async setVisible(visible) {
|
|
428
|
-
this.
|
|
429
|
-
...this.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
visible
|
|
433
|
-
}
|
|
434
|
-
};
|
|
587
|
+
this.ledger.setDisplay({
|
|
588
|
+
...this.ledger.snapshot.display,
|
|
589
|
+
visible
|
|
590
|
+
});
|
|
435
591
|
this.flush();
|
|
436
592
|
this.syncSettingsFromPet();
|
|
437
593
|
return {
|
|
438
594
|
ok: true,
|
|
439
|
-
display: this.
|
|
595
|
+
display: this.ledger.snapshot.display
|
|
440
596
|
};
|
|
441
597
|
}
|
|
442
598
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
443
599
|
async setConfig(patch) {
|
|
444
600
|
const next = {
|
|
445
|
-
...this.
|
|
601
|
+
...this.ledger.snapshot.display,
|
|
446
602
|
...patch
|
|
447
603
|
};
|
|
448
604
|
next.size = Math.round(Math.min(512, Math.max(32, next.size)));
|
|
449
605
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
|
|
450
606
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
|
|
451
|
-
this.
|
|
452
|
-
...this.persist,
|
|
453
|
-
display: next
|
|
454
|
-
};
|
|
607
|
+
this.ledger.setDisplay(next);
|
|
455
608
|
this.flush();
|
|
456
609
|
this.syncSettingsFromPet();
|
|
457
610
|
return {
|
|
458
611
|
ok: true,
|
|
459
|
-
display: this.
|
|
612
|
+
display: this.ledger.snapshot.display
|
|
460
613
|
};
|
|
461
614
|
}
|
|
462
615
|
/** RPC: rename the pet (trimmed, 1–20 chars). */
|
|
@@ -470,10 +623,7 @@ var PetService = class extends Service {
|
|
|
470
623
|
ok: false,
|
|
471
624
|
error: "name-too-long"
|
|
472
625
|
};
|
|
473
|
-
this.
|
|
474
|
-
...this.persist,
|
|
475
|
-
name: trimmed
|
|
476
|
-
};
|
|
626
|
+
this.ledger.setName(trimmed);
|
|
477
627
|
this.flush();
|
|
478
628
|
this.syncSettingsFromPet();
|
|
479
629
|
return {
|
|
@@ -488,101 +638,55 @@ var PetService = class extends Service {
|
|
|
488
638
|
* @param section - the resolved settings section.
|
|
489
639
|
*/
|
|
490
640
|
applySettingsSection(section) {
|
|
491
|
-
const next = { ...this.
|
|
641
|
+
const next = { ...this.ledger.snapshot.display };
|
|
492
642
|
next.visible = section.visible && (section.enabled ?? true);
|
|
493
643
|
next.size = Math.round(Math.min(512, Math.max(32, section.size)));
|
|
494
644
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
|
|
495
645
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
|
|
496
|
-
this.
|
|
497
|
-
|
|
498
|
-
display: next,
|
|
499
|
-
name: section.name.trim()
|
|
500
|
-
};
|
|
646
|
+
this.ledger.setDisplay(next);
|
|
647
|
+
this.ledger.setName(section.name.trim());
|
|
501
648
|
this.flush();
|
|
502
649
|
}
|
|
503
650
|
/** Mirror the persisted display config into the settings document (best-effort). */
|
|
504
651
|
syncSettingsFromPet() {
|
|
505
652
|
const settings = this.ctx.get("settings", false);
|
|
506
653
|
if (settings === void 0) return;
|
|
654
|
+
const snapshot = this.ledger.snapshot;
|
|
507
655
|
settings.update("pet", {
|
|
508
|
-
visible:
|
|
509
|
-
size:
|
|
510
|
-
right:
|
|
511
|
-
bottom:
|
|
512
|
-
name:
|
|
656
|
+
visible: snapshot.display.visible,
|
|
657
|
+
size: snapshot.display.size,
|
|
658
|
+
right: snapshot.display.right,
|
|
659
|
+
bottom: snapshot.display.bottom,
|
|
660
|
+
name: snapshot.name
|
|
513
661
|
}).catch(() => {});
|
|
514
662
|
}
|
|
515
663
|
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
516
664
|
rewardTurn(sessionId, turn) {
|
|
517
|
-
if (
|
|
518
|
-
this.rewardedTurns.set(sessionId, turn);
|
|
519
|
-
this.applyTurnReward();
|
|
665
|
+
if (this.ledger.rewardTurn(sessionId, turn, Date.now())) this.flush();
|
|
520
666
|
}
|
|
521
667
|
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
522
668
|
rewardLegacyTurn() {
|
|
523
|
-
|
|
524
|
-
if (nowMs - this.lastLegacyTurnRewardAt < 5e3) return;
|
|
525
|
-
this.lastLegacyTurnRewardAt = nowMs;
|
|
526
|
-
this.applyTurnReward();
|
|
527
|
-
}
|
|
528
|
-
/** Persist one accepted completed-turn reward. */
|
|
529
|
-
applyTurnReward() {
|
|
530
|
-
this.persist = {
|
|
531
|
-
...this.persist,
|
|
532
|
-
affinity: applyTurnReward(this.persist.affinity, this.affinityConfig)
|
|
533
|
-
};
|
|
534
|
-
this.flush();
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Settle the treat economy (work + time output since the last settlement)
|
|
538
|
-
* and persist whenever the ledger changed. A zero-gain first settlement
|
|
539
|
-
* still starts the time clock (anchor write), which is what lets the
|
|
540
|
-
* 30-minute time output ever accrue.
|
|
541
|
-
*/
|
|
542
|
-
settleTreats(nowMs) {
|
|
543
|
-
const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
|
|
544
|
-
if (settlement.ledger !== this.persist.treats) {
|
|
545
|
-
this.persist = {
|
|
546
|
-
...this.persist,
|
|
547
|
-
treats: settlement.ledger
|
|
548
|
-
};
|
|
549
|
-
this.flush();
|
|
550
|
-
}
|
|
669
|
+
if (this.ledger.rewardLegacyTurn(Date.now())) this.flush();
|
|
551
670
|
}
|
|
552
671
|
view() {
|
|
553
672
|
const snapshot = this.machine.render();
|
|
554
|
-
this.settleTreats(Date.now());
|
|
555
673
|
return {
|
|
556
674
|
animation: snapshot.animation,
|
|
557
675
|
...snapshot.bubble === void 0 ? {} : { bubble: snapshot.bubble },
|
|
558
676
|
phase: snapshot.phase,
|
|
559
677
|
sessionActive: snapshot.sessionActive,
|
|
560
|
-
affinity: this.affinityView(
|
|
561
|
-
display: { ...this.
|
|
562
|
-
name: this.
|
|
678
|
+
affinity: this.ledger.affinityView(Date.now()),
|
|
679
|
+
display: { ...this.ledger.snapshot.display },
|
|
680
|
+
name: this.ledger.snapshot.name,
|
|
563
681
|
treats: {
|
|
564
|
-
stocked: this.
|
|
565
|
-
max: this.
|
|
682
|
+
stocked: this.ledger.snapshot.treats.treats,
|
|
683
|
+
max: this.ledger.treatMax
|
|
566
684
|
}
|
|
567
685
|
};
|
|
568
686
|
}
|
|
569
|
-
affinityView(affinity) {
|
|
570
|
-
const nowMs = Date.now();
|
|
571
|
-
const rank = rankOf(affinity.points);
|
|
572
|
-
return {
|
|
573
|
-
points: affinity.points,
|
|
574
|
-
rank: rank.name,
|
|
575
|
-
rankEmoji: rank.emoji,
|
|
576
|
-
pets: affinity.pets,
|
|
577
|
-
feeds: affinity.feeds,
|
|
578
|
-
turns: affinity.turns,
|
|
579
|
-
petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
|
|
580
|
-
feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
687
|
flush() {
|
|
584
688
|
try {
|
|
585
|
-
savePetPersist(this.
|
|
689
|
+
savePetPersist(this.ledger.snapshot, this.persistDir);
|
|
586
690
|
} catch {}
|
|
587
691
|
}
|
|
588
692
|
};
|