@linxin666/dsh-pet 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +296 -347
- package/lib/client.js.map +1 -1
- package/lib/index.js +975 -317
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +32 -14
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +79 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +146 -0
- package/lib/types/persist.d.ts +15 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +39 -8
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +59 -57
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +111 -211
- package/package.json +2 -2
- package/src/affinity.ts +33 -0
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/index.ts +38 -16
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/client/spritesheet.ts +16 -92
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +69 -0
- package/src/ledger.ts +190 -0
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +49 -11
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +153 -285
- package/lib/types/client/WhalePet.d.ts.map +0 -1
|
@@ -15,11 +15,13 @@ export const zh = {
|
|
|
15
15
|
'pet.rank': '亲密度 {rank}',
|
|
16
16
|
'pet.points': '{points} 点',
|
|
17
17
|
'pet.treats': '小鱼干 ×{n}',
|
|
18
|
-
'pet.state.loading': '
|
|
19
|
-
'pet.state.error': '
|
|
18
|
+
'pet.state.loading': '宠物正在赶来…',
|
|
19
|
+
'pet.state.error': '宠物迷路了(连接失败)',
|
|
20
20
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
21
21
|
'settings.title': '宠物',
|
|
22
|
-
'settings.description': '
|
|
22
|
+
'settings.description': '选择宠物并调整它的显示布局。',
|
|
23
|
+
'settings.pet': '宠物',
|
|
24
|
+
'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。',
|
|
23
25
|
'settings.enabled': '启用宠物',
|
|
24
26
|
'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。',
|
|
25
27
|
'settings.visible': '显示宠物',
|
|
@@ -30,8 +32,6 @@ export const zh = {
|
|
|
30
32
|
'settings.rightHint': '距视口右边缘的水平内缩距离。',
|
|
31
33
|
'settings.bottom': '距底部(px)',
|
|
32
34
|
'settings.bottomHint': '距视口底边的垂直内缩距离。',
|
|
33
|
-
'settings.name': '名字',
|
|
34
|
-
'settings.nameHint': '宠物显示名,1–20 个字符。',
|
|
35
35
|
'settings.inherit': '继承',
|
|
36
36
|
'settings.on': '开',
|
|
37
37
|
'settings.off': '关',
|
|
@@ -59,11 +59,13 @@ export const en = {
|
|
|
59
59
|
'pet.rank': 'Affinity {rank}',
|
|
60
60
|
'pet.points': '{points} pts',
|
|
61
61
|
'pet.treats': 'Treats ×{n}',
|
|
62
|
-
'pet.state.loading': 'The
|
|
63
|
-
'pet.state.error': 'The
|
|
62
|
+
'pet.state.loading': 'The pet is on its way…',
|
|
63
|
+
'pet.state.error': 'The pet is lost (connection failed)',
|
|
64
64
|
// Plugin settings card (the `settings.plugin.item` seat).
|
|
65
65
|
'settings.title': 'Pet',
|
|
66
|
-
'settings.description': '
|
|
66
|
+
'settings.description': 'Pick a pet and tune its display layout.',
|
|
67
|
+
'settings.pet': 'Pet',
|
|
68
|
+
'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.',
|
|
67
69
|
'settings.enabled': 'Enable the pet',
|
|
68
70
|
'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.',
|
|
69
71
|
'settings.visible': 'Show the pet',
|
|
@@ -74,8 +76,6 @@ export const en = {
|
|
|
74
76
|
'settings.rightHint': 'Horizontal inset from the viewport right edge.',
|
|
75
77
|
'settings.bottom': 'Bottom inset (px)',
|
|
76
78
|
'settings.bottomHint': 'Vertical inset from the viewport bottom edge.',
|
|
77
|
-
'settings.name': 'Name',
|
|
78
|
-
'settings.nameHint': 'The pet\u2019s display name, 1\u201320 characters.',
|
|
79
79
|
'settings.inherit': 'Inherit',
|
|
80
80
|
'settings.on': 'On',
|
|
81
81
|
'settings.off': 'Off',
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
9
|
import type { PetStateView } from '../service.ts';
|
|
10
10
|
import type { PetInteraction } from '../affinity.ts';
|
|
11
|
+
import type { PetDefinition } from '../registry.ts';
|
|
11
12
|
/** One transient reaction bubble on the pet. */
|
|
12
13
|
export interface PetFeedback {
|
|
13
14
|
/** Bubble copy. */
|
|
@@ -21,6 +22,8 @@ export interface PetFeedback {
|
|
|
21
22
|
export interface PetUiState {
|
|
22
23
|
/** Latest host snapshot; null before the first successful fetch. */
|
|
23
24
|
snapshot: PetStateView | null;
|
|
25
|
+
/** The registry list the host serves (atlas URLs + geometry + tracks). */
|
|
26
|
+
pets: PetDefinition[];
|
|
24
27
|
/** Fetch lifecycle. */
|
|
25
28
|
state: 'loading' | 'ready' | 'error';
|
|
26
29
|
/** Transport error message (for the debug surface), when any. */
|
|
@@ -32,6 +35,8 @@ export interface PetUiState {
|
|
|
32
35
|
export type PetUiActions = {
|
|
33
36
|
/** Replace the host snapshot (poll result). */
|
|
34
37
|
setSnapshot: (draft: PetUiState, snapshot: PetStateView) => void;
|
|
38
|
+
/** Replace the registry list. */
|
|
39
|
+
setPets: (draft: PetUiState, pets: PetDefinition[]) => void;
|
|
35
40
|
/** Mark the fetch lifecycle. */
|
|
36
41
|
setState: (draft: PetUiState, state: PetUiState['state'], error: string | null) => void;
|
|
37
42
|
/** Show a reaction bubble. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,uBAAuB;IACvB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAC7B;AAED,uBAAuB;AACvB,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IAChE,iCAAiC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC3D,gCAAgC;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvF,8BAA8B;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CA2B5E;AAED,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA"}
|
|
@@ -11,6 +11,7 @@ export function createPetStore() {
|
|
|
11
11
|
return defineStore({
|
|
12
12
|
init: () => ({
|
|
13
13
|
snapshot: null,
|
|
14
|
+
pets: [],
|
|
14
15
|
state: 'loading',
|
|
15
16
|
error: null,
|
|
16
17
|
feedback: null,
|
|
@@ -21,6 +22,9 @@ export function createPetStore() {
|
|
|
21
22
|
draft.state = 'ready';
|
|
22
23
|
draft.error = null;
|
|
23
24
|
},
|
|
25
|
+
setPets: (draft, pets) => {
|
|
26
|
+
draft.pets = pets;
|
|
27
|
+
},
|
|
24
28
|
setState: (draft, state, error) => {
|
|
25
29
|
draft.state = state;
|
|
26
30
|
draft.error = error;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Staged form model behind the plugin settings card. A card stages what the
|
|
3
3
|
* user types and writes it only when they save — the settings write is a
|
|
4
4
|
* durable, revision-fenced document mutation, so staging keeps what is on
|
|
5
|
-
* screen exactly what a save would store.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* screen exactly what a save would store. Family-shared slice inlined into
|
|
6
|
+
* each plugin's client bundle; mirrors the official ui-plugin-config
|
|
7
|
+
* card-store pattern.
|
|
8
8
|
*/
|
|
9
9
|
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
10
|
/** The write one field's staged text performs when the card is saved. */
|
|
@@ -18,6 +18,14 @@ export type FieldWrite = {
|
|
|
18
18
|
export interface FieldSpec {
|
|
19
19
|
/** Field name inside the namespace section. */
|
|
20
20
|
field: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the Host treats this field as a secret and redacts its value from
|
|
23
|
+
* the read-back (role('secret') in the section schema). Redacted secrets are
|
|
24
|
+
* never compared against the draft on save; the field lands when the scope
|
|
25
|
+
* reports the write succeeded (its secret-set marker under the bridge), so
|
|
26
|
+
* a successful secret save is not misreported as failed.
|
|
27
|
+
*/
|
|
28
|
+
secret?: boolean;
|
|
21
29
|
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
22
30
|
format: (value: unknown) => string;
|
|
23
31
|
/**
|
|
@@ -57,6 +65,12 @@ export interface CardShell {
|
|
|
57
65
|
saving: boolean;
|
|
58
66
|
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
59
67
|
failed: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The rejection code/message the Host returned for the last failed save,
|
|
70
|
+
* surfaced next to the generic failure text. Undefined while no save has
|
|
71
|
+
* failed (or the failure carried no server reason).
|
|
72
|
+
*/
|
|
73
|
+
failedReason?: string;
|
|
60
74
|
}
|
|
61
75
|
/** The write actions the card's slot entry injects. */
|
|
62
76
|
export interface CardActions {
|
|
@@ -69,12 +83,59 @@ export interface CardActions {
|
|
|
69
83
|
/** Drop every staged edit. */
|
|
70
84
|
discard: () => void;
|
|
71
85
|
}
|
|
72
|
-
/**
|
|
73
|
-
export
|
|
86
|
+
/** One durable write a batched settings scope performs. */
|
|
87
|
+
export interface BatchedWrite {
|
|
88
|
+
/** Field this entry writes. */
|
|
89
|
+
field: string;
|
|
90
|
+
/** set stores a value; unset drops the leaf. */
|
|
91
|
+
op: 'set' | 'unset';
|
|
92
|
+
/** Value for op set (absent for unset). */
|
|
93
|
+
value?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/** Per-field outcome of one batched scope write. */
|
|
96
|
+
export interface BatchedFieldResult {
|
|
97
|
+
/** Field this entry writes. */
|
|
98
|
+
field: string;
|
|
99
|
+
/** Whether the Host accepted this field's write (per the read-back view). */
|
|
100
|
+
landed: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Result of a batched scope write. The bridge scope posts every planned write
|
|
104
|
+
* in one /mutate so the Host validate hook judges baseURL+model together; a
|
|
105
|
+
* batched refusal fails the whole save rather than per-field.
|
|
106
|
+
*/
|
|
107
|
+
export interface BatchResult {
|
|
108
|
+
/** Whether the whole mutate was accepted. */
|
|
109
|
+
ok: boolean;
|
|
110
|
+
/** Per-field success, in the request order (always present when ok). */
|
|
111
|
+
fields: BatchedFieldResult[];
|
|
112
|
+
/** Host rejection code (mutate refused). */
|
|
113
|
+
code?: string;
|
|
114
|
+
/** Host rejection message (mutate refused). */
|
|
115
|
+
message?: string;
|
|
116
|
+
}
|
|
117
|
+
/** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
|
|
118
|
+
export interface NumberConstraints {
|
|
119
|
+
/** The accepted value must be a whole number. */
|
|
120
|
+
integer?: boolean;
|
|
121
|
+
/** The accepted value must be at least this. */
|
|
122
|
+
min?: number;
|
|
123
|
+
}
|
|
124
|
+
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
125
|
+
export declare function numberField(field: string, constraints?: NumberConstraints): FieldSpec;
|
|
74
126
|
/** A free-text field. An empty draft clears the field. */
|
|
75
127
|
export declare function textField(field: string): FieldSpec;
|
|
128
|
+
/**
|
|
129
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
130
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
131
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
132
|
+
* reporting the write landed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function secretField(field: string): FieldSpec;
|
|
76
135
|
/** A boolean field, edited through true/false draft text. */
|
|
77
136
|
export declare function booleanField(field: string): FieldSpec;
|
|
137
|
+
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
138
|
+
export declare function choiceField(field: string, choices: readonly string[]): FieldSpec;
|
|
78
139
|
/**
|
|
79
140
|
* Stages one card's edits over one settings namespace and writes them on save.
|
|
80
141
|
*
|
|
@@ -90,6 +151,7 @@ export declare class CardForm<T> {
|
|
|
90
151
|
private readonly listeners;
|
|
91
152
|
private saving;
|
|
92
153
|
private failed;
|
|
154
|
+
private failedReason;
|
|
93
155
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
94
156
|
constructor(scope: SettingsScope<T>, specs: FieldSpec[]);
|
|
95
157
|
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
@@ -102,9 +164,18 @@ export declare class CardForm<T> {
|
|
|
102
164
|
actions(): CardActions;
|
|
103
165
|
/**
|
|
104
166
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
167
|
+
*
|
|
168
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
169
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
170
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
171
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
172
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
173
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
105
174
|
* @returns settlement after every write and the read-back.
|
|
106
175
|
*/
|
|
107
176
|
save(): Promise<void>;
|
|
177
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
178
|
+
private batchedScope;
|
|
108
179
|
/**
|
|
109
180
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
110
181
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAyB,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAGjH,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,2FAA2F;IAC3F,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAClC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAChD;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,gFAAgF;IAChF,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,yEAAyE;IACzE,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAoBD,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,EAAE,EAAE,KAAK,GAAG,OAAO,CAAA;IACnB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,EAAE,EAAE,OAAO,CAAA;IACX,wEAAwE;IACxE,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAQD,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,6JAA6J;AAC7J,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,GAAE,iBAAsB,GAAG,SAAS,CAezF;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CASlD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAYrD;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAShF;AAED;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC;IAUnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IATxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAoB;IAExC,yEAAyE;gBAEtD,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACxC,KAAK,EAAE,SAAS,EAAE;IAMpB,wFAAwF;IACxF,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;IAM3C,iFAAiF;IACjF,KAAK,IAAI,SAAS;IAelB,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAchC,wDAAwD;IACxD,OAAO,IAAI,WAAW;IAiBtB;;;;;;;;;;OAUG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,0FAA0F;IAC1F,OAAO,CAAC,YAAY;IAKpB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;YAiBE,KAAK;YAKL,KAAK;IAUnB,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,OAAO;CAGhB"}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/settings-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
1
2
|
/**
|
|
2
3
|
* Staged form model behind the plugin settings card. A card stages what the
|
|
3
4
|
* user types and writes it only when they save — the settings write is a
|
|
4
5
|
* durable, revision-fenced document mutation, so staging keeps what is on
|
|
5
|
-
* screen exactly what a save would store.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* screen exactly what a save would store. Family-shared slice inlined into
|
|
7
|
+
* each plugin's client bundle; mirrors the official ui-plugin-config
|
|
8
|
+
* card-store pattern.
|
|
8
9
|
*/
|
|
9
10
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
|
-
/** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
|
|
11
|
-
export function numberField(field) {
|
|
11
|
+
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
12
|
+
export function numberField(field, constraints = {}) {
|
|
13
|
+
const { integer = false, min } = constraints;
|
|
12
14
|
return {
|
|
13
15
|
field,
|
|
14
16
|
format: value => typeof value === 'number' ? String(value) : '',
|
|
@@ -17,7 +19,13 @@ export function numberField(field) {
|
|
|
17
19
|
if (trimmed === '')
|
|
18
20
|
return { kind: 'clear' };
|
|
19
21
|
const parsed = Number(trimmed);
|
|
20
|
-
|
|
22
|
+
if (!Number.isFinite(parsed))
|
|
23
|
+
return undefined;
|
|
24
|
+
if (integer && !Number.isInteger(parsed))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (min !== undefined && parsed < min)
|
|
27
|
+
return undefined;
|
|
28
|
+
return { kind: 'set', value: parsed };
|
|
21
29
|
},
|
|
22
30
|
};
|
|
23
31
|
}
|
|
@@ -32,20 +40,44 @@ export function textField(field) {
|
|
|
32
40
|
},
|
|
33
41
|
};
|
|
34
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
45
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
46
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
47
|
+
* reporting the write landed.
|
|
48
|
+
*/
|
|
49
|
+
export function secretField(field) {
|
|
50
|
+
return { ...textField(field), secret: true };
|
|
51
|
+
}
|
|
35
52
|
/** A boolean field, edited through true/false draft text. */
|
|
36
53
|
export function booleanField(field) {
|
|
37
54
|
return {
|
|
38
55
|
field,
|
|
39
56
|
format: value => typeof value === 'boolean' ? String(value) : '',
|
|
40
57
|
parse: (text) => {
|
|
41
|
-
|
|
58
|
+
const trimmed = text.trim();
|
|
59
|
+
if (trimmed === '')
|
|
60
|
+
return { kind: 'clear' };
|
|
61
|
+
if (trimmed === 'true')
|
|
42
62
|
return { kind: 'set', value: true };
|
|
43
|
-
if (
|
|
63
|
+
if (trimmed === 'false')
|
|
44
64
|
return { kind: 'set', value: false };
|
|
45
65
|
return undefined;
|
|
46
66
|
},
|
|
47
67
|
};
|
|
48
68
|
}
|
|
69
|
+
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
70
|
+
export function choiceField(field, choices) {
|
|
71
|
+
return {
|
|
72
|
+
field,
|
|
73
|
+
format: value => typeof value === 'string' && choices.includes(value) ? value : '',
|
|
74
|
+
parse: (text) => {
|
|
75
|
+
if (text === '')
|
|
76
|
+
return { kind: 'clear' };
|
|
77
|
+
return choices.includes(text) ? { kind: 'set', value: text } : undefined;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
49
81
|
/**
|
|
50
82
|
* Stages one card's edits over one settings namespace and writes them on save.
|
|
51
83
|
*
|
|
@@ -61,6 +93,7 @@ export class CardForm {
|
|
|
61
93
|
listeners = new Set();
|
|
62
94
|
saving = false;
|
|
63
95
|
failed = false;
|
|
96
|
+
failedReason;
|
|
64
97
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
65
98
|
constructor(scope, specs) {
|
|
66
99
|
this.scope = scope;
|
|
@@ -85,6 +118,7 @@ export class CardForm {
|
|
|
85
118
|
invalid: plan.some(item => item.run === undefined),
|
|
86
119
|
saving: this.saving,
|
|
87
120
|
failed: this.failed,
|
|
121
|
+
...this.failedReason === undefined ? {} : { failedReason: this.failedReason },
|
|
88
122
|
};
|
|
89
123
|
}
|
|
90
124
|
/** Read one field's state from the effective section and its staged draft. */
|
|
@@ -114,32 +148,68 @@ export class CardForm {
|
|
|
114
148
|
return;
|
|
115
149
|
this.staged.clear();
|
|
116
150
|
this.failed = false;
|
|
151
|
+
this.failedReason = undefined;
|
|
117
152
|
this.publish();
|
|
118
153
|
},
|
|
119
154
|
};
|
|
120
155
|
}
|
|
121
156
|
/**
|
|
122
157
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
158
|
+
*
|
|
159
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
160
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
161
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
162
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
163
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
164
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
123
165
|
* @returns settlement after every write and the read-back.
|
|
124
166
|
*/
|
|
125
167
|
async save() {
|
|
126
168
|
const plan = this.plan();
|
|
127
|
-
const
|
|
128
|
-
if (plan.length === 0 || this.saving ||
|
|
169
|
+
const valid = plan.filter(item => item.run !== undefined);
|
|
170
|
+
if (plan.length === 0 || this.saving || valid.length !== plan.length)
|
|
129
171
|
return;
|
|
172
|
+
const plannedWrites = valid.map(item => item.op);
|
|
173
|
+
// Snapshot the fields this save writes, so edits staged while it is in
|
|
174
|
+
// flight survive: only the staged keys this save actually wrote are cleared.
|
|
175
|
+
const fields = new Set(plan.map(item => item.field));
|
|
130
176
|
this.saving = true;
|
|
131
177
|
this.failed = false;
|
|
178
|
+
this.failedReason = undefined;
|
|
132
179
|
this.publish();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
180
|
+
const landed = new Set();
|
|
181
|
+
const batch = this.batchedScope();
|
|
182
|
+
if (batch !== undefined) {
|
|
183
|
+
const result = await batch.mutate(plannedWrites);
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
for (const field of result.fields) {
|
|
186
|
+
if (field.landed)
|
|
187
|
+
landed.add(field.field);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.failedReason = result.message;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
for (const item of valid) {
|
|
196
|
+
if (await item.run())
|
|
197
|
+
landed.add(item.field);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const field of fields) {
|
|
201
|
+
if (landed.has(field))
|
|
202
|
+
this.staged.delete(field);
|
|
136
203
|
}
|
|
137
|
-
if (landed)
|
|
138
|
-
this.staged.clear();
|
|
139
204
|
this.saving = false;
|
|
140
|
-
this.failed =
|
|
205
|
+
this.failed = landed.size !== fields.size;
|
|
141
206
|
this.publish();
|
|
142
207
|
}
|
|
208
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
209
|
+
batchedScope() {
|
|
210
|
+
const candidate = this.scope;
|
|
211
|
+
return typeof candidate?.mutate === 'function' ? candidate : undefined;
|
|
212
|
+
}
|
|
143
213
|
/**
|
|
144
214
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
145
215
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -153,18 +223,18 @@ export class CardForm {
|
|
|
153
223
|
const spec = this.specOf(field);
|
|
154
224
|
if (staged.clear) {
|
|
155
225
|
if (this.stored(field))
|
|
156
|
-
plan.push({ field, run: () => this.clear(field) });
|
|
226
|
+
plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) });
|
|
157
227
|
continue;
|
|
158
228
|
}
|
|
159
229
|
if (staged.text === spec.format(this.sectionValue(field)))
|
|
160
230
|
continue;
|
|
161
231
|
const write = spec.parse(staged.text);
|
|
162
232
|
if (write === undefined)
|
|
163
|
-
plan.push({ field, run: undefined });
|
|
233
|
+
plan.push({ field, op: { field, op: 'unset' }, run: undefined });
|
|
164
234
|
else if (write.kind === 'clear')
|
|
165
|
-
plan.push({ field, run: () => this.clear(field) });
|
|
235
|
+
plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) });
|
|
166
236
|
else
|
|
167
|
-
plan.push({ field, run: () => this.store(field, write.value) });
|
|
237
|
+
plan.push({ field, op: { field, op: 'set', value: write.value }, run: () => this.store(field, write.value) });
|
|
168
238
|
}
|
|
169
239
|
return plan;
|
|
170
240
|
}
|
|
@@ -174,11 +244,18 @@ export class CardForm {
|
|
|
174
244
|
}
|
|
175
245
|
async store(field, value) {
|
|
176
246
|
await this.scope.set(field, value);
|
|
247
|
+
// A redacted secret never appears in the user layer read-back; judging it
|
|
248
|
+
// by value would misreport a successful secret save as failed. The bridge
|
|
249
|
+
// reports secret writes through its secret-set markers (batch path); on
|
|
250
|
+
// the per-field path the scope resolved, so the write is landed.
|
|
251
|
+
if (this.specOf(field).secret)
|
|
252
|
+
return true;
|
|
177
253
|
return this.userLayer()?.[field] === value;
|
|
178
254
|
}
|
|
179
255
|
stage(field, edit) {
|
|
180
256
|
this.staged.set(field, edit);
|
|
181
257
|
this.failed = false;
|
|
258
|
+
this.failedReason = undefined;
|
|
182
259
|
this.publish();
|
|
183
260
|
}
|
|
184
261
|
specOf(field) {
|
|
@@ -1,42 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 5 failed, 6 waiting, 7 running, 8 review
|
|
8
|
-
*
|
|
9
|
-
* Frame counts and per-frame durations are per-track definitions below; the
|
|
10
|
-
* whale-girl atlas is produced by the hatch-pet pipeline, so calibrate
|
|
11
|
-
* `TRACKS` against the actual run (`pet_request.json` frame counts) when the
|
|
12
|
-
* asset lands. Tracks that do not loop hand off to `fallback`.
|
|
2
|
+
* Spritesheet geometry helpers — parameterized by the pet definition the
|
|
3
|
+
* host serves over '/api/pet/pets', so the browser half renders any registry
|
|
4
|
+
* entry without per-pet code. The per-track tables (frames, durations, loop,
|
|
5
|
+
* fallback) also come from the registry; these helpers only place frames,
|
|
6
|
+
* guard track lengths, and map the fixed 9-row animation contract.
|
|
13
7
|
* @module @linxin666/dsh-pet/client/spritesheet
|
|
14
8
|
*/
|
|
15
9
|
import type { PetAnimation } from '../state.ts';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
/**
|
|
20
|
-
export declare const FRAME_COLUMNS = 8;
|
|
21
|
-
/** One animation track: frame indices into the row + per-frame durations. */
|
|
22
|
-
export interface TrackDef {
|
|
23
|
-
/** Frame indices (columns) played in order; must be < FRAME_COLUMNS. */
|
|
24
|
-
frames: readonly number[];
|
|
25
|
-
/** Per-frame duration in ms; same length as frames. */
|
|
26
|
-
durations: readonly number[];
|
|
27
|
-
/** Whether the track loops; a non-looping track hands off to fallback. */
|
|
28
|
-
loop: boolean;
|
|
29
|
-
/** Track to play after a non-looping track finishes. */
|
|
30
|
-
fallback?: PetAnimation;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Track definitions for the whale-girl. Durations are tuned for a soft,
|
|
34
|
-
* slow-healing feel (roughly 2.5× the earlier fast draft — the pet should
|
|
35
|
-
* breathe, not race); calibrate frame counts against the hatch-pet run when
|
|
36
|
-
* the asset lands (rows may carry 4–8 frames).
|
|
37
|
-
*/
|
|
38
|
-
export declare const TRACKS: Record<PetAnimation, TrackDef>;
|
|
39
|
-
/** Row index of one animation track (mirrors state.ts rowOf). */
|
|
10
|
+
import type { PetCell, PetTrackDef } from '../registry.ts';
|
|
11
|
+
/** Animation track shape the frame loop consumes. */
|
|
12
|
+
export type TrackDef = PetTrackDef;
|
|
13
|
+
/** Row index of one animation track (the fixed 9-row contract). */
|
|
40
14
|
export declare function rowOfTrack(animation: PetAnimation): number;
|
|
41
15
|
/**
|
|
42
16
|
* Background-position (px) of one frame cell within the scaled atlas.
|
|
@@ -45,25 +19,17 @@ export declare function rowOfTrack(animation: PetAnimation): number;
|
|
|
45
19
|
* atlas coordinates here would drift each frame by the scale factor and
|
|
46
20
|
* render torn/overlapping frames.
|
|
47
21
|
*/
|
|
48
|
-
export declare function framePosition(row: number, col: number, scale?: number): {
|
|
22
|
+
export declare function framePosition(cell: PetCell, columns: number, row: number, col: number, scale?: number): {
|
|
49
23
|
x: number;
|
|
50
24
|
y: number;
|
|
51
25
|
};
|
|
52
26
|
/** Total duration of one track, ms. */
|
|
53
27
|
export declare function trackDuration(track: TrackDef): number;
|
|
54
28
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @param image - the fully decoded spritesheet (natural size 1536×1872).
|
|
60
|
-
* @returns per-row frame counts, length 9.
|
|
61
|
-
*/
|
|
62
|
-
export declare function detectFrameCounts(image: HTMLImageElement): number[];
|
|
63
|
-
/**
|
|
64
|
-
* Trim a track to the actual frame count of its row. A row with 0 detected
|
|
65
|
-
* frames degrades to the first frame (the atlas is still loading or corrupt)
|
|
66
|
-
* so the pet never renders blank.
|
|
29
|
+
* Trim a track to the actual frame count of its row (the manifest's per-row
|
|
30
|
+
* counts are authoritative; this is a last-line guard against a definition
|
|
31
|
+
* whose row count disagrees with its track table). A row with 0 detected
|
|
32
|
+
* frames degrades to the first frame so the pet never renders blank.
|
|
67
33
|
*/
|
|
68
34
|
export declare function trimTrack(track: TrackDef, frameCount: number): TrackDef;
|
|
69
35
|
//# sourceMappingURL=spritesheet.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spritesheet.d.ts","sourceRoot":"","sources":["../../../src/client/spritesheet.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"spritesheet.d.ts","sourceRoot":"","sources":["../../../src/client/spritesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE1D,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAA;AAElC,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAa1D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAE3H;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,CAQvE"}
|