@linxin666/dsh-pet 0.4.2 → 0.4.4
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 +16 -28
- package/README.zh.md +16 -28
- package/lib/client.js +74 -65
- package/lib/client.js.map +1 -1
- package/lib/index.js +169 -21
- package/lib/types/client/PetSettingsCard.d.ts +16 -12
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +77 -49
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -2
- package/lib/types/index.d.ts +6 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +82 -10
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/service.d.ts +9 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +11 -1
- package/package.json +1 -1
- package/src/client/PetSettingsCard.test.tsx +115 -4
- package/src/client/PetSettingsCard.tsx +76 -66
- package/src/client/locales.ts +0 -2
- package/src/client/pet-css.test.ts +8 -0
- package/src/client/settings-section.module.css +0 -23
- package/src/index.test.ts +39 -0
- package/src/index.ts +105 -11
- package/src/mount-once.ts +107 -13
- package/src/service.ts +11 -1
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAI5D,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet
|
|
12
12
|
*/
|
|
13
13
|
import z from '@deepseek-ai/schemastery';
|
|
14
|
-
import { PetService } from "./service.js";
|
|
14
|
+
import { PET_SETTINGS_NAMESPACE, PetService } from "./service.js";
|
|
15
15
|
import { makePetRoutes } from "./routes.js";
|
|
16
16
|
import { loadPetRegistry, petPackageRoot } from "./registry.js";
|
|
17
17
|
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DEFAULT_PET_ID, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from "./persist.js";
|
|
@@ -82,28 +82,93 @@ function readLive(field, fallback) {
|
|
|
82
82
|
const ref = field;
|
|
83
83
|
return typeof ref.get === 'function' ? ref.get() ?? fallback : field;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolve one display field of the section. The precedence is deliberate:
|
|
87
|
+
*
|
|
88
|
+
* 1. an explicit profile value (`committed`) — the user's own choice, and the
|
|
89
|
+
* only signal that carries a value equal to the schema default on purpose;
|
|
90
|
+
* 2. a live value the row committed at runtime, i.e. one that differs from the
|
|
91
|
+
* schema default, which is all an untouched field ever resolves to;
|
|
92
|
+
* 3. what the pet's own `pet.json` already holds (`persisted`), so a field the
|
|
93
|
+
* profile never touched keeps the position the user dragged instead of
|
|
94
|
+
* snapping back to the schema default on the next mount.
|
|
95
|
+
*
|
|
96
|
+
* Step 3 is what makes a drag survive a restart. The display fields carry
|
|
97
|
+
* schema defaults (`right: 24`, `bottom: 20` — the bottom-right corner), and a
|
|
98
|
+
* config that never set them reads back exactly like one that set them to the
|
|
99
|
+
* defaults, so without `persisted` the startup section resets the layout every
|
|
100
|
+
* time the profile commits none — which is what an aggregate install does.
|
|
101
|
+
* @param key - the display field to resolve.
|
|
102
|
+
* @param live - the row config's own field, absent when the entry declares none.
|
|
103
|
+
* @param persisted - the display the pet already shows (its own `pet.json`).
|
|
104
|
+
* @param committed - the profile layer's explicit display values, when known.
|
|
105
|
+
* @returns the value the pet runs with.
|
|
106
|
+
*/
|
|
107
|
+
function displayField(key, live, persisted, committed) {
|
|
108
|
+
const explicit = committed[key];
|
|
109
|
+
if (explicit !== undefined)
|
|
110
|
+
return explicit;
|
|
111
|
+
const value = readLive(live, PET_FORM_DEFAULTS[key]);
|
|
112
|
+
if (value !== PET_FORM_DEFAULTS[key])
|
|
113
|
+
return value;
|
|
114
|
+
const kept = persisted[key];
|
|
115
|
+
return kept === undefined ? value : kept;
|
|
116
|
+
}
|
|
85
117
|
/**
|
|
86
118
|
* The settings section the pet runs with: the effective values of the row's
|
|
87
119
|
* own config. `fallbackPetId` covers a mount whose config names no pet at all
|
|
88
120
|
* (a direct mount outside a Loader) — under a Loader the schema default is
|
|
89
121
|
* always present, so the persisted selection stands whenever the config
|
|
90
|
-
* carries it.
|
|
122
|
+
* carries it. `persisted` and `committed` give the display fields the same
|
|
123
|
+
* treatment; see {@link displayField}.
|
|
91
124
|
* @param config - the effective config of the pet row.
|
|
92
125
|
* @param fallbackPetId - pet id to use when the config names none.
|
|
126
|
+
* @param persisted - the display the pet already shows (its own `pet.json`).
|
|
127
|
+
* @param committed - the profile layer's explicit display values, when known.
|
|
93
128
|
* @returns the resolved settings section.
|
|
94
129
|
*/
|
|
95
|
-
export function petSettingsSection(config, fallbackPetId) {
|
|
130
|
+
export function petSettingsSection(config, fallbackPetId, persisted = {}, committed = {}) {
|
|
96
131
|
return {
|
|
97
|
-
visible:
|
|
98
|
-
size:
|
|
99
|
-
right:
|
|
100
|
-
bottom:
|
|
101
|
-
bubbleScale:
|
|
132
|
+
visible: displayField('visible', config.visible, persisted, committed),
|
|
133
|
+
size: displayField('size', config.size, persisted, committed),
|
|
134
|
+
right: displayField('right', config.right, persisted, committed),
|
|
135
|
+
bottom: displayField('bottom', config.bottom, persisted, committed),
|
|
136
|
+
bubbleScale: displayField('bubbleScale', config.bubbleScale, persisted, committed),
|
|
102
137
|
petId: readLive(config.petId, fallbackPetId),
|
|
103
138
|
enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
|
|
104
139
|
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
105
140
|
};
|
|
106
141
|
}
|
|
142
|
+
/** The settings row ids a pet install is served under, most specific first. */
|
|
143
|
+
const PET_SETTINGS_ROW_IDS = ['web-ui-pet', 'ui-pet', PET_SETTINGS_NAMESPACE];
|
|
144
|
+
/**
|
|
145
|
+
* The settings row serving this plugin's own config, or undefined when the
|
|
146
|
+
* settings service is not mounted (a direct mount) or serves no row for the pet.
|
|
147
|
+
*
|
|
148
|
+
* Every settings write has to address this row's id, and that id is not the
|
|
149
|
+
* package's plugin name: an aggregate bundle renames child rows (`pet` becomes
|
|
150
|
+
* `web-ui-pet`). The browser half resolves the same id through its own
|
|
151
|
+
* `servedEntryId`, so both halves write to one namespace.
|
|
152
|
+
* @param ctx - the plugin's context.
|
|
153
|
+
* @returns the serving row, when there is one.
|
|
154
|
+
*/
|
|
155
|
+
function servedRow(ctx) {
|
|
156
|
+
try {
|
|
157
|
+
const settings = ctx.get('settings', false);
|
|
158
|
+
if (settings === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const rows = settings.describe();
|
|
161
|
+
for (const id of PET_SETTINGS_ROW_IDS) {
|
|
162
|
+
const row = rows.find(candidate => candidate.ns === id);
|
|
163
|
+
if (row !== undefined)
|
|
164
|
+
return row;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
107
172
|
/** Register the pet service and its API + asset routes on the context. */
|
|
108
173
|
export const apply = mountOnce('@linxin666/dsh-pet', applyImpl);
|
|
109
174
|
function applyImpl(ctx, config = {}) {
|
|
@@ -118,12 +183,19 @@ function applyImpl(ctx, config = {}) {
|
|
|
118
183
|
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
119
184
|
registry,
|
|
120
185
|
});
|
|
186
|
+
// The mirror back into the settings document (see syncSettingsFromPet) has to
|
|
187
|
+
// address the row that serves this plugin's config, which an aggregate bundle
|
|
188
|
+
// renames; the id is resolved lazily, once the composition serves it.
|
|
189
|
+
service.settingsNamespaceProvider = () => servedRow(ctx)?.ns;
|
|
121
190
|
// The effective settings ARE this row's own config: the Host serves one form
|
|
122
191
|
// per profile entry from `Config` above, and every edit is committed into the
|
|
123
192
|
// running config, so the plugin re-reads the section here instead of holding
|
|
124
193
|
// a separate settings document (0.1.6 registered one through
|
|
125
|
-
// settings.installSection/register and re-resolved it on every change).
|
|
126
|
-
|
|
194
|
+
// settings.installSection/register and re-resolved it on every change). The
|
|
195
|
+
// display fields additionally read the persisted layout and the profile layer,
|
|
196
|
+
// so a mount with a config that never set them cannot reset a dragged
|
|
197
|
+
// position to the schema defaults (see displayField).
|
|
198
|
+
const current = () => petSettingsSection(config, service.selectedPetId(), service.display(), servedRow(ctx)?.user);
|
|
127
199
|
// The browser half talks to the pet through same-origin JSON endpoints and
|
|
128
200
|
// loads each pet's atlas from the registry's own media route (RPC domains
|
|
129
201
|
// are platform-registered, so the pet serves its own API — the same
|
|
@@ -4,19 +4,50 @@
|
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
-
* and fail the boot. mountOnce makes
|
|
8
|
-
*
|
|
9
|
-
* package name in the client module host).
|
|
7
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
8
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
9
|
+
* deduped by package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
12
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
13
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
14
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
15
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
16
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
17
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
18
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
19
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
20
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
21
|
+
* restart recovered it).
|
|
22
|
+
*
|
|
23
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
24
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
25
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
26
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
27
|
+
* dedupes against it.
|
|
10
28
|
*
|
|
11
29
|
* The registry rides a global symbol so two module instances of the same
|
|
12
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
13
|
-
*
|
|
30
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
31
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
32
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
33
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
34
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
35
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
36
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
37
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
38
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
39
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
40
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
41
|
+
* mounted after it failed with "claims.get is not a function".
|
|
42
|
+
*
|
|
43
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
44
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
45
|
*/
|
|
16
46
|
/**
|
|
17
47
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
-
* The first mount registers normally and
|
|
19
|
-
*
|
|
48
|
+
* The first mount registers normally and releases the name when its fiber
|
|
49
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
50
|
+
* then runs, unless its own fiber disposes first.
|
|
20
51
|
* @param packageName - npm package identity shared by every install source.
|
|
21
52
|
* @param fn - the original plugin apply.
|
|
22
53
|
* @returns an apply of the same shape.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA
|
|
1
|
+
{"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AA6CH;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CA0C9F"}
|
package/lib/types/mount-once.js
CHANGED
|
@@ -5,38 +5,122 @@
|
|
|
5
5
|
* the loader accepts a standalone install of the same package side by side;
|
|
6
6
|
* without this guard the second instance would still re-register the same
|
|
7
7
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
8
|
-
* and fail the boot. mountOnce makes
|
|
9
|
-
*
|
|
10
|
-
* package name in the client module host).
|
|
8
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
9
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
10
|
+
* deduped by package name in the client module host).
|
|
11
|
+
*
|
|
12
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
13
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
14
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
15
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
16
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
17
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
18
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
19
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
20
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
21
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
22
|
+
* restart recovered it).
|
|
23
|
+
*
|
|
24
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
25
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
26
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
27
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
28
|
+
* dedupes against it.
|
|
11
29
|
*
|
|
12
30
|
* The registry rides a global symbol so two module instances of the same
|
|
13
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
14
|
-
*
|
|
31
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
32
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
33
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
34
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
35
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
36
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
37
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
38
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
39
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
40
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
41
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
42
|
+
* mounted after it failed with "claims.get is not a function".
|
|
43
|
+
*
|
|
44
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
45
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
46
|
*/
|
|
47
|
+
/** Published cross-repository contract: package names currently mounted. */
|
|
17
48
|
const MOUNTED = Symbol.for('dsh-web.mounted-plugins');
|
|
49
|
+
/** Additive key this guard owns: refused mounts waiting for the name. */
|
|
50
|
+
const WAITERS = Symbol.for('dsh-web.mounted-plugins.waiters');
|
|
51
|
+
/**
|
|
52
|
+
* The shared name registry, always a `Set` whatever another build stored here:
|
|
53
|
+
* a foreign value (an interim shape, a hand-written global) must not take every
|
|
54
|
+
* family plugin down with it.
|
|
55
|
+
* @returns the process-wide set of mounted package names.
|
|
56
|
+
*/
|
|
18
57
|
function mountedSet() {
|
|
19
58
|
const registry = globalThis;
|
|
20
|
-
|
|
59
|
+
const existing = registry[MOUNTED];
|
|
60
|
+
if (existing instanceof Set)
|
|
61
|
+
return existing;
|
|
62
|
+
const created = new Set();
|
|
63
|
+
registry[MOUNTED] = created;
|
|
64
|
+
return created;
|
|
65
|
+
}
|
|
66
|
+
/** Queue per package name for mounts refused while a holder was alive. */
|
|
67
|
+
function mountWaiters() {
|
|
68
|
+
const registry = globalThis;
|
|
69
|
+
return (registry[WAITERS] ??= new Map());
|
|
21
70
|
}
|
|
22
71
|
/**
|
|
23
72
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
24
|
-
* The first mount registers normally and
|
|
25
|
-
*
|
|
73
|
+
* The first mount registers normally and releases the name when its fiber
|
|
74
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
75
|
+
* then runs, unless its own fiber disposes first.
|
|
26
76
|
* @param packageName - npm package identity shared by every install source.
|
|
27
77
|
* @param fn - the original plugin apply.
|
|
28
78
|
* @returns an apply of the same shape.
|
|
29
79
|
*/
|
|
30
80
|
export function mountOnce(packageName, fn) {
|
|
31
|
-
|
|
81
|
+
const mount = (...args) => {
|
|
32
82
|
const mounted = mountedSet();
|
|
33
|
-
|
|
83
|
+
const ctx = args[0];
|
|
84
|
+
if (mounted.has(packageName)) {
|
|
85
|
+
// Another fiber (the aggregate row, a standalone install, or a copy built
|
|
86
|
+
// from an older revision of this guard) owns the name. Queue instead of
|
|
87
|
+
// dropping: dropping lost the plugin for the rest of the process when the
|
|
88
|
+
// holder was disposed by a reload.
|
|
89
|
+
const waiters = mountWaiters();
|
|
90
|
+
const queue = waiters.get(packageName) ?? [];
|
|
91
|
+
let alive = true;
|
|
92
|
+
const pending = {
|
|
93
|
+
run: () => {
|
|
94
|
+
if (alive)
|
|
95
|
+
mount(...args);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
// A fiber that dies while it waits must not be replayed afterwards: its
|
|
99
|
+
// context is disposed and registering effects on it would throw.
|
|
100
|
+
ctx?.effect?.(() => () => {
|
|
101
|
+
alive = false;
|
|
102
|
+
const index = queue.indexOf(pending);
|
|
103
|
+
if (index >= 0)
|
|
104
|
+
queue.splice(index, 1);
|
|
105
|
+
});
|
|
106
|
+
queue.push(pending);
|
|
107
|
+
waiters.set(packageName, queue);
|
|
34
108
|
return;
|
|
109
|
+
}
|
|
35
110
|
mounted.add(packageName);
|
|
36
|
-
const ctx = args[0];
|
|
37
111
|
ctx?.effect?.(() => () => {
|
|
38
112
|
mounted.delete(packageName);
|
|
113
|
+
const waiters = mountWaiters();
|
|
114
|
+
const queue = waiters.get(packageName);
|
|
115
|
+
if (queue === undefined)
|
|
116
|
+
return;
|
|
117
|
+
waiters.delete(packageName);
|
|
118
|
+
// Defer the handover one microtask so the disposing mount's own effects
|
|
119
|
+
// (routes, ledger, timers) are torn down before the next one registers.
|
|
120
|
+
for (const waiter of queue.splice(0))
|
|
121
|
+
queueMicrotask(() => { waiter.run(); });
|
|
39
122
|
});
|
|
40
123
|
return fn(...args);
|
|
41
|
-
}
|
|
124
|
+
};
|
|
125
|
+
return mount;
|
|
42
126
|
}
|
package/lib/types/service.d.ts
CHANGED
|
@@ -222,6 +222,15 @@ export declare class PetService extends Service {
|
|
|
222
222
|
* the same turn again after the pet is disabled and re-enabled.
|
|
223
223
|
*/
|
|
224
224
|
private readonly officialEventSessions;
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the settings namespace this plugin's own config is served as, when
|
|
227
|
+
* the caller can read it off the composition. A plugin's settings ARE its own
|
|
228
|
+
* config, so the namespace is the row's id — and an aggregate bundle renames
|
|
229
|
+
* child rows (`pet` becomes `web-ui-pet`), which is why it cannot be assumed
|
|
230
|
+
* to be {@link PET_SETTINGS_NAMESPACE}. `apply` sets this; absent means the
|
|
231
|
+
* plugin runs outside a Loader and the package's own id stands.
|
|
232
|
+
*/
|
|
233
|
+
settingsNamespaceProvider: (() => string | undefined) | undefined;
|
|
225
234
|
constructor(ctx: Context, config?: PetConfig);
|
|
226
235
|
/**
|
|
227
236
|
* The draw-time voice-pool provider handed to every projection runtime.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"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"}
|
package/lib/types/service.js
CHANGED
|
@@ -66,6 +66,15 @@ export class PetService extends Service {
|
|
|
66
66
|
* the same turn again after the pet is disabled and re-enabled.
|
|
67
67
|
*/
|
|
68
68
|
officialEventSessions = new WeakSet();
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the settings namespace this plugin's own config is served as, when
|
|
71
|
+
* the caller can read it off the composition. A plugin's settings ARE its own
|
|
72
|
+
* config, so the namespace is the row's id — and an aggregate bundle renames
|
|
73
|
+
* child rows (`pet` becomes `web-ui-pet`), which is why it cannot be assumed
|
|
74
|
+
* to be {@link PET_SETTINGS_NAMESPACE}. `apply` sets this; absent means the
|
|
75
|
+
* plugin runs outside a Loader and the package's own id stands.
|
|
76
|
+
*/
|
|
77
|
+
settingsNamespaceProvider = undefined;
|
|
69
78
|
constructor(ctx, config = {}) {
|
|
70
79
|
super(ctx, 'pet');
|
|
71
80
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
@@ -609,8 +618,9 @@ export class PetService extends Service {
|
|
|
609
618
|
const settings = this.ctx.get('settings', false);
|
|
610
619
|
if (settings === undefined)
|
|
611
620
|
return;
|
|
621
|
+
const ns = this.settingsNamespaceProvider?.() ?? PET_SETTINGS_NAMESPACE;
|
|
612
622
|
const snapshot = this.ledger.snapshot;
|
|
613
|
-
void settings.update(
|
|
623
|
+
void settings.update(ns, {
|
|
614
624
|
visible: snapshot.display.visible,
|
|
615
625
|
size: snapshot.display.size,
|
|
616
626
|
right: snapshot.display.right,
|
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.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The pet settings card under the aggregate shell, which serves no Host
|
|
4
|
+
* settings form for the 'pet' namespace: the pet choice and its visibility
|
|
5
|
+
* switch both write through the pet API, so the pet can still be turned off
|
|
6
|
+
* when the Host form is absent.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
10
|
+
import { useSyncExternalStore } from 'react'
|
|
3
11
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
4
|
-
import { PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
|
|
12
|
+
import { PetSettingsCard, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
|
|
13
|
+
import { t } from './locales.ts'
|
|
5
14
|
|
|
6
15
|
vi.mock('@deepseek-ai/dsh-client-store', () => ({ // test-standards-allow: the SDK client bundle is a browser module factory and cannot be imported by Vitest
|
|
7
16
|
createSnapshotStore: (initial: unknown) => {
|
|
@@ -32,7 +41,12 @@ function response(value: unknown, ok = true): Response {
|
|
|
32
41
|
return { ok, status: ok ? 200 : 500, json: async () => value } as Response
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
document.documentElement.lang = 'zh'
|
|
46
|
+
})
|
|
47
|
+
|
|
35
48
|
afterEach(() => {
|
|
49
|
+
cleanup()
|
|
36
50
|
vi.useRealTimers()
|
|
37
51
|
vi.unstubAllGlobals()
|
|
38
52
|
})
|
|
@@ -46,7 +60,6 @@ describe('pet selection without a Host settings form', () => {
|
|
|
46
60
|
const fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
47
61
|
const path = String(input)
|
|
48
62
|
if (path === '/api/pet/pets') return response([{ id: 'whale-girl', displayName: '鲸鱼娘' }, { id: 'jyn', displayName: '女仆鲸鱼娘' }])
|
|
49
|
-
if (path === '/api/pet/diagnostics') return response({ diagnostics: [] })
|
|
50
63
|
if (path === '/api/pet/state') return response({ pet: { id: selected } })
|
|
51
64
|
if (path === '/api/pet/set-pet') {
|
|
52
65
|
selected = JSON.parse(String(init?.body)).petId as string
|
|
@@ -84,7 +97,6 @@ describe('pet selection without a Host settings form', () => {
|
|
|
84
97
|
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
|
85
98
|
const path = String(input)
|
|
86
99
|
if (path === '/api/pet/pets') return response([{ id: 'whale-girl', displayName: '鲸鱼娘' }, { id: 'jyn', displayName: '女仆鲸鱼娘' }])
|
|
87
|
-
if (path === '/api/pet/diagnostics') return response({ diagnostics: [] })
|
|
88
100
|
if (path === '/api/pet/state') return response({ pet: { id: 'whale-girl' } })
|
|
89
101
|
if (path === '/api/pet/set-pet') return response({ ok: false, error: 'unknown-pet' })
|
|
90
102
|
throw new Error('unexpected request: ' + path)
|
|
@@ -101,3 +113,102 @@ describe('pet selection without a Host settings form', () => {
|
|
|
101
113
|
controller.dispose()
|
|
102
114
|
})
|
|
103
115
|
})
|
|
116
|
+
|
|
117
|
+
describe('pet visibility without a Host settings form', () => {
|
|
118
|
+
it('user turns the pet off through its persisted API and confirms the read-back', async () => {
|
|
119
|
+
// Given the aggregate shell exposes no pet configuration form and the pet is showing
|
|
120
|
+
vi.useFakeTimers()
|
|
121
|
+
const { scope, mutate } = unavailableForm()
|
|
122
|
+
let visible = true
|
|
123
|
+
const fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
124
|
+
const path = String(input)
|
|
125
|
+
if (path === '/api/pet/pets') return response([{ id: 'whale-girl', displayName: '鲸鱼娘' }])
|
|
126
|
+
if (path === '/api/pet/state') return response({ pet: { id: 'whale-girl' }, display: { visible } })
|
|
127
|
+
if (path === '/api/pet/set-visible') {
|
|
128
|
+
visible = JSON.parse(String(init?.body)).visible as boolean
|
|
129
|
+
return response({ ok: true, display: { visible } })
|
|
130
|
+
}
|
|
131
|
+
throw new Error('unexpected request: ' + path)
|
|
132
|
+
})
|
|
133
|
+
vi.stubGlobal('fetch', fetch)
|
|
134
|
+
const controller = new PetSettingsCardController(scope)
|
|
135
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
136
|
+
const face = controller.inject()
|
|
137
|
+
const state = () => face.hooks.petSettingsCard.getSnapshot()
|
|
138
|
+
|
|
139
|
+
// When the user switches the pet off and clicks Save
|
|
140
|
+
expect(state()).toMatchObject({ petSelectionFallback: true, visible: { text: 'true' }, dirty: false })
|
|
141
|
+
face.edit('visible', 'false')
|
|
142
|
+
expect(state()).toMatchObject({ dirty: true, visible: { text: 'false' } })
|
|
143
|
+
face.save()
|
|
144
|
+
await vi.waitFor(() => { expect(state().saving).toBe(false) })
|
|
145
|
+
|
|
146
|
+
// Then the pet service holds it, the draft clears, and the absent Host form is untouched
|
|
147
|
+
expect(visible).toBe(false)
|
|
148
|
+
expect(state()).toMatchObject({ dirty: false, failed: false, visible: { text: 'false' } })
|
|
149
|
+
expect(mutate).not.toHaveBeenCalled()
|
|
150
|
+
expect(fetch).toHaveBeenCalledWith('/api/pet/set-visible', expect.objectContaining({ body: JSON.stringify({ visible: false }) }))
|
|
151
|
+
controller.dispose()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('user keeps the draft when the pet API refuses the visibility write', async () => {
|
|
155
|
+
// Given the Host has no pet form and the pet API refuses visibility writes
|
|
156
|
+
vi.useFakeTimers()
|
|
157
|
+
const { scope } = unavailableForm()
|
|
158
|
+
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
|
159
|
+
const path = String(input)
|
|
160
|
+
if (path === '/api/pet/pets') return response([{ id: 'whale-girl', displayName: '鲸鱼娘' }])
|
|
161
|
+
if (path === '/api/pet/state') return response({ pet: { id: 'whale-girl' }, display: { visible: true } })
|
|
162
|
+
if (path === '/api/pet/set-visible') return response({ ok: false }, false)
|
|
163
|
+
throw new Error('unexpected request: ' + path)
|
|
164
|
+
}))
|
|
165
|
+
const controller = new PetSettingsCardController(scope)
|
|
166
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
167
|
+
const face = controller.inject()
|
|
168
|
+
// When the user saves a visibility the pet API refuses
|
|
169
|
+
face.edit('visible', 'false')
|
|
170
|
+
face.save()
|
|
171
|
+
await vi.waitFor(() => { expect(face.hooks.petSettingsCard.getSnapshot().saving).toBe(false) })
|
|
172
|
+
// Then the switch keeps the draft and the failure is visible
|
|
173
|
+
expect(face.hooks.petSettingsCard.getSnapshot()).toMatchObject({ dirty: true, failed: true, visible: { text: 'false' } })
|
|
174
|
+
controller.dispose()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('pet settings card rendered without a Host settings form', () => {
|
|
179
|
+
it('shows the visibility switch beside the pet choice and hides the Host-only fields', async () => {
|
|
180
|
+
// Given the aggregate shell exposes no pet configuration form
|
|
181
|
+
vi.useFakeTimers()
|
|
182
|
+
const { scope } = unavailableForm()
|
|
183
|
+
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
|
184
|
+
const path = String(input)
|
|
185
|
+
if (path === '/api/pet/pets') return response([{ id: 'whale-girl', displayName: '鲸鱼娘' }])
|
|
186
|
+
if (path === '/api/pet/state') return response({ pet: { id: 'whale-girl' }, display: { visible: true } })
|
|
187
|
+
throw new Error('unexpected request: ' + path)
|
|
188
|
+
}))
|
|
189
|
+
const controller = new PetSettingsCardController(scope)
|
|
190
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
191
|
+
const face = controller.inject()
|
|
192
|
+
const store = face.hooks.petSettingsCard
|
|
193
|
+
|
|
194
|
+
// When the card renders
|
|
195
|
+
render(
|
|
196
|
+
<PetSettingsCard
|
|
197
|
+
t={t}
|
|
198
|
+
usePetSettingsCard={select => useSyncExternalStore(store.subscribe, () => select(store.getSnapshot()))}
|
|
199
|
+
save={face.save}
|
|
200
|
+
discard={face.discard}
|
|
201
|
+
edit={face.edit}
|
|
202
|
+
resetField={face.resetField}
|
|
203
|
+
/>,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Then the pet choice and the switch that turns the pet off are both on screen
|
|
207
|
+
expect(screen.getByLabelText(t('settings.pet'))).toBeTruthy()
|
|
208
|
+
expect(screen.getByLabelText(t('settings.visible'))).toBeTruthy()
|
|
209
|
+
// ...while the fields only the absent Host form owns stay hidden
|
|
210
|
+
expect(screen.queryByLabelText(t('settings.enabled'))).toBeNull()
|
|
211
|
+
expect(screen.queryByLabelText(t('settings.size'))).toBeNull()
|
|
212
|
+
controller.dispose()
|
|
213
|
+
})
|
|
214
|
+
})
|