@linxin666/dsh-pet 0.2.2 → 0.2.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 +129 -3
- package/README.zh.md +129 -3
- package/assets/decorations/whale/decoration.json +20 -0
- package/assets/decorations/whale/whale-frames.png +0 -0
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/contracts/pet-manifest-v2.schema.json +281 -0
- package/contracts/status-decoration-v1.schema.json +144 -0
- package/contracts/voice-pack-v1.schema.json +234 -0
- package/lib/client.js +581 -38
- package/lib/client.js.map +1 -1
- package/lib/index.js +1684 -59
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- package/lib/types/chatter.d.ts +61 -3
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +71 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +2 -1
- package/lib/types/client/PetSettingsCard.d.ts +14 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +24 -1
- package/lib/types/client/PetSprite.d.ts +7 -1
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +120 -12
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -0
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +14 -0
- package/lib/types/client/phase-stream.d.ts +22 -0
- package/lib/types/client/phase-stream.d.ts.map +1 -0
- package/lib/types/client/phase-stream.js +28 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
- package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
- package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
- package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/runtime.js +63 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
- package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
- package/lib/types/client/renderers/live2d.d.ts +52 -0
- package/lib/types/client/renderers/live2d.d.ts.map +1 -0
- package/lib/types/client/renderers/live2d.js +192 -0
- package/lib/types/client/renderers/registry.d.ts +32 -0
- package/lib/types/client/renderers/registry.d.ts.map +1 -0
- package/lib/types/client/renderers/registry.js +49 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +9 -6
- package/lib/types/contracts/renderer.d.ts +50 -0
- package/lib/types/contracts/renderer.d.ts.map +1 -0
- package/lib/types/contracts/renderer.js +15 -0
- package/lib/types/contracts/status-decoration.d.ts +85 -0
- package/lib/types/contracts/status-decoration.d.ts.map +1 -0
- package/lib/types/contracts/status-decoration.js +21 -0
- package/lib/types/decoration.d.ts +39 -0
- package/lib/types/decoration.d.ts.map +1 -0
- package/lib/types/decoration.js +210 -0
- package/lib/types/event-projection.d.ts +8 -3
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +9 -4
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -0
- package/lib/types/manifest-v2.d.ts +129 -0
- package/lib/types/manifest-v2.d.ts.map +1 -0
- package/lib/types/manifest-v2.js +387 -0
- package/lib/types/model3.d.ts +26 -0
- package/lib/types/model3.d.ts.map +1 -0
- package/lib/types/model3.js +96 -0
- package/lib/types/registry.d.ts +112 -5
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +395 -38
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +312 -13
- package/lib/types/service.d.ts +38 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +46 -3
- package/lib/types/voice-pack.d.ts +98 -0
- package/lib/types/voice-pack.d.ts.map +1 -0
- package/lib/types/voice-pack.js +384 -0
- package/package.json +13 -10
- package/src/chatter.test.ts +89 -2
- package/src/chatter.ts +120 -14
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +57 -0
- package/src/client/PetSprite.test.tsx +265 -12
- package/src/client/PetSprite.tsx +165 -33
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +14 -0
- package/src/client/phase-stream.test.ts +76 -0
- package/src/client/phase-stream.ts +39 -0
- package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
- package/src/client/renderers/PetRendererSwitch.tsx +48 -0
- package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
- package/src/client/renderers/live2d/runtime.test.ts +61 -0
- package/src/client/renderers/live2d/runtime.ts +119 -0
- package/src/client/renderers/live2d/vendor-entry.ts +14 -0
- package/src/client/renderers/live2d.test.ts +263 -0
- package/src/client/renderers/live2d.ts +231 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-form.ts +8 -6
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -0
- package/src/contracts/status-decoration.ts +78 -0
- package/src/decoration.test.ts +178 -0
- package/src/decoration.ts +220 -0
- package/src/event-projection.ts +10 -5
- package/src/index.ts +2 -0
- package/src/manifest-v2.test.ts +251 -0
- package/src/manifest-v2.ts +414 -0
- package/src/model3.test.ts +79 -0
- package/src/model3.ts +91 -0
- package/src/registry.test.ts +438 -2
- package/src/registry.ts +470 -38
- package/src/routes.ts +328 -14
- package/src/service.ts +65 -3
- package/src/voice-pack.test.ts +216 -0
- package/src/voice-pack.ts +413 -0
package/src/chatter.ts
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* plugin has always shown, so existing installs keep their wording until the
|
|
18
18
|
* scene cycles. No emoji anywhere (repository rule); ~ is the whale-girl's
|
|
19
19
|
* signature.
|
|
20
|
+
*
|
|
21
|
+
* Since pet-center M4 (issue #677) every pool is overridable through a
|
|
22
|
+
* {@link VoicePoolsProvider}: the built-in pools are the fallback layer, and
|
|
23
|
+
* voice packs (per-pet voice.json / the global .voice.json) layer their
|
|
24
|
+
* pools on top at draw time.
|
|
20
25
|
* @module @linxin666/dsh-pet/chatter
|
|
21
26
|
*/
|
|
22
27
|
|
|
@@ -193,6 +198,18 @@ export type ToolCategory =
|
|
|
193
198
|
| 'ask'
|
|
194
199
|
| 'generic'
|
|
195
200
|
|
|
201
|
+
/** Every status scene key, in declaration order (voice-pack key allow-list). */
|
|
202
|
+
export const STATUS_SCENES: readonly StatusScene[] = [
|
|
203
|
+
'prepare', 'waiting', 'thinking', 'review', 'toolResult', 'done',
|
|
204
|
+
'failed', 'toolFailed', 'maxTokens', 'interrupted', 'blocked',
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
/** Every tool-family key, in declaration order (voice-pack key allow-list). */
|
|
208
|
+
export const TOOL_CATEGORIES: readonly ToolCategory[] = [
|
|
209
|
+
'read', 'write', 'edit', 'shell', 'grep', 'find', 'ls', 'webSearch',
|
|
210
|
+
'webFetch', 'mcp', 'memory', 'subagent', 'todo', 'browser', 'git', 'ask', 'generic',
|
|
211
|
+
]
|
|
212
|
+
|
|
196
213
|
/** Map a raw tool name onto its copy family (working-activity style regexes). */
|
|
197
214
|
export function toolCategory(toolName: string): ToolCategory {
|
|
198
215
|
const name = toolName.toLowerCase()
|
|
@@ -433,12 +450,19 @@ export function toolArgHint(toolName: string, argumentsJson: string): string | u
|
|
|
433
450
|
* stretch keeps changing its wording.
|
|
434
451
|
*/
|
|
435
452
|
export class StatusVoice {
|
|
453
|
+
private readonly pools: VoicePoolsProvider
|
|
454
|
+
private readonly rotateMs: number
|
|
436
455
|
private readonly counters = new Map<string, number>()
|
|
437
456
|
private lastScene = ''
|
|
438
457
|
private lastLine = ''
|
|
439
458
|
private lastLineAt = Number.NEGATIVE_INFINITY
|
|
440
459
|
|
|
441
|
-
constructor(
|
|
460
|
+
constructor(pools: VoicePoolsProvider = () => BUILTIN_VOICE_PACK, rotateMs: number = STATUS_ROTATE_MS) {
|
|
461
|
+
// Plain property assignment, not parameter properties: this module is
|
|
462
|
+
// imported by scripts/ under node's strip-only mode (pet-center M4).
|
|
463
|
+
this.pools = pools
|
|
464
|
+
this.rotateMs = rotateMs
|
|
465
|
+
}
|
|
442
466
|
|
|
443
467
|
/** Draw the next line of one pool, advancing its round-robin cursor. */
|
|
444
468
|
private draw(poolKey: string, pool: readonly string[]): string {
|
|
@@ -456,24 +480,38 @@ export class StatusVoice {
|
|
|
456
480
|
return this.lastLine
|
|
457
481
|
}
|
|
458
482
|
|
|
483
|
+
/**
|
|
484
|
+
* A scene's effective pool: the voice-pack override when it carries lines,
|
|
485
|
+
* else the built-in pool. Empty overrides fall back rather than blank the
|
|
486
|
+
* bubble — a scene line always renders.
|
|
487
|
+
*/
|
|
488
|
+
private scenePool(scene: StatusScene): readonly string[] {
|
|
489
|
+
const override = this.pools().status?.[scene]
|
|
490
|
+
return override !== undefined && override.length > 0 ? override : STATUS_POOLS[scene]
|
|
491
|
+
}
|
|
492
|
+
|
|
459
493
|
/** Status line for a phase scene. */
|
|
460
494
|
scene(scene: StatusScene, nowMs: number): string {
|
|
461
|
-
return this.voice('scene:' + scene, 'pool:' + scene,
|
|
495
|
+
return this.voice('scene:' + scene, 'pool:' + scene, this.scenePool(scene), nowMs)
|
|
462
496
|
}
|
|
463
497
|
|
|
464
498
|
/** Status line for a tool call, with the real-argument hint when known. */
|
|
465
499
|
tool(toolName: string, displayName: string, hint: string | undefined, nowMs: number): string {
|
|
466
500
|
const category = toolCategory(toolName)
|
|
467
|
-
const
|
|
501
|
+
const override = this.pools().tools?.[category]
|
|
502
|
+
const pool = override !== undefined && override.length > 0 ? override : TOOL_POOLS[category]
|
|
503
|
+
const line = this.voice('tool:' + category, 'tool:' + category, pool, nowMs)
|
|
468
504
|
return line
|
|
469
|
-
.
|
|
470
|
-
.
|
|
505
|
+
.replaceAll('{tool}', displayName)
|
|
506
|
+
.replaceAll('{hint}', hint ?? displayName)
|
|
471
507
|
}
|
|
472
508
|
|
|
473
509
|
/** Status line while sibling tools still run (always reflects the count). */
|
|
474
510
|
toolRemaining(count: number, nowMs: number): string {
|
|
475
|
-
|
|
476
|
-
|
|
511
|
+
const override = this.pools().toolRemaining
|
|
512
|
+
const pool = override !== undefined && override.length > 0 ? override : TOOL_REMAINING_POOL
|
|
513
|
+
return this.voice('toolRemaining', 'toolRemaining', pool, nowMs)
|
|
514
|
+
.replaceAll('{n}', String(count))
|
|
477
515
|
}
|
|
478
516
|
}
|
|
479
517
|
|
|
@@ -734,24 +772,89 @@ export const WHISPER_RULES: readonly WhisperRule[] = [
|
|
|
734
772
|
},
|
|
735
773
|
]
|
|
736
774
|
|
|
775
|
+
/**
|
|
776
|
+
* Voice-pack overrides (pet-center M4, issue #677): the content a voice
|
|
777
|
+
* pack can replace, one pool at a time. Every field is optional — missing
|
|
778
|
+
* keys inherit the built-in pools. Resolution happens at draw time through
|
|
779
|
+
* a provider function, so swapping pets (or editing the global file) re-
|
|
780
|
+
* voices live engines without rebuilding them.
|
|
781
|
+
*
|
|
782
|
+
* Override semantics:
|
|
783
|
+
* - status/tools/toolRemaining: a non-empty override replaces the built-in
|
|
784
|
+
* pool for that key; an empty override falls back to the built-in pool
|
|
785
|
+
* (a scene line always renders, so it can never be blanked).
|
|
786
|
+
* - whispers.generic / whispers.rules: the override REPLACES the built-in
|
|
787
|
+
* section; an empty array mutes that channel (ambient or keyword).
|
|
788
|
+
*/
|
|
789
|
+
export interface VoicePackOverrides {
|
|
790
|
+
/** Status copy pools by scene; each key replaces that scene's pool. */
|
|
791
|
+
status?: Partial<Record<StatusScene, readonly string[]>>
|
|
792
|
+
/** Tool copy pools by family; each key replaces that family's pool. */
|
|
793
|
+
tools?: Partial<Record<ToolCategory, readonly string[]>>
|
|
794
|
+
/** The parallel-tools count line pool ({n} interpolates the count). */
|
|
795
|
+
toolRemaining?: readonly string[]
|
|
796
|
+
/** Murmur pools; each section replaces the built-in one as a whole. */
|
|
797
|
+
whispers?: {
|
|
798
|
+
/** Ambient inner-whisper pool (empty mutes ambient whispers). */
|
|
799
|
+
generic?: readonly string[]
|
|
800
|
+
/** Ordered keyword rules (empty disables keyword-triggered whispers). */
|
|
801
|
+
rules?: readonly WhisperRule[]
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Read the current effective voice-pack overrides (draw-time resolution). */
|
|
806
|
+
export type VoicePoolsProvider = () => VoicePackOverrides
|
|
807
|
+
|
|
808
|
+
/** The built-in voice pack: the plugin's default copy, unchanged since v1. */
|
|
809
|
+
export const BUILTIN_VOICE_PACK: VoicePackOverrides = {
|
|
810
|
+
status: STATUS_POOLS,
|
|
811
|
+
tools: TOOL_POOLS,
|
|
812
|
+
toolRemaining: TOOL_REMAINING_POOL,
|
|
813
|
+
whispers: { generic: WHISPER_GENERIC_POOL, rules: WHISPER_RULES },
|
|
814
|
+
}
|
|
815
|
+
|
|
737
816
|
/**
|
|
738
817
|
* The murmur engine (碎碎念): watches the model's own output and lets the pet
|
|
739
818
|
* whisper its inner voice. Two ways to earn a whisper:
|
|
740
819
|
* - a keyword rule matches the fresh chunk text (themed whisper);
|
|
741
820
|
* - enough output volume flowed by without one (ambient whisper).
|
|
742
821
|
* A cooldown keeps whispers occasional; all picks are round-robin so tests
|
|
743
|
-
* reproduce exact lines.
|
|
822
|
+
* reproduce exact lines. The voice-pack provider (pet-center M4) swaps the
|
|
823
|
+
* pools at draw time, so a pet switch re-voices live engines in place.
|
|
744
824
|
*/
|
|
745
825
|
export class WhisperEngine {
|
|
826
|
+
private readonly pools: VoicePoolsProvider
|
|
827
|
+
private readonly cooldownMs: number
|
|
828
|
+
private readonly charBudget: number
|
|
746
829
|
private readonly counters = new Map<number, number>()
|
|
747
830
|
private genericCursor = 0
|
|
748
831
|
private lastWhisperAt = Number.NEGATIVE_INFINITY
|
|
749
832
|
private charsSinceWhisper = 0
|
|
750
833
|
|
|
751
834
|
constructor(
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
835
|
+
pools: VoicePoolsProvider = () => BUILTIN_VOICE_PACK,
|
|
836
|
+
cooldownMs: number = WHISPER_COOLDOWN_MS,
|
|
837
|
+
charBudget: number = WHISPER_CHAR_BUDGET,
|
|
838
|
+
) {
|
|
839
|
+
this.pools = pools
|
|
840
|
+
this.cooldownMs = cooldownMs
|
|
841
|
+
this.charBudget = charBudget
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Effective keyword rules: an override replaces the built-in rules as a
|
|
846
|
+
* whole; an explicit empty array disables keyword-triggered whispers.
|
|
847
|
+
*/
|
|
848
|
+
private rules(): readonly WhisperRule[] {
|
|
849
|
+
const override = this.pools().whispers?.rules
|
|
850
|
+
return override === undefined ? WHISPER_RULES : override
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Effective ambient pool (an explicit empty array mutes ambient whispers). */
|
|
854
|
+
private generic(): readonly string[] {
|
|
855
|
+
const override = this.pools().whispers?.generic
|
|
856
|
+
return override === undefined ? WHISPER_GENERIC_POOL : override
|
|
857
|
+
}
|
|
755
858
|
|
|
756
859
|
/**
|
|
757
860
|
* Feed one model-output chunk (reasoning or text). Returns the whisper to
|
|
@@ -765,8 +868,9 @@ export class WhisperEngine {
|
|
|
765
868
|
return undefined
|
|
766
869
|
}
|
|
767
870
|
const haystack = text.toLowerCase()
|
|
768
|
-
|
|
769
|
-
|
|
871
|
+
const rules = this.rules()
|
|
872
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
873
|
+
const rule = rules[ruleIndex]!
|
|
770
874
|
if (!rule.keywords.some(keyword => haystack.includes(keyword))) continue
|
|
771
875
|
const index = (this.counters.get(ruleIndex) ?? 0) % rule.pool.length
|
|
772
876
|
this.counters.set(ruleIndex, index + 1)
|
|
@@ -774,7 +878,9 @@ export class WhisperEngine {
|
|
|
774
878
|
}
|
|
775
879
|
this.charsSinceWhisper += text.length
|
|
776
880
|
if (this.charsSinceWhisper < this.charBudget) return undefined
|
|
777
|
-
const
|
|
881
|
+
const generic = this.generic()
|
|
882
|
+
if (generic.length === 0) return undefined
|
|
883
|
+
const line = generic[this.genericCursor % generic.length]!
|
|
778
884
|
this.genericCursor += 1
|
|
779
885
|
return this.speak(line, nowMs)
|
|
780
886
|
}
|
|
@@ -16,6 +16,7 @@ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
16
16
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
17
17
|
import type { PetStoreInstance } from './pet-store.ts'
|
|
18
18
|
import { PetSprite } from './PetSprite.tsx'
|
|
19
|
+
import { PetRendererSwitch } from './renderers/PetRendererSwitch.tsx'
|
|
19
20
|
import { NS } from './locales.ts'
|
|
20
21
|
import styles from './pet.module.css'
|
|
21
22
|
|
|
@@ -75,20 +76,27 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
75
76
|
{snapshot === null || definition === null
|
|
76
77
|
? null
|
|
77
78
|
: (
|
|
78
|
-
<
|
|
79
|
-
snapshot={snapshot}
|
|
79
|
+
<PetRendererSwitch
|
|
80
80
|
definition={definition}
|
|
81
|
-
|
|
82
|
-
feedback={feedback}
|
|
81
|
+
phase={snapshot?.phase ?? 'idle'}
|
|
83
82
|
onPet={props.pet}
|
|
84
|
-
onFeed={props.feed}
|
|
85
|
-
onHide={props.hide}
|
|
86
|
-
onDragEnd={props.dragEnd}
|
|
87
|
-
onRename={props.rename}
|
|
88
|
-
onOpenSession={props.openSession}
|
|
89
|
-
onFeedbackDone={props.feedbackDone}
|
|
90
83
|
t={props.t}
|
|
91
|
-
|
|
84
|
+
>
|
|
85
|
+
<PetSprite
|
|
86
|
+
snapshot={snapshot}
|
|
87
|
+
definition={definition}
|
|
88
|
+
display={snapshot.display}
|
|
89
|
+
feedback={feedback}
|
|
90
|
+
onPet={props.pet}
|
|
91
|
+
onFeed={props.feed}
|
|
92
|
+
onHide={props.hide}
|
|
93
|
+
onDragEnd={props.dragEnd}
|
|
94
|
+
onRename={props.rename}
|
|
95
|
+
onOpenSession={props.openSession}
|
|
96
|
+
onFeedbackDone={props.feedbackDone}
|
|
97
|
+
t={props.t}
|
|
98
|
+
/>
|
|
99
|
+
</PetRendererSwitch>
|
|
92
100
|
)}
|
|
93
101
|
</span>
|
|
94
102
|
)
|
|
@@ -30,6 +30,8 @@ export interface PetSettings {
|
|
|
30
30
|
bottom?: number
|
|
31
31
|
/** Selected pet id (a registry entry). */
|
|
32
32
|
petId?: string
|
|
33
|
+
/** Status-decoration master switch (pet-center M5, #567). */
|
|
34
|
+
decorationEnabled?: boolean
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** What the pet settings card renders. */
|
|
@@ -46,8 +48,12 @@ export interface PetSettingsCardState extends CardShell {
|
|
|
46
48
|
bottom: CardFieldState
|
|
47
49
|
/** Selected pet. */
|
|
48
50
|
petId: CardFieldState
|
|
51
|
+
/** Status-decoration master switch. */
|
|
52
|
+
decorationEnabled: CardFieldState
|
|
49
53
|
/** Pet choices (registry ids + display names), loaded from the host. */
|
|
50
54
|
petChoices: readonly { value: string; label: string }[]
|
|
55
|
+
/** Registry diagnostics (v1 migration hints, invalid entries), host-served. */
|
|
56
|
+
petDiagnostics: readonly PetDiagnosticView[]
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -64,6 +70,12 @@ interface PetChoice {
|
|
|
64
70
|
displayName: string
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
/** One registry diagnostic as served by '/api/pet/diagnostics' (#623). */
|
|
74
|
+
export interface PetDiagnosticView {
|
|
75
|
+
level: 'error' | 'warning'
|
|
76
|
+
message: string
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
/** Fetch the registry list (the same data the sprite renders from). */
|
|
68
80
|
async function fetchPetChoices(): Promise<PetChoice[]> {
|
|
69
81
|
const response = await fetch('/api/pet/pets')
|
|
@@ -71,6 +83,14 @@ async function fetchPetChoices(): Promise<PetChoice[]> {
|
|
|
71
83
|
return (await response.json()) as PetChoice[]
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
/** Fetch the registry diagnostics (v1 migration hints, invalid entries). */
|
|
87
|
+
async function fetchPetDiagnostics(): Promise<PetDiagnosticView[]> {
|
|
88
|
+
const response = await fetch('/api/pet/diagnostics')
|
|
89
|
+
if (!response.ok) throw new Error('pet diagnostics failed: ' + response.status)
|
|
90
|
+
const body = (await response.json()) as { diagnostics?: PetDiagnosticView[] }
|
|
91
|
+
return body.diagnostics ?? []
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
75
95
|
export class PetSettingsCardController {
|
|
76
96
|
private readonly form: CardForm<PetSettings>
|
|
@@ -80,6 +100,7 @@ export class PetSettingsCardController {
|
|
|
80
100
|
// without rebuilding the form.
|
|
81
101
|
private readonly petChoices: string[] = []
|
|
82
102
|
private readonly petLabels = new Map<string, string>()
|
|
103
|
+
private diagnostics: PetDiagnosticView[] = []
|
|
83
104
|
private loaded = false
|
|
84
105
|
private attempts = 0
|
|
85
106
|
|
|
@@ -87,6 +108,7 @@ export class PetSettingsCardController {
|
|
|
87
108
|
constructor(scope: SettingsScope<PetSettings>) {
|
|
88
109
|
this.form = new CardForm(scope, [
|
|
89
110
|
booleanField('enabled'),
|
|
111
|
+
booleanField('decorationEnabled'),
|
|
90
112
|
booleanField('visible'),
|
|
91
113
|
numberField('size'),
|
|
92
114
|
numberField('right'),
|
|
@@ -95,6 +117,17 @@ export class PetSettingsCardController {
|
|
|
95
117
|
])
|
|
96
118
|
this.store = this.form.bind(() => this.projection())
|
|
97
119
|
void this.loadPets()
|
|
120
|
+
void this.loadDiagnostics()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Fetch registry diagnostics once (soft-fail: an empty list on error). */
|
|
124
|
+
private async loadDiagnostics(): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
this.diagnostics = await fetchPetDiagnostics()
|
|
127
|
+
this.store.set(this.projection())
|
|
128
|
+
} catch {
|
|
129
|
+
this.diagnostics = []
|
|
130
|
+
}
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
/** Resolve the registry choices once (retried a few times on failure). */
|
|
@@ -118,12 +151,14 @@ export class PetSettingsCardController {
|
|
|
118
151
|
return {
|
|
119
152
|
...this.form.shell(),
|
|
120
153
|
enabled: this.form.field('enabled'),
|
|
154
|
+
decorationEnabled: this.form.field('decorationEnabled'),
|
|
121
155
|
visible: this.form.field('visible'),
|
|
122
156
|
size: this.form.field('size'),
|
|
123
157
|
right: this.form.field('right'),
|
|
124
158
|
bottom: this.form.field('bottom'),
|
|
125
159
|
petId: this.form.field('petId'),
|
|
126
160
|
petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
|
|
161
|
+
petDiagnostics: this.diagnostics,
|
|
127
162
|
}
|
|
128
163
|
}
|
|
129
164
|
|
|
@@ -186,6 +221,18 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
186
221
|
onEdit={(text) => { props.edit('enabled', text) }}
|
|
187
222
|
onReset={() => { props.resetField('enabled') }}
|
|
188
223
|
/>
|
|
224
|
+
<BooleanField
|
|
225
|
+
id="settings-pet-decoration"
|
|
226
|
+
label={t('settings.decoration')}
|
|
227
|
+
hint={t('settings.decorationHint')}
|
|
228
|
+
inheritLabel={t('settings.inherit')}
|
|
229
|
+
onLabel={t('settings.on')}
|
|
230
|
+
offLabel={t('settings.off')}
|
|
231
|
+
{...fieldProps}
|
|
232
|
+
{...state.decorationEnabled}
|
|
233
|
+
onEdit={(text) => { props.edit('decorationEnabled', text) }}
|
|
234
|
+
onReset={() => { props.resetField('decorationEnabled') }}
|
|
235
|
+
/>
|
|
189
236
|
<ChoiceField
|
|
190
237
|
id="settings-pet-pet"
|
|
191
238
|
label={t('settings.pet')}
|
|
@@ -197,6 +244,16 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
197
244
|
onEdit={(text) => { props.edit('petId', text) }}
|
|
198
245
|
onReset={() => { props.resetField('petId') }}
|
|
199
246
|
/>
|
|
247
|
+
{state.petDiagnostics.length === 0 ? null : (
|
|
248
|
+
<li className={sectionCss.diagnostics} data-dsh-part="diagnostics">
|
|
249
|
+
<span className={sectionCss.diagnosticsTitle}>{t('settings.diagnosticsTitle')}</span>
|
|
250
|
+
<ul>
|
|
251
|
+
{state.petDiagnostics.map((diagnostic, index) => (
|
|
252
|
+
<li key={index} data-level={diagnostic.level}>{diagnostic.message}</li>
|
|
253
|
+
))}
|
|
254
|
+
</ul>
|
|
255
|
+
</li>
|
|
256
|
+
)}
|
|
200
257
|
<BooleanField
|
|
201
258
|
id="settings-pet-visible"
|
|
202
259
|
label={t('settings.visible')}
|