@linxin666/dsh-pet 0.1.20 → 0.2.1
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 +18 -11
- package/README.zh.md +18 -11
- package/assets/whale/pet.json +9 -0
- package/lib/client.js +394 -58
- package/lib/client.js.map +1 -1
- package/lib/index.js +1007 -43
- package/lib/invariant.js +1 -1
- package/lib/{state-Diiq8vjk.js → state-DrMX22GL.js} +86 -28
- package/lib/types/affinity.d.ts +4 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +32 -6
- package/lib/types/chatter.d.ts +111 -0
- package/lib/types/chatter.d.ts.map +1 -0
- package/lib/types/chatter.js +756 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +1 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +133 -16
- package/lib/types/client/PluginSettingsCard.d.ts +28 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +147 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +1 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/sequences.d.ts +10 -0
- package/lib/types/client/sequences.d.ts.map +1 -0
- package/lib/types/client/sequences.js +20 -0
- package/lib/types/event-projection.d.ts +14 -1
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +38 -17
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +15 -7
- package/lib/types/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +3 -1
- package/lib/types/registry.d.ts +7 -1
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +47 -3
- package/lib/types/remarks.d.ts +2 -0
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/remarks.js +35 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +14 -0
- package/lib/types/service.d.ts +6 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +15 -2
- package/lib/types/state.d.ts +7 -4
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +20 -20
- package/package.json +9 -9
- package/src/affinity.test.ts +4 -2
- package/src/affinity.ts +37 -6
- package/src/chatter.test.ts +154 -0
- package/src/chatter.ts +787 -0
- package/src/client/PetDockEntry.test.tsx +93 -0
- package/src/client/PetDockEntry.tsx +1 -0
- package/src/client/PetSprite.test.tsx +253 -3
- package/src/client/PetSprite.tsx +192 -37
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/index.test.tsx +95 -0
- package/src/client/index.ts +1 -0
- package/src/client/locales.ts +4 -0
- package/src/client/pet-css.test.ts +54 -0
- package/src/client/pet.module.css +215 -57
- package/src/client/sequences.test.ts +33 -0
- package/src/client/sequences.ts +33 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/event-projection.ts +49 -16
- package/src/ledger.test.ts +42 -1
- package/src/ledger.ts +15 -7
- package/src/loopback.ts +63 -0
- package/src/persist.test.ts +18 -1
- package/src/persist.ts +3 -1
- package/src/registry.test.ts +79 -4
- package/src/registry.ts +58 -6
- package/src/remarks.ts +36 -0
- package/src/routes.ts +11 -0
- package/src/service.ts +27 -2
- package/src/state.test.ts +13 -0
- package/src/state.ts +24 -18
package/lib/types/routes.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { petEntryView } from './registry.js';
|
|
15
|
+
import { isLoopbackRequest } from './loopback.js';
|
|
15
16
|
/** Browser-facing base path of the pet API. */
|
|
16
17
|
export const PET_API_PREFIX = '/api/pet';
|
|
17
18
|
/** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
|
|
@@ -75,12 +76,21 @@ function readJsonBody(req) {
|
|
|
75
76
|
req.on('error', reject);
|
|
76
77
|
});
|
|
77
78
|
}
|
|
79
|
+
/** Shared route fence: the browser UI is a loopback client; LAN hosts stay out. */
|
|
80
|
+
function guard(req, res) {
|
|
81
|
+
if (isLoopbackRequest(req))
|
|
82
|
+
return true;
|
|
83
|
+
json(res, 403, { ok: false, error: 'forbidden: loopback-only' });
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
78
86
|
/** Wrap one async service call as a GET JSON route. */
|
|
79
87
|
function getRoute(path, run) {
|
|
80
88
|
return {
|
|
81
89
|
kind: 'exact',
|
|
82
90
|
path,
|
|
83
91
|
handler: (req, res) => {
|
|
92
|
+
if (!guard(req, res))
|
|
93
|
+
return;
|
|
84
94
|
if (!requireMethod(req, res, 'GET'))
|
|
85
95
|
return;
|
|
86
96
|
run().then((value) => json(res, 200, value), (error) => {
|
|
@@ -95,6 +105,8 @@ function postRoute(path, run) {
|
|
|
95
105
|
kind: 'exact',
|
|
96
106
|
path,
|
|
97
107
|
handler: (req, res) => {
|
|
108
|
+
if (!guard(req, res))
|
|
109
|
+
return Promise.resolve();
|
|
98
110
|
if (!requireMethod(req, res, 'POST'))
|
|
99
111
|
return Promise.resolve();
|
|
100
112
|
return readJsonBody(req).then((body) => {
|
|
@@ -127,6 +139,8 @@ function dirAliases(registry) {
|
|
|
127
139
|
function assetHandler(registry) {
|
|
128
140
|
const aliases = dirAliases(registry);
|
|
129
141
|
return (req, res) => {
|
|
142
|
+
if (!guard(req, res))
|
|
143
|
+
return;
|
|
130
144
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
131
145
|
res.writeHead(405);
|
|
132
146
|
res.end();
|
package/lib/types/service.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface PetStateView {
|
|
|
113
113
|
/** Stock cap. */
|
|
114
114
|
max: number;
|
|
115
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* The display session's fresh inner whisper (碎碎念), when one is within
|
|
118
|
+
* its TTL — short inner-voice copy woken by the model's own output,
|
|
119
|
+
* rendered by the client as a distinct whisper bubble.
|
|
120
|
+
*/
|
|
121
|
+
whisper?: string;
|
|
116
122
|
}
|
|
117
123
|
/** Result of `pet.interact`. */
|
|
118
124
|
export type PetInteractResult = LedgerInteractionResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,OAAO,EAAgC,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EASL,KAAK,gBAAgB,EAEtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -16,6 +16,7 @@ import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from '
|
|
|
16
16
|
import { PetLedger } from './ledger.js';
|
|
17
17
|
import { DEFAULT_PET_NAME, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from './persist.js';
|
|
18
18
|
import { loadPetRegistry, petEntryView, petPackageRoot, } from './registry.js';
|
|
19
|
+
import { WHISPER_TTL_MS } from './chatter.js';
|
|
19
20
|
import { defaultPetStateConfig, PetStateMachine, } from './state.js';
|
|
20
21
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
21
22
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
@@ -159,7 +160,7 @@ export class PetService extends Service {
|
|
|
159
160
|
if (transition === undefined)
|
|
160
161
|
return;
|
|
161
162
|
runtime.officialEventsSeen = true;
|
|
162
|
-
this.applyActivity(session, transition.input);
|
|
163
|
+
this.applyActivity(session, transition.input, transition.whisper);
|
|
163
164
|
if (transition.completedTurn !== undefined) {
|
|
164
165
|
this.rewardTurn(String(session.id), transition.completedTurn);
|
|
165
166
|
}
|
|
@@ -206,9 +207,11 @@ export class PetService extends Service {
|
|
|
206
207
|
* the session becomes the host-global display session (most recent
|
|
207
208
|
* meaningful event wins the sprite animation).
|
|
208
209
|
*/
|
|
209
|
-
applyActivity(session, input) {
|
|
210
|
+
applyActivity(session, input, whisper) {
|
|
210
211
|
const activity = this.activityOf(session);
|
|
211
212
|
activity.lastInput = input;
|
|
213
|
+
if (whisper !== undefined)
|
|
214
|
+
activity.whisper = { text: whisper, at: Date.now() };
|
|
212
215
|
activity.machine.onActivityStatus(input);
|
|
213
216
|
activity.machine.onSessionActive();
|
|
214
217
|
// Move to the tail so map order reads most-recent-last, then trim the
|
|
@@ -339,6 +342,15 @@ export class PetService extends Service {
|
|
|
339
342
|
phase: perSession.phase,
|
|
340
343
|
});
|
|
341
344
|
}
|
|
345
|
+
// The display session's inner whisper rides the global view while fresh;
|
|
346
|
+
// an expired whisper simply stops appearing (the client's 2s poll drops it).
|
|
347
|
+
const displayActivity = this.displaySession === undefined
|
|
348
|
+
? undefined
|
|
349
|
+
: this.sessionActivity.get(this.displaySession);
|
|
350
|
+
const whisper = displayActivity?.whisper;
|
|
351
|
+
const freshWhisper = whisper !== undefined && Date.now() - whisper.at < WHISPER_TTL_MS
|
|
352
|
+
? whisper.text
|
|
353
|
+
: undefined;
|
|
342
354
|
// Read-only: the ledger settles on economic events only, never on a read,
|
|
343
355
|
// so polling the state cannot trigger pet.json writes.
|
|
344
356
|
return {
|
|
@@ -347,6 +359,7 @@ export class PetService extends Service {
|
|
|
347
359
|
phase: snapshot.phase,
|
|
348
360
|
sessionActive: snapshot.sessionActive,
|
|
349
361
|
sessions,
|
|
362
|
+
...(freshWhisper === undefined ? {} : { whisper: freshWhisper }),
|
|
350
363
|
affinity: this.ledger.affinityView(Date.now()),
|
|
351
364
|
display: { ...this.ledger.snapshot.display },
|
|
352
365
|
pet: {
|
package/lib/types/state.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface PetStateSnapshot {
|
|
|
39
39
|
export interface PetStateConfig {
|
|
40
40
|
/** Celebration window after `done` before settling to idle, ms (default 2400). */
|
|
41
41
|
celebrateMs: number;
|
|
42
|
+
/** Failure display window before settling to idle, ms (default 2400). */
|
|
43
|
+
failureMs: number;
|
|
42
44
|
}
|
|
43
45
|
export declare const defaultPetStateConfig: PetStateConfig;
|
|
44
46
|
/**
|
|
@@ -47,7 +49,7 @@ export declare const defaultPetStateConfig: PetStateConfig;
|
|
|
47
49
|
* - review → `review` while answer text is streaming.
|
|
48
50
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
49
51
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
50
|
-
* - failed → `failed`
|
|
52
|
+
* - failed → `failed` briefly, then back to `idle`.
|
|
51
53
|
* - idle → `idle` (calm breathing loop).
|
|
52
54
|
*/
|
|
53
55
|
export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
|
|
@@ -55,17 +57,18 @@ export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
|
|
|
55
57
|
export declare function rowOf(animation: PetAnimation): number;
|
|
56
58
|
/**
|
|
57
59
|
* PetStateMachine — one instance per host process. Holds only the latest
|
|
58
|
-
* input snapshot and
|
|
60
|
+
* input snapshot and terminal-state timing; no storage, no side effects.
|
|
59
61
|
*/
|
|
60
62
|
export declare class PetStateMachine {
|
|
61
|
-
private readonly config;
|
|
62
63
|
private readonly now;
|
|
63
64
|
private phase;
|
|
64
65
|
private line;
|
|
65
66
|
private phrase;
|
|
66
67
|
private sessionActive;
|
|
67
68
|
private doneAt;
|
|
68
|
-
|
|
69
|
+
private failedAt;
|
|
70
|
+
private readonly config;
|
|
71
|
+
constructor(config?: Partial<PetStateConfig>, now?: () => number);
|
|
69
72
|
/** Consume one projected activity update. */
|
|
70
73
|
onActivityStatus(input: PetStateInput): void;
|
|
71
74
|
/** A session became the active one (or a fresh session started). */
|
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;;;;;;;;;;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;
|
|
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;IACnB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAuD,CAAA;AAE3F;;;;;;;;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;IAWxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,YACE,MAAM,GAAE,OAAO,CAAC,cAAc,CAAyB,EACtC,GAAG,GAAE,MAAM,MAAiB,EAG9C;IAED,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAM3C;IAED,oEAAoE;IACpE,eAAe,IAAI,IAAI,CAEtB;IAED,sDAAsD;IACtD,iBAAiB,IAAI,IAAI,CAOxB;IAED,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB,CAqBzB;CACF"}
|
package/lib/types/state.js
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* function of (input, nowMs); persistence and RPC live in the service.
|
|
10
10
|
* @module @linxin666/dsh-pet/state
|
|
11
11
|
*/
|
|
12
|
-
export const defaultPetStateConfig = { celebrateMs: 2400 };
|
|
12
|
+
export const defaultPetStateConfig = { celebrateMs: 2400, failureMs: 2400 };
|
|
13
13
|
/**
|
|
14
14
|
* Map one activity phase onto the animation contract.
|
|
15
15
|
* - thinking → `running` and tool → `running-right` (focused work).
|
|
16
16
|
* - review → `review` while answer text is streaming.
|
|
17
17
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
18
18
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
19
|
-
* - failed → `failed`
|
|
19
|
+
* - failed → `failed` briefly, then back to `idle`.
|
|
20
20
|
* - idle → `idle` (calm breathing loop).
|
|
21
21
|
*/
|
|
22
22
|
export function animationForPhase(phase) {
|
|
@@ -47,27 +47,28 @@ export function rowOf(animation) {
|
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
49
|
* PetStateMachine — one instance per host process. Holds only the latest
|
|
50
|
-
* input snapshot and
|
|
50
|
+
* input snapshot and terminal-state timing; no storage, no side effects.
|
|
51
51
|
*/
|
|
52
52
|
export class PetStateMachine {
|
|
53
|
-
config;
|
|
54
53
|
now;
|
|
55
54
|
phase = 'idle';
|
|
56
55
|
line;
|
|
57
56
|
phrase;
|
|
58
57
|
sessionActive = false;
|
|
59
58
|
doneAt;
|
|
59
|
+
failedAt;
|
|
60
|
+
config;
|
|
60
61
|
constructor(config = defaultPetStateConfig, now = Date.now) {
|
|
61
|
-
this.config = config;
|
|
62
62
|
this.now = now;
|
|
63
|
+
this.config = { ...defaultPetStateConfig, ...config };
|
|
63
64
|
}
|
|
64
65
|
/** Consume one projected activity update. */
|
|
65
66
|
onActivityStatus(input) {
|
|
66
67
|
this.phase = input.phase;
|
|
67
68
|
this.line = input.line;
|
|
68
69
|
this.phrase = input.phrase;
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
this.doneAt = input.phase === 'done' ? this.now() : undefined;
|
|
71
|
+
this.failedAt = input.phase === 'failed' ? this.now() : undefined;
|
|
71
72
|
}
|
|
72
73
|
/** A session became the active one (or a fresh session started). */
|
|
73
74
|
onSessionActive() {
|
|
@@ -80,24 +81,23 @@ export class PetStateMachine {
|
|
|
80
81
|
this.line = undefined;
|
|
81
82
|
this.phrase = undefined;
|
|
82
83
|
this.doneAt = undefined;
|
|
84
|
+
this.failedAt = undefined;
|
|
83
85
|
}
|
|
84
86
|
/** Render the current animation decision. */
|
|
85
87
|
render() {
|
|
86
88
|
const nowMs = this.now();
|
|
87
89
|
let animation = animationForPhase(this.phase);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
const settled = this.phase === 'idle'
|
|
100
|
-
|| (this.phase === 'done' && this.doneAt !== undefined && nowMs - this.doneAt >= this.config.celebrateMs);
|
|
90
|
+
const doneSettled = this.phase === 'done'
|
|
91
|
+
&& this.doneAt !== undefined
|
|
92
|
+
&& nowMs - this.doneAt >= this.config.celebrateMs;
|
|
93
|
+
const failedSettled = this.phase === 'failed'
|
|
94
|
+
&& this.failedAt !== undefined
|
|
95
|
+
&& nowMs - this.failedAt >= this.config.failureMs;
|
|
96
|
+
if (doneSettled || failedSettled)
|
|
97
|
+
animation = 'idle';
|
|
98
|
+
// Settled sessions never bubble: idle (e.g. an aborted/stopped turn),
|
|
99
|
+
// completed celebration expiry, and failed display expiry all fall silent.
|
|
100
|
+
const settled = this.phase === 'idle' || doneSettled || failedSettled;
|
|
101
101
|
const bubble = settled ? undefined : this.phrase ?? this.line;
|
|
102
102
|
return {
|
|
103
103
|
animation,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
62
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
61
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
62
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
69
69
|
"@testing-library/dom": "^10.4.1",
|
|
70
70
|
"@testing-library/react": "^16.3.2",
|
|
71
71
|
"@types/node": "^22.20.0",
|
package/src/affinity.test.ts
CHANGED
|
@@ -26,8 +26,9 @@ describe('applyInteraction', () => {
|
|
|
26
26
|
const second = applyInteraction(first.affinity, 'pet', now + defaultAffinityConfig.petCooldownMs - 1)
|
|
27
27
|
expect(second.accepted).toBe(false)
|
|
28
28
|
expect(second.delta).toBe(0)
|
|
29
|
-
expect(second.affinity).toBe(first.affinity)
|
|
29
|
+
expect(second.affinity).not.toBe(first.affinity)
|
|
30
30
|
expect(second.affinity.pets).toBe(1)
|
|
31
|
+
expect(second.affinity.petRejects).toBe(1)
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
it('accepts a pet again after the cooldown elapsed', () => {
|
|
@@ -44,8 +45,9 @@ describe('applyInteraction', () => {
|
|
|
44
45
|
const second = applyInteraction(first.affinity, 'feed', now + defaultAffinityConfig.feedCooldownMs - 1)
|
|
45
46
|
expect(second.accepted).toBe(false)
|
|
46
47
|
expect(second.delta).toBe(0)
|
|
47
|
-
expect(second.affinity).toBe(first.affinity)
|
|
48
|
+
expect(second.affinity).not.toBe(first.affinity)
|
|
48
49
|
expect(second.affinity.feeds).toBe(1)
|
|
50
|
+
expect(second.affinity.feedRejects).toBe(1)
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
it('clamps points at AFFINITY_MAX', () => {
|
package/src/affinity.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module @linxin666/dsh-pet/affinity
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { builtinRemark } from './remarks.ts'
|
|
11
|
+
import { BUILTIN_REMARKS, builtinRemark } from './remarks.ts'
|
|
12
12
|
|
|
13
13
|
/** One interaction the user can perform on the pet. */
|
|
14
14
|
export type PetInteraction = 'pet' | 'feed'
|
|
@@ -25,6 +25,10 @@ export interface AffinityState {
|
|
|
25
25
|
pets: number
|
|
26
26
|
/** Total feed count (lifetime). */
|
|
27
27
|
feeds: number
|
|
28
|
+
/** Total pet attempts rejected by cooldown (lifetime). */
|
|
29
|
+
petRejects: number
|
|
30
|
+
/** Total feed attempts rejected by cooldown (lifetime). */
|
|
31
|
+
feedRejects: number
|
|
28
32
|
/** Total completed turns witnessed (lifetime). */
|
|
29
33
|
turns: number
|
|
30
34
|
}
|
|
@@ -75,7 +79,16 @@ export const defaultAffinityConfig: AffinityConfig = {
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
export function emptyAffinity(): AffinityState {
|
|
78
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
points: 0,
|
|
84
|
+
lastPetAt: 0,
|
|
85
|
+
lastFeedAt: 0,
|
|
86
|
+
pets: 0,
|
|
87
|
+
feeds: 0,
|
|
88
|
+
petRejects: 0,
|
|
89
|
+
feedRejects: 0,
|
|
90
|
+
turns: 0,
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
/** Outcome of one interaction. */
|
|
@@ -136,6 +149,12 @@ function clamp(points: number): number {
|
|
|
136
149
|
return Math.min(AFFINITY_MAX, Math.max(0, points))
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
/** Built-in reaction selected deterministically from a persisted counter. */
|
|
153
|
+
function countedRemark(kind: 'pet' | 'petCooldown' | 'feed' | 'feedCooldown', count: number): string {
|
|
154
|
+
const pool = BUILTIN_REMARKS[kind]
|
|
155
|
+
return pool[Math.max(0, Math.floor(count)) % pool.length] ?? builtinRemark(kind)
|
|
156
|
+
}
|
|
157
|
+
|
|
139
158
|
/**
|
|
140
159
|
* Apply one interaction to a copy of the state (immutable style: returns a
|
|
141
160
|
* new object; the caller replaces the persisted state). Cooldowns only
|
|
@@ -151,7 +170,13 @@ export function applyInteraction(
|
|
|
151
170
|
const next = { ...state }
|
|
152
171
|
if (kind === 'pet') {
|
|
153
172
|
if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) {
|
|
154
|
-
|
|
173
|
+
next.petRejects += 1
|
|
174
|
+
return {
|
|
175
|
+
affinity: next,
|
|
176
|
+
delta: 0,
|
|
177
|
+
reaction: countedRemark('petCooldown', state.petRejects),
|
|
178
|
+
accepted: false,
|
|
179
|
+
}
|
|
155
180
|
}
|
|
156
181
|
next.lastPetAt = nowMs
|
|
157
182
|
next.pets += 1
|
|
@@ -159,13 +184,19 @@ export function applyInteraction(
|
|
|
159
184
|
return {
|
|
160
185
|
affinity: next,
|
|
161
186
|
delta: config.petReward,
|
|
162
|
-
reaction:
|
|
187
|
+
reaction: countedRemark('pet', state.pets),
|
|
163
188
|
accepted: true,
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
if (kind === 'feed') {
|
|
167
192
|
if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) {
|
|
168
|
-
|
|
193
|
+
next.feedRejects += 1
|
|
194
|
+
return {
|
|
195
|
+
affinity: next,
|
|
196
|
+
delta: 0,
|
|
197
|
+
reaction: countedRemark('feedCooldown', state.feedRejects),
|
|
198
|
+
accepted: false,
|
|
199
|
+
}
|
|
169
200
|
}
|
|
170
201
|
next.lastFeedAt = nowMs
|
|
171
202
|
next.feeds += 1
|
|
@@ -173,7 +204,7 @@ export function applyInteraction(
|
|
|
173
204
|
return {
|
|
174
205
|
affinity: next,
|
|
175
206
|
delta: config.feedReward,
|
|
176
|
-
reaction:
|
|
207
|
+
reaction: countedRemark('feed', state.feeds),
|
|
177
208
|
accepted: true,
|
|
178
209
|
}
|
|
179
210
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chatter unit tests: the status voice (scene-stable lines, cadence rotation,
|
|
3
|
+
* per-tool families, argument hints) and the murmur engine (keyword moods,
|
|
4
|
+
* cooldown, output-volume budget). Everything is deterministic round-robin,
|
|
5
|
+
* so exact lines are asserted.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
8
|
+
import {
|
|
9
|
+
STATUS_POOLS,
|
|
10
|
+
StatusVoice,
|
|
11
|
+
TOOL_POOLS,
|
|
12
|
+
toolArgHint,
|
|
13
|
+
toolCategory,
|
|
14
|
+
WHISPER_GENERIC_POOL,
|
|
15
|
+
WHISPER_RULES,
|
|
16
|
+
WhisperEngine,
|
|
17
|
+
} from './chatter.ts'
|
|
18
|
+
|
|
19
|
+
describe('StatusVoice', () => {
|
|
20
|
+
it('opens every scene with its legacy fixed line', () => {
|
|
21
|
+
// The first pool line is the wording the plugin has always shown.
|
|
22
|
+
expect(STATUS_POOLS.prepare[0]).toBe('准备开始')
|
|
23
|
+
expect(STATUS_POOLS.waiting[0]).toBe('等待模型响应')
|
|
24
|
+
expect(STATUS_POOLS.thinking[0]).toBe('正在思考')
|
|
25
|
+
expect(STATUS_POOLS.review[0]).toBe('整理回复中')
|
|
26
|
+
expect(STATUS_POOLS.toolResult[0]).toBe('处理工具结果')
|
|
27
|
+
expect(STATUS_POOLS.done[0]).toBe('完成啦')
|
|
28
|
+
expect(STATUS_POOLS.failed[0]).toBe('执行失败')
|
|
29
|
+
expect(STATUS_POOLS.toolFailed[0]).toBe('工具执行失败')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('keeps the line stable while a scene repeats within the rotation window', () => {
|
|
33
|
+
const voice = new StatusVoice()
|
|
34
|
+
// Streamed chunks re-emit the same scene many times a second: the copy
|
|
35
|
+
// must not flicker per chunk.
|
|
36
|
+
expect(voice.scene('thinking', 1000)).toBe('正在思考')
|
|
37
|
+
expect(voice.scene('thinking', 2000)).toBe('正在思考')
|
|
38
|
+
expect(voice.scene('thinking', 4999)).toBe('正在思考')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('advances the line once the scene persists past the rotation cadence', () => {
|
|
42
|
+
const voice = new StatusVoice()
|
|
43
|
+
expect(voice.scene('thinking', 1000)).toBe('正在思考')
|
|
44
|
+
expect(voice.scene('thinking', 5001)).toBe(STATUS_POOLS.thinking[1])
|
|
45
|
+
expect(voice.scene('thinking', 9002)).toBe(STATUS_POOLS.thinking[2])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('rotates round-robin when a scene is revisited after another scene', () => {
|
|
49
|
+
const voice = new StatusVoice()
|
|
50
|
+
expect(voice.scene('thinking', 0)).toBe(STATUS_POOLS.thinking[0])
|
|
51
|
+
expect(voice.scene('review', 100)).toBe(STATUS_POOLS.review[0])
|
|
52
|
+
expect(voice.scene('thinking', 200)).toBe(STATUS_POOLS.thinking[1])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('interpolates the tool name and argument hint into tool lines', () => {
|
|
56
|
+
const voice = new StatusVoice()
|
|
57
|
+
expect(voice.tool('bash', 'bash', 'npm test', 0)).toBe('正在使用 bash')
|
|
58
|
+
// Same family within the window: the pool line repeats but the CURRENT
|
|
59
|
+
// call's name/hint are interpolated, so copy never goes stale.
|
|
60
|
+
expect(voice.tool('bash', 'bash', 'pnpm build', 100)).toBe('正在使用 bash')
|
|
61
|
+
// Past the cadence the shell family rotates to a hint-carrying line.
|
|
62
|
+
const rotated = voice.tool('bash', 'bash', 'npm test', 5000)
|
|
63
|
+
expect(rotated).toBe(TOOL_POOLS.shell[1]!.replace('{hint}', 'npm test'))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('falls back to the tool name when no argument hint is available', () => {
|
|
67
|
+
const voice = new StatusVoice()
|
|
68
|
+
const line = voice.tool('grep', 'grep', undefined, 5000)
|
|
69
|
+
expect(line).not.toContain('{hint}')
|
|
70
|
+
expect(line).not.toContain('{tool}')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('interpolates the running count into the remaining-tools line', () => {
|
|
74
|
+
const voice = new StatusVoice()
|
|
75
|
+
expect(voice.toolRemaining(1, 0)).toBe('还有 1 个工具运行中')
|
|
76
|
+
expect(voice.toolRemaining(3, 5000)).toContain('3')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('toolCategory', () => {
|
|
81
|
+
it('maps the common tool vocabulary onto copy families', () => {
|
|
82
|
+
expect(toolCategory('read')).toBe('read')
|
|
83
|
+
expect(toolCategory('write')).toBe('write')
|
|
84
|
+
expect(toolCategory('edit')).toBe('edit')
|
|
85
|
+
expect(toolCategory('str_replace_editor')).toBe('edit')
|
|
86
|
+
expect(toolCategory('run_code')).toBe('shell')
|
|
87
|
+
expect(toolCategory('bash')).toBe('shell')
|
|
88
|
+
expect(toolCategory('grep')).toBe('grep')
|
|
89
|
+
expect(toolCategory('glob')).toBe('find')
|
|
90
|
+
expect(toolCategory('web_search')).toBe('webSearch')
|
|
91
|
+
expect(toolCategory('browserFetch')).toBe('webFetch')
|
|
92
|
+
expect(toolCategory('mcp__mem0__search_memories')).toBe('memory')
|
|
93
|
+
expect(toolCategory('subagent')).toBe('subagent')
|
|
94
|
+
expect(toolCategory('todo_write')).toBe('todo')
|
|
95
|
+
expect(toolCategory('some_future_tool')).toBe('generic')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('toolArgHint', () => {
|
|
100
|
+
it('extracts the shell command', () => {
|
|
101
|
+
expect(toolArgHint('bash', '{"command":"npm test"}')).toBe('npm test')
|
|
102
|
+
expect(toolArgHint('run_code', '{"code":"console.log(1)"}')).toBe('console.log(1)')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('reduces file paths to their basename', () => {
|
|
106
|
+
expect(toolArgHint('read', '{"file_path":"/repo/src/client/PetSprite.tsx"}')).toBe('PetSprite.tsx')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('extracts search patterns and web queries', () => {
|
|
110
|
+
expect(toolArgHint('grep', '{"pattern":"bubbleStack"}')).toBe('bubbleStack')
|
|
111
|
+
expect(toolArgHint('web_search', '{"query":"dsh 插件"}')).toBe('dsh 插件')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('stays hintless for unparseable or shapeless arguments', () => {
|
|
115
|
+
expect(toolArgHint('bash', 'not json')).toBeUndefined()
|
|
116
|
+
expect(toolArgHint('bash', '[]')).toBeUndefined()
|
|
117
|
+
expect(toolArgHint('bash', '{"timeout":1000}')).toBeUndefined()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('caps long hints so the bubble stays compact', () => {
|
|
121
|
+
const hint = toolArgHint('bash', JSON.stringify({ command: 'x'.repeat(60) }))
|
|
122
|
+
expect(hint).toBeDefined()
|
|
123
|
+
expect(hint!.length).toBeLessThanOrEqual(28)
|
|
124
|
+
expect(hint!.endsWith('...')).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('WhisperEngine', () => {
|
|
129
|
+
it('wakes a themed whisper from a keyword in the model output', () => {
|
|
130
|
+
const engine = new WhisperEngine()
|
|
131
|
+
expect(engine.feed('这里有个错误需要处理', 0)).toBe(WHISPER_RULES[1]!.pool[0])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('suppresses whispers during the cooldown, then rotates the pool', () => {
|
|
135
|
+
const engine = new WhisperEngine()
|
|
136
|
+
expect(engine.feed('出现一个错误', 0)).toBe(WHISPER_RULES[1]!.pool[0])
|
|
137
|
+
expect(engine.feed('又一个错误', 1000)).toBeUndefined()
|
|
138
|
+
expect(engine.feed('还有一个错误', 9000)).toBe(WHISPER_RULES[1]!.pool[1])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('earns an ambient whisper from output volume alone', () => {
|
|
142
|
+
const engine = new WhisperEngine(0, 10)
|
|
143
|
+
expect(engine.feed('aaaaaaaa', 0)).toBeUndefined()
|
|
144
|
+
expect(engine.feed('bbbb', 1)).toBe(WHISPER_GENERIC_POOL[0])
|
|
145
|
+
// The counter resets after speaking: another budget must accumulate.
|
|
146
|
+
expect(engine.feed('cccc', 2)).toBeUndefined()
|
|
147
|
+
expect(engine.feed('dddddddddd', 3)).toBe(WHISPER_GENERIC_POOL[1])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('ignores empty chunks', () => {
|
|
151
|
+
const engine = new WhisperEngine(0, 0)
|
|
152
|
+
expect(engine.feed('', 0)).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
})
|